- 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>
277 lines
12 KiB
JavaScript
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
|