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.
- package/dist/bin/optimal.d.ts +2 -0
- package/dist/bin/optimal.js +1590 -0
- package/dist/lib/assets/index.d.ts +79 -0
- package/dist/lib/assets/index.js +153 -0
- package/dist/lib/assets.d.ts +20 -0
- package/dist/lib/assets.js +112 -0
- package/dist/lib/auth/index.d.ts +83 -0
- package/dist/lib/auth/index.js +146 -0
- package/dist/lib/board/index.d.ts +39 -0
- package/dist/lib/board/index.js +285 -0
- package/dist/lib/board/types.d.ts +111 -0
- package/dist/lib/board/types.js +1 -0
- package/dist/lib/bot/claim.d.ts +3 -0
- package/dist/lib/bot/claim.js +20 -0
- package/dist/lib/bot/coordinator.d.ts +27 -0
- package/dist/lib/bot/coordinator.js +178 -0
- package/dist/lib/bot/heartbeat.d.ts +6 -0
- package/dist/lib/bot/heartbeat.js +30 -0
- package/dist/lib/bot/index.d.ts +9 -0
- package/dist/lib/bot/index.js +6 -0
- package/dist/lib/bot/protocol.d.ts +12 -0
- package/dist/lib/bot/protocol.js +74 -0
- package/dist/lib/bot/reporter.d.ts +3 -0
- package/dist/lib/bot/reporter.js +27 -0
- package/dist/lib/bot/skills.d.ts +26 -0
- package/dist/lib/bot/skills.js +69 -0
- package/dist/lib/budget/projections.d.ts +115 -0
- package/dist/lib/budget/projections.js +384 -0
- package/dist/lib/budget/scenarios.d.ts +93 -0
- package/dist/lib/budget/scenarios.js +214 -0
- package/dist/lib/cms/publish-blog.d.ts +62 -0
- package/dist/lib/cms/publish-blog.js +74 -0
- package/dist/lib/cms/strapi-client.d.ts +123 -0
- package/dist/lib/cms/strapi-client.js +213 -0
- package/dist/lib/config/registry.d.ts +17 -0
- package/dist/lib/config/registry.js +182 -0
- package/dist/lib/config/schema.d.ts +31 -0
- package/dist/lib/config/schema.js +25 -0
- package/dist/lib/config.d.ts +55 -0
- package/dist/lib/config.js +206 -0
- package/dist/lib/errors.d.ts +25 -0
- package/dist/lib/errors.js +91 -0
- package/dist/lib/format.d.ts +28 -0
- package/dist/lib/format.js +98 -0
- package/dist/lib/infra/deploy.d.ts +29 -0
- package/dist/lib/infra/deploy.js +58 -0
- package/dist/lib/infra/migrate.d.ts +34 -0
- package/dist/lib/infra/migrate.js +103 -0
- package/dist/lib/newsletter/distribute.d.ts +52 -0
- package/dist/lib/newsletter/distribute.js +193 -0
- package/{lib/newsletter/generate-insurance.ts → dist/lib/newsletter/generate-insurance.d.ts} +7 -24
- package/dist/lib/newsletter/generate-insurance.js +36 -0
- package/dist/lib/newsletter/generate.d.ts +104 -0
- package/dist/lib/newsletter/generate.js +571 -0
- package/dist/lib/returnpro/anomalies.d.ts +64 -0
- package/dist/lib/returnpro/anomalies.js +166 -0
- package/dist/lib/returnpro/audit.d.ts +32 -0
- package/dist/lib/returnpro/audit.js +147 -0
- package/dist/lib/returnpro/diagnose.d.ts +52 -0
- package/dist/lib/returnpro/diagnose.js +281 -0
- package/dist/lib/returnpro/kpis.d.ts +32 -0
- package/dist/lib/returnpro/kpis.js +192 -0
- package/dist/lib/returnpro/templates.d.ts +48 -0
- package/dist/lib/returnpro/templates.js +229 -0
- package/dist/lib/returnpro/upload-income.d.ts +25 -0
- package/dist/lib/returnpro/upload-income.js +235 -0
- package/dist/lib/returnpro/upload-netsuite.d.ts +37 -0
- package/dist/lib/returnpro/upload-netsuite.js +566 -0
- package/dist/lib/returnpro/upload-r1.d.ts +48 -0
- package/dist/lib/returnpro/upload-r1.js +398 -0
- package/dist/lib/returnpro/validate.d.ts +37 -0
- package/dist/lib/returnpro/validate.js +124 -0
- package/dist/lib/social/meta.d.ts +90 -0
- package/dist/lib/social/meta.js +160 -0
- package/dist/lib/social/post-generator.d.ts +83 -0
- package/dist/lib/social/post-generator.js +333 -0
- package/dist/lib/social/publish.d.ts +66 -0
- package/dist/lib/social/publish.js +226 -0
- package/dist/lib/social/scraper.d.ts +67 -0
- package/dist/lib/social/scraper.js +361 -0
- package/dist/lib/supabase.d.ts +4 -0
- package/dist/lib/supabase.js +20 -0
- package/dist/lib/transactions/delete-batch.d.ts +60 -0
- package/dist/lib/transactions/delete-batch.js +203 -0
- package/dist/lib/transactions/ingest.d.ts +43 -0
- package/dist/lib/transactions/ingest.js +555 -0
- package/dist/lib/transactions/stamp.d.ts +51 -0
- package/dist/lib/transactions/stamp.js +524 -0
- package/package.json +3 -4
- package/bin/optimal.ts +0 -1731
- package/lib/assets/index.ts +0 -225
- package/lib/assets.ts +0 -124
- package/lib/auth/index.ts +0 -189
- package/lib/board/index.ts +0 -309
- package/lib/board/types.ts +0 -124
- package/lib/bot/claim.ts +0 -43
- package/lib/bot/coordinator.ts +0 -254
- package/lib/bot/heartbeat.ts +0 -37
- package/lib/bot/index.ts +0 -9
- package/lib/bot/protocol.ts +0 -99
- package/lib/bot/reporter.ts +0 -42
- package/lib/bot/skills.ts +0 -81
- package/lib/budget/projections.ts +0 -561
- package/lib/budget/scenarios.ts +0 -312
- package/lib/cms/publish-blog.ts +0 -129
- package/lib/cms/strapi-client.ts +0 -302
- package/lib/config/registry.ts +0 -228
- package/lib/config/schema.ts +0 -58
- package/lib/config.ts +0 -247
- package/lib/errors.ts +0 -129
- package/lib/format.ts +0 -120
- package/lib/infra/.gitkeep +0 -0
- package/lib/infra/deploy.ts +0 -70
- package/lib/infra/migrate.ts +0 -141
- package/lib/newsletter/.gitkeep +0 -0
- package/lib/newsletter/distribute.ts +0 -256
- package/lib/newsletter/generate.ts +0 -735
- package/lib/returnpro/.gitkeep +0 -0
- package/lib/returnpro/anomalies.ts +0 -258
- package/lib/returnpro/audit.ts +0 -194
- package/lib/returnpro/diagnose.ts +0 -400
- package/lib/returnpro/kpis.ts +0 -255
- package/lib/returnpro/templates.ts +0 -323
- package/lib/returnpro/upload-income.ts +0 -311
- package/lib/returnpro/upload-netsuite.ts +0 -696
- package/lib/returnpro/upload-r1.ts +0 -563
- package/lib/returnpro/validate.ts +0 -154
- package/lib/social/meta.ts +0 -228
- package/lib/social/post-generator.ts +0 -468
- package/lib/social/publish.ts +0 -301
- package/lib/social/scraper.ts +0 -503
- package/lib/supabase.ts +0 -25
- package/lib/transactions/delete-batch.ts +0 -258
- package/lib/transactions/ingest.ts +0 -659
- package/lib/transactions/stamp.ts +0 -654
package/lib/returnpro/.gitkeep
DELETED
|
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
|
-
}
|
package/lib/returnpro/audit.ts
DELETED
|
@@ -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
|
-
}
|