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}