Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: setup price list import #2210

Merged
merged 20 commits into from
Sep 28, 2022
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
9eed9bc
fix: setup price list import
srindom Sep 14, 2022
2f6a8d0
Merge remote-tracking branch 'origin/develop' into feat/price-list-im…
srindom Sep 14, 2022
ebc1637
fix: unit test price list import
srindom Sep 16, 2022
bbf2ee2
Merge remote-tracking branch 'origin/develop' into feat/price-list-im…
srindom Sep 16, 2022
e623021
fix: add integration test for price list imports
srindom Sep 16, 2022
739fa97
fix: add integration test for price list imports
srindom Sep 16, 2022
75f81dc
fix: add test for failing parse
srindom Sep 16, 2022
dcd550d
fix: product import should take human price amounts
srindom Sep 16, 2022
03ba61b
Merge branch 'develop' into feat/price-list-import
srindom Sep 16, 2022
2171462
fix: update import test to use human amounts
srindom Sep 16, 2022
5e4edf2
Create lazy-apes-unite.md
srindom Sep 16, 2022
67246c5
Create green-snakes-return.md
srindom Sep 16, 2022
3ff7561
Merge remote-tracking branch 'origin/develop' into feat/price-list-im…
srindom Sep 20, 2022
8db458e
fix: pr feedback
srindom Sep 20, 2022
253423c
Merge branch 'develop' into feat/price-list-import
olivermrbl Sep 22, 2022
b9a3bd3
Merge branch 'develop' into feat/price-list-import
srindom Sep 26, 2022
844320b
Merge branch 'develop' into feat/price-list-import
srindom Sep 27, 2022
aebef2e
fix: cleanup unused props
srindom Sep 28, 2022
0f4f9a8
Merge remote-tracking branch 'origin/develop' into feat/price-list-im…
srindom Sep 28, 2022
a579131
Merge branch 'develop' into feat/price-list-import
olivermrbl Sep 28, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/green-snakes-return.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@medusajs/medusa": minor
---

Adds a BatchJob strategy for importing prices to PriceLists
5 changes: 5 additions & 0 deletions .changeset/lazy-apes-unite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"medusa-core-utils": minor
---

Adds `computerizeAmount` utility to convert human money format into the DB format Medusa uses (integer of lowest currency unit)
290 changes: 290 additions & 0 deletions integration-tests/api/__tests__/batch-jobs/price-list/import.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,290 @@
const fs = require("fs")
const path = require("path")

const setupServer = require("../../../../helpers/setup-server")
const { useApi } = require("../../../../helpers/use-api")
const { initDb, useDb } = require("../../../../helpers/use-db")

const adminSeeder = require("../../../helpers/admin-seeder")
const {
simpleRegionFactory,
simplePriceListFactory,
simpleProductFactory,
} = require("../../../factories")

const adminReqConfig = {
headers: {
Authorization: "Bearer test_token",
},
}

jest.setTimeout(1000000)

function cleanTempData() {
// cleanup tmp ops files
const opsFiles = path.resolve(
"__tests__",
"batch-jobs",
"price-list",
"imports"
)

fs.rmSync(opsFiles, { recursive: true, force: true })
}

function getImportFile() {
return path.resolve(
"__tests__",
"batch-jobs",
"price-list",
"price-list-import.csv"
)
}

function copyTemplateFile() {
const csvTemplate = path.resolve(
"__tests__",
"batch-jobs",
"price-list",
"price-list-import-template.csv"
)
const destination = getImportFile()
fs.copyFileSync(csvTemplate, destination)
}

describe("Price list import batch job", () => {
let medusaProcess
let dbConnection

beforeAll(async () => {
const cwd = path.resolve(path.join(__dirname, "..", "..", ".."))
dbConnection = await initDb({ cwd })

cleanTempData() // cleanup if previous process didn't manage to do it

medusaProcess = await setupServer({
cwd,
redisUrl: "redis://127.0.0.1:6379",
uploadDir: __dirname,
verbose: false,
})
})

afterAll(async () => {
const db = useDb()
await db.shutdown()

cleanTempData()

medusaProcess.kill()
})

beforeEach(async () => {
await adminSeeder(dbConnection)
})

afterEach(async () => {
const db = useDb()
await db.teardown()
})

it("should import a csv file", async () => {
jest.setTimeout(1000000)
const api = useApi()

copyTemplateFile()

const product = await simpleProductFactory(dbConnection, {
variants: [
{
id: "test-pl-variant",
},
{
id: "test-pl-sku-variant",
sku: "pl-sku",
},
],
})

await simpleRegionFactory(dbConnection, {
id: "test-pl-region",
name: "PL Region",
currency_code: "eur",
})

const priceList = await simplePriceListFactory(dbConnection, {
id: "pl_my_price_list",
name: "Test price list",
prices: [
{
variant_id: product.variants[0].id,
currency_code: "usd",
amount: 1000,
},
{
variant_id: product.variants[0].id,
currency_code: "eur",
amount: 2080,
},
],
})

const response = await api.post(
"/admin/batch-jobs",
{
type: "price-list-import",
context: {
price_list_id: priceList.id,
fileKey: "price-list-import.csv",
},
},
adminReqConfig
)

const batchJobId = response.data.batch_job.id

expect(batchJobId).toBeTruthy()

// Pull to check the status until it is completed
let batchJob
let shouldContinuePulling = true
while (shouldContinuePulling) {
const res = await api.get(
`/admin/batch-jobs/${batchJobId}`,
adminReqConfig
)

await new Promise((resolve, _) => {
setTimeout(resolve, 1000)
})

batchJob = res.data.batch_job

shouldContinuePulling = !(
batchJob.status === "completed" || batchJob.status === "failed"
)
}

expect(batchJob.status).toBe("completed")

const priceListRes = await api.get(
"/admin/price-lists/pl_my_price_list",
adminReqConfig
)

// Verify that file service deleted file
const importFilePath = getImportFile()
expect(fs.existsSync(importFilePath)).toBe(false)

expect(priceListRes.data.price_list.prices.length).toEqual(5)
expect(priceListRes.data.price_list.prices).toEqual(
expect.arrayContaining([
expect.objectContaining({
variant_id: "test-pl-variant",
currency_code: "usd",
amount: 1111,
}),
expect.objectContaining({
variant_id: "test-pl-variant",
currency_code: "eur",
region_id: "test-pl-region",
amount: 2222,
}),
expect.objectContaining({
variant_id: "test-pl-variant",
currency_code: "jpy",
amount: 3333,
}),
expect.objectContaining({
variant_id: "test-pl-sku-variant",
currency_code: "usd",
amount: 4444,
}),
expect.objectContaining({
variant_id: "test-pl-sku-variant",
currency_code: "eur",
region_id: "test-pl-region",
amount: 5555,
}),
])
)
})

it("should fail with invalid import format", async () => {
jest.setTimeout(1000000)
const api = useApi()

const product = await simpleProductFactory(dbConnection, {
variants: [
{ id: "test-pl-variant" },
{ id: "test-pl-sku-variant", sku: "pl-sku" },
],
})

await simpleRegionFactory(dbConnection, {
id: "test-pl-region",
name: "PL Region",
currency_code: "eur",
})

const priceList = await simplePriceListFactory(dbConnection, {
id: "pl_my_price_list",
name: "Test price list",
prices: [
{
variant_id: product.variants[0].id,
currency_code: "usd",
amount: 1000,
},
{
variant_id: product.variants[0].id,
currency_code: "eur",
amount: 2080,
},
],
})

const response = await api.post(
"/admin/batch-jobs",
{
type: "price-list-import",
context: {
price_list_id: priceList.id,
fileKey: "invalid-format.csv",
},
},
adminReqConfig
)

const batchJobId = response.data.batch_job.id

expect(batchJobId).toBeTruthy()

// Pull to check the status until it is completed
let batchJob
let shouldContinuePulling = true
while (shouldContinuePulling) {
const res = await api.get(
`/admin/batch-jobs/${batchJobId}`,
adminReqConfig
)

await new Promise((resolve, _) => {
setTimeout(resolve, 1000)
})

batchJob = res.data.batch_job

shouldContinuePulling = !(
batchJob.status === "completed" || batchJob.status === "failed"
)
}

expect(batchJob.status).toBe("failed")
expect(batchJob.result).toEqual({
errors: [
"The csv file parsing failed due to: Unable to treat column non-descript-column from the csv file. No target column found in the provided schema",
],
})
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
non-descript-column,SKU,Price USD,Price PL Region [EUR], Price JPY
test-pl-variant,,11.11,22.22,3333
,pl-sku,44.441,55.55,
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Product Variant ID,SKU,Price USD,Price PL Region [EUR], Price JPY
test-pl-variant,,11.11,22.22,3333
,pl-sku,44.441,55.55,
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Product id,Product Handle,Product Title,Product Subtitle,Product Description,Product Status,Product Thumbnail,Product Weight,Product Length,Product Width,Product Height,Product HS Code,Product Origin Country,Product MID Code,Product Material,Product Collection Title,Product Collection Handle,Product Type,Product Tags,Product Discountable,Product External ID,Product Profile Name,Product Profile Type,Variant id,Variant Title,Variant SKU,Variant Barcode,Variant Inventory Quantity,Variant Allow backorder,Variant Manage inventory,Variant Weight,Variant Length,Variant Width,Variant Height,Variant HS Code,Variant Origin Country,Variant MID Code,Variant Material,Price ImportLand [EUR],Price USD,Price denmark [DKK],Price Denmark [DKK],Option 1 Name,Option 1 Value,Option 2 Name,Option 2 Value,Image 1 Url,Sales Channel 1 Name,Sales Channel 2 Name,Sales Channel 1 Id,Sales Channel 2 Id
O6S1YQ6mKm,test-product-product-1,Test product,,test-product-description-1,draft,,,,,,,,,,Test collection 1,test-collection1,test-type-1,123_1,TRUE,,profile_1,profile_type_1,,Test variant,test-sku-1,test-barcode-1,10,FALSE,TRUE,,,,,,,,,100,110,130,,test-option-1,option 1 value red,test-option-2,option 2 value 1,test-image.png,Import Sales Channel 1,Import Sales Channel 2,,
5VxiEkmnPV,test-product-product-2,Test product,,test-product-description,draft,,,,,,,,,,Test collection,test-collection2,test-type,123,TRUE,,profile_2,profile_type_2,,Test variant,test-sku-2,test-barcode-2,10,FALSE,TRUE,,,,,,,,,,,,110,test-option,Option 1 value 1,,,test-image.png,,,,
5VxiEkmnPV,test-product-product-2,Test product,,test-product-description,draft,,,,,,,,,,Test collection,test-collection2,test-type,123,TRUE,,profile_2,profile_type_2,,Test variant,test-sku-3,test-barcode-3,10,FALSE,TRUE,,,,,,,,,,120,,,test-option,Option 1 Value blue,,,test-image.png,,,,
O6S1YQ6mKm,test-product-product-1,Test product,,test-product-description-1,draft,,,,,,,,,,Test collection 1,test-collection1,test-type-1,123_1,TRUE,,profile_1,profile_type_1,,Test variant,test-sku-1,test-barcode-1,10,FALSE,TRUE,,,,,,,,,1.00,1.10,1.30,,test-option-1,option 1 value red,test-option-2,option 2 value 1,test-image.png,Import Sales Channel 1,Import Sales Channel 2,,
5VxiEkmnPV,test-product-product-2,Test product,,test-product-description,draft,,,,,,,,,,Test collection,test-collection2,test-type,123,TRUE,,profile_2,profile_type_2,,Test variant,test-sku-2,test-barcode-2,10,FALSE,TRUE,,,,,,,,,,,,1.10,test-option,Option 1 value 1,,,test-image.png,,,,
5VxiEkmnPV,test-product-product-2,Test product,,test-product-description,draft,,,,,,,,,,Test collection,test-collection2,test-type,123,TRUE,,profile_2,profile_type_2,,Test variant,test-sku-3,test-barcode-3,10,FALSE,TRUE,,,,,,,,,,1.20,,,test-option,Option 1 Value blue,,,test-image.png,,,,
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Product id,Product Handle,Product Title,Product Subtitle,Product Description,Product Status,Product Thumbnail,Product Weight,Product Length,Product Width,Product Height,Product HS Code,Product Origin Country,Product MID Code,Product Material,Product Collection Title,Product Collection Handle,Product Type,Product Tags,Product Discountable,Product External ID,Product Profile Name,Product Profile Type,Variant id,Variant Title,Variant SKU,Variant Barcode,Variant Inventory Quantity,Variant Allow backorder,Variant Manage inventory,Variant Weight,Variant Length,Variant Width,Variant Height,Variant HS Code,Variant Origin Country,Variant MID Code,Variant Material,Price ImportLand [EUR],Price USD,Price denmark [DKK],Price Denmark [DKK],Option 1 Name,Option 1 Value,Option 2 Name,Option 2 Value,Image 1 Url
O6S1YQ6mKm,test-product-product-1,Test product,,"Hopper Stripes Bedding, available as duvet cover, pillow sham and sheet.\n100% organic cotton, soft and crisp to the touch. Made in Portugal.",draft,,,,,,,,,,Test collection 1,test-collection1,test-type-1,123_1,TRUE,,profile_1,profile_type_1,,Test variant,test-sku-1,test-barcode-1,10,FALSE,TRUE,,,,,,,,,100,110,130,,test-option-1,option 1 value red,test-option-2,option 2 value 1,test-image.png
5VxiEkmnPV,test-product-product-2,Test product,,test-product-description,draft,,,,,,,,,,Test collection,test-collection2,test-type,123,TRUE,,profile_2,profile_type_2,,Test variant,test-sku-2,test-barcode-2,10,FALSE,TRUE,,,,,,,,,,,,110,test-option,Option 1 value 1,,,test-image.png
5VxiEkmnPV,test-product-product-2,Test product,,test-product-description,draft,,,,,,,,,,Test collection,test-collection2,test-type,123,TRUE,,profile_2,profile_type_2,,Test variant,test-sku-3,test-barcode-3,10,FALSE,TRUE,,,,,,,,,,120,,,test-option,Option 1 Value blue,,,test-image.png
O6S1YQ6mKm,test-product-product-1,Test product,,"Hopper Stripes Bedding, available as duvet cover, pillow sham and sheet.\n100% organic cotton, soft and crisp to the touch. Made in Portugal.",draft,,,,,,,,,,Test collection 1,test-collection1,test-type-1,123_1,TRUE,,profile_1,profile_type_1,,Test variant,test-sku-1,test-barcode-1,10,FALSE,TRUE,,,,,,,,,1.00,1.10,1.30,,test-option-1,option 1 value red,test-option-2,option 2 value 1,test-image.png
5VxiEkmnPV,test-product-product-2,Test product,,test-product-description,draft,,,,,,,,,,Test collection,test-collection2,test-type,123,TRUE,,profile_2,profile_type_2,,Test variant,test-sku-2,test-barcode-2,10,FALSE,TRUE,,,,,,,,,,,,1.10,test-option,Option 1 value 1,,,test-image.png
5VxiEkmnPV,test-product-product-2,Test product,,test-product-description,draft,,,,,,,,,,Test collection,test-collection2,test-type,123,TRUE,,profile_2,profile_type_2,,Test variant,test-sku-3,test-barcode-3,10,FALSE,TRUE,,,,,,,,,,1.20,,,test-option,Option 1 Value blue,,,test-image.png
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export type ProductVariantFactoryData = {
product_id: string
id?: string
is_giftcard?: boolean
sku?: string
inventory_quantity?: number
title?: string
options?: { option_id: string; value: string }[]
Expand All @@ -31,6 +32,7 @@ export const simpleProductVariantFactory = async (
const toSave = manager.create(ProductVariant, {
id,
product_id: data.product_id,
sku: data.sku ?? null,
inventory_quantity:
typeof data.inventory_quantity !== "undefined"
? data.inventory_quantity
Expand Down
13 changes: 13 additions & 0 deletions packages/medusa-core-utils/src/computerize-amount.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import zeroDecimalCurrencies from "./zero-decimal-currencies"

const computerizeAmount = (amount, currency) => {
let divisor = 100

if (zeroDecimalCurrencies.includes(currency.toLowerCase())) {
divisor = 1
}

return Math.round(amount * divisor)
}

export default computerizeAmount
2 changes: 1 addition & 1 deletion packages/medusa-core-utils/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ export { default as createRequireFromPath } from "./create-require-from-path"
export { default as MedusaError } from "./errors"
export { default as getConfigFile } from "./get-config-file"
export { default as humanizeAmount } from "./humanize-amount"
export { default as computerizeAmount } from "./computerize-amount"
export { indexTypes } from "./index-types"
export { transformIdableFields } from "./transform-idable-fields"
export { default as Validator } from "./validator"
export { default as zeroDecimalCurrencies } from "./zero-decimal-currencies"

1 change: 1 addition & 0 deletions packages/medusa/src/services/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export { default as OrderService } from "./order"
export { default as OrderEditService } from "./order-edit"
export { default as PaymentProviderService } from "./payment-provider"
export { default as PricingService } from "./pricing"
export { default as PriceListService } from "./price-list"
export { default as ProductCollectionService } from "./product-collection"
export { default as ProductService } from "./product"
export { default as ProductTypeService } from "./product-type"
Expand Down
13 changes: 13 additions & 0 deletions packages/medusa/src/services/price-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,19 @@ class PriceListService extends TransactionBaseService {
})
}

/**
* Removes all prices from a price list and deletes the removed prices in bulk
* @param id - id of the price list
* @returns {Promise<void>} updated Price List
*/
async clearPrices(id: string): Promise<void> {
return await this.atomicPhase_(async (manager: EntityManager) => {
const moneyAmountRepo = manager.getCustomRepository(this.moneyAmountRepo_)
const priceList = await this.retrieve(id, { select: ["id"] })
await moneyAmountRepo.delete({ price_list_id: priceList.id })
})
}

/**
* Deletes a Price List
* Will never fail due to delete being idempotent.
Expand Down
Loading