From 895b51fe2b47d55f9354e2fe2abf9599a27f6820 Mon Sep 17 00:00:00 2001 From: tungnguyentu Date: Fri, 15 May 2026 08:04:56 +0700 Subject: [PATCH 1/2] feat: add payment send/list API with schema and tests Adds a /payments/send POST endpoint that creates and immediately marks a payment as sent, and a /payments/list GET endpoint with optional filtering by recipient_email and status. Co-Authored-By: Claude Sonnet 4.6 --- lib/db/db-client.ts | 34 ++++++++++++++++++++++- lib/db/schema.ts | 12 +++++++++ routes/payments/list.ts | 43 ++++++++++++++++++++++++++++++ routes/payments/send.ts | 43 ++++++++++++++++++++++++++++++ tests/routes/payments/send.test.ts | 41 ++++++++++++++++++++++++++++ 5 files changed, 172 insertions(+), 1 deletion(-) create mode 100644 routes/payments/list.ts create mode 100644 routes/payments/send.ts create mode 100644 tests/routes/payments/send.test.ts diff --git a/lib/db/db-client.ts b/lib/db/db-client.ts index e525e65..ab063c9 100644 --- a/lib/db/db-client.ts +++ b/lib/db/db-client.ts @@ -2,7 +2,12 @@ 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 Payment, + type Thing, +} from "./schema.ts" import { combine } from "zustand/middleware" export const createDatabase = () => { @@ -21,4 +26,31 @@ const initializer = combine(databaseSchema.parse({}), (set) => ({ idCounter: state.idCounter + 1, })) }, + addPayment: ( + payment: Omit, + ) => { + set((state) => ({ + payments: [ + ...state.payments, + { + ...payment, + payment_id: `pay_${state.idCounter}`, + status: "pending" as const, + created_at: new Date().toISOString(), + }, + ], + idCounter: state.idCounter + 1, + })) + }, + updatePaymentStatus: ( + payment_id: string, + status: Payment["status"], + sent_at?: string, + ) => { + set((state) => ({ + payments: state.payments.map((p) => + p.payment_id === payment_id ? { ...p, status, sent_at } : p, + ), + })) + }, })) diff --git a/lib/db/schema.ts b/lib/db/schema.ts index 8377516..38ab9c6 100644 --- a/lib/db/schema.ts +++ b/lib/db/schema.ts @@ -9,8 +9,20 @@ export const thingSchema = z.object({ }) export type Thing = z.infer +export const paymentSchema = z.object({ + payment_id: z.string(), + recipient_email: z.string().email(), + amount_usd: z.number().positive(), + note: z.string().optional(), + status: z.enum(["pending", "sent", "failed"]).default("pending"), + created_at: z.string(), + sent_at: z.string().optional(), +}) +export type Payment = z.infer + 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 diff --git a/routes/payments/list.ts b/routes/payments/list.ts new file mode 100644 index 0000000..7badd6a --- /dev/null +++ b/routes/payments/list.ts @@ -0,0 +1,43 @@ +import { withRouteSpec } from "lib/middleware/with-winter-spec" +import { z } from "zod" + +const paymentResponseSchema = z.object({ + payment_id: z.string(), + recipient_email: z.string(), + amount_usd: z.number(), + note: z.string().optional(), + status: z.enum(["pending", "sent", "failed"]), + created_at: z.string(), + sent_at: z.string().optional(), +}) + +export default withRouteSpec({ + methods: ["GET"], + queryParams: z.object({ + recipient_email: z.string().email().optional(), + status: z.enum(["pending", "sent", "failed"]).optional(), + }), + jsonResponse: z.object({ + ok: z.boolean(), + payments: z.array(paymentResponseSchema), + }), +})(async (req, ctx) => { + const url = new URL(req.url) + const recipient_email = url.searchParams.get("recipient_email") ?? undefined + const status = url.searchParams.get("status") as + | "pending" + | "sent" + | "failed" + | undefined + + let payments = ctx.db.getState().payments + + if (recipient_email) { + payments = payments.filter((p) => p.recipient_email === recipient_email) + } + if (status) { + payments = payments.filter((p) => p.status === status) + } + + return ctx.json({ ok: true, payments }) +}) diff --git a/routes/payments/send.ts b/routes/payments/send.ts new file mode 100644 index 0000000..e803b70 --- /dev/null +++ b/routes/payments/send.ts @@ -0,0 +1,43 @@ +import { withRouteSpec } from "lib/middleware/with-winter-spec" +import { z } from "zod" + +export default withRouteSpec({ + methods: ["POST"], + jsonBody: z.object({ + recipient_email: z.string().email(), + amount_usd: z.number().positive(), + note: z.string().optional(), + }), + jsonResponse: z.object({ + ok: z.boolean(), + payment: z.object({ + payment_id: z.string(), + recipient_email: z.string(), + amount_usd: z.number(), + note: z.string().optional(), + status: z.enum(["pending", "sent", "failed"]), + created_at: z.string(), + sent_at: z.string().optional(), + }), + }), +})(async (req, ctx) => { + const { recipient_email, amount_usd, note } = await req.json() + + ctx.db.addPayment({ recipient_email, amount_usd, note }) + + const payments = ctx.db.getState().payments + const payment = payments[payments.length - 1] + + // Simulate async send: immediately mark as sent in fake mode + ctx.db.updatePaymentStatus( + payment.payment_id, + "sent", + new Date().toISOString(), + ) + + const sent = ctx.db + .getState() + .payments.find((p) => p.payment_id === payment.payment_id)! + + return ctx.json({ ok: true, payment: sent }) +}) diff --git a/tests/routes/payments/send.test.ts b/tests/routes/payments/send.test.ts new file mode 100644 index 0000000..6742a26 --- /dev/null +++ b/tests/routes/payments/send.test.ts @@ -0,0 +1,41 @@ +import { getTestServer } from "tests/fixtures/get-test-server" +import { test, expect } from "bun:test" + +test("send a payment and verify it appears in list", async () => { + const { axios } = await getTestServer() + + const { data: sendData } = await axios.post("/payments/send", { + recipient_email: "alice@example.com", + amount_usd: 25, + note: "Bounty reward", + }) + + expect(sendData.ok).toBe(true) + expect(sendData.payment.recipient_email).toBe("alice@example.com") + expect(sendData.payment.amount_usd).toBe(25) + expect(sendData.payment.status).toBe("sent") + expect(sendData.payment.sent_at).toBeDefined() + + const { data: listData } = await axios.get("/payments/list") + expect(listData.payments).toHaveLength(1) + expect(listData.payments[0].payment_id).toBe(sendData.payment.payment_id) +}) + +test("filter payments by recipient_email", async () => { + const { axios } = await getTestServer() + + await axios.post("/payments/send", { + recipient_email: "alice@example.com", + amount_usd: 25, + }) + await axios.post("/payments/send", { + recipient_email: "bob@example.com", + amount_usd: 50, + }) + + const { data } = await axios.get( + "/payments/list?recipient_email=alice@example.com", + ) + expect(data.payments).toHaveLength(1) + expect(data.payments[0].recipient_email).toBe("alice@example.com") +}) From 00243319d491d1e79b5284eb23090c42361e1d83 Mon Sep 17 00:00:00 2001 From: tungnguyentu Date: Fri, 15 May 2026 09:12:21 +0700 Subject: [PATCH 2/2] chore: switch from redaxios to ky for test HTTP client Replace redaxios with ky in the test fixture and all test files. ky is already used in template-api-fake and is the preferred HTTP client across the tscircuit template ecosystem. --- bun.lockb | Bin 67071 -> 68508 bytes package.json | 2 +- tests/fixtures/get-test-server.ts | 12 ++++----- tests/routes/health.test.ts | 7 +++--- tests/routes/payments/send.test.ts | 38 ++++++++++++++++------------- tests/routes/things/create.test.ts | 9 +++---- 6 files changed, 34 insertions(+), 34 deletions(-) diff --git a/bun.lockb b/bun.lockb index 557d79cc3b88accaaa0f1f96a19cff1aa0a4aa86..9ef212388bb3a8728ee6ae9c74b1d263c6a1838b 100755 GIT binary patch delta 13959 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 Qx4hevZk-=VwaeE1FQrI2AOHXW diff --git a/package.json b/package.json index d03438f..8caed8f 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "@types/bun": "latest", "@types/react": "18.3.4", "next": "^14.2.5", - "redaxios": "^0.5.1" + "ky": "^1.8.1" }, "peerDependencies": { "typescript": "^5.0.0" diff --git a/tests/fixtures/get-test-server.ts b/tests/fixtures/get-test-server.ts index 7063c41..ffaf284 100644 --- a/tests/fixtures/get-test-server.ts +++ b/tests/fixtures/get-test-server.ts @@ -1,12 +1,11 @@ import { afterEach } from "bun:test" -import { tmpdir } from "node:os" -import defaultAxios from "redaxios" +import ky from "ky" import { startServer } from "./start-server" interface TestFixture { url: string server: any - axios: typeof defaultAxios + ky: typeof ky } export const getTestServer = async (): Promise => { @@ -20,18 +19,17 @@ export const getTestServer = async (): Promise => { }) const url = `http://127.0.0.1:${port}` - const axios = defaultAxios.create({ - baseURL: url, + const kyInstance = ky.create({ + prefixUrl: url, }) afterEach(async () => { await server.stop() - // Here you might want to add logic to drop the test database }) return { url, server, - axios, + ky: kyInstance, } } diff --git a/tests/routes/health.test.ts b/tests/routes/health.test.ts index 935db09..e97fcc3 100644 --- a/tests/routes/health.test.ts +++ b/tests/routes/health.test.ts @@ -2,8 +2,7 @@ import { it, expect } from "bun:test" import { getTestServer } from "tests/fixtures/get-test-server" it("GET /health should return ok", async () => { - const { axios } = await getTestServer() - const res = await axios.get("/health") - expect(res.status).toBe(200) - expect(res.data).toEqual({ ok: true }) + const { ky } = await getTestServer() + const data = await ky.get("health").json() + expect(data).toEqual({ ok: true }) }) diff --git a/tests/routes/payments/send.test.ts b/tests/routes/payments/send.test.ts index 6742a26..bb93a6d 100644 --- a/tests/routes/payments/send.test.ts +++ b/tests/routes/payments/send.test.ts @@ -2,13 +2,17 @@ import { getTestServer } from "tests/fixtures/get-test-server" import { test, expect } from "bun:test" test("send a payment and verify it appears in list", async () => { - const { axios } = await getTestServer() + const { ky } = await getTestServer() - const { data: sendData } = await axios.post("/payments/send", { - recipient_email: "alice@example.com", - amount_usd: 25, - note: "Bounty reward", - }) + const sendData = await ky + .post("payments/send", { + json: { + recipient_email: "alice@example.com", + amount_usd: 25, + note: "Bounty reward", + }, + }) + .json() expect(sendData.ok).toBe(true) expect(sendData.payment.recipient_email).toBe("alice@example.com") @@ -16,26 +20,26 @@ test("send a payment and verify it appears in list", async () => { expect(sendData.payment.status).toBe("sent") expect(sendData.payment.sent_at).toBeDefined() - const { data: listData } = await axios.get("/payments/list") + const listData = await ky.get("payments/list").json() expect(listData.payments).toHaveLength(1) expect(listData.payments[0].payment_id).toBe(sendData.payment.payment_id) }) test("filter payments by recipient_email", async () => { - const { axios } = await getTestServer() + const { ky } = await getTestServer() - await axios.post("/payments/send", { - recipient_email: "alice@example.com", - amount_usd: 25, + await ky.post("payments/send", { + json: { recipient_email: "alice@example.com", amount_usd: 25 }, }) - await axios.post("/payments/send", { - recipient_email: "bob@example.com", - amount_usd: 50, + await ky.post("payments/send", { + json: { recipient_email: "bob@example.com", amount_usd: 50 }, }) - const { data } = await axios.get( - "/payments/list?recipient_email=alice@example.com", - ) + const data = await ky + .get("payments/list", { + searchParams: { recipient_email: "alice@example.com" }, + }) + .json() expect(data.payments).toHaveLength(1) expect(data.payments[0].recipient_email).toBe("alice@example.com") }) diff --git a/tests/routes/things/create.test.ts b/tests/routes/things/create.test.ts index 4ea7077..66f9a5d 100644 --- a/tests/routes/things/create.test.ts +++ b/tests/routes/things/create.test.ts @@ -2,14 +2,13 @@ import { getTestServer } from "tests/fixtures/get-test-server" import { test, expect } from "bun:test" test("create a thing", async () => { - const { axios } = await getTestServer() + const { ky } = await getTestServer() - axios.post("/things/create", { - name: "Thing1", - description: "Thing1 Description", + await ky.post("things/create", { + json: { name: "Thing1", description: "Thing1 Description" }, }) - const { data } = await axios.get("/things/list") + const data = await ky.get("things/list").json() expect(data.things).toHaveLength(1) })