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;