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}