import { defineSchema, defineTable } from "convex/server"; import { v } from "convex/values"; export default defineSchema({ // ─── Customers ───────────────────────────────────────────────────────── users: defineTable({ externalId: v.string(), email: v.string(), name: v.string(), firstName: v.optional(v.string()), lastName: v.optional(v.string()), role: v.union( v.literal("customer"), v.literal("admin"), v.literal("super_admin"), ), avatarUrl: v.optional(v.string()), phone: v.optional(v.string()), stripeCustomerId: v.optional(v.string()), createdAt: v.optional(v.number()), lastLoginAt: v.optional(v.number()), }) .index("by_external_id", ["externalId"]) .index("by_email", ["email"]) .index("by_role", ["role"]), addresses: defineTable({ userId: v.id("users"), type: v.union(v.literal("shipping"), v.literal("billing")), fullName: v.string(), firstName: v.string(), lastName: v.string(), phone: v.string(), addressLine1: v.string(), additionalInformation: v.optional(v.string()), city: v.string(), postalCode: v.string(), country: v.string(), isDefault: v.boolean(), isValidated: v.optional(v.boolean()), }) .index("by_user", ["userId"]) .index("by_user_and_type", ["userId", "type"]), // ─── Catalog (Categories & Products) ──────────────────────────────────── categories: defineTable({ name: v.string(), slug: v.string(), description: v.optional(v.string()), imageUrl: v.optional(v.string()), parentId: v.optional(v.id("categories")), topCategorySlug: v.optional(v.string()), seoTitle: v.optional(v.string()), seoDescription: v.optional(v.string()), }) .index("by_slug", ["slug"]) .index("by_parent", ["parentId"]) .index("by_parent_slug", ["parentId", "slug"]) .index("by_top_category_slug", ["topCategorySlug"]), products: defineTable({ name: v.string(), slug: v.string(), description: v.optional(v.string()), shortDescription: v.optional(v.string()), status: v.union( v.literal("active"), v.literal("draft"), v.literal("archived"), ), categoryId: v.id("categories"), brand: v.optional(v.string()), tags: v.array(v.string()), attributes: v.optional( v.object({ petSize: v.optional(v.array(v.string())), ageRange: v.optional(v.array(v.string())), specialDiet: v.optional(v.array(v.string())), material: v.optional(v.string()), flavor: v.optional(v.string()), }), ), seoTitle: v.optional(v.string()), seoDescription: v.optional(v.string()), canonicalSlug: v.optional(v.string()), averageRating: v.optional(v.number()), reviewCount: v.optional(v.number()), createdAt: v.optional(v.number()), updatedAt: v.optional(v.number()), parentCategorySlug: v.string(), childCategorySlug: v.string(), topCategorySlug: v.optional(v.string()), }) .index("by_slug", ["slug"]) .index("by_status", ["status"]) .index("by_category", ["categoryId"]) .index("by_status_and_category", ["status", "categoryId"]) .index("by_brand", ["brand"]) .index("by_status_and_parent_slug", ["status", "parentCategorySlug"]) .index("by_status_and_parent_and_child_slug", [ "status", "parentCategorySlug", "childCategorySlug", ]) .index("by_status_and_top_category_slug", ["status", "topCategorySlug"]) .index("by_status_and_top_and_parent_slug", [ "status", "topCategorySlug", "parentCategorySlug", ]) .searchIndex("search_products", { searchField: "name", filterFields: ["status", "categoryId", "brand", "parentCategorySlug"], }), productImages: defineTable({ productId: v.id("products"), url: v.string(), alt: v.optional(v.string()), position: v.number(), }).index("by_product", ["productId"]), productVariants: defineTable({ productId: v.id("products"), name: v.string(), sku: v.string(), price: v.number(), compareAtPrice: v.optional(v.number()), stockQuantity: v.number(), attributes: v.optional( v.object({ size: v.optional(v.string()), flavor: v.optional(v.string()), color: v.optional(v.string()), }), ), isActive: v.boolean(), weight: v.number(), weightUnit: v.union( v.literal("g"), v.literal("kg"), v.literal("lb"), v.literal("oz"), ), length: v.optional(v.number()), width: v.optional(v.number()), height: v.optional(v.number()), dimensionUnit: v.optional(v.union( v.literal("cm"), v.literal("in"), )), }) .index("by_product", ["productId"]) .index("by_sku", ["sku"]) .index("by_product_and_active", ["productId", "isActive"]), // ─── Orders & Payments ────────────────────────────────────────────────── orders: defineTable({ orderNumber: v.string(), userId: v.id("users"), email: v.string(), status: v.union( v.literal("pending"), v.literal("confirmed"), v.literal("processing"), v.literal("shipped"), v.literal("delivered"), v.literal("cancelled"), v.literal("refunded"), ), paymentStatus: v.union( v.literal("pending"), v.literal("paid"), v.literal("failed"), v.literal("refunded"), ), subtotal: v.number(), tax: v.number(), shipping: v.number(), discount: v.number(), total: v.number(), currency: v.string(), shippingAddressSnapshot: v.object({ fullName: v.string(), firstName: v.string(), lastName: v.string(), addressLine1: v.string(), additionalInformation: v.optional(v.string()), city: v.string(), postalCode: v.string(), country: v.string(), phone: v.optional(v.string()), }), billingAddressSnapshot: v.object({ firstName: v.string(), lastName: v.string(), addressLine1: v.string(), additionalInformation: v.optional(v.string()), city: v.string(), postalCode: v.string(), country: v.string(), }), stripePaymentIntentId: v.optional(v.string()), stripeCheckoutSessionId: v.optional(v.string()), shippoOrderId: v.optional(v.string()), shippoShipmentId: v.string(), shippingMethod: v.string(), shippingServiceCode: v.string(), carrier: v.string(), trackingNumber: v.optional(v.string()), trackingUrl: v.optional(v.string()), estimatedDelivery: v.optional(v.number()), actualDelivery: v.optional(v.number()), notes: v.optional(v.string()), createdAt: v.number(), updatedAt: v.number(), paidAt: v.optional(v.number()), shippedAt: v.optional(v.number()), }) .index("by_user", ["userId"]) .index("by_status", ["status"]) .index("by_payment_status", ["paymentStatus"]) .index("by_order_number", ["orderNumber"]) .index("by_email", ["email"]) .index("by_created_at", ["createdAt"]) .index("by_stripe_checkout_session_id", ["stripeCheckoutSessionId"]), orderItems: defineTable({ orderId: v.id("orders"), variantId: v.id("productVariants"), productName: v.string(), variantName: v.string(), sku: v.string(), quantity: v.number(), unitPrice: v.number(), totalPrice: v.number(), imageUrl: v.optional(v.string()), }).index("by_order", ["orderId"]), // ─── Reviews ─────────────────────────────────────────────────────────── reviews: defineTable({ productId: v.id("products"), userId: v.id("users"), orderId: v.optional(v.id("orders")), rating: v.number(), title: v.string(), content: v.string(), images: v.optional(v.array(v.string())), verifiedPurchase: v.boolean(), helpfulCount: v.number(), createdAt: v.number(), updatedAt: v.optional(v.number()), isApproved: v.boolean(), }) .index("by_product", ["productId"]) .index("by_user", ["userId"]) .index("by_product_approved", ["productId", "isApproved"]), // ─── Wishlists ────────────────────────────────────────────────────────── wishlists: defineTable({ userId: v.id("users"), productId: v.id("products"), variantId: v.optional(v.id("productVariants")), addedAt: v.number(), notifyOnPriceDrop: v.boolean(), notifyOnBackInStock: v.boolean(), priceWhenAdded: v.number(), }) .index("by_user", ["userId"]) .index("by_product", ["productId"]) .index("by_user_and_product", ["userId", "productId"]), // ─── Carts ────────────────────────────────────────────────────────────── carts: defineTable({ userId: v.optional(v.id("users")), sessionId: v.optional(v.string()), items: v.array( v.object({ productId: v.id("products"), variantId: v.optional(v.id("productVariants")), quantity: v.number(), price: v.number(), }), ), createdAt: v.number(), updatedAt: v.number(), expiresAt: v.number(), }) .index("by_user", ["userId"]) .index("by_session", ["sessionId"]), });