@things-factory/operato-hub 4.3.744 → 4.3.745

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (25) hide show
  1. package/dist-server/routers/api/restful-apis/v1/company/add-contact-points.js +71 -2
  2. package/dist-server/routers/api/restful-apis/v1/company/add-contact-points.js.map +1 -1
  3. package/dist-server/routers/api/restful-apis/v1/company/index.js +1 -0
  4. package/dist-server/routers/api/restful-apis/v1/company/index.js.map +1 -1
  5. package/dist-server/routers/api/restful-apis/v1/company/update-contact-points.js +243 -0
  6. package/dist-server/routers/api/restful-apis/v1/company/update-contact-points.js.map +1 -0
  7. package/dist-server/routers/api/restful-apis/v1/utils/params.js +109 -28
  8. package/dist-server/routers/api/restful-apis/v1/utils/params.js.map +1 -1
  9. package/dist-server/routers/api/restful-apis/v1/warehouse/index.js +1 -0
  10. package/dist-server/routers/api/restful-apis/v1/warehouse/index.js.map +1 -1
  11. package/dist-server/routers/api/restful-apis/v1/warehouse/update-arrival-notice.js +365 -0
  12. package/dist-server/routers/api/restful-apis/v1/warehouse/update-arrival-notice.js.map +1 -0
  13. package/dist-server/routers/api/restful-apis/v1/warehouse/update-release-order-details.js +946 -19
  14. package/dist-server/routers/api/restful-apis/v1/warehouse/update-release-order-details.js.map +1 -1
  15. package/openapi/v1/contact-point.yaml +266 -0
  16. package/openapi/v1/inbound.yaml +349 -0
  17. package/openapi/v1/outbound.yaml +256 -150
  18. package/package.json +18 -18
  19. package/server/routers/api/restful-apis/v1/company/add-contact-points.ts +91 -2
  20. package/server/routers/api/restful-apis/v1/company/index.ts +1 -0
  21. package/server/routers/api/restful-apis/v1/company/update-contact-points.ts +267 -0
  22. package/server/routers/api/restful-apis/v1/utils/params.ts +109 -28
  23. package/server/routers/api/restful-apis/v1/warehouse/index.ts +1 -0
  24. package/server/routers/api/restful-apis/v1/warehouse/update-arrival-notice.ts +429 -0
  25. package/server/routers/api/restful-apis/v1/warehouse/update-release-order-details.ts +1122 -29
@@ -0,0 +1,429 @@
1
+ import { EntityManager, getConnection, SelectQueryBuilder, In } from 'typeorm'
2
+ import { v4 as uuidv4 } from 'uuid'
3
+
4
+ import { restfulApiRouter as router } from '@things-factory/api'
5
+ import { Bizplace, ContactPoint, getCompanyBizplace } from '@things-factory/biz-base'
6
+ import { ProductDetail } from '@things-factory/product-base'
7
+ import {
8
+ ArrivalNotice,
9
+ ORDER_PRODUCT_STATUS,
10
+ ORDER_STATUS,
11
+ ORDER_TYPES,
12
+ OrderProduct
13
+ } from '@things-factory/sales-base'
14
+ import { WorksheetDetail } from '@things-factory/worksheet-base'
15
+ import { businessMiddleware, loggingMiddleware, validationMiddleware } from '../middlewares'
16
+ import { ApiError, ApiErrorHandler, throwInternalServerError } from '../utils/error-util'
17
+
18
+ // Allowed statuses for update
19
+ const ALLOWED_STATUSES = [
20
+ ORDER_STATUS.PENDING,
21
+ ORDER_STATUS.PENDING_RECEIVE,
22
+ ORDER_STATUS.INTRANSIT,
23
+ ORDER_STATUS.ARRIVED,
24
+ ORDER_STATUS.READY_TO_UNLOAD
25
+ ]
26
+
27
+ router.post(
28
+ '/v1/warehouse/update-arrival-notice',
29
+ businessMiddleware,
30
+ validationMiddleware,
31
+ loggingMiddleware,
32
+ async (context, next) => {
33
+ try {
34
+ const { domain } = context.state
35
+ const bodyReq = context.request.body.data
36
+
37
+ await getConnection().transaction(async (tx: EntityManager) => {
38
+ // Validate arrival notice exists and get it
39
+ if (!bodyReq.arrivalNoticeId && !bodyReq.ganNo) {
40
+ throw new ApiError('E01', 'arrivalNoticeId or ganNo is required')
41
+ }
42
+
43
+ // Build where clause: arrivalNoticeId maps to id, ganNo maps to name
44
+ const whereClause: any = { domain }
45
+ if (bodyReq.arrivalNoticeId) {
46
+ whereClause.id = bodyReq.arrivalNoticeId
47
+ }
48
+ if (bodyReq.ganNo) {
49
+ whereClause.name = bodyReq.ganNo
50
+ }
51
+
52
+ const foundArrivalNotice: ArrivalNotice = await tx.getRepository(ArrivalNotice).findOne({
53
+ where: whereClause,
54
+ relations: ['bizplace', 'orderProducts', 'orderProducts.product', 'orderProducts.productDetail']
55
+ })
56
+
57
+ if (!foundArrivalNotice) {
58
+ const searchCriteria = []
59
+ if (bodyReq.arrivalNoticeId) searchCriteria.push(`id: ${bodyReq.arrivalNoticeId}`)
60
+ if (bodyReq.ganNo) searchCriteria.push(`ganNo: ${bodyReq.ganNo}`)
61
+ throw new ApiError('E04', `Arrival notice not found: ${searchCriteria.join(', ')}`)
62
+ }
63
+
64
+ // Validate status
65
+ if (!ALLOWED_STATUSES.includes(foundArrivalNotice.status)) {
66
+ throw new ApiError(
67
+ 'E04',
68
+ `Arrival notice status "${
69
+ foundArrivalNotice.status
70
+ }" does not allow updates. Allowed statuses: ${ALLOWED_STATUSES.join(', ')}`
71
+ )
72
+ }
73
+
74
+ // Update arrival notice fields if provided
75
+ const updateData: any = {
76
+ updater: context.state.user
77
+ }
78
+
79
+ if (bodyReq.hasOwnProperty('refNo')) updateData.refNo = bodyReq.refNo
80
+ if (bodyReq.hasOwnProperty('refNo2')) updateData.refNo2 = bodyReq.refNo2
81
+ if (bodyReq.hasOwnProperty('refNo3')) updateData.refNo3 = bodyReq.refNo3
82
+ if (bodyReq.hasOwnProperty('etaDate')) updateData.etaDate = bodyReq.etaDate
83
+ if (bodyReq.hasOwnProperty('ownTransport')) updateData.ownTransport = bodyReq.ownTransport
84
+ if (bodyReq.hasOwnProperty('importCargo')) updateData.importCargo = bodyReq.importCargo
85
+ if (bodyReq.hasOwnProperty('container')) updateData.container = bodyReq.container
86
+ if (bodyReq.hasOwnProperty('containerNo')) updateData.containerNo = bodyReq.containerNo
87
+ if (bodyReq.hasOwnProperty('containerSize')) updateData.containerSize = bodyReq.containerSize
88
+ if (bodyReq.hasOwnProperty('truckNo')) updateData.truckNo = bodyReq.truckNo
89
+ if (bodyReq.hasOwnProperty('deliveryOrderNo')) updateData.deliveryOrderNo = bodyReq.deliveryOrderNo
90
+ if (bodyReq.hasOwnProperty('remark')) updateData.remark = bodyReq.remark
91
+ if (bodyReq.hasOwnProperty('description')) updateData.description = bodyReq.description
92
+
93
+ // Update supplier if provided
94
+ if (bodyReq.hasOwnProperty('supplierId')) {
95
+ if (bodyReq.supplierId) {
96
+ const foundSupplier: ContactPoint = await tx.getRepository(ContactPoint).findOne({
97
+ where: { id: bodyReq.supplierId }
98
+ })
99
+ if (!foundSupplier) {
100
+ throw new ApiError('E04', `Supplier not found: ${bodyReq.supplierId}`)
101
+ }
102
+ updateData.supplier = foundSupplier
103
+ } else {
104
+ updateData.supplier = null
105
+ }
106
+ }
107
+
108
+ // Handle order products: add, update, remove
109
+ if (bodyReq.orderProducts && Array.isArray(bodyReq.orderProducts)) {
110
+ const existingOrderProducts = foundArrivalNotice.orderProducts || []
111
+ const orderProductsToAdd = []
112
+ const orderProductsToUpdate = []
113
+ const orderProductIdsToRemove = []
114
+
115
+ // Helper function to find matching order product by id, sku, refCode, batchId, packingType, or refItemId
116
+ const findMatchingOrderProduct = (orderProductReq: any): OrderProduct | null => {
117
+ // Match by ID if provided
118
+ if (orderProductReq.id) {
119
+ const found = existingOrderProducts.find(op => op.id === orderProductReq.id)
120
+ if (found) return found
121
+ }
122
+
123
+ // Extract matching fields
124
+ const sku = orderProductReq.product?.sku || orderProductReq.sku
125
+ const refCode =
126
+ orderProductReq.product?.refCode || orderProductReq.productDetail?.refCode || orderProductReq.refCode
127
+ const batchId = orderProductReq.batchId
128
+ const packingType = orderProductReq.packingType
129
+ const refItemId = orderProductReq.refItemId
130
+
131
+ // Need at least one matching field (besides id)
132
+ if (!sku && !refCode && !batchId && !packingType && !refItemId) {
133
+ return null
134
+ }
135
+
136
+ // Match by provided fields (all provided fields must match)
137
+ return (
138
+ existingOrderProducts.find(op => {
139
+ const matchesSku = !sku || op.product?.sku === sku
140
+ const matchesRefCode = !refCode || (op.productDetail as any)?.refCode === refCode
141
+ const matchesBatchId = !batchId || op.batchId === batchId
142
+ const matchesPackingType = !packingType || op.packingType === packingType
143
+ const matchesRefItemId = !refItemId || op.refItemId === refItemId
144
+
145
+ return matchesSku && matchesRefCode && matchesBatchId && matchesPackingType && matchesRefItemId
146
+ }) || null
147
+ )
148
+ }
149
+
150
+ // Process each order product in the request
151
+ for (const orderProductReq of bodyReq.orderProducts) {
152
+ // Handle removal request
153
+ if (orderProductReq._action === 'remove') {
154
+ const matchingOP = findMatchingOrderProduct(orderProductReq)
155
+ if (!matchingOP) {
156
+ throw new ApiError(
157
+ 'E04',
158
+ `Order product not found for removal. Provide id, sku, refCode, batchId, packingType, or refItemId to identify the item.`
159
+ )
160
+ }
161
+ orderProductIdsToRemove.push(matchingOP.id)
162
+ continue
163
+ }
164
+
165
+ // Try to find existing order product for update
166
+ const matchingOP = findMatchingOrderProduct(orderProductReq)
167
+
168
+ if (matchingOP) {
169
+ // Update existing order product
170
+ // Validate and prepare update data
171
+ const opUpdateData: any = {
172
+ updater: context.state.user
173
+ }
174
+
175
+ // Update quantity if provided (accepts 'qty' or 'packQty')
176
+ if (orderProductReq.hasOwnProperty('qty') || orderProductReq.hasOwnProperty('packQty')) {
177
+ const qty = orderProductReq.qty !== undefined ? orderProductReq.qty : orderProductReq.packQty
178
+ if (qty <= 0) {
179
+ throw new ApiError('E01', 'Quantity must be greater than 0')
180
+ }
181
+
182
+ // Round quantity based on product's decimal support
183
+ if (!matchingOP.product.isInventoryDecimal) {
184
+ opUpdateData.packQty = Math.round(qty)
185
+ } else {
186
+ opUpdateData.packQty = parseFloat(qty.toFixed(3))
187
+ }
188
+
189
+ // Recalculate total UOM value
190
+ if (matchingOP.productDetail) {
191
+ opUpdateData.totalUomValue = `${(
192
+ opUpdateData.packQty * (matchingOP.productDetail.uomValue || 1)
193
+ ).toFixed(2)} ${matchingOP.productDetail.uom}`
194
+ }
195
+ }
196
+
197
+ // Update other fields if provided
198
+ if (orderProductReq.hasOwnProperty('batchId')) opUpdateData.batchId = orderProductReq.batchId
199
+ if (orderProductReq.hasOwnProperty('unitPrice'))
200
+ opUpdateData.unitPrice = orderProductReq.unitPrice ? parseFloat(orderProductReq.unitPrice) : null
201
+ if (orderProductReq.hasOwnProperty('manufactureDate'))
202
+ opUpdateData.manufactureDate = orderProductReq.manufactureDate || null
203
+ if (orderProductReq.hasOwnProperty('remark')) opUpdateData.remark = orderProductReq.remark || null
204
+ if (orderProductReq.hasOwnProperty('refItemId')) opUpdateData.refItemId = orderProductReq.refItemId
205
+
206
+ orderProductsToUpdate.push({
207
+ id: matchingOP.id,
208
+ ...opUpdateData
209
+ })
210
+ } else {
211
+ // No matching order product found - add as new
212
+ orderProductsToAdd.push(orderProductReq)
213
+ }
214
+ }
215
+
216
+ // Remove order products
217
+ // First, delete any worksheet details that reference these order products
218
+ if (orderProductIdsToRemove.length > 0) {
219
+ // Find and delete worksheet details that reference the order products to be deleted
220
+ const worksheetDetailsToDelete: WorksheetDetail[] = await tx.getRepository(WorksheetDetail).find({
221
+ where: {
222
+ targetProduct: In(orderProductIdsToRemove),
223
+ domain
224
+ }
225
+ })
226
+
227
+ if (worksheetDetailsToDelete.length > 0) {
228
+ const worksheetDetailIds = worksheetDetailsToDelete.map(wsd => wsd.id)
229
+ await tx.getRepository(WorksheetDetail).delete({
230
+ id: In(worksheetDetailIds)
231
+ })
232
+ }
233
+
234
+ // Now delete the order products
235
+ await tx.getRepository(OrderProduct).delete({
236
+ id: In(orderProductIdsToRemove),
237
+ arrivalNotice: foundArrivalNotice
238
+ })
239
+ }
240
+
241
+ // Update existing order products
242
+ for (const opUpdate of orderProductsToUpdate) {
243
+ await tx.getRepository(OrderProduct).update(opUpdate.id, opUpdate)
244
+ }
245
+
246
+ // Add new order products
247
+ if (orderProductsToAdd.length > 0) {
248
+ const massagedData = await massageOrderItems(foundArrivalNotice, orderProductsToAdd, context, tx)
249
+ for (const newOrderProduct of massagedData.orderProducts) {
250
+ const orderProduct = Object.assign(new OrderProduct(), {
251
+ ...newOrderProduct,
252
+ name: uuidv4(),
253
+ domain: domain,
254
+ bizplace: foundArrivalNotice.bizplace,
255
+ arrivalNotice: foundArrivalNotice,
256
+ type: ORDER_TYPES.ARRIVAL_NOTICE,
257
+ status:
258
+ existingOrderProducts.length > 0
259
+ ? existingOrderProducts[0].status
260
+ : ORDER_PRODUCT_STATUS.PENDING_RECEIVE,
261
+ creator: context.state.user,
262
+ updater: context.state.user
263
+ })
264
+ await tx.getRepository(OrderProduct).save(orderProduct)
265
+ }
266
+ }
267
+ }
268
+
269
+ // Save arrival notice updates
270
+ await tx.getRepository(ArrivalNotice).update(foundArrivalNotice.id, updateData)
271
+
272
+ // Fetch updated arrival notice with relations
273
+ const updatedArrivalNotice: ArrivalNotice = await tx.getRepository(ArrivalNotice).findOne({
274
+ where: { id: foundArrivalNotice.id },
275
+ relations: ['bizplace', 'orderProducts', 'orderProducts.product', 'orderProducts.productDetail', 'supplier']
276
+ })
277
+
278
+ // Format response
279
+ const resultOrderProducts = (updatedArrivalNotice.orderProducts || []).map(op => ({
280
+ id: op.id,
281
+ product: { name: op.product.name, sku: op.product.sku },
282
+ batchId: op.batchId,
283
+ packingType: op.packingType,
284
+ packingSize: op.packingSize,
285
+ qty: op.packQty,
286
+ palletQty: op.palletQty,
287
+ uom: op.uom,
288
+ uomValue: op.uomValue,
289
+ refItemId: op?.refItemId,
290
+ unitPrice: op.unitPrice,
291
+ manufactureDate: op.manufactureDate,
292
+ remark: op.remark
293
+ }))
294
+
295
+ const data = {
296
+ id: updatedArrivalNotice.id,
297
+ ganNo: updatedArrivalNotice.name,
298
+ refNo: updatedArrivalNotice.refNo,
299
+ refNo2: updatedArrivalNotice.refNo2,
300
+ refNo3: updatedArrivalNotice.refNo3,
301
+ etaDate: updatedArrivalNotice.etaDate,
302
+ ownTransport: updatedArrivalNotice.ownTransport,
303
+ importCargo: updatedArrivalNotice.importCargo,
304
+ container: updatedArrivalNotice.container,
305
+ containerNo: updatedArrivalNotice.containerNo,
306
+ containerSize: updatedArrivalNotice.containerSize,
307
+ truckNo: updatedArrivalNotice.truckNo,
308
+ deliveryOrderNo: updatedArrivalNotice.deliveryOrderNo,
309
+ remark: updatedArrivalNotice.remark,
310
+ description: updatedArrivalNotice.description,
311
+ status: updatedArrivalNotice.status,
312
+ orderProducts: resultOrderProducts,
313
+ supplier: updatedArrivalNotice?.supplier
314
+ ? {
315
+ id: updatedArrivalNotice.supplier.id,
316
+ name: updatedArrivalNotice.supplier.name,
317
+ email: updatedArrivalNotice.supplier.email,
318
+ phone: updatedArrivalNotice.supplier.phone
319
+ }
320
+ : null
321
+ }
322
+
323
+ context.body = {
324
+ responseCode: '200',
325
+ message: 'success',
326
+ data
327
+ }
328
+ })
329
+ } catch (e) {
330
+ if (e instanceof ApiError) ApiErrorHandler(context, e)
331
+ else throwInternalServerError(context, e)
332
+ }
333
+ }
334
+ )
335
+
336
+ async function massageOrderItems(
337
+ arrivalNotice: ArrivalNotice,
338
+ inputOrderProducts: any[],
339
+ context: any,
340
+ tx: EntityManager
341
+ ): Promise<{ orderProducts: OrderProduct[] }> {
342
+ let orderProducts: OrderProduct[] = []
343
+
344
+ // Get company bizplace for product lookup
345
+ const customerCompanyBizplace: Bizplace = await getCompanyBizplace(null, null, arrivalNotice.bizplaceId, tx)
346
+
347
+ await Promise.all(
348
+ inputOrderProducts.map(async inputOrderItem => {
349
+ const sku: string = inputOrderItem.product?.sku || inputOrderItem.sku
350
+ const refCode: string = inputOrderItem.product?.refCode || inputOrderItem.refCode
351
+ const packingType: string = inputOrderItem.packingType
352
+ const packingSize: string = inputOrderItem.packingSize
353
+ const packQty = inputOrderItem.qty !== undefined ? inputOrderItem.qty : inputOrderItem.packQty
354
+
355
+ if (!sku && !refCode) {
356
+ throw new ApiError('E01', 'sku or refCode is required')
357
+ }
358
+
359
+ if (!packQty || packQty <= 0) {
360
+ throw new ApiError('E01', 'Quantity must be greater than 0')
361
+ }
362
+
363
+ const qb: SelectQueryBuilder<ProductDetail> = tx
364
+ .getRepository(ProductDetail)
365
+ .createQueryBuilder('pd')
366
+ .innerJoinAndSelect('pd.product', 'prod')
367
+ .where('prod.bizplace_id = :bizplaceId', { bizplaceId: customerCompanyBizplace.id })
368
+
369
+ if (refCode) {
370
+ qb.andWhere('pd.ref_code = :refCode', { refCode })
371
+ }
372
+
373
+ if (sku) {
374
+ qb.andWhere('prod.sku = :sku', { sku })
375
+ }
376
+
377
+ if (packingType) {
378
+ qb.andWhere('pd.packing_type = :packingType', { packingType })
379
+ }
380
+
381
+ if (packingSize) {
382
+ qb.andWhere('pd.packing_size = :packingSize', { packingSize })
383
+ }
384
+
385
+ if (!refCode && !packingType && !packingSize) {
386
+ qb.andWhere('pd.is_default = :isDefault', { isDefault: true })
387
+ }
388
+
389
+ const productDetail: ProductDetail = await qb.getOne()
390
+
391
+ if (productDetail) {
392
+ // Validate packQty based on product's isInventoryDecimal
393
+ let validatedPackQty: number
394
+ if (!productDetail.product.isInventoryDecimal) {
395
+ validatedPackQty = Math.round(packQty)
396
+ } else {
397
+ validatedPackQty = parseFloat(packQty.toFixed(3))
398
+ }
399
+
400
+ let newOrderProduct: OrderProduct = {
401
+ product: productDetail.product,
402
+ productDetail: productDetail,
403
+ packingType: productDetail.packingType,
404
+ packingSize: productDetail.packingSize,
405
+ uom: productDetail.uom,
406
+ uomValue: productDetail.uomValue,
407
+ packQty: validatedPackQty,
408
+ totalUomValue: `${(validatedPackQty * (productDetail.uomValue || 1)).toFixed(2)} ${productDetail.uom}`,
409
+ batchId: inputOrderItem.batchId || null,
410
+ status: ORDER_PRODUCT_STATUS.PENDING_RECEIVE,
411
+ unitPrice: inputOrderItem?.unitPrice ? parseFloat(inputOrderItem.unitPrice) : null,
412
+ manufactureDate: inputOrderItem?.manufactureDate || null,
413
+ remark: inputOrderItem?.remark || null,
414
+ refItemId: inputOrderItem?.refItemId || null
415
+ }
416
+
417
+ orderProducts.push(newOrderProduct)
418
+ } else {
419
+ // Product not found - provide clear error message
420
+ const identifier = sku ? `sku: ${sku}` : `refCode: ${refCode}`
421
+ const packingInfo = packingType ? `, packingType: ${packingType}` : ''
422
+ const packingSizeInfo = packingSize ? `, packingSize: ${packingSize}` : ''
423
+ throw new ApiError('E04', `Product not found: ${identifier}${packingInfo}${packingSizeInfo}`)
424
+ }
425
+ })
426
+ )
427
+
428
+ return { orderProducts }
429
+ }