feat(storage): MinIO object storage, Neon Auth, Supabase removal
Deploy to route.crispygoat.com / deploy (push) Failing after 3m1s
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
This commit is contained in:
@@ -0,0 +1,373 @@
|
||||
/**
|
||||
* 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;
|
||||
Reference in New Issue
Block a user