From 8cf8e6630b2f214426a1afa966babb2517fa3d94 Mon Sep 17 00:00:00 2001 From: kirinnee Date: Sun, 7 Jan 2024 15:41:31 +0800 Subject: [PATCH] feat: allow admin to topup wallet --- App/Modules/Admin/API/V1/AdminController.cs | 54 ++++++++++++++++++++ App/Modules/Admin/API/V1/AdminModel.cs | 6 +++ App/Modules/Admin/API/V1/AdminValidator.cs | 17 ++++++ App/Modules/DomainServices.cs | 5 ++ App/Modules/Wallets/Data/WalletRepository.cs | 29 +++++++++++ App/Utility/ValidationUtility.cs | 8 ++- Domain/Admin/IService.cs | 13 +++++ Domain/Admin/Service.cs | 50 ++++++++++++++++++ Domain/Domain.csproj | 4 -- Domain/Transaction/Generator.cs | 49 +++++++++++++++++- Domain/Wallet/Repository.cs | 3 ++ 11 files changed, 232 insertions(+), 6 deletions(-) create mode 100644 App/Modules/Admin/API/V1/AdminController.cs create mode 100644 App/Modules/Admin/API/V1/AdminModel.cs create mode 100644 App/Modules/Admin/API/V1/AdminValidator.cs create mode 100644 Domain/Admin/IService.cs create mode 100644 Domain/Admin/Service.cs diff --git a/App/Modules/Admin/API/V1/AdminController.cs b/App/Modules/Admin/API/V1/AdminController.cs new file mode 100644 index 0000000..a072b82 --- /dev/null +++ b/App/Modules/Admin/API/V1/AdminController.cs @@ -0,0 +1,54 @@ +using System.Net.Mime; +using App.Modules.Common; +using App.Modules.Wallets.API.V1; +using App.StartUp.Registry; +using App.StartUp.Services.Auth; +using App.Utility; +using Asp.Versioning; +using CSharp_Result; +using Domain.Admin; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace App.Modules.Admin.API.V1; + +[ApiVersion(1.0)] +[ApiController] +[Consumes(MediaTypeNames.Application.Json)] +[Route("api/v{version:apiVersion}/[controller]")] +public class AdminController( + IAdminService service, + TransferReqValidator transferReqValidator, + IAuthHelper h +) : AtomiControllerBase(h) +{ + [Authorize(Policy = AuthPolicies.OnlyAdmin), HttpPost("inflow/{userId}")] + public async Task> TransferIn(string userId, [FromBody] TransferReq req) + { + var x = await transferReqValidator + .ValidateAsyncResult(req, "Invalid TransferReq") + .ThenAwait(q => service.TransferIn(userId, q.Amount, q.Desc)) + .Then(x => x.ToRes(), Errors.MapNone); + return this.ReturnResult(x); + } + + [Authorize(Policy = AuthPolicies.OnlyAdmin), HttpPost("outflow/{userId}")] + public async Task> TransferOut(string userId, [FromBody] TransferReq req) + { + var x = await transferReqValidator + .ValidateAsyncResult(req, "Invalid TransferReq") + .ThenAwait(q => service.TransferOut(userId, q.Amount, q.Desc)) + .Then(x => x.ToRes(), Errors.MapNone); + return this.ReturnResult(x); + } + + [Authorize(Policy = AuthPolicies.OnlyAdmin), HttpPost("promo/{userId}")] + public async Task> PromotionalCredit(string userId, [FromBody] TransferReq req) + { + var x = await transferReqValidator + .ValidateAsyncResult(req, "Invalid TransferReq") + .ThenAwait(q => service.Promo(userId, q.Amount, q.Desc)) + .Then(x => x.ToRes(), Errors.MapNone); + return this.ReturnResult(x); + } +} diff --git a/App/Modules/Admin/API/V1/AdminModel.cs b/App/Modules/Admin/API/V1/AdminModel.cs new file mode 100644 index 0000000..3a3ad0a --- /dev/null +++ b/App/Modules/Admin/API/V1/AdminModel.cs @@ -0,0 +1,6 @@ +namespace App.Modules.Admin.API.V1; + +public record TransferReq( + decimal Amount, string Desc +); + diff --git a/App/Modules/Admin/API/V1/AdminValidator.cs b/App/Modules/Admin/API/V1/AdminValidator.cs new file mode 100644 index 0000000..fd7f652 --- /dev/null +++ b/App/Modules/Admin/API/V1/AdminValidator.cs @@ -0,0 +1,17 @@ +using App.Utility; +using FluentValidation; + +namespace App.Modules.Admin.API.V1; + +public class TransferReqValidator : AbstractValidator +{ + public TransferReqValidator() + { + this.RuleFor(x => x.Amount) + .NotNull() + .Must(x => x > 0); + this.RuleFor(x => x.Desc) + .NotNull() + .TransactionDescriptionValid(); + } +} diff --git a/App/Modules/DomainServices.cs b/App/Modules/DomainServices.cs index 3438e02..810fc86 100644 --- a/App/Modules/DomainServices.cs +++ b/App/Modules/DomainServices.cs @@ -10,6 +10,7 @@ using App.Modules.Wallets.Data; using App.StartUp.Services; using Domain; +using Domain.Admin; using Domain.Booking; using Domain.Cost; using Domain.Passenger; @@ -96,6 +97,10 @@ public static IServiceCollection AddDomainServices(this IServiceCollection s) s.AddScoped() .AutoTrace(); + // Admin + s.AddScoped() + .AutoTrace(); + return s; } } diff --git a/App/Modules/Wallets/Data/WalletRepository.cs b/App/Modules/Wallets/Data/WalletRepository.cs index 0517b8a..83cc403 100644 --- a/App/Modules/Wallets/Data/WalletRepository.cs +++ b/App/Modules/Wallets/Data/WalletRepository.cs @@ -159,6 +159,35 @@ public async Task>> Search(WalletSearch sear } } + public async Task> Collect(Guid id, decimal amount) + { + try + { + logger.LogInformation("Collecting from wallet with Id '{id}' with {amount}", id, amount); + var wallet = await db + .Wallets + .Where(x => x.Id == id) + .FirstOrDefaultAsync(); + if (wallet is null) return wallet?.ToPrincipal(); + + if (wallet.Usable < amount) + return new InsufficientBalance("Insufficient balance to collect", + wallet.UserId, wallet.Id, amount, + Accounts.Usable.Id) + .ToException(); + wallet.Usable -= amount; + + await db.SaveChangesAsync(); + return wallet.ToPrincipal(); + } + catch (Exception e) + { + logger + .LogError(e, "Collecting from wallet with Id: {id} with {amount}", id, amount); + throw; + } + } + public async Task> BookStart(Guid id, decimal amount) { try diff --git a/App/Utility/ValidationUtility.cs b/App/Utility/ValidationUtility.cs index dc4aeab..3884056 100644 --- a/App/Utility/ValidationUtility.cs +++ b/App/Utility/ValidationUtility.cs @@ -153,7 +153,13 @@ public static IRuleBuilderOptions NameValid( .Length(1, 256) .WithMessage("Name has to be between 1 to 256 characters"); } - + public static IRuleBuilderOptions TransactionDescriptionValid( + this IRuleBuilder ruleBuilder) + { + return ruleBuilder + .Length(2, 4096) + .WithMessage("Description has to be between 2 to 4096 characters"); + } public static IRuleBuilderOptions DescriptionValid( this IRuleBuilder ruleBuilder) diff --git a/Domain/Admin/IService.cs b/Domain/Admin/IService.cs new file mode 100644 index 0000000..8132ae5 --- /dev/null +++ b/Domain/Admin/IService.cs @@ -0,0 +1,13 @@ +using CSharp_Result; +using Domain.Wallet; + +namespace Domain.Admin; + +public interface IAdminService +{ + Task> TransferIn(string userId, decimal amount, string desc); + + Task> TransferOut(string userId, decimal amount, string desc); + + Task> Promo(string userId, decimal amount, string desc); +} diff --git a/Domain/Admin/Service.cs b/Domain/Admin/Service.cs new file mode 100644 index 0000000..1b0f3ee --- /dev/null +++ b/Domain/Admin/Service.cs @@ -0,0 +1,50 @@ +using CSharp_Result; +using Domain.Transaction; +using Domain.Wallet; + +namespace Domain.Admin; + +public class AdminService( + IWalletRepository walletRepo, + ITransactionRepository transactionRepo, + ITransactionGenerator transactionGenerator, + ITransactionManager transaction +) : IAdminService +{ + + public Task> TransferIn(string userId, decimal amount, string desc) + { + return transaction.Start(() => + walletRepo.GetByUserId(userId) + .NullToError(userId) + .ThenAwait(w => walletRepo.Deposit(w.Principal.Id, amount)) + .NullToError(userId) + .DoAwait(DoType.MapErrors, w => transactionRepo.Create(w.Id, + transactionGenerator.AdminInflow(amount, desc))) + ); + } + + public Task> TransferOut(string userId, decimal amount, string desc) + { + return transaction.Start(() => + walletRepo.GetByUserId(userId) + .NullToError(userId) + .ThenAwait(w => walletRepo.Collect(w.Principal.Id, amount)) + .NullToError(userId) + .DoAwait(DoType.MapErrors, w => transactionRepo.Create(w.Id, + transactionGenerator.AdminOutflow(amount, desc))) + ); + } + + public Task> Promo(string userId, decimal amount, string desc) + { + return transaction.Start(() => + walletRepo.GetByUserId(userId) + .NullToError(userId) + .ThenAwait(w => walletRepo.Deposit(w.Principal.Id, amount)) + .NullToError(userId) + .DoAwait(DoType.MapErrors, w => transactionRepo.Create(w.Id, + transactionGenerator.Promotional(amount, desc))) + ); + } +} diff --git a/Domain/Domain.csproj b/Domain/Domain.csproj index 3dd298f..dcbad73 100644 --- a/Domain/Domain.csproj +++ b/Domain/Domain.csproj @@ -11,8 +11,4 @@ - - - - diff --git a/Domain/Transaction/Generator.cs b/Domain/Transaction/Generator.cs index fc2acd3..7b9a575 100644 --- a/Domain/Transaction/Generator.cs +++ b/Domain/Transaction/Generator.cs @@ -12,8 +12,14 @@ public interface ITransactionGenerator public TransactionRecord CancelBooking(TransactionRecord create, BookingRecord booking); - public TransactionRecord TerminateBooking(TransactionRecord create, BookingRecord booking); + + // Admin Flow + public TransactionRecord AdminInflow(decimal amount, string description); + + public TransactionRecord AdminOutflow(decimal amount, string description); + + public TransactionRecord Promotional(decimal amount, string description); } public class TransactionGenerator(IRefundCalculator calculator) : ITransactionGenerator @@ -99,4 +105,45 @@ public TransactionRecord TerminateBooking(TransactionRecord create, BookingRecor To = Accounts.Usable.DisplayName, }; } + + public TransactionRecord AdminInflow(decimal amount, string description) + { + return new TransactionRecord + { + Name = "BunnyBooker Admin Inflow", + Description = + $"The BunnyBooker Admin has transferred SGD ${amount} credits to your Usable account. " + description, + Type = TransactionType.Transfer, + Amount = amount, + From = Accounts.BunnyBooker.DisplayName, + To = Accounts.Usable.DisplayName, + }; + } + + public TransactionRecord AdminOutflow(decimal amount, string description) + { + return new TransactionRecord + { + Name = "BunnyBooker Admin Outflow", + Description = + $"The BunnyBooker Admin has transferred SGD ${amount} credits out of your Usable account. " + description, + Type = TransactionType.Transfer, + Amount = amount, + From = Accounts.Usable.DisplayName, + To = Accounts.BunnyBooker.DisplayName, + }; + } + + public TransactionRecord Promotional(decimal amount, string description) + { + return new TransactionRecord + { + Name = "Promotional Credits", + Description = description, + Amount = amount, + Type = TransactionType.Promotional, + From = Accounts.BunnyBooker.DisplayName, + To = Accounts.Usable.DisplayName, + }; + } } diff --git a/Domain/Wallet/Repository.cs b/Domain/Wallet/Repository.cs index 393adc3..3f5e412 100644 --- a/Domain/Wallet/Repository.cs +++ b/Domain/Wallet/Repository.cs @@ -19,6 +19,9 @@ public interface IWalletRepository // increase usable Task> Deposit(Guid id, decimal amount); + // decrease usable + Task> Collect(Guid id, decimal amount); + // usable -> booking reserve Task> BookStart(Guid id, decimal amount);