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