Professional Documents
Culture Documents
!"#
$ #
$
%
&'
)
*
*
Accounting Entries
Events
System
(
(
$
& -
.
/
0,1
*
(
-
#
(
&
&
&
&
&
.
&
(
/
021
.
Event
Agreement creates
Accounting Entry
process() find
findAgreement(Event) process(Event)
1 ✻
✻ 1
Event
Agreement
process() find
getRule(EventType)
findAgreement(Event)
&
.
&
6
3
(
'
78
'
9
6
:
.
#
#
*
.
(
Posting Rule
creates
Event Type effectivity: Date Range Accounting Entry
1 ✻ process(Event)
1
✻
✻ 1
Event
Agreement
process() find
findAgreement(Event) getRule(EventType, Date)
%
.
6
!"#$
"
%&&
(
*
%
4
&
%
&
%
(
(
4
!
;'
'
(
4
&
(
(
(
(
**
%
<
#
'
(
'
.
'
(
Adjustment
0..1 0..1
(
#
&
(
source
Event Accounting Entry
1 ✻
(
0?
'
(
%
(
*
(
'
/
1 #
&
%
! ""
.
*
&
%
@
#
%
:/28
Event ✻ 1
subject Event Type
1 ✻ when ocurred : DateTime
when noticed : DateTime
( -
(
7
(
(
(
*
(
)
A
B
(
&
5
*
(
&
:
(
*
(
7
5
3
5
5
.
%
(
5
(
interface Event {
Event newEvent (EventType type, AccountNumber account,
Date whenOccurred, Date whenNoticed);
EventType getType();
AccountNumber getAccount();
Date getWhenOccurred();
Date getWhenNoticed();
#
'
&
:
%
4
(
-
*
interface Event...
boolean isProcessed();
Set getResultingEntries();
void addResultingEntry(Entry arg);
...
&
)
#
6
09'
)
(
<
Event
{frozen} Event Process Log
1
type
isProcessed
account
resultingEntries
whenOccurred
whenNoticed
Implementation
Perspective
Sale
{frozen}
vendor
amount
)
(
)
(
)
$
)
7
""
Accounting Entry
$
011*$ )*
$
-
+
011
"$-
2
+ ) $ '
$ *
011
"$-
*
" 2
+
) ' " 3 4
#+ )
$ '
$
' 4
$
$3
4
#+
#
6
&
(
(
&
&
(
%
/&
C.
C1
*
&
(
080
Accounting Entry
descriptors
accounting entry
✻ 1
Cost Type
Entry
✻
Project
1
)
%
(
%
(
3
%
:
&
'
2
&
interface Entry {
Entry newEntry (CostType costType, Project project, Money amount, Date date);
CostType getCostType();
Project getProject();
Money getAmount();
Date getBookingDate();
(
'
5
(
6
void setCostType (CostType arg) {
if (isOpen())
costType = arg
else
throw new ImmutableEntryException();
}
A
B
source
Event Accounting Entry
1 ✻
!
$
-
1
host
✻
Posting Rule ´ createsª
Event Type Accounting Entry
1 ✻ process(Event)
5
$
*$' '
"
'"
"
+) $'
$
$'
*$$'
*
$
$ $'
' +
*
7 #
7
(
'
7
'
""
""
+
""
*' ""
' "+
%
$' ""
*
""
+
#
(
"" 6
(
'
4
#
(%
4(
'
(
%
&
Posting Rule
posting rule
event type accounting entry
host
event type
✻ 1
Event Type Service Agreement Posting Rule
date
1 1
´ createsª
✻ ✻
✻ 1
Accounting Event Customer Entry
1 ✻
'
.
.
.
class AccountingEvent {
private EventType _type;
private MfDate _whenOccurred;
private MfDate _whenNoticed;
private Customer _customer;
private Set resultingEntries = new HashSet();
Customer getCustomer() {
return customer;
}
EventType getEventType(){
return type;
}
MfDate getWhenNoticed() {
return whenNoticed;
}
MfDate getWhenOccurred() {
return whenOccurred;
}
void addResultingEntry (Entry arg) {
resultingEntries.add(arg);
}
PostingRule findRule() { /*discussed later*/}
void process() {/*discussed later*/}
}
class Entry {
private MfDate date;
private EntryType type;
private Money amount;
public Entry (Money amount, MfDate date, EntryType type) {
this.amount = amount;
this.date = date;
this.type = type;
}
public Money getAmount() {
return amount;
}
public MfDate getDate() {
return date;
}
public EntryType getType() {
return type;
}
}
class EntryType extends NamedObject {
static EntryType BASE_USAGE = new EntryType("Base Usage");
static EntryType SERVICE = new EntryType("Service Fee");
public EntryType(String name) {
super(name);
}
}
<
class Customer extends NamedObject {
private ServiceAgreement serviceAgreement;
private List entries = new ArrayList();
Customer (String name) {
super(name);
}
public void addEntry (Entry arg) {
entries.add(arg);
}
public List getEntries() {
return Collections.unmodifiableList(entries);
}
public ServiceAgreement getServiceAgreement() {
return serviceAgreement;
}
public void setServiceAgreement(ServiceAgreement arg) {
serviceAgreement = arg;
}
}
(
(
class ServiceAgreement {
private double rate;
private Map postingRules = new HashMap();
void addPostingRule (EventType eventType, PostingRule rule, MfDate date) {
if (postingRules.get(eventType) == null)
postingRules.put(eventType, new TemporalCollection());
temporalCollection(eventType).put(date, rule);
}
PostingRule getPostingRule(EventType eventType, MfDate when) {
return (PostingRule) temporalCollection(eventType).get(when);
}
private TemporalCollection temporalCollection(EventType eventType) {
TemporalCollection result = (TemporalCollection) postingRules.get(eventType);
Assert.notNull(result);
return result;
}
public double getRate() {
return rate;
}
public void setRate(double newRate) {
rate = newRate;
}
}
(
) 6&
'
(
abstract class PostingRule {
protected EntryType type;
protected PostingRule (EntryType type) {
this.type = type;
}
private void makeEntry(AccountingEvent evt, Money amount) {
Entry newEntry = new Entry (amount, evt.getWhenNoticed(), type);
evt.getCustomer().addEntry(newEntry);
evt.addResultingEntry(newEntry);
}
public void process (AccountingEvent evt) {
makeEntry(evt, calculateAmount(evt));
}
abstract protected Money calculateAmount(AccountingEvent evt);
}
&
'.
(
#
public class Usage extends AccountingEvent
{
private Quantity amount;
public Usage(Quantity amount, MfDate whenOccurred, MfDate whenNoticed, Customer customer) {
super(EventType.USAGE, whenOccurred, whenNoticed, customer);
this.amount = amount;
}
public mf.Quantity getAmount() {
return amount;
}
double getRate() {
return getCustomer().getServiceAgreement().getRate();
}
}
7
#
5
%
.
-
3'
(
(
&
class MultiplyByRatePR extends PostingRule{
public MultiplyByRatePR (EntryType type) {
super(type);
}
protected Money calculateAmount(AccountingEvent evt) {
Usage usageEvent = (Usage) evt;
return Money.dollars(usageEvent.getAmount().getAmount() * usageEvent.getRate());
}
}
&
/
0821
C(
/
08D1
PostingRule
AccountingEvent
process(AccountingEvent)
calculateAmount(AccountingEvent)
UsageEvent MultiplyByRatePR
amount : Quantity
calculateAmount(AccountingEvent)
{type = USAGE}
class AccountingEvent {
public void process() {
findRule().process(this);
}
PostingRule findRule() {
PostingRule rule =
customer.getServiceAgreement().getPostingRule(this.getEventType(),
this.whenOccurred);
Assert.notNull("missing posting rule", rule);
return rule;
}...
multiply by rate
usage event service agreement
posting rule
process
find rule
get rate
new
usage entry
.
/
1
(
public void setUpRegular (){
acm = new Customer("Acme Coffee Makers");
ServiceAgreement standard = new ServiceAgreement();
standard.setRate(10);
standard.addPostingRule(
EventType.USAGE,
new MultiplyByRatePR(EntryType.BASE_USAGE),
new MfDate(1999, 10, 1));
acm.setServiceAgreement(standard);
}
...
.
&
.
3
"
(
class MonetaryEvent extends AccountingEvent {
Money amount;
MonetaryEvent(Money amount, EventType type, mf.MfDate whenOccurred,
mf.MfDate whenNoticed, Customer customer) {
super(type, whenOccurred, whenNoticed, customer);
this.amount = amount;
}
public mf.Money getAmount() {
return amount;
}
}
-
(
.
public void testService() {
AccountingEvent evt = new MonetaryEvent(
Money.dollars(40),
EventType.SERVICE_CALL,
new MfDate(1999, 10, 5),
new MfDate(1999, 10, 5),
acm);
evt.process();
Entry resultingEntry = (Entry) acm.getEntries().get(0);
assertEquals (Money.dollars(30), resultingEntry.getAmount());
&
.
entry type = BASE USAGE entry type = SERVICE entry type = SERVICE
multiplier = 0.5 multiplier = 0.5
fixed fee = $10 fixed fee = $15
E
'
"
&
+0
(
class PoorCapPR extends PostingRule {
double rate;
Quantity usageLimit;
PoorCapPR (EntryType type, double rate, Quantity usageLimit) {
super(type);
this.rate = rate;
this.usageLimit = usageLimit;
}
protected Money calculateAmount(AccountingEvent evt) {
Usage usageEvent = (Usage) evt;
Quantity amountUsed = usageEvent.getAmount();
Money amount;
return (amountUsed.isGreaterThan(usageLimit)) ?
Money.dollars(amountUsed.getAmount() * usageEvent.getRate()):
Money.dollars(amountUsed.getAmount() * this.rate);
}
}
.
&
'
:
(
)
/
1
/
1
@
)
#
'
'
++F
'
)
'#
(
*
(
(
(
A
'
B
'
'
'
)
'
++F
'
'
#
$
class Tester...
public void setUpRegular (){
acm = new Customer("Acme Coffee Makers");
ServiceAgreement standard = new ServiceAgreement();
...
standard.addPostingRule(
EventType.TAX,
new AmountFormulaPR(0.055, Money.dollars(0), EntryType.TAX),
new MfDate(1999, 10, 1));
acm.setServiceAgreement(standard);
}
*
'
class PostingRule...
public void process (AccountingEvent evt) {
makeEntry(evt, calculateAmount(evt));
if (isTaxable()) new TaxEvent(evt, calculateAmount(evt)).process();
}
-
'
'
#
class PostingRule...
private boolean isTaxable() {
return !(type == EntryType.TAX);
}
multiply by rate
usage event service agreement
posting rule
process
find rule
get rate
new
usage entry
new
tax event
process
('
G
-
'
(
'
class TaxEvent extends MonetaryEvent {
private AccountingEvent base;
public TaxEvent(AccountingEvent base, Money taxableAmount) {
super (taxableAmount, EventType.TAX, base.getWhenOccurred(),
base.getWhenNoticed(), base.getCustomer());
this.base = base;
Assert.isFalse("Probable endless recursion", base.getEventType() == getEventType());
}
}
/
1
!
"#
#
6
(
class TaxEvent...
public TaxEvent(AccountingEvent base, Money taxableAmount) {
super (taxableAmount, EventType.TAX, base.getWhenOccurred(),
base.getWhenNoticed(), base.getCustomer());
this.base = base;
base.friendAddSecondaryEvent(this);
Assert.isFalse("Probable endless recursion", base.getEventType() == getEventType());
...
class AccountingEvent ...
private List secondaryEvents = new ArrayList();
void friendAddSecondaryEvent (AccountingEvent arg) {
// only to be called by the secondary event's setting method
secondaryEvents.add(arg);
}
.
class AccountingEvent...
Set getAllResultingEntries() {
Set result = new HashSet();
result.addAll(resultingEntries);
Iterator it = secondaryEvents.iterator();
while (it.hasNext()) {
AccountingEvent each = (AccountingEvent) it.next();
result.addAll(each.getResultingEntries());
}
return result;
}
3
class Tester
public void testUsage() {
Usage evt = new Usage(
Unit.KWH.amount(50),
new MfDate(1999, 10, 1),
new MfDate(1999, 10, 1),
acm);
evt.process();
Entry usageEntry = getEntry(acm, 0);
Entry taxEntry = getEntry(acm, 1);
assertEquals (Money.dollars(500), usageEntry.getAmount());
assertEquals (EntryType.BASE_USAGE, usageEntry.getType());
assertEquals (Money.dollars(27.5), taxEntry.getAmount());
assertEquals (EntryType.TAX, taxEntry.getType());
assert(evt.getResultingEntries().contains(usageEntry));
assert(evt.getAllResultingEntries().contains(taxEntry));
}
$
##
%
&
Account
balance
Entry
balance (DateRange)
withdrawels(DateRange) 1 ✻
deposits(DateRange)
#
.
H
@
1
Customer
✻
✻ 1
Entry Location
✻
1
Entry Type
1
Customer
✻
✻ 1 ✻ 1
Entry Account Location
✻
1
Entry Type
.
3
.
<
$ E
(
%
(
3
'
)
'
&
()
!
(
4
(
*#+
!I
*#
!
(
'
(
*
+
'
&
3
%
.
'
H
(
/
' 1
<
class Account ...
private Collection entries = new HashSet();
private Currency currency;
void addEntry(Money amount, MfDate date){
Assert.equals(currency, amount.currency());
entries.add(new Entry(amount, date));
}
class Account...
Money balance(DateRange period) {
Money result = new Money (0, currency);
Iterator it = entries.iterator();
while (it.hasNext()) {
Entry each = (Entry) it.next();
if (period.includes(each.date())) result = result.add(each.amount());
}
return result;
}
Money balance(MfDate date) {
return balance(DateRange.upTo(date));
}
Money balance() {
return balance(MfDate.today());
}
*
Entry 2 1 Accounting
Account
Transaction
1 ✻ amount: Money
{sum of amounts of
entries equals 0}
!
3
6
(
%
(
.
#
(
#
4
an Entry
cash: Account
amount = $100
an Accounting
Transaction
an Entry
checking: Account
amount = -$100
(
.
I
(
.
&
/
1
/
1
#&
1 from ✻
Accounting Transaction
Account
amount: money
1 to ✻
{sum of amounts of
entries equals 0}
.
#
%
3
&
.&" !
an Entry
royalties: Account
amount = - $50
an Entry
an Accounting
salary: Account
Transaction
amount = - $100
an Entry
checking: Account
amount = $150
!
(
.
-
(
*
//
*
//
&
*
//
#
*
//
(
&
'
I
*
//
&
'
'
'
"
'
*
//
7
'
3
7
&
%
&
( %
E
$
void withdraw(Money amount, Account target, MfDate date) {
new AccountingTransaction (amount, this, target, date);
}
(
public void testBalanceUsingTransactions() {
revenue = new Account(Currency.USD);
deferred = new Account(Currency.USD);
receivables = new Account(Currency.USD);
revenue.withdraw(Money.dollars(500), receivables, new MfDate(1,4,99));
revenue.withdraw(Money.dollars(200), deferred, new MfDate(1,4,99));
assertEquals(Money.dollars(500), receivables.balance());
assertEquals(Money.dollars(200), deferred.balance());
assertEquals(Money.dollars(-700), revenue.balance());
}
(
.&" !
.
*
4
public class AccountingTransaction {
private MfDate date;
private Collection entries = new HashSet();
private boolean wasPosted = false;
public AccountingTransaction(MfDate date) {
this.date = date;
}
&
'
(
'
(
class Transaction...
public void add (Money amount, Account account) {
if (wasPosted) throw new ImmutableTransactionException
("cannot add entry to a transaction that's already posted");
entries.add(new Entry (amount, date, account, this));
}
/5
1
class Entry...
private Money amount;
private MfDate date;
private Account account;
private AccountingTransaction transaction;
Entry(Money amount, MfDate date, Account account, AccountingTransaction transaction) {
// only used by AccountingTransaction
this.amount = amount;
this.date = date;
this.account = account;
this.transaction = transaction;
}
*
class AccountingTransaction...
public void post() {
if (!canPost())
throw new UnableToPostException();
Iterator it = entries.iterator();
while (it.hasNext()) {
Entry each = (Entry) it.next();
each.post();
}
wasPosted = true;
}
public boolean canPost(){
return balance().isZero();
}
private Money balance() {
if (entries.isEmpty()) return Money.dollars(0);
Iterator it = entries.iterator();
Entry firstEntry = (Entry) it.next();
Money result = firstEntry.amount();
while (it.hasNext()) {
Entry each = (Entry) it.next();
result = result.add(each.amount());
}
return result;
}
class Entry...
void post() {
// only used by AccountingTransaction
account.addEntry(this);
}
AccountingTransaction multi = new AccountingTransaction(new MfDate(2000,1,4));
multi.add(Money.dollars(-700), revenue);
multi.add(Money.dollars(500), receivables);
multi.add(Money.dollars(200), deferred);
multi.post();
assertEquals(Money.dollars(500), receivables.balance());
assertEquals(Money.dollars(200), deferred.balance());
assertEquals(Money.dollars(-700), revenue.balance());
#
(
class Account...
void withdraw(Money amount, Account target, MfDate date) {
AccountingTransaction trans = new AccountingTransaction(date);
trans.add(amount.negate(), this);
trans.add(amount, target);
trans.post();
}
1
#
&"&
2
0
adjusted event
amount = ($500)
replacement event
$
*
(
(8
E
4
=0(
0,2
)
(
&
.
amount = ($500)
replacement event date = 15 Oct 99
@
"#
&
'
&
%
&
%
$
#"#4(
6
:
.
,
"#
@
(
('
.
'
&
+0
8
>0
8+
class Tester...
public void setUp(){
setUpRegular();
setUpLowPay();
usageEvent = new Usage(
Unit.KWH.amount(50),
new MfDate(1999, 10, 1),
new MfDate(1999, 10, 1),
acm);
eventList.add(usageEvent);
eventList.process();
}
(
class AccountingEvent...
public void process() {
Assert.isFalse ("Cannot process an event twice", isProcessed);
if (adjustedEvent != null) adjustedEvent.reverse();
findRule().process(this);
isProcessed = true;
}
void reverse() {
Collection entries = new HashSet(getResultingEntries());
Iterator it = entries.iterator();
while (it.hasNext()) {
Entry each = (Entry) it.next();
Entry reversingEntry = new Entry(
each.getAmount().reverse(),
whenNoticed,
each.getType());
getCustomer().addEntry(reversingEntry);
this.addResultingEntry(reversingEntry);
}
reverseSecondaryEvents();
}
private void reverseSecondaryEvents(){
Iterator it = getSecondaryEvents().iterator();
while (it.hasNext()) {
AccountingEvent each = (AccountingEvent) it.next();
each.reverse();
}
}
secondary event of
replacement event adjusted event an existing Entry
adjusted event
process
[adjusted event != null]
reverse
* get information
* create
reversing entry
* reverse
find rule
"
&
& 0
replacement
event
"#
6
.
#"#4
old events
:Entry
:Adjustment
amount = ($550)
new events
'
0,+
-
)
(
(
"#
:
(
(
.
Usage Account
: Customer
(
5
Usage Account
: Customer
shadow account
-
Usage Account
balance = $2050
: Customer
shadow account
balance = $1500
(
Usage Account
balance = $1500
: Customer
J
J
J
J
/
0201(
Adjustment
0..1 0..1
(
(
5
"# ,
"#
'
'
"#
,
"#
"#
,
"#
(
/)
"#
1
#
// replacement events
MfDate adjDate = new MfDate(2000,1,12);
Usage new1 = new Usage (// snip constructor args
Usage new2 = new Usage (// snip constructor args
Usage new3 = new Usage (// snip constructor args
Adjustment adj = new Adjustment(adjDate, adjDate, acm);
adj.addOld(usageEvent);
adj.addOld(usage2);
adj.addOld(usage3);
adj.addNew(new1);
adj.addNew(new2);
adj.addNew(new3);
eventList.add(adj);
eventList.process();
(
class Adjustment...
private java.util.Map savedAccounts;
public void process() {
Assert.isFalse ("Cannot process an event twice", isProcessed);
adjust();
markProcessed();
}
void adjust() {
snapshotAccounts();
reverseOldEvents();
processReplacements();
commit();
secondaryEvents = new ArrayList();
secondaryEvents.addAll(oldEvents);
}
public void snapshotAccounts() {
savedAccounts = getCustomer().getAccounts();
getCustomer().setAccounts(copyAccounts(savedAccounts));
}
void reverseOldEvents() {
Iterator it = oldEvents.iterator();
while (it.hasNext()) {
AccountingEvent each = (AccountingEvent) it.next();
each.reverse();
}
}
void processReplacements() {
AccountingEvent[] list = (AccountingEvent[])newEvents.toArray(new AccountingEvent[0]);
for (int i = 0; i < list.length; i++){
list[i].process();}
}
public void commit() {
AccountType[] types = AccountType.types();
for (int i = 0; i < types.length; i++) {
adjustAccount(types[i]);
}
restoreAccounts();
}
public void adjustAccount(AccountType type) {
Account correctedAccount = getCustomer().accountFor(type);
Account originalAccount = (Account) getSavedAccounts().get(type);
Money difference = correctedAccount.balance().subtract(originalAccount.balance());
Entry result = new Entry (difference, MfDate.today());
originalAccount.addEntry(result);
resultingEntries.add(result);
}
public void restoreAccounts() {
getCustomer().setAccounts(savedAccounts);
}
(
(
*
/
1 (
"
& #
#
original Usage Event resulting entries original Usage Entry entries {destroyed}
adjusted event
:customer
replacement event
$
( %
.
!
(
4
'
.
"# ,
+
"#
(
/
1
(
#"#4
(
#
"# 4
"# ,
"#
@
#"#4
(
%
#
#"#4
#
/
1
)
(
(
.
class AccountingEvent
private AccountingEvent adjustedEvent, replacementEvent;
public AccountingEvent (EventType type, MfDate whenOccurred,
MfDate whenNoticed, AccountingEvent adjustedEvent)
{
if (adjustedEvent.hasBeenAdjusted())
throw new IllegalArgumentException
("Cannot create " + this + ". " + adjustedEvent + " is already adjusted");
this.type = type;
this.whenOccurred = whenOccurred;
this.whenNoticed = whenNoticed;
this.adjustedEvent = adjustedEvent;
adjustedEvent.replacementEvent = this;
}
protected boolean hasBeenAdjusted() {
return (replacementEvent != null);
}
(