@@ -70,9 +70,8 @@ async function createUser(): Promise<string> {
7070 return userId
7171}
7272
73- /** A user on a pro subscription for `period`, whose close marker has caught up to it. */
74- async function createPayer ( period : { start : Date ; end : Date } ) : Promise < Payer > {
75- const userId = await createUser ( )
73+ /** Puts `userId` on a pro subscription for `period`, whose close marker has caught up to it. */
74+ async function subscribePro ( userId : string , period : { start : Date ; end : Date } ) : Promise < string > {
7675 const subscriptionId = generateId ( )
7776 await db . insert ( subscription ) . values ( {
7877 id : subscriptionId ,
@@ -83,6 +82,13 @@ async function createPayer(period: { start: Date; end: Date }): Promise<Payer> {
8382 periodEnd : period . end ,
8483 lastClosedPeriodStart : period . start ,
8584 } )
85+ return subscriptionId
86+ }
87+
88+ /** A user on a pro subscription for `period`. */
89+ async function createPayer ( period : { start : Date ; end : Date } ) : Promise < Payer > {
90+ const userId = await createUser ( )
91+ const subscriptionId = await subscribePro ( userId , period )
8692 const decision = serializeAccountBillingDecisionHeader ( {
8793 userId,
8894 billingEntity : { type : 'user' , id : userId } ,
@@ -245,6 +251,52 @@ describe('direct-v1 cost callbacks in PostgreSQL', () => {
245251 } )
246252} )
247253
254+ /**
255+ * A free payer who subscribes mid-run: the run's spend stays in the open default period it was
256+ * admitted in, which no subscription period ever matches, so each callback must still settle
257+ * rather than answer a retryable refusal on every attempt.
258+ */
259+ describe ( 'a free payer who subscribes mid-run' , ( ) => {
260+ it . each ( [
261+ {
262+ protocol : 'direct-v1' ,
263+ admit : async ( ) => {
264+ const payer = await createFreePayer ( )
265+ return {
266+ userId : payer . userId ,
267+ send : ( key : string , cost : number ) => callback ( payer , key , cost ) ,
268+ }
269+ } ,
270+ } ,
271+ {
272+ protocol : 'attribution-v1' ,
273+ admit : async ( ) => {
274+ const attribution = await createFreeWorkspacePayer ( )
275+ return {
276+ userId : attribution . billedAccountUserId ,
277+ send : ( key : string , cost : number ) => attributedCallback ( attribution , key , cost ) ,
278+ }
279+ } ,
280+ } ,
281+ ] ) ( 'settles $protocol spend in the period it was admitted in' , async ( { admit } ) => {
282+ const { userId, send } = await admit ( )
283+ const requestKey = generateId ( )
284+ const periodStart = new Date ( Date . now ( ) - 60 * 60 * 1000 )
285+
286+ expect ( ( await POST ( send ( requestKey , 0.5 ) , { } ) ) . status ) . toBe ( 200 )
287+ await subscribePro ( userId , {
288+ start : periodStart ,
289+ end : new Date ( periodStart . getTime ( ) + 30 * DAY_MS ) ,
290+ } )
291+ expect ( ( await POST ( send ( requestKey , 0.8 ) , { } ) ) . status ) . toBe ( 200 )
292+ expect ( ( await POST ( send ( requestKey , 0.8 ) , { } ) ) . status ) . toBe ( 409 )
293+
294+ const rows = await requestRows ( requestKey )
295+ expect ( rows . map ( ( row ) => Number ( row . cost ) ) ) . toEqual ( [ 0.8 ] )
296+ expect ( rows [ 0 ] ?. billingPeriodStart ?. getTime ( ) ) . toBe ( defaultBillingPeriod ( ) . start . getTime ( ) )
297+ } )
298+ } )
299+
248300/**
249301 * Each payer's first callback caches an admission ($1 of a $5 limit), and its second, inside the
250302 * near-limit cache window, records spend that takes it to $6.
0 commit comments