feat: defer invoices below 5 EUR with carry-over#3525
Conversation
Add an administration invoices page that shows per-organization invoices and carry-over history. Refactor ProportionalUsageItemRow to accept organizationId as a prop so the row is reusable outside the org context. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add minInvoiceAmount to UsageModel (null for invoice views, populated for expected-usage endpoint). When usage-only total is below the threshold, a LabelHint question-mark tooltip on the Total row explains that the amount will be carried over and billed later. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…odel Move the carry-over threshold from the per-request UsageModel response into PublicBillingConfigurationDTO (served via init-data). TotalRow now reads the value from useGlobalContext rather than receiving it as a prop. Remove minInvoiceAmount from UsageModel and the overloaded toModel(). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add carryOverTotal to UsageData/UsageModel and display a 'Deferred from previous periods' row in UsageTable when carry-over is present. The dialog total now matches the actual invoice total for invoices that settled previously deferred usage. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughAdds billing carry-overs and invoices management features across backend and frontend. Introduces carryOverTotal and minUsageInvoiceAmount data fields, new API endpoints for retrieving invoices and carry-overs, and administration dashboard components for browsing organization invoices, viewing usage details, downloading PDF invoices, and tracking deferred billing amounts. Changes
Sequence Diagram(s)sequenceDiagram
participant User as Admin User
participant UI as AdministrationInvoicesView
participant OrgSec as OrgInvoicesSection
participant CarrySec as CarryOversSection
participant API as Billing API
participant DB as Backend
User->>UI: Navigate to Invoices Page
UI->>OrgSec: Render Organization Invoices Tab
UI->>CarrySec: Render Carry-Overs Tab
OrgSec->>API: Fetch Organizations
API->>DB: Query Organizations
DB-->>API: Return Organizations List
API-->>OrgSec: Organizations Data
OrgSec->>API: Fetch Invoices (with pagination)
API->>DB: Query Invoices
DB-->>API: Return Invoices
API-->>OrgSec: Invoices List + Metadata
User->>OrgSec: Click Invoice Download
OrgSec->>API: GET /invoices/{id}/pdf
API->>DB: Retrieve Invoice PDF
DB-->>API: PDF Binary
API-->>OrgSec: PDF Blob
OrgSec->>User: Trigger Browser Download
User->>OrgSec: Click Invoice Usage Details
OrgSec->>API: GET /invoices/{id}/usage
API->>DB: Fetch Usage Data
DB-->>API: UsageData (with carryOverTotal)
API-->>OrgSec: Usage Details
OrgSec->>User: Display UsageTable (including CarryOverRow)
CarrySec->>API: Fetch Active Carry-Overs
API->>DB: Query Carry-Overs
DB-->>API: CarryOverModel List
API-->>CarrySec: Carry-Over Data
CarrySec->>User: Display Carry-Over History
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
📝 Coding Plan
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 8
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
webapp/src/ee/billing/common/usage/ProportionalUsageItemRow.tsx (1)
9-15:⚠️ Potential issue | 🟠 MajorType-safe invoice downloads by requiring both IDs together.
The optional
organizationIdprop causes silent feature loss.AdminInvoiceUsage.tsx(lines 55–59) passesinvoiceIdbut omitsorganizationId, and the runtime guard on lines 23–25 silently disables the download action. This compiles cleanly despite being incomplete.Model these props as a union type so invoice-backed rows require both IDs together—making the error obvious at compile time instead of discovering it at runtime:
💡 Tighten the prop contract
-export const ProportionalUsageItemRow = (props: { - item: components['schemas']['AverageProportionalUsageItemModel']; - invoiceId?: number; - invoiceNumber?: string; - type: ProportionalUsageType; - organizationId?: number; -}) => { +type ProportionalUsageItemRowProps = + | { + item: components['schemas']['AverageProportionalUsageItemModel']; + invoiceId: number; + invoiceNumber?: string; + type: ProportionalUsageType; + organizationId: number; + } + | { + item: components['schemas']['AverageProportionalUsageItemModel']; + invoiceId?: undefined; + invoiceNumber?: undefined; + type: ProportionalUsageType; + organizationId?: undefined; + }; + +export const ProportionalUsageItemRow = ( + props: ProportionalUsageItemRowProps +) => {Fix
AdminInvoiceUsage.tsxby passingorganizationIdtoUsageTable, or exclude the download feature for admin invoices if the organization context is unavailable.Also applies to: 23–30
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@webapp/src/ee/billing/common/usage/ProportionalUsageItemRow.tsx` around lines 9 - 15, The component ProportionalUsageItemRow currently accepts invoiceId? and organizationId? independently which allows passing invoiceId without organizationId and causes the download guard to silently disable the feature; change the props to a discriminated union so that either both invoiceId and organizationId are present (required together) or neither is present (e.g. { invoiceId: number; invoiceNumber?: string; organizationId: number; type: ProportionalUsageType; item: ... } | { invoiceId?: undefined; invoiceNumber?: string; organizationId?: undefined; type: ProportionalUsageType; item: ... }), update the ProportionalUsageItemRow signature accordingly, and then fix callers (e.g. AdminInvoiceUsage -> UsageTable) to pass organizationId when they pass invoiceId or intentionally omit invoice-related props if organization context is unavailable so the compiler surfaces the missing organizationId instead of failing at runtime.
🧹 Nitpick comments (6)
webapp/src/ee/billing/administration/invoices/OrgInvoicesSection.tsx (2)
79-112: Adddata-cyattribute for E2E testing.The organization filter Autocomplete should have a
data-cyattribute to enable E2E test selectors.Proposed fix
<Autocomplete<OrgItem> options={orgItems} getOptionLabel={(o) => o.name} isOptionEqualToValue={(a, b) => a.id === b.id} loading={orgsLoadable.isFetching} value={selectedOrg} onChange={(_, value) => { setSelectedOrg(value); setInvoicePage(0); }} onInputChange={(_, value) => setSearch(value)} sx={{ width: 280 }} + data-cy="admin-invoices-org-filter" renderInput={(params) => (As per coding guidelines: "STRICTLY use
data-cyattributes for E2E selectors".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@webapp/src/ee/billing/administration/invoices/OrgInvoicesSection.tsx` around lines 79 - 112, Add the missing data-cy attribute to the organization filter Autocomplete's input so E2E tests can target it: update the Autocomplete renderInput/TextField block (in the component using Autocomplete<OrgItem> and renderInput) to include a data-cy prop (e.g., data-cy="administration-invoices-filter-org") on the TextField (or its input props) so the selector is present while preserving existing params and InputProps, value/onChange handlers, and loading behavior.
114-175: Adddata-cyattribute to the list component.The invoice list should have a
data-cyattribute for E2E testing purposes.Proposed fix
<PaginatedHateoasList onPageChange={(p) => setInvoicePage(p)} listComponent={Paper} listComponentProps={{ variant: 'outlined', sx: { display: 'grid', gridTemplateColumns: 'auto auto 1fr auto auto auto', alignItems: 'center', }, + 'data-cy': 'admin-invoices-list', }}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@webapp/src/ee/billing/administration/invoices/OrgInvoicesSection.tsx` around lines 114 - 175, The PaginatedHateoasList rendering the invoices is missing a data-cy attribute for E2E tests; add a data-cy (e.g. data-cy="invoices-list") to the list component props by updating the PaginatedHateoasList call where listComponent={Paper} — put the attribute inside listComponentProps (the same object with variant/sx) so the rendered Paper element includes the data-cy for testing.webapp/src/ee/billing/administration/invoices/CarryOversSection.tsx (1)
161-170: Adddata-cyattributes for E2E testing.The Tabs component should have
data-cyattributes for E2E test selectors.Proposed fix
- <Tabs value={tab} onChange={(_, value) => setTab(value)} sx={{ mb: 2 }}> + <Tabs value={tab} onChange={(_, value) => setTab(value)} sx={{ mb: 2 }} data-cy="carry-overs-tabs"> <Tab value="active" label={t('administration_carry_overs_tab_active', 'Active')} + data-cy="carry-overs-tab-active" /> <Tab value="history" label={t('administration_carry_overs_tab_history', 'History')} + data-cy="carry-overs-tab-history" /> </Tabs>As per coding guidelines: "STRICTLY use
data-cyattributes for E2E selectors".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@webapp/src/ee/billing/administration/invoices/CarryOversSection.tsx` around lines 161 - 170, Add data-cy attributes to the Tabs and each Tab for E2E selectors: update the Tabs component (where value={tab} onChange={(_, value) => setTab(value)}) to include a data-cy like data-cy="carry-overs-tabs" and add data-cy attributes to the two Tab components (e.g., data-cy="carry-overs-tab-active" and data-cy="carry-overs-tab-history") so tests can reliably target them; keep the existing props (value, label, sx) and only add the data-cy attributes to Tabs and Tab.webapp/src/ee/billing/administration/invoices/AdminDownloadButton.tsx (2)
34-36: Redundant type assertion on blob.
res.blob()already returnsPromise<Blob>, so theas unknown as Blobcast is unnecessary.Proposed fix
async onSuccess(response) { const res = response as unknown as Response; const data = await res.blob(); - const url = URL.createObjectURL(data as unknown as Blob); + const url = URL.createObjectURL(data);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@webapp/src/ee/billing/administration/invoices/AdminDownloadButton.tsx` around lines 34 - 36, The blob cast is redundant: res.blob() returns a Promise<Blob>, so remove the unnecessary "as unknown as Blob" and pass the returned blob directly to URL.createObjectURL; ensure the earlier cast of response to Response (const res = response as unknown as Response) remains appropriate or replace with a direct Response typing for the response variable so that data is used as a Blob when calling URL.createObjectURL(data).
56-65: Adddata-cyattribute for E2E testing.The download button should have a
data-cyattribute to enable reliable E2E test selectors.Proposed fix
<LoadingButton disabled={!invoice.pdfReady} loading={pdfMutation.isLoading} onClick={onDownload} size="small" + data-cy="admin-invoice-download-button" > PDF </LoadingButton>As per coding guidelines: "STRICTLY use
data-cyattributes for E2E selectors".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@webapp/src/ee/billing/administration/invoices/AdminDownloadButton.tsx` around lines 56 - 65, The LoadingButton rendered in AdminDownloadButton lacks the required E2E selector; add a data-cy attribute to the LoadingButton (the component using props: disabled={!invoice.pdfReady}, loading={pdfMutation.isLoading}, onClick={onDownload})—e.g. data-cy="admin-invoice-download-pdf"—so tests can reliably target the PDF download button.webapp/src/ee/billing/common/usage/TotalRow.tsx (1)
21-26: Optional simplification: Redundant truthy check.The condition
usageOnlyTotal &&is redundant when followed byusageOnlyTotal > 0, since the latter already handlesundefined,null, and0cases by returningfalse. However, this is a common defensive pattern for explicitness.♻️ Optional simplification
const showHint = Boolean( minUsageInvoiceAmount && - usageOnlyTotal && usageOnlyTotal > 0 && usageOnlyTotal < minUsageInvoiceAmount );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@webapp/src/ee/billing/common/usage/TotalRow.tsx` around lines 21 - 26, Simplify the showHint boolean expression in the TotalRow component by removing the redundant "usageOnlyTotal &&" check; keep the Boolean(...) wrapper and the remaining conditions "minUsageInvoiceAmount && usageOnlyTotal > 0 && usageOnlyTotal < minUsageInvoiceAmount" so the logic is unchanged but less verbose and still handles undefined/null correctly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@backend/data/src/main/kotlin/io/tolgee/dtos/response/PublicBillingConfigurationDTO.kt`:
- Around line 5-7: PublicBillingConfigurationDTO adds minUsageInvoiceAmount but
no provider populates it; update the provider(s) to supply this value or remove
the field. Concretely, either (A) remove minUsageInvoiceAmount from
PublicBillingConfigurationDTO if unused, or (B) modify
BasePublicBillingConfProvider and all concrete providers that return
PublicBillingConfigurationDTO (look for the constructor call in
BasePublicBillingConfProvider and other providers) to pass the actual billing
minimum invoice amount (e.g., read from your billing configuration/service and
call PublicBillingConfigurationDTO(enabled, minUsageInvoiceAmount)) so the DTO
is not always null.
In `@webapp/src/ee/billing/administration/invoices/AdminInvoiceUsage.tsx`:
- Around line 55-59: The UsageTable is rendered without the optional
organizationId prop so ProportionalUsageItemRow instances receive no
organizationId and CSV download buttons remain disabled; update the UsageTable
invocation in AdminInvoiceUsage to pass organizationId={invoice.organizationId}
so the prop flows into ProportionalUsageItemRow and enables CSV downloads.
- Around line 40-51: The IconButton opening the usage dialog (the element using
setOpen(true) and labelled via t('billing_invoices_show_usage_button')) needs a
data-cy attribute for E2E selection and all translation calls must include
default values; add a data-cy (e.g. billing-invoice-show-usage-button) to the
IconButton and update the t() usages around the IconButton (aria-label and
Tooltip title) and the DialogTitle call (t('invoice_usage_dialog_title')) to
supply sensible defaultValue strings so meaningful text appears before
translations load.
In
`@webapp/src/ee/billing/administration/invoices/AdministrationInvoicesView.tsx`:
- Around line 23-28: The navigation entry in AdministrationInvoicesView uses
t('administration_organizations') without a defaultValue; update the call to
include a defaultValue (e.g., defaultValue: 'Organizations') so the navigation
label renders before translations load—locate the navigation prop in
AdministrationInvoicesView.tsx and add the defaultValue option to the t(...)
invocation (or to the T component if used) for the
'administration_organizations' key.
In `@webapp/src/ee/billing/administration/invoices/CarryOversSection.tsx`:
- Around line 76-89: Replace the hardcoded "Settled by" string in
CarryOversSection's JSX (inside the conditional that checks showSettledBy and
item.resolvedByInvoiceNumber) with a call to the project's translation function
(e.g., t('...') or i18n.t('...')); import/use the same translation hook used
elsewhere in this component (or module) and pick an appropriate key (for example
'billing.settledBy' or 'invoices.settledBy') so the Box displaying the label
becomes translated while keeping the surrounding layout and Link logic intact.
- Around line 91-97: The AmountItem label strings in CarryOversSection are
hardcoded; update the four labels ("Credits", "Seats", "Translations", "Keys")
to use the i18n translation helper used in this component (e.g., the t function
or <Trans>) so they are localized; locate the CarryOversSection component and
replace the literal label props passed to AmountItem with translated keys (e.g.,
t('billing.credits'), t('billing.seats'), t('billing.translations'),
t('billing.keys')) ensuring the appropriate translation keys are added to the
locale files.
- Around line 67-72: The Chip labels are hardcoded ("Cloud" / "Self-hosted EE");
replace them with the translation helper (use the T component or t() function)
and provide defaultValue strings per guidelines. Update the label prop in the
Chip rendering (the conditional using isCloud) to call t('Cloud', {
defaultValue: 'Cloud' }) and t('Self-hosted EE', { defaultValue: 'Self-hosted
EE' }) or wrap with <T defaultValue="..."> accordingly so both branches use
i18n.
In `@webapp/src/eeSetup/eeModule.ee.tsx`:
- Around line 141-143: The Administration invoices route is currently only
protected by PrivateRoute (authentication) and needs an admin permission guard;
update the route handling for LINKS.ADMINISTRATION_BILLING_INVOICES.template so
only admin/supporter users can access AdministrationInvoicesView. Either wrap
that <PrivateRoute> with an admin-check component (e.g., AdminGuard) or replace
PrivateRoute with an AdminRoute that calls useIsAdminOrSupporter() and redirects
non-admins to a safe page; ensure the check happens in RootRouter around the
route for AdministrationInvoicesView so direct URL access is blocked.
---
Outside diff comments:
In `@webapp/src/ee/billing/common/usage/ProportionalUsageItemRow.tsx`:
- Around line 9-15: The component ProportionalUsageItemRow currently accepts
invoiceId? and organizationId? independently which allows passing invoiceId
without organizationId and causes the download guard to silently disable the
feature; change the props to a discriminated union so that either both invoiceId
and organizationId are present (required together) or neither is present (e.g. {
invoiceId: number; invoiceNumber?: string; organizationId: number; type:
ProportionalUsageType; item: ... } | { invoiceId?: undefined; invoiceNumber?:
string; organizationId?: undefined; type: ProportionalUsageType; item: ... }),
update the ProportionalUsageItemRow signature accordingly, and then fix callers
(e.g. AdminInvoiceUsage -> UsageTable) to pass organizationId when they pass
invoiceId or intentionally omit invoice-related props if organization context is
unavailable so the compiler surfaces the missing organizationId instead of
failing at runtime.
---
Nitpick comments:
In `@webapp/src/ee/billing/administration/invoices/AdminDownloadButton.tsx`:
- Around line 34-36: The blob cast is redundant: res.blob() returns a
Promise<Blob>, so remove the unnecessary "as unknown as Blob" and pass the
returned blob directly to URL.createObjectURL; ensure the earlier cast of
response to Response (const res = response as unknown as Response) remains
appropriate or replace with a direct Response typing for the response variable
so that data is used as a Blob when calling URL.createObjectURL(data).
- Around line 56-65: The LoadingButton rendered in AdminDownloadButton lacks the
required E2E selector; add a data-cy attribute to the LoadingButton (the
component using props: disabled={!invoice.pdfReady},
loading={pdfMutation.isLoading}, onClick={onDownload})—e.g.
data-cy="admin-invoice-download-pdf"—so tests can reliably target the PDF
download button.
In `@webapp/src/ee/billing/administration/invoices/CarryOversSection.tsx`:
- Around line 161-170: Add data-cy attributes to the Tabs and each Tab for E2E
selectors: update the Tabs component (where value={tab} onChange={(_, value) =>
setTab(value)}) to include a data-cy like data-cy="carry-overs-tabs" and add
data-cy attributes to the two Tab components (e.g.,
data-cy="carry-overs-tab-active" and data-cy="carry-overs-tab-history") so tests
can reliably target them; keep the existing props (value, label, sx) and only
add the data-cy attributes to Tabs and Tab.
In `@webapp/src/ee/billing/administration/invoices/OrgInvoicesSection.tsx`:
- Around line 79-112: Add the missing data-cy attribute to the organization
filter Autocomplete's input so E2E tests can target it: update the Autocomplete
renderInput/TextField block (in the component using Autocomplete<OrgItem> and
renderInput) to include a data-cy prop (e.g.,
data-cy="administration-invoices-filter-org") on the TextField (or its input
props) so the selector is present while preserving existing params and
InputProps, value/onChange handlers, and loading behavior.
- Around line 114-175: The PaginatedHateoasList rendering the invoices is
missing a data-cy attribute for E2E tests; add a data-cy (e.g.
data-cy="invoices-list") to the list component props by updating the
PaginatedHateoasList call where listComponent={Paper} — put the attribute inside
listComponentProps (the same object with variant/sx) so the rendered Paper
element includes the data-cy for testing.
In `@webapp/src/ee/billing/common/usage/TotalRow.tsx`:
- Around line 21-26: Simplify the showHint boolean expression in the TotalRow
component by removing the redundant "usageOnlyTotal &&" check; keep the
Boolean(...) wrapper and the remaining conditions "minUsageInvoiceAmount &&
usageOnlyTotal > 0 && usageOnlyTotal < minUsageInvoiceAmount" so the logic is
unchanged but less verbose and still handles undefined/null correctly.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 2a1abfb5-1fb7-4a98-929b-5a2ad9ef9646
📒 Files selected for processing (19)
backend/api/src/main/kotlin/io/tolgee/hateoas/ee/uasge/proportional/UsageModel.ktbackend/data/src/main/kotlin/io/tolgee/dtos/response/PublicBillingConfigurationDTO.ktee/backend/app/src/main/kotlin/io/tolgee/ee/api/v2/hateoas/assemblers/UsageModelAssembler.ktee/backend/app/src/main/kotlin/io/tolgee/ee/data/UsageData.ktwebapp/src/constants/links.tsxwebapp/src/ee/billing/Invoices/InvoiceUsage.tsxwebapp/src/ee/billing/administration/invoices/AdminDownloadButton.tsxwebapp/src/ee/billing/administration/invoices/AdminInvoiceUsage.tsxwebapp/src/ee/billing/administration/invoices/AdministrationInvoicesView.tsxwebapp/src/ee/billing/administration/invoices/CarryOversSection.tsxwebapp/src/ee/billing/administration/invoices/OrgInvoicesSection.tsxwebapp/src/ee/billing/common/usage/CarryOverRow.tsxwebapp/src/ee/billing/common/usage/ExpectedUsageDialogButton.tsxwebapp/src/ee/billing/common/usage/ProportionalUsageItemRow.tsxwebapp/src/ee/billing/common/usage/TotalRow.tsxwebapp/src/ee/billing/common/usage/UsageTable.tsxwebapp/src/eeSetup/eeModule.ee.tsxwebapp/src/service/apiSchema.generated.tswebapp/src/service/billingApiSchema.generated.ts
backend/data/src/main/kotlin/io/tolgee/dtos/response/PublicBillingConfigurationDTO.kt
Show resolved
Hide resolved
webapp/src/ee/billing/administration/invoices/AdministrationInvoicesView.tsx
Show resolved
Hide resolved
webapp/src/ee/billing/administration/invoices/CarryOversSection.tsx
Outdated
Show resolved
Hide resolved
…fix missing download button for CSV
…s on invoices page
Summary by CodeRabbit