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 { DeepPartial } from "typeorm"
|
||||||
|
import { IdMap } from "medusa-test-utils"
|
||||||
import {
|
import {
|
||||||
FulfillmentStatus,
|
FulfillmentStatus,
|
||||||
Order,
|
Order,
|
||||||
@@ -39,6 +40,11 @@ export const ordersToExport: DeepPartial<Order>[] = [
|
|||||||
last_name: "Doe",
|
last_name: "Doe",
|
||||||
email: "John@Doe.com",
|
email: "John@Doe.com",
|
||||||
},
|
},
|
||||||
|
sales_channel: {
|
||||||
|
id: IdMap.getId("sc_1"),
|
||||||
|
name: "SC 1",
|
||||||
|
description: "SC 1",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "order_2",
|
id: "order_2",
|
||||||
@@ -70,5 +76,10 @@ export const ordersToExport: DeepPartial<Order>[] = [
|
|||||||
last_name: "Doe",
|
last_name: "Doe",
|
||||||
email: "Jane@Doe.com",
|
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 = [
|
export const productsToExport = [
|
||||||
{
|
{
|
||||||
|
sales_channels: [
|
||||||
|
{ id: IdMap.getId("sc_1"), name: "SC 1", description: "SC 1" },
|
||||||
|
],
|
||||||
collection: {
|
collection: {
|
||||||
created_at: "randomString",
|
created_at: "randomString",
|
||||||
deleted_at: null,
|
deleted_at: null,
|
||||||
@@ -202,6 +205,10 @@ export const productsToExport = [
|
|||||||
width: null,
|
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: {
|
collection: {
|
||||||
created_at: "randomString",
|
created_at: "randomString",
|
||||||
deleted_at: null,
|
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,7 +3,31 @@ import { IdMap, MockManager } from "medusa-test-utils"
|
|||||||
import { User } from "../../../../models"
|
import { User } from "../../../../models"
|
||||||
import { BatchJobStatus } from "../../../../types/batch-job"
|
import { BatchJobStatus } from "../../../../types/batch-job"
|
||||||
import { ordersToExport } from "../../../__fixtures__/order-export-data"
|
import { ordersToExport } from "../../../__fixtures__/order-export-data"
|
||||||
|
import { FlagRouter } from "../../../../utils/flag-router";
|
||||||
|
import SalesChannelFeatureFlag from "../../../../loaders/feature-flags/sales-channels";
|
||||||
|
|
||||||
|
const orderServiceMock = {
|
||||||
|
withTransaction: function (): any {
|
||||||
|
return this
|
||||||
|
},
|
||||||
|
listAndCount: jest
|
||||||
|
.fn()
|
||||||
|
.mockImplementation(() =>
|
||||||
|
Promise.resolve([ordersToExport, ordersToExport.length])
|
||||||
|
),
|
||||||
|
list: jest.fn().mockImplementation(() => Promise.resolve(ordersToExport)),
|
||||||
|
}
|
||||||
|
const orderServiceWithoutDataMock = {
|
||||||
|
...orderServiceMock,
|
||||||
|
listAndCount: jest
|
||||||
|
.fn()
|
||||||
|
.mockImplementation(() =>
|
||||||
|
Promise.resolve([[], 0])
|
||||||
|
),
|
||||||
|
list: jest.fn().mockImplementation(() => Promise.resolve([])),
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("Order export strategy", () => {
|
||||||
const outputDataStorage: string[] = []
|
const outputDataStorage: string[] = []
|
||||||
|
|
||||||
let fakeJob = {
|
let fakeJob = {
|
||||||
@@ -78,33 +102,13 @@ const batchJobServiceMock = {
|
|||||||
return fakeJob
|
return fakeJob
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
const orderServiceMock = {
|
|
||||||
withTransaction: function (): any {
|
|
||||||
return this
|
|
||||||
},
|
|
||||||
listAndCount: jest
|
|
||||||
.fn()
|
|
||||||
.mockImplementation(() =>
|
|
||||||
Promise.resolve([ordersToExport, ordersToExport.length])
|
|
||||||
),
|
|
||||||
list: jest.fn().mockImplementation(() => Promise.resolve(ordersToExport)),
|
|
||||||
}
|
|
||||||
const orderServiceWithoutDataMock = {
|
|
||||||
...orderServiceMock,
|
|
||||||
listAndCount: jest
|
|
||||||
.fn()
|
|
||||||
.mockImplementation(() =>
|
|
||||||
Promise.resolve([[], 0])
|
|
||||||
),
|
|
||||||
list: jest.fn().mockImplementation(() => Promise.resolve([])),
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("Order export strategy", () => {
|
|
||||||
const orderExportStrategy = new OrderExportStrategy({
|
const orderExportStrategy = new OrderExportStrategy({
|
||||||
batchJobService: batchJobServiceMock as any,
|
batchJobService: batchJobServiceMock as any,
|
||||||
fileService: fileServiceMock as any,
|
fileService: fileServiceMock as any,
|
||||||
orderService: orderServiceMock as any,
|
orderService: orderServiceMock as any,
|
||||||
manager: MockManager,
|
manager: MockManager,
|
||||||
|
featureFlagRouter: new FlagRouter({}),
|
||||||
})
|
})
|
||||||
|
|
||||||
it("Should generate header as template", async () => {
|
it("Should generate header as template", async () => {
|
||||||
@@ -150,6 +154,147 @@ describe("Order export strategy", () => {
|
|||||||
fileService: fileServiceMock as any,
|
fileService: fileServiceMock as any,
|
||||||
orderService: orderServiceWithoutDataMock as any,
|
orderService: orderServiceWithoutDataMock as any,
|
||||||
manager: MockManager,
|
manager: MockManager,
|
||||||
|
featureFlagRouter: new FlagRouter({}),
|
||||||
|
})
|
||||||
|
|
||||||
|
await orderExportStrategy.processJob(fakeJob.id)
|
||||||
|
|
||||||
|
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)
|
await orderExportStrategy.processJob(fakeJob.id)
|
||||||
|
|||||||
+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,26 +6,31 @@ import { productsToExport } from "../../../__fixtures__/product-export-data"
|
|||||||
import { AdminPostBatchesReq, defaultAdminProductRelations } from "../../../../api"
|
import { AdminPostBatchesReq, defaultAdminProductRelations } from "../../../../api"
|
||||||
import { ProductExportBatchJob } from "../../../batch-jobs/product"
|
import { ProductExportBatchJob } from "../../../batch-jobs/product"
|
||||||
import { Request } from "express"
|
import { Request } from "express"
|
||||||
|
import { FlagRouter } from "../../../../utils/flag-router";
|
||||||
|
import SalesChannelFeatureFlag from "../../../../loaders/feature-flags/sales-channels";
|
||||||
|
|
||||||
|
const productServiceMock = {
|
||||||
|
withTransaction: function () {
|
||||||
|
return this
|
||||||
|
},
|
||||||
|
list: jest.fn().mockImplementation(() => Promise.resolve(productsToExport)),
|
||||||
|
count: jest.fn().mockImplementation(() => Promise.resolve(productsToExport.length)),
|
||||||
|
listAndCount: jest.fn().mockImplementation(() => {
|
||||||
|
return Promise.resolve([productsToExport, productsToExport.length])
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
const productServiceWithNoDataMock = {
|
||||||
|
...productServiceMock,
|
||||||
|
list: jest.fn().mockImplementation(() => Promise.resolve([])),
|
||||||
|
count: jest.fn().mockImplementation(() => Promise.resolve(0)),
|
||||||
|
listAndCount: jest.fn().mockImplementation(() => {
|
||||||
|
return Promise.resolve([[], 0])
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
const managerMock = MockManager
|
||||||
|
|
||||||
|
describe("Product export strategy", () => {
|
||||||
const outputDataStorage: string[] = []
|
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 = {
|
const fileServiceMock = {
|
||||||
delete: jest.fn(),
|
delete: jest.fn(),
|
||||||
getUploadStreamDescriptor: jest.fn().mockImplementation(() => {
|
getUploadStreamDescriptor: jest.fn().mockImplementation(() => {
|
||||||
@@ -44,6 +49,23 @@ const fileServiceMock = {
|
|||||||
return this
|
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 = {
|
const batchJobServiceMock = {
|
||||||
withTransaction: function () {
|
withTransaction: function () {
|
||||||
return this
|
return this
|
||||||
@@ -87,32 +109,13 @@ const batchJobServiceMock = {
|
|||||||
console.error(...args)
|
console.error(...args)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
const productServiceMock = {
|
|
||||||
withTransaction: function () {
|
|
||||||
return this
|
|
||||||
},
|
|
||||||
list: jest.fn().mockImplementation(() => Promise.resolve(productsToExport)),
|
|
||||||
count: jest.fn().mockImplementation(() => Promise.resolve(productsToExport.length)),
|
|
||||||
listAndCount: jest.fn().mockImplementation(() => {
|
|
||||||
return Promise.resolve([productsToExport, productsToExport.length])
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
const productServiceWithNoDataMock = {
|
|
||||||
...productServiceMock,
|
|
||||||
list: jest.fn().mockImplementation(() => Promise.resolve([])),
|
|
||||||
count: jest.fn().mockImplementation(() => Promise.resolve(0)),
|
|
||||||
listAndCount: jest.fn().mockImplementation(() => {
|
|
||||||
return Promise.resolve([[], 0])
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
const managerMock = MockManager
|
|
||||||
|
|
||||||
describe("Product export strategy", () => {
|
|
||||||
const productExportStrategy = new ProductExportStrategy({
|
const productExportStrategy = new ProductExportStrategy({
|
||||||
manager: managerMock,
|
manager: managerMock,
|
||||||
fileService: fileServiceMock as any,
|
fileService: fileServiceMock as any,
|
||||||
batchJobService: batchJobServiceMock as any,
|
batchJobService: batchJobServiceMock as any,
|
||||||
productService: productServiceMock as any,
|
productService: productServiceMock as any,
|
||||||
|
featureFlagRouter: new FlagRouter({}),
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should generate the appropriate template', async () => {
|
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 Name.*/)
|
||||||
expect(template).toMatch(/.*Option 2 Value.*/)
|
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 USD.*/)
|
||||||
expect(template).toMatch(/.*Price france \[USD\].*/)
|
expect(template).toMatch(/.*Price france \[USD\].*/)
|
||||||
expect(template).toMatch(/.*Price denmark \[DKK\].*/)
|
expect(template).toMatch(/.*Price denmark \[DKK\].*/)
|
||||||
@@ -242,6 +250,7 @@ describe("Product export strategy", () => {
|
|||||||
fileService: fileServiceMock as any,
|
fileService: fileServiceMock as any,
|
||||||
productService: productServiceWithNoDataMock as any,
|
productService: productServiceWithNoDataMock as any,
|
||||||
manager: MockManager,
|
manager: MockManager,
|
||||||
|
featureFlagRouter: new FlagRouter({}),
|
||||||
})
|
})
|
||||||
|
|
||||||
await productExportStrategy.prepareBatchJobForProcessing(fakeJob, {} as Request)
|
await productExportStrategy.prepareBatchJobForProcessing(fakeJob, {} as Request)
|
||||||
@@ -257,6 +266,7 @@ describe("Product export strategy", () => {
|
|||||||
fileService: fileServiceMock as any,
|
fileService: fileServiceMock as any,
|
||||||
productService: productServiceMock as any,
|
productService: productServiceMock as any,
|
||||||
manager: MockManager,
|
manager: MockManager,
|
||||||
|
featureFlagRouter: new FlagRouter({}),
|
||||||
})
|
})
|
||||||
|
|
||||||
await productExportStrategy.prepareBatchJobForProcessing(canceledFakeJob, {} as Request)
|
await productExportStrategy.prepareBatchJobForProcessing(canceledFakeJob, {} as Request)
|
||||||
@@ -267,3 +277,165 @@ describe("Product export strategy", () => {
|
|||||||
expect((canceledFakeJob.result as any).file_size).not.toBeDefined()
|
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,
|
OrderExportBatchJobContext,
|
||||||
orderExportPropertiesDescriptors,
|
orderExportPropertiesDescriptors,
|
||||||
} from "."
|
} from "."
|
||||||
import { AdminPostBatchesReq } from "../../../api/routes/admin/batch/create-batch-job"
|
import { AdminPostBatchesReq } from "../../../api"
|
||||||
import { IFileService } from "../../../interfaces"
|
import { IFileService } from "../../../interfaces"
|
||||||
import { AbstractBatchJobStrategy } from "../../../interfaces/batch-job-strategy"
|
import { AbstractBatchJobStrategy } from "../../../interfaces"
|
||||||
import { Order } from "../../../models"
|
import { Order } from "../../../models"
|
||||||
import { OrderService } from "../../../services"
|
import { OrderService } from "../../../services"
|
||||||
import BatchJobService from "../../../services/batch-job"
|
import BatchJobService from "../../../services/batch-job"
|
||||||
import { BatchJobStatus } from "../../../types/batch-job"
|
import { BatchJobStatus } from "../../../types/batch-job"
|
||||||
import { prepareListQuery } from "../../../utils/get-query-config"
|
import { prepareListQuery } from "../../../utils/get-query-config"
|
||||||
|
import { FlagRouter } from "../../../utils/flag-router"
|
||||||
|
import SalesChannelFeatureFlag from "../../../loaders/feature-flags/sales-channels"
|
||||||
|
|
||||||
type InjectedDependencies = {
|
type InjectedDependencies = {
|
||||||
fileService: IFileService<any>
|
fileService: IFileService<never>
|
||||||
orderService: OrderService
|
orderService: OrderService
|
||||||
batchJobService: BatchJobService
|
batchJobService: BatchJobService
|
||||||
manager: EntityManager
|
manager: EntityManager
|
||||||
|
featureFlagRouter: FlagRouter
|
||||||
}
|
}
|
||||||
|
|
||||||
class OrderExportStrategy extends AbstractBatchJobStrategy<OrderExportStrategy> {
|
class OrderExportStrategy extends AbstractBatchJobStrategy<OrderExportStrategy> {
|
||||||
@@ -36,6 +39,11 @@ class OrderExportStrategy extends AbstractBatchJobStrategy<OrderExportStrategy>
|
|||||||
protected readonly fileService_: IFileService<any>
|
protected readonly fileService_: IFileService<any>
|
||||||
protected readonly batchJobService_: BatchJobService
|
protected readonly batchJobService_: BatchJobService
|
||||||
protected readonly orderService_: OrderService
|
protected readonly orderService_: OrderService
|
||||||
|
protected readonly featureFlagRouter_: FlagRouter
|
||||||
|
|
||||||
|
protected readonly orderExportPropertiesDescriptors = [
|
||||||
|
...orderExportPropertiesDescriptors,
|
||||||
|
]
|
||||||
|
|
||||||
protected readonly defaultRelations_ = ["customer", "shipping_address"]
|
protected readonly defaultRelations_ = ["customer", "shipping_address"]
|
||||||
protected readonly defaultFields_ = [
|
protected readonly defaultFields_ = [
|
||||||
@@ -61,6 +69,7 @@ class OrderExportStrategy extends AbstractBatchJobStrategy<OrderExportStrategy>
|
|||||||
batchJobService,
|
batchJobService,
|
||||||
orderService,
|
orderService,
|
||||||
manager,
|
manager,
|
||||||
|
featureFlagRouter,
|
||||||
}: InjectedDependencies) {
|
}: InjectedDependencies) {
|
||||||
// eslint-disable-next-line prefer-rest-params
|
// eslint-disable-next-line prefer-rest-params
|
||||||
super(arguments[0])
|
super(arguments[0])
|
||||||
@@ -69,6 +78,12 @@ class OrderExportStrategy extends AbstractBatchJobStrategy<OrderExportStrategy>
|
|||||||
this.fileService_ = fileService
|
this.fileService_ = fileService
|
||||||
this.batchJobService_ = batchJobService
|
this.batchJobService_ = batchJobService
|
||||||
this.orderService_ = orderService
|
this.orderService_ = orderService
|
||||||
|
this.featureFlagRouter_ = featureFlagRouter
|
||||||
|
|
||||||
|
if (featureFlagRouter.isFeatureEnabled(SalesChannelFeatureFlag.key)) {
|
||||||
|
this.defaultRelations_.push("sales_channel")
|
||||||
|
this.addSalesChannelColumns()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async prepareBatchJobForProcessing(
|
async prepareBatchJobForProcessing(
|
||||||
@@ -253,13 +268,12 @@ class OrderExportStrategy extends AbstractBatchJobStrategy<OrderExportStrategy>
|
|||||||
await promise
|
await promise
|
||||||
},
|
},
|
||||||
"REPEATABLE READ",
|
"REPEATABLE READ",
|
||||||
async (err: Error) => {
|
async (err: Error) =>
|
||||||
this.handleProcessingError(batchJobId, err, {
|
this.handleProcessingError(batchJobId, err, {
|
||||||
offset,
|
offset,
|
||||||
count: orderCount,
|
count: orderCount,
|
||||||
progress: offset / orderCount,
|
progress: offset / orderCount,
|
||||||
})
|
})
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -270,7 +284,7 @@ class OrderExportStrategy extends AbstractBatchJobStrategy<OrderExportStrategy>
|
|||||||
}
|
}
|
||||||
|
|
||||||
private buildHeader(
|
private buildHeader(
|
||||||
lineDescriptor: OrderDescriptor[] = orderExportPropertiesDescriptors
|
lineDescriptor: OrderDescriptor[] = this.orderExportPropertiesDescriptors
|
||||||
): string {
|
): string {
|
||||||
return (
|
return (
|
||||||
[...lineDescriptor.map(({ title }) => title)].join(this.DELIMITER) +
|
[...lineDescriptor.map(({ title }) => title)].join(this.DELIMITER) +
|
||||||
@@ -293,11 +307,20 @@ class OrderExportStrategy extends AbstractBatchJobStrategy<OrderExportStrategy>
|
|||||||
fields: string[],
|
fields: string[],
|
||||||
relations: string[]
|
relations: string[]
|
||||||
): OrderDescriptor[] {
|
): OrderDescriptor[] {
|
||||||
return orderExportPropertiesDescriptors.filter(
|
return this.orderExportPropertiesDescriptors.filter(
|
||||||
({ fieldName }) =>
|
({ fieldName }) =>
|
||||||
fields.indexOf(fieldName) !== -1 || relations.indexOf(fieldName) !== -1
|
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
|
export default OrderExportStrategy
|
||||||
|
|||||||
@@ -13,12 +13,15 @@ import {
|
|||||||
productExportSchemaDescriptors,
|
productExportSchemaDescriptors,
|
||||||
} from "./index"
|
} from "./index"
|
||||||
import { FindProductConfig } from "../../../types/product"
|
import { FindProductConfig } from "../../../types/product"
|
||||||
|
import { FlagRouter } from "../../../utils/flag-router"
|
||||||
|
import SalesChannelFeatureFlag from "../../../loaders/feature-flags/sales-channels"
|
||||||
|
|
||||||
type InjectedDependencies = {
|
type InjectedDependencies = {
|
||||||
manager: EntityManager
|
manager: EntityManager
|
||||||
batchJobService: BatchJobService
|
batchJobService: BatchJobService
|
||||||
productService: ProductService
|
productService: ProductService
|
||||||
fileService: IFileService<never>
|
fileService: IFileService<never>
|
||||||
|
featureFlagRouter: FlagRouter
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class ProductExportStrategy extends AbstractBatchJobStrategy<
|
export default class ProductExportStrategy extends AbstractBatchJobStrategy<
|
||||||
@@ -34,6 +37,7 @@ export default class ProductExportStrategy extends AbstractBatchJobStrategy<
|
|||||||
protected readonly batchJobService_: BatchJobService
|
protected readonly batchJobService_: BatchJobService
|
||||||
protected readonly productService_: ProductService
|
protected readonly productService_: ProductService
|
||||||
protected readonly fileService_: IFileService<never>
|
protected readonly fileService_: IFileService<never>
|
||||||
|
protected readonly featureFlagRouter_: FlagRouter
|
||||||
|
|
||||||
protected readonly defaultRelations_ = [
|
protected readonly defaultRelations_ = [
|
||||||
...defaultAdminProductRelations,
|
...defaultAdminProductRelations,
|
||||||
@@ -49,7 +53,7 @@ export default class ProductExportStrategy extends AbstractBatchJobStrategy<
|
|||||||
protected readonly columnDescriptors: Map<
|
protected readonly columnDescriptors: Map<
|
||||||
string,
|
string,
|
||||||
ProductExportColumnSchemaDescriptor
|
ProductExportColumnSchemaDescriptor
|
||||||
> = productExportSchemaDescriptors
|
> = new Map(productExportSchemaDescriptors)
|
||||||
|
|
||||||
private readonly NEWLINE_ = "\r\n"
|
private readonly NEWLINE_ = "\r\n"
|
||||||
private readonly DELIMITER_ = ";"
|
private readonly DELIMITER_ = ";"
|
||||||
@@ -60,18 +64,25 @@ export default class ProductExportStrategy extends AbstractBatchJobStrategy<
|
|||||||
batchJobService,
|
batchJobService,
|
||||||
productService,
|
productService,
|
||||||
fileService,
|
fileService,
|
||||||
|
featureFlagRouter,
|
||||||
}: InjectedDependencies) {
|
}: InjectedDependencies) {
|
||||||
super({
|
super({
|
||||||
manager,
|
manager,
|
||||||
batchJobService,
|
batchJobService,
|
||||||
productService,
|
productService,
|
||||||
fileService,
|
fileService,
|
||||||
|
featureFlagRouter,
|
||||||
})
|
})
|
||||||
|
|
||||||
this.manager_ = manager
|
this.manager_ = manager
|
||||||
this.batchJobService_ = batchJobService
|
this.batchJobService_ = batchJobService
|
||||||
this.productService_ = productService
|
this.productService_ = productService
|
||||||
this.fileService_ = fileService
|
this.fileService_ = fileService
|
||||||
|
this.featureFlagRouter_ = featureFlagRouter
|
||||||
|
|
||||||
|
if (featureFlagRouter.isFeatureEnabled(SalesChannelFeatureFlag.key)) {
|
||||||
|
this.defaultRelations_.push("sales_channels")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async buildTemplate(): Promise<string> {
|
async buildTemplate(): Promise<string> {
|
||||||
@@ -138,8 +149,8 @@ export default class ProductExportStrategy extends AbstractBatchJobStrategy<
|
|||||||
|
|
||||||
let dynamicOptionColumnCount = 0
|
let dynamicOptionColumnCount = 0
|
||||||
let dynamicImageColumnCount = 0
|
let dynamicImageColumnCount = 0
|
||||||
|
let dynamicSalesChannelsColumnCount = 0
|
||||||
const pricesData = new Set<string>()
|
let pricesData = new Set<string>()
|
||||||
|
|
||||||
while (offset < productCount) {
|
while (offset < productCount) {
|
||||||
if (!products?.length) {
|
if (!products?.length) {
|
||||||
@@ -152,39 +163,20 @@ export default class ProductExportStrategy extends AbstractBatchJobStrategy<
|
|||||||
} as FindProductConfig)
|
} as FindProductConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retrieve the highest count of each object to build the dynamic columns later
|
const shapeData = this.getProductRelationsDynamicColumnsShape(products)
|
||||||
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 = Math.max(
|
||||||
dynamicImageColumnCount,
|
shapeData.imageColumnCount,
|
||||||
imageCount
|
dynamicImageColumnCount
|
||||||
)
|
)
|
||||||
|
dynamicOptionColumnCount = Math.max(
|
||||||
for (const variant of product?.variants ?? []) {
|
shapeData.optionColumnCount,
|
||||||
if (variant.prices?.length) {
|
dynamicOptionColumnCount
|
||||||
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,
|
|
||||||
})
|
|
||||||
)
|
)
|
||||||
})
|
dynamicSalesChannelsColumnCount = Math.max(
|
||||||
}
|
shapeData.salesChannelsColumnCount,
|
||||||
}
|
dynamicSalesChannelsColumnCount
|
||||||
}
|
)
|
||||||
|
pricesData = new Set([...pricesData, ...shapeData.pricesData])
|
||||||
|
|
||||||
offset += products.length
|
offset += products.length
|
||||||
products = []
|
products = []
|
||||||
@@ -197,6 +189,7 @@ export default class ProductExportStrategy extends AbstractBatchJobStrategy<
|
|||||||
shape: {
|
shape: {
|
||||||
dynamicImageColumnCount,
|
dynamicImageColumnCount,
|
||||||
dynamicOptionColumnCount,
|
dynamicOptionColumnCount,
|
||||||
|
dynamicSalesChannelsColumnCount,
|
||||||
prices: [...pricesData].map((stringifyData) =>
|
prices: [...pricesData].map((stringifyData) =>
|
||||||
JSON.parse(stringifyData)
|
JSON.parse(stringifyData)
|
||||||
),
|
),
|
||||||
@@ -325,11 +318,13 @@ export default class ProductExportStrategy extends AbstractBatchJobStrategy<
|
|||||||
prices = [],
|
prices = [],
|
||||||
dynamicImageColumnCount,
|
dynamicImageColumnCount,
|
||||||
dynamicOptionColumnCount,
|
dynamicOptionColumnCount,
|
||||||
|
dynamicSalesChannelsColumnCount,
|
||||||
} = batchJob?.context?.shape ?? {}
|
} = batchJob?.context?.shape ?? {}
|
||||||
|
|
||||||
this.appendMoneyAmountDescriptors(prices)
|
this.appendMoneyAmountDescriptors(prices)
|
||||||
this.appendOptionsDescriptors(dynamicOptionColumnCount)
|
this.appendOptionsDescriptors(dynamicOptionColumnCount)
|
||||||
this.appendImagesDescriptors(dynamicImageColumnCount)
|
this.appendImagesDescriptors(dynamicImageColumnCount)
|
||||||
|
this.appendSalesChannelsDescriptors(dynamicSalesChannelsColumnCount)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
[...this.columnDescriptors.keys()].join(this.DELIMITER_) + this.NEWLINE_
|
[...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 {
|
private appendOptionsDescriptors(maxOptionsCount: number): void {
|
||||||
for (let i = 0; i < maxOptionsCount; ++i) {
|
for (let i = 0; i < maxOptionsCount; ++i) {
|
||||||
this.columnDescriptors
|
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[]
|
prices: ProductExportPriceData[]
|
||||||
dynamicOptionColumnCount: number
|
dynamicOptionColumnCount: number
|
||||||
dynamicImageColumnCount: number
|
dynamicImageColumnCount: number
|
||||||
|
dynamicSalesChannelsColumnCount: number
|
||||||
}
|
}
|
||||||
list_config?: {
|
list_config?: {
|
||||||
select?: string[]
|
select?: string[]
|
||||||
|
|||||||
Reference in New Issue
Block a user