Skip to content

Commit abc6b8e

Browse files
authored
Merge pull request #72 from openboxes/OBPIH-6969
OBPIH-6969 Improve approach to product creation
2 parents caf8118 + 0eaf31a commit abc6b8e

61 files changed

Lines changed: 529 additions & 360 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

playwright.config.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export default defineConfig({
3232

3333
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
3434
trace: 'retain-on-failure',
35-
35+
3636
launchOptions: {
3737
// slowMo: 1000,
3838
},
@@ -60,14 +60,23 @@ export default defineConfig({
6060
storageState: appConfig.users['main'].storagePath,
6161
},
6262
},
63+
{
64+
name: 'data-import-setup',
65+
testMatch: 'dataImport.setup.ts',
66+
testDir: './src/setup',
67+
dependencies: ['create-data-setup'],
68+
use: {
69+
storageState: appConfig.users['main'].storagePath,
70+
},
71+
},
6372
{
6473
name: 'chromium',
6574
use: {
6675
...devices['Desktop Chrome'],
6776
viewport: { width: 1366, height: 768 },
6877
storageState: appConfig.users['main'].storagePath,
6978
},
70-
dependencies: ['auth-setup', 'create-data-setup'],
79+
dependencies: ['auth-setup', 'create-data-setup', 'data-import-setup'],
7180
},
7281
],
7382
});

src/api/InventoryService.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import BaseServiceModel from '@/api/BaseServiceModel';
2+
import { jsonToCsv } from '@/utils/ServiceUtils';
3+
4+
class InventoryService extends BaseServiceModel {
5+
async importInventories(data: Record<string, string>[], facilityId: string): Promise<void> {
6+
try {
7+
const csvContent = jsonToCsv(data);
8+
9+
const response = await this.request.post(`./api/facilities/${facilityId}/inventories/import`, {
10+
data: csvContent,
11+
headers: { 'Content-Type': 'text/csv' },
12+
});
13+
14+
if (!response.ok()) {
15+
throw new Error(`Import failed with status ${response.status()}: ${await response.text()}`);
16+
}
17+
} catch (error) {
18+
throw new Error(`Problem importing inventories: ${error instanceof Error ? error.message : String(error)}`);
19+
}
20+
}
21+
}
22+
23+
export default InventoryService;

src/api/ProductService.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import BaseServiceModel from '@/api/BaseServiceModel';
22
import { ApiResponse, ProductDemandResponse, ProductResponse } from '@/types';
3-
import { parseRequestToJSON } from '@/utils/ServiceUtils';
3+
import { jsonToCsv, parseRequestToJSON } from '@/utils/ServiceUtils';
44

55
class ProductService extends BaseServiceModel {
66
async getDemand(id: string): Promise<ApiResponse<ProductDemandResponse>> {
@@ -22,6 +22,24 @@ class ProductService extends BaseServiceModel {
2222
throw new Error('Problem fetching product data');
2323
}
2424
}
25+
26+
async importProducts(data: Record<string, string>[]): Promise<ApiResponse<ProductResponse[]>> {
27+
try {
28+
const csvContent = jsonToCsv(data);
29+
30+
const apiResponse = await this.request.post(
31+
'./api/products/import',
32+
{
33+
data: csvContent,
34+
headers: { 'Content-Type': 'text/csv' }
35+
}
36+
);
37+
38+
return await parseRequestToJSON(apiResponse);
39+
} catch (error) {
40+
throw new Error(`Problem importing products: ${error instanceof Error ? error.message : String(error)}`);
41+
}
42+
}
2543
}
2644

2745
export default ProductService;

src/config/AppConfig.ts

Lines changed: 17 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import TestUserConfig from '@/config/TestUserConfig';
88
import { ActivityCode } from '@/constants/ActivityCodes';
99
import { LocationTypeCode } from '@/constants/LocationTypeCode';
1010
import RoleType from '@/constants/RoleTypes';
11+
import { readCsvFile } from '@/utils/FileIOUtils';
1112
import UniqueIdentifier from '@/utils/UniqueIdentifier';
1213

1314
export enum USER_KEY {
@@ -54,6 +55,12 @@ class AppConfig {
5455

5556
public static TEST_DATA_FILE_PATH = path.join(process.cwd(), '.data.json');
5657

58+
public static DATA_IMPORT_DIRECTORY_PATH = path.join(process.cwd(), 'src/setup/dataImport');
59+
60+
public static PRODUCTS_IMPORT_FILE_PATH = path.join(AppConfig.DATA_IMPORT_DIRECTORY_PATH, '/products.csv');
61+
62+
public static INVENTORY_IMPORT_FILE_PATH = path.join(AppConfig.DATA_IMPORT_DIRECTORY_PATH, '/inventory.csv');
63+
5764
// Base URL to use in actions like `await page.goto('./dashboard')`.
5865
public appURL!: string;
5966

@@ -67,7 +74,7 @@ class AppConfig {
6774
public locations!: Record<LOCATION_KEY, LocationConfig>;
6875

6976
// test products used in all of the tests
70-
public products!: Record<PRODUCT_KEY, ProductConfig>;
77+
public products: Record<string, ProductConfig> = {};
7178

7279
//recivingbin configurable prefix
7380
public receivingBinPrefix!: string;
@@ -286,44 +293,16 @@ class AppConfig {
286293
}),
287294
};
288295

289-
this.products = {
290-
productOne: new ProductConfig({
291-
id: env.get('PRODUCT_ONE').asString(),
292-
key: PRODUCT_KEY.ONE,
293-
name: this.uniqueIdentifier.generateUniqueString('product-one'),
294-
quantity: 122,
295-
required: false,
296-
}),
297-
productTwo: new ProductConfig({
298-
id: env.get('PRODUCT_TWO').asString(),
299-
key: PRODUCT_KEY.TWO,
300-
name: this.uniqueIdentifier.generateUniqueString('product-two'),
301-
quantity: 123,
302-
required: false,
303-
}),
304-
productThree: new ProductConfig({
305-
id: env.get('PRODUCT_THREE').asString(),
306-
key: PRODUCT_KEY.THREE,
307-
name: this.uniqueIdentifier.generateUniqueString('product-three'),
308-
quantity: 150,
296+
// Fulfill products data in app config dynamically based on the products.csv
297+
const productsData = readCsvFile(AppConfig.PRODUCTS_IMPORT_FILE_PATH);
298+
productsData.forEach((productData) => {
299+
this.products[productData['ProductCode']] = new ProductConfig({
300+
key: productData['ProductCode'],
301+
name: productData['Name'],
302+
quantity: parseInt(productData['Quantity']),
309303
required: false,
310-
}),
311-
productFour: new ProductConfig({
312-
id: env.get('PRODUCT_FOUR').asString(),
313-
key: PRODUCT_KEY.FOUR,
314-
name: this.uniqueIdentifier.generateUniqueString('product-four'),
315-
quantity: 100,
316-
required: false,
317-
}),
318-
productFive: new ProductConfig({
319-
id: env.get('PRODUCT_FIVE').asString(),
320-
key: PRODUCT_KEY.FIVE,
321-
name: this.uniqueIdentifier.generateUniqueString('aa-product-five'),
322-
//'aa' part was added to improve visibility of ordering products alphabetically
323-
quantity: 0,
324-
required: false,
325-
}),
326-
};
304+
})
305+
})
327306

328307
this.receivingBinPrefix = env
329308
.get('RECEIVING_BIN_PREFIX')

src/config/ProductConfig.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ class ProductConfig {
4444
* @returns {boolean}
4545
*/
4646
get isCreateNew() {
47-
return !this.id;
47+
return !this.readId();
4848
}
4949

5050
/**

src/fixtures/fixtures.ts

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import LocationChooser from '@/components/LocationChooser';
1010
import Navbar from '@/components/Navbar';
1111
import AppConfig, {
1212
LOCATION_KEY,
13-
PRODUCT_KEY,
1413
USER_KEY,
1514
} from '@/config/AppConfig';
1615
import CreateInbound from '@/pages/inbound/create/CreateInboundPage';
@@ -95,11 +94,7 @@ type Fixtures = {
9594
internalLocation2Service: LocationData;
9695

9796
// PRODUCT DATA
98-
mainProductService: ProductData;
99-
otherProductService: ProductData;
100-
thirdProductService: ProductData;
101-
fourthProductService: ProductData;
102-
fifthProductService: ProductData;
97+
productService: ProductData;
10398
// USERS DATA
10499
mainUserService: UserData;
105100
altUserService: UserData;
@@ -184,16 +179,8 @@ export const test = baseTest.extend<Fixtures>({
184179
internalLocation2Service: async ({ page }, use) =>
185180
use(new LocationData(LOCATION_KEY.BIN_LOCATION2, page.request)),
186181
// PRODUCTS
187-
mainProductService: async ({ page }, use) =>
188-
use(new ProductData(PRODUCT_KEY.ONE, page.request)),
189-
otherProductService: async ({ page }, use) =>
190-
use(new ProductData(PRODUCT_KEY.TWO, page.request)),
191-
thirdProductService: async ({ page }, use) =>
192-
use(new ProductData(PRODUCT_KEY.THREE, page.request)),
193-
fourthProductService: async ({ page }, use) =>
194-
use(new ProductData(PRODUCT_KEY.FOUR, page.request)),
195-
fifthProductService: async ({ page }, use) =>
196-
use(new ProductData(PRODUCT_KEY.FIVE, page.request)),
182+
productService: async ({ page }, use) =>
183+
use(new ProductData(page.request)),
197184
// USERS
198185
mainUserService: async ({ page }, use) =>
199186
use(new UserData(USER_KEY.MAIN, page.request)),

src/setup/createData.setup.ts

Lines changed: 1 addition & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,19 @@
11
import AppConfig from '@/config/AppConfig';
22
import { test } from '@/fixtures/fixtures';
33
import { readFile, writeToFile } from '@/utils/FileIOUtils';
4-
import { parseUrl } from '@/utils/UrlUtils';
54

65
test('create data', async ({
7-
page,
8-
createProductPage,
9-
productShowPage,
106
locationService,
117
mainLocationService,
128
}) => {
139
// eslint-disable-next-line playwright/no-conditional-in-test
1410
const data = readFile(AppConfig.TEST_DATA_FILE_PATH) || {};
1511

16-
const seedData: Record<'products' | 'locations', Record<string, string>> = {
12+
const seedData: Record<'locations', Record<string, string>> = {
1713
...data,
18-
products: {},
1914
locations: {},
2015
};
2116

22-
// // PRODUCST
23-
const products = Object.values(AppConfig.instance.products).filter(
24-
(product) => product.isCreateNew
25-
);
26-
27-
for (const product of products) {
28-
await test.step(`create product ${product.key}`, async () => {
29-
await createProductPage.goToPage();
30-
await createProductPage.waitForUrl();
31-
await createProductPage.productDetails.nameField.fill(product.name);
32-
await createProductPage.productDetails.categorySelect.click();
33-
await createProductPage.productDetails.categorySelectDropdown
34-
.getByRole('listitem')
35-
.first()
36-
.click();
37-
await createProductPage.saveButton.click();
38-
39-
await productShowPage.recordStockButton.click();
40-
41-
await productShowPage.recordStock.lineItemsTable
42-
.row(1)
43-
.newQuantity.getByRole('textbox')
44-
.fill(`${product.quantity}`);
45-
await productShowPage.recordStock.lineItemsTable.saveButton.click();
46-
await productShowPage.showStockCardButton.click();
47-
48-
const productUrl = parseUrl(
49-
page.url(),
50-
'/openboxes/inventoryItem/showStockCard/$id'
51-
);
52-
seedData.products[`${product.key}`] = productUrl.id;
53-
});
54-
}
55-
5617
// LOCATIONS
5718
const { organization } = await mainLocationService.getLocation();
5819
const { data: locationTypes } = await locationService.getLocationTypes();

src/setup/dataImport.setup.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import InventoryService from '@/api/InventoryService';
2+
import ProductService from '@/api/ProductService';
3+
import AppConfig from '@/config/AppConfig';
4+
import { test } from '@/fixtures/fixtures';
5+
import { readCsvFile, readFile, writeToFile } from '@/utils/FileIOUtils';
6+
7+
test('import data', async ({ request }) => {
8+
// eslint-disable-next-line playwright/no-conditional-in-test
9+
const data = readFile(AppConfig.TEST_DATA_FILE_PATH) || {};
10+
11+
const seedData: Record<'products', Record<string, string>> = {
12+
...data,
13+
products: {},
14+
};
15+
16+
// PRODUCTS
17+
const productService = new ProductService(request);
18+
19+
const productsData = readCsvFile(AppConfig.PRODUCTS_IMPORT_FILE_PATH);
20+
21+
await test.step(`importing ${productsData.length} products`, async () => {
22+
const importedData = await productService.importProducts(productsData);
23+
importedData.data.forEach((product) => {
24+
seedData.products[product.productCode] = product.id;
25+
})
26+
})
27+
28+
// INVENTORIES
29+
const inventoryService = new InventoryService(request);
30+
31+
const inventoriesData = readCsvFile(AppConfig.INVENTORY_IMPORT_FILE_PATH);
32+
33+
await test.step(`importing ${inventoriesData.length} inventories`, async () => {
34+
await inventoryService.importInventories(
35+
inventoriesData,
36+
AppConfig.instance.locations.main.id,
37+
);
38+
});
39+
40+
writeToFile(AppConfig.TEST_DATA_FILE_PATH, seedData);
41+
})

src/setup/dataImport/inventory.csv

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Product code,Product,Lot number,Expiration date,Bin location,Quantity,Comment
2+
1,E2E-product-one,,,,122,
3+
2,E2E-product-two,,,,123,
4+
3,E2E-product-three,,,,100,
5+
4,E2E-product-four,,,,100,

src/setup/dataImport/products.csv

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Id,Active,ProductCode,ProductType,Name,ProductFamily,Category,GLAccount,Description,UnitOfMeasure,Tags,UnitCost,LotAndExpiryControl,ColdChain,ControlledSubstance,HazardousMaterial,Reconditioned,Manufacturer,BrandName,ManufacturerCode,ManufacturerName,Vendor,VendorCode,VendorName,UPC,NDC,Created,Updated
2+
,true,1,Default,E2E-product-one,,ARVS,,,,,,,,,,,,,,,,,,,,,
3+
,true,2,Default,E2E-product-two,,ARVS,,,,,,,,,,,,,,,,,,,,,
4+
,true,3,Default,E2E-product-three,,ARVS,,,,,,,,,,,,,,,,,,,,,
5+
,true,4,Default,E2E-product-four,,ARVS,,,,,,,,,,,,,,,,,,,,,
6+
,true,5,Default,E2E-product-five,,ARVS,,,,,,,,,,,,,,,,,,,,,

0 commit comments

Comments
 (0)