@things-factory/operato-hub 4.3.130 → 4.3.132

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