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

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

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.

Zkopírovat odkaz na sekci1. 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
1 public class PriceListUsageCondition extends UsageCondition implements UsageSpecificIndexedCondition {2	@Serial private static final long serialVersionUID = 6407632885392385727L;3	4	public static final String PRICE_LIST_CODE = "priceListCode";5
6	/**7     * The price list code for which the condition is valid and the user has access to the price list.8	 */9	private String priceListCode;10
11	@Override12	public UsageConditionIndexedProperty[] getUsageIndexedProperty() {13		return new UsageConditionIndexedProperty[] {14				new UsageConditionIndexedProperty(PRICE_LIST_CODE, priceListCode)15		};16	}17
18	@JsonCreator19	public PriceListUsageCondition() {20		// constructor for deserialization21	}22}

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).

Zkopírovat odkaz na sekci2. 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
1 public class ProjectUsageConditionEvaluator implements UsageConditionEvaluator<PriceListUsageCondition> {2	private final CommonUserProvider commonUserProvider;3
4	/**5	 * The type of the condition that this evaluator can evaluate.6	 */7	@Override8	public Class<PriceListUsageCondition> getUsageConditionType() {9		// specify the type of the condition that this evaluator can evaluate10		return PriceListUsageCondition.class;11	}12
13	/**14	 * Get the properties that are used as keys for filtering the price lists.15	 * @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.16	 */17	@Override18	public UsageConditionIndexedProperty[] getUsageConditionKeyProperties(@NonNull String catalogCode) {19		final String currentUser = getCurrentUser(); // get external id of the current user20		if (currentUser == null) {21			// no user is logged in - return default for anonymous users, no applicable price lists will be returned for that user22			return getDefault();23		}24		final DefaultCompany company = PermissionService.executeAsSuperUser(() -> companyService.getCompanyOfUser(currentUser));25		if (company == null) {26			// no company is assigned to the user - return default for anonymous users or B2C customers, no applicable price lists will be returned for that user27			return getDefault();28		}29
30		final List<String> priceListCodes = ((CustomCompany) company).getPriceListCodes(); // Project specific method to get price list codes31		if (isEmpty(priceListCodes)) {32			// no specific price list codes are assigned to the company - return default, no applicable price lists will be returned for that user33			return getDefault();34		}35		// return the price list codes as indexed properties to be used for filtering the price lists36		return priceListCodes.stream()37				.map(code -> new UsageConditionIndexedProperty(PRICE_LIST_CODE, code, 38						anotherProperty -> !PRICE_LIST_CODE.equals(anotherProperty.getName())))39				.toArray(UsageConditionIndexedProperty[]::new);40	}41
42	/**43	 * Check if the price list with the condition is valid for the current user.44	 * @param usageCondition the condition to check45	 * @return true if the price list with the condition is valid for the current user46	 */47	@Override48	public boolean validFor(PriceListUsageCondition usageCondition) {49		final String currentUser = getCurrentUser(); // get external id of the current user50		if (currentUser == null) {51			// no user is logged in - price list with this condition is not valid for anonymous users52			return false;53		}54
55		final Company company = PermissionService.executeAsSuperUser(() -> companyService.getCompanyOfUser(currentUser));56		if (company == null) {57			// no company is assigned to the user - price list with this condition is not valid for anonymous users or B2C customers58			return false;59		}60
61		// get priceList code from company62		final List<String> priceListCodes = ((CustomCompany) company).getPriceListCodes(); // Project specific method to get price list codes63		if (isEmpty(priceListCodes)) {64			// no specific price list codes are assigned to the company - price list with this condition is not valid for anonymous users or B2C customers65			return false;66		}67		// check if the price list code in the condition is valid for the current user68		return priceListCodes.contains(usageCondition.getPriceListCode());69	}70
71	/**72	 * Get the current user from the context or from the logged-in user.73     * 74     * @see PublishedEntityContextSimulator#getUser()75     * @see CommonUserProvider#getLoggedInUserId() 76	 * @return the current user or null if no user is logged in77	 */78	@Nullable79	protected String getCurrentUser() {80		return ofNullable(PublishedEntityContextSimulator.getUser())81				.map(commonUser -> {82					if (log.isDebugEnabled()) {83						log.debug("Current user  is retrieved from the context.", commonUser.getExternalId());84					}85					return commonUser.getExternalId();86				})87				.orElseGet(() -> {88					final String loggedInUserId = commonUserProvider.getLoggedInUserId();89					if (log.isDebugEnabled()) {90						log.debug("Current user is not retrieved from the context. Logged-in user is `{}`.", loggedInUserId);91					}92					return loggedInUserId;93				});94	}95
96	/**97	 * Return properties to get default price lists.98	 */99	@NonNull100	private UsageConditionIndexedProperty[] getDefault() {101		return new UsageConditionIndexedProperty[]{};102	}103}

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.

Zkopírovat odkaz na sekci3. 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.