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
16 changes: 15 additions & 1 deletion lib/db/db-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { createStore, type StoreApi } from "zustand/vanilla"
import { immer } from "zustand/middleware/immer"
import { hoist, type HoistedStoreApi } from "zustand-hoist"

import { databaseSchema, type DatabaseSchema, type Thing } from "./schema.ts"
import { databaseSchema, type DatabaseSchema, type Thing, type Payment } from "./schema.ts"
import { combine } from "zustand/middleware"

export const createDatabase = () => {
Expand All @@ -21,4 +21,18 @@ const initializer = combine(databaseSchema.parse({}), (set) => ({
idCounter: state.idCounter + 1,
}))
},
addPayment: (payment: Omit<Payment, "payment_id" | "created_at">) => {
const now = new Date().toISOString()
set((state) => ({
payments: [
...state.payments,
{
...payment,
payment_id: `pay_${state.idCounter}`,
created_at: now,
},
],
idCounter: state.idCounter + 1,
}))
},
}))
12 changes: 12 additions & 0 deletions lib/db/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,20 @@ export const thingSchema = z.object({
})
export type Thing = z.infer<typeof thingSchema>

export const paymentSchema = z.object({
payment_id: z.string(),
amount: z.number().positive(),
currency: z.string().default("USD"),
recipient: z.string(),
description: z.string(),
status: z.enum(["pending", "completed", "failed"]).default("completed"),
created_at: z.string(),
})
export type Payment = z.infer<typeof paymentSchema>

export const databaseSchema = z.object({
idCounter: z.number().default(0),
things: z.array(thingSchema).default([]),
payments: z.array(paymentSchema).default([]),
})
export type DatabaseSchema = z.infer<typeof databaseSchema>
21 changes: 21 additions & 0 deletions routes/payments/list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { withRouteSpec } from "lib/middleware/with-winter-spec"
import { z } from "zod"

export default withRouteSpec({
methods: ["GET"],
jsonResponse: z.object({
payments: z.array(
z.object({
payment_id: z.string(),
amount: z.number(),
currency: z.string(),
recipient: z.string(),
description: z.string(),
status: z.string(),
created_at: z.string(),
}),
),
}),
})((req, ctx) => {
return ctx.json({ payments: ctx.db.payments })
})
28 changes: 28 additions & 0 deletions routes/payments/send.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { withRouteSpec } from "lib/middleware/with-winter-spec"
import { z } from "zod"

export default withRouteSpec({
methods: ["POST"],
jsonBody: z.object({
amount: z.number().positive(),
currency: z.string().default("USD"),
recipient: z.string(),
description: z.string(),
}),
jsonResponse: z.object({
ok: z.boolean(),
payment_id: z.string(),
}),
})(async (req, ctx) => {
const { amount, currency, recipient, description } = await req.json()
ctx.db.addPayment({
amount,
currency,
recipient,
description,
status: "completed",
})
const payments = ctx.db.payments
const payment = payments[payments.length - 1]
return ctx.json({ ok: true, payment_id: payment.payment_id })
})
28 changes: 28 additions & 0 deletions tests/routes/payments/list.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { getTestServer } from "tests/fixtures/get-test-server"
import { test, expect } from "bun:test"

test("list payments returns empty array initially", async () => {
const { axios } = await getTestServer()

const { data } = await axios.get("/payments/list")

expect(data.payments).toEqual([])
})

test("list payments after sending one", async () => {
const { axios } = await getTestServer()

await axios.post("/payments/send", {
amount: 100,
recipient: "frank@example.com",
description: "Large payment",
})

const { data } = await axios.get("/payments/list")

expect(data.payments).toHaveLength(1)
expect(data.payments[0].amount).toBe(100)
expect(data.payments[0].recipient).toBe("frank@example.com")
expect(data.payments[0].description).toBe("Large payment")
expect(data.payments[0].status).toBe("completed")
})
92 changes: 92 additions & 0 deletions tests/routes/payments/send.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { getTestServer } from "tests/fixtures/get-test-server"
import { test, expect } from "bun:test"

test("send a payment", async () => {
const { axios } = await getTestServer()

const res = await axios.post("/payments/send", {
amount: 42.50,
currency: "USD",
recipient: "alice@example.com",
description: "Test payment",
})

expect(res.status).toBe(200)
expect(res.data.ok).toBe(true)
expect(res.data.payment_id).toBeDefined()
expect(res.data.payment_id).toMatch(/^pay_/)
})

test("send payment with default currency", async () => {
const { axios } = await getTestServer()

const res = await axios.post("/payments/send", {
amount: 10,
recipient: "bob@example.com",
description: "Coffee",
})

expect(res.status).toBe(200)
expect(res.data.ok).toBe(true)
expect(res.data.payment_id).toBeDefined()
})

test("send and list payments", async () => {
const { axios } = await getTestServer()

// Send two payments
await axios.post("/payments/send", {
amount: 25,
recipient: "carol@example.com",
description: "First payment",
})

await axios.post("/payments/send", {
amount: 15.99,
currency: "EUR",
recipient: "dave@example.com",
description: "Second payment",
})

// List payments
const { data } = await axios.get("/payments/list")

expect(data.payments).toHaveLength(2)
expect(data.payments[0].amount).toBe(25)
expect(data.payments[0].recipient).toBe("carol@example.com")
expect(data.payments[0].currency).toBe("USD")
expect(data.payments[1].amount).toBe(15.99)
expect(data.payments[1].currency).toBe("EUR")
expect(data.payments[1].recipient).toBe("dave@example.com")
})

test("send payment with invalid amount", async () => {
const { axios } = await getTestServer()

try {
await axios.post("/payments/send", {
amount: -10,
recipient: "eve@example.com",
description: "Negative amount",
})
// Should not reach here
expect(true).toBe(false)
} catch (err: any) {
expect(err.response.status).toBe(400)
}
})

test("send payment with missing recipient", async () => {
const { axios } = await getTestServer()

try {
await axios.post("/payments/send", {
amount: 50,
description: "Missing recipient",
})
// Should not reach here
expect(true).toBe(false)
} catch (err: any) {
expect(err.response.status).toBe(400)
}
})