@@ -15,12 +15,6 @@ type PolarCustomerSession = {
1515 customer_portal_url ?: string
1616}
1717
18- type PolarCustomer = {
19- id ?: string
20- email ?: string
21- external_id ?: string | null
22- }
23-
2418type PolarListResource < T > = {
2519 items ?: T [ ]
2620}
@@ -119,6 +113,10 @@ export type CloudWorkerBillingStatus = {
119113 hasActivePlan : boolean
120114 checkoutRequired : boolean
121115 checkoutUrl : string | null
116+ activeWorkerSubscriptions : number
117+ workerCheckoutUrl : string | null
118+ workerCheckoutRequired : boolean
119+ workerPrice : CloudWorkerBillingPrice | null
122120 portalUrl : string | null
123121 price : CloudWorkerBillingPrice | null
124122 subscription : CloudWorkerBillingSubscription | null
@@ -139,6 +137,7 @@ type CloudAccessInput = {
139137 userId : string
140138 email : string
141139 name : string
140+ orgId ?: string | null
142141}
143142
144143type BillingStatusOptions = {
@@ -163,6 +162,10 @@ function isRecord(value: unknown): value is Record<string, unknown> {
163162 return typeof value === "object" && value !== null
164163}
165164
165+ function getExternalCustomerId ( input : CloudAccessInput ) {
166+ return input . orgId ?. trim ( ) || input . userId
167+ }
168+
166169async function polarFetch ( path : string , init : RequestInit = { } ) {
167170 const headers = new Headers ( init . headers )
168171 headers . set ( "Authorization" , `Bearer ${ env . polar . accessToken } ` )
@@ -216,75 +219,21 @@ async function getCustomerStateByExternalId(externalCustomerId: string): Promise
216219 return payload
217220}
218221
219- async function getCustomerStateById ( customerId : string ) : Promise < PolarCustomerState | null > {
220- const encodedCustomerId = encodeURIComponent ( customerId )
221- const { response, payload, text } = await polarFetchJson < PolarCustomerState > ( `/v1/customers/${ encodedCustomerId } /state` , {
222- method : "GET" ,
223- } )
224-
225- if ( response . status === 404 ) {
226- return null
227- }
228-
229- if ( ! response . ok ) {
230- throw new Error ( `Polar customer state lookup by ID failed (${ response . status } ): ${ text . slice ( 0 , 400 ) } ` )
231- }
232-
233- return payload
234- }
235-
236- async function getCustomerByEmail ( email : string ) : Promise < PolarCustomer | null > {
237- const normalizedEmail = email . trim ( ) . toLowerCase ( )
238- if ( ! normalizedEmail ) {
239- return null
240- }
241-
242- const encodedEmail = encodeURIComponent ( normalizedEmail )
243- const { response, payload, text } = await polarFetchJson < PolarListResource < PolarCustomer > > ( `/v1/customers/?email=${ encodedEmail } ` , {
244- method : "GET" ,
245- } )
246-
247- if ( ! response . ok ) {
248- throw new Error ( `Polar customer lookup by email failed (${ response . status } ): ${ text . slice ( 0 , 400 ) } ` )
249- }
250-
251- const customers = payload ?. items ?? [ ]
252- const exact = customers . find ( ( customer ) => customer . email ?. trim ( ) . toLowerCase ( ) === normalizedEmail )
253- return exact ?? customers [ 0 ] ?? null
254- }
255-
256- async function linkCustomerExternalId ( customer : PolarCustomer , externalCustomerId : string ) : Promise < void > {
257- if ( ! customer . id ) {
258- return
259- }
260-
261- if ( typeof customer . external_id === "string" && customer . external_id . length > 0 ) {
262- return
263- }
264-
265- const encodedCustomerId = encodeURIComponent ( customer . id )
266- await polarFetch ( `/v1/customers/${ encodedCustomerId } ` , {
267- method : "PATCH" ,
268- body : JSON . stringify ( {
269- external_id : externalCustomerId ,
270- } ) ,
271- } )
272- }
273-
274- function hasRequiredBenefit ( state : PolarCustomerState | null ) {
275- if ( ! state ?. granted_benefits || ! env . polar . benefitId ) {
222+ function hasBenefit ( state : PolarCustomerState | null , benefitId : string | undefined ) {
223+ if ( ! state ?. granted_benefits || ! benefitId ) {
276224 return false
277225 }
278226
279- return state . granted_benefits . some ( ( grant ) => grant . benefit_id === env . polar . benefitId )
227+ return state . granted_benefits . some ( ( grant ) => grant . benefit_id === benefitId )
280228}
281229
282- async function createCheckoutSession ( input : CloudAccessInput ) : Promise < string > {
230+ async function createCheckoutSessionForProduct ( input : CloudAccessInput , productId : string ) : Promise < string > {
231+ const externalCustomerId = getExternalCustomerId ( input )
283232 const payload = {
284- products : [ env . polar . productId ] ,
233+ products : [ productId ] ,
285234 success_url : env . polar . successUrl ,
286235 return_url : env . polar . returnUrl ,
287- external_customer_id : input . userId ,
236+ external_customer_id : externalCustomerId ,
288237 customer_email : input . email ,
289238 customer_name : input . name ,
290239 }
@@ -325,33 +274,44 @@ async function evaluateCloudWorkerAccess(
325274
326275 assertPaywallConfig ( )
327276
328- const externalState = await getCustomerStateByExternalId ( input . userId )
329- if ( hasRequiredBenefit ( externalState ) ) {
277+ const externalCustomerId = getExternalCustomerId ( input )
278+ const externalState = await getCustomerStateByExternalId ( externalCustomerId )
279+ if ( hasBenefit ( externalState , env . polar . benefitId ) ) {
330280 return {
331281 featureGateEnabled : true ,
332282 hasActivePlan : true ,
333283 checkoutUrl : null ,
334284 }
335285 }
336286
337- const customer = await getCustomerByEmail ( input . email )
338- if ( customer ?. id ) {
339- const emailState = await getCustomerStateById ( customer . id )
340- if ( hasRequiredBenefit ( emailState ) ) {
341- await linkCustomerExternalId ( customer , input . userId ) . catch ( ( ) => undefined )
342- return {
343- featureGateEnabled : true ,
344- hasActivePlan : true ,
345- checkoutUrl : null ,
346- }
347- }
348- }
349-
287+ const productId = env . polar . productId
350288 return {
351289 featureGateEnabled : true ,
352290 hasActivePlan : false ,
353- checkoutUrl : options . includeCheckoutUrl ? await createCheckoutSession ( input ) : null ,
291+ checkoutUrl : options . includeCheckoutUrl && productId ? await createCheckoutSessionForProduct ( input , productId ) : null ,
292+ }
293+ }
294+
295+ async function getActiveWorkerSubscriptionCount ( input : CloudAccessInput ) : Promise < number > {
296+ if ( ! env . polar . workerProductId ) {
297+ return 0
298+ }
299+
300+ const subscriptions = await listSubscriptionsByExternalCustomer ( getExternalCustomerId ( input ) , {
301+ activeOnly : true ,
302+ limit : 100 ,
303+ productId : env . polar . workerProductId ,
304+ } )
305+
306+ return subscriptions . filter ( ( subscription ) => isActiveSubscriptionStatus ( subscription . status ) ) . length
307+ }
308+
309+ async function createWorkerCheckoutSession ( input : CloudAccessInput ) : Promise < string | null > {
310+ if ( ! env . polar . workerProductId ) {
311+ return null
354312 }
313+
314+ return createCheckoutSessionForProduct ( input , env . polar . workerProductId )
355315}
356316
357317function normalizeRecurringInterval ( value : string | null | undefined ) : string | null {
@@ -419,12 +379,12 @@ async function getSubscriptionById(subscriptionId: string): Promise<PolarSubscri
419379
420380async function listSubscriptionsByExternalCustomer (
421381 externalCustomerId : string ,
422- options : { activeOnly ?: boolean ; limit ?: number } = { } ,
382+ options : { activeOnly ?: boolean ; limit ?: number ; productId ?: string | null } = { } ,
423383) : Promise < PolarSubscription [ ] > {
424384 const params = new URLSearchParams ( )
425385 params . set ( "external_customer_id" , externalCustomerId )
426- if ( env . polar . productId ) {
427- params . set ( "product_id" , env . polar . productId )
386+ if ( options . productId ) {
387+ params . set ( "product_id" , options . productId )
428388 }
429389 params . set ( "limit" , String ( options . limit ?? 1 ) )
430390 params . set ( "sorting" , "-started_at" )
@@ -458,21 +418,26 @@ async function listSubscriptionsByExternalCustomer(
458418}
459419
460420async function getPrimarySubscriptionForCustomer ( externalCustomerId : string ) : Promise < PolarSubscription | null > {
461- const active = await listSubscriptionsByExternalCustomer ( externalCustomerId , { activeOnly : true , limit : 1 } )
421+ const active = await listSubscriptionsByExternalCustomer ( externalCustomerId , {
422+ activeOnly : true ,
423+ limit : 1 ,
424+ productId : env . polar . productId ,
425+ } )
462426 if ( active [ 0 ] ) {
463427 return active [ 0 ]
464428 }
465429
466- const recent = await listSubscriptionsByExternalCustomer ( externalCustomerId , { activeOnly : false , limit : 1 } )
430+ const recent = await listSubscriptionsByExternalCustomer ( externalCustomerId , {
431+ activeOnly : false ,
432+ limit : 1 ,
433+ productId : env . polar . productId ,
434+ } )
467435 return recent [ 0 ] ?? null
468436}
469437
470438async function listRecentOrdersByExternalCustomer ( externalCustomerId : string , limit = 6 ) : Promise < PolarOrder [ ] > {
471439 const params = new URLSearchParams ( )
472440 params . set ( "external_customer_id" , externalCustomerId )
473- if ( env . polar . productId ) {
474- params . set ( "product_id" , env . polar . productId )
475- }
476441 params . set ( "limit" , String ( limit ) )
477442 params . set ( "sorting" , "-created_at" )
478443
@@ -649,6 +614,27 @@ export async function requireCloudWorkerAccess(input: CloudAccessInput): Promise
649614 }
650615}
651616
617+ export async function requireAdditionalCloudWorkerAccess ( input : CloudAccessInput & { ownedWorkerCount : number } ) : Promise < CloudWorkerAccess > {
618+ if ( ! env . polar . featureGateEnabled || ! env . polar . workerProductId ) {
619+ return { allowed : true }
620+ }
621+
622+ const activeWorkerSubscriptions = await getActiveWorkerSubscriptionCount ( input )
623+ if ( input . ownedWorkerCount < activeWorkerSubscriptions ) {
624+ return { allowed : true }
625+ }
626+
627+ const checkoutUrl = await createWorkerCheckoutSession ( input )
628+ if ( ! checkoutUrl ) {
629+ throw new Error ( "Polar worker checkout URL unavailable" )
630+ }
631+
632+ return {
633+ allowed : false ,
634+ checkoutUrl,
635+ }
636+ }
637+
652638export async function getCloudWorkerBillingStatus (
653639 input : CloudAccessInput ,
654640 options : BillingStatusOptions = { } ,
@@ -665,6 +651,10 @@ export async function getCloudWorkerBillingStatus(
665651 hasActivePlan : true ,
666652 checkoutRequired : false ,
667653 checkoutUrl : null ,
654+ activeWorkerSubscriptions : 0 ,
655+ workerCheckoutUrl : null ,
656+ workerCheckoutRequired : false ,
657+ workerPrice : null ,
668658 portalUrl : null ,
669659 price : null ,
670660 subscription : null ,
@@ -676,11 +666,19 @@ export async function getCloudWorkerBillingStatus(
676666 await sendSubscribedToDenEvent ( input )
677667 }
678668
669+ const [ activeWorkerSubscriptions , workerCheckoutUrl , workerPrice ] = await Promise . all ( [
670+ getActiveWorkerSubscriptionCount ( input ) . catch ( ( ) => 0 ) ,
671+ evaluation . hasActivePlan && options . includeCheckoutUrl
672+ ? createWorkerCheckoutSession ( input ) . catch ( ( ) => null )
673+ : Promise . resolve < string | null > ( null ) ,
674+ env . polar . workerProductId ? getProductBillingPrice ( env . polar . workerProductId ) . catch ( ( ) => null ) : Promise . resolve < CloudWorkerBillingPrice | null > ( null ) ,
675+ ] )
676+
679677 const [ subscriptionResult , priceResult , portalResult , invoicesResult ] = await Promise . all ( [
680- getPrimarySubscriptionForCustomer ( input . userId ) . catch ( ( ) => null ) ,
678+ getPrimarySubscriptionForCustomer ( getExternalCustomerId ( input ) ) . catch ( ( ) => null ) ,
681679 env . polar . productId ? getProductBillingPrice ( env . polar . productId ) . catch ( ( ) => null ) : Promise . resolve < CloudWorkerBillingPrice | null > ( null ) ,
682- includePortalUrl ? createCustomerPortalUrl ( input . userId ) . catch ( ( ) => null ) : Promise . resolve < string | null > ( null ) ,
683- includeInvoices ? listBillingInvoices ( input . userId ) . catch ( ( ) => [ ] ) : Promise . resolve < CloudWorkerBillingInvoice [ ] > ( [ ] ) ,
680+ includePortalUrl ? createCustomerPortalUrl ( getExternalCustomerId ( input ) ) . catch ( ( ) => null ) : Promise . resolve < string | null > ( null ) ,
681+ includeInvoices ? listBillingInvoices ( getExternalCustomerId ( input ) ) . catch ( ( ) => [ ] ) : Promise . resolve < CloudWorkerBillingInvoice [ ] > ( [ ] ) ,
684682 ] )
685683
686684 const subscription = toBillingSubscription ( subscriptionResult )
@@ -693,6 +691,10 @@ export async function getCloudWorkerBillingStatus(
693691 hasActivePlan : evaluation . hasActivePlan ,
694692 checkoutRequired : evaluation . featureGateEnabled && ! evaluation . hasActivePlan ,
695693 checkoutUrl : evaluation . checkoutUrl ,
694+ activeWorkerSubscriptions,
695+ workerCheckoutUrl,
696+ workerCheckoutRequired : evaluation . hasActivePlan && activeWorkerSubscriptions <= 0 ,
697+ workerPrice,
696698 portalUrl,
697699 price : productPrice ?? toBillingPriceFromSubscription ( subscription ) ,
698700 subscription,
@@ -732,24 +734,15 @@ export async function getCloudWorkerAdminBillingStatus(
732734 let paidByBenefit = false
733735
734736 if ( env . polar . benefitId ) {
735- const externalState = await getCustomerStateByExternalId ( input . userId )
736- if ( hasRequiredBenefit ( externalState ) ) {
737+ const externalCustomerId = getExternalCustomerId ( input )
738+ const externalState = await getCustomerStateByExternalId ( externalCustomerId )
739+ if ( hasBenefit ( externalState , env . polar . benefitId ) ) {
737740 paidByBenefit = true
738741 note = "Benefit granted via external customer id."
739- } else {
740- const customer = await getCustomerByEmail ( input . email )
741- if ( customer ?. id ) {
742- const emailState = await getCustomerStateById ( customer . id )
743- if ( hasRequiredBenefit ( emailState ) ) {
744- paidByBenefit = true
745- note = "Benefit granted via matching customer email."
746- await linkCustomerExternalId ( customer , input . userId ) . catch ( ( ) => undefined )
747- }
748- }
749742 }
750743 }
751744
752- const subscription = env . polar . productId ? await getPrimarySubscriptionForCustomer ( input . userId ) : null
745+ const subscription = env . polar . productId ? await getPrimarySubscriptionForCustomer ( getExternalCustomerId ( input ) ) : null
753746 const normalizedSubscription = toBillingSubscription ( subscription )
754747 const paidBySubscription = isActiveSubscriptionStatus ( normalizedSubscription ?. status )
755748
@@ -789,9 +782,10 @@ export async function setCloudWorkerSubscriptionCancellation(
789782
790783 assertPaywallConfig ( )
791784
792- const activeSubscriptions = await listSubscriptionsByExternalCustomer ( input . userId , {
785+ const activeSubscriptions = await listSubscriptionsByExternalCustomer ( getExternalCustomerId ( input ) , {
793786 activeOnly : true ,
794787 limit : 1 ,
788+ productId : env . polar . productId ,
795789 } )
796790 const active = activeSubscriptions [ 0 ]
797791 if ( ! active ?. id ) {
0 commit comments