mirror of
https://github.com/bitwarden/server.git
synced 2025-04-15 10:08:14 -05:00
369 lines
14 KiB
C#
369 lines
14 KiB
C#
using System;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
using Bit.Core.Models.Table;
|
|
using Braintree;
|
|
using Bit.Core.Exceptions;
|
|
using Bit.Core.Models.Business;
|
|
|
|
namespace Bit.Core.Services
|
|
{
|
|
public class BraintreePaymentService : IPaymentService
|
|
{
|
|
private const string PremiumPlanId = "premium-annually";
|
|
private const string StoragePlanId = "storage-gb-annually";
|
|
private readonly BraintreeGateway _gateway;
|
|
|
|
public BraintreePaymentService(
|
|
GlobalSettings globalSettings)
|
|
{
|
|
_gateway = new BraintreeGateway
|
|
{
|
|
Environment = globalSettings.Braintree.Production ?
|
|
Braintree.Environment.PRODUCTION : Braintree.Environment.SANDBOX,
|
|
MerchantId = globalSettings.Braintree.MerchantId,
|
|
PublicKey = globalSettings.Braintree.PublicKey,
|
|
PrivateKey = globalSettings.Braintree.PrivateKey
|
|
};
|
|
}
|
|
|
|
public async Task AdjustStorageAsync(IStorableSubscriber storableSubscriber, int additionalStorage, string storagePlanId)
|
|
{
|
|
var sub = await _gateway.Subscription.FindAsync(storableSubscriber.GatewaySubscriptionId);
|
|
if(sub == null)
|
|
{
|
|
throw new GatewayException("Subscription was not found.");
|
|
}
|
|
|
|
var req = new SubscriptionRequest
|
|
{
|
|
AddOns = new AddOnsRequest(),
|
|
Options = new SubscriptionOptionsRequest
|
|
{
|
|
ProrateCharges = true,
|
|
RevertSubscriptionOnProrationFailure = true
|
|
}
|
|
};
|
|
|
|
var storageItem = sub.AddOns?.FirstOrDefault(a => a.Id == storagePlanId);
|
|
if(additionalStorage > 0 && storageItem == null)
|
|
{
|
|
req.AddOns.Add = new AddAddOnRequest[]
|
|
{
|
|
new AddAddOnRequest
|
|
{
|
|
InheritedFromId = storagePlanId,
|
|
Quantity = additionalStorage,
|
|
NeverExpires = true
|
|
}
|
|
};
|
|
}
|
|
else if(additionalStorage > 0 && storageItem != null)
|
|
{
|
|
req.AddOns.Update = new UpdateAddOnRequest[]
|
|
{
|
|
new UpdateAddOnRequest
|
|
{
|
|
ExistingId = storageItem.Id,
|
|
Quantity = additionalStorage,
|
|
NeverExpires = true
|
|
}
|
|
};
|
|
}
|
|
else if(additionalStorage == 0 && storageItem != null)
|
|
{
|
|
req.AddOns.Remove = new string[] { storageItem.Id };
|
|
}
|
|
|
|
var result = await _gateway.Subscription.UpdateAsync(sub.Id, req);
|
|
if(!result.IsSuccess())
|
|
{
|
|
throw new GatewayException("Failed to adjust storage.");
|
|
}
|
|
}
|
|
|
|
public async Task CancelAndRecoverChargesAsync(ISubscriber subscriber)
|
|
{
|
|
if(!string.IsNullOrWhiteSpace(subscriber.GatewaySubscriptionId))
|
|
{
|
|
await _gateway.Subscription.CancelAsync(subscriber.GatewaySubscriptionId);
|
|
}
|
|
|
|
if(string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var transactionRequest = new TransactionSearchRequest().CustomerId.Is(subscriber.GatewayCustomerId);
|
|
var transactions = _gateway.Transaction.Search(transactionRequest);
|
|
|
|
if((transactions?.MaximumCount ?? 0) > 0)
|
|
{
|
|
foreach(var transaction in transactions.Cast<Transaction>().Where(c => c.RefundedTransactionId == null))
|
|
{
|
|
await _gateway.Transaction.RefundAsync(transaction.Id);
|
|
}
|
|
}
|
|
|
|
await _gateway.Customer.DeleteAsync(subscriber.GatewayCustomerId);
|
|
}
|
|
|
|
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 sub = await _gateway.Subscription.FindAsync(subscriber.GatewaySubscriptionId);
|
|
if(sub == null)
|
|
{
|
|
throw new GatewayException("Subscription was not found.");
|
|
}
|
|
|
|
if(sub.Status == SubscriptionStatus.CANCELED || sub.Status == SubscriptionStatus.EXPIRED ||
|
|
!sub.NeverExpires.GetValueOrDefault())
|
|
{
|
|
throw new GatewayException("Subscription is already canceled.");
|
|
}
|
|
|
|
if(endOfPeriod)
|
|
{
|
|
var req = new SubscriptionRequest
|
|
{
|
|
NeverExpires = false,
|
|
NumberOfBillingCycles = sub.CurrentBillingCycle
|
|
};
|
|
|
|
var result = await _gateway.Subscription.UpdateAsync(subscriber.GatewaySubscriptionId, req);
|
|
if(!result.IsSuccess())
|
|
{
|
|
throw new GatewayException("Unable to cancel subscription.");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
var result = await _gateway.Subscription.CancelAsync(subscriber.GatewaySubscriptionId);
|
|
if(!result.IsSuccess())
|
|
{
|
|
throw new GatewayException("Unable to cancel subscription.");
|
|
}
|
|
}
|
|
}
|
|
|
|
public async Task<BillingInfo.BillingInvoice> GetUpcomingInvoiceAsync(ISubscriber subscriber)
|
|
{
|
|
if(!string.IsNullOrWhiteSpace(subscriber.GatewaySubscriptionId))
|
|
{
|
|
var sub = await _gateway.Subscription.FindAsync(subscriber.GatewaySubscriptionId);
|
|
if(sub != null)
|
|
{
|
|
var cancelAtEndDate = !sub.NeverExpires.GetValueOrDefault();
|
|
var canceled = sub.Status == SubscriptionStatus.CANCELED;
|
|
if(!canceled && !cancelAtEndDate && sub.NextBillingDate.HasValue)
|
|
{
|
|
return new BillingInfo.BillingInvoice(sub);
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
public async Task<BillingInfo> GetBillingAsync(ISubscriber subscriber)
|
|
{
|
|
var billingInfo = new BillingInfo();
|
|
if(!string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId))
|
|
{
|
|
var customer = await _gateway.Customer.FindAsync(subscriber.GatewayCustomerId);
|
|
if(customer != null)
|
|
{
|
|
if(customer.DefaultPaymentMethod != null)
|
|
{
|
|
billingInfo.PaymentSource = new BillingInfo.BillingSource(customer.DefaultPaymentMethod);
|
|
}
|
|
|
|
var transactionRequest = new TransactionSearchRequest().CustomerId.Is(customer.Id);
|
|
var transactions = _gateway.Transaction.Search(transactionRequest);
|
|
billingInfo.Charges = transactions?.Cast<Transaction>().OrderByDescending(t => t.CreatedAt)
|
|
.Select(t => new BillingInfo.BillingCharge(t));
|
|
}
|
|
}
|
|
|
|
if(!string.IsNullOrWhiteSpace(subscriber.GatewaySubscriptionId))
|
|
{
|
|
var sub = await _gateway.Subscription.FindAsync(subscriber.GatewaySubscriptionId);
|
|
if(sub != null)
|
|
{
|
|
var plans = await _gateway.Plan.AllAsync();
|
|
var plan = plans?.FirstOrDefault(p => p.Id == sub.PlanId);
|
|
billingInfo.Subscription = new BillingInfo.BillingSubscription(sub, plan);
|
|
}
|
|
|
|
if(!billingInfo.Subscription.Cancelled && !billingInfo.Subscription.CancelAtEndDate &&
|
|
sub.NextBillingDate.HasValue)
|
|
{
|
|
billingInfo.UpcomingInvoice = new BillingInfo.BillingInvoice(sub);
|
|
}
|
|
}
|
|
|
|
return billingInfo;
|
|
}
|
|
|
|
public async Task PurchasePremiumAsync(User user, string paymentToken, short additionalStorageGb)
|
|
{
|
|
var customerResult = await _gateway.Customer.CreateAsync(new CustomerRequest
|
|
{
|
|
PaymentMethodNonce = paymentToken,
|
|
Email = user.Email
|
|
});
|
|
|
|
if(!customerResult.IsSuccess() || customerResult.Target.PaymentMethods.Length == 0)
|
|
{
|
|
throw new GatewayException("Failed to create customer.");
|
|
}
|
|
|
|
var subId = "u" + user.Id.ToString("N").ToLower() +
|
|
Utilities.CoreHelpers.RandomString(3, upper: false, numeric: false);
|
|
|
|
var subRequest = new SubscriptionRequest
|
|
{
|
|
Id = subId,
|
|
PaymentMethodToken = customerResult.Target.PaymentMethods[0].Token,
|
|
PlanId = PremiumPlanId
|
|
};
|
|
|
|
if(additionalStorageGb > 0)
|
|
{
|
|
subRequest.AddOns = new AddOnsRequest();
|
|
subRequest.AddOns.Add = new AddAddOnRequest[]
|
|
{
|
|
new AddAddOnRequest
|
|
{
|
|
InheritedFromId = StoragePlanId,
|
|
Quantity = additionalStorageGb
|
|
}
|
|
};
|
|
}
|
|
|
|
var subResult = await _gateway.Subscription.CreateAsync(subRequest);
|
|
|
|
if(!subResult.IsSuccess())
|
|
{
|
|
await _gateway.Customer.DeleteAsync(customerResult.Target.Id);
|
|
throw new GatewayException("Failed to create subscription.");
|
|
}
|
|
|
|
user.Gateway = Enums.GatewayType.Braintree;
|
|
user.GatewayCustomerId = customerResult.Target.Id;
|
|
user.GatewaySubscriptionId = subResult.Target.Id;
|
|
user.Premium = true;
|
|
user.PremiumExpirationDate = subResult.Target.BillingPeriodEndDate;
|
|
}
|
|
|
|
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 sub = await _gateway.Subscription.FindAsync(subscriber.GatewaySubscriptionId);
|
|
if(sub == null)
|
|
{
|
|
throw new GatewayException("Subscription was not found.");
|
|
}
|
|
|
|
if(sub.Status != SubscriptionStatus.ACTIVE || sub.NeverExpires.GetValueOrDefault())
|
|
{
|
|
throw new GatewayException("Subscription is not marked for cancellation.");
|
|
}
|
|
|
|
var req = new SubscriptionRequest
|
|
{
|
|
NeverExpires = true,
|
|
NumberOfBillingCycles = null
|
|
};
|
|
|
|
var result = await _gateway.Subscription.UpdateAsync(subscriber.GatewaySubscriptionId, req);
|
|
if(!result.IsSuccess())
|
|
{
|
|
throw new GatewayException("Unable to reinstate subscription.");
|
|
}
|
|
}
|
|
|
|
public async Task<bool> UpdatePaymentMethodAsync(ISubscriber subscriber, string paymentToken)
|
|
{
|
|
if(subscriber == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(subscriber));
|
|
}
|
|
|
|
if(subscriber.Gateway.HasValue && subscriber.Gateway.Value != Enums.GatewayType.Braintree)
|
|
{
|
|
throw new GatewayException("Switching from one payment type to another is not supported. " +
|
|
"Contact us for assistance.");
|
|
}
|
|
|
|
var updatedSubscriber = false;
|
|
Customer customer = null;
|
|
|
|
if(!string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId))
|
|
{
|
|
customer = await _gateway.Customer.FindAsync(subscriber.GatewayCustomerId);
|
|
}
|
|
|
|
if(customer == null)
|
|
{
|
|
var result = await _gateway.Customer.CreateAsync(new CustomerRequest
|
|
{
|
|
Email = subscriber.BillingEmailAddress(),
|
|
PaymentMethodNonce = paymentToken
|
|
});
|
|
|
|
if(!result.IsSuccess())
|
|
{
|
|
throw new GatewayException("Cannot create customer.");
|
|
}
|
|
|
|
customer = result.Target;
|
|
subscriber.Gateway = Enums.GatewayType.Braintree;
|
|
subscriber.GatewayCustomerId = customer.Id;
|
|
updatedSubscriber = true;
|
|
}
|
|
else
|
|
{
|
|
if(customer.DefaultPaymentMethod != null)
|
|
{
|
|
var deleteResult = await _gateway.PaymentMethod.DeleteAsync(customer.DefaultPaymentMethod.Token);
|
|
if(!deleteResult.IsSuccess())
|
|
{
|
|
throw new GatewayException("Cannot delete old payment method.");
|
|
}
|
|
}
|
|
|
|
var result = await _gateway.PaymentMethod.CreateAsync(new PaymentMethodRequest
|
|
{
|
|
PaymentMethodNonce = paymentToken,
|
|
CustomerId = customer.Id
|
|
});
|
|
if(!result.IsSuccess())
|
|
{
|
|
throw new GatewayException("Cannot add new payment method.");
|
|
}
|
|
}
|
|
|
|
return updatedSubscriber;
|
|
}
|
|
}
|
|
}
|