Renamed Student to Attendee
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
-- Rename Student model (students table) to Attendee (attendees table)
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "students" RENAME TO "attendees";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "payments" RENAME COLUMN "studentId" TO "attendeeId";
|
||||
|
||||
-- RenameIndex
|
||||
ALTER INDEX "students_groupId_idx" RENAME TO "attendees_groupId_idx";
|
||||
|
||||
-- RenameIndex
|
||||
ALTER INDEX "payments_studentId_idx" RENAME TO "payments_attendeeId_idx";
|
||||
|
||||
-- RenameForeignKey
|
||||
ALTER TABLE "payments" RENAME CONSTRAINT "payments_studentId_fkey" TO "payments_attendeeId_fkey";
|
||||
@@ -14,10 +14,10 @@ model Group {
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
owner User @relation("OwnerGroups", fields: [createdById], references: [id], onDelete: Cascade)
|
||||
members GroupMember[]
|
||||
students Student[]
|
||||
payments Payment[]
|
||||
owner User @relation("OwnerGroups", fields: [createdById], references: [id], onDelete: Cascade)
|
||||
members GroupMember[]
|
||||
attendees Attendee[]
|
||||
payments Payment[]
|
||||
|
||||
@@map("groups")
|
||||
}
|
||||
@@ -57,42 +57,42 @@ enum Role {
|
||||
MEMBER
|
||||
}
|
||||
|
||||
model Student {
|
||||
id String @id @default(cuid())
|
||||
groupId String
|
||||
fullName String
|
||||
email String?
|
||||
phone String?
|
||||
guardianName String?
|
||||
guardianPhone String?
|
||||
notes String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
model Attendee {
|
||||
id String @id @default(cuid())
|
||||
groupId String
|
||||
fullName String
|
||||
email String?
|
||||
phone String?
|
||||
guardianName String?
|
||||
guardianPhone String?
|
||||
notes String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
group Group @relation(fields: [groupId], references: [id], onDelete: Cascade)
|
||||
payments Payment[]
|
||||
|
||||
@@index([groupId])
|
||||
@@map("students")
|
||||
@@map("attendees")
|
||||
}
|
||||
|
||||
model Payment {
|
||||
id String @id @default(cuid())
|
||||
groupId String
|
||||
studentId String
|
||||
amount Decimal @db.Decimal(10, 2)
|
||||
currency String @default("MXN")
|
||||
status PaymentStatus @default(PENDING) // PENDING, PAID, OVERDUE, CANCELLED
|
||||
dueDate DateTime
|
||||
paidAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
id String @id @default(cuid())
|
||||
groupId String
|
||||
attendeeId String
|
||||
amount Decimal @db.Decimal(10, 2)
|
||||
currency String @default("MXN")
|
||||
status PaymentStatus @default(PENDING) // PENDING, PAID, OVERDUE, CANCELLED
|
||||
dueDate DateTime
|
||||
paidAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
group Group @relation(fields: [groupId], references: [id], onDelete: Cascade)
|
||||
student Student @relation(fields: [studentId], references: [id], onDelete: Cascade)
|
||||
group Group @relation(fields: [groupId], references: [id], onDelete: Cascade)
|
||||
attendee Attendee @relation(fields: [attendeeId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([groupId])
|
||||
@@index([studentId])
|
||||
@@index([attendeeId])
|
||||
@@map("payments")
|
||||
}
|
||||
|
||||
|
||||
@@ -10,12 +10,12 @@ import { requestLoggerMiddleware } from '@/http/request-logger';
|
||||
import { corsMiddleware, securityHeadersMiddleware } from '@/http/security-headers';
|
||||
import { sessionAuthMiddleware } from '@/http/session-auth';
|
||||
import { logger } from '@/logger';
|
||||
import { attendeesRoutes } from './modules/attendees';
|
||||
import { authRoutes } from './modules/auth';
|
||||
import { groupsRoutes } from './modules/groups';
|
||||
import { healthCheckRoutes } from './modules/health-check';
|
||||
import { onboardingRoutes } from './modules/onboarding';
|
||||
import { paymentsRoutes } from './modules/payments';
|
||||
import { studentsRoutes } from './modules/students';
|
||||
import { waitlistRoutes } from './modules/waitlist';
|
||||
|
||||
const app = new Hono<BackendEnv>();
|
||||
@@ -35,7 +35,7 @@ api.route('/auth', authRoutes);
|
||||
api.route('/health', healthCheckRoutes);
|
||||
api.route('/groups', groupsRoutes);
|
||||
api.route('/onboarding', onboardingRoutes);
|
||||
api.route('/students', studentsRoutes);
|
||||
api.route('/attendees', attendeesRoutes);
|
||||
api.route('/payments', paymentsRoutes);
|
||||
api.route('/waitlist', waitlistRoutes);
|
||||
|
||||
|
||||
18
apps/backend/src/modules/attendees/features/get-all/route.ts
Normal file
18
apps/backend/src/modules/attendees/features/get-all/route.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { AttendeeQuery } from '@gruperly/shared';
|
||||
import { AttendeeQuerySchema } from '@gruperly/shared';
|
||||
import { Hono } from 'hono';
|
||||
import { resultJson } from '@/http/problem-details';
|
||||
import { validate } from '@/http/validate';
|
||||
import { ListAttendees } from './use-case';
|
||||
|
||||
const route = new Hono();
|
||||
|
||||
route.get('/', validate.query(AttendeeQuerySchema), async (c) => {
|
||||
const query = c.req.valid('query') as AttendeeQuery;
|
||||
const useCase = new ListAttendees();
|
||||
const result: Awaited<ReturnType<typeof useCase.execute>> = await useCase.execute(query);
|
||||
|
||||
return resultJson(c, result);
|
||||
});
|
||||
|
||||
export default route;
|
||||
@@ -1,14 +1,14 @@
|
||||
import type { PrismaClient } from '@generated/prisma/client';
|
||||
import type { ProblemDetails, Result, StudentDto, StudentList, StudentQuery } from '@gruperly/shared';
|
||||
import type { AttendeeDto, AttendeeList, AttendeeQuery, ProblemDetails, Result } from '@gruperly/shared';
|
||||
import { ok } from '@gruperly/shared';
|
||||
import { getPaginationMetadata, getPaginationOffset } from '@/lib/pagination';
|
||||
import prisma from '@/lib/prisma';
|
||||
|
||||
type ListStudentsDeps = {
|
||||
db?: Pick<PrismaClient, 'student'>;
|
||||
type ListAttendeesDeps = {
|
||||
db?: Pick<PrismaClient, 'attendee'>;
|
||||
};
|
||||
|
||||
type StudentRecord = {
|
||||
type AttendeeRecord = {
|
||||
id: string;
|
||||
groupId: string;
|
||||
fullName: string;
|
||||
@@ -21,7 +21,7 @@ type StudentRecord = {
|
||||
updatedAt: Date;
|
||||
};
|
||||
|
||||
function toStudentDto(record: StudentRecord): StudentDto {
|
||||
function toAttendeeDto(record: AttendeeRecord): AttendeeDto {
|
||||
return {
|
||||
id: record.id,
|
||||
groupId: record.groupId,
|
||||
@@ -36,22 +36,22 @@ function toStudentDto(record: StudentRecord): StudentDto {
|
||||
};
|
||||
}
|
||||
|
||||
export class ListStudents {
|
||||
constructor(private readonly deps: ListStudentsDeps = {}) {}
|
||||
export class ListAttendees {
|
||||
constructor(private readonly deps: ListAttendeesDeps = {}) {}
|
||||
|
||||
async execute(query: StudentQuery): Promise<Result<StudentList, ProblemDetails>> {
|
||||
async execute(query: AttendeeQuery): Promise<Result<AttendeeList, ProblemDetails>> {
|
||||
const db = this.deps.db ?? prisma;
|
||||
const [records, total] = await Promise.all([
|
||||
db.student.findMany({
|
||||
db.attendee.findMany({
|
||||
skip: getPaginationOffset(query),
|
||||
take: query.pageSize,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
}),
|
||||
db.student.count(),
|
||||
db.attendee.count(),
|
||||
]);
|
||||
|
||||
return ok({
|
||||
data: records.map(toStudentDto),
|
||||
data: records.map(toAttendeeDto),
|
||||
pagination: getPaginationMetadata(query, total),
|
||||
});
|
||||
}
|
||||
1
apps/backend/src/modules/attendees/index.ts
Normal file
1
apps/backend/src/modules/attendees/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as attendeesRoutes } from './routes';
|
||||
@@ -11,7 +11,7 @@ type ListPaymentsDeps = {
|
||||
type PaymentRecord = {
|
||||
id: string;
|
||||
groupId: string;
|
||||
studentId: string;
|
||||
attendeeId: string;
|
||||
amount: { toString(): string };
|
||||
currency: string;
|
||||
status: PaymentDto['status'];
|
||||
@@ -25,7 +25,7 @@ function toPaymentDto(record: PaymentRecord): PaymentDto {
|
||||
return {
|
||||
id: record.id,
|
||||
groupId: record.groupId,
|
||||
studentId: record.studentId,
|
||||
attendeeId: record.attendeeId,
|
||||
amount: Number(record.amount.toString()),
|
||||
currency: record.currency,
|
||||
status: record.status,
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import type { StudentQuery } from '@gruperly/shared';
|
||||
import { StudentQuerySchema } from '@gruperly/shared';
|
||||
import { Hono } from 'hono';
|
||||
import { resultJson } from '@/http/problem-details';
|
||||
import { validate } from '@/http/validate';
|
||||
import { ListStudents } from './use-case';
|
||||
|
||||
const route = new Hono();
|
||||
|
||||
route.get('/', validate.query(StudentQuerySchema), async (c) => {
|
||||
const query = c.req.valid('query') as StudentQuery;
|
||||
const useCase = new ListStudents();
|
||||
const result: Awaited<ReturnType<typeof useCase.execute>> = await useCase.execute(query);
|
||||
|
||||
return resultJson(c, result);
|
||||
});
|
||||
|
||||
export default route;
|
||||
@@ -1 +0,0 @@
|
||||
export { default as studentsRoutes } from './routes';
|
||||
@@ -3,7 +3,7 @@ import { Hono } from 'hono';
|
||||
|
||||
mock.module('@/lib/prisma', () => ({
|
||||
default: {
|
||||
student: {
|
||||
attendee: {
|
||||
findMany: mock(),
|
||||
count: mock(),
|
||||
},
|
||||
@@ -13,13 +13,13 @@ mock.module('@/lib/prisma', () => ({
|
||||
}));
|
||||
|
||||
import prisma from '@/lib/prisma';
|
||||
import { studentsRoutes } from '@/modules/students';
|
||||
import { attendeesRoutes } from '@/modules/attendees';
|
||||
|
||||
const app = new Hono();
|
||||
app.route('/students', studentsRoutes);
|
||||
app.route('/attendees', attendeesRoutes);
|
||||
|
||||
const student = {
|
||||
id: 'student-1',
|
||||
const attendee = {
|
||||
id: 'attendee-1',
|
||||
groupId: 'group-1',
|
||||
fullName: 'Ana Pérez',
|
||||
email: 'ana@example.com',
|
||||
@@ -31,41 +31,41 @@ const student = {
|
||||
updatedAt: new Date('2026-08-01T10:00:00.000Z'),
|
||||
};
|
||||
|
||||
describe('students routes', () => {
|
||||
describe('attendees routes', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('lists students with pagination', async () => {
|
||||
prisma.student.findMany.mockResolvedValue([student]);
|
||||
prisma.student.count.mockResolvedValue(21);
|
||||
it('lists attendees with pagination', async () => {
|
||||
prisma.attendee.findMany.mockResolvedValue([attendee]);
|
||||
prisma.attendee.count.mockResolvedValue(21);
|
||||
|
||||
const res = await app.request('/students?page=2&pageSize=10');
|
||||
const res = await app.request('/attendees?page=2&pageSize=10');
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(await res.json()).toEqual({
|
||||
data: [
|
||||
{
|
||||
...student,
|
||||
createdAt: student.createdAt.toISOString(),
|
||||
updatedAt: student.updatedAt.toISOString(),
|
||||
...attendee,
|
||||
createdAt: attendee.createdAt.toISOString(),
|
||||
updatedAt: attendee.updatedAt.toISOString(),
|
||||
},
|
||||
],
|
||||
pagination: { page: 2, pageSize: 10, total: 21, totalPages: 3 },
|
||||
});
|
||||
expect(prisma.student.findMany).toHaveBeenCalledWith({
|
||||
expect(prisma.attendee.findMany).toHaveBeenCalledWith({
|
||||
skip: 10,
|
||||
take: 10,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
expect(prisma.student.count).toHaveBeenCalledWith();
|
||||
expect(prisma.attendee.count).toHaveBeenCalledWith();
|
||||
});
|
||||
|
||||
it('uses default pagination', async () => {
|
||||
prisma.student.findMany.mockResolvedValue([]);
|
||||
prisma.student.count.mockResolvedValue(0);
|
||||
prisma.attendee.findMany.mockResolvedValue([]);
|
||||
prisma.attendee.count.mockResolvedValue(0);
|
||||
|
||||
const res = await app.request('/students');
|
||||
const res = await app.request('/attendees');
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(await res.json()).toEqual({
|
||||
@@ -75,7 +75,7 @@ describe('students routes', () => {
|
||||
});
|
||||
|
||||
it('rejects invalid pagination query parameters', async () => {
|
||||
const res = await app.request('/students?page=0&pageSize=25');
|
||||
const res = await app.request('/attendees?page=0&pageSize=25');
|
||||
|
||||
expect(res.status).toBe(400);
|
||||
expect(await res.json()).toMatchObject({ status: 400, title: 'Bad Request' });
|
||||
@@ -20,8 +20,8 @@ app.route('/payments', paymentsRoutes);
|
||||
|
||||
const payment = {
|
||||
id: 'payment-1',
|
||||
groupId: 'group-1',
|
||||
studentId: 'student-1',
|
||||
groupId: 'group-1',
|
||||
attendeeId: 'attendee-1',
|
||||
amount: { toString: () => '150.50' } as { toString(): string },
|
||||
currency: 'MXN',
|
||||
status: 'pending',
|
||||
@@ -47,8 +47,8 @@ describe('payments routes', () => {
|
||||
data: [
|
||||
{
|
||||
id: 'payment-1',
|
||||
groupId: 'group-1',
|
||||
studentId: 'student-1',
|
||||
groupId: 'group-1',
|
||||
attendeeId: 'attendee-1',
|
||||
amount: 150.5,
|
||||
currency: 'MXN',
|
||||
status: 'pending',
|
||||
|
||||
@@ -35,7 +35,7 @@ describe('session auth', () => {
|
||||
expect(isPublicApiRequest('GET', '/api/v1/health')).toBe(true);
|
||||
expect(isPublicApiRequest('POST', '/api/v1/auth/sign-in/email')).toBe(true);
|
||||
expect(isPublicApiRequest('GET', '/api/v1/groups')).toBe(false);
|
||||
expect(isPublicApiRequest('GET', '/api/v1/students')).toBe(false);
|
||||
expect(isPublicApiRequest('GET', '/api/v1/attendees')).toBe(false);
|
||||
});
|
||||
|
||||
it('allows public requests without a session', async () => {
|
||||
|
||||
Reference in New Issue
Block a user