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

io.evitadb.api.requestResponse.data.structure.ExistingPricesBuilder Maven / Gradle / Ivy

The 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;

import io.evitadb.api.exception.AmbiguousPriceException;
import io.evitadb.api.exception.ContextMissingException;
import io.evitadb.api.exception.UnexpectedResultCountException;
import io.evitadb.api.query.require.QueryPriceMode;
import io.evitadb.api.requestResponse.data.Droppable;
import io.evitadb.api.requestResponse.data.PriceContract;
import io.evitadb.api.requestResponse.data.PriceInnerRecordHandling;
import io.evitadb.api.requestResponse.data.PricesEditor.PricesBuilder;
import io.evitadb.api.requestResponse.data.mutation.LocalMutation;
import io.evitadb.api.requestResponse.data.mutation.price.PriceMutation;
import io.evitadb.api.requestResponse.data.mutation.price.RemovePriceMutation;
import io.evitadb.api.requestResponse.data.mutation.price.SetPriceInnerRecordHandlingMutation;
import io.evitadb.api.requestResponse.data.mutation.price.UpsertPriceMutation;
import io.evitadb.api.requestResponse.data.structure.Price.PriceKey;
import io.evitadb.api.requestResponse.data.structure.predicate.PriceContractSerializablePredicate;
import io.evitadb.api.requestResponse.schema.EntitySchemaContract;
import io.evitadb.dataType.DateTimeRange;
import io.evitadb.exception.GenericEvitaInternalError;
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.math.BigDecimal;
import java.util.Collection;
import java.util.Collections;
import java.util.Currency;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static java.util.Optional.empty;
import static java.util.Optional.ofNullable;

/**
 * Builder that is used to alter existing {@link Prices}. Prices is immutable object so there is need for another object
 * that would simplify the process of updating its contents. This is why the builder class exists.
 *
 * @author Jan Novotný ([email protected]), FG Forrest a.s. (c) 2021
 */
public class ExistingPricesBuilder implements PricesBuilder {
	@Serial private static final long serialVersionUID = 5366182867172493114L;

	/**
	 * This predicate filters out prices that were not fetched in query.
	 */
	@Getter private final PriceContractSerializablePredicate pricePredicate;
	private final Map priceMutations;
	private final EntitySchemaContract entitySchema;
	private final Prices basePrices;
	@Nullable private SetPriceInnerRecordHandlingMutation priceInnerRecordHandlingEntityMutation;
	private boolean removeAllNonModifiedPrices;

	public ExistingPricesBuilder(
		@Nonnull EntitySchemaContract entitySchema,
		@Nonnull Prices prices
	) {
		this.basePrices = prices;
		this.entitySchema = entitySchema;
		this.priceMutations = new HashMap<>(16);
		this.pricePredicate = new PriceContractSerializablePredicate();
	}

	public ExistingPricesBuilder(
		@Nonnull EntitySchemaContract entitySchema,
		@Nonnull Prices prices,
		@Nonnull PriceContractSerializablePredicate pricePredicate
	) {
		this.basePrices = prices;
		this.entitySchema = entitySchema;
		this.priceMutations = new HashMap<>(16);
		this.pricePredicate = pricePredicate;
	}

	/**
	 * Method allows adding specific mutation on the fly.
	 */
	public void addMutation(@Nonnull SetPriceInnerRecordHandlingMutation setPriceInnerRecordHandlingMutation) {
		this.priceInnerRecordHandlingEntityMutation = setPriceInnerRecordHandlingMutation;
	}

	/**
	 * Method allows adding specific mutation on the fly.
	 */
	public void addMutation(@Nonnull PriceMutation localMutation) {
		if (localMutation instanceof UpsertPriceMutation upsertPriceMutation) {
			final PriceKey priceKey = upsertPriceMutation.getPriceKey();
			assertPriceNotAmbiguousBeforeAdding(
				new Price(
					priceKey,
					upsertPriceMutation.getInnerRecordId(),
					upsertPriceMutation.getPriceWithoutTax(),
					upsertPriceMutation.getTaxRate(),
					upsertPriceMutation.getPriceWithTax(),
					upsertPriceMutation.getValidity(),
					upsertPriceMutation.isIndexed()
				)
			);
			this.priceMutations.put(priceKey, upsertPriceMutation);
		} else if (localMutation instanceof RemovePriceMutation removePriceMutation) {
			final PriceKey priceKey = removePriceMutation.getPriceKey();
			Assert.notNull(basePrices.getPriceWithoutSchemaCheck(priceKey), "Price " + priceKey + " doesn't exist!");
			final RemovePriceMutation mutation = new RemovePriceMutation(priceKey);
			this.priceMutations.put(priceKey, mutation);
		} else {
			throw new GenericEvitaInternalError("Unknown Evita price mutation: `" + localMutation.getClass() + "`!");
		}
	}

	@Override
	public PricesBuilder setPrice(int priceId, @Nonnull String priceList, @Nonnull Currency currency, @Nonnull BigDecimal priceWithoutTax, @Nonnull BigDecimal taxRate, @Nonnull BigDecimal priceWithTax, boolean indexed) {
		return setPrice(priceId, priceList, currency, null, priceWithoutTax, taxRate, priceWithTax, null, indexed);
	}

	@Override
	public PricesBuilder setPrice(int priceId, @Nonnull String priceList, @Nonnull Currency currency, @Nullable Integer innerRecordId, @Nonnull BigDecimal priceWithoutTax, @Nonnull BigDecimal taxRate, @Nonnull BigDecimal priceWithTax, boolean indexed) {
		return setPrice(priceId, priceList, currency, innerRecordId, priceWithoutTax, taxRate, priceWithTax, null, indexed);
	}

	@Override
	public PricesBuilder setPrice(int priceId, @Nonnull String priceList, @Nonnull Currency currency, @Nonnull BigDecimal priceWithoutTax, @Nonnull BigDecimal taxRate, @Nonnull BigDecimal priceWithTax, @Nullable DateTimeRange validity, boolean indexed) {
		return setPrice(priceId, priceList, currency, null, priceWithoutTax, taxRate, priceWithTax, validity, indexed);
	}

	@Override
	public PricesBuilder setPrice(int priceId, @Nonnull String priceList, @Nonnull Currency currency, @Nullable Integer innerRecordId, @Nonnull BigDecimal priceWithoutTax, @Nonnull BigDecimal taxRate, @Nonnull BigDecimal priceWithTax, @Nullable DateTimeRange validity, boolean indexed) {
		final PriceKey priceKey = new PriceKey(priceId, priceList, currency);
		final UpsertPriceMutation mutation = new UpsertPriceMutation(priceKey, innerRecordId, priceWithoutTax, taxRate, priceWithTax, validity, indexed);
		assertPriceNotAmbiguousBeforeAdding(new Price(priceKey, innerRecordId, priceWithoutTax, taxRate, priceWithTax, validity, indexed));
		this.priceMutations.put(priceKey, mutation);
		return this;
	}

	@Override
	public PricesBuilder removePrice(int priceId, @Nonnull String priceList, @Nonnull Currency currency) {
		final PriceKey priceKey = new PriceKey(priceId, priceList, currency);
		Assert.notNull(basePrices.getPriceWithoutSchemaCheck(priceKey), "Price " + priceKey + " doesn't exist!");
		final RemovePriceMutation mutation = new RemovePriceMutation(priceKey);
		this.priceMutations.put(priceKey, mutation);
		return this;
	}

	@Override
	public PricesBuilder setPriceInnerRecordHandling(@Nonnull PriceInnerRecordHandling priceInnerRecordHandling) {
		this.priceInnerRecordHandlingEntityMutation = new SetPriceInnerRecordHandlingMutation(priceInnerRecordHandling);
		return this;
	}

	@Override
	public PricesBuilder removePriceInnerRecordHandling() {
		Assert.isTrue(
			basePrices.getPriceInnerRecordHandling() != PriceInnerRecordHandling.NONE,
			"Price inner record handling is already set to NONE!"
		);
		this.priceInnerRecordHandlingEntityMutation = new SetPriceInnerRecordHandlingMutation(PriceInnerRecordHandling.NONE);
		return this;
	}

	@Override
	public PricesBuilder removeAllNonTouchedPrices() {
		this.removeAllNonModifiedPrices = true;
		return this;
	}

	@Nonnull
	@Override
	public Optional getPrice(int priceId, @Nonnull String priceList, @Nonnull Currency currency) {
		return getPriceInternal(new PriceKey(priceId, priceList, currency));
	}

	@Nullable
	@Override
	public Optional getPrice(@Nonnull PriceKey priceKey) throws ContextMissingException {
		return getPriceInternal(priceKey);
	}

	@Nonnull
	@Override
	public Optional getPrice(@Nonnull String priceList, @Nonnull Currency currency) throws UnexpectedResultCountException, ContextMissingException {
		final List matchingPrices = getPrices()
			.stream()
			.filter(it -> it.priceList().equals(priceList) && it.currency().equals(currency))
			.toList();
		if (matchingPrices.size() > 1) {
			throw new UnexpectedResultCountException(
				matchingPrices.size(),
				"More than one price found for price list `" + priceList + "` and currency `" + currency + "`."
			);
		}
		return matchingPrices.isEmpty() ? Optional.empty() : Optional.of(matchingPrices.get(0));
	}

	@Override
	public boolean isPriceForSaleContextAvailable() {
		return pricePredicate.isContextAvailable();
	}

	@Nonnull
	@Override
	public Optional getPriceForSaleContext() {
		return pricePredicate.getPriceForSaleContext();
	}

	@Override
	public boolean pricesAvailable() {
		return basePrices.pricesAvailable();
	}

	@Nonnull
	@Override
	public Optional getPriceForSale() throws ContextMissingException {
		if (pricePredicate.getCurrency() != null && !ArrayUtils.isEmpty(pricePredicate.getPriceLists())) {
			return getPriceForSale(
				pricePredicate.getCurrency(),
				pricePredicate.getValidIn(),
				pricePredicate.getPriceLists()
			);
		} else {
			throw new ContextMissingException();
		}
	}

	@Nonnull
	@Override
	public Optional getPriceForSaleIfAvailable() {
		if (pricePredicate.getCurrency() != null && !ArrayUtils.isEmpty(pricePredicate.getPriceLists())) {
			return getPriceForSale(
				pricePredicate.getCurrency(),
				pricePredicate.getValidIn(),
				pricePredicate.getPriceLists()
			);
		} else {
			return empty();
		}
	}

	@Nonnull
	@Override
	public List getAllPricesForSale() {
		return getAllPricesForSale(
			pricePredicate.getCurrency(),
			pricePredicate.getValidIn(),
			pricePredicate.getPriceLists()
		);
	}

	@Override
	public boolean hasPriceInInterval(@Nonnull BigDecimal from, @Nonnull BigDecimal to, @Nonnull QueryPriceMode queryPriceMode) throws ContextMissingException {
		if (pricePredicate.getCurrency() != null && !ArrayUtils.isEmpty(pricePredicate.getPriceLists())) {
			return hasPriceInInterval(
				from, to, queryPriceMode,
				pricePredicate.getCurrency(),
				pricePredicate.getValidIn(),
				pricePredicate.getPriceLists()
			);
		} else {
			throw new ContextMissingException();
		}
	}

	@Nonnull
	@Override
	public Collection getPrices() {
		return getPricesWithoutPredicate()
			.filter(pricePredicate)
			.collect(Collectors.toList());
	}

	@Nonnull
	@Override
	public PriceInnerRecordHandling getPriceInnerRecordHandling() {
		return ofNullable(priceInnerRecordHandlingEntityMutation)
			.map(it -> it.mutateLocal(entitySchema, basePrices).getPriceInnerRecordHandling())
			.orElseGet(basePrices::getPriceInnerRecordHandling);
	}

	@Override
	public int version() {
		return basePrices.version();
	}

	@Nonnull
	@Override
	public Stream> buildChangeSet() {
		final Collection originalPrices = basePrices.pricesAvailable() ? basePrices.getPrices() : Collections.emptyList();
		final Map builtPrices = CollectionUtils.createHashMap(originalPrices.size());
		for (PriceContract originalPrice : originalPrices) {
			builtPrices.put(originalPrice.priceKey(), originalPrice);
		}
		if (removeAllNonModifiedPrices) {
			return Stream.concat(
					priceInnerRecordHandlingEntityMutation == null || Objects.equals(basePrices.getPriceInnerRecordHandling(), priceInnerRecordHandlingEntityMutation.getPriceInnerRecordHandling()) ?
						Stream.empty() : Stream.of(priceInnerRecordHandlingEntityMutation),
					Stream.concat(
						priceMutations.values()
							.stream()
							.filter(it -> {
								final PriceContract existingValue = builtPrices.get(it.getPriceKey());
								final PriceContract newPrice = it.mutateLocal(entitySchema, existingValue);
								builtPrices.put(it.getPriceKey(), newPrice);
								return existingValue == null || newPrice.version() > existingValue.version();
							}),
						originalPrices
							.stream()
							.filter(it -> priceMutations.get(it.priceKey()) == null)
							.map(it -> new RemovePriceMutation(it.priceKey()))
					)
				)
				.filter(Objects::nonNull);
		} else {
			return Stream.concat(
					Objects.equals(basePrices.getPriceInnerRecordHandling(), ofNullable(priceInnerRecordHandlingEntityMutation).map(SetPriceInnerRecordHandlingMutation::getPriceInnerRecordHandling).orElse(null)) ?
						Stream.empty() : Stream.of(priceInnerRecordHandlingEntityMutation),
					priceMutations.values()
						.stream()
						.filter(it -> {
							final PriceContract existingValue = builtPrices.get(it.getPriceKey());
							final PriceContract newPrice = it.mutateLocal(entitySchema, existingValue);
							builtPrices.put(it.getPriceKey(), newPrice);
							return existingValue == null || newPrice.version() > existingValue.version();
						})
				)
				.filter(Objects::nonNull);
		}
	}

	@Nonnull
	@Override
	public Prices build() {
		final Collection newPrices = getPricesWithoutPredicate().collect(Collectors.toList());
		final Map newPriceIndex = newPrices
			.stream()
			.collect(
				Collectors.toMap(
					PriceContract::priceKey,
					Function.identity()
				)
			);
		final PriceInnerRecordHandling newPriceInnerRecordHandling = getPriceInnerRecordHandling();
		if (basePrices.getPriceInnerRecordHandling() != newPriceInnerRecordHandling ||
			basePrices.getPrices().size() != newPrices.size() ||
			basePrices.getPrices()
				.stream()
				.anyMatch(
					it -> ofNullable(newPriceIndex.get(it.priceKey()))
						.map(price -> price.differsFrom(it))
						.orElse(true)
				)
			) {
			return new Prices(
				basePrices.entitySchema,
				basePrices.version() + 1,
				newPrices,
				newPriceInnerRecordHandling,
				!newPrices.isEmpty()
			);
		} else {
			return basePrices;
		}
	}

	@Nonnull
	private Stream getPricesWithoutPredicate() {
		return Stream.concat(
			basePrices
				.getPrices()
				.stream()
				.map(it ->
					ofNullable(priceMutations.get(it.priceKey()))
						.map(mut -> {
							final PriceContract mutatedValue = mut.mutateLocal(entitySchema, it);
							return mutatedValue.differsFrom(it) ? mutatedValue : it;
						})
						.orElseGet(() -> removeAllNonModifiedPrices ? null : it)
				)
				.filter(Objects::nonNull)
				.filter(PriceContract::exists),
			priceMutations
				.values()
				.stream()
				.filter(it -> basePrices.getPriceWithoutSchemaCheck(it.getPriceKey()).isEmpty())
				.map(it -> it.mutateLocal(entitySchema, null))
		);
	}

	@Nonnull
	private Optional getPriceInternal(@Nonnull PriceKey priceKey) {
		final Optional price = basePrices.getPriceWithoutSchemaCheck(priceKey)
			.map(it -> ofNullable(priceMutations.get(priceKey))
				.map(x -> x.mutateLocal(entitySchema, it))
				.orElse(it)
			)
			.or(
				() -> ofNullable(priceMutations.get(priceKey))
					.map(it -> it.mutateLocal(entitySchema, null))
			);
		return price.filter(pricePredicate);
	}

	/**
	 * Method throws {@link AmbiguousPriceException} when there is conflict in prices.
	 */
	private void assertPriceNotAmbiguousBeforeAdding(@Nonnull PriceContract price) {
		// check whether new price doesn't conflict with original prices
		final PriceContract conflictingPrice = basePrices.getPrices().stream()
			.filter(Droppable::exists)
			.filter(it -> it.priceList().equals(price.priceList()))
			.filter(it -> it.currency().equals(price.currency()))
			.filter(it -> it.priceId() != price.priceId())
			.filter(it -> Objects.equals(it.innerRecordId(), price.innerRecordId()))
			.filter(it ->
				price.validity() == null ||
					ofNullable(it.validity()).map(existingValidity -> existingValidity.overlaps(price.validity()))
						.orElse(true)
			)
			// the conflicting prices don't play role if they're going to be removed in the same update
			.filter(it -> !(priceMutations.get(it.priceKey()) instanceof RemovePriceMutation))
			.findFirst()
			.orElse(null);
		if (conflictingPrice != null && !removeAllNonModifiedPrices) {
			throw new AmbiguousPriceException(conflictingPrice, price);
		}
		// check whether there is no conflicting update
		final UpsertPriceMutation conflictingMutation = priceMutations.values().stream()
			.filter(it -> it instanceof UpsertPriceMutation)
			.map(UpsertPriceMutation.class::cast)
			.filter(it -> it.getPriceKey().priceList().equals(price.priceList()))
			.filter(it -> it.getPriceKey().currency().equals(price.currency()))
			.filter(it -> it.getPriceKey().priceId() != price.priceId())
			.filter(it -> Objects.equals(it.getInnerRecordId(), price.innerRecordId()))
			.filter(it ->
				price.validity() == null ||
					ofNullable(it.getValidity()).map(existingValidity -> existingValidity.overlaps(price.validity()))
						.orElse(true)
			)
			.findFirst()
			.orElse(null);
		if (conflictingMutation != null) {
			throw new AmbiguousPriceException(
				conflictingMutation.mutateLocal(
					entitySchema,
					basePrices.getPriceWithoutSchemaCheck(conflictingMutation.getPriceKey()).orElse(null)
				),
				price
			);
		}
	}

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy