diff --git a/src/content/queries/cs-product.js b/src/content/queries/cs-product.js index a8f1d32..975959f 100644 --- a/src/content/queries/cs-product.js +++ b/src/content/queries/cs-product.js @@ -19,14 +19,6 @@ import { gql, parseRating, parseSpecialToDate } from '../../utils/product.js'; * @returns {Product} */ export const adapter = (config, productData) => { - let minPrice = productData.priceRange?.minimum ?? productData.price; - let maxPrice = productData.priceRange?.maximum ?? productData.price; - - if (minPrice == null) { - minPrice = maxPrice; - } else if (maxPrice == null) { - maxPrice = minPrice; - } /** @type {Product} */ const product = { sku: productData.sku, @@ -45,7 +37,12 @@ export const adapter = (config, productData) => { attributes: productData.attributes ?? [], attributeMap: Object.fromEntries((productData.attributes ?? []) .map(({ name, value }) => [name, value])), - options: (productData.options ?? []).map((option) => ({ + // eslint-disable-next-line no-underscore-dangle + type: productData.__typename === 'SimpleProductView' ? 'simple' : 'complex', + }; + + if (productData.options) { + product.options = productData.options.map((option) => ({ id: option.id, label: option.title, // eslint-disable-next-line no-underscore-dangle @@ -62,7 +59,7 @@ export const adapter = (config, productData) => { ? { sku: value.product.sku, name: value.product.name, - prices: value.product.price ? { + price: value.product.price ? { regular: value.product.price.regular, final: value.product.price.final, visible: value.product.price.roles?.includes('visible'), @@ -72,25 +69,31 @@ export const adapter = (config, productData) => { quantity: value.quantity, isDefault: value.isDefault, })), - })), - prices: (minPrice && maxPrice) ? { - regular: { - // TODO: determine whether to use min or max - amount: minPrice.regular.amount.value, - currency: minPrice.regular.amount.currency, - maximumAmount: maxPrice.regular.amount.value, - minimumAmount: minPrice.regular.amount.value, + })); + } + + if (productData.price) { + product.price = { + regular: productData.price.regular, + final: productData.price.final, + visible: productData.price.roles?.includes('visible'), + }; + } + + if (productData.priceRange) { + product.priceRange = { + minimum: { + regular: productData.priceRange.minimum.regular, + final: productData.priceRange.minimum.final, + visible: productData.priceRange.minimum.roles?.includes('visible'), }, - final: { - // TODO: determine whether to use min or max - amount: minPrice.final.amount.value, - currency: minPrice.final.amount.currency, - maximumAmount: maxPrice.final.amount.value, - minimumAmount: minPrice.final.amount.value, + maximum: { + regular: productData.priceRange.maximum.regular, + final: productData.priceRange.maximum.final, + visible: productData.priceRange.maximum.roles?.includes('visible'), }, - visible: minPrice.roles?.includes('visible'), - } : null, - }; + }; + } if (config.attributeOverrides?.product) { Object.entries(config.attributeOverrides.product).forEach(([key, value]) => { @@ -122,6 +125,7 @@ export default ({ sku, imageRoles = [] }) => gql`{ products( skus: ["${sku}"] ) { + __typename id sku name diff --git a/src/content/queries/cs-variants.js b/src/content/queries/cs-variants.js index a380dce..1f8a5fe 100644 --- a/src/content/queries/cs-variants.js +++ b/src/content/queries/cs-variants.js @@ -19,9 +19,6 @@ import { gql, parseRating, parseSpecialToDate } from '../../utils/product.js'; * @returns {Variant[]} */ export const adapter = (config, variants) => variants.map(({ selections, product }) => { - const minPrice = product.priceRange?.minimum ?? product.price; - const maxPrice = product.priceRange?.maximum ?? product.price; - /** @type {Variant} */ const variant = { name: product.name, @@ -34,25 +31,34 @@ export const adapter = (config, variants) => variants.map(({ selections, product attributeMap: Object.fromEntries((product.attributes ?? []) .map(({ name, value }) => [name, value])), externalId: product.externalId, - prices: { - regular: { - // TODO: determine whether to use min or max - amount: minPrice?.regular.amount.value, - currency: minPrice?.regular.amount.currency, - maximumAmount: maxPrice?.regular.amount.value, - minimumAmount: minPrice?.regular.amount.value, - }, - final: { - // TODO: determine whether to use min or max - amount: minPrice?.final.amount.value, - currency: minPrice?.final.amount.currency, - maximumAmount: maxPrice?.final.amount.value, - minimumAmount: minPrice?.final.amount.value, - }, - }, selections: (selections ?? []).sort(), + // eslint-disable-next-line no-underscore-dangle + type: product.__typename === 'SimpleProductView' ? 'simple' : 'complex', }; + if (product.price) { + variant.price = { + regular: product.price.regular, + final: product.price.final, + visible: product.price.roles?.includes('visible'), + }; + } + + if (product.priceRange) { + variant.priceRange = { + minimum: { + regular: product.priceRange.minimum.regular, + final: product.priceRange.minimum.final, + visible: product.priceRange.minimum.roles?.includes('visible'), + }, + maximum: { + regular: product.priceRange.maximum.regular, + final: product.priceRange.maximum.final, + visible: product.priceRange.maximum.roles?.includes('visible'), + }, + }; + } + if (config.attributeOverrides?.variant) { Object.entries(config.attributeOverrides.variant).forEach(([key, value]) => { variant.attributeMap[key] = variant.attributeMap[value] ?? variant[key]; @@ -84,6 +90,7 @@ export default ({ sku, imageRoles = [] }) => gql` variants { selections product { + __typename name sku inStock diff --git a/src/templates/html/HTMLTemplate.js b/src/templates/html/HTMLTemplate.js index 3fcfaee..86cd39f 100644 --- a/src/templates/html/HTMLTemplate.js +++ b/src/templates/html/HTMLTemplate.js @@ -38,7 +38,7 @@ export class HTMLTemplate { * @param {number|undefined} max * @returns {string} */ - static priceRange = (min, max) => (min !== max ? ` (${min} - ${max})` : ''); + static priceRange = (min, max) => (min !== max ? `${min} - ${max}` : `${min}`); /** * @param {string} str @@ -108,7 +108,7 @@ ${HTMLTemplate.metaName('twitter:card', 'summary_large_image')} ${HTMLTemplate.metaName('twitter:title', product.name)} ${HTMLTemplate.metaName('twitter:description', product.metaDescription)} ${HTMLTemplate.metaName('twitter:label1', 'Price')} -${HTMLTemplate.metaName('twitter:data1', product.prices?.final?.amount)} +${HTMLTemplate.metaName('twitter:data1', product.price?.final?.amount?.value ?? product.priceRange?.minimum?.final?.amount?.value)} ${HTMLTemplate.metaName('twitter:label2', 'Availability')} ${HTMLTemplate.metaName('twitter:data2', product.inStock ? 'In stock' : 'Out of stock')}`; } @@ -126,8 +126,9 @@ ${HTMLTemplate.metaName('externalId', product.externalId)} ${HTMLTemplate.metaName('addToCartAllowed', product.addToCartAllowed)} ${HTMLTemplate.metaName('inStock', product.inStock ? 'true' : 'false')} ${HTMLTemplate.metaProperty('product:availability', product.inStock ? 'In stock' : 'Out of stock')} -${HTMLTemplate.metaProperty('product:price.amount', product.prices?.final?.amount)} -${HTMLTemplate.metaProperty('product:price.currency', product.prices?.final?.currency)}`; +${HTMLTemplate.metaProperty('product:price.amount', product.price?.final?.amount?.value ?? product.priceRange?.minimum?.final?.amount?.value)} +${HTMLTemplate.metaProperty('product:price.currency', product.price?.final?.amount?.currency ?? product.priceRange?.minimum?.final?.amount?.currency)} +${HTMLTemplate.metaProperty('product:type', product.type)}`; } /** @@ -169,6 +170,38 @@ ${HTMLTemplate.indent(this.renderJSONLD(), 2)} `; } + /** + * Render product price block + * @param {Product} product + * @returns {string} + */ + renderProductPrices(product) { + const { price, priceRange } = product; + const hasFinal = price + ? price.final?.amount?.value < price.regular?.amount?.value + : priceRange?.minimum?.final.amount.value < priceRange?.minimum?.regular.amount.value; + const isRange = !!priceRange + && priceRange.minimum?.final?.amount?.value !== priceRange.maximum?.final?.amount?.value; + + const regularPrice = price ? price.regular?.amount : priceRange.minimum?.regular?.amount; + const finalPrice = price ? price.final?.amount : priceRange.minimum?.final?.amount; + + return /* html */ `\ +
+
+
Regular
+
${regularPrice.value} ${regularPrice.currency}
+ ${isRange ? `
${priceRange.maximum?.regular?.amount?.value} ${priceRange.maximum?.regular?.amount?.currency}
` : ''} +
+ ${hasFinal ? /* html */ `\ +
+
${finalPrice.value} ${finalPrice.currency}
+
${price ? price.final?.amount?.value : priceRange.minimum?.final?.amount?.value}
+ ${isRange ? `
${priceRange.maximum?.final?.amount?.value} ${priceRange.maximum?.final?.amount?.currency}
` : ''} +
` : ''} +
`; + } + /** * Create the product images * @param {Image[]} images @@ -235,7 +268,7 @@ ${attributes.map((attr) => /* html */`\ * @returns {string} */ renderProductOptions(options) { - return options.length > 0 ? /* html */ `\ + return options?.length > 0 ? /* html */ `\
${options.map((opt) => /* html */ `\
@@ -302,13 +335,23 @@ ${HTMLTemplate.indent(this.renderProductItems(opt.items), 2)}`).join('\n')} /** * Create the variant prices - * @param {Pick} prices + * @param {Variant} variant * @returns {string} */ - renderVariantPrices(prices) { - return /* html */ `\ -
Regular: ${prices.regular?.amount} ${prices.regular?.currency}${HTMLTemplate.priceRange(prices.regular?.minimumAmount, prices.regular?.maximumAmount)}
-
Final: ${prices.final?.amount} ${prices.final?.currency}${HTMLTemplate.priceRange(prices.final?.minimumAmount, prices.final?.maximumAmount)}
`; + renderVariantPrices(variant) { + if (variant.price) { + const { price } = variant; + return /* html */ `\ +
Regular: ${price.regular?.amount?.value} ${price.regular?.amount?.currency}
+
Final: ${price.final?.amount?.value} ${price.final?.amount?.currency}
`; + } + if (variant.priceRange) { + const { minimum, maximum } = variant.priceRange; + return /* html */ `\ +
Regular: ${HTMLTemplate.priceRange(minimum?.regular?.amount?.value, maximum?.regular?.amount?.value)} ${minimum?.regular?.amount?.currency}
+
Final: ${HTMLTemplate.priceRange(minimum?.final?.amount?.value, maximum?.final?.amount?.value)} ${minimum?.final?.amount?.currency}
`; + } + return ''; } /** @@ -328,7 +371,7 @@ ${this.variants.map((v) => /* html */`\
${v.name}
${v.description}
${v.inStock ? 'inStock' : ''}
-${v.prices ? HTMLTemplate.indent(this.renderVariantPrices(v.prices), 4) : ''} +${v.price || v.priceRange ? HTMLTemplate.indent(this.renderVariantPrices(v), 4) : ''}
${HTMLTemplate.indent(this.renderVariantImages(v.images), 6)}
@@ -389,6 +432,7 @@ ${HTMLTemplate.indent(this.renderHead(), 2)}

${name}

${description ? `

${description}

` : ''} ${HTMLTemplate.indent(this.renderProductImages(images), 8)} +${HTMLTemplate.indent(this.renderProductPrices(this.product), 8)} ${HTMLTemplate.indent(this.renderProductAttributes(attributes), 8)} ${HTMLTemplate.indent(this.renderProductOptions(options), 8)} ${HTMLTemplate.indent(this.renderProductVariants(), 8)} diff --git a/src/templates/json/JSONTemplate.js b/src/templates/json/JSONTemplate.js index 1285aee..de8c129 100644 --- a/src/templates/json/JSONTemplate.js +++ b/src/templates/json/JSONTemplate.js @@ -126,11 +126,21 @@ export class JSONTemplate { const configurableProduct = this.variants?.length > 0; const offers = configurableProduct ? this.variants : [this.product]; return offers.map((v) => { - const { prices: variantPrices } = v; const offerUrl = this.constructProductURL(configurableProduct ? v : undefined); const mpn = this.constructMPN(configurableProduct ? v : undefined); - const finalPrice = variantPrices?.final?.amount; - const regularPrice = variantPrices?.regular?.amount; + + let finalPrice; + let regularPrice; + let currency; + if (v.price) { + finalPrice = v.price.final.amount.value; + regularPrice = v.price.regular.amount.value; + currency = v.price.final.amount.currency; + } else if (v.priceRange) { + finalPrice = v.priceRange.minimum.final.amount.value; + regularPrice = v.priceRange.minimum.regular.amount.value; + currency = v.priceRange.minimum.final.amount.currency; + } const offer = { '@type': 'Offer', @@ -140,16 +150,14 @@ export class JSONTemplate { image: v.images?.[0]?.url ?? image, availability: v.inStock ? 'InStock' : 'OutOfStock', price: finalPrice, - priceCurrency: variantPrices?.final?.currency, + priceCurrency: currency, gtin: v.attributeMap.gtin, priceValidUntil: v.specialToDate, aggregateRating: this.renderRating(v), }; - if (variantPrices) { - if (finalPrice < regularPrice) { - offer.priceSpecification = this.renderOffersPriceSpecification(v); - } + if (finalPrice < regularPrice) { + offer.priceSpecification = this.renderOffersPriceSpecification(regularPrice, currency); } return pruneUndefined(offer); @@ -157,14 +165,14 @@ export class JSONTemplate { } /** - * @param {Variant} variant + * @param {number} regularPrice + * @param {string} currency */ - renderOffersPriceSpecification(variant) { - const { prices: { regular: { amount, currency } } } = variant; + renderOffersPriceSpecification(regularPrice, currency) { return { '@type': 'UnitPriceSpecification', priceType: 'https://schema.org/ListPrice', - price: amount, + price: regularPrice, priceCurrency: currency, }; } diff --git a/src/types.d.ts b/src/types.d.ts index 8d2d073..17b0a4a 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -185,15 +185,20 @@ declare global { metaTitle?: string; description?: string; images: Image[]; - prices: Prices; attributes: Attribute[]; - options: ProductOption[]; + options?: ProductOption[]; url?: string; urlKey?: string; externalId?: string; variants?: Variant[]; // variants exist on products in helix commerce but not on magento specialToDate?: string; - rating?: Rating + rating?: Rating; + price?: Price; + priceRange?: { + minimum: Price; + maximum: Price; + } + type: 'simple' | 'complex'; // not handled currently: externalParentId?: string; @@ -211,13 +216,18 @@ declare global { url: string; inStock: boolean; images: Image[]; - prices: Pick; selections: string[]; attributes: Attribute[]; externalId: string; specialToDate?: string; gtin?: string; rating?: Rating; + price?: Price; + priceRange?: { + minimum: Price; + maximum: Price; + } + type: 'simple' | 'complex'; // internal use: attributeMap: Record; @@ -242,16 +252,18 @@ declare global { } interface Price { - amount?: number; - currency?: string; - maximumAmount?: number; - minimumAmount?: number; - variant?: 'default' | 'strikethrough'; - } - - interface Prices { - regular: Price; - final: Price; + regular: { + amount: { + value: number; + currency: string; + } + }, + final: { + amount: { + value: number; + currency: string; + } + }, visible: boolean; } @@ -278,7 +290,7 @@ declare global { product?: { name: string; sku: string; - prices?: Prices; + price?: Price; }; }