1
0
mirror of https://github.com/bitwarden/server.git synced 2025-02-16 01:51:21 +01:00

Merge branch 'stripechanges'

This commit is contained in:
Kyle Spearrin 2019-02-09 18:13:49 -05:00
commit 8fad9d849f
45 changed files with 2311 additions and 798 deletions

View File

@ -22,6 +22,7 @@ namespace Bit.Admin.Jobs
protected async override Task ExecuteJobAsync(IJobExecutionContext context)
{
await _maintenanceRepository.UpdateStatisticsAsync();
await _maintenanceRepository.DisableCipherAutoStatsAsync();
}
}
}

View File

@ -28,6 +28,7 @@ namespace Bit.Api.Controllers
private readonly ICipherService _cipherService;
private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly ILicensingService _licenseService;
private readonly IPaymentService _paymentService;
private readonly GlobalSettings _globalSettings;
public AccountsController(
@ -38,6 +39,7 @@ namespace Bit.Api.Controllers
ICipherService cipherService,
IOrganizationUserRepository organizationUserRepository,
ILicensingService licenseService,
IPaymentService paymentService,
GlobalSettings globalSettings)
{
_userService = userService;
@ -47,6 +49,7 @@ namespace Bit.Api.Controllers
_cipherService = cipherService;
_organizationUserRepository = organizationUserRepository;
_licenseService = licenseService;
_paymentService = paymentService;
_globalSettings = globalSettings;
}
@ -476,8 +479,7 @@ namespace Bit.Api.Controllers
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);
return new BillingResponseModel(user, billingInfo, license);
}

View File

@ -24,6 +24,7 @@ namespace Bit.Api.Controllers
private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IOrganizationService _organizationService;
private readonly IUserService _userService;
private readonly IPaymentService _paymentService;
private readonly CurrentContext _currentContext;
private readonly GlobalSettings _globalSettings;
@ -32,6 +33,7 @@ namespace Bit.Api.Controllers
IOrganizationUserRepository organizationUserRepository,
IOrganizationService organizationService,
IUserService userService,
IPaymentService paymentService,
CurrentContext currentContext,
GlobalSettings globalSettings)
{
@ -39,6 +41,7 @@ namespace Bit.Api.Controllers
_organizationUserRepository = organizationUserRepository;
_organizationService = organizationService;
_userService = userService;
_paymentService = paymentService;
_currentContext = currentContext;
_globalSettings = globalSettings;
}
@ -78,8 +81,7 @@ namespace Bit.Api.Controllers
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)
{
throw new NotFoundException();
@ -110,7 +112,7 @@ namespace Bit.Api.Controllers
try
{
var invoice = await new StripeInvoiceService().GetAsync(invoiceId);
var invoice = await new InvoiceService().GetAsync(invoiceId);
if(invoice != null && invoice.CustomerId == organization.GatewayCustomerId &&
!string.IsNullOrWhiteSpace(invoice.HostedInvoiceUrl))
{

View File

@ -6,5 +6,15 @@
public virtual string StripeWebhookKey { get; set; }
public virtual string StripeWebhookSecret { 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; }
}
}
}

View 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();
}
}
}

View File

@ -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.Services;
using Microsoft.AspNetCore.Hosting;
@ -7,6 +9,7 @@ using Microsoft.Extensions.Options;
using Stripe;
using System;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
@ -20,14 +23,18 @@ namespace Bit.Billing.Controllers
private readonly IHostingEnvironment _hostingEnvironment;
private readonly IOrganizationService _organizationService;
private readonly IOrganizationRepository _organizationRepository;
private readonly ITransactionRepository _transactionRepository;
private readonly IUserService _userService;
private readonly IMailService _mailService;
private readonly Braintree.BraintreeGateway _btGateway;
public StripeController(
GlobalSettings globalSettings,
IOptions<BillingSettings> billingSettings,
IHostingEnvironment hostingEnvironment,
IOrganizationService organizationService,
IOrganizationRepository organizationRepository,
ITransactionRepository transactionRepository,
IUserService userService,
IMailService mailService)
{
@ -35,8 +42,18 @@ namespace Bit.Billing.Controllers
_hostingEnvironment = hostingEnvironment;
_organizationService = organizationService;
_organizationRepository = organizationRepository;
_transactionRepository = transactionRepository;
_userService = userService;
_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")]
@ -47,11 +64,11 @@ namespace Bit.Billing.Controllers
return new BadRequestResult();
}
StripeEvent parsedEvent;
Stripe.Event parsedEvent;
using(var sr = new StreamReader(HttpContext.Request.Body))
{
var json = await sr.ReadToEndAsync();
parsedEvent = StripeEventUtility.ConstructEvent(json, Request.Headers["Stripe-Signature"],
parsedEvent = EventUtility.ConstructEvent(json, Request.Headers["Stripe-Signature"],
_billingSettings.StripeWebhookSecret);
}
@ -60,20 +77,17 @@ namespace Bit.Billing.Controllers
return new BadRequestResult();
}
if(_hostingEnvironment.IsProduction() && !parsedEvent.LiveMode)
if(_hostingEnvironment.IsProduction() && !parsedEvent.Livemode)
{
return new BadRequestResult();
}
var invUpcoming = parsedEvent.Type.Equals("invoice.upcoming");
var subDeleted = parsedEvent.Type.Equals("customer.subscription.deleted");
var subUpdated = parsedEvent.Type.Equals("customer.subscription.updated");
if(subDeleted || subUpdated)
{
StripeSubscription subscription = Mapper<StripeSubscription>.MapFromJson(
parsedEvent.Data.Object.ToString());
if(subscription == null)
if(!(parsedEvent.Data.Object is Subscription subscription))
{
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(
parsedEvent.Data.Object.ToString());
if(invoice == null)
if(!(parsedEvent.Data.Object is Invoice invoice))
{
throw new Exception("Invoice is null.");
}
var subscriptionService = new StripeSubscriptionService();
var subscriptionService = new SubscriptionService();
var subscription = await subscriptionService.GetAsync(invoice.SubscriptionId);
if(subscription == null)
{
@ -152,11 +164,153 @@ namespace Bit.Billing.Controllers
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,
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();
}
@ -204,13 +358,88 @@ namespace Bit.Billing.Controllers
{
switch(org.PlanType)
{
case Core.Enums.PlanType.FamiliesAnnually:
case Core.Enums.PlanType.TeamsAnnually:
case Core.Enums.PlanType.EnterpriseAnnually:
case PlanType.FamiliesAnnually:
case PlanType.TeamsAnnually:
case PlanType.EnterpriseAnnually:
return true;
default:
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;
}
}
}

View File

@ -17,12 +17,14 @@ namespace Bit.Billing.Jobs
private readonly GlobalSettings _globalSettings;
private readonly IUserRepository _userRepository;
private readonly IMailService _mailService;
private readonly IPaymentService _paymentService;
public PremiumRenewalRemindersJob(
IOptions<BillingSettings> billingSettings,
GlobalSettings globalSettings,
IUserRepository userRepository,
IMailService mailService,
IPaymentService paymentService,
ILogger<PremiumRenewalRemindersJob> logger)
: base(logger)
{
@ -30,6 +32,7 @@ namespace Bit.Billing.Jobs
_globalSettings = globalSettings;
_userRepository = userRepository;
_mailService = mailService;
_paymentService = paymentService;
}
protected async override Task ExecuteJobAsync(IJobExecutionContext context)
@ -37,8 +40,7 @@ namespace Bit.Billing.Jobs
var users = await _userRepository.GetManyByPremiumRenewalAsync();
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)
{
var items = new List<string> { "1 × Premium Membership (Annually)" };

View File

@ -39,6 +39,9 @@ namespace Bit.Billing
// Repositories
services.AddSqlServerRepositories(globalSettings);
// PayPal Client
services.AddSingleton<Utilities.PayPalClient>();
// Context
services.AddScoped<CurrentContext>();

View 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);
}
}
}

View File

@ -15,5 +15,10 @@
"braintree": {
"production": true
}
},
"billingSettings": {
"payPal": {
"production": false
}
}
}

View File

@ -45,18 +45,25 @@
"notificationHub": {
"connectionString": "SECRET",
"hubName": "SECRET"
},
"braintree": {
"production": false,
"merchantId": "SECRET",
"publicKey": "SECRET",
"privateKey": "SECRET"
}
},
"billingSettings": {
"jobsKey": "SECRET",
"stripeWebhookKey": "SECRET",
"stripeWebhookSecret": "SECRET",
"braintreeWebhookKey": "SECRET"
},
"braintree": {
"production": false,
"merchantId": "SECRET",
"publicKey": "SECRET",
"privateKey": "SECRET"
"braintreeWebhookKey": "SECRET",
"payPal": {
"production": false,
"clientId": "SECRET",
"clientSecret": "SECRET",
"webhookId": "SECRET",
"webhookKey": "SECRET"
}
}
}

View File

@ -38,7 +38,7 @@
<PackageReference Include="AspNetCoreRateLimit" Version="2.1.0" />
<PackageReference Include="Braintree" Version="4.6.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="Otp.NET" Version="1.2.0" />
<PackageReference Include="YubicoDotNetClient" Version="1.2.0" />

View File

@ -13,6 +13,8 @@ namespace Bit.Core.Enums
[Display(Name = "Google Play Store")]
PlayStore = 3,
[Display(Name = "Coinbase")]
Coinbase = 4
Coinbase = 4,
[Display(Name = "PayPal")]
PayPal = 5,
}
}

View File

@ -16,7 +16,7 @@
United = 11,
Yahoo = 12,
Zonelabs = 13,
Paypal = 14,
PayPal = 14,
Avon = 15,
Diapers = 16,
Contacts = 17,

View File

@ -0,0 +1,11 @@
namespace Bit.Core.Enums
{
public enum TransactionType : byte
{
Charge = 0,
Credit = 1,
PromotionalCredit = 2,
ReferralCredit = 3,
Refund = 4,
}
}

View File

@ -12,10 +12,12 @@ namespace Bit.Core.Models.Api
public BillingResponseModel(User user, BillingInfo billing, UserLicense license)
: base("billing")
{
CreditAmount = billing.CreditAmount;
PaymentSource = billing.PaymentSource != null ? new BillingSource(billing.PaymentSource) : null;
Subscription = billing.Subscription != null ? new BillingSubscription(billing.Subscription) : null;
Charges = billing.Charges.Select(c => new BillingCharge(c));
UpcomingInvoice = billing.UpcomingInvoice != null ? new BillingInvoice(billing.UpcomingInvoice) : null;
Transactions = billing.Transactions?.Select(t => new BillingTransaction(t));
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;
StorageGb = user.Storage.HasValue ? Math.Round(user.Storage.Value / 1073741824D, 2) : 0; // 1 GB
MaxStorageGb = user.MaxStorageGb;
@ -37,13 +39,15 @@ namespace Bit.Core.Models.Api
}
}
public decimal CreditAmount { get; set; }
public string StorageName { get; set; }
public double? StorageGb { get; set; }
public short? MaxStorageGb { get; set; }
public BillingSource PaymentSource { get; set; }
public BillingSubscription Subscription { get; set; }
public BillingInvoice UpcomingInvoice { get; set; }
public IEnumerable<BillingCharge> Charges { get; set; }
public BillingInvoiceInfo UpcomingInvoice { get; set; }
public IEnumerable<BillingInvoice> Invoices { get; set; }
public IEnumerable<BillingTransaction> Transactions { get; set; }
public UserLicense License { 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;
Date = inv.Date;
@ -121,28 +125,44 @@ namespace Bit.Core.Models.Api
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;
RefundedAmount = charge.RefundedAmount;
PaymentSource = charge.PaymentSource != null ? new BillingSource(charge.PaymentSource) : null;
CreatedDate = charge.CreatedDate;
FailureMessage = charge.FailureMessage;
Refunded = charge.Refunded;
Status = charge.Status;
InvoiceId = charge.InvoiceId;
Url = inv.Url;
PdfUrl = inv.PdfUrl;
Number = inv.Number;
Paid = inv.Paid;
}
public string Url { get; set; }
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 decimal Amount { get; set; }
public BillingSource PaymentSource { get; set; }
public string Status { get; set; }
public string FailureMessage { get; set; }
public bool Refunded { get; set; }
public bool PartiallyRefunded => !Refunded && RefundedAmount > 0;
public decimal RefundedAmount { get; set; }
public string InvoiceId { get; set; }
public bool? Refunded { get; set; }
public bool? PartiallyRefunded { get; set; }
public decimal? RefundedAmount { get; set; }
public TransactionType Type { get; set; }
public PaymentMethodType? PaymentMethodType { get; set; }
public string Details { get; set; }
}
}

View File

@ -67,8 +67,9 @@ namespace Bit.Core.Models.Api
{
PaymentSource = billing.PaymentSource != null ? new BillingSource(billing.PaymentSource) : null;
Subscription = billing.Subscription != null ? new BillingSubscription(billing.Subscription) : null;
Charges = billing.Charges.Select(c => new BillingCharge(c));
UpcomingInvoice = billing.UpcomingInvoice != null ? new BillingInvoice(billing.UpcomingInvoice) : null;
Transactions = billing.Transactions?.Select(t => new BillingTransaction(t));
Invoices = billing.Invoices?.Select(i => new BillingInvoice(i));
UpcomingInvoice = billing.UpcomingInvoice != null ? new BillingInvoiceInfo(billing.UpcomingInvoice) : null;
StorageName = organization.Storage.HasValue ?
Utilities.CoreHelpers.ReadableBytesSize(organization.Storage.Value) : null;
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 BillingSource PaymentSource { get; set; }
public BillingSubscription Subscription { get; set; }
public BillingInvoice UpcomingInvoice { get; set; }
public IEnumerable<BillingCharge> Charges { get; set; }
public BillingInvoiceInfo UpcomingInvoice { get; set; }
public IEnumerable<BillingInvoice> Invoices { get; set; }
public IEnumerable<BillingTransaction> Transactions { get; set; }
public DateTime? Expiration { get; set; }
}
}

View File

@ -1,5 +1,5 @@
using Bit.Core.Enums;
using Braintree;
using Bit.Core.Models.Table;
using Stripe;
using System;
using System.Collections.Generic;
@ -9,47 +9,47 @@ namespace Bit.Core.Models.Business
{
public class BillingInfo
{
public decimal CreditAmount { get; set; }
public BillingSource PaymentSource { get; set; }
public BillingSubscription Subscription { get; set; }
public BillingInvoice UpcomingInvoice { get; set; }
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 BillingSource(Source source)
public BillingSource(IPaymentSource source)
{
switch(source.Type)
if(source is BankAccount bankAccount)
{
case SourceType.Card:
Type = PaymentMethodType.Card;
Description = $"{source.Card.Brand}, *{source.Card.Last4}, " +
string.Format("{0}/{1}",
string.Concat(source.Card.ExpirationMonth < 10 ?
"0" : string.Empty, source.Card.ExpirationMonth),
source.Card.ExpirationYear);
CardBrand = source.Card.Brand;
break;
case SourceType.BankAccount:
Type = PaymentMethodType.BankAccount;
Description = $"{source.BankAccount.BankName}, *{source.BankAccount.Last4} - " +
(source.BankAccount.Status == "verified" ? "verified" :
source.BankAccount.Status == "errored" ? "invalid" :
source.BankAccount.Status == "verification_failed" ? "verification failed" : "unverified");
NeedsVerification = source.BankAccount.Status == "new" || source.BankAccount.Status == "validated";
break;
default:
break;
Type = PaymentMethodType.BankAccount;
Description = $"{bankAccount.BankName}, *{bankAccount.Last4} - " +
(bankAccount.Status == "verified" ? "verified" :
bankAccount.Status == "errored" ? "invalid" :
bankAccount.Status == "verification_failed" ? "verification failed" : "unverified");
NeedsVerification = bankAccount.Status == "new" || bankAccount.Status == "validated";
}
else if(source is Card card)
{
Type = PaymentMethodType.Card;
Description = $"{card.Brand}, *{card.Last4}, " +
string.Format("{0}/{1}",
string.Concat(card.ExpMonth < 10 ?
"0" : string.Empty, card.ExpMonth),
card.ExpYear);
CardBrand = card.Brand;
}
}
public BillingSource(PaymentMethod method)
public BillingSource(Braintree.PaymentMethod method)
{
if(method is PayPalAccount paypal)
if(method is Braintree.PayPalAccount paypal)
{
Type = PaymentMethodType.PayPal;
Description = paypal.Email;
}
else if(method is CreditCard card)
else if(method is Braintree.CreditCard card)
{
Type = PaymentMethodType.Card;
Description = $"{card.CardType.ToString()}, *{card.LastFour}, " +
@ -59,7 +59,7 @@ namespace Bit.Core.Models.Business
card.ExpirationYear);
CardBrand = card.CardType.ToString();
}
else if(method is UsBankAccount bank)
else if(method is Braintree.UsBankAccount bank)
{
Type = PaymentMethodType.BankAccount;
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;
Description = $"{bank.BankName}, *{bank.Last4}";
}
public BillingSource(PayPalDetails paypal)
public BillingSource(Braintree.PayPalDetails paypal)
{
Type = PaymentMethodType.PayPal;
Description = paypal.PayerEmail;
@ -90,7 +90,7 @@ namespace Bit.Core.Models.Business
public class BillingSubscription
{
public BillingSubscription(StripeSubscription sub)
public BillingSubscription(Subscription sub)
{
Status = sub.Status;
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();
if(sub.HasTrialPeriod.GetValueOrDefault() && sub.CreatedAt.HasValue && sub.TrialDuration.HasValue)
{
TrialStartDate = sub.CreatedAt.Value;
if(sub.TrialDurationUnit == SubscriptionDurationUnit.DAY)
if(sub.TrialDurationUnit == Braintree.SubscriptionDurationUnit.DAY)
{
TrialEndDate = TrialStartDate.Value.AddDays(sub.TrialDuration.Value);
}
@ -127,7 +127,7 @@ namespace Bit.Core.Models.Business
PeriodEndDate = sub.BillingPeriodEndDate;
CancelAtEndDate = !sub.NeverExpires.GetValueOrDefault();
Cancelled = sub.Status == SubscriptionStatus.CANCELED;
Cancelled = sub.Status == Braintree.SubscriptionStatus.CANCELED;
if(Cancelled)
{
CancelledDate = sub.UpdatedAt.Value;
@ -159,7 +159,7 @@ namespace Bit.Core.Models.Business
public class BillingSubscriptionItem
{
public BillingSubscriptionItem(StripeSubscriptionItem item)
public BillingSubscriptionItem(SubscriptionItem item)
{
if(item.Plan != null)
{
@ -168,10 +168,10 @@ namespace Bit.Core.Models.Business
Interval = item.Plan.Interval;
}
Quantity = item.Quantity;
Quantity = (int)item.Quantity;
}
public BillingSubscriptionItem(Plan plan)
public BillingSubscriptionItem(Braintree.Plan plan)
{
Name = plan.Name;
Amount = plan.Price.GetValueOrDefault();
@ -179,7 +179,7 @@ namespace Bit.Core.Models.Business
Quantity = 1;
}
public BillingSubscriptionItem(Plan plan, AddOn addon)
public BillingSubscriptionItem(Braintree.Plan plan, Braintree.AddOn addon)
{
Name = addon.Name;
Amount = addon.Amount.GetValueOrDefault();
@ -196,13 +196,15 @@ namespace Bit.Core.Models.Business
public class BillingInvoice
{
public BillingInvoice(StripeInvoice inv)
public BillingInvoice() { }
public BillingInvoice(Invoice inv)
{
Amount = inv.AmountDue / 100M;
Date = inv.Date.Value;
}
public BillingInvoice(Subscription sub)
public BillingInvoice(Braintree.Subscription sub)
{
Amount = sub.NextBillAmount.GetValueOrDefault() + sub.Balance.GetValueOrDefault();
if(Amount < 0)
@ -218,7 +220,7 @@ namespace Bit.Core.Models.Business
public class BillingCharge
{
public BillingCharge(StripeCharge charge)
public BillingCharge(Charge charge)
{
Amount = charge.Amount / 100M;
RefundedAmount = charge.AmountRefunded / 100M;
@ -230,7 +232,7 @@ namespace Bit.Core.Models.Business
InvoiceId = charge.InvoiceId;
}
public BillingCharge(Transaction transaction)
public BillingCharge(Braintree.Transaction transaction)
{
Amount = transaction.Amount.GetValueOrDefault();
RefundedAmount = 0; // TODO?
@ -239,7 +241,8 @@ namespace Bit.Core.Models.Business
{
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);
}
@ -265,5 +268,63 @@ namespace Bit.Core.Models.Business
public decimal RefundedAmount { 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; }
}
}
}

View File

@ -1,15 +1,19 @@
using Bit.Core.Enums;
using System;
using Bit.Core.Enums;
using Bit.Core.Services;
namespace Bit.Core.Models.Table
{
public interface ISubscriber
{
Guid Id { get; }
GatewayType? Gateway { get; set; }
string GatewayCustomerId { get; set; }
string GatewaySubscriptionId { get; set; }
string BillingEmailAddress();
string BillingName();
IPaymentService GetPaymentService(GlobalSettings globalSettings);
string BraintreeCustomerIdPrefix();
string BraintreeIdField();
string GatewayIdField();
}
}

View File

@ -63,6 +63,21 @@ namespace Bit.Core.Models.Table
return BusinessName;
}
public string BraintreeCustomerIdPrefix()
{
return "o";
}
public string BraintreeIdField()
{
return "organization_id";
}
public string GatewayIdField()
{
return "organizationId";
}
public long StorageBytesRemaining()
{
if(!MaxStorageGb.HasValue)
@ -84,29 +99,6 @@ namespace Bit.Core.Models.Table
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()
{
if(string.IsNullOrWhiteSpace(TwoFactorProviders))

View 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();
}
}
}

View File

@ -58,6 +58,21 @@ namespace Bit.Core.Models.Table
return Name;
}
public string BraintreeCustomerIdPrefix()
{
return "u";
}
public string BraintreeIdField()
{
return "user_id";
}
public string GatewayIdField()
{
return "userId";
}
public Dictionary<TwoFactorProviderType, TwoFactorProvider> GetTwoFactorProviders()
{
if(string.IsNullOrWhiteSpace(TwoFactorProviders))
@ -133,29 +148,6 @@ namespace Bit.Core.Models.Table
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)
{
return new IdentityUser

View File

@ -5,6 +5,7 @@ namespace Bit.Core.Repositories
public interface IMaintenanceRepository
{
Task UpdateStatisticsAsync();
Task DisableCipherAutoStatsAsync();
Task RebuildIndexesAsync();
Task DeleteExpiredGrantsAsync();
}

View 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);
}
}

View File

@ -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()
{
using(var connection = new SqlConnection(ConnectionString))

View 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();
}
}
}
}

View File

@ -1,17 +1,22 @@
using System.Threading.Tasks;
using Bit.Core.Models.Table;
using Bit.Core.Models.Business;
using Bit.Core.Enums;
namespace Bit.Core.Services
{
public interface IPaymentService
{
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 CancelSubscriptionAsync(ISubscriber subscriber, bool endOfPeriod = false);
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> GetBillingAsync(ISubscriber subscriber);
}

View File

@ -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;
}
}
}

View File

@ -32,7 +32,7 @@ namespace Bit.Core.Services
private readonly IEventService _eventService;
private readonly IInstallationRepository _installationRepository;
private readonly IApplicationCacheService _applicationCacheService;
private readonly StripePaymentService _stripePaymentService;
private readonly IPaymentService _paymentService;
private readonly GlobalSettings _globalSettings;
public OrganizationService(
@ -50,6 +50,7 @@ namespace Bit.Core.Services
IEventService eventService,
IInstallationRepository installationRepository,
IApplicationCacheService applicationCacheService,
IPaymentService paymentService,
GlobalSettings globalSettings)
{
_organizationRepository = organizationRepository;
@ -66,7 +67,7 @@ namespace Bit.Core.Services
_eventService = eventService;
_installationRepository = installationRepository;
_applicationCacheService = applicationCacheService;
_stripePaymentService = new StripePaymentService();
_paymentService = paymentService;
_globalSettings = globalSettings;
}
@ -78,7 +79,22 @@ namespace Bit.Core.Services
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)
{
await ReplaceAndUpdateCache(organization);
@ -100,7 +116,7 @@ namespace Bit.Core.Services
eop = false;
}
await _stripePaymentService.CancelSubscriptionAsync(organization, eop);
await _paymentService.CancelSubscriptionAsync(organization, eop);
}
public async Task ReinstateSubscriptionAsync(Guid organizationId)
@ -111,7 +127,7 @@ namespace Bit.Core.Services
throw new NotFoundException();
}
await _stripePaymentService.ReinstateSubscriptionAsync(organization);
await _paymentService.ReinstateSubscriptionAsync(organization);
}
public async Task UpgradePlanAsync(Guid organizationId, PlanType plan, int additionalSeats)
@ -186,15 +202,15 @@ namespace Bit.Core.Services
// TODO: Groups?
var subscriptionService = new StripeSubscriptionService();
var subscriptionService = new Stripe.SubscriptionService();
if(string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId))
{
// They must have been on a free plan. Create new sub.
var subCreateOptions = new StripeSubscriptionCreateOptions
var subCreateOptions = new SubscriptionCreateOptions
{
CustomerId = organization.GatewayCustomerId,
TrialPeriodDays = newPlan.TrialPeriodDays,
Items = new List<StripeSubscriptionItemOption>(),
Items = new List<SubscriptionItemOption>(),
Metadata = new Dictionary<string, string> {
{ "organizationId", organization.Id.ToString() }
}
@ -202,7 +218,7 @@ namespace Bit.Core.Services
if(newPlan.StripePlanId != null)
{
subCreateOptions.Items.Add(new StripeSubscriptionItemOption
subCreateOptions.Items.Add(new SubscriptionItemOption
{
PlanId = newPlan.StripePlanId,
Quantity = 1
@ -211,7 +227,7 @@ namespace Bit.Core.Services
if(additionalSeats > 0 && newPlan.StripeSeatPlanId != null)
{
subCreateOptions.Items.Add(new StripeSubscriptionItemOption
subCreateOptions.Items.Add(new SubscriptionItemOption
{
PlanId = newPlan.StripeSeatPlanId,
Quantity = additionalSeats
@ -223,14 +239,14 @@ namespace Bit.Core.Services
else
{
// Update existing sub.
var subUpdateOptions = new StripeSubscriptionUpdateOptions
var subUpdateOptions = new SubscriptionUpdateOptions
{
Items = new List<StripeSubscriptionItemUpdateOption>()
Items = new List<SubscriptionItemUpdateOption>()
};
if(newPlan.StripePlanId != null)
{
subUpdateOptions.Items.Add(new StripeSubscriptionItemUpdateOption
subUpdateOptions.Items.Add(new SubscriptionItemUpdateOption
{
PlanId = newPlan.StripePlanId,
Quantity = 1
@ -239,7 +255,7 @@ namespace Bit.Core.Services
if(additionalSeats > 0 && newPlan.StripeSeatPlanId != null)
{
subUpdateOptions.Items.Add(new StripeSubscriptionItemUpdateOption
subUpdateOptions.Items.Add(new SubscriptionItemUpdateOption
{
PlanId = newPlan.StripeSeatPlanId,
Quantity = additionalSeats
@ -271,7 +287,7 @@ namespace Bit.Core.Services
throw new BadRequestException("Plan does not allow additional storage.");
}
await BillingHelpers.AdjustStorageAsync(_stripePaymentService, organization, storageAdjustmentGb,
await BillingHelpers.AdjustStorageAsync(_paymentService, organization, storageAdjustmentGb,
plan.StripeStoragePlanId);
await ReplaceAndUpdateCache(organization);
}
@ -333,44 +349,74 @@ namespace Bit.Core.Services
}
}
var subscriptionItemService = new StripeSubscriptionItemService();
var subscriptionService = new StripeSubscriptionService();
var subscriptionItemService = new SubscriptionItemService();
var subscriptionService = new SubscriptionService();
var sub = await subscriptionService.GetAsync(organization.GatewaySubscriptionId);
if(sub == null)
{
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 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)
{
await subscriptionItemService.CreateAsync(new StripeSubscriptionItemCreateOptions
subItemOptions.Add(new InvoiceSubscriptionItemOptions
{
PlanId = plan.StripeSeatPlanId,
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)
{
await subscriptionItemService.UpdateAsync(seatItem.Id, new StripeSubscriptionItemUpdateOptions
subItemOptions.Add(new InvoiceSubscriptionItemOptions
{
Id = seatItem.Id,
PlanId = plan.StripeSeatPlanId,
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)
{
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)
{
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;
await ReplaceAndUpdateCache(organization);
}
@ -389,7 +435,7 @@ namespace Bit.Core.Services
}
var bankService = new BankAccountService();
var customerService = new StripeCustomerService();
var customerService = new CustomerService();
var customer = await customerService.GetAsync(organization.GatewayCustomerId);
if(customer == null)
{
@ -397,7 +443,7 @@ namespace Bit.Core.Services
}
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)
{
throw new GatewayException("Cannot find an unverified bank account.");
@ -406,7 +452,7 @@ namespace Bit.Core.Services
try
{
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")
{
throw new GatewayException("Unable to verify account.");
@ -431,6 +477,11 @@ namespace Bit.Core.Services
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)
{
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!");
}
if(signup.AdditionalSeats < 0)
{
throw new BadRequestException("You can't subtract seats!");
}
if(!plan.CanBuyAdditionalSeats && signup.AdditionalSeats > 0)
{
throw new BadRequestException("Plan does not allow additional users.");
@ -453,96 +509,10 @@ namespace Bit.Core.Services
$"{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
{
Id = newOrgId,
// Pre-generate the org id so that we can save it with the Stripe subscription..
Id = CoreHelpers.GenerateComb(),
Name = signup.Name,
BillingEmail = signup.BillingEmail,
BusinessName = signup.BusinessName,
@ -560,16 +530,43 @@ namespace Bit.Core.Services
SelfHost = plan.SelfHost,
UsersGetPremium = plan.UsersGetPremium || signup.PremiumAccessAddon,
Plan = plan.Name,
Gateway = plan.Type == PlanType.Free ? null : (GatewayType?)GatewayType.Stripe,
GatewayCustomerId = customer?.Id,
GatewaySubscriptionId = subscription?.Id,
Gateway = null,
Enabled = true,
ExpirationDate = subscription?.CurrentPeriodEnd,
LicenseKey = CoreHelpers.SecureRandomString(20),
CreationDate = 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);
}
@ -630,7 +627,8 @@ namespace Bit.Core.Services
var dir = $"{_globalSettings.LicenseDirectory}/organization";
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;
}
@ -679,7 +677,7 @@ namespace Bit.Core.Services
{
if(withPayment)
{
await _stripePaymentService.CancelAndRecoverChargesAsync(organization);
await _paymentService.CancelAndRecoverChargesAsync(organization);
}
if(organization.Id != default(Guid))
@ -756,7 +754,8 @@ namespace Bit.Core.Services
var dir = $"{_globalSettings.LicenseDirectory}/organization";
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.BusinessName = license.BusinessName;
@ -787,7 +786,7 @@ namespace Bit.Core.Services
{
var eop = !organization.ExpirationDate.HasValue ||
organization.ExpirationDate.Value >= DateTime.UtcNow;
await _stripePaymentService.CancelSubscriptionAsync(organization, eop);
await _paymentService.CancelSubscriptionAsync(organization, eop);
}
catch(GatewayException) { }
}
@ -842,8 +841,8 @@ namespace Bit.Core.Services
if(updateBilling && !string.IsNullOrWhiteSpace(organization.GatewayCustomerId))
{
var customerService = new StripeCustomerService();
await customerService.UpdateAsync(organization.GatewayCustomerId, new StripeCustomerUpdateOptions
var customerService = new CustomerService();
await customerService.UpdateAsync(organization.GatewayCustomerId, new CustomerUpdateOptions
{
Email = organization.BillingEmail,
Description = organization.BusinessName
@ -1207,9 +1206,8 @@ namespace Bit.Core.Services
{
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);
}

File diff suppressed because it is too large Load Diff

View File

@ -42,6 +42,7 @@ namespace Bit.Core.Services
private readonly ILicensingService _licenseService;
private readonly IEventService _eventService;
private readonly IApplicationCacheService _applicationCacheService;
private readonly IPaymentService _paymentService;
private readonly IDataProtector _organizationServiceDataProtector;
private readonly CurrentContext _currentContext;
private readonly GlobalSettings _globalSettings;
@ -67,6 +68,7 @@ namespace Bit.Core.Services
IEventService eventService,
IApplicationCacheService applicationCacheService,
IDataProtectionProvider dataProtectionProvider,
IPaymentService paymentService,
CurrentContext currentContext,
GlobalSettings globalSettings)
: base(
@ -94,6 +96,7 @@ namespace Bit.Core.Services
_licenseService = licenseService;
_eventService = eventService;
_applicationCacheService = applicationCacheService;
_paymentService = paymentService;
_organizationServiceDataProtector = dataProtectionProvider.CreateProtector(
"OrganizationServiceDataProtector");
_currentContext = currentContext;
@ -682,6 +685,11 @@ namespace Bit.Core.Services
throw new BadRequestException("Already a premium user.");
}
if(additionalStorageGb < 0)
{
throw new BadRequestException("You can't subtract storage!");
}
IPaymentService paymentService = null;
if(_globalSettings.SelfHosted)
{
@ -706,16 +714,14 @@ namespace Bit.Core.Services
throw new BadRequestException("Invalid token.");
}
if(paymentToken.StartsWith("tok_"))
var paymentMethodType = PaymentMethodType.Card;
if(!paymentToken.StartsWith("tok_"))
{
paymentService = new StripePaymentService();
}
else
{
paymentService = new BraintreePaymentService(_globalSettings);
paymentMethodType = PaymentMethodType.PayPal;
}
await paymentService.PurchasePremiumAsync(user, paymentToken, additionalStorageGb);
await _paymentService.PurchasePremiumAsync(user, paymentMethodType,
paymentToken, additionalStorageGb);
}
else
{
@ -789,9 +795,8 @@ namespace Bit.Core.Services
{
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);
}
@ -802,17 +807,17 @@ namespace Bit.Core.Services
throw new BadRequestException("Invalid token.");
}
IPaymentService paymentService = null;
PaymentMethodType paymentMethodType;
if(paymentToken.StartsWith("tok_"))
{
paymentService = new StripePaymentService();
paymentMethodType = PaymentMethodType.Card;
}
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)
{
await SaveUserAsync(user);
@ -821,20 +826,18 @@ namespace Bit.Core.Services
public async Task CancelPremiumAsync(User user, bool? endOfPeriod = null)
{
var paymentService = user.GetPaymentService(_globalSettings);
var eop = endOfPeriod.GetValueOrDefault(true);
if(!endOfPeriod.HasValue && user.PremiumExpirationDate.HasValue &&
user.PremiumExpirationDate.Value < DateTime.UtcNow)
{
eop = false;
}
await paymentService.CancelSubscriptionAsync(user, eop);
await _paymentService.CancelSubscriptionAsync(user, eop);
}
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)
@ -874,8 +877,7 @@ namespace Bit.Core.Services
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) :

View File

@ -52,6 +52,7 @@ namespace Bit.Core.Utilities
services.AddSingleton<IU2fRepository, SqlServerRepos.U2fRepository>();
services.AddSingleton<IInstallationRepository, SqlServerRepos.InstallationRepository>();
services.AddSingleton<IMaintenanceRepository, SqlServerRepos.MaintenanceRepository>();
services.AddSingleton<ITransactionRepository, SqlServerRepos.TransactionRepository>();
}
if(globalSettings.SelfHosted)
@ -77,6 +78,7 @@ namespace Bit.Core.Utilities
public static void AddDefaultServices(this IServiceCollection services, GlobalSettings globalSettings)
{
services.AddSingleton<IPaymentService, StripePaymentService>();
services.AddSingleton<IMailService, HandlebarsMailService>();
services.AddSingleton<ILicensingService, LicensingService>();
services.AddSingleton<IApplicationCacheService, InMemoryApplicationCacheService>();

View File

@ -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.Yahoo, new List<string> { "overture.com", "yahoo.com", "flickr.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.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" });

View File

@ -240,5 +240,14 @@
<Build Include="dbo\Stored Procedures\Cipher_CreateWithCollections.sql" />
<Build Include="dbo\Stored Procedures\Cipher_UpdateCollections.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>
</Project>

View 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

View 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

View File

@ -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

View 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

View File

@ -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

View 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

View 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

View 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);

View File

@ -0,0 +1,6 @@
CREATE VIEW [dbo].[TransactionView]
AS
SELECT
*
FROM
[dbo].[Transaction]

View 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

View File

@ -16,6 +16,7 @@
<None Remove="DbScripts\2018-09-25_00_OrgPurge.sql" />
<None Remove="DbScripts\2018-10-17_00_ManagerRole.sql" />
<None Remove="DbScripts\2018-12-19_00_OrgUserTwoFactorEnabled.sql" />
<None Remove="DbScripts\2019-01-31_00_Transactions.sql" />
</ItemGroup>
<ItemGroup>