mirror of
https://github.com/bitwarden/server.git
synced 2025-02-19 02:21:21 +01:00
Merge branch 'stripechanges'
This commit is contained in:
commit
8fad9d849f
@ -22,6 +22,7 @@ namespace Bit.Admin.Jobs
|
|||||||
protected async override Task ExecuteJobAsync(IJobExecutionContext context)
|
protected async override Task ExecuteJobAsync(IJobExecutionContext context)
|
||||||
{
|
{
|
||||||
await _maintenanceRepository.UpdateStatisticsAsync();
|
await _maintenanceRepository.UpdateStatisticsAsync();
|
||||||
|
await _maintenanceRepository.DisableCipherAutoStatsAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,6 +28,7 @@ namespace Bit.Api.Controllers
|
|||||||
private readonly ICipherService _cipherService;
|
private readonly ICipherService _cipherService;
|
||||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||||
private readonly ILicensingService _licenseService;
|
private readonly ILicensingService _licenseService;
|
||||||
|
private readonly IPaymentService _paymentService;
|
||||||
private readonly GlobalSettings _globalSettings;
|
private readonly GlobalSettings _globalSettings;
|
||||||
|
|
||||||
public AccountsController(
|
public AccountsController(
|
||||||
@ -38,6 +39,7 @@ namespace Bit.Api.Controllers
|
|||||||
ICipherService cipherService,
|
ICipherService cipherService,
|
||||||
IOrganizationUserRepository organizationUserRepository,
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
ILicensingService licenseService,
|
ILicensingService licenseService,
|
||||||
|
IPaymentService paymentService,
|
||||||
GlobalSettings globalSettings)
|
GlobalSettings globalSettings)
|
||||||
{
|
{
|
||||||
_userService = userService;
|
_userService = userService;
|
||||||
@ -47,6 +49,7 @@ namespace Bit.Api.Controllers
|
|||||||
_cipherService = cipherService;
|
_cipherService = cipherService;
|
||||||
_organizationUserRepository = organizationUserRepository;
|
_organizationUserRepository = organizationUserRepository;
|
||||||
_licenseService = licenseService;
|
_licenseService = licenseService;
|
||||||
|
_paymentService = paymentService;
|
||||||
_globalSettings = globalSettings;
|
_globalSettings = globalSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -476,8 +479,7 @@ namespace Bit.Api.Controllers
|
|||||||
|
|
||||||
if(!_globalSettings.SelfHosted && user.Gateway != null)
|
if(!_globalSettings.SelfHosted && user.Gateway != null)
|
||||||
{
|
{
|
||||||
var paymentService = user.GetPaymentService(_globalSettings);
|
var billingInfo = await _paymentService.GetBillingAsync(user);
|
||||||
var billingInfo = await paymentService.GetBillingAsync(user);
|
|
||||||
var license = await _userService.GenerateLicenseAsync(user, billingInfo);
|
var license = await _userService.GenerateLicenseAsync(user, billingInfo);
|
||||||
return new BillingResponseModel(user, billingInfo, license);
|
return new BillingResponseModel(user, billingInfo, license);
|
||||||
}
|
}
|
||||||
|
@ -24,6 +24,7 @@ namespace Bit.Api.Controllers
|
|||||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||||
private readonly IOrganizationService _organizationService;
|
private readonly IOrganizationService _organizationService;
|
||||||
private readonly IUserService _userService;
|
private readonly IUserService _userService;
|
||||||
|
private readonly IPaymentService _paymentService;
|
||||||
private readonly CurrentContext _currentContext;
|
private readonly CurrentContext _currentContext;
|
||||||
private readonly GlobalSettings _globalSettings;
|
private readonly GlobalSettings _globalSettings;
|
||||||
|
|
||||||
@ -32,6 +33,7 @@ namespace Bit.Api.Controllers
|
|||||||
IOrganizationUserRepository organizationUserRepository,
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
IOrganizationService organizationService,
|
IOrganizationService organizationService,
|
||||||
IUserService userService,
|
IUserService userService,
|
||||||
|
IPaymentService paymentService,
|
||||||
CurrentContext currentContext,
|
CurrentContext currentContext,
|
||||||
GlobalSettings globalSettings)
|
GlobalSettings globalSettings)
|
||||||
{
|
{
|
||||||
@ -39,6 +41,7 @@ namespace Bit.Api.Controllers
|
|||||||
_organizationUserRepository = organizationUserRepository;
|
_organizationUserRepository = organizationUserRepository;
|
||||||
_organizationService = organizationService;
|
_organizationService = organizationService;
|
||||||
_userService = userService;
|
_userService = userService;
|
||||||
|
_paymentService = paymentService;
|
||||||
_currentContext = currentContext;
|
_currentContext = currentContext;
|
||||||
_globalSettings = globalSettings;
|
_globalSettings = globalSettings;
|
||||||
}
|
}
|
||||||
@ -78,8 +81,7 @@ namespace Bit.Api.Controllers
|
|||||||
|
|
||||||
if(!_globalSettings.SelfHosted && organization.Gateway != null)
|
if(!_globalSettings.SelfHosted && organization.Gateway != null)
|
||||||
{
|
{
|
||||||
var paymentService = new StripePaymentService();
|
var billingInfo = await _paymentService.GetBillingAsync(organization);
|
||||||
var billingInfo = await paymentService.GetBillingAsync(organization);
|
|
||||||
if(billingInfo == null)
|
if(billingInfo == null)
|
||||||
{
|
{
|
||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
@ -110,7 +112,7 @@ namespace Bit.Api.Controllers
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var invoice = await new StripeInvoiceService().GetAsync(invoiceId);
|
var invoice = await new InvoiceService().GetAsync(invoiceId);
|
||||||
if(invoice != null && invoice.CustomerId == organization.GatewayCustomerId &&
|
if(invoice != null && invoice.CustomerId == organization.GatewayCustomerId &&
|
||||||
!string.IsNullOrWhiteSpace(invoice.HostedInvoiceUrl))
|
!string.IsNullOrWhiteSpace(invoice.HostedInvoiceUrl))
|
||||||
{
|
{
|
||||||
|
@ -6,5 +6,15 @@
|
|||||||
public virtual string StripeWebhookKey { get; set; }
|
public virtual string StripeWebhookKey { get; set; }
|
||||||
public virtual string StripeWebhookSecret { get; set; }
|
public virtual string StripeWebhookSecret { get; set; }
|
||||||
public virtual string BraintreeWebhookKey { get; set; }
|
public virtual string BraintreeWebhookKey { get; set; }
|
||||||
|
public virtual PayPalSettings PayPal { get; set; } = new PayPalSettings();
|
||||||
|
|
||||||
|
public class PayPalSettings
|
||||||
|
{
|
||||||
|
public virtual bool Production { get; set; }
|
||||||
|
public virtual string ClientId { get; set; }
|
||||||
|
public virtual string ClientSecret { get; set; }
|
||||||
|
public virtual string WebhookId { get; set; }
|
||||||
|
public virtual string WebhookKey { get; set; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
141
src/Billing/Controllers/PayPalController.cs
Normal file
141
src/Billing/Controllers/PayPalController.cs
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
using Bit.Billing.Utilities;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using System.Data.SqlClient;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Bit.Billing.Controllers
|
||||||
|
{
|
||||||
|
[Route("paypal")]
|
||||||
|
public class PayPalController : Controller
|
||||||
|
{
|
||||||
|
private readonly BillingSettings _billingSettings;
|
||||||
|
private readonly PayPalClient _paypalClient;
|
||||||
|
private readonly ITransactionRepository _transactionRepository;
|
||||||
|
|
||||||
|
public PayPalController(
|
||||||
|
IOptions<BillingSettings> billingSettings,
|
||||||
|
PayPalClient paypalClient,
|
||||||
|
ITransactionRepository transactionRepository)
|
||||||
|
{
|
||||||
|
_billingSettings = billingSettings?.Value;
|
||||||
|
_paypalClient = paypalClient;
|
||||||
|
_transactionRepository = transactionRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("webhook")]
|
||||||
|
public async Task<IActionResult> PostWebhook([FromQuery] string key)
|
||||||
|
{
|
||||||
|
if(key != _billingSettings.PayPal.WebhookKey)
|
||||||
|
{
|
||||||
|
return new BadRequestResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
if(HttpContext?.Request == null)
|
||||||
|
{
|
||||||
|
return new BadRequestResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
string body = null;
|
||||||
|
using(var reader = new StreamReader(HttpContext.Request.Body, Encoding.UTF8))
|
||||||
|
{
|
||||||
|
body = await reader.ReadToEndAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
if(body == null)
|
||||||
|
{
|
||||||
|
return new BadRequestResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
var verified = await _paypalClient.VerifyWebhookAsync(body, HttpContext.Request.Headers,
|
||||||
|
_billingSettings.PayPal.WebhookId);
|
||||||
|
if(!verified)
|
||||||
|
{
|
||||||
|
return new BadRequestResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
if(body.Contains("\"PAYMENT.SALE.COMPLETED\""))
|
||||||
|
{
|
||||||
|
var ev = JsonConvert.DeserializeObject<PayPalClient.Event<PayPalClient.Sale>>(body);
|
||||||
|
var sale = ev.Resource;
|
||||||
|
var saleTransaction = await _transactionRepository.GetByGatewayIdAsync(
|
||||||
|
GatewayType.PayPal, sale.Id);
|
||||||
|
if(saleTransaction == null)
|
||||||
|
{
|
||||||
|
var ids = sale.GetIdsFromCustom();
|
||||||
|
if(ids.Item1.HasValue || ids.Item2.HasValue)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _transactionRepository.CreateAsync(new Core.Models.Table.Transaction
|
||||||
|
{
|
||||||
|
Amount = sale.Amount.TotalAmount,
|
||||||
|
CreationDate = sale.CreateTime,
|
||||||
|
OrganizationId = ids.Item1,
|
||||||
|
UserId = ids.Item2,
|
||||||
|
Type = TransactionType.Charge,
|
||||||
|
Gateway = GatewayType.PayPal,
|
||||||
|
GatewayId = sale.Id,
|
||||||
|
PaymentMethodType = PaymentMethodType.PayPal,
|
||||||
|
Details = sale.Id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Catch foreign key violations because user/org could have been deleted.
|
||||||
|
catch(SqlException e) when(e.Number == 547) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if(body.Contains("\"PAYMENT.SALE.REFUNDED\""))
|
||||||
|
{
|
||||||
|
var ev = JsonConvert.DeserializeObject<PayPalClient.Event<PayPalClient.Refund>>(body);
|
||||||
|
var refund = ev.Resource;
|
||||||
|
var refundTransaction = await _transactionRepository.GetByGatewayIdAsync(
|
||||||
|
GatewayType.PayPal, refund.Id);
|
||||||
|
if(refundTransaction == null)
|
||||||
|
{
|
||||||
|
var saleTransaction = await _transactionRepository.GetByGatewayIdAsync(
|
||||||
|
GatewayType.PayPal, refund.SaleId);
|
||||||
|
if(saleTransaction == null)
|
||||||
|
{
|
||||||
|
return new BadRequestResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!saleTransaction.Refunded.GetValueOrDefault() &&
|
||||||
|
saleTransaction.RefundedAmount.GetValueOrDefault() < refund.TotalRefundedAmount.ValueAmount)
|
||||||
|
{
|
||||||
|
saleTransaction.RefundedAmount = refund.TotalRefundedAmount.ValueAmount;
|
||||||
|
if(saleTransaction.RefundedAmount == saleTransaction.Amount)
|
||||||
|
{
|
||||||
|
saleTransaction.Refunded = true;
|
||||||
|
}
|
||||||
|
await _transactionRepository.ReplaceAsync(saleTransaction);
|
||||||
|
|
||||||
|
var ids = refund.GetIdsFromCustom();
|
||||||
|
if(ids.Item1.HasValue || ids.Item2.HasValue)
|
||||||
|
{
|
||||||
|
await _transactionRepository.CreateAsync(new Core.Models.Table.Transaction
|
||||||
|
{
|
||||||
|
Amount = refund.Amount.TotalAmount,
|
||||||
|
CreationDate = refund.CreateTime,
|
||||||
|
OrganizationId = ids.Item1,
|
||||||
|
UserId = ids.Item2,
|
||||||
|
Type = TransactionType.Refund,
|
||||||
|
Gateway = GatewayType.PayPal,
|
||||||
|
GatewayId = refund.Id,
|
||||||
|
PaymentMethodType = PaymentMethodType.PayPal,
|
||||||
|
Details = refund.Id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new OkResult();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,6 @@
|
|||||||
using Bit.Core.Models.Table;
|
using Bit.Core;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Models.Table;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
@ -7,6 +9,7 @@ using Microsoft.Extensions.Options;
|
|||||||
using Stripe;
|
using Stripe;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Data.SqlClient;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@ -20,14 +23,18 @@ namespace Bit.Billing.Controllers
|
|||||||
private readonly IHostingEnvironment _hostingEnvironment;
|
private readonly IHostingEnvironment _hostingEnvironment;
|
||||||
private readonly IOrganizationService _organizationService;
|
private readonly IOrganizationService _organizationService;
|
||||||
private readonly IOrganizationRepository _organizationRepository;
|
private readonly IOrganizationRepository _organizationRepository;
|
||||||
|
private readonly ITransactionRepository _transactionRepository;
|
||||||
private readonly IUserService _userService;
|
private readonly IUserService _userService;
|
||||||
private readonly IMailService _mailService;
|
private readonly IMailService _mailService;
|
||||||
|
private readonly Braintree.BraintreeGateway _btGateway;
|
||||||
|
|
||||||
public StripeController(
|
public StripeController(
|
||||||
|
GlobalSettings globalSettings,
|
||||||
IOptions<BillingSettings> billingSettings,
|
IOptions<BillingSettings> billingSettings,
|
||||||
IHostingEnvironment hostingEnvironment,
|
IHostingEnvironment hostingEnvironment,
|
||||||
IOrganizationService organizationService,
|
IOrganizationService organizationService,
|
||||||
IOrganizationRepository organizationRepository,
|
IOrganizationRepository organizationRepository,
|
||||||
|
ITransactionRepository transactionRepository,
|
||||||
IUserService userService,
|
IUserService userService,
|
||||||
IMailService mailService)
|
IMailService mailService)
|
||||||
{
|
{
|
||||||
@ -35,8 +42,18 @@ namespace Bit.Billing.Controllers
|
|||||||
_hostingEnvironment = hostingEnvironment;
|
_hostingEnvironment = hostingEnvironment;
|
||||||
_organizationService = organizationService;
|
_organizationService = organizationService;
|
||||||
_organizationRepository = organizationRepository;
|
_organizationRepository = organizationRepository;
|
||||||
|
_transactionRepository = transactionRepository;
|
||||||
_userService = userService;
|
_userService = userService;
|
||||||
_mailService = mailService;
|
_mailService = mailService;
|
||||||
|
|
||||||
|
_btGateway = new Braintree.BraintreeGateway
|
||||||
|
{
|
||||||
|
Environment = globalSettings.Braintree.Production ?
|
||||||
|
Braintree.Environment.PRODUCTION : Braintree.Environment.SANDBOX,
|
||||||
|
MerchantId = globalSettings.Braintree.MerchantId,
|
||||||
|
PublicKey = globalSettings.Braintree.PublicKey,
|
||||||
|
PrivateKey = globalSettings.Braintree.PrivateKey
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("webhook")]
|
[HttpPost("webhook")]
|
||||||
@ -47,11 +64,11 @@ namespace Bit.Billing.Controllers
|
|||||||
return new BadRequestResult();
|
return new BadRequestResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
StripeEvent parsedEvent;
|
Stripe.Event parsedEvent;
|
||||||
using(var sr = new StreamReader(HttpContext.Request.Body))
|
using(var sr = new StreamReader(HttpContext.Request.Body))
|
||||||
{
|
{
|
||||||
var json = await sr.ReadToEndAsync();
|
var json = await sr.ReadToEndAsync();
|
||||||
parsedEvent = StripeEventUtility.ConstructEvent(json, Request.Headers["Stripe-Signature"],
|
parsedEvent = EventUtility.ConstructEvent(json, Request.Headers["Stripe-Signature"],
|
||||||
_billingSettings.StripeWebhookSecret);
|
_billingSettings.StripeWebhookSecret);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,20 +77,17 @@ namespace Bit.Billing.Controllers
|
|||||||
return new BadRequestResult();
|
return new BadRequestResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
if(_hostingEnvironment.IsProduction() && !parsedEvent.LiveMode)
|
if(_hostingEnvironment.IsProduction() && !parsedEvent.Livemode)
|
||||||
{
|
{
|
||||||
return new BadRequestResult();
|
return new BadRequestResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
var invUpcoming = parsedEvent.Type.Equals("invoice.upcoming");
|
|
||||||
var subDeleted = parsedEvent.Type.Equals("customer.subscription.deleted");
|
var subDeleted = parsedEvent.Type.Equals("customer.subscription.deleted");
|
||||||
var subUpdated = parsedEvent.Type.Equals("customer.subscription.updated");
|
var subUpdated = parsedEvent.Type.Equals("customer.subscription.updated");
|
||||||
|
|
||||||
if(subDeleted || subUpdated)
|
if(subDeleted || subUpdated)
|
||||||
{
|
{
|
||||||
StripeSubscription subscription = Mapper<StripeSubscription>.MapFromJson(
|
if(!(parsedEvent.Data.Object is Subscription subscription))
|
||||||
parsedEvent.Data.Object.ToString());
|
|
||||||
if(subscription == null)
|
|
||||||
{
|
{
|
||||||
throw new Exception("Subscription is null.");
|
throw new Exception("Subscription is null.");
|
||||||
}
|
}
|
||||||
@ -113,16 +127,14 @@ namespace Bit.Billing.Controllers
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if(invUpcoming)
|
else if(parsedEvent.Type.Equals("invoice.upcoming"))
|
||||||
{
|
{
|
||||||
StripeInvoice invoice = Mapper<StripeInvoice>.MapFromJson(
|
if(!(parsedEvent.Data.Object is Invoice invoice))
|
||||||
parsedEvent.Data.Object.ToString());
|
|
||||||
if(invoice == null)
|
|
||||||
{
|
{
|
||||||
throw new Exception("Invoice is null.");
|
throw new Exception("Invoice is null.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var subscriptionService = new StripeSubscriptionService();
|
var subscriptionService = new SubscriptionService();
|
||||||
var subscription = await subscriptionService.GetAsync(invoice.SubscriptionId);
|
var subscription = await subscriptionService.GetAsync(invoice.SubscriptionId);
|
||||||
if(subscription == null)
|
if(subscription == null)
|
||||||
{
|
{
|
||||||
@ -152,11 +164,153 @@ namespace Bit.Billing.Controllers
|
|||||||
|
|
||||||
if(!string.IsNullOrWhiteSpace(email) && invoice.NextPaymentAttempt.HasValue)
|
if(!string.IsNullOrWhiteSpace(email) && invoice.NextPaymentAttempt.HasValue)
|
||||||
{
|
{
|
||||||
var items = invoice.StripeInvoiceLineItems.Select(i => i.Description).ToList();
|
var items = invoice.Lines.Select(i => i.Description).ToList();
|
||||||
await _mailService.SendInvoiceUpcomingAsync(email, invoice.AmountDue / 100M,
|
await _mailService.SendInvoiceUpcomingAsync(email, invoice.AmountDue / 100M,
|
||||||
invoice.NextPaymentAttempt.Value, items, ids.Item1.HasValue);
|
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();
|
return new OkResult();
|
||||||
}
|
}
|
||||||
@ -204,13 +358,88 @@ namespace Bit.Billing.Controllers
|
|||||||
{
|
{
|
||||||
switch(org.PlanType)
|
switch(org.PlanType)
|
||||||
{
|
{
|
||||||
case Core.Enums.PlanType.FamiliesAnnually:
|
case PlanType.FamiliesAnnually:
|
||||||
case Core.Enums.PlanType.TeamsAnnually:
|
case PlanType.TeamsAnnually:
|
||||||
case Core.Enums.PlanType.EnterpriseAnnually:
|
case PlanType.EnterpriseAnnually:
|
||||||
return true;
|
return true;
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool UnpaidAutoChargeInvoiceForSubscriptionCycle(Invoice invoice)
|
||||||
|
{
|
||||||
|
return invoice.AmountDue > 0 && !invoice.Paid && invoice.Billing == Stripe.Billing.ChargeAutomatically &&
|
||||||
|
invoice.BillingReason == "subscription_cycle" && invoice.SubscriptionId != null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,12 +17,14 @@ namespace Bit.Billing.Jobs
|
|||||||
private readonly GlobalSettings _globalSettings;
|
private readonly GlobalSettings _globalSettings;
|
||||||
private readonly IUserRepository _userRepository;
|
private readonly IUserRepository _userRepository;
|
||||||
private readonly IMailService _mailService;
|
private readonly IMailService _mailService;
|
||||||
|
private readonly IPaymentService _paymentService;
|
||||||
|
|
||||||
public PremiumRenewalRemindersJob(
|
public PremiumRenewalRemindersJob(
|
||||||
IOptions<BillingSettings> billingSettings,
|
IOptions<BillingSettings> billingSettings,
|
||||||
GlobalSettings globalSettings,
|
GlobalSettings globalSettings,
|
||||||
IUserRepository userRepository,
|
IUserRepository userRepository,
|
||||||
IMailService mailService,
|
IMailService mailService,
|
||||||
|
IPaymentService paymentService,
|
||||||
ILogger<PremiumRenewalRemindersJob> logger)
|
ILogger<PremiumRenewalRemindersJob> logger)
|
||||||
: base(logger)
|
: base(logger)
|
||||||
{
|
{
|
||||||
@ -30,6 +32,7 @@ namespace Bit.Billing.Jobs
|
|||||||
_globalSettings = globalSettings;
|
_globalSettings = globalSettings;
|
||||||
_userRepository = userRepository;
|
_userRepository = userRepository;
|
||||||
_mailService = mailService;
|
_mailService = mailService;
|
||||||
|
_paymentService = paymentService;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async override Task ExecuteJobAsync(IJobExecutionContext context)
|
protected async override Task ExecuteJobAsync(IJobExecutionContext context)
|
||||||
@ -37,8 +40,7 @@ namespace Bit.Billing.Jobs
|
|||||||
var users = await _userRepository.GetManyByPremiumRenewalAsync();
|
var users = await _userRepository.GetManyByPremiumRenewalAsync();
|
||||||
foreach(var user in users)
|
foreach(var user in users)
|
||||||
{
|
{
|
||||||
var paymentService = user.GetPaymentService(_globalSettings);
|
var upcomingInvoice = await _paymentService.GetUpcomingInvoiceAsync(user);
|
||||||
var upcomingInvoice = await paymentService.GetUpcomingInvoiceAsync(user);
|
|
||||||
if(upcomingInvoice?.Date != null)
|
if(upcomingInvoice?.Date != null)
|
||||||
{
|
{
|
||||||
var items = new List<string> { "1 × Premium Membership (Annually)" };
|
var items = new List<string> { "1 × Premium Membership (Annually)" };
|
||||||
|
@ -39,6 +39,9 @@ namespace Bit.Billing
|
|||||||
// Repositories
|
// Repositories
|
||||||
services.AddSqlServerRepositories(globalSettings);
|
services.AddSqlServerRepositories(globalSettings);
|
||||||
|
|
||||||
|
// PayPal Client
|
||||||
|
services.AddSingleton<Utilities.PayPalClient>();
|
||||||
|
|
||||||
// Context
|
// Context
|
||||||
services.AddScoped<CurrentContext>();
|
services.AddScoped<CurrentContext>();
|
||||||
|
|
||||||
|
237
src/Billing/Utilities/PayPalClient.cs
Normal file
237
src/Billing/Utilities/PayPalClient.cs
Normal file
@ -0,0 +1,237 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace Bit.Billing.Utilities
|
||||||
|
{
|
||||||
|
public class PayPalClient
|
||||||
|
{
|
||||||
|
private readonly HttpClient _httpClient = new HttpClient();
|
||||||
|
private readonly string _baseApiUrl;
|
||||||
|
private readonly string _clientId;
|
||||||
|
private readonly string _clientSecret;
|
||||||
|
|
||||||
|
private AuthResponse _authResponse;
|
||||||
|
|
||||||
|
public PayPalClient(BillingSettings billingSettings)
|
||||||
|
{
|
||||||
|
_baseApiUrl = _baseApiUrl = !billingSettings.PayPal.Production ? "https://api.sandbox.paypal.com/{0}" :
|
||||||
|
"https://api.paypal.com/{0}";
|
||||||
|
_clientId = billingSettings.PayPal.ClientId;
|
||||||
|
_clientSecret = billingSettings.PayPal.ClientSecret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> VerifyWebhookAsync(string webhookJson, IHeaderDictionary headers, string webhookId)
|
||||||
|
{
|
||||||
|
if(webhookJson == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentException("No webhook json.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(headers == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentException("No headers.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!headers.ContainsKey("PAYPAL-TRANSMISSION-ID"))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await AuthIfNeededAsync();
|
||||||
|
|
||||||
|
var req = new HttpRequestMessage
|
||||||
|
{
|
||||||
|
Method = HttpMethod.Post,
|
||||||
|
RequestUri = new Uri(string.Format(_baseApiUrl, "v1/notifications/verify-webhook-signature"))
|
||||||
|
};
|
||||||
|
req.Headers.Authorization = new AuthenticationHeaderValue(
|
||||||
|
_authResponse.TokenType, _authResponse.AccessToken);
|
||||||
|
req.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
||||||
|
|
||||||
|
var verifyRequest = new VerifyWebookRequest
|
||||||
|
{
|
||||||
|
AuthAlgo = headers["PAYPAL-AUTH-ALGO"],
|
||||||
|
CertUrl = headers["PAYPAL-CERT-URL"],
|
||||||
|
TransmissionId = headers["PAYPAL-TRANSMISSION-ID"],
|
||||||
|
TransmissionTime = headers["PAYPAL-TRANSMISSION-TIME"],
|
||||||
|
TransmissionSig = headers["PAYPAL-TRANSMISSION-SIG"],
|
||||||
|
WebhookId = webhookId
|
||||||
|
};
|
||||||
|
var verifyRequestJson = JsonConvert.SerializeObject(verifyRequest);
|
||||||
|
verifyRequestJson = verifyRequestJson.Replace("\"__WEBHOOK_BODY__\"", webhookJson);
|
||||||
|
req.Content = new StringContent(verifyRequestJson, Encoding.UTF8, "application/json");
|
||||||
|
|
||||||
|
var response = await _httpClient.SendAsync(req);
|
||||||
|
if(!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
throw new Exception("Failed to verify webhook");
|
||||||
|
}
|
||||||
|
|
||||||
|
var responseContent = await response.Content.ReadAsStringAsync();
|
||||||
|
var verifyResponse = JsonConvert.DeserializeObject<VerifyWebookResponse>(responseContent);
|
||||||
|
return verifyResponse.Verified;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<bool> AuthIfNeededAsync()
|
||||||
|
{
|
||||||
|
if(_authResponse?.Expired ?? true)
|
||||||
|
{
|
||||||
|
var req = new HttpRequestMessage
|
||||||
|
{
|
||||||
|
Method = HttpMethod.Post,
|
||||||
|
RequestUri = new Uri(string.Format(_baseApiUrl, "v1/oauth2/token"))
|
||||||
|
};
|
||||||
|
var authVal = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{_clientId}:{_clientSecret}"));
|
||||||
|
req.Headers.Authorization = new AuthenticationHeaderValue("Basic", authVal);
|
||||||
|
req.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
||||||
|
req.Content = new FormUrlEncodedContent(new[]
|
||||||
|
{
|
||||||
|
new KeyValuePair<string, string>("grant_type", "client_credentials")
|
||||||
|
});
|
||||||
|
|
||||||
|
var response = await _httpClient.SendAsync(req);
|
||||||
|
if(!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
throw new Exception("Failed to auth with PayPal");
|
||||||
|
}
|
||||||
|
|
||||||
|
var responseContent = await response.Content.ReadAsStringAsync();
|
||||||
|
_authResponse = JsonConvert.DeserializeObject<AuthResponse>(responseContent);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class VerifyWebookRequest
|
||||||
|
{
|
||||||
|
[JsonProperty("auth_algo")]
|
||||||
|
public string AuthAlgo { get; set; }
|
||||||
|
[JsonProperty("cert_url")]
|
||||||
|
public string CertUrl { get; set; }
|
||||||
|
[JsonProperty("transmission_id")]
|
||||||
|
public string TransmissionId { get; set; }
|
||||||
|
[JsonProperty("transmission_sig")]
|
||||||
|
public string TransmissionSig { get; set; }
|
||||||
|
[JsonProperty("transmission_time")]
|
||||||
|
public string TransmissionTime { get; set; }
|
||||||
|
[JsonProperty("webhook_event")]
|
||||||
|
public string WebhookEvent { get; set; } = "__WEBHOOK_BODY__";
|
||||||
|
[JsonProperty("webhook_id")]
|
||||||
|
public string WebhookId { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class VerifyWebookResponse
|
||||||
|
{
|
||||||
|
[JsonProperty("verification_status")]
|
||||||
|
public string VerificationStatus { get; set; }
|
||||||
|
public bool Verified => VerificationStatus == "SUCCESS";
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AuthResponse
|
||||||
|
{
|
||||||
|
private DateTime _created;
|
||||||
|
|
||||||
|
public AuthResponse()
|
||||||
|
{
|
||||||
|
_created = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
[JsonProperty("scope")]
|
||||||
|
public string Scope { get; set; }
|
||||||
|
[JsonProperty("nonce")]
|
||||||
|
public string Nonce { get; set; }
|
||||||
|
[JsonProperty("access_token")]
|
||||||
|
public string AccessToken { get; set; }
|
||||||
|
[JsonProperty("token_type")]
|
||||||
|
public string TokenType { get; set; }
|
||||||
|
[JsonProperty("app_id")]
|
||||||
|
public string AppId { get; set; }
|
||||||
|
[JsonProperty("expires_in")]
|
||||||
|
public long ExpiresIn { get; set; }
|
||||||
|
public bool Expired => DateTime.UtcNow > _created.AddSeconds(ExpiresIn - 30);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Event<T>
|
||||||
|
{
|
||||||
|
[JsonProperty("id")]
|
||||||
|
public string Id { get; set; }
|
||||||
|
[JsonProperty("event_type")]
|
||||||
|
public string EventType { get; set; }
|
||||||
|
[JsonProperty("resource_type")]
|
||||||
|
public string ResourceType { get; set; }
|
||||||
|
[JsonProperty("create_time")]
|
||||||
|
public DateTime CreateTime { get; set; }
|
||||||
|
public T Resource { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Refund : Sale
|
||||||
|
{
|
||||||
|
[JsonProperty("total_refunded_amount")]
|
||||||
|
public ValueInfo TotalRefundedAmount { get; set; }
|
||||||
|
[JsonProperty("sale_id")]
|
||||||
|
public string SaleId { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Sale
|
||||||
|
{
|
||||||
|
[JsonProperty("id")]
|
||||||
|
public string Id { get; set; }
|
||||||
|
[JsonProperty("state")]
|
||||||
|
public string State { get; set; }
|
||||||
|
[JsonProperty("amount")]
|
||||||
|
public AmountInfo Amount { get; set; }
|
||||||
|
[JsonProperty("parent_payment")]
|
||||||
|
public string ParentPayment { get; set; }
|
||||||
|
[JsonProperty("custom")]
|
||||||
|
public string Custom { get; set; }
|
||||||
|
[JsonProperty("create_time")]
|
||||||
|
public DateTime CreateTime { get; set; }
|
||||||
|
[JsonProperty("update_time")]
|
||||||
|
public DateTime UpdateTime { get; set; }
|
||||||
|
|
||||||
|
public Tuple<Guid?, Guid?> GetIdsFromCustom()
|
||||||
|
{
|
||||||
|
Guid? orgId = null;
|
||||||
|
Guid? userId = null;
|
||||||
|
|
||||||
|
if(!string.IsNullOrWhiteSpace(Custom) && Custom.Contains(":"))
|
||||||
|
{
|
||||||
|
var parts = Custom.Split(':');
|
||||||
|
if(parts.Length > 1 && Guid.TryParse(parts[1], out var id))
|
||||||
|
{
|
||||||
|
if(parts[0] == "user_id")
|
||||||
|
{
|
||||||
|
userId = id;
|
||||||
|
}
|
||||||
|
else if(parts[0] == "organization_id")
|
||||||
|
{
|
||||||
|
orgId = id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Tuple<Guid?, Guid?>(orgId, userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AmountInfo
|
||||||
|
{
|
||||||
|
[JsonProperty("total")]
|
||||||
|
public string Total { get; set; }
|
||||||
|
public decimal TotalAmount => Convert.ToDecimal(Total);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ValueInfo
|
||||||
|
{
|
||||||
|
[JsonProperty("value")]
|
||||||
|
public string Value { get; set; }
|
||||||
|
public decimal ValueAmount => Convert.ToDecimal(Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -15,5 +15,10 @@
|
|||||||
"braintree": {
|
"braintree": {
|
||||||
"production": true
|
"production": true
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"billingSettings": {
|
||||||
|
"payPal": {
|
||||||
|
"production": false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -45,18 +45,25 @@
|
|||||||
"notificationHub": {
|
"notificationHub": {
|
||||||
"connectionString": "SECRET",
|
"connectionString": "SECRET",
|
||||||
"hubName": "SECRET"
|
"hubName": "SECRET"
|
||||||
|
},
|
||||||
|
"braintree": {
|
||||||
|
"production": false,
|
||||||
|
"merchantId": "SECRET",
|
||||||
|
"publicKey": "SECRET",
|
||||||
|
"privateKey": "SECRET"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"billingSettings": {
|
"billingSettings": {
|
||||||
"jobsKey": "SECRET",
|
"jobsKey": "SECRET",
|
||||||
"stripeWebhookKey": "SECRET",
|
"stripeWebhookKey": "SECRET",
|
||||||
"stripeWebhookSecret": "SECRET",
|
"stripeWebhookSecret": "SECRET",
|
||||||
"braintreeWebhookKey": "SECRET"
|
"braintreeWebhookKey": "SECRET",
|
||||||
},
|
"payPal": {
|
||||||
"braintree": {
|
"production": false,
|
||||||
"production": false,
|
"clientId": "SECRET",
|
||||||
"merchantId": "SECRET",
|
"clientSecret": "SECRET",
|
||||||
"publicKey": "SECRET",
|
"webhookId": "SECRET",
|
||||||
"privateKey": "SECRET"
|
"webhookKey": "SECRET"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -38,7 +38,7 @@
|
|||||||
<PackageReference Include="AspNetCoreRateLimit" Version="2.1.0" />
|
<PackageReference Include="AspNetCoreRateLimit" Version="2.1.0" />
|
||||||
<PackageReference Include="Braintree" Version="4.6.0" />
|
<PackageReference Include="Braintree" Version="4.6.0" />
|
||||||
<PackageReference Include="Sendgrid" Version="9.10.0" />
|
<PackageReference Include="Sendgrid" Version="9.10.0" />
|
||||||
<PackageReference Include="Stripe.net" Version="19.10.0" />
|
<PackageReference Include="Stripe.net" Version="22.8.0" />
|
||||||
<PackageReference Include="U2F.Core" Version="1.0.4" />
|
<PackageReference Include="U2F.Core" Version="1.0.4" />
|
||||||
<PackageReference Include="Otp.NET" Version="1.2.0" />
|
<PackageReference Include="Otp.NET" Version="1.2.0" />
|
||||||
<PackageReference Include="YubicoDotNetClient" Version="1.2.0" />
|
<PackageReference Include="YubicoDotNetClient" Version="1.2.0" />
|
||||||
|
@ -13,6 +13,8 @@ namespace Bit.Core.Enums
|
|||||||
[Display(Name = "Google Play Store")]
|
[Display(Name = "Google Play Store")]
|
||||||
PlayStore = 3,
|
PlayStore = 3,
|
||||||
[Display(Name = "Coinbase")]
|
[Display(Name = "Coinbase")]
|
||||||
Coinbase = 4
|
Coinbase = 4,
|
||||||
|
[Display(Name = "PayPal")]
|
||||||
|
PayPal = 5,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
United = 11,
|
United = 11,
|
||||||
Yahoo = 12,
|
Yahoo = 12,
|
||||||
Zonelabs = 13,
|
Zonelabs = 13,
|
||||||
Paypal = 14,
|
PayPal = 14,
|
||||||
Avon = 15,
|
Avon = 15,
|
||||||
Diapers = 16,
|
Diapers = 16,
|
||||||
Contacts = 17,
|
Contacts = 17,
|
||||||
|
11
src/Core/Enums/TransactionType.cs
Normal file
11
src/Core/Enums/TransactionType.cs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
namespace Bit.Core.Enums
|
||||||
|
{
|
||||||
|
public enum TransactionType : byte
|
||||||
|
{
|
||||||
|
Charge = 0,
|
||||||
|
Credit = 1,
|
||||||
|
PromotionalCredit = 2,
|
||||||
|
ReferralCredit = 3,
|
||||||
|
Refund = 4,
|
||||||
|
}
|
||||||
|
}
|
@ -12,10 +12,12 @@ namespace Bit.Core.Models.Api
|
|||||||
public BillingResponseModel(User user, BillingInfo billing, UserLicense license)
|
public BillingResponseModel(User user, BillingInfo billing, UserLicense license)
|
||||||
: base("billing")
|
: base("billing")
|
||||||
{
|
{
|
||||||
|
CreditAmount = billing.CreditAmount;
|
||||||
PaymentSource = billing.PaymentSource != null ? new BillingSource(billing.PaymentSource) : null;
|
PaymentSource = billing.PaymentSource != null ? new BillingSource(billing.PaymentSource) : null;
|
||||||
Subscription = billing.Subscription != null ? new BillingSubscription(billing.Subscription) : null;
|
Subscription = billing.Subscription != null ? new BillingSubscription(billing.Subscription) : null;
|
||||||
Charges = billing.Charges.Select(c => new BillingCharge(c));
|
Transactions = billing.Transactions?.Select(t => new BillingTransaction(t));
|
||||||
UpcomingInvoice = billing.UpcomingInvoice != null ? new BillingInvoice(billing.UpcomingInvoice) : null;
|
Invoices = billing.Invoices?.Select(i => new BillingInvoice(i));
|
||||||
|
UpcomingInvoice = billing.UpcomingInvoice != null ? new BillingInvoiceInfo(billing.UpcomingInvoice) : null;
|
||||||
StorageName = user.Storage.HasValue ? Utilities.CoreHelpers.ReadableBytesSize(user.Storage.Value) : null;
|
StorageName = user.Storage.HasValue ? Utilities.CoreHelpers.ReadableBytesSize(user.Storage.Value) : null;
|
||||||
StorageGb = user.Storage.HasValue ? Math.Round(user.Storage.Value / 1073741824D, 2) : 0; // 1 GB
|
StorageGb = user.Storage.HasValue ? Math.Round(user.Storage.Value / 1073741824D, 2) : 0; // 1 GB
|
||||||
MaxStorageGb = user.MaxStorageGb;
|
MaxStorageGb = user.MaxStorageGb;
|
||||||
@ -37,13 +39,15 @@ namespace Bit.Core.Models.Api
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public decimal CreditAmount { get; set; }
|
||||||
public string StorageName { get; set; }
|
public string StorageName { get; set; }
|
||||||
public double? StorageGb { get; set; }
|
public double? StorageGb { get; set; }
|
||||||
public short? MaxStorageGb { get; set; }
|
public short? MaxStorageGb { get; set; }
|
||||||
public BillingSource PaymentSource { get; set; }
|
public BillingSource PaymentSource { get; set; }
|
||||||
public BillingSubscription Subscription { get; set; }
|
public BillingSubscription Subscription { get; set; }
|
||||||
public BillingInvoice UpcomingInvoice { get; set; }
|
public BillingInvoiceInfo UpcomingInvoice { get; set; }
|
||||||
public IEnumerable<BillingCharge> Charges { get; set; }
|
public IEnumerable<BillingInvoice> Invoices { get; set; }
|
||||||
|
public IEnumerable<BillingTransaction> Transactions { get; set; }
|
||||||
public UserLicense License { get; set; }
|
public UserLicense License { get; set; }
|
||||||
public DateTime? Expiration { get; set; }
|
public DateTime? Expiration { get; set; }
|
||||||
}
|
}
|
||||||
@ -109,9 +113,9 @@ namespace Bit.Core.Models.Api
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class BillingInvoice
|
public class BillingInvoiceInfo
|
||||||
{
|
{
|
||||||
public BillingInvoice(BillingInfo.BillingInvoice inv)
|
public BillingInvoiceInfo(BillingInfo.BillingInvoice inv)
|
||||||
{
|
{
|
||||||
Amount = inv.Amount;
|
Amount = inv.Amount;
|
||||||
Date = inv.Date;
|
Date = inv.Date;
|
||||||
@ -121,28 +125,44 @@ namespace Bit.Core.Models.Api
|
|||||||
public DateTime? Date { get; set; }
|
public DateTime? Date { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class BillingCharge
|
public class BillingInvoice : BillingInvoiceInfo
|
||||||
{
|
{
|
||||||
public BillingCharge(BillingInfo.BillingCharge charge)
|
public BillingInvoice(BillingInfo.BillingInvoice2 inv)
|
||||||
|
: base(inv)
|
||||||
{
|
{
|
||||||
Amount = charge.Amount;
|
Url = inv.Url;
|
||||||
RefundedAmount = charge.RefundedAmount;
|
PdfUrl = inv.PdfUrl;
|
||||||
PaymentSource = charge.PaymentSource != null ? new BillingSource(charge.PaymentSource) : null;
|
Number = inv.Number;
|
||||||
CreatedDate = charge.CreatedDate;
|
Paid = inv.Paid;
|
||||||
FailureMessage = charge.FailureMessage;
|
}
|
||||||
Refunded = charge.Refunded;
|
|
||||||
Status = charge.Status;
|
public string Url { get; set; }
|
||||||
InvoiceId = charge.InvoiceId;
|
public string PdfUrl { get; set; }
|
||||||
|
public string Number { get; set; }
|
||||||
|
public bool Paid { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class BillingTransaction
|
||||||
|
{
|
||||||
|
public BillingTransaction(BillingInfo.BillingTransaction transaction)
|
||||||
|
{
|
||||||
|
CreatedDate = transaction.CreatedDate;
|
||||||
|
Amount = transaction.Amount;
|
||||||
|
Refunded = transaction.Refunded;
|
||||||
|
RefundedAmount = transaction.RefundedAmount;
|
||||||
|
PartiallyRefunded = transaction.PartiallyRefunded;
|
||||||
|
Type = transaction.Type;
|
||||||
|
PaymentMethodType = transaction.PaymentMethodType;
|
||||||
|
Details = transaction.Details;
|
||||||
}
|
}
|
||||||
|
|
||||||
public DateTime CreatedDate { get; set; }
|
public DateTime CreatedDate { get; set; }
|
||||||
public decimal Amount { get; set; }
|
public decimal Amount { get; set; }
|
||||||
public BillingSource PaymentSource { get; set; }
|
public bool? Refunded { get; set; }
|
||||||
public string Status { get; set; }
|
public bool? PartiallyRefunded { get; set; }
|
||||||
public string FailureMessage { get; set; }
|
public decimal? RefundedAmount { get; set; }
|
||||||
public bool Refunded { get; set; }
|
public TransactionType Type { get; set; }
|
||||||
public bool PartiallyRefunded => !Refunded && RefundedAmount > 0;
|
public PaymentMethodType? PaymentMethodType { get; set; }
|
||||||
public decimal RefundedAmount { get; set; }
|
public string Details { get; set; }
|
||||||
public string InvoiceId { get; set; }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -67,8 +67,9 @@ namespace Bit.Core.Models.Api
|
|||||||
{
|
{
|
||||||
PaymentSource = billing.PaymentSource != null ? new BillingSource(billing.PaymentSource) : null;
|
PaymentSource = billing.PaymentSource != null ? new BillingSource(billing.PaymentSource) : null;
|
||||||
Subscription = billing.Subscription != null ? new BillingSubscription(billing.Subscription) : null;
|
Subscription = billing.Subscription != null ? new BillingSubscription(billing.Subscription) : null;
|
||||||
Charges = billing.Charges.Select(c => new BillingCharge(c));
|
Transactions = billing.Transactions?.Select(t => new BillingTransaction(t));
|
||||||
UpcomingInvoice = billing.UpcomingInvoice != null ? new BillingInvoice(billing.UpcomingInvoice) : null;
|
Invoices = billing.Invoices?.Select(i => new BillingInvoice(i));
|
||||||
|
UpcomingInvoice = billing.UpcomingInvoice != null ? new BillingInvoiceInfo(billing.UpcomingInvoice) : null;
|
||||||
StorageName = organization.Storage.HasValue ?
|
StorageName = organization.Storage.HasValue ?
|
||||||
Utilities.CoreHelpers.ReadableBytesSize(organization.Storage.Value) : null;
|
Utilities.CoreHelpers.ReadableBytesSize(organization.Storage.Value) : null;
|
||||||
StorageGb = organization.Storage.HasValue ? Math.Round(organization.Storage.Value / 1073741824D) : 0; // 1 GB
|
StorageGb = organization.Storage.HasValue ? Math.Round(organization.Storage.Value / 1073741824D) : 0; // 1 GB
|
||||||
@ -88,8 +89,9 @@ namespace Bit.Core.Models.Api
|
|||||||
public double? StorageGb { get; set; }
|
public double? StorageGb { get; set; }
|
||||||
public BillingSource PaymentSource { get; set; }
|
public BillingSource PaymentSource { get; set; }
|
||||||
public BillingSubscription Subscription { get; set; }
|
public BillingSubscription Subscription { get; set; }
|
||||||
public BillingInvoice UpcomingInvoice { get; set; }
|
public BillingInvoiceInfo UpcomingInvoice { get; set; }
|
||||||
public IEnumerable<BillingCharge> Charges { get; set; }
|
public IEnumerable<BillingInvoice> Invoices { get; set; }
|
||||||
|
public IEnumerable<BillingTransaction> Transactions { get; set; }
|
||||||
public DateTime? Expiration { get; set; }
|
public DateTime? Expiration { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Braintree;
|
using Bit.Core.Models.Table;
|
||||||
using Stripe;
|
using Stripe;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
@ -9,47 +9,47 @@ namespace Bit.Core.Models.Business
|
|||||||
{
|
{
|
||||||
public class BillingInfo
|
public class BillingInfo
|
||||||
{
|
{
|
||||||
|
public decimal CreditAmount { get; set; }
|
||||||
public BillingSource PaymentSource { get; set; }
|
public BillingSource PaymentSource { get; set; }
|
||||||
public BillingSubscription Subscription { get; set; }
|
public BillingSubscription Subscription { get; set; }
|
||||||
public BillingInvoice UpcomingInvoice { get; set; }
|
public BillingInvoice UpcomingInvoice { get; set; }
|
||||||
public IEnumerable<BillingCharge> Charges { get; set; } = new List<BillingCharge>();
|
public IEnumerable<BillingCharge> Charges { get; set; } = new List<BillingCharge>();
|
||||||
|
public IEnumerable<BillingInvoice2> Invoices { get; set; } = new List<BillingInvoice2>();
|
||||||
|
public IEnumerable<BillingTransaction> Transactions { get; set; } = new List<BillingTransaction>();
|
||||||
|
|
||||||
public class BillingSource
|
public class BillingSource
|
||||||
{
|
{
|
||||||
public BillingSource(Source source)
|
public BillingSource(IPaymentSource source)
|
||||||
{
|
{
|
||||||
switch(source.Type)
|
if(source is BankAccount bankAccount)
|
||||||
{
|
{
|
||||||
case SourceType.Card:
|
Type = PaymentMethodType.BankAccount;
|
||||||
Type = PaymentMethodType.Card;
|
Description = $"{bankAccount.BankName}, *{bankAccount.Last4} - " +
|
||||||
Description = $"{source.Card.Brand}, *{source.Card.Last4}, " +
|
(bankAccount.Status == "verified" ? "verified" :
|
||||||
string.Format("{0}/{1}",
|
bankAccount.Status == "errored" ? "invalid" :
|
||||||
string.Concat(source.Card.ExpirationMonth < 10 ?
|
bankAccount.Status == "verification_failed" ? "verification failed" : "unverified");
|
||||||
"0" : string.Empty, source.Card.ExpirationMonth),
|
NeedsVerification = bankAccount.Status == "new" || bankAccount.Status == "validated";
|
||||||
source.Card.ExpirationYear);
|
}
|
||||||
CardBrand = source.Card.Brand;
|
else if(source is Card card)
|
||||||
break;
|
{
|
||||||
case SourceType.BankAccount:
|
Type = PaymentMethodType.Card;
|
||||||
Type = PaymentMethodType.BankAccount;
|
Description = $"{card.Brand}, *{card.Last4}, " +
|
||||||
Description = $"{source.BankAccount.BankName}, *{source.BankAccount.Last4} - " +
|
string.Format("{0}/{1}",
|
||||||
(source.BankAccount.Status == "verified" ? "verified" :
|
string.Concat(card.ExpMonth < 10 ?
|
||||||
source.BankAccount.Status == "errored" ? "invalid" :
|
"0" : string.Empty, card.ExpMonth),
|
||||||
source.BankAccount.Status == "verification_failed" ? "verification failed" : "unverified");
|
card.ExpYear);
|
||||||
NeedsVerification = source.BankAccount.Status == "new" || source.BankAccount.Status == "validated";
|
CardBrand = card.Brand;
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public BillingSource(PaymentMethod method)
|
public BillingSource(Braintree.PaymentMethod method)
|
||||||
{
|
{
|
||||||
if(method is PayPalAccount paypal)
|
if(method is Braintree.PayPalAccount paypal)
|
||||||
{
|
{
|
||||||
Type = PaymentMethodType.PayPal;
|
Type = PaymentMethodType.PayPal;
|
||||||
Description = paypal.Email;
|
Description = paypal.Email;
|
||||||
}
|
}
|
||||||
else if(method is CreditCard card)
|
else if(method is Braintree.CreditCard card)
|
||||||
{
|
{
|
||||||
Type = PaymentMethodType.Card;
|
Type = PaymentMethodType.Card;
|
||||||
Description = $"{card.CardType.ToString()}, *{card.LastFour}, " +
|
Description = $"{card.CardType.ToString()}, *{card.LastFour}, " +
|
||||||
@ -59,7 +59,7 @@ namespace Bit.Core.Models.Business
|
|||||||
card.ExpirationYear);
|
card.ExpirationYear);
|
||||||
CardBrand = card.CardType.ToString();
|
CardBrand = card.CardType.ToString();
|
||||||
}
|
}
|
||||||
else if(method is UsBankAccount bank)
|
else if(method is Braintree.UsBankAccount bank)
|
||||||
{
|
{
|
||||||
Type = PaymentMethodType.BankAccount;
|
Type = PaymentMethodType.BankAccount;
|
||||||
Description = $"{bank.BankName}, *{bank.Last4}";
|
Description = $"{bank.BankName}, *{bank.Last4}";
|
||||||
@ -70,13 +70,13 @@ namespace Bit.Core.Models.Business
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public BillingSource(UsBankAccountDetails bank)
|
public BillingSource(Braintree.UsBankAccountDetails bank)
|
||||||
{
|
{
|
||||||
Type = PaymentMethodType.BankAccount;
|
Type = PaymentMethodType.BankAccount;
|
||||||
Description = $"{bank.BankName}, *{bank.Last4}";
|
Description = $"{bank.BankName}, *{bank.Last4}";
|
||||||
}
|
}
|
||||||
|
|
||||||
public BillingSource(PayPalDetails paypal)
|
public BillingSource(Braintree.PayPalDetails paypal)
|
||||||
{
|
{
|
||||||
Type = PaymentMethodType.PayPal;
|
Type = PaymentMethodType.PayPal;
|
||||||
Description = paypal.PayerEmail;
|
Description = paypal.PayerEmail;
|
||||||
@ -90,7 +90,7 @@ namespace Bit.Core.Models.Business
|
|||||||
|
|
||||||
public class BillingSubscription
|
public class BillingSubscription
|
||||||
{
|
{
|
||||||
public BillingSubscription(StripeSubscription sub)
|
public BillingSubscription(Subscription sub)
|
||||||
{
|
{
|
||||||
Status = sub.Status;
|
Status = sub.Status;
|
||||||
TrialStartDate = sub.TrialStart;
|
TrialStartDate = sub.TrialStart;
|
||||||
@ -106,14 +106,14 @@ namespace Bit.Core.Models.Business
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public BillingSubscription(Subscription sub, Plan plan)
|
public BillingSubscription(Braintree.Subscription sub, Braintree.Plan plan)
|
||||||
{
|
{
|
||||||
Status = sub.Status.ToString();
|
Status = sub.Status.ToString();
|
||||||
|
|
||||||
if(sub.HasTrialPeriod.GetValueOrDefault() && sub.CreatedAt.HasValue && sub.TrialDuration.HasValue)
|
if(sub.HasTrialPeriod.GetValueOrDefault() && sub.CreatedAt.HasValue && sub.TrialDuration.HasValue)
|
||||||
{
|
{
|
||||||
TrialStartDate = sub.CreatedAt.Value;
|
TrialStartDate = sub.CreatedAt.Value;
|
||||||
if(sub.TrialDurationUnit == SubscriptionDurationUnit.DAY)
|
if(sub.TrialDurationUnit == Braintree.SubscriptionDurationUnit.DAY)
|
||||||
{
|
{
|
||||||
TrialEndDate = TrialStartDate.Value.AddDays(sub.TrialDuration.Value);
|
TrialEndDate = TrialStartDate.Value.AddDays(sub.TrialDuration.Value);
|
||||||
}
|
}
|
||||||
@ -127,7 +127,7 @@ namespace Bit.Core.Models.Business
|
|||||||
PeriodEndDate = sub.BillingPeriodEndDate;
|
PeriodEndDate = sub.BillingPeriodEndDate;
|
||||||
|
|
||||||
CancelAtEndDate = !sub.NeverExpires.GetValueOrDefault();
|
CancelAtEndDate = !sub.NeverExpires.GetValueOrDefault();
|
||||||
Cancelled = sub.Status == SubscriptionStatus.CANCELED;
|
Cancelled = sub.Status == Braintree.SubscriptionStatus.CANCELED;
|
||||||
if(Cancelled)
|
if(Cancelled)
|
||||||
{
|
{
|
||||||
CancelledDate = sub.UpdatedAt.Value;
|
CancelledDate = sub.UpdatedAt.Value;
|
||||||
@ -159,7 +159,7 @@ namespace Bit.Core.Models.Business
|
|||||||
|
|
||||||
public class BillingSubscriptionItem
|
public class BillingSubscriptionItem
|
||||||
{
|
{
|
||||||
public BillingSubscriptionItem(StripeSubscriptionItem item)
|
public BillingSubscriptionItem(SubscriptionItem item)
|
||||||
{
|
{
|
||||||
if(item.Plan != null)
|
if(item.Plan != null)
|
||||||
{
|
{
|
||||||
@ -168,10 +168,10 @@ namespace Bit.Core.Models.Business
|
|||||||
Interval = item.Plan.Interval;
|
Interval = item.Plan.Interval;
|
||||||
}
|
}
|
||||||
|
|
||||||
Quantity = item.Quantity;
|
Quantity = (int)item.Quantity;
|
||||||
}
|
}
|
||||||
|
|
||||||
public BillingSubscriptionItem(Plan plan)
|
public BillingSubscriptionItem(Braintree.Plan plan)
|
||||||
{
|
{
|
||||||
Name = plan.Name;
|
Name = plan.Name;
|
||||||
Amount = plan.Price.GetValueOrDefault();
|
Amount = plan.Price.GetValueOrDefault();
|
||||||
@ -179,7 +179,7 @@ namespace Bit.Core.Models.Business
|
|||||||
Quantity = 1;
|
Quantity = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
public BillingSubscriptionItem(Plan plan, AddOn addon)
|
public BillingSubscriptionItem(Braintree.Plan plan, Braintree.AddOn addon)
|
||||||
{
|
{
|
||||||
Name = addon.Name;
|
Name = addon.Name;
|
||||||
Amount = addon.Amount.GetValueOrDefault();
|
Amount = addon.Amount.GetValueOrDefault();
|
||||||
@ -196,13 +196,15 @@ namespace Bit.Core.Models.Business
|
|||||||
|
|
||||||
public class BillingInvoice
|
public class BillingInvoice
|
||||||
{
|
{
|
||||||
public BillingInvoice(StripeInvoice inv)
|
public BillingInvoice() { }
|
||||||
|
|
||||||
|
public BillingInvoice(Invoice inv)
|
||||||
{
|
{
|
||||||
Amount = inv.AmountDue / 100M;
|
Amount = inv.AmountDue / 100M;
|
||||||
Date = inv.Date.Value;
|
Date = inv.Date.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public BillingInvoice(Subscription sub)
|
public BillingInvoice(Braintree.Subscription sub)
|
||||||
{
|
{
|
||||||
Amount = sub.NextBillAmount.GetValueOrDefault() + sub.Balance.GetValueOrDefault();
|
Amount = sub.NextBillAmount.GetValueOrDefault() + sub.Balance.GetValueOrDefault();
|
||||||
if(Amount < 0)
|
if(Amount < 0)
|
||||||
@ -218,7 +220,7 @@ namespace Bit.Core.Models.Business
|
|||||||
|
|
||||||
public class BillingCharge
|
public class BillingCharge
|
||||||
{
|
{
|
||||||
public BillingCharge(StripeCharge charge)
|
public BillingCharge(Charge charge)
|
||||||
{
|
{
|
||||||
Amount = charge.Amount / 100M;
|
Amount = charge.Amount / 100M;
|
||||||
RefundedAmount = charge.AmountRefunded / 100M;
|
RefundedAmount = charge.AmountRefunded / 100M;
|
||||||
@ -230,7 +232,7 @@ namespace Bit.Core.Models.Business
|
|||||||
InvoiceId = charge.InvoiceId;
|
InvoiceId = charge.InvoiceId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public BillingCharge(Transaction transaction)
|
public BillingCharge(Braintree.Transaction transaction)
|
||||||
{
|
{
|
||||||
Amount = transaction.Amount.GetValueOrDefault();
|
Amount = transaction.Amount.GetValueOrDefault();
|
||||||
RefundedAmount = 0; // TODO?
|
RefundedAmount = 0; // TODO?
|
||||||
@ -239,7 +241,8 @@ namespace Bit.Core.Models.Business
|
|||||||
{
|
{
|
||||||
PaymentSource = new BillingSource(transaction.PayPalDetails);
|
PaymentSource = new BillingSource(transaction.PayPalDetails);
|
||||||
}
|
}
|
||||||
else if(transaction.CreditCard != null && transaction.CreditCard.CardType != CreditCardCardType.UNRECOGNIZED)
|
else if(transaction.CreditCard != null &&
|
||||||
|
transaction.CreditCard.CardType != Braintree.CreditCardCardType.UNRECOGNIZED)
|
||||||
{
|
{
|
||||||
PaymentSource = new BillingSource(transaction.CreditCard);
|
PaymentSource = new BillingSource(transaction.CreditCard);
|
||||||
}
|
}
|
||||||
@ -265,5 +268,63 @@ namespace Bit.Core.Models.Business
|
|||||||
public decimal RefundedAmount { get; set; }
|
public decimal RefundedAmount { get; set; }
|
||||||
public string InvoiceId { get; set; }
|
public string InvoiceId { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class BillingTransaction
|
||||||
|
{
|
||||||
|
public BillingTransaction(Transaction transaction)
|
||||||
|
{
|
||||||
|
CreatedDate = transaction.CreationDate;
|
||||||
|
Refunded = transaction.Refunded;
|
||||||
|
Type = transaction.Type;
|
||||||
|
PaymentMethodType = transaction.PaymentMethodType;
|
||||||
|
Details = transaction.Details;
|
||||||
|
|
||||||
|
if(transaction.RefundedAmount.HasValue)
|
||||||
|
{
|
||||||
|
RefundedAmount = Math.Abs(transaction.RefundedAmount.Value);
|
||||||
|
}
|
||||||
|
switch(transaction.Type)
|
||||||
|
{
|
||||||
|
case TransactionType.Charge:
|
||||||
|
case TransactionType.Credit:
|
||||||
|
case TransactionType.PromotionalCredit:
|
||||||
|
case TransactionType.ReferralCredit:
|
||||||
|
Amount = -1 * Math.Abs(transaction.Amount);
|
||||||
|
break;
|
||||||
|
case TransactionType.Refund:
|
||||||
|
Amount = Math.Abs(transaction.Amount);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public DateTime CreatedDate { get; set; }
|
||||||
|
public decimal Amount { get; set; }
|
||||||
|
public bool? Refunded { get; set; }
|
||||||
|
public bool? PartiallyRefunded => !Refunded.GetValueOrDefault() && RefundedAmount.GetValueOrDefault() > 0;
|
||||||
|
public decimal? RefundedAmount { get; set; }
|
||||||
|
public TransactionType Type { get; set; }
|
||||||
|
public PaymentMethodType? PaymentMethodType { get; set; }
|
||||||
|
public string Details { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class BillingInvoice2 : BillingInvoice
|
||||||
|
{
|
||||||
|
public BillingInvoice2(Invoice inv)
|
||||||
|
{
|
||||||
|
Url = inv.HostedInvoiceUrl;
|
||||||
|
PdfUrl = inv.InvoicePdf;
|
||||||
|
Number = inv.Number;
|
||||||
|
Paid = inv.Paid;
|
||||||
|
Amount = inv.Total / 100M;
|
||||||
|
Date = inv.Date.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Url { get; set; }
|
||||||
|
public string PdfUrl { get; set; }
|
||||||
|
public string Number { get; set; }
|
||||||
|
public bool Paid { get; set; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,19 @@
|
|||||||
using Bit.Core.Enums;
|
using System;
|
||||||
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
|
|
||||||
namespace Bit.Core.Models.Table
|
namespace Bit.Core.Models.Table
|
||||||
{
|
{
|
||||||
public interface ISubscriber
|
public interface ISubscriber
|
||||||
{
|
{
|
||||||
|
Guid Id { get; }
|
||||||
GatewayType? Gateway { get; set; }
|
GatewayType? Gateway { get; set; }
|
||||||
string GatewayCustomerId { get; set; }
|
string GatewayCustomerId { get; set; }
|
||||||
string GatewaySubscriptionId { get; set; }
|
string GatewaySubscriptionId { get; set; }
|
||||||
string BillingEmailAddress();
|
string BillingEmailAddress();
|
||||||
string BillingName();
|
string BillingName();
|
||||||
IPaymentService GetPaymentService(GlobalSettings globalSettings);
|
string BraintreeCustomerIdPrefix();
|
||||||
|
string BraintreeIdField();
|
||||||
|
string GatewayIdField();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -63,6 +63,21 @@ namespace Bit.Core.Models.Table
|
|||||||
return BusinessName;
|
return BusinessName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public string BraintreeCustomerIdPrefix()
|
||||||
|
{
|
||||||
|
return "o";
|
||||||
|
}
|
||||||
|
|
||||||
|
public string BraintreeIdField()
|
||||||
|
{
|
||||||
|
return "organization_id";
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GatewayIdField()
|
||||||
|
{
|
||||||
|
return "organizationId";
|
||||||
|
}
|
||||||
|
|
||||||
public long StorageBytesRemaining()
|
public long StorageBytesRemaining()
|
||||||
{
|
{
|
||||||
if(!MaxStorageGb.HasValue)
|
if(!MaxStorageGb.HasValue)
|
||||||
@ -84,29 +99,6 @@ namespace Bit.Core.Models.Table
|
|||||||
return maxStorageBytes - Storage.Value;
|
return maxStorageBytes - Storage.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public IPaymentService GetPaymentService(GlobalSettings globalSettings)
|
|
||||||
{
|
|
||||||
if(Gateway == null)
|
|
||||||
{
|
|
||||||
throw new BadRequestException("No gateway.");
|
|
||||||
}
|
|
||||||
|
|
||||||
IPaymentService paymentService = null;
|
|
||||||
switch(Gateway)
|
|
||||||
{
|
|
||||||
case GatewayType.Stripe:
|
|
||||||
paymentService = new StripePaymentService();
|
|
||||||
break;
|
|
||||||
case GatewayType.Braintree:
|
|
||||||
paymentService = new BraintreePaymentService(globalSettings);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new NotSupportedException("Unsupported gateway.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return paymentService;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Dictionary<TwoFactorProviderType, TwoFactorProvider> GetTwoFactorProviders()
|
public Dictionary<TwoFactorProviderType, TwoFactorProvider> GetTwoFactorProviders()
|
||||||
{
|
{
|
||||||
if(string.IsNullOrWhiteSpace(TwoFactorProviders))
|
if(string.IsNullOrWhiteSpace(TwoFactorProviders))
|
||||||
|
27
src/Core/Models/Table/Transaction.cs
Normal file
27
src/Core/Models/Table/Transaction.cs
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
using System;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
|
||||||
|
namespace Bit.Core.Models.Table
|
||||||
|
{
|
||||||
|
public class Transaction : ITableObject<Guid>
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public Guid? UserId { get; set; }
|
||||||
|
public Guid? OrganizationId { get; set; }
|
||||||
|
public TransactionType Type { get; set; }
|
||||||
|
public decimal Amount { get; set; }
|
||||||
|
public bool? Refunded { get; set; }
|
||||||
|
public decimal? RefundedAmount { get; set; }
|
||||||
|
public string Details { get; set; }
|
||||||
|
public PaymentMethodType? PaymentMethodType { get; set; }
|
||||||
|
public GatewayType? Gateway { get; set; }
|
||||||
|
public string GatewayId { get; set; }
|
||||||
|
public DateTime CreationDate { get; set; } = DateTime.UtcNow;
|
||||||
|
|
||||||
|
public void SetNewId()
|
||||||
|
{
|
||||||
|
Id = CoreHelpers.GenerateComb();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -58,6 +58,21 @@ namespace Bit.Core.Models.Table
|
|||||||
return Name;
|
return Name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public string BraintreeCustomerIdPrefix()
|
||||||
|
{
|
||||||
|
return "u";
|
||||||
|
}
|
||||||
|
|
||||||
|
public string BraintreeIdField()
|
||||||
|
{
|
||||||
|
return "user_id";
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GatewayIdField()
|
||||||
|
{
|
||||||
|
return "userId";
|
||||||
|
}
|
||||||
|
|
||||||
public Dictionary<TwoFactorProviderType, TwoFactorProvider> GetTwoFactorProviders()
|
public Dictionary<TwoFactorProviderType, TwoFactorProvider> GetTwoFactorProviders()
|
||||||
{
|
{
|
||||||
if(string.IsNullOrWhiteSpace(TwoFactorProviders))
|
if(string.IsNullOrWhiteSpace(TwoFactorProviders))
|
||||||
@ -133,29 +148,6 @@ namespace Bit.Core.Models.Table
|
|||||||
return maxStorageBytes - Storage.Value;
|
return maxStorageBytes - Storage.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public IPaymentService GetPaymentService(GlobalSettings globalSettings)
|
|
||||||
{
|
|
||||||
if(Gateway == null)
|
|
||||||
{
|
|
||||||
throw new BadRequestException("No gateway.");
|
|
||||||
}
|
|
||||||
|
|
||||||
IPaymentService paymentService = null;
|
|
||||||
switch(Gateway)
|
|
||||||
{
|
|
||||||
case GatewayType.Stripe:
|
|
||||||
paymentService = new StripePaymentService();
|
|
||||||
break;
|
|
||||||
case GatewayType.Braintree:
|
|
||||||
paymentService = new BraintreePaymentService(globalSettings);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new NotSupportedException("Unsupported gateway.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return paymentService;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IdentityUser ToIdentityUser(bool twoFactorEnabled)
|
public IdentityUser ToIdentityUser(bool twoFactorEnabled)
|
||||||
{
|
{
|
||||||
return new IdentityUser
|
return new IdentityUser
|
||||||
|
@ -5,6 +5,7 @@ namespace Bit.Core.Repositories
|
|||||||
public interface IMaintenanceRepository
|
public interface IMaintenanceRepository
|
||||||
{
|
{
|
||||||
Task UpdateStatisticsAsync();
|
Task UpdateStatisticsAsync();
|
||||||
|
Task DisableCipherAutoStatsAsync();
|
||||||
Task RebuildIndexesAsync();
|
Task RebuildIndexesAsync();
|
||||||
Task DeleteExpiredGrantsAsync();
|
Task DeleteExpiredGrantsAsync();
|
||||||
}
|
}
|
||||||
|
15
src/Core/Repositories/ITransactionRepository.cs
Normal file
15
src/Core/Repositories/ITransactionRepository.cs
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
using System;
|
||||||
|
using Bit.Core.Models.Table;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
|
||||||
|
namespace Bit.Core.Repositories
|
||||||
|
{
|
||||||
|
public interface ITransactionRepository : IRepository<Transaction, Guid>
|
||||||
|
{
|
||||||
|
Task<ICollection<Transaction>> GetManyByUserIdAsync(Guid userId);
|
||||||
|
Task<ICollection<Transaction>> GetManyByOrganizationIdAsync(Guid organizationId);
|
||||||
|
Task<Transaction> GetByGatewayIdAsync(GatewayType gatewayType, string gatewayId);
|
||||||
|
}
|
||||||
|
}
|
@ -27,6 +27,17 @@ namespace Bit.Core.Repositories.SqlServer
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task DisableCipherAutoStatsAsync()
|
||||||
|
{
|
||||||
|
using(var connection = new SqlConnection(ConnectionString))
|
||||||
|
{
|
||||||
|
await connection.ExecuteAsync(
|
||||||
|
"sp_autostats",
|
||||||
|
new { tblname = "[dbo].[Cipher]", flagc = "OFF" },
|
||||||
|
commandType: CommandType.StoredProcedure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async Task RebuildIndexesAsync()
|
public async Task RebuildIndexesAsync()
|
||||||
{
|
{
|
||||||
using(var connection = new SqlConnection(ConnectionString))
|
using(var connection = new SqlConnection(ConnectionString))
|
||||||
|
62
src/Core/Repositories/SqlServer/TransactionRepository.cs
Normal file
62
src/Core/Repositories/SqlServer/TransactionRepository.cs
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
using System;
|
||||||
|
using Bit.Core.Models.Table;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Dapper;
|
||||||
|
using System.Data;
|
||||||
|
using System.Data.SqlClient;
|
||||||
|
using System.Linq;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
|
||||||
|
namespace Bit.Core.Repositories.SqlServer
|
||||||
|
{
|
||||||
|
public class TransactionRepository : Repository<Transaction, Guid>, ITransactionRepository
|
||||||
|
{
|
||||||
|
public TransactionRepository(GlobalSettings globalSettings)
|
||||||
|
: this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString)
|
||||||
|
{ }
|
||||||
|
|
||||||
|
public TransactionRepository(string connectionString, string readOnlyConnectionString)
|
||||||
|
: base(connectionString, readOnlyConnectionString)
|
||||||
|
{ }
|
||||||
|
|
||||||
|
public async Task<ICollection<Transaction>> GetManyByUserIdAsync(Guid userId)
|
||||||
|
{
|
||||||
|
using(var connection = new SqlConnection(ConnectionString))
|
||||||
|
{
|
||||||
|
var results = await connection.QueryAsync<Transaction>(
|
||||||
|
$"[{Schema}].[Transaction_ReadByUserId]",
|
||||||
|
new { UserId = userId },
|
||||||
|
commandType: CommandType.StoredProcedure);
|
||||||
|
|
||||||
|
return results.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ICollection<Transaction>> GetManyByOrganizationIdAsync(Guid organizationId)
|
||||||
|
{
|
||||||
|
using(var connection = new SqlConnection(ConnectionString))
|
||||||
|
{
|
||||||
|
var results = await connection.QueryAsync<Transaction>(
|
||||||
|
$"[{Schema}].[Transaction_ReadByOrganizationId]",
|
||||||
|
new { OrganizationId = organizationId },
|
||||||
|
commandType: CommandType.StoredProcedure);
|
||||||
|
|
||||||
|
return results.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Transaction> GetByGatewayIdAsync(GatewayType gatewayType, string gatewayId)
|
||||||
|
{
|
||||||
|
using(var connection = new SqlConnection(ConnectionString))
|
||||||
|
{
|
||||||
|
var results = await connection.QueryAsync<Transaction>(
|
||||||
|
$"[{Schema}].[Transaction_ReadByGatewayId]",
|
||||||
|
new { Gateway = gatewayType, GatewayId = gatewayId },
|
||||||
|
commandType: CommandType.StoredProcedure);
|
||||||
|
|
||||||
|
return results.SingleOrDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,17 +1,22 @@
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Bit.Core.Models.Table;
|
using Bit.Core.Models.Table;
|
||||||
using Bit.Core.Models.Business;
|
using Bit.Core.Models.Business;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
|
||||||
namespace Bit.Core.Services
|
namespace Bit.Core.Services
|
||||||
{
|
{
|
||||||
public interface IPaymentService
|
public interface IPaymentService
|
||||||
{
|
{
|
||||||
Task CancelAndRecoverChargesAsync(ISubscriber subscriber);
|
Task CancelAndRecoverChargesAsync(ISubscriber subscriber);
|
||||||
Task PurchasePremiumAsync(User user, string paymentToken, short additionalStorageGb);
|
Task PurchaseOrganizationAsync(Organization org, PaymentMethodType paymentMethodType, string paymentToken,
|
||||||
|
Models.StaticStore.Plan plan, short additionalStorageGb, short additionalSeats, bool premiumAccessAddon);
|
||||||
|
Task PurchasePremiumAsync(User user, PaymentMethodType paymentMethodType, string paymentToken,
|
||||||
|
short additionalStorageGb);
|
||||||
Task AdjustStorageAsync(IStorableSubscriber storableSubscriber, int additionalStorage, string storagePlanId);
|
Task AdjustStorageAsync(IStorableSubscriber storableSubscriber, int additionalStorage, string storagePlanId);
|
||||||
Task CancelSubscriptionAsync(ISubscriber subscriber, bool endOfPeriod = false);
|
Task CancelSubscriptionAsync(ISubscriber subscriber, bool endOfPeriod = false);
|
||||||
Task ReinstateSubscriptionAsync(ISubscriber subscriber);
|
Task ReinstateSubscriptionAsync(ISubscriber subscriber);
|
||||||
Task<bool> UpdatePaymentMethodAsync(ISubscriber subscriber, string paymentToken);
|
Task<bool> UpdatePaymentMethodAsync(ISubscriber subscriber, PaymentMethodType paymentMethodType,
|
||||||
|
string paymentToken);
|
||||||
Task<BillingInfo.BillingInvoice> GetUpcomingInvoiceAsync(ISubscriber subscriber);
|
Task<BillingInfo.BillingInvoice> GetUpcomingInvoiceAsync(ISubscriber subscriber);
|
||||||
Task<BillingInfo> GetBillingAsync(ISubscriber subscriber);
|
Task<BillingInfo> GetBillingAsync(ISubscriber subscriber);
|
||||||
}
|
}
|
||||||
|
@ -1,368 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -32,7 +32,7 @@ namespace Bit.Core.Services
|
|||||||
private readonly IEventService _eventService;
|
private readonly IEventService _eventService;
|
||||||
private readonly IInstallationRepository _installationRepository;
|
private readonly IInstallationRepository _installationRepository;
|
||||||
private readonly IApplicationCacheService _applicationCacheService;
|
private readonly IApplicationCacheService _applicationCacheService;
|
||||||
private readonly StripePaymentService _stripePaymentService;
|
private readonly IPaymentService _paymentService;
|
||||||
private readonly GlobalSettings _globalSettings;
|
private readonly GlobalSettings _globalSettings;
|
||||||
|
|
||||||
public OrganizationService(
|
public OrganizationService(
|
||||||
@ -50,6 +50,7 @@ namespace Bit.Core.Services
|
|||||||
IEventService eventService,
|
IEventService eventService,
|
||||||
IInstallationRepository installationRepository,
|
IInstallationRepository installationRepository,
|
||||||
IApplicationCacheService applicationCacheService,
|
IApplicationCacheService applicationCacheService,
|
||||||
|
IPaymentService paymentService,
|
||||||
GlobalSettings globalSettings)
|
GlobalSettings globalSettings)
|
||||||
{
|
{
|
||||||
_organizationRepository = organizationRepository;
|
_organizationRepository = organizationRepository;
|
||||||
@ -66,7 +67,7 @@ namespace Bit.Core.Services
|
|||||||
_eventService = eventService;
|
_eventService = eventService;
|
||||||
_installationRepository = installationRepository;
|
_installationRepository = installationRepository;
|
||||||
_applicationCacheService = applicationCacheService;
|
_applicationCacheService = applicationCacheService;
|
||||||
_stripePaymentService = new StripePaymentService();
|
_paymentService = paymentService;
|
||||||
_globalSettings = globalSettings;
|
_globalSettings = globalSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -78,7 +79,22 @@ namespace Bit.Core.Services
|
|||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
var updated = await _stripePaymentService.UpdatePaymentMethodAsync(organization, paymentToken);
|
PaymentMethodType paymentMethodType;
|
||||||
|
if(paymentToken.StartsWith("btok_"))
|
||||||
|
{
|
||||||
|
paymentMethodType = PaymentMethodType.BankAccount;
|
||||||
|
}
|
||||||
|
else if(paymentToken.StartsWith("tok_"))
|
||||||
|
{
|
||||||
|
paymentMethodType = PaymentMethodType.Card;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
paymentMethodType = PaymentMethodType.PayPal;
|
||||||
|
}
|
||||||
|
|
||||||
|
var updated = await _paymentService.UpdatePaymentMethodAsync(organization,
|
||||||
|
paymentMethodType, paymentToken);
|
||||||
if(updated)
|
if(updated)
|
||||||
{
|
{
|
||||||
await ReplaceAndUpdateCache(organization);
|
await ReplaceAndUpdateCache(organization);
|
||||||
@ -100,7 +116,7 @@ namespace Bit.Core.Services
|
|||||||
eop = false;
|
eop = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
await _stripePaymentService.CancelSubscriptionAsync(organization, eop);
|
await _paymentService.CancelSubscriptionAsync(organization, eop);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ReinstateSubscriptionAsync(Guid organizationId)
|
public async Task ReinstateSubscriptionAsync(Guid organizationId)
|
||||||
@ -111,7 +127,7 @@ namespace Bit.Core.Services
|
|||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
await _stripePaymentService.ReinstateSubscriptionAsync(organization);
|
await _paymentService.ReinstateSubscriptionAsync(organization);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task UpgradePlanAsync(Guid organizationId, PlanType plan, int additionalSeats)
|
public async Task UpgradePlanAsync(Guid organizationId, PlanType plan, int additionalSeats)
|
||||||
@ -186,15 +202,15 @@ namespace Bit.Core.Services
|
|||||||
|
|
||||||
// TODO: Groups?
|
// TODO: Groups?
|
||||||
|
|
||||||
var subscriptionService = new StripeSubscriptionService();
|
var subscriptionService = new Stripe.SubscriptionService();
|
||||||
if(string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId))
|
if(string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId))
|
||||||
{
|
{
|
||||||
// They must have been on a free plan. Create new sub.
|
// They must have been on a free plan. Create new sub.
|
||||||
var subCreateOptions = new StripeSubscriptionCreateOptions
|
var subCreateOptions = new SubscriptionCreateOptions
|
||||||
{
|
{
|
||||||
CustomerId = organization.GatewayCustomerId,
|
CustomerId = organization.GatewayCustomerId,
|
||||||
TrialPeriodDays = newPlan.TrialPeriodDays,
|
TrialPeriodDays = newPlan.TrialPeriodDays,
|
||||||
Items = new List<StripeSubscriptionItemOption>(),
|
Items = new List<SubscriptionItemOption>(),
|
||||||
Metadata = new Dictionary<string, string> {
|
Metadata = new Dictionary<string, string> {
|
||||||
{ "organizationId", organization.Id.ToString() }
|
{ "organizationId", organization.Id.ToString() }
|
||||||
}
|
}
|
||||||
@ -202,7 +218,7 @@ namespace Bit.Core.Services
|
|||||||
|
|
||||||
if(newPlan.StripePlanId != null)
|
if(newPlan.StripePlanId != null)
|
||||||
{
|
{
|
||||||
subCreateOptions.Items.Add(new StripeSubscriptionItemOption
|
subCreateOptions.Items.Add(new SubscriptionItemOption
|
||||||
{
|
{
|
||||||
PlanId = newPlan.StripePlanId,
|
PlanId = newPlan.StripePlanId,
|
||||||
Quantity = 1
|
Quantity = 1
|
||||||
@ -211,7 +227,7 @@ namespace Bit.Core.Services
|
|||||||
|
|
||||||
if(additionalSeats > 0 && newPlan.StripeSeatPlanId != null)
|
if(additionalSeats > 0 && newPlan.StripeSeatPlanId != null)
|
||||||
{
|
{
|
||||||
subCreateOptions.Items.Add(new StripeSubscriptionItemOption
|
subCreateOptions.Items.Add(new SubscriptionItemOption
|
||||||
{
|
{
|
||||||
PlanId = newPlan.StripeSeatPlanId,
|
PlanId = newPlan.StripeSeatPlanId,
|
||||||
Quantity = additionalSeats
|
Quantity = additionalSeats
|
||||||
@ -223,14 +239,14 @@ namespace Bit.Core.Services
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Update existing sub.
|
// Update existing sub.
|
||||||
var subUpdateOptions = new StripeSubscriptionUpdateOptions
|
var subUpdateOptions = new SubscriptionUpdateOptions
|
||||||
{
|
{
|
||||||
Items = new List<StripeSubscriptionItemUpdateOption>()
|
Items = new List<SubscriptionItemUpdateOption>()
|
||||||
};
|
};
|
||||||
|
|
||||||
if(newPlan.StripePlanId != null)
|
if(newPlan.StripePlanId != null)
|
||||||
{
|
{
|
||||||
subUpdateOptions.Items.Add(new StripeSubscriptionItemUpdateOption
|
subUpdateOptions.Items.Add(new SubscriptionItemUpdateOption
|
||||||
{
|
{
|
||||||
PlanId = newPlan.StripePlanId,
|
PlanId = newPlan.StripePlanId,
|
||||||
Quantity = 1
|
Quantity = 1
|
||||||
@ -239,7 +255,7 @@ namespace Bit.Core.Services
|
|||||||
|
|
||||||
if(additionalSeats > 0 && newPlan.StripeSeatPlanId != null)
|
if(additionalSeats > 0 && newPlan.StripeSeatPlanId != null)
|
||||||
{
|
{
|
||||||
subUpdateOptions.Items.Add(new StripeSubscriptionItemUpdateOption
|
subUpdateOptions.Items.Add(new SubscriptionItemUpdateOption
|
||||||
{
|
{
|
||||||
PlanId = newPlan.StripeSeatPlanId,
|
PlanId = newPlan.StripeSeatPlanId,
|
||||||
Quantity = additionalSeats
|
Quantity = additionalSeats
|
||||||
@ -271,7 +287,7 @@ namespace Bit.Core.Services
|
|||||||
throw new BadRequestException("Plan does not allow additional storage.");
|
throw new BadRequestException("Plan does not allow additional storage.");
|
||||||
}
|
}
|
||||||
|
|
||||||
await BillingHelpers.AdjustStorageAsync(_stripePaymentService, organization, storageAdjustmentGb,
|
await BillingHelpers.AdjustStorageAsync(_paymentService, organization, storageAdjustmentGb,
|
||||||
plan.StripeStoragePlanId);
|
plan.StripeStoragePlanId);
|
||||||
await ReplaceAndUpdateCache(organization);
|
await ReplaceAndUpdateCache(organization);
|
||||||
}
|
}
|
||||||
@ -333,44 +349,74 @@ namespace Bit.Core.Services
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var subscriptionItemService = new StripeSubscriptionItemService();
|
var subscriptionItemService = new SubscriptionItemService();
|
||||||
var subscriptionService = new StripeSubscriptionService();
|
var subscriptionService = new SubscriptionService();
|
||||||
var sub = await subscriptionService.GetAsync(organization.GatewaySubscriptionId);
|
var sub = await subscriptionService.GetAsync(organization.GatewaySubscriptionId);
|
||||||
if(sub == null)
|
if(sub == null)
|
||||||
{
|
{
|
||||||
throw new BadRequestException("Subscription not found.");
|
throw new BadRequestException("Subscription not found.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Func<bool, Task<SubscriptionItem>> subUpdateAction = null;
|
||||||
var seatItem = sub.Items?.Data?.FirstOrDefault(i => i.Plan.Id == plan.StripeSeatPlanId);
|
var seatItem = sub.Items?.Data?.FirstOrDefault(i => i.Plan.Id == plan.StripeSeatPlanId);
|
||||||
|
var subItemOptions = sub.Items.Where(i => i.Plan.Id != plan.StripeSeatPlanId)
|
||||||
|
.Select(i => new InvoiceSubscriptionItemOptions
|
||||||
|
{
|
||||||
|
Id = i.Id,
|
||||||
|
PlanId = i.Plan.Id,
|
||||||
|
Quantity = i.Quantity,
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
if(additionalSeats > 0 && seatItem == null)
|
if(additionalSeats > 0 && seatItem == null)
|
||||||
{
|
{
|
||||||
await subscriptionItemService.CreateAsync(new StripeSubscriptionItemCreateOptions
|
subItemOptions.Add(new InvoiceSubscriptionItemOptions
|
||||||
{
|
{
|
||||||
PlanId = plan.StripeSeatPlanId,
|
PlanId = plan.StripeSeatPlanId,
|
||||||
Quantity = additionalSeats,
|
Quantity = additionalSeats,
|
||||||
Prorate = true,
|
|
||||||
SubscriptionId = sub.Id
|
|
||||||
});
|
});
|
||||||
|
subUpdateAction = (prorate) => subscriptionItemService.CreateAsync(
|
||||||
|
new SubscriptionItemCreateOptions
|
||||||
|
{
|
||||||
|
PlanId = plan.StripeSeatPlanId,
|
||||||
|
Quantity = additionalSeats,
|
||||||
|
Prorate = prorate,
|
||||||
|
SubscriptionId = sub.Id
|
||||||
|
});
|
||||||
}
|
}
|
||||||
else if(additionalSeats > 0 && seatItem != null)
|
else if(additionalSeats > 0 && seatItem != null)
|
||||||
{
|
{
|
||||||
await subscriptionItemService.UpdateAsync(seatItem.Id, new StripeSubscriptionItemUpdateOptions
|
subItemOptions.Add(new InvoiceSubscriptionItemOptions
|
||||||
{
|
{
|
||||||
|
Id = seatItem.Id,
|
||||||
PlanId = plan.StripeSeatPlanId,
|
PlanId = plan.StripeSeatPlanId,
|
||||||
Quantity = additionalSeats,
|
Quantity = additionalSeats,
|
||||||
Prorate = true
|
|
||||||
});
|
});
|
||||||
|
subUpdateAction = (prorate) => subscriptionItemService.UpdateAsync(seatItem.Id,
|
||||||
|
new SubscriptionItemUpdateOptions
|
||||||
|
{
|
||||||
|
PlanId = plan.StripeSeatPlanId,
|
||||||
|
Quantity = additionalSeats,
|
||||||
|
Prorate = prorate
|
||||||
|
});
|
||||||
}
|
}
|
||||||
else if(seatItem != null && additionalSeats == 0)
|
else if(seatItem != null && additionalSeats == 0)
|
||||||
{
|
{
|
||||||
await subscriptionItemService.DeleteAsync(seatItem.Id);
|
subItemOptions.Add(new InvoiceSubscriptionItemOptions
|
||||||
|
{
|
||||||
|
Id = seatItem.Id,
|
||||||
|
Deleted = true
|
||||||
|
});
|
||||||
|
subUpdateAction = (prorate) => subscriptionItemService.DeleteAsync(seatItem.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var invoicedNow = false;
|
||||||
if(additionalSeats > 0)
|
if(additionalSeats > 0)
|
||||||
{
|
{
|
||||||
await _stripePaymentService.PreviewUpcomingInvoiceAndPayAsync(organization, plan.StripeSeatPlanId, 500);
|
invoicedNow = await (_paymentService as StripePaymentService).PreviewUpcomingInvoiceAndPayAsync(
|
||||||
|
organization, plan.StripeSeatPlanId, subItemOptions, 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await subUpdateAction(!invoicedNow);
|
||||||
organization.Seats = (short?)newSeatTotal;
|
organization.Seats = (short?)newSeatTotal;
|
||||||
await ReplaceAndUpdateCache(organization);
|
await ReplaceAndUpdateCache(organization);
|
||||||
}
|
}
|
||||||
@ -389,7 +435,7 @@ namespace Bit.Core.Services
|
|||||||
}
|
}
|
||||||
|
|
||||||
var bankService = new BankAccountService();
|
var bankService = new BankAccountService();
|
||||||
var customerService = new StripeCustomerService();
|
var customerService = new CustomerService();
|
||||||
var customer = await customerService.GetAsync(organization.GatewayCustomerId);
|
var customer = await customerService.GetAsync(organization.GatewayCustomerId);
|
||||||
if(customer == null)
|
if(customer == null)
|
||||||
{
|
{
|
||||||
@ -397,7 +443,7 @@ namespace Bit.Core.Services
|
|||||||
}
|
}
|
||||||
|
|
||||||
var bankAccount = customer.Sources
|
var bankAccount = customer.Sources
|
||||||
.FirstOrDefault(s => s.BankAccount != null && s.BankAccount.Status != "verified")?.BankAccount;
|
.FirstOrDefault(s => s is BankAccount && ((BankAccount)s).Status != "verified") as BankAccount;
|
||||||
if(bankAccount == null)
|
if(bankAccount == null)
|
||||||
{
|
{
|
||||||
throw new GatewayException("Cannot find an unverified bank account.");
|
throw new GatewayException("Cannot find an unverified bank account.");
|
||||||
@ -406,7 +452,7 @@ namespace Bit.Core.Services
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var result = await bankService.VerifyAsync(organization.GatewayCustomerId, bankAccount.Id,
|
var result = await bankService.VerifyAsync(organization.GatewayCustomerId, bankAccount.Id,
|
||||||
new BankAccountVerifyOptions { AmountOne = amount1, AmountTwo = amount2 });
|
new BankAccountVerifyOptions { Amounts = new List<long> { amount1, amount2 } });
|
||||||
if(result.Status != "verified")
|
if(result.Status != "verified")
|
||||||
{
|
{
|
||||||
throw new GatewayException("Unable to verify account.");
|
throw new GatewayException("Unable to verify account.");
|
||||||
@ -431,6 +477,11 @@ namespace Bit.Core.Services
|
|||||||
throw new BadRequestException("Plan does not allow additional storage.");
|
throw new BadRequestException("Plan does not allow additional storage.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(signup.AdditionalStorageGb < 0)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("You can't subtract storage!");
|
||||||
|
}
|
||||||
|
|
||||||
if(!plan.CanBuyPremiumAccessAddon && signup.PremiumAccessAddon)
|
if(!plan.CanBuyPremiumAccessAddon && signup.PremiumAccessAddon)
|
||||||
{
|
{
|
||||||
throw new BadRequestException("This plan does not allow you to buy the premium access addon.");
|
throw new BadRequestException("This plan does not allow you to buy the premium access addon.");
|
||||||
@ -441,6 +492,11 @@ namespace Bit.Core.Services
|
|||||||
throw new BadRequestException("You do not have any seats!");
|
throw new BadRequestException("You do not have any seats!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(signup.AdditionalSeats < 0)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("You can't subtract seats!");
|
||||||
|
}
|
||||||
|
|
||||||
if(!plan.CanBuyAdditionalSeats && signup.AdditionalSeats > 0)
|
if(!plan.CanBuyAdditionalSeats && signup.AdditionalSeats > 0)
|
||||||
{
|
{
|
||||||
throw new BadRequestException("Plan does not allow additional users.");
|
throw new BadRequestException("Plan does not allow additional users.");
|
||||||
@ -453,96 +509,10 @@ namespace Bit.Core.Services
|
|||||||
$"{plan.MaxAdditionalSeats.GetValueOrDefault(0)} additional users.");
|
$"{plan.MaxAdditionalSeats.GetValueOrDefault(0)} additional users.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var customerService = new StripeCustomerService();
|
|
||||||
var subscriptionService = new StripeSubscriptionService();
|
|
||||||
StripeCustomer customer = null;
|
|
||||||
StripeSubscription subscription = null;
|
|
||||||
|
|
||||||
// Pre-generate the org id so that we can save it with the Stripe subscription..
|
|
||||||
var newOrgId = CoreHelpers.GenerateComb();
|
|
||||||
|
|
||||||
if(plan.Type == PlanType.Free)
|
|
||||||
{
|
|
||||||
var adminCount =
|
|
||||||
await _organizationUserRepository.GetCountByFreeOrganizationAdminUserAsync(signup.Owner.Id);
|
|
||||||
if(adminCount > 0)
|
|
||||||
{
|
|
||||||
throw new BadRequestException("You can only be an admin of one free organization.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
customer = await customerService.CreateAsync(new StripeCustomerCreateOptions
|
|
||||||
{
|
|
||||||
Description = signup.BusinessName,
|
|
||||||
Email = signup.BillingEmail,
|
|
||||||
SourceToken = signup.PaymentToken
|
|
||||||
});
|
|
||||||
|
|
||||||
var subCreateOptions = new StripeSubscriptionCreateOptions
|
|
||||||
{
|
|
||||||
CustomerId = customer.Id,
|
|
||||||
TrialPeriodDays = plan.TrialPeriodDays,
|
|
||||||
Items = new List<StripeSubscriptionItemOption>(),
|
|
||||||
Metadata = new Dictionary<string, string> {
|
|
||||||
{ "organizationId", newOrgId.ToString() }
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if(plan.StripePlanId != null)
|
|
||||||
{
|
|
||||||
subCreateOptions.Items.Add(new StripeSubscriptionItemOption
|
|
||||||
{
|
|
||||||
PlanId = plan.StripePlanId,
|
|
||||||
Quantity = 1
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if(signup.AdditionalSeats > 0 && plan.StripeSeatPlanId != null)
|
|
||||||
{
|
|
||||||
subCreateOptions.Items.Add(new StripeSubscriptionItemOption
|
|
||||||
{
|
|
||||||
PlanId = plan.StripeSeatPlanId,
|
|
||||||
Quantity = signup.AdditionalSeats
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if(signup.AdditionalStorageGb > 0)
|
|
||||||
{
|
|
||||||
subCreateOptions.Items.Add(new StripeSubscriptionItemOption
|
|
||||||
{
|
|
||||||
PlanId = plan.StripeStoragePlanId,
|
|
||||||
Quantity = signup.AdditionalStorageGb
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if(signup.PremiumAccessAddon && plan.StripePremiumAccessPlanId != null)
|
|
||||||
{
|
|
||||||
subCreateOptions.Items.Add(new StripeSubscriptionItemOption
|
|
||||||
{
|
|
||||||
PlanId = plan.StripePremiumAccessPlanId,
|
|
||||||
Quantity = 1
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
subscription = await subscriptionService.CreateAsync(subCreateOptions);
|
|
||||||
}
|
|
||||||
catch(StripeException)
|
|
||||||
{
|
|
||||||
if(customer != null)
|
|
||||||
{
|
|
||||||
await customerService.DeleteAsync(customer.Id);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var organization = new Organization
|
var organization = new Organization
|
||||||
{
|
{
|
||||||
Id = newOrgId,
|
// Pre-generate the org id so that we can save it with the Stripe subscription..
|
||||||
|
Id = CoreHelpers.GenerateComb(),
|
||||||
Name = signup.Name,
|
Name = signup.Name,
|
||||||
BillingEmail = signup.BillingEmail,
|
BillingEmail = signup.BillingEmail,
|
||||||
BusinessName = signup.BusinessName,
|
BusinessName = signup.BusinessName,
|
||||||
@ -560,16 +530,43 @@ namespace Bit.Core.Services
|
|||||||
SelfHost = plan.SelfHost,
|
SelfHost = plan.SelfHost,
|
||||||
UsersGetPremium = plan.UsersGetPremium || signup.PremiumAccessAddon,
|
UsersGetPremium = plan.UsersGetPremium || signup.PremiumAccessAddon,
|
||||||
Plan = plan.Name,
|
Plan = plan.Name,
|
||||||
Gateway = plan.Type == PlanType.Free ? null : (GatewayType?)GatewayType.Stripe,
|
Gateway = null,
|
||||||
GatewayCustomerId = customer?.Id,
|
|
||||||
GatewaySubscriptionId = subscription?.Id,
|
|
||||||
Enabled = true,
|
Enabled = true,
|
||||||
ExpirationDate = subscription?.CurrentPeriodEnd,
|
|
||||||
LicenseKey = CoreHelpers.SecureRandomString(20),
|
LicenseKey = CoreHelpers.SecureRandomString(20),
|
||||||
CreationDate = DateTime.UtcNow,
|
CreationDate = DateTime.UtcNow,
|
||||||
RevisionDate = DateTime.UtcNow
|
RevisionDate = DateTime.UtcNow
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if(plan.Type == PlanType.Free)
|
||||||
|
{
|
||||||
|
var adminCount =
|
||||||
|
await _organizationUserRepository.GetCountByFreeOrganizationAdminUserAsync(signup.Owner.Id);
|
||||||
|
if(adminCount > 0)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("You can only be an admin of one free organization.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
PaymentMethodType paymentMethodType;
|
||||||
|
if(signup.PaymentToken.StartsWith("btok_"))
|
||||||
|
{
|
||||||
|
paymentMethodType = PaymentMethodType.BankAccount;
|
||||||
|
}
|
||||||
|
else if(signup.PaymentToken.StartsWith("tok_"))
|
||||||
|
{
|
||||||
|
paymentMethodType = PaymentMethodType.Card;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
paymentMethodType = PaymentMethodType.PayPal;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _paymentService.PurchaseOrganizationAsync(organization, paymentMethodType,
|
||||||
|
signup.PaymentToken, plan, signup.AdditionalStorageGb, signup.AdditionalSeats,
|
||||||
|
signup.PremiumAccessAddon);
|
||||||
|
}
|
||||||
|
|
||||||
return await SignUpAsync(organization, signup.Owner.Id, signup.OwnerKey, signup.CollectionName, true);
|
return await SignUpAsync(organization, signup.Owner.Id, signup.OwnerKey, signup.CollectionName, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -630,7 +627,8 @@ namespace Bit.Core.Services
|
|||||||
|
|
||||||
var dir = $"{_globalSettings.LicenseDirectory}/organization";
|
var dir = $"{_globalSettings.LicenseDirectory}/organization";
|
||||||
Directory.CreateDirectory(dir);
|
Directory.CreateDirectory(dir);
|
||||||
File.WriteAllText($"{dir}/{organization.Id}.json", JsonConvert.SerializeObject(license, Formatting.Indented));
|
System.IO.File.WriteAllText($"{dir}/{organization.Id}.json",
|
||||||
|
JsonConvert.SerializeObject(license, Formatting.Indented));
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -679,7 +677,7 @@ namespace Bit.Core.Services
|
|||||||
{
|
{
|
||||||
if(withPayment)
|
if(withPayment)
|
||||||
{
|
{
|
||||||
await _stripePaymentService.CancelAndRecoverChargesAsync(organization);
|
await _paymentService.CancelAndRecoverChargesAsync(organization);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(organization.Id != default(Guid))
|
if(organization.Id != default(Guid))
|
||||||
@ -756,7 +754,8 @@ namespace Bit.Core.Services
|
|||||||
|
|
||||||
var dir = $"{_globalSettings.LicenseDirectory}/organization";
|
var dir = $"{_globalSettings.LicenseDirectory}/organization";
|
||||||
Directory.CreateDirectory(dir);
|
Directory.CreateDirectory(dir);
|
||||||
File.WriteAllText($"{dir}/{organization.Id}.json", JsonConvert.SerializeObject(license, Formatting.Indented));
|
System.IO.File.WriteAllText($"{dir}/{organization.Id}.json",
|
||||||
|
JsonConvert.SerializeObject(license, Formatting.Indented));
|
||||||
|
|
||||||
organization.Name = license.Name;
|
organization.Name = license.Name;
|
||||||
organization.BusinessName = license.BusinessName;
|
organization.BusinessName = license.BusinessName;
|
||||||
@ -787,7 +786,7 @@ namespace Bit.Core.Services
|
|||||||
{
|
{
|
||||||
var eop = !organization.ExpirationDate.HasValue ||
|
var eop = !organization.ExpirationDate.HasValue ||
|
||||||
organization.ExpirationDate.Value >= DateTime.UtcNow;
|
organization.ExpirationDate.Value >= DateTime.UtcNow;
|
||||||
await _stripePaymentService.CancelSubscriptionAsync(organization, eop);
|
await _paymentService.CancelSubscriptionAsync(organization, eop);
|
||||||
}
|
}
|
||||||
catch(GatewayException) { }
|
catch(GatewayException) { }
|
||||||
}
|
}
|
||||||
@ -842,8 +841,8 @@ namespace Bit.Core.Services
|
|||||||
|
|
||||||
if(updateBilling && !string.IsNullOrWhiteSpace(organization.GatewayCustomerId))
|
if(updateBilling && !string.IsNullOrWhiteSpace(organization.GatewayCustomerId))
|
||||||
{
|
{
|
||||||
var customerService = new StripeCustomerService();
|
var customerService = new CustomerService();
|
||||||
await customerService.UpdateAsync(organization.GatewayCustomerId, new StripeCustomerUpdateOptions
|
await customerService.UpdateAsync(organization.GatewayCustomerId, new CustomerUpdateOptions
|
||||||
{
|
{
|
||||||
Email = organization.BillingEmail,
|
Email = organization.BillingEmail,
|
||||||
Description = organization.BusinessName
|
Description = organization.BusinessName
|
||||||
@ -1207,9 +1206,8 @@ namespace Bit.Core.Services
|
|||||||
{
|
{
|
||||||
throw new BadRequestException("Invalid installation id");
|
throw new BadRequestException("Invalid installation id");
|
||||||
}
|
}
|
||||||
|
|
||||||
var paymentService = new StripePaymentService();
|
var billingInfo = await _paymentService.GetBillingAsync(organization);
|
||||||
var billingInfo = await paymentService.GetBillingAsync(organization);
|
|
||||||
return new OrganizationLicense(organization, billingInfo, installationId, _licensingService);
|
return new OrganizationLicense(organization, billingInfo, installationId, _licensingService);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -42,6 +42,7 @@ namespace Bit.Core.Services
|
|||||||
private readonly ILicensingService _licenseService;
|
private readonly ILicensingService _licenseService;
|
||||||
private readonly IEventService _eventService;
|
private readonly IEventService _eventService;
|
||||||
private readonly IApplicationCacheService _applicationCacheService;
|
private readonly IApplicationCacheService _applicationCacheService;
|
||||||
|
private readonly IPaymentService _paymentService;
|
||||||
private readonly IDataProtector _organizationServiceDataProtector;
|
private readonly IDataProtector _organizationServiceDataProtector;
|
||||||
private readonly CurrentContext _currentContext;
|
private readonly CurrentContext _currentContext;
|
||||||
private readonly GlobalSettings _globalSettings;
|
private readonly GlobalSettings _globalSettings;
|
||||||
@ -67,6 +68,7 @@ namespace Bit.Core.Services
|
|||||||
IEventService eventService,
|
IEventService eventService,
|
||||||
IApplicationCacheService applicationCacheService,
|
IApplicationCacheService applicationCacheService,
|
||||||
IDataProtectionProvider dataProtectionProvider,
|
IDataProtectionProvider dataProtectionProvider,
|
||||||
|
IPaymentService paymentService,
|
||||||
CurrentContext currentContext,
|
CurrentContext currentContext,
|
||||||
GlobalSettings globalSettings)
|
GlobalSettings globalSettings)
|
||||||
: base(
|
: base(
|
||||||
@ -94,6 +96,7 @@ namespace Bit.Core.Services
|
|||||||
_licenseService = licenseService;
|
_licenseService = licenseService;
|
||||||
_eventService = eventService;
|
_eventService = eventService;
|
||||||
_applicationCacheService = applicationCacheService;
|
_applicationCacheService = applicationCacheService;
|
||||||
|
_paymentService = paymentService;
|
||||||
_organizationServiceDataProtector = dataProtectionProvider.CreateProtector(
|
_organizationServiceDataProtector = dataProtectionProvider.CreateProtector(
|
||||||
"OrganizationServiceDataProtector");
|
"OrganizationServiceDataProtector");
|
||||||
_currentContext = currentContext;
|
_currentContext = currentContext;
|
||||||
@ -682,6 +685,11 @@ namespace Bit.Core.Services
|
|||||||
throw new BadRequestException("Already a premium user.");
|
throw new BadRequestException("Already a premium user.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(additionalStorageGb < 0)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("You can't subtract storage!");
|
||||||
|
}
|
||||||
|
|
||||||
IPaymentService paymentService = null;
|
IPaymentService paymentService = null;
|
||||||
if(_globalSettings.SelfHosted)
|
if(_globalSettings.SelfHosted)
|
||||||
{
|
{
|
||||||
@ -706,16 +714,14 @@ namespace Bit.Core.Services
|
|||||||
throw new BadRequestException("Invalid token.");
|
throw new BadRequestException("Invalid token.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if(paymentToken.StartsWith("tok_"))
|
var paymentMethodType = PaymentMethodType.Card;
|
||||||
|
if(!paymentToken.StartsWith("tok_"))
|
||||||
{
|
{
|
||||||
paymentService = new StripePaymentService();
|
paymentMethodType = PaymentMethodType.PayPal;
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
paymentService = new BraintreePaymentService(_globalSettings);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await paymentService.PurchasePremiumAsync(user, paymentToken, additionalStorageGb);
|
await _paymentService.PurchasePremiumAsync(user, paymentMethodType,
|
||||||
|
paymentToken, additionalStorageGb);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -789,9 +795,8 @@ namespace Bit.Core.Services
|
|||||||
{
|
{
|
||||||
throw new BadRequestException("Not a premium user.");
|
throw new BadRequestException("Not a premium user.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var paymentService = user.GetPaymentService(_globalSettings);
|
await BillingHelpers.AdjustStorageAsync(_paymentService, user, storageAdjustmentGb, StoragePlanId);
|
||||||
await BillingHelpers.AdjustStorageAsync(paymentService, user, storageAdjustmentGb, StoragePlanId);
|
|
||||||
await SaveUserAsync(user);
|
await SaveUserAsync(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -802,17 +807,17 @@ namespace Bit.Core.Services
|
|||||||
throw new BadRequestException("Invalid token.");
|
throw new BadRequestException("Invalid token.");
|
||||||
}
|
}
|
||||||
|
|
||||||
IPaymentService paymentService = null;
|
PaymentMethodType paymentMethodType;
|
||||||
if(paymentToken.StartsWith("tok_"))
|
if(paymentToken.StartsWith("tok_"))
|
||||||
{
|
{
|
||||||
paymentService = new StripePaymentService();
|
paymentMethodType = PaymentMethodType.Card;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
paymentService = new BraintreePaymentService(_globalSettings);
|
paymentMethodType = PaymentMethodType.PayPal;
|
||||||
}
|
}
|
||||||
|
|
||||||
var updated = await paymentService.UpdatePaymentMethodAsync(user, paymentToken);
|
var updated = await _paymentService.UpdatePaymentMethodAsync(user, paymentMethodType, paymentToken);
|
||||||
if(updated)
|
if(updated)
|
||||||
{
|
{
|
||||||
await SaveUserAsync(user);
|
await SaveUserAsync(user);
|
||||||
@ -821,20 +826,18 @@ namespace Bit.Core.Services
|
|||||||
|
|
||||||
public async Task CancelPremiumAsync(User user, bool? endOfPeriod = null)
|
public async Task CancelPremiumAsync(User user, bool? endOfPeriod = null)
|
||||||
{
|
{
|
||||||
var paymentService = user.GetPaymentService(_globalSettings);
|
|
||||||
var eop = endOfPeriod.GetValueOrDefault(true);
|
var eop = endOfPeriod.GetValueOrDefault(true);
|
||||||
if(!endOfPeriod.HasValue && user.PremiumExpirationDate.HasValue &&
|
if(!endOfPeriod.HasValue && user.PremiumExpirationDate.HasValue &&
|
||||||
user.PremiumExpirationDate.Value < DateTime.UtcNow)
|
user.PremiumExpirationDate.Value < DateTime.UtcNow)
|
||||||
{
|
{
|
||||||
eop = false;
|
eop = false;
|
||||||
}
|
}
|
||||||
await paymentService.CancelSubscriptionAsync(user, eop);
|
await _paymentService.CancelSubscriptionAsync(user, eop);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ReinstatePremiumAsync(User user)
|
public async Task ReinstatePremiumAsync(User user)
|
||||||
{
|
{
|
||||||
var paymentService = user.GetPaymentService(_globalSettings);
|
await _paymentService.ReinstateSubscriptionAsync(user);
|
||||||
await paymentService.ReinstateSubscriptionAsync(user);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task DisablePremiumAsync(Guid userId, DateTime? expirationDate)
|
public async Task DisablePremiumAsync(Guid userId, DateTime? expirationDate)
|
||||||
@ -874,8 +877,7 @@ namespace Bit.Core.Services
|
|||||||
|
|
||||||
if(billingInfo == null && user.Gateway != null)
|
if(billingInfo == null && user.Gateway != null)
|
||||||
{
|
{
|
||||||
var paymentService = user.GetPaymentService(_globalSettings);
|
billingInfo = await _paymentService.GetBillingAsync(user);
|
||||||
billingInfo = await paymentService.GetBillingAsync(user);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return billingInfo == null ? new UserLicense(user, _licenseService) :
|
return billingInfo == null ? new UserLicense(user, _licenseService) :
|
||||||
|
@ -52,6 +52,7 @@ namespace Bit.Core.Utilities
|
|||||||
services.AddSingleton<IU2fRepository, SqlServerRepos.U2fRepository>();
|
services.AddSingleton<IU2fRepository, SqlServerRepos.U2fRepository>();
|
||||||
services.AddSingleton<IInstallationRepository, SqlServerRepos.InstallationRepository>();
|
services.AddSingleton<IInstallationRepository, SqlServerRepos.InstallationRepository>();
|
||||||
services.AddSingleton<IMaintenanceRepository, SqlServerRepos.MaintenanceRepository>();
|
services.AddSingleton<IMaintenanceRepository, SqlServerRepos.MaintenanceRepository>();
|
||||||
|
services.AddSingleton<ITransactionRepository, SqlServerRepos.TransactionRepository>();
|
||||||
}
|
}
|
||||||
|
|
||||||
if(globalSettings.SelfHosted)
|
if(globalSettings.SelfHosted)
|
||||||
@ -77,6 +78,7 @@ namespace Bit.Core.Utilities
|
|||||||
|
|
||||||
public static void AddDefaultServices(this IServiceCollection services, GlobalSettings globalSettings)
|
public static void AddDefaultServices(this IServiceCollection services, GlobalSettings globalSettings)
|
||||||
{
|
{
|
||||||
|
services.AddSingleton<IPaymentService, StripePaymentService>();
|
||||||
services.AddSingleton<IMailService, HandlebarsMailService>();
|
services.AddSingleton<IMailService, HandlebarsMailService>();
|
||||||
services.AddSingleton<ILicensingService, LicensingService>();
|
services.AddSingleton<ILicensingService, LicensingService>();
|
||||||
services.AddSingleton<IApplicationCacheService, InMemoryApplicationCacheService>();
|
services.AddSingleton<IApplicationCacheService, InMemoryApplicationCacheService>();
|
||||||
|
@ -26,7 +26,7 @@ namespace Bit.Core.Utilities
|
|||||||
GlobalDomains.Add(GlobalEquivalentDomainsType.United, new List<string> { "ua2go.com", "ual.com", "united.com", "unitedwifi.com" });
|
GlobalDomains.Add(GlobalEquivalentDomainsType.United, new List<string> { "ua2go.com", "ual.com", "united.com", "unitedwifi.com" });
|
||||||
GlobalDomains.Add(GlobalEquivalentDomainsType.Yahoo, new List<string> { "overture.com", "yahoo.com", "flickr.com" });
|
GlobalDomains.Add(GlobalEquivalentDomainsType.Yahoo, new List<string> { "overture.com", "yahoo.com", "flickr.com" });
|
||||||
GlobalDomains.Add(GlobalEquivalentDomainsType.Zonelabs, new List<string> { "zonealarm.com", "zonelabs.com" });
|
GlobalDomains.Add(GlobalEquivalentDomainsType.Zonelabs, new List<string> { "zonealarm.com", "zonelabs.com" });
|
||||||
GlobalDomains.Add(GlobalEquivalentDomainsType.Paypal, new List<string> { "paypal.com", "paypal-search.com" });
|
GlobalDomains.Add(GlobalEquivalentDomainsType.PayPal, new List<string> { "paypal.com", "paypal-search.com" });
|
||||||
GlobalDomains.Add(GlobalEquivalentDomainsType.Avon, new List<string> { "avon.com", "youravon.com" });
|
GlobalDomains.Add(GlobalEquivalentDomainsType.Avon, new List<string> { "avon.com", "youravon.com" });
|
||||||
GlobalDomains.Add(GlobalEquivalentDomainsType.Diapers, new List<string> { "diapers.com", "soap.com", "wag.com", "yoyo.com", "beautybar.com", "casa.com", "afterschool.com", "vine.com", "bookworm.com", "look.com", "vinemarket.com" });
|
GlobalDomains.Add(GlobalEquivalentDomainsType.Diapers, new List<string> { "diapers.com", "soap.com", "wag.com", "yoyo.com", "beautybar.com", "casa.com", "afterschool.com", "vine.com", "bookworm.com", "look.com", "vinemarket.com" });
|
||||||
GlobalDomains.Add(GlobalEquivalentDomainsType.Contacts, new List<string> { "1800contacts.com", "800contacts.com" });
|
GlobalDomains.Add(GlobalEquivalentDomainsType.Contacts, new List<string> { "1800contacts.com", "800contacts.com" });
|
||||||
|
@ -240,5 +240,14 @@
|
|||||||
<Build Include="dbo\Stored Procedures\Cipher_CreateWithCollections.sql" />
|
<Build Include="dbo\Stored Procedures\Cipher_CreateWithCollections.sql" />
|
||||||
<Build Include="dbo\Stored Procedures\Cipher_UpdateCollections.sql" />
|
<Build Include="dbo\Stored Procedures\Cipher_UpdateCollections.sql" />
|
||||||
<Build Include="dbo\Stored Procedures\CipherDetails_CreateWithCollections.sql" />
|
<Build Include="dbo\Stored Procedures\CipherDetails_CreateWithCollections.sql" />
|
||||||
|
<Build Include="dbo\Tables\Transaction.sql" />
|
||||||
|
<Build Include="dbo\Stored Procedures\Transaction_Create.sql" />
|
||||||
|
<Build Include="dbo\Stored Procedures\Transaction_DeleteById.sql" />
|
||||||
|
<Build Include="dbo\Stored Procedures\Transaction_ReadById.sql" />
|
||||||
|
<Build Include="dbo\Stored Procedures\Transaction_ReadByOrganizationId.sql" />
|
||||||
|
<Build Include="dbo\Stored Procedures\Transaction_Update.sql" />
|
||||||
|
<Build Include="dbo\Views\TransactionView.sql" />
|
||||||
|
<Build Include="dbo\Stored Procedures\Transaction_ReadByUserId.sql" />
|
||||||
|
<Build Include="dbo\Stored Procedures\Transaction_ReadByGatewayId.sql" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
48
src/Sql/dbo/Stored Procedures/Transaction_Create.sql
Normal file
48
src/Sql/dbo/Stored Procedures/Transaction_Create.sql
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
CREATE PROCEDURE [dbo].[Transaction_Create]
|
||||||
|
@Id UNIQUEIDENTIFIER,
|
||||||
|
@UserId UNIQUEIDENTIFIER,
|
||||||
|
@OrganizationId UNIQUEIDENTIFIER,
|
||||||
|
@Type TINYINT,
|
||||||
|
@Amount MONEY,
|
||||||
|
@Refunded BIT,
|
||||||
|
@RefundedAmount MONEY,
|
||||||
|
@Details NVARCHAR(100),
|
||||||
|
@PaymentMethodType TINYINT,
|
||||||
|
@Gateway TINYINT,
|
||||||
|
@GatewayId VARCHAR(50),
|
||||||
|
@CreationDate DATETIME2(7)
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
|
||||||
|
INSERT INTO [dbo].[Transaction]
|
||||||
|
(
|
||||||
|
[Id],
|
||||||
|
[UserId],
|
||||||
|
[OrganizationId],
|
||||||
|
[Type],
|
||||||
|
[Amount],
|
||||||
|
[Refunded],
|
||||||
|
[RefundedAmount],
|
||||||
|
[Details],
|
||||||
|
[PaymentMethodType],
|
||||||
|
[Gateway],
|
||||||
|
[GatewayId],
|
||||||
|
[CreationDate]
|
||||||
|
)
|
||||||
|
VALUES
|
||||||
|
(
|
||||||
|
@Id,
|
||||||
|
@UserId,
|
||||||
|
@OrganizationId,
|
||||||
|
@Type,
|
||||||
|
@Amount,
|
||||||
|
@Refunded,
|
||||||
|
@RefundedAmount,
|
||||||
|
@Details,
|
||||||
|
@PaymentMethodType,
|
||||||
|
@Gateway,
|
||||||
|
@GatewayId,
|
||||||
|
@CreationDate
|
||||||
|
)
|
||||||
|
END
|
12
src/Sql/dbo/Stored Procedures/Transaction_DeleteById.sql
Normal file
12
src/Sql/dbo/Stored Procedures/Transaction_DeleteById.sql
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
CREATE PROCEDURE [dbo].[Transaction_DeleteById]
|
||||||
|
@Id UNIQUEIDENTIFIER
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
|
||||||
|
DELETE
|
||||||
|
FROM
|
||||||
|
[dbo].[Transaction]
|
||||||
|
WHERE
|
||||||
|
[Id] = @Id
|
||||||
|
END
|
@ -0,0 +1,15 @@
|
|||||||
|
CREATE PROCEDURE [dbo].[Transaction_ReadByGatewayId]
|
||||||
|
@Gateway TINYINT,
|
||||||
|
@GatewayId VARCHAR(50)
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
*
|
||||||
|
FROM
|
||||||
|
[dbo].[TransactionView]
|
||||||
|
WHERE
|
||||||
|
[Gateway] = @Gateway
|
||||||
|
AND [GatewayId] = @GatewayId
|
||||||
|
END
|
13
src/Sql/dbo/Stored Procedures/Transaction_ReadById.sql
Normal file
13
src/Sql/dbo/Stored Procedures/Transaction_ReadById.sql
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
CREATE PROCEDURE [dbo].[Transaction_ReadById]
|
||||||
|
@Id UNIQUEIDENTIFIER
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
*
|
||||||
|
FROM
|
||||||
|
[dbo].[TransactionView]
|
||||||
|
WHERE
|
||||||
|
[Id] = @Id
|
||||||
|
END
|
@ -0,0 +1,14 @@
|
|||||||
|
CREATE PROCEDURE [dbo].[Transaction_ReadByOrganizationId]
|
||||||
|
@OrganizationId UNIQUEIDENTIFIER
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
*
|
||||||
|
FROM
|
||||||
|
[dbo].[TransactionView]
|
||||||
|
WHERE
|
||||||
|
[UserId] = NULL
|
||||||
|
AND [OrganizationId] = @OrganizationId
|
||||||
|
END
|
13
src/Sql/dbo/Stored Procedures/Transaction_ReadByUserId.sql
Normal file
13
src/Sql/dbo/Stored Procedures/Transaction_ReadByUserId.sql
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
CREATE PROCEDURE [dbo].[Transaction_ReadByUserId]
|
||||||
|
@UserId UNIQUEIDENTIFIER
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
*
|
||||||
|
FROM
|
||||||
|
[dbo].[TransactionView]
|
||||||
|
WHERE
|
||||||
|
[UserId] = @UserId
|
||||||
|
END
|
34
src/Sql/dbo/Stored Procedures/Transaction_Update.sql
Normal file
34
src/Sql/dbo/Stored Procedures/Transaction_Update.sql
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
CREATE PROCEDURE [dbo].[Transaction_Update]
|
||||||
|
@Id UNIQUEIDENTIFIER,
|
||||||
|
@UserId UNIQUEIDENTIFIER,
|
||||||
|
@OrganizationId UNIQUEIDENTIFIER,
|
||||||
|
@Type TINYINT,
|
||||||
|
@Amount MONEY,
|
||||||
|
@Refunded BIT,
|
||||||
|
@RefundedAmount MONEY,
|
||||||
|
@Details NVARCHAR(100),
|
||||||
|
@PaymentMethodType TINYINT,
|
||||||
|
@Gateway TINYINT,
|
||||||
|
@GatewayId VARCHAR(50),
|
||||||
|
@CreationDate DATETIME2(7)
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
|
||||||
|
UPDATE
|
||||||
|
[dbo].[Transaction]
|
||||||
|
SET
|
||||||
|
[UserId] = @UserId,
|
||||||
|
[OrganizationId] = @OrganizationId,
|
||||||
|
[Type] = @Type,
|
||||||
|
[Amount] = @Amount,
|
||||||
|
[Refunded] = @Refunded,
|
||||||
|
[RefundedAmount] = @RefundedAmount,
|
||||||
|
[Details] = @Details,
|
||||||
|
[PaymentMethodType] = @PaymentMethodType,
|
||||||
|
[Gateway] = @Gateway,
|
||||||
|
[GatewayId] = @GatewayId,
|
||||||
|
[CreationDate] = @CreationDate
|
||||||
|
WHERE
|
||||||
|
[Id] = @Id
|
||||||
|
END
|
28
src/Sql/dbo/Tables/Transaction.sql
Normal file
28
src/Sql/dbo/Tables/Transaction.sql
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
CREATE TABLE [dbo].[Transaction] (
|
||||||
|
[Id] UNIQUEIDENTIFIER NOT NULL,
|
||||||
|
[UserId] UNIQUEIDENTIFIER NULL,
|
||||||
|
[OrganizationId] UNIQUEIDENTIFIER NULL,
|
||||||
|
[Type] TINYINT NOT NULL,
|
||||||
|
[Amount] MONEY NOT NULL,
|
||||||
|
[Refunded] BIT NULL,
|
||||||
|
[RefundedAmount] MONEY NULL,
|
||||||
|
[Details] NVARCHAR(100) NULL,
|
||||||
|
[PaymentMethodType] TINYINT NULL,
|
||||||
|
[Gateway] TINYINT NULL,
|
||||||
|
[GatewayId] VARCHAR(50) NULL,
|
||||||
|
[CreationDate] DATETIME2 (7) NOT NULL,
|
||||||
|
CONSTRAINT [PK_Transaction] PRIMARY KEY CLUSTERED ([Id] ASC),
|
||||||
|
CONSTRAINT [FK_Transaction_User] FOREIGN KEY ([UserId]) REFERENCES [dbo].[User] ([Id]) ON DELETE CASCADE,
|
||||||
|
CONSTRAINT [FK_Transaction_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id]) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
GO
|
||||||
|
CREATE UNIQUE NONCLUSTERED INDEX [IX_Transaction_Gateway_GatewayId]
|
||||||
|
ON [dbo].[Transaction]([Gateway] ASC, [GatewayId] ASC);
|
||||||
|
|
||||||
|
|
||||||
|
GO
|
||||||
|
CREATE NONCLUSTERED INDEX [IX_Transaction_UserId_OrganizationId_CreationDate]
|
||||||
|
ON [dbo].[Transaction]([UserId] ASC, [OrganizationId] ASC, [CreationDate] ASC);
|
||||||
|
|
6
src/Sql/dbo/Views/TransactionView.sql
Normal file
6
src/Sql/dbo/Views/TransactionView.sql
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
CREATE VIEW [dbo].[TransactionView]
|
||||||
|
AS
|
||||||
|
SELECT
|
||||||
|
*
|
||||||
|
FROM
|
||||||
|
[dbo].[Transaction]
|
247
util/Setup/DbScripts/2019-01-31_00_Transactions.sql
Normal file
247
util/Setup/DbScripts/2019-01-31_00_Transactions.sql
Normal file
@ -0,0 +1,247 @@
|
|||||||
|
IF OBJECT_ID('[dbo].[Transaction]') IS NULL
|
||||||
|
BEGIN
|
||||||
|
CREATE TABLE [dbo].[Transaction] (
|
||||||
|
[Id] UNIQUEIDENTIFIER NOT NULL,
|
||||||
|
[UserId] UNIQUEIDENTIFIER NULL,
|
||||||
|
[OrganizationId] UNIQUEIDENTIFIER NULL,
|
||||||
|
[Type] TINYINT NOT NULL,
|
||||||
|
[Amount] MONEY NOT NULL,
|
||||||
|
[Refunded] BIT NULL,
|
||||||
|
[RefundedAmount] MONEY NULL,
|
||||||
|
[Details] NVARCHAR(100) NULL,
|
||||||
|
[PaymentMethodType] TINYINT NULL,
|
||||||
|
[Gateway] TINYINT NULL,
|
||||||
|
[GatewayId] VARCHAR(50) NULL,
|
||||||
|
[CreationDate] DATETIME2 (7) NOT NULL,
|
||||||
|
CONSTRAINT [PK_Transaction] PRIMARY KEY CLUSTERED ([Id] ASC),
|
||||||
|
CONSTRAINT [FK_Transaction_User] FOREIGN KEY ([UserId]) REFERENCES [dbo].[User] ([Id]) ON DELETE CASCADE,
|
||||||
|
CONSTRAINT [FK_Transaction_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id]) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE NONCLUSTERED INDEX [IX_Transaction_Gateway_GatewayId]
|
||||||
|
ON [dbo].[Transaction]([Gateway] ASC, [GatewayId] ASC);
|
||||||
|
|
||||||
|
|
||||||
|
CREATE NONCLUSTERED INDEX [IX_Transaction_UserId_OrganizationId_CreationDate]
|
||||||
|
ON [dbo].[Transaction]([UserId] ASC, [OrganizationId] ASC, [CreationDate] ASC);
|
||||||
|
END
|
||||||
|
GO
|
||||||
|
|
||||||
|
IF EXISTS(SELECT * FROM sys.views WHERE [Name] = 'TransactionView')
|
||||||
|
BEGIN
|
||||||
|
DROP VIEW [dbo].[TransactionView]
|
||||||
|
END
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE VIEW [dbo].[TransactionView]
|
||||||
|
AS
|
||||||
|
SELECT
|
||||||
|
*
|
||||||
|
FROM
|
||||||
|
[dbo].[Transaction]
|
||||||
|
GO
|
||||||
|
|
||||||
|
IF OBJECT_ID('[dbo].[Transaction_Create]') IS NOT NULL
|
||||||
|
BEGIN
|
||||||
|
DROP PROCEDURE [dbo].[Transaction_Create]
|
||||||
|
END
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE PROCEDURE [dbo].[Transaction_Create]
|
||||||
|
@Id UNIQUEIDENTIFIER,
|
||||||
|
@UserId UNIQUEIDENTIFIER,
|
||||||
|
@OrganizationId UNIQUEIDENTIFIER,
|
||||||
|
@Type TINYINT,
|
||||||
|
@Amount MONEY,
|
||||||
|
@Refunded BIT,
|
||||||
|
@RefundedAmount MONEY,
|
||||||
|
@Details NVARCHAR(100),
|
||||||
|
@PaymentMethodType TINYINT,
|
||||||
|
@Gateway TINYINT,
|
||||||
|
@GatewayId VARCHAR(50),
|
||||||
|
@CreationDate DATETIME2(7)
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
|
||||||
|
INSERT INTO [dbo].[Transaction]
|
||||||
|
(
|
||||||
|
[Id],
|
||||||
|
[UserId],
|
||||||
|
[OrganizationId],
|
||||||
|
[Type],
|
||||||
|
[Amount],
|
||||||
|
[Refunded],
|
||||||
|
[RefundedAmount],
|
||||||
|
[Details],
|
||||||
|
[PaymentMethodType],
|
||||||
|
[Gateway],
|
||||||
|
[GatewayId],
|
||||||
|
[CreationDate]
|
||||||
|
)
|
||||||
|
VALUES
|
||||||
|
(
|
||||||
|
@Id,
|
||||||
|
@UserId,
|
||||||
|
@OrganizationId,
|
||||||
|
@Type,
|
||||||
|
@Amount,
|
||||||
|
@Refunded,
|
||||||
|
@RefundedAmount,
|
||||||
|
@Details,
|
||||||
|
@PaymentMethodType,
|
||||||
|
@Gateway,
|
||||||
|
@GatewayId,
|
||||||
|
@CreationDate
|
||||||
|
)
|
||||||
|
END
|
||||||
|
GO
|
||||||
|
|
||||||
|
IF OBJECT_ID('[dbo].[Transaction_DeleteById]') IS NOT NULL
|
||||||
|
BEGIN
|
||||||
|
DROP PROCEDURE [dbo].[Transaction_DeleteById]
|
||||||
|
END
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE PROCEDURE [dbo].[Transaction_DeleteById]
|
||||||
|
@Id UNIQUEIDENTIFIER
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
|
||||||
|
DELETE
|
||||||
|
FROM
|
||||||
|
[dbo].[Transaction]
|
||||||
|
WHERE
|
||||||
|
[Id] = @Id
|
||||||
|
END
|
||||||
|
GO
|
||||||
|
|
||||||
|
IF OBJECT_ID('[dbo].[Transaction_ReadById]') IS NOT NULL
|
||||||
|
BEGIN
|
||||||
|
DROP PROCEDURE [dbo].[Transaction_ReadById]
|
||||||
|
END
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE PROCEDURE [dbo].[Transaction_ReadById]
|
||||||
|
@Id UNIQUEIDENTIFIER
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
*
|
||||||
|
FROM
|
||||||
|
[dbo].[TransactionView]
|
||||||
|
WHERE
|
||||||
|
[Id] = @Id
|
||||||
|
END
|
||||||
|
GO
|
||||||
|
|
||||||
|
IF OBJECT_ID('[dbo].[Transaction_ReadByOrganizationId]') IS NOT NULL
|
||||||
|
BEGIN
|
||||||
|
DROP PROCEDURE [dbo].[Transaction_ReadByOrganizationId]
|
||||||
|
END
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE PROCEDURE [dbo].[Transaction_ReadByOrganizationId]
|
||||||
|
@OrganizationId UNIQUEIDENTIFIER
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
*
|
||||||
|
FROM
|
||||||
|
[dbo].[TransactionView]
|
||||||
|
WHERE
|
||||||
|
[UserId] = NULL
|
||||||
|
AND [OrganizationId] = @OrganizationId
|
||||||
|
END
|
||||||
|
GO
|
||||||
|
|
||||||
|
IF OBJECT_ID('[dbo].[Transaction_ReadByUserId]') IS NOT NULL
|
||||||
|
BEGIN
|
||||||
|
DROP PROCEDURE [dbo].[Transaction_ReadByUserId]
|
||||||
|
END
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE PROCEDURE [dbo].[Transaction_ReadByUserId]
|
||||||
|
@UserId UNIQUEIDENTIFIER
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
*
|
||||||
|
FROM
|
||||||
|
[dbo].[TransactionView]
|
||||||
|
WHERE
|
||||||
|
[UserId] = @UserId
|
||||||
|
END
|
||||||
|
GO
|
||||||
|
|
||||||
|
IF OBJECT_ID('[dbo].[Transaction_Update]') IS NOT NULL
|
||||||
|
BEGIN
|
||||||
|
DROP PROCEDURE [dbo].[Transaction_Update]
|
||||||
|
END
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE PROCEDURE [dbo].[Transaction_Update]
|
||||||
|
@Id UNIQUEIDENTIFIER,
|
||||||
|
@UserId UNIQUEIDENTIFIER,
|
||||||
|
@OrganizationId UNIQUEIDENTIFIER,
|
||||||
|
@Type TINYINT,
|
||||||
|
@Amount MONEY,
|
||||||
|
@Refunded BIT,
|
||||||
|
@RefundedAmount MONEY,
|
||||||
|
@Details NVARCHAR(100),
|
||||||
|
@PaymentMethodType TINYINT,
|
||||||
|
@Gateway TINYINT,
|
||||||
|
@GatewayId VARCHAR(50),
|
||||||
|
@CreationDate DATETIME2(7)
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
|
||||||
|
UPDATE
|
||||||
|
[dbo].[Transaction]
|
||||||
|
SET
|
||||||
|
[UserId] = @UserId,
|
||||||
|
[OrganizationId] = @OrganizationId,
|
||||||
|
[Type] = @Type,
|
||||||
|
[Amount] = @Amount,
|
||||||
|
[Refunded] = @Refunded,
|
||||||
|
[RefundedAmount] = @RefundedAmount,
|
||||||
|
[Details] = @Details,
|
||||||
|
[PaymentMethodType] = @PaymentMethodType,
|
||||||
|
[Gateway] = @Gateway,
|
||||||
|
[GatewayId] = @GatewayId,
|
||||||
|
[CreationDate] = @CreationDate
|
||||||
|
WHERE
|
||||||
|
[Id] = @Id
|
||||||
|
END
|
||||||
|
GO
|
||||||
|
|
||||||
|
IF OBJECT_ID('[dbo].[Transaction_ReadByGatewayId]') IS NOT NULL
|
||||||
|
BEGIN
|
||||||
|
DROP PROCEDURE [dbo].[Transaction_ReadByGatewayId]
|
||||||
|
END
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE PROCEDURE [dbo].[Transaction_ReadByGatewayId]
|
||||||
|
@Gateway TINYINT,
|
||||||
|
@GatewayId VARCHAR(50)
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
*
|
||||||
|
FROM
|
||||||
|
[dbo].[TransactionView]
|
||||||
|
WHERE
|
||||||
|
[Gateway] = @Gateway
|
||||||
|
AND [GatewayId] = @GatewayId
|
||||||
|
END
|
||||||
|
GO
|
@ -16,6 +16,7 @@
|
|||||||
<None Remove="DbScripts\2018-09-25_00_OrgPurge.sql" />
|
<None Remove="DbScripts\2018-09-25_00_OrgPurge.sql" />
|
||||||
<None Remove="DbScripts\2018-10-17_00_ManagerRole.sql" />
|
<None Remove="DbScripts\2018-10-17_00_ManagerRole.sql" />
|
||||||
<None Remove="DbScripts\2018-12-19_00_OrgUserTwoFactorEnabled.sql" />
|
<None Remove="DbScripts\2018-12-19_00_OrgUserTwoFactorEnabled.sql" />
|
||||||
|
<None Remove="DbScripts\2019-01-31_00_Transactions.sql" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
Loading…
Reference in New Issue
Block a user