"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