Created
June 21, 2017 16:19
-
-
Save SteveBate/5d8b611d1fdcbe7672a97b3a7e52576a to your computer and use it in GitHub Desktop.
Example of a Sale inside a POS system using event sourcing (Linqpad)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
void Main() | |
{ | |
var p1 = new StockItem { PartNo = "P100", Description = "Coca Cola", Qty = 1, Cost = 1.00m }; | |
var p2 = new StockItem { PartNo = "P101", Description = "Pepsi Max", Qty = 2, Cost = 1.00m }; | |
var p3 = new StockItem { PartNo = "P102", Description = "Pint San Miguel", Qty = 1, Cost = 3.50m }; | |
var cfg = new SaleConfig { AddOnTax = false, TaxRate = 20.0m }; | |
var sut = new Sale(Guid.NewGuid(), cfg); | |
sut.AddItem(p1); | |
sut.AddItem(p2); | |
sut.AddItem(p3); | |
var events = sut.GetUncommittedChanges(); | |
var copy = new Sale(events); | |
copy.RemoveItem(p2); | |
copy.Discount(new Discount { Percent = 10, Reason = "Father's Day" }); | |
copy.GetUncommittedChanges(); | |
//copy.Discount(new Discount { Percent = 10, Reason = "Father's Day" }); | |
} | |
public class AggregateRoot | |
{ | |
public IEnumerable<Event> GetUncommittedChanges() | |
{ | |
return _changes; | |
} | |
public void MarkAsCommitted() | |
{ | |
_changes.Clear(); | |
} | |
public void LoadHistoryFromEvents(IEnumerable<Event> events) | |
{ | |
foreach (var e in events) | |
{ | |
ApplyChange(e, false); | |
} | |
} | |
protected void ApplyChange(Event @event, bool isNew) | |
{ | |
@event.Version = ++Version; | |
Action<Event> handler; | |
if (_handlers.TryGetValue(@event.GetType(), out handler)) | |
handler(@event); | |
if (isNew) _changes.Add(@event); | |
} | |
protected void ApplyChange(Event @event) | |
{ | |
ApplyChange(@event, true); | |
} | |
protected void Handles<T>(Action<T> handler) where T : Event | |
{ | |
_handlers.Add(typeof(T), e => handler((T)e)); | |
} | |
protected Guid Id { get; set; } | |
protected int Version { get; set; } | |
readonly Dictionary<Type, Action<Event>> _handlers = new Dictionary<Type, Action<Event>>(); | |
readonly List<Event> _changes = new List<Event>(); | |
} | |
public class Event | |
{ | |
public Guid Id; | |
public int Version; | |
} | |
// domain | |
public class Sale : AggregateRoot | |
{ | |
public Sale() | |
{ | |
Handles<SaleWasCreated>(Apply); | |
Handles<StockItemWasAdded>(Apply); | |
Handles<StockItemWasRemoved>(Apply); | |
Handles<SaleWasDiscounted>(Apply); | |
Handles<TaxWasCalculated>(Apply); | |
} | |
public Sale(IEnumerable<Event> events) : this() | |
{ | |
LoadHistoryFromEvents(events); | |
} | |
public Sale(Guid userId, SaleConfig config) : this() | |
{ | |
ApplyChange(new SaleWasCreated | |
{ | |
Id = Guid.NewGuid(), | |
UserId = userId, | |
Total = 0, | |
UsesAddOnTax = config.AddOnTax, | |
TaxRate = config.TaxRate | |
}); | |
} | |
public void AddItem(StockItem item) | |
{ | |
ApplyChange(new StockItemWasAdded { | |
Id = _id, | |
Part = item.PartNo, | |
Description = item.Description, | |
Qty = item.Qty, | |
Cost = item.Cost, | |
LineTotal = item.Qty * item.Cost, | |
Total = _currentTotal.Value + item.Qty * item.Cost | |
}); | |
ApplyChange(new TaxWasCalculated | |
{ | |
Id = _id, | |
Percent = _taxRate, | |
Amount = GetTax().Value, | |
AddOn = _addOnTax, | |
SubTotal = _addOnTax ? _currentTotal.Value : _currentTotal.Value - GetTax().Value, | |
Total = _addOnTax ? _currentTotal.Value + GetTax().Value : _currentTotal.Value | |
}); | |
} | |
public void RemoveItem(StockItem item) | |
{ | |
ApplyChange(new StockItemWasRemoved | |
{ | |
Id = _id, | |
Part = item.PartNo, | |
Description = item.Description, | |
Qty = item.Qty, | |
Cost = item.Cost, | |
LineTotal = item.Qty * item.Cost, | |
Total = _currentTotal.Value - (item.Qty * item.Cost) | |
}); | |
ApplyChange(new TaxWasCalculated | |
{ | |
Id = _id, | |
Percent = _taxRate, | |
Amount = GetTax().Value, | |
AddOn = _addOnTax, | |
SubTotal = _addOnTax ? _currentTotal.Value : _currentTotal.Value - GetTax().Value, | |
Total = _addOnTax ? _currentTotal.Value + GetTax().Value : _currentTotal.Value | |
}); | |
} | |
public void Discount(Discount discount) | |
{ | |
if (_discounted) throw new InvalidOperationException("Discount cannot be applied more than once"); | |
decimal disc = _currentTotal.Value / 100 * discount.Percent; | |
ApplyChange(new SaleWasDiscounted { | |
Id = _id, | |
Percent = discount.Percent, | |
Amount = Money.WithValue(disc).Value, | |
Reason = discount.Reason, | |
Total = _currentTotal.Value - Money.WithValue(disc).Value | |
}); | |
ApplyChange(new TaxWasCalculated | |
{ | |
Id = _id, | |
Percent = _taxRate, | |
Amount = GetTax().Value, | |
AddOn = _addOnTax, | |
SubTotal = _addOnTax ? _currentTotal.Value : _currentTotal.Value - GetTax().Value, | |
Total = _addOnTax ? _currentTotal.Value + GetTax().Value : _currentTotal.Value | |
}); | |
} | |
void Apply(SaleWasCreated @event) | |
{ | |
_id = @event.Id; | |
_currentTotal = Money.WithValue(@event.Total); | |
_addOnTax = @event.UsesAddOnTax; | |
_taxRate = @event.TaxRate; | |
} | |
void Apply(StockItemWasAdded @event) | |
{ | |
_currentTotal += Money.WithValue(@event.LineTotal); | |
} | |
void Apply(StockItemWasRemoved @event) | |
{ | |
_currentTotal -= Money.WithValue(@event.LineTotal); | |
} | |
void Apply(SaleWasDiscounted @event) | |
{ | |
_discounted = true; | |
_currentTotal = Money.WithValue(@event.Total); | |
} | |
void Apply(TaxWasCalculated @event) | |
{ | |
_currentTax = Money.WithValue(@event.Amount); | |
} | |
Money GetTax() | |
{ | |
decimal amt = _addOnTax ? _currentTotal.Value * _taxRate / 100 : _currentTotal.Value * _taxRate / (100 + _taxRate); | |
return Money.WithValue(amt); | |
} | |
Guid _id; | |
bool _addOnTax; | |
bool _discounted; | |
decimal _taxRate; | |
Money _currentTotal = Money.Default; | |
Money _currentTax = Money.Default; | |
} | |
public class StockItem | |
{ | |
public string PartNo { get; set; } | |
public string Description { get; set; } | |
public int Qty { get; set; } | |
public decimal Cost { get; set; } | |
} | |
// value objects | |
public class SaleConfig | |
{ | |
public bool AddOnTax { get; set; } | |
public decimal TaxRate { get; set; } | |
} | |
public class Discount | |
{ | |
public int Percent { get; set; } | |
public string Reason { get; set; } | |
public override bool Equals(object obj) | |
{ | |
if (obj.GetType() != GetType()) return false; | |
var d = (Discount)obj; | |
return Percent.Equals(d.Percent) && Reason.Equals(d.Reason); | |
} | |
public override int GetHashCode() | |
{ | |
int hash = 997; | |
hash = hash * 23 + Percent.GetHashCode(); | |
hash = hash * 23 + Reason.GetHashCode(); | |
return hash; | |
} | |
} | |
public class Money | |
{ | |
public Money(decimal value) | |
{ | |
Value = Math.Round(value, 2, MidpointRounding.AwayFromZero); ; | |
} | |
public decimal Value { get; private set; } | |
public virtual Money Negate() | |
{ | |
return new Money(-Value); | |
} | |
// The addition (+), subtraction (-), multiplication (*) and division (/) operators | |
// are overloaded to allow us to provide a more natural means of calculating totals | |
// between two IMoney instances. For instance, Instead of having to add up the | |
// Value properties of two instances which are of type decimal, we can add now add | |
// up two IMoney instances. | |
public static Money operator +(Money left, Money right) | |
{ | |
return new Money(left.Value + right.Value); | |
} | |
public static Money operator -(Money left, Money right) | |
{ | |
return new Money(left.Value - right.Value); | |
} | |
public static Money operator *(Money left, Money right) | |
{ | |
return new Money(left.Value * right.Value); | |
} | |
public static Money operator /(Money left, Money right) | |
{ | |
return new Money(left.Value / right.Value); | |
} | |
public override bool Equals(object obj) | |
{ | |
if (obj.GetType() != GetType()) return false; | |
var m = (Money)obj; | |
return Value.Equals(m.Value); | |
} | |
public override int GetHashCode() | |
{ | |
int hash = 997; | |
hash = hash * 23 + Value.GetHashCode(); | |
return hash; | |
} | |
public static Money Default | |
{ | |
get { return new Money(0); } | |
} | |
public static Money WithValue(decimal value) | |
{ | |
return new Money(value); | |
} | |
} | |
// events | |
public class SaleWasCreated : Event | |
{ | |
public Guid UserId { get; set; } | |
public decimal Total { get; set; } | |
public bool UsesAddOnTax { get; set; } | |
public decimal TaxRate { get; set; } | |
} | |
public class StockItemWasAdded : Event | |
{ | |
public string Part { get; set; } | |
public string Description { get; set; } | |
public int Qty { get; set; } | |
public decimal Cost { get; set; } | |
public decimal LineTotal { get; set; } | |
public decimal Total { get; set; } | |
} | |
public class StockItemWasRemoved : Event | |
{ | |
public string Part { get; set; } | |
public string Description { get; set; } | |
public int Qty { get; set; } | |
public decimal Cost { get; set; } | |
public decimal LineTotal { get; set; } | |
public decimal Total { get; set; } | |
} | |
public class SaleWasDiscounted : Event | |
{ | |
public int Percent { get; set; } | |
public decimal Amount { get; set; } | |
public decimal Total { get; set; } | |
public string Reason { get; set; } | |
} | |
public class TaxWasCalculated : Event | |
{ | |
public decimal Percent { get; set; } | |
public decimal Amount { get; set; } | |
public bool AddOn { get; set; } | |
public decimal SubTotal { get; set; } | |
public decimal Total { get; set; } | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment