seahaven-ap/packages/api/src/db/schema/index.ts
Adam Moussa 9d3646876e
feat(api): stand up Hono Drizzle foundation with auth and Redocly (AP-14) (#12)
* feat(api): stand up Hono Drizzle foundation with auth and Redocly

* fix(api): bump drizzle-orm and hono node-server past audit highs

* fix(api): harden auth upsert and Cognito token verification
2026-08-11 00:10:49 +00:00

198 lines
7.7 KiB
TypeScript

import {
boolean,
integer,
numeric,
pgEnum,
pgTable,
text,
timestamp,
uniqueIndex,
uuid,
} from "drizzle-orm/pg-core";
import { sql } from "drizzle-orm";
/** Keep aligned with `@seahaven-ap/shared` and `USER_ROLES` in env.ts. */
export const userRoleEnum = pgEnum("user_role", ["admin", "ap_processor", "approver", "viewer"]);
export const invoiceStatusEnum = pgEnum("invoice_status", [
"pending_approval",
"approved",
"scheduled",
"paid",
"rejected",
"void",
]);
export const paymentStatusEnum = pgEnum("payment_status", [
"scheduled",
"payment_submitted",
"issued",
"outstanding",
"cleared",
]);
export const paymentMethodEnum = pgEnum("payment_method", ["check", "ach"]);
export const approvalStatusEnum = pgEnum("approval_status", [
"pending",
"approved",
"rejected",
"skipped",
]);
export const users = pgTable(
"users",
{
id: uuid("id").defaultRandom().primaryKey(),
cognitoSub: text("cognito_sub").notNull(),
email: text("email").notNull(),
name: text("name").notNull(),
role: userRoleEnum("role").notNull().default("viewer"),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
},
(table) => [
uniqueIndex("users_cognito_sub_uidx").on(table.cognitoSub),
uniqueIndex("users_email_uidx").on(table.email),
],
);
export const vendors = pgTable("vendors", {
id: uuid("id").defaultRandom().primaryKey(),
name: text("name").notNull(),
email: text("email"),
defaultPaymentMethod: paymentMethodEnum("default_payment_method").notNull().default("check"),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
});
export const glAccounts = pgTable("gl_accounts", {
id: uuid("id").defaultRandom().primaryKey(),
code: text("code").notNull(),
name: text("name").notNull(),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
});
export const departments = pgTable("departments", {
id: uuid("id").defaultRandom().primaryKey(),
code: text("code").notNull(),
name: text("name").notNull(),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
});
export const invoices = pgTable(
"invoices",
{
id: uuid("id").defaultRandom().primaryKey(),
vendorId: uuid("vendor_id")
.notNull()
.references(() => vendors.id),
invoiceNumber: text("invoice_number").notNull(),
amount: numeric("amount", { precision: 14, scale: 2 }).notNull(),
amountDue: numeric("amount_due", { precision: 14, scale: 2 }).notNull(),
dueDate: text("due_date").notNull(),
payDate: text("pay_date"),
sendPaymentOn: text("send_payment_on"),
status: invoiceStatusEnum("status").notNull().default("pending_approval"),
paymentMethod: paymentMethodEnum("payment_method").notNull().default("check"),
memo: text("memo").notNull().default(""),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
},
(table) => [
uniqueIndex("invoices_vendor_number_active_uidx")
.on(table.vendorId, table.invoiceNumber)
.where(sql`${table.status} <> 'void'`),
],
);
export const invoiceLines = pgTable("invoice_lines", {
id: uuid("id").defaultRandom().primaryKey(),
invoiceId: uuid("invoice_id")
.notNull()
.references(() => invoices.id, { onDelete: "cascade" }),
description: text("description").notNull(),
amount: numeric("amount", { precision: 14, scale: 2 }).notNull(),
glAccountId: uuid("gl_account_id").references(() => glAccounts.id),
departmentId: uuid("department_id").references(() => departments.id),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
});
export const invoiceComments = pgTable("invoice_comments", {
id: uuid("id").defaultRandom().primaryKey(),
invoiceId: uuid("invoice_id")
.notNull()
.references(() => invoices.id, { onDelete: "cascade" }),
authorUserId: uuid("author_user_id").references(() => users.id),
body: text("body").notNull(),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
});
export const activityLog = pgTable("activity_log", {
id: uuid("id").defaultRandom().primaryKey(),
invoiceId: uuid("invoice_id")
.notNull()
.references(() => invoices.id, { onDelete: "cascade" }),
actorUserId: uuid("actor_user_id").references(() => users.id),
message: text("message").notNull(),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
});
export const approvalPolicies = pgTable("approval_policies", {
id: uuid("id").defaultRandom().primaryKey(),
name: text("name").notNull(),
priority: integer("priority").notNull().default(100),
amountThreshold: numeric("amount_threshold", { precision: 14, scale: 2 }),
skipBelowAmount: numeric("skip_below_amount", { precision: 14, scale: 2 }),
active: boolean("active").notNull().default(true),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
});
export const approvalSteps = pgTable("approval_steps", {
id: uuid("id").defaultRandom().primaryKey(),
invoiceId: uuid("invoice_id")
.notNull()
.references(() => invoices.id, { onDelete: "cascade" }),
policyId: uuid("policy_id").references(() => approvalPolicies.id),
stepOrder: integer("step_order").notNull().default(1),
approverRole: userRoleEnum("approver_role").notNull().default("approver"),
assigneeUserId: uuid("assignee_user_id").references(() => users.id),
status: approvalStatusEnum("status").notNull().default("pending"),
actedByUserId: uuid("acted_by_user_id").references(() => users.id),
actedAt: timestamp("acted_at", { withTimezone: true }),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
});
export const documents = pgTable("documents", {
id: uuid("id").defaultRandom().primaryKey(),
invoiceId: uuid("invoice_id").references(() => invoices.id, { onDelete: "cascade" }),
objectKey: text("object_key").notNull(),
contentType: text("content_type").notNull().default("application/pdf"),
fileName: text("file_name").notNull(),
uploadedByUserId: uuid("uploaded_by_user_id").references(() => users.id),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
});
export const payRuns = pgTable("pay_runs", {
id: uuid("id").defaultRandom().primaryKey(),
createdByUserId: uuid("created_by_user_id").references(() => users.id),
status: text("status").notNull().default("draft"),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
});
export const payments = pgTable("payments", {
id: uuid("id").defaultRandom().primaryKey(),
payRunId: uuid("pay_run_id").references(() => payRuns.id),
invoiceId: uuid("invoice_id").references(() => invoices.id),
vendorId: uuid("vendor_id").references(() => vendors.id),
amount: numeric("amount", { precision: 14, scale: 2 }).notNull(),
paymentMethod: paymentMethodEnum("payment_method").notNull().default("check"),
checkNumber: integer("check_number"),
status: paymentStatusEnum("status").notNull().default("scheduled"),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
});