Skip to content

feat: replace billing_date with billing_date_from/to for multi-month …#99

Merged
emilwojtaszek merged 3 commits into
mainfrom
feat/billing-date-range
Mar 23, 2026
Merged

feat: replace billing_date with billing_date_from/to for multi-month …#99
emilwojtaszek merged 3 commits into
mainfrom
feat/billing-date-range

Conversation

@emilwojtaszek

@emilwojtaszek emilwojtaszek commented Mar 23, 2026

Copy link
Copy Markdown
Member

…cost allocation

Invoices can now span multiple billing months (e.g. quarterly contracts). The net_amount is proportionally allocated across each month in the range, with rounding remainder assigned to the last month.

  • Migration: add billing_date_from/to, backfill from billing_date, drop old column
  • Schema: validate both fields as first-of-month, enforce from <= to
  • Context: expand multi-month invoices in aggregation queries (expense_monthly_totals, expense_by_category, income_monthly_summary), overlap-based filter semantics
  • API: replace billing_date with billing_date_from/to in request/response schemas
  • LiveView: dual month picker for billing period editing, format_billing_period display
  • ADR 0034 created, ADR 0033 superseded

Summary by CodeRabbit

  • New Features

    • Invoices now use billing-period ranges (billing_date_from / billing_date_to) with From/To month inputs, overlap-based filters, and proportional multi-month allocations (deterministic rounding).
  • Documentation

    • ADR 0034 added; ADR 0033 marked superseded. OpenAPI schemas and API docs updated to describe range fields and constraints.
  • User Interface

    • Invoice views/forms and CSV export display billing periods as single-month or ranged labels.
  • Tests

    • Suite updated for range semantics, validations, allocations, and API payloads.
  • Chores

    • Database migration to replace single billing_date with billing_date_from/to.

@coderabbitai

coderabbitai Bot commented Mar 23, 2026

Copy link
Copy Markdown

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1ef7ed4d-559e-463f-878a-2d46f67dc42b

📥 Commits

Reviewing files that changed from the base of the PR and between d28573f and c30f777.

📒 Files selected for processing (2)
  • lib/ksef_hub/exports/csv_builder.ex
  • test/ksef_hub/exports/csv_builder_test.exs

📝 Walkthrough

Walkthrough

Replaces single billing_date with a billing-period range (billing_date_from, billing_date_to), updates validation (first-of-month, to >= from), changes filter semantics to overlap-based matching, implements deterministic per-month proportional allocations for multi-month invoices, and updates schema, API, UI, migration, factories, and tests.

Changes

Cohort / File(s) Summary
Architecture Documentation
docs/adr/0033-billing-date-field.md, docs/adr/0034-billing-date-range.md
ADR 0033 marked superseded; ADR 0034 added defining billing-range semantics, overlap filters, allocation/rounding rules, and migration/backfill plan.
Invoice Schema & Changesets
lib/ksef_hub/invoices/invoice.ex
Removed billing_date; added billing_date_from/billing_date_to; updated casts, changesets, and new range validations (first-of-month, to >= from, nullable handling).
Business Logic & Aggregations
lib/ksef_hub/invoices.ex
Filter semantics changed to range-overlap; added helpers to expand invoices into per-month allocations, deterministic rounding/remainder handling, trimming to windows, and updated expense_monthly_totals, expense_by_category, and income_monthly_summary to use per-month allocations.
API Layer & OpenAPI Schemas
lib/ksef_hub_web/controllers/api/invoice_controller.ex, lib/ksef_hub_web/schemas/*.ex
Replaced request/response billing_date with billing_date_from/billing_date_to; updated allowed create/update keys, OpenAPI schemas and index parameter docs to describe overlap semantics and patterns.
UI Components & LiveView
lib/ksef_hub_web/components/invoice_components.ex, lib/ksef_hub_web/live/invoice_live/show.ex
Added format_billing_period/2; invoice show/edit UI changed to two month inputs, parameter normalization updated, save handler now submits billing range and rebuilds forms.
Database Migration
priv/repo/migrations/20260323221841_replace_billing_date_with_range.exs
Adds billing_date_from/billing_date_to, backfills by copying old billing_date into both, drops old column/index, creates composite index; down/0 refuses rollback if multi-month rows exist.
Exports / CSV
lib/ksef_hub/exports/csv_builder.ex
CSV billing column now uses format_billing_period/2 (new), replacing previous format_month/1.
Tests & Factories
test/**, test/support/factory.ex
Factories updated to produce billing_date_from/billing_date_to; tests converted to range semantics; added allocation and rounding tests, adjusted listing/filtering and controller tests.

Sequence Diagram(s)

sequenceDiagram
    actor Client
    participant API as API Controller
    participant Service as Invoices Service
    participant Repo as Database
    participant Alloc as Allocator
    Client->>API: GET expense_monthly_totals(params)
    API->>Service: build query with overlap filters
    Service->>Repo: fetch invoices (billing_date_from, billing_date_to, net_amount, metadata)
    Repo-->>Service: invoice rows
    Service->>Alloc: expand rows into monthly allocations (allocate_across_months)
    Alloc-->>Service: per-month allocations (month, amount, metadata)
    Service->>Service: trim to window, group, sum amounts
    Service-->>API: aggregated monthly results
    API-->>Client: return response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐇 I hopped the ledger, split the span,
From month to month I made my plan.
I slice, I round, I keep the sum,
From start to end—no cents undone.
A carrot tally—neat and done.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: replace billing_date with billing_date_from/to for multi-month …' directly and clearly summarizes the main change: replacing a single billing date field with a range to support multi-month invoices.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/billing-date-range

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
lib/ksef_hub_web/live/invoice_live/show.ex (1)

396-406: ⚠️ Potential issue | 🟠 Major

Refresh edit_form after saving the billing period.

The full details editor still includes billing_date_from / billing_date_to, but this branch only refreshes invoice and billing_date_form. If both editors are open, the main form keeps the old range and a later save_edit can overwrite the just-saved billing period with stale values.

Suggested fix
         {:noreply,
          socket
          |> put_flash(:info, "Billing period updated.")
          |> assign(
            invoice: reloaded,
+           edit_form: build_edit_form(reloaded),
            editing_billing_date: false,
            billing_date_form: billing_date_form(reloaded)
          )}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/ksef_hub_web/live/invoice_live/show.ex` around lines 396 - 406, The
billing period save branch updates invoice and billing_date_form but fails to
refresh the main edit form, causing stale billing_date_from/to to persist and
potentially overwrite the saved period; modify the {:ok, updated} branch (in the
save billing period handler where reload_details(updated, socket) is called and
billing_date_form(reloaded) is assigned) to also regenerate and assign the edit
form (e.g., assign edit_form: edit_form(reloaded) or the equivalent function
used to build the main editor) and clear any editing flags so both editors
reflect the fresh billing period values.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/ksef_hub/invoices.ex`:
- Around line 1102-1117: The expanded allocations returned by
expand_to_monthly_allocations() are not being trimmed to the requested billing
window, so expense_monthly_totals/2 (and the analogous income_monthly_totals
function) still include months outside filters.billing_date_from /
filters.billing_date_to; after Repo.all() |> expand_to_monthly_allocations()
insert a filter step that drops any allocation whose billing_date is before
filters.billing_date_from or after filters.billing_date_to (guarding when those
keys are present), then continue grouping and summing—i.e., filter the list of
allocations by comparing each allocation.billing_date to
filters.billing_date_from and filters.billing_date_to before Enum.group_by to
prevent leakage outside the requested period.

In `@lib/ksef_hub/invoices/invoice.ex`:
- Around line 372-390: validate_billing_date_range/1 currently only enforces
ordering when both :billing_date_from and :billing_date_to are present, allowing
one-sided ranges; update validate_billing_date_range to require either both nil
or both set: if both present, keep the Date.compare check and add_error on
:billing_date_to when from > to; if exactly one is present (from xor to), add an
error on the missing field (e.g. on :billing_date_from when :billing_date_to is
set, and on :billing_date_to when :billing_date_from is set) so the changeset
enforces that both endpoints must be provided. Ensure you keep the function name
validate_billing_date_range and the field names :billing_date_from and
:billing_date_to so callers like validate_billing_dates continue to work.

In `@priv/repo/migrations/20260323221841_replace_billing_date_with_range.exs`:
- Around line 21-35: The down/0 currently overwrites multi-month ranges by
setting billing_date = billing_date_from; instead, add a guard at the top of
down/0 to detect any rows where billing_date_to IS NOT NULL AND billing_date_to
> billing_date_from and abort the rollback (raise an Ecto.MigrationError or
similar) so the migration fails rather than losing data; specifically, in the
down/0 function (where execute "UPDATE invoices SET billing_date =
billing_date_from" is used) run a SQL check via execute or Repo.query to count
offending rows (using billing_date_from and billing_date_to) and raise if count
> 0, or alternatively mark the migration irreversible by raising immediately
when such rows exist, before performing the alter/drop/create steps.

---

Outside diff comments:
In `@lib/ksef_hub_web/live/invoice_live/show.ex`:
- Around line 396-406: The billing period save branch updates invoice and
billing_date_form but fails to refresh the main edit form, causing stale
billing_date_from/to to persist and potentially overwrite the saved period;
modify the {:ok, updated} branch (in the save billing period handler where
reload_details(updated, socket) is called and billing_date_form(reloaded) is
assigned) to also regenerate and assign the edit form (e.g., assign edit_form:
edit_form(reloaded) or the equivalent function used to build the main editor)
and clear any editing flags so both editors reflect the fresh billing period
values.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0e2a06af-50ba-4af1-a1ae-d6de6791bf75

📥 Commits

Reviewing files that changed from the base of the PR and between 5f8446c and 3638966.

📒 Files selected for processing (14)
  • docs/adr/0033-billing-date-field.md
  • docs/adr/0034-billing-date-range.md
  • lib/ksef_hub/invoices.ex
  • lib/ksef_hub/invoices/invoice.ex
  • lib/ksef_hub_web/components/invoice_components.ex
  • lib/ksef_hub_web/controllers/api/invoice_controller.ex
  • lib/ksef_hub_web/live/invoice_live/show.ex
  • lib/ksef_hub_web/schemas/create_invoice_request.ex
  • lib/ksef_hub_web/schemas/invoice.ex
  • lib/ksef_hub_web/schemas/update_invoice_request.ex
  • priv/repo/migrations/20260323221841_replace_billing_date_with_range.exs
  • test/ksef_hub/invoices_test.exs
  • test/ksef_hub_web/controllers/api/invoice_controller_test.exs
  • test/support/factory.ex

Comment thread lib/ksef_hub/invoices.ex
Comment thread lib/ksef_hub/invoices/invoice.ex
emilwojtaszek and others added 2 commits March 24, 2026 00:03
…cost allocation

Invoices can now span multiple billing months (e.g. quarterly contracts).
The net_amount is proportionally allocated across each month in the range,
with rounding remainder assigned to the last month.

- Migration: add billing_date_from/to, backfill from billing_date, drop old column
- Schema: validate both fields as first-of-month, enforce from <= to
- Context: expand multi-month invoices in aggregation queries (expense_monthly_totals,
  expense_by_category, income_monthly_summary), overlap-based filter semantics
- API: replace billing_date with billing_date_from/to in request/response schemas
- LiveView: dual month picker for billing period editing, format_billing_period display
- ADR 0034 created, ADR 0033 superseded

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Trim expanded monthly allocations to the requested billing window so
  expense_monthly_totals and expense_by_category don't leak months
  outside the filter range
- Require both billing_date_from and billing_date_to or neither
  (reject one-sided ranges in validate_billing_date_range)
- Guard migration rollback against data loss when multi-month ranges
  exist (raise instead of silently truncating to billing_date_from)
- Refresh edit_form after billing period save in LiveView to prevent
  stale values from overwriting the saved period

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@emilwojtaszek emilwojtaszek force-pushed the feat/billing-date-range branch from e874327 to d28573f Compare March 23, 2026 23:06
…d billing_date

The CsvBuilder and its tests still referenced the old billing_date field
which was replaced by billing_date_from/billing_date_to in the schema.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@emilwojtaszek emilwojtaszek merged commit 42da2e5 into main Mar 23, 2026
2 checks passed
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