import type { Id } from "../_generated/dataModel"; import type { QueryCtx } from "../_generated/server"; // ─── Cart Item Issues (discriminated union) ───────────────────────────────── export type CartItemIssue = | { type: "out_of_stock"; variantId: Id<"productVariants">; requested: number; available: number } | { type: "insufficient_stock"; variantId: Id<"productVariants">; requested: number; available: number } | { type: "variant_inactive"; variantId: Id<"productVariants"> } | { type: "variant_not_found"; variantId: Id<"productVariants"> } | { type: "product_not_found"; productId: Id<"products"> } | { type: "price_changed"; variantId: Id<"productVariants">; oldPrice: number; newPrice: number }; // ─── Validated & Enriched Cart Item ───────────────────────────────────────── export type ValidatedCartItem = { variantId: Id<"productVariants">; productId: Id<"products">; quantity: number; unitPrice: number; originalPrice: number; productName: string; variantName: string; sku: string; imageUrl: string | undefined; stockQuantity: number; weight: number; weightUnit: "g" | "kg" | "lb" | "oz"; length: number | undefined; width: number | undefined; height: number | undefined; dimensionUnit: "cm" | "in" | undefined; productSlug: string; parentCategorySlug: string; childCategorySlug: string; }; // ─── Validation Result ────────────────────────────────────────────────────── export type CartValidationResult = { valid: boolean; items: ValidatedCartItem[]; issues: CartItemIssue[]; subtotal: number; }; // ─── Validation & Enrichment Helper ───────────────────────────────────────── /** * Validates every cart line item (stock, active status, price drift) and * enriches each with weight/dimensions for downstream shipping-rate lookups. * * Price-change issues are warnings — they do NOT block checkout. * Stock/missing/inactive issues ARE blocking. */ export async function validateAndEnrichCart( ctx: Pick, items: { productId: Id<"products">; variantId?: Id<"productVariants">; quantity: number; price: number }[] ): Promise { const validatedItems: ValidatedCartItem[] = []; const issues: CartItemIssue[] = []; const blockingVariantIds = new Set>(); for (const item of items) { if (!item.variantId) continue; const variantId = item.variantId; const variant = await ctx.db.get(variantId); if (!variant) { issues.push({ type: "variant_not_found" as const, variantId }); continue; } if (!variant.isActive) { issues.push({ type: "variant_inactive" as const, variantId }); continue; } const product = await ctx.db.get(variant.productId); if (!product || product.status !== "active") { issues.push({ type: "product_not_found" as const, productId: item.productId }); continue; } if (variant.stockQuantity === 0) { issues.push({ type: "out_of_stock" as const, variantId, requested: item.quantity, available: 0, }); blockingVariantIds.add(variantId); } else if (variant.stockQuantity < item.quantity) { issues.push({ type: "insufficient_stock" as const, variantId, requested: item.quantity, available: variant.stockQuantity, }); blockingVariantIds.add(variantId); } if (variant.price !== item.price) { issues.push({ type: "price_changed" as const, variantId, oldPrice: item.price, newPrice: variant.price, }); } const images = await ctx.db .query("productImages") .withIndex("by_product", (q) => q.eq("productId", variant.productId)) .collect(); images.sort((a, b) => a.position - b.position); validatedItems.push({ variantId, productId: variant.productId, quantity: item.quantity, unitPrice: variant.price, originalPrice: item.price, productName: product.name, variantName: variant.name, sku: variant.sku, imageUrl: images[0]?.url, stockQuantity: variant.stockQuantity, weight: variant.weight, weightUnit: variant.weightUnit, length: variant.length, width: variant.width, height: variant.height, dimensionUnit: variant.dimensionUnit, productSlug: product.slug, parentCategorySlug: product.parentCategorySlug, childCategorySlug: product.childCategorySlug, }); } const blockingIssues = issues.filter(i => i.type !== "price_changed"); const valid = blockingIssues.length === 0; const subtotal = validatedItems .filter(item => !blockingVariantIds.has(item.variantId)) .reduce((sum, item) => sum + item.unitPrice * item.quantity, 0); return { valid, items: validatedItems, issues, subtotal }; } // ─── Shippo Shipping Rate Types ────────────────────────────────────────────── export type ShippoRate = { objectId: string; provider: string; servicelevelName: string; servicelevelToken: string; amount: string; currency: string; estimatedDays: number | null; durationTerms: string; arrivesBy: string | null; carrierAccount: string; }; export type SelectedShippingRate = { provider: string; serviceName: string; serviceToken: string; amount: number; currency: string; estimatedDays: number | null; durationTerms: string; carrierAccount: string; }; export type ShippingRateResult = { shipmentObjectId: string; selectedRate: SelectedShippingRate; alternativeRates: SelectedShippingRate[]; cartSubtotal: number; shippingTotal: number; orderTotal: number; }; // ─── Checkout Session Types (Stripe) ───────────────────────────────────────── export type CreateCheckoutSessionInput = { addressId: Id<"addresses">; shipmentObjectId: string; shippingRate: { provider: string; serviceName: string; serviceToken: string; amount: number; currency: string; estimatedDays: number | null; durationTerms: string; carrierAccount: string; }; sessionId: string | undefined; }; export type CheckoutSessionResult = { clientSecret: string; }; export type CheckoutSessionStatus = { status: "complete" | "expired" | "open"; paymentStatus: string; customerEmail: string | null; }; // ─── Shippo Address Validation Types ───────────────────────────────────────── export type ShippoConfidenceScore = "high" | "medium" | "low"; export type ShippoValidationValue = "valid" | "partially_valid" | "invalid"; export type ShippoAddressType = | "residential" | "commercial" | "unknown" | "po_box" | "military"; export type ShippoValidationReason = { code: string; description: string; }; export type RecommendedAddress = { addressLine1: string; additionalInformation?: string; city: string; postalCode: string; country: string; completeAddress?: string; confidenceScore: ShippoConfidenceScore; confidenceCode: string; confidenceDescription: string; }; export type AddressValidationResult = { isValid: boolean; validationValue: ShippoValidationValue; reasons: ShippoValidationReason[]; addressType: ShippoAddressType; changedAttributes: string[]; recommendedAddress?: RecommendedAddress; originalAddress: { addressLine1: string; additionalInformation?: string; city: string; postalCode: string; country: string; }; };