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
64 changes: 60 additions & 4 deletions lib/db/db-client.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
import { createStore, type StoreApi } from "zustand/vanilla"
import { type HoistedStoreApi, hoist } from "zustand-hoist"
import { immer } from "zustand/middleware/immer"
import { hoist, type HoistedStoreApi } from "zustand-hoist"
import { type StoreApi, createStore } from "zustand/vanilla"

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

export const createDatabase = () => {
return hoist(createStore(initializer))
}

export type DbClient = ReturnType<typeof createDatabase>

const initializer = combine(databaseSchema.parse({}), (set) => ({
const initializer = combine(databaseSchema.parse({}), (set, get) => ({
addThing: (thing: Omit<Thing, "thing_id">) => {
set((state) => ({
things: [
Expand All @@ -21,4 +26,55 @@ const initializer = combine(databaseSchema.parse({}), (set) => ({
idCounter: state.idCounter + 1,
}))
},

createPayment: (
payment: Omit<Payment, "payment_id" | "created_at" | "status">,
) => {
const newPayment: Payment = {
...payment,
payment_id: `pay_${get().idCounter}`,
status: "pending",
created_at: new Date().toISOString(),
}
set((state) => ({
payments: [...state.payments, newPayment],
idCounter: state.idCounter + 1,
}))
return newPayment
},

completePayment: (payment_id: string) => {
set((state) => ({
payments: state.payments.map((p) =>
p.payment_id === payment_id
? {
...p,
status: "completed" as const,
completed_at: new Date().toISOString(),
}
: p,
),
}))
},

getPayment: (payment_id: string) => {
return get().payments.find((p) => p.payment_id === payment_id)
},

listPayments: (filters?: {
recipient?: string
status?: Payment["status"]
}) => {
let payments = get().payments

if (filters?.recipient) {
payments = payments.filter((p) => p.recipient === filters.recipient)
}

if (filters?.status) {
payments = payments.filter((p) => p.status === filters.status)
}

return payments
},
}))
15 changes: 15 additions & 0 deletions lib/db/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,23 @@ export const thingSchema = z.object({
})
export type Thing = z.infer<typeof thingSchema>

export const paymentSchema = z.object({
payment_id: z.string(),
recipient: z.string(),
amount: z.number().positive(),
currency: z.string().default("USD"),
status: z.enum(["pending", "completed", "failed"]).default("pending"),
bounty_id: z.string().optional(),
issue_number: z.number().optional(),
repository: z.string().optional(),
created_at: z.string(),
completed_at: z.string().optional(),
})
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>
16 changes: 16 additions & 0 deletions routes/payments/complete.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { withRouteSpec } from "lib/middleware/with-winter-spec"
import { z } from "zod"

export default withRouteSpec({
methods: ["POST"],
jsonBody: z.object({
payment_id: z.string(),
}),
jsonResponse: z.object({
ok: z.boolean(),
}),
})(async (req, ctx) => {
const { payment_id } = await req.json()
ctx.db.completePayment(payment_id)
return ctx.json({ ok: true })
})
22 changes: 22 additions & 0 deletions routes/payments/create.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { paymentSchema } from "lib/db/schema"
import { withRouteSpec } from "lib/middleware/with-winter-spec"
import { z } from "zod"

export default withRouteSpec({
methods: ["POST"],
jsonBody: z.object({
recipient: z.string(),
amount: z.number().positive(),
currency: z.string().optional().default("USD"),
bounty_id: z.string().optional(),
issue_number: z.number().optional(),
repository: z.string().optional(),
}),
jsonResponse: z.object({
payment: paymentSchema,
}),
})(async (req, ctx) => {
const body = await req.json()
const payment = ctx.db.createPayment(body)
return ctx.json({ payment })
})
23 changes: 23 additions & 0 deletions routes/payments/get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { paymentSchema } from "lib/db/schema"
import { withRouteSpec } from "lib/middleware/with-winter-spec"
import { z } from "zod"

export default withRouteSpec({
methods: ["GET"],
queryParams: z.object({
payment_id: z.string(),
}),
jsonResponse: z.object({
payment: paymentSchema.nullable(),
}),
})(async (req, ctx) => {
const url = new URL(req.url)
const payment_id = url.searchParams.get("payment_id")

if (!payment_id) {
return ctx.json({ payment: null })
}

const payment = ctx.db.getPayment(payment_id)
return ctx.json({ payment: payment || null })
})
25 changes: 25 additions & 0 deletions routes/payments/list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { paymentSchema } from "lib/db/schema"
import { withRouteSpec } from "lib/middleware/with-winter-spec"
import { z } from "zod"

export default withRouteSpec({
methods: ["GET"],
queryParams: z.object({
recipient: z.string().optional(),
status: z.enum(["pending", "completed", "failed"]).optional(),
}),
jsonResponse: z.object({
payments: z.array(paymentSchema),
}),
})(async (req, ctx) => {
const url = new URL(req.url)
const recipient = url.searchParams.get("recipient") || undefined
const status = url.searchParams.get("status") as
| "pending"
| "completed"
| "failed"
| undefined

const payments = ctx.db.listPayments({ recipient, status })
return ctx.json({ payments })
})
26 changes: 26 additions & 0 deletions tests/routes/payments/complete.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { expect, test } from "bun:test"
import { getTestServer } from "tests/fixtures/get-test-server"

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

// Create a payment
const createResponse = await axios.post("/payments/create", {
recipient: "user1",
amount: 100,
})

const payment_id = createResponse.data.payment.payment_id

// Complete the payment
const completeResponse = await axios.post("/payments/complete", {
payment_id,
})

expect(completeResponse.data.ok).toBe(true)

// Verify payment status
const getResponse = await axios.get(`/payments/get?payment_id=${payment_id}`)
expect(getResponse.data.payment.status).toBe("completed")
expect(getResponse.data.payment.completed_at).toBeDefined()
})
35 changes: 35 additions & 0 deletions tests/routes/payments/create.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { expect, test } from "bun:test"
import { getTestServer } from "tests/fixtures/get-test-server"

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

const { data } = await axios.post("/payments/create", {
recipient: "user123",
amount: 100,
currency: "USD",
bounty_id: "bounty_456",
issue_number: 123,
repository: "tscircuit/test-repo",
})

expect(data.payment).toBeDefined()
expect(data.payment.recipient).toBe("user123")
expect(data.payment.amount).toBe(100)
expect(data.payment.status).toBe("pending")
expect(data.payment.payment_id).toContain("pay_")
})

test("create payment with minimal fields", async () => {
const { axios } = await getTestServer()

const { data } = await axios.post("/payments/create", {
recipient: "user456",
amount: 50,
})

expect(data.payment).toBeDefined()
expect(data.payment.recipient).toBe("user456")
expect(data.payment.amount).toBe(50)
expect(data.payment.currency).toBe("USD")
})
42 changes: 42 additions & 0 deletions tests/routes/payments/list.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { expect, test } from "bun:test"
import { getTestServer } from "tests/fixtures/get-test-server"

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

// Create some payments
await axios.post("/payments/create", {
recipient: "user1",
amount: 100,
})

await axios.post("/payments/create", {
recipient: "user2",
amount: 200,
})

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

expect(data.payments).toBeDefined()
expect(data.payments.length).toBe(2)
})

test("list payments filtered by recipient", async () => {
const { axios } = await getTestServer()

await axios.post("/payments/create", {
recipient: "user1",
amount: 100,
})

await axios.post("/payments/create", {
recipient: "user2",
amount: 200,
})

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

expect(data.payments).toBeDefined()
expect(data.payments.length).toBe(1)
expect(data.payments[0].recipient).toBe("user1")
})