@@ -25,6 +25,7 @@ import { AppError } from '../utils/AppError';
2525import { ERRORS } from '../utils/Errors' ;
2626import { Logger } from '../utils/Logger' ;
2727import { Now } from '../utils/Timestamp' ;
28+ import { PrepareCheckoutPaymentInput } from '@zoneless/shared-schemas' ;
2829import {
2930 Account ,
3031 BalanceTransaction ,
@@ -64,6 +65,41 @@ export interface PreparedCheckoutPayment {
6465 subscription_step ?: 'init_authority' | 'subscribe' ;
6566}
6667
68+ /** Convert a stored nullable address into CreateCustomer address input. */
69+ function ToCreateCustomerAddress (
70+ address :
71+ | {
72+ city : string | null ;
73+ country : string | null ;
74+ line1 : string | null ;
75+ line2 : string | null ;
76+ postal_code : string | null ;
77+ state : string | null ;
78+ }
79+ | null
80+ | undefined
81+ ) :
82+ | {
83+ city ?: string ;
84+ country ?: string ;
85+ line1 ?: string ;
86+ line2 ?: string ;
87+ postal_code ?: string ;
88+ state ?: string ;
89+ }
90+ | undefined {
91+ if ( ! address ) return undefined ;
92+ const result = {
93+ ...( address . city ? { city : address . city } : { } ) ,
94+ ...( address . country ? { country : address . country } : { } ) ,
95+ ...( address . line1 ? { line1 : address . line1 } : { } ) ,
96+ ...( address . line2 ? { line2 : address . line2 } : { } ) ,
97+ ...( address . postal_code ? { postal_code : address . postal_code } : { } ) ,
98+ ...( address . state ? { state : address . state } : { } ) ,
99+ } ;
100+ return Object . keys ( result ) . length > 0 ? result : undefined ;
101+ }
102+
67103export class CheckoutPaymentModule {
68104 private readonly db : Database ;
69105 private readonly accountModule : AccountModule ;
@@ -210,7 +246,7 @@ export class CheckoutPaymentModule {
210246 async PreparePayment (
211247 urlSlug : string ,
212248 payerWallet : string | undefined ,
213- email ?: string
249+ customerDetails ?: Omit < PrepareCheckoutPaymentInput , 'payer_wallet' >
214250 ) : Promise < PreparedCheckoutPayment > {
215251 if ( ! payerWallet || typeof payerWallet !== 'string' ) {
216252 throw new AppError (
@@ -223,12 +259,27 @@ export class CheckoutPaymentModule {
223259 const session = await this . GetSessionOrThrow ( urlSlug ) ;
224260 this . AssertSessionPayable ( session ) ;
225261
262+ // Existing customers with an email cannot change it at checkout.
263+ const lockedEmail = session . customer
264+ ? session . customer_email ?. trim ( ) ||
265+ session . customer_details ?. email ?. trim ( ) ||
266+ null
267+ : null ;
268+ const resolvedDetails = lockedEmail
269+ ? { ...customerDetails , email : lockedEmail }
270+ : customerDetails ;
271+
272+ this . AssertCollectedCustomerDetails ( session , resolvedDetails ) ;
273+
226274 const merchantWallet = await this . ResolveMerchantWallet (
227275 session . platform_account
228276 ) ;
229277
230- if ( email && typeof email === 'string' ) {
231- await this . checkoutSessionModule . SetCustomerEmail ( session . id , email ) ;
278+ if ( resolvedDetails ) {
279+ await this . checkoutSessionModule . SetCollectedCustomerDetails (
280+ session . id ,
281+ resolvedDetails
282+ ) ;
232283 }
233284
234285 Logger . info ( 'Preparing checkout transaction' , {
@@ -525,6 +576,10 @@ export class CheckoutPaymentModule {
525576 latestCharge : charge ?. id ?? null ,
526577 } ) ;
527578
579+ if ( this . ShouldCreateCheckoutCustomer ( session ) && this . customerModule ) {
580+ await this . EnsureCheckoutCustomer ( session , verification . payer_address ) ;
581+ }
582+
528583 const completedSession =
529584 await this . checkoutSessionModule . CompleteCheckoutSession ( session . id , {
530585 transaction_signature : signature ,
@@ -793,9 +848,18 @@ export class CheckoutPaymentModule {
793848 ) ;
794849 }
795850
851+ private ShouldCreateCheckoutCustomer ( session : CheckoutSession ) : boolean {
852+ if ( session . customer ) return false ;
853+ if ( session . mode === 'subscription' ) return true ;
854+ return (
855+ session . customer_creation === 'always' ||
856+ session . customer_creation === 'if_required'
857+ ) ;
858+ }
859+
796860 private async EnsureCheckoutCustomer (
797861 session : CheckoutSession ,
798- subscriberWallet : string
862+ payerWallet : string
799863 ) : Promise < string > {
800864 if ( session . customer ) return session . customer ;
801865 if ( ! this . customerModule ) {
@@ -806,17 +870,56 @@ export class CheckoutPaymentModule {
806870 ) ;
807871 }
808872
809- const email =
810- session . customer_email ?? session . customer_details ?. email ?? undefined ;
873+ const details = session . customer_details ;
874+ const email = session . customer_email ?? details ?. email ?? undefined ;
875+ const individualName = details ?. individual_name ?? undefined ;
876+ const businessName = details ?. business_name ?? undefined ;
877+ const name = details ?. name ?? individualName ?? businessName ?? undefined ;
878+ const phone = details ?. phone ?? undefined ;
879+ const address = ToCreateCustomerAddress ( details ?. address ) ;
880+ const shippingDetails = session . collected_information ?. shipping_details ;
881+ const shippingAddress = ToCreateCustomerAddress ( shippingDetails ?. address ) ;
882+ const shipping =
883+ shippingDetails ?. name && shippingAddress
884+ ? {
885+ name : shippingDetails . name ,
886+ address : shippingAddress ,
887+ ...( phone ? { phone } : { } ) ,
888+ }
889+ : undefined ;
890+ const taxIdData = ( details ?. tax_ids ?? [ ] )
891+ . filter ( ( taxId ) => ! ! taxId . value ?. trim ( ) )
892+ . map ( ( taxId ) => ( {
893+ type : taxId . type ?. trim ( ) || 'unknown' ,
894+ value : taxId . value . trim ( ) ,
895+ } ) ) ;
896+ const customFieldMetadata : Record < string , string > = { } ;
897+ for ( const field of session . custom_fields ?? [ ] ) {
898+ const value =
899+ field . text ?. value ??
900+ field . numeric ?. value ??
901+ field . dropdown ?. value ??
902+ null ;
903+ if ( ! value ?. trim ( ) ) continue ;
904+ customFieldMetadata [ `custom_field_${ field . key } ` ] = value . trim ( ) ;
905+ }
811906
812907 const customer = await this . customerModule . CreateCustomer (
813908 session . platform_account ,
814909 {
815910 email,
816- description : `Checkout subscriber ${ subscriberWallet } ` ,
911+ name,
912+ individual_name : individualName ?? undefined ,
913+ business_name : businessName ?? undefined ,
914+ phone,
915+ address,
916+ shipping,
917+ ...( taxIdData . length > 0 ? { tax_id_data : taxIdData } : { } ) ,
918+ description : `Checkout customer ${ payerWallet } ` ,
817919 metadata : {
818- wallet_address : subscriberWallet ,
920+ wallet_address : payerWallet ,
819921 checkout_session : session . id ,
922+ ...customFieldMetadata ,
820923 } ,
821924 }
822925 ) ;
@@ -1035,6 +1138,110 @@ export class CheckoutPaymentModule {
10351138 }
10361139 }
10371140
1141+ /**
1142+ * Enforce required collection fields configured on the session (name,
1143+ * phone, address, tax ID, custom fields, terms) before building a payment.
1144+ */
1145+ private AssertCollectedCustomerDetails (
1146+ session : CheckoutSession ,
1147+ details ?: Omit < PrepareCheckoutPaymentInput , 'payer_wallet' >
1148+ ) : void {
1149+ const Require = ( condition : boolean , message : string ) : void => {
1150+ if ( condition ) {
1151+ throw new AppError (
1152+ message ,
1153+ ERRORS . VALIDATION_ERROR . status ,
1154+ ERRORS . VALIDATION_ERROR . type
1155+ ) ;
1156+ }
1157+ } ;
1158+
1159+ Require ( ! details ?. email ?. trim ( ) , 'Email is required' ) ;
1160+
1161+ const individual = session . name_collection ?. individual ;
1162+ Require (
1163+ ! ! individual ?. enabled && ! individual . optional && ! details ?. name ?. trim ( ) ,
1164+ 'Name is required'
1165+ ) ;
1166+
1167+ const business = session . name_collection ?. business ;
1168+ Require (
1169+ ! ! business ?. enabled &&
1170+ ! business . optional &&
1171+ ! details ?. business_name ?. trim ( ) ,
1172+ 'Business name is required'
1173+ ) ;
1174+
1175+ Require (
1176+ ! ! session . phone_number_collection ?. enabled && ! details ?. phone ?. trim ( ) ,
1177+ 'Phone number is required'
1178+ ) ;
1179+
1180+ const AssertAddress = (
1181+ required : boolean ,
1182+ address :
1183+ | {
1184+ line1 ?: string ;
1185+ city ?: string ;
1186+ postal_code ?: string ;
1187+ country ?: string ;
1188+ }
1189+ | undefined ,
1190+ label : string
1191+ ) : void => {
1192+ Require (
1193+ required && ! address ?. line1 ?. trim ( ) ,
1194+ `${ label } line 1 is required`
1195+ ) ;
1196+ Require ( required && ! address ?. city ?. trim ( ) , `${ label } city is required` ) ;
1197+ Require (
1198+ required && ! address ?. postal_code ?. trim ( ) ,
1199+ `${ label } postal code is required`
1200+ ) ;
1201+ Require (
1202+ required && ! address ?. country ?. trim ( ) ,
1203+ `${ label } country is required`
1204+ ) ;
1205+ } ;
1206+
1207+ AssertAddress (
1208+ session . billing_address_collection === 'required' ,
1209+ details ?. address ,
1210+ 'Billing address'
1211+ ) ;
1212+ AssertAddress (
1213+ ! ! session . shipping_address_collection ,
1214+ details ?. shipping_address ,
1215+ 'Shipping address'
1216+ ) ;
1217+ Require (
1218+ ! ! session . shipping_address_collection &&
1219+ ! details ?. shipping_address ?. name ?. trim ( ) ,
1220+ 'Shipping name is required'
1221+ ) ;
1222+
1223+ Require (
1224+ ! ! session . tax_id_collection ?. enabled &&
1225+ session . tax_id_collection . required === 'if_supported' &&
1226+ ! details ?. tax_id ?. trim ( ) ,
1227+ 'Tax ID is required'
1228+ ) ;
1229+
1230+ Require (
1231+ session . consent_collection ?. terms_of_service === 'required' &&
1232+ ! details ?. terms_of_service_accepted ,
1233+ 'Terms of service must be accepted'
1234+ ) ;
1235+
1236+ for ( const field of session . custom_fields ?? [ ] ) {
1237+ if ( field . optional ) continue ;
1238+ const value = details ?. custom_fields ?. find (
1239+ ( entry ) => entry . key === field . key
1240+ ) ?. value ;
1241+ Require ( ! value ?. trim ( ) , `${ field . label . custom ?? field . key } is required` ) ;
1242+ }
1243+ }
1244+
10381245 private async MarkPaymentIntentRequiresConfirmation (
10391246 session : CheckoutSession ,
10401247 payerWallet : string
0 commit comments