import { internalAction, internalMutation, internalQuery, } from "./_generated/server"; import { internal } from "./_generated/api"; import { v } from "convex/values"; import { recordOrderTimelineEvent } from "./model/orders"; // ─── Internal queries ───────────────────────────────────────────────────────── export const getOrderByTracking = internalQuery({ args: { trackingNumber: v.string(), carrier: v.string() }, handler: async (ctx, { trackingNumber, carrier }) => { return await ctx.db .query("orders") .withIndex("by_tracking_number_and_carrier", (q) => q.eq("trackingNumber", trackingNumber).eq("carrier", carrier), ) .first(); }, }); export const getOrderByReturnTracking = internalQuery({ args: { returnTrackingNumber: v.string(), returnCarrier: v.string() }, handler: async (ctx, { returnTrackingNumber, returnCarrier }) => { return await ctx.db .query("orders") .withIndex("by_return_tracking_number_and_carrier", (q) => q.eq("returnTrackingNumber", returnTrackingNumber).eq("returnCarrier", returnCarrier), ) .first(); }, }); // ─── Internal mutation ──────────────────────────────────────────────────────── export const applyTrackingUpdate = internalMutation({ args: { orderId: v.id("orders"), trackingStatus: v.string(), estimatedDelivery: v.optional(v.number()), isDelivered: v.boolean(), isReturnTracking: v.boolean(), payload: v.string(), }, handler: async (ctx, args) => { const order = await ctx.db.get(args.orderId); if (!order) return; // Idempotency: skip if this exact status was already applied if (order.trackingStatus === args.trackingStatus) { console.log( `[shippoWebhook] Skipping duplicate tracking status "${args.trackingStatus}" for order ${args.orderId}`, ); return; } const now = Date.now(); // Return tracking updates never set status to "delivered" — only outbound does if (args.isDelivered && !args.isReturnTracking) { await ctx.db.patch(args.orderId, { trackingStatus: args.trackingStatus, ...(args.estimatedDelivery !== undefined ? { estimatedDelivery: args.estimatedDelivery } : {}), status: "delivered", actualDelivery: now, updatedAt: now, }); } else { await ctx.db.patch(args.orderId, { trackingStatus: args.trackingStatus, ...(args.estimatedDelivery !== undefined ? { estimatedDelivery: args.estimatedDelivery } : {}), updatedAt: now, }); } await recordOrderTimelineEvent(ctx, { orderId: args.orderId, eventType: args.isReturnTracking ? "return_tracking_update" : "tracking_update", source: "shippo_webhook", ...(args.isDelivered && !args.isReturnTracking ? { fromStatus: order.status, toStatus: "delivered" } : {}), payload: args.payload, }); if (args.isDelivered && !args.isReturnTracking) { const customer = await ctx.db.get(order.userId); if (customer) { await ctx.scheduler.runAfter(0, internal.emails.sendDeliveryConfirmation, { to: customer.email, firstName: customer.firstName ?? customer.name.split(" ")[0] ?? "there", orderNumber: order.orderNumber, }); } } }, }); // ─── Internal action ────────────────────────────────────────────────────────── type ShippoTrackUpdatedPayload = { event?: string; data?: { carrier?: string; tracking_number?: string; eta?: string | null; tracking_status?: { status?: string; status_details?: string; status_date?: string; }; }; }; export const handleTrackUpdated = internalAction({ args: { body: v.string() }, handler: async (ctx, { body }) => { let payload: ShippoTrackUpdatedPayload; try { payload = JSON.parse(body) as ShippoTrackUpdatedPayload; } catch { console.error("[shippoWebhook] Failed to parse JSON body"); return; } if (payload.event !== "track_updated") { console.log("[shippoWebhook] Ignoring event:", payload.event); return; } const { data } = payload; const trackingNumber = data?.tracking_number; const carrier = data?.carrier; if (!trackingNumber || !carrier) { console.error( "[shippoWebhook] Missing tracking_number or carrier in payload", ); return; } let order = await ctx.runQuery( internal.shippoWebhook.getOrderByTracking, { trackingNumber, carrier }, ); let isReturnTracking = false; if (!order) { order = await ctx.runQuery( internal.shippoWebhook.getOrderByReturnTracking, { returnTrackingNumber: trackingNumber, returnCarrier: carrier }, ); isReturnTracking = !!order; } if (!order) { console.log( `[shippoWebhook] No order found for tracking ${trackingNumber} / ${carrier}`, ); return; } const trackingStatus = data?.tracking_status?.status ?? "UNKNOWN"; const isDelivered = trackingStatus === "DELIVERED"; const eta = data?.eta; const estimatedDelivery = eta && !isNaN(new Date(eta).getTime()) ? new Date(eta).getTime() : undefined; await ctx.runMutation(internal.shippoWebhook.applyTrackingUpdate, { orderId: order._id, trackingStatus, estimatedDelivery, isDelivered, isReturnTracking, payload: body, }); }, });