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