import { action } from "./_generated/server"; import { internal } from "./_generated/api"; import { v } from "convex/values"; import { getShipmentRateObjectId } from "./model/shippo"; const SHIPPO_TRANSACTIONS_URL = "https://api.goshippo.com/transactions/"; type LabelResult = | { success: true; trackingNumber: string; trackingUrl: string } | { success: false; code: string; message: string }; export const createShippingLabel = action({ args: { orderId: v.id("orders") }, handler: async (ctx, { orderId }): Promise => { // 1. Auth — must be admin const userId = await ctx.runQuery(internal.checkout.getCurrentUserId); const user = await ctx.runQuery(internal.users.getById, { userId }); if (user.role !== "admin" && user.role !== "super_admin") { throw new Error("Unauthorized: admin access required"); } // 2. Load order const order = await ctx.runQuery(internal.orders.getOrderForRefund, { id: orderId, }); if (!order) throw new Error("Order not found"); // 3. Validate — only confirmed orders without an existing label if (order.status !== "confirmed") { return { success: false, code: "INVALID_STATUS", message: "Only confirmed orders can receive a shipping label.", }; } if (order.trackingNumber) { return { success: false, code: "DUPLICATE_LABEL", message: "A shipping label already exists for this order.", }; } if (!order.shippoShipmentId) { return { success: false, code: "NO_SHIPMENT", message: "Order has no Shippo shipment ID.", }; } // 4. Resolve rate object ID from the stored shipment let rateObjectId: string; try { rateObjectId = await getShipmentRateObjectId( order.shippoShipmentId, order.shippingServiceCode, order.carrier, ); } catch (err) { const message = err instanceof Error ? err.message : "Failed to resolve shipping rate."; return { success: false, code: "RATE_ERROR", message }; } // 5. Purchase label via Shippo POST /transactions (synchronous) const apiKey = process.env.SHIPPO_API_KEY; if (!apiKey) { return { success: false, code: "CONFIG_ERROR", message: "Shippo API key not configured.", }; } let txResponse: Response; try { txResponse = await fetch(SHIPPO_TRANSACTIONS_URL, { method: "POST", headers: { Authorization: `ShippoToken ${apiKey}`, "Content-Type": "application/json", }, body: JSON.stringify({ rate: rateObjectId, async: false }), }); } catch { return { success: false, code: "SHIPPO_UNREACHABLE", message: "Could not reach Shippo to create the label.", }; } if (!txResponse.ok) { let detail = ""; try { detail = JSON.stringify(await txResponse.json()); } catch {} console.error( "Shippo /transactions/ error:", txResponse.status, detail, ); return { success: false, code: "SHIPPO_ERROR", message: `Shippo returned status ${txResponse.status}.`, }; } let tx: { object_id: string; status: string; tracking_number?: string; tracking_url_provider?: string; label_url?: string; eta?: string; messages?: Array<{ source: string; text: string; code: string }>; }; try { tx = await txResponse.json(); } catch { return { success: false, code: "PARSE_ERROR", message: "Shippo response could not be parsed.", }; } if (tx.status !== "SUCCESS") { const errMsg = tx.messages?.map((m) => m.text).join("; ") ?? "Unknown Shippo error"; console.error("Shippo transaction failed:", tx.status, errMsg); const isExpired = tx.messages?.some( (m) => m.code === "carrier_account_invalid_credentials" || m.text.toLowerCase().includes("expired"), ); return { success: false, code: isExpired ? "RATE_EXPIRED" : "SHIPPO_ERROR", message: isExpired ? "The shipping rate has expired. Please try again." : errMsg, }; } if (!tx.tracking_number) { return { success: false, code: "NO_TRACKING", message: "Shippo returned success but no tracking number.", }; } // 6. Persist label data and update order status const etaMs = tx.eta && !isNaN(new Date(tx.eta).getTime()) ? new Date(tx.eta).getTime() : undefined; await ctx.runMutation(internal.orders.applyLabel, { orderId, adminUserId: userId, trackingNumber: tx.tracking_number, trackingUrl: tx.tracking_url_provider ?? "", labelUrl: tx.label_url, estimatedDelivery: etaMs, }); return { success: true, trackingNumber: tx.tracking_number, trackingUrl: tx.tracking_url_provider ?? "", }; }, });