/**
* @file entry-billable.js — Billable-status helpers for log entries (LEAF MODULE).
*
* Extracted from `05-entries.js` and `02-utils.js` (issue #336, extraction #19).
* Both functions depend on category and plan-task lookups, so they are not pure;
* they live in their own leaf module imported at the top of the built bundle so
* the concatenated files can call them from shared scope.
*/
import { getPlanTasks } from './state.js';
import { getCat } from './cat-utils.js';
import { roundToNearest30 } from './pure-fns.js';
/**
* Determines whether a log entry is billable, using a three-tier lookup:
* 1. The entry's own `billable` flag (if explicitly set).
* 2. The matching plan task's `billable` flag (matched case-insensitively by trimmed text).
* 3. The category default.
*
* Assumption: entries and tasks where `billable` is `undefined` are treated as
* billable by default. This preserves backward compatibility with data created
* before the billable flag was introduced — older entries must not silently
* disappear from billing reports after an upgrade.
* If the default should change to non-billable, a migration of existing
* localStorage data is required (see DATA.md § wl_entries).
*
* @param {Object} entry - Log entry object.
* @returns {boolean} True if the entry should be counted as billable.
* @example
* isEntryBillable({ text: 'Write report', tag: 'work' }) // → true (category default)
* isEntryBillable({ text: 'Write report', tag: 'work', billable: false }) // → false (explicit flag)
* isEntryBillable({ text: 'Write report', signifier: 'cancelled' }) // → false (cancelled)
*/
export function isEntryBillable(entry) {
if (entry.signifier === 'cancelled') return false;
if (entry.billable !== undefined) return entry.billable;
const task = getPlanTasks().find(
(planTask) => planTask.text.toLowerCase().trim() === entry.text.toLowerCase().trim()
);
// `!== false` (not `=== true`) — undefined means billable (see Assumption above).
if (task) return task.billable !== false;
// Same `!== false` convention for categories — undefined → billable.
return getCat(entry.tag || 'other').billable !== false;
}
/**
* Rounds `ts` to the nearest 30-minute mark only when `entry` is billable.
* Non-billable entries keep their exact timestamps for accurate reporting.
* @param {number} ts - Unix timestamp in milliseconds.
* @param {Object|null} entry - Work-log entry; if null or undefined, always rounds.
* @returns {number} Timestamp, conditionally rounded.
* @example
* roundToNearest30IfBillable(1000 * 60 * 47, null) // → rounded (null always rounds)
* roundToNearest30IfBillable(1000 * 60 * 47, { billable: false }) // → exact ts (non-billable)
* roundToNearest30IfBillable(1000 * 60 * 47, { billable: true }) // → rounded (billable)
*/
export function roundToNearest30IfBillable(ts, entry) {
// Assumption: non-billable entries keep exact timestamps for accurate time reporting.
// Billable entries are rounded because clients are invoiced in 30-minute increments.
// Changing this requires updating the export format in 05-entries.js and DATA.md.
if (entry && !isEntryBillable(entry)) return ts;
return roundToNearest30(ts);
}