Build the Solaire inventory intelligence MVP
Some checks failed
CI / verify (push) Has been cancelled
Some checks failed
CI / verify (push) Has been cancelled
Add the tenant-ready dashboard, freshness and pricing analytics, mock integration adapters, persistence schema, and verification tooling needed for the initial pilot. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
18
src/db/index.ts
Normal file
18
src/db/index.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { drizzle } from "drizzle-orm/postgres-js";
|
||||
import postgres from "postgres";
|
||||
import * as schema from "@/db/schema";
|
||||
|
||||
let client: ReturnType<typeof postgres> | undefined;
|
||||
|
||||
export function getDatabase() {
|
||||
if (!process.env.DATABASE_URL) {
|
||||
throw new Error("DATABASE_URL is required for persistent database access");
|
||||
}
|
||||
|
||||
client ??= postgres(process.env.DATABASE_URL, {
|
||||
max: process.env.NODE_ENV === "production" ? 10 : 1,
|
||||
prepare: false,
|
||||
});
|
||||
|
||||
return drizzle(client, { schema });
|
||||
}
|
||||
194
src/db/schema.ts
Normal file
194
src/db/schema.ts
Normal file
@@ -0,0 +1,194 @@
|
||||
import {
|
||||
boolean,
|
||||
index,
|
||||
integer,
|
||||
jsonb,
|
||||
numeric,
|
||||
pgEnum,
|
||||
pgTable,
|
||||
primaryKey,
|
||||
text,
|
||||
timestamp,
|
||||
uniqueIndex,
|
||||
uuid,
|
||||
} from "drizzle-orm/pg-core";
|
||||
|
||||
const id = () => uuid("id").defaultRandom().primaryKey();
|
||||
const tenant = () => uuid("organization_id").notNull().references(() => organizations.id);
|
||||
const timestamps = {
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
|
||||
};
|
||||
|
||||
export const integrationProvider = pgEnum("integration_provider", ["odoo", "salesforce"]);
|
||||
export const freshnessState = pgEnum("freshness_state", ["fresh", "watch", "at_risk", "expired"]);
|
||||
export const recommendationState = pgEnum("recommendation_state", ["pending", "reviewed", "dismissed"]);
|
||||
export const syncState = pgEnum("sync_state", ["queued", "running", "succeeded", "failed"]);
|
||||
export const memberRole = pgEnum("member_role", ["owner", "manager", "viewer"]);
|
||||
|
||||
export const organizations = pgTable("organizations", {
|
||||
id: id(),
|
||||
name: text("name").notNull(),
|
||||
slug: text("slug").notNull().unique(),
|
||||
...timestamps,
|
||||
});
|
||||
|
||||
export const users = pgTable("users", {
|
||||
id: id(),
|
||||
email: text("email").notNull().unique(),
|
||||
name: text("name").notNull(),
|
||||
passwordHash: text("password_hash"),
|
||||
...timestamps,
|
||||
});
|
||||
|
||||
export const memberships = pgTable(
|
||||
"memberships",
|
||||
{
|
||||
organizationId: uuid("organization_id").notNull().references(() => organizations.id),
|
||||
userId: uuid("user_id").notNull().references(() => users.id),
|
||||
role: memberRole("role").default("viewer").notNull(),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
||||
},
|
||||
(table) => [primaryKey({ columns: [table.organizationId, table.userId] })],
|
||||
);
|
||||
|
||||
export const branches = pgTable("branches", {
|
||||
id: id(),
|
||||
organizationId: tenant(),
|
||||
name: text("name").notNull(),
|
||||
city: text("city").notNull(),
|
||||
region: text("region").notNull(),
|
||||
sourceId: text("source_id"),
|
||||
...timestamps,
|
||||
}, (table) => [
|
||||
index("branches_organization_idx").on(table.organizationId),
|
||||
uniqueIndex("branches_source_unique").on(table.organizationId, table.sourceId),
|
||||
]);
|
||||
|
||||
export const salespeople = pgTable("salespeople", {
|
||||
id: id(),
|
||||
organizationId: tenant(),
|
||||
branchId: uuid("branch_id").notNull().references(() => branches.id),
|
||||
name: text("name").notNull(),
|
||||
email: text("email"),
|
||||
sourceId: text("source_id"),
|
||||
...timestamps,
|
||||
}, (table) => [index("salespeople_organization_idx").on(table.organizationId)]);
|
||||
|
||||
export const products = pgTable("products", {
|
||||
id: id(),
|
||||
organizationId: tenant(),
|
||||
sku: text("sku").notNull(),
|
||||
name: text("name").notNull(),
|
||||
category: text("category").notNull(),
|
||||
unit: text("unit").notNull(),
|
||||
shelfLifeDays: integer("shelf_life_days").notNull(),
|
||||
cost: numeric("cost", { precision: 12, scale: 2 }).notNull(),
|
||||
listPrice: numeric("list_price", { precision: 12, scale: 2 }).notNull(),
|
||||
marginFloorPercent: numeric("margin_floor_percent", { precision: 5, scale: 2 }).notNull(),
|
||||
sourceSystem: integrationProvider("source_system").notNull(),
|
||||
sourceId: text("source_id").notNull(),
|
||||
...timestamps,
|
||||
}, (table) => [
|
||||
uniqueIndex("products_sku_unique").on(table.organizationId, table.sku),
|
||||
uniqueIndex("products_source_unique").on(table.organizationId, table.sourceSystem, table.sourceId),
|
||||
]);
|
||||
|
||||
export const inventoryBatches = pgTable("inventory_batches", {
|
||||
id: id(),
|
||||
organizationId: tenant(),
|
||||
productId: uuid("product_id").notNull().references(() => products.id),
|
||||
branchId: uuid("branch_id").notNull().references(() => branches.id),
|
||||
receivedAt: timestamp("received_at", { withTimezone: true }).notNull(),
|
||||
quantityReceived: integer("quantity_received").notNull(),
|
||||
quantityOnHand: integer("quantity_on_hand").notNull(),
|
||||
freshnessScore: integer("freshness_score").notNull(),
|
||||
freshnessState: freshnessState("freshness_state").notNull(),
|
||||
sourceId: text("source_id").notNull(),
|
||||
...timestamps,
|
||||
}, (table) => [
|
||||
index("inventory_tenant_product_idx").on(table.organizationId, table.productId),
|
||||
uniqueIndex("inventory_source_unique").on(table.organizationId, table.sourceId),
|
||||
]);
|
||||
|
||||
export const sales = pgTable("sales", {
|
||||
id: id(),
|
||||
organizationId: tenant(),
|
||||
productId: uuid("product_id").notNull().references(() => products.id),
|
||||
branchId: uuid("branch_id").notNull().references(() => branches.id),
|
||||
salespersonId: uuid("salesperson_id").references(() => salespeople.id),
|
||||
soldAt: timestamp("sold_at", { withTimezone: true }).notNull(),
|
||||
quantity: integer("quantity").notNull(),
|
||||
unitPrice: numeric("unit_price", { precision: 12, scale: 2 }).notNull(),
|
||||
sourceId: text("source_id").notNull(),
|
||||
...timestamps,
|
||||
}, (table) => [
|
||||
index("sales_tenant_date_idx").on(table.organizationId, table.soldAt),
|
||||
uniqueIndex("sales_source_unique").on(table.organizationId, table.sourceId),
|
||||
]);
|
||||
|
||||
export const integrationConnections = pgTable("integration_connections", {
|
||||
id: id(),
|
||||
organizationId: tenant(),
|
||||
provider: integrationProvider("provider").notNull(),
|
||||
displayName: text("display_name").notNull(),
|
||||
enabled: boolean("enabled").default(false).notNull(),
|
||||
encryptedCredentials: text("encrypted_credentials"),
|
||||
configuration: jsonb("configuration").$type<Record<string, unknown>>().default({}).notNull(),
|
||||
lastSyncedAt: timestamp("last_synced_at", { withTimezone: true }),
|
||||
...timestamps,
|
||||
}, (table) => [uniqueIndex("integration_provider_unique").on(table.organizationId, table.provider)]);
|
||||
|
||||
export const syncRuns = pgTable("sync_runs", {
|
||||
id: id(),
|
||||
organizationId: tenant(),
|
||||
connectionId: uuid("connection_id").references(() => integrationConnections.id),
|
||||
provider: integrationProvider("provider").notNull(),
|
||||
state: syncState("state").default("queued").notNull(),
|
||||
cursor: text("cursor"),
|
||||
recordsRead: integer("records_read").default(0).notNull(),
|
||||
recordsWritten: integer("records_written").default(0).notNull(),
|
||||
error: text("error"),
|
||||
startedAt: timestamp("started_at", { withTimezone: true }),
|
||||
finishedAt: timestamp("finished_at", { withTimezone: true }),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
||||
});
|
||||
|
||||
export const recommendations = pgTable("recommendations", {
|
||||
id: id(),
|
||||
organizationId: tenant(),
|
||||
productId: uuid("product_id").notNull().references(() => products.id),
|
||||
branchId: uuid("branch_id").notNull().references(() => branches.id),
|
||||
type: text("type").notNull(),
|
||||
currentPrice: numeric("current_price", { precision: 12, scale: 2 }).notNull(),
|
||||
recommendedLow: numeric("recommended_low", { precision: 12, scale: 2 }).notNull(),
|
||||
recommendedHigh: numeric("recommended_high", { precision: 12, scale: 2 }).notNull(),
|
||||
confidence: integer("confidence").notNull(),
|
||||
explanation: text("explanation").notNull(),
|
||||
state: recommendationState("state").default("pending").notNull(),
|
||||
expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
|
||||
reviewedBy: uuid("reviewed_by").references(() => users.id),
|
||||
...timestamps,
|
||||
});
|
||||
|
||||
export const alerts = pgTable("alerts", {
|
||||
id: id(),
|
||||
organizationId: tenant(),
|
||||
inventoryBatchId: uuid("inventory_batch_id").references(() => inventoryBatches.id),
|
||||
severity: text("severity").notNull(),
|
||||
title: text("title").notNull(),
|
||||
message: text("message").notNull(),
|
||||
acknowledgedAt: timestamp("acknowledged_at", { withTimezone: true }),
|
||||
...timestamps,
|
||||
}, (table) => [index("alerts_tenant_idx").on(table.organizationId)]);
|
||||
|
||||
export const auditEvents = pgTable("audit_events", {
|
||||
id: id(),
|
||||
organizationId: tenant(),
|
||||
userId: uuid("user_id").references(() => users.id),
|
||||
action: text("action").notNull(),
|
||||
entityType: text("entity_type").notNull(),
|
||||
entityId: text("entity_id").notNull(),
|
||||
metadata: jsonb("metadata").$type<Record<string, unknown>>().default({}).notNull(),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
||||
});
|
||||
Reference in New Issue
Block a user