BillTracker/services/statusService.js

99 lines
3.3 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const { getSetting } = require('../db/database');
/**
* Resolves the due date for a bill in a given year/month.
* Bills use a recurring day-of-month template field. Legacy override_due_date
* values are intentionally ignored by the current product behavior.
*/
function resolveDueDate(bill, year, month) {
const daysInMonth = new Date(year, month, 0).getDate();
const day = Math.min(bill.due_day, daysInMonth);
return `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
}
/**
* Auto-assigns bucket from due_day: 114 → '1st', 15+ → '15th'
*/
function resolveBucket(bill) {
if (bill.bucket) return bill.bucket;
return bill.due_day <= 14 ? '1st' : '15th';
}
/**
* Computes the payment cycle start/end for a bill in a given month.
* For monthly bills the cycle is the calendar month.
*/
function getCycleRange(year, month) {
const start = `${year}-${String(month).padStart(2, '0')}-01`;
const daysInMonth = new Date(year, month, 0).getDate();
const end = `${year}-${String(month).padStart(2, '0')}-${String(daysInMonth).padStart(2, '0')}`;
return { start, end };
}
/**
* Returns status for a bill given its payments and due date.
*
* Statuses:
* paid — total payments >= expected_amount
* autodraft — autopay_enabled and assumed_paid (no confirmed payment yet)
* upcoming — due_date in the future
* due_soon — due within 3 days
* late — past due, within grace period
* missed — past grace period, unpaid
*/
function calculateStatus(bill, payments, dueDate, today) {
const gracePeriodDays = parseInt(getSetting('grace_period_days') || '5', 10);
const totalPaid = payments.reduce((sum, p) => sum + p.amount, 0);
const isPaid = totalPaid >= bill.expected_amount;
if (isPaid) return 'paid';
if (bill.autopay_enabled && bill.autodraft_status === 'assumed_paid') {
return 'autodraft';
}
const due = new Date(dueDate + 'T00:00:00');
const todayDate = new Date(today + 'T00:00:00');
const diffDays = Math.floor((due - todayDate) / 86400000);
if (diffDays > 3) return 'upcoming';
if (diffDays >= 0) return 'due_soon';
if (Math.abs(diffDays) <= gracePeriodDays) return 'late';
return 'missed';
}
/**
* Builds a full tracker row for a bill in a given month.
*/
function buildTrackerRow(bill, payments, year, month, todayStr) {
const dueDate = resolveDueDate(bill, year, month);
const bucket = resolveBucket(bill);
const status = calculateStatus(bill, payments, dueDate, todayStr);
const totalPaid = payments.reduce((sum, p) => sum + p.amount, 0);
const lastPayment = payments.length
? payments.sort((a, b) => b.paid_date.localeCompare(a.paid_date))[0]
: null;
return {
id: bill.id,
name: bill.name,
category_id: bill.category_id,
category_name: bill.category_name || null,
due_date: dueDate,
due_day: bill.due_day,
bucket,
expected_amount: bill.expected_amount,
total_paid: totalPaid,
balance: bill.expected_amount - totalPaid,
last_paid_date: lastPayment ? lastPayment.paid_date : null,
last_payment_amount: lastPayment ? lastPayment.amount : null,
status,
autopay_enabled: !!bill.autopay_enabled,
autodraft_status: bill.autodraft_status,
billing_cycle: bill.billing_cycle,
payments,
};
}
module.exports = { resolveDueDate, resolveBucket, getCycleRange, calculateStatus, buildTrackerRow };