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