22using Bit . Core . Billing . Commands ;
33using Bit . Core . Billing . Constants ;
44using Bit . Core . Billing . Extensions ;
5+ using Bit . Core . Billing . Payment . Commands ;
56using Bit . Core . Billing . Payment . Models ;
7+ using Bit . Core . Billing . Payment . Queries ;
68using Bit . Core . Billing . Pricing ;
79using Bit . Core . Billing . Services ;
810using Bit . Core . Entities ;
2123
2224namespace Bit . Core . Billing . Premium . Commands ;
2325
26+ using static StripeConstants ;
2427using static Utilities ;
2528
2629/// <summary>
@@ -32,7 +35,7 @@ public interface ICreatePremiumCloudHostedSubscriptionCommand
3235 /// <summary>
3336 /// Creates a premium cloud-hosted subscription for the specified user.
3437 /// </summary>
35- /// <param name="user">The user to create the premium subscription for. Must not already be a premium user.</param>
38+ /// <param name="user">The user to create the premium subscription for. Must not yet be a premium user.</param>
3639 /// <param name="paymentMethod">The tokenized payment method containing the payment type and token for billing.</param>
3740 /// <param name="billingAddress">The billing address information required for tax calculation and customer creation.</param>
3841 /// <param name="additionalStorageGb">Additional storage in GB beyond the base 1GB included with premium (must be >= 0).</param>
@@ -53,7 +56,9 @@ public class CreatePremiumCloudHostedSubscriptionCommand(
5356 IUserService userService ,
5457 IPushNotificationService pushNotificationService ,
5558 ILogger < CreatePremiumCloudHostedSubscriptionCommand > logger ,
56- IPricingClient pricingClient )
59+ IPricingClient pricingClient ,
60+ IHasPaymentMethodQuery hasPaymentMethodQuery ,
61+ IUpdatePaymentMethodCommand updatePaymentMethodCommand )
5762 : BaseBillingCommand < CreatePremiumCloudHostedSubscriptionCommand > ( logger ) , ICreatePremiumCloudHostedSubscriptionCommand
5863{
5964 private static readonly List < string > _expand = [ "tax" ] ;
@@ -75,10 +80,30 @@ public Task<BillingCommandResult<None>> Run(
7580 return new BadRequest ( "Additional storage must be greater than 0." ) ;
7681 }
7782
78- // Note: A customer will already exist if the customer has purchased account credits.
79- var customer = string . IsNullOrEmpty ( user . GatewayCustomerId )
80- ? await CreateCustomerAsync ( user , paymentMethod , billingAddress )
81- : await subscriberService . GetCustomerOrThrow ( user , new CustomerGetOptions { Expand = _expand } ) ;
83+ Customer ? customer ;
84+
85+ /*
86+ * For a new customer purchasing a new subscription, we attach the payment method while creating the customer.
87+ */
88+ if ( string . IsNullOrEmpty ( user . GatewayCustomerId ) )
89+ {
90+ customer = await CreateCustomerAsync ( user , paymentMethod , billingAddress ) ;
91+ }
92+ /*
93+ * An existing customer without a payment method starting a new subscription indicates a user who previously
94+ * purchased account credit but chose to use a tokenizable payment method to pay for the subscription. In this case,
95+ * we need to add the payment method to their customer first. If the incoming payment method is account credit,
96+ * we can just go straight to fetching the customer since there's no payment method to apply.
97+ */
98+ else if ( paymentMethod . IsTokenized && ! await hasPaymentMethodQuery . Run ( user ) )
99+ {
100+ await updatePaymentMethodCommand . Run ( user , paymentMethod . AsTokenized , billingAddress ) ;
101+ customer = await subscriberService . GetCustomerOrThrow ( user , new CustomerGetOptions { Expand = _expand } ) ;
102+ }
103+ else
104+ {
105+ customer = await subscriberService . GetCustomerOrThrow ( user , new CustomerGetOptions { Expand = _expand } ) ;
106+ }
82107
83108 customer = await ReconcileBillingLocationAsync ( customer , billingAddress ) ;
84109
@@ -91,23 +116,25 @@ public Task<BillingCommandResult<None>> Run(
91116 switch ( tokenized )
92117 {
93118 case { Type : TokenizablePaymentMethodType . PayPal }
94- when subscription . Status == StripeConstants . SubscriptionStatus . Incomplete :
119+ when subscription . Status == SubscriptionStatus . Incomplete :
95120 case { Type : not TokenizablePaymentMethodType . PayPal }
96- when subscription . Status == StripeConstants . SubscriptionStatus . Active :
121+ when subscription . Status == SubscriptionStatus . Active :
97122 {
98123 user . Premium = true ;
99124 user . PremiumExpirationDate = subscription . GetCurrentPeriodEnd ( ) ;
100125 break ;
101126 }
102127 }
103128 } ,
104- nonTokenized =>
129+ _ =>
105130 {
106- if ( subscription . Status == StripeConstants . SubscriptionStatus . Active )
131+ if ( subscription . Status != SubscriptionStatus . Active )
107132 {
108- user . Premium = true ;
109- user . PremiumExpirationDate = subscription . GetCurrentPeriodEnd ( ) ;
133+ return ;
110134 }
135+
136+ user . Premium = true ;
137+ user . PremiumExpirationDate = subscription . GetCurrentPeriodEnd ( ) ;
111138 } ) ;
112139
113140 user . Gateway = GatewayType . Stripe ;
@@ -163,25 +190,25 @@ private async Task<Customer> CreateCustomerAsync(User user,
163190 } ,
164191 Metadata = new Dictionary < string , string >
165192 {
166- [ StripeConstants . MetadataKeys . Region ] = globalSettings . BaseServiceUri . CloudRegion ,
167- [ StripeConstants . MetadataKeys . UserId ] = user . Id . ToString ( )
193+ [ MetadataKeys . Region ] = globalSettings . BaseServiceUri . CloudRegion ,
194+ [ MetadataKeys . UserId ] = user . Id . ToString ( )
168195 } ,
169196 Tax = new CustomerTaxOptions
170197 {
171- ValidateLocation = StripeConstants . ValidateTaxLocationTiming . Immediately
198+ ValidateLocation = ValidateTaxLocationTiming . Immediately
172199 }
173200 } ;
174201
175202 var braintreeCustomerId = "" ;
176203
177204 // We have checked that the payment method is tokenized, so we can safely cast it.
178- // ReSharper disable once SwitchStatementHandlesSomeKnownEnumValuesWithDefault
179- switch ( paymentMethod . AsT0 . Type )
205+ var tokenizedPaymentMethod = paymentMethod . AsTokenized ;
206+ switch ( tokenizedPaymentMethod . Type )
180207 {
181208 case TokenizablePaymentMethodType . BankAccount :
182209 {
183210 var setupIntent =
184- ( await stripeAdapter . SetupIntentList ( new SetupIntentListOptions { PaymentMethod = paymentMethod . AsT0 . Token } ) )
211+ ( await stripeAdapter . SetupIntentList ( new SetupIntentListOptions { PaymentMethod = tokenizedPaymentMethod . Token } ) )
185212 . FirstOrDefault ( ) ;
186213
187214 if ( setupIntent == null )
@@ -195,19 +222,19 @@ private async Task<Customer> CreateCustomerAsync(User user,
195222 }
196223 case TokenizablePaymentMethodType . Card :
197224 {
198- customerCreateOptions . PaymentMethod = paymentMethod . AsT0 . Token ;
199- customerCreateOptions . InvoiceSettings . DefaultPaymentMethod = paymentMethod . AsT0 . Token ;
225+ customerCreateOptions . PaymentMethod = tokenizedPaymentMethod . Token ;
226+ customerCreateOptions . InvoiceSettings . DefaultPaymentMethod = tokenizedPaymentMethod . Token ;
200227 break ;
201228 }
202229 case TokenizablePaymentMethodType . PayPal :
203230 {
204- braintreeCustomerId = await subscriberService . CreateBraintreeCustomer ( user , paymentMethod . AsT0 . Token ) ;
231+ braintreeCustomerId = await subscriberService . CreateBraintreeCustomer ( user , tokenizedPaymentMethod . Token ) ;
205232 customerCreateOptions . Metadata [ BraintreeCustomerIdKey ] = braintreeCustomerId ;
206233 break ;
207234 }
208235 default :
209236 {
210- _logger . LogError ( "Cannot create customer for user ({UserID}) using payment method type ({PaymentMethodType}) as it is not supported" , user . Id , paymentMethod . AsT0 . Type . ToString ( ) ) ;
237+ _logger . LogError ( "Cannot create customer for user ({UserID}) using payment method type ({PaymentMethodType}) as it is not supported" , user . Id , tokenizedPaymentMethod . Type . ToString ( ) ) ;
211238 throw new BillingException ( ) ;
212239 }
213240 }
@@ -225,21 +252,18 @@ private async Task<Customer> CreateCustomerAsync(User user,
225252 async Task Revert ( )
226253 {
227254 // ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault
228- if ( paymentMethod . IsTokenized )
255+ switch ( tokenizedPaymentMethod . Type )
229256 {
230- switch ( paymentMethod . AsT0 . Type )
231- {
232- case TokenizablePaymentMethodType . BankAccount :
233- {
234- await setupIntentCache . RemoveSetupIntentForSubscriber ( user . Id ) ;
235- break ;
236- }
237- case TokenizablePaymentMethodType . PayPal when ! string . IsNullOrEmpty ( braintreeCustomerId ) :
238- {
239- await braintreeGateway . Customer . DeleteAsync ( braintreeCustomerId ) ;
240- break ;
241- }
242- }
257+ case TokenizablePaymentMethodType . BankAccount :
258+ {
259+ await setupIntentCache . RemoveSetupIntentForSubscriber ( user . Id ) ;
260+ break ;
261+ }
262+ case TokenizablePaymentMethodType . PayPal when ! string . IsNullOrEmpty ( braintreeCustomerId ) :
263+ {
264+ await braintreeGateway . Customer . DeleteAsync ( braintreeCustomerId ) ;
265+ break ;
266+ }
243267 }
244268 }
245269 }
@@ -271,7 +295,7 @@ private async Task<Customer> ReconcileBillingLocationAsync(
271295 Expand = _expand ,
272296 Tax = new CustomerTaxOptions
273297 {
274- ValidateLocation = StripeConstants . ValidateTaxLocationTiming . Immediately
298+ ValidateLocation = ValidateTaxLocationTiming . Immediately
275299 }
276300 } ;
277301 return await stripeAdapter . CustomerUpdateAsync ( customer . Id , options ) ;
@@ -310,15 +334,15 @@ private async Task<Subscription> CreateSubscriptionAsync(
310334 {
311335 Enabled = true
312336 } ,
313- CollectionMethod = StripeConstants . CollectionMethod . ChargeAutomatically ,
337+ CollectionMethod = CollectionMethod . ChargeAutomatically ,
314338 Customer = customer . Id ,
315339 Items = subscriptionItemOptionsList ,
316340 Metadata = new Dictionary < string , string >
317341 {
318- [ StripeConstants . MetadataKeys . UserId ] = userId . ToString ( )
342+ [ MetadataKeys . UserId ] = userId . ToString ( )
319343 } ,
320344 PaymentBehavior = usingPayPal
321- ? StripeConstants . PaymentBehavior . DefaultIncomplete
345+ ? PaymentBehavior . DefaultIncomplete
322346 : null ,
323347 OffSession = true
324348 } ;
0 commit comments