diff --git a/.changeset/happy-poets-wait.md b/.changeset/happy-poets-wait.md new file mode 100644 index 0000000..2faf674 --- /dev/null +++ b/.changeset/happy-poets-wait.md @@ -0,0 +1,13 @@ +--- +"saleor-app-products-feed": minor +--- + +Updated pricing attributes according to the Google guidelines. + +Was: +- Price: base or discounted price + +Now: + +- Price: always the base price. Attribute skipped if amount is equal to 0. +- Sale price: discounted price. Attribute skipped if value is the same as base price diff --git a/apps/products-feed/graphql/fragments/GoogleFeedProductVariantFragment.graphql b/apps/products-feed/graphql/fragments/GoogleFeedProductVariantFragment.graphql index e6107a1..2ec4d2f 100644 --- a/apps/products-feed/graphql/fragments/GoogleFeedProductVariantFragment.graphql +++ b/apps/products-feed/graphql/fragments/GoogleFeedProductVariantFragment.graphql @@ -3,6 +3,12 @@ fragment GoogleFeedProductVariant on ProductVariant { name sku pricing { + priceUndiscounted{ + gross { + currency + amount + } + } price { gross { currency diff --git a/apps/products-feed/src/modules/google-feed/generate-google-xml-feed.test.ts b/apps/products-feed/src/modules/google-feed/generate-google-xml-feed.test.ts index edc8685..d768a7b 100644 --- a/apps/products-feed/src/modules/google-feed/generate-google-xml-feed.test.ts +++ b/apps/products-feed/src/modules/google-feed/generate-google-xml-feed.test.ts @@ -29,6 +29,14 @@ const priceBase: GoogleFeedProductVariantFragment["pricing"] = { currency: "USD", }, }, + priceUndiscounted: { + __typename: "TaxedMoney", + gross: { + __typename: "Money", + amount: 2, + currency: "USD", + }, + }, }; describe("generateGoogleXmlFeed", () => { @@ -78,7 +86,8 @@ describe("generateGoogleXmlFeed", () => { Category Name 1 https://example.com/p/product-slug - 1.00 USD + 2.00 USD + 1.00 USD sku2 @@ -89,7 +98,8 @@ describe("generateGoogleXmlFeed", () => { Category Name 1 https://example.com/p/product-slug - 1.00 USD + 2.00 USD + 1.00 USD " diff --git a/apps/products-feed/src/modules/google-feed/generate-google-xml-feed.ts b/apps/products-feed/src/modules/google-feed/generate-google-xml-feed.ts index 283792e..3c95781 100644 --- a/apps/products-feed/src/modules/google-feed/generate-google-xml-feed.ts +++ b/apps/products-feed/src/modules/google-feed/generate-google-xml-feed.ts @@ -5,6 +5,7 @@ import { shopDetailsToProxy } from "./shop-details-to-proxy"; import { EditorJsPlaintextRenderer } from "../editor-js/editor-js-plaintext-renderer"; import { RootConfig } from "../app-configuration/app-config"; import { getMappedAttributes } from "./attribute-mapping"; +import { priceMapping } from "./price-mapping"; interface GenerateGoogleXmlFeedArgs { productVariants: GoogleFeedProductVariantFragment[]; @@ -15,22 +16,6 @@ interface GenerateGoogleXmlFeedArgs { shopDescription?: string; } -/** - * Price format has to be altered from the en format to the one expected by Google - * eg. 1.00 USD, 5.00 PLN - */ -const formatCurrency = (currency: string, amount: number) => { - return ( - new Intl.NumberFormat("en-EN", { - useGrouping: false, - minimumFractionDigits: 2, - style: "decimal", - currencyDisplay: "code", - currency: currency, - }).format(amount) + ` ${currency}` - ); -}; - export const generateGoogleXmlFeed = ({ attributeMapping, productVariants, @@ -45,10 +30,7 @@ export const generateGoogleXmlFeed = ({ variant, }); - const currency = variant.pricing?.price?.gross.currency; - const amount = variant.pricing?.price?.gross.amount; - - const price = currency ? formatCurrency(currency, amount!) : undefined; + const pricing = priceMapping({ pricing: variant.pricing }); return productToProxy({ storefrontUrlTemplate: productStorefrontUrl, @@ -62,13 +44,13 @@ export const generateGoogleXmlFeed = ({ variant.quantityAvailable && variant.quantityAvailable > 0 ? "in_stock" : "out_of_stock", category: variant.product.category?.name || "unknown", googleProductCategory: variant.product.category?.googleCategoryId || "", - price: price, imageUrl: variant.product.thumbnail?.url || "", material: attributes?.material, color: attributes?.color, brand: attributes?.brand, pattern: attributes?.pattern, size: attributes?.size, + ...pricing, }); }); diff --git a/apps/products-feed/src/modules/google-feed/price-mapping.test.ts b/apps/products-feed/src/modules/google-feed/price-mapping.test.ts new file mode 100644 index 0000000..d12a2d3 --- /dev/null +++ b/apps/products-feed/src/modules/google-feed/price-mapping.test.ts @@ -0,0 +1,60 @@ +import { describe, expect, it } from "vitest"; +import { priceMapping } from "./price-mapping"; + +describe("priceMapping", () => { + it("Return undefined, when no pricing available", () => { + expect( + priceMapping({ + pricing: undefined, + }) + ).toStrictEqual(undefined); + }); + it("Return undefined, when amount is equal to 0", () => { + expect( + priceMapping({ + pricing: { + priceUndiscounted: { + gross: { + amount: 0, + currency: "USD", + }, + }, + }, + }) + ).toStrictEqual(undefined); + }); + it("Return formatted base price, when there is no sale", () => { + expect( + priceMapping({ + pricing: { + priceUndiscounted: { + gross: { + amount: 10.5, + currency: "USD", + }, + }, + }, + }) + ).toStrictEqual({ price: "10.50 USD" }); + }); + it("Return formatted base and sale prices, when there is a sale", () => { + expect( + priceMapping({ + pricing: { + priceUndiscounted: { + gross: { + amount: 10.5, + currency: "USD", + }, + }, + price: { + gross: { + amount: 5.25, + currency: "USD", + }, + }, + }, + }) + ).toStrictEqual({ price: "10.50 USD", salePrice: "5.25 USD" }); + }); +}); diff --git a/apps/products-feed/src/modules/google-feed/price-mapping.ts b/apps/products-feed/src/modules/google-feed/price-mapping.ts new file mode 100644 index 0000000..cb0ecea --- /dev/null +++ b/apps/products-feed/src/modules/google-feed/price-mapping.ts @@ -0,0 +1,56 @@ +import { GoogleFeedProductVariantFragment } from "../../../generated/graphql"; + +/** + * Price format has to be altered from the en format to the one expected by Google + * eg. 1.00 USD, 5.00 PLN + */ +const formatCurrency = (currency: string, amount: number) => { + return ( + new Intl.NumberFormat("en-EN", { + useGrouping: false, + minimumFractionDigits: 2, + style: "decimal", + currencyDisplay: "code", + currency: currency, + }).format(amount) + ` ${currency}` + ); +}; + +interface priceMappingArgs { + pricing: GoogleFeedProductVariantFragment["pricing"]; +} + +/* + * Maps variant pricing to Google Feed format. + * https://support.google.com/merchants/answer/6324371 + */ +export const priceMapping = ({ pricing }: priceMappingArgs) => { + const priceUndiscounted = pricing?.priceUndiscounted?.gross; + + // Pricing should not be submitted empty or with 0 value + if (!priceUndiscounted?.amount) { + return; + } + + // Price attribute is expected to be a base price + const formattedUndiscountedPrice = formatCurrency( + priceUndiscounted.currency, + priceUndiscounted.amount + ); + + const discountedPrice = pricing?.price?.gross; + + // Return early if there is no sale + if (!discountedPrice || discountedPrice?.amount === priceUndiscounted.amount) { + return { + price: formattedUndiscountedPrice, + }; + } + + const formattedDiscountedPrice = formatCurrency(discountedPrice.currency, discountedPrice.amount); + + return { + price: formattedUndiscountedPrice, + salePrice: formattedDiscountedPrice, + }; +}; diff --git a/apps/products-feed/src/modules/google-feed/product-to-proxy.ts b/apps/products-feed/src/modules/google-feed/product-to-proxy.ts index 23571af..77f8e16 100644 --- a/apps/products-feed/src/modules/google-feed/product-to-proxy.ts +++ b/apps/products-feed/src/modules/google-feed/product-to-proxy.ts @@ -109,6 +109,16 @@ export const productToProxy = (p: ProductEntry) => { }); } + if (p.salePrice?.length) { + item.push({ + "g:sale_price": [ + { + "#text": p.salePrice, + }, + ], + }); + } + if (p.material) { item.push({ "g:material": [ diff --git a/apps/products-feed/src/modules/google-feed/types.ts b/apps/products-feed/src/modules/google-feed/types.ts index c316844..9ae378a 100644 --- a/apps/products-feed/src/modules/google-feed/types.ts +++ b/apps/products-feed/src/modules/google-feed/types.ts @@ -9,6 +9,7 @@ export type ProductEntry = { imageUrl?: string; condition?: "new" | "refurbished" | "used"; price?: string; + salePrice?: string; googleProductCategory?: string; availability: "in_stock" | "out_of_stock" | "preorder" | "backorder"; category: string;