feat: habits completion with daily validation and rewards
This commit is contained in:
parent
2c48c8b55b
commit
abfca19833
87
server/api/habits/[id]/complete.post.ts
Normal file
87
server/api/habits/[id]/complete.post.ts
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
import { useSession } from 'h3';
|
||||
|
||||
interface CompletionResponse {
|
||||
message: string;
|
||||
reward: {
|
||||
coins: number;
|
||||
};
|
||||
updatedCoins: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper function to safely get the authenticated user's ID from the session.
|
||||
*/
|
||||
async function getUserIdFromSession(event: any): Promise<number> {
|
||||
const session = await useSession(event, {
|
||||
password: process.env.SESSION_PASSWORD,
|
||||
});
|
||||
const userId = session.data?.user?.id;
|
||||
if (!userId) {
|
||||
throw createError({ statusCode: 401, statusMessage: 'Unauthorized' });
|
||||
}
|
||||
return userId;
|
||||
}
|
||||
|
||||
export default defineEventHandler(async (event): Promise<CompletionResponse> => {
|
||||
const userId = await getUserIdFromSession(event);
|
||||
const habitId = parseInt(event.context.params?.id || '', 10);
|
||||
|
||||
if (isNaN(habitId)) {
|
||||
throw createError({ statusCode: 400, statusMessage: 'Invalid habit ID.' });
|
||||
}
|
||||
|
||||
const habit = await prisma.habit.findFirst({
|
||||
where: { id: habitId, userId },
|
||||
});
|
||||
|
||||
if (!habit) {
|
||||
throw createError({ statusCode: 404, statusMessage: 'Habit not found.' });
|
||||
}
|
||||
|
||||
const today = new Date();
|
||||
const dayOfWeek = today.getDay(); // 0 (Sunday) to 6 (Saturday)
|
||||
if (!(habit.daysOfWeek as number[]).includes(dayOfWeek)) {
|
||||
throw createError({ statusCode: 400, statusMessage: 'Habit is not active today.' });
|
||||
}
|
||||
|
||||
// Normalize date to the beginning of the day for consistent checks
|
||||
const startOfToday = new Date();
|
||||
startOfToday.setUTCHours(0, 0, 0, 0);
|
||||
|
||||
const existingCompletion = await prisma.habitCompletion.findFirst({
|
||||
where: {
|
||||
habitId: habitId,
|
||||
date: startOfToday, // Use precise equality check
|
||||
},
|
||||
});
|
||||
|
||||
if (existingCompletion) {
|
||||
throw createError({ statusCode: 409, statusMessage: 'Habit already completed today.' });
|
||||
}
|
||||
|
||||
const rewardCoins = 3;
|
||||
const [, updatedUser] = await prisma.$transaction([
|
||||
prisma.habitCompletion.create({
|
||||
data: {
|
||||
habitId: habitId,
|
||||
date: startOfToday, // Save the normalized date
|
||||
},
|
||||
}),
|
||||
prisma.user.update({
|
||||
where: { id: userId },
|
||||
data: {
|
||||
coins: {
|
||||
increment: rewardCoins,
|
||||
},
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
return {
|
||||
message: 'Habit completed successfully!',
|
||||
reward: {
|
||||
coins: rewardCoins,
|
||||
},
|
||||
updatedCoins: updatedUser.coins,
|
||||
};
|
||||
});
|
||||
56
server/api/habits/index.get.ts
Normal file
56
server/api/habits/index.get.ts
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import { useSession } from 'h3';
|
||||
|
||||
// DTO to shape the output
|
||||
interface HabitCompletionDto {
|
||||
date: Date;
|
||||
}
|
||||
|
||||
interface HabitDto {
|
||||
id: number;
|
||||
name: string;
|
||||
daysOfWeek: number[];
|
||||
completions: HabitCompletionDto[];
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper function to safely get the authenticated user's ID from the session.
|
||||
*/
|
||||
async function getUserIdFromSession(event: any): Promise<number> {
|
||||
const session = await useSession(event, {
|
||||
password: process.env.SESSION_PASSWORD,
|
||||
});
|
||||
|
||||
const userId = session.data?.user?.id;
|
||||
if (!userId) {
|
||||
throw createError({ statusCode: 401, statusMessage: 'Unauthorized' });
|
||||
}
|
||||
return userId;
|
||||
}
|
||||
|
||||
export default defineEventHandler(async (event): Promise<HabitDto[]> => {
|
||||
const userId = await getUserIdFromSession(event);
|
||||
|
||||
const habits = await prisma.habit.findMany({
|
||||
where: {
|
||||
userId: userId,
|
||||
},
|
||||
include: {
|
||||
completions: {
|
||||
orderBy: {
|
||||
date: 'desc',
|
||||
},
|
||||
take: 30, // Limit completions to the last 30 for performance
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Map to DTOs
|
||||
return habits.map((habit: any) => ({
|
||||
id: habit.id,
|
||||
name: habit.name,
|
||||
daysOfWeek: habit.daysOfWeek, // Assuming daysOfWeek is already in the correct format
|
||||
completions: habit.completions.map((comp: any) => ({
|
||||
date: comp.date,
|
||||
})),
|
||||
}));
|
||||
});
|
||||
62
server/api/habits/index.post.ts
Normal file
62
server/api/habits/index.post.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import { useSession } from 'h3';
|
||||
|
||||
interface HabitDto {
|
||||
id: number;
|
||||
name: string;
|
||||
daysOfWeek: number[];
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper function to safely get the authenticated user's ID from the session.
|
||||
*/
|
||||
async function getUserIdFromSession(event: any): Promise<number> {
|
||||
const session = await useSession(event, {
|
||||
password: process.env.SESSION_PASSWORD,
|
||||
});
|
||||
|
||||
const userId = session.data?.user?.id;
|
||||
if (!userId) {
|
||||
throw createError({ statusCode: 401, statusMessage: 'Unauthorized' });
|
||||
}
|
||||
return userId;
|
||||
}
|
||||
|
||||
export default defineEventHandler(async (event): Promise<HabitDto> => {
|
||||
const userId = await getUserIdFromSession(event);
|
||||
const { name, daysOfWeek } = await readBody(event);
|
||||
|
||||
// --- Validation ---
|
||||
if (!name || !Array.isArray(daysOfWeek)) {
|
||||
throw createError({ statusCode: 400, statusMessage: 'Invalid input: name and daysOfWeek are required.' });
|
||||
}
|
||||
|
||||
// Sanitize daysOfWeek to ensure it's a unique set of valid numbers
|
||||
const validDays = daysOfWeek.filter(day => typeof day === 'number' && day >= 0 && day <= 6);
|
||||
const sanitizedDays = [...new Set(validDays)].sort();
|
||||
|
||||
|
||||
// --- Business Rule: Max 3 Habits ---
|
||||
const habitCount = await prisma.habit.count({ where: { userId } });
|
||||
if (habitCount >= 3) {
|
||||
throw createError({ statusCode: 400, statusMessage: 'Maximum number of 3 habits reached.' });
|
||||
}
|
||||
|
||||
// --- Create Habit ---
|
||||
const newHabit = await prisma.habit.create({
|
||||
data: {
|
||||
userId,
|
||||
name,
|
||||
daysOfWeek: sanitizedDays,
|
||||
},
|
||||
});
|
||||
|
||||
// Set 201 Created status
|
||||
setResponseStatus(event, 201);
|
||||
|
||||
// Return DTO
|
||||
return {
|
||||
id: newHabit.id,
|
||||
name: newHabit.name,
|
||||
daysOfWeek: newHabit.daysOfWeek as number[],
|
||||
};
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user