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>
This commit is contained in:
399
reactrebuild0825/functions/lib/disputes.js
Normal file
399
reactrebuild0825/functions/lib/disputes.js
Normal file
@@ -0,0 +1,399 @@
|
||||
"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
|
||||
Reference in New Issue
Block a user