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.model.control.associate.server.logic.AssociateReferralLogic;
020import com.echothree.model.control.cancellationpolicy.common.CancellationKinds;
021import com.echothree.model.control.cancellationpolicy.server.logic.CancellationPolicyLogic;
022import com.echothree.model.control.inventory.common.exception.UnknownDefaultInventoryConditionException;
023import com.echothree.model.control.inventory.server.control.InventoryConditionControl;
024import com.echothree.model.control.inventory.server.logic.InventoryConditionLogic;
025import com.echothree.model.control.item.common.ItemPriceTypes;
026import com.echothree.model.control.item.common.exception.UnknownDefaultItemUnitOfMeasureTypeException;
027import com.echothree.model.control.item.common.workflow.ItemStatusConstants;
028import com.echothree.model.control.item.server.control.ItemControl;
029import com.echothree.model.control.item.server.logic.ItemLogic;
030import com.echothree.model.control.offer.common.exception.UnknownOfferItemPriceException;
031import com.echothree.model.control.offer.server.control.OfferItemControl;
032import com.echothree.model.control.offer.server.logic.OfferItemLogic;
033import com.echothree.model.control.offer.server.logic.SourceLogic;
034import com.echothree.model.control.order.common.OrderTypes;
035import com.echothree.model.control.order.server.logic.BaseOrderLineLogic;
036import com.echothree.model.control.returnpolicy.common.ReturnKinds;
037import com.echothree.model.control.returnpolicy.server.logic.ReturnPolicyLogic;
038import com.echothree.model.control.sales.common.exception.CurrentTimeAfterSalesOrderEndTimeException;
039import com.echothree.model.control.sales.common.exception.CurrentTimeBeforeSalesOrderStartTimeException;
040import com.echothree.model.control.sales.common.exception.ItemDiscontinuedException;
041import com.echothree.model.control.sales.common.exception.QuantityAboveMaximumItemUnitCustomerTypeLimitException;
042import com.echothree.model.control.sales.common.exception.QuantityAboveMaximumItemUnitLimitException;
043import com.echothree.model.control.sales.common.exception.QuantityBelowMinimumItemUnitCustomerTypeLimitException;
044import com.echothree.model.control.sales.common.exception.QuantityBelowMinimumItemUnitLimitException;
045import com.echothree.model.control.sales.common.exception.UnitAmountAboveMaximumItemUnitPriceLimitException;
046import com.echothree.model.control.sales.common.exception.UnitAmountAboveMaximumUnitPriceException;
047import com.echothree.model.control.sales.common.exception.UnitAmountBelowMinimumItemUnitPriceLimitException;
048import com.echothree.model.control.sales.common.exception.UnitAmountBelowMinimumUnitPriceException;
049import com.echothree.model.control.sales.common.exception.UnitAmountNotMultipleOfUnitPriceIncrementException;
050import com.echothree.model.control.sales.common.exception.UnitAmountRequiredException;
051import com.echothree.model.control.sales.server.control.SalesOrderControl;
052import com.echothree.model.control.uom.server.logic.UnitOfMeasureTypeLogic;
053import com.echothree.model.control.workflow.server.logic.WorkflowStepLogic;
054import com.echothree.model.data.associate.server.entity.AssociateReferral;
055import com.echothree.model.data.batch.server.entity.Batch;
056import com.echothree.model.data.cancellationpolicy.server.entity.CancellationPolicy;
057import com.echothree.model.data.contact.server.entity.PartyContactMechanism;
058import com.echothree.model.data.inventory.server.entity.InventoryCondition;
059import com.echothree.model.data.item.server.entity.Item;
060import com.echothree.model.data.offer.server.entity.Source;
061import com.echothree.model.data.order.server.entity.Order;
062import com.echothree.model.data.order.server.entity.OrderLine;
063import com.echothree.model.data.order.server.entity.OrderShipmentGroup;
064import com.echothree.model.data.party.server.entity.Party;
065import com.echothree.model.data.returnpolicy.server.entity.ReturnPolicy;
066import com.echothree.model.data.shipping.server.entity.ShippingMethod;
067import com.echothree.model.data.uom.server.entity.UnitOfMeasureType;
068import com.echothree.model.data.user.server.entity.UserVisit;
069import com.echothree.util.common.message.ExecutionErrors;
070import com.echothree.util.server.message.DummyExecutionErrorAccumulator;
071import com.echothree.util.server.message.ExecutionErrorAccumulator;
072import com.echothree.util.server.persistence.Session;
073import javax.enterprise.context.ApplicationScoped;
074import javax.enterprise.inject.spi.CDI;
075import javax.inject.Inject;
076
077@ApplicationScoped
078public class SalesOrderLineLogic
079        extends BaseOrderLineLogic {
080
081    @Inject
082    InventoryConditionControl inventoryConditionControl;
083
084    @Inject
085    ItemControl itemControl;
086
087    @Inject
088    OfferItemControl offerItemControl;
089
090    @Inject
091    SalesOrderControl salesOrderControl;
092
093    @Inject
094    AssociateReferralLogic associateReferralLogic;
095
096    @Inject
097    CancellationPolicyLogic cancellationPolicyLogic;
098
099    @Inject
100    InventoryConditionLogic inventoryConditionLogic;
101
102    @Inject
103    ItemLogic itemLogic;
104
105    @Inject
106    OfferItemLogic offerItemLogic;
107
108    @Inject
109    ReturnPolicyLogic returnPolicyLogic;
110
111    @Inject
112    SalesOrderLogic salesOrderLogic;
113
114    @Inject
115    SalesOrderShipmentGroupLogic salesOrderShipmentGroupLogic;
116
117    @Inject
118    SourceLogic sourceLogic;
119
120    @Inject
121    UnitOfMeasureTypeLogic unitOfMeasureTypeLogic;
122
123    @Inject
124    WorkflowStepLogic workflowStepLogic;
125
126    protected SalesOrderLineLogic() {
127        super();
128    }
129
130    public static SalesOrderLineLogic getInstance() {
131        return CDI.current().select(SalesOrderLineLogic.class).get();
132    }
133
134    /**
135     * Create a new Sales Order Line using appropriate defaults for Optional values when possible.
136     * 
137     * @param session Required.
138     * @param eea Optional.
139     * @param userVisit Required.
140     * @param order Optional.
141     * @param orderShipmentGroup Optional.
142     * @param orderShipmentGroupSequence Optional.
143     * @param orderLineSequence Optional.
144     * @param parentOrderLine Optional.
145     * @param partyContactMechanism Optional.
146     * @param shippingMethod Optional.
147     * @param item Required.
148     * @param inventoryCondition Optional.
149     * @param unitOfMeasureType Optional.
150     * @param quantity Required.
151     * @param unitAmount Optional for Items with a FIXED ItemPriceType, Required for VARIABLE.
152     * @param description Optional.
153     * @param taxable Optional.
154     * @param source Optional.
155     * @param associateReferral Optional.
156     * @param createdByParty Required.
157     * @return The newly created OrderLine, otherwise null if there was an error.
158     */
159    public OrderLine createSalesOrderLine(final Session session, final ExecutionErrorAccumulator eea, final UserVisit userVisit,
160            Order order, OrderShipmentGroup orderShipmentGroup, final Integer orderShipmentGroupSequence, Integer orderLineSequence,
161            final OrderLine parentOrderLine, PartyContactMechanism partyContactMechanism, ShippingMethod shippingMethod, final Item item,
162            InventoryCondition inventoryCondition, UnitOfMeasureType unitOfMeasureType, final Long quantity, Long unitAmount,
163            final String description, CancellationPolicy cancellationPolicy, ReturnPolicy returnPolicy, Boolean taxable, final Source source,
164            final AssociateReferral associateReferral, final Party createdByParty) {
165        var createdByPartyPK = createdByParty.getPrimaryKey();
166        OrderLine orderLine = null;
167
168        // Create a new Sales Order if there was not one supplied. Defaults will be used for nearly all options.
169        if(order == null) {
170            order = salesOrderLogic.createSalesOrder(session, eea, userVisit, (Batch)null, null, null,
171                    null, null, null, null, null, null, null, null, null, null, null, createdByParty);
172        }
173
174        salesOrderLogic.checkOrderAvailableForModification(session, eea, order, createdByPartyPK);
175
176        if(eea == null || !eea.hasExecutionErrors()) {
177            var orderDetail = order.getLastDetail();
178            var itemDetail = item.getLastDetail();
179            var itemDeliveryType = itemDetail.getItemDeliveryType();
180            var currency = orderDetail.getCurrency();
181            var customerType = salesOrderLogic.getOrderBillToCustomerType(order);
182
183            if(customerType != null && shippingMethod != null) {
184                salesOrderShipmentGroupLogic.checkCustomerTypeShippingMethod(eea, customerType, shippingMethod);
185            }
186
187            if(eea == null || !eea.hasExecutionErrors()) {
188                // ItemDeliveryType must be checked against the ContactMechanismType for the partyContactMechanism.
189
190                // Check to see if an orderShipmentGroup was supplied. If it wasn't, try to get a default one for this order and itemDeliveryType.
191                // If a default doesn't exist, then create one.
192                if(orderShipmentGroup == null) {
193                    var dummyExecutionErrorAccumulator = new DummyExecutionErrorAccumulator(); // No Execution Errors, don't throw Exceptions
194                    orderShipmentGroup = salesOrderShipmentGroupLogic.getDefaultOrderShipmentGroup(dummyExecutionErrorAccumulator,
195                            order, itemDeliveryType);
196
197                    if(orderShipmentGroup == null) {
198                        var holdUntilComplete = order.getLastDetail().getHoldUntilComplete();
199                        var orderShipToParty = salesOrderLogic.getOrderShipToParty(order, true, createdByPartyPK);
200
201                        // If partyContactMechanism is null, attempt to get from SHIP_TO party for the order.
202                        // If no SHIP_TO party exists, try to copy from the BILL_TO party.
203
204                        // TODO.
205
206                        // Select an appropriate partyContactMechanism for the itemDeliveryType.
207
208                        // TODO.
209
210                        orderShipmentGroup = salesOrderShipmentGroupLogic.createSalesOrderShipmentGroup(session, eea, order,
211                                orderShipmentGroupSequence, itemDeliveryType, true, partyContactMechanism,
212                                shippingMethod, holdUntilComplete, createdByPartyPK);
213                    } else {
214                        var orderShipmentGroupDetail = orderShipmentGroup.getLastDetail();
215
216                        partyContactMechanism = orderShipmentGroupDetail.getPartyContactMechanism();
217                        shippingMethod = orderShipmentGroupDetail.getShippingMethod();
218                    }
219                }
220
221                // If shippingMethod was specified, check to see if it can be used for this item and partyContactMechanism.
222
223                // Check InventoryCondition.
224                if(inventoryCondition == null) {
225                    inventoryCondition = inventoryConditionControl.getDefaultInventoryCondition();
226
227                    if(inventoryCondition == null) {
228                        handleExecutionError(UnknownDefaultInventoryConditionException.class, eea, ExecutionErrors.UnknownDefaultInventoryCondition.name());
229                    }
230                }
231
232                // Check UnitOfMeasureType.
233                if(unitOfMeasureType == null) {
234                    var itemUnitOfMeasureType = itemControl.getDefaultItemUnitOfMeasureType(item);
235
236                    if(itemUnitOfMeasureType == null) {
237                        handleExecutionError(UnknownDefaultItemUnitOfMeasureTypeException.class, eea, ExecutionErrors.UnknownDefaultItemUnitOfMeasureType.name());
238                    } else {
239                        unitOfMeasureType = itemUnitOfMeasureType.getUnitOfMeasureType();
240                    }
241                }
242
243                // Verify the OfferItem exists.
244                var offerUse = source == null ? null : source.getLastDetail().getOfferUse();
245                if(offerUse == null) {
246                    var salesOrder = salesOrderControl.getSalesOrder(order);
247
248                    offerUse = salesOrder.getOfferUse();
249                }
250
251                var offerItem = offerItemLogic.getOfferItem(eea, offerUse.getLastDetail().getOffer(), item);
252
253                // Verify unitAmount.
254                if(offerItem != null) {
255                    var offerItemPrice = offerItemControl.getOfferItemPrice(offerItem, inventoryCondition, unitOfMeasureType, currency);
256
257                    if(offerItemPrice == null) {
258                        handleExecutionError(UnknownOfferItemPriceException.class, eea, ExecutionErrors.UnknownOfferItemPrice.name());
259                    } else {
260                        var itemPriceTypeName = itemDetail.getItemPriceType().getItemPriceTypeName();
261
262                        if(itemPriceTypeName.equals(ItemPriceTypes.FIXED.name())) {
263                            // We'll accept the supplied unitAmount as long as it passes the limit checks later on. Any enforcement of
264                            // security should come in the UC.
265                            if(unitAmount == null) {
266                                var offerItemFixedPrice = offerItemControl.getOfferItemFixedPrice(offerItemPrice);
267
268                                unitAmount = offerItemFixedPrice.getUnitPrice();
269                            }
270                        } else if(itemPriceTypeName.equals(ItemPriceTypes.VARIABLE.name())) {
271                            if(unitAmount == null) {
272                                handleExecutionError(UnitAmountRequiredException.class, eea, ExecutionErrors.UnitAmountRequired.name());
273                            } else {
274                                var offerItemVariablePrice = offerItemControl.getOfferItemVariablePrice(offerItemPrice);
275
276                                if(unitAmount < offerItemVariablePrice.getMinimumUnitPrice()) {
277                                    handleExecutionError(UnitAmountBelowMinimumUnitPriceException.class, eea, ExecutionErrors.UnitAmountBelowMinimumUnitPrice.name());
278                                }
279
280                                if(unitAmount > offerItemVariablePrice.getMaximumUnitPrice()) {
281                                    handleExecutionError(UnitAmountAboveMaximumUnitPriceException.class, eea, ExecutionErrors.UnitAmountAboveMaximumUnitPrice.name());
282                                }
283
284                                if(unitAmount % offerItemVariablePrice.getUnitPriceIncrement() != 0) {
285                                    handleExecutionError(UnitAmountNotMultipleOfUnitPriceIncrementException.class, eea, ExecutionErrors.UnitAmountNotMultipleOfUnitPriceIncrement.name());
286                                }
287                            }
288                        } else {
289                            handleExecutionError(UnknownOfferItemPriceException.class, eea, ExecutionErrors.UnknownItemPriceType.name(), itemPriceTypeName);
290                        }
291                    }
292                }
293
294                // Check ItemUnitPriceLimits.
295                if(unitAmount != null) {
296                    var itemUnitPriceLimit = itemControl.getItemUnitPriceLimit(item, inventoryCondition, unitOfMeasureType, currency);
297
298                    // This isn't required. If it is missing, no check is performed.
299                    if(itemUnitPriceLimit != null) {
300                        var minimumUnitPrice = itemUnitPriceLimit.getMinimumUnitPrice();
301                        var maximumUnitPrice = itemUnitPriceLimit.getMaximumUnitPrice();
302
303                        if(minimumUnitPrice != null && unitAmount < minimumUnitPrice) {
304                            handleExecutionError(UnitAmountBelowMinimumItemUnitPriceLimitException.class, eea, ExecutionErrors.UnitAmountBelowMinimumItemUnitPriceLimit.name());
305                        }
306
307                        if(maximumUnitPrice != null && unitAmount > maximumUnitPrice) {
308                            handleExecutionError(UnitAmountAboveMaximumItemUnitPriceLimitException.class, eea, ExecutionErrors.UnitAmountAboveMaximumItemUnitPriceLimit.name());
309                        }
310                    }
311                }
312
313                // Check quantity being ordered and make sure that it's within acceptible limits. Both ItemUnitLimits and ItemUnitCustomerTypeLimits.
314                if(inventoryCondition != null && unitOfMeasureType != null) {
315                    var itemUnitLimit = itemControl.getItemUnitLimit(item, inventoryCondition, unitOfMeasureType);
316
317                    if(itemUnitLimit != null) {
318                        var minimumQuantity = itemUnitLimit.getMinimumQuantity();
319                        var maximumQuantity = itemUnitLimit.getMaximumQuantity();
320
321                        if(minimumQuantity != null && quantity < minimumQuantity) {
322                            handleExecutionError(QuantityBelowMinimumItemUnitLimitException.class, eea, ExecutionErrors.QuantityBelowMinimumItemUnitLimit.name());
323                        }
324
325                        if(maximumQuantity != null && quantity > maximumQuantity) {
326                            handleExecutionError(QuantityAboveMaximumItemUnitLimitException.class, eea, ExecutionErrors.QuantityAboveMaximumItemUnitLimit.name());
327                        }
328                    }
329
330                    if(customerType != null) {
331                        var itemUnitCustomerTypeLimit = itemControl.getItemUnitCustomerTypeLimit(item, inventoryCondition, unitOfMeasureType, customerType);
332
333                        if(itemUnitCustomerTypeLimit != null) {
334                            var minimumQuantity = itemUnitCustomerTypeLimit.getMinimumQuantity();
335                            var maximumQuantity = itemUnitCustomerTypeLimit.getMaximumQuantity();
336
337                            if(minimumQuantity != null && quantity < minimumQuantity) {
338                                handleExecutionError(QuantityBelowMinimumItemUnitCustomerTypeLimitException.class, eea, ExecutionErrors.QuantityBelowMinimumItemUnitCustomerTypeLimit.name());
339                            }
340
341                            if(maximumQuantity != null && quantity > maximumQuantity) {
342                                handleExecutionError(QuantityAboveMaximumItemUnitCustomerTypeLimitException.class, eea, ExecutionErrors.QuantityAboveMaximumItemUnitCustomerTypeLimit.name());
343                            }
344                        }
345                    }
346                }
347
348                // Check Item's SalesOrderStartTime and SalesOrderEndTime.
349                var salesOrderStartTime = itemDetail.getSalesOrderStartTime();
350                if(salesOrderStartTime != null && session.getStartTime() < salesOrderStartTime) {
351                    handleExecutionError(CurrentTimeBeforeSalesOrderStartTimeException.class, eea, ExecutionErrors.CurrentTimeBeforeSalesOrderStartTime.name());
352                }
353
354                var salesOrderEndTime = itemDetail.getSalesOrderEndTime();
355                if(salesOrderEndTime != null && session.getStartTime() > salesOrderEndTime) {
356                    handleExecutionError(CurrentTimeAfterSalesOrderEndTimeException.class, eea, ExecutionErrors.CurrentTimeAfterSalesOrderEndTime.name());
357                }
358
359                // Check Item's status.
360                if(!workflowStepLogic.isEntityInWorkflowSteps(eea, ItemStatusConstants.Workflow_ITEM_STATUS, item,
361                        ItemStatusConstants.WorkflowStep_ITEM_STATUS_DISCONTINUED).isEmpty()) {
362                    handleExecutionError(ItemDiscontinuedException.class, eea, ExecutionErrors.ItemDiscontinued.name(), item.getLastDetail().getItemName());
363                }
364
365                // Create the line.
366                if(eea == null || !eea.hasExecutionErrors()) {
367                    // If a specific CancellationPolicy was specified, use that. Otherwise, try to use the one for the Item.
368                    // If that's null, we'll leave it null for the OrderLine, which indicates that we should fall back to the
369                    // one on the Order if it's ever needed.
370                    if(cancellationPolicy == null) {
371                        cancellationPolicy = itemDetail.getCancellationPolicy();
372                    }
373
374                    // If a specific ReturnPolicy was specified, use that. Otherwise, try to use the one for the Item.
375                    // If that's null, we'll leave it null for the OrderLine, which indicates that we should fall back to the
376                    // one on the Order if it's ever needed.
377                    if(returnPolicy == null) {
378                        returnPolicy = itemDetail.getReturnPolicy();
379                    }
380
381                    // If there was no taxable flag passed in, then get the taxable value from the Item. If that is true,
382                    // the taxable flag from the Order will override it.
383                    if(taxable == null) {
384                        // taxable = itemDetail.getTaxable();
385                        taxable = true; // TODO: This needs to consider the GeoCode-aware taxing system.
386                        
387                        if(taxable) {
388                            taxable = orderDetail.getTaxable();
389                        }
390                    }
391
392                    orderLine = createOrderLine(session, eea, order, orderLineSequence, parentOrderLine, orderShipmentGroup, item, inventoryCondition,
393                            unitOfMeasureType, quantity, unitAmount, description, cancellationPolicy, returnPolicy, taxable, createdByPartyPK);
394
395                    if(eea == null || !eea.hasExecutionErrors()) {
396                        salesOrderControl.createSalesOrderLine(orderLine, offerUse, associateReferral, createdByPartyPK);
397                    }
398                }
399            }
400        }
401        
402        return orderLine;
403    }
404
405    public OrderLine createOrderLine(final Session session, final ExecutionErrorAccumulator eea, final UserVisit userVisit,
406            final String orderName, final String itemName, final String inventoryConditionName, final String cancellationPolicyName,
407            final String returnPolicyName, final String unitOfMeasureTypeName, final String sourceName,
408            final String strOrderLineSequence, final String strQuantity, final String strUnitAmount, final String description,
409            final String strTaxable, final Party createdByParty) {
410        var order = orderName == null ? null : salesOrderLogic.getOrderByName(eea, orderName);
411        var item = itemLogic.getItemByNameThenAlias(eea, itemName);
412        var inventoryCondition = inventoryConditionName == null ? null : inventoryConditionLogic.getInventoryConditionByName(eea, inventoryConditionName);
413        var cancellationPolicy = cancellationPolicyName == null ? null : cancellationPolicyLogic.getCancellationPolicyByName(eea, CancellationKinds.CUSTOMER_CANCELLATION.name(), cancellationPolicyName);
414        var returnPolicy = returnPolicyName == null ? null : returnPolicyLogic.getReturnPolicyByName(eea, ReturnKinds.CUSTOMER_RETURN.name(), returnPolicyName);
415        var source = sourceName == null ? null : sourceLogic.getSourceByName(eea, sourceName);
416        OrderLine orderLine = null;
417
418        if(eea == null || !eea.hasExecutionErrors()) {
419            var itemDetail = item.getLastDetail();
420            var unitOfMeasureKind = itemDetail.getUnitOfMeasureKind();
421            var unitOfMeasureType = unitOfMeasureTypeName == null ? null : unitOfMeasureTypeLogic.getUnitOfMeasureTypeByName(eea, unitOfMeasureKind, unitOfMeasureTypeName);
422
423            if(eea == null || !eea.hasExecutionErrors()) {
424                var orderLineSequence = strOrderLineSequence == null ? null : Integer.valueOf(strOrderLineSequence);
425                var quantity = Long.valueOf(strQuantity);
426                var unitAmount = strUnitAmount == null ? null : Long.valueOf(strUnitAmount);
427                var taxable = strTaxable == null ? null : Boolean.valueOf(strTaxable);
428                var associateReferral = associateReferralLogic.getAssociateReferral(session, userVisit);
429
430                orderLine = createSalesOrderLine(session, eea, userVisit, order, null,
431                        null, orderLineSequence, null, null, null, item, inventoryCondition, unitOfMeasureType, quantity,
432                        unitAmount, description, cancellationPolicy, returnPolicy, taxable, source, associateReferral,
433                        createdByParty);
434            }
435        }
436
437        return orderLine;
438    }
439
440    public OrderLine getOrderLineByName(final ExecutionErrorAccumulator eea, final String orderName, final String orderLineSequence) {
441        return getOrderLineByName(eea, OrderTypes.SALES_ORDER.name(), orderName, orderLineSequence);
442    }
443
444    public OrderLine getOrderLineByNameForUpdate(final ExecutionErrorAccumulator eea, final String orderName, final String orderLineSequence) {
445        return getOrderLineByNameForUpdate(eea, OrderTypes.SALES_ORDER.name(), orderName, orderLineSequence);
446    }
447
448}