diff --git a/apps/backend/lambdas/donors/README.md b/apps/backend/lambdas/donors/README.md index f04d7359..a83b98a6 100644 --- a/apps/backend/lambdas/donors/README.md +++ b/apps/backend/lambdas/donors/README.md @@ -10,7 +10,7 @@ Lambda for managing donors. |--------|------|-------------| | GET | /health | Health check | | GET | /donors | | -| GET | /donations | | +| POST | /donations | | | POST | /donors | | ## Setup diff --git a/apps/backend/lambdas/donors/handler.ts b/apps/backend/lambdas/donors/handler.ts index d81e1b90..4e0ec35d 100644 --- a/apps/backend/lambdas/donors/handler.ts +++ b/apps/backend/lambdas/donors/handler.ts @@ -135,6 +135,44 @@ export const handler = async (event: any): Promise => { return json(200, { data: donations }); } + // POST /donations + if (normalizedPath === '/donations' && method === 'POST') { + const body = event.body ? JSON.parse(event.body) as Record : {}; + const { donor_id, project_id, amount } = body; + + if (donor_id === undefined || project_id === undefined || amount === undefined) { + return json(400, { message: 'donor_id, project_id, and amount are required' }); + } + if (!Number.isInteger(donor_id) || (donor_id as number) < 1) { + return json(400, { message: 'donor_id must be a positive integer' }); + } + if (!Number.isInteger(project_id) || (project_id as number) < 1) { + return json(400, { message: 'project_id must be a positive integer' }); + } + if (typeof amount !== 'number' || amount <= 0 || !isFinite(amount)) { + return json(400, { message: 'amount must be a positive number' }); + } + + try { + const donation = await db + .insertInto('branch.project_donations') + .values({ + donor_id: donor_id as number, + project_id: project_id as number, + amount: amount as number, + }) + .returningAll() + .executeTakeFirstOrThrow(); + + return json(201, { data: donation }); + } catch (err: any) { + if (err?.code === '23505') { + return json(409, { message: 'A donation from this donor to this project already exists' }); + } + throw err; + } + } + // POST /donors if ((normalizedPath === '/' || normalizedPath === '/donors') && method === 'POST') { const body = event.body ? JSON.parse(event.body) as Record : {}; diff --git a/apps/backend/lambdas/donors/test/donors.test.ts b/apps/backend/lambdas/donors/test/donors.test.ts index 54294413..d79ebeb6 100644 --- a/apps/backend/lambdas/donors/test/donors.test.ts +++ b/apps/backend/lambdas/donors/test/donors.test.ts @@ -247,6 +247,41 @@ describe("Donor API with data", () => { const res = await handler(createEvent('GET', '/donations')); expect(res.statusCode).toBe(401); }); + test("POST /donations returns 201 and created donation", async () => { + mockAuthenticateRequest.mockResolvedValueOnce(authenticatedUser); + const res = await handler(createEvent('POST', '/donations', { + donor_id: 1, project_id: 4, amount: 500, + })); + const body = JSON.parse(res.body); + expect(res.statusCode).toBe(201); + expect(body.data.donor_id).toBe(1); + expect(body.data.project_id).toBe(4); + expect(Number(body.data.amount)).toBe(500); + }); + + test("POST /donations returns 400 when donor_id is missing", async () => { + mockAuthenticateRequest.mockResolvedValueOnce(authenticatedUser); + const res = await handler(createEvent('POST', '/donations', { project_id: 1, amount: 100 })); + expect(res.statusCode).toBe(400); + }); + + test("POST /donations returns 400 when amount is zero", async () => { + mockAuthenticateRequest.mockResolvedValueOnce(authenticatedUser); + const res = await handler(createEvent('POST', '/donations', { donor_id: 1, project_id: 1, amount: 0 })); + expect(res.statusCode).toBe(400); + }); + + test("POST /donations returns 400 when amount is negative", async () => { + mockAuthenticateRequest.mockResolvedValueOnce(authenticatedUser); + const res = await handler(createEvent('POST', '/donations', { donor_id: 1, project_id: 1, amount: -50 })); + expect(res.statusCode).toBe(400); + }); + + test("POST /donations returns 401 when unauthenticated", async () => { + mockAuthenticateRequest.mockResolvedValueOnce({ isAuthenticated: false }); + const res = await handler(createEvent('POST', '/donations', { donor_id: 1, project_id: 1, amount: 100 })); + expect(res.statusCode).toBe(401); + }); }); describe("Donor API when DB is empty", () => {