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}