Skip to content

Recurring: every N days (daily frequency) - #44

Merged
joaoguimaraespro merged 1 commit into
mainfrom
feat/recurring-every-n-days
Oct 5, 2026
Merged

joaoguimaraespro merged 1 commit into
mainfrom
feat/recurring-every-n-days

Conversation

@joaoguimaraespro

Copy link
Copy Markdown
Owner

Summary

Some payments repeat every N days from a date (gym every 15 days) rather than on fixed days of the month. "Weekly × 2" drifts from 15 days, and two monthly items stay tied to the calendar. This PR adds a Daily frequency that uses the existing interval: "every 15 days" is frequency: Daily, interval: 15.

Rules implemented

  • Anchor: the first occurrence is start_on, then start_on + k × interval days (k = 1, 2, …). This is computed in closed form with DateOnly, with no DateTime or offsets, so a DST change never moves an occurrence.
  • day_of_month: the validator rejects it for daily items (API and upsert_recurring). The domain ignores and clears any stored value, and editing an item into daily drops the old day.
  • end_on: no occurrence after it.
  • Interval limits: 1–365 for daily; weekly, monthly and yearly keep 1–24.
  • Next due: nextDueOn in GET /api/recurring and get_recurring is now the first scheduled occurrence on or after today, even when that occurrence is already awaiting confirmation. This applies to every frequency. Before, it was the scheduler's internal pointer, which sits past the 7-day look-ahead.
  • Pending items: generated the same way as for monthly items, overdue occurrences included (from start_on).
  • Editing the schedule (frequency, interval, day, start or end):
    • Pending proposals dated today or later are deleted and proposed again from the new anchor.
    • Confirmed, skipped and overdue pending items stay.
    • Past dates are never proposed again.
    • Behaviour change: before this PR, a schedule edit only moved the pointer forward and left stale pending items behind. The new behaviour applies to every frequency.
  • Monthly equivalent (monthlyEquivalentEur, monthlyFixedCostsEur and the income/savings totals in get_recurring): amount × 365 / (interval × 12) for daily items. The formula moved into the domain as RecurringTransaction.MonthlyEquivalent.
  • MCP: upsert_recurring accepts frequency: "daily", and the interval and day_of_month descriptions are updated. Argument validation stays strict. get_recurring returns frequency: "Daily" (same casing as the existing values) plus interval. docs/ai.md is updated.
  • UI:
    • The frequency selector has a new option "A cada N dias" / "Every N days", with a "A cada quantos dias" field (1–365) next to the start date.
    • The list shows "A cada 15 dias" (or "Todos os dias" when N is 1).
    • At 390px, the schedule and next date move under the name, so they no longer sit off-screen in a horizontally scrolled column.
    • The form now sends interval. Before, saving a "weekly × 2" item from the UI silently reset it to 1.
    • Changing the frequency in the form resets the interval to 1.
  • Persistence: enums are stored as strings (HaveConversion<string>, varchar(32), no check constraint), so no migration was needed. Daily = 3 is appended at the end anyway.

Weekly normalisation finding

Weekly items were normalised as amount × 52 / 12 / interval (4.333 weeks per month). The intent is the same as the 365-day formula (average occurrences per month), so weekly now uses amount × 365 / (7 × interval × 12) (4.345 weeks per month). That makes weekly × 1 identical to every 7 days, and weekly × 2 identical to every 14 days. Weekly monthly-equivalents rise by about 0.27%. Monthly (amount / interval) and yearly (amount / (12 × interval)) are unchanged.

Test plan

  • dotnet build (0 warnings)
  • Finance.Tests (79):
    • every 15 days from 2026-10-01 gives 10-01, 10-16, 10-31, 11-15, 11-30
    • DST days 2026-10-25 and 2027-03-28 keep their date
    • end_on mid-cycle
    • interval 15 → 14 recalculates only future occurrences
    • moving start_on
    • an amount-only edit keeps the schedule
    • day_of_month ignored for daily items
    • monthly cost for 1, 7, 15 and 30 days, plus weekly/monthly/yearly
    • validator: interval bounds per frequency, day_of_month rejected for daily
  • Architecture.Tests (18)
  • Integration (FinanceFlow, AiWrite*, AiGateway, AiToolCoverage, Recurring):
    • API: daily create, overdue + upcoming pending items, nextDueOn, 400 on dayOfMonth/366, interval 15 → 14 regenerates pending items while the confirmed one stays
    • AI gateway upsert_recurring daily: pending item proposed, get_recurring frequency/interval/monthly equivalent/upcoming, 400 on day_of_month/366/monthly 30, interval edit
  • Web: ng build, ng test (90, including the new schedule.spec.ts for labels, request body and form rules)
  • Full Docker E2E locally: production images, playwright test passed, stack torn down
  • Screenshots of the recurring list and edit form with a mocked API, at 1280 light and 390 dark (PT and EN)

Known limitation: "today" is still the UTC calendar day, the same as the rest of the app (proposer, AI tools). With Europe/Lisbon at UTC+0/+1, a schedule can switch to the next day up to one hour late around midnight. Occurrence dates themselves don't depend on time zone.

Adds a Daily frequency: occurrences on start_on + k x interval days (1-365),
counted with DateOnly so DST never moves one. day_of_month is refused for
daily items and ignored if stored.

- Next due is the first occurrence on or after today (list and get_recurring).
- Editing the schedule drops pending proposals dated today or later and
  proposes again from the new anchor; confirmed, skipped and overdue ones stay.
- Monthly equivalent: amount x 365 / (days x 12) for daily; weekly aligned to
  the same 365-day basis (was x 52 / 12).
- upsert_recurring accepts frequency daily, interval 1-365 for daily.
- UI: 'Every N days' option with a days field; list shows 'Every 15 days'
  and, on narrow screens, schedule and next date under the name.
@joaoguimaraespro
joaoguimaraespro merged commit 02adafe into main Oct 5, 2026
7 checks passed
@joaoguimaraespro
joaoguimaraespro deleted the feat/recurring-every-n-days branch October 5, 2026 21:37
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