package gw.financials uses java.util.Date uses java.lang.IllegalStateException uses java.util.HashMap uses java.util.Collection uses gw.api.domain.financials.PCFinancialsLogger uses gw.pl.currency.MonetaryAmount uses java.util.List class TransactionAdjustmentCalculator { var _defaultProrater : Prorater var _segmenter = new DateRangeSegmenter>() construct (prorater : Prorater) { _defaultProrater = prorater } function addTransactionToOffset(toOffset : WrappedProratable) : TransactionAdjustmentCalculator { var start = toOffset.Start var end = toOffset.End _segmenter.add(start, end, toOffset.subPeriod(start, end, -toOffset.Amount, false)) return this } function addTransactionToOnset(onsetTransaction : WrappedProratable) : TransactionAdjustmentCalculator { _segmenter.add(onsetTransaction.Start, onsetTransaction.End, onsetTransaction) return this } function adjustingTransactions() : List> { var results : List> = {} var existingTransactionsManager = new Collector(results).reverseSign() var proposedTransactionsManager = new Collector(results) abstractAdjustingCalculations( \ start, end, existing -> existingTransactionsManager.add( start, end, existing), \ start, end, proposed -> proposedTransactionsManager.add( start, end, proposed), \ errorMsg, bad, start, end -> { throw new IllegalStateException(errorMsg) } ) return results } function abstractAdjustingCalculations( forExisting : block(start : Date, end : Date, existing : WrappedProratable), forProposed : block(start : Date, end : Date, proposed : WrappedProratable), error : block(message : String, transactions : Collection>, start : Date, end : Date) ) { validate(error) _segmenter.segment( \ start, end, transactions -> { var existing = transactions.firstWhere( \ t -> not t.Original) //we reversed var proposed = transactions.firstWhere( \ t -> t.Original) if (perfectlyEqual(start, end, existing, proposed)) { return //no adjustments needed if everything lines up perfectly. } if (existing != null) { forExisting(start, end, existing) } if (proposed != null) { forProposed(start, end, proposed) } }) } function validate(error : block(message : String, transactions : Collection>, start : Date, end : Date)) { _segmenter.segment( \ start, end, transactions -> { if (transactions.toList().partition( \ t -> t.Original).values().hasMatch( \ tList -> tList.Count > 1)) { error("At most one offset and one onset allowed. Programming error.", transactions, start, end ) } }) } private function perfectlyEqual(start : Date, end : Date, onset : WrappedProratable, offset : WrappedProratable) : boolean { if (onset == null or offset == null) { return false } return offset.haveEquivalentCosts(onset) and (prorate(start, end, onset) + prorate(start, end, offset)).IsZero } /** * If we can, calculate the amount using the cost information from the Rating engine (TermAmount and DaysInTerm). * In the case when we have a policy period for 7 months and the term is 1 year. Then we cancel * the period to shorten it to 4 months. Then when we comparing the transaction generated for * the 7 months period with the cost for the 4 months period (in the first 4-month segment), * we may want to prorate the 7 month transaction using the ActualTermAmount instead of prorating * from the Amount of the transaction, which is already prorated from 12 to 7 months. This will * help improve that chance that it will match up with the cancellation cost for the 4 months * period, which is prorated from 12 months. * * Also allow the TransactionReducer to use the same calculation code, because if the results are inconsistent we * can get extra offsets and onsets. */ static function prorateTransaction(prorater : Prorater, start : Date, end : Date, transaction : WrappedProratable) : MonetaryAmount { var scaledTermAmount : MonetaryAmount var proratedAmount : MonetaryAmount // We are able to do this only if we have all data available to us if ( transaction.DaysInTerm != null and transaction.DaysInTerm != 0 and transaction.TermAmount != null ){ // OK, see if the transaction amount matches what would have been generated by prorating against the term amount // AND the scale on the TermAmount matches the scale used by the prorater. (This doesn't always happen) scaledTermAmount = prorater.scaleAmount(transaction.TermAmount) var signedTermAmount = transaction.TermAmount.abs() * transaction.Amount.signum() proratedAmount = prorater.prorate(transaction.PeriodStart, transaction.TermEnd, transaction.Start, transaction.End, signedTermAmount) if ( scaledTermAmount == transaction.TermAmount and proratedAmount == transaction.Amount ) { // Yes! The amount looks as though it has been prorated from the term amount. Do this calculation the same way. return prorater.prorate(transaction.PeriodStart, transaction.TermEnd, start, end, signedTermAmount) } } // The amount was calculated from something other than the term amount, // OR we couldn't make the determination. Either way, we prorate directly // from the Transaction. if (PCFinancialsLogger.isDebugEnabled() and (transaction.Start != start or transaction.End != end)) { PCFinancialsLogger.logDebug("TransactionAdjustmentCalculator could not prorate from start of " +transaction.Reference) var reason : String = null switch (true) { // short-circuit eval of all of the conditions above case transaction.DaysInTerm == null: reason = "DaysInTerm was null" break case transaction.DaysInTerm == 0: reason = "DaysInTerm was zero" break case transaction.TermAmount == null: reason = "TermAmount was null" break case scaledTermAmount != transaction.TermAmount: reason = "scale does not match prorater" break case proratedAmount != transaction.Amount: reason = "transaction amount does not match proration" break } PCFinancialsLogger.logDebug("because "+ reason + ". Will prorate existing amount " +transaction.Amount) PCFinancialsLogger.logDebug("with date range " + transaction.Start + ".." + transaction.End + " to new range " + start + ".." +end) } return prorater.prorate(transaction.Start, transaction.End, start, end, transaction.Amount) } private function prorate(start : Date, end : Date, transaction : WrappedProratable) : MonetaryAmount { var _prorater = transaction.getProrater(_defaultProrater) return prorateTransaction(_prorater, start, end, transaction) } private class Collector { var _reverseSign : boolean = false var _asEmittedByBaseTransaction = new HashMap, WrappedProratable>() var _collected : List> construct(collected : List>) { _collected = collected } function reverseSign() : Collector { _reverseSign = true return this } function add(start : Date, end : Date, baseTransaction : WrappedProratable) { var previous = _asEmittedByBaseTransaction.get(baseTransaction) var shouldLengthenPrevious = previous != null && previous.End == start var newStart = shouldLengthenPrevious ? previous.Start : start var transaction = baseTransaction.subPeriod(newStart, end, prorate(newStart, end, baseTransaction)) _asEmittedByBaseTransaction.put(baseTransaction, transaction) if (shouldLengthenPrevious) { //lengthen previous _collected[_collected.indexOf(previous)] = transaction } else { _collected.add(transaction) } } } }