Skip to content

Commit d77d062

Browse files
authored
OBPIH-6971 Create Receiving Service (#60)
* OBPIH-6971 Add partial receipt status enum * OBPIH-6971 Add new types * OBPIH-6971 Add receiving service * OBPIH-6971 Add unflatten util * OBPIH-6971 Add receiving service to fixtures
1 parent ac0dc1b commit d77d062

5 files changed

Lines changed: 407 additions & 0 deletions

File tree

src/api/ReceivingService.ts

Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
import { APIRequestContext } from '@playwright/test';
2+
import _ from 'lodash';
3+
4+
import BaseServiceModel from '@/api/BaseServiceModel';
5+
import { PartialReceiptStatus } from '@/constants/PartialReceiptStatus';
6+
import {
7+
ApiResponse,
8+
Container, ReceiptPayload,
9+
ReceiptResponse,
10+
ReceivingItemPayload,
11+
ShipmentItem,
12+
UnflattenContainer,
13+
} from '@/types';
14+
import { parseRequestToJSON, unflatten } from '@/utils/ServiceUtils';
15+
16+
class ReceivingService extends BaseServiceModel {
17+
constructor(request: APIRequestContext) {
18+
super(request);
19+
}
20+
21+
/*
22+
As an argument takes the shipment id, returns the receipt object.
23+
*/
24+
async getReceipt(id: string): Promise<ApiResponse<ReceiptResponse>> {
25+
try {
26+
const apiResponse = await this.request.get(
27+
`./api/partialReceiving/${id}`
28+
);
29+
return await parseRequestToJSON(apiResponse);
30+
} catch (error) {
31+
throw new Error('Problem fetching partial receipt');
32+
}
33+
}
34+
35+
/*
36+
As an argument takes the shipment id, changes the receipt status
37+
to completed.
38+
*/
39+
async completeReceipt(id: string): Promise<void> {
40+
try {
41+
await this.changeReceiptStatus(id, PartialReceiptStatus.COMPLETED);
42+
} catch (error) {
43+
throw new Error('Problem completing partial receipt');
44+
}
45+
}
46+
47+
/*
48+
As an argument takes the shipment id, rolls back the receipt (changes
49+
the receipt status).
50+
*/
51+
async rollbackReceipt(id: string): Promise<void> {
52+
try {
53+
await this.changeReceiptStatus(id, PartialReceiptStatus.ROLLBACK);
54+
} catch (error) {
55+
throw new Error('Problem rolling back partial receipt')
56+
}
57+
}
58+
59+
/*
60+
As arguments take the shipment id and items that need to be updated.
61+
It updates the receiving now quantity, comment and bin location field.
62+
Usage:
63+
await receivingService.updateReceivingItems(
64+
'shipmentId', [
65+
{
66+
shipmentItemId: 'firstShipmentItemId',
67+
quantityReceiving: quantityReceiving,
68+
comment: 'comment',
69+
binLocation: 'binLocationId'
70+
},
71+
{
72+
shipmentItemId: 'secondShipmentItemId',
73+
quantityReceiving: quantityReceiving,
74+
comment: 'comment',
75+
binLocation: 'binLocationId'
76+
}
77+
]);
78+
*/
79+
async updateReceivingItems(
80+
id: string,
81+
items: ReceivingItemPayload[],
82+
): Promise<void> {
83+
try {
84+
const receipt = await this.getReceipt(id);
85+
const shipmentItemsToUpdate = this.extractShipmentItemIds(items);
86+
const containers = this.buildUpdatedContainers(receipt.data.containers, items, shipmentItemsToUpdate);
87+
const payload: ReceiptPayload = {
88+
...receipt.data,
89+
containers: containers,
90+
recipient: receipt?.data?.recipient?.id,
91+
}
92+
await this.request.post(`./api/partialReceiving/${id}`, {
93+
data: payload,
94+
});
95+
} catch (error) {
96+
throw new Error('Problem updating items');
97+
}
98+
}
99+
100+
/*
101+
As arguments take the shipment id, id of receipt item that needs to be split,
102+
new lines are an array filled with new items, including the original one, that is split.
103+
The sum of all quantities in that batch should be equal to the quantity of the original item.
104+
This function does exactly the same that can be done using the split line modal.
105+
For new line lot number, expiration date and quantity shipped can be passed.
106+
Usage:
107+
await receivingService.splitReceivingLine(
108+
'shipmentId',
109+
'id of receipt item that will be split', [
110+
{
111+
shipmentItemId: 'shipment item id',
112+
receiptItemId: 'receipt item id', <- this line is treated as an original
113+
quantityShipped: new quantity shipped,
114+
},
115+
{
116+
shipmentItemId: 'shipment item id',
117+
receiptItemId: null, <- receipt item id should be set to null for all new items (backend requirement)
118+
quantityShipped: new quantity shipped,
119+
lotNumber: 'new lot number',
120+
expirationDate: 'new expiration date',
121+
newLine: true, <- new line should be set to true for all new items (backend requirement)
122+
},
123+
{
124+
shipmentItemId: 'shipment item id',
125+
receiptItemId: null,
126+
quantityShipped: new quantity shipped,
127+
lotNumber: 'new lot number',
128+
expirationDate: 'new expiration date',
129+
newLine: true,
130+
}
131+
]);
132+
*/
133+
async splitReceivingLine(
134+
id: string,
135+
originalReceiptItemId: string,
136+
newLines: ReceivingItemPayload[],
137+
) {
138+
try {
139+
const receipt = await this.getReceipt(id);
140+
141+
const originalShipmentItem = this.findOriginalShipmentItem(receipt.data.containers, originalReceiptItemId);
142+
if (!originalShipmentItem) {
143+
throw new Error(`Original shipment item with ID ${originalReceiptItemId} not found`);
144+
}
145+
146+
this.validateQuantity(originalShipmentItem.quantityShipped, newLines);
147+
148+
const containers = this.buildSplitContainers(
149+
receipt.data.containers,
150+
originalReceiptItemId,
151+
newLines
152+
);
153+
154+
const payload = this.buildPayload(receipt.data, containers);
155+
156+
await this.saveSplitLines(id, payload);
157+
} catch (error) {
158+
throw new Error('Problem splitting lines');
159+
}
160+
}
161+
162+
private findOriginalShipmentItem(containers: Container[], receiptItemId: string): ShipmentItem | undefined {
163+
return _.flatten(containers.map(c => c.shipmentItems))
164+
.find(item => item.receiptItemId === receiptItemId);
165+
}
166+
167+
private validateQuantity(originalQuantityShipped: number | undefined, newLines: ReceivingItemPayload[]) {
168+
const sumOfQuantityShipped = _.sumBy(newLines, 'quantityShipped');
169+
const originalQty = originalQuantityShipped || 0;
170+
171+
if (originalQty < sumOfQuantityShipped) {
172+
throw new Error('Sum of quantity shipped is greater than the original quantity shipped');
173+
}
174+
}
175+
176+
private buildSplitContainers(
177+
containers: Container[],
178+
originalReceiptItemId: string,
179+
newLines: ReceivingItemPayload[]
180+
): UnflattenContainer[] {
181+
const splittedItem = newLines.find(line => line.receiptItemId === originalReceiptItemId);
182+
const linesToSave = newLines.filter(line => !line.receiptItemId);
183+
184+
return containers.map(container => {
185+
const updatedShipmentItems = _.flatten(
186+
container.shipmentItems.map((shipmentItem: ShipmentItem) => {
187+
if (shipmentItem.receiptItemId === originalReceiptItemId) {
188+
return [
189+
{ ...shipmentItem, ...splittedItem },
190+
...linesToSave
191+
];
192+
}
193+
return shipmentItem;
194+
})
195+
);
196+
197+
return unflatten({ ...container, shipmentItems: updatedShipmentItems }) as UnflattenContainer;
198+
});
199+
}
200+
201+
private buildPayload(receiptData: ReceiptResponse, containers: UnflattenContainer[]): ReceiptPayload {
202+
return {
203+
...receiptData,
204+
containers,
205+
recipient: receiptData.recipient.id
206+
};
207+
}
208+
209+
private async saveSplitLines(id: string, payload: ReceiptPayload) {
210+
await this.request.post(`./api/partialReceiving/${id}`, {
211+
data: payload
212+
});
213+
}
214+
215+
216+
private async changeReceiptStatus(
217+
id: string,
218+
status: PartialReceiptStatus
219+
): Promise<void> {
220+
await this.request.post(`./api/partialReceiving/${id}`, {
221+
data: {
222+
id,
223+
stepNumber: 2,
224+
receiptStatus: status,
225+
},
226+
});
227+
}
228+
229+
private extractShipmentItemIds(items: ReceivingItemPayload[]): string[] {
230+
return items.map((item) => item.shipmentItemId);
231+
}
232+
233+
private buildUpdatedContainers(
234+
containers: Container[],
235+
items: ReceivingItemPayload[],
236+
shipmentItemsToUpdate: string[],
237+
): UnflattenContainer[] {
238+
return containers.map((container) => {
239+
return unflatten({
240+
...container,
241+
shipmentItems: container.shipmentItems.map((shipmentItem: ShipmentItem) => {
242+
if (shipmentItemsToUpdate.includes(shipmentItem.shipmentItemId)) {
243+
const updatedItem = this.findItemToUpdate(items, shipmentItem.shipmentItemId);
244+
return this.mergeShipmentItem(shipmentItem, updatedItem);
245+
}
246+
247+
return shipmentItem;
248+
}),
249+
}) as UnflattenContainer;
250+
});
251+
}
252+
253+
private findItemToUpdate(
254+
items: ReceivingItemPayload[],
255+
shipmentItemId: string,
256+
): ReceivingItemPayload | undefined {
257+
return items.find((item) => item.shipmentItemId === shipmentItemId);
258+
}
259+
260+
private mergeShipmentItem(
261+
original: ShipmentItem,
262+
update?: ReceivingItemPayload,
263+
): ShipmentItem {
264+
return {
265+
...original,
266+
binLocationId: update?.binLocationId ?? original.binLocationId,
267+
quantityReceiving: update?.quantityReceiving ?? original.quantityReceiving,
268+
comment: update?.comment ?? original.comment,
269+
};
270+
}
271+
272+
}
273+
274+
export default ReceivingService;
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export enum PartialReceiptStatus {
2+
PENDING = 'PENDING',
3+
CHECKING = 'CHECKING',
4+
COMPLETED = 'COMPLETED',
5+
ROLLBACK = 'ROLLBACK',
6+
}

src/fixtures/fixtures.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { BrowserContext, test as baseTest } from '@playwright/test';
33
import AuthService from '@/api/AuthService';
44
import GenericService from '@/api/GenericService';
55
import LocationService from '@/api/LocationService';
6+
import ReceivingService from '@/api/ReceivingService';
67
import StockMovementService from '@/api/StockMovementService';
78
import ImpersonateBanner from '@/components/ImpersonateBanner';
89
import LocationChooser from '@/components/LocationChooser';
@@ -71,6 +72,7 @@ type Fixtures = {
7172
locationService: LocationService;
7273
authService: AuthService;
7374
stockMovementService: StockMovementService;
75+
receivingService: ReceivingService;
7476
// LOCATIONS DATA
7577
mainLocationService: LocationData;
7678
noManageInventoryDepotService: LocationData;
@@ -139,6 +141,8 @@ export const test = baseTest.extend<Fixtures>({
139141
authService: async ({ page }, use) => use(new AuthService(page.request)),
140142
stockMovementService: async ({ page }, use) =>
141143
use(new StockMovementService(page.request)),
144+
receivingService: async ({ page }, use) =>
145+
use(new ReceivingService(page.request)),
142146
// LOCATIONS
143147
mainLocationService: async ({ page }, use) =>
144148
use(new LocationData(LOCATION_KEY.MAIN, page.request)),

0 commit comments

Comments
 (0)