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