Skip to content

Give recurring revenue a real schedule: frequency and a start date. - #3

Open
Herklos wants to merge 1 commit into
masterfrom
recurring-revenue-frequency
Open

Give recurring revenue a real schedule: frequency and a start date.#3
Herklos wants to merge 1 commit into
masterfrom
recurring-revenue-frequency

Conversation

@Herklos

@Herklos Herklos commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

No description provided.

@Herklos Herklos self-assigned this Jul 29, 2026
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 29, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
chrono cbdf094 Jul 29 2026, 12:14 PM

Adding a recurring revenue source only asked for a flat amount, which was
silently read as "per month, forever, starting now". A weekly retainer, a
quarterly licence or a contract beginning next quarter had no way to be
expressed.

A recurring source now carries a schedule in `content`:

    { "frequency": "daily|weekly|biweekly|monthly|quarterly|yearly",
      "amount_cents": <what ONE occurrence is worth> }

anchored on revenue_sources.starts_on and bounded by ends_on. Both columns
already existed and recognize_project_revenue already filtered on them; only
the app never wrote them. No column is added.

The schedule is expanded per month, not below it. revenue_entries.period_month
is a hard monthly grain, and invoices, rem_months, rem_lines, referral_earnings
and company_fee_reserve_ledger all key off it, so a weekly source does not
write weekly rows: recognition counts the occurrences landing inside the month
and writes that month's single entry. Weekly 500 EUR from 2026-03-11 recognizes
1500 in March and 2500 in April. Daily counts calendar days, weekends included.

Legacy sources are untouched. No `frequency` key means a pre-frequency source:
the flat monthly_amount_cents, once a month, exactly as before. Those rows also
have starts_on = null, so nothing about them shifts.

The occurrence math is a deliberate TS/SQL mirror pair -- occurrencesInMonth in
revenue-source.lib.ts and recurring_occurrences_in_month in the migration -- the
same way project_cost_cumulative mirrors project-cost.lib.ts. Change both
together. Both fallbacks are pinned by a test on each side, including the
awkward one: a frequency with no anchor (a hand-edited or imported row) pays
one occurrence a month rather than silently paying zero.

A backdated start date has months to catch up on, and recognize_project_revenue
only ever handles the one month it is given, so recognize_project_revenue_range
loops them server-side: one round trip, one transaction. It inherits the manager
gate from the inner function, and its own guards raise slugs
(revenue-range-invalid, revenue-range-too-wide:<months>), never prose.

Two renames the change forced. monthlyRecurringAmount is gone: the name lies as
soon as an amount is per-week, and it is replaced by recurringSchedule (the
configured per-occurrence amount) and recurringRevenue(source, period) (what a
given month recognizes). The project detail total now sums the current month's
recognized amount, because adding a weekly 500 to a monthly 3000 was not a
meaningful number.

Covered by 32 pgTAP assertions and 30 vitest ones, asserted against the same
numbers on both sides of the mirror: per-frequency occurrence counting, leap and
short-month anchor clamping, end-date truncation, idempotent re-recognition,
retirement of months a shrunk schedule no longer covers, range back-fill amounts,
and the manager gate on the range wrapper.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Herklos
Herklos force-pushed the recurring-revenue-frequency branch from 9a6387e to cbdf094 Compare July 29, 2026 12:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant