"use strict"; const __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.getOrderDisputes = void 0; exports.handleDisputeCreated = handleDisputeCreated; exports.handleDisputeClosed = handleDisputeClosed; 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 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); } } /** * Helper function to find order by payment intent or charge ID */ async function findOrderByStripeData(paymentIntentId, chargeId) { try { let orderSnapshot; if (paymentIntentId) { orderSnapshot = await db.collection("orders") .where("paymentIntentId", "==", paymentIntentId) .limit(1) .get(); } if (orderSnapshot?.empty && chargeId) { // Try to find by charge ID (stored in metadata or retrieved from Stripe) orderSnapshot = await db.collection("orders") .where("stripe.chargeId", "==", chargeId) .limit(1) .get(); } if (orderSnapshot?.empty) { return null; } const orderDoc = orderSnapshot.docs[0]; return { orderId: orderDoc.id, orderData: orderDoc.data(), }; } catch (error) { console.error("Error finding order by Stripe data:", error); return null; } } /** * Helper function to update ticket statuses */ async function updateTicketStatusesForOrder(orderId, newStatus, transaction) { try { const ticketsSnapshot = await db.collection("tickets") .where("orderId", "==", orderId) .get(); let updatedCount = 0; for (const ticketDoc of ticketsSnapshot.docs) { const ticketData = ticketDoc.data(); const currentStatus = ticketData.status; // Only update tickets that can be changed if (newStatus === "locked_dispute") { // Lock all issued or scanned tickets if (["issued", "scanned"].includes(currentStatus)) { const updates = { status: newStatus, previousStatus: currentStatus, updatedAt: firestore_1.Timestamp.now(), }; if (transaction) { transaction.update(ticketDoc.ref, updates); } else { await ticketDoc.ref.update(updates); } updatedCount++; } } else if (newStatus === "void") { // Void locked dispute tickets if (currentStatus === "locked_dispute") { const updates = { status: newStatus, updatedAt: firestore_1.Timestamp.now(), }; if (transaction) { transaction.update(ticketDoc.ref, updates); } else { await ticketDoc.ref.update(updates); } updatedCount++; } } else if (currentStatus === "locked_dispute") { // Restore tickets from dispute lock const restoreStatus = ticketData.previousStatus || "issued"; const updates = { status: restoreStatus, previousStatus: undefined, updatedAt: firestore_1.Timestamp.now(), }; if (transaction) { transaction.update(ticketDoc.ref, updates); } else { await ticketDoc.ref.update(updates); } updatedCount++; } } return updatedCount; } catch (error) { console.error("Error updating ticket statuses:", error); return 0; } } /** * Handles charge.dispute.created webhook */ async function handleDisputeCreated(dispute, stripeAccountId) { const action = "dispute_created"; const startTime = Date.now(); try { console.log(`[${action}] Processing dispute created`, { disputeId: dispute.id, chargeId: dispute.charge, amount: dispute.amount, reason: dispute.reason, status: dispute.status, stripeAccountId, }); // Get charge details to find payment intent const charge = await stripe.charges.retrieve(dispute.charge, { stripeAccount: stripeAccountId, }); const paymentIntentId = charge.payment_intent; // Find the order const orderResult = await findOrderByStripeData(paymentIntentId, charge.id); if (!orderResult) { console.error(`[${action}] Order not found for dispute`, { disputeId: dispute.id, paymentIntentId, chargeId: charge.id, }); return; } const { orderId, orderData } = orderResult; const { orgId, eventId } = orderData; console.log(`[${action}] Found order for dispute`, { orderId, orgId, eventId, }); // Process dispute in transaction await db.runTransaction(async (transaction) => { // Lock tickets related to this order const ticketsUpdated = await updateTicketStatusesForOrder(orderId, "locked_dispute", transaction); console.log(`[${action}] Locked ${ticketsUpdated} tickets for dispute`, { orderId, disputeId: dispute.id, }); // Create dispute fee ledger entry if there's a fee if (dispute.balance_transactions && dispute.balance_transactions.length > 0) { for (const balanceTxn of dispute.balance_transactions) { if (balanceTxn.fee > 0) { await createLedgerEntry({ orgId, eventId, orderId, type: "dispute_fee", amountCents: -balanceTxn.fee, // Negative because it's a cost currency: "USD", stripe: { balanceTxnId: balanceTxn.id, chargeId: charge.id, disputeId: dispute.id, accountId: stripeAccountId, }, meta: { disputeReason: dispute.reason, disputeStatus: dispute.status, }, }, transaction); } } } // Update order with dispute information const orderRef = db.collection("orders").doc(orderId); transaction.update(orderRef, { "dispute.disputeId": dispute.id, "dispute.status": dispute.status, "dispute.reason": dispute.reason, "dispute.amount": dispute.amount, "dispute.createdAt": firestore_1.Timestamp.now(), updatedAt: firestore_1.Timestamp.now(), }); }); console.log(`[${action}] Dispute processing completed`, { disputeId: dispute.id, orderId, processingTime: Date.now() - startTime, }); } catch (error) { console.error(`[${action}] Error processing dispute created`, { disputeId: dispute.id, error: error.message, stack: error.stack, processingTime: Date.now() - startTime, }); throw error; } } /** * Handles charge.dispute.closed webhook */ async function handleDisputeClosed(dispute, stripeAccountId) { const action = "dispute_closed"; const startTime = Date.now(); try { console.log(`[${action}] Processing dispute closed`, { disputeId: dispute.id, status: dispute.status, outcome: dispute.outcome, chargeId: dispute.charge, stripeAccountId, }); // Get charge details to find payment intent const charge = await stripe.charges.retrieve(dispute.charge, { stripeAccount: stripeAccountId, }); const paymentIntentId = charge.payment_intent; // Find the order const orderResult = await findOrderByStripeData(paymentIntentId, charge.id); if (!orderResult) { console.error(`[${action}] Order not found for dispute`, { disputeId: dispute.id, paymentIntentId, chargeId: charge.id, }); return; } const { orderId, orderData } = orderResult; const { orgId, eventId } = orderData; console.log(`[${action}] Found order for dispute`, { orderId, orgId, eventId, outcome: dispute.outcome?.outcome, }); // Process dispute closure in transaction await db.runTransaction(async (transaction) => { let ticketsUpdated = 0; if (dispute.outcome?.outcome === "won") { // Dispute won - restore tickets to previous status ticketsUpdated = await updateTicketStatusesForOrder(orderId, "restore", transaction); console.log(`[${action}] Dispute won - restored ${ticketsUpdated} tickets`, { orderId, disputeId: dispute.id, }); } else if (dispute.outcome?.outcome === "lost") { // Dispute lost - void tickets and create refund-style ledger entries ticketsUpdated = await updateTicketStatusesForOrder(orderId, "void", transaction); // Create negative sale entry (effectively a refund due to dispute loss) await createLedgerEntry({ orgId, eventId, orderId, type: "refund", amountCents: -dispute.amount, currency: "USD", stripe: { chargeId: charge.id, disputeId: dispute.id, accountId: stripeAccountId, }, meta: { reason: "dispute_lost", disputeReason: dispute.reason, }, }, transaction); // Also create negative platform fee entry const platformFeeBps = parseInt(process.env.PLATFORM_FEE_BPS || "300"); const platformFeeAmount = Math.round((dispute.amount * platformFeeBps) / 10000); await createLedgerEntry({ orgId, eventId, orderId, type: "platform_fee", amountCents: -platformFeeAmount, currency: "USD", stripe: { chargeId: charge.id, disputeId: dispute.id, accountId: stripeAccountId, }, meta: { reason: "dispute_lost", }, }, transaction); console.log(`[${action}] Dispute lost - voided ${ticketsUpdated} tickets and created loss entries`, { orderId, disputeId: dispute.id, lossAmount: dispute.amount, platformFeeLoss: platformFeeAmount, }); } // Update order with final dispute status const orderRef = db.collection("orders").doc(orderId); transaction.update(orderRef, { "dispute.status": dispute.status, "dispute.outcome": dispute.outcome?.outcome, "dispute.closedAt": firestore_1.Timestamp.now(), updatedAt: firestore_1.Timestamp.now(), }); }); console.log(`[${action}] Dispute closure processing completed`, { disputeId: dispute.id, orderId, outcome: dispute.outcome?.outcome, processingTime: Date.now() - startTime, }); } catch (error) { console.error(`[${action}] Error processing dispute closed`, { disputeId: dispute.id, error: error.message, stack: error.stack, processingTime: Date.now() - startTime, }); throw error; } } /** * Gets dispute information for an order */ exports.getOrderDisputes = (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; } // Get order with dispute information const orderDoc = await db.collection("orders").doc(orderId).get(); if (!orderDoc.exists) { res.status(404).json({ error: "Order not found" }); return; } const orderData = orderDoc.data(); const dispute = orderData?.dispute; res.status(200).json({ orderId, dispute: dispute || null, }); } catch (error) { console.error("Error getting order disputes:", error); res.status(500).json({ error: "Internal server error", details: error.message, }); } }); // # sourceMappingURL=disputes.js.map