@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.
Files changed (29) hide show
  1. package/config.development.js +89 -45
  2. package/dist-server/graphql/resolvers/email/send-emails.js +5 -1
  3. package/dist-server/graphql/resolvers/email/send-emails.js.map +1 -1
  4. package/dist-server/graphql/resolvers/synchronization/powrup-get-airway-bill.js +103 -53
  5. package/dist-server/graphql/resolvers/synchronization/powrup-get-airway-bill.js.map +1 -1
  6. package/dist-server/graphql/resolvers/synchronization/powrup-get-tracking-no.js +80 -39
  7. package/dist-server/graphql/resolvers/synchronization/powrup-get-tracking-no.js.map +1 -1
  8. package/dist-server/graphql/resolvers/synchronization/powrup-initiate-shipment.js +95 -65
  9. package/dist-server/graphql/resolvers/synchronization/powrup-initiate-shipment.js.map +1 -1
  10. package/dist-server/graphql/resolvers/synchronization/sellercraft-initiate-shipment.js +109 -74
  11. package/dist-server/graphql/resolvers/synchronization/sellercraft-initiate-shipment.js.map +1 -1
  12. package/dist-server/graphql/types/synchronization/index.js +7 -4
  13. package/dist-server/graphql/types/synchronization/index.js.map +1 -1
  14. package/dist-server/routers/api/restful-apis/v1/utils/params.js +1 -0
  15. package/dist-server/routers/api/restful-apis/v1/utils/params.js.map +1 -1
  16. package/dist-server/routers/api/restful-apis/v1/warehouse/add-release-order.js +3 -2
  17. package/dist-server/routers/api/restful-apis/v1/warehouse/add-release-order.js.map +1 -1
  18. package/dist-server/routers/xilnex-router.js +92 -7
  19. package/dist-server/routers/xilnex-router.js.map +1 -1
  20. package/package.json +20 -20
  21. package/server/graphql/resolvers/email/send-emails.ts +5 -1
  22. package/server/graphql/resolvers/synchronization/powrup-get-airway-bill.ts +119 -66
  23. package/server/graphql/resolvers/synchronization/powrup-get-tracking-no.ts +81 -41
  24. package/server/graphql/resolvers/synchronization/powrup-initiate-shipment.ts +117 -79
  25. package/server/graphql/resolvers/synchronization/sellercraft-initiate-shipment.ts +132 -89
  26. package/server/graphql/types/synchronization/index.ts +7 -4
  27. package/server/routers/api/restful-apis/v1/utils/params.ts +1 -0
  28. package/server/routers/api/restful-apis/v1/warehouse/add-release-order.ts +14 -2
  29. package/server/routers/xilnex-router.ts +165 -37
@@ -7,87 +7,140 @@ import { ORDER_STATUS, OrderPackage } from '@things-factory/sales-base'
7
7
 
8
8
  const { PerformanceObserver, performance } = require('node:perf_hooks')
9
9
 
10
+ // Stable key for pg_try_advisory_lock — keeps two concurrent invocations of this
11
+ // resolver from contending on the same order_packages rows.
12
+ const ADVISORY_LOCK_KEY = 'powrup-get-airway-bill'
13
+
10
14
  export const powrupGetAirwayBillResolver = {
11
15
  async powrupGetAirwayBill(_: void, { }, context: any): Promise<void> {
16
+ // We deliberately do NOT wrap the resolver in a transaction. The previous
17
+ // implementation held a single long transaction across up to 100 external
18
+ // PowrupAPI HTTP calls, keeping row locks on every UPDATE'd order_packages
19
+ // row until the loop finished — minutes of contention against the packing
20
+ // UI, sellercraft updates, and any other path that touches those rows.
21
+ //
22
+ // Instead we use a single QueryRunner connection for the duration of the
23
+ // resolver, acquire a session-level advisory lock so concurrent runs skip
24
+ // early, and let each UPDATE auto-commit on its own (row lock held only for
25
+ // the duration of that single statement).
26
+ const queryRunner = getConnection().createQueryRunner()
27
+ let connected = false
28
+
12
29
  try {
13
- await getConnection().transaction(async tx => {
14
- try {
15
- const powrup: Powrup = await tx
16
- .getRepository(Powrup)
17
- .findOne({ where: { platform: Platform.POWRUP, active: true } })
30
+ await queryRunner.connect()
31
+ connected = true
18
32
 
19
- let orderPackageQb = await tx
20
- .getRepository(OrderPackage)
21
- .createQueryBuilder('opk')
22
- .innerJoinAndSelect('opk.releaseGood', 'rg')
23
- .innerJoinAndSelect('opk.domain', 'domain')
24
- .innerJoinAndSelect('opk.bizplace', 'bizplace')
25
- .innerJoinAndSelect('opk.creator', 'creator')
26
- .where(`rg.source = :source`, { source: ApplicationType.POWRUP })
27
- .andWhere(`rg.type = :rgType`, { rgType: 'b2c' })
28
- .andWhere(`rg.ref_order_id NOTNULL`)
29
- .andWhere(`(rg.lmd_option = false or rg.lmd_option ISNULL)`)
30
- .andWhere(`rg.status NOT IN (:...status) `, {
31
- status: [ORDER_STATUS.PACKING, ORDER_STATUS.CANCELLED, ORDER_STATUS.PENDING_CANCEL, ORDER_STATUS.DONE]
32
- })
33
- .andWhere(`opk.airway_bill ISNULL`)
34
- .orderBy(`rg.created_at`)
35
- .limit(100)
33
+ const lockResult: { acquired: boolean }[] = await queryRunner.query(
34
+ `SELECT pg_try_advisory_lock(hashtextextended($1, 0)) AS acquired`,
35
+ [ADVISORY_LOCK_KEY]
36
+ )
37
+ if (!lockResult?.[0]?.acquired) {
38
+ logger.info(`powrupGetAirwayBill: another instance is running, skipping`)
39
+ return
40
+ }
36
41
 
37
- let orderPackagesWithoutAirwayBill: OrderPackage[] = await orderPackageQb.getMany()
42
+ try {
43
+ const manager = queryRunner.manager
38
44
 
39
- if (orderPackagesWithoutAirwayBill?.length > 0) {
40
- for (let i = 0; i < orderPackagesWithoutAirwayBill.length; i++) {
41
- const orderPackage: OrderPackage = orderPackagesWithoutAirwayBill[i]
42
- performance.mark(`start:${orderPackage.releaseGood.refOrderId} - ${orderPackage.packageId}`)
43
- try {
44
- const {
45
- trackingNo,
46
- transporter,
47
- printLabels
48
- }: { trackingNo: string; transporter: string; printLabels: any[] } = await PowrupAPI.fetchDocument(
49
- powrup,
50
- {
51
- context: { state: { domain: null, user: null } },
52
- refOrderId: orderPackage.releaseGood.refOrderId,
53
- packageId: orderPackage.packageId
54
- }
55
- )
56
- performance.mark(`end:${orderPackage.releaseGood.refOrderId} - ${orderPackage.packageId}`)
57
- let updateInfo: any
58
- if (printLabels?.length) {
59
- printLabels.map(pl => {
60
- updateInfo = { ...updateInfo, airwayBill: pl.awbUrl }
61
- })
45
+ const powrup: Powrup = await manager.findOne(Powrup, {
46
+ where: { platform: Platform.POWRUP, active: true }
47
+ })
62
48
 
63
- updateInfo = {
64
- ...updateInfo,
65
- trackingNo: trackingNo,
66
- transporter: transporter
67
- }
49
+ const orderPackagesWithoutAirwayBill: OrderPackage[] = await manager
50
+ .createQueryBuilder(OrderPackage, 'opk')
51
+ .innerJoinAndSelect('opk.releaseGood', 'rg')
52
+ .innerJoinAndSelect('opk.domain', 'domain')
53
+ .innerJoinAndSelect('opk.bizplace', 'bizplace')
54
+ .innerJoinAndSelect('opk.creator', 'creator')
55
+ .where(`rg.source = :source`, { source: ApplicationType.POWRUP })
56
+ .andWhere(`rg.type = :rgType`, { rgType: 'b2c' })
57
+ .andWhere(`rg.ref_order_id NOTNULL`)
58
+ .andWhere(`(rg.lmd_option = false or rg.lmd_option ISNULL)`)
59
+ .andWhere(`rg.status NOT IN (:...status) `, {
60
+ status: [ORDER_STATUS.PACKING, ORDER_STATUS.CANCELLED, ORDER_STATUS.PENDING_CANCEL, ORDER_STATUS.DONE]
61
+ })
62
+ .andWhere(`opk.airway_bill ISNULL`)
63
+ .orderBy(`rg.created_at`)
64
+ .limit(100)
65
+ .getMany()
68
66
 
69
- await tx.getRepository(OrderPackage).update({ id: orderPackage.id }, { ...updateInfo })
67
+ if (orderPackagesWithoutAirwayBill?.length > 0) {
68
+ for (let i = 0; i < orderPackagesWithoutAirwayBill.length; i++) {
69
+ const orderPackage: OrderPackage = orderPackagesWithoutAirwayBill[i]
70
+ performance.mark(`start:${orderPackage.releaseGood.refOrderId} - ${orderPackage.packageId}`)
71
+ try {
72
+ const {
73
+ trackingNo,
74
+ transporter,
75
+ printLabels
76
+ }: { trackingNo: string; transporter: string; printLabels: any[] } = await PowrupAPI.fetchDocument(
77
+ powrup,
78
+ {
79
+ context: { state: { domain: null, user: null } },
80
+ refOrderId: orderPackage.releaseGood.refOrderId,
81
+ packageId: orderPackage.packageId
70
82
  }
71
- } catch (e) {
72
- performance.mark(`end:${orderPackage.releaseGood.refOrderId} - ${orderPackage.packageId}`)
73
- logger.error(`powrupGetAirwayBill[GetAirwayBill][Order Package ID: ${orderPackage.id}]: ${e}`)
74
- continue
75
- } finally {
76
- let perf = performance.measure(
77
- `powrupGetAirwayBill: ${orderPackage.releaseGood.refOrderId} - ${orderPackage.packageId}`,
78
- `start:${orderPackage.releaseGood.refOrderId} - ${orderPackage.packageId}`,
79
- `end:${orderPackage.releaseGood.refOrderId} - ${orderPackage.packageId}`
80
- )
81
- console.info(perf.toJSON())
83
+ )
84
+ performance.mark(`end:${orderPackage.releaseGood.refOrderId} - ${orderPackage.packageId}`)
85
+
86
+ if (printLabels?.length) {
87
+ let updateInfo: any
88
+ printLabels.map(pl => {
89
+ updateInfo = { ...updateInfo, airwayBill: pl.awbUrl }
90
+ })
91
+
92
+ updateInfo = {
93
+ ...updateInfo,
94
+ trackingNo: trackingNo,
95
+ transporter: transporter
96
+ }
97
+
98
+ // Auto-commit single-statement UPDATE on the QueryRunner's connection.
99
+ // Row lock held only for this UPDATE, not across the API call above.
100
+ await manager.update(OrderPackage, { id: orderPackage.id }, updateInfo)
82
101
  }
102
+ } catch (e) {
103
+ performance.mark(`end:${orderPackage.releaseGood.refOrderId} - ${orderPackage.packageId}`)
104
+ logger.error(`powrupGetAirwayBill[GetAirwayBill][Order Package ID: ${orderPackage.id}]: ${e}`)
105
+ continue
106
+ } finally {
107
+ let perf = performance.measure(
108
+ `powrupGetAirwayBill: ${orderPackage.releaseGood.refOrderId} - ${orderPackage.packageId}`,
109
+ `start:${orderPackage.releaseGood.refOrderId} - ${orderPackage.packageId}`,
110
+ `end:${orderPackage.releaseGood.refOrderId} - ${orderPackage.packageId}`
111
+ )
112
+ console.info(perf.toJSON())
83
113
  }
84
114
  }
85
- } catch (e) {
86
- logger.error(`powrupGetAirwayBill[GetAirwayBill]: ${e}`)
87
115
  }
88
- })
116
+ } catch (e) {
117
+ logger.error(`powrupGetAirwayBill[GetAirwayBill]: ${e}`)
118
+ } finally {
119
+ // Release the advisory lock so the next scheduled invocation can proceed.
120
+ // Wrapped in try/catch because release must not mask the original error.
121
+ try {
122
+ await queryRunner.query(`SELECT pg_advisory_unlock(hashtextextended($1, 0))`, [ADVISORY_LOCK_KEY])
123
+ } catch (releaseErr) {
124
+ logger.error(`powrupGetAirwayBill[AdvisoryUnlock]: ${releaseErr}`)
125
+ }
126
+ }
89
127
  } catch (e) {
90
128
  logger.error(`powrupGetAirwayBill[MainError]: ${e}`)
129
+ } finally {
130
+ // Defensive: clear any session-level advisory locks still held on this
131
+ // connection before returning it to the pool. Guards against the lock
132
+ // leaking if pg_advisory_unlock above failed (DB blip, statement timeout)
133
+ // and the pool then hands this connection to a future invocation.
134
+ // Skip when connect() itself failed — querying an unconnected QueryRunner
135
+ // would just throw with nothing to actually clean up.
136
+ if (connected) {
137
+ try {
138
+ await queryRunner.query(`SELECT pg_advisory_unlock_all()`)
139
+ } catch (unlockAllErr) {
140
+ logger.error(`powrupGetAirwayBill[AdvisoryUnlockAll]: ${unlockAllErr}`)
141
+ }
142
+ }
143
+ await queryRunner.release()
91
144
  }
92
145
  }
93
146
  }
@@ -5,60 +5,100 @@ import { logger } from '@things-factory/env'
5
5
  import { Platform, Powrup, PowrupAPI } from '@things-factory/integration-powrup'
6
6
  import { ORDER_STATUS, OrderPackage } from '@things-factory/sales-base'
7
7
 
8
+ const ADVISORY_LOCK_KEY = 'powrup-get-tracking-no'
9
+
8
10
  export const powrupGetTrackingNoResolver = {
9
11
  async powrupGetTrackingNo(_: void, { }, context: any): Promise<void> {
12
+ // Same fix as powrupGetAirwayBill: avoid wrapping the resolver in a long
13
+ // transaction across an external API call. Use a single QueryRunner with a
14
+ // session-level advisory lock for concurrency safety, and let each UPDATE
15
+ // auto-commit on its own.
16
+ const queryRunner = getConnection().createQueryRunner()
17
+ let connected = false
18
+
10
19
  try {
11
- await getConnection().transaction(async tx => {
12
- try {
13
- const powrup: Powrup = await tx
14
- .getRepository(Powrup)
15
- .findOne({ where: { platform: Platform.POWRUP, active: true } })
20
+ await queryRunner.connect()
21
+ connected = true
22
+
23
+ const lockResult: { acquired: boolean }[] = await queryRunner.query(
24
+ `SELECT pg_try_advisory_lock(hashtextextended($1, 0)) AS acquired`,
25
+ [ADVISORY_LOCK_KEY]
26
+ )
27
+ if (!lockResult?.[0]?.acquired) {
28
+ logger.info(`powrupGetTrackingNo: another instance is running, skipping`)
29
+ return
30
+ }
16
31
 
17
- let orderPackageQb = await tx
18
- .getRepository(OrderPackage)
19
- .createQueryBuilder('opk')
20
- .innerJoinAndSelect('opk.releaseGood', 'rg')
21
- .innerJoinAndSelect('opk.domain', 'domain')
22
- .innerJoinAndSelect('opk.bizplace', 'bizplace')
23
- .innerJoinAndSelect('opk.creator', 'creator')
24
- .where(`rg.source = :source`, { source: ApplicationType.POWRUP })
25
- .andWhere(`rg.type = :rgType`, { rgType: 'b2c' })
26
- .andWhere(`rg.ref_order_id NOTNULL`)
27
- .andWhere(`rg.status NOT IN(:...status) `, {
28
- status: [ORDER_STATUS.CANCELLED, ORDER_STATUS.PENDING_CANCEL]
29
- })
30
- .andWhere(`opk.tracking_no IS NULL`)
31
- .andWhere(`opk.airway_bill NOTNULL`)
32
- .andWhere(`opk.package_id NOTNULL`)
33
- .limit(500)
32
+ try {
33
+ const manager = queryRunner.manager
34
34
 
35
- let orderPackagesWithoutTrackingNo: OrderPackage[] = await orderPackageQb.getMany()
35
+ const powrup: Powrup = await manager.findOne(Powrup, {
36
+ where: { platform: Platform.POWRUP, active: true }
37
+ })
36
38
 
37
- if (orderPackagesWithoutTrackingNo?.length > 0) {
38
- // Combine all package id into packageIds
39
- let orderPackageIds: any = orderPackagesWithoutTrackingNo.map(op => op.packageId)
39
+ const orderPackagesWithoutTrackingNo: OrderPackage[] = await manager
40
+ .createQueryBuilder(OrderPackage, 'opk')
41
+ .innerJoinAndSelect('opk.releaseGood', 'rg')
42
+ .innerJoinAndSelect('opk.domain', 'domain')
43
+ .innerJoinAndSelect('opk.bizplace', 'bizplace')
44
+ .innerJoinAndSelect('opk.creator', 'creator')
45
+ .where(`rg.source = :source`, { source: ApplicationType.POWRUP })
46
+ .andWhere(`rg.type = :rgType`, { rgType: 'b2c' })
47
+ .andWhere(`rg.ref_order_id NOTNULL`)
48
+ .andWhere(`rg.status NOT IN(:...status) `, {
49
+ status: [ORDER_STATUS.CANCELLED, ORDER_STATUS.PENDING_CANCEL]
50
+ })
51
+ .andWhere(`opk.tracking_no IS NULL`)
52
+ .andWhere(`opk.airway_bill NOTNULL`)
53
+ .andWhere(`opk.package_id NOTNULL`)
54
+ .limit(500)
55
+ .getMany()
40
56
 
41
- // Call API with multiple package ids in one call
42
- const trackingNos: any = await PowrupAPI.getTrackingNo(powrup, {
43
- context: { state: { domain: null, user: null } },
44
- packageIds: orderPackageIds
45
- })
57
+ if (orderPackagesWithoutTrackingNo?.length > 0) {
58
+ // Batch the API call for all package IDs in one shot.
59
+ const orderPackageIds: any = orderPackagesWithoutTrackingNo.map(op => op.packageId)
60
+ const trackingNos: any = await PowrupAPI.getTrackingNo(powrup, {
61
+ context: { state: { domain: null, user: null } },
62
+ packageIds: orderPackageIds
63
+ })
46
64
 
47
- if (trackingNos?.length) {
48
- await Promise.all(
49
- trackingNos.map(async tn => {
50
- const { packageId, trackingNo } = tn
51
- await tx.getRepository(OrderPackage).update({ packageId: packageId }, { trackingNo: trackingNo })
52
- })
53
- )
65
+ if (trackingNos?.length) {
66
+ // Run UPDATEs sequentially as auto-commits. Parallel saves on the same
67
+ // QueryRunner connection are not safe (single connection), and parallel
68
+ // saves across separate connections gain little because the rows are
69
+ // independent and each UPDATE is fast.
70
+ for (const tn of trackingNos) {
71
+ const { packageId, trackingNo } = tn
72
+ await manager.update(OrderPackage, { packageId }, { trackingNo })
54
73
  }
55
74
  }
56
- } catch (e) {
57
- logger.error(`powrupGetTrackingNo[GetTrackingNo]: ${e}`)
58
75
  }
59
- })
76
+ } catch (e) {
77
+ logger.error(`powrupGetTrackingNo[GetTrackingNo]: ${e}`)
78
+ } finally {
79
+ try {
80
+ await queryRunner.query(`SELECT pg_advisory_unlock(hashtextextended($1, 0))`, [ADVISORY_LOCK_KEY])
81
+ } catch (releaseErr) {
82
+ logger.error(`powrupGetTrackingNo[AdvisoryUnlock]: ${releaseErr}`)
83
+ }
84
+ }
60
85
  } catch (e) {
61
86
  logger.error(`powrupGetTrackingNo[MainError]: ${e}`)
87
+ } finally {
88
+ // Defensive: clear any session-level advisory locks still held on this
89
+ // connection before returning it to the pool. Guards against the lock
90
+ // leaking if pg_advisory_unlock above failed (DB blip, statement timeout)
91
+ // and the pool then hands this connection to a future invocation.
92
+ // Skip when connect() itself failed — querying an unconnected QueryRunner
93
+ // would just throw with nothing to actually clean up.
94
+ if (connected) {
95
+ try {
96
+ await queryRunner.query(`SELECT pg_advisory_unlock_all()`)
97
+ } catch (unlockAllErr) {
98
+ logger.error(`powrupGetTrackingNo[AdvisoryUnlockAll]: ${unlockAllErr}`)
99
+ }
100
+ }
101
+ await queryRunner.release()
62
102
  }
63
103
  }
64
104
  }
@@ -1,100 +1,138 @@
1
- import { getConnection, getRepository } from 'typeorm'
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 { Platform, Powrup, PowrupAPI } from '@things-factory/integration-powrup'
6
6
  import { ORDER_STATUS, OrderPackage } from '@things-factory/sales-base'
7
7
 
8
+ const ADVISORY_LOCK_KEY = 'powrup-initiate-shipment'
9
+
8
10
  export const powrupInitiateShipmentResolver = {
9
11
  async powrupInitiateShipment(_: void, {}, context: any): Promise<void> {
10
12
  const maxRTSRetry: number = 10
13
+
14
+ // Same fix as powrupGetAirwayBill / powrupGetTrackingNo: avoid wrapping the
15
+ // resolver in a long transaction across external PowrupAPI HTTP calls. Use a
16
+ // single QueryRunner with a session-level advisory lock for concurrency safety,
17
+ // and let each UPDATE auto-commit so row locks are held only for the duration
18
+ // of the UPDATE statement, not across the API call.
19
+ const queryRunner = getConnection().createQueryRunner()
20
+ let connected = false
21
+
11
22
  try {
12
- await getConnection().transaction(async tx => {
13
- try {
14
- const powrup: Powrup = await tx
15
- .getRepository(Powrup)
16
- .findOne({ where: { platform: Platform.POWRUP, active: true } })
17
-
18
- let orderPackageQb = tx
19
- .getRepository(OrderPackage)
20
- .createQueryBuilder('opk')
21
- .innerJoinAndSelect('opk.releaseGood', 'rg')
22
- .innerJoinAndSelect('opk.domain', 'domain')
23
- .innerJoinAndSelect('opk.bizplace', 'bizplace')
24
- .leftJoinAndSelect('opk.creator', 'creator')
25
- .leftJoinAndSelect('rg.lastMileDelivery', 'lmd')
26
- .where(`rg.source = :source`, { source: ApplicationType.POWRUP })
27
- .andWhere(`rg.type = :rgType`, { rgType: 'b2c' })
28
- .andWhere(`rg.ref_order_id NOTNULL`)
29
- .andWhere(`rg.status = :status`, {
30
- status: ORDER_STATUS.DONE
31
- })
32
- .andWhere(`opk.market_rts_call_success ISNULL`)
33
- .andWhere(`COALESCE(opk.market_rts_call_retry, 0) <= :maxRTSRetry`, { maxRTSRetry: maxRTSRetry })
34
- .orderBy(`opk.created_at`)
35
-
36
- let orderPackagesWithoutRTS: OrderPackage[] = await orderPackageQb.getMany()
37
-
38
- if (orderPackagesWithoutRTS?.length > 0) {
39
- for (let i = 0; i < orderPackagesWithoutRTS.length; i++) {
40
- const orderPackage: OrderPackage = orderPackagesWithoutRTS[i]
41
- const releaseGood = orderPackage.releaseGood
42
-
43
- if (releaseGood.lmdOption == true && orderPackage?.trackingNo == null) {
44
- continue
45
- }
23
+ await queryRunner.connect()
24
+ connected = true
46
25
 
47
- let orderInformation: any = {
48
- refOrderId: orderPackage.releaseGood.refOrderId,
49
- packageId: orderPackage.packageId,
50
- platform: orderPackage.releaseGood.platformCode
51
- }
52
- if (orderPackage?.releaseGood?.lmdOption) {
53
- orderInformation = {
54
- ...orderInformation,
55
- awb: orderPackage.airwayBill,
56
- trackingNo: orderPackage.trackingNo,
57
- lmdOption: orderPackage?.releaseGood?.lmdOption,
58
- shipperName: orderPackage?.releaseGood?.lastMileDelivery?.platform
59
- }
60
- }
26
+ const lockResult: { acquired: boolean }[] = await queryRunner.query(
27
+ `SELECT pg_try_advisory_lock(hashtextextended($1, 0)) AS acquired`,
28
+ [ADVISORY_LOCK_KEY]
29
+ )
30
+ if (!lockResult?.[0]?.acquired) {
31
+ logger.info(`powrupInitiateShipment: another instance is running, skipping`)
32
+ return
33
+ }
34
+
35
+ try {
36
+ const manager = queryRunner.manager
37
+
38
+ const powrup: Powrup = await manager.findOne(Powrup, {
39
+ where: { platform: Platform.POWRUP, active: true }
40
+ })
41
+
42
+ const orderPackagesWithoutRTS: OrderPackage[] = await manager
43
+ .createQueryBuilder(OrderPackage, 'opk')
44
+ .innerJoinAndSelect('opk.releaseGood', 'rg')
45
+ .innerJoinAndSelect('opk.domain', 'domain')
46
+ .innerJoinAndSelect('opk.bizplace', 'bizplace')
47
+ .leftJoinAndSelect('opk.creator', 'creator')
48
+ .leftJoinAndSelect('rg.lastMileDelivery', 'lmd')
49
+ .where(`rg.source = :source`, { source: ApplicationType.POWRUP })
50
+ .andWhere(`rg.type = :rgType`, { rgType: 'b2c' })
51
+ .andWhere(`rg.ref_order_id NOTNULL`)
52
+ .andWhere(`rg.status = :status`, { status: ORDER_STATUS.DONE })
53
+ .andWhere(`opk.market_rts_call_success ISNULL`)
54
+ .andWhere(`COALESCE(opk.market_rts_call_retry, 0) <= :maxRTSRetry`, { maxRTSRetry })
55
+ .orderBy(`opk.created_at`)
56
+ .getMany()
57
+
58
+ if (orderPackagesWithoutRTS?.length > 0) {
59
+ for (let i = 0; i < orderPackagesWithoutRTS.length; i++) {
60
+ const orderPackage: OrderPackage = orderPackagesWithoutRTS[i]
61
+ const releaseGood = orderPackage.releaseGood
62
+
63
+ if (releaseGood.lmdOption == true && orderPackage?.trackingNo == null) {
64
+ continue
65
+ }
61
66
 
62
- performance.mark(`start:${orderPackage.releaseGood.refOrderId} - ${orderPackage.packageId}`)
63
- try {
64
- await PowrupAPI.initiateShipment(powrup, {
65
- ...orderInformation,
66
- context: { state: { domain: null, user: null } }
67
- })
68
- await tx
69
- .getRepository(OrderPackage)
70
- .update({ id: orderPackage.id }, { marketRtsCallSuccess: new Date() })
71
- performance.mark(`end:${orderPackage.releaseGood.refOrderId} - ${orderPackage.packageId}`)
72
- } catch (error) {
73
- performance.mark(`end:${orderPackage.releaseGood.refOrderId} - ${orderPackage.packageId}`)
74
- logger.error(`powrupInitiateShipment[InitiateShipment][Order Package ID: ${orderPackage.id}]: ${error}`)
75
- await getRepository(OrderPackage)
76
- .createQueryBuilder()
77
- .update(OrderPackage)
78
- .set({ marketRtsCallRetry: () => `coalesce("market_rts_call_retry", 0) + 1` })
79
- .where('id = :id', { id: orderPackage.id })
80
- .execute()
81
- continue
82
- } finally {
83
- let perf = performance.measure(
84
- `powrupInitiateShipment: ${orderPackage.releaseGood.refOrderId} - ${orderPackage.packageId}`,
85
- `start:${orderPackage.releaseGood.refOrderId} - ${orderPackage.packageId}`,
86
- `end:${orderPackage.releaseGood.refOrderId} - ${orderPackage.packageId}`
87
- )
88
- console.info(perf.toJSON())
67
+ let orderInformation: any = {
68
+ refOrderId: orderPackage.releaseGood.refOrderId,
69
+ packageId: orderPackage.packageId,
70
+ platform: orderPackage.releaseGood.platformCode
71
+ }
72
+ if (orderPackage?.releaseGood?.lmdOption) {
73
+ orderInformation = {
74
+ ...orderInformation,
75
+ awb: orderPackage.airwayBill,
76
+ trackingNo: orderPackage.trackingNo,
77
+ lmdOption: orderPackage?.releaseGood?.lmdOption,
78
+ shipperName: orderPackage?.releaseGood?.lastMileDelivery?.platform
89
79
  }
90
80
  }
81
+
82
+ performance.mark(`start:${orderPackage.releaseGood.refOrderId} - ${orderPackage.packageId}`)
83
+ try {
84
+ await PowrupAPI.initiateShipment(powrup, {
85
+ ...orderInformation,
86
+ context: { state: { domain: null, user: null } }
87
+ })
88
+
89
+ // Auto-commit single-statement UPDATE — row lock held only for this UPDATE,
90
+ // not across the API call above.
91
+ await manager.update(OrderPackage, { id: orderPackage.id }, { marketRtsCallSuccess: new Date() })
92
+
93
+ performance.mark(`end:${orderPackage.releaseGood.refOrderId} - ${orderPackage.packageId}`)
94
+ } catch (error) {
95
+ performance.mark(`end:${orderPackage.releaseGood.refOrderId} - ${orderPackage.packageId}`)
96
+ logger.error(`powrupInitiateShipment[InitiateShipment][Order Package ID: ${orderPackage.id}]: ${error}`)
97
+
98
+ await manager
99
+ .createQueryBuilder()
100
+ .update(OrderPackage)
101
+ .set({ marketRtsCallRetry: () => `coalesce("market_rts_call_retry", 0) + 1` })
102
+ .where('id = :id', { id: orderPackage.id })
103
+ .execute()
104
+
105
+ continue
106
+ } finally {
107
+ let perf = performance.measure(
108
+ `powrupInitiateShipment: ${orderPackage.releaseGood.refOrderId} - ${orderPackage.packageId}`,
109
+ `start:${orderPackage.releaseGood.refOrderId} - ${orderPackage.packageId}`,
110
+ `end:${orderPackage.releaseGood.refOrderId} - ${orderPackage.packageId}`
111
+ )
112
+ console.info(perf.toJSON())
113
+ }
91
114
  }
92
- } catch (e) {
93
- logger.error(`powrupInitiateShipment[InitiateShipment]: ${e}`)
94
115
  }
95
- })
116
+ } catch (e) {
117
+ logger.error(`powrupInitiateShipment[InitiateShipment]: ${e}`)
118
+ } finally {
119
+ try {
120
+ await queryRunner.query(`SELECT pg_advisory_unlock(hashtextextended($1, 0))`, [ADVISORY_LOCK_KEY])
121
+ } catch (releaseErr) {
122
+ logger.error(`powrupInitiateShipment[AdvisoryUnlock]: ${releaseErr}`)
123
+ }
124
+ }
96
125
  } catch (e) {
97
126
  logger.error(`powrupInitiateShipment[MainError]: ${e}`)
127
+ } finally {
128
+ if (connected) {
129
+ try {
130
+ await queryRunner.query(`SELECT pg_advisory_unlock_all()`)
131
+ } catch (unlockAllErr) {
132
+ logger.error(`powrupInitiateShipment[AdvisoryUnlockAll]: ${unlockAllErr}`)
133
+ }
134
+ }
135
+ await queryRunner.release()
98
136
  }
99
137
  }
100
138
  }