Skip to content

feat(customers): group credit notes by entity#5565

Merged
aquinofb merged 2 commits into
mainfrom
ing-209
May 25, 2026
Merged

feat(customers): group credit notes by entity#5565
aquinofb merged 2 commits into
mainfrom
ing-209

Conversation

@aquinofb

Copy link
Copy Markdown
Contributor

Context

The customer detail page is replacing its single "Total amount available" credit-notes summary card with a per-(currency × billing entity) breakdown table, mirroring the Invoice balances treatment that's landing alongside it. Today the customer.creditNotesBalances GraphQL field only groups by currency, so customers transacting across multiple billing entities cannot see their balances split by entity, and there's no per-bucket count of credit notes for the row subtitle.

Description

CustomerCreditNotesBalance now exposes two new non-null fields (billingEntityId: ID! and creditsAvailableCount: Int!) in addition to the existing currency and amountCents. The customer.creditNotesBalances resolver groups finalized credit notes by both currency and billing entity (joining invoices) and emits one row per (currency, billingEntity) pair, returning the sum of balance_amount_cents and the count of finalized credit notes with credit_amount_cents > 0.

The previous balance_amount_cents > 0 filter is removed so buckets with fully-consumed credits still flow through: this is required so the consumer can render the "credit notes issued & credited" subtitle even when the available balance is zero. The consumer applies visibility client-side via amountCents > 0 OR creditsAvailableCount > 0.

Old vs new response shape for the same customer (one billing entity, single currency, and one cross-entity case):

Customer Before After
Single-entity, single-currency [{ currency: "EUR", amountCents: 1500 }] [{ currency: "EUR", billingEntityId: "...", amountCents: 1500, creditsAvailableCount: 3 }]
Cross-currency, cross-entity (3 buckets) 1 row per currency only 1 row per (currency, billingEntity) pair
Fully-consumed bucket omitted included with amountCents: 0 and creditsAvailableCount > 0

Implementation notes:

  • Uses PostgreSQL's COUNT(*) FILTER (WHERE ...) rather than COUNT(CASE WHEN ...). Lago is PG-only; the FILTER form is equivalent and more idiomatic.
  • Raw SQL aggregates are wrapped in Arel.sql(...) to satisfy Rails 8's disable_joins-style protection against unsafe pluck inputs.
  • joins(:invoice) is safe: credit_notes.invoice_id is NOT NULL with a FK, and invoices.billing_entity_id is NOT NULL at the DB level, so no rows are dropped and the new ID! typing is sound.
  • schema.graphql and schema.json are regenerated.

How to try locally

  1. Check out the branch and rebuild the API container if needed.
  2. Seed or open a customer with credit notes across multiple currencies and billing entities (a quick rails console script that creates a couple of :billing_entity, a couple of :invoice, and several :credit_note records works).
  3. Run the GraphQL query against the customer:
    query($id: ID!) {
      customer(id: $id) {
        creditNotesBalances {
          currency
          billingEntityId
          amountCents
          creditsAvailableCount
        }
      }
    }
    Confirm: one row per (currency, billingEntityId) pair, amounts sum correctly per bucket, and counts only include credit notes with credit_amount_cents > 0.
  4. Verify a single-entity, single-currency customer still returns exactly one row (no regression).
  5. bundle exec rspec spec/graphql/types/customers/object_spec.rb spec/graphql/lago_api_schema_spec.rb to confirm the new behavior specs and the schema-dump cross-check both pass.

aquinofb added 2 commits May 22, 2026 10:26
## Context

The customer detail page is gaining a per-(currency × billing entity)
breakdown table for credit notes balances, replacing the single
"Total amount available" card. The previous resolver only grouped by
currency, so multi-entity customers could not see balances split by
entity, and there was no count of credit notes per bucket.

## Description

`CustomerCreditNotesBalance` now exposes `billingEntityId` and
`creditsAvailableCount` alongside `currency` and `amountCents`. The
`customer.creditNotesBalances` resolver groups by both currency and
billing entity (joining invoices) and emits one row per pair, with
the sum of balances and the count of finalized credit notes that
issued credits. The `balance_amount_cents > 0` filter is removed so
buckets with fully-consumed credits still flow through; the consumer
applies visibility client-side. Specs cover single, multi-currency,
multi-entity, combined, fully-consumed, and empty-customer scenarios.
## Context

CI's `GraphQL/ResolverMethodLength` cop caps resolver methods at 10
lines; the new `#credit_notes_balances` came in at 11.

## Description

Chain `.joins(:invoice)` onto the same line as `object.credit_notes.finalized`
to drop one line. No behavior change.
@aquinofb
aquinofb requested review from a team, domenicofalco, lovrocolic and mariohd May 22, 2026 13:40
Comment thread app/graphql/types/customers/object.rb
@aquinofb
aquinofb merged commit 60a0cfe into main May 25, 2026
13 checks passed
@aquinofb
aquinofb deleted the ing-209 branch May 25, 2026 11:55
D1353L pushed a commit that referenced this pull request May 26, 2026
## Context

The customer detail page is replacing its single "Total amount
available" credit-notes summary card with a per-(currency × billing
entity) breakdown table, mirroring the Invoice balances treatment that's
landing alongside it. Today the `customer.creditNotesBalances` GraphQL
field only groups by currency, so customers transacting across multiple
billing entities cannot see their balances split by entity, and there's
no per-bucket count of credit notes for the row subtitle.

## Description

`CustomerCreditNotesBalance` now exposes two new non-null fields
(`billingEntityId: ID!` and `creditsAvailableCount: Int!`) in addition
to the existing `currency` and `amountCents`. The
`customer.creditNotesBalances` resolver groups finalized credit notes by
both currency and billing entity (joining `invoices`) and emits one row
per `(currency, billingEntity)` pair, returning the sum of
`balance_amount_cents` and the count of finalized credit notes with
`credit_amount_cents > 0`.

The previous `balance_amount_cents > 0` filter is removed so buckets
with fully-consumed credits still flow through: this is required so the
consumer can render the "credit notes issued & credited" subtitle even
when the available balance is zero. The consumer applies visibility
client-side via `amountCents > 0 OR creditsAvailableCount > 0`.

Old vs new response shape for the same customer (one billing entity,
single currency, and one cross-entity case):

| Customer | Before | After |
| --- | --- | --- |
| Single-entity, single-currency | `[{ currency: "EUR", amountCents:
1500 }]` | `[{ currency: "EUR", billingEntityId: "...", amountCents:
1500, creditsAvailableCount: 3 }]` |
| Cross-currency, cross-entity (3 buckets) | 1 row per currency only | 1
row per `(currency, billingEntity)` pair |
| Fully-consumed bucket | omitted | included with `amountCents: 0` and
`creditsAvailableCount > 0` |

Implementation notes:
- Uses PostgreSQL's `COUNT(*) FILTER (WHERE ...)` rather than
`COUNT(CASE WHEN ...)`. Lago is PG-only; the FILTER form is equivalent
and more idiomatic.
- Raw SQL aggregates are wrapped in `Arel.sql(...)` to satisfy Rails 8's
`disable_joins`-style protection against unsafe `pluck` inputs.
- `joins(:invoice)` is safe: `credit_notes.invoice_id` is `NOT NULL`
with a FK, and `invoices.billing_entity_id` is `NOT NULL` at the DB
level, so no rows are dropped and the new `ID!` typing is sound.
- `schema.graphql` and `schema.json` are regenerated.

## How to try locally

1. Check out the branch and rebuild the API container if needed.
2. Seed or open a customer with credit notes across multiple currencies
and billing entities (a quick `rails console` script that creates a
couple of `:billing_entity`, a couple of `:invoice`, and several
`:credit_note` records works).
3. Run the GraphQL query against the customer:
   ```graphql
   query($id: ID!) {
     customer(id: $id) {
       creditNotesBalances {
         currency
         billingEntityId
         amountCents
         creditsAvailableCount
       }
     }
   }
   ```
Confirm: one row per `(currency, billingEntityId)` pair, amounts sum
correctly per bucket, and counts only include credit notes with
`credit_amount_cents > 0`.
4. Verify a single-entity, single-currency customer still returns
exactly one row (no regression).
5. `bundle exec rspec spec/graphql/types/customers/object_spec.rb
spec/graphql/lago_api_schema_spec.rb` to confirm the new behavior specs
and the schema-dump cross-check both pass.
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.

3 participants