API
XPV Object
In th Script Engine sandbox there is no global, or window object, instead there is a global xpv object.
Its shape depends on the product the script runs for. xpv.version tells a script which one it
got: 1 for products built on the original catalogue, 2 for products built on Catalogs. The two
sections below describe each. Connectors, Globals and xpv.return are identical in both.
V1
type xpv = {
articles: {
getById(id: number): null|Article,
getByName(name: string): null|Article,
getBySKU(sku: string): null|Article,
getAll(): Article[],
},
attributes: {
getById(id: number): null|Attribute,
getByName(name: string): null|Attribute,
getByKey(key: string): null|Attribute,
getAll(): Attribute[],
},
catalogue: {
sku: null|string,
},
connectors: {
getByName(name: string): null|AdapterContract,
},
globals: {
getByName(name: string): null|string,
getAll(): string[],
},
return: (data: any) => any,
user_data: {
getAll(): any[],
},
};
type Article = {
material_group_id: number,
material_group_sku: string|null,
models_id: number,
models_name: string,
models_price: number|null,
models_sku: string|null,
price_sku: string|null,
};
type Attribute = {
attribute_id: number,
attribute_name: string,
attribute_key: null|string,
attribute_value: AttributeValue,
attribute_value_name: string,
attribute_value_key: null|string,
};
type AttributeValue = {
attribute_value_id: number,
};
The structure of AttributeValue can vary depending on the type of attribute.
V2
A V2 configuration can hold more than one product — a bundle and the products selected inside it —
so attributes are grouped per product instead of being one flat list. cid identifies one
instance of a product in the configuration, which matters when the same product is selected
twice; id identifies the product itself.
xpv.current_product is the product instance the script is being asked to price. A script pricing
one product can ignore the grouping and read xpv.attributes.getAllItems().
type xpv = {
version: 2,
locale: string,
currency: null|string,
current_product: null|Product,
products: {
getById(id: string): null|Product,
getByCid(cid: string): null|Product,
getByIdentifier(identifier: string): null|Product,
getBySku(sku: string): null|Product,
getAll(): Product[],
},
attributes: {
getAll(): ProductAttributes[],
getAllItems(): Attribute[],
getSelected(): Attribute[],
getByIdentifier(identifier: string): null|Attribute,
getByProductId(productId: string): null|Attribute[],
getByProductCid(productCid: string): null|Attribute[],
getByProductIdentifier(productIdentifier: string): null|Attribute[],
getByProductIdAndAttributeIdentifier(productId: string, attributeIdentifier: string): Attribute[],
getById(id: string): null|Attribute,
getByName(name: string): null|Attribute,
getByKey(key: string): null|Attribute,
},
articles: {
getAllParts(): ProductParts[],
getPartsByProductId(productId: string): null|Part[],
getPartsByProductCid(productCid: string): null|Part[],
getPartByProductIdAndPartIdentifier(productId: string, partIdentifier: string): Part[],
getAllComponents(): PartComponents[],
getComponentsByPartIdentifier(partIdentifier: string): null|Component[],
getComponentByPartIdentifierAndComponentIdentifier(partIdentifier: string, componentIdentifier: string): Component[],
getAll(): Part[],
getByIdentifier(identifier: string): null|Part,
getById(id: string): null|Part,
getByName(name: string): null|Part,
getBySKU(sku: string): null|Part,
},
connectors: {
getByName(name: string): null|AdapterContract,
},
globals: {
getByName(name: string): null|string,
getAll(): string[],
},
return: (data: any) => any,
user_data: {
getAll(): any[],
},
};
type Product = {
cid: string,
id: string,
identifier: null|string,
name: null|string,
sku: null|string,
};
type ProductAttributes = {
product_cid: string,
product_id: null|string,
product_identifier: null|string,
items: Attribute[],
};
type Attribute = {
id: string,
identifier: null|string,
name: null|string,
type: string,
value_input_type: null|string,
enabled: boolean,
values: AttributeValue[],
};
type AttributeValue = {
id: null|string,
identifier: null|string,
label: null|string,
type: null|string,
value: any,
data: object,
};
type ProductParts = {
product_cid: string,
product_id: string,
items: Part[],
};
type Part = {
identifier: null|string,
name: null|string,
sku: null|string,
quantity: number,
uom: null|string,
price_per_unit: number,
total_price: number,
thumbnail_url: null|string,
};
type PartComponents = {
part_identifier: string,
items: Component[],
};
type Component = {
identifier: null|string,
name: null|string,
sku: null|string,
quantity: number,
uom: null|string,
thumbnail_url: null|string,
};
Reading an attribute value correctly comes down to picking the right one of three fields:
identifieris the stable code configured in the Backoffice. It never changes with the locale, so it is the field to build a pricing SKU from or to match against an external feed.labelis what the customer sees, translated intoxpv.locale. Some value types have no translatable label at all — a number or a selected product, for instance — and those arrive asnull.valueis what the customer entered: the text of an engraving, the number typed into a configurable number. Selecting an option from a list leaves itnull, because the choice is the value'sid.
An attribute the configurator shows but the customer never touched arrives with an empty values
list; xpv.attributes.getSelected() filters those out. enabled is false when a catalog rule
turned the attribute off for this configuration.
Bill of Materials
xpv.articles exposes the Bill of Materials of the configuration: the parts each product resolves
to, and the components each part is built from. A product whose parts generation mode is None
contributes no parts at all.
Every part carries the same eight fields whatever produced it — a global part matched by the
System mode, or one a Part script returned. Unknown values arrive as null, and quantity,
price_per_unit and total_price always arrive as numbers. total_price is the part's own total
when it carries one, and price_per_unit × quantity otherwise, which is how the BOM price
calculator itself prices a part: a Part script may return a total that disagrees with the per-unit
price, and that total wins.
xpv.articles.getAll() returns the parts of xpv.current_product only. A configuration holding a
bundle carries one group per product and the bundle's own group aggregating its sub-products'
parts, so a script summing total_price across every group would count those parts twice. Without
a current product — in the debugger, for instance — getAll() falls back to every group flattened.
getAllParts() always returns every group, grouped: it is the explicit whole-configuration view.
V1-compatible lookups
xpv.attributes.getById()/getByName()/getByKey() and xpv.articles.getById()/getByName()/
getBySKU()/getAll() mirror the V1 lookup methods so a V1 script can be ported to V2 with the
same accessor names. They differ from their V1 counterparts in two ways:
idis astring(V2 uses string identifiers, e.g."01KATTR1"), not the numeric V1attribute_id/models_id.- The returned item has the V2 shape (
id,identifier,name,values, …), not the V1attribute_*/models_*keys.
Both are resolved against xpv.current_product's own attributes/parts first; if nothing matches
there, every other configured product is searched. getByKey() matches an attribute's
identifier; getById() on xpv.articles matches a part's identifier. All comparisons are
exact and case-sensitive, and every lookup returns null (or [] for getAll()) instead of
throwing when nothing matches or the underlying payload section is missing or empty.
xpv.articles.getAll() returns the current product's parts flattened, unlike getAllParts()
which returns every configured product's parts, grouped by product.
Connectors
The xpv.connectors.getByName() method returns any type of connector based on what is configured in the Backoffice.
type AdapterContract = {
getName(): string,
};
Spreadsheet Connectors
All spreadsheet connectors (Spreadsheet, CSV Zip) have the same API.
type SpreadsheetAdapter = AdapterContract & {
getSheetByName(name: null|string): SpreadsheetAdapterContract,
rangeToArray(range: string): any[][],
};
HTTP Connector
type HttpAdapter = AdapterContract & {
setHeaders(headers: { [key: string]: string }): HttpAdapter,
setHeader(key: string, value: string): HttpAdapter,
setPathParams(params: { [key: string]: string }): HttpAdapter,
setPathParam(key: string, value: string): HttpAdapter,
request(parameters: { [key: string]: string }): HttpResponse,
};
type HttpResponse = {
body(): string,
json(): object|any,
status(): int,
header(header: string): null|string,
headers(): { [key: string]: string },
};
HTTP Feed Connector
type HttpFeedAdapter = AdapterContract & {
get(): HttpResponse,
};