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.inject.Inject; 052 053@ApplicationScoped 054public class TransactionLogic 055 extends BaseLogic { 056 057 @Inject 058 AccountingControl accountingControl; 059 060 @Inject 061 EntityInstanceControl entityInstanceControl; 062 063 @Inject 064 PartyControl partyControl; 065 066 @Inject 067 WorkflowControl workflowControl; 068 069 @Inject 070 EntityInstanceLogic entityInstanceLogic; 071 072 @Inject 073 PostingLogic postingLogic; 074 075 @Inject 076 TransactionTimeLogic transactionTimeLogic; 077 078 @Inject 079 WorkflowEntranceLogic workflowEntranceLogic; 080 081 @Inject 082 WorkflowLogic workflowLogic; 083 084 protected TransactionLogic() { 085 super(); 086 } 087 088 public Transaction createTransactionUsingNames(final Session session, final Party groupParty, final String transactionTypeName, 089 final Long transactionTime, final BasePK createdBy) { 090 return createTransaction(session, groupParty, accountingControl.getTransactionTypeByName(transactionTypeName), 091 transactionTime, createdBy); 092 } 093 094 public Transaction createTransaction(final Session session, final Party groupParty, final TransactionType transactionType, 095 final Long transactionTime, final BasePK createdBy) { 096 var transaction = accountingControl.createTransaction(groupParty, transactionType, createdBy); 097 098 transactionTimeLogic.createTransactionTime(null, transaction, TransactionTimeTypes.TRANSACTION_TIME.name(), 099 transactionTime == null ? session.getStartTime() : transactionTime, createdBy); 100 101 return transaction; 102 } 103 104 public Transaction getTransactionByName(final ExecutionErrorAccumulator eea, final String transactionName, 105 final EntityPermission entityPermission) { 106 var transaction = accountingControl.getTransactionByName(transactionName, entityPermission); 107 108 if(transaction == null) { 109 handleExecutionError(UnknownTransactionNameException.class, eea, ExecutionErrors.UnknownTransactionName.name(), transactionName); 110 } 111 112 return transaction; 113 } 114 115 public Transaction getTransactionByName(final ExecutionErrorAccumulator eea, final String transactionName) { 116 return getTransactionByName(eea, transactionName, EntityPermission.READ_ONLY); 117 } 118 119 public Transaction getTransactionByNameForUpdate(final ExecutionErrorAccumulator eea, final String transactionName) { 120 return getTransactionByName(eea, transactionName, EntityPermission.READ_WRITE); 121 } 122 123 public Transaction getTransactionByUniversalSpec(final ExecutionErrorAccumulator eea, 124 final TransactionUniversalSpec universalSpec, final EntityPermission entityPermission) { 125 Transaction transaction = null; 126 var transactionName = universalSpec.getTransactionName(); 127 var parameterCount = (transactionName == null ? 0 : 1) + entityInstanceLogic.countPossibleEntitySpecs(universalSpec); 128 129 switch(parameterCount) { 130 case 1 -> { 131 if(transactionName == null) { 132 var entityInstance = entityInstanceLogic.getEntityInstance(eea, universalSpec, 133 ComponentVendors.ECHO_THREE.name(), EntityTypes.Transaction.name()); 134 135 if(eea == null || !eea.hasExecutionErrors()) { 136 transaction = accountingControl.getTransactionByEntityInstance(entityInstance, entityPermission); 137 } 138 } else { 139 transaction = getTransactionByName(eea, transactionName, entityPermission); 140 } 141 } 142 default -> 143 handleExecutionError(InvalidParameterCountException.class, eea, ExecutionErrors.InvalidParameterCount.name()); 144 } 145 146 return transaction; 147 } 148 149 public Transaction getTransactionByUniversalSpec(final ExecutionErrorAccumulator eea, 150 final TransactionUniversalSpec universalSpec) { 151 return getTransactionByUniversalSpec(eea, universalSpec, EntityPermission.READ_ONLY); 152 } 153 154 public Transaction getTransactionByUniversalSpecForUpdate(final ExecutionErrorAccumulator eea, 155 final TransactionUniversalSpec universalSpec) { 156 return getTransactionByUniversalSpec(eea, universalSpec, EntityPermission.READ_WRITE); 157 } 158 159 public TransactionGlEntry createTransactionGlEntryUsingNames(final Transaction transaction, final Party groupParty, 160 final String transactionGlAccountCategoryName, final GlAccount glAccount, final Currency originalCurrency, 161 final Long originalDebit, final Long originalCredit, final BasePK createdBy) { 162 var transactionDetail = transaction.getLastDetail(); 163 164 return createTransactionGlEntry(transaction, groupParty == null ? transactionDetail.getGroupParty() : groupParty, 165 accountingControl.getTransactionGlAccountCategoryByName(transactionDetail.getTransactionType(), transactionGlAccountCategoryName), 166 glAccount, originalCurrency, originalDebit, originalCredit, createdBy); 167 } 168 169 private GlAccount getGlAccount(final AccountingControl accountingControl, final TransactionGlAccountCategory transactionGlAccountCategory, 170 GlAccount glAccount) { 171 if(glAccount == null) { 172 var transactionGlAccount = accountingControl.getTransactionGlAccount(transactionGlAccountCategory); 173 174 if(transactionGlAccount == null) { 175 throw new IllegalArgumentException("glAccount is a required parameter"); 176 } else { 177 glAccount = transactionGlAccount.getGlAccount(); 178 } 179 } 180 181 return glAccount; 182 } 183 184 private Integer getTransactionGlEntrySequence(final AccountingControl accountingControl, final Transaction transaction) { 185 var transactionStatus = accountingControl.getTransactionStatusForUpdate(transaction); 186 Integer transactionGlEntrySequence = transactionStatus.getTransactionGlEntrySequence() + 1; 187 188 transactionStatus.setTransactionGlEntrySequence(transactionGlEntrySequence); 189 190 return transactionGlEntrySequence; 191 } 192 193 private Long getAmount(final GlAccount glAccount, final Currency originalCurrency, final Long originalAmount) { 194 var currency = glAccount.getLastDetail().getCurrency(); 195 196 Long amount; 197 if(originalCurrency.equals(currency)) { 198 amount = originalAmount; 199 } else { 200 throw new IllegalArgumentException("Currency conversion is not available"); 201 } 202 203 return amount; 204 } 205 206 public TransactionGlEntry createTransactionGlEntry(final Transaction transaction, final Party groupParty, 207 final TransactionGlAccountCategory transactionGlAccountCategory, GlAccount glAccount, final Currency originalCurrency, 208 final Long originalDebit, final Long originalCredit, final BasePK createdBy) { 209 glAccount = getGlAccount(accountingControl, transactionGlAccountCategory, glAccount); 210 211 var debit = originalDebit == null ? null : getAmount(glAccount, originalCurrency, originalDebit); 212 var credit = originalCredit == null ? null : getAmount(glAccount, originalCurrency, originalCredit); 213 214 return accountingControl.createTransactionGlEntry(transaction, getTransactionGlEntrySequence(accountingControl, transaction), 215 groupParty, transactionGlAccountCategory, glAccount, originalCurrency, originalDebit, 216 originalCredit, debit, credit, createdBy); 217 } 218 219 public TransactionEntityRole createTransactionEntityRoleUsingNames(final Transaction transaction, 220 final String transactionEntityRoleTypeName, final BasePK pk, final BasePK createdBy) { 221 var transactionEntityRoleType = accountingControl.getTransactionEntityRoleTypeByName(transaction.getLastDetail().getTransactionType(), 222 transactionEntityRoleTypeName); 223 224 return createTransactionEntityRole(transaction, transactionEntityRoleType, pk, createdBy); 225 } 226 227 public TransactionEntityRole createTransactionEntityRole(final Transaction transaction, 228 final TransactionEntityRoleType transactionEntityRoleType, final BasePK pk, final BasePK createdBy) { 229 var entityInstance = entityInstanceControl.getEntityInstanceByBasePK(pk); 230 231 if(!transactionEntityRoleType.getLastDetail().getEntityType().equals(entityInstance.getEntityType())) { 232 throw new IllegalArgumentException("entityInstance is not of the required EntityType"); 233 } 234 235 return accountingControl.createTransactionEntityRole(transaction, transactionEntityRoleType, entityInstance, createdBy); 236 } 237 238 private void validateTransactionBalanced(final ExecutionErrorAccumulator eea, final Transaction transaction) { 239 var transactionBalancedResults = new TransactionBalancedQuery().execute(transaction); 240 241 if(!transactionBalancedResults.isEmpty()) { 242 var transactionBalancedResult = transactionBalancedResults.getFirst(); 243 var originalDifference = transactionBalancedResult.getOriginalDifference(); 244 var difference = transactionBalancedResult.getDifference(); 245 246 if(originalDifference != 0 || difference != 0) { 247 handleExecutionError(TransactionNotBalancedException.class, eea, ExecutionErrors.TransactionNotBalanced.name(), 248 originalDifference, difference); 249 } 250 } 251 } 252 253 public void postTransaction(final ExecutionErrorAccumulator eea, final Session session, final Transaction transaction, 254 final BasePK createdBy) { 255 validateTransactionBalanced(eea, transaction); 256 257 if(eea != null && !hasExecutionErrors(eea)) { 258 accountingControl.removeTransactionStatusByTransaction(transaction); 259 260 postingLogic.postTransaction(session, transaction, createdBy); 261 262 // If it isn't in the Transaction Status workflow, assume this is a system generated transaction and 263 // we've gone directly to posting it. 264 var workflow = workflowLogic.getWorkflowByName(null, TransactionStatusConstants.Workflow_TRANSACTION_STATUS); 265 var entityInstance = entityInstanceControl.getEntityInstanceByBasePK(transaction.getPrimaryKey()); 266 if(!workflowControl.isEntityInWorkflow(workflow, entityInstance)) { 267 var workflowEntrance = workflowEntranceLogic.getWorkflowEntranceByName(null, workflow, 268 TransactionStatusConstants.WorkflowEntrance_TRANSACTION_STATUS_NEW_POSTED); 269 270 workflowControl.addEntityToWorkflow(workflowEntrance, entityInstance, null, null, createdBy); 271 } 272 } 273 } 274 275 public void testTransaction(final ExecutionErrorAccumulator eea, final Session session, final BasePK testedBy) { 276 var companyParty = partyControl.getDefaultPartyCompany().getParty(); 277 var divisionParty = partyControl.getDefaultPartyDivision(companyParty).getParty(); 278 var departmentParty = partyControl.getDefaultPartyDepartment(divisionParty).getParty(); 279 var originalCurrency = accountingControl.getDefaultCurrency(); 280 281 var transaction = createTransactionUsingNames(session, departmentParty, "TEST", null, testedBy); 282 createTransactionGlEntryUsingNames(transaction, null, "TEST_ACCOUNT_A", null, originalCurrency, null, 1999L, testedBy); 283 createTransactionGlEntryUsingNames(transaction, null, "TEST_ACCOUNT_B", null, originalCurrency, 1999L, null, testedBy); 284 createTransactionEntityRoleUsingNames(transaction, "TEST_ENTITY_INSTANCE_ROLE_TYPE", testedBy, testedBy); 285 postTransaction(eea, session, transaction, testedBy); 286 } 287 288}