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.
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ů.
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:
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.
