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.purchase.server.logic;
018
019import com.echothree.model.control.cancellationpolicy.common.CancellationKinds;
020import com.echothree.model.control.cancellationpolicy.server.logic.CancellationPolicyLogic;
021import com.echothree.model.control.core.server.control.EntityInstanceControl;
022import com.echothree.model.control.order.common.OrderRoleTypes;
023import com.echothree.model.control.order.common.OrderTypes;
024import com.echothree.model.control.order.server.control.OrderControl;
025import com.echothree.model.control.order.server.control.OrderRoleControl;
026import com.echothree.model.control.order.server.logic.BaseOrderLogic;
027import com.echothree.model.control.order.server.logic.OrderPriorityLogic;
028import com.echothree.model.control.purchase.common.choice.PurchaseOrderStatusChoicesBean;
029import com.echothree.model.control.purchase.common.exception.DuplicateHandlingInPurchaseOrderStatusTransitionException;
030import com.echothree.model.control.purchase.common.exception.InvalidPurchaseOrderReferenceException;
031import com.echothree.model.control.purchase.common.exception.InvalidPurchaseOrderStatusException;
032import com.echothree.model.control.purchase.common.exception.PurchaseOrderDuplicateReferenceException;
033import com.echothree.model.control.purchase.common.exception.PurchaseOrderReferenceRequiredException;
034import com.echothree.model.control.purchase.common.exception.UnhandledPurchaseOrderStatusTransitionException;
035import com.echothree.model.control.purchase.common.exception.UnknownPurchaseOrderStatusChoiceException;
036import com.echothree.model.control.purchase.common.workflow.PurchaseOrderStatusConstants;
037import com.echothree.model.control.returnpolicy.common.ReturnKinds;
038import com.echothree.model.control.returnpolicy.server.logic.ReturnPolicyLogic;
039import com.echothree.model.control.sequence.common.SequenceTypes;
040import com.echothree.model.control.sequence.server.logic.SequenceGeneratorLogic;
041import com.echothree.model.control.shipment.server.control.PartyFreeOnBoardControl;
042import com.echothree.model.control.shipment.server.logic.FreeOnBoardLogic;
043import com.echothree.model.control.term.server.control.TermControl;
044import com.echothree.model.control.term.server.logic.TermLogic;
045import com.echothree.model.control.user.server.control.UserControl;
046import com.echothree.model.control.vendor.server.control.VendorControl;
047import com.echothree.model.control.vendor.server.logic.VendorLogic;
048import com.echothree.model.control.workflow.server.control.WorkflowControl;
049import com.echothree.model.control.workflow.server.logic.WorkflowDestinationLogic;
050import com.echothree.model.control.workflow.server.logic.WorkflowLogic;
051import com.echothree.model.control.workflow.server.logic.WorkflowStepLogic;
052import com.echothree.model.data.cancellationpolicy.server.entity.CancellationPolicy;
053import com.echothree.model.data.core.server.entity.EntityInstance;
054import com.echothree.model.data.order.server.entity.Order;
055import com.echothree.model.data.order.server.entity.OrderPriority;
056import com.echothree.model.data.party.common.pk.PartyPK;
057import com.echothree.model.data.party.server.entity.Language;
058import com.echothree.model.data.party.server.entity.Party;
059import com.echothree.model.data.returnpolicy.server.entity.ReturnPolicy;
060import com.echothree.model.data.shipment.server.entity.FreeOnBoard;
061import com.echothree.model.data.term.server.entity.Term;
062import com.echothree.model.data.user.server.entity.UserVisit;
063import com.echothree.model.data.vendor.server.entity.Vendor;
064import com.echothree.model.data.vendor.server.entity.VendorType;
065import com.echothree.util.common.message.ExecutionErrors;
066import com.echothree.util.common.persistence.BasePK;
067import com.echothree.util.server.message.ExecutionErrorAccumulator;
068import com.echothree.util.server.persistence.Session;
069import javax.enterprise.context.ApplicationScoped;
070import javax.inject.Inject;
071
072@ApplicationScoped
073public class PurchaseOrderLogic
074        extends BaseOrderLogic {
075
076    @Inject
077    EntityInstanceControl entityInstanceControl;
078
079    @Inject
080    OrderControl orderControl;
081
082    @Inject
083    OrderRoleControl orderRoleControl;
084
085    @Inject
086    PartyFreeOnBoardControl partyFreeOnBoardControl;
087
088    @Inject
089    TermControl termControl;
090
091    @Inject
092    UserControl userControl;
093
094    @Inject
095    VendorControl vendorControl;
096
097    @Inject
098    WorkflowControl workflowControl;
099
100    @Inject
101    CancellationPolicyLogic cancellationPolicyLogic;
102
103    @Inject
104    FreeOnBoardLogic freeOnBoardLogic;
105
106    @Inject
107    OrderPriorityLogic orderPriorityLogic;
108
109    @Inject
110    ReturnPolicyLogic returnPolicyLogic;
111
112    @Inject
113    SequenceGeneratorLogic sequenceGeneratorLogic;
114
115    @Inject
116    TermLogic termLogic;
117
118    @Inject
119    VendorLogic vendorLogic;
120
121    @Inject
122    WorkflowDestinationLogic workflowDestinationLogic;
123
124    @Inject
125    WorkflowLogic workflowLogic;
126
127    @Inject
128    WorkflowStepLogic workflowStepLogic;
129
130    protected PurchaseOrderLogic() {
131        super();
132    }
133    
134    public void validatePurchaseOrderReference(final ExecutionErrorAccumulator eea, final String reference,
135            final Vendor vendor) {
136        var requireReference = vendor.getRequireReference();
137
138        if(requireReference != null) {
139            var allowReferenceDuplicates = vendor.getAllowReferenceDuplicates();
140            var referenceValidationPattern = vendor.getReferenceValidationPattern();
141
142            if(requireReference && reference == null) {
143                handleExecutionError(PurchaseOrderReferenceRequiredException.class, eea, ExecutionErrors.PurchaseOrderReferenceRequired.name());
144            } else if(reference != null) {
145                if(!allowReferenceDuplicates && orderControl.countOrdersByBillToAndReference(vendor.getParty(), reference) != 0) {
146                    handleExecutionError(PurchaseOrderDuplicateReferenceException.class, eea, ExecutionErrors.PurchaseOrderDuplicateReference.name());
147                }
148
149                if(referenceValidationPattern != null && !reference.matches(referenceValidationPattern)) {
150                    handleExecutionError(InvalidPurchaseOrderReferenceException.class, eea, ExecutionErrors.InvalidPurchaseOrderReference.name());
151                }
152            }
153        }
154    }
155
156    public CancellationPolicy getCancellationPolicy(final ExecutionErrorAccumulator eea, final VendorType vendorType, final Vendor billToVendor) {
157        return cancellationPolicyLogic.getDefaultCancellationPolicyByKind(eea, CancellationKinds.VENDOR_CANCELLATION.name(),
158                new CancellationPolicy[]{
159                    billToVendor == null ? null : billToVendor.getCancellationPolicy(),
160                    vendorType.getLastDetail().getDefaultCancellationPolicy()
161                });
162    }
163
164    public ReturnPolicy getReturnPolicy(final ExecutionErrorAccumulator eea, final VendorType vendorType, final Vendor billToVendor) {
165        return returnPolicyLogic.getDefaultReturnPolicyByKind(eea, ReturnKinds.VENDOR_RETURN.name(),
166                new ReturnPolicy[]{
167                    billToVendor == null ? null : billToVendor.getReturnPolicy(),
168                    vendorType.getLastDetail().getDefaultReturnPolicy()
169                });
170    }
171
172    /**
173     * Create a new Purchase Order using appropriate defaults where possible for Optional parameters.
174     *
175     * @param session Required.
176     * @param eea Required.
177     * @param userVisit Required.
178     * @param vendorParty Required.
179     * @param holdUntilComplete Optional.
180     * @param allowBackorders Optional.
181     * @param allowSubstitutions Optional.
182     * @param allowCombiningShipments Optional.
183     * @param reference Optional.
184     * @param term Optional.
185     * @param workflowEntranceName Optional.
186     * @param createdByParty Required.
187     * @return The newly created Order, or null if there was an error.
188     */
189    public Order createPurchaseOrder(final Session session, final ExecutionErrorAccumulator eea, final UserVisit userVisit,
190            final Party vendorParty, Boolean holdUntilComplete, Boolean allowBackorders, Boolean allowSubstitutions,
191            Boolean allowCombiningShipments, final String reference, Term term, FreeOnBoard freeOnBoard,
192            final String workflowEntranceName, final Party createdByParty) {
193        var orderType = getOrderTypeByName(eea, OrderTypes.PURCHASE_ORDER.name());
194        var billToOrderRoleType = getOrderRoleTypeByName(eea, OrderRoleTypes.BILL_TO.name());
195        var placingOrderRoleType = getOrderRoleTypeByName(eea, OrderRoleTypes.PLACING.name());
196        Order order = null;
197
198        if(eea == null || !eea.hasExecutionErrors()) {
199            var currency = userControl.getPreferredCurrencyFromParty(vendorParty);
200            var vendor = vendorControl.getVendor(vendorParty);
201            var vendorType = vendor.getVendorType();
202
203            holdUntilComplete = holdUntilComplete == null ? vendor.getHoldUntilComplete() : holdUntilComplete;
204            allowBackorders = allowBackorders == null ? vendor.getAllowBackorders() : allowBackorders;
205            allowSubstitutions = allowSubstitutions == null ? vendor.getAllowSubstitutions() : allowSubstitutions;
206            allowCombiningShipments = allowCombiningShipments == null ? vendor.getAllowCombiningShipments() : allowCombiningShipments;
207
208            term = term == null ? termControl.getPartyTerm(vendorParty).getTerm() : term;
209            freeOnBoard = freeOnBoard == null ? partyFreeOnBoardControl.getPartyFreeOnBoard(vendorParty).getFreeOnBoard() : freeOnBoard;
210
211            var cancellationPolicy = getCancellationPolicy(eea, vendorType, vendor);
212            var returnPolicy = getReturnPolicy(eea, vendorType, vendor);
213
214            validatePurchaseOrderReference(eea, reference, vendor);
215
216            var sequence = sequenceGeneratorLogic.getDefaultSequence(eea, SequenceTypes.PURCHASE_ORDER.name());
217
218            if(eea == null || !eea.hasExecutionErrors()) {
219                var userSesson = userControl.getUserSessionByUserVisit(userVisit);;
220                var createdByPartyPK = createdByParty.getPrimaryKey();
221
222                order = createOrder(eea, orderType, sequence, null, currency, holdUntilComplete, allowBackorders,
223                        allowSubstitutions, allowCombiningShipments, term, freeOnBoard, reference, null,
224                        cancellationPolicy, returnPolicy, null, createdByPartyPK);
225
226                orderControl.createOrderUserVisit(order, userVisit);
227
228                var entityInstance = entityInstanceControl.getEntityInstanceByBasePK(order.getPrimaryKey());
229                workflowControl.addEntityToWorkflowUsingNames(null, PurchaseOrderStatusConstants.Workflow_PURCHASE_ORDER_STATUS,
230                        workflowEntranceName, entityInstance, null, null, createdByPartyPK);
231
232                orderRoleControl.createOrderRole(order, userSesson.getPartyRelationship().getFromParty(), billToOrderRoleType, createdByPartyPK);
233
234                orderRoleControl.createOrderRole(order, createdByParty, placingOrderRoleType, createdByPartyPK);
235            }
236        }
237
238        return order;
239    }
240
241    public Order createPurchaseOrder(final Session session, final ExecutionErrorAccumulator eea, final UserVisit userVisit,
242            final String vendorName, final String termName, final String strHoldUntilComplete, final String strAllowBackorders,
243            final String strAllowSubstitutions, final String strAllowCombiningShipments, final String reference, final String freeOnBoardName,
244            final String workflowEntranceName, final Party createdByParty) {
245        var vendor = vendorLogic.getVendorByName(eea, vendorName, null, null);
246        var term = termName == null ? null : termLogic.getTermByName(eea, termName);
247        var freeOnBoard = freeOnBoardName == null ? null : freeOnBoardLogic.getFreeOnBoardByName(eea, freeOnBoardName);
248        Order order = null;
249
250        if(eea == null || !eea.hasExecutionErrors()) {
251            var holdUntilComplete = strHoldUntilComplete == null ? null : Boolean.valueOf(strHoldUntilComplete);
252            var allowBackorders = strAllowBackorders == null ? null : Boolean.valueOf(strAllowBackorders);
253            var allowSubstitutions = strAllowSubstitutions == null ? null : Boolean.valueOf(strAllowSubstitutions);
254            var allowCombiningShipments = strAllowCombiningShipments == null ? null : Boolean.valueOf(strAllowCombiningShipments);
255
256            order = createPurchaseOrder(session, eea, userVisit, vendor.getParty(), holdUntilComplete, allowBackorders,
257                allowSubstitutions, allowCombiningShipments, reference, term, freeOnBoard, workflowEntranceName,
258                createdByParty);
259        }
260
261        return order;
262    }
263
264    public boolean isOrderInWorkflowSteps(final ExecutionErrorAccumulator eea, final Order order, final String... workflowStepNames) {
265        return isOrderInWorkflowSteps(eea, getEntityInstanceByBaseEntity(order), workflowStepNames);
266    }
267
268    public boolean isOrderInWorkflowSteps(final ExecutionErrorAccumulator eea, final EntityInstance entityInstance, final String... workflowStepNames) {
269        return !workflowStepLogic.isEntityInWorkflowSteps(eea, PurchaseOrderStatusConstants.Workflow_PURCHASE_ORDER_STATUS, entityInstance,
270                workflowStepNames).isEmpty();
271    }
272
273    public Order getOrderByName(final ExecutionErrorAccumulator eea, final String orderName) {
274        return getOrderByName(eea, OrderTypes.PURCHASE_ORDER.name(), orderName);
275    }
276
277    public Order getOrderByNameForUpdate(final ExecutionErrorAccumulator eea, final String orderName) {
278        return getOrderByNameForUpdate(eea, OrderTypes.PURCHASE_ORDER.name(), orderName);
279    }
280
281    public OrderPriority getOrderPriorityByName(final ExecutionErrorAccumulator eea, final String orderPriorityName) {
282        return orderPriorityLogic.getOrderPriorityByName(eea, OrderTypes.PURCHASE_ORDER.name(), orderPriorityName);
283    }
284
285    public OrderPriority getOrderPriorityByNameForUpdate(final ExecutionErrorAccumulator eea, final String orderPriorityName) {
286        return orderPriorityLogic.getOrderPriorityByNameForUpdate(eea, OrderTypes.PURCHASE_ORDER.name(), orderPriorityName);
287    }
288
289    public PurchaseOrderStatusChoicesBean getPurchaseOrderStatusChoices(final String defaultOrderStatusChoice, final Language language, final boolean allowNullChoice,
290            final Order order, final PartyPK partyPK) {
291        var purchaseOrderStatusChoicesBean = new PurchaseOrderStatusChoicesBean();
292
293        if(order == null) {
294            workflowControl.getWorkflowEntranceChoices(purchaseOrderStatusChoicesBean, defaultOrderStatusChoice, language, allowNullChoice,
295                    workflowControl.getWorkflowByName(PurchaseOrderStatusConstants.Workflow_PURCHASE_ORDER_STATUS), partyPK);
296        } else {
297            var entityInstance = entityInstanceControl.getEntityInstanceByBasePK(order.getPrimaryKey());
298            var workflowEntityStatus = workflowControl.getWorkflowEntityStatusByEntityInstanceUsingNames(PurchaseOrderStatusConstants.Workflow_PURCHASE_ORDER_STATUS, entityInstance);
299
300            workflowControl.getWorkflowDestinationChoices(purchaseOrderStatusChoicesBean, defaultOrderStatusChoice, language, allowNullChoice, workflowEntityStatus.getWorkflowStep(), partyPK);
301        }
302
303        return purchaseOrderStatusChoicesBean;
304    }
305
306    public void setPurchaseOrderStatus(final Session session, final ExecutionErrorAccumulator eea, final Order order, final String orderStatusChoice, final PartyPK modifiedBy) {
307        var workflow = workflowLogic.getWorkflowByName(eea, PurchaseOrderStatusConstants.Workflow_PURCHASE_ORDER_STATUS);
308        var entityInstance = entityInstanceControl.getEntityInstanceByBasePK(order.getPrimaryKey());
309        var workflowEntityStatus = workflowControl.getWorkflowEntityStatusByEntityInstanceForUpdate(workflow, entityInstance);
310        var workflowDestination = orderStatusChoice == null? null: workflowControl.getWorkflowDestinationByName(workflowEntityStatus.getWorkflowStep(), orderStatusChoice);
311
312        if(workflowDestination != null || orderStatusChoice == null) {
313            var currentWorkflowStepName = workflowEntityStatus.getWorkflowStep().getLastDetail().getWorkflowStepName();
314            var map = workflowDestinationLogic.getWorkflowDestinationsAsMap(workflowDestination);
315            var handled = false;
316            Long triggerTime = null;
317
318            if(currentWorkflowStepName.equals(PurchaseOrderStatusConstants.WorkflowStep_ENTRY)) {
319                if(workflowDestinationLogic.workflowDestinationMapContainsStep(map, PurchaseOrderStatusConstants.Workflow_PURCHASE_ORDER_STATUS, PurchaseOrderStatusConstants.WorkflowStep_ENTRY_COMPLETE)) {
320                    // TODO: What happens moving from ENTRY to ENTRY_COMPLETE?
321                    handled = true;
322                }
323
324                if(workflowDestinationLogic.workflowDestinationMapContainsStep(map, PurchaseOrderStatusConstants.Workflow_PURCHASE_ORDER_STATUS, PurchaseOrderStatusConstants.WorkflowStep_CANCELED)) {
325                    if(!handled) {
326                        // TODO: What happens moving from ENTRY to CANCELED?
327                        // TODO: Purchase Order Lines need to be canceled as well.
328                        handled = true;
329                    } else {
330                        handleExecutionError(DuplicateHandlingInPurchaseOrderStatusTransitionException.class, eea, ExecutionErrors.DuplicateHandlingInPurchaseOrderStatusTransition.name(), orderStatusChoice);
331                    }
332                }
333            }
334
335            if(eea == null || !eea.hasExecutionErrors()) {
336                if(handled) {
337                    workflowControl.transitionEntityInWorkflow(eea, workflowEntityStatus, workflowDestination, triggerTime, modifiedBy);
338                } else {
339                    handleExecutionError(UnhandledPurchaseOrderStatusTransitionException.class, eea, ExecutionErrors.UnhandledPurchaseOrderStatusTransition.name(), orderStatusChoice);
340                }
341            }
342        } else {
343            handleExecutionError(UnknownPurchaseOrderStatusChoiceException.class, eea, ExecutionErrors.UnknownPurchaseOrderStatusChoice.name(), orderStatusChoice);
344        }
345    }
346
347    /** Check to see if an Order is available for modification, and if it isn't, send back an error.
348     *
349     * @param session Required.
350     * @param eea Required.
351     * @param order Required.
352     * @param modifiedBy Required.
353     */
354    public void checkOrderAvailableForModification(final Session session, final ExecutionErrorAccumulator eea, final Order order, final PartyPK modifiedBy) {
355        var workflowEntityStatus = workflowControl.getWorkflowEntityStatusByEntityInstanceForUpdateUsingNames(PurchaseOrderStatusConstants.Workflow_PURCHASE_ORDER_STATUS, getEntityInstanceByBaseEntity(order));
356        var workflowStepName = workflowEntityStatus.getWorkflowStep().getLastDetail().getWorkflowStepName();
357
358        if(!workflowStepName.equals(PurchaseOrderStatusConstants.WorkflowStep_ENTRY)) {
359            handleExecutionError(InvalidPurchaseOrderStatusException.class, eea, ExecutionErrors.InvalidPurchaseOrderStatus.name(), order.getLastDetail().getOrderName(), workflowStepName);
360        }
361    }
362
363    /** Find the BILL_TO Party for a given Order.
364     * 
365     * @param order Required.
366     * @return The Party used for the BILL_TO OrderRoleType. May be null.
367     */
368    public Party getOrderBillToParty(final Order order) {
369        var billToOrderRole = orderRoleControl.getOrderRoleByOrderAndOrderRoleTypeUsingNames(order, OrderRoleTypes.BILL_TO.name());
370        Party party = null;
371        
372        if(billToOrderRole != null) {
373            party = billToOrderRole.getParty();
374        }
375        
376        return party;
377    }
378    
379    /** Find the VendorType for a given Party.
380     * 
381     * @param party Optional.
382     * @return The VendorType for the Party. May be null.
383     */
384    public VendorType getVendorTypeFromParty(final Party party) {
385        var vendor = party == null ? null : vendorControl.getVendor(party);
386        VendorType vendorType = null;
387        
388        if(vendor != null) {
389            vendorType = vendor.getVendorType();
390        }
391        
392        return vendorType;
393    }
394    
395    /** Find the VendorType for the BILL_TO Party for a given Order.
396     * 
397     * @param order Required.
398     * @return The VendorType for the BILL_TO Party. May be null.
399     */
400    public VendorType getOrderBillToVendorType(final Order order) {
401        return getVendorTypeFromParty(getOrderBillToParty(order));
402    }
403    
404    /** Attempt to find a SHIP_TO Party for the Order. If none is found, and billToFallback is set to true, then
405     * attempt to find the BILL_TO Party. If a BILL_TO Party is found, copy it to the SHIP_TO.
406     * 
407     * @param order Required.
408     * @param billToFallback Required.
409     * @param createdBy Required if billToFallback is true.
410     * @return The Party that is to be used for the SHIP_TO OrderRoleType. May be null.
411     */
412    public Party getOrderShipToParty(final Order order, final boolean billToFallback, final BasePK createdBy) {
413        var shipToOrderRole = orderRoleControl.getOrderRoleByOrderAndOrderRoleTypeUsingNames(order, OrderRoleTypes.SHIP_TO.name());
414        
415        if(shipToOrderRole == null && billToFallback) {
416            shipToOrderRole = orderRoleControl.getOrderRoleByOrderAndOrderRoleTypeUsingNames(order, OrderRoleTypes.BILL_TO.name());
417            
418            if(shipToOrderRole != null) {
419                orderRoleControl.createOrderRoleUsingNames(order, shipToOrderRole.getParty(), OrderRoleTypes.SHIP_TO.name(), createdBy);
420            }
421        }
422        
423        return shipToOrderRole == null ? null : shipToOrderRole.getParty();
424    }
425    
426}