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

349 lines
13 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.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