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