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