diff --git a/CHANGELOG.md b/CHANGELOG.md index 77f21b4..55ef504 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,36 @@ Changelog for ghacupha book-keeper. ## Unreleased ### No issue +**added currency parameter to SimpleTransaction** + + +[a972b7f4334c5cd](https://github.com/ghacupha/book-keeper/commit/a972b7f4334c5cd) Edwin Njeru *2018-03-29 16:08:07* + +**simplified account pattern to only implement directed accounting transaction pattern** + + +[bf67a14b0a76fd8](https://github.com/ghacupha/book-keeper/commit/bf67a14b0a76fd8) Edwin Njeru *2018-03-29 15:35:58* + +**simplified account pattern to only implement directed accounting transaction pattern** + + +[adf6ba14643217b](https://github.com/ghacupha/book-keeper/commit/adf6ba14643217b) Edwin Njeru *2018-03-29 15:07:25* + +**Used filtered streams to capture debits abd credits in the DirectedTransaction. Created more intuitive naming for Account implementation** + + +[08b8f29d9446f36](https://github.com/ghacupha/book-keeper/commit/08b8f29d9446f36) Edwin Njeru *2018-03-28 12:23:15* + +**failed account-reversal test. To review later using spotbugs** + + +[09709770ed39efa](https://github.com/ghacupha/book-keeper/commit/09709770ed39efa) Edwin Njeru *2018-03-26 15:15:47* + +**added configurations for maven reversing java version to 1.8_162 to resolve maven-integration issues** + + +[7674e3d3da5e389](https://github.com/ghacupha/book-keeper/commit/7674e3d3da5e389) Edwin Njeru *2018-03-26 13:22:58* + **release preparation** @@ -189,12 +219,12 @@ Changelog for ghacupha book-keeper. [c366bc1cdb1e91f](https://github.com/ghacupha/book-keeper/commit/c366bc1cdb1e91f) ghacupha *2018-03-18 09:02:18* -**amended wrong type in the forAccount** +**amended wrong type in the account** [9f80fed5aabbb7d](https://github.com/ghacupha/book-keeper/commit/9f80fed5aabbb7d) ghacupha *2018-03-18 08:58:38* -**added tests for the forAccount** +**added tests for the account** [23bbdf320d952d3](https://github.com/ghacupha/book-keeper/commit/23bbdf320d952d3) ghacupha *2018-03-18 08:57:42* diff --git a/README.md b/README.md index 6337052..b9c876c 100644 --- a/README.md +++ b/README.md @@ -6,136 +6,85 @@ Business accounts comprehension library for java. The book-keeper implements Martin Fowler's Accounting Entry, Account and Accounting Transaction design patterns to create accounting records of business transactions in any java program or game. -The Account and Entry interfaces, can be used on their own to track monetary traffic as shown +The Account and Entry interfaces are used together to relate different account balance positions at different time periods. The +balance of the account is not signed, but the account side (Debit or Credit) to which the state of the account is in keeps +changing according to the entries posted. +The transaction interface is used to post multiple entries into multiple accounts at the same time ensuring that the money +is not created or destroyed (as per double-entry requirements), only transferred from one account to another. Again, money is +not created or destroyed, but transferred from one account to another. Any transaction must have accounts where we are debiting +and accounts we are crediting, and the debit entries must be equivalent to credit entries and in the same currency. ```java -public class AccountTest { +public class DirectedSimpleAccountTest { - private Account electronicEquipmentAssetAccount; + // Required for the reversal tests + Account advertisement = new SimpleAccount(DEBIT,Currency.getInstance("KES"),new AccountDetails("Advertisements","5280", SimpleDate.on(2017,3,31))); + Account vat = new SimpleAccount(CREDIT,Currency.getInstance("KES"),new AccountDetails("VAT","5281", SimpleDate.on(2017,3,31))); + Account chequeAccount = new SimpleAccount(CREDIT,Currency.getInstance("KES"),new AccountDetails("Cheque","5282", SimpleDate.on(2017,3,31))); + Account edwinsAccount = new SimpleAccount(CREDIT,Currency.getInstance("KES"),new AccountDetails("Edwin Njeru","40001", SimpleDate.on(2017,10,5))); - @Before - public void setUp() throws Exception { - TimePoint openingDate = Moment.newMoment(2017,5,12); - AccountAttributes details = new AccountDetails("Electronics","001548418",openingDate); - electronicEquipmentAssetAccount = new AccountImpl(DEBIT, Currency.getInstance("KES"),details); - } - - @Test - public void addEntry() throws Exception { - EntryAttributes details = new EntryDetails("Keeper Supermarket invoice 10 Television set","Invoice#10","For Office"); - Cash amount = HardCash.of(105.23,"KES"); - Entry entry = new AccountingEntry(electronicEquipmentAssetAccount,details,amount,new Moment(2018,2,12)); - electronicEquipmentAssetAccount.addEntry(entry); - - Assert.assertEquals(105.23, electronicEquipmentAssetAccount.balance(new Moment()).getAmount().getNumber().doubleValue(),0.00); - } @Test - public void balance() throws Exception { - - EntryAttributes details = new EntryDetails("Keeper Supermarket invoice 10 Television set","Invoice#10","For Office"); - Cash tvPrice = HardCash.of(105.23,"KES"); - Entry purchaseOfTV = new AccountingEntry(electronicEquipmentAssetAccount,details,tvPrice,new Moment(2018,2,12)); - electronicEquipmentAssetAccount.addEntry(purchaseOfTV); - - EntryAttributes details2 = new EntryDetails("Keeper Supermarket invoice 12 Fridge","Invoice#12","For Home"); - Cash amount2 = HardCash.of(200.23,"KES"); - Entry purchaseOfFridge = new AccountingEntry(electronicEquipmentAssetAccount,details2,amount2,new Moment(2018,2,15)); - electronicEquipmentAssetAccount.addEntry(purchaseOfFridge); - - EntryAttributes etrPurchaseDetails = new EntryDetails("Electronic Tax Register Machine"); - etrPurchaseDetails.setStringAttribute("Tax code","EY83E8"); - Cash etrPrice = HardCash.shilling(50.18); - TimePoint etrPurchaseDate = new Moment(2018,3,1); - Entry purchaseOfETR = new AccountingEntry(electronicEquipmentAssetAccount,etrPurchaseDetails,etrPrice,etrPurchaseDate); - electronicEquipmentAssetAccount.addEntry(purchaseOfETR); - - Assert.assertEquals(305.46, electronicEquipmentAssetAccount.balance(new Moment(2018,2,16)).getAmount().getNumber().doubleValue(),0.00); - Assert.assertEquals(105.23, electronicEquipmentAssetAccount.balance(new Moment(2018,2,13)).getAmount().getNumber().doubleValue(),0.00); - Assert.assertEquals(355.64, electronicEquipmentAssetAccount.balance().getAmount().getNumber().doubleValue(),0.00); - } - -} -``` - -The book-keeper can also track entire transactions, each of which may contain several accounts as illustrated bellow : - -```java -public class AccountingTransactionTest { - - // Subscriptions expense journal - Account subscriptionExpenseAccount; - AccountAttributes subscriptionExpenseAccountAttributes; - EntryAttributes subscriptionAccountEntryDetails; - - // Withhoding tax journal - Account withholdingTaxAccount; - AccountAttributes withholdingTaxAccountAttributes; - EntryAttributes withholdingTaxDetailsEntry; - - // Banker's cheque suspense journal - Account bankersChqAccountSuspense; - AccountAttributes bankersChequeAccountDetails; - EntryAttributes bankersChequeAccountEntry; - - - @Before - public void setUp() throws Exception { - - subscriptionExpenseAccountAttributes = - new AccountDetails("Subscriptions","506",Moment.newMoment(2017,6,30)); - subscriptionExpenseAccount = new AccountImpl(DEBIT,Currency.getInstance("USD"),subscriptionExpenseAccountAttributes); - subscriptionAccountEntryDetails = new EntryDetails("DSTV subscriptionAccountEntryDetails","Invoice# 1023","Approved in the budget", - "MultiChoice Group Inc"); - - withholdingTaxAccountAttributes = - new AccountDetails("WithholdingTax","808",Moment.newMoment(2017,6,30)); - withholdingTaxAccount = new AccountImpl(CREDIT,Currency.getInstance("USD"),withholdingTaxAccountAttributes); - withholdingTaxDetailsEntry = new EntryDetails("6% Withholding VAT","PIN#25646","Vendor under advisement","MultiChoice Group Inc"); - withholdingTaxDetailsEntry.setStringAttribute("Invoice#","1023"); - - bankersChequeAccountDetails = - AccountDetails.newDetails("Banker's Cheque A/C Suspense","303",Moment.newMoment(2017,6,30)); - bankersChqAccountSuspense = new AccountImpl(CREDIT,Currency.getInstance("USD"),bankersChequeAccountDetails); - bankersChequeAccountEntry = EntryDetails.newDetails("BCHQ ifo MultiChoice Group","CHQ#5642","To print","MultiChoiceGroup Inc"); - bankersChequeAccountEntry.setStringAttribute("Bank Name","ABC Banks"); - bankersChequeAccountEntry.setStringAttribute("Bank Branch","WestLands"); - bankersChequeAccountEntry.setStringAttribute("Bank Branch Code","01"); - } - - @Test - public void accountingTransactionWorks() throws Exception, UnableToPostException, MismatchedCurrencyException, ImmutableEntryException { + public void directedTransactionWorks() throws Exception, UnableToPostException, MismatchedCurrencyException, ImmutableEntryException { // Create the transaction - Transaction transaction = new AccountingTransaction(new Moment(2018,1,2), Currency.getInstance("USD")); - - transaction.add(HardCash.dollar(-800), subscriptionExpenseAccount, subscriptionAccountEntryDetails); - transaction.add(HardCash.dollar(36),withholdingTaxAccount, withholdingTaxDetailsEntry); - transaction.add(HardCash.dollar(764),bankersChqAccountSuspense,bankersChequeAccountEntry); - - transaction.post(); // Transaction must be posted to be effective - - assertEquals(AccountBalance.newBalance(HardCash.dollar(-800), DEBIT),subscriptionExpenseAccount.balance()); - assertEquals(AccountBalance.newBalance(HardCash.dollar(36), CREDIT),withholdingTaxAccount.balance()); - assertEquals(AccountBalance.newBalance(HardCash.dollar(764), CREDIT),bankersChqAccountSuspense.balance()); - } - - @Test - public void unPostedAccountingTransactionFails() throws Exception, MismatchedCurrencyException, ImmutableEntryException, UnableToPostException { - - Transaction transaction = - new AccountingTransaction(new Moment(2018,1,2), Currency.getInstance("USD")); - - transaction.add(HardCash.dollar(-800), subscriptionExpenseAccount, subscriptionAccountEntryDetails); - transaction.add(HardCash.dollar(36),withholdingTaxAccount, withholdingTaxDetailsEntry); - transaction.add(HardCash.dollar(764),bankersChqAccountSuspense,bankersChequeAccountEntry); - - // Crickets... - - assertEquals(AccountBalance.newBalance(HardCash.dollar(0), DEBIT),subscriptionExpenseAccount.balance()); - assertEquals(AccountBalance.newBalance(HardCash.dollar(0), CREDIT),withholdingTaxAccount.balance()); - assertEquals(AccountBalance.newBalance(HardCash.dollar(0), CREDIT),bankersChqAccountSuspense.balance()); + Transaction payForBillBoards = new SimpleTransaction(new SimpleDate(2017,11,2),Currency.getInstance("KES")); + + payForBillBoards.addEntry(DEBIT,HardCash.shilling(200),advertisement,new EntryDetails("Billboards ltd inv 10")); + payForBillBoards.addEntry(CREDIT,HardCash.shilling(32),vat,new EntryDetails("VAT for billBoards")); + payForBillBoards.addEntry(CREDIT,HardCash.shilling(168),chequeAccount,new EntryDetails("CHQ IFO Billboards Ltd")); + + // non-posted + assertEquals(AccountBalance.newBalance(HardCash.shilling(0),DEBIT),advertisement.balance(2018,1,3)); + assertEquals(AccountBalance.newBalance(HardCash.shilling(0),CREDIT),vat.balance(2018,1,3)); + assertEquals(AccountBalance.newBalance(HardCash.shilling(0),CREDIT),chequeAccount.balance(2018,1,3)); + + // after posting + payForBillBoards.post(); + assertEquals(AccountBalance.newBalance(HardCash.shilling(200),DEBIT),advertisement.balance(2017,11,30)); + assertEquals(AccountBalance.newBalance(HardCash.shilling(32),CREDIT),vat.balance(2017,11,30)); + assertEquals(AccountBalance.newBalance(HardCash.shilling(168),CREDIT),chequeAccount.balance(2017,11,30)); + + // Reimbursement Transaction + Transaction reimbursement = new SimpleTransaction(new SimpleDate(2017,12,20), Currency.getInstance("KES")); + + reimbursement.addEntry(DEBIT,HardCash.shilling(150),advertisement,new EntryDetails("Reimburse Edwin For Meeting expenses with Billboard guys")); + reimbursement.addEntry(CREDIT,HardCash.shilling(150), edwinsAccount,new EntryDetails("Reimbursement for meeting expenses with billboard guys")); + + // before posting + assertEquals(AccountBalance.newBalance(HardCash.shilling(0),CREDIT), edwinsAccount.balance(2017,12,31)); + assertEquals(AccountBalance.newBalance(HardCash.shilling(200),DEBIT),advertisement.balance(2017,12,31)); + assertEquals(AccountBalance.newBalance(HardCash.shilling(32),CREDIT),vat.balance(2017,12,31)); + assertEquals(AccountBalance.newBalance(HardCash.shilling(168),CREDIT),chequeAccount.balance(2017,12,31)); + + // after posting the reimbursement + reimbursement.post(); + assertEquals(AccountBalance.newBalance(HardCash.shilling(150),CREDIT), edwinsAccount.balance(2018,1,31)); + assertEquals(AccountBalance.newBalance(HardCash.shilling(350),DEBIT),advertisement.balance(2018,1,31)); + assertEquals(AccountBalance.newBalance(HardCash.shilling(32),CREDIT),vat.balance(2018,1,31)); + assertEquals(AccountBalance.newBalance(HardCash.shilling(168),CREDIT),chequeAccount.balance(2018,1,31)); + + // what if the manager wants a previous position as at 5th November 2017 + assertEquals(AccountBalance.newBalance(HardCash.shilling(0),CREDIT), edwinsAccount.balance(2017,11,5)); + assertEquals(AccountBalance.newBalance(HardCash.shilling(200),DEBIT),advertisement.balance(2017,11,5)); + assertEquals(AccountBalance.newBalance(HardCash.shilling(32),CREDIT),vat.balance(2017,11,5)); + assertEquals(AccountBalance.newBalance(HardCash.shilling(168),CREDIT),chequeAccount.balance(2017,11,5)); + + // Someone screwed up the taxes, we have to reverse + Transaction taxReversal = new SimpleTransaction(SimpleDate.on(2018,4,20), Currency.getInstance("KES")); + + taxReversal.addEntry(DEBIT,HardCash.shilling(45),vat,new EntryDetails("Reversal of Excess VAT")); + taxReversal.addEntry(CREDIT,HardCash.shilling(45),advertisement,new EntryDetails("Reversal of Excess VAT")); + + taxReversal.post(); + + // balance after reversal transaction is posted... + assertEquals(AccountBalance.newBalance(HardCash.shilling(150),CREDIT), edwinsAccount.balance(2018,4,25)); + assertEquals(AccountBalance.newBalance(HardCash.shilling(305),DEBIT),advertisement.balance(2018,4,25)); + assertEquals(AccountBalance.newBalance(HardCash.shilling(13),DEBIT),vat.balance(2018,4,25)); + assertEquals(AccountBalance.newBalance(HardCash.shilling(168),CREDIT),chequeAccount.balance(2018,4,25)); } } -``` \ No newline at end of file +``` + diff --git a/src/main/java/io/github/ghacupha/keeper/book/base/AccountDetails.java b/src/main/java/io/github/ghacupha/keeper/book/base/AccountDetails.java index 9350c0a..bd0f143 100644 --- a/src/main/java/io/github/ghacupha/keeper/book/base/AccountDetails.java +++ b/src/main/java/io/github/ghacupha/keeper/book/base/AccountDetails.java @@ -39,15 +39,15 @@ public AccountDetails(String name, String number, TimePoint openingDate) { this.openingDate = openingDate; } - public String getName() { + String getName() { return name; } - public String getNumber() { + String getNumber() { return number; } - public TimePoint getOpeningDate() { + TimePoint getOpeningDate() { return openingDate; } diff --git a/src/main/java/io/github/ghacupha/keeper/book/base/EntryDetails.java b/src/main/java/io/github/ghacupha/keeper/book/base/EntryDetails.java index ae7911f..3127ef7 100644 --- a/src/main/java/io/github/ghacupha/keeper/book/base/EntryDetails.java +++ b/src/main/java/io/github/ghacupha/keeper/book/base/EntryDetails.java @@ -47,7 +47,7 @@ public void setAttribute(String label, Object attribute){ public Object getAttribute(String label) throws UnEnteredDetailsException { if(!entryMap.containsKey(label)){ - throw new UnEnteredDetailsException(String.format("Could not find %s since it was never added in the first place")); + throw new UnEnteredDetailsException(String.format("Could not find %s since it was never added in the first place",label)); } else { return entryMap.get(label); } diff --git a/src/main/java/io/github/ghacupha/keeper/book/base/SimpleAccount.java b/src/main/java/io/github/ghacupha/keeper/book/base/SimpleAccount.java index 76a65a7..e24ae31 100644 --- a/src/main/java/io/github/ghacupha/keeper/book/base/SimpleAccount.java +++ b/src/main/java/io/github/ghacupha/keeper/book/base/SimpleAccount.java @@ -111,6 +111,7 @@ public AccountBalance balance(TimePoint asAt) { */ @Override public AccountBalance balance(int... asAt) { + AccountBalance balance = balance(new SimpleDate(asAt[0],asAt[1],asAt[2])); log.debug("Returning accounting balance as at : {} as : {}", asAt, balance); @@ -171,10 +172,6 @@ private Cash getDebits(DateRange dateRange) { .reduce(0.00,(acc,value) -> acc + value), this.getCurrency()); } - private static boolean entryIsCreditingUs(Entry entry) { - return entry.getAccountSide() == CREDIT; - } - /** * @return Currency of the account */ diff --git a/src/main/java/io/github/ghacupha/keeper/book/base/SimpleTransaction.java b/src/main/java/io/github/ghacupha/keeper/book/base/SimpleTransaction.java index 010616c..5b37226 100644 --- a/src/main/java/io/github/ghacupha/keeper/book/base/SimpleTransaction.java +++ b/src/main/java/io/github/ghacupha/keeper/book/base/SimpleTransaction.java @@ -122,7 +122,7 @@ public void post() throws UnableToPostException, ImmutableEntryException { log.debug("Posting : {} entries ...",entries.size()); - entries.forEach(Entry::post); + entries.parallelStream().forEach(Entry::post); wasPosted = true; } @@ -130,9 +130,9 @@ public void post() throws UnableToPostException, ImmutableEntryException { private double balanced() { - double debits = entries.stream().filter(SimpleTransaction::predicateDebits).map(SimpleTransaction::mapCashToDouble).reduce(0.00, (acc, val) -> acc + val); + double debits = entries.parallelStream().filter(SimpleTransaction::predicateDebits).map(SimpleTransaction::mapCashToDouble).reduce(0.00, (acc, val) -> acc + val); - return debits - entries.stream().filter(SimpleTransaction::predicateCredits).map(SimpleTransaction::mapCashToDouble).reduce(0.00, (acc, val) -> acc + val); + return debits - entries.parallelStream().filter(SimpleTransaction::predicateCredits).map(SimpleTransaction::mapCashToDouble).reduce(0.00, (acc, val) -> acc + val); } @Override diff --git a/src/test/java/io/github/ghacupha/keeper/book/base/DirectedSimpleAccountTest.java b/src/test/java/io/github/ghacupha/keeper/book/base/AccountTest.java similarity index 88% rename from src/test/java/io/github/ghacupha/keeper/book/base/DirectedSimpleAccountTest.java rename to src/test/java/io/github/ghacupha/keeper/book/base/AccountTest.java index d36ddc6..21271f5 100644 --- a/src/test/java/io/github/ghacupha/keeper/book/base/DirectedSimpleAccountTest.java +++ b/src/test/java/io/github/ghacupha/keeper/book/base/AccountTest.java @@ -16,13 +16,13 @@ import static io.github.ghacupha.keeper.book.balance.AccountSide.DEBIT; import static org.junit.Assert.assertEquals; -public class DirectedSimpleAccountTest { +public class AccountTest { // Required for the reversal tests - Account advertisement = new SimpleAccount(DEBIT,Currency.getInstance("KES"),new AccountDetails("Advertisements","5280", SimpleDate.on(2017,3,31))); - Account vat = new SimpleAccount(CREDIT,Currency.getInstance("KES"),new AccountDetails("VAT","5281", SimpleDate.on(2017,3,31))); - Account chequeAccount = new SimpleAccount(CREDIT,Currency.getInstance("KES"),new AccountDetails("Cheque","5282", SimpleDate.on(2017,3,31))); - Account edwinsAccount = new SimpleAccount(CREDIT,Currency.getInstance("KES"),new AccountDetails("Edwin Njeru","40001", SimpleDate.on(2017,10,5))); + private Account advertisement = new SimpleAccount(DEBIT,Currency.getInstance("KES"),new AccountDetails("Advertisements","5280", SimpleDate.on(2017,3,31))); + private Account vat = new SimpleAccount(CREDIT,Currency.getInstance("KES"),new AccountDetails("VAT","5281", SimpleDate.on(2017,3,31))); + private Account chequeAccount = new SimpleAccount(CREDIT,Currency.getInstance("KES"),new AccountDetails("Cheque","5282", SimpleDate.on(2017,3,31))); + private Account edwinsAccount = new SimpleAccount(CREDIT,Currency.getInstance("KES"),new AccountDetails("Edwin Njeru","40001", SimpleDate.on(2017,10,5))); @@ -47,7 +47,7 @@ public void directedTransactionWorks() throws Exception, UnableToPostException, assertEquals(AccountBalance.newBalance(HardCash.shilling(32),CREDIT),vat.balance(2017,11,30)); assertEquals(AccountBalance.newBalance(HardCash.shilling(168),CREDIT),chequeAccount.balance(2017,11,30)); - // Reversal Transaction + // Reimbursement Transaction Transaction reimbursement = new SimpleTransaction(new SimpleDate(2017,12,20), Currency.getInstance("KES")); reimbursement.addEntry(DEBIT,HardCash.shilling(150),advertisement,new EntryDetails("Reimburse Edwin For Meeting expenses with Billboard guys"));