1
0
mirror of https://github.com/bitwarden/server synced 2026-02-18 18:33:29 +00:00
Files
server/src/Core/Services/Implementations/BraintreeService.cs
Alex Morask cfa8d4a165 [PM-29604] [PM-29605] [PM-29606] Support premium subscription page redesign (#6821)
* feat(get-subscription): Add EnumMemberJsonConverter

* feat(get-subscription): Add BitwardenDiscount model

* feat(get-subscription): Add Cart model

* feat(get-subscription): Add Storage model

* feat(get-subscription): Add BitwardenSubscription model

* feat(get-subscription): Add DiscountExtensions

* feat(get-subscription): Add error code to StripeConstants

* feat(get-subscription): Add GetBitwardenSubscriptionQuery

* feat(get-subscription): Expose GET /account/billing/vnext/subscription

* feat(reinstate-subscription): Add ReinstateSubscriptionCommand

* feat(reinstate-subscription): Expose POST /account/billing/vnext/subscription/reinstate

* feat(pay-with-paypal-immediately): Add SubscriberId union

* feat(pay-with-paypal-immediately): Add BraintreeService with PayInvoice method

* feat(pay-with-paypal-immediately): Pay PayPal invoice immediately when starting premium subscription

* feat(pay-with-paypal-immediately): Pay invoice with Braintree on invoice.created for subscription cycles only

* fix(update-storage): Always invoice for premium storage update

* fix(update-storage): Move endpoint to subscription path

* docs: Note FF removal POIs

* (format): Run dotnet format
2026-01-12 10:45:41 -06:00

108 lines
3.6 KiB
C#

using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Services;
using Bit.Core.Billing.Subscriptions.Models;
using Bit.Core.Exceptions;
using Bit.Core.Settings;
using Braintree;
using Microsoft.Extensions.Logging;
using Stripe;
namespace Bit.Core.Services.Implementations;
using static StripeConstants;
public class BraintreeService(
IBraintreeGateway braintreeGateway,
IGlobalSettings globalSettings,
ILogger<BraintreeService> logger,
IMailService mailService,
IStripeAdapter stripeAdapter) : IBraintreeService
{
private readonly ConflictException _problemPayingInvoice = new("There was a problem paying for your invoice. Please contact customer support.");
public async Task PayInvoice(
SubscriberId subscriberId,
Invoice invoice)
{
if (invoice.Customer == null)
{
logger.LogError("Invoice's ({InvoiceID}) `customer` property must be expanded to be paid with Braintree",
invoice.Id);
throw _problemPayingInvoice;
}
if (!invoice.Customer.Metadata.TryGetValue(MetadataKeys.BraintreeCustomerId, out var braintreeCustomerId))
{
logger.LogError(
"Cannot pay invoice ({InvoiceID}) with Braintree for Customer ({CustomerID}) that does not have a Braintree Customer ID",
invoice.Id, invoice.Customer.Id);
throw _problemPayingInvoice;
}
if (invoice is not
{
AmountDue: > 0,
Status: not InvoiceStatus.Paid,
CollectionMethod: CollectionMethod.ChargeAutomatically
})
{
logger.LogWarning("Attempted to pay invoice ({InvoiceID}) with Braintree that is not eligible for payment", invoice.Id);
return;
}
var amount = Math.Round(invoice.AmountDue / 100M, 2);
var idKey = subscriberId.Match(
_ => "user_id",
_ => "organization_id",
_ => "provider_id");
var idValue = subscriberId.Match(
userId => userId.Value,
organizationId => organizationId.Value,
providerId => providerId.Value);
var request = new TransactionRequest
{
Amount = amount,
CustomerId = braintreeCustomerId,
Options = new TransactionOptionsRequest
{
SubmitForSettlement = true,
PayPal = new TransactionOptionsPayPalRequest
{
CustomField = $"{idKey}:{idValue},region:{globalSettings.BaseServiceUri.CloudRegion}"
}
},
CustomFields = new Dictionary<string, string>
{
[idKey] = idValue.ToString(),
["region"] = globalSettings.BaseServiceUri.CloudRegion
}
};
var result = await braintreeGateway.Transaction.SaleAsync(request);
if (!result.IsSuccess())
{
if (invoice.AttemptCount < 4)
{
await mailService.SendPaymentFailedAsync(invoice.Customer.Email, amount, true);
}
return;
}
await stripeAdapter.UpdateInvoiceAsync(invoice.Id, new InvoiceUpdateOptions
{
Metadata = new Dictionary<string, string>
{
[MetadataKeys.BraintreeTransactionId] = result.Target.Id,
[MetadataKeys.PayPalTransactionId] = result.Target.PayPalDetails.AuthorizationId
}
});
await stripeAdapter.PayInvoiceAsync(invoice.Id, new InvoicePayOptions { PaidOutOfBand = true });
}
}