Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
8.7 KiB
PRD — Billing Cycle Anchors (Day-of-Month) + Exclusive End Dates
- Slug:
billing-cycle-anchors - Date:
2026-01-09 - Status: Draft
Summary
Add per-client billing cycle anchors so MSPs can invoice clients on non-calendar boundaries (e.g., “bill on the 10th of each month”), while standardizing the billing domain on a single, consistent date-range convention: billing period end dates are exclusive.
Problem
Today Alga supports picking a billing cycle type per client (weekly/bi-weekly/monthly/quarterly/semi-annually/annually), but cycle generation is effectively aligned to calendar boundaries for monthly+ (and has inconsistent logic for weekly/bi-weekly), and the billing domain has mixed semantics for whether a period end date is inclusive or exclusive. This prevents common MSP workflows like:
- “Invoice this client monthly on the 10th.”
- “Invoice this client annually on their service anniversary.”
- “Invoice this client every 2 weeks starting next Friday.”
Goals
- Allow admins to configure a per-client billing cycle anchor appropriate to the client’s billing frequency.
- Ensure billing, proration, and overlap validation all treat billing periods as
[start, end)(end is exclusive). - Ensure the design works across cycle types:
weekly,bi-weekly,monthly,quarterly,semi-annually,annually. - Preserve existing behavior by default (clients without anchors keep the current “calendar-aligned” schedule for monthly+/annual and the current rolling schedule for weekly/bi-weekly).
Non-goals
- Replacing
client_billing_cycleswith a new “bill run” system; we continue to useclient_billing_cyclesas the source of truth for concrete billing periods. - Retroactively rewriting already-invoiced billing periods.
- Supporting arbitrary time-of-day boundaries; billing periods remain UTC-midnight dates.
- V1: complex “29/30/31st of month” semantics unless explicitly requested (see Open Questions).
Users and Primary Flows
Personas
- Billing Admin (MSP): configures billing cycles and anchors per client; generates invoices.
- Ops/Admin: adjusts client billing settings during onboarding/renewal.
Primary flows
- Billing admin selects a client from the Clients menu.
- In the client detail view, navigates to the client’s Billing tab.
- Chooses the billing cycle type and configures the anchor (cycle-type-aware).
- System uses the configured schedule to generate upcoming billing cycles (automated nightly + manual “create next cycle”).
- Billing admin generates invoices from billing cycles (existing).
Secondary flow:
- Billing admin opens Billing → Billing Cycles to review a cross-client summary and click through to a specific client’s Billing tab.
UX / UI Notes
UI placement
- Billing schedule configuration (cycle type + anchor + preview) lives on the Client → Billing tab.
- The Billing → Billing Cycles tab is summary-only (no schedule editing); it links to the client’s Billing tab for changes.
Anchor UX should be cycle-type aware:
- Weekly: choose “weekday” (Mon–Sun). Optionally show the computed “next start date” preview.
- Bi-weekly: choose a concrete “first cycle start date” (UTC date) so parity is stable.
- Monthly: choose “day of month” (default 1). V1 is limited to 1–28 for predictability.
- Quarterly: choose “start month” + day-of-month (1–28), with a preview of next cycle boundaries.
- Semi-annually: choose “start month” + day-of-month (1–28), with a preview of next cycle boundaries.
- Annually: choose “start month” + day-of-month (1–28), with a preview of next cycle boundaries.
UI should make the date semantics explicit in copy/tooltips: “Billing periods are [start, end); end date is the start of the next period.”
Requirements
Functional Requirements
Billing schedule configuration (cycle type + anchor)
- Store a per-client anchor configuration.
- Validate anchor values (ranges, required fields per cycle type).
- Use anchor configuration when generating new
client_billing_cyclesrows. - Allow updating schedule configuration (cycle type and/or anchor), with guardrails to avoid breaking already-invoiced periods.
Cycle generation
- Initial cycle creation uses anchor logic to pick the correct current cycle start.
- Subsequent cycle creation advances deterministically by cycle type from the last cycle’s boundary.
- Manual “Create Next Cycle” uses the same logic as the scheduled job and respects anchor configuration.
- Schedule updates (cycle type and/or anchor) take effect after the last invoiced cycle:
- Compute
cutoverStart = lastInvoiced.period_end_date(exclusive end = next start). - Create a transition period
[cutoverStart, nextAnchorBoundary)(ifcutoverStartis not already aligned). - Create subsequent full periods aligned to the configured anchor.
- Compute
- Transition periods require deterministic proration of fixed recurring charges based on actual period length vs canonical cycle length.
Exclusive end date consistency
- All billing periods are treated as
[period_start_date, period_end_date). - Proration factors and “days in period” calculations must be consistent with exclusive end semantics.
- Overlap detection and “cannot span cycle change” validation must use exclusive end semantics.
Non-functional Requirements
- Deterministic and timezone-safe: all persisted billing boundary dates are UTC-midnight ISO8601 strings.
- Backward compatible: existing clients with no anchor configured continue behaving as today.
Data / API / Integrations
Data model
Add anchor fields to client_billing_settings (preferred, since it already stores billing knobs) rather than adding new columns to clients or client_billing_cycles.
Proposed shape (final schema is a design decision in implementation):
client_billing_settings.billing_cycle_anchor_day_of_month(int, nullable)client_billing_settings.billing_cycle_anchor_month_of_year(int, nullable; for annual/semi/quarterly where applicable)client_billing_settings.billing_cycle_anchor_day_of_week(int, nullable; ISO 1=Mon..7=Sun)client_billing_settings.billing_cycle_anchor_reference_date(date/timestamp UTC-midnight, nullable; for bi-weekly parity / “first cycle start”)
API / server actions
- Add server actions for reading/updating the anchor configuration.
- Add a server action for updating a client’s billing schedule (cycle type + anchor) that applies the same cutover behavior as anchor updates.
- Ensure invoice generation continues to rely on
client_billing_cycles.period_start_date/period_end_date(no anchor logic in invoice generation).
Security / Permissions
- Same permissions as existing billing configuration: only tenant users with billing/admin privileges can update anchors and create cycles.
Observability
- Not in scope for V1 (no new dashboards/metrics); rely on existing logs and error surfaces.
Rollout / Migration
- Ship schema changes for anchor fields on
client_billing_settings. - Backfill default anchor values for existing clients:
- monthly/quarterly/semi/annually: default day-of-month = 1, default start-month = current calendar convention (Jan/Apr/Jul/Oct; Jan/Jul; Jan).
- weekly/bi-weekly: preserve current rolling behavior unless the admin sets an anchor.
- Update cycle generation to use anchor settings for newly created cycles.
- Update date-range semantics and fix inconsistent end-date usage.
- Document the semantics and migration notes in billing docs.
Open Questions
- For weekly: do we also want a “first cycle start date” option (like bi-weekly) to make weekday-only anchors less ambiguous for initial creation?
- When changing schedule (cycle type and/or anchor), should the UI show an explicit warning that future non-invoiced billing cycles will be regenerated under the new schedule?
Acceptance Criteria (Definition of Done)
- A billing admin can set a monthly anchor day (e.g., 10) for a client and the system generates monthly billing cycles starting/ending on the 10th.
- Weekly, bi-weekly, quarterly, semi-annual, and annual cycle generation continues to work and respects anchors when configured.
- Billing, proration, and overlap validation treat billing periods as
[start, end)with end exclusive. - Manual “Create Next Cycle” respects anchor configuration and no longer ignores provided anchor/effective date inputs.
- Billing cycle type changes follow the same cutover behavior as anchor changes (no retroactive modification of invoiced cycles; future non-invoiced cycles are regenerated under the new schedule).
- Existing clients without anchors are not behaviorally broken; already-invoiced cycles are not modified.