diff --git a/src/Modules/Billing/Modules.Billing/Domain/Wallet.cs b/src/Modules/Billing/Modules.Billing/Domain/Wallet.cs index 4aac34a6d8..034b7fc04e 100644 --- a/src/Modules/Billing/Modules.Billing/Domain/Wallet.cs +++ b/src/Modules/Billing/Modules.Billing/Domain/Wallet.cs @@ -31,26 +31,27 @@ public static Wallet Create(string tenantId, string currency) }; } - public WalletTransaction Credit(decimal amount, WalletTransactionKind kind, string description, string? referenceId) + public WalletTransaction Credit(Money amount, WalletTransactionKind kind, string description, string? referenceId) { - ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(amount, 0m); - var credit = new Money(amount, Balance.Currency); - var tx = WalletTransaction.Create(Id, TenantId, credit, kind, description, referenceId); + ArgumentNullException.ThrowIfNull(amount); + ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(amount.Amount, 0m); + var tx = WalletTransaction.Create(Id, TenantId, amount, kind, description, referenceId); _transactions.Add(tx); - Balance = Balance.Add(credit); + Balance = Balance.Add(amount); UpdatedAtUtc = DateTime.UtcNow; return tx; } - public WalletTransaction Debit(decimal amount, WalletTransactionKind kind, string description, string? referenceId) + public WalletTransaction Debit(Money amount, WalletTransactionKind kind, string description, string? referenceId) { - ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(amount, 0m); - if (amount > Balance.Amount) + ArgumentNullException.ThrowIfNull(amount); + ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(amount.Amount, 0m); + var remaining = Balance.Subtract(amount); + if (remaining.Amount < 0m) throw new InvalidOperationException("Insufficient wallet balance."); - var debit = new Money(amount, Balance.Currency); - var tx = WalletTransaction.Create(Id, TenantId, new Money(-amount, Balance.Currency), kind, description, referenceId); + var tx = WalletTransaction.Create(Id, TenantId, amount with { Amount = -amount.Amount }, kind, description, referenceId); _transactions.Add(tx); - Balance = Balance.Subtract(debit); + Balance = remaining; UpdatedAtUtc = DateTime.UtcNow; return tx; } diff --git a/src/Modules/Billing/Modules.Billing/Services/BillingService.cs b/src/Modules/Billing/Modules.Billing/Services/BillingService.cs index 0a6643d140..7b8aae2eda 100644 --- a/src/Modules/Billing/Modules.Billing/Services/BillingService.cs +++ b/src/Modules/Billing/Modules.Billing/Services/BillingService.cs @@ -1,3 +1,4 @@ +using System.Net; using Finbuckle.MultiTenant.Abstractions; using FSH.Framework.Core.Exceptions; using FSH.Framework.Eventing.Abstractions; @@ -255,9 +256,16 @@ public async Task MarkInvoicePaidAsync(Guid invoiceId, CancellationToken cancell wallet = Wallet.Create(invoice.TenantId, invoice.Currency); _db.Wallets.Add(wallet); } + else if (!string.Equals(wallet.Currency, invoice.Currency, StringComparison.Ordinal)) + { + throw new CustomException( + $"Cannot credit a {invoice.Currency} top-up to a {wallet.Currency} wallet.", + (IEnumerable?)null, + HttpStatusCode.Conflict); + } wallet.Credit( - invoice.SubtotalAmount.Amount, + invoice.SubtotalAmount, WalletTransactionKind.Topup, "WhatsApp wallet top-up", topupRequest.Id.ToString()); diff --git a/src/Tests/Billing.Tests/Domain/WalletTests.cs b/src/Tests/Billing.Tests/Domain/WalletTests.cs index 36ef5e9ae6..5b9fa7cf25 100644 --- a/src/Tests/Billing.Tests/Domain/WalletTests.cs +++ b/src/Tests/Billing.Tests/Domain/WalletTests.cs @@ -1,3 +1,4 @@ +using FSH.Framework.Core.Domain; using FSH.Modules.Billing.Contracts; using FSH.Modules.Billing.Domain; using Shouldly; @@ -22,7 +23,7 @@ public void Create_starts_active_with_zero_balance() public void Credit_increases_balance_and_returns_ledger_row() { var w = Wallet.Create("tenant-a", "USD"); - var tx = w.Credit(50m, WalletTransactionKind.Topup, "Top-up", "req-1"); + var tx = w.Credit(new Money(50m, "USD"), WalletTransactionKind.Topup, "Top-up", "req-1"); w.Balance.Amount.ShouldBe(50m); tx.Amount.Amount.ShouldBe(50m); tx.WalletId.ShouldBe(w.Id); @@ -33,14 +34,22 @@ public void Credit_increases_balance_and_returns_ledger_row() [Fact] public void Credit_rejects_non_positive_amount() => Should.Throw( - () => Wallet.Create("t", "USD").Credit(0m, WalletTransactionKind.Topup, "x", null)); + () => Wallet.Create("t", "USD").Credit(new Money(0m, "USD"), WalletTransactionKind.Topup, "x", null)); + + [Fact] + public void Credit_rejects_currency_mismatch() + { + var w = Wallet.Create("tenant-a", "USD"); + Should.Throw( + () => w.Credit(new Money(50m, "EUR"), WalletTransactionKind.Topup, "Top-up", null)); + } [Fact] public void Debit_beyond_balance_throws() { var w = Wallet.Create("tenant-a", "USD"); - w.Credit(10m, WalletTransactionKind.Topup, "Top-up", null); + w.Credit(new Money(10m, "USD"), WalletTransactionKind.Topup, "Top-up", null); Should.Throw( - () => w.Debit(25m, WalletTransactionKind.MessageCharge, "msg", null)); + () => w.Debit(new Money(25m, "USD"), WalletTransactionKind.MessageCharge, "msg", null)); } } diff --git a/src/Tests/Billing.Tests/Mappings/WalletMappingTests.cs b/src/Tests/Billing.Tests/Mappings/WalletMappingTests.cs index ad613f2ef1..a5272b8e89 100644 --- a/src/Tests/Billing.Tests/Mappings/WalletMappingTests.cs +++ b/src/Tests/Billing.Tests/Mappings/WalletMappingTests.cs @@ -1,3 +1,4 @@ +using FSH.Framework.Core.Domain; using FSH.Modules.Billing.Domain; using FSH.Modules.Billing.Mappings; using FSH.Modules.Billing.Contracts; @@ -12,7 +13,7 @@ public sealed class WalletMappingTests public void Wallet_ToDto_emits_string_status_and_balance() { var w = Wallet.Create("tenant-a", "USD"); - w.Credit(50m, WalletTransactionKind.Topup, "Top-up", "req-1"); + w.Credit(new Money(50m, "USD"), WalletTransactionKind.Topup, "Top-up", "req-1"); var dto = w.ToDto(); dto.Balance.ShouldBe(50m); dto.Status.ShouldBe("Active");