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