@@ -970,18 +970,18 @@ describe('POST /api/billing/update-cost — mid-run usage gate', () => {
970970 it ( 'serves a cached admission to every step and re-reads a refusal' , async ( ) => {
971971 mockCheckAttributedUsageLimits . mockResolvedValue ( { isExceeded : false } )
972972
973- for ( let step = 0 ; step < 5 ; step ++ ) {
973+ expect ( ( await ( await POST ( attributedCallback ( ) ) ) . json ( ) ) . usageExceeded ) . toBe ( false )
974+ mockCheckAttributedUsageLimits . mockResolvedValue ( { isExceeded : true , scope : 'payer' } )
975+ for ( let step = 0 ; step < 4 ; step ++ ) {
974976 const body = await ( await POST ( attributedCallback ( ) ) ) . json ( )
975977 expect ( body . usageExceeded ) . toBe ( false )
976978 expect ( body ) . not . toHaveProperty ( 'usageUpgrade' )
977979 }
978- expect ( mockCheckAttributedUsageLimits ) . toHaveBeenCalledTimes ( 1 )
979980
980981 resetUsageGateCache ( )
981- mockCheckAttributedUsageLimits . mockResolvedValue ( { isExceeded : true , scope : 'payer' } )
982- await POST ( attributedCallback ( ) )
983- await POST ( attributedCallback ( ) )
984- expect ( mockCheckAttributedUsageLimits ) . toHaveBeenCalledTimes ( 3 )
982+ expect ( ( await ( await POST ( attributedCallback ( ) ) ) . json ( ) ) . usageExceeded ) . toBe ( true )
983+ mockCheckAttributedUsageLimits . mockResolvedValue ( { isExceeded : false } )
984+ expect ( ( await ( await POST ( attributedCallback ( ) ) ) . json ( ) ) . usageExceeded ) . toBe ( false )
985985 } )
986986
987987 it ( 'answers a duplicate retry with the verdict its lost first answer carried' , async ( ) => {
@@ -1050,7 +1050,6 @@ describe('POST /api/billing/update-cost — mid-run usage gate', () => {
10501050 const body = await ( await POST ( directCallback ( ) ) ) . json ( )
10511051
10521052 expect ( body . usageExceeded ) . toBe ( false )
1053- expect ( billingUsageMonitorMockFns . mockCheckUsageStatus ) . not . toHaveBeenCalled ( )
10541053 } )
10551054 } )
10561055
@@ -1061,56 +1060,70 @@ describe('POST /api/billing/update-cost — mid-run usage gate', () => {
10611060 status : 'active' ,
10621061 seats : 4 ,
10631062 }
1063+ const ADMITTED_PERIOD = {
1064+ start : new Date ( '2026-07-01T00:00:00.000Z' ) ,
1065+ end : new Date ( '2026-08-01T00:00:00.000Z' ) ,
1066+ }
10641067 const CURRENT_PERIOD = {
10651068 start : new Date ( '2026-08-01T00:00:00.000Z' ) ,
10661069 end : new Date ( '2026-09-01T00:00:00.000Z' ) ,
10671070 }
10681071
1072+ function admittedWithSource ( source : 'stripe' | 'reporting' | 'default' ) {
1073+ mockToBillingContext . mockReturnValue ( {
1074+ billingEntity : { type : 'organization' , id : 'org-1' } ,
1075+ billingPeriod : { ...ADMITTED_PERIOD , source } ,
1076+ } )
1077+ }
1078+
1079+ /** Threshold settlement for a payer whose charges belong to `period` refuses any other. */
1080+ function settlesOnlyAgainst ( period : typeof ADMITTED_PERIOD ) {
1081+ mockCheckAndBillPayerOverageThreshold . mockImplementation (
1082+ async ( _payer : unknown , options : { expectedBillingPeriod : typeof ADMITTED_PERIOD } ) => {
1083+ if ( options . expectedBillingPeriod . start . getTime ( ) !== period . start . getTime ( ) ) {
1084+ throw new Error ( 'Settled against a period the charge did not land in' )
1085+ }
1086+ }
1087+ )
1088+ }
1089+
10691090 beforeEach ( ( ) => {
10701091 mockCheckAttributedUsageLimits . mockResolvedValue ( { isExceeded : false } )
10711092 mockRequireBillingAttributionHeader . mockReturnValue ( {
10721093 ...CURRENT_ATTRIBUTION ,
10731094 payerSubscription : PAYER_SUBSCRIPTION ,
10741095 } )
1075- mockToBillingContext . mockReturnValue ( {
1076- billingEntity : { type : 'organization' , id : 'org-1' } ,
1077- billingPeriod : {
1078- start : new Date ( '2026-07-01T00:00:00.000Z' ) ,
1079- end : new Date ( '2026-08-01T00:00:00.000Z' ) ,
1080- source : 'stripe' ,
1081- } ,
1082- } )
1096+ // As the ledger behaves: a charge given the payer's subscription lands in its current
1097+ // period, any other stays in the period it was admitted in.
1098+ mockRecordCumulativeUsage . mockImplementation (
1099+ async ( params : {
1100+ payerSubscriptionId ?: string
1101+ billingPeriod : typeof ADMITTED_PERIOD
1102+ } ) => ( {
1103+ billed : true ,
1104+ delta : 0.5 ,
1105+ total : 1.5 ,
1106+ billingPeriod : params . payerSubscriptionId
1107+ ? CURRENT_PERIOD
1108+ : { start : params . billingPeriod . start , end : params . billingPeriod . end } ,
1109+ } )
1110+ )
10831111 } )
10841112
1085- it ( 'settles the threshold against the period the charge was stamped into' , async ( ) => {
1086- mockRecordCumulativeUsage . mockResolvedValue ( {
1087- billed : true ,
1088- delta : 0.5 ,
1089- total : 1.5 ,
1090- billingPeriod : CURRENT_PERIOD ,
1091- } )
1092-
1093- const res = await POST ( attributedCallback ( ) )
1113+ it ( "records a Stripe payer's charge in its current period and settles it there" , async ( ) => {
1114+ admittedWithSource ( 'stripe' )
1115+ settlesOnlyAgainst ( CURRENT_PERIOD )
10941116
1095- expect ( res . status ) . toBe ( 200 )
1096- expect ( mockRecordCumulativeUsage ) . toHaveBeenCalledWith (
1097- expect . objectContaining ( { payerSubscriptionId : 'sub-1' } )
1098- )
1099- expect ( mockCheckAndBillPayerOverageThreshold ) . toHaveBeenCalledWith (
1100- { type : 'organization' , id : 'org-1' } ,
1101- expect . objectContaining ( { expectedBillingPeriod : CURRENT_PERIOD } )
1102- )
1117+ expect ( ( await POST ( attributedCallback ( ) ) ) . status ) . toBe ( 200 )
11031118 } )
11041119
11051120 it ( 'leaves a period that closed under a recorded charge to the cycle close' , async ( ) => {
1121+ admittedWithSource ( 'stripe' )
11061122 mockRecordCumulativeUsage . mockResolvedValue ( {
11071123 billed : true ,
11081124 delta : 0.5 ,
11091125 total : 1.5 ,
1110- billingPeriod : {
1111- start : new Date ( '2026-07-01T00:00:00.000Z' ) ,
1112- end : new Date ( '2026-08-01T00:00:00.000Z' ) ,
1113- } ,
1126+ billingPeriod : ADMITTED_PERIOD ,
11141127 } )
11151128 mockCheckAndBillPayerOverageThreshold . mockRejectedValue (
11161129 new MockThresholdSettlementError ( 'billing_period_elapsed' )
@@ -1121,51 +1134,15 @@ describe('POST /api/billing/update-cost — mid-run usage gate', () => {
11211134 expect ( res . status ) . toBe ( 200 )
11221135 } )
11231136
1124- it ( 'keeps a payer whose period is not a Stripe period on its frozen period' , async ( ) => {
1125- mockToBillingContext . mockReturnValue ( {
1126- billingEntity : { type : 'organization' , id : 'org-1' } ,
1127- billingPeriod : {
1128- start : new Date ( '2026-07-01T00:00:00.000Z' ) ,
1129- end : new Date ( '2026-08-01T00:00:00.000Z' ) ,
1130- source : 'default' ,
1131- } ,
1132- } )
1133- mockRecordCumulativeUsage . mockResolvedValue ( {
1134- billed : true ,
1135- delta : 0.5 ,
1136- total : 0.5 ,
1137- billingPeriod : CURRENT_PERIOD ,
1138- } )
1139-
1140- await POST ( attributedCallback ( ) )
1141-
1142- expect ( mockRecordCumulativeUsage ) . toHaveBeenCalledWith (
1143- expect . not . objectContaining ( { payerSubscriptionId : expect . anything ( ) } )
1144- )
1145- } )
1146-
1147- it ( 'keeps reporting-window payers on their frozen period' , async ( ) => {
1148- mockToBillingContext . mockReturnValue ( {
1149- billingEntity : { type : 'organization' , id : 'org-1' } ,
1150- billingPeriod : {
1151- start : new Date ( '2026-07-01T00:00:00.000Z' ) ,
1152- end : new Date ( '2026-08-01T00:00:00.000Z' ) ,
1153- source : 'reporting' ,
1154- } ,
1155- } )
1156- mockRecordCumulativeUsage . mockResolvedValue ( {
1157- billed : true ,
1158- delta : 0.5 ,
1159- total : 0.5 ,
1160- billingPeriod : CURRENT_PERIOD ,
1161- } )
1162-
1163- await POST ( attributedCallback ( ) )
1137+ it . each ( [ 'reporting' , 'default' ] as const ) (
1138+ 'keeps a payer with a %s period on the period it was admitted in' ,
1139+ async ( source ) => {
1140+ admittedWithSource ( source )
1141+ settlesOnlyAgainst ( ADMITTED_PERIOD )
11641142
1165- expect ( mockRecordCumulativeUsage ) . toHaveBeenCalledWith (
1166- expect . not . objectContaining ( { payerSubscriptionId : expect . anything ( ) } )
1167- )
1168- } )
1143+ expect ( ( await POST ( attributedCallback ( ) ) ) . status ) . toBe ( 200 )
1144+ }
1145+ )
11691146 } )
11701147
11711148 it . each ( [
@@ -1280,7 +1257,6 @@ describe('POST /api/billing/update-cost — mid-run usage gate', () => {
12801257 const body = await ( await POST ( attributedCallback ( ) ) ) . json ( )
12811258
12821259 expect ( body . usageExceeded ) . toBe ( false )
1283- expect ( mockRefreshAttributionPeriod ) . toHaveBeenCalledTimes ( 2 )
12841260 } )
12851261
12861262 it ( 'answers not exceeded when the standing read outlasts the callback budget' , async ( ) => {
@@ -1341,8 +1317,7 @@ describe('POST /api/billing/update-cost — mid-run usage gate', () => {
13411317 const res = await POST ( attributedCallback ( ) )
13421318
13431319 expect ( res . status ) . toBe ( 200 )
1344- await expect ( res . json ( ) ) . resolves . toMatchObject ( { usageExceeded : false } )
1345- expect ( mockRecordCumulativeUsage ) . toHaveBeenCalledOnce ( )
1320+ await expect ( res . json ( ) ) . resolves . toMatchObject ( { success : true , usageExceeded : false } )
13461321 } )
13471322
13481323 it ( 'reports no exceeded usage when billing is disabled' , async ( ) => {
0 commit comments