Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/backend/lambdas/donors/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Lambda for managing donors.
|--------|------|-------------|
| GET | /health | Health check |
| GET | /donors | |
| GET | /donations | |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add this GET /donations back?

| POST | /donations | |
| POST | /donors | |

## Setup
Expand Down
38 changes: 38 additions & 0 deletions apps/backend/lambdas/donors/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,44 @@ export const handler = async (event: any): Promise<APIGatewayProxyResult> => {
return json(200, { data: donations });
}

// POST /donations
if (normalizedPath === '/donations' && method === 'POST') {
const body = event.body ? JSON.parse(event.body) as Record<string, unknown> : {};
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' });
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we also check to make sure the user is a member of the project?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or an admin

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<string, unknown> : {};
Expand Down
35 changes: 35 additions & 0 deletions apps/backend/lambdas/donors/test/donors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down
Loading