"use strict"; const __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.getOrderRefunds = exports.createRefund = void 0; const https_1 = require("firebase-functions/v2/https"); const app_1 = require("firebase-admin/app"); const firestore_1 = require("firebase-admin/firestore"); const stripe_1 = __importDefault(require("stripe")); const uuid_1 = require("uuid"); // Initialize Firebase Admin if not already initialized try { (0, app_1.initializeApp)(); } catch (error) { // App already initialized } const db = (0, firestore_1.getFirestore)(); // Initialize Stripe const stripe = new stripe_1.default(process.env.STRIPE_SECRET_KEY || "", { apiVersion: "2024-06-20", }); /** * Helper function to check user permissions */ async function checkRefundPermissions(uid, orgId) { try { // Check if user is super admin const userDoc = await db.collection("users").doc(uid).get(); if (!userDoc.exists) { return false; } const userData = userDoc.data(); if (userData?.role === "super_admin") { return true; } // Check if user is org admin if (userData?.organization?.id === orgId && userData?.role === "admin") { return true; } // TODO: Add territory manager check when territories are implemented // if (userData?.role === "territory_manager" && userData?.territories?.includes(orgTerritory)) { // return true; // } return false; } catch (error) { console.error("Error checking refund permissions:", error); return false; } } /** * Helper function to create ledger entry */ async function createLedgerEntry(entry, transaction) { const ledgerEntry = { ...entry, createdAt: firestore_1.Timestamp.now(), }; const entryId = (0, uuid_1.v4)(); const docRef = db.collection("ledger").doc(entryId); if (transaction) { transaction.set(docRef, ledgerEntry); } else { await docRef.set(ledgerEntry); } } /** * Creates a refund for an order or specific ticket */ exports.createRefund = (0, https_1.onRequest)({ cors: true, enforceAppCheck: false, region: "us-central1" }, async (req, res) => { const startTime = Date.now(); const action = "create_refund"; try { console.log(`[${action}] Starting refund creation`, { method: req.method, body: req.body, timestamp: new Date().toISOString(), }); if (req.method !== "POST") { res.status(405).json({ error: "Method not allowed" }); return; } const { orderId, ticketId, amountCents, reason } = req.body; if (!orderId) { res.status(400).json({ error: "orderId is required" }); return; } // Get user ID from Authorization header or Firebase Auth token // For now, we'll use a mock uid - in production, extract from JWT const uid = req.headers.authorization?.replace("Bearer ", "") || "mock-uid"; // Load order by orderId (sessionId) const orderDoc = await db.collection("orders").doc(orderId).get(); if (!orderDoc.exists) { console.error(`[${action}] Order not found: ${orderId}`); res.status(404).json({ error: "Order not found" }); return; } const orderData = orderDoc.data(); if (!orderData) { res.status(404).json({ error: "Order data not found" }); return; } const { orgId, eventId, paymentIntentId, stripeAccountId, totalCents, status } = orderData; if (status !== "paid") { res.status(400).json({ error: "Can only refund paid orders" }); return; } // Check permissions const hasPermission = await checkRefundPermissions(uid, orgId); if (!hasPermission) { console.error(`[${action}] Permission denied for user ${uid} in org ${orgId}`); res.status(403).json({ error: "Insufficient permissions" }); return; } let refundAmountCents = amountCents; let ticketData = null; // If ticketId is provided, validate and get ticket price if (ticketId) { const ticketDoc = await db.collection("tickets").doc(ticketId).get(); if (!ticketDoc.exists) { res.status(404).json({ error: "Ticket not found" }); return; } ticketData = ticketDoc.data(); if (ticketData?.orderId !== orderId) { res.status(400).json({ error: "Ticket does not belong to this order" }); return; } if (!["issued", "scanned"].includes(ticketData?.status)) { res.status(400).json({ error: `Cannot refund ticket with status: ${ticketData?.status}` }); return; } // If no amount specified, use ticket type price if (!refundAmountCents) { const ticketTypeDoc = await db.collection("ticket_types").doc(ticketData.ticketTypeId).get(); if (ticketTypeDoc.exists) { refundAmountCents = ticketTypeDoc.data()?.priceCents || 0; } } } // Default to full order amount if no amount specified if (!refundAmountCents) { refundAmountCents = totalCents; } // Validate refund amount if (refundAmountCents <= 0 || refundAmountCents > totalCents) { res.status(400).json({ error: `Invalid refund amount: ${refundAmountCents}. Must be between 1 and ${totalCents}` }); return; } // Create idempotency key for refund const idempotencyKey = `${orderId}_${ticketId || "full"}_${refundAmountCents}`; const refundId = (0, uuid_1.v4)(); // Create pending refund record for idempotency const refundDoc = { orgId, eventId, orderId, ticketId, amountCents: refundAmountCents, reason, requestedByUid: uid, stripe: { paymentIntentId, accountId: stripeAccountId, }, status: "pending", createdAt: firestore_1.Timestamp.now(), }; // Check for existing refund with same idempotency key const existingRefundQuery = await db.collection("refunds") .where("orderId", "==", orderId) .where("amountCents", "==", refundAmountCents) .get(); if (!existingRefundQuery.empty) { const existingRefund = existingRefundQuery.docs[0].data(); if (existingRefund.ticketId === ticketId) { console.log(`[${action}] Duplicate refund request detected`, { idempotencyKey }); res.status(200).json({ refundId: existingRefundQuery.docs[0].id, status: existingRefund.status, message: "Refund already exists" }); return; } } // Create pending refund document await db.collection("refunds").doc(refundId).set(refundDoc); console.log(`[${action}] Created pending refund record`, { refundId, idempotencyKey }); try { // Create Stripe refund console.log(`[${action}] Creating Stripe refund`, { paymentIntentId, amount: refundAmountCents, stripeAccountId, }); const stripeRefund = await stripe.refunds.create({ payment_intent: paymentIntentId, amount: refundAmountCents, reason: reason ? "requested_by_customer" : undefined, refund_application_fee: true, reverse_transfer: true, metadata: { orderId, ticketId: ticketId || "", refundId, orgId, eventId, }, }, { stripeAccount: stripeAccountId, idempotencyKey, }); console.log(`[${action}] Stripe refund created successfully`, { stripeRefundId: stripeRefund.id, status: stripeRefund.status, }); // Update refund record and related entities in transaction await db.runTransaction(async (transaction) => { // Update refund status const refundRef = db.collection("refunds").doc(refundId); transaction.update(refundRef, { "stripe.refundId": stripeRefund.id, status: "succeeded", updatedAt: firestore_1.Timestamp.now(), }); // Update ticket status if single ticket refund if (ticketId) { const ticketRef = db.collection("tickets").doc(ticketId); transaction.update(ticketRef, { status: "refunded", updatedAt: firestore_1.Timestamp.now(), }); } // Create ledger entries // Refund entry (negative) await createLedgerEntry({ orgId, eventId, orderId, type: "refund", amountCents: -refundAmountCents, currency: "USD", stripe: { refundId: stripeRefund.id, accountId: stripeAccountId, }, }, transaction); // Platform fee refund (negative of original platform fee portion) const platformFeeBps = parseInt(process.env.PLATFORM_FEE_BPS || "300"); const platformFeeRefund = Math.round((refundAmountCents * platformFeeBps) / 10000); await createLedgerEntry({ orgId, eventId, orderId, type: "platform_fee", amountCents: -platformFeeRefund, currency: "USD", stripe: { refundId: stripeRefund.id, accountId: stripeAccountId, }, }, transaction); }); console.log(`[${action}] Refund completed successfully`, { refundId, stripeRefundId: stripeRefund.id, amountCents: refundAmountCents, processingTime: Date.now() - startTime, }); res.status(200).json({ refundId, stripeRefundId: stripeRefund.id, amountCents: refundAmountCents, status: "succeeded", }); } catch (stripeError) { console.error(`[${action}] Stripe refund failed`, { error: stripeError.message, code: stripeError.code, type: stripeError.type, }); // Update refund status to failed await db.collection("refunds").doc(refundId).update({ status: "failed", failureReason: stripeError.message, updatedAt: firestore_1.Timestamp.now(), }); res.status(400).json({ error: "Refund failed", details: stripeError.message, refundId, }); } } catch (error) { console.error(`[${action}] Unexpected error`, { error: error.message, stack: error.stack, processingTime: Date.now() - startTime, }); res.status(500).json({ error: "Internal server error", details: error.message, }); } }); /** * Gets refunds for an order */ exports.getOrderRefunds = (0, https_1.onRequest)({ cors: true, enforceAppCheck: false, region: "us-central1" }, async (req, res) => { try { if (req.method !== "POST") { res.status(405).json({ error: "Method not allowed" }); return; } const { orderId } = req.body; if (!orderId) { res.status(400).json({ error: "orderId is required" }); return; } const refundsSnapshot = await db.collection("refunds") .where("orderId", "==", orderId) .orderBy("createdAt", "desc") .get(); const refunds = refundsSnapshot.docs.map(doc => ({ id: doc.id, ...doc.data(), createdAt: doc.data().createdAt.toDate().toISOString(), updatedAt: doc.data().updatedAt?.toDate().toISOString(), })); res.status(200).json({ refunds }); } catch (error) { console.error("Error getting order refunds:", error); res.status(500).json({ error: "Internal server error", details: error.message, }); } }); // # sourceMappingURL=refunds.js.map