From 70d8477ed02a3eada9a4d5a16c5209cc5b3c56ba Mon Sep 17 00:00:00 2001 From: Daniyar Askarov Date: Fri, 4 Feb 2022 21:11:19 +0600 Subject: [PATCH 1/8] Set active stripe account for both account and member --- .../Models/HospitalityFacility/Account.cs | 1 + .../Data/Models/HospitalityFacility/Member.cs | 1 + .../Payments/IStripeAccountService.cs | 5 +- .../Services/Payments/PaymentService.cs | 29 +-- .../Services/Payments/StripeAccountService.cs | 166 +++++++++++++++++- TipCatDotNet.ApiTests/MemberServiceTests.cs | 2 +- .../StripeAccountServiceTests.cs | 2 +- 7 files changed, 190 insertions(+), 16 deletions(-) diff --git a/TipCatDotNet.Api/Data/Models/HospitalityFacility/Account.cs b/TipCatDotNet.Api/Data/Models/HospitalityFacility/Account.cs index 89704fb..a9d166c 100644 --- a/TipCatDotNet.Api/Data/Models/HospitalityFacility/Account.cs +++ b/TipCatDotNet.Api/Data/Models/HospitalityFacility/Account.cs @@ -15,6 +15,7 @@ public class Account public string Email { get; set; } = null!; [StringLength(32)] public string Phone { get; set; } = null!; + public string StripeAccount { get; set; } = null!; public DateTime Created { get; set; } public DateTime Modified { get; set; } public bool IsActive { get; set; } diff --git a/TipCatDotNet.Api/Data/Models/HospitalityFacility/Member.cs b/TipCatDotNet.Api/Data/Models/HospitalityFacility/Member.cs index 5892628..be4ce7a 100644 --- a/TipCatDotNet.Api/Data/Models/HospitalityFacility/Member.cs +++ b/TipCatDotNet.Api/Data/Models/HospitalityFacility/Member.cs @@ -22,6 +22,7 @@ public class Member public string MemberCode { get; set; } = null!; [StringLength(64)] public string? Position { get; set; } + public string ActiveStripeId { get; set; } = null!; public string QrCodeUrl { get; set; } = null!; public MemberPermissions Permissions { get; set; } = MemberPermissions.None; public DateTime Created { get; set; } diff --git a/TipCatDotNet.Api/Services/Payments/IStripeAccountService.cs b/TipCatDotNet.Api/Services/Payments/IStripeAccountService.cs index 98dc988..b694489 100644 --- a/TipCatDotNet.Api/Services/Payments/IStripeAccountService.cs +++ b/TipCatDotNet.Api/Services/Payments/IStripeAccountService.cs @@ -1,14 +1,17 @@ using System.Threading; using System.Threading.Tasks; using CSharpFunctionalExtensions; +using TipCatDotNet.Api.Data.Models.HospitalityFacility; using TipCatDotNet.Api.Models.HospitalityFacilities; namespace TipCatDotNet.Api.Services.Payments; public interface IStripeAccountService { - Task Add(MemberRequest request, CancellationToken cancellationToken); + Task AddForMember(MemberRequest request, CancellationToken cancellationToken); + Task AddForAccount(Account request, CancellationToken cancellationToken); Task AttachDefaultExternal(PayoutMethodRequest request, CancellationToken cancellationToken); + Task SetStripeAccountActive(string accountId, int memberId, CancellationToken cancellationToken); Task> Retrieve(MemberRequest request, CancellationToken cancellationToken); Task Update(MemberRequest request, CancellationToken cancellationToken); Task Remove(int memberId, CancellationToken cancellationToken); diff --git a/TipCatDotNet.Api/Services/Payments/PaymentService.cs b/TipCatDotNet.Api/Services/Payments/PaymentService.cs index 7c90c23..c7deca2 100644 --- a/TipCatDotNet.Api/Services/Payments/PaymentService.cs +++ b/TipCatDotNet.Api/Services/Payments/PaymentService.cs @@ -49,21 +49,26 @@ Result Validate() } - async Task> GetOperatingName() + async Task> GetOperatingName() => await _context.Members .Where(m => m.Id == paymentRequest.MemberId) - .Join(_context.Accounts, m => m.AccountId, a => a.Id, (m, a) => a.OperatingName) + .Join(_context.Accounts, m => m.AccountId, a => a.Id, (m, a) + => new Tuple(a.OperatingName, m.ActiveStripeId).ToValueTuple()) .SingleAsync(); - async Task> ProceedPayment(string operatingName) + async Task> ProceedPayment((string name, string accountId) receiver) { var createOptions = new PaymentIntentCreateOptions { PaymentMethodTypes = PaymentEnums.PaymentMethodService.GetAllowed(), - Description = $"Tips left at {operatingName}", + Description = $"Tips left at {receiver.name}", Amount = ToIntegerUnits(paymentRequest.TipsAmount), Currency = paymentRequest.TipsAmount.Currency.ToString(), + TransferData = new PaymentIntentTransferDataOptions + { + Destination = receiver.accountId, + }, Metadata = new Dictionary { { "MemberId", paymentRequest.MemberId.ToString() }, @@ -182,15 +187,15 @@ async Task PerformAction(Event stripeEvent) switch (stripeEvent.Type) { case "payment_intent.created": - { - // TODO: call method for handle created event - break; - } + { + // TODO: call method for handle created event + break; + } case "payment_intent.succeeded": - { - await _transactionService.Update(paymentIntent!, null); - break; - } + { + await _transactionService.Update(paymentIntent!, null); + break; + } } return Result.Success(); diff --git a/TipCatDotNet.Api/Services/Payments/StripeAccountService.cs b/TipCatDotNet.Api/Services/Payments/StripeAccountService.cs index 5515306..3087afa 100644 --- a/TipCatDotNet.Api/Services/Payments/StripeAccountService.cs +++ b/TipCatDotNet.Api/Services/Payments/StripeAccountService.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading; using System.Threading.Tasks; using CSharpFunctionalExtensions; @@ -11,6 +12,7 @@ using TipCatDotNet.Api.Infrastructure; using TipCatDotNet.Api.Models.HospitalityFacilities; using TipCatDotNet.Api.Options; +using TipCatData = TipCatDotNet.Api.Data.Models; namespace TipCatDotNet.Api.Services.Payments; @@ -24,7 +26,7 @@ public StripeAccountService(AetherDbContext context, Stripe.AccountService accou } - public Task Add(MemberRequest request, CancellationToken cancellationToken) + public Task AddForMember(MemberRequest request, CancellationToken cancellationToken) { return Result.Success() .Bind(CreateStripeAccount) @@ -87,11 +89,126 @@ async Task CreateRelatedAccount(string accountId) await _context.SaveChangesAsync(cancellationToken); _context.DetachEntities(); + await SetStripeAccountActive(accountId, request.Id.Value, cancellationToken); + return Result.Success(); } } + public Task AddForAccount(TipCatData.HospitalityFacility.Account account, CancellationToken cancellationToken) + { + return Result.Success() + .Bind(CreateStripeAccount) + .Bind(SetStripeAccount); + + + async Task> CreateStripeAccount() + { + var options = new AccountCreateOptions + { + Country = "AE", + Type = "custom", + BusinessType = "company", + BusinessProfile = new AccountBusinessProfileOptions + { + Name = account.Name, + ProductDescription = account.OperatingName, + // Mcc = "", TODO + // SupportAddress = new AddressOptions + // { + // City = "", + // Country = "", + // Line1 = "", + // Line2 = "", + // PostalCode = "", + // State = "" + // }, + SupportPhone = account.Phone, + SupportEmail = account.Email, + }, + Company = new AccountCompanyOptions + { + // Address = new AddressOptions + // { + // City = "", + // Country = "", + // Line1 = "", + // Line2 = "", + // PostalCode = "", + // State = "" + // }, + Name = account.Name, + Phone = account.Phone, + // RegistrationNumber = "", + // TaxId = "", + // VatId = "", + // Verification = new AccountCompanyVerificationOptions + // { + // Document = new AccountCompanyVerificationDocumentOptions + // { + // Back = "", //The back of a document returned by a file upload + // Front = "" //The front of a document returned by a file upload + // } + // } + }, + Metadata = new Dictionary() + { + { "AccountId", account.Id!.ToString() ?? string.Empty }, + }, + Capabilities = new AccountCapabilitiesOptions + { + CardPayments = new AccountCapabilitiesCardPaymentsOptions + { + Requested = true, + }, + Transfers = new AccountCapabilitiesTransfersOptions + { + Requested = true, + }, + // TODO: Figure out which cababilities (Payment_methods) account requested + } + }; + try + { + var account = await _accountService.CreateAsync(options, cancellationToken: cancellationToken); + return account.Id; + } + catch (StripeException ex) + { + return Result.Failure(ex.Message); + } + } + + + async Task SetStripeAccount(string stripeAccountId) + { + account.StripeAccount = stripeAccountId; + + _context.Accounts.Update(account); + await _context.SaveChangesAsync(); + + return Result.Success(); + } + } + + + + public async Task SetStripeAccountActive(string stripeAccountId, int memberId, CancellationToken cancellationToken) + { + var targetMember = await _context.Members + .SingleAsync(m => m.Id == memberId, cancellationToken); + + targetMember.ActiveStripeId = stripeAccountId; + targetMember.Modified = DateTime.UtcNow; + + _context.Members.Update(targetMember); + await _context.SaveChangesAsync(cancellationToken); + + return Result.Success(); + } + + public async Task> Retrieve(MemberRequest request, CancellationToken cancellationToken) { var stripeAccount = await _context.StripeAccounts @@ -226,6 +343,46 @@ async Task RemoveRelatedAccount(StripeAccount account) } + private async Task> CreateStripeAccount(RelatedObjects relatedObject, int? relatedObjectId, CancellationToken cancellationToken) + { + var options = new AccountCreateOptions + { + Country = "AE", + Type = "custom", + BusinessType = "company", + BusinessProfile = new AccountBusinessProfileOptions + { + + }, + Metadata = new Dictionary() + { + { (relatedObject == RelatedObjects.Member) ? "MemberId": "AccountId", relatedObjectId!.ToString() ?? string.Empty }, + }, + Capabilities = new AccountCapabilitiesOptions + { + CardPayments = new AccountCapabilitiesCardPaymentsOptions + { + Requested = true, + }, + Transfers = new AccountCapabilitiesTransfersOptions + { + Requested = true, + }, + // TODO: Figure out which cababilities (Payment_methods) account requested + } + }; + try + { + var account = await _accountService.CreateAsync(options, cancellationToken: cancellationToken); + return account.Id; + } + catch (StripeException ex) + { + return Result.Failure(ex.Message); + } + } + + private async Task> AreAccountsMatch(int memberId, string accountId, CancellationToken cancellationToken) { try @@ -247,6 +404,13 @@ private async Task> AreAccountsMatch(int memberId, string accountId } + private enum RelatedObjects + { + Member, + Account + } + + private readonly AetherDbContext _context; private readonly Stripe.AccountService _accountService; private readonly IOptions _stripeOptions; diff --git a/TipCatDotNet.ApiTests/MemberServiceTests.cs b/TipCatDotNet.ApiTests/MemberServiceTests.cs index dc10cfc..58cc61e 100644 --- a/TipCatDotNet.ApiTests/MemberServiceTests.cs +++ b/TipCatDotNet.ApiTests/MemberServiceTests.cs @@ -53,7 +53,7 @@ public MemberServiceTests() _invitationService = invitationServiceMock.Object; var stripeAccountServiceMock = new Mock(); - stripeAccountServiceMock.Setup(s => s.Add(It.IsAny(), It.IsAny())) + stripeAccountServiceMock.Setup(s => s.AddForMember(It.IsAny(), It.IsAny())) .ReturnsAsync(Result.Success()); stripeAccountServiceMock.Setup(s => s.Update(It.IsAny(), It.IsAny())) .ReturnsAsync(Result.Success()); diff --git a/TipCatDotNet.ApiTests/StripeAccountServiceTests.cs b/TipCatDotNet.ApiTests/StripeAccountServiceTests.cs index 8687cd6..41a1222 100644 --- a/TipCatDotNet.ApiTests/StripeAccountServiceTests.cs +++ b/TipCatDotNet.ApiTests/StripeAccountServiceTests.cs @@ -51,7 +51,7 @@ public async Task Add_should_return_success() var memberRequest = new MemberRequest(1, accountId, firstName, lastName, null, MemberPermissions.Manager); var service = new StripeAccountService(_aetherDbContext, _stripeAccountService, It.IsAny>()); - var (_, isFailure) = await service.Add(memberRequest, It.IsAny()); + var (_, isFailure) = await service.AddForMember(memberRequest, It.IsAny()); var isRelatedAccountCreate = await _aetherDbContext.StripeAccounts .AnyAsync(s => s.MemberId == 1); From e7252ac0aec6e9e5088b5edb769f03e60c956bd6 Mon Sep 17 00:00:00 2001 From: Daniyar Askarov Date: Fri, 4 Feb 2022 21:32:10 +0600 Subject: [PATCH 2/8] Bug and test fix --- .../HospitalityFacilities/AccountService.cs | 13 +++-- .../Services/Payments/StripeAccountService.cs | 48 ------------------- TipCatDotNet.ApiTests/AccountServiceTests.cs | 48 +++++++++++-------- 3 files changed, 37 insertions(+), 72 deletions(-) diff --git a/TipCatDotNet.Api/Services/HospitalityFacilities/AccountService.cs b/TipCatDotNet.Api/Services/HospitalityFacilities/AccountService.cs index 69c351f..353f0f4 100644 --- a/TipCatDotNet.Api/Services/HospitalityFacilities/AccountService.cs +++ b/TipCatDotNet.Api/Services/HospitalityFacilities/AccountService.cs @@ -12,16 +12,18 @@ using TipCatDotNet.Api.Infrastructure.FunctionalExtensions; using TipCatDotNet.Api.Models.HospitalityFacilities; using TipCatDotNet.Api.Models.HospitalityFacilities.Validators; +using TipCatDotNet.Api.Services.Payments; namespace TipCatDotNet.Api.Services.HospitalityFacilities; public class AccountService : IAccountService { - public AccountService(AetherDbContext context, IMemberContextCacheService memberContextCacheService, IFacilityService facilityService) + public AccountService(AetherDbContext context, IStripeAccountService stripeAccountService, IMemberContextCacheService memberContextCacheService, IFacilityService facilityService) { _context = context; _memberContextCacheService = memberContextCacheService; _facilityService = facilityService; + _stripeAccountService = stripeAccountService; } @@ -64,6 +66,8 @@ async Task> AddAccount() _context.Accounts.Add(newAccount); await _context.SaveChangesAsync(cancellationToken); + await _stripeAccountService.AddForAccount(newAccount, cancellationToken); + return newAccount; } @@ -71,8 +75,8 @@ async Task> AddAccount() { var (_, isFailure, facilityId) = await _facilityService.AddDefault(account.Id, account.OperatingName, cancellationToken); - return isFailure - ? Result.Failure<(int, int)>("Default facility hadn't been created.") + return isFailure + ? Result.Failure<(int, int)>("Default facility hadn't been created.") : (account.Id, facilityId); } @@ -159,7 +163,7 @@ private async Task> GetAccount(int accountId, List> AccountProjection(List facilities) => a => new AccountResponse(a.Id, a.Name, a.OperatingName, a.Address, a.AvatarUrl, a.Email, a.Phone, a.IsActive, facilities); @@ -167,4 +171,5 @@ private static Expression> AccountProjection(List private readonly AetherDbContext _context; private readonly IMemberContextCacheService _memberContextCacheService; private readonly IFacilityService _facilityService; + private readonly IStripeAccountService _stripeAccountService; } \ No newline at end of file diff --git a/TipCatDotNet.Api/Services/Payments/StripeAccountService.cs b/TipCatDotNet.Api/Services/Payments/StripeAccountService.cs index 3087afa..7f62bec 100644 --- a/TipCatDotNet.Api/Services/Payments/StripeAccountService.cs +++ b/TipCatDotNet.Api/Services/Payments/StripeAccountService.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Threading; using System.Threading.Tasks; using CSharpFunctionalExtensions; @@ -343,46 +342,6 @@ async Task RemoveRelatedAccount(StripeAccount account) } - private async Task> CreateStripeAccount(RelatedObjects relatedObject, int? relatedObjectId, CancellationToken cancellationToken) - { - var options = new AccountCreateOptions - { - Country = "AE", - Type = "custom", - BusinessType = "company", - BusinessProfile = new AccountBusinessProfileOptions - { - - }, - Metadata = new Dictionary() - { - { (relatedObject == RelatedObjects.Member) ? "MemberId": "AccountId", relatedObjectId!.ToString() ?? string.Empty }, - }, - Capabilities = new AccountCapabilitiesOptions - { - CardPayments = new AccountCapabilitiesCardPaymentsOptions - { - Requested = true, - }, - Transfers = new AccountCapabilitiesTransfersOptions - { - Requested = true, - }, - // TODO: Figure out which cababilities (Payment_methods) account requested - } - }; - try - { - var account = await _accountService.CreateAsync(options, cancellationToken: cancellationToken); - return account.Id; - } - catch (StripeException ex) - { - return Result.Failure(ex.Message); - } - } - - private async Task> AreAccountsMatch(int memberId, string accountId, CancellationToken cancellationToken) { try @@ -404,13 +363,6 @@ private async Task> AreAccountsMatch(int memberId, string accountId } - private enum RelatedObjects - { - Member, - Account - } - - private readonly AetherDbContext _context; private readonly Stripe.AccountService _accountService; private readonly IOptions _stripeOptions; diff --git a/TipCatDotNet.ApiTests/AccountServiceTests.cs b/TipCatDotNet.ApiTests/AccountServiceTests.cs index 132c8d1..229004f 100644 --- a/TipCatDotNet.ApiTests/AccountServiceTests.cs +++ b/TipCatDotNet.ApiTests/AccountServiceTests.cs @@ -9,6 +9,7 @@ using TipCatDotNet.Api.Data.Models.HospitalityFacility; using TipCatDotNet.Api.Models.HospitalityFacilities; using TipCatDotNet.Api.Services.HospitalityFacilities; +using TipCatDotNet.Api.Services.Payments; using TipCatDotNet.ApiTests.Utils; using Microsoft.EntityFrameworkCore; using TipCatDotNet.Api.Services; @@ -35,13 +36,19 @@ public AccountServiceTests() .ReturnsAsync(new List()); _facilityService = facilityServiceMock.Object; + + var stripeAccountServiceMock = new Mock(); + stripeAccountServiceMock.Setup(c => c.AddForAccount(It.IsAny(), It.IsAny())) + .ReturnsAsync(Result.Success()); + + _stripeAccountService = stripeAccountServiceMock.Object; } [Fact] public async Task Add_should_not_add_account_when_member_has_one() { - var service = new AccountService(_aetherDbContext, _memberContextCacheService, _facilityService); + var service = new AccountService(_aetherDbContext, _stripeAccountService, _memberContextCacheService, _facilityService); var (_, isFailure) = await service.Add(new MemberContext(1, "hash", 1, string.Empty), new AccountRequest()); @@ -54,7 +61,7 @@ public async Task Add_should_not_add_account_when_name_is_not_specified() { var accountRequest = new AccountRequest(null, string.Empty, string.Empty); var memberContext = new MemberContext(1, "hash", null, string.Empty); - var service = new AccountService(_aetherDbContext, _memberContextCacheService, _facilityService); + var service = new AccountService(_aetherDbContext, _stripeAccountService, _memberContextCacheService, _facilityService); var (_, isFailure) = await service.Add(memberContext, accountRequest); @@ -67,7 +74,7 @@ public async Task Add_should_not_add_account_when_address_is_not_specified() { var accountRequest = new AccountRequest(null, string.Empty, "Tipcat.net"); var memberContext = new MemberContext(1, "hash", null, string.Empty); - var service = new AccountService(_aetherDbContext, _memberContextCacheService, _facilityService); + var service = new AccountService(_aetherDbContext, _stripeAccountService, _memberContextCacheService, _facilityService); var (_, isFailure) = await service.Add(memberContext, accountRequest); @@ -80,7 +87,7 @@ public async Task Add_should_not_add_account_when_phone_and_email_are_not_specif { var accountRequest = new AccountRequest(null, "Dubai, Saraya Avenue Building, B2, 205", "Tipcat.net"); var memberContext = new MemberContext(1, "hash", null, string.Empty); - var service = new AccountService(_aetherDbContext, _memberContextCacheService, _facilityService); + var service = new AccountService(_aetherDbContext, _stripeAccountService, _memberContextCacheService, _facilityService); var (_, isFailure) = await service.Add(memberContext, accountRequest); @@ -93,7 +100,7 @@ public async Task Add_should_add_account_when_phone_is_specified() { var request = new AccountRequest(null, "Dubai, Saraya Avenue Building, B2, 205", "Tipcat.net", null, null, "+8 (800) 2000 500"); var memberContext = new MemberContext(1, "hash", null, string.Empty); - var service = new AccountService(_aetherDbContext, _memberContextCacheService, _facilityService); + var service = new AccountService(_aetherDbContext, _stripeAccountService, _memberContextCacheService, _facilityService); var (_, isFailure, response) = await service.Add(memberContext, request); @@ -111,7 +118,7 @@ public async Task Add_should_add_account_when_email_is_specified() { var request = new AccountRequest(null, "Dubai, Saraya Avenue Building, B2, 205", "Tipcat.net", email: "kirill.taran@tipcat.net"); var memberContext = new MemberContext(1, "hash", null, string.Empty); - var service = new AccountService(_aetherDbContext, _memberContextCacheService, _facilityService); + var service = new AccountService(_aetherDbContext, _stripeAccountService, _memberContextCacheService, _facilityService); var (_, isFailure, response) = await service.Add(memberContext, request); @@ -129,11 +136,11 @@ public async Task Add_should_add_account() { var request = new AccountRequest(null, "Dubai, Saraya Avenue Building, B2, 205", "Tipcat.net", null, null, "+8 (800) 2000 500"); var memberContext = new MemberContext(1, "hash", null, "kirill.taran@tipcat.net"); - var service = new AccountService(_aetherDbContext, _memberContextCacheService, _facilityService); + var service = new AccountService(_aetherDbContext, _stripeAccountService, _memberContextCacheService, _facilityService); var (_, isFailure, response) = await service.Add(memberContext, request); - Assert.False(isFailure); + Assert.False(isFailure); Assert.Equal(request.Name, response.Name); Assert.Equal(request.Address, response.Address); Assert.Equal(memberContext.Email, response.Email); @@ -161,8 +168,8 @@ public async Task Add_should_create_default_facility() return new List(); })); - - var service = new AccountService(_aetherDbContext, _memberContextCacheService, facilityServiceMock.Object); + + var service = new AccountService(_aetherDbContext, _stripeAccountService, _memberContextCacheService, facilityServiceMock.Object); var (_, isFailure, response) = await service.Add(memberContext, request); var defaultFacility = await _aetherDbContext.Facilities @@ -179,7 +186,7 @@ public async Task Get_should_not_get_account_when_context_has_no_account_ids() { const int accountId = 1; var memberContext = new MemberContext(1, "hash", null, string.Empty); - var service = new AccountService(_aetherDbContext, _memberContextCacheService, _facilityService); + var service = new AccountService(_aetherDbContext, _stripeAccountService, _memberContextCacheService, _facilityService); var (_, isFailure) = await service.Get(memberContext, accountId); @@ -192,7 +199,7 @@ public async Task Get_should_not_get_account_when_member_has_no_access_to_accoun { const int accountId = 1; var memberContext = new MemberContext(1, "hash", 0, string.Empty); - var service = new AccountService(_aetherDbContext, _memberContextCacheService, _facilityService); + var service = new AccountService(_aetherDbContext, _stripeAccountService, _memberContextCacheService, _facilityService); var (_, isFailure) = await service.Get(memberContext, accountId); @@ -205,7 +212,7 @@ public async Task Get_should_not_get_account_when_account_deactivated() { const int accountId = 1; var memberContext = new MemberContext(1, "hash", 1, string.Empty); - var service = new AccountService(_aetherDbContext, _memberContextCacheService, _facilityService); + var service = new AccountService(_aetherDbContext, _stripeAccountService, _memberContextCacheService, _facilityService); var (_, isFailure) = await service.Get(memberContext, accountId); @@ -218,7 +225,7 @@ public async Task Get_should_get_account() { const int accountId = 2; var memberContext = new MemberContext(1, "hash", accountId, string.Empty); - var service = new AccountService(_aetherDbContext, _memberContextCacheService, _facilityService); + var service = new AccountService(_aetherDbContext, _stripeAccountService, _memberContextCacheService, _facilityService); var (_, _, accountInfo) = await service.Get(memberContext, accountId); @@ -237,7 +244,7 @@ public async Task Get_should_get_account() public async Task Update_should_not_update_if_account_request_has_no_id() { var memberContext = new MemberContext(1, "hash", null, string.Empty); - var service = new AccountService(_aetherDbContext, _memberContextCacheService, _facilityService); + var service = new AccountService(_aetherDbContext, _stripeAccountService, _memberContextCacheService, _facilityService); var (_, isFailure) = await service.Update(memberContext, new AccountRequest()); @@ -252,7 +259,7 @@ public async Task Update_should_not_update_if_request_id_and_context_id_do_not_m { var memberContext = new MemberContext(1, "hash", accountId, string.Empty); var accountRequest = new AccountRequest(2, string.Empty, string.Empty, string.Empty, string.Empty, string.Empty); - var service = new AccountService(_aetherDbContext, _memberContextCacheService, _facilityService); + var service = new AccountService(_aetherDbContext, _stripeAccountService, _memberContextCacheService, _facilityService); var (_, isFailure) = await service.Update(memberContext, accountRequest); @@ -266,7 +273,7 @@ public async Task Update_should_not_update_if_account_name_is_empty() const int accountId = 2; var memberContext = new MemberContext(1, "hash", accountId, string.Empty); var accountRequest = new AccountRequest(accountId, string.Empty, string.Empty, string.Empty, string.Empty, string.Empty); - var service = new AccountService(_aetherDbContext, _memberContextCacheService, _facilityService); + var service = new AccountService(_aetherDbContext, _stripeAccountService, _memberContextCacheService, _facilityService); var (_, isFailure) = await service.Update(memberContext, accountRequest); @@ -280,7 +287,7 @@ public async Task Update_should_not_update_if_account_address_is_empty() const int accountId = 2; var memberContext = new MemberContext(1, "hash", accountId, string.Empty); var accountRequest = new AccountRequest(accountId, string.Empty, "Tipcat.net", string.Empty, string.Empty, string.Empty); - var service = new AccountService(_aetherDbContext, _memberContextCacheService, _facilityService); + var service = new AccountService(_aetherDbContext, _stripeAccountService, _memberContextCacheService, _facilityService); var (_, isFailure) = await service.Update(memberContext, accountRequest); @@ -294,7 +301,7 @@ public async Task Update_should_not_update_if_account_phone_is_empty() const int accountId = 2; var memberContext = new MemberContext(1, "hash", accountId, string.Empty); var accountRequest = new AccountRequest(accountId, "Dubai, Saraya Avenue Building, B2, 205", "Tipcat.net", string.Empty, string.Empty, string.Empty); - var service = new AccountService(_aetherDbContext, _memberContextCacheService, _facilityService); + var service = new AccountService(_aetherDbContext, _stripeAccountService, _memberContextCacheService, _facilityService); var (_, isFailure) = await service.Update(memberContext, accountRequest); @@ -312,7 +319,7 @@ public async Task Update_should_update_account() var memberContext = new MemberContext(1, "hash", accountId, string.Empty); var accountRequest = new AccountRequest(accountId, address, name, string.Empty, string.Empty, phone); - var service = new AccountService(_aetherDbContext, _memberContextCacheService, _facilityService); + var service = new AccountService(_aetherDbContext, _stripeAccountService, _memberContextCacheService, _facilityService); var (_, _, account) = await service.Update(memberContext, accountRequest); @@ -370,4 +377,5 @@ public async Task Update_should_update_account() private readonly AetherDbContext _aetherDbContext; private readonly IMemberContextCacheService _memberContextCacheService; private readonly IFacilityService _facilityService; + private readonly IStripeAccountService _stripeAccountService; } \ No newline at end of file From ebedeca3c253c410c4fdba4c537f864568147d76 Mon Sep 17 00:00:00 2001 From: Daniyar Askarov Date: Wed, 9 Feb 2022 19:43:28 +0600 Subject: [PATCH 3/8] Set organization's stripe account as active for new members --- .../HospitalityFacilities/AccountService.cs | 2 +- .../HospitalityFacilities/MemberService.cs | 24 +++++++++++++++---- .../Payments/IStripeAccountService.cs | 4 ++-- .../Services/Payments/StripeAccountService.cs | 15 ++++++------ TipCatDotNet.ApiTests/AccountServiceTests.cs | 2 +- TipCatDotNet.ApiTests/MemberServiceTests.cs | 12 ++++++++-- 6 files changed, 41 insertions(+), 18 deletions(-) diff --git a/TipCatDotNet.Api/Services/HospitalityFacilities/AccountService.cs b/TipCatDotNet.Api/Services/HospitalityFacilities/AccountService.cs index 353f0f4..35a5fb3 100644 --- a/TipCatDotNet.Api/Services/HospitalityFacilities/AccountService.cs +++ b/TipCatDotNet.Api/Services/HospitalityFacilities/AccountService.cs @@ -66,7 +66,7 @@ async Task> AddAccount() _context.Accounts.Add(newAccount); await _context.SaveChangesAsync(cancellationToken); - await _stripeAccountService.AddForAccount(newAccount, cancellationToken); + await _stripeAccountService.AddForAccountAndManager(context.Id, newAccount, cancellationToken); return newAccount; } diff --git a/TipCatDotNet.Api/Services/HospitalityFacilities/MemberService.cs b/TipCatDotNet.Api/Services/HospitalityFacilities/MemberService.cs index 7c6d9a7..9681c6e 100644 --- a/TipCatDotNet.Api/Services/HospitalityFacilities/MemberService.cs +++ b/TipCatDotNet.Api/Services/HospitalityFacilities/MemberService.cs @@ -296,12 +296,26 @@ private async Task> AddMemberInternal(string identityHash, int? acco await _context.SaveChangesAsync(cancellationToken); _context.DetachEntities(); - // var (_, isFailure, error) = await _stripeAccountService - // .Add(new MemberRequest(newMember.Id, accountId, firstName, lastName, email, permissions, position), cancellationToken); - // if (isFailure) - // return Result.Failure(error); + if (accountId is null) + return newMember.Id; - return newMember.Id; + return await SetStripeAccoint(newMember.Id); + + + async Task> SetStripeAccoint(int memberId) + { + var stripeAccountId = await _context.Accounts + .Where(a => a.Id == accountId) + .Select(a => a.StripeAccount) + .SingleAsync(); + + var (_, isFailure, error) = await _stripeAccountService + .SetActiveStripeAccount(stripeAccountId, memberId, cancellationToken); + if (isFailure) + return Result.Failure(error); + + return Result.Success(memberId); + } async Task GetFacilityId() diff --git a/TipCatDotNet.Api/Services/Payments/IStripeAccountService.cs b/TipCatDotNet.Api/Services/Payments/IStripeAccountService.cs index b694489..02f15ba 100644 --- a/TipCatDotNet.Api/Services/Payments/IStripeAccountService.cs +++ b/TipCatDotNet.Api/Services/Payments/IStripeAccountService.cs @@ -9,9 +9,9 @@ namespace TipCatDotNet.Api.Services.Payments; public interface IStripeAccountService { Task AddForMember(MemberRequest request, CancellationToken cancellationToken); - Task AddForAccount(Account request, CancellationToken cancellationToken); + Task AddForAccountAndManager(int memberId, Account request, CancellationToken cancellationToken); Task AttachDefaultExternal(PayoutMethodRequest request, CancellationToken cancellationToken); - Task SetStripeAccountActive(string accountId, int memberId, CancellationToken cancellationToken); + Task SetActiveStripeAccount(string accountId, int memberId, CancellationToken cancellationToken); Task> Retrieve(MemberRequest request, CancellationToken cancellationToken); Task Update(MemberRequest request, CancellationToken cancellationToken); Task Remove(int memberId, CancellationToken cancellationToken); diff --git a/TipCatDotNet.Api/Services/Payments/StripeAccountService.cs b/TipCatDotNet.Api/Services/Payments/StripeAccountService.cs index 7f62bec..85c3718 100644 --- a/TipCatDotNet.Api/Services/Payments/StripeAccountService.cs +++ b/TipCatDotNet.Api/Services/Payments/StripeAccountService.cs @@ -88,18 +88,19 @@ async Task CreateRelatedAccount(string accountId) await _context.SaveChangesAsync(cancellationToken); _context.DetachEntities(); - await SetStripeAccountActive(accountId, request.Id.Value, cancellationToken); + await SetActiveStripeAccount(accountId, request.Id.Value, cancellationToken); return Result.Success(); } } - public Task AddForAccount(TipCatData.HospitalityFacility.Account account, CancellationToken cancellationToken) + public Task AddForAccountAndManager(int memberId, TipCatData.HospitalityFacility.Account account, CancellationToken cancellationToken) { return Result.Success() .Bind(CreateStripeAccount) - .Bind(SetStripeAccount); + .Bind(SetStripeAccount) + .Bind(stripeAccountId => SetActiveStripeAccount(stripeAccountId, memberId, cancellationToken)); async Task> CreateStripeAccount() @@ -180,23 +181,23 @@ async Task> CreateStripeAccount() } - async Task SetStripeAccount(string stripeAccountId) + async Task> SetStripeAccount(string stripeAccountId) { account.StripeAccount = stripeAccountId; _context.Accounts.Update(account); await _context.SaveChangesAsync(); - return Result.Success(); + return Result.Success(stripeAccountId); } } - public async Task SetStripeAccountActive(string stripeAccountId, int memberId, CancellationToken cancellationToken) + public async Task SetActiveStripeAccount(string stripeAccountId, int memberId, CancellationToken cancellationToken) { var targetMember = await _context.Members - .SingleAsync(m => m.Id == memberId, cancellationToken); + .SingleAsync(m => m.Id == memberId, cancellationToken); targetMember.ActiveStripeId = stripeAccountId; targetMember.Modified = DateTime.UtcNow; diff --git a/TipCatDotNet.ApiTests/AccountServiceTests.cs b/TipCatDotNet.ApiTests/AccountServiceTests.cs index 229004f..7e37d7a 100644 --- a/TipCatDotNet.ApiTests/AccountServiceTests.cs +++ b/TipCatDotNet.ApiTests/AccountServiceTests.cs @@ -38,7 +38,7 @@ public AccountServiceTests() _facilityService = facilityServiceMock.Object; var stripeAccountServiceMock = new Mock(); - stripeAccountServiceMock.Setup(c => c.AddForAccount(It.IsAny(), It.IsAny())) + stripeAccountServiceMock.Setup(c => c.AddForAccountAndManager(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(Result.Success()); _stripeAccountService = stripeAccountServiceMock.Object; diff --git a/TipCatDotNet.ApiTests/MemberServiceTests.cs b/TipCatDotNet.ApiTests/MemberServiceTests.cs index 58cc61e..580e9d1 100644 --- a/TipCatDotNet.ApiTests/MemberServiceTests.cs +++ b/TipCatDotNet.ApiTests/MemberServiceTests.cs @@ -757,12 +757,20 @@ public async Task Update_should_update_member() new Account { Id = 1, - IsActive = false + IsActive = false, + StripeAccount = "acc_1" }, new Account { Id = 2, - IsActive = true + IsActive = true, + StripeAccount = "acc_2" + }, + new Account + { + Id = 5, + IsActive = true, + StripeAccount = "acc_7" } }; From 5ae4fa91105a9fffaedb1f6141a3cf47d0a2a210 Mon Sep 17 00:00:00 2001 From: Daniyar Askarov Date: Wed, 9 Feb 2022 22:11:38 +0600 Subject: [PATCH 4/8] Took nodes from review and migration was added --- ...dBothForOrganizationAndMembers.Designer.cs | 337 ++++++++++++++++++ ...sWereAddedBothForOrganizationAndMembers.cs | 37 ++ .../AetherDbContextModelSnapshot.cs | 8 + .../Payments/IStripeAccountService.cs | 2 +- .../Services/Payments/StripeAccountService.cs | 2 +- 5 files changed, 384 insertions(+), 2 deletions(-) create mode 100644 TipCatDotNet.Api/Migrations/20220209161037_ActiveStripeAccountsWereAddedBothForOrganizationAndMembers.Designer.cs create mode 100644 TipCatDotNet.Api/Migrations/20220209161037_ActiveStripeAccountsWereAddedBothForOrganizationAndMembers.cs diff --git a/TipCatDotNet.Api/Migrations/20220209161037_ActiveStripeAccountsWereAddedBothForOrganizationAndMembers.Designer.cs b/TipCatDotNet.Api/Migrations/20220209161037_ActiveStripeAccountsWereAddedBothForOrganizationAndMembers.Designer.cs new file mode 100644 index 0000000..d4449e5 --- /dev/null +++ b/TipCatDotNet.Api/Migrations/20220209161037_ActiveStripeAccountsWereAddedBothForOrganizationAndMembers.Designer.cs @@ -0,0 +1,337 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using TipCatDotNet.Api.Data; + +#nullable disable + +namespace TipCatDotNet.Api.Migrations +{ + [DbContext(typeof(AetherDbContext))] + [Migration("20220209161037_ActiveStripeAccountsWereAddedBothForOrganizationAndMembers")] + partial class ActiveStripeAccountsWereAddedBothForOrganizationAndMembers + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.1") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("TipCatDotNet.Api.Data.Analitics.AccountStats", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("integer"); + + b.Property("AmountPerDay") + .HasColumnType("numeric"); + + b.Property("CurrentDate") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Modified") + .HasColumnType("timestamp with time zone"); + + b.Property("TotalAmount") + .HasColumnType("numeric"); + + b.Property("TransactionsCount") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("AccountsStats"); + }); + + modelBuilder.Entity("TipCatDotNet.Api.Data.Models.Auth.MemberInvitation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("text"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Link") + .HasColumnType("text"); + + b.Property("MemberId") + .HasColumnType("integer"); + + b.Property("Modified") + .HasColumnType("timestamp with time zone"); + + b.Property("State") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("MemberInvitations"); + }); + + modelBuilder.Entity("TipCatDotNet.Api.Data.Models.HospitalityFacility.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Address") + .IsRequired() + .HasColumnType("text"); + + b.Property("AvatarUrl") + .HasColumnType("text"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Modified") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OperatingName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Phone") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("StripeAccount") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Accounts"); + }); + + modelBuilder.Entity("TipCatDotNet.Api.Data.Models.HospitalityFacility.Facility", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("integer"); + + b.Property("Address") + .IsRequired() + .HasColumnType("text"); + + b.Property("AvatarUrl") + .HasColumnType("text"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDefault") + .HasColumnType("boolean"); + + b.Property("Modified") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("SessionEndTime") + .HasColumnType("time without time zone"); + + b.HasKey("Id"); + + b.ToTable("Facilities"); + }); + + modelBuilder.Entity("TipCatDotNet.Api.Data.Models.HospitalityFacility.Member", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("integer"); + + b.Property("ActiveStripeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("AvatarUrl") + .HasColumnType("text"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("FacilityId") + .HasColumnType("integer"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("IdentityHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("MemberCode") + .IsRequired() + .HasMaxLength(16) + .HasColumnType("character varying(16)"); + + b.Property("Modified") + .HasColumnType("timestamp with time zone"); + + b.Property("Permissions") + .HasColumnType("integer"); + + b.Property("Position") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("QrCodeUrl") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Members"); + }); + + modelBuilder.Entity("TipCatDotNet.Api.Data.Models.Payment.Transaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasColumnType("numeric"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("text"); + + b.Property("FacilityId") + .HasColumnType("integer"); + + b.Property("MemberId") + .HasColumnType("integer"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Modified") + .HasColumnType("timestamp with time zone"); + + b.Property("PaymentIntentId") + .IsRequired() + .HasColumnType("text"); + + b.Property("State") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Amount"); + + NpgsqlIndexBuilderExtensions.HasSortOrder(b.HasIndex("Amount"), new[] { SortOrder.Ascending, SortOrder.Descending }); + + b.HasIndex("Created"); + + NpgsqlIndexBuilderExtensions.HasSortOrder(b.HasIndex("Created"), new[] { SortOrder.Ascending, SortOrder.Descending }); + + b.ToTable("Transactions"); + }); + + modelBuilder.Entity("TipCatDotNet.Api.Data.Models.Stripe.StripeAccount", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("LastPaidOut") + .HasColumnType("timestamp with time zone"); + + b.Property("LastReceived") + .HasColumnType("timestamp with time zone"); + + b.Property("MemberId") + .HasColumnType("integer"); + + b.Property("StripeId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("StripeAccounts"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/TipCatDotNet.Api/Migrations/20220209161037_ActiveStripeAccountsWereAddedBothForOrganizationAndMembers.cs b/TipCatDotNet.Api/Migrations/20220209161037_ActiveStripeAccountsWereAddedBothForOrganizationAndMembers.cs new file mode 100644 index 0000000..7e58f64 --- /dev/null +++ b/TipCatDotNet.Api/Migrations/20220209161037_ActiveStripeAccountsWereAddedBothForOrganizationAndMembers.cs @@ -0,0 +1,37 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace TipCatDotNet.Api.Migrations +{ + public partial class ActiveStripeAccountsWereAddedBothForOrganizationAndMembers : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ActiveStripeId", + table: "Members", + type: "text", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "StripeAccount", + table: "Accounts", + type: "text", + nullable: false, + defaultValue: ""); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ActiveStripeId", + table: "Members"); + + migrationBuilder.DropColumn( + name: "StripeAccount", + table: "Accounts"); + } + } +} diff --git a/TipCatDotNet.Api/Migrations/AetherDbContextModelSnapshot.cs b/TipCatDotNet.Api/Migrations/AetherDbContextModelSnapshot.cs index 671fe90..d153dd5 100644 --- a/TipCatDotNet.Api/Migrations/AetherDbContextModelSnapshot.cs +++ b/TipCatDotNet.Api/Migrations/AetherDbContextModelSnapshot.cs @@ -130,6 +130,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasMaxLength(32) .HasColumnType("character varying(32)"); + b.Property("StripeAccount") + .IsRequired() + .HasColumnType("text"); + b.HasKey("Id"); b.ToTable("Accounts"); @@ -188,6 +192,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("AccountId") .HasColumnType("integer"); + b.Property("ActiveStripeId") + .IsRequired() + .HasColumnType("text"); + b.Property("AvatarUrl") .HasColumnType("text"); diff --git a/TipCatDotNet.Api/Services/Payments/IStripeAccountService.cs b/TipCatDotNet.Api/Services/Payments/IStripeAccountService.cs index 02f15ba..8474f7a 100644 --- a/TipCatDotNet.Api/Services/Payments/IStripeAccountService.cs +++ b/TipCatDotNet.Api/Services/Payments/IStripeAccountService.cs @@ -8,7 +8,7 @@ namespace TipCatDotNet.Api.Services.Payments; public interface IStripeAccountService { - Task AddForMember(MemberRequest request, CancellationToken cancellationToken); + Task Add(MemberRequest request, CancellationToken cancellationToken); Task AddForAccountAndManager(int memberId, Account request, CancellationToken cancellationToken); Task AttachDefaultExternal(PayoutMethodRequest request, CancellationToken cancellationToken); Task SetActiveStripeAccount(string accountId, int memberId, CancellationToken cancellationToken); diff --git a/TipCatDotNet.Api/Services/Payments/StripeAccountService.cs b/TipCatDotNet.Api/Services/Payments/StripeAccountService.cs index 85c3718..0106fd0 100644 --- a/TipCatDotNet.Api/Services/Payments/StripeAccountService.cs +++ b/TipCatDotNet.Api/Services/Payments/StripeAccountService.cs @@ -25,7 +25,7 @@ public StripeAccountService(AetherDbContext context, Stripe.AccountService accou } - public Task AddForMember(MemberRequest request, CancellationToken cancellationToken) + public Task Add(MemberRequest request, CancellationToken cancellationToken) { return Result.Success() .Bind(CreateStripeAccount) From 211b6a35a1cce59d59c1a4ce7dcdceff1bcd0e50 Mon Sep 17 00:00:00 2001 From: Daniyar Askarov Date: Wed, 9 Feb 2022 22:12:56 +0600 Subject: [PATCH 5/8] Tests were fixed --- TipCatDotNet.ApiTests/MemberServiceTests.cs | 2 +- TipCatDotNet.ApiTests/StripeAccountServiceTests.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/TipCatDotNet.ApiTests/MemberServiceTests.cs b/TipCatDotNet.ApiTests/MemberServiceTests.cs index 580e9d1..fa1195b 100644 --- a/TipCatDotNet.ApiTests/MemberServiceTests.cs +++ b/TipCatDotNet.ApiTests/MemberServiceTests.cs @@ -53,7 +53,7 @@ public MemberServiceTests() _invitationService = invitationServiceMock.Object; var stripeAccountServiceMock = new Mock(); - stripeAccountServiceMock.Setup(s => s.AddForMember(It.IsAny(), It.IsAny())) + stripeAccountServiceMock.Setup(s => s.Add(It.IsAny(), It.IsAny())) .ReturnsAsync(Result.Success()); stripeAccountServiceMock.Setup(s => s.Update(It.IsAny(), It.IsAny())) .ReturnsAsync(Result.Success()); diff --git a/TipCatDotNet.ApiTests/StripeAccountServiceTests.cs b/TipCatDotNet.ApiTests/StripeAccountServiceTests.cs index 41a1222..8687cd6 100644 --- a/TipCatDotNet.ApiTests/StripeAccountServiceTests.cs +++ b/TipCatDotNet.ApiTests/StripeAccountServiceTests.cs @@ -51,7 +51,7 @@ public async Task Add_should_return_success() var memberRequest = new MemberRequest(1, accountId, firstName, lastName, null, MemberPermissions.Manager); var service = new StripeAccountService(_aetherDbContext, _stripeAccountService, It.IsAny>()); - var (_, isFailure) = await service.AddForMember(memberRequest, It.IsAny()); + var (_, isFailure) = await service.Add(memberRequest, It.IsAny()); var isRelatedAccountCreate = await _aetherDbContext.StripeAccounts .AnyAsync(s => s.MemberId == 1); From 1263ec87d11d22c549fc9c3056784b5eb178b465 Mon Sep 17 00:00:00 2001 From: Daniyar Askarov Date: Thu, 10 Feb 2022 19:46:28 +0600 Subject: [PATCH 6/8] Method to change active stripe account was added --- .../Controllers/MemberController.cs | 23 +++++++ .../Payments/Enums/ActiveStripeAccountType.cs | 12 ++++ .../HospitalityFacilities/IMemberService.cs | 2 + .../HospitalityFacilities/MemberService.cs | 61 ++++++++++++++++++- TipCatDotNet.Api/TipCatDotNet.Api.xml | 9 +++ 5 files changed, 106 insertions(+), 1 deletion(-) create mode 100644 TipCatDotNet.Api/Models/Payments/Enums/ActiveStripeAccountType.cs diff --git a/TipCatDotNet.Api/Controllers/MemberController.cs b/TipCatDotNet.Api/Controllers/MemberController.cs index 368e734..c387d1d 100644 --- a/TipCatDotNet.Api/Controllers/MemberController.cs +++ b/TipCatDotNet.Api/Controllers/MemberController.cs @@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Mvc; using TipCatDotNet.Api.Infrastructure; using TipCatDotNet.Api.Models.HospitalityFacilities; +using TipCatDotNet.Api.Models.Payments.Enums; using TipCatDotNet.Api.Services; using TipCatDotNet.Api.Services.HospitalityFacilities; @@ -132,6 +133,28 @@ public async Task UpdateCurrent([FromRoute] int memberId, [FromRo } + /// + /// Updates member's active stripe account. + /// + /// Target member ID + /// Target account ID + /// Type of active stripe account + /// + [HttpPut("accounts/{accountId:int}/members/{memberId:int}/stripe-account/active")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + public async Task Update([FromRoute] int memberId, [FromRoute] int accountId, + [FromQuery] ActiveStripeAccountType accountType) + { + var (_, isFailure, memberContext, error) = await _memberContextService.Get(); + if (isFailure) + return NotFound(error); + + return NoContentOrBadRequest(await _memberService.Update(memberContext, new MemberRequest(memberId, accountId), accountType)); + } + + private readonly IMemberContextService _memberContextService; private readonly IMemberService _memberService; } \ No newline at end of file diff --git a/TipCatDotNet.Api/Models/Payments/Enums/ActiveStripeAccountType.cs b/TipCatDotNet.Api/Models/Payments/Enums/ActiveStripeAccountType.cs new file mode 100644 index 0000000..fef85c4 --- /dev/null +++ b/TipCatDotNet.Api/Models/Payments/Enums/ActiveStripeAccountType.cs @@ -0,0 +1,12 @@ +using System; +using System.Text.Json.Serialization; + +namespace TipCatDotNet.Api.Models.Payments.Enums; + +[JsonConverter(typeof(JsonStringEnumConverter))] +[Flags] +public enum ActiveStripeAccountType +{ + Organizational = 1, + Personal = 2 +} \ No newline at end of file diff --git a/TipCatDotNet.Api/Services/HospitalityFacilities/IMemberService.cs b/TipCatDotNet.Api/Services/HospitalityFacilities/IMemberService.cs index 7b94f4f..55ad59b 100644 --- a/TipCatDotNet.Api/Services/HospitalityFacilities/IMemberService.cs +++ b/TipCatDotNet.Api/Services/HospitalityFacilities/IMemberService.cs @@ -3,6 +3,7 @@ using System.Threading.Tasks; using CSharpFunctionalExtensions; using TipCatDotNet.Api.Models.HospitalityFacilities; +using TipCatDotNet.Api.Models.Payments.Enums; namespace TipCatDotNet.Api.Services.HospitalityFacilities; @@ -15,4 +16,5 @@ public interface IMemberService Task> RegenerateQr(MemberContext memberContext, int memberId, int accountId, CancellationToken cancellationToken = default); Task Remove(MemberContext memberContext, int memberId, int accountId, CancellationToken cancellationToken = default); Task> Update(MemberContext memberContext, MemberRequest request, CancellationToken cancellationToken = default); + Task Update(MemberContext memberContext, MemberRequest request, ActiveStripeAccountType accountType, CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/TipCatDotNet.Api/Services/HospitalityFacilities/MemberService.cs b/TipCatDotNet.Api/Services/HospitalityFacilities/MemberService.cs index 9681c6e..adcf649 100644 --- a/TipCatDotNet.Api/Services/HospitalityFacilities/MemberService.cs +++ b/TipCatDotNet.Api/Services/HospitalityFacilities/MemberService.cs @@ -16,6 +16,7 @@ using TipCatDotNet.Api.Models.Auth.Enums; using TipCatDotNet.Api.Models.HospitalityFacilities; using TipCatDotNet.Api.Models.HospitalityFacilities.Validators; +using TipCatDotNet.Api.Models.Payments.Enums; using TipCatDotNet.Api.Models.Permissions.Enums; using TipCatDotNet.Api.Services.Auth; using TipCatDotNet.Api.Services.Images; @@ -224,6 +225,64 @@ async Task UpdateMember() } + public Task Update(MemberContext memberContext, MemberRequest request, + ActiveStripeAccountType accountType, CancellationToken cancellationToken = default) + { + return ValidateGeneral(memberContext, request) + .Bind(DefineActiveAccount) + .Bind(UpdateActiveAccount); + + + async Task> DefineActiveAccount() + { + var activeStripeId = string.Empty; + + switch (accountType) + { + case ActiveStripeAccountType.Organizational: + { + activeStripeId = await _context.Accounts + .Where(a => a.Id == request.AccountId) + .Select(a => a.StripeAccount) + .SingleAsync(cancellationToken); + + break; + } + case ActiveStripeAccountType.Personal: + { + activeStripeId = await _context.Members + .Where(m => m.Id == request.Id) + .Select(m => m.ActiveStripeId) + .SingleAsync(cancellationToken); + + break; + } + } + + if (String.IsNullOrEmpty(activeStripeId)) + Result.Failure("Target stripe account wasn't defined!"); + + return Result.Success(activeStripeId); + } + + + async Task UpdateActiveAccount(string activeStripeId) + { + var targetMember = await _context.Members + .SingleAsync(m => m.Id == request.Id, cancellationToken); + + targetMember.ActiveStripeId = activeStripeId; + + targetMember.Modified = DateTime.UtcNow; + + _context.Members.Update(targetMember); + await _context.SaveChangesAsync(cancellationToken); + + return Result.Success(); + } + } + + private Result ValidateGeneral(MemberContext memberContext, MemberRequest request) { var validator = new MemberRequestValidator(memberContext, _context); @@ -307,7 +366,7 @@ async Task> SetStripeAccoint(int memberId) var stripeAccountId = await _context.Accounts .Where(a => a.Id == accountId) .Select(a => a.StripeAccount) - .SingleAsync(); + .SingleAsync(cancellationToken); var (_, isFailure, error) = await _stripeAccountService .SetActiveStripeAccount(stripeAccountId, memberId, cancellationToken); diff --git a/TipCatDotNet.Api/TipCatDotNet.Api.xml b/TipCatDotNet.Api/TipCatDotNet.Api.xml index 030a690..582bd28 100644 --- a/TipCatDotNet.Api/TipCatDotNet.Api.xml +++ b/TipCatDotNet.Api/TipCatDotNet.Api.xml @@ -171,6 +171,15 @@ Change request + + + Updates member's active stripe account. + + Target member ID + Target account ID + Type of active stripe account + + Gets payment details by member code. From 506e0fe9856226d92dd1664803b508da3b5069c9 Mon Sep 17 00:00:00 2001 From: Daniyar Askarov Date: Fri, 11 Feb 2022 17:42:11 +0600 Subject: [PATCH 7/8] Took nodes from review --- TipCatDotNet.Api/Controllers/MemberController.cs | 2 +- .../Models/Payments/Enums/ActiveStripeAccountType.cs | 1 + .../Services/HospitalityFacilities/MemberService.cs | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/TipCatDotNet.Api/Controllers/MemberController.cs b/TipCatDotNet.Api/Controllers/MemberController.cs index 6797899..2cc2f3f 100644 --- a/TipCatDotNet.Api/Controllers/MemberController.cs +++ b/TipCatDotNet.Api/Controllers/MemberController.cs @@ -180,7 +180,7 @@ public async Task UpdateCurrent([FromRoute] int memberId, [FromRo /// Target account ID /// Type of active stripe account /// - [HttpPut("accounts/{accountId:int}/members/{memberId:int}/stripe-account/active")] + [HttpPut("accounts/{accountId:int}/members/{memberId:int}/stripe-account/set-active")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] diff --git a/TipCatDotNet.Api/Models/Payments/Enums/ActiveStripeAccountType.cs b/TipCatDotNet.Api/Models/Payments/Enums/ActiveStripeAccountType.cs index fef85c4..c78d532 100644 --- a/TipCatDotNet.Api/Models/Payments/Enums/ActiveStripeAccountType.cs +++ b/TipCatDotNet.Api/Models/Payments/Enums/ActiveStripeAccountType.cs @@ -7,6 +7,7 @@ namespace TipCatDotNet.Api.Models.Payments.Enums; [Flags] public enum ActiveStripeAccountType { + Undefined = 0, Organizational = 1, Personal = 2 } \ No newline at end of file diff --git a/TipCatDotNet.Api/Services/HospitalityFacilities/MemberService.cs b/TipCatDotNet.Api/Services/HospitalityFacilities/MemberService.cs index 42f65b9..5aba247 100644 --- a/TipCatDotNet.Api/Services/HospitalityFacilities/MemberService.cs +++ b/TipCatDotNet.Api/Services/HospitalityFacilities/MemberService.cs @@ -273,7 +273,7 @@ public Task Update(MemberContext memberContext, MemberRequest request, async Task> DefineActiveAccount() { - var activeStripeId = string.Empty; + var activeStripeId = null as string; switch (accountType) { @@ -300,7 +300,7 @@ async Task> DefineActiveAccount() if (String.IsNullOrEmpty(activeStripeId)) Result.Failure("Target stripe account wasn't defined!"); - return Result.Success(activeStripeId); + return Result.Success(activeStripeId!); } From 539907a7b5a3c38adb8e673e41e51540730fb1af Mon Sep 17 00:00:00 2001 From: Daniyar Askarov Date: Fri, 11 Feb 2022 18:43:52 +0600 Subject: [PATCH 8/8] Fix semantic --- TipCatDotNet.Api/Controllers/MemberController.cs | 2 +- .../Services/HospitalityFacilities/MemberService.cs | 11 ++++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/TipCatDotNet.Api/Controllers/MemberController.cs b/TipCatDotNet.Api/Controllers/MemberController.cs index 2cc2f3f..0d14f31 100644 --- a/TipCatDotNet.Api/Controllers/MemberController.cs +++ b/TipCatDotNet.Api/Controllers/MemberController.cs @@ -185,7 +185,7 @@ public async Task UpdateCurrent([FromRoute] int memberId, [FromRo [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] public async Task Update([FromRoute] int memberId, [FromRoute] int accountId, - [FromQuery] ActiveStripeAccountType accountType) + [FromQuery] ActiveStripeAccountType accountType = ActiveStripeAccountType.Undefined) { var (_, isFailure, memberContext, error) = await _memberContextService.Get(); if (isFailure) diff --git a/TipCatDotNet.Api/Services/HospitalityFacilities/MemberService.cs b/TipCatDotNet.Api/Services/HospitalityFacilities/MemberService.cs index 5aba247..69cd695 100644 --- a/TipCatDotNet.Api/Services/HospitalityFacilities/MemberService.cs +++ b/TipCatDotNet.Api/Services/HospitalityFacilities/MemberService.cs @@ -273,7 +273,7 @@ public Task Update(MemberContext memberContext, MemberRequest request, async Task> DefineActiveAccount() { - var activeStripeId = null as string; + string? activeStripeId = null; switch (accountType) { @@ -295,12 +295,17 @@ async Task> DefineActiveAccount() break; } + case ActiveStripeAccountType.Undefined: + { + activeStripeId = string.Empty; + break; + } } - if (String.IsNullOrEmpty(activeStripeId)) + if (activeStripeId is null) Result.Failure("Target stripe account wasn't defined!"); - return Result.Success(activeStripeId!); + return Result.Success(activeStripeId); }