mirror of
https://github.com/bitwarden/server.git
synced 2025-03-28 16:15:52 +01:00
[AC-2567] Billing Performance Improvements (#4143)
* Moved AccountsBilling controller to be owned by Billing * Added org billing history endpoint * Updated GetBillingInvoicesAsync to only retrieve paid, open, and uncollectible invoices, and added option to limit results * Removed invoices and transactions from GetBillingAsync * Limiting the number of invoices and transactions returned * Moved Billing models to Billing namespace * Split billing info and billing history objects * Removed billing method GetBillingBalanceAndSourceAsync * Removed unused using * Cleaned up BillingInfo a bit * Update migration scripts to use `CREATE OR ALTER` instead of checking for the `OBJECT_ID` * Applying limit to aggregated invoices after they return from Stripe
This commit is contained in:
parent
f615858724
commit
fc1c488a78
@ -198,15 +198,32 @@ public class OrganizationsController : Controller
|
||||
}
|
||||
var users = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(id);
|
||||
var billingInfo = await _paymentService.GetBillingAsync(organization);
|
||||
var billingHistoryInfo = await _paymentService.GetBillingHistoryAsync(organization);
|
||||
var billingSyncConnection = _globalSettings.EnableCloudCommunication ? await _organizationConnectionRepository.GetByOrganizationIdTypeAsync(id, OrganizationConnectionType.CloudBillingSync) : null;
|
||||
var secrets = organization.UseSecretsManager ? await _secretRepository.GetSecretsCountByOrganizationIdAsync(id) : -1;
|
||||
var projects = organization.UseSecretsManager ? await _projectRepository.GetProjectCountByOrganizationIdAsync(id) : -1;
|
||||
var serviceAccounts = organization.UseSecretsManager ? await _serviceAccountRepository.GetServiceAccountCountByOrganizationIdAsync(id) : -1;
|
||||
|
||||
var smSeats = organization.UseSecretsManager
|
||||
? await _organizationUserRepository.GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id)
|
||||
: -1;
|
||||
return View(new OrganizationEditModel(organization, provider, users, ciphers, collections, groups, policies,
|
||||
billingInfo, billingSyncConnection, _globalSettings, secrets, projects, serviceAccounts, smSeats));
|
||||
|
||||
return View(new OrganizationEditModel(
|
||||
organization,
|
||||
provider,
|
||||
users,
|
||||
ciphers,
|
||||
collections,
|
||||
groups,
|
||||
policies,
|
||||
billingInfo,
|
||||
billingHistoryInfo,
|
||||
billingSyncConnection,
|
||||
_globalSettings,
|
||||
secrets,
|
||||
projects,
|
||||
serviceAccounts,
|
||||
smSeats));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
|
@ -3,9 +3,9 @@ using System.Net;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
@ -27,14 +27,38 @@ public class OrganizationEditModel : OrganizationViewModel
|
||||
LicenseKey = RandomLicenseKey;
|
||||
}
|
||||
|
||||
public OrganizationEditModel(Organization org, Provider provider, IEnumerable<OrganizationUserUserDetails> orgUsers,
|
||||
IEnumerable<Cipher> ciphers, IEnumerable<Collection> collections, IEnumerable<Group> groups,
|
||||
IEnumerable<Policy> policies, BillingInfo billingInfo, IEnumerable<OrganizationConnection> connections,
|
||||
GlobalSettings globalSettings, int secrets, int projects, int serviceAccounts, int occupiedSmSeats)
|
||||
: base(org, provider, connections, orgUsers, ciphers, collections, groups, policies, secrets, projects,
|
||||
serviceAccounts, occupiedSmSeats)
|
||||
public OrganizationEditModel(
|
||||
Organization org,
|
||||
Provider provider,
|
||||
IEnumerable<OrganizationUserUserDetails> orgUsers,
|
||||
IEnumerable<Cipher> ciphers,
|
||||
IEnumerable<Collection> collections,
|
||||
IEnumerable<Group> groups,
|
||||
IEnumerable<Policy> policies,
|
||||
BillingInfo billingInfo,
|
||||
BillingHistoryInfo billingHistoryInfo,
|
||||
IEnumerable<OrganizationConnection> connections,
|
||||
GlobalSettings globalSettings,
|
||||
int secrets,
|
||||
int projects,
|
||||
int serviceAccounts,
|
||||
int occupiedSmSeats)
|
||||
: base(
|
||||
org,
|
||||
provider,
|
||||
connections,
|
||||
orgUsers,
|
||||
ciphers,
|
||||
collections,
|
||||
groups,
|
||||
policies,
|
||||
secrets,
|
||||
projects,
|
||||
serviceAccounts,
|
||||
occupiedSmSeats)
|
||||
{
|
||||
BillingInfo = billingInfo;
|
||||
BillingHistoryInfo = billingHistoryInfo;
|
||||
BraintreeMerchantId = globalSettings.Braintree.MerchantId;
|
||||
|
||||
Name = org.DisplayName();
|
||||
@ -73,6 +97,7 @@ public class OrganizationEditModel : OrganizationViewModel
|
||||
}
|
||||
|
||||
public BillingInfo BillingInfo { get; set; }
|
||||
public BillingHistoryInfo BillingHistoryInfo { get; set; }
|
||||
public string RandomLicenseKey => CoreHelpers.SecureRandomString(20);
|
||||
public string FourteenDayExpirationDate => DateTime.Now.AddDays(14).ToString("yyyy-MM-ddTHH:mm");
|
||||
public string BraintreeMerchantId { get; set; }
|
||||
|
@ -95,7 +95,7 @@
|
||||
{
|
||||
<h2>Billing Information</h2>
|
||||
@await Html.PartialAsync("_BillingInformation",
|
||||
new BillingInformationModel { BillingInfo = Model.BillingInfo, OrganizationId = Model.Organization.Id, Entity = "Organization" })
|
||||
new BillingInformationModel { BillingInfo = Model.BillingInfo, BillingHistoryInfo = Model.BillingHistoryInfo, OrganizationId = Model.Organization.Id, Entity = "Organization" })
|
||||
}
|
||||
|
||||
@await Html.PartialAsync("~/AdminConsole/Views/Shared/_OrganizationForm.cshtml", Model)
|
||||
|
@ -95,7 +95,8 @@ public class UsersController : Controller
|
||||
|
||||
var ciphers = await _cipherRepository.GetManyByUserIdAsync(id, useFlexibleCollections: UseFlexibleCollections);
|
||||
var billingInfo = await _paymentService.GetBillingAsync(user);
|
||||
return View(new UserEditModel(user, ciphers, billingInfo, _globalSettings));
|
||||
var billingHistoryInfo = await _paymentService.GetBillingHistoryAsync(user);
|
||||
return View(new UserEditModel(user, ciphers, billingInfo, billingHistoryInfo, _globalSettings));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
|
@ -1,10 +1,11 @@
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.Billing.Models;
|
||||
|
||||
namespace Bit.Admin.Models;
|
||||
|
||||
public class BillingInformationModel
|
||||
{
|
||||
public BillingInfo BillingInfo { get; set; }
|
||||
public BillingHistoryInfo BillingHistoryInfo { get; set; }
|
||||
public Guid? UserId { get; set; }
|
||||
public Guid? OrganizationId { get; set; }
|
||||
public string Entity { get; set; }
|
||||
|
@ -1,6 +1,6 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Core.Vault.Entities;
|
||||
@ -11,11 +11,16 @@ public class UserEditModel : UserViewModel
|
||||
{
|
||||
public UserEditModel() { }
|
||||
|
||||
public UserEditModel(User user, IEnumerable<Cipher> ciphers, BillingInfo billingInfo,
|
||||
public UserEditModel(
|
||||
User user,
|
||||
IEnumerable<Cipher> ciphers,
|
||||
BillingInfo billingInfo,
|
||||
BillingHistoryInfo billingHistoryInfo,
|
||||
GlobalSettings globalSettings)
|
||||
: base(user, ciphers)
|
||||
{
|
||||
BillingInfo = billingInfo;
|
||||
BillingHistoryInfo = billingHistoryInfo;
|
||||
BraintreeMerchantId = globalSettings.Braintree.MerchantId;
|
||||
|
||||
Name = user.Name;
|
||||
@ -31,6 +36,7 @@ public class UserEditModel : UserViewModel
|
||||
}
|
||||
|
||||
public BillingInfo BillingInfo { get; set; }
|
||||
public BillingHistoryInfo BillingHistoryInfo { get; set; }
|
||||
public string RandomLicenseKey => CoreHelpers.SecureRandomString(20);
|
||||
public string OneYearExpirationDate => DateTime.Now.AddYears(1).ToString("yyyy-MM-ddTHH:mm");
|
||||
public string BraintreeMerchantId { get; set; }
|
||||
|
@ -3,10 +3,10 @@
|
||||
@model BillingInformationModel
|
||||
|
||||
@{
|
||||
var canManageTransactions = Model.Entity == "User" ? AccessControlService.UserHasPermission(Permission.User_BillingInformation_CreateEditTransaction)
|
||||
var canManageTransactions = Model.Entity == "User" ? AccessControlService.UserHasPermission(Permission.User_BillingInformation_CreateEditTransaction)
|
||||
: AccessControlService.UserHasPermission(Permission.Org_BillingInformation_CreateEditTransaction);
|
||||
|
||||
var canDownloadInvoice = Model.Entity == "User" ? AccessControlService.UserHasPermission(Permission.User_BillingInformation_DownloadInvoice)
|
||||
var canDownloadInvoice = Model.Entity == "User" ? AccessControlService.UserHasPermission(Permission.User_BillingInformation_DownloadInvoice)
|
||||
: AccessControlService.UserHasPermission(Permission.Org_BillingInformation_DownloadInvoice);
|
||||
}
|
||||
|
||||
@ -16,11 +16,11 @@
|
||||
|
||||
<dt class="col-sm-4 col-lg-3">Invoices</dt>
|
||||
<dd class="col-sm-8 col-lg-9">
|
||||
@if(Model.BillingInfo.Invoices?.Any() ?? false)
|
||||
@if(Model.BillingHistoryInfo.Invoices?.Any() ?? false)
|
||||
{
|
||||
<table class="table">
|
||||
<tbody>
|
||||
@foreach(var invoice in Model.BillingInfo.Invoices)
|
||||
@foreach(var invoice in Model.BillingHistoryInfo.Invoices)
|
||||
{
|
||||
<tr>
|
||||
<td>@invoice.Date</td>
|
||||
@ -28,7 +28,7 @@
|
||||
</td>
|
||||
<td>@invoice.Amount.ToString("C")</td>
|
||||
<td>@(invoice.Paid ? "Paid" : "Unpaid")</td>
|
||||
@if (canDownloadInvoice)
|
||||
@if (canDownloadInvoice)
|
||||
{
|
||||
<td>
|
||||
<a target="_blank" rel="noreferrer" href="@invoice.PdfUrl" title="Download Invoice">
|
||||
@ -49,11 +49,11 @@
|
||||
|
||||
<dt class="col-sm-4 col-lg-3">Transactions</dt>
|
||||
<dd class="col-sm-8 col-lg-9">
|
||||
@if(Model.BillingInfo.Transactions?.Any() ?? false)
|
||||
@if(Model.BillingHistoryInfo.Transactions?.Any() ?? false)
|
||||
{
|
||||
<table class="table">
|
||||
<tbody>
|
||||
@foreach(var transaction in Model.BillingInfo.Transactions)
|
||||
@foreach(var transaction in Model.BillingHistoryInfo.Transactions)
|
||||
{
|
||||
<tr>
|
||||
<td>@transaction.CreatedDate</td>
|
||||
|
@ -92,7 +92,7 @@
|
||||
{
|
||||
<h2>Billing Information</h2>
|
||||
@await Html.PartialAsync("_BillingInformation",
|
||||
new BillingInformationModel { BillingInfo = Model.BillingInfo, UserId = Model.User.Id, Entity = "User" })
|
||||
new BillingInformationModel { BillingInfo = Model.BillingInfo, BillingHistoryInfo = Model.BillingHistoryInfo, UserId = Model.User.Id, Entity = "User" })
|
||||
}
|
||||
@if (canViewGeneral)
|
||||
{
|
||||
|
@ -1,51 +1,42 @@
|
||||
using Bit.Api.Models.Response;
|
||||
using Bit.Api.Billing.Models.Responses;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Bit.Api.Controllers;
|
||||
namespace Bit.Api.Billing.Controllers;
|
||||
|
||||
[Route("accounts/billing")]
|
||||
[Authorize("Application")]
|
||||
public class AccountsBillingController : Controller
|
||||
public class AccountsBillingController(
|
||||
IPaymentService paymentService,
|
||||
IUserService userService) : Controller
|
||||
{
|
||||
private readonly IPaymentService _paymentService;
|
||||
private readonly IUserService _userService;
|
||||
|
||||
public AccountsBillingController(
|
||||
IPaymentService paymentService,
|
||||
IUserService userService)
|
||||
{
|
||||
_paymentService = paymentService;
|
||||
_userService = userService;
|
||||
}
|
||||
|
||||
[HttpGet("history")]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public async Task<BillingHistoryResponseModel> GetBillingHistory()
|
||||
public async Task<BillingHistoryResponseModel> GetBillingHistoryAsync()
|
||||
{
|
||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||
var user = await userService.GetUserByPrincipalAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
throw new UnauthorizedAccessException();
|
||||
}
|
||||
|
||||
var billingInfo = await _paymentService.GetBillingHistoryAsync(user);
|
||||
var billingInfo = await paymentService.GetBillingHistoryAsync(user);
|
||||
return new BillingHistoryResponseModel(billingInfo);
|
||||
}
|
||||
|
||||
[HttpGet("payment-method")]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public async Task<BillingPaymentResponseModel> GetPaymentMethod()
|
||||
public async Task<BillingPaymentResponseModel> GetPaymentMethodAsync()
|
||||
{
|
||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||
var user = await userService.GetUserByPrincipalAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
throw new UnauthorizedAccessException();
|
||||
}
|
||||
|
||||
var billingInfo = await _paymentService.GetBillingBalanceAndSourceAsync(user);
|
||||
var billingInfo = await paymentService.GetBillingAsync(user);
|
||||
return new BillingPaymentResponseModel(billingInfo);
|
||||
}
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
using Bit.Api.Billing.Models.Responses;
|
||||
using Bit.Api.Models.Response;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Repositories;
|
||||
@ -33,6 +32,21 @@ public class OrganizationBillingController(
|
||||
return TypedResults.Ok(response);
|
||||
}
|
||||
|
||||
[HttpGet("history")]
|
||||
public async Task<IResult> GetHistoryAsync([FromRoute] Guid organizationId)
|
||||
{
|
||||
var organization = await organizationRepository.GetByIdAsync(organizationId);
|
||||
|
||||
if (organization == null)
|
||||
{
|
||||
return TypedResults.NotFound();
|
||||
}
|
||||
|
||||
var billingInfo = await paymentService.GetBillingHistoryAsync(organization);
|
||||
|
||||
return TypedResults.Ok(billingInfo);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public async Task<IResult> GetBillingAsync(Guid organizationId)
|
||||
|
@ -45,7 +45,7 @@ public class OrganizationsController(
|
||||
ISubscriberService subscriberService)
|
||||
: Controller
|
||||
{
|
||||
[HttpGet("{id}/billing-status")]
|
||||
[HttpGet("{id:guid}/billing-status")]
|
||||
public async Task<OrganizationBillingStatusResponseModel> GetBillingStatus(Guid id)
|
||||
{
|
||||
if (!await currentContext.EditPaymentMethods(id))
|
||||
|
@ -1,45 +1,24 @@
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Api;
|
||||
using Bit.Core.Models.Business;
|
||||
|
||||
namespace Bit.Api.Models.Response;
|
||||
namespace Bit.Api.Billing.Models.Responses;
|
||||
|
||||
public class BillingResponseModel : ResponseModel
|
||||
public class BillingHistoryResponseModel : ResponseModel
|
||||
{
|
||||
public BillingResponseModel(BillingInfo billing)
|
||||
: base("billing")
|
||||
public BillingHistoryResponseModel(BillingHistoryInfo billing)
|
||||
: base("billingHistory")
|
||||
{
|
||||
Balance = billing.Balance;
|
||||
PaymentSource = billing.PaymentSource != null ? new BillingSource(billing.PaymentSource) : null;
|
||||
Transactions = billing.Transactions?.Select(t => new BillingTransaction(t));
|
||||
Invoices = billing.Invoices?.Select(i => new BillingInvoice(i));
|
||||
}
|
||||
|
||||
public decimal Balance { get; set; }
|
||||
public BillingSource PaymentSource { get; set; }
|
||||
public IEnumerable<BillingInvoice> Invoices { get; set; }
|
||||
public IEnumerable<BillingTransaction> Transactions { get; set; }
|
||||
}
|
||||
|
||||
public class BillingSource
|
||||
{
|
||||
public BillingSource(BillingInfo.BillingSource source)
|
||||
{
|
||||
Type = source.Type;
|
||||
CardBrand = source.CardBrand;
|
||||
Description = source.Description;
|
||||
NeedsVerification = source.NeedsVerification;
|
||||
}
|
||||
|
||||
public PaymentMethodType Type { get; set; }
|
||||
public string CardBrand { get; set; }
|
||||
public string Description { get; set; }
|
||||
public bool NeedsVerification { get; set; }
|
||||
}
|
||||
|
||||
public class BillingInvoice
|
||||
{
|
||||
public BillingInvoice(BillingInfo.BillingInvoice inv)
|
||||
public BillingInvoice(BillingHistoryInfo.BillingInvoice inv)
|
||||
{
|
||||
Amount = inv.Amount;
|
||||
Date = inv.Date;
|
||||
@ -59,7 +38,7 @@ public class BillingInvoice
|
||||
|
||||
public class BillingTransaction
|
||||
{
|
||||
public BillingTransaction(BillingInfo.BillingTransaction transaction)
|
||||
public BillingTransaction(BillingHistoryInfo.BillingTransaction transaction)
|
||||
{
|
||||
CreatedDate = transaction.CreatedDate;
|
||||
Amount = transaction.Amount;
|
@ -1,7 +1,7 @@
|
||||
using Bit.Core.Models.Api;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Models.Api;
|
||||
|
||||
namespace Bit.Api.Models.Response;
|
||||
namespace Bit.Api.Billing.Models.Responses;
|
||||
|
||||
public class BillingPaymentResponseModel : ResponseModel
|
||||
{
|
34
src/Api/Billing/Models/Responses/BillingResponseModel.cs
Normal file
34
src/Api/Billing/Models/Responses/BillingResponseModel.cs
Normal file
@ -0,0 +1,34 @@
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Api;
|
||||
|
||||
namespace Bit.Api.Billing.Models.Responses;
|
||||
|
||||
public class BillingResponseModel : ResponseModel
|
||||
{
|
||||
public BillingResponseModel(BillingInfo billing)
|
||||
: base("billing")
|
||||
{
|
||||
Balance = billing.Balance;
|
||||
PaymentSource = billing.PaymentSource != null ? new BillingSource(billing.PaymentSource) : null;
|
||||
}
|
||||
|
||||
public decimal Balance { get; set; }
|
||||
public BillingSource PaymentSource { get; set; }
|
||||
}
|
||||
|
||||
public class BillingSource
|
||||
{
|
||||
public BillingSource(BillingInfo.BillingSource source)
|
||||
{
|
||||
Type = source.Type;
|
||||
CardBrand = source.CardBrand;
|
||||
Description = source.Description;
|
||||
NeedsVerification = source.NeedsVerification;
|
||||
}
|
||||
|
||||
public PaymentMethodType Type { get; set; }
|
||||
public string CardBrand { get; set; }
|
||||
public string Description { get; set; }
|
||||
public bool NeedsVerification { get; set; }
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
using Bit.Core.Models.Api;
|
||||
using Bit.Core.Models.Business;
|
||||
|
||||
namespace Bit.Api.Models.Response;
|
||||
|
||||
public class BillingHistoryResponseModel : ResponseModel
|
||||
{
|
||||
public BillingHistoryResponseModel(BillingInfo billing)
|
||||
: base("billingHistory")
|
||||
{
|
||||
Transactions = billing.Transactions?.Select(t => new BillingTransaction(t));
|
||||
Invoices = billing.Invoices?.Select(i => new BillingInvoice(i));
|
||||
}
|
||||
public IEnumerable<BillingInvoice> Invoices { get; set; }
|
||||
public IEnumerable<BillingTransaction> Transactions { get; set; }
|
||||
}
|
57
src/Core/Billing/Models/BillingHistoryInfo.cs
Normal file
57
src/Core/Billing/Models/BillingHistoryInfo.cs
Normal file
@ -0,0 +1,57 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Stripe;
|
||||
|
||||
namespace Bit.Core.Billing.Models;
|
||||
|
||||
public class BillingHistoryInfo
|
||||
{
|
||||
public IEnumerable<BillingInvoice> Invoices { get; set; } = new List<BillingInvoice>();
|
||||
public IEnumerable<BillingTransaction> Transactions { get; set; } = new List<BillingTransaction>();
|
||||
|
||||
public class BillingTransaction
|
||||
{
|
||||
public BillingTransaction(Transaction transaction)
|
||||
{
|
||||
Id = transaction.Id;
|
||||
CreatedDate = transaction.CreationDate;
|
||||
Refunded = transaction.Refunded;
|
||||
Type = transaction.Type;
|
||||
PaymentMethodType = transaction.PaymentMethodType;
|
||||
Details = transaction.Details;
|
||||
Amount = transaction.Amount;
|
||||
RefundedAmount = transaction.RefundedAmount;
|
||||
}
|
||||
|
||||
public Guid Id { get; set; }
|
||||
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 BillingInvoice
|
||||
{
|
||||
public BillingInvoice(Invoice inv)
|
||||
{
|
||||
Date = inv.Created;
|
||||
Url = inv.HostedInvoiceUrl;
|
||||
PdfUrl = inv.InvoicePdf;
|
||||
Number = inv.Number;
|
||||
Paid = inv.Paid;
|
||||
Amount = inv.Total / 100M;
|
||||
}
|
||||
|
||||
public decimal Amount { get; set; }
|
||||
public DateTime? Date { get; set; }
|
||||
public string Url { get; set; }
|
||||
public string PdfUrl { get; set; }
|
||||
public string Number { get; set; }
|
||||
public bool Paid { get; set; }
|
||||
}
|
||||
|
||||
}
|
97
src/Core/Billing/Models/BillingInfo.cs
Normal file
97
src/Core/Billing/Models/BillingInfo.cs
Normal file
@ -0,0 +1,97 @@
|
||||
using Bit.Core.Enums;
|
||||
using Stripe;
|
||||
|
||||
namespace Bit.Core.Billing.Models;
|
||||
|
||||
public class BillingInfo
|
||||
{
|
||||
public decimal Balance { get; set; }
|
||||
public BillingSource PaymentSource { get; set; }
|
||||
|
||||
public class BillingSource
|
||||
{
|
||||
public BillingSource() { }
|
||||
|
||||
public BillingSource(PaymentMethod method)
|
||||
{
|
||||
if (method.Card == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Type = PaymentMethodType.Card;
|
||||
var card = method.Card;
|
||||
Description = $"{card.Brand?.ToUpperInvariant()}, *{card.Last4}, {card.ExpMonth:00}/{card.ExpYear}";
|
||||
CardBrand = card.Brand;
|
||||
}
|
||||
|
||||
public BillingSource(IPaymentSource source)
|
||||
{
|
||||
switch (source)
|
||||
{
|
||||
case BankAccount bankAccount:
|
||||
var bankStatus = bankAccount.Status switch
|
||||
{
|
||||
"verified" => "verified",
|
||||
"errored" => "invalid",
|
||||
"verification_failed" => "verification failed",
|
||||
_ => "unverified"
|
||||
};
|
||||
Type = PaymentMethodType.BankAccount;
|
||||
Description = $"{bankAccount.BankName}, *{bankAccount.Last4} - {bankStatus}";
|
||||
NeedsVerification = bankAccount.Status is "new" or "validated";
|
||||
break;
|
||||
case Card card:
|
||||
Type = PaymentMethodType.Card;
|
||||
Description = $"{card.Brand}, *{card.Last4}, {card.ExpMonth:00}/{card.ExpYear}";
|
||||
CardBrand = card.Brand;
|
||||
break;
|
||||
case Source { Card: not null } src:
|
||||
Type = PaymentMethodType.Card;
|
||||
Description = $"{src.Card.Brand}, *{src.Card.Last4}, {src.Card.ExpMonth:00}/{src.Card.ExpYear}";
|
||||
CardBrand = src.Card.Brand;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public BillingSource(Braintree.PaymentMethod method)
|
||||
{
|
||||
switch (method)
|
||||
{
|
||||
case Braintree.PayPalAccount paypal:
|
||||
Type = PaymentMethodType.PayPal;
|
||||
Description = paypal.Email;
|
||||
break;
|
||||
case Braintree.CreditCard card:
|
||||
Type = PaymentMethodType.Card;
|
||||
Description = $"{card.CardType.ToString()}, *{card.LastFour}, " +
|
||||
$"{card.ExpirationMonth.PadLeft(2, '0')}/{card.ExpirationYear}";
|
||||
CardBrand = card.CardType.ToString();
|
||||
break;
|
||||
case Braintree.UsBankAccount bank:
|
||||
Type = PaymentMethodType.BankAccount;
|
||||
Description = $"{bank.BankName}, *{bank.Last4}";
|
||||
break;
|
||||
default:
|
||||
throw new NotSupportedException("Method not supported.");
|
||||
}
|
||||
}
|
||||
|
||||
public BillingSource(Braintree.UsBankAccountDetails bank)
|
||||
{
|
||||
Type = PaymentMethodType.BankAccount;
|
||||
Description = $"{bank.BankName}, *{bank.Last4}";
|
||||
}
|
||||
|
||||
public BillingSource(Braintree.PayPalDetails paypal)
|
||||
{
|
||||
Type = PaymentMethodType.PayPal;
|
||||
Description = paypal.PayerEmail;
|
||||
}
|
||||
|
||||
public PaymentMethodType Type { get; set; }
|
||||
public string CardBrand { get; set; }
|
||||
public string Description { get; set; }
|
||||
public bool NeedsVerification { get; set; }
|
||||
}
|
||||
}
|
@ -1,155 +0,0 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Stripe;
|
||||
|
||||
namespace Bit.Core.Models.Business;
|
||||
|
||||
public class BillingInfo
|
||||
{
|
||||
public decimal Balance { get; set; }
|
||||
public BillingSource PaymentSource { get; set; }
|
||||
public IEnumerable<BillingInvoice> Invoices { get; set; } = new List<BillingInvoice>();
|
||||
public IEnumerable<BillingTransaction> Transactions { get; set; } = new List<BillingTransaction>();
|
||||
|
||||
public class BillingSource
|
||||
{
|
||||
public BillingSource() { }
|
||||
|
||||
public BillingSource(PaymentMethod method)
|
||||
{
|
||||
if (method.Card != null)
|
||||
{
|
||||
Type = PaymentMethodType.Card;
|
||||
Description = $"{method.Card.Brand?.ToUpperInvariant()}, *{method.Card.Last4}, " +
|
||||
string.Format("{0}/{1}",
|
||||
string.Concat(method.Card.ExpMonth < 10 ?
|
||||
"0" : string.Empty, method.Card.ExpMonth),
|
||||
method.Card.ExpYear);
|
||||
CardBrand = method.Card.Brand;
|
||||
}
|
||||
}
|
||||
|
||||
public BillingSource(IPaymentSource source)
|
||||
{
|
||||
if (source is BankAccount bankAccount)
|
||||
{
|
||||
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;
|
||||
}
|
||||
else if (source is Source src && src.Card != null)
|
||||
{
|
||||
Type = PaymentMethodType.Card;
|
||||
Description = $"{src.Card.Brand}, *{src.Card.Last4}, " +
|
||||
string.Format("{0}/{1}",
|
||||
string.Concat(src.Card.ExpMonth < 10 ?
|
||||
"0" : string.Empty, src.Card.ExpMonth),
|
||||
src.Card.ExpYear);
|
||||
CardBrand = src.Card.Brand;
|
||||
}
|
||||
}
|
||||
|
||||
public BillingSource(Braintree.PaymentMethod method)
|
||||
{
|
||||
if (method is Braintree.PayPalAccount paypal)
|
||||
{
|
||||
Type = PaymentMethodType.PayPal;
|
||||
Description = paypal.Email;
|
||||
}
|
||||
else if (method is Braintree.CreditCard card)
|
||||
{
|
||||
Type = PaymentMethodType.Card;
|
||||
Description = $"{card.CardType.ToString()}, *{card.LastFour}, " +
|
||||
string.Format("{0}/{1}",
|
||||
string.Concat(card.ExpirationMonth.Length == 1 ?
|
||||
"0" : string.Empty, card.ExpirationMonth),
|
||||
card.ExpirationYear);
|
||||
CardBrand = card.CardType.ToString();
|
||||
}
|
||||
else if (method is Braintree.UsBankAccount bank)
|
||||
{
|
||||
Type = PaymentMethodType.BankAccount;
|
||||
Description = $"{bank.BankName}, *{bank.Last4}";
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotSupportedException("Method not supported.");
|
||||
}
|
||||
}
|
||||
|
||||
public BillingSource(Braintree.UsBankAccountDetails bank)
|
||||
{
|
||||
Type = PaymentMethodType.BankAccount;
|
||||
Description = $"{bank.BankName}, *{bank.Last4}";
|
||||
}
|
||||
|
||||
public BillingSource(Braintree.PayPalDetails paypal)
|
||||
{
|
||||
Type = PaymentMethodType.PayPal;
|
||||
Description = paypal.PayerEmail;
|
||||
}
|
||||
|
||||
public PaymentMethodType Type { get; set; }
|
||||
public string CardBrand { get; set; }
|
||||
public string Description { get; set; }
|
||||
public bool NeedsVerification { get; set; }
|
||||
}
|
||||
|
||||
public class BillingTransaction
|
||||
{
|
||||
public BillingTransaction(Transaction transaction)
|
||||
{
|
||||
Id = transaction.Id;
|
||||
CreatedDate = transaction.CreationDate;
|
||||
Refunded = transaction.Refunded;
|
||||
Type = transaction.Type;
|
||||
PaymentMethodType = transaction.PaymentMethodType;
|
||||
Details = transaction.Details;
|
||||
Amount = transaction.Amount;
|
||||
RefundedAmount = transaction.RefundedAmount;
|
||||
}
|
||||
|
||||
public Guid Id { get; set; }
|
||||
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 BillingInvoice
|
||||
{
|
||||
public BillingInvoice(Invoice inv)
|
||||
{
|
||||
Date = inv.Created;
|
||||
Url = inv.HostedInvoiceUrl;
|
||||
PdfUrl = inv.InvoicePdf;
|
||||
Number = inv.Number;
|
||||
Paid = inv.Paid;
|
||||
Amount = inv.Total / 100M;
|
||||
}
|
||||
|
||||
public decimal Amount { get; set; }
|
||||
public DateTime? Date { get; set; }
|
||||
public string Url { get; set; }
|
||||
public string PdfUrl { get; set; }
|
||||
public string Number { get; set; }
|
||||
public bool Paid { get; set; }
|
||||
}
|
||||
}
|
@ -5,8 +5,8 @@ namespace Bit.Core.Repositories;
|
||||
|
||||
public interface ITransactionRepository : IRepository<Transaction, Guid>
|
||||
{
|
||||
Task<ICollection<Transaction>> GetManyByUserIdAsync(Guid userId);
|
||||
Task<ICollection<Transaction>> GetManyByOrganizationIdAsync(Guid organizationId);
|
||||
Task<ICollection<Transaction>> GetManyByProviderIdAsync(Guid providerId);
|
||||
Task<ICollection<Transaction>> GetManyByUserIdAsync(Guid userId, int? limit = null);
|
||||
Task<ICollection<Transaction>> GetManyByOrganizationIdAsync(Guid organizationId, int? limit = null);
|
||||
Task<ICollection<Transaction>> GetManyByProviderIdAsync(Guid providerId, int? limit = null);
|
||||
Task<Transaction> GetByGatewayIdAsync(GatewayType gatewayType, string gatewayId);
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Business;
|
||||
@ -43,8 +44,7 @@ public interface IPaymentService
|
||||
string paymentToken, TaxInfo taxInfo = null);
|
||||
Task<bool> CreditAccountAsync(ISubscriber subscriber, decimal creditAmount);
|
||||
Task<BillingInfo> GetBillingAsync(ISubscriber subscriber);
|
||||
Task<BillingInfo> GetBillingHistoryAsync(ISubscriber subscriber);
|
||||
Task<BillingInfo> GetBillingBalanceAndSourceAsync(ISubscriber subscriber);
|
||||
Task<BillingHistoryInfo> GetBillingHistoryAsync(ISubscriber subscriber);
|
||||
Task<SubscriptionInfo> GetSubscriptionAsync(ISubscriber subscriber);
|
||||
Task<TaxInfo> GetTaxInfoAsync(ISubscriber subscriber);
|
||||
Task SaveTaxInfoAsync(ISubscriber subscriber, TaxInfo taxInfo);
|
||||
|
@ -1,6 +1,7 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
@ -1555,20 +1556,6 @@ public class StripePaymentService : IPaymentService
|
||||
}
|
||||
|
||||
public async Task<BillingInfo> GetBillingAsync(ISubscriber subscriber)
|
||||
{
|
||||
var customer = await GetCustomerAsync(subscriber.GatewayCustomerId, GetCustomerPaymentOptions());
|
||||
var billingInfo = new BillingInfo
|
||||
{
|
||||
Balance = GetBillingBalance(customer),
|
||||
PaymentSource = await GetBillingPaymentSourceAsync(customer),
|
||||
Invoices = await GetBillingInvoicesAsync(customer),
|
||||
Transactions = await GetBillingTransactionsAsync(subscriber)
|
||||
};
|
||||
|
||||
return billingInfo;
|
||||
}
|
||||
|
||||
public async Task<BillingInfo> GetBillingBalanceAndSourceAsync(ISubscriber subscriber)
|
||||
{
|
||||
var customer = await GetCustomerAsync(subscriber.GatewayCustomerId, GetCustomerPaymentOptions());
|
||||
var billingInfo = new BillingInfo
|
||||
@ -1580,13 +1567,13 @@ public class StripePaymentService : IPaymentService
|
||||
return billingInfo;
|
||||
}
|
||||
|
||||
public async Task<BillingInfo> GetBillingHistoryAsync(ISubscriber subscriber)
|
||||
public async Task<BillingHistoryInfo> GetBillingHistoryAsync(ISubscriber subscriber)
|
||||
{
|
||||
var customer = await GetCustomerAsync(subscriber.GatewayCustomerId);
|
||||
var billingInfo = new BillingInfo
|
||||
var billingInfo = new BillingHistoryInfo
|
||||
{
|
||||
Transactions = await GetBillingTransactionsAsync(subscriber),
|
||||
Invoices = await GetBillingInvoicesAsync(customer)
|
||||
Transactions = await GetBillingTransactionsAsync(subscriber, 20),
|
||||
Invoices = await GetBillingInvoicesAsync(customer, 20)
|
||||
};
|
||||
|
||||
return billingInfo;
|
||||
@ -1936,44 +1923,66 @@ public class StripePaymentService : IPaymentService
|
||||
return customer;
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<BillingInfo.BillingTransaction>> GetBillingTransactionsAsync(ISubscriber subscriber)
|
||||
private async Task<IEnumerable<BillingHistoryInfo.BillingTransaction>> GetBillingTransactionsAsync(ISubscriber subscriber, int? limit = null)
|
||||
{
|
||||
ICollection<Transaction> transactions = null;
|
||||
if (subscriber is User)
|
||||
var transactions = subscriber switch
|
||||
{
|
||||
transactions = await _transactionRepository.GetManyByUserIdAsync(subscriber.Id);
|
||||
}
|
||||
else if (subscriber is Organization)
|
||||
{
|
||||
transactions = await _transactionRepository.GetManyByOrganizationIdAsync(subscriber.Id);
|
||||
}
|
||||
User => await _transactionRepository.GetManyByUserIdAsync(subscriber.Id, limit),
|
||||
Organization => await _transactionRepository.GetManyByOrganizationIdAsync(subscriber.Id, limit),
|
||||
_ => null
|
||||
};
|
||||
|
||||
return transactions?.OrderByDescending(i => i.CreationDate)
|
||||
.Select(t => new BillingInfo.BillingTransaction(t));
|
||||
|
||||
.Select(t => new BillingHistoryInfo.BillingTransaction(t));
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<BillingInfo.BillingInvoice>> GetBillingInvoicesAsync(Customer customer)
|
||||
private async Task<IEnumerable<BillingHistoryInfo.BillingInvoice>> GetBillingInvoicesAsync(Customer customer,
|
||||
int? limit = null)
|
||||
{
|
||||
if (customer == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var options = new StripeInvoiceListOptions
|
||||
{
|
||||
Customer = customer.Id,
|
||||
SelectAll = true
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
var invoices = await _stripeAdapter.InvoiceListAsync(options);
|
||||
var paidInvoicesTask = _stripeAdapter.InvoiceListAsync(new StripeInvoiceListOptions
|
||||
{
|
||||
Customer = customer.Id,
|
||||
SelectAll = !limit.HasValue,
|
||||
Limit = limit,
|
||||
Status = "paid"
|
||||
});
|
||||
var openInvoicesTask = _stripeAdapter.InvoiceListAsync(new StripeInvoiceListOptions
|
||||
{
|
||||
Customer = customer.Id,
|
||||
SelectAll = !limit.HasValue,
|
||||
Limit = limit,
|
||||
Status = "open"
|
||||
});
|
||||
var uncollectibleInvoicesTask = _stripeAdapter.InvoiceListAsync(new StripeInvoiceListOptions
|
||||
{
|
||||
Customer = customer.Id,
|
||||
SelectAll = !limit.HasValue,
|
||||
Limit = limit,
|
||||
Status = "uncollectible"
|
||||
});
|
||||
|
||||
return invoices
|
||||
.Where(invoice => invoice.Status != "void" && invoice.Status != "draft")
|
||||
var paidInvoices = await paidInvoicesTask;
|
||||
var openInvoices = await openInvoicesTask;
|
||||
var uncollectibleInvoices = await uncollectibleInvoicesTask;
|
||||
|
||||
var invoices = paidInvoices
|
||||
.Concat(openInvoices)
|
||||
.Concat(uncollectibleInvoices);
|
||||
|
||||
var result = invoices
|
||||
.OrderByDescending(invoice => invoice.Created)
|
||||
.Select(invoice => new BillingInfo.BillingInvoice(invoice));
|
||||
.Select(invoice => new BillingHistoryInfo.BillingInvoice(invoice));
|
||||
|
||||
return limit.HasValue
|
||||
? result.Take(limit.Value)
|
||||
: result;
|
||||
}
|
||||
catch (StripeException exception)
|
||||
{
|
||||
|
@ -18,44 +18,46 @@ public class TransactionRepository : Repository<Transaction, Guid>, ITransaction
|
||||
: base(connectionString, readOnlyConnectionString)
|
||||
{ }
|
||||
|
||||
public async Task<ICollection<Transaction>> GetManyByUserIdAsync(Guid userId)
|
||||
public async Task<ICollection<Transaction>> GetManyByUserIdAsync(Guid userId, int? limit = null)
|
||||
{
|
||||
using (var connection = new SqlConnection(ConnectionString))
|
||||
{
|
||||
var results = await connection.QueryAsync<Transaction>(
|
||||
$"[{Schema}].[Transaction_ReadByUserId]",
|
||||
new { UserId = userId },
|
||||
new { UserId = userId, Limit = limit ?? int.MaxValue },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
|
||||
return results.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ICollection<Transaction>> GetManyByOrganizationIdAsync(Guid organizationId)
|
||||
public async Task<ICollection<Transaction>> GetManyByOrganizationIdAsync(Guid organizationId, int? limit = null)
|
||||
{
|
||||
using (var connection = new SqlConnection(ConnectionString))
|
||||
{
|
||||
var results = await connection.QueryAsync<Transaction>(
|
||||
$"[{Schema}].[Transaction_ReadByOrganizationId]",
|
||||
new { OrganizationId = organizationId },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
await using var connection = new SqlConnection(ConnectionString);
|
||||
|
||||
return results.ToList();
|
||||
}
|
||||
var results = await connection.QueryAsync<Transaction>(
|
||||
$"[{Schema}].[Transaction_ReadByOrganizationId]",
|
||||
new { OrganizationId = organizationId, Limit = limit ?? int.MaxValue },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
|
||||
return results.ToList();
|
||||
}
|
||||
|
||||
public async Task<ICollection<Transaction>> GetManyByProviderIdAsync(Guid providerId)
|
||||
public async Task<ICollection<Transaction>> GetManyByProviderIdAsync(Guid providerId, int? limit = null)
|
||||
{
|
||||
await using var sqlConnection = new SqlConnection(ConnectionString);
|
||||
|
||||
var results = await sqlConnection.QueryAsync<Transaction>(
|
||||
$"[{Schema}].[Transaction_ReadByProviderId]",
|
||||
new { ProviderId = providerId },
|
||||
new { ProviderId = providerId, Limit = limit ?? int.MaxValue },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
|
||||
return results.ToList();
|
||||
}
|
||||
|
||||
public async Task<Transaction> GetByGatewayIdAsync(GatewayType gatewayType, string gatewayId)
|
||||
{
|
||||
// maybe come back to this
|
||||
using (var connection = new SqlConnection(ConnectionString))
|
||||
{
|
||||
var results = await connection.QueryAsync<Transaction>(
|
||||
|
@ -2,6 +2,7 @@
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Infrastructure.EntityFramework.Models;
|
||||
using LinqToDB;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
@ -15,46 +16,60 @@ public class TransactionRepository : Repository<Core.Entities.Transaction, Trans
|
||||
|
||||
public async Task<Core.Entities.Transaction> GetByGatewayIdAsync(GatewayType gatewayType, string gatewayId)
|
||||
{
|
||||
using (var scope = ServiceScopeFactory.CreateScope())
|
||||
{
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
var results = await dbContext.Transactions
|
||||
.FirstOrDefaultAsync(t => (t.GatewayId == gatewayId && t.Gateway == gatewayType));
|
||||
return Mapper.Map<Core.Entities.Transaction>(results);
|
||||
}
|
||||
using var scope = ServiceScopeFactory.CreateScope();
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
var results = await EntityFrameworkQueryableExtensions.FirstOrDefaultAsync(dbContext.Transactions, t => (t.GatewayId == gatewayId && t.Gateway == gatewayType));
|
||||
return Mapper.Map<Core.Entities.Transaction>(results);
|
||||
}
|
||||
|
||||
public async Task<ICollection<Core.Entities.Transaction>> GetManyByOrganizationIdAsync(Guid organizationId)
|
||||
public async Task<ICollection<Core.Entities.Transaction>> GetManyByOrganizationIdAsync(Guid organizationId, int? limit = null)
|
||||
{
|
||||
using (var scope = ServiceScopeFactory.CreateScope())
|
||||
using var scope = ServiceScopeFactory.CreateScope();
|
||||
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
var query = dbContext.Transactions
|
||||
.Where(t => t.OrganizationId == organizationId && !t.UserId.HasValue);
|
||||
|
||||
if (limit.HasValue)
|
||||
{
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
var results = await dbContext.Transactions
|
||||
.Where(t => (t.OrganizationId == organizationId && !t.UserId.HasValue))
|
||||
.ToListAsync();
|
||||
return Mapper.Map<List<Core.Entities.Transaction>>(results);
|
||||
query = query.OrderByDescending(o => o.CreationDate).Take(limit.Value);
|
||||
}
|
||||
|
||||
var results = await EntityFrameworkQueryableExtensions.ToListAsync(query);
|
||||
return Mapper.Map<List<Core.Entities.Transaction>>(results);
|
||||
}
|
||||
|
||||
public async Task<ICollection<Core.Entities.Transaction>> GetManyByUserIdAsync(Guid userId)
|
||||
public async Task<ICollection<Core.Entities.Transaction>> GetManyByUserIdAsync(Guid userId, int? limit = null)
|
||||
{
|
||||
using (var scope = ServiceScopeFactory.CreateScope())
|
||||
using var scope = ServiceScopeFactory.CreateScope();
|
||||
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
var query = dbContext.Transactions
|
||||
.Where(t => t.UserId == userId);
|
||||
|
||||
if (limit.HasValue)
|
||||
{
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
var results = await dbContext.Transactions
|
||||
.Where(t => (t.UserId == userId))
|
||||
.ToListAsync();
|
||||
return Mapper.Map<List<Core.Entities.Transaction>>(results);
|
||||
query = query.OrderByDescending(o => o.CreationDate).Take(limit.Value);
|
||||
}
|
||||
|
||||
var results = await EntityFrameworkQueryableExtensions.ToListAsync(query);
|
||||
|
||||
return Mapper.Map<List<Core.Entities.Transaction>>(results);
|
||||
}
|
||||
|
||||
public async Task<ICollection<Core.Entities.Transaction>> GetManyByProviderIdAsync(Guid providerId)
|
||||
public async Task<ICollection<Core.Entities.Transaction>> GetManyByProviderIdAsync(Guid providerId, int? limit = null)
|
||||
{
|
||||
using var serviceScope = ServiceScopeFactory.CreateScope();
|
||||
var databaseContext = GetDatabaseContext(serviceScope);
|
||||
var results = await databaseContext.Transactions
|
||||
.Where(transaction => transaction.ProviderId == providerId)
|
||||
.ToListAsync();
|
||||
var query = databaseContext.Transactions
|
||||
.Where(transaction => transaction.ProviderId == providerId);
|
||||
|
||||
if (limit.HasValue)
|
||||
{
|
||||
query = query.Take(limit.Value);
|
||||
}
|
||||
|
||||
var results = await EntityFrameworkQueryableExtensions.ToListAsync(query);
|
||||
return Mapper.Map<List<Core.Entities.Transaction>>(results);
|
||||
}
|
||||
}
|
||||
|
@ -1,14 +1,16 @@
|
||||
CREATE PROCEDURE [dbo].[Transaction_ReadByOrganizationId]
|
||||
@OrganizationId UNIQUEIDENTIFIER
|
||||
@OrganizationId UNIQUEIDENTIFIER,
|
||||
@Limit INT
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
SELECT
|
||||
*
|
||||
TOP (@Limit) *
|
||||
FROM
|
||||
[dbo].[TransactionView]
|
||||
WHERE
|
||||
[UserId] IS NULL
|
||||
AND [OrganizationId] = @OrganizationId
|
||||
[OrganizationId] = @OrganizationId
|
||||
ORDER BY
|
||||
[CreationDate] DESC
|
||||
END
|
||||
|
@ -1,13 +1,16 @@
|
||||
CREATE PROCEDURE [dbo].[Transaction_ReadByProviderId]
|
||||
@ProviderId UNIQUEIDENTIFIER
|
||||
@ProviderId UNIQUEIDENTIFIER,
|
||||
@Limit INT
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
SELECT
|
||||
*
|
||||
TOP (@Limit) *
|
||||
FROM
|
||||
[dbo].[TransactionView]
|
||||
WHERE
|
||||
[ProviderId] = @ProviderId
|
||||
ORDER BY
|
||||
[CreationDate] DESC
|
||||
END
|
||||
|
@ -1,13 +1,16 @@
|
||||
CREATE PROCEDURE [dbo].[Transaction_ReadByUserId]
|
||||
@UserId UNIQUEIDENTIFIER
|
||||
@UserId UNIQUEIDENTIFIER,
|
||||
@Limit INT
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
SELECT
|
||||
*
|
||||
TOP (@Limit) *
|
||||
FROM
|
||||
[dbo].[TransactionView]
|
||||
WHERE
|
||||
[UserId] = @UserId
|
||||
ORDER BY
|
||||
[CreationDate] DESC
|
||||
END
|
||||
|
@ -1,7 +1,7 @@
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Models.Business;
|
||||
namespace Bit.Core.Test.Billing.Models;
|
||||
|
||||
public class BillingInfoTests
|
||||
{
|
||||
@ -14,7 +14,7 @@ public class BillingInfoTests
|
||||
Total = 2000,
|
||||
};
|
||||
|
||||
var billingInvoice = new BillingInfo.BillingInvoice(invoice);
|
||||
var billingInvoice = new BillingHistoryInfo.BillingInvoice(invoice);
|
||||
|
||||
// Should have been set from Total
|
||||
Assert.Equal(20M, billingInvoice.Amount);
|
@ -0,0 +1,16 @@
|
||||
CREATE OR ALTER PROCEDURE [dbo].[Transaction_ReadByOrganizationId]
|
||||
@OrganizationId UNIQUEIDENTIFIER,
|
||||
@Limit INT
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
SELECT
|
||||
TOP (@Limit) *
|
||||
FROM
|
||||
[dbo].[TransactionView]
|
||||
WHERE
|
||||
[OrganizationId] = @OrganizationId
|
||||
ORDER BY
|
||||
[CreationDate] DESC
|
||||
END
|
@ -0,0 +1,16 @@
|
||||
CREATE OR ALTER PROCEDURE [dbo].[Transaction_ReadByUserId]
|
||||
@UserId UNIQUEIDENTIFIER,
|
||||
@Limit INT
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
SELECT
|
||||
TOP (@Limit) *
|
||||
FROM
|
||||
[dbo].[TransactionView]
|
||||
WHERE
|
||||
[UserId] = @UserId
|
||||
ORDER BY
|
||||
[CreationDate] DESC
|
||||
END
|
@ -0,0 +1,16 @@
|
||||
CREATE OR ALTER PROCEDURE [dbo].[Transaction_ReadByProviderId]
|
||||
@ProviderId UNIQUEIDENTIFIER,
|
||||
@Limit INT
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
SELECT
|
||||
TOP (@Limit) *
|
||||
FROM
|
||||
[dbo].[TransactionView]
|
||||
WHERE
|
||||
[ProviderId] = @ProviderId
|
||||
ORDER BY
|
||||
[CreationDate] DESC
|
||||
END
|
Loading…
Reference in New Issue
Block a user