- Add comprehensive analytics components with export functionality - Implement territory management with manager performance tracking - Add seatmap components for venue layout management - Create customer management features with modal interface - Add advanced hooks for dashboard flags and territory data - Implement seat selection and venue management utilities - Add type definitions for ticketing and seatmap systems 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
399 lines
15 KiB
JavaScript
399 lines
15 KiB
JavaScript
"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
|