From d04d92bb89230ae979e6c01cc32bc5288e87e6b0 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 23 Sep 2019 09:03:18 -0400 Subject: [PATCH] AttemptToPayInvoiceWithAppleReceipt --- src/Billing/Controllers/StripeController.cs | 117 +++++++++++++++++++- 1 file changed, 114 insertions(+), 3 deletions(-) diff --git a/src/Billing/Controllers/StripeController.cs b/src/Billing/Controllers/StripeController.cs index db193b942..a8e869be7 100644 --- a/src/Billing/Controllers/StripeController.cs +++ b/src/Billing/Controllers/StripeController.cs @@ -21,12 +21,15 @@ namespace Bit.Billing.Controllers [Route("stripe")] public class StripeController : Controller { + private const decimal PremiumPlanAppleIapPrice = 14.99M; + private readonly BillingSettings _billingSettings; private readonly IHostingEnvironment _hostingEnvironment; private readonly IOrganizationService _organizationService; private readonly IOrganizationRepository _organizationRepository; private readonly ITransactionRepository _transactionRepository; private readonly IUserService _userService; + private readonly IAppleIapService _appleIapService; private readonly IMailService _mailService; private readonly ILogger _logger; private readonly Braintree.BraintreeGateway _btGateway; @@ -39,6 +42,7 @@ namespace Bit.Billing.Controllers IOrganizationRepository organizationRepository, ITransactionRepository transactionRepository, IUserService userService, + IAppleIapService appleIapService, IMailService mailService, ILogger logger) { @@ -48,6 +52,7 @@ namespace Bit.Billing.Controllers _organizationRepository = organizationRepository; _transactionRepository = transactionRepository; _userService = userService; + _appleIapService = appleIapService; _mailService = mailService; _logger = logger; _btGateway = new Braintree.BraintreeGateway @@ -384,7 +389,7 @@ namespace Bit.Billing.Controllers var invoice = await GetInvoiceAsync(parsedEvent, true); if(!invoice.Paid && invoice.AttemptCount > 1 && UnpaidAutoChargeInvoiceForSubscriptionCycle(invoice)) { - await AttemptToPayInvoiceWithBraintreeAsync(invoice); + await AttemptToPayInvoiceAsync(invoice); } } else if(parsedEvent.Type.Equals("invoice.created")) @@ -392,7 +397,7 @@ namespace Bit.Billing.Controllers var invoice = await GetInvoiceAsync(parsedEvent, true); if(!invoice.Paid && UnpaidAutoChargeInvoiceForSubscriptionCycle(invoice)) { - await AttemptToPayInvoiceWithBraintreeAsync(invoice); + await AttemptToPayInvoiceAsync(invoice); } } else @@ -455,10 +460,116 @@ namespace Bit.Billing.Controllers } } - private async Task AttemptToPayInvoiceWithBraintreeAsync(Invoice invoice) + private async Task AttemptToPayInvoiceAsync(Invoice invoice) { var customerService = new CustomerService(); var customer = await customerService.GetAsync(invoice.CustomerId); + if(customer?.Metadata?.ContainsKey("appleReceipt") ?? false) + { + return await AttemptToPayInvoiceWithAppleReceiptAsync(invoice, customer); + } + else if(customer?.Metadata?.ContainsKey("btCustomerId") ?? false) + { + return await AttemptToPayInvoiceWithBraintreeAsync(invoice, customer); + } + return false; + } + + private async Task AttemptToPayInvoiceWithAppleReceiptAsync(Invoice invoice, Customer customer) + { + if(!customer?.Metadata?.ContainsKey("appleReceipt") ?? true) + { + return false; + } + + var originalAppleReceiptTransactionId = customer.Metadata["appleReceipt"]; + var appleReceiptRecord = await _appleIapService.GetReceiptAsync(originalAppleReceiptTransactionId); + if(string.IsNullOrWhiteSpace(appleReceiptRecord?.Item1) || !appleReceiptRecord.Item2.HasValue) + { + return false; + } + + var subscriptionService = new SubscriptionService(); + var subscription = await subscriptionService.GetAsync(invoice.SubscriptionId); + var ids = GetIdsFromMetaData(subscription?.Metadata); + if(!ids.Item2.HasValue) + { + // Apple receipt is only for user subscriptions + return false; + } + + if(appleReceiptRecord.Item2.Value != ids.Item2.Value) + { + _logger.LogError("User Ids for Apple Receipt and subscription do not match: {0} != {1}.", + appleReceiptRecord.Item2.Value, ids.Item2.Value); + return false; + } + + var appleReceiptStatus = await _appleIapService.GetVerifiedReceiptStatusAsync(appleReceiptRecord.Item1); + if(appleReceiptStatus == null) + { + // TODO: cancel sub if receipt is cancelled? + return false; + } + + var receiptExpiration = appleReceiptStatus.GetLastExpiresDate().GetValueOrDefault(DateTime.MinValue); + var invoiceDue = invoice.DueDate.GetValueOrDefault(DateTime.MinValue); + if(receiptExpiration <= invoiceDue) + { + _logger.LogWarning("Apple receipt expiration is before invoice due date. {0} <= {1}", + receiptExpiration, invoiceDue); + return false; + } + + var receiptLastTransactionId = appleReceiptStatus.GetLastTransactionId(); + var existingTransaction = await _transactionRepository.GetByGatewayIdAsync( + GatewayType.AppStore, receiptLastTransactionId); + if(existingTransaction != null) + { + _logger.LogWarning("There is already an existing transaction for this Apple receipt.", + receiptLastTransactionId); + return false; + } + + var appleTransaction = appleReceiptStatus.BuildTransactionFromLastTransaction( + PremiumPlanAppleIapPrice, ids.Item2.Value); + appleTransaction.Type = TransactionType.Charge; + + var invoiceService = new InvoiceService(); + try + { + await invoiceService.UpdateAsync(invoice.Id, new InvoiceUpdateOptions + { + Metadata = new Dictionary + { + ["appleReceipt"] = appleReceiptStatus.GetOriginalTransactionId(), + ["appleReceiptTransactionId"] = receiptLastTransactionId + } + }); + + await _transactionRepository.CreateAsync(appleTransaction); + await invoiceService.PayAsync(invoice.Id, new InvoicePayOptions { PaidOutOfBand = true }); + } + catch(Exception e) + { + if(e.Message.Contains("Invoice is already paid")) + { + await invoiceService.UpdateAsync(invoice.Id, new InvoiceUpdateOptions + { + Metadata = invoice.Metadata + }); + } + else + { + throw e; + } + } + + return true; + } + + private async Task AttemptToPayInvoiceWithBraintreeAsync(Invoice invoice, Customer customer) + { if(!customer?.Metadata?.ContainsKey("btCustomerId") ?? true) { return false;