Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance. Project price only 1 $
You can buy this project and download/modify it how often you want.
/*
*
* _ _ ____ ____
* _____ _(_) |_ __ _| _ \| __ )
* / _ \ \ / / | __/ _` | | | | _ \
* | __/\ V /| | || (_| | |_| | |_) |
* \___| \_/ |_|\__\__,_|____/|____/
*
* Copyright (c) 2023-2024
*
* Licensed under the Business Source License, Version 1.1 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://github.com/FgForrest/evitaDB/blob/master/LICENSE
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.evitadb.api.requestResponse.data;
import io.evitadb.api.exception.ContextMissingException;
import io.evitadb.api.exception.EntityHasNoPricesException;
import io.evitadb.api.exception.UnexpectedResultCountException;
import io.evitadb.api.query.Query;
import io.evitadb.api.query.require.QueryPriceMode;
import io.evitadb.api.requestResponse.data.structure.Entity;
import io.evitadb.api.requestResponse.data.structure.Price;
import io.evitadb.api.requestResponse.data.structure.Price.PriceKey;
import io.evitadb.exception.GenericEvitaInternalError;
import io.evitadb.utils.Assert;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.OffsetDateTime;
import java.util.*;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static io.evitadb.utils.CollectionUtils.createHashMap;
import static java.util.Optional.empty;
import static java.util.Optional.of;
import static java.util.Optional.ofNullable;
/**
* Contract for classes that allow reading information about prices in {@link Entity} instance.
*
* @author Jan Novotný ([email protected]), FG Forrest a.s. (c) 2021
*/
public interface PricesContract extends Versioned, Serializable {
AccompanyingPrice[] NO_ACCOMPANYING_PRICES = new AccompanyingPrice[0];
/**
* Computes a price for which the entity should be sold. Only indexed prices in requested currency, valid
* at the passed moment are taken into an account. Prices are also limited by the passed set of price lists and
* the first price found in the order of the requested price list ids will be returned.
*/
@Nonnull
static Optional computePriceForSale(
@Nonnull Collection entityPrices,
@Nonnull PriceInnerRecordHandling innerRecordHandling,
@Nonnull Currency currency,
@Nullable OffsetDateTime atTheMoment,
@Nonnull String[] priceListPriority,
@Nonnull Predicate filterPredicate
) {
return computePriceForSale(
entityPrices, innerRecordHandling, currency, atTheMoment, priceListPriority,
filterPredicate, NO_ACCOMPANYING_PRICES
)
.map(PriceForSaleWithAccompanyingPrices::priceForSale);
}
/**
* Computes a price for which the entity should be sold. Only indexed prices in requested currency, valid
* at the passed moment are taken into an account. Prices are also limited by the passed set of price lists and
* the first price found in the order of the requested price list ids will be returned.
*/
@Nonnull
static Optional computePriceForSale(
@Nonnull Collection entityPrices,
@Nonnull PriceInnerRecordHandling innerRecordHandling,
@Nonnull Currency currency,
@Nullable OffsetDateTime atTheMoment,
@Nonnull String[] priceListPriority,
@Nonnull Predicate filterPredicate,
@Nonnull AccompanyingPrice[] accompanyingPrices
) {
if (entityPrices.isEmpty()) {
return empty();
}
final Map priorityIndex = getPriceListPriorityIndex(priceListPriority);
final Stream pricesStream = entityPrices
.stream()
.filter(PriceContract::exists)
.filter(it -> currency.equals(it.currency()))
.filter(it -> ofNullable(atTheMoment).map(mmt -> it.validity() == null || it.validity().isValidFor(mmt)).orElse(true));
switch (innerRecordHandling) {
case NONE -> {
final Optional priceForSale = pricesStream
.filter(PriceContract::sellable)
.filter(it -> priorityIndex.containsKey(it.priceList()))
.min(Comparator.comparing(o -> priorityIndex.get(o.priceList())))
.filter(filterPredicate);
return priceForSale
.map(
priceContract -> new PriceForSaleWithAccompanyingPrices(
priceContract,
calculateAccompanyingPricesForNoneInnerRecordHandling(
entityPrices, currency, atTheMoment, accompanyingPrices
)
)
);
}
case LOWEST_PRICE -> {
final Map> pricesByInnerId = pricesStream
.collect(Collectors.groupingBy(it -> ofNullable(it.innerRecordId()).orElse(0)));
final Optional priceForSale = pricesByInnerId
.values()
.stream()
.map(prices -> prices.stream()
.filter(PriceContract::sellable)
.filter(it -> priorityIndex.containsKey(it.priceList()))
.min(Comparator.comparing(o -> priorityIndex.get(o.priceList())))
.orElse(null))
.filter(Objects::nonNull)
.filter(filterPredicate)
.min(Comparator.comparing(PriceContract::priceWithTax));
return priceForSale
.map(
priceContract -> new PriceForSaleWithAccompanyingPrices(
priceContract,
calculateAccompanyingPricesForLowestPriceInnerRecordHandling(
pricesByInnerId.get(priceContract.innerRecordId()), accompanyingPrices
)
)
);
}
case SUM -> {
final List pricesToSum = pricesStream
.collect(Collectors.groupingBy(it -> ofNullable(it.innerRecordId()).orElse(0)))
.values()
.stream()
.map(prices -> prices.stream()
.filter(PriceContract::sellable)
.filter(it -> priorityIndex.containsKey(it.priceList()))
.min(Comparator.comparing(o -> priorityIndex.get(o.priceList())))
.orElse(null))
.filter(Objects::nonNull)
.toList();
if (pricesToSum.isEmpty()) {
return empty();
} else {
final PriceContract priceForSale = calculateSumPrice(pricesToSum);
return filterPredicate.test(priceForSale) ?
of(
new PriceForSaleWithAccompanyingPrices(
priceForSale,
calculateAccompanyingPricesForSumInnerRecordHandling(
entityPrices, priorityIndex.keySet(), currency, atTheMoment, accompanyingPrices
)
)
) : empty();
}
}
default ->
throw new GenericEvitaInternalError("Unknown price inner record handling mode: " + innerRecordHandling);
}
}
/**
* Returns true if single price differs between first and second instance.
*/
static boolean anyPriceDifferBetween(@Nonnull PricesContract first, @Nonnull PricesContract second) {
final Collection thisValues = first.pricesAvailable() ? first.getPrices() : Collections.emptyList();
final Collection otherValues = second.pricesAvailable() ? second.getPrices() : Collections.emptyList();
if (thisValues.size() != otherValues.size()) {
return true;
} else {
return thisValues
.stream()
.anyMatch(it -> it.differsFrom(second.getPrice(it.priceId(), it.priceList(), it.currency()).orElse(null)));
}
}
/**
* Method will calculate all required accompanying prices for the entity. The method is used when the entity
* has no inner record handling.
*
* @param entityPrices source collection of all entity prices
* @param currency currency used for price for sale calculation
* @param atTheMoment moment used for price for sale calculation
* @param accompanyingPrices array of requirements for accompanying prices
* @return map of calculated accompanying prices
*/
@Nonnull
private static Map> calculateAccompanyingPricesForNoneInnerRecordHandling(
@Nonnull Collection entityPrices,
@Nonnull Currency currency,
@Nullable OffsetDateTime atTheMoment,
@Nonnull AccompanyingPrice[] accompanyingPrices
) {
if (accompanyingPrices.length > 0) {
final List accompanyingPriceBaseCollection = entityPrices
.stream()
.filter(PriceContract::exists)
.filter(it -> currency.equals(it.currency()))
.filter(it -> ofNullable(atTheMoment).map(mmt -> it.validity() == null || it.validity().isValidFor(mmt)).orElse(true))
.toList();
return Arrays.stream(accompanyingPrices)
.collect(
Collectors.toMap(
AccompanyingPrice::priceName,
accompanyingPrice -> {
final Map accompanyingPriorityIndex = getPriceListPriorityIndex(
accompanyingPrice.priceListPriority()
);
return accompanyingPriceBaseCollection
.stream()
.filter(it -> accompanyingPriorityIndex.containsKey(it.priceList()))
.min(Comparator.comparing(o -> accompanyingPriorityIndex.get(o.priceList())));
}
)
);
} else {
return Collections.emptyMap();
}
}
/**
* Method will calculate all required accompanying prices for the entity. The method is used when the entity
* has lowest price record handling.
*
* @param entityPrices source collection of all entity prices
* @param accompanyingPrices array of requirements for accompanying prices
* @return map of calculated accompanying prices
*/
@Nonnull
private static Map> calculateAccompanyingPricesForLowestPriceInnerRecordHandling(
@Nonnull Collection entityPrices,
@Nonnull AccompanyingPrice[] accompanyingPrices
) {
if (accompanyingPrices.length > 0) {
return Arrays.stream(accompanyingPrices)
.collect(
Collectors.toMap(
AccompanyingPrice::priceName,
accompanyingPrice -> {
final Map accompanyingPriorityIndex = getPriceListPriorityIndex(
accompanyingPrice.priceListPriority()
);
return entityPrices.stream()
.filter(it -> accompanyingPriorityIndex.containsKey(it.priceList()))
.min(Comparator.comparing(o -> accompanyingPriorityIndex.get(o.priceList())));
}
)
);
} else {
return Collections.emptyMap();
}
}
/**
* Method will calculate all required accompanying prices for the entity. The method is used when the entity
* has no inner record handling.
*
* @param entityPrices source collection of all entity prices
* @param currency currency used for price for sale calculation
* @param atTheMoment moment used for price for sale calculation
* @param accompanyingPrices array of requirements for accompanying prices
* @return map of calculated accompanying prices
*/
@Nonnull
private static Map> calculateAccompanyingPricesForSumInnerRecordHandling(
@Nonnull Collection entityPrices,
@Nonnull Set priceForSalePriceLists,
@Nonnull Currency currency,
@Nullable OffsetDateTime atTheMoment,
@Nonnull AccompanyingPrice[] accompanyingPrices
) {
if (accompanyingPrices.length > 0) {
final Collection