@sakeetech/vendure-payment-viva 0.2.1

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 (135) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +952 -0
  3. package/dist/api/admin-internal.controller.d.ts +59 -0
  4. package/dist/api/admin-internal.controller.d.ts.map +1 -0
  5. package/dist/api/admin-internal.controller.js +229 -0
  6. package/dist/api/admin-internal.controller.js.map +1 -0
  7. package/dist/api/admin-onboarding.controller.d.ts +72 -0
  8. package/dist/api/admin-onboarding.controller.d.ts.map +1 -0
  9. package/dist/api/admin-onboarding.controller.js +496 -0
  10. package/dist/api/admin-onboarding.controller.js.map +1 -0
  11. package/dist/api/admin-sources.controller.d.ts +50 -0
  12. package/dist/api/admin-sources.controller.d.ts.map +1 -0
  13. package/dist/api/admin-sources.controller.js +283 -0
  14. package/dist/api/admin-sources.controller.js.map +1 -0
  15. package/dist/api/shop-api.extension.d.ts +15 -0
  16. package/dist/api/shop-api.extension.d.ts.map +1 -0
  17. package/dist/api/shop-api.extension.js +35 -0
  18. package/dist/api/shop-api.extension.js.map +1 -0
  19. package/dist/api/shop-api.resolver.d.ts +42 -0
  20. package/dist/api/shop-api.resolver.d.ts.map +1 -0
  21. package/dist/api/shop-api.resolver.js +256 -0
  22. package/dist/api/shop-api.resolver.js.map +1 -0
  23. package/dist/api/webhook.controller.d.ts +58 -0
  24. package/dist/api/webhook.controller.d.ts.map +1 -0
  25. package/dist/api/webhook.controller.js +204 -0
  26. package/dist/api/webhook.controller.js.map +1 -0
  27. package/dist/cli/bin.d.ts +28 -0
  28. package/dist/cli/bin.d.ts.map +1 -0
  29. package/dist/cli/bin.js +104 -0
  30. package/dist/cli/bin.js.map +1 -0
  31. package/dist/cli/plan.d.ts +41 -0
  32. package/dist/cli/plan.d.ts.map +1 -0
  33. package/dist/cli/plan.js +115 -0
  34. package/dist/cli/plan.js.map +1 -0
  35. package/dist/cli/register-webhooks.d.ts +45 -0
  36. package/dist/cli/register-webhooks.d.ts.map +1 -0
  37. package/dist/cli/register-webhooks.js +400 -0
  38. package/dist/cli/register-webhooks.js.map +1 -0
  39. package/dist/cli/types.d.ts +75 -0
  40. package/dist/cli/types.d.ts.map +1 -0
  41. package/dist/cli/types.js +10 -0
  42. package/dist/cli/types.js.map +1 -0
  43. package/dist/constants.d.ts +35 -0
  44. package/dist/constants.d.ts.map +1 -0
  45. package/dist/constants.js +40 -0
  46. package/dist/constants.js.map +1 -0
  47. package/dist/entities/index.d.ts +4 -0
  48. package/dist/entities/index.d.ts.map +1 -0
  49. package/dist/entities/index.js +3 -0
  50. package/dist/entities/index.js.map +1 -0
  51. package/dist/entities/viva-transaction.entity.d.ts +70 -0
  52. package/dist/entities/viva-transaction.entity.d.ts.map +1 -0
  53. package/dist/entities/viva-transaction.entity.js +133 -0
  54. package/dist/entities/viva-transaction.entity.js.map +1 -0
  55. package/dist/entities/viva-webhook-event.entity.d.ts +71 -0
  56. package/dist/entities/viva-webhook-event.entity.d.ts.map +1 -0
  57. package/dist/entities/viva-webhook-event.entity.js +138 -0
  58. package/dist/entities/viva-webhook-event.entity.js.map +1 -0
  59. package/dist/index.d.ts +27 -0
  60. package/dist/index.d.ts.map +1 -0
  61. package/dist/index.js +23 -0
  62. package/dist/index.js.map +1 -0
  63. package/dist/jobs/process-viva-webhook.handler.d.ts +95 -0
  64. package/dist/jobs/process-viva-webhook.handler.d.ts.map +1 -0
  65. package/dist/jobs/process-viva-webhook.handler.js +530 -0
  66. package/dist/jobs/process-viva-webhook.handler.js.map +1 -0
  67. package/dist/jobs/queue-names.d.ts +18 -0
  68. package/dist/jobs/queue-names.d.ts.map +1 -0
  69. package/dist/jobs/queue-names.js +19 -0
  70. package/dist/jobs/queue-names.js.map +1 -0
  71. package/dist/jobs/retention-cleanup.handler.d.ts +31 -0
  72. package/dist/jobs/retention-cleanup.handler.d.ts.map +1 -0
  73. package/dist/jobs/retention-cleanup.handler.js +94 -0
  74. package/dist/jobs/retention-cleanup.handler.js.map +1 -0
  75. package/dist/loaders/bootstrap.d.ts +28 -0
  76. package/dist/loaders/bootstrap.d.ts.map +1 -0
  77. package/dist/loaders/bootstrap.js +90 -0
  78. package/dist/loaders/bootstrap.js.map +1 -0
  79. package/dist/migrations/1714000000000-create-viva-tables.d.ts +22 -0
  80. package/dist/migrations/1714000000000-create-viva-tables.d.ts.map +1 -0
  81. package/dist/migrations/1714000000000-create-viva-tables.js +105 -0
  82. package/dist/migrations/1714000000000-create-viva-tables.js.map +1 -0
  83. package/dist/observability/metrics-state.service.d.ts +43 -0
  84. package/dist/observability/metrics-state.service.d.ts.map +1 -0
  85. package/dist/observability/metrics-state.service.js +207 -0
  86. package/dist/observability/metrics-state.service.js.map +1 -0
  87. package/dist/payment-method-handler.d.ts +26 -0
  88. package/dist/payment-method-handler.d.ts.map +1 -0
  89. package/dist/payment-method-handler.js +693 -0
  90. package/dist/payment-method-handler.js.map +1 -0
  91. package/dist/plugin.d.ts +95 -0
  92. package/dist/plugin.d.ts.map +1 -0
  93. package/dist/plugin.js +241 -0
  94. package/dist/plugin.js.map +1 -0
  95. package/dist/providers/viva-oauth2-strategy.provider.d.ts +41 -0
  96. package/dist/providers/viva-oauth2-strategy.provider.d.ts.map +1 -0
  97. package/dist/providers/viva-oauth2-strategy.provider.js +60 -0
  98. package/dist/providers/viva-oauth2-strategy.provider.js.map +1 -0
  99. package/dist/services/connected-accounts.service.d.ts +53 -0
  100. package/dist/services/connected-accounts.service.d.ts.map +1 -0
  101. package/dist/services/connected-accounts.service.js +108 -0
  102. package/dist/services/connected-accounts.service.js.map +1 -0
  103. package/dist/services/per-merchant-semaphore.service.d.ts +49 -0
  104. package/dist/services/per-merchant-semaphore.service.d.ts.map +1 -0
  105. package/dist/services/per-merchant-semaphore.service.js +156 -0
  106. package/dist/services/per-merchant-semaphore.service.js.map +1 -0
  107. package/dist/services/state-machine.service.d.ts +100 -0
  108. package/dist/services/state-machine.service.d.ts.map +1 -0
  109. package/dist/services/state-machine.service.js +233 -0
  110. package/dist/services/state-machine.service.js.map +1 -0
  111. package/dist/types.d.ts +286 -0
  112. package/dist/types.d.ts.map +1 -0
  113. package/dist/types.js +23 -0
  114. package/dist/types.js.map +1 -0
  115. package/dist/util/currency.d.ts +32 -0
  116. package/dist/util/currency.d.ts.map +1 -0
  117. package/dist/util/currency.js +90 -0
  118. package/dist/util/currency.js.map +1 -0
  119. package/dist/util/error-envelope.d.ts +51 -0
  120. package/dist/util/error-envelope.d.ts.map +1 -0
  121. package/dist/util/error-envelope.js +157 -0
  122. package/dist/util/error-envelope.js.map +1 -0
  123. package/dist/util/ip-allowlist.d.ts +44 -0
  124. package/dist/util/ip-allowlist.d.ts.map +1 -0
  125. package/dist/util/ip-allowlist.js +139 -0
  126. package/dist/util/ip-allowlist.js.map +1 -0
  127. package/dist/util/normalize-options.d.ts +24 -0
  128. package/dist/util/normalize-options.d.ts.map +1 -0
  129. package/dist/util/normalize-options.js +189 -0
  130. package/dist/util/normalize-options.js.map +1 -0
  131. package/dist/util/url-template.d.ts +18 -0
  132. package/dist/util/url-template.d.ts.map +1 -0
  133. package/dist/util/url-template.js +22 -0
  134. package/dist/util/url-template.js.map +1 -0
  135. package/package.json +75 -0
@@ -0,0 +1,108 @@
1
+ /**
2
+ * services/connected-accounts.service.ts — Channel ↔ Viva account helpers.
3
+ *
4
+ * Used by the webhook worker (V7) for event 8194 and by the onboarding
5
+ * endpoints (V9). Encapsulates the mandatory field-write order rule:
6
+ *
7
+ * 1. vivaMerchantId (write FIRST — storefront does NOT gate on this)
8
+ * 2. vivaPayoutsEnabled = true (write LAST — storefront gates on this)
9
+ *
10
+ * Writing in reverse order would create a race window where the storefront
11
+ * sees payoutsEnabled=true but vivaMerchantId is still null, causing every
12
+ * createPayment to fail with VIVA_CHANNEL_MISCONFIGURED.
13
+ *
14
+ * @see docs/plans/vendure-plugin-v0.md §"Webhook Design — Process flow" step 4 (8194)
15
+ * @see docs/VENDURE-CONTRACT.MD §10 (field-write order)
16
+ */
17
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
18
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
19
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
20
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
21
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
22
+ };
23
+ var __metadata = (this && this.__metadata) || function (k, v) {
24
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
25
+ };
26
+ import { Injectable } from '@nestjs/common';
27
+ import { ChannelService, TransactionalConnection, Logger } from '@vendure/core';
28
+ import { Channel } from '@vendure/core';
29
+ import { VIVA_LOG_CONTEXT } from '../constants.js';
30
+ // ---------------------------------------------------------------------------
31
+ // Service
32
+ // ---------------------------------------------------------------------------
33
+ let ConnectedAccountsService = class ConnectedAccountsService {
34
+ channelService;
35
+ connection;
36
+ constructor(channelService, connection) {
37
+ this.channelService = channelService;
38
+ this.connection = connection;
39
+ }
40
+ /**
41
+ * Write `vivaAccountId` on the channel.
42
+ *
43
+ * Called at onboarding-create time (V9) immediately after IsvAccounts.create
44
+ * returns the accountId. This is always the first write for a new channel.
45
+ */
46
+ async writeAccountId(ctx, channel, accountId) {
47
+ Logger.info(`[ConnectedAccounts] Writing vivaAccountId=${accountId} to channel ${String(channel.id)}.`, VIVA_LOG_CONTEXT);
48
+ await this.channelService.update(ctx, {
49
+ id: channel.id,
50
+ customFields: { vivaAccountId: accountId },
51
+ });
52
+ }
53
+ /**
54
+ * Find a channel where `customFields.vivaAccountId === accountId`.
55
+ *
56
+ * Loads all channels and filters in-process (Vendure doesn't support
57
+ * custom-field WHERE queries natively without raw SQL).
58
+ *
59
+ * Returns null if no channel matches.
60
+ */
61
+ async findChannelByAccountId(accountId) {
62
+ const repo = this.connection.rawConnection.getRepository(Channel);
63
+ const channels = await repo.find();
64
+ for (const ch of channels) {
65
+ const cf = ch.customFields;
66
+ if (cf && cf['vivaAccountId'] === accountId) {
67
+ return ch;
68
+ }
69
+ }
70
+ return null;
71
+ }
72
+ /**
73
+ * Write `vivaMerchantId` on the channel.
74
+ *
75
+ * Must be called BEFORE `flipPayoutsEnabled`. Do not reorder.
76
+ */
77
+ async writeMerchantId(ctx, channel, merchantId) {
78
+ Logger.info(`[ConnectedAccounts] Writing vivaMerchantId=${merchantId} to channel ${String(channel.id)}.`, VIVA_LOG_CONTEXT);
79
+ // TODO(impl): ChannelService.update in Vendure 3.x accepts a partial UpdateChannelInput.
80
+ // The exact shape of the update payload for customFields depends on the Vendure version
81
+ // and whether the channel custom fields are registered as AdminUI-visible.
82
+ // Line below assumes Vendure 3.6 ChannelService.update signature:
83
+ // update(ctx, input: DeepPartial<Channel> & { id: ID }): Promise<Channel>
84
+ await this.channelService.update(ctx, {
85
+ id: channel.id,
86
+ customFields: { vivaMerchantId: merchantId },
87
+ });
88
+ }
89
+ /**
90
+ * Flip `vivaPayoutsEnabled` on the channel.
91
+ *
92
+ * Must be called AFTER `writeMerchantId` when enabling. Do not reorder.
93
+ */
94
+ async flipPayoutsEnabled(ctx, channel, value) {
95
+ Logger.info(`[ConnectedAccounts] Flipping vivaPayoutsEnabled=${value} on channel ${String(channel.id)}.`, VIVA_LOG_CONTEXT);
96
+ await this.channelService.update(ctx, {
97
+ id: channel.id,
98
+ customFields: { vivaPayoutsEnabled: value },
99
+ });
100
+ }
101
+ };
102
+ ConnectedAccountsService = __decorate([
103
+ Injectable(),
104
+ __metadata("design:paramtypes", [ChannelService,
105
+ TransactionalConnection])
106
+ ], ConnectedAccountsService);
107
+ export { ConnectedAccountsService };
108
+ //# sourceMappingURL=connected-accounts.service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connected-accounts.service.js","sourceRoot":"","sources":["../../src/services/connected-accounts.service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;;;;;;;;;;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAC5C,OAAO,EAAE,cAAc,EAAE,uBAAuB,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAEhF,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AACxC,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAEnD,8EAA8E;AAC9E,UAAU;AACV,8EAA8E;AAGvE,IAAM,wBAAwB,GAA9B,MAAM,wBAAwB;IAEhB;IACA;IAFnB,YACmB,cAA8B,EAC9B,UAAmC;QADnC,mBAAc,GAAd,cAAc,CAAgB;QAC9B,eAAU,GAAV,UAAU,CAAyB;IACnD,CAAC;IAEJ;;;;;OAKG;IACH,KAAK,CAAC,cAAc,CAAC,GAAmB,EAAE,OAAgB,EAAE,SAAiB;QAC3E,MAAM,CAAC,IAAI,CACT,6CAA6C,SAAS,eAAe,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,GAAG,EAC1F,gBAAgB,CACjB,CAAC;QACF,MAAM,IAAI,CAAC,cAAc,CAAC,MAAM,CAAC,GAAG,EAAE;YACpC,EAAE,EAAE,OAAO,CAAC,EAAE;YACd,YAAY,EAAE,EAAE,aAAa,EAAE,SAAS,EAAE;SACpC,CAAC,CAAC;IACZ,CAAC;IAED;;;;;;;OAOG;IACH,KAAK,CAAC,sBAAsB,CAAC,SAAiB;QAC5C,MAAM,IAAI,GAAG,IAAI,CAAC,UAAU,CAAC,aAAa,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;QAClE,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC;QACnC,KAAK,MAAM,EAAE,IAAI,QAAQ,EAAE,CAAC;YAC1B,MAAM,EAAE,GAAI,EAAU,CAAC,YAAmD,CAAC;YAC3E,IAAI,EAAE,IAAI,EAAE,CAAC,eAAe,CAAC,KAAK,SAAS,EAAE,CAAC;gBAC5C,OAAO,EAAE,CAAC;YACZ,CAAC;QACH,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,eAAe,CAAC,GAAmB,EAAE,OAAgB,EAAE,UAAkB;QAC7E,MAAM,CAAC,IAAI,CACT,8CAA8C,UAAU,eAAe,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,GAAG,EAC5F,gBAAgB,CACjB,CAAC;QACF,yFAAyF;QACzF,wFAAwF;QACxF,2EAA2E;QAC3E,kEAAkE;QAClE,4EAA4E;QAC5E,MAAM,IAAI,CAAC,cAAc,CAAC,MAAM,CAAC,GAAG,EAAE;YACpC,EAAE,EAAE,OAAO,CAAC,EAAE;YACd,YAAY,EAAE,EAAE,cAAc,EAAE,UAAU,EAAE;SACtC,CAAC,CAAC;IACZ,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,kBAAkB,CAAC,GAAmB,EAAE,OAAgB,EAAE,KAAc;QAC5E,MAAM,CAAC,IAAI,CACT,mDAAmD,KAAK,eAAe,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,GAAG,EAC5F,gBAAgB,CACjB,CAAC;QACF,MAAM,IAAI,CAAC,cAAc,CAAC,MAAM,CAAC,GAAG,EAAE;YACpC,EAAE,EAAE,OAAO,CAAC,EAAE;YACd,YAAY,EAAE,EAAE,kBAAkB,EAAE,KAAK,EAAE;SACrC,CAAC,CAAC;IACZ,CAAC;CACF,CAAA;AA/EY,wBAAwB;IADpC,UAAU,EAAE;qCAGwB,cAAc;QAClB,uBAAuB;GAH3C,wBAAwB,CA+EpC"}
@@ -0,0 +1,49 @@
1
+ /**
2
+ * services/per-merchant-semaphore.service.ts — In-process per-merchant concurrency semaphore.
3
+ *
4
+ * Limits concurrent webhook processing per merchantId to PER_MERCHANT_SEMAPHORE_PERMITS
5
+ * (default 5) per worker process, matching Medusa A11.
6
+ *
7
+ * Optional Redlock injection: when `redlock` is set in plugin options the service
8
+ * acquires a Redis-backed distributed lock on `merchant:{merchantId}` before
9
+ * entering the in-process semaphore. Release always happens in `finally`.
10
+ *
11
+ * LRU: idle merchant entries (no waiting/active holders) are evicted after 5 min.
12
+ *
13
+ * Multi-worker note: without Redlock the cap is PERMITS × WORKERS (each worker has
14
+ * its own independent in-process counter — no global coordination).
15
+ *
16
+ * @see docs/plans/vendure-plugin-v0.md §"Webhook Design — Per-tenant concurrency" D9
17
+ */
18
+ import type { VivaPaymentPluginOptions } from '../types.js';
19
+ export type Release = () => Promise<void>;
20
+ export declare class PerMerchantSemaphore {
21
+ private readonly options;
22
+ private readonly slots;
23
+ private readonly permits;
24
+ private readonly redlock;
25
+ private _evictionTimer;
26
+ readonly EVICTION_IDLE_MS: number;
27
+ constructor(options: VivaPaymentPluginOptions);
28
+ /**
29
+ * Acquire a semaphore permit for `merchantId`.
30
+ *
31
+ * Returns a `Release` function that MUST be called in a `finally` block.
32
+ *
33
+ * If a Redlock client is configured, a distributed lock is also acquired
34
+ * before returning. The distributed lock is released together with the
35
+ * in-process permit.
36
+ */
37
+ acquire(merchantId: string): Promise<Release>;
38
+ private _getSlot;
39
+ private _acquireInProcess;
40
+ private _releaseInProcess;
41
+ private _startEvictionTimer;
42
+ private _evict;
43
+ /** @internal Force an eviction pass (for tests). */
44
+ _forceEvict(): void;
45
+ /** @internal Snapshot of current slot map size (for tests). */
46
+ get _slotCount(): number;
47
+ onApplicationShutdown(): void;
48
+ }
49
+ //# sourceMappingURL=per-merchant-semaphore.service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"per-merchant-semaphore.service.d.ts","sourceRoot":"","sources":["../../src/services/per-merchant-semaphore.service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAIH,OAAO,KAAK,EAAE,wBAAwB,EAAiB,MAAM,aAAa,CAAC;AAM3E,MAAM,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;AAY1C,qBACa,oBAAoB;IAYA,OAAO,CAAC,QAAQ,CAAC,OAAO;IAXvD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAmC;IACzD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA4B;IAGpD,OAAO,CAAC,cAAc,CAA6C;IAGnE,QAAQ,CAAC,gBAAgB,SAAkB;gBAGK,OAAO,EAAE,wBAAwB;IAOjF;;;;;;;;OAQG;IACG,OAAO,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAwBnD,OAAO,CAAC,QAAQ;IAShB,OAAO,CAAC,iBAAiB;IAYzB,OAAO,CAAC,iBAAiB;IAoBzB,OAAO,CAAC,mBAAmB;IAS3B,OAAO,CAAC,MAAM;IASd,oDAAoD;IACpD,WAAW,IAAI,IAAI;IAInB,+DAA+D;IAC/D,IAAI,UAAU,IAAI,MAAM,CAEvB;IAED,qBAAqB,IAAI,IAAI;CAK9B"}
@@ -0,0 +1,156 @@
1
+ /**
2
+ * services/per-merchant-semaphore.service.ts — In-process per-merchant concurrency semaphore.
3
+ *
4
+ * Limits concurrent webhook processing per merchantId to PER_MERCHANT_SEMAPHORE_PERMITS
5
+ * (default 5) per worker process, matching Medusa A11.
6
+ *
7
+ * Optional Redlock injection: when `redlock` is set in plugin options the service
8
+ * acquires a Redis-backed distributed lock on `merchant:{merchantId}` before
9
+ * entering the in-process semaphore. Release always happens in `finally`.
10
+ *
11
+ * LRU: idle merchant entries (no waiting/active holders) are evicted after 5 min.
12
+ *
13
+ * Multi-worker note: without Redlock the cap is PERMITS × WORKERS (each worker has
14
+ * its own independent in-process counter — no global coordination).
15
+ *
16
+ * @see docs/plans/vendure-plugin-v0.md §"Webhook Design — Per-tenant concurrency" D9
17
+ */
18
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
19
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
20
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
21
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
22
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
23
+ };
24
+ var __metadata = (this && this.__metadata) || function (k, v) {
25
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
26
+ };
27
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
28
+ return function (target, key) { decorator(target, key, paramIndex); }
29
+ };
30
+ import { Injectable, Inject } from '@nestjs/common';
31
+ import { PER_MERCHANT_SEMAPHORE_PERMITS, VIVA_PLUGIN_OPTIONS } from '../constants.js';
32
+ // ---------------------------------------------------------------------------
33
+ // Service
34
+ // ---------------------------------------------------------------------------
35
+ let PerMerchantSemaphore = class PerMerchantSemaphore {
36
+ options;
37
+ slots = new Map();
38
+ permits;
39
+ redlock;
40
+ // LRU eviction timer handle
41
+ _evictionTimer;
42
+ // Exposed for tests
43
+ EVICTION_IDLE_MS = 5 * 60 * 1_000; // 5 minutes
44
+ constructor(options) {
45
+ this.options = options;
46
+ this.permits = PER_MERCHANT_SEMAPHORE_PERMITS;
47
+ this.redlock = options.redlock;
48
+ this._startEvictionTimer();
49
+ }
50
+ /**
51
+ * Acquire a semaphore permit for `merchantId`.
52
+ *
53
+ * Returns a `Release` function that MUST be called in a `finally` block.
54
+ *
55
+ * If a Redlock client is configured, a distributed lock is also acquired
56
+ * before returning. The distributed lock is released together with the
57
+ * in-process permit.
58
+ */
59
+ async acquire(merchantId) {
60
+ // 1. Acquire distributed lock (if configured)
61
+ let redlockRelease;
62
+ if (this.redlock) {
63
+ const lock = await this.redlock.acquire([`merchant:${merchantId}`], 30_000);
64
+ redlockRelease = () => lock.release();
65
+ }
66
+ // 2. Acquire in-process slot
67
+ await this._acquireInProcess(merchantId);
68
+ return async () => {
69
+ try {
70
+ if (redlockRelease)
71
+ await redlockRelease();
72
+ }
73
+ finally {
74
+ this._releaseInProcess(merchantId);
75
+ }
76
+ };
77
+ }
78
+ // ---------------------------------------------------------------------------
79
+ // In-process semaphore helpers
80
+ // ---------------------------------------------------------------------------
81
+ _getSlot(merchantId) {
82
+ let slot = this.slots.get(merchantId);
83
+ if (!slot) {
84
+ slot = { active: 0, queue: [], lastIdleAt: Date.now() };
85
+ this.slots.set(merchantId, slot);
86
+ }
87
+ return slot;
88
+ }
89
+ _acquireInProcess(merchantId) {
90
+ const slot = this._getSlot(merchantId);
91
+ if (slot.active < this.permits) {
92
+ slot.active++;
93
+ return Promise.resolve();
94
+ }
95
+ // Queue the waiter
96
+ return new Promise((resolve) => {
97
+ slot.queue.push(resolve);
98
+ });
99
+ }
100
+ _releaseInProcess(merchantId) {
101
+ const slot = this.slots.get(merchantId);
102
+ if (!slot)
103
+ return;
104
+ const next = slot.queue.shift();
105
+ if (next) {
106
+ // hand the permit directly to the next waiter (active count unchanged)
107
+ next();
108
+ }
109
+ else {
110
+ slot.active = Math.max(0, slot.active - 1);
111
+ if (slot.active === 0) {
112
+ slot.lastIdleAt = Date.now();
113
+ }
114
+ }
115
+ }
116
+ // ---------------------------------------------------------------------------
117
+ // LRU eviction
118
+ // ---------------------------------------------------------------------------
119
+ _startEvictionTimer() {
120
+ // Run every minute; evict entries idle for >5 min with no active/queued holders
121
+ this._evictionTimer = setInterval(() => {
122
+ this._evict();
123
+ }, 60_000);
124
+ // Don't block the Node.js event loop from exiting
125
+ if (this._evictionTimer.unref)
126
+ this._evictionTimer.unref();
127
+ }
128
+ _evict() {
129
+ const now = Date.now();
130
+ for (const [merchantId, slot] of this.slots.entries()) {
131
+ if (slot.active === 0 && slot.queue.length === 0 && now - slot.lastIdleAt > this.EVICTION_IDLE_MS) {
132
+ this.slots.delete(merchantId);
133
+ }
134
+ }
135
+ }
136
+ /** @internal Force an eviction pass (for tests). */
137
+ _forceEvict() {
138
+ this._evict();
139
+ }
140
+ /** @internal Snapshot of current slot map size (for tests). */
141
+ get _slotCount() {
142
+ return this.slots.size;
143
+ }
144
+ onApplicationShutdown() {
145
+ if (this._evictionTimer) {
146
+ clearInterval(this._evictionTimer);
147
+ }
148
+ }
149
+ };
150
+ PerMerchantSemaphore = __decorate([
151
+ Injectable(),
152
+ __param(0, Inject(VIVA_PLUGIN_OPTIONS)),
153
+ __metadata("design:paramtypes", [Object])
154
+ ], PerMerchantSemaphore);
155
+ export { PerMerchantSemaphore };
156
+ //# sourceMappingURL=per-merchant-semaphore.service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"per-merchant-semaphore.service.js","sourceRoot":"","sources":["../../src/services/per-merchant-semaphore.service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,100 @@
1
+ /**
2
+ * services/state-machine.service.ts — Low-level VivaTransaction row helpers
3
+ * AND Vendure order/payment transition wrappers (V7 extension).
4
+ *
5
+ * Row helpers: INSERT-OR-NOTHING idempotent writes + lookups for VivaTransaction.
6
+ * Transition helpers: wraps OrderService + PaymentService state-machine calls;
7
+ * used exclusively by the webhook worker (V7). Implements the stale-recovery
8
+ * re-walk (AddingItems → ArrangingPayment → PaymentAuthorized → PaymentSettled)
9
+ * with fail-loud semantics per Q4 decision.
10
+ *
11
+ * Repository injection via Vendure's TransactionalConnection.
12
+ *
13
+ * @see docs/plans/vendure-plugin-v0.md §"Build Plan — V5 + V7"
14
+ * @see docs/plans/vendure-plugin-v0.md §D6 (state transitions via OrderService)
15
+ * @see docs/plans/vendure-plugin-v0.md §D11 (idempotency)
16
+ */
17
+ import { TransactionalConnection, OrderService, PaymentService } from '@vendure/core';
18
+ import type { RequestContext } from '@vendure/core';
19
+ import { VivaTransaction } from '../entities/viva-transaction.entity.js';
20
+ import type { VivaTransactionStatus } from '../entities/viva-transaction.entity.js';
21
+ export interface UpsertPendingInput {
22
+ channelId: string | number;
23
+ paymentId: string | number;
24
+ idempotencyKey: string;
25
+ amountMinor: bigint;
26
+ currencyCode: string;
27
+ isvAmountMinor: bigint;
28
+ }
29
+ export declare class StateMachineService {
30
+ private readonly connection;
31
+ private readonly orderService;
32
+ private readonly paymentService;
33
+ constructor(connection: TransactionalConnection, orderService: OrderService, paymentService: PaymentService);
34
+ /**
35
+ * Load a VivaTransaction row by (channelId, paymentId).
36
+ * Returns null if not found.
37
+ */
38
+ getVivaTransaction(ctx: RequestContext, channelId: string | number, paymentId: string | number): Promise<VivaTransaction | null>;
39
+ /**
40
+ * INSERT-OR-NOTHING write of a pending VivaTransaction row.
41
+ *
42
+ * Idempotency key D11: keyed on (channelId, paymentId).
43
+ * If a row already exists with the same key and a non-null vivaOrderCode,
44
+ * returns that existing row so the handler can skip the Viva API call.
45
+ *
46
+ * Returns: { row, wasInserted }
47
+ * wasInserted=true → freshly inserted; handler must call Viva.
48
+ * wasInserted=false → already existed; check row.vivaOrderCode.
49
+ */
50
+ upsertPendingTransaction(ctx: RequestContext, input: UpsertPendingInput): Promise<{
51
+ row: VivaTransaction;
52
+ wasInserted: boolean;
53
+ }>;
54
+ /**
55
+ * Update the vivaOrderCode on a pending row after createOrder succeeds.
56
+ */
57
+ setOrderCode(ctx: RequestContext, rowId: string | number, vivaOrderCode: string, metadata: Record<string, unknown>): Promise<void>;
58
+ /**
59
+ * Update row status (e.g. pending → captured, pending → cancelled).
60
+ */
61
+ setStatus(ctx: RequestContext, rowId: string | number, status: VivaTransactionStatus, metadata?: Record<string, unknown>): Promise<void>;
62
+ /**
63
+ * Transition a Payment to `Settled` via `PaymentService.transitionToState`.
64
+ *
65
+ * Vendure's canonical path for webhook-driven settlement.
66
+ * @see docs/plans/vendure-plugin-v0.md §D6, §D5 (1796 settle path)
67
+ */
68
+ transitionPaymentToSettled(ctx: RequestContext, orderId: string | number, paymentId: string | number): Promise<void>;
69
+ /**
70
+ * Transition a Payment to `Declined` via `PaymentService.transitionToState`.
71
+ *
72
+ * Called on webhook 1798 (Transaction Failed). Order stays in ArrangingPayment.
73
+ * @see docs/plans/vendure-plugin-v0.md §D5 (1798 path)
74
+ */
75
+ transitionPaymentToDeclined(ctx: RequestContext, paymentId: string | number): Promise<void>;
76
+ /**
77
+ * Transition a Payment to `Cancelled` via `PaymentService.transitionToState`.
78
+ *
79
+ * Called on webhook 4865 user-cancel detection.
80
+ * @see docs/plans/vendure-plugin-v0.md §D5 (4865 path)
81
+ */
82
+ transitionPaymentToCancelled(ctx: RequestContext, paymentId: string | number): Promise<void>;
83
+ /**
84
+ * Stale-order re-walk: AddingItems → ArrangingPayment → PaymentSettled.
85
+ *
86
+ * Implements the Q4 decision: fail-loud on any transition failure — log an
87
+ * ERROR-level message, throw, let the webhook row stay unprocessed. The
88
+ * storefront will surface "basket changed" via the live order state.
89
+ *
90
+ * Sequence:
91
+ * 1. AddingItems → ArrangingPayment (transitionOrderToState)
92
+ * 2. ArrangingPayment → settlePayment (OrderService.settlePayment)
93
+ *
94
+ * @see docs/plans/vendure-plugin-v0.md §"Process flow" step 4e
95
+ * @see docs/plans/vendure-plugin-v0.md §"Architecture Decisions Q4"
96
+ */
97
+ recoverStaleOrderAndSettle(ctx: RequestContext, orderId: string | number, paymentId: string | number): Promise<void>;
98
+ private _isDuplicateKeyError;
99
+ }
100
+ //# sourceMappingURL=state-machine.service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state-machine.service.d.ts","sourceRoot":"","sources":["../../src/services/state-machine.service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAGH,OAAO,EAAE,uBAAuB,EAAU,YAAY,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC9F,OAAO,KAAK,EAAE,cAAc,EAAW,MAAM,eAAe,CAAC;AAE7D,OAAO,EAAE,eAAe,EAAE,MAAM,wCAAwC,CAAC;AACzE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,wCAAwC,CAAC;AAOpF,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC;IAC3B,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC;IAC3B,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;CACxB;AAMD,qBACa,mBAAmB;IAE5B,OAAO,CAAC,QAAQ,CAAC,UAAU;IAC3B,OAAO,CAAC,QAAQ,CAAC,YAAY;IAC7B,OAAO,CAAC,QAAQ,CAAC,cAAc;gBAFd,UAAU,EAAE,uBAAuB,EACnC,YAAY,EAAE,YAAY,EAC1B,cAAc,EAAE,cAAc;IAGjD;;;OAGG;IACG,kBAAkB,CACtB,GAAG,EAAE,cAAc,EACnB,SAAS,EAAE,MAAM,GAAG,MAAM,EAC1B,SAAS,EAAE,MAAM,GAAG,MAAM,GACzB,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC;IAOlC;;;;;;;;;;OAUG;IACG,wBAAwB,CAC5B,GAAG,EAAE,cAAc,EACnB,KAAK,EAAE,kBAAkB,GACxB,OAAO,CAAC;QAAE,GAAG,EAAE,eAAe,CAAC;QAAC,WAAW,EAAE,OAAO,CAAA;KAAE,CAAC;IAgD1D;;OAEG;IACG,YAAY,CAChB,GAAG,EAAE,cAAc,EACnB,KAAK,EAAE,MAAM,GAAG,MAAM,EACtB,aAAa,EAAE,MAAM,EACrB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAChC,OAAO,CAAC,IAAI,CAAC;IAYhB;;OAEG;IACG,SAAS,CACb,GAAG,EAAE,cAAc,EACnB,KAAK,EAAE,MAAM,GAAG,MAAM,EACtB,MAAM,EAAE,qBAAqB,EAC7B,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACjC,OAAO,CAAC,IAAI,CAAC;IAwBhB;;;;;OAKG;IACG,0BAA0B,CAC9B,GAAG,EAAE,cAAc,EACnB,OAAO,EAAE,MAAM,GAAG,MAAM,EACxB,SAAS,EAAE,MAAM,GAAG,MAAM,GACzB,OAAO,CAAC,IAAI,CAAC;IAWhB;;;;;OAKG;IACG,2BAA2B,CAC/B,GAAG,EAAE,cAAc,EACnB,SAAS,EAAE,MAAM,GAAG,MAAM,GACzB,OAAO,CAAC,IAAI,CAAC;IAIhB;;;;;OAKG;IACG,4BAA4B,CAChC,GAAG,EAAE,cAAc,EACnB,SAAS,EAAE,MAAM,GAAG,MAAM,GACzB,OAAO,CAAC,IAAI,CAAC;IAIhB;;;;;;;;;;;;;OAaG;IACG,0BAA0B,CAC9B,GAAG,EAAE,cAAc,EACnB,OAAO,EAAE,MAAM,GAAG,MAAM,EACxB,SAAS,EAAE,MAAM,GAAG,MAAM,GACzB,OAAO,CAAC,IAAI,CAAC;IAgChB,OAAO,CAAC,oBAAoB;CAQ7B"}