@ -7,7 +7,6 @@ using Bit.Core.Models.Business;
using Bit.Core.Repositories ;
using Bit.Core.Repositories ;
using Bit.Core.Settings ;
using Bit.Core.Settings ;
using Microsoft.Extensions.Logging ;
using Microsoft.Extensions.Logging ;
using Stripe ;
using StaticStore = Bit . Core . Models . StaticStore ;
using StaticStore = Bit . Core . Models . StaticStore ;
using TaxRate = Bit . Core . Entities . TaxRate ;
using TaxRate = Bit . Core . Entities . TaxRate ;
@ -751,14 +750,16 @@ public class StripePaymentService : IPaymentService
prorationDate ? ? = DateTime . UtcNow ;
prorationDate ? ? = DateTime . UtcNow ;
var collectionMethod = sub . CollectionMethod ;
var collectionMethod = sub . CollectionMethod ;
var daysUntilDue = sub . DaysUntilDue ;
var daysUntilDue = sub . DaysUntilDue ;
var chargeNow = collectionMethod = = "charge_automatically" ;
var updatedItemOptions = subscriptionUpdate . UpgradeItemsOptions ( sub ) ;
var updatedItemOptions = subscriptionUpdate . UpgradeItemsOptions ( sub ) ;
var subUpdateOptions = new Stripe . SubscriptionUpdateOptions
var subUpdateOptions = new Stripe . SubscriptionUpdateOptions
{
{
Items = updatedItemOptions ,
Items = updatedItemOptions ,
ProrationBehavior = Constants . CreateProrations ,
ProrationBehavior = "always_invoice" ,
DaysUntilDue = daysUntilDue ? ? 1 ,
DaysUntilDue = daysUntilDue ? ? 1 ,
CollectionMethod = "send_invoice"
CollectionMethod = "send_invoice" ,
ProrationDate = prorationDate ,
} ;
} ;
if ( ! subscriptionUpdate . UpdateNeeded ( sub ) )
if ( ! subscriptionUpdate . UpdateNeeded ( sub ) )
@ -792,26 +793,34 @@ public class StripePaymentService : IPaymentService
string paymentIntentClientSecret = null ;
string paymentIntentClientSecret = null ;
try
try
{
{
var subItemOptions = updatedItemOptions . Select ( itemOption = >
new Stripe . InvoiceSubscriptionItemOptions
{
Id = itemOption . Id ,
Plan = itemOption . Plan ,
Quantity = itemOption . Quantity ,
} ) . ToList ( ) ;
var reviewInvoiceResponse = await PreviewUpcomingInvoiceAndPayAsync ( storableSubscriber , subItemOptions ) ;
paymentIntentClientSecret = reviewInvoiceResponse . PaymentIntentClientSecret ;
var subResponse = await _ stripeAdapter . SubscriptionUpdateAsync ( sub . Id , subUpdateOptions ) ;
var subResponse = await _ stripeAdapter . SubscriptionUpdateAsync ( sub . Id , subUpdateOptions ) ;
var invoice =
await _ stripeAdapter . InvoiceGetAsync ( subResponse ? . LatestInvoiceId , new Stripe . InvoiceGetOptions ( ) ) ;
var invoice = await _ stripeAdapter . InvoiceGetAsync ( subResponse ? . LatestInvoiceId , new Stripe . InvoiceGetOptions ( ) ) ;
if ( invoice = = null )
if ( invoice = = null )
{
{
throw new BadRequestException ( "Unable to locate draft invoice for subscription update." ) ;
throw new BadRequestException ( "Unable to locate draft invoice for subscription update." ) ;
}
}
if ( invoice . AmountDue > 0 & & updatedItemOptions . Any ( i = > i . Quantity > 0 ) )
{
try
{
if ( chargeNow )
{
paymentIntentClientSecret = await PayInvoiceAfterSubscriptionChangeAsync (
storableSubscriber , invoice ) ;
}
}
catch ( Exception e )
else
{
invoice = await _ stripeAdapter . InvoiceFinalizeInvoiceAsync ( subResponse . LatestInvoiceId , new Stripe . InvoiceFinalizeOptions
{
AutoAdvance = false ,
} ) ;
await _ stripeAdapter . InvoiceSendInvoiceAsync ( invoice . Id , new Stripe . InvoiceSendOptions ( ) ) ;
paymentIntentClientSecret = null ;
}
}
catch
{
{
// Need to revert the subscription
// Need to revert the subscription
await _ stripeAdapter . SubscriptionUpdateAsync ( sub . Id , new Stripe . SubscriptionUpdateOptions
await _ stripeAdapter . SubscriptionUpdateAsync ( sub . Id , new Stripe . SubscriptionUpdateOptions
@ -825,13 +834,21 @@ public class StripePaymentService : IPaymentService
} ) ;
} ) ;
throw ;
throw ;
}
}
}
else if ( ! invoice . Paid )
{
// Pay invoice with no charge to customer this completes the invoice immediately without waiting the scheduled 1h
invoice = await _ stripeAdapter . InvoicePayAsync ( subResponse . LatestInvoiceId ) ;
paymentIntentClientSecret = null ;
}
}
finally
finally
{
{
// Change back the subscription collection method and/or days until due
// Change back the subscription collection method and/or days until due
if ( collectionMethod ! = "send_invoice" | | daysUntilDue = = null )
if ( collectionMethod ! = "send_invoice" | | daysUntilDue = = null )
{
{
await _ stripeAdapter . SubscriptionUpdateAsync ( sub . Id ,
await _ stripeAdapter . SubscriptionUpdateAsync ( sub . Id , new Stripe . SubscriptionUpdateOptions
new Stripe . SubscriptionUpdateOptions
{
{
CollectionMethod = collectionMethod ,
CollectionMethod = collectionMethod ,
DaysUntilDue = daysUntilDue ,
DaysUntilDue = daysUntilDue ,
@ -918,7 +935,6 @@ public class StripePaymentService : IPaymentService
await _ stripeAdapter . CustomerDeleteAsync ( subscriber . GatewayCustomerId ) ;
await _ stripeAdapter . CustomerDeleteAsync ( subscriber . GatewayCustomerId ) ;
}
}
//This method is no-longer is use because we return the dollar threshold feature on invoice will be generated. but we dont want to lose this implementation.
public async Task < string > PayInvoiceAfterSubscriptionChangeAsync ( ISubscriber subscriber , Stripe . Invoice invoice )
public async Task < string > PayInvoiceAfterSubscriptionChangeAsync ( ISubscriber subscriber , Stripe . Invoice invoice )
{
{
var customerOptions = new Stripe . CustomerGetOptions ( ) ;
var customerOptions = new Stripe . CustomerGetOptions ( ) ;
@ -1088,310 +1104,6 @@ public class StripePaymentService : IPaymentService
return paymentIntentClientSecret ;
return paymentIntentClientSecret ;
}
}
internal async Task < InvoicePreviewResult > PreviewUpcomingInvoiceAndPayAsync ( ISubscriber subscriber ,
List < Stripe . InvoiceSubscriptionItemOptions > subItemOptions , int prorateThreshold = 5 0 0 0 0 )
{
var customer = await CheckInAppPurchaseMethod ( subscriber ) ;
string paymentIntentClientSecret = null ;
var pendingInvoiceItems = GetPendingInvoiceItems ( subscriber ) ;
var upcomingPreview = await GetUpcomingInvoiceAsync ( subscriber , subItemOptions ) ;
var itemsForInvoice = GetItemsForInvoice ( subItemOptions , upcomingPreview , pendingInvoiceItems ) ;
var invoiceAmount = itemsForInvoice ? . Sum ( i = > i . Amount ) ? ? 0 ;
var invoiceNow = invoiceAmount > = prorateThreshold ;
if ( invoiceNow )
{
await ProcessImmediateInvoiceAsync ( subscriber , upcomingPreview , invoiceAmount , customer , itemsForInvoice , pendingInvoiceItems , paymentIntentClientSecret ) ;
}
return new InvoicePreviewResult { IsInvoicedNow = invoiceNow , PaymentIntentClientSecret = paymentIntentClientSecret } ;
}
private async Task < InvoicePreviewResult > ProcessImmediateInvoiceAsync ( ISubscriber subscriber , Invoice upcomingPreview , long invoiceAmount ,
Customer customer , IEnumerable < InvoiceLineItem > itemsForInvoice , PendingInoviceItems pendingInvoiceItems ,
string paymentIntentClientSecret )
{
// Owes more than prorateThreshold on the next invoice.
// Invoice them and pay now instead of waiting until the next billing cycle.
string cardPaymentMethodId = null ;
var invoiceAmountDue = upcomingPreview . StartingBalance + invoiceAmount ;
cardPaymentMethodId = GetCardPaymentMethodId ( invoiceAmountDue , customer , cardPaymentMethodId ) ;
Stripe . Invoice invoice = null ;
var createdInvoiceItems = new List < Stripe . InvoiceItem > ( ) ;
Braintree . Transaction braintreeTransaction = null ;
try
{
await CreateInvoiceItemsAsync ( subscriber , itemsForInvoice , pendingInvoiceItems , createdInvoiceItems ) ;
invoice = await CreateInvoiceAsync ( subscriber , cardPaymentMethodId ) ;
var invoicePayOptions = new Stripe . InvoicePayOptions ( ) ;
await CreateBrainTreeTransactionRequestAsync ( subscriber , invoice , customer , invoicePayOptions ,
cardPaymentMethodId , braintreeTransaction ) ;
await InvoicePayAsync ( invoicePayOptions , invoice , paymentIntentClientSecret ) ;
}
catch ( Exception e )
{
if ( braintreeTransaction ! = null )
{
await _ btGateway . Transaction . RefundAsync ( braintreeTransaction . Id ) ;
}
if ( invoice ! = null )
{
if ( invoice . Status = = "paid" )
{
// It's apparently paid, so we return without throwing an exception
return new InvoicePreviewResult
{
IsInvoicedNow = false ,
PaymentIntentClientSecret = paymentIntentClientSecret
} ;
}
await RestoreInvoiceItemsAsync ( invoice , customer , pendingInvoiceItems . PendingInvoiceItems ) ;
}
else
{
foreach ( var ii in createdInvoiceItems )
{
await _ stripeAdapter . InvoiceDeleteAsync ( ii . Id ) ;
}
}
if ( e is Stripe . StripeException strEx & &
( strEx . StripeError ? . Message ? . Contains ( "cannot be used because it is not verified" ) ? ? false ) )
{
throw new GatewayException ( "Bank account is not yet verified." ) ;
}
throw ;
}
return new InvoicePreviewResult
{
IsInvoicedNow = false ,
PaymentIntentClientSecret = paymentIntentClientSecret
} ;
}
private static IEnumerable < InvoiceLineItem > GetItemsForInvoice ( List < InvoiceSubscriptionItemOptions > subItemOptions , Invoice upcomingPreview ,
PendingInoviceItems pendingInvoiceItems )
{
var itemsForInvoice = upcomingPreview . Lines ? . Data ?
. Where ( i = > pendingInvoiceItems . PendingInvoiceItemsDict . ContainsKey ( i . Id ) | |
( i . Plan . Id = = subItemOptions [ 0 ] ? . Plan & & i . Proration ) ) ;
return itemsForInvoice ;
}
private PendingInoviceItems GetPendingInvoiceItems ( ISubscriber subscriber )
{
var pendingInvoiceItems = new PendingInoviceItems ( ) ;
var invoiceItems = _ stripeAdapter . InvoiceItemListAsync ( new Stripe . InvoiceItemListOptions
{
Customer = subscriber . GatewayCustomerId
} ) . ToList ( ) . Where ( i = > i . InvoiceId = = null ) ;
pendingInvoiceItems . PendingInvoiceItemsDict = invoiceItems . ToDictionary ( pii = > pii . Id ) ;
return pendingInvoiceItems ;
}
private async Task < Customer > CheckInAppPurchaseMethod ( ISubscriber subscriber )
{
var customerOptions = GetCustomerPaymentOptions ( ) ;
var customer = await _ stripeAdapter . CustomerGetAsync ( subscriber . GatewayCustomerId , customerOptions ) ;
var usingInAppPaymentMethod = customer . Metadata . ContainsKey ( "appleReceipt" ) ;
if ( usingInAppPaymentMethod )
{
throw new BadRequestException ( "Cannot perform this action with in-app purchase payment method. " +
"Contact support." ) ;
}
return customer ;
}
private string GetCardPaymentMethodId ( long invoiceAmountDue , Customer customer , string cardPaymentMethodId )
{
try
{
if ( invoiceAmountDue < = 0 | | customer . Metadata . ContainsKey ( "btCustomerId" ) ) return cardPaymentMethodId ;
var hasDefaultCardPaymentMethod = customer . InvoiceSettings ? . DefaultPaymentMethod ? . Type = = "card" ;
var hasDefaultValidSource = customer . DefaultSource ! = null & &
( customer . DefaultSource is Stripe . Card | |
customer . DefaultSource is Stripe . BankAccount ) ;
if ( hasDefaultCardPaymentMethod | | hasDefaultValidSource ) return cardPaymentMethodId ;
cardPaymentMethodId = GetLatestCardPaymentMethod ( customer . Id ) ? . Id ;
if ( cardPaymentMethodId = = null )
{
throw new BadRequestException ( "No payment method is available." ) ;
}
}
catch ( Exception e )
{
throw new BadRequestException ( "No payment method is available." ) ;
}
return cardPaymentMethodId ;
}
private async Task < Invoice > GetUpcomingInvoiceAsync ( ISubscriber subscriber , List < InvoiceSubscriptionItemOptions > subItemOptions )
{
var upcomingPreview = await _ stripeAdapter . InvoiceUpcomingAsync ( new Stripe . UpcomingInvoiceOptions
{
Customer = subscriber . GatewayCustomerId ,
Subscription = subscriber . GatewaySubscriptionId ,
SubscriptionItems = subItemOptions
} ) ;
return upcomingPreview ;
}
private async Task RestoreInvoiceItemsAsync ( Invoice invoice , Customer customer , IEnumerable < InvoiceItem > pendingInvoiceItems )
{
invoice = await _ stripeAdapter . InvoiceVoidInvoiceAsync ( invoice . Id , new Stripe . InvoiceVoidOptions ( ) ) ;
if ( invoice . StartingBalance ! = 0 )
{
await _ stripeAdapter . CustomerUpdateAsync ( customer . Id ,
new Stripe . CustomerUpdateOptions { Balance = customer . Balance } ) ;
}
// Restore invoice items that were brought in
foreach ( var item in pendingInvoiceItems )
{
var i = new Stripe . InvoiceItemCreateOptions
{
Currency = item . Currency ,
Description = item . Description ,
Customer = item . CustomerId ,
Subscription = item . SubscriptionId ,
Discountable = item . Discountable ,
Metadata = item . Metadata ,
Quantity = item . Proration ? 1 : item . Quantity ,
UnitAmount = item . UnitAmount
} ;
await _ stripeAdapter . InvoiceItemCreateAsync ( i ) ;
}
}
private async Task InvoicePayAsync ( InvoicePayOptions invoicePayOptions , Invoice invoice , string paymentIntentClientSecret )
{
try
{
await _ stripeAdapter . InvoicePayAsync ( invoice . Id , invoicePayOptions ) ;
}
catch ( Stripe . StripeException e )
{
if ( e . HttpStatusCode = = System . Net . HttpStatusCode . PaymentRequired & &
e . StripeError ? . Code = = "invoice_payment_intent_requires_action" )
{
// SCA required, get intent client secret
var invoiceGetOptions = new Stripe . InvoiceGetOptions ( ) ;
invoiceGetOptions . AddExpand ( "payment_intent" ) ;
invoice = await _ stripeAdapter . InvoiceGetAsync ( invoice . Id , invoiceGetOptions ) ;
paymentIntentClientSecret = invoice ? . PaymentIntent ? . ClientSecret ;
}
else
{
throw new GatewayException ( "Unable to pay invoice." ) ;
}
}
}
private async Task CreateBrainTreeTransactionRequestAsync ( ISubscriber subscriber , Invoice invoice , Customer customer ,
InvoicePayOptions invoicePayOptions , string cardPaymentMethodId , Braintree . Transaction braintreeTransaction )
{
if ( invoice . AmountDue > 0 )
{
if ( customer ? . Metadata ? . ContainsKey ( "btCustomerId" ) ? ? false )
{
invoicePayOptions . PaidOutOfBand = true ;
var btInvoiceAmount = ( invoice . AmountDue / 1 0 0 M ) ;
var transactionResult = await _ btGateway . Transaction . SaleAsync (
new Braintree . TransactionRequest
{
Amount = btInvoiceAmount ,
CustomerId = customer . Metadata [ "btCustomerId" ] ,
Options = new Braintree . TransactionOptionsRequest
{
SubmitForSettlement = true ,
PayPal = new Braintree . TransactionOptionsPayPalRequest
{
CustomField = $"{subscriber.BraintreeIdField()}:{subscriber.Id}"
}
} ,
CustomFields = new Dictionary < string , string >
{
[subscriber.BraintreeIdField()] = subscriber . Id . ToString ( )
}
} ) ;
if ( ! transactionResult . IsSuccess ( ) )
{
throw new GatewayException ( "Failed to charge PayPal customer." ) ;
}
braintreeTransaction = transactionResult . Target ;
await _ stripeAdapter . InvoiceUpdateAsync ( invoice . Id , new Stripe . InvoiceUpdateOptions
{
Metadata = new Dictionary < string , string >
{
["btTransactionId"] = braintreeTransaction . Id ,
["btPayPalTransactionId"] =
braintreeTransaction . PayPalDetails . AuthorizationId
}
} ) ;
}
else
{
invoicePayOptions . OffSession = true ;
invoicePayOptions . PaymentMethod = cardPaymentMethodId ;
}
}
}
private async Task < Invoice > CreateInvoiceAsync ( ISubscriber subscriber , string cardPaymentMethodId )
{
Invoice invoice ;
invoice = await _ stripeAdapter . InvoiceCreateAsync ( new Stripe . InvoiceCreateOptions
{
CollectionMethod = "send_invoice" ,
DaysUntilDue = 1 ,
Customer = subscriber . GatewayCustomerId ,
Subscription = subscriber . GatewaySubscriptionId ,
DefaultPaymentMethod = cardPaymentMethodId
} ) ;
return invoice ;
}
private async Task CreateInvoiceItemsAsync ( ISubscriber subscriber , IEnumerable < InvoiceLineItem > itemsForInvoice ,
PendingInoviceItems pendingInvoiceItems , List < InvoiceItem > createdInvoiceItems )
{
foreach ( var invoiceLineItem in itemsForInvoice )
{
if ( pendingInvoiceItems . PendingInvoiceItemsDict . ContainsKey ( invoiceLineItem . Id ) )
{
continue ;
}
var invoiceItem = await _ stripeAdapter . InvoiceItemCreateAsync ( new Stripe . InvoiceItemCreateOptions
{
Currency = invoiceLineItem . Currency ,
Description = invoiceLineItem . Description ,
Customer = subscriber . GatewayCustomerId ,
Subscription = invoiceLineItem . Subscription ,
Discountable = invoiceLineItem . Discountable ,
Amount = invoiceLineItem . Amount
} ) ;
createdInvoiceItems . Add ( invoiceItem ) ;
}
}
public async Task CancelSubscriptionAsync ( ISubscriber subscriber , bool endOfPeriod = false ,
public async Task CancelSubscriptionAsync ( ISubscriber subscriber , bool endOfPeriod = false ,
bool skipInAppPurchaseCheck = false )
bool skipInAppPurchaseCheck = false )
{
{