refactor(types): type-check batch 3 — 4 large routes (bills/snowball/aboutAdmin/summary)

~141 errors, mostly param/callback annotations. Two real correctness
touch-ups the checker forced:
- bills.cts: the end-date guard was `if (ey != null)` but used `em` in
  arithmetic; tightened to `ey != null && em != null` (they're validated
  as a both-or-neither pair, so behavior-identical — but the guard now
  says what it means)
- aboutAdmin.cts: `dateB - dateA` on two Dates -> `.getTime()` (coerced
  fine at runtime; correct form now)

@ts-nocheck server count: 14 -> 10. Server suite 252/252.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
null 2026-07-11 07:03:23 -05:00
parent 6c915a62c8
commit 197fdd1733
4 changed files with 91 additions and 91 deletions

View File

@ -1,4 +1,3 @@
// @ts-nocheck — route controller converted to .cts; handler req/res typed, full type-check deferred (thin glue over typed services).
import type { Req, Res, Next } from '../types/http';
const express = require('express');
const { log } = require('../utils/logger.cts');
@ -9,7 +8,7 @@ const { ValidationError } = require('../utils/apiError.cts');
const router = express.Router();
let pkg;
let pkg: any;
try {
pkg = require('../package.json');
} catch {
@ -23,7 +22,7 @@ const ALLOWED_FILES = {
};
// Priority emoji to label mapping
const PRIORITY_MAP = {
const PRIORITY_MAP: Record<string, string> = {
'🔴': 'CRITICAL',
'🟠': 'HIGH',
'🟡': 'MEDIUM',
@ -34,7 +33,7 @@ const PRIORITY_MAP = {
/**
* Generate a slug from a title: lowercase, hyphens, strip emojis
*/
function slugify(title) {
function slugify(title: any) {
return title
.replace(
/[\u{1F300}-\u{1F9FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}\u{FE00}-\u{FE0F}\u{1F000}-\u{1FAFF}]/gu,
@ -49,7 +48,7 @@ function slugify(title) {
* Extract effort estimate from implementation notes text.
* Matches patterns like "Estimated effort: 3-4 hours", "Estimated effort: 8 hours"
*/
function extractEffort(text) {
function extractEffort(text: any) {
if (!text) return null;
const match = text.match(/Estimated effort:\s*(\d+(?:\s*-\s*\d+)?\s*hours?)/i);
if (!match) return null;
@ -61,10 +60,10 @@ function extractEffort(text) {
* Parse FUTURE.md into structured roadmap items.
* Filters out completed/strikethrough items and template/meta sections.
*/
function parseFutureMd(content) {
function parseFutureMd(content: any) {
if (!content) return { items: [], counts: {} };
const items = [];
const items: any[] = [];
const counts = { critical: 0, high: 0, medium: 0, low: 0, niceToHave: 0 };
const lines = content.split('\n');
@ -279,7 +278,7 @@ function parseFutureMd(content) {
/**
* Add a parsed item to the items array and update counts.
*/
function _addItem(items, counts, emoji, label, title, bodyLines) {
function _addItem(items: any, counts: any, emoji: any, label: any, title: any, bodyLines: any) {
const body = bodyLines.join('\n');
const description = _extractField(body, 'Description');
const rationale = _extractField(body, 'Rationale');
@ -297,6 +296,7 @@ function _addItem(items, counts, emoji, label, title, bodyLines) {
// Map priority label to count key
const countKey =
(
{
CRITICAL: 'critical',
HIGH: 'high',
@ -305,7 +305,8 @@ function _addItem(items, counts, emoji, label, title, bodyLines) {
'NICE TO HAVE': 'niceToHave',
NICE_TO_HAVE: 'niceToHave',
MEH: 'niceToHave',
}[label] || 'medium';
} as Record<string, string>
)[label] || 'medium';
counts[countKey]++;
items.push({
@ -327,7 +328,7 @@ function _addItem(items, counts, emoji, label, title, bodyLines) {
* Extract a named field from markdown body text.
* Looks for **Field Name:** and captures everything until the next ** field or ### heading or end.
*/
function _extractField(body, fieldName) {
function _extractField(body: any, fieldName: any) {
// Match **FieldName:** followed by content until next ** or ### heading
const regex = new RegExp(
`\\*\\*${fieldName}:\\*\\*\\s*\n([\\s\\S]*?)(?=\\n\\*\\*[^*]|\\n###|$)`,
@ -342,7 +343,7 @@ function _extractField(body, fieldName) {
* Parse DEVELOPMENT_LOG.md into structured log entries.
* Returns entries sorted by date descending.
*/
function parseDevLogMd(content) {
function parseDevLogMd(content: any) {
if (!content) return [];
const entries = [];
@ -367,10 +368,10 @@ function parseDevLogMd(content) {
}
// Sort by date descending
entries.sort((a, b) => {
entries.sort((a: any, b: any) => {
const dateA = a.date ? new Date(a.date) : new Date(0);
const dateB = b.date ? new Date(b.date) : new Date(0);
return dateB - dateA;
return dateB.getTime() - dateA.getTime();
});
return entries;
@ -379,7 +380,7 @@ function parseDevLogMd(content) {
/**
* Parse a single dev log entry block.
*/
function _parseDevLogEntry(block, version, title) {
function _parseDevLogEntry(block: any, version: any, title: any) {
// Status
const statusMatch = block.match(/\*\*Status:\*\*\s*(.+)/);
const status = statusMatch ? statusMatch[1].trim() : null;
@ -423,7 +424,7 @@ function _parseDevLogEntry(block, version, title) {
const filesModified = filesMatch
? filesMatch[1]
.split(',')
.map((f) => f.trim().replace(/^`|`$/g, ''))
.map((f: any) => f.trim().replace(/^`|`$/g, ''))
.filter(Boolean)
: [];
@ -433,7 +434,7 @@ function _parseDevLogEntry(block, version, title) {
if (workMatch) {
const items = workMatch[1].match(/- \[[ x]\] .+/g);
if (items) {
workCompleted.push(...items.map((item) => item.replace(/^- \[[ x]\]\s*/, '').trim()));
workCompleted.push(...items.map((item: any) => item.replace(/^- \[[ x]\]\s*/, '').trim()));
}
}
@ -454,7 +455,7 @@ function _parseDevLogEntry(block, version, title) {
* @param {string} content - The content to redact
* @returns {string} - The redacted content
*/
function redactSensitiveContent(content) {
function redactSensitiveContent(content: any) {
if (!content) return content;
return (
@ -493,7 +494,7 @@ function redactSensitiveContent(content) {
// ── Forgejo issues cache ──────────────────────────────────────────────────────
const FORGEJO_BASE = 'https://dream.scheller.ltd/api/v1/repos/null/BillTracker';
let _forgejoCache = null;
let _forgejoCache: any = null;
let _forgejoCacheTs = 0;
const FORGEJO_TTL_MS = 5 * 60 * 1000;
@ -524,7 +525,7 @@ router.get('/', requireAuth, requireAdmin, (req: Req, res: Res) => {
future: sanitizedFutureContent,
developmentLog: sanitizedDevLogContent,
});
} catch (err) {
} catch (err: any) {
// Generic error message to prevent path disclosure
log.error('[aboutAdmin] Error reading files');
res.status(500).json({
@ -546,7 +547,7 @@ router.get('/roadmap', requireAuth, requireAdmin, async (req: Req, res: Res) =>
_forgejoCache = { issues, fetchedAt: new Date().toISOString() };
_forgejoCacheTs = now;
res.json(_forgejoCache);
} catch (err) {
} catch (err: any) {
log.error('[aboutAdmin] Forgejo issues error:', err.message);
if (_forgejoCache) return res.json({ ..._forgejoCache, stale: true });
res
@ -562,7 +563,7 @@ router.get('/dev-log', requireAuth, requireAdmin, (req: Req, res: Res) => {
const sanitized = redactSensitiveContent(devLogContent);
const entries = parseDevLogMd(sanitized);
res.json({ entries, version: pkg.version });
} catch (err) {
} catch (err: any) {
log.error('[aboutAdmin] Error reading DEVELOPMENT_LOG.md for dev-log');
res.status(500).json({
error: 'Failed to read dev log data',
@ -578,7 +579,7 @@ router.post('/check-updates', requireAuth, requireAdmin, async (req: Req, res: R
try {
const result = await checkForUpdates(true);
res.json(result);
} catch (err) {
} catch (err: any) {
res.status(500).json({ error: 'Update check failed' });
}
});

View File

@ -1,4 +1,3 @@
// @ts-nocheck — route controller converted to .cts; handler req/res typed, full type-check deferred (thin glue over typed services).
import type { Req, Res, Next } from '../types/http';
const express = require('express');
const { log } = require('../utils/logger.cts');
@ -87,7 +86,7 @@ router.get('/deleted', (req: Req, res: Res) => {
)
.all(req.user.id);
res.json(
rows.map((row) => ({
rows.map((row: any) => ({
...serializeBill(row),
category_name: row.category_name,
deleted_at: row.deleted_at,
@ -137,7 +136,7 @@ router.put('/reorder', (req: Req, res: Res) => {
const update = db.prepare(
"UPDATE bills SET sort_order = ?, updated_at = datetime('now') WHERE id = ? AND user_id = ?",
);
const applyOrder = db.transaction((items) => {
const applyOrder = db.transaction((items: any) => {
for (const item of items) update.run(item.sortOrder, item.billId, req.user.id);
});
applyOrder(entries);
@ -208,7 +207,7 @@ router.post('/:id/snooze-drift', (req: Req, res: Res) => {
// Bill templates store money fields (expected_amount, current_balance, minimum_payment)
// in integer cents, matching validateBillData's normalized output. Convert back to
// dollars for API responses, mirroring serializeBill.
function serializeTemplateData(data) {
function serializeTemplateData(data: any) {
if (!data) return data;
const out = { ...data };
if (out.expected_amount != null) out.expected_amount = fromCents(out.expected_amount);
@ -232,7 +231,7 @@ router.get('/templates', (req: Req, res: Res) => {
.all(req.user.id);
res.json(
rows.map((row) => ({
rows.map((row: any) => ({
...row,
data: serializeTemplateData(parseTemplateData(row.data)),
})),
@ -812,7 +811,7 @@ router.get('/:id/transactions', (req: Req, res: Res) => {
)
.all(billId, req.user.id, billId);
const transactions = rows.map((row) =>
const transactions = rows.map((row: any) =>
decorateTransaction({
...row,
linked_payment: row.linked_payment_id
@ -1020,7 +1019,7 @@ router.post('/:id/history-ranges', (req: Req, res: Res) => {
'end_year',
);
}
if (ey != null) {
if (ey != null && em != null) {
const startVal = sy * 12 + sm;
const endVal = ey * 12 + em;
if (endVal < startVal)
@ -1174,7 +1173,7 @@ router.get('/:id/amortization', (req: Req, res: Res) => {
current_balance: balance,
minimum_payment: minPmt,
});
const total_interest = schedule.reduce((s, r) => s + r.interest, 0);
const total_interest = schedule.reduce((s: number, r: any) => s + r.interest, 0);
res.json({
bill_id: billId,
@ -1185,7 +1184,7 @@ router.get('/:id/amortization', (req: Req, res: Res) => {
summary: {
months: schedule.length,
total_interest: roundMoney(total_interest),
total_paid: sumMoney(schedule, (r) => r.payment),
total_paid: sumMoney(schedule, (r: any) => r.payment),
capped: schedule.length >= maxMonths && schedule[schedule.length - 1]?.balance > 0,
},
apr_snapshot,
@ -1258,14 +1257,14 @@ router.patch('/:id/balance', (req: Req, res: Res) => {
// ── Merchant rule helpers ─────────────────────────────────────────────────────
function requireBill(db, billId, userId) {
function requireBill(db: any, billId: any, userId: any) {
return db
.prepare('SELECT id, name FROM bills WHERE id = ? AND user_id = ? AND deleted_at IS NULL')
.get(billId, userId);
}
// Count unmatched transactions that would match a normalized merchant string.
function previewMatchCount(db, userId, normalized) {
function previewMatchCount(db: any, userId: any, normalized: any) {
if (!normalized || normalized.length < 2) return 0;
const txRows = db
.prepare(
@ -1281,14 +1280,14 @@ function previewMatchCount(db, userId, normalized) {
`,
)
.all(userId);
return txRows.filter((tx) => {
return txRows.filter((tx: any) => {
const txMerchant = normalizeMerchant(tx.payee || tx.description || tx.memo || '');
return txMerchant && merchantMatches(txMerchant, normalized);
}).length;
}
// Find bills (other than this one) that already claim this merchant.
function findConflicts(db, userId, billId, normalized) {
function findConflicts(db: any, userId: any, billId: any, normalized: any) {
return db
.prepare(
`
@ -1336,7 +1335,7 @@ router.get('/:id/merchant-rules', (req: Req, res: Res) => {
`,
)
.all(req.user.id)
.map((tx) => {
.map((tx: any) => {
const raw = tx.payee || tx.description || tx.memo || '';
const normalized = normalizeMerchant(raw);
return {
@ -1347,7 +1346,7 @@ router.get('/:id/merchant-rules', (req: Req, res: Res) => {
date: tx.posted_date || String(tx.transacted_at || '').slice(0, 10),
};
})
.filter((s) => s.normalized.length >= 2);
.filter((s: any) => s.normalized.length >= 2);
res.json({ rules, suggestions });
});
@ -1422,7 +1421,7 @@ router.get('/:id/merchant-rules/candidates', (req: Req, res: Res) => {
const rules = db
.prepare('SELECT merchant FROM bill_merchant_rules WHERE user_id = ? AND bill_id = ?')
.all(req.user.id, billId)
.map((r) => r.merchant);
.map((r: any) => r.merchant);
if (rules.length === 0) return res.json({ candidates: [] });
@ -1455,14 +1454,14 @@ router.get('/:id/merchant-rules/candidates', (req: Req, res: Res) => {
'SELECT transaction_id FROM payments WHERE bill_id = ? AND transaction_id IS NOT NULL AND deleted_at IS NULL',
)
.all(billId)
.map((r) => r.transaction_id),
.map((r: any) => r.transaction_id),
);
const candidates = [];
for (const tx of txRows) {
const txMerchant = normalizeMerchant(tx.payee || tx.description || tx.memo || '');
if (!txMerchant) continue;
const matches = rules.some((r) => merchantMatches(txMerchant, r));
const matches = rules.some((r: any) => merchantMatches(txMerchant, r));
if (!matches) continue;
const paidDate =
@ -1523,7 +1522,7 @@ router.post('/:id/merchant-rules/import-historical', (req: Req, res: Res) => {
`);
let imported = 0;
const lateAttributions = [];
const lateAttributions: any[] = [];
try {
db.transaction(() => {
@ -1578,7 +1577,7 @@ router.post('/:id/merchant-rules/import-historical', (req: Req, res: Res) => {
}
}
})();
} catch (err) {
} catch (err: any) {
log.error('[import-historical] Transaction failed:', err.message);
throw new ApiError('DB_ERROR', 'Import failed', 500);
}

View File

@ -1,4 +1,3 @@
// @ts-nocheck — route controller converted to .cts; handler req/res typed, full type-check deferred (thin glue over typed services).
import type { Req, Res, Next } from '../types/http';
const express = require('express');
const router = express.Router();
@ -23,7 +22,7 @@ const DEBT_LIKE_CLAUSES = `(
)
)`;
function isRamseyMode(userId) {
function isRamseyMode(userId: any) {
const db = getDb();
const row = db
.prepare(
@ -37,7 +36,7 @@ function isRamseyMode(userId) {
return row ? row.value !== 'false' && row.value !== '0' : true;
}
function getUserBoolSetting(userId, key, fallback = false) {
function getUserBoolSetting(userId: any, key: string, fallback = false) {
const db = getDb();
const row = db
.prepare(
@ -52,7 +51,7 @@ function getUserBoolSetting(userId, key, fallback = false) {
return row.value === 'true' || row.value === '1';
}
function upsertUserSetting(db, userId, key, value) {
function upsertUserSetting(db: any, userId: any, key: string, value: any) {
db.prepare(
`
INSERT INTO user_settings (user_id, key, value, updated_at)
@ -64,7 +63,7 @@ function upsertUserSetting(db, userId, key, value) {
).run(userId, key, String(value));
}
function getDebtQuery(ramseyMode) {
function getDebtQuery(ramseyMode: any) {
const orderBy = ramseyMode
? `
CASE WHEN b.current_balance IS NULL THEN 1 ELSE 0 END ASC,
@ -89,7 +88,7 @@ function getDebtQuery(ramseyMode) {
`;
}
function getDebtBills(userId, ramseyMode) {
function getDebtBills(userId: any, ramseyMode?: any) {
const db = getDb();
const mode = ramseyMode !== undefined ? ramseyMode : isRamseyMode(userId);
return db.prepare(getDebtQuery(mode)).all(userId);
@ -179,7 +178,7 @@ router.get('/projection', (req: Req, res: Res) => {
// Money fields on `bills` are stored as integer cents; the snowball/APR math
// and the API response are dollar-denominated, so convert before computing.
const billsForMath = bills.map((b) => ({
const billsForMath = bills.map((b: any) => ({
...b,
current_balance: fromCents(b.current_balance),
minimum_payment: fromCents(b.minimum_payment),
@ -194,17 +193,17 @@ router.get('/projection', (req: Req, res: Res) => {
: fromCents(user?.snowball_extra_payment ?? 0);
// Build a lookup of APR snapshots keyed by bill id (computed once from current balances)
const aprByBill = {};
const aprByBill: Record<string, any> = {};
for (const b of billsForMath) {
const snap = debtAprSnapshot(b);
if (snap) aprByBill[b.id] = snap;
}
// Enrich each debt result with its APR snapshot
function enrich(projection) {
function enrich(projection: any) {
return {
...projection,
debts: projection.debts.map((d) => ({
debts: projection.debts.map((d: any) => ({
...d,
apr_snapshot: aprByBill[d.id] ?? null,
})),
@ -223,7 +222,7 @@ router.get('/projection', (req: Req, res: Res) => {
});
// Build a summary comparing snowball to the minimum-only baseline
function buildComparison(snowball, minimum_only) {
function buildComparison(snowball: any, minimum_only: any) {
const sbMonths = snowball.months_to_freedom;
const moMonths = minimum_only.months_to_freedom;
const sbInterest = snowball.total_interest_paid;
@ -259,7 +258,7 @@ router.patch('/order', (req: Req, res: Res) => {
}
// Validate every row before touching the DB — no silent skips
const parsed = [];
const parsed: any[] = [];
for (let i = 0; i < items.length; i++) {
const row = items[i];
const id = parseInt(row?.id, 10);
@ -292,7 +291,7 @@ router.patch('/order', (req: Req, res: Res) => {
// ── Snowball Plan helpers ─────────────────────────────────────────────────────
function enrichPlanWithProgress(db, plan) {
function enrichPlanWithProgress(db: any, plan: any) {
let snapshot;
try {
snapshot = JSON.parse(plan.plan_snapshot);
@ -302,8 +301,8 @@ function enrichPlanWithProgress(db, plan) {
// Fetch every snapshot bill in one query (user-scoped), not one-per-debt.
const snapDebts = snapshot?.debts ?? [];
const billIds = [...new Set(snapDebts.map((d) => d.bill_id).filter(Boolean))];
const billsById = {};
const billIds = [...new Set(snapDebts.map((d: any) => d.bill_id).filter(Boolean))];
const billsById: Record<string, any> = {};
if (billIds.length) {
const placeholders = billIds.map(() => '?').join(',');
for (const b of db
@ -315,7 +314,7 @@ function enrichPlanWithProgress(db, plan) {
}
}
const currentDebts = snapDebts.map((d) => {
const currentDebts = snapDebts.map((d: any) => {
const bill = billsById[d.bill_id];
const currentBalance = bill && !bill.deleted_at ? fromCents(bill.current_balance) : null;
const startingBalance = d.starting_balance ?? 0;
@ -360,7 +359,7 @@ router.post('/plans', (req: Req, res: Res) => {
const ramseyMode = isRamseyMode(userId);
const debts = getDebtBills(userId, ramseyMode);
const activeDebts = debts.filter((b) => (b.current_balance ?? 0) > 0);
const activeDebts = debts.filter((b: any) => (b.current_balance ?? 0) > 0);
if (activeDebts.length === 0) {
throw ValidationError(
'No debts with a balance found. Add a balance to at least one bill.',
@ -371,7 +370,7 @@ router.post('/plans', (req: Req, res: Res) => {
// Money fields on `debts` are stored as integer cents; the snowball/APR
// math and plan_snapshot are dollar-denominated, so convert before computing.
const debtsForMath = debts.map((b) => ({
const debtsForMath = debts.map((b: any) => ({
...b,
current_balance: fromCents(b.current_balance),
minimum_payment: fromCents(b.minimum_payment),
@ -393,8 +392,8 @@ router.post('/plans', (req: Req, res: Res) => {
100,
);
const debtSnaps = debtsForMath.map((b, i) => {
const proj = snowball.debts?.find((d) => d.id === b.id);
const debtSnaps = debtsForMath.map((b: any, i: number) => {
const proj = snowball.debts?.find((d: any) => d.id === b.id);
return {
bill_id: b.id,
name: b.name,
@ -451,7 +450,7 @@ router.get('/plans', (req: Req, res: Res) => {
`,
)
.all(req.user.id);
res.json({ plans: plans.map((p) => enrichPlanWithProgress(db, p)) });
res.json({ plans: plans.map((p: any) => enrichPlanWithProgress(db, p)) });
});
// GET /api/snowball/plans/active — return the active or paused plan (or null)
@ -495,7 +494,7 @@ router.patch('/plans/:id', (req: Req, res: Res) => {
// Shared status-transition handler for pause / resume / complete / abandon.
// `setSql` is a fixed constant per route (never user input).
function transitionPlan(req, res, { allowedFrom, setSql, past }) {
function transitionPlan(req: Req, res: Res, { allowedFrom, setSql, past }: any) {
const db = getDb();
const id = parseInt(req.params.id, 10);
if (!Number.isInteger(id) || id <= 0) {

View File

@ -1,4 +1,3 @@
// @ts-nocheck — route controller converted to .cts; handler req/res typed, full type-check deferred (thin glue over typed services).
import type { Req, Res, Next } from '../types/http';
const express = require('express');
const { log } = require('../utils/logger.cts');
@ -15,7 +14,7 @@ const DEFAULT_PENDING_DAYS = 3;
// Build bank tracking summary when the user has enabled SimpleFIN bank tracking.
// Returns null when disabled, the account isn't found, or bank sync isn't set up.
function buildBankTrackingSummary(db, userId, year, month) {
function buildBankTrackingSummary(db: any, userId: any, year: any, month: any) {
try {
const settings = getUserSettings(userId);
if (settings.bank_tracking_enabled !== 'true') return null;
@ -92,8 +91,8 @@ function buildBankTrackingSummary(db, userId, year, month) {
)
.all(year, month, start, end, userId);
const unpaidTotalCents = unpaidCandidates
.filter((row) => resolveDueDate(row, year, month))
.reduce((sum, row) => sum + (row.amount_cents || 0), 0);
.filter((row: any) => resolveDueDate(row, year, month))
.reduce((sum: number, row: any) => sum + (row.amount_cents || 0), 0);
const balanceDollars = money(account.balance / 100);
const pendingDollars = fromCents(pendingRow.pending_total);
@ -116,13 +115,13 @@ function buildBankTrackingSummary(db, userId, year, month) {
unpaid_this_month: unpaidDollars,
remaining: money(effectiveDollars - unpaidDollars),
};
} catch (err) {
} catch (err: any) {
log.error('[buildBankTrackingSummary] Error:', err.message);
return null;
}
}
function parseYearMonth(source) {
function parseYearMonth(source: any) {
const now = new Date();
const year = parseInt(source.year || now.getFullYear(), 10);
const month = parseInt(source.month || now.getMonth() + 1, 10);
@ -137,12 +136,12 @@ function parseYearMonth(source) {
return { year, month };
}
function money(value) {
function money(value: any) {
const n = Number(value);
return Number.isFinite(n) ? Math.round(n * 100) / 100 : 0;
}
function getStartingAmounts(db, userId, year, month) {
function getStartingAmounts(db: any, userId: any, year: any, month: any) {
const row = db
.prepare(
`
@ -160,7 +159,7 @@ function getStartingAmounts(db, userId, year, month) {
};
}
function calculatePaidDeductions(db, userId, year, month) {
function calculatePaidDeductions(db: any, userId: any, year: any, month: any) {
const { start, end } = getCycleRange(year, month);
// Paid from first bucket: bills with due_day 1-14
@ -237,7 +236,7 @@ function calculatePaidDeductions(db, userId, year, month) {
};
}
function buildStartingAmountsSummary(db, userId, year, month) {
function buildStartingAmountsSummary(db: any, userId: any, year: any, month: any) {
const amounts = getStartingAmounts(db, userId, year, month);
const paid = calculatePaidDeductions(db, userId, year, month);
@ -264,7 +263,7 @@ function buildStartingAmountsSummary(db, userId, year, month) {
};
}
function getIncome(db, userId, year, month) {
function getIncome(db: any, userId: any, year: any, month: any) {
const row = db
.prepare(
`
@ -282,7 +281,7 @@ function getIncome(db, userId, year, month) {
};
}
function buildSummary(db, userId, year, month) {
function buildSummary(db: any, userId: any, year: any, month: any) {
const income = getIncome(db, userId, year, month);
const { start, end } = getCycleRange(year, month);
@ -321,9 +320,9 @@ function buildSummary(db, userId, year, month) {
// QA-B5-01: only bills that actually occur in this month (matches the Tracker's
// resolveDueDate gating) — otherwise annual / off-month quarterly bills inflate
// the monthly expense list and total, disagreeing with the Tracker.
.filter((row) => resolveDueDate(row, year, month));
.filter((row: any) => resolveDueDate(row, year, month));
const billIds = billRows.map((row) => row.bill_id);
const billIds = billRows.map((row: any) => row.bill_id);
const paymentMap = new Map();
if (billIds.length > 0) {
@ -353,7 +352,7 @@ function buildSummary(db, userId, year, month) {
}
}
const expenses = billRows.map((row) => {
const expenses = billRows.map((row: any) => {
const payment = paymentMap.get(row.bill_id) || { payment_count: 0, paid_amount: 0 };
const hasActual = row.actual_amount !== null && row.actual_amount !== undefined;
const displayAmount = fromCents(hasActual ? row.actual_amount : row.expected_amount);
@ -378,13 +377,15 @@ function buildSummary(db, userId, year, month) {
};
});
const countedExpenses = expenses.filter((expense) => !expense.is_skipped);
const countedExpenses = expenses.filter((expense: any) => !expense.is_skipped);
const incomeTotal = money(income.amount);
const expenseTotal = money(
countedExpenses.reduce((sum, expense) => sum + expense.display_amount, 0),
countedExpenses.reduce((sum: number, expense: any) => sum + expense.display_amount, 0),
);
const paidTotal = money(countedExpenses.reduce((sum, expense) => sum + expense.paid_amount, 0));
const paidExpenseCount = countedExpenses.filter((expense) => expense.is_paid).length;
const paidTotal = money(
countedExpenses.reduce((sum: number, expense: any) => sum + expense.paid_amount, 0),
);
const paidExpenseCount = countedExpenses.filter((expense: any) => expense.is_paid).length;
const starting_amounts = buildStartingAmountsSummary(db, userId, year, month);
const bank_tracking = buildBankTrackingSummary(db, userId, year, month);
// When bank tracking is on, drive the "plan base" from the effective bank balance