billing skeleton: bun + hono + sqlite, seed + happy path

This commit is contained in:
Prad Nukala
2026-08-09 19:59:40 -04:00
commit e9ddb3304b
8 changed files with 265 additions and 0 deletions
+40
View File
@@ -0,0 +1,40 @@
import { Database } from "bun:sqlite";
import { join } from "node:path";
export const db = new Database(join(import.meta.dir, "..", "data.db"), { create: true });
db.exec("PRAGMA journal_mode = WAL;");
db.exec("PRAGMA foreign_keys = ON;");
// Migrate: schema is idempotent (CREATE TABLE IF NOT EXISTS)
db.exec(await Bun.file(join(import.meta.dir, "schema.sql")).text());
// Seed once, only if empty
const { n } = db.query<{ n: number }, []>("SELECT count(*) AS n FROM customers").get()!;
if (n === 0) {
db.transaction(() => {
db.run(
`INSERT INTO customers (id, name, email) VALUES
('cus_ada', 'Ada Lovelace', 'ada@example.com'),
('cus_grace', 'Grace Hopper', 'grace@example.com')`,
);
db.run(
`INSERT INTO plans (id, name, amount_cents, currency, interval) VALUES
('plan_basic', 'Basic', 1000, 'usd', 'month'),
('plan_pro', 'Pro', 5000, 'usd', 'month')`,
);
db.run(
`INSERT INTO subscriptions (id, customer_id, plan_id, status, current_period_start, current_period_end) VALUES
('sub_1', 'cus_ada', 'plan_pro', 'active', datetime('now'), datetime('now', '+1 month'))`,
);
db.run(
`INSERT INTO invoices (id, subscription_id, customer_id, amount_cents, currency, status, due_at) VALUES
('inv_1', 'sub_1', 'cus_ada', 5000, 'usd', 'open', datetime('now', '+14 days'))`,
);
})();
console.log("seeded database");
}
// `bun run db:reset` executes this file directly after deleting data.db
if (import.meta.main) {
console.log("db ready:", db.filename);
}
+72
View File
@@ -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}`);
+38
View File
@@ -0,0 +1,38 @@
CREATE TABLE IF NOT EXISTS customers (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS plans (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
amount_cents INTEGER NOT NULL,
currency TEXT NOT NULL DEFAULT 'usd',
interval TEXT NOT NULL DEFAULT 'month' CHECK (interval IN ('month', 'year'))
);
CREATE TABLE IF NOT EXISTS subscriptions (
id TEXT PRIMARY KEY,
customer_id TEXT NOT NULL REFERENCES customers(id),
plan_id TEXT NOT NULL REFERENCES plans(id),
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'past_due', 'canceled')),
current_period_start TEXT NOT NULL,
current_period_end TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS invoices (
id TEXT PRIMARY KEY,
subscription_id TEXT NOT NULL REFERENCES subscriptions(id),
customer_id TEXT NOT NULL REFERENCES customers(id),
amount_cents INTEGER NOT NULL,
currency TEXT NOT NULL DEFAULT 'usd',
status TEXT NOT NULL DEFAULT 'open' CHECK (status IN ('open', 'paid', 'void')),
due_at TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_subscriptions_customer ON subscriptions(customer_id);
CREATE INDEX IF NOT EXISTS idx_invoices_customer ON invoices(customer_id);