2026-07-03 18:28:12 -05:00
|
|
|
'use strict';
|
|
|
|
|
|
|
|
|
|
// X2 — analyticsService's month-window math (which drives the expected-vs-actual
|
|
|
|
|
// reconciliation, the QA-B5-03 area) and query validation had no unit tests.
|
|
|
|
|
// These pin the pure helpers with hand-calculated examples, including the
|
|
|
|
|
// year-boundary and leap-year edges that off-by-one bugs hide in.
|
|
|
|
|
const test = require('node:test');
|
|
|
|
|
const assert = require('node:assert/strict');
|
|
|
|
|
|
|
|
|
|
const {
|
|
|
|
|
monthKey, monthLabel, addMonths, monthEndDate, buildMonths, validateSummaryQuery,
|
refactor(server): migrate 10 services to TypeScript (.cts)
Continues the incremental server TS migration (after utils/*.mts and
paymentValidation.cts): converts 10 services to .cts (CommonJS TypeScript,
run natively via Node type-stripping — no build step), type-checked by
tsconfig.server.json.
Migrated: totpService, amountSuggestionService, encryptionService,
paymentAccountingService, statusService, aprService, driftService,
analyticsService, spendingService, billMerchantRuleService.
- types/db.d.ts: shared minimal better-sqlite3 Db/Statement types (the lib
ships none), reused across the migrated modules.
- Every require of a migrated service updated to the explicit .cts extension
(extensionless require does not resolve .cts) across routes / services /
db migrations / server.js / workers / tests — require-only changes.
- package.json: check:server find pattern now includes *.cts (it silently
skipped .cts before, so paymentValidation.cts had no node --check gate).
Behavior-preserving (types only). Verified: typecheck:server 0,
check:server 0, full server suite 226/226, real boot → /api/health 200.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-06 10:35:29 -05:00
|
|
|
} = require('../services/analyticsService.cts');
|
2026-07-03 18:28:12 -05:00
|
|
|
|
|
|
|
|
test('monthKey zero-pads the month', () => {
|
|
|
|
|
assert.equal(monthKey(2026, 6), '2026-06');
|
|
|
|
|
assert.equal(monthKey(2026, 12), '2026-12');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('monthLabel is a UTC short-month + 2-digit year (no TZ drift)', () => {
|
|
|
|
|
assert.equal(monthLabel(2026, 1), 'Jan 26');
|
|
|
|
|
assert.equal(monthLabel(2026, 6), 'Jun 26');
|
|
|
|
|
assert.equal(monthLabel(2025, 12), 'Dec 25');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('addMonths crosses year boundaries both ways', () => {
|
|
|
|
|
assert.deepEqual(addMonths(2026, 1, -1), { year: 2025, month: 12 });
|
|
|
|
|
assert.deepEqual(addMonths(2026, 12, 1), { year: 2027, month: 1 });
|
|
|
|
|
assert.deepEqual(addMonths(2026, 6, 0), { year: 2026, month: 6 });
|
|
|
|
|
assert.deepEqual(addMonths(2026, 3, -6), { year: 2025, month: 9 });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('monthEndDate handles 30/31-day months and leap Februaries', () => {
|
|
|
|
|
assert.equal(monthEndDate(2026, 6), '2026-06-30');
|
|
|
|
|
assert.equal(monthEndDate(2026, 7), '2026-07-31');
|
|
|
|
|
assert.equal(monthEndDate(2026, 2), '2026-02-28'); // not a leap year
|
|
|
|
|
assert.equal(monthEndDate(2024, 2), '2024-02-29'); // leap year
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('buildMonths yields `count` contiguous months ending at (endYear, endMonth)', () => {
|
|
|
|
|
const months = buildMonths(2026, 3, 3); // Jan, Feb, Mar 2026
|
|
|
|
|
assert.equal(months.length, 3);
|
|
|
|
|
assert.deepEqual(months.map(m => m.key), ['2026-01', '2026-02', '2026-03']);
|
|
|
|
|
assert.equal(months[0].start, '2026-01-01');
|
|
|
|
|
assert.equal(months[2].end, '2026-03-31');
|
|
|
|
|
assert.equal(months[2].label, 'Mar 26');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('buildMonths spanning a year boundary', () => {
|
|
|
|
|
const months = buildMonths(2026, 1, 3); // Nov 25, Dec 25, Jan 26
|
|
|
|
|
assert.deepEqual(months.map(m => m.key), ['2025-11', '2025-12', '2026-01']);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('validateSummaryQuery: defaults, parsing, and range errors', () => {
|
|
|
|
|
const ok = validateSummaryQuery({ year: '2026', month: '6', months: '12' });
|
|
|
|
|
assert.equal(ok.error, undefined);
|
|
|
|
|
assert.equal(ok.year, 2026);
|
|
|
|
|
assert.equal(ok.month, 6);
|
|
|
|
|
assert.equal(ok.months, 12);
|
|
|
|
|
assert.equal(ok.includeSkipped, true, 'skipped included unless explicitly false');
|
|
|
|
|
|
|
|
|
|
assert.match(validateSummaryQuery({ month: '13' }, new Date('2026-06-15')).error, /month/);
|
|
|
|
|
assert.match(validateSummaryQuery({ months: '37' }, new Date('2026-06-15')).error, /months/);
|
|
|
|
|
assert.match(validateSummaryQuery({ year: '1999' }, new Date('2026-06-15')).error, /year/);
|
|
|
|
|
assert.match(validateSummaryQuery({ category_id: '-1' }, new Date('2026-06-15')).error, /category_id/);
|
|
|
|
|
|
|
|
|
|
// include_skipped=false flips the flag
|
|
|
|
|
assert.equal(validateSummaryQuery({ include_skipped: 'false' }, new Date('2026-06-15')).includeSkipped, false);
|
|
|
|
|
});
|