package gw.rating.flow uses com.google.common.collect.ImmutableCollection uses com.google.common.collect.ImmutableListMultimap uses gw.api.database.DBFunction uses gw.api.database.preload.ArrayLoader uses gw.api.database.QuerySelectColumns uses gw.api.locale.DisplayKey uses gw.api.database.Query uses gw.api.productmodel.PolicyLinePatternLookup uses gw.api.system.PLDependenciesGateway uses gw.api.util.DisplayableException uses gw.api.util.LocaleUtil uses gw.pl.persistence.core.Key uses gw.plugin.Plugins uses gw.plugin.rateflow.IRateRoutinePlugin uses gw.rating.flow.domain.ParseUtil uses gw.rating.flow.util.CopyVersionType uses gw.rating.flow.util.QueryUtils uses gw.validation.PCValidationContext uses java.util.List uses java.util.stream.Collectors @ReadOnly enhancement CalcRoutineDefinitionEnhancement : entity.CalcRoutineDefinition { static function availableTables(line: String) : List { //if a rate table's policy line is null, it is available on all lines var availableTables = QueryUtils.getRateTableDefinitionsForLine(line) if (line != null) { availableTables = availableTables.concat(QueryUtils.getRateTableDefinitionsForLine(null)) as List } return availableTables } function availableTables(): List { return CalcRoutineDefinitionEnhancement.availableTables(this.PolicyLinePatternCode) } property get PolicyLineDisplayName() : String { return PolicyLinePatternLookup.getByPublicID(this.PolicyLinePatternCode).DisplayName } property get OrderedSteps() : List{ return this.Steps.OrderedByStepSortOrder } /** * @returns the list of steps in order, excluding any steps which return false from the cond() block. */ function FilteredOrderedSteps(cond(elt:CalcStepDefinition):boolean) : List{ return this.Steps.where(\ c -> cond(c) ).OrderedByStepSortOrder } property get StepIndentLevels(): Map { var orderedSteps = this.OrderedSteps var indentation = 0 var indentMap = new HashMap() for (step in orderedSteps) { if (step.StepType.hasCategory(CalcStepCategory.TC_FLOWCONTROL) and step.StepType != TC_IF and step.StepType != TC_LOOP) { if (indentation > 0) { // in case the rate routine starts with something funny like an else, don't go negative. indentation-- } } indentMap.put(step, indentation) if (step.StepType.hasCategory(CalcStepCategory.TC_FLOWCONTROL) and step.StepType != TC_ENDIF and step.StepType != TC_ENDLOOP) { indentation++ } } return indentMap } /** * A Copy sets the Version = 1 and makes copies of the Code and Name * A Version bumps the Version and retains the same Code and Name * A Branch sets the Version = 1, retains the same Code, while the Name and Jurisdiction are allowed to change. * For validation, the Jurisdiction MUST change or else the uniqueness constraint will be violated. */ function initializeCopy(copyVersionType : CopyVersionType) : CalcRoutineDefinition { var newRoutineDefinition = this.copy() as CalcRoutineDefinition if (copyVersionType == VERSION) { newRoutineDefinition.setFieldValue("Version", this.Version + 1) } else if (copyVersionType == COPY or copyVersionType == BRANCH) { newRoutineDefinition.setFieldValue("Version", 1) } if (copyVersionType == COPY) { var newCode = DisplayKey.get("Web.Rating.RateRoutines.CopyPrefix", this.Code) if (newCode.length() > 64) { // Just use the old code: user will get error and chance to fix up on commit() newCode = this.Code } newRoutineDefinition.Code = newCode var newName = DisplayKey.get("Web.Rating.RateRoutines.CopyPrefix", this.Name) if (newName.length() > 64) { newName = newName.substring(0, 64) } newRoutineDefinition.Name = newName } return newRoutineDefinition } /** * A virtual property for the Branching fields. Together with "Code" and "Version", the branching fields uniquely identify a CalcRoutineDefinition * @returns An ordered list of Fields. */ static property get BranchingFields() : List { return Plugins.get(IRateRoutinePlugin).BranchingFields.map(\ field -> field.Name).toList() } /** * An ordered list of values of the Branching fields for this CalcRoutineDefinition */ property get BranchingFieldValues() : List { return BranchingFields.map(\ s -> this.getFieldValue(s)) } /** * The columns in the unique index (not including 'Retired' because the query layer will add 'Retired' to queries automatically). * Used in RateRoutineValidation.gs */ static property get ColumnsInUniqueIndex() : List{ return {"Code", "Version"}.concat(BranchingFields).toList() } /** * The columns in the unique index excluding 'Version' (and not including 'Retired'). * Used in methods isLatestVersion() and hasOtherVersions() where we want to build queries * which compare on the unique columns other than the 'Version' column. */ static property get ColumnsInIndexMinusVersion() : List{ var columnsMinusVersion = ColumnsInUniqueIndex.copy() columnsMinusVersion.remove("Version") return columnsMinusVersion } /** * Returns true if this calc routine's version is the highest one in the database. */ function isLatestVersion() : boolean { var q = Query.make(CalcRoutineDefinition) for (column in ColumnsInIndexMinusVersion) { q.compare(column, Equals, this.getFieldValue(column)) } var subselectQuery = Query.make(CalcRoutineDefinition) // using a query joined to itself is the Query Layer's way to do a MAX for (column in ColumnsInIndexMinusVersion) { subselectQuery.compare(column, Equals, this.getFieldValue(column)) } q.subselect(CalcRoutineDefinition#Version, CompareIn, subselectQuery, DBFunction.Max(subselectQuery.getColumnRef(CalcRoutineDefinition#Version.PropertyInfo.Name))) var maxVersion = q.select().single().Version return (this.Version == maxVersion) } /** * A rate routine can be edited if it's the first version, there aren't other versions, and it's not yet included in any ratebook. */ function canEditIdentifyingFields() : boolean { return isFirstVersion() and not hasOtherVersions() and not isIncludedInAnyRateBook() } function isFirstVersion() : boolean { return this.Version == 1 } function hasOtherVersions() : boolean { var q = Query.make(CalcRoutineDefinition) for (column in ColumnsInIndexMinusVersion) { q.compare(column, Equals, this.getFieldValue(column)) } var result = q.select() //When user is copying a Calc Routine, result is still empty, //i.e. there are, as yet, no other versions of that routine if (result.Empty) { return false } if (result.Count > 1) { return true } //if we are here, we know that "this.Version == result.single().Version" //i.e. there are no other versions return false } /** * @returns true if any of the associated RateBooks are not in draft state. */ function isIncludedInPromotedRateBook() : boolean { return this.RateBooks.hasMatch(\ rateBook -> not rateBook.isDraft()) } /** * @returns true if there are no associated RateBooks. */ function isIncludedInAnyRateBook() : boolean { return not this.RateBooks.IsEmpty } // TODO: Is an array really the right thing here? Don't we actually want a set (no duplicates)? property get RateBooks() : RateBook[] { return this.RateBookCalcRoutines*.RateBook } /** * Clears unused instructions from each step. */ function clearUnusedInstructions() { for (step in this.Steps) { step.clearUnusedInstruction() } } /** * Before deleting, verify that the Rate Routine is not included in a non-draft Rate Book. */ function canDelete() { if (IncludedInPromotedRateBookDatabase) { throw new DisplayableException(DisplayKey.get("Validation.Rating.RateRoutineDefinition.ReferencedByPromotedRateBook")) } } /** * @returns true if it's not included in a promoted RateBook, and it can be saved. */ function canUpdate() { if (IncludedInPromotedRateBookDatabase) { throw new DisplayableException(DisplayKey.get("Validation.Rating.RateRoutineDefinition.ReferencedByPromotedRateBook")) } if (not canSave()) { throw new DisplayableException(DisplayKey.get("Validation.Rating.RateRoutineDefinition.DuplicateRateRoutineCode", CalcRoutineDefinition.ColumnsInUniqueIndex.join(", "))) } } /** * @returns true if the Rate Routine's code isn't already in the database. */ function canSave() : boolean { var q = Query.make(CalcRoutineDefinition) for (column in CalcRoutineDefinition.ColumnsInUniqueIndex) { q.compare(column, Equals, this.getFieldValue(column)) } return not q.select().hasMatch(\ calcRoutine -> calcRoutine.ID != this.ID) } /** * Use a Query to make sure we hit the database so that another browser's update will be known. */ private property get IncludedInPromotedRateBookDatabase(): boolean { var rateBookTable = Query.make(RateBook) rateBookTable.compare(RateBook#Status, NotEquals, TC_DRAFT) var joinTable = Query.make(RateBookCalcRoutine) joinTable.compare(RateBookCalcRoutine#CalcRoutineDefinition, Equals, this) joinTable.subselect(RateBookCalcRoutine#RateBook, CompareIn, rateBookTable, RateBook#ID) return not joinTable.select().Empty } /** * Performs pre-display (form initialization) validation on this Rate Routine * Definition for its {@link typekey.CalcStepOperandType#TC_RATETABLE * TC_RATETABLE} steps including both checking if the rate table exists and * whether the column is valid. This also loads _all_ operands as a side- * effect. *

* Operates on the persisted instance since it must be an already existing * instance for display through a form. */ function performPreDisplayValidation() : String[] { var stepToOperands = mapStepsToOrderedOperands() var context = new PCValidationContext(TC_DEFAULT) /* load all operand subarrays... */ var allOperands = stepToOperands.values() loadOperandSubarrays(allOperands) // load ArgumentSourceSets, ArgumentSources of any referenced RateTableDefinitions... var rateTableDefinitions = allOperands.stream() .filter(\operand -> operand.HasRateTableArgumentSourceSetReference) .map(\operand -> operand.RateTableDefinition) .filter(\tableDef -> tableDef != null) // can be null even if reference is specified... .collect(Collectors.toSet()) RateTableDefinition.loadArgumentSourceSetsAndSources(rateTableDefinitions) for (step in this.Steps) { final var stepOrderedOperands = stepToOperands.get(step) if (stepOrderedOperands.stream().anyMatch(\op -> op.OperandType == TC_RATETABLE)) { // validates only operands of type TC_RATETABLE... ParseUtil.performRateTableExistsValidation(stepOrderedOperands, context) ParseUtil.performRateTableColumnValidation(stepOrderedOperands, context) } } cacheStepOperands(stepToOperands) // for performance _after_ processing steps... var result = context.Result if (result.Empty) { return {} } else { return result.ErrorMessages } } private function mapStepsToOrderedOperands(): ImmutableListMultimap { // load _all_ operands from store; don't filter w/ // .compare(CalcStepDefinitionOperand#OperandType, Equals, TC_RATETABLE) var stepOperandQuery = makeStepsOperandQuery() var orderedStepsOperandResults = stepOperandQuery.select() .thenBy(QuerySelectColumns.path("CalcStepDefinitionOperand.CalcStep.ID")) .thenBy(CalcStepDefinitionOperand.OPERANDORDER_PROP.get()) var stepToOrderedOperandsBuilder = ImmutableListMultimap.builder(); for (operand in orderedStepsOperandResults) { stepToOrderedOperandsBuilder.put(operand.CalcStep, operand) } return stepToOrderedOperandsBuilder.build() } private function makeStepsOperandQuery(): Query { var stepOperandQuery = Query.make(CalcStepDefinitionOperand) stepOperandQuery.join(CalcStepDefinitionOperand#CalcStep) .compare(CalcStepDefinition#CalcRoutineDefinition, Equals, this) return stepOperandQuery } function loadOperandSubarrays(operands: ImmutableCollection) { new ArrayLoader(CalcStepDefinitionOperand.RETURNFACTORCOLUMNS_PROP.get(), operands).iterateOverAllElements() new ArrayLoader(CalcStepDefinitionOperand.ARGUMENTSOURCES_PROP.get(), operands).iterateOverAllElements() } private function cacheStepOperands( stepsToOperands: ImmutableListMultimap) { // cache all step -> Operands arrays... // noinspection HiddenPackageReference PLDependenciesGateway.GlobalCache.EntitySource.loadArrayIds( stepsToOperands.keys().stream().map(\step -> step.ID).collect(Collectors.toSet()), CalcStepDefinition.OPERANDS_PROP.get() ) } function loadSteps() { new ArrayLoader(this.STEPS_PROP.get(), Collections.singletonList(this)).iterateOverAllElements() } property get LocalizedName(): String { return this["Name_" + LocaleUtil.getDefaultLanguageType().Code] as String } }