All files / src/api/analytics finance.ts

100% Statements 98/98
82.35% Branches 14/17
100% Functions 2/2
100% Lines 98/98

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 1011x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 5x 5x 5x 5x 5x 5x 4x 5x 5x 5x 5x 6x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 6x 1x 1x 6x      
/**
* @file finance.ts
* @module api/analytics/finance
*
* @summary
* Financial analytics (WAC-based) for a given date window and optional supplier.
* Tolerant parsing: unknown/missing fields → 0. Functions never throw; callers
* get a fully-populated object with zeros on error to keep the UI resilient.
* @enterprise
* - Finance endpoint uses `from/to` (not `start/end` like other analytics).
* - Accepts either a direct object or an envelope (e.g., { summary } or { data }).
* - Returns a fully-populated zero object on any error.
*/
 
import http from '../httpClient';
import { isRecord, pickNumber } from './util';
import type { Rec } from './util';
 
/** Canonical FE shape for financial summary (numbers are always defined). 
 * Returns 0 for missing/invalid fields.
 * @example
 * ```typescript
 * const summary = await getFinancialSummary({
 *   from: '2025-09-01',
 *  to: '2025-11-30',
 *  supplierId: 'SUP-001'
 * });
 * return (
 * <FinanceDashboard data={summary} />
 * );
 * ```
*/
export type FinancialSummary = {
    purchases: number;
    cogs: number;
    writeOffs: number;
    returns: number;
    openingValue: number;
    endingValue: number;
};
 
/** Zero object for graceful fallbacks. 
 * @internal 
*/
const ZERO_FINANCE: FinancialSummary = {
    purchases: 0,
    cogs: 0,
    writeOffs: 0,
    returns: 0,
    openingValue: 0,
    endingValue: 0,
};
 
/**
 * Fetch financial summary for a window.
 * Backend: GET /api/analytics/financial/summary?from&to[&supplierId]
 *
 * @enterprise
 * - Finance endpoint uses `from/to` (not `start/end` like other analytics).
 * - Accepts either a direct object or an envelope (e.g., { summary } or { data }).
 * - Returns a fully-populated zero object on any error.
 */
export async function getFinancialSummary(
  p?: { from?: string; to?: string; supplierId?: string }
): Promise<FinancialSummary> {
  try {
    const params: Record<string, string> = {};
    if (p?.from) params.from = p.from;
    if (p?.to) params.to = p.to;
    if (p?.supplierId) params.supplierId = p.supplierId;
 
    const { data } = await http.get<unknown>('/api/analytics/financial/summary', { params });
 
    // Accept direct object or envelope
    const pickPayload = (x: unknown): Rec | null => {
      if (!isRecord(x)) return null;
      if (isRecord(x.summary)) return x.summary as Rec;
      if (isRecord(x.data)) return x.data as Rec;
      return x as Rec;
    };
 
    // Parse fields with tolerant picking
    const body = pickPayload(data);
    if (!body) return ZERO_FINANCE;
 
    return {
        purchases:    pickNumber(body, ['purchases', 'purchasesCost', 'totalPurchases', 'purchaseTotal']),
        cogs:         pickNumber(body, ['cogs', 'cogsCost', 'costOfGoodsSold']),
        writeOffs:    pickNumber(body, ['writeOffs', 'writeOffCost', 'writeoffs', 'write_offs']),
        returns:      pickNumber(body, ['returns', 'returnsInCost', 'returnsCost', 'salesReturns', 'returnsTotal']),
        openingValue: pickNumber(body, ['openingValue', 'opening', 'startValue']),
        endingValue:  pickNumber(body, ['endingValue', 'ending', 'endValue']),
    };
 
  } catch {
    return ZERO_FINANCE;
  }
}