@things-factory/operato-hub 4.3.130 → 4.3.131

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.
@@ -1,28 +1,41 @@
1
1
  import Router from 'koa-router'
2
- import { getRepository } from 'typeorm'
2
+ import { EntityManager, getConnection, getRepository, In, Not, SelectQueryBuilder } from 'typeorm'
3
3
  import { v4 as uuidv4 } from 'uuid'
4
4
 
5
5
  import { Bizplace, getCompanyBizplace } from '@things-factory/biz-base'
6
6
  import { logger } from '@things-factory/env'
7
7
  import { generateId } from '@things-factory/id-rule-base'
8
8
  import {
9
- Account,
10
9
  ACCOUNT_PLATFORM,
11
10
  ACCOUNT_SYNC_STATUS,
12
11
  AccountingAPI,
13
12
  AccountWebhookOrder,
14
13
  Xilnex
15
14
  } from '@things-factory/integration-accounting'
16
- import { ProductDetail } from '@things-factory/product-base'
15
+ // import { createPayloadLog, PayloadType } from '@things-factory/integration-base'
16
+ import { Product, ProductBundle, ProductDetail } from '@things-factory/product-base'
17
17
  import {
18
18
  ArrivalNotice,
19
+ DRAFT_RELEASE_ORDER_STATUS,
20
+ DraftReleaseGood,
21
+ ORDER_INVENTORY_STATUS,
19
22
  ORDER_NUMBER_RULE_TYPE,
20
23
  ORDER_NUMBER_SETTING_KEY,
24
+ ORDER_PRODUCT_STATUS,
25
+ ORDER_STATUS,
26
+ ORDER_TYPES,
27
+ OrderInventory,
21
28
  OrderNoGenerator,
22
- OrderProduct
29
+ OrderProduct,
30
+ PURCHASE_ORDER_STATUS,
31
+ PurchaseOrder,
32
+ ReleaseGood
23
33
  } from '@things-factory/sales-base'
24
34
  import { Setting } from '@things-factory/setting-base'
25
35
  import { Domain } from '@things-factory/shell'
36
+ import { Inventory, INVENTORY_STATUS, LOCATION_TYPE } from '@things-factory/warehouse-base'
37
+
38
+ import { ValidationError } from '../errors/index'
26
39
 
27
40
  const debug = require('debug')('things-factory:integration-accounting:xilnex-router')
28
41
 
@@ -30,127 +43,1329 @@ export const xilnexRouter = new Router()
30
43
 
31
44
  xilnexRouter.post('/webhook-xilnex-po', async (context, next) => {
32
45
  try {
33
- const { id, warehouseId } = context.query
34
- const { body, headers } = context.request
35
- const data = body?.Data ? JSON.parse(body.Data) : {}
46
+ await getConnection().transaction(async (tx: EntityManager) => {
47
+ const { id, warehouseId } = context.query
48
+ const { body, headers } = context.request
49
+ const data = body?.Data ? JSON.parse(body.Data) : {}
36
50
 
37
- const account = await AccountingAPI.getAccounting(id)
38
- if (!account) {
39
- throw new Error(`${id} not found in Account`)
40
- }
51
+ const account = await AccountingAPI.getAccounting(id)
52
+ if (!account) {
53
+ throw new Error(`${id} not found in Account`)
54
+ }
41
55
 
42
- const { webhookKey } = account
43
- const signature = headers['xilnex-webhook-signature']
56
+ const { webhookKey } = account
57
+ const signature = headers['xilnex-webhook-signature']
44
58
 
45
- const xilnex = new Xilnex({})
46
- if (!xilnex.isValidSignature(body, webhookKey, signature)) {
47
- context.throw(400, 'Signature validation failed')
48
- }
59
+ const xilnex = new Xilnex({})
60
+ if (!xilnex.isValidSignature(body, webhookKey, signature)) {
61
+ context.throw(400, 'Signature validation failed')
62
+ }
49
63
 
50
- const customerDomain: Domain = account.domain
51
- const customerBizplace: Bizplace = await getRepository(Bizplace).findOne({
52
- where: { domain: customerDomain }
53
- })
54
- const custCompanyBizplace: Bizplace = await getCompanyBizplace(null, null, customerBizplace.id)
64
+ const customerDomain: Domain = account.domain
65
+ const customerBizplace: Bizplace = await tx.getRepository(Bizplace).findOne({
66
+ where: { domain: customerDomain }
67
+ })
68
+ const custCompanyBizplace: Bizplace = await getCompanyBizplace(null, null, customerBizplace.id, tx)
55
69
 
56
- const warehouseDomain: Domain = await getRepository(Domain).findOne({
57
- where: { id: warehouseId, extType: 'warehouse' }
58
- })
70
+ const warehouseDomain: Domain = await tx.getRepository(Domain).findOne({
71
+ where: { id: warehouseId, extType: 'warehouse' }
72
+ })
59
73
 
60
- if (!warehouseDomain) {
61
- context.throw(400, `${warehouseId} not found in warehouse domain`)
62
- }
74
+ if (!warehouseDomain) {
75
+ context.throw(400, `${warehouseId} not found in warehouse domain`)
76
+ }
63
77
 
64
- let accountWebhookOrder: any = {
65
- name: uuidv4(),
66
- request: body,
67
- platform: ACCOUNT_PLATFORM.XILNEX,
68
- domain: account.domain,
69
- status: ACCOUNT_SYNC_STATUS.FAILED
70
- }
78
+ let accountWebhookOrder: any = {
79
+ name: uuidv4(),
80
+ request: body,
81
+ platform: ACCOUNT_PLATFORM.XILNEX,
82
+ domain: account.domain,
83
+ status: ACCOUNT_SYNC_STATUS.FAILED
84
+ }
85
+
86
+ if (data?.id) {
87
+ const purchaseOrder: any = await AccountingAPI.getPurchaseOrder(account, { orderId: data.id })
88
+ const { orderInfo, orderItems } = purchaseOrder
71
89
 
72
- if (data?.id) {
73
- const purchaseOrder: any = await AccountingAPI.getPurchaseOrder(account, { orderId: data.id })
74
- const { orderInfo, orderItems } = purchaseOrder
90
+ let accountInfo: any = account?.accountInfo ? JSON.parse(account.accountInfo) : {}
91
+
92
+ if (accountInfo.outlet == orderInfo.toOutlet) {
93
+ const poNoSetting: Setting = await tx.getRepository(Setting).findOne({
94
+ where: {
95
+ domain: warehouseDomain,
96
+ name: ORDER_NUMBER_SETTING_KEY.PO_NUMBER_RULE
97
+ }
98
+ })
75
99
 
76
- const ganNoSetting: Setting = await getRepository(Setting).findOne({
77
- where: {
78
- domain: warehouseDomain,
79
- name: ORDER_NUMBER_SETTING_KEY.GAN_NUMBER_RULE
100
+ let orderNo: string = poNoSetting
101
+ ? await generateId({ domain: warehouseDomain, type: ORDER_NUMBER_RULE_TYPE.PO_NUMBER, seed: {} })
102
+ : OrderNoGenerator.purchaseOrder()
103
+
104
+ let purchaseOrder: any = {
105
+ name: orderNo,
106
+ refNo: orderInfo.orderId,
107
+ refNo2: null,
108
+ refNo3: null,
109
+ etaDate: orderInfo.etaDate,
110
+ ownTransport: orderInfo.ownTransport,
111
+ importCargo: orderInfo.importCargo,
112
+ container: orderInfo.container,
113
+ remark: orderInfo.remark,
114
+ currency: orderInfo?.currency || 'MYR',
115
+ source: 'XILNEX',
116
+ orderSourceType: 'XILNEX_PO',
117
+ domain: warehouseDomain,
118
+ bizplace: customerBizplace,
119
+ status: PURCHASE_ORDER_STATUS.DRAFT
120
+ }
121
+ purchaseOrder = await tx.getRepository(PurchaseOrder).save(purchaseOrder)
122
+
123
+ let orderProducts: any[] = []
124
+ await Promise.all(
125
+ orderItems.map(async item => {
126
+ const refCode: string = item.product.refCode
127
+ let productDetail: ProductDetail = await tx.getRepository(ProductDetail).findOne({
128
+ where: { domain: custCompanyBizplace.domain, refCode: refCode },
129
+ relations: ['product']
130
+ })
131
+
132
+ if (!productDetail) {
133
+ const sku: string = item.product.sku
134
+ const product: Product = await tx.getRepository(Product).findOne({
135
+ where: { domain: custCompanyBizplace.domain, sku }
136
+ })
137
+
138
+ if (product) {
139
+ productDetail = await tx.getRepository(ProductDetail).findOne({
140
+ where: { domain: custCompanyBizplace.domain, product },
141
+ relations: ['product']
142
+ })
143
+ } else {
144
+ context.throw(404, `error.sku ${sku} and refCode ${refCode} not exist in product master`)
145
+ }
146
+ }
147
+
148
+ let orderProduct: any = {
149
+ name: OrderNoGenerator.orderProduct(),
150
+ product: productDetail?.product,
151
+ productDetail,
152
+ batchId: item?.batchId || '-',
153
+ packingType: productDetail?.packingType,
154
+ packingSize: productDetail?.packingSize,
155
+ uom: productDetail?.uom,
156
+ uomValue: productDetail?.uomValue,
157
+ packQty: item.confirmedQty,
158
+ totalUomValue: `${(productDetail.uomValue * item.confirmedQty).toFixed(2)} ${productDetail.uom}`,
159
+ status: PURCHASE_ORDER_STATUS.DRAFT,
160
+ unitPrice: item.unitCost || 1,
161
+ taxRate: item.taxAmount,
162
+ refItemId: item.refItemId,
163
+ purchaseOrder,
164
+ domain: warehouseDomain,
165
+ bizplace: customerBizplace
166
+ }
167
+
168
+ orderProducts.push(orderProduct)
169
+ })
170
+ )
171
+ await tx.getRepository(OrderProduct).save(orderProducts)
80
172
  }
173
+
174
+ accountWebhookOrder.status = ACCOUNT_SYNC_STATUS.SUCCESS
175
+ await tx.getRepository(AccountWebhookOrder).save(accountWebhookOrder)
176
+ }
177
+ })
178
+
179
+ context.status = 200
180
+ } catch (err) {
181
+ logger.error(`xilnex-router[webhook-xilnex-po]: ${err}`)
182
+ }
183
+ })
184
+
185
+ xilnexRouter.post('/webhook-xilnex-transfer-note', async (context, next) => {
186
+ try {
187
+ await getConnection().transaction(async (tx: EntityManager) => {
188
+ const { id, warehouseId } = context.query
189
+ const { body, headers } = context.request
190
+ const data = body?.Data ? JSON.parse(body.Data) : {}
191
+
192
+ const account = await AccountingAPI.getAccounting(id)
193
+ if (!account) {
194
+ throw new Error(`${id} not found in Account`)
195
+ }
196
+
197
+ const { webhookKey } = account
198
+ const signature = headers['xilnex-webhook-signature']
199
+
200
+ const xilnex = new Xilnex({})
201
+ if (!xilnex.isValidSignature(body, webhookKey, signature)) {
202
+ context.throw(400, 'Signature validation failed')
203
+ }
204
+
205
+ const customerDomain: Domain = account.domain
206
+ const customerBizplace: Bizplace = await tx.getRepository(Bizplace).findOne({
207
+ where: { domain: customerDomain }
81
208
  })
209
+ const custCompanyBizplace: Bizplace = await getCompanyBizplace(null, null, customerBizplace.id, tx)
82
210
 
83
- let orderNo: string = ''
84
- if (ganNoSetting) {
85
- orderNo = await generateId({ domain: warehouseDomain, type: ORDER_NUMBER_RULE_TYPE.GAN_NUMBER, seed: {} })
86
- } else {
87
- orderNo = OrderNoGenerator.arrivalNotice()
211
+ const warehouseDomain: Domain = await tx.getRepository(Domain).findOne({
212
+ where: { id: warehouseId, extType: 'warehouse' }
213
+ })
214
+
215
+ if (!warehouseDomain) {
216
+ context.throw(400, `${warehouseId} not found in warehouse domain`)
88
217
  }
89
218
 
90
- let arrivalNotice: any = {
91
- ...orderInfo,
92
- name: orderNo,
93
- source: 'XILNEX',
94
- domain: warehouseDomain,
95
- bizplace: customerBizplace,
96
- status: 'INTRANSIT'
219
+ let accountWebhookOrder: any = {
220
+ name: uuidv4(),
221
+ request: body,
222
+ platform: ACCOUNT_PLATFORM.XILNEX,
223
+ domain: account.domain,
224
+ status: ACCOUNT_SYNC_STATUS.FAILED
97
225
  }
98
- arrivalNotice = await getRepository(ArrivalNotice).save(arrivalNotice)
99
226
 
100
- let orderProducts: any[] = []
101
- await Promise.all(
102
- orderItems.map(async item => {
103
- const productDetail: ProductDetail = await getRepository(ProductDetail).findOne({
104
- where: { domain: custCompanyBizplace.domain, refCode: item.product.sku },
105
- relations: ['product']
106
- })
227
+ let accountInfo: any = account?.accountInfo ? JSON.parse(account.accountInfo) : {}
107
228
 
108
- if (!productDetail) {
109
- context.throw(404, `no matching product found`)
110
- }
111
-
112
- let orderProduct: any = {
113
- name: OrderNoGenerator.orderProduct(),
114
- product: productDetail?.product,
115
- productDetail,
116
- batchId: item?.batchId || '-',
117
- packingType: productDetail?.packingType,
118
- packingSize: productDetail?.packingSize,
119
- uom: productDetail?.uom,
120
- uomValue: productDetail?.uomValue,
121
- packQty: item.packQty,
122
- totalUomValue: `${(productDetail.uomValue * item.packQty).toFixed(2)} ${productDetail.uom}`,
123
- status: 'PENDING',
124
- arrivalNotice,
125
- domain: warehouseDomain,
126
- bizplace: customerBizplace
229
+ if (data?.id) {
230
+ const transferNote: any = await AccountingAPI.getTransferNote(account, { orderId: data.id })
231
+ const { orderInfo, orderItems } = transferNote
232
+
233
+ if (orderInfo.fromLocation == accountInfo.outlet || orderInfo.toLocation == accountInfo.outlet) {
234
+ // VDC release
235
+ if (accountInfo.outlet == orderInfo.fromLocation) {
236
+ let releaseGood = {
237
+ domain: warehouseDomain,
238
+ bizplace: customerBizplace,
239
+ companyBizplace: custCompanyBizplace,
240
+ courierOption: false,
241
+ collectionOrderNo: orderInfo.orderId,
242
+ exportOption: false,
243
+ ownTransport: orderInfo.ownTransport,
244
+ packingOption: false,
245
+ refNo: orderInfo.orderId,
246
+ refNo2: orderInfo?.fromLocation,
247
+ refNo3: orderInfo?.toLocation,
248
+ releaseDate: orderInfo.issueDate,
249
+ type: 'b2b',
250
+ source: 'XILNEX',
251
+ orderSourceType: 'XILNEX_TRANSFER_NOTE',
252
+ marketplaceOrderStatus: orderInfo?.status,
253
+ remark: orderInfo?.remark || null
254
+ }
255
+
256
+ // check existing order
257
+ await checkExistingOrder(releaseGood, customerBizplace, true, context, tx)
258
+
259
+ //massage data
260
+ let massagedData = await massageOrderItems(releaseGood, orderItems, context)
261
+
262
+ // validation
263
+ let errorList = []
264
+ await Promise.all(
265
+ massagedData.combinedItems.map(async item => {
266
+ let itemList = await tx.query(
267
+ `
268
+ select wboi.* from warehouse_bizplace_onhand_inventories wboi
269
+ where wboi."domainId"= $1 and
270
+ wboi."bizplaceId" = $2 and
271
+ wboi."groupType" = 'SINGLE' and
272
+ wboi."productId" = $3 and
273
+ wboi."packingType" = $4 and
274
+ wboi."packingSize" = $5 and
275
+ wboi."uom" = $6 and
276
+ wboi."remainQty" >= $7 and
277
+ wboi."remainUomValue" >= $8
278
+ `,
279
+ [
280
+ warehouseDomain.id,
281
+ customerBizplace.id,
282
+ item.product.id,
283
+ item.packingType,
284
+ item.packingSize,
285
+ item.uom,
286
+ item.releaseQty,
287
+ item.releaseUomValue
288
+ ]
289
+ )
290
+ if (itemList.length <= 0) {
291
+ errorList.push(
292
+ JSON.stringify({
293
+ code: 'insufficient stock',
294
+ sku: item.product.sku,
295
+ packingType: item.packingType,
296
+ packingSize: item.packingSize,
297
+ uom: item.uom,
298
+ qty: item.releaseQty,
299
+ uom_value: item.releaseUomValue
300
+ })
301
+ )
302
+ }
303
+ console.log()
304
+ })
305
+ )
306
+
307
+ if (errorList.length > 0) {
308
+ context.throw(404, errorList.join(', '))
309
+ }
310
+
311
+ let assignedOrderProducts = await assignToInventory(
312
+ massagedData.orderProducts,
313
+ customerBizplace,
314
+ warehouseDomain,
315
+ context,
316
+ tx
317
+ )
318
+
319
+ let result = await createReleaseGood(
320
+ releaseGood,
321
+ assignedOrderProducts,
322
+ customerBizplace,
323
+ warehouseDomain,
324
+ context,
325
+ tx
326
+ )
327
+
328
+ let addReleaseGood = {
329
+ id: result.id,
330
+ name: result.name,
331
+ refNo: result.refNo,
332
+ refNo2: result.refNo2,
333
+ refNo3: result.refNo3,
334
+ status: result.status,
335
+ truckNo: result.truckNo,
336
+ ownTransport: result.ownTransport,
337
+ crossDocking: result.crossDocking,
338
+ type: result.type,
339
+ exportOption: result.exportOption,
340
+ releaseDate: result.releaseDate,
341
+ collectionOrderNo: result.collectionOrderNo,
342
+ paidAmount: result?.paidAmount,
343
+ discountAmount: result?.discountAmount,
344
+ taxAmount: result?.taxAmount,
345
+ shippingFee: result?.shippingFee,
346
+ bizplace: { name: result.bizplace.name },
347
+ domain: { name: result.domain.name },
348
+ orderInventories: result.orderInventories
349
+ }
350
+
351
+ context.body = { result: { addReleaseGood } }
352
+ // createPayloadLog(
353
+ // customerBizplace.id,
354
+ // `/webhook-xilnex-stock-request`,
355
+ // stockRequest,
356
+ // context.body,
357
+ // context,
358
+ // PayloadType.INGESTION
359
+ // )
127
360
  }
128
361
 
129
- orderProducts.push(orderProduct)
130
- })
131
- )
132
- await getRepository(OrderProduct).save(orderProducts)
133
- }
362
+ // VDC inbound
363
+ if (accountInfo.outlet == orderInfo.toLocation) {
364
+ const ganNoSetting: Setting = await tx.getRepository(Setting).findOne({
365
+ where: {
366
+ domain: warehouseDomain,
367
+ name: ORDER_NUMBER_SETTING_KEY.GAN_NUMBER_RULE
368
+ }
369
+ })
370
+
371
+ let orderNo: string = ''
372
+ if (ganNoSetting) {
373
+ orderNo = await generateId({ domain: warehouseDomain, type: ORDER_NUMBER_RULE_TYPE.GAN_NUMBER, seed: {} })
374
+ } else {
375
+ orderNo = OrderNoGenerator.arrivalNotice()
376
+ }
377
+
378
+ let arrivalNotice: any = {
379
+ name: orderNo,
380
+ refNo: orderInfo.orderId,
381
+ refNo2: orderInfo.fromLocation,
382
+ refNo3: orderInfo.toLocation,
383
+ etaDate: orderInfo.etaDate,
384
+ ownTransport: orderInfo.ownTransport,
385
+ importCargo: orderInfo.importCargo,
386
+ container: orderInfo.container,
387
+ remark: orderInfo.remark,
388
+ source: 'XILNEX',
389
+ orderSourceType: 'XILNEX_TRANSFER_NOTE',
390
+ domain: warehouseDomain,
391
+ bizplace: customerBizplace,
392
+ status: 'INTRANSIT'
393
+ }
394
+ arrivalNotice = await tx.getRepository(ArrivalNotice).save(arrivalNotice)
395
+
396
+ let orderProducts: any[] = []
397
+ await Promise.all(
398
+ orderItems.map(async item => {
399
+ const refCode: string = item.product.refCode
400
+ let productDetail: ProductDetail = await tx.getRepository(ProductDetail).findOne({
401
+ where: { domain: custCompanyBizplace.domain, refCode: refCode },
402
+ relations: ['product']
403
+ })
404
+
405
+ if (!productDetail) {
406
+ const sku: string = item.product.sku
407
+ const product: Product = await tx.getRepository(Product).findOne({
408
+ where: { domain: custCompanyBizplace.domain, sku }
409
+ })
134
410
 
135
- accountWebhookOrder.status = ACCOUNT_SYNC_STATUS.SUCCESS
136
- await getRepository(AccountWebhookOrder).save(accountWebhookOrder)
411
+ if (product) {
412
+ productDetail = await tx.getRepository(ProductDetail).findOne({
413
+ where: { domain: custCompanyBizplace.domain, product },
414
+ relations: ['product']
415
+ })
416
+ } else {
417
+ context.throw(404, `error.sku ${sku} and refCode ${refCode} not exist in product master`)
418
+ }
419
+ }
420
+
421
+ let orderProduct: any = {
422
+ name: OrderNoGenerator.orderProduct(),
423
+ product: productDetail?.product,
424
+ productDetail,
425
+ batchId: item?.batchId || '-',
426
+ packingType: productDetail?.packingType,
427
+ packingSize: productDetail?.packingSize,
428
+ uom: productDetail?.uom,
429
+ uomValue: productDetail?.uomValue,
430
+ packQty: item.confirmedQty,
431
+ totalUomValue: `${(productDetail.uomValue * item.confirmedQty).toFixed(2)} ${productDetail.uom}`,
432
+ status: 'PENDING',
433
+ arrivalNotice,
434
+ domain: warehouseDomain,
435
+ bizplace: customerBizplace
436
+ }
437
+
438
+ orderProducts.push(orderProduct)
439
+ })
440
+ )
441
+ await tx.getRepository(OrderProduct).save(orderProducts)
442
+ }
443
+ }
444
+ }
445
+
446
+ accountWebhookOrder.status = ACCOUNT_SYNC_STATUS.SUCCESS
447
+ await tx.getRepository(AccountWebhookOrder).save(accountWebhookOrder)
448
+ })
137
449
 
138
450
  context.status = 200
139
451
  } catch (err) {
140
- logger.error(`xilnex-router[webhook-xilnex-po]: ${err}`)
452
+ logger.error(`xilnex-router[webhook-xilnex-transfer-note]: ${err}`)
141
453
  }
142
454
  })
143
455
 
144
456
  xilnexRouter.post('/webhook-xilnex-stock-request', async (context, next) => {
145
457
  try {
146
- const { id } = context.query
147
- const { body } = context.request
148
- const data = body?.data ? JSON.parse(body.data) : {}
458
+ await getConnection().transaction(async (tx: EntityManager) => {
459
+ const { id, warehouseId } = context.query
460
+ const { body, headers } = context.request
461
+ const data = body?.Data ? JSON.parse(body.Data) : {}
462
+
463
+ const account = await AccountingAPI.getAccounting(id)
464
+ if (!account) {
465
+ throw new Error(`${id} not found in Account`)
466
+ }
467
+
468
+ const { webhookKey } = account
469
+ const signature = headers['xilnex-webhook-signature']
470
+
471
+ const xilnex = new Xilnex({})
472
+ if (!xilnex.isValidSignature(body, webhookKey, signature)) {
473
+ context.throw(400, 'Signature validation failed')
474
+ }
475
+
476
+ const customerDomain: Domain = account.domain
477
+ const customerBizplace: Bizplace = await tx.getRepository(Bizplace).findOne({
478
+ where: { domain: customerDomain }
479
+ })
480
+ const custCompanyBizplace: Bizplace = await getCompanyBizplace(null, null, customerBizplace.id, tx)
481
+
482
+ const warehouseDomain: Domain = await tx.getRepository(Domain).findOne({
483
+ where: { id: warehouseId, extType: 'warehouse' }
484
+ })
485
+
486
+ if (!warehouseDomain) {
487
+ context.throw(400, `${warehouseId} not found in warehouse domain`)
488
+ }
489
+
490
+ let accountWebhookOrder: any = {
491
+ name: uuidv4(),
492
+ request: body,
493
+ platform: ACCOUNT_PLATFORM.XILNEX,
494
+ domain: account.domain,
495
+ status: ACCOUNT_SYNC_STATUS.FAILED
496
+ }
497
+
498
+ let accountInfo: any = account?.accountInfo ? JSON.parse(account.accountInfo) : {}
499
+
500
+ if (data?.id) {
501
+ const stockRequest: any = await AccountingAPI.getStockRequest(account, { orderId: data.id })
502
+ const { orderInfo, orderItems } = stockRequest
503
+
504
+ if (orderInfo.fromLocation == accountInfo.outlet || orderInfo.toLocation == accountInfo.outlet) {
505
+ // VDC release
506
+ if (accountInfo.outlet == orderInfo.toLocation) {
507
+ let releaseGood = {
508
+ domain: warehouseDomain,
509
+ bizplace: customerBizplace,
510
+ companyBizplace: custCompanyBizplace,
511
+ courierOption: false,
512
+ collectionOrderNo: orderInfo.orderId,
513
+ exportOption: false,
514
+ ownTransport: orderInfo.ownTransport,
515
+ packingOption: false,
516
+ refNo: orderInfo.orderId,
517
+ refNo2: orderInfo?.fromLocation,
518
+ refNo3: orderInfo?.toLocation,
519
+ releaseDate: orderInfo.issueDate,
520
+ type: 'b2b',
521
+ source: 'XILNEX',
522
+ orderSourceType: 'XILNEX_STOCK_REQUEST',
523
+ marketplaceOrderStatus: orderInfo?.status,
524
+ remark: orderInfo?.remark || null
525
+ }
526
+
527
+ // check existing order
528
+ await checkExistingOrder(releaseGood, customerBizplace, true, context, tx)
529
+
530
+ //massage data
531
+ let massagedData = await massageOrderItems(releaseGood, orderItems, context)
532
+
533
+ // validation
534
+ let errorList = []
535
+ await Promise.all(
536
+ massagedData.combinedItems.map(async item => {
537
+ let itemList = await tx.query(
538
+ `
539
+ select wboi.* from warehouse_bizplace_onhand_inventories wboi
540
+ where wboi."domainId"= $1 and
541
+ wboi."bizplaceId" = $2 and
542
+ wboi."groupType" = 'SINGLE' and
543
+ wboi."productId" = $3 and
544
+ wboi."packingType" = $4 and
545
+ wboi."packingSize" = $5 and
546
+ wboi."uom" = $6 and
547
+ wboi."remainQty" >= $7 and
548
+ wboi."remainUomValue" >= $8
549
+ `,
550
+ [
551
+ warehouseDomain.id,
552
+ customerBizplace.id,
553
+ item.product.id,
554
+ item.packingType,
555
+ item.packingSize,
556
+ item.uom,
557
+ item.releaseQty,
558
+ item.releaseUomValue
559
+ ]
560
+ )
561
+ if (itemList.length <= 0) {
562
+ errorList.push(
563
+ JSON.stringify({
564
+ code: 'insufficient stock',
565
+ sku: item.product.sku,
566
+ packingType: item.packingType,
567
+ packingSize: item.packingSize,
568
+ uom: item.uom,
569
+ qty: item.releaseQty,
570
+ uom_value: item.releaseUomValue
571
+ })
572
+ )
573
+ }
574
+ console.log()
575
+ })
576
+ )
577
+
578
+ if (errorList.length > 0) {
579
+ context.throw(404, errorList.join(', '))
580
+ }
581
+
582
+ let assignedOrderProducts = await assignToInventory(
583
+ massagedData.orderProducts,
584
+ customerBizplace,
585
+ warehouseDomain,
586
+ context,
587
+ tx
588
+ )
589
+
590
+ let result = await createReleaseGood(
591
+ releaseGood,
592
+ assignedOrderProducts,
593
+ customerBizplace,
594
+ warehouseDomain,
595
+ context,
596
+ tx
597
+ )
598
+
599
+ let addReleaseGood = {
600
+ id: result.id,
601
+ name: result.name,
602
+ refNo: result.refNo,
603
+ refNo2: result.refNo2,
604
+ refNo3: result.refNo3,
605
+ status: result.status,
606
+ truckNo: result.truckNo,
607
+ ownTransport: result.ownTransport,
608
+ crossDocking: result.crossDocking,
609
+ type: result.type,
610
+ exportOption: result.exportOption,
611
+ releaseDate: result.releaseDate,
612
+ collectionOrderNo: result.collectionOrderNo,
613
+ paidAmount: result?.paidAmount,
614
+ discountAmount: result?.discountAmount,
615
+ taxAmount: result?.taxAmount,
616
+ shippingFee: result?.shippingFee,
617
+ bizplace: { name: result.bizplace.name },
618
+ domain: { name: result.domain.name },
619
+ orderInventories: result.orderInventories
620
+ }
149
621
 
150
- const account: Account = await getRepository(Account).findOne(id)
622
+ context.body = { result: { addReleaseGood } }
623
+ // createPayloadLog(
624
+ // customerBizplace.id,
625
+ // `/webhook-xilnex-stock-request`,
626
+ // stockRequest,
627
+ // context.body,
628
+ // context,
629
+ // PayloadType.INGESTION
630
+ // )
631
+ }
632
+
633
+ // VDC inbound
634
+ if (accountInfo.outlet == orderInfo.fromLocation) {
635
+ const ganNoSetting: Setting = await tx.getRepository(Setting).findOne({
636
+ where: {
637
+ domain: warehouseDomain,
638
+ name: ORDER_NUMBER_SETTING_KEY.GAN_NUMBER_RULE
639
+ }
640
+ })
641
+
642
+ let orderNo: string = ''
643
+ if (ganNoSetting) {
644
+ orderNo = await generateId({ domain: warehouseDomain, type: ORDER_NUMBER_RULE_TYPE.GAN_NUMBER, seed: {} })
645
+ } else {
646
+ orderNo = OrderNoGenerator.arrivalNotice()
647
+ }
648
+
649
+ let arrivalNotice: any = {
650
+ name: orderNo,
651
+ refNo: orderInfo.orderId,
652
+ refNo2: orderInfo.fromLocation,
653
+ refNo3: orderInfo.toLocation,
654
+ etaDate: orderInfo.etaDate,
655
+ ownTransport: orderInfo.ownTransport,
656
+ importCargo: orderInfo.importCargo,
657
+ container: orderInfo.container,
658
+ remark: orderInfo.remark,
659
+ source: 'XILNEX',
660
+ orderSourceType: 'XILNEX_STOCK_REQUEST',
661
+ domain: warehouseDomain,
662
+ bizplace: customerBizplace,
663
+ status: 'INTRANSIT'
664
+ }
665
+ arrivalNotice = await tx.getRepository(ArrivalNotice).save(arrivalNotice)
666
+
667
+ let orderProducts: any[] = []
668
+ await Promise.all(
669
+ orderItems.map(async item => {
670
+ const productDetail: ProductDetail = await getRepository(ProductDetail).findOne({
671
+ where: { domain: custCompanyBizplace.domain, refCode: item.product.refCode },
672
+ relations: ['product']
673
+ })
674
+
675
+ if (!productDetail) {
676
+ context.throw(404, `no matching product found`)
677
+ }
678
+
679
+ let orderProduct: any = {
680
+ name: OrderNoGenerator.orderProduct(),
681
+ product: productDetail?.product,
682
+ productDetail,
683
+ batchId: item?.batchId || '-',
684
+ packingType: productDetail?.packingType,
685
+ packingSize: productDetail?.packingSize,
686
+ uom: productDetail?.uom,
687
+ uomValue: productDetail?.uomValue,
688
+ packQty: item.confirmedQty,
689
+ totalUomValue: `${(productDetail.uomValue * item.confirmedQty).toFixed(2)} ${productDetail.uom}`,
690
+ status: 'PENDING',
691
+ arrivalNotice,
692
+ domain: warehouseDomain,
693
+ bizplace: customerBizplace
694
+ }
695
+
696
+ orderProducts.push(orderProduct)
697
+ })
698
+ )
699
+ await tx.getRepository(OrderProduct).save(orderProducts)
700
+ }
701
+ }
702
+ }
703
+
704
+ accountWebhookOrder.status = ACCOUNT_SYNC_STATUS.SUCCESS
705
+ await tx.getRepository(AccountWebhookOrder).save(accountWebhookOrder)
706
+ })
151
707
 
152
708
  context.status = 200
153
709
  } catch (err) {
154
710
  logger.error(`xilnex-router[webhook-xilnex-stock-request]: ${err}`)
155
711
  }
156
712
  })
713
+
714
+ async function checkExistingOrder(releaseGood, bizplace, checkDuplicationValidation = true, context, tx) {
715
+ const { domain }: { domain: Domain } = context.state
716
+ const refNo: string = releaseGood.refNo
717
+ const refNo2: string = releaseGood.refNo2
718
+ const refNo3: string = releaseGood.refNo3
719
+
720
+ if (checkDuplicationValidation) {
721
+ const foundDraftReleaseGood: DraftReleaseGood = await tx.getRepository(DraftReleaseGood).findOne({
722
+ where: {
723
+ domain,
724
+ refNo: refNo || null,
725
+ refNo2: refNo2 || null,
726
+ refNo3: refNo3 || null,
727
+ status: Not(In([DRAFT_RELEASE_ORDER_STATUS.CANCELLED]))
728
+ }
729
+ })
730
+
731
+ if (foundDraftReleaseGood) {
732
+ throw new ValidationError({
733
+ errorCode: 'EXISTING_ORDER',
734
+ message: 'existing draft release order found',
735
+ detail: foundDraftReleaseGood.name
736
+ })
737
+ }
738
+
739
+ const foundReleaseGood: ReleaseGood = await tx.getRepository(ReleaseGood).findOne({
740
+ where: {
741
+ domain: domain,
742
+ refNo,
743
+ status: Not(In([ORDER_STATUS.CANCELLED, ORDER_STATUS.PENDING_CANCEL]))
744
+ },
745
+ relations: ['bizplace', 'bizplace.domain', 'bizplace.company', 'bizplace.company.domain']
746
+ })
747
+
748
+ if (foundReleaseGood) {
749
+ throw new ValidationError({
750
+ errorCode: 'EXISTING_ORDER',
751
+ message: 'existing release order found',
752
+ detail: foundReleaseGood.name
753
+ })
754
+ }
755
+ }
756
+ }
757
+
758
+ async function massageOrderItems(
759
+ releaseGood,
760
+ inputOrderProducts,
761
+ context
762
+ ): Promise<{ orderProducts: OrderProduct[]; combinedItems: OrderProduct[] }> {
763
+ try {
764
+ let orderProducts: OrderProduct[] = []
765
+ let errorList = []
766
+ let totalReleaseQty = 0
767
+ //combine any repeated skus
768
+ inputOrderProducts = inputOrderProducts.reduce((acc, itm, idx) => {
769
+ let { sku, refCode } = itm.product
770
+ let { packingType, packingSize, confirmedQty, orderItemId } = itm
771
+
772
+ if (!sku && !refCode) {
773
+ errorList.push(
774
+ JSON.stringify({
775
+ code: 'INVALID_DATA',
776
+ message: 'sku or refCode not found',
777
+ sku: sku,
778
+ refCode: refCode,
779
+ packingType: packingType?.trim(),
780
+ packingSize: packingSize,
781
+ qty: confirmedQty
782
+ })
783
+ )
784
+ }
785
+
786
+ if (confirmedQty <= 0) {
787
+ errorList.push(
788
+ JSON.stringify({
789
+ code: 'INVALID_DATA',
790
+ message: 'negative stock request',
791
+ sku: sku,
792
+ refCode: refCode,
793
+ packingType: packingType?.trim(),
794
+ packingSize: packingSize,
795
+ qty: confirmedQty
796
+ })
797
+ )
798
+ }
799
+
800
+ let existingIndex = acc.findIndex(
801
+ itm =>
802
+ itm?.sku == sku?.trim() &&
803
+ itm?.refCode == refCode?.trim() &&
804
+ itm?.packingType == packingType?.trim() &&
805
+ itm?.packingSize == packingSize &&
806
+ itm?.refItemId == orderItemId.trim()
807
+ )
808
+
809
+ if (existingIndex >= 0 && confirmedQty > 0) {
810
+ acc[existingIndex].confirmedQty = acc[existingIndex].confirmedQty + confirmedQty
811
+ totalReleaseQty += confirmedQty
812
+ } else {
813
+ acc.push({
814
+ sku: sku?.trim(),
815
+ refCode: refCode?.trim(),
816
+ packingType: packingType?.trim(),
817
+ packingSize: packingSize,
818
+ releaseQty: confirmedQty,
819
+ refItemId: orderItemId
820
+ })
821
+
822
+ totalReleaseQty += confirmedQty
823
+ }
824
+
825
+ return acc
826
+ }, [])
827
+
828
+ if (errorList.length > 0) {
829
+ throw new ValidationError({
830
+ errorCode: 'INVALID_DATA',
831
+ message: 'invalid data',
832
+ detail: errorList.join(', ')
833
+ })
834
+ }
835
+
836
+ //find customer sku in system
837
+ await Promise.all(
838
+ inputOrderProducts.map(async inputOrderItem => {
839
+ const sku: string = inputOrderItem.sku
840
+ const refCode: string = inputOrderItem.refCode
841
+ const packingType: string = inputOrderItem.packingType
842
+ const packingSize: string = inputOrderItem.packingSize
843
+
844
+ const qb: SelectQueryBuilder<ProductDetail> = getRepository(ProductDetail)
845
+ .createQueryBuilder('PD')
846
+ .innerJoinAndSelect('PD.product', 'PROD')
847
+ .where('PROD.bizplace_id = :bizplaceId', { bizplaceId: releaseGood.companyBizplace.id })
848
+
849
+ if (!sku && !refCode) {
850
+ context.throw(404, `error.sku ${sku} or refCode ${refCode} not found`)
851
+ }
852
+
853
+ if (refCode) {
854
+ qb.andWhere('PD.ref_code = :refCode', { refCode })
855
+ }
856
+
857
+ if (sku) {
858
+ qb.andWhere('PROD.sku = :sku', { sku })
859
+ }
860
+
861
+ if (packingType) {
862
+ qb.andWhere('PD.packing_type = :packingType', { packingType })
863
+ }
864
+
865
+ if ((!packingType && !refCode) || !packingSize) {
866
+ qb.andWhere('PD.is_default = :isDefault', { isDefault: true })
867
+ }
868
+
869
+ const productDetail: ProductDetail = await qb.getOne()
870
+
871
+ // order Product based on product detail
872
+ if (productDetail) {
873
+ let newOrderProduct: OrderInventory = Object.assign({}, inputOrderItem)
874
+ newOrderProduct = {
875
+ ...newOrderProduct,
876
+ product: productDetail.product,
877
+ productDetail: productDetail,
878
+ packingType: productDetail.packingType,
879
+ packingSize: packingSize || productDetail.packingSize,
880
+ uom: productDetail.uom,
881
+ uomValue: productDetail.uomValue,
882
+ refCode: inputOrderItem.refCode,
883
+ releaseQty: inputOrderItem.releaseQty,
884
+ releaseUomValue: inputOrderItem.releaseQty * (productDetail.uomValue || 1),
885
+ packQty: 0,
886
+ actualPackQty: 0,
887
+ palletQty: 0,
888
+ actualPalletQty: 0,
889
+ type: 'RELEASE_OF_GOODS',
890
+ status: ORDER_PRODUCT_STATUS.ASSIGNED,
891
+ batchId: '-',
892
+ sellingPrice: inputOrderItem?.sellingPrice
893
+ }
894
+
895
+ newOrderProduct.orderInventories = [
896
+ {
897
+ ...newOrderProduct,
898
+ packQty: 0,
899
+ actualPackQty: 0,
900
+ palletQty: 0,
901
+ actualPalletQty: 0,
902
+ type: 'RELEASE_OF_GOODS',
903
+ status: ORDER_INVENTORY_STATUS.PENDING_WORKSHEET
904
+ }
905
+ ]
906
+
907
+ orderProducts.push(newOrderProduct)
908
+ }
909
+
910
+ if (!productDetail) {
911
+ const qb: SelectQueryBuilder<ProductDetail> = getRepository(ProductBundle)
912
+ .createQueryBuilder('PB')
913
+ .innerJoinAndSelect('PB.productBundleSettings', 'PBS')
914
+ .innerJoinAndSelect('PBS.productDetail', 'PBD')
915
+ .innerJoinAndSelect('PBD.product', 'PROD')
916
+ .where('PB.bizplace_id = :bizplaceId', { bizplaceId: releaseGood.companyBizplace.id })
917
+
918
+ if (!sku && !refCode) {
919
+ context.throw(404, `error.sku ${sku} or refCode ${refCode} not found`)
920
+ }
921
+
922
+ if (refCode) {
923
+ qb.andWhere('PB.ref_code = :refCode', { refCode })
924
+ }
925
+
926
+ if (sku) {
927
+ qb.andWhere('PB.sku = :sku', { sku })
928
+ }
929
+
930
+ if (packingType) {
931
+ qb.andWhere('PB.packing_type = :packingType', { packingType })
932
+ }
933
+
934
+ const foundProductBundle: ProductBundle = await qb.getOne()
935
+
936
+ if (foundProductBundle) {
937
+ let orderProduct = {
938
+ productBundle: foundProductBundle,
939
+ refCode,
940
+ packingType: foundProductBundle.packingType,
941
+ packingSize: packingSize || 1,
942
+ uom: inputOrderItem?.uom,
943
+ releaseQty: inputOrderItem.releaseQty,
944
+ releaseUomValue: inputOrderItem.releaseQty,
945
+ packQty: 0,
946
+ actualPackQty: 0,
947
+ palletQty: 0,
948
+ actualPalletQty: 0,
949
+ orderInventories: [],
950
+ type: 'RELEASE_OF_GOODS',
951
+ status: ORDER_PRODUCT_STATUS.ASSIGNED,
952
+ batchId: '-',
953
+ sellingPrice: inputOrderItem?.sellingPrice
954
+ }
955
+
956
+ const bundleProductSettings: any[] = foundProductBundle.productBundleSettings
957
+ bundleProductSettings.map(bundleProductSetting => {
958
+ const productDetailBundle: ProductDetail = bundleProductSetting.productDetail
959
+
960
+ let newOrderInventory: OrderInventory = {
961
+ product: productDetailBundle.product,
962
+ productDetail: productDetailBundle,
963
+ sku: productDetailBundle.product.sku,
964
+ packingType: productDetailBundle.packingType,
965
+ packingSize: productDetailBundle.packingSize,
966
+ uom: productDetailBundle.uom,
967
+ packQty: 0,
968
+ actualPackQty: 0,
969
+ palletQty: 0,
970
+ actualPalletQty: 0,
971
+ releaseQty: inputOrderItem.confirmedQty * bundleProductSetting.bundleQty,
972
+ releaseUomValue:
973
+ inputOrderItem.confirmedQty *
974
+ bundleProductSetting.bundleQty *
975
+ (productDetailBundle.uomValue <= 0 ? 1 : productDetailBundle.uomValue),
976
+ type: 'RELEASE_OF_GOODS',
977
+ status: ORDER_INVENTORY_STATUS.PENDING_WORKSHEET
978
+ }
979
+
980
+ orderProduct.orderInventories.push(newOrderInventory)
981
+ })
982
+ orderProducts.push(orderProduct)
983
+ } else {
984
+ if (sku && refCode) {
985
+ context.throw(404, `error.sku ${sku} and refCode ${refCode} not exist in product / bundle master`)
986
+ }
987
+
988
+ if (sku) {
989
+ context.throw(404, `error.sku ${sku} not exist in product / bundle master`)
990
+ }
991
+
992
+ if (refCode) {
993
+ context.throw(404, `error.refCode ${refCode} not exist in product / bundle master`)
994
+ }
995
+ }
996
+ }
997
+ })
998
+ )
999
+
1000
+ //combine any repeated skus
1001
+ let combinedItems: OrderProduct[] = orderProducts.reduce((acc, op, idx) => {
1002
+ let oi = op?.orderInventories || []
1003
+ oi.forEach(itm => {
1004
+ let { sku, refCode } = itm.product
1005
+ let { packingType, packingSize, batchId, releaseQty, product, productDetail, uom } = itm
1006
+
1007
+ if (!sku && !refCode) {
1008
+ context.throw(404, `sku ${sku} or refCode ${refCode} not found`)
1009
+ }
1010
+ let existingIndex = acc.findIndex(
1011
+ itm =>
1012
+ itm?.sku == sku?.trim() &&
1013
+ itm?.refCode == refCode?.trim() &&
1014
+ itm?.packingType == packingType?.trim() &&
1015
+ itm?.packingSize == packingSize &&
1016
+ itm?.batchId == batchId?.trim() &&
1017
+ itm?.uom == uom?.trim()
1018
+ )
1019
+
1020
+ if (existingIndex >= 0 && releaseQty > 0) {
1021
+ acc[existingIndex].releaseQty = acc[existingIndex].releaseQty + releaseQty
1022
+ acc[existingIndex].releaseUomValue =
1023
+ acc[existingIndex].releaseUomValue + releaseQty * (productDetail.uomValue <= 0 ? 1 : productDetail.uomValue)
1024
+ } else {
1025
+ let updateObj = {
1026
+ sku: sku?.trim(),
1027
+ packingType: packingType?.trim(),
1028
+ packingSize: packingSize,
1029
+ uom: uom?.trim(),
1030
+ releaseQty: releaseQty,
1031
+ releaseUomValue: releaseQty * (productDetail.uomValue <= 0 ? 1 : productDetail.uomValue),
1032
+ product,
1033
+ productDetail
1034
+ }
1035
+
1036
+ if (refCode) updateObj['refCode'] = refCode.trim()
1037
+ updateObj['batchId'] = batchId ? batchId?.trim() : '-'
1038
+
1039
+ acc.push(updateObj)
1040
+ }
1041
+ })
1042
+ return acc
1043
+ }, [])
1044
+
1045
+ return { orderProducts, combinedItems }
1046
+ } catch (error) {
1047
+ throw error
1048
+ }
1049
+ }
1050
+
1051
+ async function assignToInventory(orderProducts: OrderProduct[], customerBizplace, warehouseDomain, context, tx) {
1052
+ const { client, domain, user } = context.state
1053
+ for (let opIdx = 0; opIdx < orderProducts.length; opIdx++) {
1054
+ let orderProduct = orderProducts[opIdx]
1055
+
1056
+ let assignedOrderInventories = []
1057
+ const pickingProductSetting: Setting = await tx.getRepository(Setting).findOne({
1058
+ where: { domain, name: 'rule-for-picking-product' }
1059
+ })
1060
+
1061
+ for (let oiIdx = 0; oiIdx < orderProducts[opIdx].orderInventories.length; oiIdx++) {
1062
+ const orderInventory = orderProduct.orderInventories[oiIdx]
1063
+ let sortings: any = []
1064
+
1065
+ switch (orderInventory.product.pickingStrategy) {
1066
+ case 'LIFO':
1067
+ sortings.push({ name: 'iv.created_at', desc: true })
1068
+ if (pickingProductSetting?.value) {
1069
+ for (const [key, value] of Object.entries(JSON.parse(pickingProductSetting.value))) {
1070
+ sortings.push({ name: `loc.${key}`, desc: value == 'DESC' ? true : false })
1071
+ }
1072
+ } else {
1073
+ sortings.push({ name: 'loc.name', desc: false }, { name: 'iv.created_at', desc: false })
1074
+ }
1075
+
1076
+ break
1077
+ case 'FEFO':
1078
+ sortings.push({ name: 'iv.expiration_date', desc: false }, { name: 'iv.created_at', desc: false })
1079
+ if (pickingProductSetting?.value) {
1080
+ for (const [key, value] of Object.entries(JSON.parse(pickingProductSetting.value))) {
1081
+ sortings.push({ name: `loc.${key}`, desc: value == 'DESC' ? true : false })
1082
+ }
1083
+ } else {
1084
+ sortings.push({ name: 'loc.name', desc: false }, { name: 'iv.created_at', desc: false })
1085
+ }
1086
+
1087
+ break
1088
+ case 'FMFO':
1089
+ sortings.push({ name: 'iv.manufacture_date', desc: false }, { name: 'iv.created_at', desc: false })
1090
+ if (pickingProductSetting?.value) {
1091
+ for (const [key, value] of Object.entries(JSON.parse(pickingProductSetting.value))) {
1092
+ sortings.push({ name: `loc.${key}`, desc: value == 'DESC' ? true : false })
1093
+ }
1094
+ } else {
1095
+ sortings.push({ name: 'loc.name', desc: false }, { name: 'iv.created_at', desc: false })
1096
+ }
1097
+
1098
+ break
1099
+ case 'LOCATION':
1100
+ if (pickingProductSetting?.value) {
1101
+ for (const [key, value] of Object.entries(JSON.parse(pickingProductSetting.value))) {
1102
+ sortings.push({ name: `loc.${key}`, desc: value == 'DESC' ? true : false })
1103
+ }
1104
+ } else {
1105
+ sortings.push({ name: 'loc.name', desc: false }, { name: 'iv.created_at', desc: false })
1106
+ }
1107
+
1108
+ break
1109
+ //Every other case includes 'FIFO' will be applicable for this case
1110
+ default:
1111
+ sortings.push({ name: 'iv.created_at', desc: false })
1112
+ if (pickingProductSetting?.value) {
1113
+ for (const [key, value] of Object.entries(JSON.parse(pickingProductSetting.value))) {
1114
+ sortings.push({ name: `loc.${key}`, desc: value == 'DESC' ? true : false })
1115
+ }
1116
+ } else {
1117
+ sortings.push({ name: 'loc.name', desc: false })
1118
+ sortings.push({ name: 'iv.pallet_id', desc: false })
1119
+ }
1120
+ break
1121
+ }
1122
+
1123
+ let queryFilters = []
1124
+
1125
+ let queryStrings = queryFilters.reduce(
1126
+ (acc, itm, idx, arr) => {
1127
+ acc.values.push(itm.filters)
1128
+
1129
+ switch (itm?.operator) {
1130
+ case 'notin':
1131
+ case 'in':
1132
+ acc.query.push(
1133
+ `${itm.query} ${itm.operator == 'notin' ? 'NOT IN' : 'IN'} (${itm.filtes
1134
+ .map((itm, idx) => {
1135
+ return `$${acc.values.length + 1}`
1136
+ })
1137
+ .join(',')})`
1138
+ )
1139
+ break
1140
+
1141
+ default:
1142
+ acc.query.push(`${itm.query} ${itm?.operator ? itm.operator : '='} $${acc.values.length + 1}`)
1143
+ break
1144
+ }
1145
+ acc.query.push(`${itm.query} ${itm?.operator ? itm.operator : '='} $${acc.values.length + 1}`)
1146
+ },
1147
+ {
1148
+ query: [],
1149
+ values: []
1150
+ }
1151
+ )
1152
+
1153
+ let sortQuery = sortings
1154
+ .map(itm => {
1155
+ return `${itm.name} ${itm.desc ? 'DESC' : 'ASC'}`
1156
+ })
1157
+ .join(`,`)
1158
+
1159
+ let updatedInventories = await tx.getRepository(Inventory).query(
1160
+ `
1161
+ update inventories tgt set locked_qty = coalesce(locked_qty,0) + src.reserve_qty,
1162
+ locked_uom_value = coalesce(locked_uom_value,0) + src.reserve_uom_value,
1163
+ updated_at = NOW(),
1164
+ updater_id = $1
1165
+ from (
1166
+ select "id", "pallet_id","carton_id", "batch_id", "batch_id_ref", "unit", "uom", "packing_type", "packing_size", "manufacture_year",
1167
+ "reserve_qty", (("uom_value"/"qty") * "reserve_qty") as "reserve_uom_value" from (
1168
+ select "sort_seq", "id", "pallet_id", "batch_id", "batch_id_ref", "unit", "uom", "packing_type", "packing_size", "manufacture_year", "carton_id", "uom_value", "locked_uom_value", "qty", "locked_qty", "created_at",
1169
+ "release_qty", "release_uom_value", lock_amount,
1170
+ case when "lock_amount" < 0 then "available_qty" else "available_qty" - "lock_amount" end as "reserve_qty"
1171
+ from (
1172
+ select *,
1173
+ qty - locked_qty as available_qty,
1174
+ sum(qty - locked_qty - release_qty) over (order by sort_seq asc rows between unbounded preceding and current row) as lock_amount
1175
+ from (
1176
+ SELECT 0 as sort_seq, null as id, null as pallet_id, null as batch_id, null as batch_id_ref,
1177
+ null as unit, '${orderInventory.uom}' as uom, '${orderInventory.packingType}' as packing_type, '${
1178
+ orderInventory.packingSize
1179
+ }' as packing_size,
1180
+ null as manufacture_year, null as carton_id, 0 as uom_value, 0 as locked_uom_value, 0 as qty, 0 as locked_qty, null as created_at,
1181
+ '${orderInventory.releaseQty}' as release_qty, '${orderInventory.releaseUomValue}' as release_uom_value
1182
+ UNION
1183
+ (
1184
+ SELECT ROW_NUMBER() OVER(PARTITION BY iv.domain_id ORDER BY ${sortQuery}) as sort_seq,
1185
+ iv.id, iv.pallet_id, iv.batch_id, iv.batch_id_ref,
1186
+ iv.unit, iv.uom, iv.packing_type, iv.packing_size,
1187
+ iv.manufacture_year, iv.carton_id, iv.uom_value,
1188
+ coalesce(iv.locked_uom_value,0) as locked_uom_value, iv.qty, coalesce(iv.locked_qty,0) as locked_qty, iv.created_at,
1189
+ 0 as release_qty, 0 as release_uom_value
1190
+ FROM "inventories" "iv"
1191
+ INNER JOIN "locations" "loc" ON "loc"."id"="iv"."location_id"
1192
+ WHERE case when coalesce("iv"."locked_qty",0) < 0 then 0 else ("iv"."qty" - coalesce("iv"."locked_qty",0)) end > 0 AND
1193
+ "iv"."domain_id" = $2 AND "iv"."bizplace_id" = $3 AND "iv"."packing_type" = $4 AND "iv"."packing_size" = $5 AND "iv"."product_id" = $6 AND "iv"."status" = $7 AND "loc"."type" NOT IN ($8, $9)
1194
+ ${queryStrings.query.length > 0 ? `AND ${queryStrings.join(' AND ')}` : ''}
1195
+ ORDER BY ${sortQuery}
1196
+ )
1197
+ ) dt1
1198
+ ) dt2 where case when "lock_amount" < 0 then "available_qty" else "available_qty" - "lock_amount" end > 0
1199
+ ) dt3 where sort_seq > 0
1200
+ ) src where src.id = tgt.id
1201
+ returning
1202
+ tgt."id", tgt."qty", tgt."pallet_id", tgt."carton_id", tgt."batch_id", tgt."batch_id_ref", tgt."unit",
1203
+ tgt."uom", tgt."packing_type", tgt."packing_size", tgt."manufacture_year",
1204
+ tgt."locked_qty", tgt."uom_value", tgt."locked_uom_value", src."reserve_qty", src."reserve_uom_value";`,
1205
+ [
1206
+ user?.id || null,
1207
+ warehouseDomain.id,
1208
+ customerBizplace.id,
1209
+ orderInventory.packingType,
1210
+ orderInventory.packingSize,
1211
+ orderInventory.product.id,
1212
+ INVENTORY_STATUS.STORED,
1213
+ LOCATION_TYPE.QUARANTINE,
1214
+ LOCATION_TYPE.RESERVE,
1215
+ ...queryStrings.values
1216
+ ]
1217
+ )
1218
+
1219
+ let totalAssigned = updatedInventories[0].reduce((acc, inv) => {
1220
+ return acc + inv.reserve_qty
1221
+ }, 0)
1222
+
1223
+ if (orderInventory.releaseQty != totalAssigned) {
1224
+ throw new ValidationError({
1225
+ errorCode: 'INSUFFICIENT_STOCK',
1226
+ message: 'insufficient stock',
1227
+ detail: JSON.stringify({
1228
+ code: 'insufficient stock',
1229
+ sku: orderInventory.product.sku,
1230
+ packingType: orderInventory.packingType,
1231
+ packingSize: orderInventory.packingSize,
1232
+ uom: orderInventory.uom,
1233
+ qty: orderInventory.releaseQty,
1234
+ uom_value: orderInventory.releaseUomValue
1235
+ })
1236
+ })
1237
+ }
1238
+
1239
+ updatedInventories[0].forEach(inv => {
1240
+ let oi = {
1241
+ ...orderInventory,
1242
+ inventory: { id: inv.id },
1243
+ uom: inv.uom,
1244
+ packingType: inv.packing_type,
1245
+ batchId: inv.batch_id,
1246
+ releaseQty: inv.reserve_qty,
1247
+ releaseUomValue: inv.reserve_uom_value,
1248
+ pickedQty: 0,
1249
+ packedQty: 0,
1250
+ type: ORDER_TYPES.RELEASE_OF_GOODS,
1251
+ status: ORDER_INVENTORY_STATUS.PENDING_WORKSHEET,
1252
+ domain: { id: warehouseDomain.id },
1253
+ bizplace: { id: customerBizplace.id }
1254
+ }
1255
+
1256
+ assignedOrderInventories.push(oi)
1257
+ })
1258
+ }
1259
+
1260
+ orderProducts[opIdx].orderInventories = assignedOrderInventories
1261
+ }
1262
+
1263
+ return orderProducts
1264
+ }
1265
+
1266
+ async function createReleaseGood(
1267
+ releaseGood: any,
1268
+ orderProducts: OrderProduct[],
1269
+ bizplace: Bizplace,
1270
+ domain: Domain,
1271
+ context: any,
1272
+ tx?: EntityManager
1273
+ ): Promise<ReleaseGood> {
1274
+ try {
1275
+ if (orderProducts.length <= 0) {
1276
+ throw new ValidationError({
1277
+ errorCode: 'ITEM_NOT_FOUND',
1278
+ message: 'no order items found',
1279
+ detail: JSON.stringify(releaseGood)
1280
+ })
1281
+ }
1282
+
1283
+ let newReleaseGood: ReleaseGood = new ReleaseGood()
1284
+
1285
+ // find RO number rule setting
1286
+ const roNoSetting: Setting = await tx.getRepository(Setting).findOne({
1287
+ where: {
1288
+ domain,
1289
+ name: ORDER_NUMBER_SETTING_KEY.RO_NUMBER_RULE
1290
+ }
1291
+ })
1292
+
1293
+ newReleaseGood = {
1294
+ name: roNoSetting
1295
+ ? await generateId({ domain, type: ORDER_NUMBER_RULE_TYPE.RO_NUMBER, seed: {} })
1296
+ : OrderNoGenerator.releaseGood(),
1297
+ domain: domain,
1298
+ bizplace: bizplace,
1299
+ collectionOrderNo: releaseGood.collectionOrderNo,
1300
+ courierOption: releaseGood.courierOption,
1301
+ exportOption: releaseGood.exportOption,
1302
+ ownTransport: releaseGood.ownTransport,
1303
+ packingOption: releaseGood.packingOption,
1304
+ marketplaceOrderStatus: releaseGood?.marketplaceOrderStatus || null,
1305
+ refNo: releaseGood.refNo,
1306
+ refNo2: releaseGood.refNo2 || null,
1307
+ refNo3: releaseGood.refNo3 || null,
1308
+ remark: releaseGood?.remark || null,
1309
+ releaseDate: releaseGood.releaseDate,
1310
+ truckNo: releaseGood.truckNo,
1311
+ type: releaseGood?.type ? releaseGood.type : 'b2b',
1312
+ status: ORDER_STATUS.PENDING_WORKSHEET,
1313
+ noOfItems: orderProducts.length,
1314
+ source: releaseGood?.source,
1315
+ paidAmount: releaseGood?.paidAmount,
1316
+ discountAmount: releaseGood?.discountAmount,
1317
+ taxAmount: releaseGood?.taxAmount,
1318
+ shippingFee: releaseGood?.shippingFee,
1319
+ acceptedAt: new Date()
1320
+ }
1321
+
1322
+ let resultReleaseGood = await tx.getRepository(ReleaseGood).save(newReleaseGood)
1323
+ let combinedOrderInventories: OrderInventory[] = []
1324
+ let savedOrderProducts = await Promise.all(
1325
+ orderProducts.map(async orderProduct => {
1326
+ let orderInventories: OrderInventory[] = orderProduct.orderInventories
1327
+ orderProduct = Object.assign(new OrderProduct(), {
1328
+ ...orderProduct,
1329
+ name: uuidv4(),
1330
+ domain: domain,
1331
+ bizplace: bizplace,
1332
+ releaseGood: resultReleaseGood,
1333
+ type: ORDER_TYPES.RELEASE_OF_GOODS,
1334
+ status: ORDER_PRODUCT_STATUS.ASSIGNED
1335
+ })
1336
+ let result = await tx.getRepository(OrderProduct).save(orderProduct)
1337
+
1338
+ for (let oiIdx = 0; oiIdx < orderInventories.length; oiIdx++) {
1339
+ let orderInventory = Object.assign(new OrderInventory(), {
1340
+ ...orderInventories[oiIdx],
1341
+ name: uuidv4(),
1342
+ domain: domain,
1343
+ bizplace: bizplace,
1344
+ releaseGood: resultReleaseGood,
1345
+ type: ORDER_TYPES.RELEASE_OF_GOODS,
1346
+ status: ORDER_INVENTORY_STATUS.PENDING_WORKSHEET
1347
+ })
1348
+ await tx.getRepository(OrderInventory).save(orderInventory)
1349
+
1350
+ combinedOrderInventories.push({
1351
+ product: { name: orderInventory.product.name, sku: orderInventory.product.sku },
1352
+ batchId: orderInventory.batchId,
1353
+ packingType: orderInventory.packingType,
1354
+ packingSize: orderInventory.packingSize,
1355
+ releaseQty: orderInventory.releaseQty,
1356
+ releaseUomValue: orderInventory.releaseUomValue
1357
+ })
1358
+ }
1359
+
1360
+ return result
1361
+ })
1362
+ )
1363
+
1364
+ newReleaseGood.orderProducts = savedOrderProducts
1365
+ newReleaseGood.orderInventories = combinedOrderInventories
1366
+
1367
+ return newReleaseGood
1368
+ } catch (error) {
1369
+ throw error
1370
+ }
1371
+ }