What β share the identical bootstrap preamble used by the five billing cron scripts.
Why β each cron repeats the same ~25-line block (a six-import Promise.all, an environment default, a config gate, and lock name/TTL). Adding a sixth cron means copying it again.
Scope β modules/billing/crons/billing.{weeklyReset,reconcile,extrasExpiration,referralReconcile,dunningSweep}.js. Add bootstrapCron() to modules/billing/lib/billing.cron-utils.js, which already exists and currently holds only applyJitter.
The gate is NOT uniform β this is the trap. Four crons gate on the meter-mode flag; billing.referralReconcile gates on the referral-enabled flag and logs a different message. Hardcoding one gate silently changes which flag disables that cron. The gate predicate and its log line must be parameters. Copy each cron's gate, log string and lock TTL from the file itself β do not infer them.
Two things the helper must return, or converted crons crash at runtime while unit tests still pass:
getCronJitterMaxMs β every cron calls it.
- Everything else each cron currently destructures. Enumerate per file before writing the helper.
Also note the preamble includes a process.env.NODE_ENV default. If it moves into the helper, code that reads process.env.NODE_ENV directly still needs it set at the same point in the sequence.
Definition of done: existing cron unit tests pass, and each converted cron is smoke-run with its flag turned off, printing its original log line verbatim and exiting 0. Unit tests do not exercise the destructure β the smoke run is what catches a missing key.
Scope: validated 2026-07-28
Created via /dev:issue
Baseline: line numbers and counts here were verified against master @ c8049d69 (2026-07-28). Master moves β re-verify against current master before starting; the invariants above are what must hold regardless of where the code has drifted to.
What β share the identical bootstrap preamble used by the five billing cron scripts.
Why β each cron repeats the same ~25-line block (a six-import
Promise.all, an environment default, a config gate, and lock name/TTL). Adding a sixth cron means copying it again.Scope β
modules/billing/crons/billing.{weeklyReset,reconcile,extrasExpiration,referralReconcile,dunningSweep}.js. AddbootstrapCron()tomodules/billing/lib/billing.cron-utils.js, which already exists and currently holds onlyapplyJitter.The gate is NOT uniform β this is the trap. Four crons gate on the meter-mode flag;
billing.referralReconcilegates on the referral-enabled flag and logs a different message. Hardcoding one gate silently changes which flag disables that cron. The gate predicate and its log line must be parameters. Copy each cron's gate, log string and lock TTL from the file itself β do not infer them.Two things the helper must return, or converted crons crash at runtime while unit tests still pass:
getCronJitterMaxMsβ every cron calls it.Also note the preamble includes a
process.env.NODE_ENVdefault. If it moves into the helper, code that readsprocess.env.NODE_ENVdirectly still needs it set at the same point in the sequence.Definition of done: existing cron unit tests pass, and each converted cron is smoke-run with its flag turned off, printing its original log line verbatim and exiting 0. Unit tests do not exercise the destructure β the smoke run is what catches a missing key.
Scope: validated 2026-07-28
Created via /dev:issue
Baseline: line numbers and counts here were verified against
master @ c8049d69(2026-07-28). Master moves β re-verify against current master before starting; the invariants above are what must hold regardless of where the code has drifted to.