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