billing skeleton: bun + hono + sqlite, seed + happy path
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
import { Hono } from "hono";
|
||||
import { logger } from "hono/logger";
|
||||
import { z } from "zod";
|
||||
import { db } from "./db";
|
||||
|
||||
const app = new Hono();
|
||||
app.use(logger());
|
||||
|
||||
app.get("/health", (c) => c.json({ ok: true }));
|
||||
|
||||
// --- Customers -------------------------------------------------------------
|
||||
|
||||
app.get("/customers", (c) => {
|
||||
return c.json(db.query("SELECT * FROM customers ORDER BY created_at").all());
|
||||
});
|
||||
|
||||
app.get("/customers/:id/invoices", (c) => {
|
||||
const rows = db
|
||||
.query("SELECT * FROM invoices WHERE customer_id = ? ORDER BY created_at DESC")
|
||||
.all(c.req.param("id"));
|
||||
return c.json(rows);
|
||||
});
|
||||
|
||||
// --- Subscriptions (happy path: subscribe -> first invoice) -----------------
|
||||
|
||||
const CreateSubscription = z.object({
|
||||
customer_id: z.string(),
|
||||
plan_id: z.string(),
|
||||
});
|
||||
|
||||
app.post("/subscriptions", async (c) => {
|
||||
const parsed = CreateSubscription.safeParse(await c.req.json().catch(() => null));
|
||||
if (!parsed.success) return c.json({ error: z.treeifyError(parsed.error) }, 400);
|
||||
const { customer_id, plan_id } = parsed.data;
|
||||
|
||||
const customer = db.query("SELECT id FROM customers WHERE id = ?").get(customer_id);
|
||||
if (!customer) return c.json({ error: "customer not found" }, 404);
|
||||
|
||||
const plan = db
|
||||
.query<{ id: string; amount_cents: number; currency: string; interval: string }, [string]>(
|
||||
"SELECT * FROM plans WHERE id = ?",
|
||||
)
|
||||
.get(plan_id);
|
||||
if (!plan) return c.json({ error: "plan not found" }, 404);
|
||||
|
||||
const subId = `sub_${crypto.randomUUID().slice(0, 8)}`;
|
||||
const invId = `inv_${crypto.randomUUID().slice(0, 8)}`;
|
||||
|
||||
db.transaction(() => {
|
||||
db.run(
|
||||
`INSERT INTO subscriptions (id, customer_id, plan_id, status, current_period_start, current_period_end)
|
||||
VALUES (?, ?, ?, 'active', datetime('now'), datetime('now', ?))`,
|
||||
[subId, customer_id, plan_id, `+1 ${plan.interval}`],
|
||||
);
|
||||
db.run(
|
||||
`INSERT INTO invoices (id, subscription_id, customer_id, amount_cents, currency, status, due_at)
|
||||
VALUES (?, ?, ?, ?, ?, 'open', datetime('now', '+14 days'))`,
|
||||
[invId, subId, customer_id, plan.amount_cents, plan.currency],
|
||||
);
|
||||
})();
|
||||
|
||||
const subscription = db.query("SELECT * FROM subscriptions WHERE id = ?").get(subId);
|
||||
const invoice = db.query("SELECT * FROM invoices WHERE id = ?").get(invId);
|
||||
return c.json({ subscription, invoice }, 201);
|
||||
});
|
||||
|
||||
export default {
|
||||
port: Number(process.env.PORT ?? 3000),
|
||||
fetch: app.fetch,
|
||||
};
|
||||
|
||||
console.log(`listening on http://localhost:${process.env.PORT ?? 3000}`);
|
||||
Reference in New Issue
Block a user