fix: Use region name in product pricing exports (#8373)

* fix:Bug fixes to product import

* fix:Add an export failed notification if an export fails

* fix: Use region name in product export prices
This commit is contained in:
Stevche Radevski
2024-07-31 15:34:27 +02:00
committed by GitHub
parent 12c6a1a022
commit 31449972ed
8 changed files with 157 additions and 22 deletions
@@ -0,0 +1,2 @@
Product Id,Product Title,Product Subtitle,Product Status,Product External Id,Product Description,Product Handle,Product Is Giftcard,Product Discountable,Product Thumbnail,Product Collection Id,Product Type Id,Product Weight,Product Length,Product Height,Product Width,Product Hs Code,Product Origin Country,Product Mid Code,Product Material,Product Created At,Product Updated At,Product Deleted At,Product Image 1,Product Image 2,Product Tag 1,Product Tag 2,Variant Id,Variant Title,Variant Sku,Variant Barcode,Variant Ean,Variant Upc,Variant Allow Backorder,Variant Manage Inventory,Variant Hs Code,Variant Origin Country,Variant Mid Code,Variant Material,Variant Weight,Variant Length,Variant Height,Variant Width,Variant Metadata,Variant Variant Rank,Variant Product Id,Variant Created At,Variant Updated At,Variant Deleted At,Variant Price USD,Variant Price Test Region [USD],Variant Option 1 Name,Variant Option 1 Value,Variant Option 2 Name,Variant Option 2 Value
prod_01J44BH2MFG9MM3EPEQ1SEBDCV,Product with prices,,draft,,test-product-description,product-with-prices,false,true,test-image.png,,,,,,,,,,,2024-07-31T12:16:37.517Z,2024-07-31T12:16:37.517Z,,test-image.png,test-image-2.png,123,456,variant_01J44BH2MXDYM80A5VCAMX17XH,Test variant,,,,,false,true,,,,,,,,,,0,prod_01J44BH2MFG9MM3EPEQ1SEBDCV,2024-07-31T12:16:37.533Z,2024-07-31T12:16:37.533Z,,100,45,size,large,color,green
1 Product Id Product Title Product Subtitle Product Status Product External Id Product Description Product Handle Product Is Giftcard Product Discountable Product Thumbnail Product Collection Id Product Type Id Product Weight Product Length Product Height Product Width Product Hs Code Product Origin Country Product Mid Code Product Material Product Created At Product Updated At Product Deleted At Product Image 1 Product Image 2 Product Tag 1 Product Tag 2 Variant Id Variant Title Variant Sku Variant Barcode Variant Ean Variant Upc Variant Allow Backorder Variant Manage Inventory Variant Hs Code Variant Origin Country Variant Mid Code Variant Material Variant Weight Variant Length Variant Height Variant Width Variant Metadata Variant Variant Rank Variant Product Id Variant Created At Variant Updated At Variant Deleted At Variant Price USD Variant Price Test Region [USD] Variant Option 1 Name Variant Option 1 Value Variant Option 2 Name Variant Option 2 Value
2 prod_01J44BH2MFG9MM3EPEQ1SEBDCV Product with prices draft test-product-description product-with-prices false true test-image.png 2024-07-31T12:16:37.517Z 2024-07-31T12:16:37.517Z test-image.png test-image-2.png 123 456 variant_01J44BH2MXDYM80A5VCAMX17XH Test variant false true 0 prod_01J44BH2MFG9MM3EPEQ1SEBDCV 2024-07-31T12:16:37.533Z 2024-07-31T12:16:37.533Z 100 45 size large color green
@@ -17,6 +17,7 @@ const compareCSVs = async (filePath, expectedFilePath) => {
let fixturesContent = await fs.readFile(expectedFilePath, {
encoding: "utf-8",
})
await fs.rm(path.dirname(asLocalPath), { recursive: true, force: true })
// Normalize csv data to get rid of dynamic data
const idsToReplace = ["prod_", "pcol_", "variant_", "ptyp_"]
@@ -47,6 +48,7 @@ medusaIntegrationTestRunner({
let publishedCollection
let baseType
let baseRegion
let eventBus: IEventBusModuleService
beforeAll(async () => {
@@ -56,6 +58,17 @@ medusaIntegrationTestRunner({
beforeEach(async () => {
await createAdminUser(dbConnection, adminHeaders, getContainer())
baseRegion = (
await api.post(
"/admin/regions",
{
name: "Test region",
currency_code: "USD",
},
adminHeaders
)
).data.region
baseCollection = (
await api.post(
"/admin/collections",
@@ -196,10 +209,64 @@ medusaIntegrationTestRunner({
notifications[0].data.file.url,
path.join(__dirname, "__fixtures__", "exported-products-comma.csv")
)
await fs.rm(path.dirname(notifications[0].data.file.url), {
force: true,
recursive: true,
})
})
it("should export a csv file with region prices", async () => {
const subscriberExecution = TestEventUtils.waitSubscribersExecution(
"notification.notification.created",
eventBus
)
const productWithRegionPrices = (
await api.post(
"/admin/products",
getProductFixture({
title: "Product with prices",
variants: [
{
title: "Test variant",
prices: [
{
currency_code: "usd",
amount: 100,
},
{
currency_code: "usd",
rules: {
region_id: baseRegion.id,
},
amount: 45,
},
],
options: {
size: "large",
color: "green",
},
},
],
}),
adminHeaders
)
).data.product
const batchJobRes = await api.post(
"/admin/products/export?id=" + productWithRegionPrices.id,
{},
adminHeaders
)
const transactionId = batchJobRes.data.transaction_id
expect(transactionId).toBeTruthy()
await subscriberExecution
const notifications = (
await api.get("/admin/notifications", adminHeaders)
).data.notifications
await compareCSVs(
notifications[0].data.file.url,
path.join(__dirname, "__fixtures__", "prices-with-region.csv")
)
})
it("should export a csv file filtered by specific products", async () => {
@@ -229,11 +296,6 @@ medusaIntegrationTestRunner({
notifications[0].data.file.url,
path.join(__dirname, "__fixtures__", "filtered-products.csv")
)
await fs.rm(path.dirname(notifications[0].data.file.url), {
force: true,
recursive: true,
})
})
})
},
@@ -654,7 +654,7 @@ medusaIntegrationTestRunner({
title: "Test variant",
sku: "test-sku-1-1",
barcode: "test-barcode-1-1",
allow_backorder: true,
allow_backorder: false,
manage_inventory: true,
prices: [
expect.objectContaining({
@@ -1,10 +1,19 @@
import { BigNumberInput, HttpTypes, PricingTypes } from "@medusajs/types"
import { upperCaseFirst } from "@medusajs/utils"
import {
RegionTypes,
BigNumberInput,
HttpTypes,
PricingTypes,
} from "@medusajs/types"
import { MedusaError, upperCaseFirst } from "@medusajs/utils"
// We want to have one row per variant, so we need to normalize the data
export const normalizeForExport = (
product: HttpTypes.AdminProduct[]
product: HttpTypes.AdminProduct[],
{ regions }: { regions: RegionTypes.RegionDTO[] }
): object[] => {
// Currently region names are treated as case-insensitive.
const regionsMap = new Map(regions.map((r) => [r.id, r]))
const res = product.reduce((acc: object[], product) => {
const variants = product.variants ?? []
if (!variants.length) {
@@ -15,7 +24,7 @@ export const normalizeForExport = (
variants.forEach((v) => {
const toPush = {
...normalizeProductForExport(product),
...normalizeVariantForExport(v),
...normalizeVariantForExport(v, regionsMap),
} as any
delete toPush["Product Variants"]
@@ -81,16 +90,31 @@ const normalizeProductForExport = (product: HttpTypes.AdminProduct): object => {
const normalizeVariantForExport = (
variant: HttpTypes.AdminProductVariant & {
price_set?: PricingTypes.PriceSetDTO
}
},
regionsMap: Map<string, RegionTypes.RegionDTO>
): object => {
const flattenedPrices = variant.price_set?.prices
?.sort((a, b) => b.currency_code!.localeCompare(a.currency_code!))
.reduce((acc: Record<string, BigNumberInput>, price) => {
const regionRule = price.price_rules?.find(
(r) => r.attribute === "region"
(r) => r.attribute === "region_id"
)
if (regionRule) {
acc[beautifyKey(`variant_price_${regionRule.value}`)] = price.amount!
const region = regionsMap.get(regionRule?.value!)
if (!region) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`Region with id ${regionRule?.value} not found`
)
}
const regionKey = `variant_price_${region.name
.toLowerCase()
.split(" ")
.join("_")}_[${region.currency_code.toUpperCase()}]`
acc[beautifyKey(regionKey)] = price.amount!
} else if (!price.price_rules?.length) {
acc[
beautifyKey(`variant_price_${price.currency_code!.toUpperCase()}`)
@@ -70,6 +70,7 @@ const variantFieldsToOmit = new Map([["variant_product_id", true]])
// These fields can have a numeric value, but they are stored as string in the DB so we need to normalize them
const stringFields = [
"product_tag_",
"variant_option_",
"variant_barcode",
"variant_sku",
"variant_ean",
@@ -78,6 +79,12 @@ const stringFields = [
"variant_mid_code",
]
const booleanFields = [
"product_discountable",
"variant_manage_inventory",
"variant_allow_backorder",
]
const normalizeProductForImport = (
rawProduct: object
): HttpTypes.AdminCreateProduct => {
@@ -221,9 +228,20 @@ const normalizeVariantForImport = (
}
const getNormalizedValue = (key: string, value: any): any => {
return stringFields.some((field) => key.startsWith(field))
let res = stringFields.some((field) => key.startsWith(field))
? value?.toString()
: value
if (booleanFields.some((field) => key.startsWith(field))) {
if (value === "TRUE") {
res = true
}
if (value === "FALSE") {
res = false
}
}
return res
}
const snakecaseKey = (key: string): string => {
@@ -1,4 +1,8 @@
import { IFileModuleService, HttpTypes } from "@medusajs/types"
import {
IFileModuleService,
HttpTypes,
IRegionModuleService,
} from "@medusajs/types"
import { ModuleRegistrationName, convertJsonToCsv } from "@medusajs/utils"
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
import { normalizeForExport } from "../helpers/normalize-for-export"
@@ -7,7 +11,16 @@ export const generateProductCsvStepId = "generate-product-csv"
export const generateProductCsvStep = createStep(
generateProductCsvStepId,
async (products: HttpTypes.AdminProduct[], { container }) => {
const normalizedData = normalizeForExport(products)
const regionService = container.resolve<IRegionModuleService>(
ModuleRegistrationName.REGION
)
const regions = await regionService.listRegions(
{},
{ select: ["id", "name", "currency_code"], take: null }
)
const normalizedData = normalizeForExport(products, { regions })
const csvContent = convertJsonToCsv(normalizedData)
const fileModule: IFileModuleService = container.resolve(
@@ -6,7 +6,7 @@ import {
import { WorkflowTypes } from "@medusajs/types"
import { generateProductCsvStep, getAllProductsStep } from "../steps"
import { useRemoteQueryStep } from "../../common"
import { sendNotificationsStep } from "../../notification"
import { notifyOnFailureStep, sendNotificationsStep } from "../../notification"
export const exportProductsWorkflowId = "export-products"
export const exportProductsWorkflow = createWorkflow(
@@ -19,6 +19,22 @@ export const exportProductsWorkflow = createWorkflow(
backgroundExecution: true,
})
const failureNotification = transform({ input }, (data) => {
return [
{
// We don't need the recipient here for now, but if we want to push feed notifications to a specific user we could add it.
to: "",
channel: "feed",
template: "admin-ui",
data: {
title: "Product export",
description: `Failed to export products, please try again later.`,
},
},
]
})
notifyOnFailureStep(failureNotification)
const file = generateProductCsvStep(products)
const fileDetails = useRemoteQueryStep({
fields: ["id", "url"],
@@ -1571,7 +1571,7 @@ export default class ProductModuleService
if (productData.handle && !isValidHandle(productData.handle)) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"Invalid product handle. It must contain URL safe characters"
`Invalid product handle '${productData.handle}'. It must contain URL safe characters`
)
}
}