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}