22 BadRequestException ,
33 Injectable ,
44 NotFoundException ,
5+ Optional ,
56} from '@nestjs/common' ;
67import { db } from '@db' ;
78import {
@@ -12,6 +13,7 @@ import {
1213 isSubscriptionBillingSkuKey ,
1314} from '@trycompai/billing' ;
1415import { StripeService } from '../stripe/stripe.service' ;
16+ import { BillingCreditsService } from './billing-credits.service' ;
1517import { findOrCreateBillingCustomer } from './billing-customer' ;
1618import { BillingEntitlementsService } from './billing-entitlements.service' ;
1719import { listBillingInvoices } from './billing-invoices' ;
@@ -38,6 +40,10 @@ export class BillingService {
3840 constructor (
3941 private readonly stripeService : StripeService ,
4042 private readonly entitlements : BillingEntitlementsService ,
43+ // Optional so existing unit tests that hand-construct BillingService
44+ // (without a credits service) keep working. In production the
45+ // BillingModule always provides it.
46+ @Optional ( ) private readonly credits ?: BillingCreditsService ,
4147 ) { }
4248
4349 async getStatus ( organizationId : string ) : Promise < BillingStatus > {
@@ -67,18 +73,31 @@ export class BillingService {
6773 db . backgroundCheckRequest . count ( { where : { organizationId } } ) ,
6874 db . securityPenetrationTestRun . count ( { where : { organizationId } } ) ,
6975 ] ) ;
70- const [ invoices , preferences , usageRows ] = await Promise . all ( [
71- listBillingInvoices ( {
72- stripeService : this . stripeService ,
73- stripeCustomerId : billing ?. stripeCustomerId ?? null ,
74- } ) ,
75- getBillingPreferences ( {
76- stripeService : this . stripeService ,
77- stripeCustomerId : billing ?. stripeCustomerId ?? null ,
78- fallbackCompanyName : organization . name ,
79- } ) ,
80- listBillingUsageRows ( { organizationId, subscriptions } ) ,
81- ] ) ;
76+ const [ invoices , preferences , usageRows , creditBalances ] =
77+ await Promise . all ( [
78+ listBillingInvoices ( {
79+ stripeService : this . stripeService ,
80+ stripeCustomerId : billing ?. stripeCustomerId ?? null ,
81+ } ) ,
82+ getBillingPreferences ( {
83+ stripeService : this . stripeService ,
84+ stripeCustomerId : billing ?. stripeCustomerId ?? null ,
85+ fallbackCompanyName : organization . name ,
86+ } ) ,
87+ listBillingUsageRows ( { organizationId, subscriptions } ) ,
88+ // Sum wallet balances per product. There can be multiple
89+ // BillingCreditBalance rows per (org, product) when grants are
90+ // scoped to a specific SKU, so we aggregate before returning.
91+ this . credits
92+ ? this . credits . listBalances ( organizationId )
93+ : Promise . resolve ( [ ] ) ,
94+ ] ) ;
95+
96+ const creditBalancesByProduct = new Map < BillingProductKey , number > ( ) ;
97+ for ( const balance of creditBalances ) {
98+ const current = creditBalancesByProduct . get ( balance . productKey ) ?? 0 ;
99+ creditBalancesByProduct . set ( balance . productKey , current + balance . balance ) ;
100+ }
82101
83102 return {
84103 hasBilling : ! ! billing ,
@@ -98,6 +117,9 @@ export class BillingService {
98117 currentPeriodEnd : subscription . currentPeriodEnd ?. toISOString ( ) ?? null ,
99118 cancelAtPeriodEnd : subscription . cancelAtPeriodEnd ,
100119 } ) ) ,
120+ creditBalances : Array . from ( creditBalancesByProduct . entries ( ) ) . map (
121+ ( [ productKey , balance ] ) => ( { productKey, balance } ) ,
122+ ) ,
101123 invoices,
102124 } ;
103125 }
0 commit comments