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