Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 12 additions & 11 deletions src/Modules/Billing/Modules.Billing/Domain/Wallet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
10 changes: 9 additions & 1 deletion src/Modules/Billing/Modules.Billing/Services/BillingService.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Net;
using Finbuckle.MultiTenant.Abstractions;
using FSH.Framework.Core.Exceptions;
using FSH.Framework.Eventing.Abstractions;
Expand Down Expand Up @@ -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<string>?)null,
HttpStatusCode.Conflict);
}

wallet.Credit(
invoice.SubtotalAmount.Amount,
invoice.SubtotalAmount,
WalletTransactionKind.Topup,
"WhatsApp wallet top-up",
topupRequest.Id.ToString());
Expand Down
17 changes: 13 additions & 4 deletions src/Tests/Billing.Tests/Domain/WalletTests.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using FSH.Framework.Core.Domain;
using FSH.Modules.Billing.Contracts;
using FSH.Modules.Billing.Domain;
using Shouldly;
Expand All @@ -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);
Expand All @@ -33,14 +34,22 @@ public void Credit_increases_balance_and_returns_ledger_row()
[Fact]
public void Credit_rejects_non_positive_amount()
=> Should.Throw<ArgumentOutOfRangeException>(
() => 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<InvalidOperationException>(
() => 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<InvalidOperationException>(
() => w.Debit(25m, WalletTransactionKind.MessageCharge, "msg", null));
() => w.Debit(new Money(25m, "USD"), WalletTransactionKind.MessageCharge, "msg", null));
}
}
3 changes: 2 additions & 1 deletion src/Tests/Billing.Tests/Mappings/WalletMappingTests.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using FSH.Framework.Core.Domain;
using FSH.Modules.Billing.Domain;
using FSH.Modules.Billing.Mappings;
using FSH.Modules.Billing.Contracts;
Expand All @@ -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");
Expand Down
Loading