From c2e00037b614a85b658356a565a40eaca76a4603 Mon Sep 17 00:00:00 2001 From: qingsenlab Date: Thu, 21 May 2026 00:39:34 +0200 Subject: [PATCH] Add payment send API --- lib/db/db-client.ts | 29 ++++++++++++++++++++++++- lib/db/schema.ts | 13 ++++++++++++ routes/payments/list.ts | 12 +++++++++++ routes/payments/send.ts | 29 +++++++++++++++++++++++++ tests/routes/payments/send.test.ts | 34 ++++++++++++++++++++++++++++++ 5 files changed, 116 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..61e0e1a 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,26 @@ const initializer = combine(databaseSchema.parse({}), (set) => ({ idCounter: state.idCounter + 1, })) }, + addPayment: (payment: Omit) => { + let createdPayment: Payment | undefined + + set((state) => { + createdPayment = { + ...payment, + payment_id: state.paymentCounter.toString(), + created_at: new Date().toISOString(), + } + + return { + payments: [...state.payments, createdPayment], + paymentCounter: state.paymentCounter + 1, + } + }) + + if (!createdPayment) { + throw new Error("Payment was not created") + } + + return createdPayment + }, })) diff --git a/lib/db/schema.ts b/lib/db/schema.ts index 8377516..bdca9b4 100644 --- a/lib/db/schema.ts +++ b/lib/db/schema.ts @@ -9,8 +9,21 @@ export const thingSchema = z.object({ }) export type Thing = z.infer +export const paymentSchema = z.object({ + payment_id: z.string(), + recipient: z.string(), + amount_cents: z.number().int().positive(), + currency: z.string(), + memo: z.string().optional(), + status: z.enum(["sent"]), + created_at: z.string(), +}) +export type Payment = z.infer + export const databaseSchema = z.object({ idCounter: z.number().default(0), things: z.array(thingSchema).default([]), + paymentCounter: z.number().default(0), + 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..6906d9f --- /dev/null +++ b/routes/payments/list.ts @@ -0,0 +1,12 @@ +import { paymentSchema } from "lib/db/schema" +import { withRouteSpec } from "lib/middleware/with-winter-spec" +import { z } from "zod" + +export default withRouteSpec({ + methods: ["GET"], + jsonResponse: z.object({ + payments: z.array(paymentSchema), + }), +})((req, ctx) => { + return ctx.json({ payments: ctx.db.payments }) +}) diff --git a/routes/payments/send.ts b/routes/payments/send.ts new file mode 100644 index 0000000..f60d14b --- /dev/null +++ b/routes/payments/send.ts @@ -0,0 +1,29 @@ +import { withRouteSpec } from "lib/middleware/with-winter-spec" +import { paymentSchema } from "lib/db/schema" +import { z } from "zod" + +export default withRouteSpec({ + methods: ["POST"], + jsonBody: z.object({ + recipient: z.string().min(1), + amount_cents: z.number().int().positive(), + currency: z.string().min(1).default("USD"), + memo: z.string().optional(), + }), + jsonResponse: z.object({ + ok: z.boolean(), + payment: paymentSchema, + }), +})(async (req, ctx) => { + const { recipient, amount_cents, currency = "USD", memo } = await req.json() + + const payment = ctx.db.addPayment({ + recipient, + amount_cents, + currency, + memo, + status: "sent", + }) + + return ctx.json({ ok: true, payment }) +}) diff --git a/tests/routes/payments/send.test.ts b/tests/routes/payments/send.test.ts new file mode 100644 index 0000000..f39ff92 --- /dev/null +++ b/tests/routes/payments/send.test.ts @@ -0,0 +1,34 @@ +import { test, expect } from "bun:test" +import { getTestServer } from "tests/fixtures/get-test-server" + +test("send a payment", async () => { + const { axios } = await getTestServer() + + const { data } = await axios.post("/payments/send", { + recipient: "worker@example.com", + amount_cents: 1000, + memo: "Bounty payout", + }) + + expect(data.ok).toBe(true) + expect(data.payment).toMatchObject({ + payment_id: "0", + recipient: "worker@example.com", + amount_cents: 1000, + currency: "USD", + memo: "Bounty payout", + status: "sent", + }) + expect(typeof data.payment.created_at).toBe("string") + + const listRes = await axios.get("/payments/list") + + expect(listRes.data.payments).toHaveLength(1) + expect(listRes.data.payments[0]).toMatchObject({ + payment_id: "0", + recipient: "worker@example.com", + amount_cents: 1000, + currency: "USD", + status: "sent", + }) +})