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

277 lines
12 KiB
JavaScript

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getReconciliationEvents = exports.getReconciliationData = 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 csv_writer_1 = require("csv-writer");
const os_1 = require("os");
const path_1 = require("path");
const fs_1 = require("fs");
// Initialize Firebase Admin if not already initialized
try {
(0, app_1.initializeApp)();
}
catch (error) {
// App already initialized
}
const db = (0, firestore_1.getFirestore)();
/**
* Helper function to check user permissions
*/
async function checkReconciliationPermissions(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 reconciliation permissions:", error);
return false;
}
}
/**
* Gets reconciliation data for an organization
*/
exports.getReconciliationData = (0, https_1.onRequest)({ cors: true, enforceAppCheck: false, region: "us-central1" }, async (req, res) => {
const startTime = Date.now();
const action = "get_reconciliation_data";
try {
console.log(`[${action}] Starting reconciliation request`, {
method: req.method,
body: req.body,
query: req.query,
timestamp: new Date().toISOString(),
});
if (req.method !== "POST") {
res.status(405).json({ error: "Method not allowed" });
return;
}
const { orgId, eventId, startDate, endDate, format = 'json' } = req.body;
if (!orgId || !startDate || !endDate) {
res.status(400).json({ error: "orgId, startDate, and endDate are 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";
// Check permissions
const hasPermission = await checkReconciliationPermissions(uid, orgId);
if (!hasPermission) {
console.error(`[${action}] Permission denied for user ${uid} in org ${orgId}`);
res.status(403).json({ error: "Insufficient permissions" });
return;
}
// Parse date range
const start = new Date(startDate);
const end = new Date(endDate);
end.setHours(23, 59, 59, 999); // Include full end date
if (start >= end) {
res.status(400).json({ error: "Start date must be before end date" });
return;
}
console.log(`[${action}] Querying ledger entries`, {
orgId,
eventId,
startDate: start.toISOString(),
endDate: end.toISOString(),
});
// Build query
let query = db.collection("ledger")
.where("orgId", "==", orgId)
.where("createdAt", ">=", firestore_1.Timestamp.fromDate(start))
.where("createdAt", "<=", firestore_1.Timestamp.fromDate(end));
// Add event filter if specified
if (eventId && eventId !== 'all') {
query = query.where("eventId", "==", eventId);
}
// Execute query
const ledgerSnapshot = await query.orderBy("createdAt", "desc").get();
const ledgerEntries = ledgerSnapshot.docs.map(doc => {
const data = doc.data();
return {
id: doc.id,
...data,
createdAt: data.createdAt.toDate().toISOString(),
};
});
console.log(`[${action}] Found ${ledgerEntries.length} ledger entries`);
// Calculate summary
const summary = {
grossSales: ledgerEntries
.filter(e => e.type === 'sale')
.reduce((sum, e) => sum + e.amountCents, 0),
refunds: Math.abs(ledgerEntries
.filter(e => e.type === 'refund')
.reduce((sum, e) => sum + e.amountCents, 0)),
stripeFees: Math.abs(ledgerEntries
.filter(e => e.type === 'fee')
.reduce((sum, e) => sum + e.amountCents, 0)),
platformFees: Math.abs(ledgerEntries
.filter(e => e.type === 'platform_fee')
.reduce((sum, e) => sum + e.amountCents, 0)),
disputeFees: Math.abs(ledgerEntries
.filter(e => e.type === 'dispute_fee')
.reduce((sum, e) => sum + e.amountCents, 0)),
totalTransactions: new Set(ledgerEntries.map(e => e.orderId)).size,
period: {
start: startDate,
end: endDate,
},
};
summary['netToOrganizer'] = summary.grossSales - summary.refunds - summary.stripeFees - summary.platformFees - summary.disputeFees;
if (format === 'csv') {
// Generate CSV file
const csvData = await generateCSV(ledgerEntries, summary);
res.setHeader('Content-Type', 'text/csv');
res.setHeader('Content-Disposition', `attachment; filename="reconciliation-${startDate}-to-${endDate}.csv"`);
res.status(200).send(csvData);
}
else {
// Return JSON
res.status(200).json({
summary,
entries: ledgerEntries,
total: ledgerEntries.length,
});
}
console.log(`[${action}] Reconciliation completed successfully`, {
orgId,
entriesCount: ledgerEntries.length,
grossSales: summary.grossSales,
netToOrganizer: summary['netToOrganizer'],
processingTime: Date.now() - startTime,
});
}
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,
});
}
});
/**
* Generates CSV content from ledger entries
*/
async function generateCSV(entries, summary) {
const tmpFilePath = (0, path_1.join)((0, os_1.tmpdir)(), `reconciliation-${Date.now()}.csv`);
try {
const csvWriter = (0, csv_writer_1.createObjectCsvWriter)({
path: tmpFilePath,
header: [
{ id: 'date', title: 'Date' },
{ id: 'type', title: 'Type' },
{ id: 'amount', title: 'Amount (USD)' },
{ id: 'orderId', title: 'Order ID' },
{ id: 'stripeTransactionId', title: 'Stripe Transaction ID' },
{ id: 'chargeRefundId', title: 'Charge/Refund ID' },
{ id: 'accountId', title: 'Stripe Account ID' },
{ id: 'notes', title: 'Notes' },
],
});
// Prepare data for CSV
const csvRecords = entries.map(entry => ({
date: new Date(entry.createdAt).toISOString(),
type: entry.type,
amount: (entry.amountCents / 100).toFixed(2),
orderId: entry.orderId,
stripeTransactionId: entry.stripe.balanceTxnId || '',
chargeRefundId: entry.stripe.chargeId || entry.stripe.refundId || entry.stripe.disputeId || '',
accountId: entry.stripe.accountId,
notes: entry.meta ? Object.entries(entry.meta).map(([k, v]) => `${k}:${v}`).join(';') : '',
}));
// Add summary rows at the top
const summaryRows = [
{ date: 'SUMMARY', type: '', amount: '', orderId: '', stripeTransactionId: '', chargeRefundId: '', accountId: '', notes: '' },
{ date: summary.period.start, type: 'Period Start', amount: '', orderId: '', stripeTransactionId: '', chargeRefundId: '', accountId: '', notes: '' },
{ date: summary.period.end, type: 'Period End', amount: '', orderId: '', stripeTransactionId: '', chargeRefundId: '', accountId: '', notes: '' },
{ date: '', type: 'Gross Sales', amount: (summary.grossSales / 100).toFixed(2), orderId: '', stripeTransactionId: '', chargeRefundId: '', accountId: '', notes: '' },
{ date: '', type: 'Refunds', amount: (summary.refunds / 100).toFixed(2), orderId: '', stripeTransactionId: '', chargeRefundId: '', accountId: '', notes: '' },
{ date: '', type: 'Stripe Fees', amount: (summary.stripeFees / 100).toFixed(2), orderId: '', stripeTransactionId: '', chargeRefundId: '', accountId: '', notes: '' },
{ date: '', type: 'Platform Fees', amount: (summary.platformFees / 100).toFixed(2), orderId: '', stripeTransactionId: '', chargeRefundId: '', accountId: '', notes: '' },
{ date: '', type: 'Dispute Fees', amount: (summary.disputeFees / 100).toFixed(2), orderId: '', stripeTransactionId: '', chargeRefundId: '', accountId: '', notes: '' },
{ date: '', type: 'Net to Organizer', amount: (summary.netToOrganizer / 100).toFixed(2), orderId: '', stripeTransactionId: '', chargeRefundId: '', accountId: '', notes: '' },
{ date: '', type: 'Total Transactions', amount: summary.totalTransactions.toString(), orderId: '', stripeTransactionId: '', chargeRefundId: '', accountId: '', notes: '' },
{ date: '', type: '', amount: '', orderId: '', stripeTransactionId: '', chargeRefundId: '', accountId: '', notes: '' },
{ date: 'TRANSACTIONS', type: '', amount: '', orderId: '', stripeTransactionId: '', chargeRefundId: '', accountId: '', notes: '' },
];
await csvWriter.writeRecords([...summaryRows, ...csvRecords]);
// Read the file content
const csvContent = (0, fs_1.readFileSync)(tmpFilePath, 'utf8');
// Clean up temporary file
(0, fs_1.unlinkSync)(tmpFilePath);
return csvContent;
}
catch (error) {
// Clean up on error
try {
(0, fs_1.unlinkSync)(tmpFilePath);
}
catch (cleanupError) {
// Ignore cleanup errors
}
throw error;
}
}
/**
* Gets available events for reconciliation
*/
exports.getReconciliationEvents = (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 { orgId } = req.body;
if (!orgId) {
res.status(400).json({ error: "orgId is required" });
return;
}
// Get user ID and check permissions
const uid = req.headers.authorization?.replace("Bearer ", "") || "mock-uid";
const hasPermission = await checkReconciliationPermissions(uid, orgId);
if (!hasPermission) {
res.status(403).json({ error: "Insufficient permissions" });
return;
}
// Get events for the organization
const eventsSnapshot = await db.collection("events")
.where("orgId", "==", orgId)
.orderBy("startAt", "desc")
.get();
const events = eventsSnapshot.docs.map(doc => ({
id: doc.id,
name: doc.data().name,
startAt: doc.data().startAt?.toDate?.()?.toISOString() || doc.data().startAt,
}));
res.status(200).json({ events });
}
catch (error) {
console.error("Error getting reconciliation events:", error);
res.status(500).json({
error: "Internal server error",
details: error.message,
});
}
});
// # sourceMappingURL=reconciliation.js.map