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