Skip to content

Commit a6d89a4

Browse files
Merge pull request #846 from TransactionProcessing/task/#842_merchant_balance_aggregate
Aggregate added for merchant balance and all updates to balance added in
2 parents 15382fe + ff4d66f commit a6d89a4

File tree

21 files changed

+778
-129
lines changed

21 files changed

+778
-129
lines changed
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Text;
5+
using System.Threading.Tasks;
6+
using Shouldly;
7+
using TransactionProcessor.Testing;
8+
9+
namespace TransactionProcessor.Aggregates.Tests
10+
{
11+
public class MerchantBalanceAggregateTests
12+
{
13+
[Fact]
14+
public void MerchantBalanceAggregate_RecordCompletedTransaction_MerchantNotCreated_ErrorThrown()
15+
{
16+
MerchantBalanceAggregate aggregate = MerchantBalanceAggregate.Create(TestData.MerchantId);
17+
MerchantAggregate merchantAggregate = TestData.Aggregates.EmptyMerchantAggregate();
18+
Should.Throw<InvalidOperationException>(() => {
19+
aggregate.RecordCompletedTransaction(merchantAggregate, TestData.TransactionId, TestData.TransactionAmount, TestData.TransactionDateTime, true);
20+
});
21+
}
22+
23+
[Fact]
24+
public void MerchantBalanceAggregate_RecordDeposit_MerchantNotCreated_ErrorThrown()
25+
{
26+
MerchantBalanceAggregate aggregate = MerchantBalanceAggregate.Create(TestData.MerchantId);
27+
MerchantAggregate merchantAggregate = TestData.Aggregates.EmptyMerchantAggregate();
28+
Should.Throw<InvalidOperationException>(() => {
29+
aggregate.RecordMerchantDeposit(merchantAggregate, TestData.DepositId, TestData.DepositAmount.Value, TestData.DepositDateTime);
30+
});
31+
}
32+
33+
[Fact]
34+
public void MerchantBalanceAggregate_RecordMerchantWithdrawal_MerchantNotCreated_ErrorThrown()
35+
{
36+
MerchantBalanceAggregate aggregate = MerchantBalanceAggregate.Create(TestData.MerchantId);
37+
MerchantAggregate merchantAggregate = TestData.Aggregates.EmptyMerchantAggregate();
38+
Should.Throw<InvalidOperationException>(() => {
39+
aggregate.RecordMerchantWithdrawal(merchantAggregate, TestData.WithdrawalId, TestData.WithdrawalAmount.Value, TestData.WithdrawalDateTime);
40+
});
41+
}
42+
43+
[Fact]
44+
public void MerchantBalanceAggregate_RecordSettledFee_MerchantNotCreated_ErrorThrown()
45+
{
46+
MerchantBalanceAggregate aggregate = MerchantBalanceAggregate.Create(TestData.MerchantId);
47+
MerchantAggregate merchantAggregate = TestData.Aggregates.EmptyMerchantAggregate();
48+
Should.Throw<InvalidOperationException>(() => {
49+
aggregate.RecordSettledFee(merchantAggregate, TestData.SettledFeeId1, TestData.SettledFeeAmount1, TestData.SettledFeeDateTime1);
50+
});
51+
}
52+
53+
[Fact]
54+
public void MerchantBalanceAggregate_RecordCompletedTransaction_TransactionIsRecorded() {
55+
MerchantBalanceAggregate aggregate = MerchantBalanceAggregate.Create(TestData.MerchantId);
56+
MerchantAggregate merchantAggregate = TestData.Aggregates.CreatedMerchantAggregate();
57+
aggregate.RecordCompletedTransaction(merchantAggregate, TestData.TransactionId, TestData.TransactionAmount, TestData.TransactionDateTime, true);
58+
aggregate.Balance.ShouldBe(TestData.TransactionAmount * -1);
59+
aggregate.AuthorisedSales.Count.ShouldBe(1);
60+
aggregate.AuthorisedSales.Value.ShouldBe(TestData.TransactionAmount);
61+
aggregate.AuthorisedSales.LastActivity.ShouldBe(TestData.TransactionDateTime);
62+
}
63+
64+
[Fact]
65+
public void MerchantBalanceAggregate_RecordCompletedTransaction_MultipleTransactions_TransactionIsRecorded()
66+
{
67+
MerchantBalanceAggregate aggregate = MerchantBalanceAggregate.Create(TestData.MerchantId);
68+
MerchantAggregate merchantAggregate = TestData.Aggregates.CreatedMerchantAggregate();
69+
aggregate.RecordCompletedTransaction(merchantAggregate, TestData.TransactionId, TestData.TransactionAmount, TestData.TransactionDateTime, true);
70+
aggregate.RecordCompletedTransaction(merchantAggregate, TestData.TransactionId1, TestData.TransactionAmount, TestData.TransactionDateTime, true);
71+
aggregate.Balance.ShouldBe((TestData.TransactionAmount * 2) * -1);
72+
aggregate.AuthorisedSales.Count.ShouldBe(2);
73+
aggregate.AuthorisedSales.Value.ShouldBe(TestData.TransactionAmount * 2);
74+
aggregate.AuthorisedSales.LastActivity.ShouldBe(TestData.TransactionDateTime);
75+
}
76+
77+
[Fact]
78+
public void MerchantBalanceAggregate_RecordCompletedTransaction_DuplicateTransactionIsIgnored()
79+
{
80+
MerchantBalanceAggregate aggregate = MerchantBalanceAggregate.Create(TestData.MerchantId);
81+
MerchantAggregate merchantAggregate = TestData.Aggregates.CreatedMerchantAggregate();
82+
aggregate.RecordCompletedTransaction(merchantAggregate, TestData.TransactionId, TestData.TransactionAmount, TestData.TransactionDateTime, true);
83+
aggregate.RecordCompletedTransaction(merchantAggregate, TestData.TransactionId, TestData.TransactionAmount, TestData.TransactionDateTime, true);
84+
aggregate.Balance.ShouldBe(TestData.TransactionAmount * -1);
85+
aggregate.AuthorisedSales.Count.ShouldBe(1);
86+
aggregate.AuthorisedSales.Value.ShouldBe(TestData.TransactionAmount);
87+
aggregate.AuthorisedSales.LastActivity.ShouldBe(TestData.TransactionDateTime);
88+
}
89+
90+
[Fact]
91+
public void MerchantBalanceAggregate_RecordCompletedTransaction_TransactionDeclined_TransactionIsRecorded()
92+
{
93+
MerchantBalanceAggregate aggregate = MerchantBalanceAggregate.Create(TestData.MerchantId);
94+
MerchantAggregate merchantAggregate = TestData.Aggregates.CreatedMerchantAggregate();
95+
aggregate.RecordCompletedTransaction(merchantAggregate, TestData.TransactionId, TestData.TransactionAmount, TestData.TransactionDateTime, false);
96+
aggregate.Balance.ShouldBe(0);
97+
aggregate.DeclinedSales.Count.ShouldBe(1);
98+
aggregate.DeclinedSales.Value.ShouldBe(TestData.TransactionAmount);
99+
aggregate.DeclinedSales.LastActivity.ShouldBe(TestData.TransactionDateTime);
100+
}
101+
102+
[Fact]
103+
public void MerchantBalanceAggregate_RecordDeposit_DepositIsRecorded()
104+
{
105+
MerchantBalanceAggregate aggregate = MerchantBalanceAggregate.Create(TestData.MerchantId);
106+
MerchantAggregate merchantAggregate = TestData.Aggregates.CreatedMerchantAggregate();
107+
aggregate.RecordMerchantDeposit(merchantAggregate, TestData.DepositId, TestData.DepositAmount.Value, TestData.DepositDateTime);
108+
aggregate.Balance.ShouldBe(TestData.DepositAmount.Value);
109+
aggregate.Deposits.Count.ShouldBe(1);
110+
aggregate.Deposits.Value.ShouldBe(TestData.DepositAmount.Value);
111+
aggregate.Deposits.LastActivity.ShouldBe(TestData.DepositDateTime);
112+
}
113+
114+
[Fact]
115+
public void MerchantBalanceAggregate_RecordWithdrawal_WithdrawalIsRecorded()
116+
{
117+
MerchantBalanceAggregate aggregate = MerchantBalanceAggregate.Create(TestData.MerchantId);
118+
MerchantAggregate merchantAggregate = TestData.Aggregates.CreatedMerchantAggregate();
119+
aggregate.RecordMerchantWithdrawal(merchantAggregate, TestData.WithdrawalId, TestData.WithdrawalAmount.Value, TestData.WithdrawalDateTime);
120+
aggregate.Balance.ShouldBe(TestData.WithdrawalAmount.Value * -1);
121+
aggregate.Withdrawals.Count.ShouldBe(1);
122+
aggregate.Withdrawals.Value.ShouldBe(TestData.WithdrawalAmount.Value);
123+
aggregate.Withdrawals.LastActivity.ShouldBe(TestData.WithdrawalDateTime);
124+
}
125+
126+
[Fact]
127+
public void MerchantBalanceAggregate_RecordSettledFee_SettledFeeIsRecorded()
128+
{
129+
MerchantBalanceAggregate aggregate = MerchantBalanceAggregate.Create(TestData.MerchantId);
130+
MerchantAggregate merchantAggregate = TestData.Aggregates.CreatedMerchantAggregate();
131+
aggregate.RecordSettledFee(merchantAggregate, TestData.SettledFeeId1, TestData.SettledFeeAmount1, TestData.SettledFeeDateTime1);
132+
aggregate.Balance.ShouldBe(TestData.SettledFeeAmount1);
133+
aggregate.Fees.Count.ShouldBe(1);
134+
aggregate.Fees.Value.ShouldBe(TestData.SettledFeeAmount1);
135+
aggregate.Fees.LastActivity.ShouldBe(TestData.SettledFeeDateTime1);
136+
}
137+
}
138+
}
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
using Shared.DomainDrivenDesign.EventSourcing;
2+
using Shared.EventStore.Aggregate;
3+
using Shared.General;
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Diagnostics.CodeAnalysis;
7+
using System.Linq;
8+
using System.Runtime.CompilerServices;
9+
using System.Text;
10+
using System.Threading.Tasks;
11+
using TransactionProcessor.DomainEvents;
12+
using TransactionProcessor.Models.Merchant;
13+
14+
namespace TransactionProcessor.Aggregates
15+
{
16+
public static class MerchantBalanceAggregateExtensions
17+
{
18+
public static void PlayEvent(this MerchantBalanceAggregate aggregate, MerchantBalanceDomainEvents.AuthorisedTransactionRecordedEvent domainEvent)
19+
{
20+
aggregate.Balance -= domainEvent.Amount;
21+
aggregate.AuthorisedSales = aggregate.AuthorisedSales with { Count = aggregate.AuthorisedSales.Count + 1, Value = aggregate.AuthorisedSales.Value + domainEvent.Amount, LastActivity = domainEvent.DateTime };
22+
}
23+
24+
public static void PlayEvent(this MerchantBalanceAggregate aggregate, MerchantBalanceDomainEvents.DeclinedTransactionRecordedEvent domainEvent)
25+
{
26+
aggregate.DeclinedSales= aggregate.AuthorisedSales with { Count = aggregate.DeclinedSales.Count + 1, Value = aggregate.DeclinedSales.Value + domainEvent.Amount, LastActivity = domainEvent.DateTime };
27+
}
28+
29+
public static void PlayEvent(this MerchantBalanceAggregate aggregate, MerchantBalanceDomainEvents.MerchantDepositRecordedEvent domainEvent)
30+
{
31+
aggregate.Balance += domainEvent.Amount;
32+
aggregate.Deposits = aggregate.Deposits with { Count = aggregate.Deposits.Count + 1, Value = aggregate.Deposits.Value + domainEvent.Amount, LastActivity = domainEvent.DateTime };
33+
}
34+
35+
public static void PlayEvent(this MerchantBalanceAggregate aggregate, MerchantBalanceDomainEvents.MerchantWithdrawalRecordedEvent domainEvent)
36+
{
37+
aggregate.Balance -= domainEvent.Amount;
38+
aggregate.Withdrawals = aggregate.Withdrawals with { Count = aggregate.Withdrawals.Count + 1, Value = aggregate.Withdrawals.Value + domainEvent.Amount, LastActivity = domainEvent.DateTime };
39+
}
40+
41+
public static void PlayEvent(this MerchantBalanceAggregate aggregate, MerchantBalanceDomainEvents.SettledFeeRecordedEvent domainEvent)
42+
{
43+
aggregate.Balance += domainEvent.Amount;
44+
aggregate.Fees = aggregate.Fees with { Count = aggregate.Fees.Count + 1, Value = aggregate.Fees.Value + domainEvent.Amount, LastActivity = domainEvent.DateTime };
45+
}
46+
47+
public static void PlayEvent(this MerchantBalanceAggregate aggregate, MerchantBalanceDomainEvents.MerchantBalanceInitialisedEvent domainEvent)
48+
{
49+
aggregate.IsInitialised = true;
50+
aggregate.EstateId = domainEvent.EstateId;
51+
aggregate.Balance = 0;
52+
}
53+
54+
private static void EnsureMerchantHasBeenCreated(this MerchantBalanceAggregate aggregate,
55+
MerchantAggregate merchantAggregate) {
56+
if (merchantAggregate.IsCreated == false) {
57+
throw new InvalidOperationException("Merchant has not been created");
58+
}
59+
}
60+
61+
private static void EnsureMerchantBalanceHasBeenInitialised(this MerchantBalanceAggregate aggregate,
62+
MerchantAggregate merchantAggregate,
63+
DateTime dateTime)
64+
{
65+
if (aggregate.IsInitialised == false) {
66+
Merchant merchant = merchantAggregate.GetMerchant();
67+
MerchantBalanceDomainEvents.MerchantBalanceInitialisedEvent merchantBalanceInitialisedEvent = new MerchantBalanceDomainEvents.MerchantBalanceInitialisedEvent(merchant.MerchantId, merchant.EstateId,dateTime);
68+
aggregate.ApplyAndAppend(merchantBalanceInitialisedEvent);
69+
}
70+
}
71+
72+
73+
public static void RecordCompletedTransaction(this MerchantBalanceAggregate aggregate,
74+
MerchantAggregate merchantAggregate,
75+
Guid transactionId,
76+
Decimal transactionAmount,
77+
DateTime transactionDateTime,
78+
Boolean isAuthorised) {
79+
aggregate.EnsureMerchantHasBeenCreated(merchantAggregate);
80+
aggregate.EnsureMerchantBalanceHasBeenInitialised(merchantAggregate, transactionDateTime);
81+
DomainEvent domainEvent = isAuthorised switch {
82+
true => new MerchantBalanceDomainEvents.AuthorisedTransactionRecordedEvent(aggregate.AggregateId, aggregate.EstateId, transactionId, transactionAmount, transactionDateTime),
83+
_ => new MerchantBalanceDomainEvents.DeclinedTransactionRecordedEvent(aggregate.AggregateId, aggregate.EstateId, transactionId, transactionAmount, transactionDateTime)
84+
};
85+
aggregate.ApplyAndAppend(domainEvent);
86+
}
87+
88+
public static void RecordMerchantDeposit(this MerchantBalanceAggregate aggregate,
89+
MerchantAggregate merchantAggregate,
90+
Guid depositId,
91+
Decimal depositAmount,
92+
DateTime depositDateTime) {
93+
aggregate.EnsureMerchantHasBeenCreated(merchantAggregate);
94+
aggregate.EnsureMerchantBalanceHasBeenInitialised(merchantAggregate, depositDateTime);
95+
96+
MerchantBalanceDomainEvents.MerchantDepositRecordedEvent domainEvent = new MerchantBalanceDomainEvents.MerchantDepositRecordedEvent(aggregate.AggregateId, aggregate.EstateId, depositId, depositAmount, depositDateTime);
97+
aggregate.ApplyAndAppend(domainEvent);
98+
}
99+
100+
public static void RecordMerchantWithdrawal(this MerchantBalanceAggregate aggregate,
101+
MerchantAggregate merchantAggregate,
102+
Guid withdrawalId,
103+
Decimal withdrawalAmount,
104+
DateTime withdrawalDateTime) {
105+
aggregate.EnsureMerchantHasBeenCreated(merchantAggregate);
106+
aggregate.EnsureMerchantBalanceHasBeenInitialised(merchantAggregate, withdrawalDateTime);
107+
108+
MerchantBalanceDomainEvents.MerchantWithdrawalRecordedEvent domainEvent = new MerchantBalanceDomainEvents.MerchantWithdrawalRecordedEvent(aggregate.AggregateId, aggregate.EstateId, withdrawalId, withdrawalAmount, withdrawalDateTime);
109+
aggregate.ApplyAndAppend(domainEvent);
110+
}
111+
112+
public static void RecordSettledFee(this MerchantBalanceAggregate aggregate,
113+
MerchantAggregate merchantAggregate,
114+
Guid feeId,
115+
Decimal feeAmount,
116+
DateTime feeDateTime) {
117+
aggregate.EnsureMerchantHasBeenCreated(merchantAggregate);
118+
aggregate.EnsureMerchantBalanceHasBeenInitialised(merchantAggregate, feeDateTime);
119+
120+
MerchantBalanceDomainEvents.SettledFeeRecordedEvent domainEvent = new MerchantBalanceDomainEvents.SettledFeeRecordedEvent(aggregate.AggregateId, aggregate.EstateId, feeId, feeAmount, feeDateTime);
121+
aggregate.ApplyAndAppend(domainEvent);
122+
}
123+
}
124+
125+
public record ActivityType(Int32 Count, Decimal Value, DateTime LastActivity);
126+
127+
public record MerchantBalanceAggregate : Aggregate {
128+
129+
public Boolean IsInitialised { get; internal set; }
130+
public Guid EstateId { get; internal set; }
131+
public Decimal Balance { get; internal set; }
132+
public ActivityType Deposits { get; internal set; }
133+
public ActivityType Withdrawals { get; internal set; }
134+
public ActivityType AuthorisedSales { get; internal set; }
135+
public ActivityType DeclinedSales { get; internal set; }
136+
public ActivityType Fees { get; internal set; }
137+
138+
[ExcludeFromCodeCoverage]
139+
public MerchantBalanceAggregate()
140+
{
141+
// Nothing here
142+
this.AuthorisedSales = new ActivityType(0, 0, DateTime.MinValue);
143+
this.DeclinedSales = new ActivityType(0, 0, DateTime.MinValue);
144+
this.Deposits = new ActivityType(0, 0, DateTime.MinValue);
145+
this.Withdrawals = new ActivityType(0, 0, DateTime.MinValue);
146+
this.Fees = new ActivityType(0, 0, DateTime.MinValue);
147+
}
148+
149+
private MerchantBalanceAggregate(Guid aggregateId)
150+
{
151+
Guard.ThrowIfInvalidGuid(aggregateId, "Aggregate Id cannot be an Empty Guid");
152+
153+
this.AggregateId = aggregateId;
154+
this.AuthorisedSales = new ActivityType(0, 0, DateTime.MinValue);
155+
this.DeclinedSales = new ActivityType(0, 0, DateTime.MinValue);
156+
this.Deposits = new ActivityType(0, 0, DateTime.MinValue);
157+
this.Withdrawals = new ActivityType(0, 0, DateTime.MinValue);
158+
this.Fees = new ActivityType(0, 0, DateTime.MinValue);
159+
}
160+
161+
public static MerchantBalanceAggregate Create(Guid aggregateId)
162+
{
163+
return new MerchantBalanceAggregate(aggregateId);
164+
}
165+
166+
public override void PlayEvent(IDomainEvent domainEvent) => MerchantBalanceAggregateExtensions.PlayEvent(this, (dynamic)domainEvent);
167+
168+
[ExcludeFromCodeCoverage]
169+
protected override Object GetMetadata()
170+
{
171+
return new
172+
{
173+
EstateId = Guid.NewGuid() // TODO: Populate
174+
};
175+
}
176+
}
177+
}

TransactionProcessor.Aggregates/TransactionAggregate.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,8 @@ public static TransactionProcessor.Models.Transaction GetTransaction(this Transa
7373
AdditionalRequestMetadata = aggregate.AdditionalTransactionRequestMetadata,
7474
AdditionalResponseMetadata = aggregate.AdditionalTransactionResponseMetadata,
7575
ResponseCode = aggregate.ResponseCode,
76-
IsComplete = aggregate.IsCompleted
76+
IsComplete = aggregate.IsCompleted,
77+
IsAuthorised = aggregate.IsAuthorised || aggregate.IsLocallyAuthorised
7778
};
7879
}
7980

TransactionProcessor.BusinessLogic.Tests/Mediator/MediatorTests.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,12 @@ public MediatorTests()
107107
this.Requests.Add(TestData.Queries.GetVoucherByVoucherCodeQuery);
108108
this.Requests.Add(TestData.Queries.GetVoucherByTransactionIdQuery);
109109

110+
// Merchant Balance Commands
111+
this.Requests.Add(TestData.Commands.RecordDepositCommand);
112+
this.Requests.Add(TestData.Commands.RecordWithdrawalCommand);
113+
this.Requests.Add(TestData.Commands.RecordAuthorisedSaleCommand);
114+
this.Requests.Add(TestData.Commands.RecordDeclinedSaleCommand);
115+
this.Requests.Add(TestData.Commands.RecordSettledFeeCommand);
110116
}
111117

112118
[Fact]

0 commit comments

Comments
 (0)