Files
route-commerce/db/schema/wholesale.ts
T
openclaw c86e97e816
Deploy to route.crispygoat.com / deploy (push) Failing after 7s
feat(storage): MinIO object storage, Neon Auth, Supabase removal
- Add MinIO/S3-compatible storage client (src/lib/storage.ts) with uploadObject,
  deleteObject, presigned URL helpers, and BUCKETS constant
- Wire product images, brand logos, and water log photos to MinIO via the
  new storage client
- Migrate forgot-password to Neon Auth (remove Supabase /auth/v1/recover call)
- Migrate send-scheduled cron to direct Postgres + Resend (remove Supabase Edge
  Function proxy)
- Add logoUrl to email types (OrderReceipt, Welcome, PasswordReset) and pass
  brand_settings.logo_url from all call sites
- Update email templates to use dynamic logoUrl instead of hardcoded Supabase
  bucket URLs
- Remove hardcoded Supabase URLs from TuxedoVideoHero, TuxedoAboutPage,
  TimeTrackingFieldClient; use brand_settings props + local public/ fallback
- Download brand logos (3) and tuxedo-hero.mp4 (36MB) from Supabase bucket to
  public/ for local development
- Add MinIO env vars to .env.example (endpoint, access key, secret, buckets)
- Fix TimeTrackingFieldClient to destructure logoUrl and brandAccent props
- Fix admin/users.ts logoUrl type (null → undefined for optional string)
- Remove stale sb- cookie from wholesale-auth
- Migrate tuxedo/about page to remove supabase import and use pool query for
  wholesale_settings lookup
2026-06-09 12:21:57 -06:00

374 lines
12 KiB
TypeScript

/**
* Wholesale portal. Source: `db/migrations/0001_init.sql`.
*/
import {
pgTable,
uuid,
text,
numeric,
integer,
boolean,
timestamp,
date,
index,
uniqueIndex,
jsonb,
} from "drizzle-orm/pg-core";
import { brands } from "./brands";
import { authUsers } from "./brands";
export const wholesaleSettings = pgTable(
"wholesale_settings",
{
id: uuid("id").primaryKey().defaultRandom(),
brandId: uuid("brand_id")
.notNull()
.unique()
.references(() => brands.id, { onDelete: "cascade" }),
requireApproval: boolean("require_approval").notNull().default(true),
minOrderAmount: numeric("min_order_amount", { precision: 10, scale: 2 }),
onlinePaymentEnabled: boolean("online_payment_enabled")
.notNull()
.default(false),
pickupLocation: text("pickup_location"),
fobLocation: text("fob_location"),
fromEmail: text("from_email"),
invoiceBusinessName: text("invoice_business_name"),
lastInvoiceNumber: integer("last_invoice_number").notNull().default(0),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true })
.notNull()
.defaultNow(),
},
);
export const wholesaleCustomers = pgTable(
"wholesale_customers",
{
id: uuid("id").primaryKey().defaultRandom(),
userId: uuid("user_id").references(() => authUsers.id, {
onDelete: "set null",
}),
brandId: uuid("brand_id")
.notNull()
.references(() => brands.id, { onDelete: "cascade" }),
companyName: text("company_name"),
contactName: text("contact_name"),
email: text("email"),
phone: text("phone"),
billingAddress: text("billing_address"),
shippingAddress: text("shipping_address"),
accountStatus: text("account_status", {
enum: ["active", "suspended", "inactive"],
}).notNull().default("active"),
creditLimit: numeric("credit_limit", { precision: 10, scale: 2 })
.notNull()
.default("0"),
depositsEnabled: boolean("deposits_enabled").notNull().default(false),
depositThreshold: numeric("deposit_threshold", { precision: 10, scale: 2 }),
depositPercentage: integer("deposit_percentage"),
orderEmail: text("order_email"),
invoiceEmail: text("invoice_email"),
adminNotes: text("admin_notes"),
role: text("role", { enum: ["buyer", "admin"] })
.notNull()
.default("buyer"),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true })
.notNull()
.defaultNow(),
},
(t) => ({
brandIdx: index("wholesale_customers_brand_idx").on(t.brandId),
}),
);
export const wholesaleProducts = pgTable(
"wholesale_products",
{
id: uuid("id").primaryKey().defaultRandom(),
brandId: uuid("brand_id")
.notNull()
.references(() => brands.id, { onDelete: "cascade" }),
rcProductId: uuid("rc_product_id"),
name: text("name").notNull(),
description: text("description"),
unitType: text("unit_type").notNull().default("each"),
availability: text("availability", {
enum: ["available", "unavailable", "limited", "seasonal"],
}).notNull().default("unavailable"),
qtyAvailable: numeric("qty_available", { precision: 10, scale: 2 })
.default("0"),
priceTiers: jsonb("price_tiers").notNull().default([]),
hpSku: text("hp_sku"),
hpItemId: text("hp_item_id"),
internalNotes: text("internal_notes"),
handlingInstructions: text("handling_instructions"),
transportTemp: text("transport_temp"),
storageWarning: text("storage_warning"),
loadingNotes: text("loading_notes"),
productLabel: text("product_label"),
packStyle: text("pack_style"),
containerType: text("container_type"),
containerSizeCode: text("container_size_code"),
unitsPerContainer: integer("units_per_container"),
containerNotes: text("container_notes"),
defaultPickupLocation: text("default_pickup_location"),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true })
.notNull()
.defaultNow(),
},
(t) => ({
brandIdx: index("wholesale_products_brand_idx").on(t.brandId),
}),
);
export const wholesaleOrders = pgTable(
"wholesale_orders",
{
id: uuid("id").primaryKey().defaultRandom(),
brandId: uuid("brand_id")
.notNull()
.references(() => brands.id, { onDelete: "cascade" }),
customerId: uuid("customer_id")
.notNull()
.references(() => wholesaleCustomers.id),
wcOrderId: numeric("wc_order_id"),
status: text("status", {
enum: [
"pending", "confirmed", "in_production", "ready",
"fulfilled", "canceled",
],
}).notNull().default("pending"),
fulfillmentStatus: text("fulfillment_status", {
enum: ["unfulfilled", "partial", "fulfilled"],
}).notNull().default("unfulfilled"),
paymentStatus: text("payment_status", {
enum: ["unpaid", "deposit_paid", "paid", "refunded"],
}).notNull().default("unpaid"),
anticipatedPickupDate: date("anticipated_pickup_date"),
subtotal: numeric("subtotal", { precision: 10, scale: 2 })
.notNull()
.default("0"),
depositRequired: numeric("deposit_required", { precision: 10, scale: 2 })
.default("0"),
depositPaid: numeric("deposit_paid", { precision: 10, scale: 2 })
.notNull()
.default("0"),
balanceDue: numeric("balance_due", { precision: 10, scale: 2 })
.notNull()
.default("0"),
assignedEmployeeId: uuid("assigned_employee_id"),
fulfillmentNotes: text("fulfillment_notes"),
internalNotes: text("internal_notes"),
invoiceNumber: text("invoice_number"),
invoicePdfPath: text("invoice_pdf_path"),
invoiceToken: text("invoice_token"),
invoiceType: text("invoice_type"),
depositPercentage: integer("deposit_percentage"),
fulfilledAt: timestamp("fulfilled_at", { withTimezone: true }),
fulfilledBy: uuid("fulfilled_by"),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true })
.notNull()
.defaultNow(),
},
(t) => ({
brandIdx: index("wholesale_orders_brand_idx").on(t.brandId),
customerIdx: index("wholesale_orders_customer_idx").on(t.customerId),
statusIdx: index("wholesale_orders_status_idx").on(t.brandId, t.status),
}),
);
export const wholesaleOrderItems = pgTable(
"wholesale_order_items",
{
id: uuid("id").primaryKey().defaultRandom(),
wholesaleOrderId: uuid("wholesale_order_id")
.notNull()
.references(() => wholesaleOrders.id, { onDelete: "cascade" }),
productId: uuid("product_id")
.notNull()
.references(() => wholesaleProducts.id),
quantity: numeric("quantity", { precision: 10, scale: 2 }).notNull(),
unitPrice: numeric("unit_price", { precision: 10, scale: 2 }).notNull(),
lineTotal: numeric("line_total", { precision: 10, scale: 2 }).notNull(),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
},
(t) => ({
orderIdx: index("wholesale_order_items_order_idx").on(
t.wholesaleOrderId,
),
}),
);
export const wholesaleDeposits = pgTable(
"wholesale_deposits",
{
id: uuid("id").primaryKey().defaultRandom(),
wholesaleOrderId: uuid("wholesale_order_id")
.notNull()
.references(() => wholesaleOrders.id, { onDelete: "cascade" }),
amount: numeric("amount", { precision: 10, scale: 2 }).notNull(),
paymentMethod: text("payment_method"),
reference: text("reference"),
recordedBy: uuid("recorded_by").notNull(),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
},
);
export const wholesaleNotifications = pgTable(
"wholesale_notifications",
{
id: uuid("id").primaryKey().defaultRandom(),
brandId: uuid("brand_id")
.notNull()
.references(() => brands.id, { onDelete: "cascade" }),
customerId: uuid("customer_id")
.notNull()
.references(() => wholesaleCustomers.id, { onDelete: "cascade" }),
notificationType: text("notification_type", {
enum: [
"order_confirmed", "order_ready", "pickup_reminder",
"payment_reminder", "invoice",
],
}).notNull(),
channel: text("channel", { enum: ["email", "sms"] }).notNull(),
recipient: text("recipient").notNull(),
subject: text("subject"),
body: text("body").notNull(),
status: text("status", {
enum: ["pending", "sent", "failed"],
}).notNull().default("pending"),
sentAt: timestamp("sent_at", { withTimezone: true }),
errorMessage: text("error_message"),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
},
(t) => ({
brandIdx: index("wholesale_notifications_brand_idx").on(t.brandId),
}),
);
export const wholesaleCustomerProductPricing = pgTable(
"wholesale_customer_product_pricing",
{
id: uuid("id").primaryKey().defaultRandom(),
customerId: uuid("customer_id")
.notNull()
.references(() => wholesaleCustomers.id, { onDelete: "cascade" }),
productId: uuid("product_id")
.notNull()
.references(() => wholesaleProducts.id, { onDelete: "cascade" }),
customPriceCents: integer("custom_price_cents").notNull(),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true })
.notNull()
.defaultNow(),
},
(t) => ({
customerProductIdx: uniqueIndex(
"wholesale_customer_product_pricing_cust_prod_idx",
).on(t.customerId, t.productId),
}),
);
export const wholesaleWebhookSettings = pgTable(
"wholesale_webhook_settings",
{
id: uuid("id").primaryKey().defaultRandom(),
brandId: uuid("brand_id")
.notNull()
.unique()
.references(() => brands.id, { onDelete: "cascade" }),
url: text("url").notNull(),
secret: text("secret").notNull(),
enabled: boolean("enabled").notNull().default(false),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true })
.notNull()
.defaultNow(),
},
);
export const wholesaleSyncLog = pgTable(
"wholesale_sync_log",
{
id: uuid("id").primaryKey().defaultRandom(),
brandId: uuid("brand_id")
.notNull()
.references(() => brands.id, { onDelete: "cascade" }),
eventType: text("event_type").notNull(),
orderId: uuid("order_id"),
payload: jsonb("payload"),
status: text("status", {
enum: ["pending", "sent", "failed"],
}).notNull().default("pending"),
response: text("response"),
attempts: integer("attempts").notNull().default(0),
nextRetry: timestamp("next_retry", { withTimezone: true }),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
},
(t) => ({
brandIdx: index("wholesale_sync_log_brand_idx").on(t.brandId),
}),
);
export const userCarts = pgTable(
"user_carts",
{
id: uuid("id").primaryKey().defaultRandom(),
brandId: uuid("brand_id")
.notNull()
.references(() => brands.id, { onDelete: "cascade" }),
customerId: uuid("customer_id")
.notNull()
.references(() => wholesaleCustomers.id, { onDelete: "cascade" }),
items: jsonb("items").notNull().default([]),
updatedAt: timestamp("updated_at", { withTimezone: true })
.notNull()
.defaultNow(),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
},
(t) => ({
customerIdx: uniqueIndex("user_carts_customer_idx").on(
t.brandId,
t.customerId,
),
}),
);
export type WholesaleSettings = typeof wholesaleSettings.$inferSelect;
export type WholesaleCustomer = typeof wholesaleCustomers.$inferSelect;
export type WholesaleProduct = typeof wholesaleProducts.$inferSelect;
export type WholesaleOrder = typeof wholesaleOrders.$inferSelect;
export type WholesaleOrderItem = typeof wholesaleOrderItems.$inferSelect;
export type WholesaleDeposit = typeof wholesaleDeposits.$inferSelect;
export type WholesaleNotification = typeof wholesaleNotifications.$inferSelect;
export type WholesaleWebhookSettings =
typeof wholesaleWebhookSettings.$inferSelect;
export type WholesaleSyncLog = typeof wholesaleSyncLog.$inferSelect;
export type UserCart = typeof userCarts.$inferSelect;