@things-factory/operato-hub 4.3.769 → 4.3.771

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/config.development.js +107 -52
  2. package/dist-server/routers/api/restful-apis/unstable/add-release-order.js +81 -45
  3. package/dist-server/routers/api/restful-apis/unstable/add-release-order.js.map +1 -1
  4. package/dist-server/routers/api/restful-apis/v1/utils/params.js +3 -0
  5. package/dist-server/routers/api/restful-apis/v1/utils/params.js.map +1 -1
  6. package/dist-server/routers/api/restful-apis/v1/warehouse/add-release-order.js +55 -44
  7. package/dist-server/routers/api/restful-apis/v1/warehouse/add-release-order.js.map +1 -1
  8. package/dist-server/routers/api/restful-apis/v1/warehouse/get-inbound-order-details.js +4 -3
  9. package/dist-server/routers/api/restful-apis/v1/warehouse/get-inbound-order-details.js.map +1 -1
  10. package/dist-server/routers/api/restful-apis/v1/warehouse/get-inventory-warehouse-group-list.js +91 -0
  11. package/dist-server/routers/api/restful-apis/v1/warehouse/get-inventory-warehouse-group-list.js.map +1 -0
  12. package/dist-server/routers/api/restful-apis/v1/warehouse/get-release-order-details.js +2 -1
  13. package/dist-server/routers/api/restful-apis/v1/warehouse/get-release-order-details.js.map +1 -1
  14. package/dist-server/routers/api/restful-apis/v1/warehouse/get-return-order-details.js +3 -2
  15. package/dist-server/routers/api/restful-apis/v1/warehouse/get-return-order-details.js.map +1 -1
  16. package/dist-server/routers/api/restful-apis/v1/warehouse/index.js +1 -0
  17. package/dist-server/routers/api/restful-apis/v1/warehouse/index.js.map +1 -1
  18. package/dist-server/routers/api/restful-apis/v1/warehouse/update-release-order-details.js +65 -36
  19. package/dist-server/routers/api/restful-apis/v1/warehouse/update-release-order-details.js.map +1 -1
  20. package/dist-server/routers/xilnex-router.js +7 -2
  21. package/dist-server/routers/xilnex-router.js.map +1 -1
  22. package/openapi/v1/inbound.yaml +3 -0
  23. package/openapi/v1/outbound.yaml +7 -0
  24. package/openapi/v1/return-order.yaml +3 -0
  25. package/package.json +25 -25
  26. package/server/routers/api/restful-apis/unstable/add-release-order.ts +90 -50
  27. package/server/routers/api/restful-apis/v1/utils/params.ts +3 -0
  28. package/server/routers/api/restful-apis/v1/warehouse/add-release-order.ts +63 -48
  29. package/server/routers/api/restful-apis/v1/warehouse/get-inbound-order-details.ts +2 -1
  30. package/server/routers/api/restful-apis/v1/warehouse/get-inventory-warehouse-group-list.ts +114 -0
  31. package/server/routers/api/restful-apis/v1/warehouse/get-release-order-details.ts +2 -1
  32. package/server/routers/api/restful-apis/v1/warehouse/get-return-order-details.ts +2 -1
  33. package/server/routers/api/restful-apis/v1/warehouse/index.ts +1 -0
  34. package/server/routers/api/restful-apis/v1/warehouse/update-release-order-details.ts +69 -38
  35. package/server/routers/xilnex-router.ts +7 -2
@@ -141,7 +141,8 @@ router.get(
141
141
  zone: src.loc_zone,
142
142
  warehouse: {
143
143
  name: src.warehouse_name,
144
- description: src.warehouse_description
144
+ description: src.warehouse_description,
145
+ refCode: src.warehouse_ref_code
145
146
  }
146
147
  },
147
148
  orderProducts: orderProducts.find(op => op.id === src.oi_order_product_id),
@@ -121,7 +121,8 @@ router.get(
121
121
  warehouse: {
122
122
  id: orderInv.inventory?.location?.warehouse?.id,
123
123
  name: orderInv.inventory?.location?.warehouse?.name,
124
- description: orderInv.inventory?.location?.warehouse?.description
124
+ description: orderInv.inventory?.location?.warehouse?.description,
125
+ refCode: orderInv.inventory?.location?.warehouse?.refCode
125
126
  }
126
127
  }
127
128
  }
@@ -17,6 +17,7 @@ import './get-inbound-order-list'
17
17
  import './get-inventory-adjustment-details'
18
18
  import './get-inventory-adjustment-list'
19
19
  import './get-inventory-product-group-list'
20
+ import './get-inventory-warehouse-group-list'
20
21
  import './get-manifest-details'
21
22
  import './get-onhand-inventory-list'
22
23
  import './get-release-order-details'
@@ -448,10 +448,12 @@ async function removeOrderProducts(tx: EntityManager, orderProductIds: string[],
448
448
  .createQueryBuilder()
449
449
  .update(Inventory)
450
450
  .set({
451
- lockedQty: () => `COALESCE(locked_qty, 0) - ${oiReleaseQty}`,
452
- lockedUomValue: () => `COALESCE(locked_uom_value, 0) - ${oiReleaseUomValue}`,
451
+ lockedQty: () => `GREATEST(COALESCE(locked_qty, 0) - :releaseQty::numeric, 0)`,
452
+ lockedUomValue: () => `GREATEST(COALESCE(locked_uom_value, 0) - :releaseUomValue::numeric, 0)`,
453
453
  updater: orderProductEntity.updater
454
454
  })
455
+ .setParameter('releaseQty', oiReleaseQty)
456
+ .setParameter('releaseUomValue', oiReleaseUomValue)
455
457
  .where('id = :id', { id: inventoryEntity.id })
456
458
  .execute()
457
459
  }
@@ -490,9 +492,11 @@ async function removeOrderProducts(tx: EntityManager, orderProductIds: string[],
490
492
  .createQueryBuilder()
491
493
  .update(ProductDetailStock)
492
494
  .set({
493
- unassignedQty: () => `"unassigned_qty" - ${releaseQty}`,
494
- unassignedUomValue: () => `"unassigned_uom_value" - ${releaseUomValue}`
495
+ unassignedQty: () => `GREATEST("unassigned_qty" - :releaseQty::numeric, 0)`,
496
+ unassignedUomValue: () => `GREATEST("unassigned_uom_value" - :releaseUomValue::numeric, 0)`
495
497
  })
498
+ .setParameter('releaseQty', releaseQty)
499
+ .setParameter('releaseUomValue', releaseUomValue)
496
500
  .where({ productDetail: productDetail.id })
497
501
  .execute()
498
502
  }
@@ -560,9 +564,11 @@ async function updateExistingOrderProduct(
560
564
  .createQueryBuilder()
561
565
  .update(ProductDetailStock)
562
566
  .set({
563
- unassignedQty: () => `"unassigned_qty" + ${qtyDiff}`,
564
- unassignedUomValue: () => `"unassigned_uom_value" + ${uomValueDiff}`
567
+ unassignedQty: () => `"unassigned_qty" + :qtyDiff::numeric`,
568
+ unassignedUomValue: () => `"unassigned_uom_value" + :uomValueDiff::numeric`
565
569
  })
570
+ .setParameter('qtyDiff', qtyDiff)
571
+ .setParameter('uomValueDiff', uomValueDiff)
566
572
  .where({ productDetail: existing.productDetail.id })
567
573
  .execute()
568
574
  }
@@ -634,10 +640,12 @@ async function updateOrderInventoriesForQtyChange(
634
640
  .createQueryBuilder()
635
641
  .update(Inventory)
636
642
  .set({
637
- lockedQty: () => `COALESCE(locked_qty, 0) - ${deductQty}`,
638
- lockedUomValue: () => `COALESCE(locked_uom_value, 0) - ${deductUomValue}`,
643
+ lockedQty: () => `GREATEST(COALESCE(locked_qty, 0) - :deductQty::numeric, 0)`,
644
+ lockedUomValue: () => `GREATEST(COALESCE(locked_uom_value, 0) - :deductUomValue::numeric, 0)`,
639
645
  updater: user
640
646
  })
647
+ .setParameter('deductQty', deductQty)
648
+ .setParameter('deductUomValue', deductUomValue)
641
649
  .where('id = :id', { id: inventory.id })
642
650
  .execute()
643
651
  }
@@ -843,7 +851,6 @@ async function assignAdditionalInventory(
843
851
  acc.query.push(`${itm.query} ${itm?.operator ? itm.operator : '='} $${acc.values.length + 1}`)
844
852
  break
845
853
  }
846
- acc.query.push(`${itm.query} ${itm?.operator ? itm.operator : '='} $${acc.values.length + 1}`)
847
854
  return acc
848
855
  },
849
856
  {
@@ -895,6 +902,14 @@ async function assignAdditionalInventory(
895
902
  const releaseShelfLifeParamIndex = params.length + 1
896
903
  params.push(releaseShelfLifeOverride)
897
904
 
905
+ // Add seed row parameters to avoid SQL injection
906
+ const seedUomIdx = params.length + 1
907
+ const seedPackingTypeIdx = params.length + 2
908
+ const seedPackingSizeIdx = params.length + 3
909
+ const seedReleaseQtyIdx = params.length + 4
910
+ const seedReleaseUomValueIdx = params.length + 5
911
+ params.push(uom, orderProduct.packingType, orderProduct.packingSize, qtyDiff, uomValueDiff)
912
+
898
913
  // Build the sophisticated SQL query with window functions
899
914
  let query = `
900
915
  update inventories tgt set locked_qty = coalesce(locked_qty,0) + src.reserve_qty,
@@ -915,12 +930,10 @@ async function assignAdditionalInventory(
915
930
  ) as available_qty,
916
931
  sum(qty - locked_qty - release_qty - unassigned_qty) over (order by sort_seq asc rows between unbounded preceding and current row) as lock_amount
917
932
  from (
918
- SELECT 0 as sort_seq, null as id, null as pallet_id, null as batch_id, null as batch_id_ref,
919
- null as unit, '${uom}' as uom, '${orderProduct.packingType}' as packing_type, '${
920
- orderProduct.packingSize
921
- }' as packing_size,
933
+ SELECT 0 as sort_seq, null as id, null as pallet_id, null as batch_id, null as batch_id_ref,
934
+ null as unit, $${seedUomIdx} as uom, $${seedPackingTypeIdx} as packing_type, $${seedPackingSizeIdx} as packing_size,
922
935
  null as manufacture_year, null as carton_id, 0 as uom_value, 0 as locked_uom_value, 0 as qty, 0 as locked_qty, 0 as unassigned_uom_value, 0 as unassigned_qty, null as created_at,
923
- ${qtyDiff}::numeric as release_qty, ${uomValueDiff}::numeric as release_uom_value
936
+ $${seedReleaseQtyIdx}::numeric as release_qty, $${seedReleaseUomValueIdx}::numeric as release_uom_value
924
937
  UNION
925
938
  (
926
939
  SELECT ROW_NUMBER() OVER(PARTITION BY iv.domain_id ORDER BY wiar.rank ${
@@ -959,33 +972,49 @@ async function assignAdditionalInventory(
959
972
  ) dt1
960
973
  ) dt2 where case when "lock_amount" < 0 then "available_qty" else "available_qty" - "lock_amount" end > 0
961
974
  ) dt3 where sort_seq > 0
962
- ) src where src.id = tgt.id
963
- returning
964
- tgt."id", tgt."qty", tgt."pallet_id", tgt."carton_id", tgt."batch_id", tgt."batch_id_ref", tgt."unit",
965
- tgt."uom", tgt."packing_type", tgt."packing_size", tgt."manufacture_year",
975
+ ) src where src.id = tgt.id AND tgt.qty >= coalesce(tgt.locked_qty, 0) + src.reserve_qty
976
+ returning
977
+ tgt."id", tgt."qty", tgt."pallet_id", tgt."carton_id", tgt."batch_id", tgt."batch_id_ref", tgt."unit",
978
+ tgt."uom", tgt."packing_type", tgt."packing_size", tgt."manufacture_year",
966
979
  tgt."locked_qty", tgt."uom_value", tgt."locked_uom_value", src."reserve_qty", src."reserve_uom_value";`
967
980
 
968
- let updatedInventories = await tx.getRepository(Inventory).query(query, params)
981
+ const MAX_ASSIGN_RETRIES = 3
982
+ const RETRY_DELAY_MS = 10
983
+ let updatedInventories
969
984
 
970
- let totalAssigned =
971
- updatedInventories[0]?.reduce((acc: number, inv: any) => {
972
- return acc + inv.reserve_qty
973
- }, 0) || 0
985
+ for (let attempt = 1; attempt <= MAX_ASSIGN_RETRIES; attempt++) {
986
+ await tx.query('SAVEPOINT assign_retry')
974
987
 
975
- // For non-decimal products, round the values before comparison
976
- let roundedQtyDiff = qtyDiff
977
- if (!product.isInventoryDecimal) {
978
- roundedQtyDiff = Math.round(qtyDiff)
979
- totalAssigned = Math.round(totalAssigned)
980
- } else {
981
- // For decimal products, round to 3 decimal places
982
- roundedQtyDiff = Math.round(qtyDiff * 1000) / 1000
983
- totalAssigned = Math.round(totalAssigned * 1000) / 1000
984
- }
988
+ updatedInventories = await tx.getRepository(Inventory).query(query, params)
989
+
990
+ let totalAssigned =
991
+ updatedInventories[0]?.reduce((acc: number, inv: any) => {
992
+ return acc + Number(inv.reserve_qty)
993
+ }, 0) || 0
994
+
995
+ // For non-decimal products, round the values before comparison
996
+ let roundedQtyDiff = qtyDiff
997
+ if (!product.isInventoryDecimal) {
998
+ roundedQtyDiff = Math.round(qtyDiff)
999
+ totalAssigned = Math.round(totalAssigned)
1000
+ } else {
1001
+ // For decimal products, round to 3 decimal places
1002
+ roundedQtyDiff = Math.round(qtyDiff * 1000) / 1000
1003
+ totalAssigned = Math.round(totalAssigned * 1000) / 1000
1004
+ }
1005
+
1006
+ if (Math.abs(roundedQtyDiff - totalAssigned) > 0.001) {
1007
+ await tx.query('ROLLBACK TO SAVEPOINT assign_retry')
1008
+ if (attempt < MAX_ASSIGN_RETRIES) {
1009
+ await new Promise(resolve => setTimeout(resolve, RETRY_DELAY_MS))
1010
+ continue
1011
+ }
1012
+ // Using small epsilon for float comparison
1013
+ throw new ApiError('E01', 'INSUFFICIENT_STOCK')
1014
+ }
985
1015
 
986
- if (Math.abs(roundedQtyDiff - totalAssigned) > 0.001) {
987
- // Using small epsilon for float comparison
988
- throw new ApiError('E01', 'INSUFFICIENT_STOCK')
1016
+ await tx.query('RELEASE SAVEPOINT assign_retry')
1017
+ break
989
1018
  }
990
1019
 
991
1020
  // Create order inventory records for each assigned inventory
@@ -1225,9 +1254,11 @@ async function addNewOrderProduct(
1225
1254
  .createQueryBuilder()
1226
1255
  .update(ProductDetailStock)
1227
1256
  .set({
1228
- unassignedQty: () => `"unassigned_qty" + ${roundedReleaseQty}`,
1229
- unassignedUomValue: () => `"unassigned_uom_value" + ${releaseUomValue}`
1257
+ unassignedQty: () => `"unassigned_qty" + :releaseQty::numeric`,
1258
+ unassignedUomValue: () => `"unassigned_uom_value" + :releaseUomValue::numeric`
1230
1259
  })
1260
+ .setParameter('releaseQty', roundedReleaseQty)
1261
+ .setParameter('releaseUomValue', releaseUomValue)
1231
1262
  .where({ productDetail: productDetail.id })
1232
1263
  .execute()
1233
1264
  }
@@ -1281,6 +1281,7 @@ async function assignToInventory(orderProducts: OrderProduct[], customerBizplace
1281
1281
 
1282
1282
  let queryFilters = []
1283
1283
 
1284
+ // TODO: Fix reducer bugs — typo itm.filtes -> itm.filters, duplicate acc.query.push, missing return acc
1284
1285
  let queryStrings = queryFilters.reduce(
1285
1286
  (acc, itm, idx, arr) => {
1286
1287
  acc.values.push(itm.filters)
@@ -1332,7 +1333,9 @@ async function assignToInventory(orderProducts: OrderProduct[], customerBizplace
1332
1333
  qty - locked_qty as available_qty,
1333
1334
  sum(qty - locked_qty - release_qty) over (order by sort_seq asc rows between unbounded preceding and current row) as lock_amount
1334
1335
  from (
1335
- SELECT 0 as sort_seq, null as id, null as pallet_id, null as batch_id, null as batch_id_ref,
1336
+ // TODO: Replace template literal interpolation with $N positional parameters (see unstable/add-release-order.ts for pattern)
1337
+ // String values (uom, packingType, packingSize) are SQL injection vectors; numeric values need ::numeric casts
1338
+ SELECT 0 as sort_seq, null as id, null as pallet_id, null as batch_id, null as batch_id_ref,
1336
1339
  null as unit, '${orderInventory.uom}' as uom, '${orderInventory.packingType}' as packing_type, '${
1337
1340
  orderInventory.packingSize
1338
1341
  }' as packing_size,
@@ -1352,12 +1355,14 @@ async function assignToInventory(orderProducts: OrderProduct[], customerBizplace
1352
1355
  WHERE case when coalesce("iv"."locked_qty",0) < 0 then 0 else ("iv"."qty" - coalesce("iv"."locked_qty",0)) end > 0 AND
1353
1356
  "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)
1354
1357
  AND "iv"."obsolete" = false AND case when "iv"."expiration_date" is not null and "p"."min_outbound_shelf_life" is not null then CURRENT_DATE < "iv"."expiration_date" - "p"."min_outbound_shelf_life" else true end
1355
- ${queryStrings.query.length > 0 ? `AND ${queryStrings.join(' AND ')}` : ''}
1358
+ ${queryStrings.query.length > 0 ? `AND ${queryStrings.query.join(' AND ')}` : ''}
1356
1359
  ORDER BY ${sortQuery}
1357
1360
  )
1358
1361
  ) dt1
1359
1362
  ) dt2 where case when "lock_amount" < 0 then "available_qty" else "available_qty" - "lock_amount" end > 0
1360
1363
  ) dt3 where sort_seq > 0
1364
+ // TODO: Add over-assignment guard: AND tgt.qty >= coalesce(tgt.locked_qty, 0) + src.reserve_qty
1365
+ // TODO: Add SAVEPOINT retry logic with Number() cast on reserve_qty (see v1/add-release-order.ts for pattern)
1361
1366
  ) src where src.id = tgt.id
1362
1367
  returning
1363
1368
  tgt."id", tgt."qty", tgt."pallet_id", tgt."carton_id", tgt."batch_id", tgt."batch_id_ref", tgt."unit",