feat(payment-terms): structured payment terms on billing entity and customer - #4179
Draft
sarkissianraffi wants to merge 8 commits into
Draft
sarkissianraffi wants to merge 8 commits into
sarkissianraffi wants to merge 8 commits into
Conversation
AllanMichay
reviewed
Aug 31, 2026
AllanMichay
left a comment
Contributor
There was a problem hiding this comment.
Reviewed the full diff. Types are clean, the deleted translation keys have no remaining production references, and the date-math unit tests are solid. Four comments inline, plus one process note: the generated types were produced against a locally-derived schema (schema.local.graphql, per the new .gitignore entry) that includes the unmerged payment-terms API branch. UpdateCustomerInput.paymentTerm / UpdateBillingEntityInput.paymentTerm aren't confirmed live on staging. If this merges first, the unknown-field validation error kills the whole operation rather than just the row, so GetCustomerSettings, getBillingEntitySettings and getQuote all blank out. Worth gating the merge on the API side landing.
The term type list on a customer now leads with an inherit choice labelled with the value the billing entity would give, and picking it clears the override the same way the delete dialog does. Until now the shared form already accepted an `inheritedFrom` option but nothing passed it, so clearing an override was only reachable through a separate dialog. The billing entity is the last level of the chain, so it gets no such choice. Also aligns the six term-type descriptions with the copy in the design, and points the customer settings query at the dialog's fragment instead of a hand-written copy of the same selection.
…ias was read `net_payment_term` survives as a two-way alias, but the API returns it as NULL for the four term types it cannot express. Every remaining reader of the integer would therefore render "-" or the wrong copy as soon as an account picks one of those types. The quote document now resolves the term through customer -> billing entity -> due on receipt and formats it with the shared helper, so all six types read correctly. Quotes carry no term of their own, per the product decision in the backend dive-in, so this stays a resolved read-only display. The dunning email drops its payment-terms sentence outright (spec #1 §7): with terms set per customer and billing entity, one generic sentence can no longer state one truthfully. The API branch already removed it from the mailer. Removes the legacy copy left unused by this migration, in base and every locale.
…aphQL The committed types were derived from a locally composed schema, which the branch gitignored as schema.local.graphql. lago-api#6118 is now mergeable, so the local API can serve api main plus the payment-terms GraphQL directly and codegen no longer needs the composed file. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ype required
Four defects in the dialog and its preview, all reported on the pull request.
The inherit option carried an empty value, which `ComboBoxField` normalises to
`undefined` and `ComboBox` renders as `value={value || null}`: it could never
show as selected, and picking it visibly cleared the input. It now carries an
`inherit` sentinel, mapped back to a `null` payment term on submit.
An absent term type passed validation, so a billing entity that had never had a
term spun its Save button forever with no field error. `termType` is now
required, which also stops the customer Add flow from sending a no-op clear and
reporting "successfully deleted": inheriting a level that carries no term of its
own closes without a mutation.
The due-date preview took the browser zone, where `endOf('month')` is
23:59:59.999, so every End-of-month term previewed as the 1st of the next month
anywhere west of Greenwich. The preview is now anchored to a fixed example
issuing date and computed as a calendar date in UTC.
The schema's `message: ''` placeholders are replaced with real keys — Zod v4
substitutes its own untranslated "Invalid input" for an empty message.
Also drops the last `netPaymentTerm` fixtures from the quote tests, which the
structured term replaced.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
One spec drives every term type off a TERM_CASES table, so a new option is a new row rather than a new spec file: the billing entity sets each of the six types and asserts the settings row, and the customer overrides its billing entity then deletes the override to inherit again. Neither settings row carried a test hook, so `SettingsListItem` takes an optional `dataTest` and the payment-term rows and their actions expose ids from a shared `paymentTerms/dataTestConstants`. The billing-entity page passes each row's own id through, giving every invoice-setting row a hook. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
AllanMichay
force-pushed
the
feat/payment-terms-front
branch
from
September 10, 2026 12:47
d3a2074 to
4864411
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Replaces the single
net_payment_terminteger with the structuredpayment_termobject from spec #1, across the two levels the API supports today.Tickets
Covers every front-end ticket of the
[Core] Migration to new payment termmilestone and, because all six types ship at once rather than the parity subset, the[Feature] Add more payment terms optionsmilestone too.src/core/constants/paymentTerm.ts. NoSUPPORTED_TERM_TYPESgate: all six types ship together, so it has nothing to hidesrc/core/utils/paymentTerm.ts(paymentTermDueDate,buildPaymentTermInput,resolvePaymentTerm) andsrc/hooks/usePaymentTerm.tsfor the label copy, which needstranslateEditPaymentTermDialog.tsx+paymentTerms/PaymentTermFormContent.tsx. Superset of its scope — all six types, not thedue_on_receipt+netsubsetBillingEntityInvoiceSettings.tsx,CustomerSettings.tsx,DeleteCustomerPaymentTermDialog.tsxcypress/e2e/10-resources/t100-payment-terms.cy.ts. Itsdata-testhooks were not already in place, so this adds themQuotePaymentTermLinetakes the structured term; resolution happens once inEditQuote.tsxnetPaymentTermleft.src/generated/graphql.tsxkeeps its own, by design — BIL-526 keeps the alias as a permanent mirrorend_of_monthnet_end_of_month. Its own flat enum value, not ananchoron a shared type as the ticket assumesdays_end_of_month, likewise its own value, so it is not blocked by F11day_of_month+dayOfMonth/monthOffset. Bounds mirrorPaymentTerms::ValidateServiceexactly (1–31, 0–12)BIL-559 (F8) is deliberately left open — the dunning sentence is deleted rather than reworded, which needs product sign-off. Details below.
Follow-ups this PR does not attempt, on the
[Feature] Add payment term options to more billing itemsmilestone: BIL-673 (subscription, unblocked), BIL-674 and BIL-675 (both still blocked on the API).What is here
Billing entity → Invoice settings → Payment terms. The row renders the resolved term and the dialog edits it across all six types: due on receipt, net, end of month, net days after end of month, days end of month, and month following invoice.
Customer settings. Same dialog, plus inheritance. The row shows the effective term marked
(inherit from billing entity)when the customer has none. The term type list leads with an inherit choice labelled with the value it would inherit, and picking it sendspaymentTerm: null.Shared pieces.
PaymentTermFormContentis the one editor every level renders.core/utils/paymentTerm.tsmirrors the API'sPaymentTerm#due_date_for, including the day-of-month clamp and roll-forward, which drives the live due-date preview in the dialog. The two end-of-month conventions take the samedaysand give different dates, so the preview is what tells them apart before saving.Legacy alias readers.
net_payment_termstays as a two-way alias, but the API returns it asNULLfor the four types it cannot express. Every remaining reader of the integer would have rendered-or wrong copy the moment an account picked one of those types:formatPaymentTermon the resolved term.End-to-end.
cypress/e2e/10-resources/t100-payment-terms.cy.tsdrives every term type off a singleTERM_CASEStable: the billing entity sets each of the six and asserts its settings row, then the customer overrides its billing entity and deletes the override to inherit again. A new term option is a new row in that table, never a new spec file.Review fixes
Four defects reported on this PR, plus one found alongside them:
usePaymentTerm.tsendOf('month')is23:59:59.999, so every End-of-month term previewed as the 1st of the next month anywhere west of Greenwich. It is now anchored to a fixed example issuing date and computed as a calendar date in UTC. Two tests pin it, one of them under a negative-offset zone.usePaymentTerm.tsvalue: '', whichComboBoxFieldnormalises toundefinedandComboBoxrenders asvalue={value || null}— it could never show as selected, and picking it visibly cleared the input. It now carries aninheritsentinel mapped back tonullon submit.EditPaymentTermDialog.tsxtermTypeis now required.EditPaymentTermDialog.tsxPaymentTermFormContent.tsxmessage: ''placeholders are real keys now — Zod v4 substitutes its own untranslated "Invalid input" for an empty message.Codegen
The types were previously generated from a locally composed schema, gitignored as
schema.local.graphql. lago-api#6118 is green and mergeable, sosrc/generated/graphql.tsxis now a plainpnpm codegenagainst apimainmerged with that branch, and both the composed file and its.gitignoreentry are gone.That refresh also picks up api-
mainschema drift that frontmain's generated file had not caught up with — hence the one-linepermissionsConstsnapshot update for the newContractsUpdatepermission.Blocked on the API, in two places
This PR cannot leave draft until lago-api#6118 merges.
.github/workflows/codegen.ymlchecks lago-api out at its default branch for apull_requestevent, regenerates, and runstsc. Against apimainthat regeneration dropsPaymentTermTypeEnumand the typecheck fails on everything here. The job is gated ondraft == false, which is why every check currently reports SKIPPED. Same for the e2e spec:cy.signup()hits a real API.Two of the four billing-object surfaces still have no GraphQL. Since this PR was opened the API branch gained
Subscription.paymentTermwithpaymentTermon both subscription inputs, andInvoice.paymentTerm(read-only, offsnapshotted_payment_term). Still absent:Wallet.paymentTermwith its wallet inputs (BIL-522 / T16) andCreateInvoiceInput.paymentTerm(BIL-521 / T15), both unstarted.Correcting an earlier version of this description: the subscription override is not one central edit.
PaymentSettingsDrawer.tsxis mounted only bysubscriptions/form/PaymentSettingsSection.tsxandsubscriptions/details-v2/SubscriptionPaymentSection.tsx. One-off invoice, the three wallet top-up surfaces and quote invoicing mountPaymentSettingsSelector.tsxinstead. Those are two components, so the[Feature] Add payment term options to more billing itemsmilestone is two changes, tracked on BIL-673, BIL-674 and BIL-675 rather than here.Quote-level overrides are out of scope by product decision (BE dive-in, "Quotes"). Quotes show the resolved term instead, which this PR delivers.
Verification
pnpm codegenpnpm typespnpm lintpnpm translations:inspectpnpm translations:ensure-consistencypnpm testTZ=America/New_York pnpm testpnpm test:e2epayment_term; blocked with the codegen job aboveTZ=Europe/Parisfails one unrelated test,DatePicker › GIVEN a year with fewer than four digits: for year 26 AD Paris carries a +00:09 LMT offset. It predates this branch (from #4220) and passes in CI's UTC.Also noticed while typechecking the specs, unrelated and left alone:
cypress/e2e/t40-wallet-top-up.cy.tsimportsDESKTOP_ACTIONS_BLOCK_TEST_ID, whichmainHeaderTestIds.tsno longer exports. Broken onmaintoo.Fixes BIL-552
Fixes BIL-553
Fixes BIL-554
Fixes BIL-555
Fixes BIL-556
Fixes BIL-557
Fixes BIL-558
Fixes BIL-560
Fixes BIL-561
Fixes BIL-562
Fixes BIL-563
Fixes BIL-564
Ref BIL-559
Ref BIL-673
Ref BIL-674
Ref BIL-675