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}