117 lines
3.7 KiB
TypeScript
117 lines
3.7 KiB
TypeScript
import { getAuthenticatedUserId } from '../../../utils/auth';
|
|
import { REWARDS } from '../../../utils/economy';
|
|
import prisma from '../../../utils/prisma';
|
|
import { applyStreakMultiplier } from '../../../utils/streak';
|
|
import { getDayOfWeekFromGameDay } from '~/server/utils/gameDay';
|
|
|
|
interface CompletionResponse {
|
|
message: string;
|
|
reward: {
|
|
coins: number;
|
|
exp: number;
|
|
};
|
|
updatedCoins: number;
|
|
updatedExp: number;
|
|
}
|
|
|
|
export default defineEventHandler(async (event): Promise<CompletionResponse> => {
|
|
const userId = getAuthenticatedUserId(event);
|
|
const habitId = parseInt(event.context.params?.id || '', 10);
|
|
const body = await readBody(event);
|
|
const gameDay: string = body.gameDay; // Expecting "YYYY-MM-DD"
|
|
|
|
if (!gameDay || !/^\d{4}-\d{2}-\d{2}$/.test(gameDay)) {
|
|
throw createError({ statusCode: 400, statusMessage: 'Invalid or missing gameDay property in request body. Expected YYYY-MM-DD.' });
|
|
}
|
|
|
|
if (isNaN(habitId)) {
|
|
throw createError({ statusCode: 400, statusMessage: 'Invalid habit ID.' });
|
|
}
|
|
|
|
// Fetch user and habit in parallel
|
|
const [user, habit] = await Promise.all([
|
|
prisma.user.findUnique({ where: { id: userId } }),
|
|
prisma.habit.findFirst({ where: { id: habitId, userId } })
|
|
]);
|
|
|
|
if (!user) {
|
|
throw createError({ statusCode: 401, statusMessage: 'User not found.' });
|
|
}
|
|
if (!habit) {
|
|
throw createError({ statusCode: 404, statusMessage: 'Habit not found.' });
|
|
}
|
|
|
|
const appDayOfWeek = getDayOfWeekFromGameDay(gameDay);
|
|
|
|
// For permanent users, ensure the habit is scheduled for today.
|
|
// Anonymous users in the onboarding flow can complete it on any day.
|
|
if (!user.isAnonymous) {
|
|
if (!(habit.daysOfWeek as number[]).includes(appDayOfWeek)) {
|
|
throw createError({ statusCode: 400, statusMessage: 'Habit is not active today.' });
|
|
}
|
|
}
|
|
|
|
const existingCompletion = await prisma.habitCompletion.findFirst({
|
|
where: {
|
|
habitId: habitId,
|
|
date: gameDay,
|
|
},
|
|
});
|
|
|
|
if (existingCompletion) {
|
|
throw createError({ statusCode: 409, statusMessage: 'Habit already completed today.' });
|
|
}
|
|
|
|
// Determine the reward based on user type
|
|
let finalReward: { coins: number, exp: number };
|
|
if (user.isAnonymous) {
|
|
// Anonymous users in onboarding get a fixed reward from economy.ts
|
|
finalReward = REWARDS.HABITS.ONBOARDING_COMPLETION;
|
|
} else {
|
|
// Permanent users get rewards based on streak
|
|
// Streak defaults to 1 for multiplier if it's 0 or null
|
|
const currentDailyStreak = user.dailyStreak && user.dailyStreak > 0 ? user.dailyStreak : 1;
|
|
const baseReward = REWARDS.HABITS.COMPLETION;
|
|
finalReward = applyStreakMultiplier(baseReward, currentDailyStreak);
|
|
}
|
|
|
|
const village = await prisma.village.findUnique({ where: { userId } });
|
|
|
|
const [, updatedUser] = await prisma.$transaction([
|
|
prisma.habitCompletion.create({
|
|
data: {
|
|
habitId: habitId,
|
|
date: gameDay,
|
|
},
|
|
}),
|
|
prisma.user.update({
|
|
where: { id: userId },
|
|
data: {
|
|
coins: {
|
|
increment: finalReward.coins,
|
|
},
|
|
exp: {
|
|
increment: finalReward.exp,
|
|
},
|
|
},
|
|
}),
|
|
...(village ? [prisma.villageEvent.create({
|
|
data: {
|
|
villageId: village.id,
|
|
type: 'HABIT_COMPLETION',
|
|
message: `Привычка "${habit.name}" выполнена, принеся вам ${finalReward.coins} монет и ${finalReward.exp} опыта.${user.dailyStreak > 1 ? ` Ваша серия визитов (x${user.dailyStreak}) увеличила награду!` : ''}`,
|
|
coins: finalReward.coins,
|
|
exp: finalReward.exp,
|
|
}
|
|
})] : []),
|
|
]);
|
|
|
|
return {
|
|
message: 'Habit completed successfully!',
|
|
reward: finalReward,
|
|
updatedCoins: updatedUser.coins,
|
|
updatedExp: updatedUser.exp,
|
|
};
|
|
});
|
|
|