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.content.server.logic; 018 019import com.echothree.model.control.content.common.exception.DuplicateContentCategoryItemException; 020import com.echothree.model.control.content.common.exception.MalformedUrlException; 021import com.echothree.model.control.content.common.exception.UnknownContentCollectionNameException; 022import com.echothree.model.control.content.common.exception.UnknownContentWebAddressNameException; 023import com.echothree.model.control.content.server.control.ContentControl; 024import com.echothree.model.control.item.common.ItemPriceTypes; 025import com.echothree.model.control.offer.server.control.OfferItemControl; 026import com.echothree.model.control.offer.server.control.OfferUseControl; 027import com.echothree.model.control.offer.server.logic.OfferItemLogic; 028import com.echothree.model.data.accounting.server.entity.Currency; 029import com.echothree.model.data.content.server.entity.ContentCatalog; 030import com.echothree.model.data.content.server.entity.ContentCatalogItem; 031import com.echothree.model.data.content.server.entity.ContentCategory; 032import com.echothree.model.data.content.server.entity.ContentCategoryItem; 033import com.echothree.model.data.content.server.entity.ContentCollection; 034import com.echothree.model.data.content.server.value.ContentCategoryItemValue; 035import com.echothree.model.data.inventory.server.entity.InventoryCondition; 036import com.echothree.model.data.item.server.entity.Item; 037import com.echothree.model.data.offer.server.entity.Offer; 038import com.echothree.model.data.offer.server.entity.OfferItemPrice; 039import com.echothree.model.data.offer.server.entity.OfferItemVariablePrice; 040import com.echothree.model.data.offer.server.entity.OfferUse; 041import com.echothree.model.data.uom.server.entity.UnitOfMeasureType; 042import com.echothree.util.common.message.ExecutionErrors; 043import com.echothree.util.common.persistence.BasePK; 044import com.echothree.util.server.control.BaseLogic; 045import com.echothree.util.server.message.ExecutionErrorAccumulator; 046import com.echothree.util.server.persistence.EntityPermission; 047import java.net.MalformedURLException; 048import java.net.URI; 049import java.net.URISyntaxException; 050import java.util.ArrayList; 051import java.util.HashSet; 052import java.util.List; 053import java.util.Set; 054import javax.enterprise.context.ApplicationScoped; 055import javax.enterprise.inject.spi.CDI; 056import javax.inject.Inject; 057 058@ApplicationScoped 059public class ContentLogic 060 extends BaseLogic { 061 062 @Inject 063 protected ContentControl contentControl; 064 065 @Inject 066 protected OfferUseControl offerUseControl; 067 068 @Inject 069 protected OfferItemControl offerItemControl; 070 071 protected ContentLogic() { 072 super(); 073 } 074 075 public static ContentLogic getInstance() { 076 return CDI.current().select(ContentLogic.class).get(); 077 } 078 079 public ContentCollection getContentCollectionByName(final ExecutionErrorAccumulator eea, final String contentCollectionName, 080 final EntityPermission entityPermission) { 081 var contentCollection = contentControl.getContentCollectionByName(contentCollectionName, entityPermission); 082 083 if(contentCollection == null) { 084 handleExecutionError(UnknownContentCollectionNameException.class, eea, ExecutionErrors.UnknownContentCollectionName.name(), 085 contentCollectionName); 086 } 087 088 return contentCollection; 089 } 090 091 public ContentCollection getContentCollectionByName(final ExecutionErrorAccumulator eea, final String contentCollectionName) { 092 return getContentCollectionByName(eea, contentCollectionName, EntityPermission.READ_ONLY); 093 } 094 095 public ContentCollection getContentCollectionByNameForUpdate(final ExecutionErrorAccumulator eea, final String contentCollectionName) { 096 return getContentCollectionByName(eea, contentCollectionName, EntityPermission.READ_WRITE); 097 } 098 099 public void checkReferrer(final ExecutionErrorAccumulator eea, final String referrer) { 100 // A null referrer is considered valid. 101 if(referrer != null) { 102 try { 103 var uri = new URI(referrer); 104 var url = uri.toURL(); 105 var contentWebAddressName = url.getHost(); 106 107 if(!contentControl.validContentWebAddressName(contentWebAddressName)) { 108 handleExecutionError(UnknownContentWebAddressNameException.class, eea, ExecutionErrors.UnknownContentWebAddressName.name(), contentWebAddressName); 109 } 110 } catch(URISyntaxException | MalformedURLException ex) { 111 handleExecutionError(MalformedUrlException.class, eea, ExecutionErrors.MalformedUrl.name(), referrer); 112 } 113 } 114 } 115 116 /** Find a ContentCategory where a DefaultOfferUse is not null by following parents. */ 117 public ContentCategory getParentContentCategoryByNonNullDefaultOfferUse(final ContentCategory startingContentCategory) { 118 var currentContentCategory = startingContentCategory; 119 120 // Keep checking from the currentContentCategory through all its parents, until a DefaultOfferUse has been 121 // found. If all else fails, the "ROOT" one will contain the same DefaultOfferUse as the ContentCatalog. 122 while(currentContentCategory.getLastDetail().getDefaultOfferUse() == null) { 123 currentContentCategory = currentContentCategory.getLastDetail().getParentContentCategory(); 124 } 125 126 return currentContentCategory; 127 } 128 129 /** For a given ContentCategory, find the OfferUse that is used for items added to it. */ 130 public OfferUse getContentCategoryDefaultOfferUse(final ContentCategory contentCategory) { 131 return getParentContentCategoryByNonNullDefaultOfferUse(contentCategory).getLastDetail().getDefaultOfferUse(); 132 } 133 134 /** Get a Set of all OfferUses in a ContentCatalog for a given ContentCatalogItem. */ 135 private Set<OfferUse> getOfferUsesByContentCatalogItem(final ContentCatalogItem contentCatalogItem) { 136 Set<OfferUse> offerUses = new HashSet<>(); 137 var contentCategoryItems = contentControl.getContentCategoryItemsByContentCatalogItem(contentCatalogItem); 138 139 contentCategoryItems.forEach((contentCategoryItem) -> { 140 offerUses.add(getContentCategoryDefaultOfferUse(contentCategoryItem.getContentCategory())); 141 }); 142 143 return offerUses; 144 } 145 146 /** Checks across all Offers utilized in a ContentCatalog, and finds the lowest price that the ContentCatalogItem is offered for. */ 147 private Long getLowestUnitPrice(final ContentCatalogItem contentCatalogItem) { 148 var offerUses = getOfferUsesByContentCatalogItem(contentCatalogItem); 149 long unitPrice = Integer.MAX_VALUE; 150 151 for(var offerUse : offerUses) { 152 var offerItem = offerItemControl.getOfferItem(offerUse.getLastDetail().getOffer(), contentCatalogItem.getItem()); 153 var offerItemPrice = offerItemControl.getOfferItemPrice(offerItem, contentCatalogItem.getInventoryCondition(), 154 contentCatalogItem.getUnitOfMeasureType(), contentCatalogItem.getCurrency()); 155 var offerItemFixedPrice = offerItemControl.getOfferItemFixedPrice(offerItemPrice); 156 157 unitPrice = Math.min(unitPrice, offerItemFixedPrice.getUnitPrice()); 158 } 159 160 return unitPrice; 161 } 162 163 /** Checks across all Offers utilized in a ContentCatalog, and finds an OfferItemVariablePrice for this ContentCatalogItem. All 164 OfferItemVariablePrices should be the same, so we'll just use the first one that's found. */ 165 private OfferItemVariablePrice getOfferItemVariablePrice(final ContentCatalogItem contentCatalogItem) { 166 var offerUses = getOfferUsesByContentCatalogItem(contentCatalogItem); 167 var offerUsesIterator = offerUses.iterator(); 168 var offerUse = offerUsesIterator.hasNext() ? offerUsesIterator.next() : null; 169 var offerItem = offerUse == null ? null : offerItemControl.getOfferItem(offerUse.getLastDetail().getOffer(), contentCatalogItem.getItem()); 170 var offerItemPrice = offerItem == null ? null : offerItemControl.getOfferItemPrice(offerItem, contentCatalogItem.getInventoryCondition(), 171 contentCatalogItem.getUnitOfMeasureType(), contentCatalogItem.getCurrency()); 172 var offerItemVariablePrice = offerItemPrice == null ? null : offerItemControl.getOfferItemVariablePrice(offerItemPrice); 173 174 return offerItemVariablePrice; 175 } 176 177 /** Check to make sure the ContentCatalogItem has the lower price possible in the ContentCatalog, or if it is no longer used, delete it. */ 178 public void updateContentCatalogItemPriceByContentCatalogItem(final ContentCatalogItem contentCatalogItem, final BasePK updatedBy) { 179 // Check to see if it still exists in any other categories, and delete ContentCatalogItem if it doesn't. 180 if(contentControl.countContentCategoryItemsByContentCatalogItem(contentCatalogItem) == 0) { 181 contentControl.deleteContentCatalogItem(contentCatalogItem, updatedBy); 182 } else { 183 var item = contentCatalogItem.getItem(); 184 var itemPriceTypeName = item.getLastDetail().getItemPriceType().getItemPriceTypeName(); 185 186 if(itemPriceTypeName.equals(ItemPriceTypes.FIXED.name())) { 187 var unitPrice = getLowestUnitPrice(contentCatalogItem); 188 var contentCatalogItemFixedPrice = contentControl.getContentCatalogItemFixedPriceForUpdate(contentCatalogItem); 189 190 if(contentCatalogItemFixedPrice == null) { 191 contentControl.createContentCatalogItemFixedPrice(contentCatalogItem, unitPrice, updatedBy); 192 } else { 193 var contentCatalogItemFixedPriceValue = contentControl.getContentCatalogItemFixedPriceValue(contentCatalogItemFixedPrice); 194 195 contentCatalogItemFixedPriceValue.setUnitPrice(unitPrice); 196 197 contentControl.updateContentCatalogItemFixedPriceFromValue(contentCatalogItemFixedPriceValue, updatedBy); 198 } 199 } else if(itemPriceTypeName.equals(ItemPriceTypes.VARIABLE.name())) { 200 var contentCatalogItemVariablePrice = contentControl.getContentCatalogItemVariablePriceForUpdate(contentCatalogItem); 201 var offerItemVariablePrice = getOfferItemVariablePrice(contentCatalogItem); 202 var minimumUnitPrice = offerItemVariablePrice.getMinimumUnitPrice(); 203 var maximumUnitPrice = offerItemVariablePrice.getMaximumUnitPrice(); 204 var unitPriceIncrement = offerItemVariablePrice.getUnitPriceIncrement(); 205 206 if(contentCatalogItemVariablePrice == null) { 207 contentControl.createContentCatalogItemVariablePrice(contentCatalogItem, minimumUnitPrice, maximumUnitPrice, unitPriceIncrement, updatedBy); 208 } else if(!minimumUnitPrice.equals(contentCatalogItemVariablePrice.getMinimumUnitPrice()) 209 || !maximumUnitPrice.equals(contentCatalogItemVariablePrice.getMaximumUnitPrice()) 210 || !unitPriceIncrement.equals(contentCatalogItemVariablePrice.getUnitPriceIncrement())) { 211 var contentCatalogItemVariablePriceValue = contentControl.getContentCatalogItemVariablePriceValue(contentCatalogItemVariablePrice); 212 213 contentCatalogItemVariablePriceValue.setMinimumUnitPrice(minimumUnitPrice); 214 contentCatalogItemVariablePriceValue.setMaximumUnitPrice(maximumUnitPrice); 215 contentCatalogItemVariablePriceValue.setUnitPriceIncrement(unitPriceIncrement); 216 217 contentControl.updateContentCatalogItemVariablePriceFromValue(contentCatalogItemVariablePriceValue, updatedBy); 218 } 219 } 220 } 221 } 222 223 /** For all ContentCatalogItem in the Set, verify they have the lower price possible in their ContentCatalog. */ 224 public void updateContentCatalogItemPrices(final Iterable<ContentCatalogItem> contentCatalogItems, final BasePK updatedBy) { 225 for(var contentCatalogItem : contentCatalogItems) { 226 updateContentCatalogItemPriceByContentCatalogItem(contentCatalogItem, updatedBy); 227 } 228 } 229 230 private Set<ContentCatalog> getContentCatalogsByOfferUses(final Iterable<OfferUse> offerUses) { 231 Set<ContentCatalog> contentCatalogs = new HashSet<>(); 232 233 for(var offerUse : offerUses) { 234 var contentCategories = contentControl.getContentCategoriesByDefaultOfferUse(offerUse); 235 236 contentCategories.forEach((contentCategory) -> { 237 contentCatalogs.add(contentCategory.getLastDetail().getContentCatalog()); 238 }); 239 } 240 241 return contentCatalogs; 242 } 243 244 private Set<ContentCatalogItem> getContentCatalogItemsByContentCatalogs(final Iterable<ContentCatalog> contentCatalogs, final OfferItemPrice offerItemPrice) { 245 Set<ContentCatalogItem> contentCatalogItems = new HashSet<>(); 246 247 for(var contentCatalog : contentCatalogs) { 248 var contentCatalogItem = contentControl.getContentCatalogItem(contentCatalog, offerItemPrice.getOfferItem().getItem(), 249 offerItemPrice.getInventoryCondition(), offerItemPrice.getUnitOfMeasureType(), offerItemPrice.getCurrency()); 250 251 if(contentCatalogItem != null) { 252 contentCatalogItems.add(contentCatalogItem); 253 } 254 } 255 256 return contentCatalogItems; 257 } 258 259 public void updateContentCatalogItemPricesByOfferItemPrice(final OfferItemPrice offerItemPrice, final BasePK updatedBy) { 260 Iterable<OfferUse> offerUses = offerUseControl.getOfferUsesByOffer(offerItemPrice.getOfferItem().getOffer()); 261 Iterable<ContentCatalog> contentCatalogs = getContentCatalogsByOfferUses(offerUses); 262 Iterable<ContentCatalogItem> contentCatalogItems = getContentCatalogItemsByContentCatalogs(contentCatalogs, offerItemPrice); 263 264 updateContentCatalogItemPrices(contentCatalogItems, updatedBy); 265 } 266 267 private void addContentCatalogItems(final Set<ContentCatalogItem> contentCatalogItems, final ContentCategory parentContentCategory) { 268 Iterable<ContentCategoryItem> contentCategoryItems = contentControl.getContentCategoryItemsByContentCategory(parentContentCategory); 269 Iterable<ContentCategory> childContentCategories = contentControl.getContentCategoriesByParentContentCategory(parentContentCategory); 270 271 for(var contentCategoryItem : contentCategoryItems) { 272 contentCatalogItems.add(contentCategoryItem.getContentCatalogItem()); 273 } 274 275 for(var childContentCategory : childContentCategories) { 276 if(childContentCategory.getLastDetail().getDefaultOfferUse() == null) { 277 addContentCatalogItems(contentCatalogItems, childContentCategory); 278 } 279 } 280 } 281 282 /** Call when a ContentCategory is updated, and the DefaultOfferUse was modified. */ 283 public void updateContentCatalogItemPricesByContentCategory(final ContentCategory contentCategory, final BasePK updatedBy) { 284 Set<ContentCatalogItem> contentCatalogItems = new HashSet<>(); 285 286 // All ContentCatalogItems from the highest ContentCategory with a non-null DefaultOfferUse on down. 287 addContentCatalogItems(contentCatalogItems, getParentContentCategoryByNonNullDefaultOfferUse(contentCategory)); 288 289 // Check Prices for all of them. 290 updateContentCatalogItemPrices(contentCatalogItems, updatedBy); 291 } 292 293 public ContentCategoryItem createContentCategoryItem(final ExecutionErrorAccumulator eea, final ContentCategory contentCategory, final Item item, 294 final InventoryCondition inventoryCondition, final UnitOfMeasureType unitOfMeasureType, final Currency currency, final Boolean isDefault, 295 final Integer sortOrder, final BasePK createdBy) { 296 ContentCategoryItem contentCategoryItem = null; 297 var offerUse = getContentCategoryDefaultOfferUse(contentCategory); 298 var offer = offerUse.getLastDetail().getOffer(); 299 300 if(OfferItemLogic.getInstance().getOfferItemPrice(eea, offer, item, inventoryCondition, unitOfMeasureType, currency) != null) { 301 var contentCategoryDetail = contentCategory.getLastDetail(); 302 var contentCatalog = contentCategoryDetail.getContentCatalog(); 303 var contentCatalogItem = contentControl.getContentCatalogItem(contentCatalog, item, inventoryCondition, unitOfMeasureType, currency); 304 305 if(contentCatalogItem == null) { 306 contentCatalogItem = contentControl.createContentCatalogItem(contentCatalog, item, inventoryCondition, unitOfMeasureType, currency, createdBy); 307 } 308 309 if(eea == null || !eea.hasExecutionErrors()) { 310 contentCategoryItem = contentControl.getContentCategoryItem(contentCategory, contentCatalogItem); 311 312 if(contentCategoryItem == null) { 313 contentCategoryItem = contentControl.createContentCategoryItem(contentCategory, contentCatalogItem, isDefault, sortOrder, createdBy); 314 315 updateContentCatalogItemPriceByContentCatalogItem(contentCatalogItem, createdBy); 316 } else { 317 var contentCatalogDetail = contentCatalog.getLastDetail(); 318 319 handleExecutionError(DuplicateContentCategoryItemException.class, eea, ExecutionErrors.DuplicateContentCategoryItem.name(), 320 contentCatalogDetail.getContentCollection().getLastDetail().getContentCollectionName(), contentCatalogDetail.getContentCatalogName(), 321 contentCategoryDetail.getContentCategoryName(), item.getLastDetail().getItemName(), 322 inventoryCondition.getLastDetail().getInventoryConditionName(), unitOfMeasureType.getLastDetail().getUnitOfMeasureTypeName(), 323 currency.getCurrencyIsoName()); 324 } 325 } 326 } 327 328 return contentCategoryItem; 329 } 330 331 public void updateContentCategoryItemFromValue(final ContentCategoryItemValue contentCategoryItemValue, final BasePK updatedBy) { 332 contentControl.updateContentCategoryItemFromValue(contentCategoryItemValue, updatedBy); 333 } 334 335 public void deleteContentCategoryItem(final ContentCategoryItem contentCategoryItem, final BasePK deletedBy) { 336 var contentCatalogItem = contentCategoryItem.getContentCatalogItemForUpdate(); 337 338 contentControl.deleteContentCategoryItem(contentCategoryItem, deletedBy); 339 340 updateContentCatalogItemPriceByContentCatalogItem(contentCatalogItem, deletedBy); 341 } 342 343 private void getChildContentCategoriesByContentCategory(final ContentControl contentControl, final List<ContentCategory> contentCategories, 344 final ContentCategory contentCategory) { 345 contentCategories.add(contentCategory); 346 347 contentControl.getContentCategoriesByParentContentCategory(contentCategory).stream().filter((childContentCategory) -> (childContentCategory.getLastDetail().getDefaultOfferUse() == null)).forEach((childContentCategory) -> { 348 getChildContentCategoriesByContentCategory(contentControl, contentCategories, childContentCategory); 349 }); 350 } 351 352 /** Return a List of all ContentCategories, including the one passed to it, that inherit the DefaultOfferUse from it. */ 353 private List<ContentCategory> getChildContentCategoriesByContentCategory(final ContentCategory contentCategory) { 354 List<ContentCategory> contentCategories = new ArrayList<>(); 355 356 getChildContentCategoriesByContentCategory(contentControl, contentCategories, contentCategory); 357 358 return contentCategories; 359 } 360 361 private Set<ContentCategory> getContentCategoriesByOffer(Offer offer) { 362 Set<ContentCategory> contentCategories = new HashSet<>(); 363 364 offerUseControl.getOfferUsesByOffer(offer).forEach((offerUse) -> { 365 contentCategories.addAll(contentControl.getContentCategoriesByDefaultOfferUse(offerUse)); 366 }); 367 368 return contentCategories; 369 } 370 371 private Set<ContentCatalog> getContentCatalogsFromContentCategories(Iterable<ContentCategory> contentCategories) { 372 Set<ContentCatalog> contentCatalogs = new HashSet<>(); 373 374 for(var contentCategory : contentCategories) { 375 contentCatalogs.add(contentCategory.getLastDetail().getContentCatalog()); 376 } 377 378 return contentCatalogs; 379 } 380 381 private Set<ContentCatalogItem> getPossibleContentCatalogItemsByOfferItemPrice(Iterable<ContentCatalog> contentCatalogs, OfferItemPrice offerItemPrice) { 382 Set<ContentCatalogItem> contentCatalogItems = new HashSet<>(); 383 384 for(var contentCatalog : contentCatalogs) { 385 var contentCatalogItem = contentControl.getContentCatalogItem(contentCatalog, offerItemPrice.getOfferItem().getItem(), 386 offerItemPrice.getInventoryCondition(), offerItemPrice.getUnitOfMeasureType(), offerItemPrice.getCurrency()); 387 388 if(contentCatalogItem != null) { 389 contentCatalogItems.add(contentCatalogItem); 390 } 391 } 392 393 return contentCatalogItems; 394 } 395 396 public void deleteContentCategoryItemByOfferItemPrice(final OfferItemPrice offerItemPrice, final BasePK deletedBy) { 397 var offerItem = offerItemPrice.getOfferItem(); 398 399 // Create a list of all ContentCategories whose DefaultOfferUse is one that could be form this OfferItemPrice. 400 Iterable<ContentCategory> contentCategories = getContentCategoriesByOffer(offerItem.getOffer()); 401 402 // Put together a list of all ContentCatalogItems that could be this OfferItemPrice. 403 Iterable<ContentCatalog> contentCatalogs = getContentCatalogsFromContentCategories(contentCategories); 404 Iterable<ContentCatalogItem> contentCatalogItems = getPossibleContentCatalogItemsByOfferItemPrice(contentCatalogs, offerItemPrice); 405 406 // Go through the list of all the ContentCategories... 407 for(var contentCategory : contentCategories) { 408 List<ContentCategory> contentCategoriesToCheck = null; 409 var contentCatalog = contentCategory.getLastDetail().getContentCatalog(); 410 411 // And the list of all the ContentCatalogItems... 412 for(var contentCatalogItem : contentCatalogItems) { 413 // And where the ContentCatalogItem's Catalog is the one from the current ContentCategory... 414 if(contentCatalogItem.getContentCatalog().equals(contentCatalog)) { 415 if(contentCategoriesToCheck == null) { 416 // Get a list of all the possible ContentCategories that might contain the ContentCatalogItem. 417 contentCategoriesToCheck = getChildContentCategoriesByContentCategory(contentCategory); 418 } 419 420 for(var contentCategoryToCheck : contentCategoriesToCheck) { 421 var contentCategoryItem = contentControl.getContentCategoryItemForUpdate(contentCategoryToCheck, contentCatalogItem); 422 423 // If a ContentCategoryItem was found, delete it. 424 if(contentCategoryItem != null) { 425 deleteContentCategoryItem(contentCategoryItem, deletedBy); 426 } 427 } 428 } 429 } 430 } 431 } 432 433}