"use node"; import Stripe from "stripe"; import { action } from "./_generated/server"; import { internal } from "./_generated/api"; import { v } from "convex/values"; import { getShipmentRateObjectId } from "./model/shippo"; const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!); const SHIPPO_TRANSACTIONS_URL = "https://api.goshippo.com/transactions/"; type AcceptReturnResult = | { success: true; returnTrackingNumber: string } | { success: false; code: string; message: string }; export const acceptReturn = 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 and validate const order = await ctx.runQuery(internal.orders.getOrderForRefund, { id: orderId, }); if (!order) throw new Error("Order not found"); if (order.status !== "delivered") { return { success: false, code: "INVALID_STATUS", message: "Only delivered orders can have a return accepted.", }; } if (!order.returnRequestedAt) { return { success: false, code: "NO_RETURN_REQUEST", message: "No return has been requested for this order.", }; } if (order.returnTrackingNumber) { return { success: false, code: "DUPLICATE_RETURN_LABEL", message: "A return label has already been created for this order.", }; } if (!order.shippoShipmentId) { return { success: false, code: "NO_SHIPMENT", message: "Order has no Shippo shipment ID.", }; } // 3. 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 }; } // 4. Purchase return label via Shippo POST /transactions (is_return: true) 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, is_return: true }), }); } catch { return { success: false, code: "SHIPPO_UNREACHABLE", message: "Could not reach Shippo to create the return label.", }; } if (!txResponse.ok) { let detail = ""; try { detail = JSON.stringify(await txResponse.json()); } catch {} console.error("Shippo /transactions/ return 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; 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 return transaction failed:", tx.status, errMsg); return { success: false, code: "SHIPPO_ERROR", message: errMsg, }; } if (!tx.tracking_number || !tx.label_url) { return { success: false, code: "NO_TRACKING", message: "Shippo returned success but no tracking number or label URL.", }; } // 5. Persist return label data await ctx.runMutation(internal.orders.applyReturnAccepted, { orderId, adminUserId: userId, returnLabelUrl: tx.label_url, returnTrackingNumber: tx.tracking_number, returnCarrier: order.carrier, }); // 6. Send return label email to customer const customer = await ctx.runQuery(internal.users.getById, { userId: order.userId, }); await ctx.runMutation(internal.emails.sendReturnLabelEmail, { to: customer.email, firstName: customer.firstName ?? customer.name.split(" ")[0] ?? "there", orderNumber: order.orderNumber, returnLabelUrl: tx.label_url, }); return { success: true, returnTrackingNumber: tx.tracking_number }; }, }); export const issueRefund = action({ args: { orderId: v.id("orders") }, handler: async (ctx, { orderId }): Promise<{ success: boolean }> => { // 1. Auth — must be an 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"); if (!order.stripePaymentIntentId) throw new Error("Order has no Stripe payment intent to refund"); if (order.paymentStatus === "refunded") throw new Error("Order has already been refunded"); // 3. Create Stripe refund — idempotent: skip if one already exists const existingRefunds = await stripe.refunds.list({ payment_intent: order.stripePaymentIntentId, limit: 10, }); const hasRefund = existingRefunds.data.some( (r) => r.status === "succeeded" || r.status === "pending", ); if (!hasRefund) { await stripe.refunds.create({ payment_intent: order.stripePaymentIntentId, }); } // 4. Mark order refunded and restore stock — applyRefund is also idempotent await ctx.runMutation(internal.orders.applyRefund, { id: orderId, adminUserId: userId, }); return { success: true }; }, });