916ad39176
Deploy to route.crispygoat.com / deploy (push) Failing after 3m1s
- 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
374 lines
12 KiB
TypeScript
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;
|