import { QueryCtx, MutationCtx } from "../_generated/server"; import { Id, Doc } from "../_generated/dataModel"; export async function recordOrderTimelineEvent( ctx: MutationCtx, args: { orderId: Id<"orders">; eventType: string; source: string; fromStatus?: string; toStatus?: string; payload?: string; userId?: Id<"users">; }, ): Promise { await ctx.db.insert("orderTimelineEvents", { ...args, createdAt: Date.now(), }); } export async function getOrderWithItems( ctx: QueryCtx, orderId: Id<"orders">, ) { const order = await ctx.db.get(orderId); if (!order) return null; const items = await ctx.db .query("orderItems") .withIndex("by_order", (q) => q.eq("orderId", orderId)) .collect(); return { ...order, items }; } /** * Determines whether a customer is allowed to cancel a given order. * * NOTE: Cancellation only updates order status and restores stock. * Stripe refund processing is a separate concern handled via the admin * dashboard or a future automated flow. This helper does NOT trigger a refund. */ export function canCustomerCancel(order: Doc<"orders">): { allowed: boolean; reason?: string; } { switch (order.status) { case "confirmed": return { allowed: true }; case "pending": return { allowed: false, reason: "Order is still awaiting payment confirmation.", }; case "cancelled": return { allowed: false, reason: "Order is already cancelled." }; case "refunded": return { allowed: false, reason: "Order has already been refunded." }; default: return { allowed: false, reason: "Order has progressed past the cancellation window. Please contact support.", }; } } /** * Determines whether a customer is allowed to request a return for a given order. * * Eligibility: order must be `delivered` (customer has received the goods), * return not yet requested, and not already refunded. * Returns and cancellations are separate flows — cancellation is only available * on `confirmed` orders (before fulfilment begins). */ export function canCustomerRequestReturn(order: Doc<"orders">): { allowed: boolean; reason?: string; } { if (order.status !== "delivered") { return { allowed: false, reason: "Returns are only available for delivered orders.", }; } if (order.returnRequestedAt) { return { allowed: false, reason: "A return has already been requested for this order.", }; } if (order.paymentStatus === "refunded") { return { allowed: false, reason: "This order has already been refunded." }; } return { allowed: true }; } export interface OutOfStockItem { variantId: Id<"productVariants">; requested: number; available: number; } /** * Check each cart item for sufficient stock. Returns list of out-of-stock entries. */ export async function validateCartItems( ctx: Pick, items: { variantId?: Id<"productVariants">; quantity: number }[] ): Promise { const outOfStock: OutOfStockItem[] = []; for (const item of items) { if (!item.variantId) continue; const variant = await ctx.db.get(item.variantId); if (!variant) { outOfStock.push({ variantId: item.variantId, requested: item.quantity, available: 0, }); continue; } if (variant.stockQuantity < item.quantity) { outOfStock.push({ variantId: item.variantId, requested: item.quantity, available: variant.stockQuantity, }); } } return outOfStock; }