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