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:
parent
6c915a62c8
commit
197fdd1733
|
|
@ -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';
|
import type { Req, Res, Next } from '../types/http';
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const { log } = require('../utils/logger.cts');
|
const { log } = require('../utils/logger.cts');
|
||||||
|
|
@ -9,7 +8,7 @@ const { ValidationError } = require('../utils/apiError.cts');
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
let pkg;
|
let pkg: any;
|
||||||
try {
|
try {
|
||||||
pkg = require('../package.json');
|
pkg = require('../package.json');
|
||||||
} catch {
|
} catch {
|
||||||
|
|
@ -23,7 +22,7 @@ const ALLOWED_FILES = {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Priority emoji to label mapping
|
// Priority emoji to label mapping
|
||||||
const PRIORITY_MAP = {
|
const PRIORITY_MAP: Record<string, string> = {
|
||||||
'🔴': 'CRITICAL',
|
'🔴': 'CRITICAL',
|
||||||
'🟠': 'HIGH',
|
'🟠': 'HIGH',
|
||||||
'🟡': 'MEDIUM',
|
'🟡': 'MEDIUM',
|
||||||
|
|
@ -34,7 +33,7 @@ const PRIORITY_MAP = {
|
||||||
/**
|
/**
|
||||||
* Generate a slug from a title: lowercase, hyphens, strip emojis
|
* Generate a slug from a title: lowercase, hyphens, strip emojis
|
||||||
*/
|
*/
|
||||||
function slugify(title) {
|
function slugify(title: any) {
|
||||||
return title
|
return title
|
||||||
.replace(
|
.replace(
|
||||||
/[\u{1F300}-\u{1F9FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}\u{FE00}-\u{FE0F}\u{1F000}-\u{1FAFF}]/gu,
|
/[\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.
|
* Extract effort estimate from implementation notes text.
|
||||||
* Matches patterns like "Estimated effort: 3-4 hours", "Estimated effort: 8 hours"
|
* Matches patterns like "Estimated effort: 3-4 hours", "Estimated effort: 8 hours"
|
||||||
*/
|
*/
|
||||||
function extractEffort(text) {
|
function extractEffort(text: any) {
|
||||||
if (!text) return null;
|
if (!text) return null;
|
||||||
const match = text.match(/Estimated effort:\s*(\d+(?:\s*-\s*\d+)?\s*hours?)/i);
|
const match = text.match(/Estimated effort:\s*(\d+(?:\s*-\s*\d+)?\s*hours?)/i);
|
||||||
if (!match) return null;
|
if (!match) return null;
|
||||||
|
|
@ -61,10 +60,10 @@ function extractEffort(text) {
|
||||||
* Parse FUTURE.md into structured roadmap items.
|
* Parse FUTURE.md into structured roadmap items.
|
||||||
* Filters out completed/strikethrough items and template/meta sections.
|
* Filters out completed/strikethrough items and template/meta sections.
|
||||||
*/
|
*/
|
||||||
function parseFutureMd(content) {
|
function parseFutureMd(content: any) {
|
||||||
if (!content) return { items: [], counts: {} };
|
if (!content) return { items: [], counts: {} };
|
||||||
|
|
||||||
const items = [];
|
const items: any[] = [];
|
||||||
const counts = { critical: 0, high: 0, medium: 0, low: 0, niceToHave: 0 };
|
const counts = { critical: 0, high: 0, medium: 0, low: 0, niceToHave: 0 };
|
||||||
|
|
||||||
const lines = content.split('\n');
|
const lines = content.split('\n');
|
||||||
|
|
@ -279,7 +278,7 @@ function parseFutureMd(content) {
|
||||||
/**
|
/**
|
||||||
* Add a parsed item to the items array and update counts.
|
* 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 body = bodyLines.join('\n');
|
||||||
const description = _extractField(body, 'Description');
|
const description = _extractField(body, 'Description');
|
||||||
const rationale = _extractField(body, 'Rationale');
|
const rationale = _extractField(body, 'Rationale');
|
||||||
|
|
@ -297,15 +296,17 @@ function _addItem(items, counts, emoji, label, title, bodyLines) {
|
||||||
|
|
||||||
// Map priority label to count key
|
// Map priority label to count key
|
||||||
const countKey =
|
const countKey =
|
||||||
{
|
(
|
||||||
CRITICAL: 'critical',
|
{
|
||||||
HIGH: 'high',
|
CRITICAL: 'critical',
|
||||||
MEDIUM: 'medium',
|
HIGH: 'high',
|
||||||
LOW: 'low',
|
MEDIUM: 'medium',
|
||||||
'NICE TO HAVE': 'niceToHave',
|
LOW: 'low',
|
||||||
NICE_TO_HAVE: 'niceToHave',
|
'NICE TO HAVE': 'niceToHave',
|
||||||
MEH: 'niceToHave',
|
NICE_TO_HAVE: 'niceToHave',
|
||||||
}[label] || 'medium';
|
MEH: 'niceToHave',
|
||||||
|
} as Record<string, string>
|
||||||
|
)[label] || 'medium';
|
||||||
counts[countKey]++;
|
counts[countKey]++;
|
||||||
|
|
||||||
items.push({
|
items.push({
|
||||||
|
|
@ -327,7 +328,7 @@ function _addItem(items, counts, emoji, label, title, bodyLines) {
|
||||||
* Extract a named field from markdown body text.
|
* Extract a named field from markdown body text.
|
||||||
* Looks for **Field Name:** and captures everything until the next ** field or ### heading or end.
|
* 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
|
// Match **FieldName:** followed by content until next ** or ### heading
|
||||||
const regex = new RegExp(
|
const regex = new RegExp(
|
||||||
`\\*\\*${fieldName}:\\*\\*\\s*\n([\\s\\S]*?)(?=\\n\\*\\*[^*]|\\n###|$)`,
|
`\\*\\*${fieldName}:\\*\\*\\s*\n([\\s\\S]*?)(?=\\n\\*\\*[^*]|\\n###|$)`,
|
||||||
|
|
@ -342,7 +343,7 @@ function _extractField(body, fieldName) {
|
||||||
* Parse DEVELOPMENT_LOG.md into structured log entries.
|
* Parse DEVELOPMENT_LOG.md into structured log entries.
|
||||||
* Returns entries sorted by date descending.
|
* Returns entries sorted by date descending.
|
||||||
*/
|
*/
|
||||||
function parseDevLogMd(content) {
|
function parseDevLogMd(content: any) {
|
||||||
if (!content) return [];
|
if (!content) return [];
|
||||||
|
|
||||||
const entries = [];
|
const entries = [];
|
||||||
|
|
@ -367,10 +368,10 @@ function parseDevLogMd(content) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort by date descending
|
// 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 dateA = a.date ? new Date(a.date) : new Date(0);
|
||||||
const dateB = b.date ? new Date(b.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;
|
return entries;
|
||||||
|
|
@ -379,7 +380,7 @@ function parseDevLogMd(content) {
|
||||||
/**
|
/**
|
||||||
* Parse a single dev log entry block.
|
* Parse a single dev log entry block.
|
||||||
*/
|
*/
|
||||||
function _parseDevLogEntry(block, version, title) {
|
function _parseDevLogEntry(block: any, version: any, title: any) {
|
||||||
// Status
|
// Status
|
||||||
const statusMatch = block.match(/\*\*Status:\*\*\s*(.+)/);
|
const statusMatch = block.match(/\*\*Status:\*\*\s*(.+)/);
|
||||||
const status = statusMatch ? statusMatch[1].trim() : null;
|
const status = statusMatch ? statusMatch[1].trim() : null;
|
||||||
|
|
@ -423,7 +424,7 @@ function _parseDevLogEntry(block, version, title) {
|
||||||
const filesModified = filesMatch
|
const filesModified = filesMatch
|
||||||
? filesMatch[1]
|
? filesMatch[1]
|
||||||
.split(',')
|
.split(',')
|
||||||
.map((f) => f.trim().replace(/^`|`$/g, ''))
|
.map((f: any) => f.trim().replace(/^`|`$/g, ''))
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
|
|
@ -433,7 +434,7 @@ function _parseDevLogEntry(block, version, title) {
|
||||||
if (workMatch) {
|
if (workMatch) {
|
||||||
const items = workMatch[1].match(/- \[[ x]\] .+/g);
|
const items = workMatch[1].match(/- \[[ x]\] .+/g);
|
||||||
if (items) {
|
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
|
* @param {string} content - The content to redact
|
||||||
* @returns {string} - The redacted content
|
* @returns {string} - The redacted content
|
||||||
*/
|
*/
|
||||||
function redactSensitiveContent(content) {
|
function redactSensitiveContent(content: any) {
|
||||||
if (!content) return content;
|
if (!content) return content;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -493,7 +494,7 @@ function redactSensitiveContent(content) {
|
||||||
|
|
||||||
// ── Forgejo issues cache ──────────────────────────────────────────────────────
|
// ── Forgejo issues cache ──────────────────────────────────────────────────────
|
||||||
const FORGEJO_BASE = 'https://dream.scheller.ltd/api/v1/repos/null/BillTracker';
|
const FORGEJO_BASE = 'https://dream.scheller.ltd/api/v1/repos/null/BillTracker';
|
||||||
let _forgejoCache = null;
|
let _forgejoCache: any = null;
|
||||||
let _forgejoCacheTs = 0;
|
let _forgejoCacheTs = 0;
|
||||||
const FORGEJO_TTL_MS = 5 * 60 * 1000;
|
const FORGEJO_TTL_MS = 5 * 60 * 1000;
|
||||||
|
|
||||||
|
|
@ -524,7 +525,7 @@ router.get('/', requireAuth, requireAdmin, (req: Req, res: Res) => {
|
||||||
future: sanitizedFutureContent,
|
future: sanitizedFutureContent,
|
||||||
developmentLog: sanitizedDevLogContent,
|
developmentLog: sanitizedDevLogContent,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err: any) {
|
||||||
// Generic error message to prevent path disclosure
|
// Generic error message to prevent path disclosure
|
||||||
log.error('[aboutAdmin] Error reading files');
|
log.error('[aboutAdmin] Error reading files');
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
|
|
@ -546,7 +547,7 @@ router.get('/roadmap', requireAuth, requireAdmin, async (req: Req, res: Res) =>
|
||||||
_forgejoCache = { issues, fetchedAt: new Date().toISOString() };
|
_forgejoCache = { issues, fetchedAt: new Date().toISOString() };
|
||||||
_forgejoCacheTs = now;
|
_forgejoCacheTs = now;
|
||||||
res.json(_forgejoCache);
|
res.json(_forgejoCache);
|
||||||
} catch (err) {
|
} catch (err: any) {
|
||||||
log.error('[aboutAdmin] Forgejo issues error:', err.message);
|
log.error('[aboutAdmin] Forgejo issues error:', err.message);
|
||||||
if (_forgejoCache) return res.json({ ..._forgejoCache, stale: true });
|
if (_forgejoCache) return res.json({ ..._forgejoCache, stale: true });
|
||||||
res
|
res
|
||||||
|
|
@ -562,7 +563,7 @@ router.get('/dev-log', requireAuth, requireAdmin, (req: Req, res: Res) => {
|
||||||
const sanitized = redactSensitiveContent(devLogContent);
|
const sanitized = redactSensitiveContent(devLogContent);
|
||||||
const entries = parseDevLogMd(sanitized);
|
const entries = parseDevLogMd(sanitized);
|
||||||
res.json({ entries, version: pkg.version });
|
res.json({ entries, version: pkg.version });
|
||||||
} catch (err) {
|
} catch (err: any) {
|
||||||
log.error('[aboutAdmin] Error reading DEVELOPMENT_LOG.md for dev-log');
|
log.error('[aboutAdmin] Error reading DEVELOPMENT_LOG.md for dev-log');
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
error: 'Failed to read dev log data',
|
error: 'Failed to read dev log data',
|
||||||
|
|
@ -578,7 +579,7 @@ router.post('/check-updates', requireAuth, requireAdmin, async (req: Req, res: R
|
||||||
try {
|
try {
|
||||||
const result = await checkForUpdates(true);
|
const result = await checkForUpdates(true);
|
||||||
res.json(result);
|
res.json(result);
|
||||||
} catch (err) {
|
} catch (err: any) {
|
||||||
res.status(500).json({ error: 'Update check failed' });
|
res.status(500).json({ error: 'Update check failed' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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';
|
import type { Req, Res, Next } from '../types/http';
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const { log } = require('../utils/logger.cts');
|
const { log } = require('../utils/logger.cts');
|
||||||
|
|
@ -87,7 +86,7 @@ router.get('/deleted', (req: Req, res: Res) => {
|
||||||
)
|
)
|
||||||
.all(req.user.id);
|
.all(req.user.id);
|
||||||
res.json(
|
res.json(
|
||||||
rows.map((row) => ({
|
rows.map((row: any) => ({
|
||||||
...serializeBill(row),
|
...serializeBill(row),
|
||||||
category_name: row.category_name,
|
category_name: row.category_name,
|
||||||
deleted_at: row.deleted_at,
|
deleted_at: row.deleted_at,
|
||||||
|
|
@ -137,7 +136,7 @@ router.put('/reorder', (req: Req, res: Res) => {
|
||||||
const update = db.prepare(
|
const update = db.prepare(
|
||||||
"UPDATE bills SET sort_order = ?, updated_at = datetime('now') WHERE id = ? AND user_id = ?",
|
"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);
|
for (const item of items) update.run(item.sortOrder, item.billId, req.user.id);
|
||||||
});
|
});
|
||||||
applyOrder(entries);
|
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)
|
// Bill templates store money fields (expected_amount, current_balance, minimum_payment)
|
||||||
// in integer cents, matching validateBillData's normalized output. Convert back to
|
// in integer cents, matching validateBillData's normalized output. Convert back to
|
||||||
// dollars for API responses, mirroring serializeBill.
|
// dollars for API responses, mirroring serializeBill.
|
||||||
function serializeTemplateData(data) {
|
function serializeTemplateData(data: any) {
|
||||||
if (!data) return data;
|
if (!data) return data;
|
||||||
const out = { ...data };
|
const out = { ...data };
|
||||||
if (out.expected_amount != null) out.expected_amount = fromCents(out.expected_amount);
|
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);
|
.all(req.user.id);
|
||||||
|
|
||||||
res.json(
|
res.json(
|
||||||
rows.map((row) => ({
|
rows.map((row: any) => ({
|
||||||
...row,
|
...row,
|
||||||
data: serializeTemplateData(parseTemplateData(row.data)),
|
data: serializeTemplateData(parseTemplateData(row.data)),
|
||||||
})),
|
})),
|
||||||
|
|
@ -812,7 +811,7 @@ router.get('/:id/transactions', (req: Req, res: Res) => {
|
||||||
)
|
)
|
||||||
.all(billId, req.user.id, billId);
|
.all(billId, req.user.id, billId);
|
||||||
|
|
||||||
const transactions = rows.map((row) =>
|
const transactions = rows.map((row: any) =>
|
||||||
decorateTransaction({
|
decorateTransaction({
|
||||||
...row,
|
...row,
|
||||||
linked_payment: row.linked_payment_id
|
linked_payment: row.linked_payment_id
|
||||||
|
|
@ -1020,7 +1019,7 @@ router.post('/:id/history-ranges', (req: Req, res: Res) => {
|
||||||
'end_year',
|
'end_year',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (ey != null) {
|
if (ey != null && em != null) {
|
||||||
const startVal = sy * 12 + sm;
|
const startVal = sy * 12 + sm;
|
||||||
const endVal = ey * 12 + em;
|
const endVal = ey * 12 + em;
|
||||||
if (endVal < startVal)
|
if (endVal < startVal)
|
||||||
|
|
@ -1174,7 +1173,7 @@ router.get('/:id/amortization', (req: Req, res: Res) => {
|
||||||
current_balance: balance,
|
current_balance: balance,
|
||||||
minimum_payment: minPmt,
|
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({
|
res.json({
|
||||||
bill_id: billId,
|
bill_id: billId,
|
||||||
|
|
@ -1185,7 +1184,7 @@ router.get('/:id/amortization', (req: Req, res: Res) => {
|
||||||
summary: {
|
summary: {
|
||||||
months: schedule.length,
|
months: schedule.length,
|
||||||
total_interest: roundMoney(total_interest),
|
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,
|
capped: schedule.length >= maxMonths && schedule[schedule.length - 1]?.balance > 0,
|
||||||
},
|
},
|
||||||
apr_snapshot,
|
apr_snapshot,
|
||||||
|
|
@ -1258,14 +1257,14 @@ router.patch('/:id/balance', (req: Req, res: Res) => {
|
||||||
|
|
||||||
// ── Merchant rule helpers ─────────────────────────────────────────────────────
|
// ── Merchant rule helpers ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
function requireBill(db, billId, userId) {
|
function requireBill(db: any, billId: any, userId: any) {
|
||||||
return db
|
return db
|
||||||
.prepare('SELECT id, name FROM bills WHERE id = ? AND user_id = ? AND deleted_at IS NULL')
|
.prepare('SELECT id, name FROM bills WHERE id = ? AND user_id = ? AND deleted_at IS NULL')
|
||||||
.get(billId, userId);
|
.get(billId, userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Count unmatched transactions that would match a normalized merchant string.
|
// 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;
|
if (!normalized || normalized.length < 2) return 0;
|
||||||
const txRows = db
|
const txRows = db
|
||||||
.prepare(
|
.prepare(
|
||||||
|
|
@ -1281,14 +1280,14 @@ function previewMatchCount(db, userId, normalized) {
|
||||||
`,
|
`,
|
||||||
)
|
)
|
||||||
.all(userId);
|
.all(userId);
|
||||||
return txRows.filter((tx) => {
|
return txRows.filter((tx: any) => {
|
||||||
const txMerchant = normalizeMerchant(tx.payee || tx.description || tx.memo || '');
|
const txMerchant = normalizeMerchant(tx.payee || tx.description || tx.memo || '');
|
||||||
return txMerchant && merchantMatches(txMerchant, normalized);
|
return txMerchant && merchantMatches(txMerchant, normalized);
|
||||||
}).length;
|
}).length;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find bills (other than this one) that already claim this merchant.
|
// 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
|
return db
|
||||||
.prepare(
|
.prepare(
|
||||||
`
|
`
|
||||||
|
|
@ -1336,7 +1335,7 @@ router.get('/:id/merchant-rules', (req: Req, res: Res) => {
|
||||||
`,
|
`,
|
||||||
)
|
)
|
||||||
.all(req.user.id)
|
.all(req.user.id)
|
||||||
.map((tx) => {
|
.map((tx: any) => {
|
||||||
const raw = tx.payee || tx.description || tx.memo || '';
|
const raw = tx.payee || tx.description || tx.memo || '';
|
||||||
const normalized = normalizeMerchant(raw);
|
const normalized = normalizeMerchant(raw);
|
||||||
return {
|
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),
|
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 });
|
res.json({ rules, suggestions });
|
||||||
});
|
});
|
||||||
|
|
@ -1422,7 +1421,7 @@ router.get('/:id/merchant-rules/candidates', (req: Req, res: Res) => {
|
||||||
const rules = db
|
const rules = db
|
||||||
.prepare('SELECT merchant FROM bill_merchant_rules WHERE user_id = ? AND bill_id = ?')
|
.prepare('SELECT merchant FROM bill_merchant_rules WHERE user_id = ? AND bill_id = ?')
|
||||||
.all(req.user.id, billId)
|
.all(req.user.id, billId)
|
||||||
.map((r) => r.merchant);
|
.map((r: any) => r.merchant);
|
||||||
|
|
||||||
if (rules.length === 0) return res.json({ candidates: [] });
|
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',
|
'SELECT transaction_id FROM payments WHERE bill_id = ? AND transaction_id IS NOT NULL AND deleted_at IS NULL',
|
||||||
)
|
)
|
||||||
.all(billId)
|
.all(billId)
|
||||||
.map((r) => r.transaction_id),
|
.map((r: any) => r.transaction_id),
|
||||||
);
|
);
|
||||||
|
|
||||||
const candidates = [];
|
const candidates = [];
|
||||||
for (const tx of txRows) {
|
for (const tx of txRows) {
|
||||||
const txMerchant = normalizeMerchant(tx.payee || tx.description || tx.memo || '');
|
const txMerchant = normalizeMerchant(tx.payee || tx.description || tx.memo || '');
|
||||||
if (!txMerchant) continue;
|
if (!txMerchant) continue;
|
||||||
const matches = rules.some((r) => merchantMatches(txMerchant, r));
|
const matches = rules.some((r: any) => merchantMatches(txMerchant, r));
|
||||||
if (!matches) continue;
|
if (!matches) continue;
|
||||||
|
|
||||||
const paidDate =
|
const paidDate =
|
||||||
|
|
@ -1523,7 +1522,7 @@ router.post('/:id/merchant-rules/import-historical', (req: Req, res: Res) => {
|
||||||
`);
|
`);
|
||||||
|
|
||||||
let imported = 0;
|
let imported = 0;
|
||||||
const lateAttributions = [];
|
const lateAttributions: any[] = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
db.transaction(() => {
|
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);
|
log.error('[import-historical] Transaction failed:', err.message);
|
||||||
throw new ApiError('DB_ERROR', 'Import failed', 500);
|
throw new ApiError('DB_ERROR', 'Import failed', 500);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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';
|
import type { Req, Res, Next } from '../types/http';
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
@ -23,7 +22,7 @@ const DEBT_LIKE_CLAUSES = `(
|
||||||
)
|
)
|
||||||
)`;
|
)`;
|
||||||
|
|
||||||
function isRamseyMode(userId) {
|
function isRamseyMode(userId: any) {
|
||||||
const db = getDb();
|
const db = getDb();
|
||||||
const row = db
|
const row = db
|
||||||
.prepare(
|
.prepare(
|
||||||
|
|
@ -37,7 +36,7 @@ function isRamseyMode(userId) {
|
||||||
return row ? row.value !== 'false' && row.value !== '0' : true;
|
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 db = getDb();
|
||||||
const row = db
|
const row = db
|
||||||
.prepare(
|
.prepare(
|
||||||
|
|
@ -52,7 +51,7 @@ function getUserBoolSetting(userId, key, fallback = false) {
|
||||||
return row.value === 'true' || row.value === '1';
|
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(
|
db.prepare(
|
||||||
`
|
`
|
||||||
INSERT INTO user_settings (user_id, key, value, updated_at)
|
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));
|
).run(userId, key, String(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDebtQuery(ramseyMode) {
|
function getDebtQuery(ramseyMode: any) {
|
||||||
const orderBy = ramseyMode
|
const orderBy = ramseyMode
|
||||||
? `
|
? `
|
||||||
CASE WHEN b.current_balance IS NULL THEN 1 ELSE 0 END ASC,
|
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 db = getDb();
|
||||||
const mode = ramseyMode !== undefined ? ramseyMode : isRamseyMode(userId);
|
const mode = ramseyMode !== undefined ? ramseyMode : isRamseyMode(userId);
|
||||||
return db.prepare(getDebtQuery(mode)).all(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
|
// Money fields on `bills` are stored as integer cents; the snowball/APR math
|
||||||
// and the API response are dollar-denominated, so convert before computing.
|
// and the API response are dollar-denominated, so convert before computing.
|
||||||
const billsForMath = bills.map((b) => ({
|
const billsForMath = bills.map((b: any) => ({
|
||||||
...b,
|
...b,
|
||||||
current_balance: fromCents(b.current_balance),
|
current_balance: fromCents(b.current_balance),
|
||||||
minimum_payment: fromCents(b.minimum_payment),
|
minimum_payment: fromCents(b.minimum_payment),
|
||||||
|
|
@ -194,17 +193,17 @@ router.get('/projection', (req: Req, res: Res) => {
|
||||||
: fromCents(user?.snowball_extra_payment ?? 0);
|
: fromCents(user?.snowball_extra_payment ?? 0);
|
||||||
|
|
||||||
// Build a lookup of APR snapshots keyed by bill id (computed once from current balances)
|
// 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) {
|
for (const b of billsForMath) {
|
||||||
const snap = debtAprSnapshot(b);
|
const snap = debtAprSnapshot(b);
|
||||||
if (snap) aprByBill[b.id] = snap;
|
if (snap) aprByBill[b.id] = snap;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enrich each debt result with its APR snapshot
|
// Enrich each debt result with its APR snapshot
|
||||||
function enrich(projection) {
|
function enrich(projection: any) {
|
||||||
return {
|
return {
|
||||||
...projection,
|
...projection,
|
||||||
debts: projection.debts.map((d) => ({
|
debts: projection.debts.map((d: any) => ({
|
||||||
...d,
|
...d,
|
||||||
apr_snapshot: aprByBill[d.id] ?? null,
|
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
|
// 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 sbMonths = snowball.months_to_freedom;
|
||||||
const moMonths = minimum_only.months_to_freedom;
|
const moMonths = minimum_only.months_to_freedom;
|
||||||
const sbInterest = snowball.total_interest_paid;
|
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
|
// Validate every row before touching the DB — no silent skips
|
||||||
const parsed = [];
|
const parsed: any[] = [];
|
||||||
for (let i = 0; i < items.length; i++) {
|
for (let i = 0; i < items.length; i++) {
|
||||||
const row = items[i];
|
const row = items[i];
|
||||||
const id = parseInt(row?.id, 10);
|
const id = parseInt(row?.id, 10);
|
||||||
|
|
@ -292,7 +291,7 @@ router.patch('/order', (req: Req, res: Res) => {
|
||||||
|
|
||||||
// ── Snowball Plan helpers ─────────────────────────────────────────────────────
|
// ── Snowball Plan helpers ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
function enrichPlanWithProgress(db, plan) {
|
function enrichPlanWithProgress(db: any, plan: any) {
|
||||||
let snapshot;
|
let snapshot;
|
||||||
try {
|
try {
|
||||||
snapshot = JSON.parse(plan.plan_snapshot);
|
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.
|
// Fetch every snapshot bill in one query (user-scoped), not one-per-debt.
|
||||||
const snapDebts = snapshot?.debts ?? [];
|
const snapDebts = snapshot?.debts ?? [];
|
||||||
const billIds = [...new Set(snapDebts.map((d) => d.bill_id).filter(Boolean))];
|
const billIds = [...new Set(snapDebts.map((d: any) => d.bill_id).filter(Boolean))];
|
||||||
const billsById = {};
|
const billsById: Record<string, any> = {};
|
||||||
if (billIds.length) {
|
if (billIds.length) {
|
||||||
const placeholders = billIds.map(() => '?').join(',');
|
const placeholders = billIds.map(() => '?').join(',');
|
||||||
for (const b of db
|
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 bill = billsById[d.bill_id];
|
||||||
const currentBalance = bill && !bill.deleted_at ? fromCents(bill.current_balance) : null;
|
const currentBalance = bill && !bill.deleted_at ? fromCents(bill.current_balance) : null;
|
||||||
const startingBalance = d.starting_balance ?? 0;
|
const startingBalance = d.starting_balance ?? 0;
|
||||||
|
|
@ -360,7 +359,7 @@ router.post('/plans', (req: Req, res: Res) => {
|
||||||
|
|
||||||
const ramseyMode = isRamseyMode(userId);
|
const ramseyMode = isRamseyMode(userId);
|
||||||
const debts = getDebtBills(userId, ramseyMode);
|
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) {
|
if (activeDebts.length === 0) {
|
||||||
throw ValidationError(
|
throw ValidationError(
|
||||||
'No debts with a balance found. Add a balance to at least one bill.',
|
'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
|
// Money fields on `debts` are stored as integer cents; the snowball/APR
|
||||||
// math and plan_snapshot are dollar-denominated, so convert before computing.
|
// math and plan_snapshot are dollar-denominated, so convert before computing.
|
||||||
const debtsForMath = debts.map((b) => ({
|
const debtsForMath = debts.map((b: any) => ({
|
||||||
...b,
|
...b,
|
||||||
current_balance: fromCents(b.current_balance),
|
current_balance: fromCents(b.current_balance),
|
||||||
minimum_payment: fromCents(b.minimum_payment),
|
minimum_payment: fromCents(b.minimum_payment),
|
||||||
|
|
@ -393,8 +392,8 @@ router.post('/plans', (req: Req, res: Res) => {
|
||||||
100,
|
100,
|
||||||
);
|
);
|
||||||
|
|
||||||
const debtSnaps = debtsForMath.map((b, i) => {
|
const debtSnaps = debtsForMath.map((b: any, i: number) => {
|
||||||
const proj = snowball.debts?.find((d) => d.id === b.id);
|
const proj = snowball.debts?.find((d: any) => d.id === b.id);
|
||||||
return {
|
return {
|
||||||
bill_id: b.id,
|
bill_id: b.id,
|
||||||
name: b.name,
|
name: b.name,
|
||||||
|
|
@ -451,7 +450,7 @@ router.get('/plans', (req: Req, res: Res) => {
|
||||||
`,
|
`,
|
||||||
)
|
)
|
||||||
.all(req.user.id);
|
.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)
|
// 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.
|
// Shared status-transition handler for pause / resume / complete / abandon.
|
||||||
// `setSql` is a fixed constant per route (never user input).
|
// `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 db = getDb();
|
||||||
const id = parseInt(req.params.id, 10);
|
const id = parseInt(req.params.id, 10);
|
||||||
if (!Number.isInteger(id) || id <= 0) {
|
if (!Number.isInteger(id) || id <= 0) {
|
||||||
|
|
|
||||||
|
|
@ -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';
|
import type { Req, Res, Next } from '../types/http';
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const { log } = require('../utils/logger.cts');
|
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.
|
// 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.
|
// 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 {
|
try {
|
||||||
const settings = getUserSettings(userId);
|
const settings = getUserSettings(userId);
|
||||||
if (settings.bank_tracking_enabled !== 'true') return null;
|
if (settings.bank_tracking_enabled !== 'true') return null;
|
||||||
|
|
@ -92,8 +91,8 @@ function buildBankTrackingSummary(db, userId, year, month) {
|
||||||
)
|
)
|
||||||
.all(year, month, start, end, userId);
|
.all(year, month, start, end, userId);
|
||||||
const unpaidTotalCents = unpaidCandidates
|
const unpaidTotalCents = unpaidCandidates
|
||||||
.filter((row) => resolveDueDate(row, year, month))
|
.filter((row: any) => resolveDueDate(row, year, month))
|
||||||
.reduce((sum, row) => sum + (row.amount_cents || 0), 0);
|
.reduce((sum: number, row: any) => sum + (row.amount_cents || 0), 0);
|
||||||
|
|
||||||
const balanceDollars = money(account.balance / 100);
|
const balanceDollars = money(account.balance / 100);
|
||||||
const pendingDollars = fromCents(pendingRow.pending_total);
|
const pendingDollars = fromCents(pendingRow.pending_total);
|
||||||
|
|
@ -116,13 +115,13 @@ function buildBankTrackingSummary(db, userId, year, month) {
|
||||||
unpaid_this_month: unpaidDollars,
|
unpaid_this_month: unpaidDollars,
|
||||||
remaining: money(effectiveDollars - unpaidDollars),
|
remaining: money(effectiveDollars - unpaidDollars),
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err: any) {
|
||||||
log.error('[buildBankTrackingSummary] Error:', err.message);
|
log.error('[buildBankTrackingSummary] Error:', err.message);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseYearMonth(source) {
|
function parseYearMonth(source: any) {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const year = parseInt(source.year || now.getFullYear(), 10);
|
const year = parseInt(source.year || now.getFullYear(), 10);
|
||||||
const month = parseInt(source.month || now.getMonth() + 1, 10);
|
const month = parseInt(source.month || now.getMonth() + 1, 10);
|
||||||
|
|
@ -137,12 +136,12 @@ function parseYearMonth(source) {
|
||||||
return { year, month };
|
return { year, month };
|
||||||
}
|
}
|
||||||
|
|
||||||
function money(value) {
|
function money(value: any) {
|
||||||
const n = Number(value);
|
const n = Number(value);
|
||||||
return Number.isFinite(n) ? Math.round(n * 100) / 100 : 0;
|
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
|
const row = db
|
||||||
.prepare(
|
.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);
|
const { start, end } = getCycleRange(year, month);
|
||||||
|
|
||||||
// Paid from first bucket: bills with due_day 1-14
|
// 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 amounts = getStartingAmounts(db, userId, year, month);
|
||||||
const paid = calculatePaidDeductions(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
|
const row = db
|
||||||
.prepare(
|
.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 income = getIncome(db, userId, year, month);
|
||||||
const { start, end } = getCycleRange(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
|
// QA-B5-01: only bills that actually occur in this month (matches the Tracker's
|
||||||
// resolveDueDate gating) — otherwise annual / off-month quarterly bills inflate
|
// resolveDueDate gating) — otherwise annual / off-month quarterly bills inflate
|
||||||
// the monthly expense list and total, disagreeing with the Tracker.
|
// 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();
|
const paymentMap = new Map();
|
||||||
|
|
||||||
if (billIds.length > 0) {
|
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 payment = paymentMap.get(row.bill_id) || { payment_count: 0, paid_amount: 0 };
|
||||||
const hasActual = row.actual_amount !== null && row.actual_amount !== undefined;
|
const hasActual = row.actual_amount !== null && row.actual_amount !== undefined;
|
||||||
const displayAmount = fromCents(hasActual ? row.actual_amount : row.expected_amount);
|
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 incomeTotal = money(income.amount);
|
||||||
const expenseTotal = money(
|
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 paidTotal = money(
|
||||||
const paidExpenseCount = countedExpenses.filter((expense) => expense.is_paid).length;
|
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 starting_amounts = buildStartingAmountsSummary(db, userId, year, month);
|
||||||
const bank_tracking = buildBankTrackingSummary(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
|
// When bank tracking is on, drive the "plan base" from the effective bank balance
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue