001// -------------------------------------------------------------------------------- 002// Copyright 2002-2026 Echo Three, LLC 003// 004// Licensed under the Apache License, Version 2.0 (the "License"); 005// you may not use this file except in compliance with the License. 006// You may obtain a copy of the License at 007// 008// http://www.apache.org/licenses/LICENSE-2.0 009// 010// Unless required by applicable law or agreed to in writing, software 011// distributed under the License is distributed on an "AS IS" BASIS, 012// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013// See the License for the specific language governing permissions and 014// limitations under the License. 015// -------------------------------------------------------------------------------- 016 017package com.echothree.model.control.accounting.server.logic; 018 019import com.echothree.control.user.accounting.common.spec.TransactionUniversalSpec; 020import com.echothree.model.control.accounting.common.TransactionTimeTypes; 021import com.echothree.model.control.accounting.common.exception.TransactionNotBalancedException; 022import com.echothree.model.control.accounting.common.exception.UnknownTransactionNameException; 023import com.echothree.model.control.accounting.common.workflow.TransactionStatusConstants; 024import com.echothree.model.control.accounting.server.control.AccountingControl; 025import com.echothree.model.control.accounting.server.database.TransactionBalancedQuery; 026import com.echothree.model.control.core.common.ComponentVendors; 027import com.echothree.model.control.core.common.EntityTypes; 028import com.echothree.model.control.core.common.exception.InvalidParameterCountException; 029import com.echothree.model.control.core.server.control.EntityInstanceControl; 030import com.echothree.model.control.core.server.logic.EntityInstanceLogic; 031import com.echothree.model.control.party.server.control.PartyControl; 032import com.echothree.model.control.workflow.server.control.WorkflowControl; 033import com.echothree.model.control.workflow.server.logic.WorkflowEntranceLogic; 034import com.echothree.model.control.workflow.server.logic.WorkflowLogic; 035import com.echothree.model.data.accounting.server.entity.Currency; 036import com.echothree.model.data.accounting.server.entity.GlAccount; 037import com.echothree.model.data.accounting.server.entity.Transaction; 038import com.echothree.model.data.accounting.server.entity.TransactionEntityRole; 039import com.echothree.model.data.accounting.server.entity.TransactionEntityRoleType; 040import com.echothree.model.data.accounting.server.entity.TransactionGlAccountCategory; 041import com.echothree.model.data.accounting.server.entity.TransactionGlEntry; 042import com.echothree.model.data.accounting.server.entity.TransactionType; 043import com.echothree.model.data.party.server.entity.Party; 044import com.echothree.util.common.message.ExecutionErrors; 045import com.echothree.util.common.persistence.BasePK; 046import com.echothree.util.server.control.BaseLogic; 047import com.echothree.util.server.message.ExecutionErrorAccumulator; 048import com.echothree.util.server.persistence.EntityPermission; 049import com.echothree.util.server.persistence.Session; 050import javax.enterprise.context.ApplicationScoped; 051import javax.enterprise.inject.spi.CDI; 052import javax.inject.Inject; 053 054@ApplicationScoped 055public class TransactionLogic 056 extends BaseLogic { 057 058 @Inject 059 AccountingControl accountingControl; 060 061 @Inject 062 EntityInstanceControl entityInstanceControl; 063 064 @Inject 065 PartyControl partyControl; 066 067 @Inject 068 WorkflowControl workflowControl; 069 070 @Inject 071 EntityInstanceLogic entityInstanceLogic; 072 073 @Inject 074 PostingLogic postingLogic; 075 076 @Inject 077 TransactionTimeLogic transactionTimeLogic; 078 079 @Inject 080 WorkflowEntranceLogic workflowEntranceLogic; 081 082 @Inject 083 WorkflowLogic workflowLogic; 084 085 protected TransactionLogic() { 086 super(); 087 } 088 089 public static TransactionLogic getInstance() { 090 return CDI.current().select(TransactionLogic.class).get(); 091 } 092 093 public Transaction createTransactionUsingNames(final Session session, final Party groupParty, final String transactionTypeName, 094 final Long transactionTime, final BasePK createdBy) { 095 return createTransaction(session, groupParty, accountingControl.getTransactionTypeByName(transactionTypeName), 096 transactionTime, createdBy); 097 } 098 099 public Transaction createTransaction(final Session session, final Party groupParty, final TransactionType transactionType, 100 final Long transactionTime, final BasePK createdBy) { 101 var transaction = accountingControl.createTransaction(groupParty, transactionType, createdBy); 102 103 transactionTimeLogic.createTransactionTime(null, transaction, TransactionTimeTypes.TRANSACTION_TIME.name(), 104 transactionTime == null ? session.getStartTime() : transactionTime, createdBy); 105 106 return transaction; 107 } 108 109 public Transaction getTransactionByName(final ExecutionErrorAccumulator eea, final String transactionName, 110 final EntityPermission entityPermission) { 111 var transaction = accountingControl.getTransactionByName(transactionName, entityPermission); 112 113 if(transaction == null) { 114 handleExecutionError(UnknownTransactionNameException.class, eea, ExecutionErrors.UnknownTransactionName.name(), transactionName); 115 } 116 117 return transaction; 118 } 119 120 public Transaction getTransactionByName(final ExecutionErrorAccumulator eea, final String transactionName) { 121 return getTransactionByName(eea, transactionName, EntityPermission.READ_ONLY); 122 } 123 124 public Transaction getTransactionByNameForUpdate(final ExecutionErrorAccumulator eea, final String transactionName) { 125 return getTransactionByName(eea, transactionName, EntityPermission.READ_WRITE); 126 } 127 128 public Transaction getTransactionByUniversalSpec(final ExecutionErrorAccumulator eea, 129 final TransactionUniversalSpec universalSpec, final EntityPermission entityPermission) { 130 Transaction transaction = null; 131 var transactionName = universalSpec.getTransactionName(); 132 var parameterCount = (transactionName == null ? 0 : 1) + entityInstanceLogic.countPossibleEntitySpecs(universalSpec); 133 134 switch(parameterCount) { 135 case 1 -> { 136 if(transactionName == null) { 137 var entityInstance = entityInstanceLogic.getEntityInstance(eea, universalSpec, 138 ComponentVendors.ECHO_THREE.name(), EntityTypes.Transaction.name()); 139 140 if(eea == null || !eea.hasExecutionErrors()) { 141 transaction = accountingControl.getTransactionByEntityInstance(entityInstance, entityPermission); 142 } 143 } else { 144 transaction = getTransactionByName(eea, transactionName, entityPermission); 145 } 146 } 147 default -> 148 handleExecutionError(InvalidParameterCountException.class, eea, ExecutionErrors.InvalidParameterCount.name()); 149 } 150 151 return transaction; 152 } 153 154 public Transaction getTransactionByUniversalSpec(final ExecutionErrorAccumulator eea, 155 final TransactionUniversalSpec universalSpec) { 156 return getTransactionByUniversalSpec(eea, universalSpec, EntityPermission.READ_ONLY); 157 } 158 159 public Transaction getTransactionByUniversalSpecForUpdate(final ExecutionErrorAccumulator eea, 160 final TransactionUniversalSpec universalSpec) { 161 return getTransactionByUniversalSpec(eea, universalSpec, EntityPermission.READ_WRITE); 162 } 163 164 public TransactionGlEntry createTransactionGlEntryUsingNames(final Transaction transaction, final Party groupParty, 165 final String transactionGlAccountCategoryName, final GlAccount glAccount, final Currency originalCurrency, 166 final Long originalDebit, final Long originalCredit, final BasePK createdBy) { 167 var transactionDetail = transaction.getLastDetail(); 168 169 return createTransactionGlEntry(transaction, groupParty == null ? transactionDetail.getGroupParty() : groupParty, 170 accountingControl.getTransactionGlAccountCategoryByName(transactionDetail.getTransactionType(), transactionGlAccountCategoryName), 171 glAccount, originalCurrency, originalDebit, originalCredit, createdBy); 172 } 173 174 private GlAccount getGlAccount(final AccountingControl accountingControl, final TransactionGlAccountCategory transactionGlAccountCategory, 175 GlAccount glAccount) { 176 if(glAccount == null) { 177 var transactionGlAccount = accountingControl.getTransactionGlAccount(transactionGlAccountCategory); 178 179 if(transactionGlAccount == null) { 180 throw new IllegalArgumentException("glAccount is a required parameter"); 181 } else { 182 glAccount = transactionGlAccount.getGlAccount(); 183 } 184 } 185 186 return glAccount; 187 } 188 189 private Integer getTransactionGlEntrySequence(final AccountingControl accountingControl, final Transaction transaction) { 190 var transactionStatus = accountingControl.getTransactionStatusForUpdate(transaction); 191 Integer transactionGlEntrySequence = transactionStatus.getTransactionGlEntrySequence() + 1; 192 193 transactionStatus.setTransactionGlEntrySequence(transactionGlEntrySequence); 194 195 return transactionGlEntrySequence; 196 } 197 198 private Long getAmount(final GlAccount glAccount, final Currency originalCurrency, final Long originalAmount) { 199 var currency = glAccount.getLastDetail().getCurrency(); 200 201 Long amount; 202 if(originalCurrency.equals(currency)) { 203 amount = originalAmount; 204 } else { 205 throw new IllegalArgumentException("Currency conversion is not available"); 206 } 207 208 return amount; 209 } 210 211 public TransactionGlEntry createTransactionGlEntry(final Transaction transaction, final Party groupParty, 212 final TransactionGlAccountCategory transactionGlAccountCategory, GlAccount glAccount, final Currency originalCurrency, 213 final Long originalDebit, final Long originalCredit, final BasePK createdBy) { 214 glAccount = getGlAccount(accountingControl, transactionGlAccountCategory, glAccount); 215 216 var debit = originalDebit == null ? null : getAmount(glAccount, originalCurrency, originalDebit); 217 var credit = originalCredit == null ? null : getAmount(glAccount, originalCurrency, originalCredit); 218 219 return accountingControl.createTransactionGlEntry(transaction, getTransactionGlEntrySequence(accountingControl, transaction), 220 groupParty, transactionGlAccountCategory, glAccount, originalCurrency, originalDebit, 221 originalCredit, debit, credit, createdBy); 222 } 223 224 public TransactionEntityRole createTransactionEntityRoleUsingNames(final Transaction transaction, 225 final String transactionEntityRoleTypeName, final BasePK pk, final BasePK createdBy) { 226 var transactionEntityRoleType = accountingControl.getTransactionEntityRoleTypeByName(transaction.getLastDetail().getTransactionType(), 227 transactionEntityRoleTypeName); 228 229 return createTransactionEntityRole(transaction, transactionEntityRoleType, pk, createdBy); 230 } 231 232 public TransactionEntityRole createTransactionEntityRole(final Transaction transaction, 233 final TransactionEntityRoleType transactionEntityRoleType, final BasePK pk, final BasePK createdBy) { 234 var entityInstance = entityInstanceControl.getEntityInstanceByBasePK(pk); 235 236 if(!transactionEntityRoleType.getLastDetail().getEntityType().equals(entityInstance.getEntityType())) { 237 throw new IllegalArgumentException("entityInstance is not of the required EntityType"); 238 } 239 240 return accountingControl.createTransactionEntityRole(transaction, transactionEntityRoleType, entityInstance, createdBy); 241 } 242 243 private void validateTransactionBalanced(final ExecutionErrorAccumulator eea, final Transaction transaction) { 244 var transactionBalancedResults = new TransactionBalancedQuery().execute(transaction); 245 246 if(!transactionBalancedResults.isEmpty()) { 247 var transactionBalancedResult = transactionBalancedResults.getFirst(); 248 var originalDifference = transactionBalancedResult.getOriginalDifference(); 249 var difference = transactionBalancedResult.getDifference(); 250 251 if(originalDifference != 0 || difference != 0) { 252 handleExecutionError(TransactionNotBalancedException.class, eea, ExecutionErrors.TransactionNotBalanced.name(), 253 originalDifference, difference); 254 } 255 } 256 } 257 258 public void postTransaction(final ExecutionErrorAccumulator eea, final Session session, final Transaction transaction, 259 final BasePK createdBy) { 260 validateTransactionBalanced(eea, transaction); 261 262 if(eea != null && !hasExecutionErrors(eea)) { 263 accountingControl.removeTransactionStatusByTransaction(transaction); 264 265 postingLogic.postTransaction(session, transaction, createdBy); 266 267 // If it isn't in the Transaction Status workflow, assume this is a system generated transaction and 268 // we've gone directly to posting it. 269 var workflow = workflowLogic.getWorkflowByName(null, TransactionStatusConstants.Workflow_TRANSACTION_STATUS); 270 var entityInstance = entityInstanceControl.getEntityInstanceByBasePK(transaction.getPrimaryKey()); 271 if(!workflowControl.isEntityInWorkflow(workflow, entityInstance)) { 272 var workflowEntrance = workflowEntranceLogic.getWorkflowEntranceByName(null, workflow, 273 TransactionStatusConstants.WorkflowEntrance_TRANSACTION_STATUS_NEW_POSTED); 274 275 workflowControl.addEntityToWorkflow(workflowEntrance, entityInstance, null, null, createdBy); 276 } 277 } 278 } 279 280 public void testTransaction(final ExecutionErrorAccumulator eea, final Session session, final BasePK testedBy) { 281 var companyParty = partyControl.getDefaultPartyCompany().getParty(); 282 var divisionParty = partyControl.getDefaultPartyDivision(companyParty).getParty(); 283 var departmentParty = partyControl.getDefaultPartyDepartment(divisionParty).getParty(); 284 var originalCurrency = accountingControl.getDefaultCurrency(); 285 286 var transaction = createTransactionUsingNames(session, departmentParty, "TEST", null, testedBy); 287 createTransactionGlEntryUsingNames(transaction, null, "TEST_ACCOUNT_A", null, originalCurrency, null, 1999L, testedBy); 288 createTransactionGlEntryUsingNames(transaction, null, "TEST_ACCOUNT_B", null, originalCurrency, 1999L, null, testedBy); 289 createTransactionEntityRoleUsingNames(transaction, "TEST_ENTITY_INSTANCE_ROLE_TYPE", testedBy, testedBy); 290 postTransaction(eea, session, transaction, testedBy); 291 } 292 293}