All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.evitadb.api.requestResponse.data.structure.predicate.PriceContractSerializablePredicate Maven / Gradle / Ivy

There is a newer version: 2024.10.0
Show newest version
/*
 *
 *                         _ _        ____  ____
 *               _____   _(_) |_ __ _|  _ \| __ )
 *              / _ \ \ / / | __/ _` | | | |  _ \
 *             |  __/\ 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.structure.predicate;

import io.evitadb.api.EvitaSessionContract;
import io.evitadb.api.exception.ContextMissingException;
import io.evitadb.api.query.require.PriceContentMode;
import io.evitadb.api.query.require.QueryPriceMode;
import io.evitadb.api.requestResponse.EvitaRequest;
import io.evitadb.api.requestResponse.data.EntityContract;
import io.evitadb.api.requestResponse.data.PriceContract;
import io.evitadb.api.requestResponse.data.PricesContract.PriceForSaleContext;
import io.evitadb.api.requestResponse.data.structure.Entity;
import io.evitadb.api.requestResponse.data.structure.EntityDecorator;
import io.evitadb.api.requestResponse.data.structure.SerializablePredicate;
import io.evitadb.utils.ArrayUtils;
import io.evitadb.utils.Assert;
import io.evitadb.utils.CollectionUtils;
import lombok.Getter;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.Serial;
import java.time.OffsetDateTime;
import java.util.Arrays;
import java.util.Collections;
import java.util.Currency;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static java.util.Optional.ofNullable;

/**
 * This predicate allows to limit number of prices visible to the client based on query constraints.
 *
 * @author Jan Novotný ([email protected]), FG Forrest a.s. (c) 2021
 */
public class PriceContractSerializablePredicate implements SerializablePredicate {
	public static final PriceContractSerializablePredicate DEFAULT_INSTANCE = new PriceContractSerializablePredicate();
	@Serial private static final long serialVersionUID = -7100489955953977035L;
	/**
	 * Contains information about price content mode used when entity prices were fetched.
	 */
	@Getter @Nonnull private final PriceContentMode priceContentMode;
	/**
	 * Contains information about price currency used when entity prices were fetched.
	 */
	@Getter @Nullable private final Currency currency;
	/**
	 * Contains information about price validity moment used when entity prices were fetched.
	 */
	@Getter @Nullable private final OffsetDateTime validIn;
	/**
	 * Contains information about price lists used when entity prices were fetched.
	 */
	@Getter @Nullable private final String[] priceLists;
	/**
	 * Contains information about price lists that should be fetched in addition to filtered prices.
	 */
	@Getter @Nullable private final String[] additionalPriceLists;
	/**
	 * Contains the same information as {@link #priceLists} but in the form of the set for faster lookups.
	 */
	@Getter @Nonnull private final Set priceListsAsSet;
	/**
	 * Contains information about the type of price that should be used for sorting and filtering.
	 */
	@Getter @Nonnull private final QueryPriceMode queryPriceMode;
	/**
	 * Contains information about underlying predicate that is bound to the {@link EntityDecorator}. This underlying
	 * predicate represents the scope of the fetched (enriched) entity in its true form (i.e. {@link Entity}) and needs
	 * to be carried around even if {@link io.evitadb.api.EntityCollectionContract#limitEntity(EntityContract, EvitaRequest, EvitaSessionContract)}
	 * is invoked on the entity.
	 */
	@Nullable @Getter private final PriceContractSerializablePredicate underlyingPredicate;
	/**
	 * Contains information about the context availability for price for sale calculation.
	 */
	private final boolean contextAvailable;
	/**
	 * Contains information about the context for price for sale calculation. Computed lazily.
	 */
	private PriceForSaleContext priceForSaleContext;

	public PriceContractSerializablePredicate() {
		this.priceContentMode = PriceContentMode.ALL;
		this.currency = null;
		this.validIn = null;
		this.priceLists = null;
		this.additionalPriceLists = null;
		this.priceListsAsSet = Collections.emptySet();
		this.underlyingPredicate = null;
		this.contextAvailable = false;
		this.queryPriceMode = QueryPriceMode.WITH_TAX;
	}

	public PriceContractSerializablePredicate(@Nonnull EvitaRequest evitaRequest, @Nullable Boolean contextAvailable) {
		this.priceContentMode = evitaRequest.getRequiresEntityPrices();
		this.currency = evitaRequest.getRequiresCurrency();
		this.validIn = evitaRequest.getRequiresPriceValidIn();
		this.priceLists = evitaRequest.getRequiresPriceLists();
		this.additionalPriceLists = evitaRequest.getFetchesAdditionalPriceLists();
		this.priceListsAsSet = CollectionUtils.createLinkedHashSet(this.priceLists.length + this.additionalPriceLists.length);
		Collections.addAll(this.priceListsAsSet, this.priceLists);
		Collections.addAll(this.priceListsAsSet, this.additionalPriceLists);
		this.underlyingPredicate = null;
		this.contextAvailable = contextAvailable != null ?
			contextAvailable : this.currency != null && !ArrayUtils.isEmpty(this.priceLists);
		this.queryPriceMode = evitaRequest.getQueryPriceMode();
	}

	public PriceContractSerializablePredicate(@Nonnull EvitaRequest evitaRequest, @Nonnull PriceContractSerializablePredicate underlyingPredicate) {
		Assert.isPremiseValid(
			underlyingPredicate.getUnderlyingPredicate() == null,
			"Underlying predicates cannot be nested! " +
				"Underlying predicate composition expects to be maximally one: " +
				"limited view -> complete view and never limited view -> limited view -> complete view."
		);
		this.priceContentMode = evitaRequest.getRequiresEntityPrices();
		this.currency = underlyingPredicate.currency;
		this.validIn = underlyingPredicate.validIn;
		this.priceLists = underlyingPredicate.priceLists;
		this.additionalPriceLists = underlyingPredicate.additionalPriceLists;
		this.priceListsAsSet = underlyingPredicate.priceListsAsSet;
		this.underlyingPredicate = underlyingPredicate;
		this.contextAvailable = this.currency != null && !ArrayUtils.isEmpty(this.priceLists);
		this.queryPriceMode = evitaRequest.getQueryPriceMode();
	}

	PriceContractSerializablePredicate(
		@Nonnull PriceContentMode priceContentMode,
		@Nullable Currency currency,
		@Nullable OffsetDateTime validIn,
		@Nullable String[] priceLists,
		@Nullable String[] additionalPriceLists,
		@Nonnull Set priceListsAsSet,
		@Nonnull QueryPriceMode queryPriceMode,
		boolean contextAvailable
	) {
		this.priceContentMode = priceContentMode;
		this.currency = currency;
		this.validIn = validIn;
		this.priceLists = priceLists;
		this.additionalPriceLists = additionalPriceLists;
		this.priceListsAsSet = priceListsAsSet;
		this.underlyingPredicate = null;
		this.queryPriceMode = queryPriceMode;
		this.contextAvailable = contextAvailable;
	}

	@Override
	public boolean test(PriceContract priceContract) {
		return switch (priceContentMode) {
			case NONE -> false;
			case ALL -> priceContract.exists();
			case RESPECTING_FILTER -> priceContract.exists() &&
				(currency == null || Objects.equals(currency, priceContract.currency())) &&
				(ArrayUtils.isEmpty(priceLists) || priceListsAsSet.contains(priceContract.priceList())) &&
				(validIn == null || ofNullable(priceContract.validity()).map(it -> it.isValidFor(validIn)).orElse(true));
		};
	}

	/**
	 * Returns true if the context for price for sale calculation is available.
	 */
	public boolean isContextAvailable() {
		return this.contextAvailable;
	}

	/**
	 * Returns the context for price for sale calculation.
	 * @return the context for price for sale calculation
	 */
	@Nonnull
	public Optional getPriceForSaleContext() {
		if (this.contextAvailable) {
			if (this.priceForSaleContext == null) {
				this.priceForSaleContext = new PriceForSaleContext(this.priceLists, this.currency, this.validIn);
			}
			return Optional.of(this.priceForSaleContext);
		} else {
			return Optional.empty();
		}
	}

	/**
	 * Returns true if the price for particular `currency` and `priceList` combination might exist, but was not fetched
	 * along with the entity.
	 */
	public void checkFetched(@Nullable Currency currency, @Nonnull String... priceList) throws ContextMissingException {
		switch (priceContentMode) {
			case NONE -> throw ContextMissingException.pricesNotFetched();
			case RESPECTING_FILTER -> {
				if (this.currency != null && currency != null && !Objects.equals(this.currency, currency)) {
					throw ContextMissingException.pricesNotFetched(currency, this.currency);
				}
				if (!priceListsAsSet.isEmpty()) {
					for (String checkedPriceList : priceList) {
						if (!priceListsAsSet.contains(checkedPriceList)) {
							throw ContextMissingException.pricesNotFetched(checkedPriceList, priceListsAsSet);
						}
					}
				}
			}
		}
	}

	/**
	 * Returns true if the prices were fetched.
	 */
	public boolean isFetched() {
		return priceContentMode != PriceContentMode.NONE;
	}

	/**
	 * Returns true if at least single price was fetched along with the entity.
	 *
	 * @throws ContextMissingException if no price was fetched with the entity
	 */
	public void checkPricesFetched() throws ContextMissingException {
		if (priceContentMode == PriceContentMode.NONE) {
			throw ContextMissingException.pricesNotFetched();
		}
	}

	public PriceContractSerializablePredicate createRicherCopyWith(@Nonnull EvitaRequest evitaRequest) {
		final PriceContentMode requiresEntityPrices = evitaRequest.getRequiresEntityPrices();
		final String[] fetchesAdditionalPriceLists = evitaRequest.getFetchesAdditionalPriceLists();
		if (this.priceContentMode.ordinal() >= requiresEntityPrices.ordinal()) {
			if (ArrayUtils.isEmpty(fetchesAdditionalPriceLists)) {
				// this predicate cannot change since everything is taken from the filter and this cannot change in time
				return this;
			} else {
				return new PriceContractSerializablePredicate(
					requiresEntityPrices,
					this.currency, this.validIn, this.priceLists,
					this.additionalPriceLists == null ? fetchesAdditionalPriceLists : ArrayUtils.mergeArrays(this.additionalPriceLists, fetchesAdditionalPriceLists),
					this.priceListsAsSet,
					this.queryPriceMode,
					this.contextAvailable
				);
			}
		} else {
			if (ArrayUtils.isEmpty(fetchesAdditionalPriceLists)) {
				return new PriceContractSerializablePredicate(
					requiresEntityPrices,
					this.currency, this.validIn, this.priceLists,
					this.additionalPriceLists,
					this.priceListsAsSet,
					this.queryPriceMode,
					this.contextAvailable
				);
			} else {
				return new PriceContractSerializablePredicate(
					requiresEntityPrices,
					this.currency, this.validIn,
					this.priceLists,
					this.additionalPriceLists == null ? fetchesAdditionalPriceLists : ArrayUtils.mergeArrays(this.additionalPriceLists, fetchesAdditionalPriceLists),
					this.additionalPriceLists == null ? this.priceListsAsSet : Stream.concat(this.priceListsAsSet.stream(), Arrays.stream(fetchesAdditionalPriceLists)).collect(Collectors.toSet()),
					this.queryPriceMode,
					this.contextAvailable
				);
			}
		}
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy