feat(medusa): Includes Sales channels as part of the product/order export (#1882)
**What** Add support to sales channel in the product/order export strategy **How** Update the strategy to include the sales channel if the flag is enabled **Tests** Add new unit tests that check that the exported data does include the appropriate sales channel when the flag is enabled and that the data still does not include the sales channel if the flag is not including the flag FIXES CORE-303
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { DeepPartial } from "typeorm"
|
||||
import { IdMap } from "medusa-test-utils"
|
||||
import {
|
||||
FulfillmentStatus,
|
||||
Order,
|
||||
@@ -39,6 +40,11 @@ export const ordersToExport: DeepPartial<Order>[] = [
|
||||
last_name: "Doe",
|
||||
email: "John@Doe.com",
|
||||
},
|
||||
sales_channel: {
|
||||
id: IdMap.getId("sc_1"),
|
||||
name: "SC 1",
|
||||
description: "SC 1",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "order_2",
|
||||
@@ -70,5 +76,10 @@ export const ordersToExport: DeepPartial<Order>[] = [
|
||||
last_name: "Doe",
|
||||
email: "Jane@Doe.com",
|
||||
},
|
||||
sales_channel: {
|
||||
id: IdMap.getId("sc_2"),
|
||||
name: "SC 2",
|
||||
description: "SC 2",
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
@@ -11,6 +11,9 @@ const variantIds = [
|
||||
]
|
||||
export const productsToExport = [
|
||||
{
|
||||
sales_channels: [
|
||||
{ id: IdMap.getId("sc_1"), name: "SC 1", description: "SC 1" },
|
||||
],
|
||||
collection: {
|
||||
created_at: "randomString",
|
||||
deleted_at: null,
|
||||
@@ -202,6 +205,10 @@ export const productsToExport = [
|
||||
width: null,
|
||||
},
|
||||
{
|
||||
sales_channels: [
|
||||
{ id: IdMap.getId("sc_1"), name: "SC 1", description: "SC 1" },
|
||||
{ id: IdMap.getId("sc_2"), name: "SC 2", description: "SC 2" },
|
||||
],
|
||||
collection: {
|
||||
created_at: "randomString",
|
||||
deleted_at: null,
|
||||
|
||||
+11
@@ -10,3 +10,14 @@ Array [
|
||||
",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Order export strategy with sales channel should process the batch job and generate the appropriate output 1`] = `
|
||||
Array [
|
||||
"Order_ID;Display_ID;Order status;Date;Customer First name;Customer Last name;Customer Email;Customer ID;Shipping Address 1;Shipping Address 2;Shipping Country Code;Shipping City;Shipping Postal Code;Shipping Region ID;Fulfillment Status;Payment Status;Subtotal;Shipping Total;Discount Total;Gift Card Total;Refunded Total;Tax Total;Total;Currency Code;Sales channel name;Sales channel description
|
||||
",
|
||||
"order_1;123;pending;Tue, 01 Jan 2019 00:00:00 GMT;John;Doe;John@Doe.com;customer_1;123 Main St;;US;New York;10001;region_1;partially_fulfilled;captured;10;10;0;0;0;5;25;usd;SC 1;SC 1
|
||||
",
|
||||
"order_2;124;completed;Tue, 01 Jan 2019 00:00:00 GMT;Jane;Doe;Jane@Doe.com;customer_2;Hovedgaden 1;;DK;Copenhagen;1150;region_2;fulfilled;captured;125;10;0;0;0;0;135;eur;SC 2;SC 2
|
||||
",
|
||||
]
|
||||
`;
|
||||
|
||||
@@ -3,81 +3,9 @@ import { IdMap, MockManager } from "medusa-test-utils"
|
||||
import { User } from "../../../../models"
|
||||
import { BatchJobStatus } from "../../../../types/batch-job"
|
||||
import { ordersToExport } from "../../../__fixtures__/order-export-data"
|
||||
import { FlagRouter } from "../../../../utils/flag-router";
|
||||
import SalesChannelFeatureFlag from "../../../../loaders/feature-flags/sales-channels";
|
||||
|
||||
const outputDataStorage: string[] = []
|
||||
|
||||
let fakeJob = {
|
||||
id: IdMap.getId("order-export-job"),
|
||||
type: "order-export",
|
||||
context: {
|
||||
params: {},
|
||||
list_config: {
|
||||
select: [
|
||||
"id",
|
||||
"display_id",
|
||||
"status",
|
||||
"created_at",
|
||||
"fulfillment_status",
|
||||
"payment_status",
|
||||
"subtotal",
|
||||
"shipping_total",
|
||||
"discount_total",
|
||||
"gift_card_total",
|
||||
"refunded_total",
|
||||
"tax_total",
|
||||
"total",
|
||||
"currency_code",
|
||||
"region_id",
|
||||
],
|
||||
relations: ["customer", "shipping_address"],
|
||||
},
|
||||
},
|
||||
created_by: IdMap.getId("order-export-job-creator"),
|
||||
created_by_user: {} as User,
|
||||
result: {},
|
||||
dry_run: false,
|
||||
status: BatchJobStatus.PROCESSING,
|
||||
}
|
||||
|
||||
const fileServiceMock = {
|
||||
delete: jest.fn(),
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
getUploadStreamDescriptor: jest.fn().mockImplementation(() => {
|
||||
return Promise.resolve({
|
||||
writeStream: {
|
||||
write: (data: string) => {
|
||||
outputDataStorage.push(data)
|
||||
},
|
||||
end: () => void 0,
|
||||
},
|
||||
promise: Promise.resolve(),
|
||||
fileKey: "order-export.csv",
|
||||
})
|
||||
}),
|
||||
}
|
||||
const batchJobServiceMock = {
|
||||
withTransaction: function (): any {
|
||||
return this
|
||||
},
|
||||
update: jest.fn().mockImplementation(async (job, data) => {
|
||||
fakeJob = {
|
||||
...fakeJob,
|
||||
...data,
|
||||
context: { ...fakeJob?.context, ...data?.context },
|
||||
result: { ...fakeJob?.result, ...data?.result }
|
||||
}
|
||||
return Promise.resolve(fakeJob)
|
||||
}),
|
||||
complete: jest.fn().mockImplementation(async () => {
|
||||
fakeJob.status = BatchJobStatus.COMPLETED
|
||||
return fakeJob
|
||||
}),
|
||||
retrieve: jest.fn().mockImplementation(async () => {
|
||||
return fakeJob
|
||||
}),
|
||||
}
|
||||
const orderServiceMock = {
|
||||
withTransaction: function (): any {
|
||||
return this
|
||||
@@ -100,11 +28,87 @@ const orderServiceWithoutDataMock = {
|
||||
}
|
||||
|
||||
describe("Order export strategy", () => {
|
||||
const outputDataStorage: string[] = []
|
||||
|
||||
let fakeJob = {
|
||||
id: IdMap.getId("order-export-job"),
|
||||
type: "order-export",
|
||||
context: {
|
||||
params: {},
|
||||
list_config: {
|
||||
select: [
|
||||
"id",
|
||||
"display_id",
|
||||
"status",
|
||||
"created_at",
|
||||
"fulfillment_status",
|
||||
"payment_status",
|
||||
"subtotal",
|
||||
"shipping_total",
|
||||
"discount_total",
|
||||
"gift_card_total",
|
||||
"refunded_total",
|
||||
"tax_total",
|
||||
"total",
|
||||
"currency_code",
|
||||
"region_id",
|
||||
],
|
||||
relations: ["customer", "shipping_address"],
|
||||
},
|
||||
},
|
||||
created_by: IdMap.getId("order-export-job-creator"),
|
||||
created_by_user: {} as User,
|
||||
result: {},
|
||||
dry_run: false,
|
||||
status: BatchJobStatus.PROCESSING,
|
||||
}
|
||||
|
||||
const fileServiceMock = {
|
||||
delete: jest.fn(),
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
getUploadStreamDescriptor: jest.fn().mockImplementation(() => {
|
||||
return Promise.resolve({
|
||||
writeStream: {
|
||||
write: (data: string) => {
|
||||
outputDataStorage.push(data)
|
||||
},
|
||||
end: () => void 0,
|
||||
},
|
||||
promise: Promise.resolve(),
|
||||
fileKey: "order-export.csv",
|
||||
})
|
||||
}),
|
||||
}
|
||||
const batchJobServiceMock = {
|
||||
withTransaction: function (): any {
|
||||
return this
|
||||
},
|
||||
update: jest.fn().mockImplementation(async (job, data) => {
|
||||
fakeJob = {
|
||||
...fakeJob,
|
||||
...data,
|
||||
context: { ...fakeJob?.context, ...data?.context },
|
||||
result: { ...fakeJob?.result, ...data?.result }
|
||||
}
|
||||
return Promise.resolve(fakeJob)
|
||||
}),
|
||||
complete: jest.fn().mockImplementation(async () => {
|
||||
fakeJob.status = BatchJobStatus.COMPLETED
|
||||
return fakeJob
|
||||
}),
|
||||
retrieve: jest.fn().mockImplementation(async () => {
|
||||
return fakeJob
|
||||
}),
|
||||
}
|
||||
|
||||
const orderExportStrategy = new OrderExportStrategy({
|
||||
batchJobService: batchJobServiceMock as any,
|
||||
fileService: fileServiceMock as any,
|
||||
orderService: orderServiceMock as any,
|
||||
manager: MockManager,
|
||||
featureFlagRouter: new FlagRouter({}),
|
||||
})
|
||||
|
||||
it("Should generate header as template", async () => {
|
||||
@@ -150,6 +154,7 @@ describe("Order export strategy", () => {
|
||||
fileService: fileServiceMock as any,
|
||||
orderService: orderServiceWithoutDataMock as any,
|
||||
manager: MockManager,
|
||||
featureFlagRouter: new FlagRouter({}),
|
||||
})
|
||||
|
||||
await orderExportStrategy.processJob(fakeJob.id)
|
||||
@@ -157,3 +162,143 @@ describe("Order export strategy", () => {
|
||||
expect((fakeJob.result as any).file_key).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("Order export strategy with sales channel", () => {
|
||||
const outputDataStorage: string[] = []
|
||||
|
||||
let fakeJob = {
|
||||
id: IdMap.getId("order-export-job"),
|
||||
type: "order-export",
|
||||
context: {
|
||||
params: {},
|
||||
list_config: {
|
||||
select: [
|
||||
"id",
|
||||
"display_id",
|
||||
"status",
|
||||
"created_at",
|
||||
"fulfillment_status",
|
||||
"payment_status",
|
||||
"subtotal",
|
||||
"shipping_total",
|
||||
"discount_total",
|
||||
"gift_card_total",
|
||||
"refunded_total",
|
||||
"tax_total",
|
||||
"total",
|
||||
"currency_code",
|
||||
"region_id",
|
||||
],
|
||||
relations: ["customer", "shipping_address", "sales_channel"],
|
||||
},
|
||||
},
|
||||
created_by: IdMap.getId("order-export-job-creator"),
|
||||
created_by_user: {} as User,
|
||||
result: {},
|
||||
dry_run: false,
|
||||
status: BatchJobStatus.PROCESSING,
|
||||
}
|
||||
|
||||
const fileServiceMock = {
|
||||
delete: jest.fn(),
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
getUploadStreamDescriptor: jest.fn().mockImplementation(() => {
|
||||
return Promise.resolve({
|
||||
writeStream: {
|
||||
write: (data: string) => {
|
||||
outputDataStorage.push(data)
|
||||
},
|
||||
end: () => void 0,
|
||||
},
|
||||
promise: Promise.resolve(),
|
||||
fileKey: "order-export.csv",
|
||||
})
|
||||
}),
|
||||
}
|
||||
const batchJobServiceMock = {
|
||||
withTransaction: function (): any {
|
||||
return this
|
||||
},
|
||||
update: jest.fn().mockImplementation(async (job, data) => {
|
||||
fakeJob = {
|
||||
...fakeJob,
|
||||
...data,
|
||||
context: { ...fakeJob?.context, ...data?.context },
|
||||
result: { ...fakeJob?.result, ...data?.result }
|
||||
}
|
||||
return Promise.resolve(fakeJob)
|
||||
}),
|
||||
complete: jest.fn().mockImplementation(async () => {
|
||||
fakeJob.status = BatchJobStatus.COMPLETED
|
||||
return fakeJob
|
||||
}),
|
||||
retrieve: jest.fn().mockImplementation(async () => {
|
||||
return fakeJob
|
||||
}),
|
||||
}
|
||||
|
||||
const orderExportStrategy = new OrderExportStrategy({
|
||||
batchJobService: batchJobServiceMock as any,
|
||||
fileService: fileServiceMock as any,
|
||||
orderService: orderServiceMock as any,
|
||||
manager: MockManager,
|
||||
featureFlagRouter: new FlagRouter({
|
||||
[SalesChannelFeatureFlag.key]: true,
|
||||
}),
|
||||
})
|
||||
|
||||
it("Should generate header as template", async () => {
|
||||
const template = await orderExportStrategy.buildTemplate()
|
||||
expect(template.split(";")).toEqual([
|
||||
"Order_ID",
|
||||
"Display_ID",
|
||||
"Order status",
|
||||
"Date",
|
||||
"Customer First name",
|
||||
"Customer Last name",
|
||||
"Customer Email",
|
||||
"Customer ID",
|
||||
"Shipping Address 1",
|
||||
"Shipping Address 2",
|
||||
"Shipping Country Code",
|
||||
"Shipping City",
|
||||
"Shipping Postal Code",
|
||||
"Shipping Region ID",
|
||||
"Fulfillment Status",
|
||||
"Payment Status",
|
||||
"Subtotal",
|
||||
"Shipping Total",
|
||||
"Discount Total",
|
||||
"Gift Card Total",
|
||||
"Refunded Total",
|
||||
"Tax Total",
|
||||
"Total",
|
||||
"Currency Code",
|
||||
"Sales channel name",
|
||||
"Sales channel description\r\n",
|
||||
])
|
||||
})
|
||||
|
||||
it("should process the batch job and generate the appropriate output", async () => {
|
||||
await orderExportStrategy.processJob(fakeJob.id)
|
||||
|
||||
expect(outputDataStorage).toMatchSnapshot()
|
||||
expect((fakeJob.result as any).file_key).toBeDefined()
|
||||
})
|
||||
|
||||
it("should always provide a file_key even with no data", async () => {
|
||||
const orderExportStrategy = new OrderExportStrategy({
|
||||
batchJobService: batchJobServiceMock as any,
|
||||
fileService: fileServiceMock as any,
|
||||
orderService: orderServiceWithoutDataMock as any,
|
||||
manager: MockManager,
|
||||
featureFlagRouter: new FlagRouter({}),
|
||||
})
|
||||
|
||||
await orderExportStrategy.processJob(fakeJob.id)
|
||||
|
||||
expect((fakeJob.result as any).file_key).toBeDefined()
|
||||
})
|
||||
})
|
||||
+13
@@ -12,3 +12,16 @@ Array [
|
||||
",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Product export strategy with sales channels should process the batch job and generate the appropriate output 1`] = `
|
||||
Array [
|
||||
"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 france [USD];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 1 Description;Sales channel 2 Name;Sales channel 2 Description
|
||||
",
|
||||
"product-export-strategy-product-1;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;product-export-strategy-variant-1;Test variant;test-sku;test-barcode;10;false;true;;;;;;;;;100;110;130;;test-option-1;option 1 value 1;test-option-2;option 2 value 1;test-image.png;SC 1;SC 1;;
|
||||
",
|
||||
"product-export-strategy-product-2;test-product-product-2;Test product;;test-product-description;draft;;;;;;;;;;Test collection;test-collection2;test-type;123;true;;profile_2;profile_type_2;product-export-strategy-variant-2;Test variant;test-sku;test-barcode;10;false;true;;;;;;;;;;;;110;test-option;Option 1 value 1;;;test-image.png;SC 1;SC 1;SC 2;SC 2
|
||||
",
|
||||
"product-export-strategy-product-2;test-product-product-2;Test product;;test-product-description;draft;;;;;;;;;;Test collection;test-collection2;test-type;123;true;;profile_2;profile_type_2;product-export-strategy-variant-3;Test variant;test-sku;test-barcode;10;false;true;;;;;;;;;;120;;;test-option;Option 1 Value 1;;;test-image.png;SC 1;SC 1;SC 2;SC 2
|
||||
",
|
||||
]
|
||||
`;
|
||||
|
||||
@@ -6,87 +6,9 @@ import { productsToExport } from "../../../__fixtures__/product-export-data"
|
||||
import { AdminPostBatchesReq, defaultAdminProductRelations } from "../../../../api"
|
||||
import { ProductExportBatchJob } from "../../../batch-jobs/product"
|
||||
import { Request } from "express"
|
||||
import { FlagRouter } from "../../../../utils/flag-router";
|
||||
import SalesChannelFeatureFlag from "../../../../loaders/feature-flags/sales-channels";
|
||||
|
||||
const outputDataStorage: string[] = []
|
||||
|
||||
let fakeJob = {
|
||||
id: IdMap.getId("product-export-job"),
|
||||
type: 'product-export',
|
||||
created_by: IdMap.getId("product-export-job-creator"),
|
||||
created_by_user: {} as User,
|
||||
context: {},
|
||||
result: {},
|
||||
dry_run: false,
|
||||
status: BatchJobStatus.PROCESSING as BatchJobStatus
|
||||
} as ProductExportBatchJob
|
||||
|
||||
let canceledFakeJob = {
|
||||
...fakeJob,
|
||||
id: "bj_failed",
|
||||
status: BatchJobStatus.CANCELED
|
||||
} as ProductExportBatchJob
|
||||
|
||||
const fileServiceMock = {
|
||||
delete: jest.fn(),
|
||||
getUploadStreamDescriptor: jest.fn().mockImplementation(() => {
|
||||
return Promise.resolve({
|
||||
writeStream: {
|
||||
write: (data: string) => {
|
||||
outputDataStorage.push(data)
|
||||
},
|
||||
end: () => void 0
|
||||
},
|
||||
promise: Promise.resolve(),
|
||||
fileKey: 'product-export.csv'
|
||||
})
|
||||
}),
|
||||
withTransaction: function () {
|
||||
return this
|
||||
}
|
||||
}
|
||||
const batchJobServiceMock = {
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
update: jest.fn().mockImplementation((jobOrId, data) => {
|
||||
if ((jobOrId?.id ?? jobOrId) === "bj_failed") {
|
||||
canceledFakeJob = {
|
||||
...canceledFakeJob,
|
||||
...data,
|
||||
context: { ...canceledFakeJob?.context, ...data?.context },
|
||||
result: { ...canceledFakeJob?.result, ...data?.result }
|
||||
}
|
||||
|
||||
return Promise.resolve(canceledFakeJob)
|
||||
}
|
||||
|
||||
fakeJob = {
|
||||
...fakeJob,
|
||||
...data,
|
||||
context: { ...fakeJob?.context, ...data?.context },
|
||||
result: { ...fakeJob?.result, ...data?.result }
|
||||
}
|
||||
|
||||
return Promise.resolve(fakeJob)
|
||||
}),
|
||||
updateStatus: jest.fn().mockImplementation((status) => {
|
||||
fakeJob.status = status
|
||||
return Promise.resolve(fakeJob)
|
||||
}),
|
||||
complete: jest.fn().mockImplementation(() => {
|
||||
fakeJob.status = BatchJobStatus.COMPLETED
|
||||
return Promise.resolve(fakeJob)
|
||||
}),
|
||||
retrieve: jest.fn().mockImplementation((id) => {
|
||||
const targetFakeJob = id === "bj_failed"
|
||||
? canceledFakeJob
|
||||
: fakeJob
|
||||
return Promise.resolve(targetFakeJob)
|
||||
}),
|
||||
setFailed: jest.fn().mockImplementation((...args) => {
|
||||
console.error(...args)
|
||||
})
|
||||
}
|
||||
const productServiceMock = {
|
||||
withTransaction: function () {
|
||||
return this
|
||||
@@ -108,11 +30,92 @@ const productServiceWithNoDataMock = {
|
||||
const managerMock = MockManager
|
||||
|
||||
describe("Product export strategy", () => {
|
||||
const outputDataStorage: string[] = []
|
||||
const fileServiceMock = {
|
||||
delete: jest.fn(),
|
||||
getUploadStreamDescriptor: jest.fn().mockImplementation(() => {
|
||||
return Promise.resolve({
|
||||
writeStream: {
|
||||
write: (data: string) => {
|
||||
outputDataStorage.push(data)
|
||||
},
|
||||
end: () => void 0
|
||||
},
|
||||
promise: Promise.resolve(),
|
||||
fileKey: 'product-export.csv'
|
||||
})
|
||||
}),
|
||||
withTransaction: function () {
|
||||
return this
|
||||
}
|
||||
}
|
||||
let fakeJob = {
|
||||
id: IdMap.getId("product-export-job"),
|
||||
type: 'product-export',
|
||||
created_by: IdMap.getId("product-export-job-creator"),
|
||||
created_by_user: {} as User,
|
||||
context: {},
|
||||
result: {},
|
||||
dry_run: false,
|
||||
status: BatchJobStatus.PROCESSING as BatchJobStatus
|
||||
} as ProductExportBatchJob
|
||||
|
||||
let canceledFakeJob = {
|
||||
...fakeJob,
|
||||
id: "bj_failed",
|
||||
status: BatchJobStatus.CANCELED
|
||||
} as ProductExportBatchJob
|
||||
|
||||
const batchJobServiceMock = {
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
update: jest.fn().mockImplementation((jobOrId, data) => {
|
||||
if ((jobOrId?.id ?? jobOrId) === "bj_failed") {
|
||||
canceledFakeJob = {
|
||||
...canceledFakeJob,
|
||||
...data,
|
||||
context: { ...canceledFakeJob?.context, ...data?.context },
|
||||
result: { ...canceledFakeJob?.result, ...data?.result }
|
||||
}
|
||||
|
||||
return Promise.resolve(canceledFakeJob)
|
||||
}
|
||||
|
||||
fakeJob = {
|
||||
...fakeJob,
|
||||
...data,
|
||||
context: { ...fakeJob?.context, ...data?.context },
|
||||
result: { ...fakeJob?.result, ...data?.result }
|
||||
}
|
||||
|
||||
return Promise.resolve(fakeJob)
|
||||
}),
|
||||
updateStatus: jest.fn().mockImplementation((status) => {
|
||||
fakeJob.status = status
|
||||
return Promise.resolve(fakeJob)
|
||||
}),
|
||||
complete: jest.fn().mockImplementation(() => {
|
||||
fakeJob.status = BatchJobStatus.COMPLETED
|
||||
return Promise.resolve(fakeJob)
|
||||
}),
|
||||
retrieve: jest.fn().mockImplementation((id) => {
|
||||
const targetFakeJob = id === "bj_failed"
|
||||
? canceledFakeJob
|
||||
: fakeJob
|
||||
return Promise.resolve(targetFakeJob)
|
||||
}),
|
||||
setFailed: jest.fn().mockImplementation((...args) => {
|
||||
console.error(...args)
|
||||
})
|
||||
}
|
||||
|
||||
const productExportStrategy = new ProductExportStrategy({
|
||||
manager: managerMock,
|
||||
fileService: fileServiceMock as any,
|
||||
batchJobService: batchJobServiceMock as any,
|
||||
productService: productServiceMock as any,
|
||||
featureFlagRouter: new FlagRouter({}),
|
||||
})
|
||||
|
||||
it('should generate the appropriate template', async () => {
|
||||
@@ -164,6 +167,11 @@ describe("Product export strategy", () => {
|
||||
expect(template).toMatch(/.*Option 2 Name.*/)
|
||||
expect(template).toMatch(/.*Option 2 Value.*/)
|
||||
|
||||
expect(template).not.toMatch(/.*Sales channel 1 Name.*/)
|
||||
expect(template).not.toMatch(/.*Sales channel 1 Description.*/)
|
||||
expect(template).not.toMatch(/.*Sales channel 2 Name.*/)
|
||||
expect(template).not.toMatch(/.*Sales channel 2 Description.*/)
|
||||
|
||||
expect(template).toMatch(/.*Price USD.*/)
|
||||
expect(template).toMatch(/.*Price france \[USD\].*/)
|
||||
expect(template).toMatch(/.*Price denmark \[DKK\].*/)
|
||||
@@ -242,6 +250,7 @@ describe("Product export strategy", () => {
|
||||
fileService: fileServiceMock as any,
|
||||
productService: productServiceWithNoDataMock as any,
|
||||
manager: MockManager,
|
||||
featureFlagRouter: new FlagRouter({}),
|
||||
})
|
||||
|
||||
await productExportStrategy.prepareBatchJobForProcessing(fakeJob, {} as Request)
|
||||
@@ -257,6 +266,7 @@ describe("Product export strategy", () => {
|
||||
fileService: fileServiceMock as any,
|
||||
productService: productServiceMock as any,
|
||||
manager: MockManager,
|
||||
featureFlagRouter: new FlagRouter({}),
|
||||
})
|
||||
|
||||
await productExportStrategy.prepareBatchJobForProcessing(canceledFakeJob, {} as Request)
|
||||
@@ -267,3 +277,165 @@ describe("Product export strategy", () => {
|
||||
expect((canceledFakeJob.result as any).file_size).not.toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("Product export strategy with sales channels", () => {
|
||||
const outputDataStorage: string[] = []
|
||||
const fileServiceMock = {
|
||||
delete: jest.fn(),
|
||||
getUploadStreamDescriptor: jest.fn().mockImplementation(() => {
|
||||
return Promise.resolve({
|
||||
writeStream: {
|
||||
write: (data: string) => {
|
||||
outputDataStorage.push(data)
|
||||
},
|
||||
end: () => void 0
|
||||
},
|
||||
promise: Promise.resolve(),
|
||||
fileKey: 'product-export.csv'
|
||||
})
|
||||
}),
|
||||
withTransaction: function () {
|
||||
return this
|
||||
}
|
||||
}
|
||||
let fakeJob = {
|
||||
id: IdMap.getId("product-export-job"),
|
||||
type: 'product-export',
|
||||
created_by: IdMap.getId("product-export-job-creator"),
|
||||
created_by_user: {} as User,
|
||||
context: {},
|
||||
result: {},
|
||||
dry_run: false,
|
||||
status: BatchJobStatus.PROCESSING as BatchJobStatus
|
||||
} as ProductExportBatchJob
|
||||
|
||||
let canceledFakeJob = {
|
||||
...fakeJob,
|
||||
id: "bj_failed",
|
||||
status: BatchJobStatus.CANCELED
|
||||
} as ProductExportBatchJob
|
||||
|
||||
const batchJobServiceMock = {
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
update: jest.fn().mockImplementation((jobOrId, data) => {
|
||||
if ((jobOrId?.id ?? jobOrId) === "bj_failed") {
|
||||
canceledFakeJob = {
|
||||
...canceledFakeJob,
|
||||
...data,
|
||||
context: { ...canceledFakeJob?.context, ...data?.context },
|
||||
result: { ...canceledFakeJob?.result, ...data?.result }
|
||||
}
|
||||
|
||||
return Promise.resolve(canceledFakeJob)
|
||||
}
|
||||
|
||||
fakeJob = {
|
||||
...fakeJob,
|
||||
...data,
|
||||
context: { ...fakeJob?.context, ...data?.context },
|
||||
result: { ...fakeJob?.result, ...data?.result }
|
||||
}
|
||||
|
||||
return Promise.resolve(fakeJob)
|
||||
}),
|
||||
updateStatus: jest.fn().mockImplementation((status) => {
|
||||
fakeJob.status = status
|
||||
return Promise.resolve(fakeJob)
|
||||
}),
|
||||
complete: jest.fn().mockImplementation(() => {
|
||||
fakeJob.status = BatchJobStatus.COMPLETED
|
||||
return Promise.resolve(fakeJob)
|
||||
}),
|
||||
retrieve: jest.fn().mockImplementation((id) => {
|
||||
const targetFakeJob = id === "bj_failed"
|
||||
? canceledFakeJob
|
||||
: fakeJob
|
||||
return Promise.resolve(targetFakeJob)
|
||||
}),
|
||||
setFailed: jest.fn().mockImplementation((...args) => {
|
||||
console.error(...args)
|
||||
})
|
||||
}
|
||||
|
||||
const productExportStrategy = new ProductExportStrategy({
|
||||
manager: managerMock,
|
||||
fileService: fileServiceMock as any,
|
||||
batchJobService: batchJobServiceMock as any,
|
||||
productService: productServiceMock as any,
|
||||
featureFlagRouter: new FlagRouter({
|
||||
[SalesChannelFeatureFlag.key]: true,
|
||||
}),
|
||||
})
|
||||
|
||||
it('should generate the appropriate template', async () => {
|
||||
await productExportStrategy.prepareBatchJobForProcessing(fakeJob, {} as Request)
|
||||
await productExportStrategy.preProcessBatchJob(fakeJob.id)
|
||||
const template = await productExportStrategy.buildHeader(fakeJob)
|
||||
expect(template).toMatch(/.*Product ID.*/)
|
||||
expect(template).toMatch(/.*Product Handle.*/)
|
||||
expect(template).toMatch(/.*Product Title.*/)
|
||||
expect(template).toMatch(/.*Product Subtitle.*/)
|
||||
expect(template).toMatch(/.*Product Description.*/)
|
||||
expect(template).toMatch(/.*Product Status.*/)
|
||||
expect(template).toMatch(/.*Product Thumbnail.*/)
|
||||
expect(template).toMatch(/.*Product Weight.*/)
|
||||
expect(template).toMatch(/.*Product Length.*/)
|
||||
expect(template).toMatch(/.*Product Width.*/)
|
||||
expect(template).toMatch(/.*Product Height.*/)
|
||||
expect(template).toMatch(/.*Product HS Code.*/)
|
||||
expect(template).toMatch(/.*Product Origin Country.*/)
|
||||
expect(template).toMatch(/.*Product MID Code.*/)
|
||||
expect(template).toMatch(/.*Product Material.*/)
|
||||
expect(template).toMatch(/.*Product Collection Title.*/)
|
||||
expect(template).toMatch(/.*Product Collection Handle.*/)
|
||||
expect(template).toMatch(/.*Product Type.*/)
|
||||
expect(template).toMatch(/.*Product Tags.*/)
|
||||
expect(template).toMatch(/.*Product Discountable.*/)
|
||||
expect(template).toMatch(/.*Product External ID.*/)
|
||||
expect(template).toMatch(/.*Product Profile Name.*/)
|
||||
expect(template).toMatch(/.*Product Profile Type.*/)
|
||||
expect(template).toMatch(/.*Product Profile Type.*/)
|
||||
|
||||
expect(template).toMatch(/.*Variant ID.*/)
|
||||
expect(template).toMatch(/.*Variant Title.*/)
|
||||
expect(template).toMatch(/.*Variant SKU.*/)
|
||||
expect(template).toMatch(/.*Variant Barcode.*/)
|
||||
expect(template).toMatch(/.*Variant Allow backorder.*/)
|
||||
expect(template).toMatch(/.*Variant Manage inventory.*/)
|
||||
expect(template).toMatch(/.*Variant Weight.*/)
|
||||
expect(template).toMatch(/.*Variant Length.*/)
|
||||
expect(template).toMatch(/.*Variant Width.*/)
|
||||
expect(template).toMatch(/.*Variant Height.*/)
|
||||
expect(template).toMatch(/.*Variant HS Code.*/)
|
||||
expect(template).toMatch(/.*Variant Origin Country.*/)
|
||||
expect(template).toMatch(/.*Variant MID Code.*/)
|
||||
expect(template).toMatch(/.*Variant Material.*/)
|
||||
|
||||
expect(template).toMatch(/.*Option 1 Name.*/)
|
||||
expect(template).toMatch(/.*Option 1 Value.*/)
|
||||
expect(template).toMatch(/.*Option 2 Name.*/)
|
||||
expect(template).toMatch(/.*Option 2 Value.*/)
|
||||
|
||||
expect(template).toMatch(/.*Price USD.*/)
|
||||
expect(template).toMatch(/.*Price france \[USD\].*/)
|
||||
expect(template).toMatch(/.*Price denmark \[DKK\].*/)
|
||||
expect(template).toMatch(/.*Price Denmark \[DKK\].*/)
|
||||
|
||||
expect(template).toMatch(/.*Sales channel 1 Name.*/)
|
||||
expect(template).toMatch(/.*Sales channel 1 Description.*/)
|
||||
expect(template).toMatch(/.*Sales channel 2 Name.*/)
|
||||
expect(template).toMatch(/.*Sales channel 2 Description.*/)
|
||||
|
||||
expect(template).toMatch(/.*Image 1 Url.*/)
|
||||
})
|
||||
|
||||
it('should process the batch job and generate the appropriate output', async () => {
|
||||
await productExportStrategy.prepareBatchJobForProcessing(fakeJob, {} as Request)
|
||||
await productExportStrategy.preProcessBatchJob(fakeJob.id)
|
||||
await productExportStrategy.processJob(fakeJob.id)
|
||||
expect(outputDataStorage).toMatchSnapshot()
|
||||
expect((fakeJob.result as any).file_key).toBeDefined()
|
||||
})
|
||||
})
|
||||
@@ -5,20 +5,23 @@ import {
|
||||
OrderExportBatchJobContext,
|
||||
orderExportPropertiesDescriptors,
|
||||
} from "."
|
||||
import { AdminPostBatchesReq } from "../../../api/routes/admin/batch/create-batch-job"
|
||||
import { AdminPostBatchesReq } from "../../../api"
|
||||
import { IFileService } from "../../../interfaces"
|
||||
import { AbstractBatchJobStrategy } from "../../../interfaces/batch-job-strategy"
|
||||
import { AbstractBatchJobStrategy } from "../../../interfaces"
|
||||
import { Order } from "../../../models"
|
||||
import { OrderService } from "../../../services"
|
||||
import BatchJobService from "../../../services/batch-job"
|
||||
import { BatchJobStatus } from "../../../types/batch-job"
|
||||
import { prepareListQuery } from "../../../utils/get-query-config"
|
||||
import { FlagRouter } from "../../../utils/flag-router"
|
||||
import SalesChannelFeatureFlag from "../../../loaders/feature-flags/sales-channels"
|
||||
|
||||
type InjectedDependencies = {
|
||||
fileService: IFileService<any>
|
||||
fileService: IFileService<never>
|
||||
orderService: OrderService
|
||||
batchJobService: BatchJobService
|
||||
manager: EntityManager
|
||||
featureFlagRouter: FlagRouter
|
||||
}
|
||||
|
||||
class OrderExportStrategy extends AbstractBatchJobStrategy<OrderExportStrategy> {
|
||||
@@ -36,6 +39,11 @@ class OrderExportStrategy extends AbstractBatchJobStrategy<OrderExportStrategy>
|
||||
protected readonly fileService_: IFileService<any>
|
||||
protected readonly batchJobService_: BatchJobService
|
||||
protected readonly orderService_: OrderService
|
||||
protected readonly featureFlagRouter_: FlagRouter
|
||||
|
||||
protected readonly orderExportPropertiesDescriptors = [
|
||||
...orderExportPropertiesDescriptors,
|
||||
]
|
||||
|
||||
protected readonly defaultRelations_ = ["customer", "shipping_address"]
|
||||
protected readonly defaultFields_ = [
|
||||
@@ -61,6 +69,7 @@ class OrderExportStrategy extends AbstractBatchJobStrategy<OrderExportStrategy>
|
||||
batchJobService,
|
||||
orderService,
|
||||
manager,
|
||||
featureFlagRouter,
|
||||
}: InjectedDependencies) {
|
||||
// eslint-disable-next-line prefer-rest-params
|
||||
super(arguments[0])
|
||||
@@ -69,6 +78,12 @@ class OrderExportStrategy extends AbstractBatchJobStrategy<OrderExportStrategy>
|
||||
this.fileService_ = fileService
|
||||
this.batchJobService_ = batchJobService
|
||||
this.orderService_ = orderService
|
||||
this.featureFlagRouter_ = featureFlagRouter
|
||||
|
||||
if (featureFlagRouter.isFeatureEnabled(SalesChannelFeatureFlag.key)) {
|
||||
this.defaultRelations_.push("sales_channel")
|
||||
this.addSalesChannelColumns()
|
||||
}
|
||||
}
|
||||
|
||||
async prepareBatchJobForProcessing(
|
||||
@@ -253,13 +268,12 @@ class OrderExportStrategy extends AbstractBatchJobStrategy<OrderExportStrategy>
|
||||
await promise
|
||||
},
|
||||
"REPEATABLE READ",
|
||||
async (err: Error) => {
|
||||
async (err: Error) =>
|
||||
this.handleProcessingError(batchJobId, err, {
|
||||
offset,
|
||||
count: orderCount,
|
||||
progress: offset / orderCount,
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -270,7 +284,7 @@ class OrderExportStrategy extends AbstractBatchJobStrategy<OrderExportStrategy>
|
||||
}
|
||||
|
||||
private buildHeader(
|
||||
lineDescriptor: OrderDescriptor[] = orderExportPropertiesDescriptors
|
||||
lineDescriptor: OrderDescriptor[] = this.orderExportPropertiesDescriptors
|
||||
): string {
|
||||
return (
|
||||
[...lineDescriptor.map(({ title }) => title)].join(this.DELIMITER) +
|
||||
@@ -293,11 +307,20 @@ class OrderExportStrategy extends AbstractBatchJobStrategy<OrderExportStrategy>
|
||||
fields: string[],
|
||||
relations: string[]
|
||||
): OrderDescriptor[] {
|
||||
return orderExportPropertiesDescriptors.filter(
|
||||
return this.orderExportPropertiesDescriptors.filter(
|
||||
({ fieldName }) =>
|
||||
fields.indexOf(fieldName) !== -1 || relations.indexOf(fieldName) !== -1
|
||||
)
|
||||
}
|
||||
|
||||
private addSalesChannelColumns(): void {
|
||||
this.orderExportPropertiesDescriptors.push({
|
||||
fieldName: "sales_channel",
|
||||
title: ["Sales channel name", "Sales channel description"].join(";"),
|
||||
accessor: (order: Order): string =>
|
||||
[order.sales_channel.name, order.sales_channel.description].join(";"),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default OrderExportStrategy
|
||||
|
||||
@@ -13,12 +13,15 @@ import {
|
||||
productExportSchemaDescriptors,
|
||||
} from "./index"
|
||||
import { FindProductConfig } from "../../../types/product"
|
||||
import { FlagRouter } from "../../../utils/flag-router"
|
||||
import SalesChannelFeatureFlag from "../../../loaders/feature-flags/sales-channels"
|
||||
|
||||
type InjectedDependencies = {
|
||||
manager: EntityManager
|
||||
batchJobService: BatchJobService
|
||||
productService: ProductService
|
||||
fileService: IFileService<never>
|
||||
featureFlagRouter: FlagRouter
|
||||
}
|
||||
|
||||
export default class ProductExportStrategy extends AbstractBatchJobStrategy<
|
||||
@@ -34,6 +37,7 @@ export default class ProductExportStrategy extends AbstractBatchJobStrategy<
|
||||
protected readonly batchJobService_: BatchJobService
|
||||
protected readonly productService_: ProductService
|
||||
protected readonly fileService_: IFileService<never>
|
||||
protected readonly featureFlagRouter_: FlagRouter
|
||||
|
||||
protected readonly defaultRelations_ = [
|
||||
...defaultAdminProductRelations,
|
||||
@@ -49,7 +53,7 @@ export default class ProductExportStrategy extends AbstractBatchJobStrategy<
|
||||
protected readonly columnDescriptors: Map<
|
||||
string,
|
||||
ProductExportColumnSchemaDescriptor
|
||||
> = productExportSchemaDescriptors
|
||||
> = new Map(productExportSchemaDescriptors)
|
||||
|
||||
private readonly NEWLINE_ = "\r\n"
|
||||
private readonly DELIMITER_ = ";"
|
||||
@@ -60,18 +64,25 @@ export default class ProductExportStrategy extends AbstractBatchJobStrategy<
|
||||
batchJobService,
|
||||
productService,
|
||||
fileService,
|
||||
featureFlagRouter,
|
||||
}: InjectedDependencies) {
|
||||
super({
|
||||
manager,
|
||||
batchJobService,
|
||||
productService,
|
||||
fileService,
|
||||
featureFlagRouter,
|
||||
})
|
||||
|
||||
this.manager_ = manager
|
||||
this.batchJobService_ = batchJobService
|
||||
this.productService_ = productService
|
||||
this.fileService_ = fileService
|
||||
this.featureFlagRouter_ = featureFlagRouter
|
||||
|
||||
if (featureFlagRouter.isFeatureEnabled(SalesChannelFeatureFlag.key)) {
|
||||
this.defaultRelations_.push("sales_channels")
|
||||
}
|
||||
}
|
||||
|
||||
async buildTemplate(): Promise<string> {
|
||||
@@ -138,8 +149,8 @@ export default class ProductExportStrategy extends AbstractBatchJobStrategy<
|
||||
|
||||
let dynamicOptionColumnCount = 0
|
||||
let dynamicImageColumnCount = 0
|
||||
|
||||
const pricesData = new Set<string>()
|
||||
let dynamicSalesChannelsColumnCount = 0
|
||||
let pricesData = new Set<string>()
|
||||
|
||||
while (offset < productCount) {
|
||||
if (!products?.length) {
|
||||
@@ -152,39 +163,20 @@ export default class ProductExportStrategy extends AbstractBatchJobStrategy<
|
||||
} as FindProductConfig)
|
||||
}
|
||||
|
||||
// Retrieve the highest count of each object to build the dynamic columns later
|
||||
for (const product of products) {
|
||||
const optionsCount = product?.options?.length ?? 0
|
||||
dynamicOptionColumnCount = Math.max(
|
||||
dynamicOptionColumnCount,
|
||||
optionsCount
|
||||
)
|
||||
|
||||
const imageCount = product?.images?.length ?? 0
|
||||
dynamicImageColumnCount = Math.max(
|
||||
dynamicImageColumnCount,
|
||||
imageCount
|
||||
)
|
||||
|
||||
for (const variant of product?.variants ?? []) {
|
||||
if (variant.prices?.length) {
|
||||
variant.prices.forEach((price) => {
|
||||
pricesData.add(
|
||||
JSON.stringify({
|
||||
currency_code: price.currency_code,
|
||||
region: price.region
|
||||
? {
|
||||
currency_code: price.region.currency_code,
|
||||
name: price.region.name,
|
||||
id: price.region.id,
|
||||
}
|
||||
: null,
|
||||
})
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
const shapeData = this.getProductRelationsDynamicColumnsShape(products)
|
||||
dynamicImageColumnCount = Math.max(
|
||||
shapeData.imageColumnCount,
|
||||
dynamicImageColumnCount
|
||||
)
|
||||
dynamicOptionColumnCount = Math.max(
|
||||
shapeData.optionColumnCount,
|
||||
dynamicOptionColumnCount
|
||||
)
|
||||
dynamicSalesChannelsColumnCount = Math.max(
|
||||
shapeData.salesChannelsColumnCount,
|
||||
dynamicSalesChannelsColumnCount
|
||||
)
|
||||
pricesData = new Set([...pricesData, ...shapeData.pricesData])
|
||||
|
||||
offset += products.length
|
||||
products = []
|
||||
@@ -197,6 +189,7 @@ export default class ProductExportStrategy extends AbstractBatchJobStrategy<
|
||||
shape: {
|
||||
dynamicImageColumnCount,
|
||||
dynamicOptionColumnCount,
|
||||
dynamicSalesChannelsColumnCount,
|
||||
prices: [...pricesData].map((stringifyData) =>
|
||||
JSON.parse(stringifyData)
|
||||
),
|
||||
@@ -325,11 +318,13 @@ export default class ProductExportStrategy extends AbstractBatchJobStrategy<
|
||||
prices = [],
|
||||
dynamicImageColumnCount,
|
||||
dynamicOptionColumnCount,
|
||||
dynamicSalesChannelsColumnCount,
|
||||
} = batchJob?.context?.shape ?? {}
|
||||
|
||||
this.appendMoneyAmountDescriptors(prices)
|
||||
this.appendOptionsDescriptors(dynamicOptionColumnCount)
|
||||
this.appendImagesDescriptors(dynamicImageColumnCount)
|
||||
this.appendSalesChannelsDescriptors(dynamicSalesChannelsColumnCount)
|
||||
|
||||
return (
|
||||
[...this.columnDescriptors.keys()].join(this.DELIMITER_) + this.NEWLINE_
|
||||
@@ -345,6 +340,20 @@ export default class ProductExportStrategy extends AbstractBatchJobStrategy<
|
||||
}
|
||||
}
|
||||
|
||||
private appendSalesChannelsDescriptors(maxScCount: number): void {
|
||||
for (let i = 0; i < maxScCount; ++i) {
|
||||
this.columnDescriptors.set(`Sales channel ${i + 1} Name`, {
|
||||
accessor: (product: Product) => product?.sales_channels[i]?.name ?? "",
|
||||
entityName: "product",
|
||||
})
|
||||
this.columnDescriptors.set(`Sales channel ${i + 1} Description`, {
|
||||
accessor: (product: Product) =>
|
||||
product?.sales_channels[i]?.description ?? "",
|
||||
entityName: "product",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private appendOptionsDescriptors(maxOptionsCount: number): void {
|
||||
for (let i = 0; i < maxOptionsCount; ++i) {
|
||||
this.columnDescriptors
|
||||
@@ -449,4 +458,68 @@ export default class ProductExportStrategy extends AbstractBatchJobStrategy<
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the maximun number of each relation that must appears in the export.
|
||||
* The number of item of a relation can vary between 0-Infinity and therefore the number of columns
|
||||
* that will be added to the export correspond to that number
|
||||
* @param products - The main entity to get the relation shape from
|
||||
* @private
|
||||
*/
|
||||
private getProductRelationsDynamicColumnsShape(products: Product[]): {
|
||||
optionColumnCount: number
|
||||
imageColumnCount: number
|
||||
salesChannelsColumnCount: number
|
||||
pricesData: Set<string>
|
||||
} {
|
||||
let optionColumnCount = 0
|
||||
let imageColumnCount = 0
|
||||
let salesChannelsColumnCount = 0
|
||||
const pricesData = new Set<string>()
|
||||
|
||||
// Retrieve the highest count of each object to build the dynamic columns later
|
||||
for (const product of products) {
|
||||
const optionsCount = product?.options?.length ?? 0
|
||||
optionColumnCount = Math.max(optionColumnCount, optionsCount)
|
||||
|
||||
const imageCount = product?.images?.length ?? 0
|
||||
imageColumnCount = Math.max(imageColumnCount, imageCount)
|
||||
|
||||
if (
|
||||
this.featureFlagRouter_.isFeatureEnabled(SalesChannelFeatureFlag.key)
|
||||
) {
|
||||
const salesChannelCount = product?.sales_channels?.length ?? 0
|
||||
salesChannelsColumnCount = Math.max(
|
||||
salesChannelsColumnCount,
|
||||
salesChannelCount
|
||||
)
|
||||
}
|
||||
|
||||
for (const variant of product?.variants ?? []) {
|
||||
if (variant.prices?.length) {
|
||||
variant.prices.forEach((price) => {
|
||||
pricesData.add(
|
||||
JSON.stringify({
|
||||
currency_code: price.currency_code,
|
||||
region: price.region
|
||||
? {
|
||||
currency_code: price.region.currency_code,
|
||||
name: price.region.name,
|
||||
id: price.region.id,
|
||||
}
|
||||
: null,
|
||||
})
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
optionColumnCount,
|
||||
imageColumnCount,
|
||||
salesChannelsColumnCount,
|
||||
pricesData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ export type ProductExportBatchJobContext = {
|
||||
prices: ProductExportPriceData[]
|
||||
dynamicOptionColumnCount: number
|
||||
dynamicImageColumnCount: number
|
||||
dynamicSalesChannelsColumnCount: number
|
||||
}
|
||||
list_config?: {
|
||||
select?: string[]
|
||||
|
||||
Reference in New Issue
Block a user