EdeeShop · Implementace

Projektové ovlivnění ceníků a cenové politiky

Upraveno

Následující dokument popisuje, jak lze ovlivnit ceníky a cenové politiky zákazníka. Dokument uvádí příklady na ceníkách, ale obdobně lze ovlivnit i cenové politiky.

Standardně lze u ceníků definovat, zda jsou určeny:

  • pro všechny zákazníky,
  • pro registrované (a přihlášené) zákazníky,
  • pro zákazníky s určitou rolí.

Tyto podmínky lze projektově rozšířit o další kritéria. Postup si představíme na konkrétním příkladu.

Příklad vychází z požadavku, že ceníky se mají zobrazit podle toho, zda má aktuální zákazník napojení na určitou společnost.

1. Uložení nové podmínky k ceníku

Nejprve je nutné vytvořit novou podmínku, která bude přesně určovat, ke kterým ceníkům má zákazník přístup. Nová podmínka musí rozšiřovat com.fg.cps.eshop.price.service.usageCondition.UsageCondition a implementovat rozhraní com.fg.cps.eshop.price.service.usageCondition.UsageSpecificIndexedCondition.

Rozhraní UsageSpecificIndexedCondition zajistí, že implementace vrátí hodnoty pro předfiltraci ceníků.

java
public class PriceListUsageCondition extends UsageCondition implements UsageSpecificIndexedCondition {	@Serial private static final long serialVersionUID = 6407632885392385727L;		public static final String PRICE_LIST_CODE = "priceListCode";
	/**     * The price list code for which the condition is valid and the user has access to the price list.	 */	private String priceListCode;
	@Override	public UsageConditionIndexedProperty[] getUsageIndexedProperty() {		return new UsageConditionIndexedProperty[] {				new UsageConditionIndexedProperty(PRICE_LIST_CODE, priceListCode)		};	}
	@JsonCreator	public PriceListUsageCondition() {		// constructor for deserialization	}}

Tuto podmínku je nutné uložit pro každý ceník, který má být ovlivněn touto podmínkou. Podmínky se ukládají do ceníků v atributu usageConditions (viz com.fg.cps.eshop.price.model.WithUsageConstraint#getUsageCondition).

2. Vyhodnocení podmínky pro aktuálního uživatele

Dále je nutné implementovat com.fg.cps.eshop.price.service.usageCondition.UsageConditionEvaluator, který bude vyhodnocovat novou podmínku (PriceListUsageCondition) pro aktuální kontext, tedy pro aktuálního uživatele, ale případně i aktuální katalog, doménu a další.

Příklad:

java
public class ProjectUsageConditionEvaluator implements UsageConditionEvaluator<PriceListUsageCondition> {	private final CommonUserProvider commonUserProvider;
	/**	 * The type of the condition that this evaluator can evaluate.	 */	@Override	public Class<PriceListUsageCondition> getUsageConditionType() {		// specify the type of the condition that this evaluator can evaluate		return PriceListUsageCondition.class;	}
	/**	 * Get the properties that are used as keys for filtering the price lists.	 * @return the properties that are used as keys for filtering the price lists or an empty array if no properties are used to get default price lists.	 */	@Override	public UsageConditionIndexedProperty[] getUsageConditionKeyProperties(@NonNull String catalogCode) {		final String currentUser = getCurrentUser(); // get external id of the current user		if (currentUser == null) {			// no user is logged in - return default for anonymous users, no applicable price lists will be returned for that user			return getDefault();		}		final DefaultCompany company = PermissionService.executeAsSuperUser(() -> companyService.getCompanyOfUser(currentUser));		if (company == null) {			// no company is assigned to the user - return default for anonymous users or B2C customers, no applicable price lists will be returned for that user			return getDefault();		}
		final List<String> priceListCodes = ((CustomCompany) company).getPriceListCodes(); // Project specific method to get price list codes		if (isEmpty(priceListCodes)) {			// no specific price list codes are assigned to the company - return default, no applicable price lists will be returned for that user			return getDefault();		}		// return the price list codes as indexed properties to be used for filtering the price lists		return priceListCodes.stream()				.map(code -> new UsageConditionIndexedProperty(PRICE_LIST_CODE, code, 						anotherProperty -> !PRICE_LIST_CODE.equals(anotherProperty.getName())))				.toArray(UsageConditionIndexedProperty[]::new);	}
	/**	 * Check if the price list with the condition is valid for the current user.	 * @param usageCondition the condition to check	 * @return true if the price list with the condition is valid for the current user	 */	@Override	public boolean validFor(PriceListUsageCondition usageCondition) {		final String currentUser = getCurrentUser(); // get external id of the current user		if (currentUser == null) {			// no user is logged in - price list with this condition is not valid for anonymous users			return false;		}
		final Company company = PermissionService.executeAsSuperUser(() -> companyService.getCompanyOfUser(currentUser));		if (company == null) {			// no company is assigned to the user - price list with this condition is not valid for anonymous users or B2C customers			return false;		}
		// get priceList code from company		final List<String> priceListCodes = ((CustomCompany) company).getPriceListCodes(); // Project specific method to get price list codes		if (isEmpty(priceListCodes)) {			// no specific price list codes are assigned to the company - price list with this condition is not valid for anonymous users or B2C customers			return false;		}		// check if the price list code in the condition is valid for the current user		return priceListCodes.contains(usageCondition.getPriceListCode());	}
	/**	 * Get the current user from the context or from the logged-in user.     *      * @see PublishedEntityContextSimulator#getUser()     * @see CommonUserProvider#getLoggedInUserId() 	 * @return the current user or null if no user is logged in	 */	@Nullable	protected String getCurrentUser() {		return ofNullable(PublishedEntityContextSimulator.getUser())				.map(commonUser -> {					if (log.isDebugEnabled()) {						log.debug("Current user  is retrieved from the context.", commonUser.getExternalId());					}					return commonUser.getExternalId();				})				.orElseGet(() -> {					final String loggedInUserId = commonUserProvider.getLoggedInUserId();					if (log.isDebugEnabled()) {						log.debug("Current user is not retrieved from the context. Logged-in user is `{}`.", loggedInUserId);					}					return loggedInUserId;				});	}
	/**	 * Return properties to get default price lists.	 */	@NonNull	private UsageConditionIndexedProperty[] getDefault() {		return new UsageConditionIndexedProperty[]{};	}}

Tento příklad ukazuje, jak implementovat vlastní vyhodnocovač podmínek (UsageConditionEvaluator), který umožňuje filtrovat ceníky na základě vlastních pravidel. V tomto konkrétním případě jsou ceníky přiřazeny společnostem a uživatelé mají přístup pouze k ceníkům přiřazeným společnosti, ke které patří. Pro registraci tohoto vyhodnocovače je nutné vytvořit bean v konfiguraci Spring v EdeeShop modulu.

Klíčové je pochopení třídy UsageConditionIndexedProperty - ta nám říká, jaké vlastnosti musí míst publikovaný ceník, aby jej bylo možné v naší podmínce použít. V metodě getUsageConditionKeyProperties si pro aktuální kontext (např. přihlášeného uživatele) vyhodnotíme k jakým ceníkům by měl mít přístup - v ukázkové implementaci je to jednoduché - rovnou vracíme názvy ceníků přiřazené společnosti, ale logika může být mnohem komplexnější a přístup k ceníkům může být nepřímý, např. na základě vlastností společnosti (VIP / non-VIP atp.).

UsageConditionIndexedProperty má následující vlastnosti:

  • název: konstanta typicky odvozená od třídy naší implementace
  • hodnota: proměnlivá hodnota vlasnosti
  • kombinační funkce: lambda funkce, která umožňuje vyhodnoti, zda je možné tuto vlastnost kombinovat s jinými vlastnostmi nebo jinými hodnotami stejné vlastnosti

Význam kombinační funkce je náročnější na vysvětlení a vyžaduje pohopení principu, jak stroj vyhledává výsledné ceníky. Ceníků je např. v B2B prostředích obrovské množství a proto není možné je všechny načíst z databáze a pro každý volat metodu validFor. Je nutné je na úrovni databáze nějak chytře předfiltrovat, aby docházelo k detailnímu prověřování pouze u jednotek takových ceníků.

Proto je ke každému ceníků ukládána kombinace jeho UsageConditionIndexedProperty seřazených dle abecedy. Následně se při výpočtu posbírají z kontextu UsageConditionIndexedProperty a pomocí permutační funkce se spočítají všechny možné (a povolené - viz. kombinační funkce) kombinace těchto vlastností, seřadí se podle abecedy a z databáze se načtou pouze takové ceníky jejich vlasnosti odpovídají některé z kombinací, které nám vycházejí pro aktuální kontext (aktuálního uživatele). Teprve ty se fyzicky načtou z databáze a po jedné se prověří ve funkci validFor, která může ještě některé z nich vyloučit. Nicméně pokud je podmínka jednoduchá a lze ji pokrýt pomocí předfiltrace, není již nutné stejnou validační logiku provolávat v rámci validFor. Zbylé ceníky jsou zacachovány pomocí klíče sestaveného z UsageConditionIndexedProperty aktuálního kontextu a použity pro další dotazování entit.

V případě, že podmínka nějakému ceníku přiřazena není, ceník jako takový se vrátí a není nad tím voláno validFor, tj. do cílového výpisu se dostane vždy.

3. Přístup k aktuálnímu uživateli a dalším kontextovým informacím

Vyhodnocení probíhá v rámci com.fg.cps.eshop.publishing.service.simulation.PublishedEntityContextSimulator.executeInContext, lze tedy využít data z com.fg.cps.eshop.publishing.service.simulation.PublishedEntityContextSimulator.Context, např.:

  • PublishedEntityContextSimulator.getUser() - vrátí aktuálního uživatele
  • PublishedEntityContextSimulator.getDomain() - vrátí aktuální doménu
  • PublishedEntityContextSimulator.getCurrency() - vrátí aktuální měnu
  • PublishedEntityContextSimulator.getDateTimeForCurrentContext() - vrátí aktuální datum a čas

Je důležité si uvědomit, že vyhodnocení ceníků může probíhat i v rámci procesů (např.: generování feedů), kde nemusí být k dispozici standardní kontext (např. přihlášený uživatel, aktuální katalog). Implementace UsageConditionEvaluator by měla tedy umožňovat vyhodnocení i v těchto případech.