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.sales.server.logic;
018
019import com.echothree.control.user.sales.common.spec.SalesOrderBatchUniversalSpec;
020import com.echothree.model.control.batch.common.BatchTypes;
021import com.echothree.model.control.batch.common.exception.InvalidBatchTypeException;
022import com.echothree.model.control.batch.server.control.BatchControl;
023import com.echothree.model.control.batch.server.logic.BatchLogic;
024import com.echothree.model.control.core.common.ComponentVendors;
025import com.echothree.model.control.core.common.EntityTypes;
026import com.echothree.model.control.core.common.exception.InvalidParameterCountException;
027import com.echothree.model.control.core.server.control.EntityInstanceControl;
028import com.echothree.model.control.core.server.logic.EntityInstanceLogic;
029import com.echothree.model.control.order.server.control.OrderBatchControl;
030import com.echothree.model.control.order.server.control.OrderControl;
031import com.echothree.model.control.sales.common.choice.SalesOrderBatchStatusChoicesBean;
032import com.echothree.model.control.sales.common.exception.CannotDeleteSalesOrderBatchInUseException;
033import com.echothree.model.control.sales.common.exception.IncorrectSalesOrderBatchAmountException;
034import com.echothree.model.control.sales.common.exception.IncorrectSalesOrderBatchCountException;
035import com.echothree.model.control.sales.common.exception.InvalidSalesOrderBatchStatusException;
036import com.echothree.model.control.sales.common.exception.InvalidSalesOrderStatusException;
037import com.echothree.model.control.sales.common.exception.UnknownSalesOrderBatchStatusChoiceException;
038import com.echothree.model.control.sales.common.workflow.SalesOrderBatchStatusConstants;
039import com.echothree.model.control.sales.common.workflow.SalesOrderStatusConstants;
040import com.echothree.model.control.sales.server.control.SalesOrderBatchControl;
041import com.echothree.model.control.workflow.server.control.WorkflowControl;
042import com.echothree.model.control.workflow.server.logic.WorkflowDestinationLogic;
043import com.echothree.model.control.workflow.server.logic.WorkflowLogic;
044import com.echothree.model.control.workflow.server.logic.WorkflowStepLogic;
045import com.echothree.model.data.accounting.server.entity.Currency;
046import com.echothree.model.data.batch.server.entity.Batch;
047import com.echothree.model.data.batch.server.entity.BatchEntity;
048import com.echothree.model.data.order.server.entity.Order;
049import com.echothree.model.data.party.common.pk.PartyPK;
050import com.echothree.model.data.party.server.entity.Language;
051import com.echothree.model.data.payment.server.entity.PaymentMethod;
052import com.echothree.util.common.message.ExecutionErrors;
053import com.echothree.util.common.persistence.BasePK;
054import com.echothree.util.server.control.BaseLogic;
055import com.echothree.util.server.message.ExecutionErrorAccumulator;
056import com.echothree.util.server.persistence.EntityPermission;
057import com.echothree.util.server.persistence.Session;
058import com.echothree.util.server.string.AmountUtils;
059import java.util.ArrayList;
060import java.util.List;
061import javax.enterprise.context.ApplicationScoped;
062import javax.inject.Inject;
063
064@ApplicationScoped
065public class SalesOrderBatchLogic
066        extends BaseLogic {
067
068    @Inject
069    BatchControl batchControl;
070
071    @Inject
072    EntityInstanceControl entityInstanceControl;
073
074    @Inject
075    OrderBatchControl orderBatchControl;
076
077    @Inject
078    OrderControl orderControl;
079
080    @Inject
081    SalesOrderBatchControl salesOrderBatchControl;
082
083    @Inject
084    WorkflowControl workflowControl;
085
086    @Inject
087    BatchLogic batchLogic;
088
089    @Inject
090    EntityInstanceLogic entityInstanceLogic;
091
092    @Inject
093    SalesOrderLineLogic salesOrderLineLogic;
094
095    @Inject
096    WorkflowDestinationLogic workflowDestinationLogic;
097
098    @Inject
099    WorkflowLogic workflowLogic;
100
101    @Inject
102    WorkflowStepLogic workflowStepLogic;
103
104    protected SalesOrderBatchLogic() {
105        super();
106    }
107
108    public Batch createBatch(final ExecutionErrorAccumulator eea, final Currency currency, final PaymentMethod paymentMethod, final Long count,
109            final Long amount, final BasePK createdBy) {
110        var batch = batchLogic.createBatch(eea, BatchTypes.SALES_ORDER.name(), createdBy);
111
112        if(eea == null || !eea.hasExecutionErrors()) {
113            orderBatchControl.createOrderBatch(batch, currency, count, amount, createdBy);
114            salesOrderBatchControl.createSalesOrderBatch(batch, paymentMethod, createdBy);
115        }
116
117        return batch;
118    }
119    
120    public boolean checkBatchInWorkflowSteps(final ExecutionErrorAccumulator eea, final Batch batch, final String... workflowStepNames) {
121        return !workflowStepLogic.isEntityInWorkflowSteps(eea, SalesOrderBatchStatusConstants.Workflow_SALES_ORDER_BATCH_STATUS,
122                getEntityInstanceByBaseEntity(batch), workflowStepNames).isEmpty();
123    }
124
125    public boolean checkBatchAvailableForEntry(final ExecutionErrorAccumulator eea, final Batch batch) {
126        return checkBatchInWorkflowSteps(eea, batch, SalesOrderBatchStatusConstants.WorkflowStep_ENTRY);
127    }
128    
129    public BatchEntity createBatchEntity(final ExecutionErrorAccumulator eea, final Order order, final Batch batch, final BasePK createdBy) {
130        return batchLogic.createBatchEntity(eea, getEntityInstanceByBaseEntity(order), batch, createdBy);
131    }
132
133    public boolean batchEntryExists(final ExecutionErrorAccumulator eea, final Order order, final Batch batch, final String... workflowStepNames) {
134        var result = false;
135        
136        if(workflowStepNames.length == 0) {
137            result = batchLogic.batchEntityExists(order, batch);
138        } else {
139            if(!workflowStepLogic.isEntityInWorkflowSteps(eea, SalesOrderBatchStatusConstants.Workflow_SALES_ORDER_BATCH_STATUS,
140                    getEntityInstanceByBaseEntity(order), workflowStepNames).isEmpty()) {
141                result = true;
142            } else {
143                handleExecutionError(InvalidSalesOrderBatchStatusException.class, eea, ExecutionErrors.InvalidSalesOrderBatchStatus.name(),
144                        batch.getLastDetail().getBatchName());
145            }
146        }
147        
148        return result;
149    }
150    
151    public void deleteBatch(final ExecutionErrorAccumulator eea, final Batch batch, final BasePK deletedBy) {
152        if(batchControl.countBatchEntitiesByBatch(batch) == 0) {
153            batchLogic.deleteBatch(eea, batch, deletedBy);
154
155            if(eea == null || !eea.hasExecutionErrors()) {
156                orderBatchControl.deleteOrderBatch(batch, deletedBy);
157                salesOrderBatchControl.deleteSalesOrderBatch(batch, deletedBy);
158            }
159        } else {
160            handleExecutionError(CannotDeleteSalesOrderBatchInUseException.class, eea, ExecutionErrors.CannotDeleteSalesOrderBatchInUse.name(),
161                    batch.getLastDetail().getBatchName());
162        }
163    }
164
165    public Batch getBatchByName(final ExecutionErrorAccumulator eea, final String batchName, final EntityPermission entityPermission) {
166        return batchLogic.getBatchByName(eea, BatchTypes.SALES_ORDER.name(), batchName, entityPermission);
167    }
168
169    public Batch getBatchByName(final ExecutionErrorAccumulator eea, final String batchName) {
170        return getBatchByName(eea, batchName, EntityPermission.READ_ONLY);
171    }
172
173    public Batch getBatchByNameForUpdate(final ExecutionErrorAccumulator eea, final String batchName) {
174        return getBatchByName(eea, batchName, EntityPermission.READ_WRITE);
175    }
176
177    public Batch getBatchByUniversalSpec(final ExecutionErrorAccumulator eea,
178            final SalesOrderBatchUniversalSpec universalSpec, final EntityPermission entityPermission) {
179        Batch batch = null;
180        var batchName = universalSpec.getBatchName();
181        var parameterCount = (batchName == null ? 0 : 1) + entityInstanceLogic.countPossibleEntitySpecs(universalSpec);
182
183        switch(parameterCount) {
184            case 1 -> {
185                if(batchName == null) {
186                    var entityInstance = entityInstanceLogic.getEntityInstance(eea, universalSpec,
187                            ComponentVendors.ECHO_THREE.name(), EntityTypes.Batch.name());
188
189                    if(eea == null || !eea.hasExecutionErrors()) {
190                        batch = batchControl.getBatchByEntityInstance(entityInstance, entityPermission);
191                    }
192                } else {
193                    batch = getBatchByName(eea, batchName, entityPermission);
194                }
195
196                if(!hasExecutionErrors(eea)) {
197                    var batchTypeName = batch.getLastDetail().getBatchType().getLastDetail().getBatchTypeName();
198
199                    if(!batchTypeName.equals(BatchTypes.SALES_ORDER.name())) {
200                        batch = null;
201                        // Purposefully disclose nothing about the BatchType of the Batch that was found.
202                        handleExecutionError(InvalidBatchTypeException.class, eea, ExecutionErrors.InvalidBatchType.name());
203                    }
204                }
205            }
206            default ->
207                    handleExecutionError(InvalidParameterCountException.class, eea, ExecutionErrors.InvalidParameterCount.name());
208        }
209
210        return batch;
211    }
212
213    public Batch getBatchByUniversalSpec(final ExecutionErrorAccumulator eea,
214            final SalesOrderBatchUniversalSpec universalSpec) {
215        return getBatchByUniversalSpec(eea, universalSpec, EntityPermission.READ_ONLY);
216    }
217
218    public Batch getBatchByUniversalSpecForUpdate(final ExecutionErrorAccumulator eea,
219            final SalesOrderBatchUniversalSpec universalSpec) {
220        return getBatchByUniversalSpec(eea, universalSpec, EntityPermission.READ_WRITE);
221    }
222
223    public SalesOrderBatchStatusChoicesBean getSalesOrderBatchStatusChoices(String defaultSalesOrderBatchStatusChoice, Language language, boolean allowNullChoice,
224            Batch batch, PartyPK partyPK) {
225        var salesOrderBatchStatusChoicesBean = new SalesOrderBatchStatusChoicesBean();
226
227        if(batch == null) {
228            workflowControl.getWorkflowEntranceChoices(salesOrderBatchStatusChoicesBean, defaultSalesOrderBatchStatusChoice, language, allowNullChoice,
229                    workflowControl.getWorkflowByName(SalesOrderBatchStatusConstants.Workflow_SALES_ORDER_BATCH_STATUS), partyPK);
230        } else {
231            var entityInstance = entityInstanceControl.getEntityInstanceByBasePK(batch.getPrimaryKey());
232            var workflowEntityStatus = workflowControl.getWorkflowEntityStatusByEntityInstanceUsingNames(SalesOrderBatchStatusConstants.Workflow_SALES_ORDER_BATCH_STATUS,
233                    entityInstance);
234
235            workflowControl.getWorkflowDestinationChoices(salesOrderBatchStatusChoicesBean, defaultSalesOrderBatchStatusChoice, language, allowNullChoice,
236                    workflowEntityStatus.getWorkflowStep(), partyPK);
237        }
238
239        return salesOrderBatchStatusChoicesBean;
240    }
241
242    public void setSalesOrderBatchStatus(final Session session, ExecutionErrorAccumulator eea, Batch batch, String salesOrderBatchStatusChoice, PartyPK modifiedBy) {
243        var workflow = workflowLogic.getWorkflowByName(eea, SalesOrderBatchStatusConstants.Workflow_SALES_ORDER_BATCH_STATUS);
244        var entityInstance = entityInstanceControl.getEntityInstanceByBasePK(batch.getPrimaryKey());
245        var workflowEntityStatus = workflowControl.getWorkflowEntityStatusByEntityInstanceForUpdate(workflow, entityInstance);
246        var workflowDestination = salesOrderBatchStatusChoice == null ? null : workflowControl.getWorkflowDestinationByName(workflowEntityStatus.getWorkflowStep(), salesOrderBatchStatusChoice);
247
248        if(workflowDestination != null || salesOrderBatchStatusChoice == null) {
249            var currentWorkflowStepName = workflowEntityStatus.getWorkflowStep().getLastDetail().getWorkflowStepName();
250            var map = workflowDestinationLogic.getWorkflowDestinationsAsMap(workflowDestination);
251            Long triggerTime = null;
252            
253            if(currentWorkflowStepName.equals(SalesOrderBatchStatusConstants.WorkflowStep_ENTRY)) {
254                if(workflowDestinationLogic.workflowDestinationMapContainsStep(map, SalesOrderBatchStatusConstants.Workflow_SALES_ORDER_BATCH_STATUS, SalesOrderBatchStatusConstants.WorkflowStep_AUDIT)) {
255                    var batchEntities = batchControl.getBatchEntitiesByBatch(batch);
256                    
257                    // Verify all orders are in BATCH_AUDIT.
258                    batchEntities.stream().map((batchEntity) -> workflowControl.getWorkflowEntityStatusByEntityInstanceUsingNames(SalesOrderStatusConstants.Workflow_SALES_ORDER_STATUS, batchEntity.getEntityInstance())).forEach((orderWorkflowEntityStatus) -> {
259                        var orderEntityInstance = orderWorkflowEntityStatus.getEntityInstance();
260                        var orderWorkflowStepName = orderWorkflowEntityStatus.getWorkflowStep().getLastDetail().getWorkflowStepName();
261                        if (!orderWorkflowStepName.equals(SalesOrderStatusConstants.WorkflowStep_BATCH_AUDIT)) {
262                            var order = orderControl.getOrderByEntityInstance(orderEntityInstance);
263                            
264                            handleExecutionError(InvalidSalesOrderStatusException.class, eea, ExecutionErrors.InvalidSalesOrderStatus.name(),
265                                    order.getLastDetail().getOrderName(), orderWorkflowStepName);
266                        }
267                    });
268                }
269            } else if(currentWorkflowStepName.equals(SalesOrderBatchStatusConstants.WorkflowStep_AUDIT)) {
270                if(workflowDestinationLogic.workflowDestinationMapContainsStep(map, SalesOrderBatchStatusConstants.Workflow_SALES_ORDER_BATCH_STATUS, SalesOrderBatchStatusConstants.WorkflowStep_COMPLETE)) {
271                    var orderBatch = orderBatchControl.getOrderBatch(batch);
272                    var count = orderBatch.getCount();
273                    var amount = orderBatch.getAmount();
274
275                    if(count != null) {
276                        Long batchCount = batchControl.countBatchEntitiesByBatch(batch);
277
278                        if(!count.equals(batchCount)) {
279                            handleExecutionError(IncorrectSalesOrderBatchCountException.class, eea, ExecutionErrors.IncorrectSalesOrderBatchCount.name(),
280                                    count.toString(), batchCount.toString());
281                        }
282                    }
283
284                    if(amount != null) {
285                        var batchAmount = getBatchOrderTotalsWithAdjustments(batch);
286                        
287                        if(!amount.equals(batchAmount)) {
288                            var currency = orderBatch.getCurrency();
289
290                            handleExecutionError(IncorrectSalesOrderBatchAmountException.class, eea, ExecutionErrors.IncorrectSalesOrderBatchAmount.name(),
291                                    AmountUtils.getInstance().formatPriceUnit(currency, amount),
292                                    AmountUtils.getInstance().formatPriceUnit(currency, batchAmount));
293                        }
294                    }
295                }
296            }
297            
298            if(eea == null || !eea.hasExecutionErrors()) {
299                workflowControl.transitionEntityInWorkflow(eea, workflowEntityStatus, workflowDestination, triggerTime, modifiedBy);
300            }
301        } else {
302            handleExecutionError(UnknownSalesOrderBatchStatusChoiceException.class, eea, ExecutionErrors.UnknownSalesOrderBatchStatusChoice.name(), salesOrderBatchStatusChoice);
303        }
304    }
305
306    public List<Order> getBatchOrders(final Batch batch) {
307        var batchEntities = batchControl.getBatchEntitiesByBatch(batch);
308        List<Order> orders = new ArrayList<>(batchEntities.size());
309
310        batchEntities.forEach((batchEntity) -> {
311            orders.add(orderControl.getOrderByEntityInstance(batchEntity.getEntityInstance()));
312        });
313
314        return orders;
315    }
316
317    public Long getBatchOrderTotalsWithAdjustments(Batch batch) {
318        var orders = getBatchOrders(batch);
319        long total = 0;
320
321        total = orders.stream().map((order) -> salesOrderLineLogic.getOrderTotalWithAdjustments(order)).reduce(total, (accumulator, _item) -> accumulator + _item);
322
323        return total;
324    }
325
326}