From 15efb8e343934cbb696820b3979f99d5313a767f Mon Sep 17 00:00:00 2001 From: Shreeya Adhikari Date: Tue, 7 Jul 2026 16:11:47 -0400 Subject: [PATCH 1/3] implement post /donations endpoint with validation and auth --- apps/backend/lambdas/donors/handler.ts | 38 ++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/apps/backend/lambdas/donors/handler.ts b/apps/backend/lambdas/donors/handler.ts index d81e1b9..4e0ec35 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 : {}; From 6625e9f090975ebbe0e85fa340ee02d51161536d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 7 Jul 2026 20:13:09 +0000 Subject: [PATCH 2/3] chore: regenerate lambda READMEs --- apps/backend/lambdas/donors/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/backend/lambdas/donors/README.md b/apps/backend/lambdas/donors/README.md index f04d735..a83b98a 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 From 6faa6e7ebeeb525d44fc9ce1b8cb154949b9c018 Mon Sep 17 00:00:00 2001 From: Shreeya Adhikari Date: Tue, 7 Jul 2026 16:13:12 -0400 Subject: [PATCH 3/3] add post/donations tests for validation, auth, success cases --- .../lambdas/donors/test/donors.test.ts | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/apps/backend/lambdas/donors/test/donors.test.ts b/apps/backend/lambdas/donors/test/donors.test.ts index 5429441..d79ebeb 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", () => {