diff --git a/src/main/java/io/github/ghacupha/keeper/book/api/Account.java b/src/main/java/io/github/ghacupha/keeper/book/api/Account.java index 072fa18..550caa4 100644 --- a/src/main/java/io/github/ghacupha/keeper/book/api/Account.java +++ b/src/main/java/io/github/ghacupha/keeper/book/api/Account.java @@ -18,12 +18,13 @@ import io.github.ghacupha.keeper.book.balance.AccountBalance; import io.github.ghacupha.keeper.book.balance.AccountSide; +import io.github.ghacupha.keeper.book.base.AccountAppraisalDelegate; import io.github.ghacupha.keeper.book.unit.time.TimePoint; import io.github.ghacupha.keeper.book.util.MismatchedCurrencyException; -import io.github.ghacupha.keeper.book.util.UnableToPostException; import io.github.ghacupha.keeper.book.util.UntimelyBookingDateException; import java.util.Currency; +import java.util.List; /** * A collection of {@link Entry} items. @@ -73,4 +74,20 @@ public interface Account { * @return {@link TimePoint} date when the account was opened */ TimePoint getOpeningDate(); + + /** + * @implSpec As per implementation notes this is for use only by the {@link AccountAppraisalDelegate} + * allowing inexpensive evaluation of the {@link AccountBalance} without causing circular reference. Otherwise anyone else who needs + * to know the {@code AccountSide} of this needs to query the {@link AccountBalance} first, and from it acquire the {@link AccountSide} + * + * @return Shows the side of the balance sheet to which this belongs which could be either + * {@link AccountSide#DEBIT} or {@link AccountSide#CREDIT} + */ + AccountSide getAccountSide(); + + /** + * + * @return Returns this object's current copy of the {@link Entry} items + */ + List getEntries(); } diff --git a/src/main/java/io/github/ghacupha/keeper/book/base/AccountAppraisalDelegate.java b/src/main/java/io/github/ghacupha/keeper/book/base/AccountAppraisalDelegate.java new file mode 100644 index 0000000..3800f35 --- /dev/null +++ b/src/main/java/io/github/ghacupha/keeper/book/base/AccountAppraisalDelegate.java @@ -0,0 +1,86 @@ +package io.github.ghacupha.keeper.book.base; + +import io.github.ghacupha.keeper.book.api.Account; +import io.github.ghacupha.keeper.book.api.Entry; +import io.github.ghacupha.keeper.book.balance.AccountBalance; +import io.github.ghacupha.keeper.book.balance.AccountSide; +import io.github.ghacupha.keeper.book.unit.money.Cash; +import io.github.ghacupha.keeper.book.unit.money.HardCash; +import io.github.ghacupha.keeper.book.unit.time.DateRange; + +import java.util.List; + +import static io.github.ghacupha.keeper.book.balance.AccountSide.CREDIT; +import static io.github.ghacupha.keeper.book.balance.AccountSide.DEBIT; + +/** + * Okay so then we had to expose the {@link AccountSide} against better advise since calling the {@link Account#balance} + * method is going to be an expensive method, which could most likely trigger a circular dependency loop. There needs to be a method + * for getting the current {@code AccountSide} without gritting your teeth. So uncle Bob please forgive me for I have sinned, + * but there is just no practical inexpensive way of doing this stuff, and still be able to use this delegate for any + * {@link Account} implementation. + * + * @author edwin.njeru + */ +public class AccountAppraisalDelegate { + + private final Account account; + + AccountAppraisalDelegate(Account account) { + + this.account = account; + } + + public AccountBalance balance(DateRange dateRange){ + + Cash debits = getDebits(dateRange,account.getEntries()); + + Cash credits = getCredits(dateRange, account.getEntries()); + + if (debits.isZero() || credits.isZero()) { + if(!debits.isZero() && credits.isZero()){ + return new AccountBalance(debits, DEBIT); + } else if(debits.isZero() && !credits.isZero()){ + return new AccountBalance(credits, CREDIT); + } + } else if (account.getAccountSide() == DEBIT) { + + if (credits.isMoreThan(debits)) { + return new AccountBalance(credits.minus(debits).abs(), CREDIT); + } + + if (!credits.isMoreThan(debits)) { + return new AccountBalance(credits.minus(debits).abs(), DEBIT); + } + } else if (account.getAccountSide() == CREDIT) { + + if (debits.isMoreThan(credits)) { + return new AccountBalance(debits.minus(credits).abs(), DEBIT); + } + + if (!debits.isMoreThan(credits)) { + return new AccountBalance(debits.minus(credits).abs(), CREDIT); + } + } + + return new AccountBalance(HardCash.of(0.0,account.getCurrency()),account.getAccountSide()); + } + + private Cash getCredits(DateRange dateRange,List accountEntries) { + return HardCash.of(accountEntries + .parallelStream() + .filter(entry -> dateRange.includes(entry.getBookingDate())) + .filter(entry -> entry.getAccountSide() == CREDIT) + .map(entry -> entry.getAmount().getNumber().doubleValue()) + .reduce(0.00,(acc,value) -> acc + value), account.getCurrency()); + } + + private Cash getDebits(DateRange dateRange,List accountEntries) { + return HardCash.of(accountEntries + .parallelStream() + .filter(entry -> dateRange.includes(entry.getBookingDate())) + .filter(entry -> entry.getAccountSide() == DEBIT) + .map(entry -> entry.getAmount().getNumber().doubleValue()) + .reduce(0.00,(acc,value) -> acc + value), account.getCurrency()); + } +} 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 3127ef7..0508025 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 @@ -72,9 +72,9 @@ public int hashCode() { @Override public String toString() { - final StringBuffer sb = new StringBuffer("EntryDetails{"); - sb.append("narration='").append(narration).append('\''); - sb.append(", entryMap=").append(entryMap); + final StringBuffer sb = new StringBuffer("{"); + sb.append("'").append(narration).append('\''); + sb.append(", otherEntryDetails=").append(entryMap); sb.append('}'); return sb.toString(); } 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 77151f7..0058595 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 @@ -20,8 +20,6 @@ import io.github.ghacupha.keeper.book.api.Entry; import io.github.ghacupha.keeper.book.balance.AccountBalance; import io.github.ghacupha.keeper.book.balance.AccountSide; -import io.github.ghacupha.keeper.book.unit.money.Cash; -import io.github.ghacupha.keeper.book.unit.money.HardCash; import io.github.ghacupha.keeper.book.unit.time.DateRange; import io.github.ghacupha.keeper.book.unit.time.SimpleDate; import io.github.ghacupha.keeper.book.unit.time.TimePoint; @@ -60,6 +58,8 @@ public final class SimpleAccount implements Account { private static final Logger log = LoggerFactory.getLogger(SimpleAccount.class); + private AccountAppraisalDelegate evalulationDelegate = new AccountAppraisalDelegate(this); + private final Currency currency; private final AccountDetails accountDetails; private volatile AccountSide accountSide; @@ -109,7 +109,7 @@ public AccountBalance balance(TimePoint asAt) { log.debug("Account balance enquiry raised as at {}, for account : {}", asAt, this); - AccountBalance balance = balance(new DateRange(accountDetails.getOpeningDate(), asAt)); + AccountBalance balance = evalulationDelegate.balance(new DateRange(accountDetails.getOpeningDate(), asAt)); log.debug("Returning accounting balance for {} as at : {} as : {}", this, asAt, balance); @@ -118,7 +118,7 @@ public AccountBalance balance(TimePoint asAt) { /** * Similar to the balance query for a given date except the date is provided through a - * simple varags int argument + * simple {@code VarArgs} int argument * * @param asAt The date as at when the {@link AccountBalance} we want is effective given * in the following order @@ -137,59 +137,6 @@ public AccountBalance balance(int... asAt) { return balance; } - private AccountBalance balance(DateRange dateRange){ - - Cash debits = getDebits(dateRange); - - Cash credits = getCredits(dateRange); - - if (debits.isZero() || credits.isZero()) { - if(!debits.isZero() && credits.isZero()){ - return new AccountBalance(debits, DEBIT); - } else if(debits.isZero() && !credits.isZero()){ - return new AccountBalance(credits, CREDIT); - } - } else if (this.accountSide == DEBIT) { - - if (credits.isMoreThan(debits)) { - return new AccountBalance(credits.minus(debits).abs(), CREDIT); - } - - if (!credits.isMoreThan(debits)) { - return new AccountBalance(credits.minus(debits).abs(), DEBIT); - } - } else if (this.accountSide == CREDIT) { - - if (debits.isMoreThan(credits)) { - return new AccountBalance(debits.minus(credits).abs(), DEBIT); - } - - if (!debits.isMoreThan(credits)) { - return new AccountBalance(debits.minus(credits).abs(), CREDIT); - } - } - - return new AccountBalance(HardCash.of(0.0,this.currency),this.accountSide); - } - - private Cash getCredits(DateRange dateRange) { - return HardCash.of(this.getEntries() - .parallelStream() - .filter(entry -> dateRange.includes(entry.getBookingDate())) - .filter(entry -> entry.getAccountSide() == CREDIT) - .map(entry -> entry.getAmount().getNumber().doubleValue()) - .reduce(0.00,(acc,value) -> acc + value), this.getCurrency()); - } - - private Cash getDebits(DateRange dateRange) { - return HardCash.of(this.getEntries() - .parallelStream() - .filter(entry -> dateRange.includes(entry.getBookingDate())) - .filter(entry -> entry.getAccountSide() == DEBIT) - .map(entry -> entry.getAmount().getNumber().doubleValue()) - .reduce(0.00,(acc,value) -> acc + value), this.getCurrency()); - } - /** * @return Currency of the account */ @@ -198,9 +145,10 @@ public Currency getCurrency() { return currency; } - private List getEntries() { + @Override + public List getEntries() { - return entries.stream().collect(ImmutableListCollector.toImmutableList()); + return new CopyOnWriteArrayList<>(entries.parallelStream().collect(ImmutableListCollector.toImmutableList())); } @Override @@ -212,4 +160,19 @@ public TimePoint getOpeningDate() { public String toString() { return this.accountDetails.getNumber()+" "+this.accountDetails.getName(); } + + /** + * @return Shows the side of the balance sheet to which this belongs which could be either + * {@link AccountSide#DEBIT} or {@link AccountSide#CREDIT} + * @implSpec As per implementation notes this is for use only by the {@link AccountAppraisalDelegate} + * allowing inexpensive evaluation of the {@link AccountBalance} without causing circular reference. Otherwise anyone else who needs + * to know the {@code AccountSide} of this needs to query the {@link AccountBalance} first, and from it acquire the {@link AccountSide}. + * Also note that the object's {@link AccountSide} is never really exposed since this implementation is returning a value based on its + * current status. + */ + @Override + public AccountSide getAccountSide() { + + return this.accountSide == DEBIT ? DEBIT : CREDIT; + } } 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 821d439..b1e2670 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 @@ -72,17 +72,17 @@ private static Double mapCashToDouble(Entry entry) { private static boolean predicateCredits(Entry entry) { boolean credit; - log.debug("Checking if entry {} is credit ", entry.getEntryDetails()); + log.trace("Checking if entry {} is credit ", entry.getEntryDetails()); credit = entry.getAccountSide() == CREDIT; - log.debug("Entry : {} is credit {}", entry.getEntryDetails(), credit); + log.trace("Entry : {} is credit {}", entry.getEntryDetails(), credit); return credit; } private static boolean predicateDebits(Entry entry) { boolean debit; - log.debug("Checking if entry {} is debit ", entry.getEntryDetails()); + log.trace("Checking if entry {} is debit ", entry.getEntryDetails()); debit = entry.getAccountSide() == DEBIT; - log.debug("Entry : {} is credit {}", entry.getEntryDetails(), debit); + log.trace("Entry : {} is credit {}", entry.getEntryDetails(), debit); return debit; } @@ -104,7 +104,9 @@ public void addEntry(AccountSide accountSide, Cash amount, Account account, Entr throw new MismatchedCurrencyException("Cannot add entry whose getCurrency differs to that of the transaction"); } else { log.debug("Adding entry : {} into transaction : {}",details,this); - entries.add(new SimpleEntry(accountSide, account, amount, date, details)); + Entry tempEntry = new SimpleEntry(accountSide, account, amount, date, details); + entries.add(tempEntry); + log.debug("Entry {} has been added to {}",tempEntry,this); } } @@ -171,7 +173,7 @@ public int hashCode() { public String toString() { final StringBuilder sb = new StringBuilder("{"); - sb.append(label).append('\''); + sb.append('\'').append(label).append('\''); sb.append(", date=").append(date); sb.append(", currency=").append(currency); sb.append(", entries=").append(entries); diff --git a/src/main/java/io/github/ghacupha/keeper/book/unit/time/DateRange.java b/src/main/java/io/github/ghacupha/keeper/book/unit/time/DateRange.java index 513a075..313d5e0 100644 --- a/src/main/java/io/github/ghacupha/keeper/book/unit/time/DateRange.java +++ b/src/main/java/io/github/ghacupha/keeper/book/unit/time/DateRange.java @@ -90,7 +90,7 @@ public TimePoint getStart() { } public boolean includes(TimePoint arg) { - log.debug("Checking if : {} includes timePoint : {}", this, arg); + log.trace("Checking if : {} includes timePoint : {}", this, arg); return !arg.before(start) && !arg.after(end); } diff --git a/src/test/java/io/github/ghacupha/keeper/book/base/AccountTest.java b/src/test/java/io/github/ghacupha/keeper/book/base/AccountTest.java index 66bbe3b..25597e1 100644 --- a/src/test/java/io/github/ghacupha/keeper/book/base/AccountTest.java +++ b/src/test/java/io/github/ghacupha/keeper/book/base/AccountTest.java @@ -33,40 +33,40 @@ public class AccountTest { @Test public void directedTransactionWorks() throws Exception, UnableToPostException, MismatchedCurrencyException, ImmutableEntryException { - log.info("Testing if the transaction will work. First we create the pay for bills Transaction, using period 2017-11-2, and currency KES"); + log.info("\n Testing if the transaction will work. First we create the pay for bills Transaction, using period 2017-11-2, and currency KES"); Transaction payForBillBoards = new SimpleTransaction("BillboardsPayment",new SimpleDate(2017,11,2),Currency.getInstance("KES")); - log.info("Done. We DEBIT the Advertisement account, and credit the VAT and Banker's Cheque accounts...."); + log.info("\n Done. We DEBIT the Advertisement account, and credit the VAT and Banker's Cheque accounts...."); 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 - log.info("Ok so we have not yet posted the transaction but we want to check if the balances have been effected into the 3 accounts"); + log.info("\n Ok so we have not yet posted the transaction but we want to check if the balances have been effected into the 3 accounts"); 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 - log.info("Nothing? Good. So now lets post the transaction..."); + log.info("\n Nothing? Good. So now lets post the transaction..."); payForBillBoards.post(); - log.info("Posted. Now lets check if the balances in the account reflect our intentions"); + log.info("\n Posted. Now lets check if the balances in the account reflect our intentions"); 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 - log.info("Alright now we gotta reimburse Edwin for the meeting expenses, when he met with the Billboard guys. We create the" + + log.info("\n Alright now we gotta reimburse Edwin for the meeting expenses, when he met with the Billboard guys. We create the" + " reimbursement transaction, as of 2017-12-20 in currency KES"); Transaction reimbursement = new SimpleTransaction("Edwin\'s reimbursement",new SimpleDate(2017,12,20), Currency.getInstance("KES")); - log.info("Alright, all we gotta do is debit the advertisement account and credit Edwin's account..."); + log.info("\n Alright, all we gotta do is debit the advertisement account and credit Edwin's account..."); 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 - log.info("Again, we are going to check if the system has inappropriately added money into Edwin's account before our explicit posting..."); + log.info("\n Again, we are going to check if the system has inappropriately added money into Edwin's account before our explicit 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)); @@ -82,25 +82,25 @@ public void directedTransactionWorks() throws Exception, UnableToPostException, 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 - log.info("Ok, this new strategy guy, for some obviously unholy reason wants to know our position as at 2017-11-05, time for some replay..."); + log.info("\n Ok, this new strategy guy, for some obviously unholy reason wants to know our position as at 2017-11-05, time for some replay..."); 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 - log.info("The internal audit reveals that someone had screwed up our taxes. Our taxes should be on the asset side by at least 13 joys. " + + log.info("\n The internal audit reveals that someone had screwed up our taxes. Our taxes should be on the asset side by at least 13 joys. " + "Time to create some tax reversal transaction as at 2018-04-20, in KES as always"); Transaction taxReversal = new SimpleTransaction("Tax reversal",SimpleDate.on(2018,4,20), Currency.getInstance("KES")); - log.info("Adding entries to the tax reversal. We need to debit VAT by 45 joys and CREDIT advertisement expense by the same amount"); + log.info("\n Adding entries to the tax reversal. We need to debit VAT by 45 joys and CREDIT advertisement expense by the same amount"); 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")); - log.info("We now post the tax reversal transaction"); + log.info("\n We now post the tax reversal transaction"); taxReversal.post(); - log.info("As per internal audit the VAT should be at 13 joys, asset side. Meaning the advertisement should be an expense of just 305"); + log.info("\n As per internal audit the VAT should be at 13 joys, asset side. Meaning the advertisement should be an expense of just 305"); // 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));