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