Remove N+1 subscription invoice scanning, clean up billing service layer, and simplify InvoicesList component to single-purpose.
3.7 KiB
Invoice System Optimization Design
Date: 2026-03-05 Status: Approved
Problem
Subscription invoice fetching is slow due to an N+1 query pattern. When viewing a subscription's invoices, the BFF:
- Fetches ALL client invoices page by page (full table scan)
- For each invoice, makes an individual
GetInvoiceWHMCS API call to get line items - Filters in-memory by
item.serviceId === subscriptionId
For a client with 19 invoices: 1 list call + 19 individual detail calls = 20 WHMCS API calls with batching delays.
Decision
Remove subscription-specific invoice fetching entirely. Most billing portals (Stripe, AWS, DigitalOcean) don't offer per-subscription invoice lists. The WHMCS GetInvoices API doesn't support filtering by service/subscription ID, making this fundamentally expensive.
Instead:
- Subscription detail page shows billing summary (already available on subscription object) + link to main invoices page
- Main invoices page uses efficient single
GetInvoicescall (already working well) - Individual invoice detail already shows which subscription each line item belongs to
Additionally, clean up the billing service layer by removing the pass-through BillingOrchestrator.
Changes
Files to DELETE
apps/bff/src/modules/billing/services/billing-orchestrator.service.ts- zero-logic pass-through
BFF Files to EDIT
-
subscriptions-orchestrator.service.ts- Remove:getSubscriptionInvoices(),tryGetCachedInvoices(),fetchAllRelatedInvoices(),paginateInvoices(),cacheInvoiceResults()(~150 lines)- Related type imports (
InvoiceItem,InvoiceList) WhmcsInvoiceServicedependency (if no other method uses it)
-
subscriptions.controller.ts- Remove:GET :id/invoicesendpointSubscriptionInvoiceQueryDto,InvoiceListDtoDTOsinvoiceListSchema,InvoiceList,ValidationimportssubscriptionInvoiceQuerySchema
-
whmcs-invoice.service.ts- Remove:getInvoicesWithItems()method (~70 lines)chunkArray,sleepimports (if unused after removal)
-
whmcs-cache.service.ts- Remove:subscriptionInvoices+subscriptionInvoicesAllcache configsgetSubscriptionInvoices(),setSubscriptionInvoices(),getSubscriptionInvoicesAll(),setSubscriptionInvoicesAll()buildSubscriptionInvoicesKey(),buildSubscriptionInvoicesAllKey()- Subscription invoice patterns in invalidation methods
-
billing.controller.ts- ReplaceBillingOrchestratorwith directWhmcsPaymentService+WhmcsSsoServiceinjection -
billing.module.ts- RemoveBillingOrchestratorfrom providers/exports
Portal Files to EDIT
-
InvoiceList.tsx- Remove dual-mode logic:- Remove
subscriptionId,showFiltersprops - Remove
useSubscriptionInvoicesimport - Remove
useInvoicesData()helper, useuseInvoices()directly - Remove
isSubscriptionModeconditionals - Simplify
InvoicesFilterBar(remove conditional spread)
- Remove
-
SubscriptionDetail.tsx- Replace<InvoicesList>with billing link:- Remove
InvoicesListandInvoiceListSkeletonimports - Replace billing history section with "View all invoices" link to
/account/billing
- Remove
-
app/account/subscriptions/[id]/loading.tsx- RemoveInvoiceListSkeleton -
useSubscriptions.ts- RemoveuseSubscriptionInvoices()hook -
core/api/index.ts- Removesubscriptions.invoicesquery key
Impact
- Eliminates 20+ WHMCS API calls per subscription detail view
- Removes ~300 lines of scanning/caching/batching code
- Simplifies InvoicesList to single-purpose component
- Removes unused service abstraction layer (BillingOrchestrator)
- No feature regression: invoice data still accessible via main billing page