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