Files
dzinesco aa81eb5adb feat: add advanced analytics and territory management system
- 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>
2025-08-26 09:25:10 -06:00

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