|
| 1 | +import { NextResponse } from "next/server" |
| 2 | +import { requireAuthenticatedUser, finalizeAuthenticatedResponse } from "@/lib/api/route-guard" |
| 3 | +import { calculateTreasuryPosition, type TreasuryBucket } from "@/lib/treasury/service" |
| 4 | +import dbConnect from "@/lib/dbConnect" |
| 5 | +import LedgerAccount from "@/models/LedgerAccount" |
| 6 | +import LedgerEntry from "@/models/LedgerEntry" |
| 7 | +import TreasurySnapshot from "@/models/TreasurySnapshot" |
| 8 | + |
| 9 | +const CATEGORY_BUCKET: Record<string, TreasuryBucket> = { |
| 10 | + platform_clearing: "available_cash", pool_escrow: "restricted_escrow", settlement_in_transit: "settlement_in_transit", |
| 11 | + payouts_payable: "investor_payable", refunds_payable: "refund_payable", platform_reserve: "platform_reserve", |
| 12 | + revenue_fees: "fees", suspense: "suspense", |
| 13 | +} |
| 14 | + |
| 15 | +export async function GET(request: Request) { |
| 16 | + try { |
| 17 | + const auth = await requireAuthenticatedUser(request, ["admin"]) |
| 18 | + if ("response" in auth) return auth.response |
| 19 | + await dbConnect() |
| 20 | + const currency = new URL(request.url).searchParams.get("currency") || "NGN" |
| 21 | + const accounts = await LedgerAccount.find({ currency, category: { $in: Object.keys(CATEGORY_BUCKET) } }).lean() |
| 22 | + const accountIds = accounts.map((account: any) => account._id) |
| 23 | + const totals = accountIds.length ? await LedgerEntry.aggregate([ |
| 24 | + { $match: { accountId: { $in: accountIds }, currency } }, |
| 25 | + { $group: { _id: { accountId: "$accountId", direction: "$direction" }, amount: { $sum: "$amount" } } }, |
| 26 | + ]) : [] |
| 27 | + const byAccount = new Map<string, number>() |
| 28 | + for (const total of totals) { |
| 29 | + const key = total._id.accountId.toString() |
| 30 | + byAccount.set(key, (byAccount.get(key) || 0) + (total._id.direction === "debit" ? total.amount : -total.amount)) |
| 31 | + } |
| 32 | + const buckets: Partial<Record<TreasuryBucket, number>> = {} |
| 33 | + for (const account of accounts as any[]) { |
| 34 | + const bucket = CATEGORY_BUCKET[account.category] |
| 35 | + // Values must be integer minor units; legacy decimal entries are refused rather than rounded. |
| 36 | + const amount = Math.abs(byAccount.get(account._id.toString()) || 0) |
| 37 | + if (!Number.isSafeInteger(amount)) throw new Error("Treasury cannot summarize legacy non-minor-unit ledger entries.") |
| 38 | + buckets[bucket] = (buckets[bucket] || 0) + amount |
| 39 | + } |
| 40 | + const position = calculateTreasuryPosition(buckets, { minimumReserveMinor: 0 }) |
| 41 | + const response = NextResponse.json({ success: true, currency, position, source: { ledgerEntryCount: totals.length, credentialsIncluded: false } }) |
| 42 | + return finalizeAuthenticatedResponse(response, auth) |
| 43 | + } catch (error) { |
| 44 | + return NextResponse.json({ message: error instanceof Error ? error.message : "Failed to load treasury summary." }, { status: 500 }) |
| 45 | + } |
| 46 | +} |
| 47 | + |
| 48 | +export async function POST(request: Request) { |
| 49 | + try { |
| 50 | + const auth = await requireAuthenticatedUser(request, ["admin"]) |
| 51 | + if ("response" in auth) return auth.response |
| 52 | + const body = await request.json().catch(() => ({})) |
| 53 | + const currency = typeof body.currency === "string" ? body.currency : "NGN" |
| 54 | + const response = await GET(new Request(`${request.url}?currency=${encodeURIComponent(currency)}`, { headers: request.headers })) |
| 55 | + if (!response.ok) return response |
| 56 | + const payload = await response.json() |
| 57 | + const snapshotDate = new Date().toISOString().slice(0, 10) |
| 58 | + await TreasurySnapshot.findOneAndUpdate({ snapshotDate, currency }, { $set: { snapshotDate, currency, buckets: payload.position.buckets, availableLiquidityMinor: payload.position.availableLiquidityMinor, requiredLiquidityMinor: payload.position.requiredLiquidityMinor, varianceMinor: payload.position.varianceMinor, explanations: payload.position.explanations, sourceJournalCount: payload.source.ledgerEntryCount, sourceThrough: new Date() } }, { upsert: true, new: true }) |
| 59 | + return NextResponse.json({ success: true, snapshotDate, position: payload.position }) |
| 60 | + } catch (error) { return NextResponse.json({ message: error instanceof Error ? error.message : "Failed to create snapshot." }, { status: 500 }) } |
| 61 | +} |
0 commit comments