@@ -4,6 +4,8 @@ import type {
44 BillingCustomerError ,
55 ProvisionBillingCustomerParams ,
66 ProvisionBillingCustomerResult ,
7+ ProvisionFreePlanContractParams ,
8+ ProvisionFreePlanContractResult ,
79} from "@trigger.dev/billing" ;
810import { errAsync , okAsync , ResultAsync } from "neverthrow" ;
911import { afterEach , beforeEach , describe , expect , it , vi } from "vitest" ;
@@ -14,37 +16,58 @@ import {
1416 type NewOrgProvisionDependencies ,
1517} from "./provisionBillingCustomer.server" ;
1618
17- type Outcome = ( ) => ResultAsync < ProvisionBillingCustomerResult , BillingCustomerError > ;
19+ type Outcome = ( ) => ResultAsync < ProvisionFreePlanContractResult , BillingCustomerError > ;
1820
1921const created : Outcome = ( ) =>
20- okAsync ( { organizationId : "org_1" , billingCustomerId : "cus_1" , outcome : "created" as const } ) ;
22+ okAsync ( {
23+ organizationId : "org_1" ,
24+ billingCustomerId : "cus_1" ,
25+ contractId : "con_1" ,
26+ outcome : "created" as const ,
27+ } ) ;
2128
2229const never : Outcome = ( ) => new ResultAsync ( new Promise ( ( ) => { } ) ) ;
2330
24- function harness ( outcomes : Outcome [ ] , options : { usingPlugin ?: ( ) => Promise < boolean > } = { } ) {
25- const calls : ProvisionBillingCustomerParams [ ] = [ ] ;
31+ function harness (
32+ outcomes : Outcome [ ] ,
33+ options : {
34+ usingPlugin ?: ( ) => Promise < boolean > ;
35+ customerOutcomes ?: ProvisionBillingCustomerResult [ "outcome" ] [ ] ;
36+ } = { }
37+ ) {
38+ const calls : ProvisionFreePlanContractParams [ ] = [ ] ;
39+ const customerCalls : ProvisionBillingCustomerParams [ ] = [ ] ;
2640 const deleted : string [ ] = [ ] ;
41+ const customerOutcomes : ProvisionBillingCustomerResult [ "outcome" ] [ ] =
42+ options . customerOutcomes ?? [ "created" ] ;
2743
2844 const controller : BillingController = {
2945 isUsingPlugin : options . usingPlugin ?? ( async ( ) => true ) ,
3046 getCustomer ( ) : ResultAsync < BillingCustomer | null , BillingCustomerError > {
3147 return okAsync ( null ) ;
3248 } ,
3349 provisionCustomer ( params ) {
50+ customerCalls . push ( params ) ;
51+ const outcome =
52+ customerOutcomes [ Math . min ( customerCalls . length - 1 , customerOutcomes . length - 1 ) ] ;
53+ return okAsync ( { organizationId : "org_1" , billingCustomerId : "cus_1" , outcome } ) ;
54+ } ,
55+ provisionFreePlanContract ( params ) {
3456 calls . push ( params ) ;
3557 return outcomes [ Math . min ( calls . length - 1 , outcomes . length - 1 ) ] ( ) ;
3658 } ,
3759 } ;
3860
3961 const deps : NewOrgProvisionDependencies = {
4062 enabled : true ,
63+ createFreePlanContract : true ,
4164 controller,
4265 deleteOrganization : async ( id ) => {
4366 deleted . push ( id ) ;
4467 } ,
4568 } ;
4669
47- return { deps, calls, deleted } ;
70+ return { deps, calls, customerCalls , deleted } ;
4871}
4972
5073function run ( deps : NewOrgProvisionDependencies ) {
@@ -71,7 +94,7 @@ describe("provisionBillingCustomerForNewOrg", () => {
7194 expect ( deleted ) . toHaveLength ( 0 ) ;
7295 } ) ;
7396
74- it ( "blocks until the customer is provisioned and keeps the org" , async ( ) => {
97+ it ( "blocks until the customer and free plan contract are provisioned and keeps the org" , async ( ) => {
7598 const { deps, calls, deleted } = harness ( [ created ] ) ;
7699
77100 expect ( await run ( deps ) ) . toBe ( "ok" ) ;
@@ -81,12 +104,37 @@ describe("provisionBillingCustomerForNewOrg", () => {
81104 expect ( deleted ) . toHaveLength ( 0 ) ;
82105 } ) ;
83106
107+ it ( "provisions only the customer, without a contract, when free plan contracts are off" , async ( ) => {
108+ const { deps, calls, customerCalls, deleted } = harness ( [ created ] ) ;
109+
110+ expect ( await run ( { ...deps , createFreePlanContract : false } ) ) . toBe ( "ok" ) ;
111+ expect ( calls ) . toHaveLength ( 0 ) ;
112+ expect ( customerCalls ) . toHaveLength ( 1 ) ;
113+ expect ( customerCalls [ 0 ] . organizationId ) . toBe ( "org_1" ) ;
114+ expect ( deleted ) . toHaveLength ( 0 ) ;
115+ } ) ;
116+
117+ it ( "retries a customer that is still in progress when free plan contracts are off" , async ( ) => {
118+ const { deps, calls, customerCalls, deleted } = harness ( [ created ] , {
119+ customerOutcomes : [ "in_progress" , "created" ] ,
120+ } ) ;
121+
122+ const result = run ( { ...deps , createFreePlanContract : false } ) ;
123+ await vi . runAllTimersAsync ( ) ;
124+
125+ expect ( await result ) . toBe ( "ok" ) ;
126+ expect ( calls ) . toHaveLength ( 0 ) ;
127+ expect ( customerCalls ) . toHaveLength ( 2 ) ;
128+ expect ( deleted ) . toHaveLength ( 0 ) ;
129+ } ) ;
130+
84131 it ( "accepts an org that was already provisioned" , async ( ) => {
85132 const { deps, deleted } = harness ( [
86133 ( ) =>
87134 okAsync ( {
88135 organizationId : "org_1" ,
89136 billingCustomerId : "cus_1" ,
137+ contractId : "con_1" ,
90138 outcome : "already_provisioned" as const ,
91139 } ) ,
92140 ] ) ;
@@ -170,24 +218,6 @@ describe("provisionBillingCustomerForNewOrg", () => {
170218 expect ( calls ) . toHaveLength ( 2 ) ;
171219 } ) ;
172220
173- it ( "retries an in_progress outcome instead of treating it as done" , async ( ) => {
174- const { deps, calls } = harness ( [
175- ( ) =>
176- okAsync ( {
177- organizationId : "org_1" ,
178- billingCustomerId : null ,
179- outcome : "in_progress" as const ,
180- } ) ,
181- created ,
182- ] ) ;
183-
184- const settled = run ( deps ) ;
185- await vi . advanceTimersByTimeAsync ( 200 ) ;
186-
187- expect ( await settled ) . toBe ( "ok" ) ;
188- expect ( calls ) . toHaveLength ( 2 ) ;
189- } ) ;
190-
191221 it ( "deletes the org and fails creation once retries are exhausted" , async ( ) => {
192222 const { deps, calls, deleted } = harness ( [ ( ) => errAsync ( "upstream_unavailable" ) ] ) ;
193223
0 commit comments