@things-factory/operato-hub 4.3.822 → 4.3.824
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.
- package/config.development.js +89 -45
- package/dist-server/graphql/resolvers/email/send-emails.js +5 -1
- package/dist-server/graphql/resolvers/email/send-emails.js.map +1 -1
- package/dist-server/graphql/resolvers/synchronization/powrup-get-airway-bill.js +103 -53
- package/dist-server/graphql/resolvers/synchronization/powrup-get-airway-bill.js.map +1 -1
- package/dist-server/graphql/resolvers/synchronization/powrup-get-tracking-no.js +80 -39
- package/dist-server/graphql/resolvers/synchronization/powrup-get-tracking-no.js.map +1 -1
- package/dist-server/graphql/resolvers/synchronization/powrup-initiate-shipment.js +95 -65
- package/dist-server/graphql/resolvers/synchronization/powrup-initiate-shipment.js.map +1 -1
- package/dist-server/graphql/resolvers/synchronization/sellercraft-initiate-shipment.js +109 -74
- package/dist-server/graphql/resolvers/synchronization/sellercraft-initiate-shipment.js.map +1 -1
- package/dist-server/graphql/types/synchronization/index.js +7 -4
- package/dist-server/graphql/types/synchronization/index.js.map +1 -1
- package/dist-server/routers/api/restful-apis/v1/utils/params.js +1 -0
- package/dist-server/routers/api/restful-apis/v1/utils/params.js.map +1 -1
- package/dist-server/routers/api/restful-apis/v1/warehouse/add-release-order.js +3 -2
- package/dist-server/routers/api/restful-apis/v1/warehouse/add-release-order.js.map +1 -1
- package/dist-server/routers/xilnex-router.js +92 -7
- package/dist-server/routers/xilnex-router.js.map +1 -1
- package/package.json +20 -20
- package/server/graphql/resolvers/email/send-emails.ts +5 -1
- package/server/graphql/resolvers/synchronization/powrup-get-airway-bill.ts +119 -66
- package/server/graphql/resolvers/synchronization/powrup-get-tracking-no.ts +81 -41
- package/server/graphql/resolvers/synchronization/powrup-initiate-shipment.ts +117 -79
- package/server/graphql/resolvers/synchronization/sellercraft-initiate-shipment.ts +132 -89
- package/server/graphql/types/synchronization/index.ts +7 -4
- package/server/routers/api/restful-apis/v1/utils/params.ts +1 -0
- package/server/routers/api/restful-apis/v1/warehouse/add-release-order.ts +14 -2
- package/server/routers/xilnex-router.ts +165 -37
|
@@ -1,116 +1,159 @@
|
|
|
1
|
-
import { getConnection
|
|
1
|
+
import { getConnection } from 'typeorm'
|
|
2
2
|
|
|
3
3
|
import { ApplicationType } from '@things-factory/auth-base'
|
|
4
4
|
import { logger } from '@things-factory/env'
|
|
5
5
|
import { Sellercraft, SellercraftAPI } from '@things-factory/integration-sellercraft'
|
|
6
6
|
import { ORDER_STATUS, OrderPackage } from '@things-factory/sales-base'
|
|
7
7
|
|
|
8
|
+
const ADVISORY_LOCK_KEY = 'sellercraft-initiate-shipment'
|
|
9
|
+
|
|
8
10
|
export const sellercraftInitiateShipmentResolver = {
|
|
9
11
|
async sellercraftInitiateShipment(_: void, {}, context: any): Promise<void> {
|
|
10
12
|
const maxRTSRetry: number = 10
|
|
13
|
+
|
|
14
|
+
// Same fix as powrupGetAirwayBill / powrupGetTrackingNo: do not wrap the
|
|
15
|
+
// resolver in a single long transaction across external SellercraftAPI HTTP
|
|
16
|
+
// calls. The previous version held row locks on every UPDATE'd order_packages
|
|
17
|
+
// row until the loop finished, which was the direct cause of the order_packages
|
|
18
|
+
// lock chains observed in pg_stat_activity (sessions stuck `idle in transaction`
|
|
19
|
+
// for 10+ minutes after `UPDATE order_packages SET market_rts_call_success`).
|
|
20
|
+
//
|
|
21
|
+
// Use a single QueryRunner with a session-level advisory lock for concurrency
|
|
22
|
+
// safety, and let each UPDATE auto-commit on its own (row lock held only for
|
|
23
|
+
// the duration of that single statement).
|
|
24
|
+
const queryRunner = getConnection().createQueryRunner()
|
|
25
|
+
let connected = false
|
|
26
|
+
|
|
11
27
|
try {
|
|
12
|
-
await
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
28
|
+
await queryRunner.connect()
|
|
29
|
+
connected = true
|
|
30
|
+
|
|
31
|
+
const lockResult: { acquired: boolean }[] = await queryRunner.query(
|
|
32
|
+
`SELECT pg_try_advisory_lock(hashtextextended($1, 0)) AS acquired`,
|
|
33
|
+
[ADVISORY_LOCK_KEY]
|
|
34
|
+
)
|
|
35
|
+
if (!lockResult?.[0]?.acquired) {
|
|
36
|
+
logger.info(`sellercraftInitiateShipment: another instance is running, skipping`)
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const manager = queryRunner.manager
|
|
42
|
+
|
|
43
|
+
const orderPackagesWithoutRTS: OrderPackage[] = await manager
|
|
44
|
+
.createQueryBuilder(OrderPackage, 'opk')
|
|
45
|
+
.innerJoinAndSelect('opk.releaseGood', 'rg')
|
|
46
|
+
.innerJoinAndSelect('opk.domain', 'domain')
|
|
47
|
+
.innerJoinAndSelect('opk.bizplace', 'bizplace')
|
|
48
|
+
.innerJoinAndSelect('bizplace.domain', 'bdomain')
|
|
49
|
+
.leftJoinAndSelect('opk.creator', 'creator')
|
|
50
|
+
.leftJoinAndSelect('rg.lastMileDelivery', 'lmd')
|
|
51
|
+
.where(`rg.source = :source`, { source: ApplicationType.SELLERCRAFT })
|
|
52
|
+
.andWhere(`rg.type = :rgType`, { rgType: 'b2c' })
|
|
53
|
+
.andWhere(`rg.status = :status`, { status: ORDER_STATUS.DONE })
|
|
54
|
+
.andWhere(`opk.market_rts_call_success ISNULL`)
|
|
55
|
+
.andWhere(`COALESCE(opk.market_rts_call_retry, 0) <= :maxRTSRetry`, { maxRTSRetry })
|
|
56
|
+
.orderBy(`opk.created_at`)
|
|
57
|
+
.getMany()
|
|
58
|
+
|
|
59
|
+
if (orderPackagesWithoutRTS?.length > 0) {
|
|
60
|
+
for (let i = 0; i < orderPackagesWithoutRTS.length; i++) {
|
|
61
|
+
const orderPackage: OrderPackage = orderPackagesWithoutRTS[i]
|
|
62
|
+
const releaseGood = orderPackage.releaseGood
|
|
63
|
+
|
|
64
|
+
const sellercraft: Sellercraft = await manager.findOne(Sellercraft, {
|
|
65
|
+
where: { domain: { id: orderPackage.bizplace.domain.id }, status: 'ACTIVE' }
|
|
27
66
|
})
|
|
28
|
-
.andWhere(`opk.market_rts_call_success ISNULL`)
|
|
29
|
-
.andWhere(`COALESCE(opk.market_rts_call_retry, 0) <= :maxRTSRetry`, { maxRTSRetry: maxRTSRetry })
|
|
30
|
-
.orderBy(`opk.created_at`)
|
|
31
67
|
|
|
32
|
-
|
|
68
|
+
if (!sellercraft) {
|
|
69
|
+
continue
|
|
70
|
+
}
|
|
33
71
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
72
|
+
let sellercraftSetting: any = sellercraft.sellercraftSetting
|
|
73
|
+
let disableInitiateOrderShipment: boolean =
|
|
74
|
+
sellercraftSetting?.disableInitiateOrderShipment &&
|
|
75
|
+
sellercraftSetting?.disableInitiateOrderShipment == 1
|
|
76
|
+
? true
|
|
77
|
+
: false
|
|
38
78
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
79
|
+
if (
|
|
80
|
+
disableInitiateOrderShipment === true ||
|
|
81
|
+
(releaseGood.lmdOption == true && orderPackage?.trackingNo == null)
|
|
82
|
+
) {
|
|
83
|
+
continue
|
|
84
|
+
}
|
|
42
85
|
|
|
43
|
-
|
|
44
|
-
|
|
86
|
+
let orderInformation: any = {
|
|
87
|
+
accountId: sellercraft.accountId,
|
|
88
|
+
orderId: releaseGood.refNo2,
|
|
89
|
+
packageId: orderPackage.packageId
|
|
90
|
+
}
|
|
91
|
+
if (orderPackage?.releaseGood?.lmdOption) {
|
|
92
|
+
orderInformation = {
|
|
93
|
+
...orderInformation,
|
|
94
|
+
awb: orderPackage.airwayBill,
|
|
95
|
+
trackingNo: orderPackage.trackingNo,
|
|
96
|
+
lmdOption: orderPackage?.releaseGood?.lmdOption,
|
|
97
|
+
shipperName: orderPackage?.releaseGood?.lastMileDelivery?.platform
|
|
45
98
|
}
|
|
99
|
+
}
|
|
46
100
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
if (
|
|
55
|
-
disableInitiateOrderShipment === true ||
|
|
56
|
-
(releaseGood.lmdOption == true && orderPackage?.trackingNo == null)
|
|
57
|
-
) {
|
|
58
|
-
continue
|
|
59
|
-
}
|
|
101
|
+
performance.mark(`start:${orderPackage.releaseGood.refNo2} - ${orderPackage.packageId}`)
|
|
102
|
+
try {
|
|
103
|
+
await SellercraftAPI.initiateOrderShipment(sellercraft, {
|
|
104
|
+
...orderInformation,
|
|
105
|
+
context: { state: { domain: null, user: null } }
|
|
106
|
+
})
|
|
60
107
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
packageId: orderPackage.packageId
|
|
65
|
-
}
|
|
66
|
-
if (orderPackage?.releaseGood?.lmdOption) {
|
|
67
|
-
orderInformation = {
|
|
68
|
-
...orderInformation,
|
|
69
|
-
awb: orderPackage.airwayBill,
|
|
70
|
-
trackingNo: orderPackage.trackingNo,
|
|
71
|
-
lmdOption: orderPackage?.releaseGood?.lmdOption,
|
|
72
|
-
shipperName: orderPackage?.releaseGood?.lastMileDelivery?.platform
|
|
73
|
-
}
|
|
74
|
-
}
|
|
108
|
+
// Auto-commit single-statement UPDATE — row lock held only for this UPDATE,
|
|
109
|
+
// not across the API call above.
|
|
110
|
+
await manager.update(OrderPackage, { id: orderPackage.id }, { marketRtsCallSuccess: new Date() })
|
|
75
111
|
|
|
76
|
-
performance.mark(`
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
} finally {
|
|
99
|
-
let perf = performance.measure(
|
|
100
|
-
`sellercraftInitiateShipment: ${orderPackage.releaseGood.refNo2} - ${orderPackage.packageId}`,
|
|
101
|
-
`start:${orderPackage.releaseGood.refNo2} - ${orderPackage.packageId}`,
|
|
102
|
-
`end:${orderPackage.releaseGood.refNo2} - ${orderPackage.packageId}`
|
|
103
|
-
)
|
|
104
|
-
console.info(perf.toJSON())
|
|
105
|
-
}
|
|
112
|
+
performance.mark(`end:${orderPackage.releaseGood.refNo2} - ${orderPackage.packageId}`)
|
|
113
|
+
} catch (error) {
|
|
114
|
+
performance.mark(`end:${orderPackage.releaseGood.refNo2} - ${orderPackage.packageId}`)
|
|
115
|
+
logger.error(
|
|
116
|
+
`sellercraftInitiateShipment[InitiateShipment][Order Package ID: ${orderPackage.id}]: ${error}`
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
await manager
|
|
120
|
+
.createQueryBuilder()
|
|
121
|
+
.update(OrderPackage)
|
|
122
|
+
.set({ marketRtsCallRetry: () => `coalesce("market_rts_call_retry", 0) + 1` })
|
|
123
|
+
.where('id = :id', { id: orderPackage.id })
|
|
124
|
+
.execute()
|
|
125
|
+
|
|
126
|
+
continue
|
|
127
|
+
} finally {
|
|
128
|
+
let perf = performance.measure(
|
|
129
|
+
`sellercraftInitiateShipment: ${orderPackage.releaseGood.refNo2} - ${orderPackage.packageId}`,
|
|
130
|
+
`start:${orderPackage.releaseGood.refNo2} - ${orderPackage.packageId}`,
|
|
131
|
+
`end:${orderPackage.releaseGood.refNo2} - ${orderPackage.packageId}`
|
|
132
|
+
)
|
|
133
|
+
console.info(perf.toJSON())
|
|
106
134
|
}
|
|
107
135
|
}
|
|
108
|
-
} catch (e) {
|
|
109
|
-
logger.error(`sellercraftInitiateShipment[InitiateShipment]: ${e}`)
|
|
110
136
|
}
|
|
111
|
-
})
|
|
137
|
+
} catch (e) {
|
|
138
|
+
logger.error(`sellercraftInitiateShipment[InitiateShipment]: ${e}`)
|
|
139
|
+
} finally {
|
|
140
|
+
try {
|
|
141
|
+
await queryRunner.query(`SELECT pg_advisory_unlock(hashtextextended($1, 0))`, [ADVISORY_LOCK_KEY])
|
|
142
|
+
} catch (releaseErr) {
|
|
143
|
+
logger.error(`sellercraftInitiateShipment[AdvisoryUnlock]: ${releaseErr}`)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
112
146
|
} catch (e) {
|
|
113
147
|
logger.error(`sellercraftInitiateShipment[MainError]: ${e}`)
|
|
148
|
+
} finally {
|
|
149
|
+
if (connected) {
|
|
150
|
+
try {
|
|
151
|
+
await queryRunner.query(`SELECT pg_advisory_unlock_all()`)
|
|
152
|
+
} catch (unlockAllErr) {
|
|
153
|
+
logger.error(`sellercraftInitiateShipment[AdvisoryUnlockAll]: ${unlockAllErr}`)
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
await queryRunner.release()
|
|
114
157
|
}
|
|
115
158
|
}
|
|
116
159
|
}
|
|
@@ -19,11 +19,14 @@ export const Mutation = /* GraphQL */ `
|
|
|
19
19
|
syncAllProductsToSellercraft: Boolean @transaction
|
|
20
20
|
syncAllProductsToPowrup: Boolean @transaction
|
|
21
21
|
sellercraftPackMarketplaceOrder: Boolean @transaction
|
|
22
|
-
sellercraftInitiateShipment: Boolean @transaction
|
|
23
22
|
powrupPackOrder: Boolean @transaction
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
# Intentionally NOT @transaction — these resolvers manage their own QueryRunner
|
|
24
|
+
# to avoid holding row locks on order_packages across external API calls.
|
|
25
|
+
# See the resolver files for the rationale.
|
|
26
|
+
powrupGetTrackingNo: Boolean
|
|
27
|
+
powrupGetAirwayBill: Boolean
|
|
28
|
+
powrupInitiateShipment: Boolean
|
|
29
|
+
sellercraftInitiateShipment: Boolean
|
|
27
30
|
getLmdAwb: Boolean @transaction
|
|
28
31
|
createDatShipments: Boolean @transaction
|
|
29
32
|
autoUpdateAllMarketplaceProductVariationQuantity(companyDomainId: String, warehouseId: String): Boolean @transaction
|
|
@@ -929,7 +929,18 @@ async function massageOrderItems(
|
|
|
929
929
|
inputOrderProducts = await inputOrderProducts.reduce(async (accPromise, itm, idx) => {
|
|
930
930
|
const acc = await accPromise
|
|
931
931
|
let { sku, refCode } = itm.product
|
|
932
|
-
let {
|
|
932
|
+
let {
|
|
933
|
+
packingType,
|
|
934
|
+
packingSize,
|
|
935
|
+
batchId,
|
|
936
|
+
releaseQty,
|
|
937
|
+
sellingPrice,
|
|
938
|
+
paidAmount,
|
|
939
|
+
uom,
|
|
940
|
+
refItemId,
|
|
941
|
+
warehouseCode,
|
|
942
|
+
remark
|
|
943
|
+
} = itm
|
|
933
944
|
|
|
934
945
|
if (!sku && !refCode) {
|
|
935
946
|
throw new ApiError('E01', 'sku or refCode not found')
|
|
@@ -965,7 +976,8 @@ async function massageOrderItems(
|
|
|
965
976
|
uom: uom,
|
|
966
977
|
batchId: batchId?.trim() || '',
|
|
967
978
|
refItemId: refItemId ?? '',
|
|
968
|
-
warehouseCode: normalizedWarehouseCode
|
|
979
|
+
warehouseCode: normalizedWarehouseCode,
|
|
980
|
+
remark: remark || null
|
|
969
981
|
})
|
|
970
982
|
}
|
|
971
983
|
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
AccountWebhookOrder,
|
|
13
13
|
Xilnex
|
|
14
14
|
} from '@things-factory/integration-accounting'
|
|
15
|
+
import { webhookHandler, WebhookEventsEnum } from '@things-factory/integration-base'
|
|
15
16
|
// import { createPayloadLog, PayloadType } from '@things-factory/integration-base'
|
|
16
17
|
import { Product, ProductBundle, ProductDetail } from '@things-factory/product-base'
|
|
17
18
|
import {
|
|
@@ -545,27 +546,41 @@ xilnexRouter.post('/webhook-xilnex-transfer-note', async (context, next) => {
|
|
|
545
546
|
})
|
|
546
547
|
)
|
|
547
548
|
|
|
549
|
+
let result
|
|
548
550
|
if (errorList.length > 0) {
|
|
549
|
-
|
|
551
|
+
// Insufficient stock: persist the order as a DraftReleaseGood so it is
|
|
552
|
+
// not lost. Once stock arrives, the draft can be released via the
|
|
553
|
+
// existing draft-to-release flow.
|
|
554
|
+
logger.warn(
|
|
555
|
+
`xilnex-router[${releaseGood.orderSourceType}][insufficient-stock]: refNo=${releaseGood.refNo} creating DraftReleaseGood; details=${errorList.join(', ')}`
|
|
556
|
+
)
|
|
557
|
+
result = await createDraftReleaseGood(
|
|
558
|
+
releaseGood,
|
|
559
|
+
massagedData.orderProducts,
|
|
560
|
+
customerBizplace,
|
|
561
|
+
warehouseDomain,
|
|
562
|
+
context,
|
|
563
|
+
tx
|
|
564
|
+
)
|
|
565
|
+
} else {
|
|
566
|
+
const assignedOrderProducts = await assignToInventory(
|
|
567
|
+
massagedData.orderProducts,
|
|
568
|
+
customerBizplace,
|
|
569
|
+
warehouseDomain,
|
|
570
|
+
context,
|
|
571
|
+
tx
|
|
572
|
+
)
|
|
573
|
+
|
|
574
|
+
result = await createReleaseGood(
|
|
575
|
+
releaseGood,
|
|
576
|
+
assignedOrderProducts,
|
|
577
|
+
customerBizplace,
|
|
578
|
+
warehouseDomain,
|
|
579
|
+
context,
|
|
580
|
+
tx
|
|
581
|
+
)
|
|
550
582
|
}
|
|
551
583
|
|
|
552
|
-
let assignedOrderProducts = await assignToInventory(
|
|
553
|
-
massagedData.orderProducts,
|
|
554
|
-
customerBizplace,
|
|
555
|
-
warehouseDomain,
|
|
556
|
-
context,
|
|
557
|
-
tx
|
|
558
|
-
)
|
|
559
|
-
|
|
560
|
-
let result = await createReleaseGood(
|
|
561
|
-
releaseGood,
|
|
562
|
-
assignedOrderProducts,
|
|
563
|
-
customerBizplace,
|
|
564
|
-
warehouseDomain,
|
|
565
|
-
context,
|
|
566
|
-
tx
|
|
567
|
-
)
|
|
568
|
-
|
|
569
584
|
let addReleaseGood = {
|
|
570
585
|
id: result.id,
|
|
571
586
|
name: result.name,
|
|
@@ -889,27 +904,41 @@ xilnexRouter.post('/webhook-xilnex-stock-request', async (context, next) => {
|
|
|
889
904
|
})
|
|
890
905
|
)
|
|
891
906
|
|
|
907
|
+
let result
|
|
892
908
|
if (errorList.length > 0) {
|
|
893
|
-
|
|
909
|
+
// Insufficient stock: persist the order as a DraftReleaseGood so it is
|
|
910
|
+
// not lost. Once stock arrives, the draft can be released via the
|
|
911
|
+
// existing draft-to-release flow.
|
|
912
|
+
logger.warn(
|
|
913
|
+
`xilnex-router[${releaseGood.orderSourceType}][insufficient-stock]: refNo=${releaseGood.refNo} creating DraftReleaseGood; details=${errorList.join(', ')}`
|
|
914
|
+
)
|
|
915
|
+
result = await createDraftReleaseGood(
|
|
916
|
+
releaseGood,
|
|
917
|
+
massagedData.orderProducts,
|
|
918
|
+
customerBizplace,
|
|
919
|
+
warehouseDomain,
|
|
920
|
+
context,
|
|
921
|
+
tx
|
|
922
|
+
)
|
|
923
|
+
} else {
|
|
924
|
+
const assignedOrderProducts = await assignToInventory(
|
|
925
|
+
massagedData.orderProducts,
|
|
926
|
+
customerBizplace,
|
|
927
|
+
warehouseDomain,
|
|
928
|
+
context,
|
|
929
|
+
tx
|
|
930
|
+
)
|
|
931
|
+
|
|
932
|
+
result = await createReleaseGood(
|
|
933
|
+
releaseGood,
|
|
934
|
+
assignedOrderProducts,
|
|
935
|
+
customerBizplace,
|
|
936
|
+
warehouseDomain,
|
|
937
|
+
context,
|
|
938
|
+
tx
|
|
939
|
+
)
|
|
894
940
|
}
|
|
895
941
|
|
|
896
|
-
let assignedOrderProducts = await assignToInventory(
|
|
897
|
-
massagedData.orderProducts,
|
|
898
|
-
customerBizplace,
|
|
899
|
-
warehouseDomain,
|
|
900
|
-
context,
|
|
901
|
-
tx
|
|
902
|
-
)
|
|
903
|
-
|
|
904
|
-
let result = await createReleaseGood(
|
|
905
|
-
releaseGood,
|
|
906
|
-
assignedOrderProducts,
|
|
907
|
-
customerBizplace,
|
|
908
|
-
warehouseDomain,
|
|
909
|
-
context,
|
|
910
|
-
tx
|
|
911
|
-
)
|
|
912
|
-
|
|
913
942
|
let addReleaseGood = {
|
|
914
943
|
id: result.id,
|
|
915
944
|
name: result.name,
|
|
@@ -1072,12 +1101,21 @@ xilnexRouter.post('/webhook-xilnex-stock-request', async (context, next) => {
|
|
|
1072
1101
|
})
|
|
1073
1102
|
|
|
1074
1103
|
async function checkExistingOrder(releaseGood, bizplace, checkDuplicationValidation = true, context, tx) {
|
|
1075
|
-
|
|
1104
|
+
// Use the warehouse domain set on the releaseGood (caller passes warehouseDomain explicitly)
|
|
1105
|
+
// so dedupe is consistent with the domain that ReleaseGood / DraftReleaseGood are saved under.
|
|
1106
|
+
const domain: Domain = releaseGood.domain
|
|
1076
1107
|
const refNo: string = releaseGood.refNo
|
|
1077
1108
|
const refNo2: string = releaseGood.refNo2
|
|
1078
1109
|
const refNo3: string = releaseGood.refNo3
|
|
1079
1110
|
|
|
1080
1111
|
if (checkDuplicationValidation) {
|
|
1112
|
+
// Acquire a transaction-scoped advisory lock keyed on the dedupe tuple. Serializes
|
|
1113
|
+
// concurrent webhook deliveries for the same (domain, refNo, refNo2, refNo3) so the
|
|
1114
|
+
// SELECT-then-INSERT pattern below can't race two duplicates through. The lock is
|
|
1115
|
+
// automatically released when the transaction commits or rolls back.
|
|
1116
|
+
const lockKey = `xilnex-dedupe:${domain.id}:${refNo || ''}:${refNo2 || ''}:${refNo3 || ''}`
|
|
1117
|
+
await tx.query(`SELECT pg_advisory_xact_lock(hashtextextended($1, 0))`, [lockKey])
|
|
1118
|
+
|
|
1081
1119
|
const foundDraftReleaseGood: DraftReleaseGood = await tx.getRepository(DraftReleaseGood).findOne({
|
|
1082
1120
|
where: {
|
|
1083
1121
|
domain,
|
|
@@ -1770,3 +1808,93 @@ async function createReleaseGood(
|
|
|
1770
1808
|
throw error
|
|
1771
1809
|
}
|
|
1772
1810
|
}
|
|
1811
|
+
|
|
1812
|
+
// Inline draft creation that mirrors createReleaseGood's domain handling: takes warehouseDomain
|
|
1813
|
+
// explicitly instead of relying on context.state.domain. A simplified parallel of
|
|
1814
|
+
// createDraftReleaseGoodFunction in @things-factory/sales-base — kept tight (no LMD / shipping
|
|
1815
|
+
// order / attachments / country normalization) because none apply to the Xilnex webhook path.
|
|
1816
|
+
async function createDraftReleaseGood(
|
|
1817
|
+
releaseGood: any,
|
|
1818
|
+
orderProducts: OrderProduct[],
|
|
1819
|
+
bizplace: Bizplace,
|
|
1820
|
+
domain: Domain,
|
|
1821
|
+
context: any,
|
|
1822
|
+
tx: EntityManager
|
|
1823
|
+
): Promise<DraftReleaseGood> {
|
|
1824
|
+
const { user }: { user: any } = context.state
|
|
1825
|
+
|
|
1826
|
+
if (orderProducts.length <= 0) {
|
|
1827
|
+
throw new ValidationError({
|
|
1828
|
+
errorCode: 'ITEM_NOT_FOUND',
|
|
1829
|
+
message: 'no order items found',
|
|
1830
|
+
detail: JSON.stringify(releaseGood)
|
|
1831
|
+
})
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
// Resolve DRO number under the warehouse domain
|
|
1835
|
+
const droNoSetting: Setting = await tx.getRepository(Setting).findOne({
|
|
1836
|
+
where: { domain, name: ORDER_NUMBER_SETTING_KEY.DRO_NUMBER_RULE }
|
|
1837
|
+
})
|
|
1838
|
+
|
|
1839
|
+
const orderNo: string = droNoSetting
|
|
1840
|
+
? await generateId({ domain, type: ORDER_NUMBER_RULE_TYPE.DRO_NUMBER, seed: {} })
|
|
1841
|
+
: OrderNoGenerator.draftReleaseGood()
|
|
1842
|
+
|
|
1843
|
+
const newDraftReleaseGood = {
|
|
1844
|
+
name: orderNo,
|
|
1845
|
+
domain,
|
|
1846
|
+
bizplace,
|
|
1847
|
+
refNo: releaseGood.refNo,
|
|
1848
|
+
refNo2: releaseGood.refNo2 || null,
|
|
1849
|
+
refNo3: releaseGood.refNo3 || null,
|
|
1850
|
+
collectionOrderNo: releaseGood.collectionOrderNo,
|
|
1851
|
+
type: releaseGood?.type ? releaseGood.type : 'b2b',
|
|
1852
|
+
courierOption: releaseGood.courierOption,
|
|
1853
|
+
exportOption: releaseGood.exportOption,
|
|
1854
|
+
ownTransport: releaseGood.ownTransport,
|
|
1855
|
+
packingOption: releaseGood.packingOption,
|
|
1856
|
+
marketplaceOrderStatus: releaseGood?.marketplaceOrderStatus || null,
|
|
1857
|
+
releaseDate: releaseGood.releaseDate,
|
|
1858
|
+
remark: releaseGood?.remark || null,
|
|
1859
|
+
source: releaseGood?.source,
|
|
1860
|
+
status: DRAFT_RELEASE_ORDER_STATUS.DRAFT,
|
|
1861
|
+
creator: user,
|
|
1862
|
+
updater: user
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
const resultDraftReleaseGood: DraftReleaseGood = await tx.getRepository(DraftReleaseGood).save(newDraftReleaseGood)
|
|
1866
|
+
|
|
1867
|
+
// Persist OrderProduct rows linked to the draft. Mirrors createReleaseGood's
|
|
1868
|
+
// OrderProduct save loop, swapping releaseGood for draftReleaseGood and skipping
|
|
1869
|
+
// OrderInventory creation (drafts have no assigned inventory yet).
|
|
1870
|
+
const savedOrderProducts = await Promise.all(
|
|
1871
|
+
orderProducts.map(async orderProduct => {
|
|
1872
|
+
// status MUST be 'DRAFT' — draft-release-good-create.ts:130 filters
|
|
1873
|
+
// orderProducts on `op.status == 'DRAFT'` during draft → release conversion.
|
|
1874
|
+
// Setting any other ORDER_PRODUCT_STATUS value here would cause the converted
|
|
1875
|
+
// ReleaseGood to come out with zero line items.
|
|
1876
|
+
const draftOrderProduct = Object.assign(new OrderProduct(), {
|
|
1877
|
+
...orderProduct,
|
|
1878
|
+
name: OrderNoGenerator.orderProduct(),
|
|
1879
|
+
domain,
|
|
1880
|
+
bizplace,
|
|
1881
|
+
draftReleaseGood: resultDraftReleaseGood,
|
|
1882
|
+
type: ORDER_TYPES.RELEASE_OF_GOODS,
|
|
1883
|
+
status: DRAFT_RELEASE_ORDER_STATUS.DRAFT,
|
|
1884
|
+
creator: user,
|
|
1885
|
+
updater: user
|
|
1886
|
+
})
|
|
1887
|
+
return await tx.getRepository(OrderProduct).save(draftOrderProduct)
|
|
1888
|
+
})
|
|
1889
|
+
)
|
|
1890
|
+
|
|
1891
|
+
resultDraftReleaseGood.orderProducts = savedOrderProducts
|
|
1892
|
+
|
|
1893
|
+
// Notify external subscribers that a draft has been created. Fire-and-forget,
|
|
1894
|
+
// matching the pattern used at add-release-order.ts:557 for ReleaseOrderCreated.
|
|
1895
|
+
// Note: this fires inside the outer transaction; if the transaction rolls back
|
|
1896
|
+
// the webhook will still have been queued. Consumers should be idempotent.
|
|
1897
|
+
webhookHandler(resultDraftReleaseGood, bizplace, WebhookEventsEnum.DraftOrderCreated)
|
|
1898
|
+
|
|
1899
|
+
return resultDraftReleaseGood
|
|
1900
|
+
}
|