diff --git a/shared/schema.ts b/shared/schema.ts new file mode 100644 index 0000000..4d92bbf --- /dev/null +++ b/shared/schema.ts @@ -0,0 +1,284 @@ +import { pgTable, serial, text, integer, boolean, timestamp, jsonb } from "drizzle-orm/pg-core"; +import { createInsertSchema } from "drizzle-zod"; +import { sql } from "drizzle-orm"; +import { z } from "zod"; + +// ── Customers ────────────────────────────────────────────────────────────────── +export const customers = pgTable("customers", { + id: serial("id").primaryKey(), + email: text("email").notNull().unique(), + firstName: text("first_name"), + lastName: text("last_name"), + phone: text("phone"), + defaultAddressId: integer("default_address_id"), + notes: text("notes"), + tags: text("tags").array(), + authNetProfileId: text("auth_net_profile_id"), + stripeCustomerId: text("stripe_customer_id"), + storeCredit: text("store_credit").notNull().default("0.00"), + magicLinkToken: text("magic_link_token"), + magicLinkTokenExpiresAt: timestamp("magic_link_token_expires_at"), + createdAt: timestamp("created_at").default(sql`CURRENT_TIMESTAMP`).notNull(), + updatedAt: timestamp("updated_at").default(sql`CURRENT_TIMESTAMP`).notNull(), +}); + +export const insertCustomerSchema = createInsertSchema(customers).omit({ id: true, createdAt: true, updatedAt: true }); +export type Customer = typeof customers.$inferSelect; +export type InsertCustomer = z.infer; + +// ── Addresses ───────────────────────────────────────────────────────────────── +export const addresses = pgTable("addresses", { + id: serial("id").primaryKey(), + customerId: integer("customer_id").notNull().references(() => customers.id, { onDelete: "cascade" }), + firstName: text("first_name"), + lastName: text("last_name"), + address1: text("address1").notNull(), + address2: text("address2"), + city: text("city").notNull(), + state: text("state").notNull(), + zip: text("zip").notNull(), + country: text("country").notNull().default("US"), + isDefault: boolean("is_default").default(false), + createdAt: timestamp("created_at").default(sql`CURRENT_TIMESTAMP`).notNull(), +}); + +export const insertAddressSchema = createInsertSchema(addresses).omit({ id: true, createdAt: true }); +export type Address = typeof addresses.$inferSelect; +export type InsertAddress = z.infer; + +// ── Brands ──────────────────────────────────────────────────────────────────── +export const brands = pgTable("brands", { + id: serial("id").primaryKey(), + name: text("name").notNull().unique(), + slug: text("slug").notNull().unique(), + description: text("description"), + logoUrl: text("logo_url"), + websiteUrl: text("website_url"), + createdAt: timestamp("created_at").default(sql`CURRENT_TIMESTAMP`).notNull(), +}); + +export const insertBrandSchema = createInsertSchema(brands).omit({ id: true, createdAt: true }); +export type Brand = typeof brands.$inferSelect; +export type InsertBrand = z.infer; + +// ── Categories ──────────────────────────────────────────────────────────────── +export const categories = pgTable("categories", { + id: serial("id").primaryKey(), + name: text("name").notNull().unique(), + slug: text("slug").notNull().unique(), + parentId: integer("parent_id"), + description: text("description"), + imageUrl: text("image_url"), + position: integer("position").default(0), + createdAt: timestamp("created_at").default(sql`CURRENT_TIMESTAMP`).notNull(), +}); + +export const insertCategorySchema = createInsertSchema(categories).omit({ id: true, createdAt: true }); +export type Category = typeof categories.$inferSelect; +export type InsertCategory = z.infer; + +// ── Products ────────────────────────────────────────────────────────────────── +export const products = pgTable("products", { + id: serial("id").primaryKey(), + handle: text("handle").notNull().unique(), + title: text("title").notNull(), + description: text("description"), + descriptionHtml: text("description_html"), + vendor: text("vendor"), + productType: text("product_type"), + brandId: integer("brand_id").references(() => brands.id), + categoryId: integer("category_id").references(() => categories.id), + tags: text("tags").array(), + status: text("status").notNull().default("active"), + price: text("price").notNull().default("0.00"), + compareAtPrice: text("compare_at_price"), + images: jsonb("images").$type().default([]), + metaTitle: text("meta_title"), + metaDescription: text("meta_description"), + hsaFsa: boolean("hsa_fsa").default(false), + ingredients: text("ingredients"), + weight: text("weight"), + weightUnit: text("weight_unit").default("lb"), + createdAt: timestamp("created_at").default(sql`CURRENT_TIMESTAMP`).notNull(), + updatedAt: timestamp("updated_at").default(sql`CURRENT_TIMESTAMP`).notNull(), +}); + +export const insertProductSchema = createInsertSchema(products).omit({ id: true, createdAt: true, updatedAt: true }); +export type Product = typeof products.$inferSelect; +export type InsertProduct = z.infer; + +// ── Product Variants ────────────────────────────────────────────────────────── +export const productVariants = pgTable("product_variants", { + id: serial("id").primaryKey(), + productId: integer("product_id").notNull().references(() => products.id, { onDelete: "cascade" }), + title: text("title").notNull().default("Default Title"), + sku: text("sku"), + price: text("price").notNull().default("0.00"), + compareAtPrice: text("compare_at_price"), + inventoryQty: integer("inventory_qty").notNull().default(0), + inventoryPolicy: text("inventory_policy").notNull().default("deny"), + weight: text("weight"), + weightUnit: text("weight_unit").default("lb"), + imageUrl: text("image_url"), + position: integer("position").default(0), + createdAt: timestamp("created_at").default(sql`CURRENT_TIMESTAMP`).notNull(), +}); + +export const insertProductVariantSchema = createInsertSchema(productVariants).omit({ id: true, createdAt: true }); +export type ProductVariant = typeof productVariants.$inferSelect; +export type InsertProductVariant = z.infer; + +// ── Discount Codes ──────────────────────────────────────────────────────────── +export const discountCodes = pgTable("discount_codes", { + id: serial("id").primaryKey(), + code: text("code").notNull().unique(), + type: text("type").notNull().default("percentage"), + value: text("value").notNull(), + minOrderAmount: text("min_order_amount").default("0.00"), + maxUses: integer("max_uses"), + usedCount: integer("used_count").notNull().default(0), + active: boolean("active").notNull().default(true), + expiresAt: timestamp("expires_at"), + createdAt: timestamp("created_at").default(sql`CURRENT_TIMESTAMP`).notNull(), +}); + +export const insertDiscountCodeSchema = createInsertSchema(discountCodes).omit({ id: true, usedCount: true, createdAt: true }); +export type DiscountCode = typeof discountCodes.$inferSelect; +export type InsertDiscountCode = z.infer; + +// ── Carts ───────────────────────────────────────────────────────────────────── +export const carts = pgTable("carts", { + id: serial("id").primaryKey(), + sessionId: text("session_id").notNull(), + customerId: integer("customer_id").references(() => customers.id), + status: text("status").notNull().default("active"), + discountCode: text("discount_code"), + discountAmount: text("discount_amount"), + expiresAt: timestamp("expires_at"), + createdAt: timestamp("created_at").default(sql`CURRENT_TIMESTAMP`).notNull(), + updatedAt: timestamp("updated_at").default(sql`CURRENT_TIMESTAMP`).notNull(), +}); + +export const insertCartSchema = createInsertSchema(carts).omit({ id: true, createdAt: true, updatedAt: true }); +export type Cart = typeof carts.$inferSelect; +export type InsertCart = z.infer; + +// ── Cart Items ──────────────────────────────────────────────────────────────── +export const cartItems = pgTable("cart_items", { + id: serial("id").primaryKey(), + cartId: integer("cart_id").notNull().references(() => carts.id, { onDelete: "cascade" }), + productId: integer("product_id").notNull().references(() => products.id), + variantId: integer("variant_id").references(() => productVariants.id), + title: text("title").notNull(), + variantTitle: text("variant_title"), + price: text("price").notNull(), + quantity: integer("quantity").notNull().default(1), + imageUrl: text("image_url"), + productHandle: text("product_handle"), + createdAt: timestamp("created_at").default(sql`CURRENT_TIMESTAMP`).notNull(), +}); + +export const insertCartItemSchema = createInsertSchema(cartItems).omit({ id: true, createdAt: true }); +export type CartItem = typeof cartItems.$inferSelect; +export type InsertCartItem = z.infer; + +// ── Orders ──────────────────────────────────────────────────────────────────── +export const orders = pgTable("orders", { + id: serial("id").primaryKey(), + orderNumber: text("order_number").notNull().unique(), + customerId: integer("customer_id").references(() => customers.id), + email: text("email").notNull(), + status: text("status").notNull().default("pending"), + paymentStatus: text("payment_status").notNull().default("unpaid"), + fulfillmentStatus: text("fulfillment_status").notNull().default("unfulfilled"), + paymentProcessor: text("payment_processor").notNull().default("authorizenet"), + subtotal: text("subtotal").notNull(), + tax: text("tax").notNull().default("0.00"), + shipping: text("shipping").notNull().default("0.00"), + discountCode: text("discount_code"), + discountAmount: text("discount_amount").default("0.00"), + total: text("total").notNull(), + currency: text("currency").notNull().default("USD"), + authNetTransactionId: text("auth_net_transaction_id"), + stripePaymentIntentId: text("stripe_payment_intent_id"), + shippingAddress: jsonb("shipping_address"), + billingAddress: jsonb("billing_address"), + notes: text("notes"), + trackingNumber: text("tracking_number"), + trackingCarrier: text("tracking_carrier"), + shippingMethod: text("shipping_method"), + paymentVerified: boolean("payment_verified").default(false), + archived: boolean("archived").default(false), + createdAt: timestamp("created_at").default(sql`CURRENT_TIMESTAMP`).notNull(), + updatedAt: timestamp("updated_at").default(sql`CURRENT_TIMESTAMP`).notNull(), +}); + +export const insertOrderSchema = createInsertSchema(orders).omit({ id: true, createdAt: true, updatedAt: true }); +export type Order = typeof orders.$inferSelect; +export type InsertOrder = z.infer; + +// ── Order Items ─────────────────────────────────────────────────────────────── +export const orderItems = pgTable("order_items", { + id: serial("id").primaryKey(), + orderId: integer("order_id").notNull().references(() => orders.id, { onDelete: "cascade" }), + productHandle: text("product_handle"), + variantId: integer("variant_id"), + title: text("title").notNull(), + variantTitle: text("variant_title"), + quantity: integer("quantity").notNull(), + price: text("price").notNull(), + total: text("total").notNull(), + sku: text("sku"), + imageUrl: text("image_url"), + createdAt: timestamp("created_at").default(sql`CURRENT_TIMESTAMP`).notNull(), +}); + +export const insertOrderItemSchema = createInsertSchema(orderItems).omit({ id: true, createdAt: true }); +export type OrderItem = typeof orderItems.$inferSelect; +export type InsertOrderItem = z.infer; + +// ── Order Events (timeline) ─────────────────────────────────────────────────── +export const orderEvents = pgTable("order_events", { + id: serial("id").primaryKey(), + orderId: integer("order_id").notNull().references(() => orders.id, { onDelete: "cascade" }), + type: text("type").notNull(), + body: text("body"), + actor: text("actor"), + createdAt: timestamp("created_at").default(sql`CURRENT_TIMESTAMP`).notNull(), +}); + +export const insertOrderEventSchema = createInsertSchema(orderEvents).omit({ id: true, createdAt: true }); +export type OrderEvent = typeof orderEvents.$inferSelect; +export type InsertOrderEvent = z.infer; + +// ── Payment Methods (vaulted cards) ────────────────────────────────────────── +export const paymentMethods = pgTable("payment_methods", { + id: serial("id").primaryKey(), + customerId: integer("customer_id").notNull().references(() => customers.id, { onDelete: "cascade" }), + source: text("source").notNull().default("authorizenet"), + authNetPaymentProfileId: text("auth_net_payment_profile_id"), + stripePaymentMethodId: text("stripe_payment_method_id"), + cardLast4: text("card_last4"), + cardType: text("card_type"), + expirationDate: text("expiration_date"), + isDefault: boolean("is_default").default(false), + createdAt: timestamp("created_at").default(sql`CURRENT_TIMESTAMP`).notNull(), +}); + +export const insertPaymentMethodSchema = createInsertSchema(paymentMethods).omit({ id: true, createdAt: true }); +export type PaymentMethod = typeof paymentMethods.$inferSelect; +export type InsertPaymentMethod = z.infer; + +// ── Admin Users ─────────────────────────────────────────────────────────────── +export const adminUsers = pgTable("admin_users", { + id: serial("id").primaryKey(), + email: text("email").notNull().unique(), + name: text("name"), + role: text("role").notNull().default("admin"), + passwordHash: text("password_hash"), + createdAt: timestamp("created_at").default(sql`CURRENT_TIMESTAMP`).notNull(), +}); + +export const insertAdminUserSchema = createInsertSchema(adminUsers).omit({ id: true, createdAt: true }); +export type AdminUser = typeof adminUsers.$inferSelect; +export type InsertAdminUser = z.infer;