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