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:
349
reactrebuild0825/functions/lib/refunds.js
Normal file
349
reactrebuild0825/functions/lib/refunds.js
Normal file
@@ -0,0 +1,349 @@
|
||||
"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
|
||||
Reference in New Issue
Block a user