public async Task ReinstateSubscriptionAsync(ISubscriber subscriber) { if (subscriber == null) { throw new ArgumentNullException(nameof(subscriber)); } if (string.IsNullOrWhiteSpace(subscriber.GatewaySubscriptionId)) { throw new GatewayException("No subscription."); } var subscriptionService = new SubscriptionService(); var sub = await subscriptionService.GetAsync(subscriber.GatewaySubscriptionId); if (sub == null) { throw new GatewayException("Subscription was not found."); } if ((sub.Status != "active" && sub.Status != "trialing") || !sub.CanceledAt.HasValue) { throw new GatewayException("Subscription is not marked for cancellation."); } var updatedSub = await subscriptionService.UpdateAsync(sub.Id, new SubscriptionUpdateOptions { CancelAtPeriodEnd = false }); if (updatedSub.CanceledAt.HasValue) { throw new GatewayException("Unable to reinstate subscription."); } }
public async Task <SubscriptionInfo> GetSubscriptionAsync(ISubscriber subscriber) { var subscriptionInfo = new SubscriptionInfo(); var subscriptionService = new SubscriptionService(); var invoiceService = new InvoiceService(); if (!string.IsNullOrWhiteSpace(subscriber.GatewaySubscriptionId)) { var sub = await subscriptionService.GetAsync(subscriber.GatewaySubscriptionId); if (sub != null) { subscriptionInfo.Subscription = new SubscriptionInfo.BillingSubscription(sub); } if (!sub.CanceledAt.HasValue && !string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId)) { try { var upcomingInvoice = await invoiceService.UpcomingAsync( new UpcomingInvoiceOptions { CustomerId = subscriber.GatewayCustomerId }); if (upcomingInvoice != null) { subscriptionInfo.UpcomingInvoice = new SubscriptionInfo.BillingUpcomingInvoice(upcomingInvoice); } } catch (StripeException) { } } } return(subscriptionInfo); }
// cancel subscription public async Task <IdResponseModel> CancelSubscription(string subscriptionId, int userId) { var user = await EnsureCustomerCreatedAsync(userId); SetStripeApiKey(); var service = new SubscriptionService(); Subscription subscription = null; try { subscription = await service.GetAsync(subscriptionId); if (subscription.CustomerId != user.Profile.StripeCustomerId) { throw new CustomException(HttpStatusCode.BadRequest, "subscriptionId", "Invalid subscription id"); } subscription = await service.CancelAsync(subscriptionId, null); } catch (StripeException ex) { throw new CustomException(HttpStatusCode.BadRequest, ex.StripeError?.Parameter, ex.StripeError?.Message); } return(new IdResponseModel { Id = subscription?.Id }); }
private async Task UpdateSubscription(string subscriptionId, string newPlanId, bool isProrateCharged = false) { var subscriptionService = new SubscriptionService(); var subscription = await subscriptionService.GetAsync(subscriptionId); await subscriptionService.UpdateAsync(subscriptionId, new SubscriptionUpdateOptions { CancelAtPeriodEnd = false, Items = new List <SubscriptionItemOptions> { new SubscriptionItemOptions { Id = subscription.Items.Data[0].Id, Plan = newPlanId } }, Prorate = !isProrateCharged }); var lastRecurringPayment = await _subscriptionPaymentRepository.GetByGatewayAndPaymentIdAsync(SubscriptionPaymentGatewayType.Stripe, subscriptionId); var payment = await _subscriptionPaymentRepository.GetLastPaymentOrDefaultAsync( tenantId : lastRecurringPayment.TenantId, SubscriptionPaymentGatewayType.Stripe, isRecurring : true); payment.IsRecurring = false; }
public override async Task <ApiResult> RefundPaymentAsync(PaymentProviderContext <StripeCheckoutSettings> ctx) { try { // We can only refund a captured charge, so make sure we have one // otherwise there is nothing we can do var chargeId = ctx.Order.Properties["stripeChargeId"]; if (string.IsNullOrWhiteSpace(chargeId)) { return(null); } var secretKey = ctx.Settings.TestMode ? ctx.Settings.TestSecretKey : ctx.Settings.LiveSecretKey; ConfigureStripe(secretKey); var refundService = new RefundService(); var refundCreateOptions = new RefundCreateOptions() { Charge = chargeId }; var refund = refundService.Create(refundCreateOptions); var charge = refund.Charge ?? await new ChargeService().GetAsync(refund.ChargeId); // If we have a subscription then we'll cancel it as refunding an ctx.Order // should effecitvely undo any purchase if (!string.IsNullOrWhiteSpace(ctx.Order.Properties["stripeSubscriptionId"])) { var subscriptionService = new SubscriptionService(); var subscription = await subscriptionService.GetAsync(ctx.Order.Properties["stripeSubscriptionId"]); if (subscription != null) { subscriptionService.Cancel(ctx.Order.Properties["stripeSubscriptionId"], new SubscriptionCancelOptions { InvoiceNow = false, Prorate = false }); } } return(new ApiResult() { TransactionInfo = new TransactionInfoUpdate() { TransactionId = GetTransactionId(charge), PaymentStatus = GetPaymentStatus(charge) } }); } catch (Exception ex) { _logger.Error(ex, "Stripe - RefundPayment"); } return(ApiResult.Empty); }
public async Task <Subscription> GetSubscriptionAsync(string subscriptionId) { var options = new SubscriptionGetOptions { }; var subscription = await _subscriptionService.GetAsync(subscriptionId, options); return(subscription); }
public async Task <PaymentGatewayResult <IPaymentSubscription> > UpdateSubscriptionAsync(string planId, ChargeType type, string subscriptionId, string?coupon) { try { var subscription = await _subscriptionService.GetAsync(subscriptionId); if (subscription == null) { throw new StripeException("Subscription Not Found") { HttpStatusCode = HttpStatusCode.NotFound } } ; var opt = new SubscriptionUpdateOptions { CancelAtPeriodEnd = false, Items = new List <SubscriptionItemUpdateOption> { new SubscriptionItemUpdateOption { Id = subscription.Items.Data.FirstOrDefault()?.Id, PlanId = planId }, }, CouponId = coupon, Prorate = true, ProrationDate = DateTime.UtcNow, CollectionMethod = _chargeTypes[(int)type], }; if (type == ChargeType.Manual) { opt.DaysUntilDue = _appSettings.Stripe.InvoiceDueDays; } subscription = await _subscriptionService.UpdateAsync(subscription.Id, opt); return(PaymentGatewayResult <IPaymentSubscription> .Success(_mapper.Map <IPaymentSubscription>(subscription))); } catch (StripeException e) { return(PaymentGatewayResult <IPaymentSubscription> .Failed(e)); } }
public async Task CancelSubscriptionAsync(ISubscriber subscriber, bool endOfPeriod = false) { if (subscriber == null) { throw new ArgumentNullException(nameof(subscriber)); } if (string.IsNullOrWhiteSpace(subscriber.GatewaySubscriptionId)) { throw new GatewayException("No subscription."); } var subscriptionService = new SubscriptionService(); var sub = await subscriptionService.GetAsync(subscriber.GatewaySubscriptionId); if (sub == null) { throw new GatewayException("Subscription was not found."); } if (sub.CanceledAt.HasValue || sub.Status == "canceled" || sub.Status == "unpaid") { // Already canceled return; } try { var canceledSub = endOfPeriod ? await subscriptionService.UpdateAsync(sub.Id, new SubscriptionUpdateOptions { CancelAtPeriodEnd = true }) : await subscriptionService.CancelAsync(sub.Id, new SubscriptionCancelOptions()); if (!canceledSub.CanceledAt.HasValue) { throw new GatewayException("Unable to cancel subscription."); } } catch (StripeException e) { if (e.Message != $"No such subscription: {subscriber.GatewaySubscriptionId}") { throw e; } } }
private async Task<Subscription> GetSubscriptionAsync(Stripe.Event parsedEvent, bool fresh = false) { if(!(parsedEvent.Data.Object is Subscription eventSubscription)) { throw new Exception("Subscription is null (from parsed event). " + parsedEvent.Id); } if(!fresh) { return eventSubscription; } var subscriptionService = new SubscriptionService(); var subscription = await subscriptionService.GetAsync(eventSubscription.Id); if(subscription == null) { throw new Exception("Subscription is null. " + eventSubscription.Id); } return subscription; }
public async Task CancelSubscription(string subscriptionId, ClaimsPrincipal userFor, CancellationToken token) { var user = await userManager.GetUserAsync(userFor).ConfigureAwait(false); Subscription subscription = await subscriptionService.GetAsync(subscriptionId, cancellationToken : token).ConfigureAwait(false); Customer customer = await customerService.GetAsync(subscription.CustomerId, cancellationToken : token).ConfigureAwait(false); if (!customer.Email.Equals(user.Email, StringComparison.OrdinalIgnoreCase)) { throw new InvalidOperationException($"User {user.Email} doesn't match subscription e-mail {subscription.Customer.Email}"); } subscription = await subscriptionService.CancelAsync(subscriptionId, new SubscriptionCancelOptions(), cancellationToken : token).ConfigureAwait(false); context.DonationEventLog.Add(new DonationEventLog(userId: user.Id, type: DonationEventType.Internal, eventData: subscription.ToJson())); await context.SaveChangesAsync(token).ConfigureAwait(false); }
private async Task HandlePaymentFailed(Invoice invoice) { if (!invoice.Paid && invoice.AttemptCount > 1 && UnpaidAutoChargeInvoiceForSubscriptionCycle(invoice)) { var subscriptionService = new SubscriptionService(); var subscription = await subscriptionService.GetAsync(invoice.SubscriptionId); // attempt count 4 = 11 days after initial failure if (invoice.AttemptCount > 3 && subscription.Items.Any(i => i.Price.Id == PremiumPlanId || i.Price.Id == PremiumPlanIdAppStore)) { await CancelSubscription(invoice.SubscriptionId); await VoidOpenInvoices(invoice.SubscriptionId); } else { await AttemptToPayInvoiceAsync(invoice); } } }
private async Task <bool> AttemptToPayInvoiceWithBraintreeAsync(Invoice invoice) { var customerService = new CustomerService(); var customer = await customerService.GetAsync(invoice.CustomerId); if (!customer?.Metadata?.ContainsKey("btCustomerId") ?? true) { return(false); } var subscriptionService = new SubscriptionService(); var subscription = await subscriptionService.GetAsync(invoice.SubscriptionId); var ids = GetIdsFromMetaData(subscription?.Metadata); if (!ids.Item1.HasValue && !ids.Item2.HasValue) { return(false); } var btObjIdField = ids.Item1.HasValue ? "organization_id" : "user_id"; var btObjId = ids.Item1 ?? ids.Item2.Value; var btInvoiceAmount = (invoice.AmountDue / 100M); 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 = $"{btObjIdField}:{btObjId}" } }, CustomFields = new Dictionary <string, string> { [btObjIdField] = btObjId.ToString() } }); if (!transactionResult.IsSuccess()) { // TODO: Send payment failure email? return(false); } try { var invoiceService = new InvoiceService(); await invoiceService.UpdateAsync(invoice.Id, new InvoiceUpdateOptions { Metadata = new Dictionary <string, string> { ["btTransactionId"] = transactionResult.Target.Id, ["btPayPalTransactionId"] = transactionResult.Target.PayPalDetails?.AuthorizationId } }); await invoiceService.PayAsync(invoice.Id, new InvoicePayOptions { PaidOutOfBand = true }); } catch (Exception e) { await _btGateway.Transaction.RefundAsync(transactionResult.Target.Id); throw e; } return(true); }
public async Task UpdateSubscription(string subscriptionId, string newPlanId, decimal newAmount, string interval) { var subscriptionService = new SubscriptionService(); var subscription = await subscriptionService.GetAsync(subscriptionId); if (!await DoesPlanExistAsync(newPlanId)) { await CreatePlanAsync(newPlanId, newAmount, interval, ProductName); } var oldPlanId = subscription.Items.Data[0].Plan.Id; await subscriptionService.UpdateAsync(subscriptionId, new SubscriptionUpdateOptions { CancelAtPeriodEnd = false, Items = new List <SubscriptionItemUpdateOption> { new SubscriptionItemUpdateOption { Id = subscription.Items.Data[0].Id, PlanId = newPlanId } } }); var invoiceService = new InvoiceService(); var invoice = await invoiceService.CreateAsync(new InvoiceCreateOptions { SubscriptionId = subscription.Id, CustomerId = subscription.CustomerId }); if (!invoice.Paid) { invoice = await invoiceService.PayAsync(invoice.Id, null); if (!invoice.Paid) { await subscriptionService.UpdateAsync(subscriptionId, new SubscriptionUpdateOptions { CancelAtPeriodEnd = false, Items = new List <SubscriptionItemUpdateOption> { new SubscriptionItemUpdateOption { Id = subscription.Items.Data[0].Id, PlanId = oldPlanId } } }); throw new UserFriendlyException(L("PaymentCouldNotCompleted")); } } var lastRecurringPayment = await _subscriptionPaymentRepository.GetByGatewayAndPaymentIdAsync(SubscriptionPaymentGatewayType.Stripe, subscriptionId); var payment = await _subscriptionPaymentRepository.GetLastPaymentOrDefaultAsync( tenantId : lastRecurringPayment.TenantId, SubscriptionPaymentGatewayType.Stripe, isRecurring : true); payment.Amount = ConvertFromStripePrice(invoice.Total); payment.IsRecurring = false; payment.ExternalPaymentId = invoice.ChargeId; payment.SetAsPaid(); }
public async Task <IActionResult> PostWebhook([FromQuery] string key) { if (key != _billingSettings.StripeWebhookKey) { return(new BadRequestResult()); } Stripe.Event parsedEvent; using (var sr = new StreamReader(HttpContext.Request.Body)) { var json = await sr.ReadToEndAsync(); parsedEvent = EventUtility.ConstructEvent(json, Request.Headers["Stripe-Signature"], _billingSettings.StripeWebhookSecret); } if (string.IsNullOrWhiteSpace(parsedEvent?.Id)) { _logger.LogWarning("No event id."); return(new BadRequestResult()); } if (_hostingEnvironment.IsProduction() && !parsedEvent.Livemode) { _logger.LogWarning("Getting test events in production."); return(new BadRequestResult()); } var subDeleted = parsedEvent.Type.Equals("customer.subscription.deleted"); var subUpdated = parsedEvent.Type.Equals("customer.subscription.updated"); if (subDeleted || subUpdated) { var subscription = await GetSubscriptionAsync(parsedEvent, true); var ids = GetIdsFromMetaData(subscription.Metadata); var subCanceled = subDeleted && subscription.Status == "canceled"; var subUnpaid = subUpdated && subscription.Status == "unpaid"; var subIncompleteExpired = subUpdated && subscription.Status == "incomplete_expired"; if (subCanceled || subUnpaid || subIncompleteExpired) { // org if (ids.Item1.HasValue) { await _organizationService.DisableAsync(ids.Item1.Value, subscription.CurrentPeriodEnd); } // user else if (ids.Item2.HasValue) { await _userService.DisablePremiumAsync(ids.Item2.Value, subscription.CurrentPeriodEnd); } } if (subUpdated) { // org if (ids.Item1.HasValue) { await _organizationService.UpdateExpirationDateAsync(ids.Item1.Value, subscription.CurrentPeriodEnd); } // user else if (ids.Item2.HasValue) { await _userService.UpdatePremiumExpirationAsync(ids.Item2.Value, subscription.CurrentPeriodEnd); } } } else if (parsedEvent.Type.Equals("invoice.upcoming")) { var invoice = await GetInvoiceAsync(parsedEvent); var subscriptionService = new SubscriptionService(); var subscription = await subscriptionService.GetAsync(invoice.SubscriptionId); if (subscription == null) { throw new Exception("Invoice subscription is null. " + invoice.Id); } subscription = await VerifyCorrectTaxRateForCharge(invoice, subscription); string email = null; var ids = GetIdsFromMetaData(subscription.Metadata); // org if (ids.Item1.HasValue) { var org = await _organizationRepository.GetByIdAsync(ids.Item1.Value); if (org != null && OrgPlanForInvoiceNotifications(org)) { email = org.BillingEmail; } } // user else if (ids.Item2.HasValue) { var user = await _userService.GetUserByIdAsync(ids.Item2.Value); if (user.Premium) { email = user.Email; } } if (!string.IsNullOrWhiteSpace(email) && invoice.NextPaymentAttempt.HasValue) { var items = invoice.Lines.Select(i => i.Description).ToList(); await _mailService.SendInvoiceUpcomingAsync(email, invoice.AmountDue / 100M, invoice.NextPaymentAttempt.Value, items, true); } } else if (parsedEvent.Type.Equals("charge.succeeded")) { var charge = await GetChargeAsync(parsedEvent); var chargeTransaction = await _transactionRepository.GetByGatewayIdAsync( GatewayType.Stripe, charge.Id); if (chargeTransaction != null) { _logger.LogWarning("Charge success already processed. " + charge.Id); return(new OkResult()); } Tuple <Guid?, Guid?> ids = null; Subscription subscription = null; var subscriptionService = new SubscriptionService(); if (charge.InvoiceId != null) { var invoiceService = new InvoiceService(); var invoice = await invoiceService.GetAsync(charge.InvoiceId); if (invoice?.SubscriptionId != null) { subscription = await subscriptionService.GetAsync(invoice.SubscriptionId); ids = GetIdsFromMetaData(subscription?.Metadata); } } if (subscription == null || ids == null || (ids.Item1.HasValue && ids.Item2.HasValue)) { var subscriptions = await subscriptionService.ListAsync(new SubscriptionListOptions { Customer = charge.CustomerId }); foreach (var sub in subscriptions) { if (sub.Status != "canceled" && sub.Status != "incomplete_expired") { ids = GetIdsFromMetaData(sub.Metadata); if (ids.Item1.HasValue || ids.Item2.HasValue) { subscription = sub; break; } } } } if (!ids.Item1.HasValue && !ids.Item2.HasValue) { _logger.LogWarning("Charge success has no subscriber ids. " + charge.Id); return(new BadRequestResult()); } var tx = new Transaction { Amount = charge.Amount / 100M, CreationDate = charge.Created, OrganizationId = ids.Item1, UserId = ids.Item2, Type = TransactionType.Charge, Gateway = GatewayType.Stripe, GatewayId = charge.Id }; if (charge.Source != null && charge.Source is Card card) { tx.PaymentMethodType = PaymentMethodType.Card; tx.Details = $"{card.Brand}, *{card.Last4}"; } else if (charge.Source != null && charge.Source is BankAccount bankAccount) { tx.PaymentMethodType = PaymentMethodType.BankAccount; tx.Details = $"{bankAccount.BankName}, *{bankAccount.Last4}"; } else if (charge.Source != null && charge.Source is Source source) { if (source.Card != null) { tx.PaymentMethodType = PaymentMethodType.Card; tx.Details = $"{source.Card.Brand}, *{source.Card.Last4}"; } else if (source.AchDebit != null) { tx.PaymentMethodType = PaymentMethodType.BankAccount; tx.Details = $"{source.AchDebit.BankName}, *{source.AchDebit.Last4}"; } else if (source.AchCreditTransfer != null) { tx.PaymentMethodType = PaymentMethodType.BankAccount; tx.Details = $"ACH => {source.AchCreditTransfer.BankName}, " + $"{source.AchCreditTransfer.AccountNumber}"; } } else if (charge.PaymentMethodDetails != null) { if (charge.PaymentMethodDetails.Card != null) { tx.PaymentMethodType = PaymentMethodType.Card; tx.Details = $"{charge.PaymentMethodDetails.Card.Brand?.ToUpperInvariant()}, " + $"*{charge.PaymentMethodDetails.Card.Last4}"; } else if (charge.PaymentMethodDetails.AchDebit != null) { tx.PaymentMethodType = PaymentMethodType.BankAccount; tx.Details = $"{charge.PaymentMethodDetails.AchDebit.BankName}, " + $"*{charge.PaymentMethodDetails.AchDebit.Last4}"; } else if (charge.PaymentMethodDetails.AchCreditTransfer != null) { tx.PaymentMethodType = PaymentMethodType.BankAccount; tx.Details = $"ACH => {charge.PaymentMethodDetails.AchCreditTransfer.BankName}, " + $"{charge.PaymentMethodDetails.AchCreditTransfer.AccountNumber}"; } } if (!tx.PaymentMethodType.HasValue) { _logger.LogWarning("Charge success from unsupported source/method. " + charge.Id); return(new OkResult()); } try { await _transactionRepository.CreateAsync(tx); } // Catch foreign key violations because user/org could have been deleted. catch (SqlException e) when(e.Number == 547) { } } else if (parsedEvent.Type.Equals("charge.refunded")) { var charge = await GetChargeAsync(parsedEvent); var chargeTransaction = await _transactionRepository.GetByGatewayIdAsync( GatewayType.Stripe, charge.Id); if (chargeTransaction == null) { throw new Exception("Cannot find refunded charge. " + charge.Id); } var amountRefunded = charge.AmountRefunded / 100M; if (!chargeTransaction.Refunded.GetValueOrDefault() && chargeTransaction.RefundedAmount.GetValueOrDefault() < amountRefunded) { chargeTransaction.RefundedAmount = amountRefunded; if (charge.Refunded) { chargeTransaction.Refunded = true; } await _transactionRepository.ReplaceAsync(chargeTransaction); foreach (var refund in charge.Refunds) { var refundTransaction = await _transactionRepository.GetByGatewayIdAsync( GatewayType.Stripe, refund.Id); if (refundTransaction != null) { continue; } await _transactionRepository.CreateAsync(new Transaction { Amount = refund.Amount / 100M, CreationDate = refund.Created, OrganizationId = chargeTransaction.OrganizationId, UserId = chargeTransaction.UserId, Type = TransactionType.Refund, Gateway = GatewayType.Stripe, GatewayId = refund.Id, PaymentMethodType = chargeTransaction.PaymentMethodType, Details = chargeTransaction.Details }); } } else { _logger.LogWarning("Charge refund amount doesn't seem correct. " + charge.Id); } } else if (parsedEvent.Type.Equals("invoice.payment_succeeded")) { var invoice = await GetInvoiceAsync(parsedEvent, true); if (invoice.Paid && invoice.BillingReason == "subscription_create") { var subscriptionService = new SubscriptionService(); var subscription = await subscriptionService.GetAsync(invoice.SubscriptionId); if (subscription?.Status == "active") { if (DateTime.UtcNow - invoice.Created < TimeSpan.FromMinutes(1)) { await Task.Delay(5000); } var ids = GetIdsFromMetaData(subscription.Metadata); // org if (ids.Item1.HasValue) { if (subscription.Items.Any(i => StaticStore.Plans.Any(p => p.StripePlanId == i.Plan.Id))) { await _organizationService.EnableAsync(ids.Item1.Value, subscription.CurrentPeriodEnd); } } // user else if (ids.Item2.HasValue) { if (subscription.Items.Any(i => i.Plan.Id == "premium-annually")) { await _userService.EnablePremiumAsync(ids.Item2.Value, subscription.CurrentPeriodEnd); } } if (ids.Item1.HasValue || ids.Item2.HasValue) { await _referenceEventService.RaiseEventAsync( new ReferenceEvent(ReferenceEventType.Rebilled, null) { Id = ids.Item1 ?? ids.Item2 ?? default, Source = ids.Item1.HasValue ? ReferenceEventSource.Organization : ReferenceEventSource.User, });
private async Task <bool> AttemptToPayInvoiceWithBraintreeAsync(Invoice invoice, Customer customer) { if (!customer?.Metadata?.ContainsKey("btCustomerId") ?? true) { return(false); } var subscriptionService = new SubscriptionService(); var subscription = await subscriptionService.GetAsync(invoice.SubscriptionId); var ids = GetIdsFromMetaData(subscription?.Metadata); if (!ids.Item1.HasValue && !ids.Item2.HasValue) { return(false); } var orgTransaction = ids.Item1.HasValue; var btObjIdField = orgTransaction ? "organization_id" : "user_id"; var btObjId = ids.Item1 ?? ids.Item2.Value; var btInvoiceAmount = (invoice.AmountDue / 100M); var existingTransactions = orgTransaction ? await _transactionRepository.GetManyByOrganizationIdAsync(ids.Item1.Value) : await _transactionRepository.GetManyByUserIdAsync(ids.Item2.Value); var duplicateTimeSpan = TimeSpan.FromHours(24); var now = DateTime.UtcNow; var duplicateTransaction = existingTransactions? .FirstOrDefault(t => (now - t.CreationDate) < duplicateTimeSpan); if (duplicateTransaction != null) { _logger.LogWarning("There is already a recent PayPal transaction ({0}). " + "Do not charge again to prevent possible duplicate.", duplicateTransaction.GatewayId); return(false); } 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 = $"{btObjIdField}:{btObjId}" } }, CustomFields = new Dictionary <string, string> { [btObjIdField] = btObjId.ToString() } }); if (!transactionResult.IsSuccess()) { if (invoice.AttemptCount < 4) { await _mailService.SendPaymentFailedAsync(customer.Email, btInvoiceAmount, true); } return(false); } var invoiceService = new InvoiceService(); try { await invoiceService.UpdateAsync(invoice.Id, new InvoiceUpdateOptions { Metadata = new Dictionary <string, string> { ["btTransactionId"] = transactionResult.Target.Id, ["btPayPalTransactionId"] = transactionResult.Target.PayPalDetails?.AuthorizationId } }); await invoiceService.PayAsync(invoice.Id, new InvoicePayOptions { PaidOutOfBand = true }); } catch (Exception e) { await _btGateway.Transaction.RefundAsync(transactionResult.Target.Id); if (e.Message.Contains("Invoice is already paid")) { await invoiceService.UpdateAsync(invoice.Id, new InvoiceUpdateOptions { Metadata = invoice.Metadata }); } else { throw; } } return(true); }
private async Task <bool> 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 <string, string> { ["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; } } return(true); }
public async Task <IActionResult> PostWebhook([FromQuery] string key) { if (key != _billingSettings.StripeWebhookKey) { return(new BadRequestResult()); } Stripe.Event parsedEvent; using (var sr = new StreamReader(HttpContext.Request.Body)) { var json = await sr.ReadToEndAsync(); parsedEvent = EventUtility.ConstructEvent(json, Request.Headers["Stripe-Signature"], _billingSettings.StripeWebhookSecret); } if (string.IsNullOrWhiteSpace(parsedEvent?.Id)) { return(new BadRequestResult()); } if (_hostingEnvironment.IsProduction() && !parsedEvent.Livemode) { return(new BadRequestResult()); } var subDeleted = parsedEvent.Type.Equals("customer.subscription.deleted"); var subUpdated = parsedEvent.Type.Equals("customer.subscription.updated"); if (subDeleted || subUpdated) { if (!(parsedEvent.Data.Object is Subscription subscription)) { throw new Exception("Subscription is null."); } var ids = GetIdsFromMetaData(subscription.Metadata); var subCanceled = subDeleted && subscription.Status == "canceled"; var subUnpaid = subUpdated && subscription.Status == "unpaid"; if (subCanceled || subUnpaid) { // org if (ids.Item1.HasValue) { await _organizationService.DisableAsync(ids.Item1.Value, subscription.CurrentPeriodEnd); } // user else if (ids.Item2.HasValue) { await _userService.DisablePremiumAsync(ids.Item2.Value, subscription.CurrentPeriodEnd); } } if (subUpdated) { // org if (ids.Item1.HasValue) { await _organizationService.UpdateExpirationDateAsync(ids.Item1.Value, subscription.CurrentPeriodEnd); } // user else if (ids.Item2.HasValue) { await _userService.UpdatePremiumExpirationAsync(ids.Item2.Value, subscription.CurrentPeriodEnd); } } } else if (parsedEvent.Type.Equals("invoice.upcoming")) { if (!(parsedEvent.Data.Object is Invoice invoice)) { throw new Exception("Invoice is null."); } var subscriptionService = new SubscriptionService(); var subscription = await subscriptionService.GetAsync(invoice.SubscriptionId); if (subscription == null) { throw new Exception("Invoice subscription is null."); } string email = null; var ids = GetIdsFromMetaData(subscription.Metadata); // org if (ids.Item1.HasValue) { var org = await _organizationRepository.GetByIdAsync(ids.Item1.Value); if (org != null && OrgPlanForInvoiceNotifications(org)) { email = org.BillingEmail; } } // user else if (ids.Item2.HasValue) { var user = await _userService.GetUserByIdAsync(ids.Item2.Value); if (user.Premium) { email = user.Email; } } if (!string.IsNullOrWhiteSpace(email) && invoice.NextPaymentAttempt.HasValue) { var items = invoice.Lines.Select(i => i.Description).ToList(); await _mailService.SendInvoiceUpcomingAsync(email, invoice.AmountDue / 100M, invoice.NextPaymentAttempt.Value, items, ids.Item1.HasValue); } } else if (parsedEvent.Type.Equals("charge.succeeded")) { if (!(parsedEvent.Data.Object is Charge charge)) { throw new Exception("Charge is null."); } if (charge.InvoiceId == null) { return(new OkResult()); } var chargeTransaction = await _transactionRepository.GetByGatewayIdAsync( GatewayType.Stripe, charge.Id); if (chargeTransaction == null) { var invoiceService = new InvoiceService(); var invoice = await invoiceService.GetAsync(charge.InvoiceId); if (invoice == null) { return(new OkResult()); } var subscriptionService = new SubscriptionService(); var subscription = await subscriptionService.GetAsync(invoice.SubscriptionId); if (subscription == null) { return(new OkResult()); } var ids = GetIdsFromMetaData(subscription.Metadata); if (ids.Item1.HasValue || ids.Item2.HasValue) { var tx = new Transaction { Amount = charge.Amount / 100M, CreationDate = charge.Created, OrganizationId = ids.Item1, UserId = ids.Item2, Type = TransactionType.Charge, Gateway = GatewayType.Stripe, GatewayId = charge.Id }; if (charge.Source is Card card) { tx.PaymentMethodType = PaymentMethodType.Card; tx.Details = $"{card.Brand}, *{card.Last4}"; } else if (charge.Source is BankAccount bankAccount) { tx.PaymentMethodType = PaymentMethodType.BankAccount; tx.Details = $"{bankAccount.BankName}, *{bankAccount.Last4}"; } else { return(new OkResult()); } try { await _transactionRepository.CreateAsync(tx); } // Catch foreign key violations because user/org could have been deleted. catch (SqlException e) when(e.Number == 547) { } } } } else if (parsedEvent.Type.Equals("charge.refunded")) { if (!(parsedEvent.Data.Object is Charge charge)) { throw new Exception("Charge is null."); } var chargeTransaction = await _transactionRepository.GetByGatewayIdAsync( GatewayType.Stripe, charge.Id); if (chargeTransaction == null) { throw new Exception("Cannot find refunded charge."); } var amountRefunded = charge.AmountRefunded / 100M; if (!chargeTransaction.Refunded.GetValueOrDefault() && chargeTransaction.RefundedAmount.GetValueOrDefault() < amountRefunded) { chargeTransaction.RefundedAmount = amountRefunded; if (charge.Refunded) { chargeTransaction.Refunded = true; } await _transactionRepository.ReplaceAsync(chargeTransaction); foreach (var refund in charge.Refunds) { var refundTransaction = await _transactionRepository.GetByGatewayIdAsync( GatewayType.Stripe, refund.Id); if (refundTransaction != null) { continue; } await _transactionRepository.CreateAsync(new Transaction { Amount = refund.Amount / 100M, CreationDate = refund.Created, OrganizationId = chargeTransaction.OrganizationId, UserId = chargeTransaction.UserId, Type = TransactionType.Refund, Gateway = GatewayType.Stripe, GatewayId = refund.Id, PaymentMethodType = chargeTransaction.PaymentMethodType, Details = chargeTransaction.Details }); } } } else if (parsedEvent.Type.Equals("invoice.payment_failed")) { if (!(parsedEvent.Data.Object is Invoice invoice)) { throw new Exception("Invoice is null."); } if (invoice.AttemptCount > 1 && UnpaidAutoChargeInvoiceForSubscriptionCycle(invoice)) { await AttemptToPayInvoiceWithBraintreeAsync(invoice); } } else if (parsedEvent.Type.Equals("invoice.created")) { if (!(parsedEvent.Data.Object is Invoice invoice)) { throw new Exception("Invoice is null."); } if (UnpaidAutoChargeInvoiceForSubscriptionCycle(invoice)) { await AttemptToPayInvoiceWithBraintreeAsync(invoice); } } return(new OkResult()); }
public async Task <BillingInfo> GetBillingAsync(ISubscriber subscriber) { var billingInfo = new BillingInfo(); ICollection <Transaction> transactions = null; if (subscriber is User) { transactions = await _transactionRepository.GetManyByUserIdAsync(subscriber.Id); } else if (subscriber is Organization) { transactions = await _transactionRepository.GetManyByOrganizationIdAsync(subscriber.Id); } if (transactions != null) { billingInfo.Transactions = transactions?.OrderByDescending(i => i.CreationDate) .Select(t => new BillingInfo.BillingTransaction(t)); } var customerService = new CustomerService(); var subscriptionService = new SubscriptionService(); var chargeService = new ChargeService(); var invoiceService = new InvoiceService(); if (!string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId)) { var customer = await customerService.GetAsync(subscriber.GatewayCustomerId); if (customer != null) { billingInfo.CreditAmount = customer.AccountBalance / 100M; if (customer.Metadata?.ContainsKey("btCustomerId") ?? false) { try { var braintreeCustomer = await _btGateway.Customer.FindAsync( customer.Metadata["btCustomerId"]); if (braintreeCustomer?.DefaultPaymentMethod != null) { billingInfo.PaymentSource = new BillingInfo.BillingSource( braintreeCustomer.DefaultPaymentMethod); } } catch (Braintree.Exceptions.NotFoundException) { } } else if (!string.IsNullOrWhiteSpace(customer.DefaultSourceId) && customer.Sources?.Data != null) { if (customer.DefaultSourceId.StartsWith("card_") || customer.DefaultSourceId.StartsWith("ba_")) { var source = customer.Sources.Data.FirstOrDefault(s => (s is Card || s is BankAccount) && s.Id == customer.DefaultSourceId); if (source != null) { billingInfo.PaymentSource = new BillingInfo.BillingSource(source); } } } var charges = await chargeService.ListAsync(new ChargeListOptions { CustomerId = customer.Id, Limit = 20 }); billingInfo.Charges = charges?.Data?.OrderByDescending(c => c.Created) .Select(c => new BillingInfo.BillingCharge(c)); var invoices = await invoiceService.ListAsync(new InvoiceListOptions { CustomerId = customer.Id, Limit = 20 }); billingInfo.Invoices = invoices?.Data?.OrderByDescending(i => i.Date) .Select(i => new BillingInfo.BillingInvoice(i)); } } if (!string.IsNullOrWhiteSpace(subscriber.GatewaySubscriptionId)) { var sub = await subscriptionService.GetAsync(subscriber.GatewaySubscriptionId); if (sub != null) { billingInfo.Subscription = new BillingInfo.BillingSubscription(sub); } if (!sub.CanceledAt.HasValue && !string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId)) { try { var upcomingInvoice = await invoiceService.UpcomingAsync( new UpcomingInvoiceOptions { CustomerId = subscriber.GatewayCustomerId }); if (upcomingInvoice != null) { billingInfo.UpcomingInvoice = new BillingInfo.BillingInvoiceInfo(upcomingInvoice); } } catch (StripeException) { } } } return(billingInfo); }
public async Task AdjustStorageAsync(IStorableSubscriber storableSubscriber, int additionalStorage, string storagePlanId) { var subscriptionItemService = new SubscriptionItemService(); var subscriptionService = new SubscriptionService(); var sub = await subscriptionService.GetAsync(storableSubscriber.GatewaySubscriptionId); if (sub == null) { throw new GatewayException("Subscription not found."); } Func <bool, Task <SubscriptionItem> > subUpdateAction = null; var storageItem = sub.Items?.FirstOrDefault(i => i.Plan.Id == storagePlanId); var subItemOptions = sub.Items.Where(i => i.Plan.Id != storagePlanId) .Select(i => new InvoiceSubscriptionItemOptions { Id = i.Id, PlanId = i.Plan.Id, Quantity = i.Quantity, }).ToList(); if (additionalStorage > 0 && storageItem == null) { subItemOptions.Add(new InvoiceSubscriptionItemOptions { PlanId = storagePlanId, Quantity = additionalStorage, }); subUpdateAction = (prorate) => subscriptionItemService.CreateAsync( new SubscriptionItemCreateOptions { PlanId = storagePlanId, Quantity = additionalStorage, SubscriptionId = sub.Id, Prorate = prorate }); } else if (additionalStorage > 0 && storageItem != null) { subItemOptions.Add(new InvoiceSubscriptionItemOptions { Id = storageItem.Id, PlanId = storagePlanId, Quantity = additionalStorage, }); subUpdateAction = (prorate) => subscriptionItemService.UpdateAsync(storageItem.Id, new SubscriptionItemUpdateOptions { PlanId = storagePlanId, Quantity = additionalStorage, Prorate = prorate }); } else if (additionalStorage == 0 && storageItem != null) { subItemOptions.Add(new InvoiceSubscriptionItemOptions { Id = storageItem.Id, Deleted = true }); subUpdateAction = (prorate) => subscriptionItemService.DeleteAsync(storageItem.Id); } var invoicedNow = false; if (additionalStorage > 0) { invoicedNow = await PreviewUpcomingInvoiceAndPayAsync( storableSubscriber, storagePlanId, subItemOptions, 400); } await subUpdateAction(!invoicedNow); }
public async Task <ActionResult <Subscription> > RetrieveSubscriptionAsync([FromBody] SubscriptionRetrieveRequest request) { var subscriptionService = new SubscriptionService(this.client); return(await subscriptionService.GetAsync(request.SubscriptionId)); }
public override async Task <CallbackResult> ProcessCallbackAsync(PaymentProviderContext <StripeCheckoutSettings> ctx) { // The ProcessCallback method is only intendid to be called via a Stripe Webhook and so // it's job is to process the webhook event and finalize / update the ctx.Order accordingly try { var secretKey = ctx.Settings.TestMode ? ctx.Settings.TestSecretKey : ctx.Settings.LiveSecretKey; var webhookSigningSecret = ctx.Settings.TestMode ? ctx.Settings.TestWebhookSigningSecret : ctx.Settings.LiveWebhookSigningSecret; ConfigureStripe(secretKey); var stripeEvent = await GetWebhookStripeEventAsync(ctx, webhookSigningSecret); if (stripeEvent != null && stripeEvent.Type == Events.CheckoutSessionCompleted) { if (stripeEvent.Data?.Object?.Instance is Session stripeSession) { if (stripeSession.Mode == "payment") { var paymentIntentService = new PaymentIntentService(); var paymentIntent = await paymentIntentService.GetAsync(stripeSession.PaymentIntentId, new PaymentIntentGetOptions { Expand = new List <string>(new[] { "review" }) }); return(CallbackResult.Ok(new TransactionInfo { TransactionId = GetTransactionId(paymentIntent), AmountAuthorized = AmountFromMinorUnits(paymentIntent.Amount), PaymentStatus = GetPaymentStatus(paymentIntent) }, new Dictionary <string, string> { { "stripeSessionId", stripeSession.Id }, { "stripeCustomerId", stripeSession.CustomerId }, { "stripePaymentIntentId", stripeSession.PaymentIntentId }, { "stripeSubscriptionId", stripeSession.SubscriptionId }, { "stripeChargeId", GetTransactionId(paymentIntent) }, { "stripeCardCountry", paymentIntent.Charges?.Data?.FirstOrDefault()?.PaymentMethodDetails?.Card?.Country } })); } else if (stripeSession.Mode == "subscription") { var subscriptionService = new SubscriptionService(); var subscription = await subscriptionService.GetAsync(stripeSession.SubscriptionId, new SubscriptionGetOptions { Expand = new List <string>(new[] { "latest_invoice", "latest_invoice.charge", "latest_invoice.charge.review", "latest_invoice.payment_intent", "latest_invoice.payment_intent.review" }) }); var invoice = subscription.LatestInvoice; return(CallbackResult.Ok(new TransactionInfo { TransactionId = GetTransactionId(invoice), AmountAuthorized = AmountFromMinorUnits(invoice.PaymentIntent.Amount), PaymentStatus = GetPaymentStatus(invoice) }, new Dictionary <string, string> { { "stripeSessionId", stripeSession.Id }, { "stripeCustomerId", stripeSession.CustomerId }, { "stripePaymentIntentId", invoice.PaymentIntentId }, { "stripeSubscriptionId", stripeSession.SubscriptionId }, { "stripeChargeId", invoice.ChargeId }, { "stripeCardCountry", invoice.Charge?.PaymentMethodDetails?.Card?.Country } })); } } else if (stripeEvent != null && stripeEvent.Type == Events.ReviewClosed) { if (stripeEvent.Data?.Object?.Instance is Review stripeReview && !string.IsNullOrWhiteSpace(stripeReview.PaymentIntentId)) { var paymentIntentService = new PaymentIntentService(); var paymentIntent = paymentIntentService.Get(stripeReview.PaymentIntentId, new PaymentIntentGetOptions { Expand = new List <string>(new[] { "review" }) }); return(CallbackResult.Ok(new TransactionInfo { TransactionId = GetTransactionId(paymentIntent), AmountAuthorized = AmountFromMinorUnits(paymentIntent.Amount), PaymentStatus = GetPaymentStatus(paymentIntent) })); } } } } catch (Exception ex) { _logger.Error(ex, "Stripe - ProcessCallback"); } return(CallbackResult.BadRequest()); }