optimal-cli 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/dist/bin/optimal.d.ts +2 -0
  2. package/dist/bin/optimal.js +1590 -0
  3. package/dist/lib/assets/index.d.ts +79 -0
  4. package/dist/lib/assets/index.js +153 -0
  5. package/dist/lib/assets.d.ts +20 -0
  6. package/dist/lib/assets.js +112 -0
  7. package/dist/lib/auth/index.d.ts +83 -0
  8. package/dist/lib/auth/index.js +146 -0
  9. package/dist/lib/board/index.d.ts +39 -0
  10. package/dist/lib/board/index.js +285 -0
  11. package/dist/lib/board/types.d.ts +111 -0
  12. package/dist/lib/board/types.js +1 -0
  13. package/dist/lib/bot/claim.d.ts +3 -0
  14. package/dist/lib/bot/claim.js +20 -0
  15. package/dist/lib/bot/coordinator.d.ts +27 -0
  16. package/dist/lib/bot/coordinator.js +178 -0
  17. package/dist/lib/bot/heartbeat.d.ts +6 -0
  18. package/dist/lib/bot/heartbeat.js +30 -0
  19. package/dist/lib/bot/index.d.ts +9 -0
  20. package/dist/lib/bot/index.js +6 -0
  21. package/dist/lib/bot/protocol.d.ts +12 -0
  22. package/dist/lib/bot/protocol.js +74 -0
  23. package/dist/lib/bot/reporter.d.ts +3 -0
  24. package/dist/lib/bot/reporter.js +27 -0
  25. package/dist/lib/bot/skills.d.ts +26 -0
  26. package/dist/lib/bot/skills.js +69 -0
  27. package/dist/lib/budget/projections.d.ts +115 -0
  28. package/dist/lib/budget/projections.js +384 -0
  29. package/dist/lib/budget/scenarios.d.ts +93 -0
  30. package/dist/lib/budget/scenarios.js +214 -0
  31. package/dist/lib/cms/publish-blog.d.ts +62 -0
  32. package/dist/lib/cms/publish-blog.js +74 -0
  33. package/dist/lib/cms/strapi-client.d.ts +123 -0
  34. package/dist/lib/cms/strapi-client.js +213 -0
  35. package/dist/lib/config/registry.d.ts +17 -0
  36. package/dist/lib/config/registry.js +182 -0
  37. package/dist/lib/config/schema.d.ts +31 -0
  38. package/dist/lib/config/schema.js +25 -0
  39. package/dist/lib/config.d.ts +55 -0
  40. package/dist/lib/config.js +206 -0
  41. package/dist/lib/errors.d.ts +25 -0
  42. package/dist/lib/errors.js +91 -0
  43. package/dist/lib/format.d.ts +28 -0
  44. package/dist/lib/format.js +98 -0
  45. package/dist/lib/infra/deploy.d.ts +29 -0
  46. package/dist/lib/infra/deploy.js +58 -0
  47. package/dist/lib/infra/migrate.d.ts +34 -0
  48. package/dist/lib/infra/migrate.js +103 -0
  49. package/dist/lib/newsletter/distribute.d.ts +52 -0
  50. package/dist/lib/newsletter/distribute.js +193 -0
  51. package/{lib/newsletter/generate-insurance.ts → dist/lib/newsletter/generate-insurance.d.ts} +7 -24
  52. package/dist/lib/newsletter/generate-insurance.js +36 -0
  53. package/dist/lib/newsletter/generate.d.ts +104 -0
  54. package/dist/lib/newsletter/generate.js +571 -0
  55. package/dist/lib/returnpro/anomalies.d.ts +64 -0
  56. package/dist/lib/returnpro/anomalies.js +166 -0
  57. package/dist/lib/returnpro/audit.d.ts +32 -0
  58. package/dist/lib/returnpro/audit.js +147 -0
  59. package/dist/lib/returnpro/diagnose.d.ts +52 -0
  60. package/dist/lib/returnpro/diagnose.js +281 -0
  61. package/dist/lib/returnpro/kpis.d.ts +32 -0
  62. package/dist/lib/returnpro/kpis.js +192 -0
  63. package/dist/lib/returnpro/templates.d.ts +48 -0
  64. package/dist/lib/returnpro/templates.js +229 -0
  65. package/dist/lib/returnpro/upload-income.d.ts +25 -0
  66. package/dist/lib/returnpro/upload-income.js +235 -0
  67. package/dist/lib/returnpro/upload-netsuite.d.ts +37 -0
  68. package/dist/lib/returnpro/upload-netsuite.js +566 -0
  69. package/dist/lib/returnpro/upload-r1.d.ts +48 -0
  70. package/dist/lib/returnpro/upload-r1.js +398 -0
  71. package/dist/lib/returnpro/validate.d.ts +37 -0
  72. package/dist/lib/returnpro/validate.js +124 -0
  73. package/dist/lib/social/meta.d.ts +90 -0
  74. package/dist/lib/social/meta.js +160 -0
  75. package/dist/lib/social/post-generator.d.ts +83 -0
  76. package/dist/lib/social/post-generator.js +333 -0
  77. package/dist/lib/social/publish.d.ts +66 -0
  78. package/dist/lib/social/publish.js +226 -0
  79. package/dist/lib/social/scraper.d.ts +67 -0
  80. package/dist/lib/social/scraper.js +361 -0
  81. package/dist/lib/supabase.d.ts +4 -0
  82. package/dist/lib/supabase.js +20 -0
  83. package/dist/lib/transactions/delete-batch.d.ts +60 -0
  84. package/dist/lib/transactions/delete-batch.js +203 -0
  85. package/dist/lib/transactions/ingest.d.ts +43 -0
  86. package/dist/lib/transactions/ingest.js +555 -0
  87. package/dist/lib/transactions/stamp.d.ts +51 -0
  88. package/dist/lib/transactions/stamp.js +524 -0
  89. package/package.json +3 -4
  90. package/bin/optimal.ts +0 -1731
  91. package/lib/assets/index.ts +0 -225
  92. package/lib/assets.ts +0 -124
  93. package/lib/auth/index.ts +0 -189
  94. package/lib/board/index.ts +0 -309
  95. package/lib/board/types.ts +0 -124
  96. package/lib/bot/claim.ts +0 -43
  97. package/lib/bot/coordinator.ts +0 -254
  98. package/lib/bot/heartbeat.ts +0 -37
  99. package/lib/bot/index.ts +0 -9
  100. package/lib/bot/protocol.ts +0 -99
  101. package/lib/bot/reporter.ts +0 -42
  102. package/lib/bot/skills.ts +0 -81
  103. package/lib/budget/projections.ts +0 -561
  104. package/lib/budget/scenarios.ts +0 -312
  105. package/lib/cms/publish-blog.ts +0 -129
  106. package/lib/cms/strapi-client.ts +0 -302
  107. package/lib/config/registry.ts +0 -228
  108. package/lib/config/schema.ts +0 -58
  109. package/lib/config.ts +0 -247
  110. package/lib/errors.ts +0 -129
  111. package/lib/format.ts +0 -120
  112. package/lib/infra/.gitkeep +0 -0
  113. package/lib/infra/deploy.ts +0 -70
  114. package/lib/infra/migrate.ts +0 -141
  115. package/lib/newsletter/.gitkeep +0 -0
  116. package/lib/newsletter/distribute.ts +0 -256
  117. package/lib/newsletter/generate.ts +0 -735
  118. package/lib/returnpro/.gitkeep +0 -0
  119. package/lib/returnpro/anomalies.ts +0 -258
  120. package/lib/returnpro/audit.ts +0 -194
  121. package/lib/returnpro/diagnose.ts +0 -400
  122. package/lib/returnpro/kpis.ts +0 -255
  123. package/lib/returnpro/templates.ts +0 -323
  124. package/lib/returnpro/upload-income.ts +0 -311
  125. package/lib/returnpro/upload-netsuite.ts +0 -696
  126. package/lib/returnpro/upload-r1.ts +0 -563
  127. package/lib/returnpro/validate.ts +0 -154
  128. package/lib/social/meta.ts +0 -228
  129. package/lib/social/post-generator.ts +0 -468
  130. package/lib/social/publish.ts +0 -301
  131. package/lib/social/scraper.ts +0 -503
  132. package/lib/supabase.ts +0 -25
  133. package/lib/transactions/delete-batch.ts +0 -258
  134. package/lib/transactions/ingest.ts +0 -659
  135. package/lib/transactions/stamp.ts +0 -654
File without changes
@@ -1,258 +0,0 @@
1
- import { getSupabase } from '../supabase.js'
2
-
3
- // --- Types ---
4
-
5
- export interface RateAnomaly {
6
- /** Master program name (e.g., "Bass Pro Shops Liquidation") */
7
- master_program: string
8
- /** Program code (e.g., "BRTON-BPS-LIQ") */
9
- program_code: string | null
10
- /** Numeric program ID key from dim_program_id */
11
- program_id: number | null
12
- /** Client ID from dim_client */
13
- client_id: number | null
14
- /** Client display name */
15
- client_name: string | null
16
- /** YYYY-MM period */
17
- month: string
18
- /** Service Check In Fee dollars for this program+month */
19
- checkin_fee_dollars: number
20
- /** Checked-in units for this program+month */
21
- units: number
22
- /** Dollars per unit = checkin_fee_dollars / units */
23
- rate_per_unit: number
24
- /** Prior month's rate_per_unit for comparison */
25
- prev_month_rate: number | null
26
- /** % change in rate vs prior month */
27
- rate_delta_pct: number | null
28
- /** % change in units vs prior month */
29
- units_change_pct: number | null
30
- /** % change in dollars vs prior month */
31
- dollars_change_pct: number | null
32
- /** Z-score of rate_per_unit within the portfolio cross-section */
33
- zscore: number
34
- /**
35
- * The [mean - 2σ, mean + 2σ] interval computed from all program rates
36
- * in the same period. Rates outside this window are flagged.
37
- */
38
- expected_range: [number, number]
39
- }
40
-
41
- export interface AnomalyResult {
42
- /** Anomalies that exceed the z-score threshold */
43
- anomalies: RateAnomaly[]
44
- /** Total rows fetched from v_rate_anomaly_analysis before filtering */
45
- totalRows: number
46
- /** Z-score threshold used (default 2.0) */
47
- threshold: number
48
- /** Months included in the analysis */
49
- months: string[]
50
- }
51
-
52
- // --- Helpers ---
53
-
54
- const PAGE_SIZE = 1000
55
-
56
- interface ViewRow {
57
- master_program: string
58
- program_code: string | null
59
- program_id: number | null
60
- client_id: number | null
61
- client_name: string | null
62
- month: string
63
- checkin_fee_dollars: number | string
64
- units: number | string
65
- rate_per_unit: number | string | null
66
- prev_month_rate: number | string | null
67
- rate_delta_pct: number | string | null
68
- units_change_pct: number | string | null
69
- dollars_change_pct: number | string | null
70
- }
71
-
72
- function toNum(v: number | string | null | undefined): number {
73
- if (v === null || v === undefined) return 0
74
- return typeof v === 'string' ? parseFloat(v) || 0 : Number(v) || 0
75
- }
76
-
77
- function toNumOrNull(v: number | string | null | undefined): number | null {
78
- if (v === null || v === undefined) return null
79
- const n = typeof v === 'string' ? parseFloat(v) : Number(v)
80
- return isFinite(n) ? n : null
81
- }
82
-
83
- /**
84
- * Paginate through v_rate_anomaly_analysis with optional month filters.
85
- * Returns raw view rows.
86
- */
87
- async function fetchViewRows(months?: string[]): Promise<ViewRow[]> {
88
- const sb = getSupabase('returnpro')
89
- const allRows: ViewRow[] = []
90
- let from = 0
91
-
92
- while (true) {
93
- let query = sb
94
- .from('v_rate_anomaly_analysis')
95
- .select(
96
- 'master_program,program_code,program_id,client_id,client_name,' +
97
- 'month,checkin_fee_dollars,units,rate_per_unit,prev_month_rate,' +
98
- 'rate_delta_pct,units_change_pct,dollars_change_pct'
99
- )
100
- .order('month', { ascending: false })
101
- .order('master_program')
102
- .range(from, from + PAGE_SIZE - 1)
103
-
104
- if (months && months.length > 0) {
105
- query = query.in('month', months)
106
- }
107
-
108
- const { data, error } = await query
109
-
110
- if (error) throw new Error(`Fetch v_rate_anomaly_analysis failed: ${error.message}`)
111
- if (!data || data.length === 0) break
112
-
113
- allRows.push(...(data as unknown as ViewRow[]))
114
- if (data.length < PAGE_SIZE) break
115
- from += PAGE_SIZE
116
- }
117
-
118
- return allRows
119
- }
120
-
121
- /**
122
- * Compute mean and standard deviation for an array of numbers.
123
- * Returns { mean, stddev }. If fewer than 2 values, stddev = 0.
124
- */
125
- function computeStats(values: number[]): { mean: number; stddev: number } {
126
- if (values.length === 0) return { mean: 0, stddev: 0 }
127
- const mean = values.reduce((s, v) => s + v, 0) / values.length
128
- if (values.length < 2) return { mean, stddev: 0 }
129
- const variance = values.reduce((s, v) => s + (v - mean) ** 2, 0) / (values.length - 1)
130
- return { mean, stddev: Math.sqrt(variance) }
131
- }
132
-
133
- // --- Core ---
134
-
135
- /**
136
- * Detect $/unit rate outliers across all programs in stg_financials_raw.
137
- *
138
- * Method:
139
- * 1. Fetch all rows from v_rate_anomaly_analysis (paginated) filtered to
140
- * the requested months (or fiscal YTD if omitted).
141
- * 2. For each month, compute mean and population stddev of rate_per_unit
142
- * across all programs with valid rates.
143
- * 3. Flag any program-month where |z-score| > threshold (default 2.0).
144
- * 4. Return the flagged rows sorted by |z-score| descending.
145
- *
146
- * @param options.months - YYYY-MM strings to analyse. If omitted, uses fiscal
147
- * YTD (April of current/previous fiscal year → today).
148
- * @param options.threshold - Z-score magnitude threshold. Default 2.0.
149
- */
150
- export async function detectRateAnomalies(
151
- options?: { months?: string[]; threshold?: number }
152
- ): Promise<AnomalyResult> {
153
- const threshold = options?.threshold ?? 2.0
154
-
155
- // Resolve target months: explicit list, or derive fiscal YTD
156
- let targetMonths: string[] | undefined = options?.months
157
-
158
- if (!targetMonths || targetMonths.length === 0) {
159
- // Fiscal year starts April. If Jan-Mar, fiscal year began previous calendar year.
160
- const now = new Date()
161
- const month0 = now.getMonth() // 0-indexed
162
- const year = now.getFullYear()
163
- const fiscalStartYear = month0 < 3 ? year - 1 : year
164
- const fiscalStart = `${fiscalStartYear}-04`
165
- const currentMonthStr = `${year}-${String(month0 + 1).padStart(2, '0')}`
166
-
167
- // Build explicit month list for fiscal YTD so the DB filter is tight
168
- const start = new Date(`${fiscalStart}-01`)
169
- const end = new Date(`${currentMonthStr}-01`)
170
- const months: string[] = []
171
- const cursor = new Date(start)
172
- while (cursor <= end) {
173
- months.push(
174
- `${cursor.getFullYear()}-${String(cursor.getMonth() + 1).padStart(2, '0')}`
175
- )
176
- cursor.setMonth(cursor.getMonth() + 1)
177
- }
178
- targetMonths = months
179
- }
180
-
181
- // Fetch view rows
182
- const rawRows = await fetchViewRows(targetMonths)
183
- const totalRows = rawRows.length
184
-
185
- if (totalRows === 0) {
186
- return { anomalies: [], totalRows: 0, threshold, months: targetMonths }
187
- }
188
-
189
- // Group rows by month for per-month z-score calculation
190
- const byMonth = new Map<string, ViewRow[]>()
191
- for (const row of rawRows) {
192
- const m = row.month
193
- if (!byMonth.has(m)) byMonth.set(m, [])
194
- byMonth.get(m)!.push(row)
195
- }
196
-
197
- // Compute z-scores per month and collect anomalies
198
- const anomalies: RateAnomaly[] = []
199
-
200
- for (const [month, rows] of byMonth) {
201
- // Collect valid (non-null, positive-unit) rate values for this month
202
- const validRates = rows
203
- .map(r => toNumOrNull(r.rate_per_unit))
204
- .filter((v): v is number => v !== null && isFinite(v))
205
-
206
- const { mean, stddev } = computeStats(validRates)
207
-
208
- for (const row of rows) {
209
- const rate = toNumOrNull(row.rate_per_unit)
210
- if (rate === null) continue // cannot score rows with no rate
211
-
212
- const units = toNum(row.units)
213
- if (units <= 0) continue // require positive units for a meaningful rate
214
-
215
- // Z-score: how many std-deviations from the mean
216
- const zscore = stddev > 0 ? (rate - mean) / stddev : 0
217
-
218
- if (Math.abs(zscore) <= threshold) continue // within normal range
219
-
220
- const expectedLow = mean - threshold * stddev
221
- const expectedHigh = mean + threshold * stddev
222
-
223
- anomalies.push({
224
- master_program: row.master_program,
225
- program_code: row.program_code,
226
- program_id: typeof row.program_id === 'number' ? row.program_id : null,
227
- client_id: typeof row.client_id === 'number' ? row.client_id : null,
228
- client_name: row.client_name,
229
- month,
230
- checkin_fee_dollars: toNum(row.checkin_fee_dollars),
231
- units,
232
- rate_per_unit: rate,
233
- prev_month_rate: toNumOrNull(row.prev_month_rate),
234
- rate_delta_pct: toNumOrNull(row.rate_delta_pct),
235
- units_change_pct: toNumOrNull(row.units_change_pct),
236
- dollars_change_pct: toNumOrNull(row.dollars_change_pct),
237
- zscore: Math.round(zscore * 100) / 100,
238
- expected_range: [
239
- Math.round(expectedLow * 10000) / 10000,
240
- Math.round(expectedHigh * 10000) / 10000,
241
- ],
242
- })
243
- }
244
- }
245
-
246
- // Sort by absolute z-score descending (most extreme outliers first)
247
- anomalies.sort((a, b) => Math.abs(b.zscore) - Math.abs(a.zscore))
248
-
249
- // Collect distinct months that were actually present in the data
250
- const observedMonths = [...new Set(rawRows.map(r => r.month))].sort().reverse()
251
-
252
- return {
253
- anomalies,
254
- totalRows,
255
- threshold,
256
- months: observedMonths,
257
- }
258
- }
@@ -1,194 +0,0 @@
1
- import { getSupabase } from '../supabase.js'
2
-
3
- // --- Types ---
4
-
5
- export interface MonthSummary {
6
- month: string // YYYY-MM
7
- confirmedAccounts: number
8
- stagedAccounts: number
9
- exactMatch: number
10
- signFlipMatch: number
11
- mismatch: number
12
- confirmedOnly: number
13
- stagingOnly: number
14
- accuracy: number | null // percentage on overlap, null if no overlap
15
- stagedTotal: number // absolute sum of staged amounts
16
- confirmedTotal: number // absolute sum of confirmed amounts
17
- }
18
-
19
- export interface AuditResult {
20
- summaries: MonthSummary[]
21
- totalStagingRows: number
22
- totalConfirmedRows: number
23
- }
24
-
25
- // --- Helpers ---
26
-
27
- const PAGE_SIZE = 1000
28
-
29
- /**
30
- * Paginate through a Supabase table, fetching all rows.
31
- * Uses .range() to bypass the 1000-row cap.
32
- */
33
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
34
- async function paginateAll(
35
- table: string,
36
- select: string,
37
- orderCol: string,
38
- ): Promise<any[]> {
39
- const sb = getSupabase('returnpro')
40
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
41
- const allRows: any[] = []
42
- let from = 0
43
-
44
- while (true) {
45
- const query = sb.from(table).select(select).order(orderCol).range(from, from + PAGE_SIZE - 1)
46
-
47
- const { data, error } = await query
48
-
49
- if (error) throw new Error(`Fetch ${table} failed: ${error.message}`)
50
- if (!data || data.length === 0) break
51
-
52
- allRows.push(...data)
53
- if (data.length < PAGE_SIZE) break
54
- from += PAGE_SIZE
55
- }
56
-
57
- return allRows
58
- }
59
-
60
- // --- Core ---
61
-
62
- /**
63
- * Compare staged financials against confirmed income statements.
64
- *
65
- * Replicates the logic from dashboard-returnpro's /api/staging/audit-summary route:
66
- * 1. Paginate stg_financials_raw (amount is TEXT, must parseFloat)
67
- * 2. Paginate confirmed_income_statements
68
- * 3. Aggregate staging by account_code|YYYY-MM key
69
- * 4. Compare with tolerance (default $1.00), detect sign-flips
70
- * 5. Return per-month summaries with accuracy %
71
- *
72
- * @param months - Optional array of YYYY-MM strings to filter to. If omitted, all months are included.
73
- * @param tolerance - Dollar tolerance for match detection. Default $1.00.
74
- */
75
- export async function runAuditComparison(
76
- months?: string[],
77
- tolerance = 1.00,
78
- ): Promise<AuditResult> {
79
- // 1. Fetch all staging rows (paginated)
80
- const stagingRows = await paginateAll(
81
- 'stg_financials_raw', 'account_code,date,amount', 'date',
82
- ) as Array<{ account_code: string; date: string; amount: string }>
83
-
84
- // 2. Fetch all confirmed income statements (paginated)
85
- const confirmedRows = await paginateAll(
86
- 'confirmed_income_statements', 'account_code,period,total_amount', 'period',
87
- ) as Array<{ account_code: string; period: string; total_amount: number }>
88
-
89
- // 3. Aggregate staging: account_code|YYYY-MM -> sum(amount)
90
- // amount is TEXT in the DB — must parseFloat
91
- const stagingAgg = new Map<string, number>()
92
- for (const row of stagingRows) {
93
- const month = row.date ? row.date.substring(0, 7) : null
94
- if (!month) continue
95
- const key = `${row.account_code}|${month}`
96
- stagingAgg.set(key, (stagingAgg.get(key) ?? 0) + (parseFloat(row.amount) || 0))
97
- }
98
-
99
- // 4. Build confirmed lookup: account_code|YYYY-MM -> total_amount
100
- const confirmedMap = new Map<string, number>()
101
- for (const row of confirmedRows) {
102
- const key = `${row.account_code}|${row.period}`
103
- confirmedMap.set(key, parseFloat(String(row.total_amount)) || 0)
104
- }
105
-
106
- // 5. Collect all months present in either dataset
107
- const allMonths = new Set<string>()
108
- for (const key of stagingAgg.keys()) allMonths.add(key.split('|')[1])
109
- for (const key of confirmedMap.keys()) allMonths.add(key.split('|')[1])
110
-
111
- // 6. Filter to requested months if specified
112
- const targetMonths = months
113
- ? [...allMonths].filter(m => months.includes(m)).sort()
114
- : [...allMonths].sort()
115
-
116
- // 7. Build per-month summaries
117
- const summaries: MonthSummary[] = []
118
-
119
- for (const month of targetMonths) {
120
- // Collect accounts present in each dataset for this month
121
- const cAccounts = new Set<string>()
122
- const sAccounts = new Set<string>()
123
-
124
- for (const key of confirmedMap.keys()) {
125
- if (key.endsWith(`|${month}`)) cAccounts.add(key.split('|')[0])
126
- }
127
- for (const key of stagingAgg.keys()) {
128
- if (key.endsWith(`|${month}`)) sAccounts.add(key.split('|')[0])
129
- }
130
-
131
- let exactMatch = 0
132
- let signFlipMatch = 0
133
- let mismatch = 0
134
- let confirmedOnly = 0
135
- let stagingOnly = 0
136
- let stagedTotal = 0
137
- let confirmedTotal = 0
138
-
139
- // Compare confirmed accounts against staging
140
- for (const acct of cAccounts) {
141
- const cAmt = confirmedMap.get(`${acct}|${month}`) ?? 0
142
- confirmedTotal += Math.abs(cAmt)
143
-
144
- if (sAccounts.has(acct)) {
145
- const sAmt = stagingAgg.get(`${acct}|${month}`) ?? 0
146
- const directDiff = Math.abs(cAmt - sAmt)
147
- const signFlipDiff = Math.abs(cAmt + sAmt)
148
-
149
- if (directDiff <= tolerance) {
150
- exactMatch++
151
- } else if (signFlipDiff <= tolerance) {
152
- signFlipMatch++
153
- } else {
154
- mismatch++
155
- }
156
- } else {
157
- confirmedOnly++
158
- }
159
- }
160
-
161
- // Count staging-only accounts and accumulate staged total
162
- for (const acct of sAccounts) {
163
- const sAmt = stagingAgg.get(`${acct}|${month}`) ?? 0
164
- stagedTotal += Math.abs(sAmt)
165
- if (!cAccounts.has(acct)) stagingOnly++
166
- }
167
-
168
- // Accuracy = (exactMatch + signFlipMatch) / overlap, null if no overlap
169
- const overlap = exactMatch + signFlipMatch + mismatch
170
- const accuracy = overlap > 0
171
- ? Math.round(((exactMatch + signFlipMatch) / overlap) * 1000) / 10
172
- : null
173
-
174
- summaries.push({
175
- month,
176
- confirmedAccounts: cAccounts.size,
177
- stagedAccounts: sAccounts.size,
178
- exactMatch,
179
- signFlipMatch,
180
- mismatch,
181
- confirmedOnly,
182
- stagingOnly,
183
- accuracy,
184
- stagedTotal,
185
- confirmedTotal,
186
- })
187
- }
188
-
189
- return {
190
- summaries,
191
- totalStagingRows: stagingRows.length,
192
- totalConfirmedRows: confirmedRows.length,
193
- }
194
- }