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