diff --git a/packages/medusa/src/strategies/__fixtures__/order-export-data.ts b/packages/medusa/src/strategies/__fixtures__/order-export-data.ts index c993150f1b..2c5fd5209c 100644 --- a/packages/medusa/src/strategies/__fixtures__/order-export-data.ts +++ b/packages/medusa/src/strategies/__fixtures__/order-export-data.ts @@ -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[] = [ 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[] = [ last_name: "Doe", email: "Jane@Doe.com", }, + sales_channel: { + id: IdMap.getId("sc_2"), + name: "SC 2", + description: "SC 2", + }, }, ] diff --git a/packages/medusa/src/strategies/__fixtures__/product-export-data.ts b/packages/medusa/src/strategies/__fixtures__/product-export-data.ts index 1403170edf..9e2ef61887 100644 --- a/packages/medusa/src/strategies/__fixtures__/product-export-data.ts +++ b/packages/medusa/src/strategies/__fixtures__/product-export-data.ts @@ -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, diff --git a/packages/medusa/src/strategies/__tests__/batch-jobs/order/__snapshots__/order-export.ts.snap b/packages/medusa/src/strategies/__tests__/batch-jobs/order/__snapshots__/order-export.ts.snap index 150d2d471e..6cc44b37c8 100644 --- a/packages/medusa/src/strategies/__tests__/batch-jobs/order/__snapshots__/order-export.ts.snap +++ b/packages/medusa/src/strategies/__tests__/batch-jobs/order/__snapshots__/order-export.ts.snap @@ -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 +", +] +`; diff --git a/packages/medusa/src/strategies/__tests__/batch-jobs/order/order-export.ts b/packages/medusa/src/strategies/__tests__/batch-jobs/order/order-export.ts index e7495d5bfe..74e141665a 100644 --- a/packages/medusa/src/strategies/__tests__/batch-jobs/order/order-export.ts +++ b/packages/medusa/src/strategies/__tests__/batch-jobs/order/order-export.ts @@ -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() + }) +}) \ No newline at end of file diff --git a/packages/medusa/src/strategies/__tests__/batch-jobs/product/__snapshots__/export.ts.snap b/packages/medusa/src/strategies/__tests__/batch-jobs/product/__snapshots__/export.ts.snap index 537bc939a9..e2ba7d7ad4 100644 --- a/packages/medusa/src/strategies/__tests__/batch-jobs/product/__snapshots__/export.ts.snap +++ b/packages/medusa/src/strategies/__tests__/batch-jobs/product/__snapshots__/export.ts.snap @@ -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 +", +] +`; diff --git a/packages/medusa/src/strategies/__tests__/batch-jobs/product/export.ts b/packages/medusa/src/strategies/__tests__/batch-jobs/product/export.ts index 887a238d75..402a4ffc5b 100644 --- a/packages/medusa/src/strategies/__tests__/batch-jobs/product/export.ts +++ b/packages/medusa/src/strategies/__tests__/batch-jobs/product/export.ts @@ -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() + }) +}) \ No newline at end of file diff --git a/packages/medusa/src/strategies/batch-jobs/order/export.ts b/packages/medusa/src/strategies/batch-jobs/order/export.ts index d8edbd3774..49195615f1 100644 --- a/packages/medusa/src/strategies/batch-jobs/order/export.ts +++ b/packages/medusa/src/strategies/batch-jobs/order/export.ts @@ -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 + fileService: IFileService orderService: OrderService batchJobService: BatchJobService manager: EntityManager + featureFlagRouter: FlagRouter } class OrderExportStrategy extends AbstractBatchJobStrategy { @@ -36,6 +39,11 @@ class OrderExportStrategy extends AbstractBatchJobStrategy protected readonly fileService_: IFileService 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 batchJobService, orderService, manager, + featureFlagRouter, }: InjectedDependencies) { // eslint-disable-next-line prefer-rest-params super(arguments[0]) @@ -69,6 +78,12 @@ class OrderExportStrategy extends AbstractBatchJobStrategy 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 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 } 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 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 diff --git a/packages/medusa/src/strategies/batch-jobs/product/export.ts b/packages/medusa/src/strategies/batch-jobs/product/export.ts index 4f6f5b3313..2e52e70c46 100644 --- a/packages/medusa/src/strategies/batch-jobs/product/export.ts +++ b/packages/medusa/src/strategies/batch-jobs/product/export.ts @@ -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 + 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 + 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 { @@ -138,8 +149,8 @@ export default class ProductExportStrategy extends AbstractBatchJobStrategy< let dynamicOptionColumnCount = 0 let dynamicImageColumnCount = 0 - - const pricesData = new Set() + let dynamicSalesChannelsColumnCount = 0 + let pricesData = new Set() 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 + } { + let optionColumnCount = 0 + let imageColumnCount = 0 + let salesChannelsColumnCount = 0 + const pricesData = new Set() + + // 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, + } + } } diff --git a/packages/medusa/src/strategies/batch-jobs/product/index.ts b/packages/medusa/src/strategies/batch-jobs/product/index.ts index 47743c0485..4bad5a93e8 100644 --- a/packages/medusa/src/strategies/batch-jobs/product/index.ts +++ b/packages/medusa/src/strategies/batch-jobs/product/index.ts @@ -14,6 +14,7 @@ export type ProductExportBatchJobContext = { prices: ProductExportPriceData[] dynamicOptionColumnCount: number dynamicImageColumnCount: number + dynamicSalesChannelsColumnCount: number } list_config?: { select?: string[]