70 lines
3.4 KiB
TypeScript
70 lines
3.4 KiB
TypeScript
import type { InferInsertModel, InferSelectModel } from "drizzle-orm";
|
|
import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// drinkkaart — one row per user account (1:1 with user)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export const drinkkaart = sqliteTable("drinkkaart", {
|
|
id: text("id").primaryKey(),
|
|
userId: text("user_id").notNull().unique(),
|
|
balance: integer("balance").notNull().default(0), // in cents
|
|
version: integer("version").notNull().default(0), // optimistic lock counter
|
|
qrSecret: text("qr_secret").notNull(), // CSPRNG 32-byte hex; signs QR tokens
|
|
createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull(),
|
|
updatedAt: integer("updated_at", { mode: "timestamp_ms" }).notNull(),
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// drinkkaart_transaction — immutable audit log of deductions and reversals
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export const drinkkaartTransaction = sqliteTable("drinkkaart_transaction", {
|
|
id: text("id").primaryKey(),
|
|
drinkkaartId: text("drinkkaart_id").notNull(),
|
|
userId: text("user_id").notNull(), // denormalized for fast audit queries
|
|
adminId: text("admin_id").notNull(), // FK → user.id (admin who processed it)
|
|
amountCents: integer("amount_cents").notNull(),
|
|
balanceBefore: integer("balance_before").notNull(),
|
|
balanceAfter: integer("balance_after").notNull(),
|
|
type: text("type", { enum: ["deduction", "reversal"] }).notNull(),
|
|
reversedBy: text("reversed_by"), // nullable; id of the reversal transaction
|
|
reverses: text("reverses"), // nullable; id of the original deduction
|
|
note: text("note"),
|
|
createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull(),
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// drinkkaart_topup — immutable log of every credited amount
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export const drinkkaartTopup = sqliteTable("drinkkaart_topup", {
|
|
id: text("id").primaryKey(),
|
|
drinkkaartId: text("drinkkaart_id").notNull(),
|
|
userId: text("user_id").notNull(), // denormalized
|
|
amountCents: integer("amount_cents").notNull(),
|
|
balanceBefore: integer("balance_before").notNull(),
|
|
balanceAfter: integer("balance_after").notNull(),
|
|
type: text("type", { enum: ["payment", "admin_credit"] }).notNull(),
|
|
lemonsqueezyOrderId: text("lemonsqueezy_order_id").unique(), // nullable; only for type="payment"
|
|
lemonsqueezyCustomerId: text("lemonsqueezy_customer_id"),
|
|
adminId: text("admin_id"), // nullable; only for type="admin_credit"
|
|
reason: text("reason"),
|
|
paidAt: integer("paid_at", { mode: "timestamp_ms" }).notNull(),
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Inferred Drizzle types
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export type Drinkkaart = InferSelectModel<typeof drinkkaart>;
|
|
export type NewDrinkkaart = InferInsertModel<typeof drinkkaart>;
|
|
export type DrinkkaartTransaction = InferSelectModel<
|
|
typeof drinkkaartTransaction
|
|
>;
|
|
export type NewDrinkkaartTransaction = InferInsertModel<
|
|
typeof drinkkaartTransaction
|
|
>;
|
|
export type DrinkkaartTopup = InferSelectModel<typeof drinkkaartTopup>;
|
|
export type NewDrinkkaartTopup = InferInsertModel<typeof drinkkaartTopup>;
|