package gw.financials uses gw.api.domain.FKLoader uses gw.api.domain.financials.CostKey uses gw.financials.jobtypes.JobType uses gw.pl.currency.MonetaryAmount uses java.lang.IllegalStateException uses java.math.RoundingMode uses java.util.Collection uses java.util.Date uses java.util.Map uses java.util.HashMap uses java.util.Set uses java.util.List uses gw.api.extension.ExtensionInterfaces class PolicyPeriodTransactionCalculator { // default prorater to use if cost/transaction doesn't specify rounding level var _defaultProrater : Prorater var _period : PolicyPeriod var _onsetsPartitioned : Map> var _offsetsPartitioned : Map> var _adapterPerCostKey : Map = new HashMap() {}.toAutoMap(\ costKey -> new CostKeyTransactionCalculatorAdapter(costKey, _onsetsPartitioned[costKey], _offsetsPartitioned[costKey])) var _postedTransactions : List var _allCostKeys : Set construct (period : PolicyPeriod) { _defaultProrater = Prorater.forRounding(period.Policy.Product.QuoteRoundingLevel, period.Policy.Product.QuoteRoundingMode, TC_PRORATABYDAYS) _period = period _onsetsPartitioned = _period.AllCosts.toList().partition(\ c -> c.CostKey).mapValues(\ s -> s.sortBy(\ c -> c.EffDate)).toAutoMap(\ c -> {return {}}) _postedTransactions = ExtensionInterfaces.IPostedTransactionFinderExtensions.findPostedTransactions(_period) FKLoader.preLoadFKs(_postedTransactions, Cost) _offsetsPartitioned = _postedTransactions.partition(\t -> t.Cost.CostKey).toAutoMap(\ c -> {return {}}) _allCostKeys = _onsetsPartitioned.Keys.union(_offsetsPartitioned.Keys) } function calculateTransactions() { _allCostKeys.each(\costKey -> calculateTransactions(costKey)) } /** * Ensure that no two Costs with the same CostKey have overlapping dates. * @throw IllegalStateException if overlaps are found. */ static function ensureSingleCostPerDateSegment(costsByKey : Map>) { for (costKey in costsByKey.Keys) { var costsWithSameKey = costsByKey[costKey] var overlapChecker = new DateRangeSegmenter() costsWithSameKey.each( \ c -> overlapChecker.add(c.EffDate, c.ExpDate, c)) overlapChecker.segment( \ segmentStart, segmentEnd, costs -> { if (costs.Count > 1) { var msg = "Multiple onsetting costs with same cost key (" + costKey + ") may not overlap in time" msg += "\n The following costs overlap between " + segmentStart + " and " + segmentEnd msg += "\n" + costs.map(\ c -> " Cost ${c} ${c.debugString()}").join("\n") throw new IllegalStateException(msg) } }) } } function validateOnsetTransactions() : PolicyPeriodTransactionCalculator { var costsByKey : Map> = {} for (costKey in _allCostKeys) { costsByKey.put(costKey, getAdapterForCostKey(costKey).OnsetCosts) } ensureSingleCostPerDateSegment(costsByKey) return this } function getAdapterForCostKey(costKey : CostKey) : CostKeyTransactionCalculatorAdapter { return _adapterPerCostKey[costKey] } internal static class CostKeyTransactionCalculatorAdapter { var _costKey : CostKey var _allCosts : Set var _subjectToApproval : boolean as SubjectToAccrual var _subjectToReporting : boolean as SubjectToReporting var _onsets : List as OnsetCosts var _offsets : List as OffsetTransactions construct (costKey : CostKey, onsets : List, offsets : List) { _costKey = costKey _onsets = onsets _offsets = offsets _allCosts = offsets.map(\ t -> t.Cost).toSet().union(onsets) _subjectToApproval = _allCosts.mapSoleValue(\c -> ExtensionInterfaces.ICostExtensions.isSubjectToAccrual(c), \q, q2 -> {}) _subjectToReporting = _allCosts.mapSoleValue(\c -> c.SubjectToReporting, \q, q2 -> {}) } } // For a given written value, actually go ahead and do the transaction calculation private function calculateTransactions(costKey : CostKey) { var matchingTransactions = getAdapterForCostKey(costKey).OffsetTransactions.partition(\ t -> t.Written).toAutoMap(\ k -> {return {}}) validateAndCalculateTransactions(costKey, matchingTransactions, true) // once for written transactions validateAndCalculateTransactions(costKey, matchingTransactions, false) // once for non-written transactions } private function validateAndCalculateTransactions(costKey : CostKey, matchingTransactions : Map>, written : boolean) { validateAndCalculateTransactions(costKey, matchingTransactions[written], written) } private function validateAndCalculateTransactions(costKey : CostKey, matchingTransactions : List, whenWritten: boolean) { var info = getAdapterForCostKey(costKey) var jobType = ExtensionInterfaces.IPolicyPeriodExtensions.determineJobType(_period, info.SubjectToReporting, whenWritten) validateTransactionsByWCCombo(matchingTransactions, jobType, info.SubjectToAccrual) var isCharged = jobType.Charged var isWritten = jobType.Written if (isWritten or isCharged) { var transactions = calculateAdjustingTransactions(info.OnsetCosts, matchingTransactions) createEntityTransactions(transactions, isWritten, isCharged, info.SubjectToAccrual and isCharged) } } /** * Reduce the to-offset and onsetting costs/transactions to at most one onset and at most one offset * in any time period, then eliminate those that cancel out. */ private function calculateAdjustingTransactions(costsToOnset : List, transactionsToOffset : List) : List> { var calculator = new TransactionAdjustmentCalculator(_defaultProrater) reduceExistingTransactions(transactionsToOffset).each( \ t -> calculator.addTransactionToOffset(t)) costsToOnset.each( \ c -> calculator.addTransactionToOnset(new WrappedCost(c, true, ExtensionInterfaces.IPolicyPeriodExtensions.getStartOfRatedTerm(_period)))) return calculator.adjustingTransactions() } private function isOffsetWithMatchingNewOnset(t : WrappedProratable, transactions : List>) : boolean { return (not t.Original) and transactions.hasMatch(\ txn -> txn.Amount.IsNotZero and txn.Start == t.Start and !txn.End.before(t.End) and txn.Original) } private function createEntityTransactions(transactions : List>, written : boolean, charged : boolean, toBeAccrued : boolean) { for (t in transactions) { if (t.Amount.IsNotZero or isOffsetWithMatchingNewOnset(t, transactions)) { var entityTransaction = t.Reference.createTransaction(_period) entityTransaction.EffDate = t.Start entityTransaction.ExpDate = t.End entityTransaction.Amount = t.Amount entityTransaction.Written = written entityTransaction.Charged = charged entityTransaction.ToBeAccrued = toBeAccrued var transactionCost = entityTransaction.Cost entityTransaction.AmountBilling = ExtensionInterfaces.IMonetaryAmountExtensions.convertAndScale(t.Amount, transactionCost.PolicyFXRate) entityTransaction.PolicyFXRate = transactionCost.PolicyFXRate } } } protected function validateTransactionsByWCCombo(transactions: List, jobType: JobType, subjectToAccrual : boolean) { var expectedCharged = jobType.Charged var expectedAccrued = subjectToAccrual and expectedCharged var writtenNegationWording = jobType.Written ? "Written" : "Unwritten" if (transactions.hasMatch( \ t -> t.Charged != expectedCharged)) { throw writtenNegationWording + " transactions " + "should always have a charged value of " + expectedCharged + " for " + jobType + " jobs." } var subjectToAccrualWording = subjectToAccrual ? "subject" : "not subject" if (transactions.hasMatch( \ t -> t.ToBeAccrued != expectedAccrued)) { throw writtenNegationWording + " transactions " + "should always have a charged value of " + expectedCharged + " for " + jobType + " jobs " + "which are " + subjectToAccrualWording + " to accrual." } } private function reduceExistingTransactions(transactionsToOffset : List) : List> { var fallbackReducer = new LegacyTransactionReducer(_defaultProrater, \transactions->transactions.maxBy(\t -> t.Reference.Cost.CreateTime)) var reducer = new StandardTransactionReducer(_defaultProrater, fallbackReducer) transactionsToOffset.each( \t -> reducer.add(new WrappedTransaction(t, isOriginalTransaction(t), ExtensionInterfaces.IPolicyPeriodExtensions.getStartOfRatedTerm(_period)))) return reducer.reduce().map(\t -> new WrappedCost(t.Start, t.End, t.Amount, t.Original, t.Reference.Cost, ExtensionInterfaces.IPolicyPeriodExtensions.getStartOfRatedTerm(_period))) } private static function isOriginalTransaction(t : Transaction) : boolean { var costBranch = t.Cost.BranchUntyped var transactionBranch = (t as EffDated).BranchUntyped return costBranch == transactionBranch } protected static class WrappedCost extends WrappedProratable { construct (cost : entity.Cost, __orig : boolean, __periodStart : Date) { this(cost.EffDate, cost.ExpDate, cost.ActualAmount, __orig, cost, __periodStart) } construct (__start : Date, __end : Date, __amount : MonetaryAmount, __orig : boolean, cost : entity.Cost, __periodStart : Date) { super(__start, __end, __amount, __orig, cost, __periodStart) } override function subPeriod( __start: Date, __end: Date, __amount: MonetaryAmount, __orig : boolean) : WrappedProratable { return new WrappedCost(__start, __end, __amount, __orig, this.Reference, this.PeriodStart) } override property get RoundingLevel(): Integer { return this.Reference.RoundingLevel } override property get RoundingMode(): RoundingModeType { return this.Reference.RoundingMode } override property get ProrationMethod(): ProrationMethod { return this.Reference.ProrationMethod } override property get DaysInTerm(): Integer { return this.Reference.NumDaysInRatedTerm } override property get TermAmount(): MonetaryAmount { return this.Reference.ActualTermAmount } override function haveEquivalentCosts(thatWrappedProratable : WrappedProratable) : boolean { var that = thatWrappedProratable as WrappedCost return this.Reference.isCostEqual(that.Reference) } } protected static class WrappedTransaction extends WrappedProratable { var cost: Cost construct (t : entity.Transaction, __orig : boolean, __periodStart : Date) { this(t.EffDate, t.ExpDate, t.Amount, __orig, t, __periodStart) } construct (__start : Date, __end : Date, __amount : MonetaryAmount, __orig : boolean, transaction : entity.Transaction, __periodStart : Date) { super(__start, __end, __amount, __orig, transaction, __periodStart) cost = this.Reference.Cost } override function subPeriod( __start: Date, __end: Date, __amount: MonetaryAmount, __orig : boolean) : WrappedProratable { return new WrappedTransaction(__start, __end, __amount, __orig, this.Reference, this.PeriodStart) } override function sameOriginalTransactionPartitioner() : Object { return cost } property get RoundingLevel(): Integer { return cost.RoundingLevel } property get RoundingMode(): RoundingModeType { return cost.RoundingMode } property get ProrationMethod(): ProrationMethod { return cost.ProrationMethod } override property get DaysInTerm(): Integer { return cost.NumDaysInRatedTerm } override property get TermAmount(): MonetaryAmount { return cost.ActualTermAmount } } }