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}