@sakeetech/medusa-payment-viva 0.2.2

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 (171) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +816 -0
  3. package/dist/api/index.d.ts +15 -0
  4. package/dist/api/index.d.ts.map +1 -0
  5. package/dist/api/index.js +22 -0
  6. package/dist/api/index.js.map +1 -0
  7. package/dist/api/middlewares.d.ts +27 -0
  8. package/dist/api/middlewares.d.ts.map +1 -0
  9. package/dist/api/middlewares.js +62 -0
  10. package/dist/api/middlewares.js.map +1 -0
  11. package/dist/api/viva/admin/_admin-auth.d.ts +26 -0
  12. package/dist/api/viva/admin/_admin-auth.d.ts.map +1 -0
  13. package/dist/api/viva/admin/_admin-auth.js +49 -0
  14. package/dist/api/viva/admin/_admin-auth.js.map +1 -0
  15. package/dist/api/viva/admin/_mode-gate.d.ts +28 -0
  16. package/dist/api/viva/admin/_mode-gate.d.ts.map +1 -0
  17. package/dist/api/viva/admin/_mode-gate.js +45 -0
  18. package/dist/api/viva/admin/_mode-gate.js.map +1 -0
  19. package/dist/api/viva/admin/connected-accounts/[id]/reconcile/route.d.ts +21 -0
  20. package/dist/api/viva/admin/connected-accounts/[id]/reconcile/route.d.ts.map +1 -0
  21. package/dist/api/viva/admin/connected-accounts/[id]/reconcile/route.js +93 -0
  22. package/dist/api/viva/admin/connected-accounts/[id]/reconcile/route.js.map +1 -0
  23. package/dist/api/viva/admin/connected-accounts/[id]/route.d.ts +18 -0
  24. package/dist/api/viva/admin/connected-accounts/[id]/route.d.ts.map +1 -0
  25. package/dist/api/viva/admin/connected-accounts/[id]/route.js +59 -0
  26. package/dist/api/viva/admin/connected-accounts/[id]/route.js.map +1 -0
  27. package/dist/api/viva/admin/connected-accounts/[id]/sources/route.d.ts +34 -0
  28. package/dist/api/viva/admin/connected-accounts/[id]/sources/route.d.ts.map +1 -0
  29. package/dist/api/viva/admin/connected-accounts/[id]/sources/route.js +234 -0
  30. package/dist/api/viva/admin/connected-accounts/[id]/sources/route.js.map +1 -0
  31. package/dist/api/viva/admin/connected-accounts/route.d.ts +19 -0
  32. package/dist/api/viva/admin/connected-accounts/route.d.ts.map +1 -0
  33. package/dist/api/viva/admin/connected-accounts/route.js +78 -0
  34. package/dist/api/viva/admin/connected-accounts/route.js.map +1 -0
  35. package/dist/api/viva/internal/auth-status/route.d.ts +19 -0
  36. package/dist/api/viva/internal/auth-status/route.d.ts.map +1 -0
  37. package/dist/api/viva/internal/auth-status/route.js +91 -0
  38. package/dist/api/viva/internal/auth-status/route.js.map +1 -0
  39. package/dist/api/viva/internal/metrics/route.d.ts +13 -0
  40. package/dist/api/viva/internal/metrics/route.d.ts.map +1 -0
  41. package/dist/api/viva/internal/metrics/route.js +48 -0
  42. package/dist/api/viva/internal/metrics/route.js.map +1 -0
  43. package/dist/api/viva/webhook/health/route.d.ts +16 -0
  44. package/dist/api/viva/webhook/health/route.d.ts.map +1 -0
  45. package/dist/api/viva/webhook/health/route.js +27 -0
  46. package/dist/api/viva/webhook/health/route.js.map +1 -0
  47. package/dist/api/viva/webhook/route.d.ts +57 -0
  48. package/dist/api/viva/webhook/route.d.ts.map +1 -0
  49. package/dist/api/viva/webhook/route.js +269 -0
  50. package/dist/api/viva/webhook/route.js.map +1 -0
  51. package/dist/cli/bin.d.ts +12 -0
  52. package/dist/cli/bin.d.ts.map +1 -0
  53. package/dist/cli/bin.js +78 -0
  54. package/dist/cli/bin.js.map +1 -0
  55. package/dist/cli/index.d.ts +12 -0
  56. package/dist/cli/index.d.ts.map +1 -0
  57. package/dist/cli/index.js +14 -0
  58. package/dist/cli/index.js.map +1 -0
  59. package/dist/cli/plan.d.ts +51 -0
  60. package/dist/cli/plan.d.ts.map +1 -0
  61. package/dist/cli/plan.js +128 -0
  62. package/dist/cli/plan.js.map +1 -0
  63. package/dist/cli/register-webhooks.d.ts +54 -0
  64. package/dist/cli/register-webhooks.d.ts.map +1 -0
  65. package/dist/cli/register-webhooks.js +366 -0
  66. package/dist/cli/register-webhooks.js.map +1 -0
  67. package/dist/cli/types.d.ts +62 -0
  68. package/dist/cli/types.d.ts.map +1 -0
  69. package/dist/cli/types.js +12 -0
  70. package/dist/cli/types.js.map +1 -0
  71. package/dist/config.d.ts +158 -0
  72. package/dist/config.d.ts.map +1 -0
  73. package/dist/config.js +236 -0
  74. package/dist/config.js.map +1 -0
  75. package/dist/index.d.ts +21 -0
  76. package/dist/index.d.ts.map +1 -0
  77. package/dist/index.js +29 -0
  78. package/dist/index.js.map +1 -0
  79. package/dist/loaders/viva-oauth2-strategy.d.ts +26 -0
  80. package/dist/loaders/viva-oauth2-strategy.d.ts.map +1 -0
  81. package/dist/loaders/viva-oauth2-strategy.js +58 -0
  82. package/dist/loaders/viva-oauth2-strategy.js.map +1 -0
  83. package/dist/migrations/Migration_20260425000001_init_viva_payments.d.ts +19 -0
  84. package/dist/migrations/Migration_20260425000001_init_viva_payments.d.ts.map +1 -0
  85. package/dist/migrations/Migration_20260425000001_init_viva_payments.js +136 -0
  86. package/dist/migrations/Migration_20260425000001_init_viva_payments.js.map +1 -0
  87. package/dist/migrations/Migration_20260425000002_allow_null_order_code.d.ts +31 -0
  88. package/dist/migrations/Migration_20260425000002_allow_null_order_code.d.ts.map +1 -0
  89. package/dist/migrations/Migration_20260425000002_allow_null_order_code.js +71 -0
  90. package/dist/migrations/Migration_20260425000002_allow_null_order_code.js.map +1 -0
  91. package/dist/migrations/Migration_20260425000003_webhook_retry_count.d.ts +18 -0
  92. package/dist/migrations/Migration_20260425000003_webhook_retry_count.d.ts.map +1 -0
  93. package/dist/migrations/Migration_20260425000003_webhook_retry_count.js +42 -0
  94. package/dist/migrations/Migration_20260425000003_webhook_retry_count.js.map +1 -0
  95. package/dist/migrations/Migration_20260425000004_webhook_error_and_nullable_merchant.d.ts +29 -0
  96. package/dist/migrations/Migration_20260425000004_webhook_error_and_nullable_merchant.d.ts.map +1 -0
  97. package/dist/migrations/Migration_20260425000004_webhook_error_and_nullable_merchant.js +74 -0
  98. package/dist/migrations/Migration_20260425000004_webhook_error_and_nullable_merchant.js.map +1 -0
  99. package/dist/models/index.d.ts +7 -0
  100. package/dist/models/index.d.ts.map +1 -0
  101. package/dist/models/index.js +10 -0
  102. package/dist/models/index.js.map +1 -0
  103. package/dist/models/viva-tenant-merchant.d.ts +11 -0
  104. package/dist/models/viva-tenant-merchant.d.ts.map +1 -0
  105. package/dist/models/viva-tenant-merchant.js +54 -0
  106. package/dist/models/viva-tenant-merchant.js.map +1 -0
  107. package/dist/models/viva-transaction.d.ts +34 -0
  108. package/dist/models/viva-transaction.d.ts.map +1 -0
  109. package/dist/models/viva-transaction.js +104 -0
  110. package/dist/models/viva-transaction.js.map +1 -0
  111. package/dist/models/viva-webhook-event.d.ts +32 -0
  112. package/dist/models/viva-webhook-event.d.ts.map +1 -0
  113. package/dist/models/viva-webhook-event.js +88 -0
  114. package/dist/models/viva-webhook-event.js.map +1 -0
  115. package/dist/observability/config.d.ts +34 -0
  116. package/dist/observability/config.d.ts.map +1 -0
  117. package/dist/observability/config.js +57 -0
  118. package/dist/observability/config.js.map +1 -0
  119. package/dist/observability/index.d.ts +8 -0
  120. package/dist/observability/index.d.ts.map +1 -0
  121. package/dist/observability/index.js +15 -0
  122. package/dist/observability/index.js.map +1 -0
  123. package/dist/observability/prom-metrics.d.ts +41 -0
  124. package/dist/observability/prom-metrics.d.ts.map +1 -0
  125. package/dist/observability/prom-metrics.js +219 -0
  126. package/dist/observability/prom-metrics.js.map +1 -0
  127. package/dist/providers/payment-provider.d.ts +19 -0
  128. package/dist/providers/payment-provider.d.ts.map +1 -0
  129. package/dist/providers/payment-provider.js +24 -0
  130. package/dist/providers/payment-provider.js.map +1 -0
  131. package/dist/resolvers/auth-strategy-factory.d.ts +42 -0
  132. package/dist/resolvers/auth-strategy-factory.d.ts.map +1 -0
  133. package/dist/resolvers/auth-strategy-factory.js +60 -0
  134. package/dist/resolvers/auth-strategy-factory.js.map +1 -0
  135. package/dist/resolvers/tenant-resolver.d.ts +104 -0
  136. package/dist/resolvers/tenant-resolver.d.ts.map +1 -0
  137. package/dist/resolvers/tenant-resolver.js +118 -0
  138. package/dist/resolvers/tenant-resolver.js.map +1 -0
  139. package/dist/service.d.ts +200 -0
  140. package/dist/service.d.ts.map +1 -0
  141. package/dist/service.js +1003 -0
  142. package/dist/service.js.map +1 -0
  143. package/dist/subscribers/index.d.ts +5 -0
  144. package/dist/subscribers/index.d.ts.map +1 -0
  145. package/dist/subscribers/index.js +10 -0
  146. package/dist/subscribers/index.js.map +1 -0
  147. package/dist/subscribers/viva-webhook-event.d.ts +38 -0
  148. package/dist/subscribers/viva-webhook-event.d.ts.map +1 -0
  149. package/dist/subscribers/viva-webhook-event.js +133 -0
  150. package/dist/subscribers/viva-webhook-event.js.map +1 -0
  151. package/dist/workflows/cleanup-old-webhook-events.d.ts +39 -0
  152. package/dist/workflows/cleanup-old-webhook-events.d.ts.map +1 -0
  153. package/dist/workflows/cleanup-old-webhook-events.js +68 -0
  154. package/dist/workflows/cleanup-old-webhook-events.js.map +1 -0
  155. package/dist/workflows/index.d.ts +14 -0
  156. package/dist/workflows/index.d.ts.map +1 -0
  157. package/dist/workflows/index.js +19 -0
  158. package/dist/workflows/index.js.map +1 -0
  159. package/dist/workflows/per-tenant-semaphore.d.ts +47 -0
  160. package/dist/workflows/per-tenant-semaphore.d.ts.map +1 -0
  161. package/dist/workflows/per-tenant-semaphore.js +89 -0
  162. package/dist/workflows/per-tenant-semaphore.js.map +1 -0
  163. package/dist/workflows/process-webhook-event.d.ts +80 -0
  164. package/dist/workflows/process-webhook-event.d.ts.map +1 -0
  165. package/dist/workflows/process-webhook-event.js +280 -0
  166. package/dist/workflows/process-webhook-event.js.map +1 -0
  167. package/dist/workflows/reprocess-unresolved-tenants.d.ts +58 -0
  168. package/dist/workflows/reprocess-unresolved-tenants.d.ts.map +1 -0
  169. package/dist/workflows/reprocess-unresolved-tenants.js +121 -0
  170. package/dist/workflows/reprocess-unresolved-tenants.js.map +1 -0
  171. package/package.json +63 -0
@@ -0,0 +1,1003 @@
1
+ "use strict";
2
+ /**
3
+ * service.ts — VivaPaymentProvider (Medusa v2 AbstractPaymentProvider)
4
+ *
5
+ * Implements all required Medusa v2 payment provider methods, bridging the
6
+ * Medusa payment lifecycle to the Viva Wallet ISV Smart Checkout flow.
7
+ *
8
+ * Key design choices:
9
+ * - A4 (write-pending-first): INSERT into viva_transaction with status='initiated'
10
+ * and viva_order_code=NULL BEFORE calling Viva's createOrder API.
11
+ * - P14 (idempotency): deduplicate by idempotency_key = medusa_payment_id.
12
+ * - P18 (refund validation): validate amount <= captured - refunded before Viva API.
13
+ * - P19 (single-tenant invariant): assertSingleTenantCart before any DB write.
14
+ * - Error model (plan lines 339–344): wrap all Viva errors into MedusaError types.
15
+ *
16
+ * AbstractPaymentProvider required methods (read from @medusajs/utils dist):
17
+ * capturePayment, authorizePayment, cancelPayment, initiatePayment,
18
+ * deletePayment, getPaymentStatus, refundPayment, retrievePayment,
19
+ * updatePayment, getWebhookActionAndData
20
+ *
21
+ * Smart Checkout redirect URL:
22
+ * - Demo: https://demo.vivapayments.com/web/checkout?ref={OrderCode}
23
+ * - Production: https://www.vivapayments.com/web/checkout?ref={OrderCode}
24
+ *
25
+ * @see references/viva-docs/md/payment-isv-api.txt:1 (ISV API overview)
26
+ * @see references/viva-docs/md/smart-checkout-save-payment.txt:1 (redirect URL)
27
+ * @see references/viva-docs/md/tut-create-recurring-payment.txt:1 (checkout URL format)
28
+ * @see references/viva-docs/md/isv-partner-program.txt:61 (P14, P15, P18, P19, A4)
29
+ * @see references/viva-docs/md/isv-credentials.txt:107 (auth credential types)
30
+ * @see references/viva-docs/md/oauth2-authentication.txt:128 (token endpoint)
31
+ */
32
+ Object.defineProperty(exports, "__esModule", { value: true });
33
+ exports.VivaPaymentProvider = void 0;
34
+ const utils_1 = require("@medusajs/framework/utils");
35
+ const uuid_1 = require("uuid");
36
+ const isv_1 = require("@sakeetech/viva-payments-core/isv");
37
+ const payments_1 = require("@sakeetech/viva-payments-core/payments");
38
+ const legacy_1 = require("@sakeetech/viva-payments-core/legacy");
39
+ const refunds_1 = require("@sakeetech/viva-payments-core/refunds");
40
+ const errors_1 = require("@sakeetech/viva-payments-core/errors");
41
+ const webhooks_1 = require("@sakeetech/viva-payments-core/webhooks");
42
+ const tenant_resolver_js_1 = require("./resolvers/tenant-resolver.js");
43
+ const auth_strategy_factory_js_1 = require("./resolvers/auth-strategy-factory.js");
44
+ // ---------------------------------------------------------------------------
45
+ // Smart Checkout redirect URL builder
46
+ // ---------------------------------------------------------------------------
47
+ /**
48
+ * Builds the Smart Checkout redirect URL for a given order code.
49
+ *
50
+ * Pattern confirmed from Viva docs:
51
+ * Demo: https://demo.vivapayments.com/web/checkout?ref={OrderCode}
52
+ * Production: https://www.vivapayments.com/web/checkout?ref={OrderCode}
53
+ *
54
+ * @see references/viva-docs/md/tut-create-recurring-payment.txt:1 (URL format)
55
+ * @see references/viva-docs/md/smart-checkout-save-payment.txt:1 (Smart Checkout)
56
+ */
57
+ function buildCheckoutUrl(environment, orderCode) {
58
+ const baseUrl = environment === 'demo'
59
+ ? 'https://demo.vivapayments.com'
60
+ : 'https://www.vivapayments.com';
61
+ // @see references/viva-docs/md/tut-create-recurring-payment.txt:1
62
+ return `${baseUrl}/web/checkout?ref=${orderCode.toString()}`;
63
+ }
64
+ // ---------------------------------------------------------------------------
65
+ // ISO 4217 numeric currency code lookup
66
+ // ---------------------------------------------------------------------------
67
+ /**
68
+ * Maps ISO 3-char alphabetic currency codes to ISO 4217 numeric codes.
69
+ * Viva expects numeric codes in the createOrder request body.
70
+ *
71
+ * TODO(impl): extend this map for all currencies Viva supports.
72
+ * Currently covers the primary markets listed in Viva docs.
73
+ *
74
+ * @see references/viva-docs/md/isv-partner-program.txt:83 (P15 currency encoding)
75
+ */
76
+ const CURRENCY_ALPHA_TO_NUMERIC = {
77
+ eur: '978',
78
+ gbp: '826',
79
+ usd: '840',
80
+ pln: '985',
81
+ ron: '946',
82
+ czk: '203',
83
+ huf: '348',
84
+ bgn: '975',
85
+ dkk: '208',
86
+ sek: '752',
87
+ nok: '578',
88
+ };
89
+ function toCurrencyCode(isoAlpha) {
90
+ const numeric = CURRENCY_ALPHA_TO_NUMERIC[isoAlpha.toLowerCase()];
91
+ if (!numeric) {
92
+ // TODO(impl): add full ISO 4217 numeric lookup or accept numeric codes directly
93
+ // Default to EUR (978) if unknown — operator should configure the currency correctly.
94
+ console.warn(`[viva] Unknown currency code '${isoAlpha}', defaulting to EUR (978). ` +
95
+ `Add the mapping in CURRENCY_ALPHA_TO_NUMERIC if needed.`);
96
+ return '978';
97
+ }
98
+ return numeric;
99
+ }
100
+ // ---------------------------------------------------------------------------
101
+ // Medusa status mapping (plan P17)
102
+ // ---------------------------------------------------------------------------
103
+ /**
104
+ * Maps internal VivaTransactionStatus to Medusa PaymentSessionStatus.
105
+ *
106
+ * Medusa status values: 'authorized' | 'captured' | 'pending' | 'requires_more'
107
+ * | 'error' | 'canceled'
108
+ *
109
+ * @see references/viva-docs/md/isv-partner-program.txt:61 (P17 status lattice)
110
+ */
111
+ function toMedusaStatus(vivaStatus) {
112
+ switch (vivaStatus) {
113
+ case 'authorized':
114
+ return 'authorized';
115
+ case 'captured':
116
+ return 'authorized'; // Medusa treats captured as authorized from session perspective
117
+ case 'initiated':
118
+ return 'pending';
119
+ case 'failed':
120
+ return 'error';
121
+ case 'cancelled':
122
+ return 'canceled';
123
+ case 'refunded':
124
+ return 'authorized'; // session is complete; refunds tracked separately
125
+ case 'disputed':
126
+ return 'requires_more';
127
+ default:
128
+ return 'pending';
129
+ }
130
+ }
131
+ // ---------------------------------------------------------------------------
132
+ // Error mapping (plan lines 339–344)
133
+ // ---------------------------------------------------------------------------
134
+ /**
135
+ * Maps Viva error types to the appropriate MedusaError.
136
+ *
137
+ * Mapping (plan lines 339–344):
138
+ * VivaAuthError → MedusaError.Types.UNAUTHORIZED
139
+ * VivaApiError → MedusaError.Types.PAYMENT_AUTHORIZATION_ERROR
140
+ * VivaValidationError → MedusaError.Types.INVALID_DATA
141
+ * VivaRateLimitError → MedusaError.Types.PAYMENT_AUTHORIZATION_ERROR (retriable)
142
+ * VivaModeMismatchError → MedusaError.Types.NOT_ALLOWED
143
+ * MedusaError → pass through unchanged
144
+ * Other → MedusaError.Types.UNEXPECTED_STATE
145
+ *
146
+ * @see references/viva-docs/md/isv-partner-program.txt:104 (error model, plan 339)
147
+ */
148
+ function toMedusaError(err) {
149
+ if (err instanceof utils_1.MedusaError) {
150
+ return err;
151
+ }
152
+ if (err instanceof errors_1.VivaModeMismatchError) {
153
+ return new utils_1.MedusaError(utils_1.MedusaError.Types.NOT_ALLOWED, err.message);
154
+ }
155
+ if (err instanceof errors_1.VivaAuthError) {
156
+ return new utils_1.MedusaError(utils_1.MedusaError.Types.UNAUTHORIZED, err.message);
157
+ }
158
+ if (err instanceof errors_1.VivaRateLimitError) {
159
+ return new utils_1.MedusaError(utils_1.MedusaError.Types.PAYMENT_AUTHORIZATION_ERROR, err.message);
160
+ }
161
+ if (err instanceof errors_1.VivaApiError) {
162
+ return new utils_1.MedusaError(utils_1.MedusaError.Types.PAYMENT_AUTHORIZATION_ERROR, err.message);
163
+ }
164
+ if (err instanceof errors_1.VivaValidationError) {
165
+ return new utils_1.MedusaError(utils_1.MedusaError.Types.INVALID_DATA, err.message);
166
+ }
167
+ const message = err instanceof Error ? err.message : String(err);
168
+ return new utils_1.MedusaError(utils_1.MedusaError.Types.UNEXPECTED_STATE, message);
169
+ }
170
+ // ---------------------------------------------------------------------------
171
+ // Mode-aware client construction (slice B)
172
+ // ---------------------------------------------------------------------------
173
+ /**
174
+ * Default Smart Checkout source code for merchant mode when `config.sourceCode`
175
+ * is unset. Viva accepts this on any merchant account out of the box.
176
+ *
177
+ * @see references/viva-docs/md/payment-source-for-isv.txt:101
178
+ */
179
+ const DEFAULT_SOURCE_CODE = 'Default';
180
+ /**
181
+ * Build the right {@link BasicAuthClient} (legacy host + Basic auth) per mode.
182
+ *
183
+ * Both modes use the merchant variant in slice B — the reseller variant is only
184
+ * needed for IsvSources (`POST /api/sources`), which the payment provider does
185
+ * not call. In ISV mode, the reseller-flavoured client is still built by
186
+ * {@link buildAuthStrategies} for OAuth2 401-fallback.
187
+ *
188
+ * @see docs/AUTH.md §6.2
189
+ */
190
+ function buildLegacyClient(config) {
191
+ return new legacy_1.BasicAuthClient({
192
+ authVariant: 'merchant',
193
+ environment: config.environment,
194
+ merchantId: config.legacyMerchantId,
195
+ apiKey: config.legacyApiKey,
196
+ });
197
+ }
198
+ /**
199
+ * Build a mode-aware {@link Payments} instance.
200
+ *
201
+ * In merchant mode the constructed client emits URL paths without `/isv` and
202
+ * without the `merchantId={uuid}` query parameter. The `Payments` class
203
+ * silently ignores `opts.merchantId` when constructed with `mode='merchant'`.
204
+ *
205
+ * @see docs/plans/multi-mode-v0.md §9
206
+ */
207
+ function buildPaymentsClient(config, httpClient, legacyClient) {
208
+ return new payments_1.Payments({
209
+ mode: config.mode,
210
+ client: httpClient,
211
+ legacyClient,
212
+ });
213
+ }
214
+ /**
215
+ * Build a {@link FastRefundClient} bound to the OAuth2 acquiring scope.
216
+ *
217
+ * Caller is expected to fall back to standard refund on HTTP 403 when the
218
+ * refund strategy is `'auto'`; when strategy is `'fast'` a 403 surfaces as
219
+ * `VIVA_FAST_REFUND_INELIGIBLE`.
220
+ *
221
+ * @see docs/ENDPOINTS.md §4
222
+ */
223
+ function buildFastRefundClient(httpClient) {
224
+ return new refunds_1.FastRefundClient({ client: httpClient });
225
+ }
226
+ /**
227
+ * Resolve the Viva merchantId to pass through to `Payments` calls per mode.
228
+ *
229
+ * In ISV mode this is the per-tenant merchant resolved from the cart. In
230
+ * merchant mode there is no per-cart tenant — the `Payments` client ignores
231
+ * `opts.merchantId` entirely so the value is irrelevant, but we still pass
232
+ * the configured legacy merchant id for consistency in stored DB rows.
233
+ */
234
+ function effectiveMerchantId(config, tenantMerchantId) {
235
+ // In ISV mode the tenant resolver always returns a value before we get here.
236
+ if (config.mode === 'isv' && tenantMerchantId)
237
+ return tenantMerchantId;
238
+ return config.legacyMerchantId;
239
+ }
240
+ // ---------------------------------------------------------------------------
241
+ // VivaPaymentProvider
242
+ // ---------------------------------------------------------------------------
243
+ /**
244
+ * Medusa v2 payment provider for Viva Wallet Smart Checkout (ISV multi-tenant).
245
+ *
246
+ * Identifier: 'viva'. Payment provider ID format: pp_viva_<id>.
247
+ * The <id> comes from the `id` field in medusa-config.ts providers array.
248
+ *
249
+ * @see references/viva-docs/md/payment-isv-api.txt:1 (ISV payment creation)
250
+ * @see references/viva-docs/md/smart-checkout-save-payment.txt:1 (redirect URL)
251
+ * @see references/viva-docs/md/isv-partner-program.txt:61 (ISV overview)
252
+ */
253
+ class VivaPaymentProvider extends utils_1.AbstractPaymentProvider {
254
+ static identifier = 'viva';
255
+ vivaConfig;
256
+ isvPayments;
257
+ legacyClient;
258
+ fastRefundClient;
259
+ tenantResolver;
260
+ em;
261
+ constructor(container, options) {
262
+ super(container, options);
263
+ this.vivaConfig = options.config;
264
+ // Build auth strategies from config
265
+ const authStrategies = (0, auth_strategy_factory_js_1.buildAuthStrategies)(options.config);
266
+ // Build ISV HTTP client with OAuth2 primary strategy
267
+ const httpClient = new isv_1.IsvHttpClient({
268
+ environment: options.config.environment,
269
+ authStrategy: authStrategies.primary,
270
+ });
271
+ // Build legacy Basic-auth client for refundPayment + Standard refund path.
272
+ // Probe-verified 2026-04-25 (F1): POST /checkout/v2/transactions/{id} returns 405.
273
+ // Refund must use legacy host with Basic auth (merchantId:apiKey).
274
+ // @see references/viva-docs/md/tut-create-recurring-payment.txt:288
275
+ this.legacyClient = buildLegacyClient(options.config);
276
+ // Slice B: build the `Payments` client mode-aware. In merchant mode the
277
+ // URL paths do NOT include the `/isv` segment and do NOT carry
278
+ // `merchantId={uuid}` — see Payments.client.ts buildOrderCreateUrl etc.
279
+ // @see docs/plans/multi-mode-v0.md §9
280
+ this.isvPayments = buildPaymentsClient(options.config, httpClient, this.legacyClient);
281
+ // Fast Refund client — used by the merchant-mode refundPayment flow when
282
+ // strategy resolves to 'fast' (auto or explicit).
283
+ // @see docs/ENDPOINTS.md §4
284
+ this.fastRefundClient = buildFastRefundClient(httpClient);
285
+ // EntityManager from container for DB access.
286
+ // TODO(impl): verify the exact container key for Mikro-ORM EM in Medusa v2.
287
+ // Standard pattern in Medusa v2: container['manager'] for the base EM.
288
+ this.em = (container['manager'] ?? container['entityManager']);
289
+ if (options.tenantResolver) {
290
+ this.tenantResolver = options.tenantResolver;
291
+ }
292
+ else {
293
+ this.tenantResolver = new tenant_resolver_js_1.DefaultTenantResolver(this.em);
294
+ }
295
+ }
296
+ // --------------------------------------------------------------------------
297
+ // initiatePayment
298
+ // --------------------------------------------------------------------------
299
+ /**
300
+ * Initiates a payment session (Medusa calls this when customer selects Viva):
301
+ * 1. P19: assert single-tenant cart (from input.context)
302
+ * 2. Resolve tenant → Viva merchant
303
+ * 3. P14 dedup: check for existing transaction by idempotency_key
304
+ * 4. A4 write-pending-first: INSERT transaction row BEFORE Viva API call
305
+ * 5. Call Viva createOrder
306
+ * 6. Update row with viva_order_code
307
+ * 7. Return redirect URL in data (storefront uses this to redirect customer)
308
+ *
309
+ * The medusa_payment_id is derived from input.context.idempotency_key (set by
310
+ * Medusa Payment Module) or generated per call.
311
+ *
312
+ * @see references/viva-docs/md/payment-isv-api.txt:1 (createOrder)
313
+ * @see references/viva-docs/md/smart-checkout-save-payment.txt:1 (redirect URL)
314
+ * @see references/viva-docs/md/isv-partner-program.txt:61 (P14, P19, A4)
315
+ */
316
+ async initiatePayment(input) {
317
+ try {
318
+ // Extract cart-like context for tenant resolution and P19 validation.
319
+ // Medusa passes cart metadata via input.context. The actual cart is
320
+ // not available here — only the customer context.
321
+ // P19: validate that we have a single tenant (from context metadata if available)
322
+ // TODO(impl): when Medusa passes cart items in context, pass them here.
323
+ // For now assertSingleTenantCart is a no-op when items is empty.
324
+ const contextMetadata = (input.data?.['cart_metadata'] ??
325
+ input.data?.['metadata']);
326
+ const cartLike = {
327
+ id: input.data?.['cart_id'] ?? '',
328
+ metadata: contextMetadata ?? null,
329
+ items: input.data?.['items'] ?? [],
330
+ };
331
+ // P19: validate single-tenant cart before any DB write (ISV mode only —
332
+ // merchant mode has no per-cart tenant concept).
333
+ if (this.vivaConfig.mode === 'isv') {
334
+ (0, tenant_resolver_js_1.assertSingleTenantCart)(cartLike);
335
+ }
336
+ // Resolve tenant → Viva merchant per mode:
337
+ // ISV mode: resolveTenant from cart → vivaMerchantId, sourceCode.
338
+ // Merchant mode: no per-cart tenant — vivaMerchantId is undefined and
339
+ // sourceCode falls back to config.sourceCode ?? 'Default'.
340
+ // @see docs/plans/multi-mode-v0.md §9
341
+ let vivaMerchantId;
342
+ let sourceCode;
343
+ if (this.vivaConfig.mode === 'isv') {
344
+ const { tenantId } = await this.tenantResolver.resolveTenantFromCart(cartLike);
345
+ const account = await this.tenantResolver.resolveVivaAccount(tenantId);
346
+ vivaMerchantId = account.vivaMerchantId;
347
+ // ISV mode currently has no per-tenant sourceCode — use 'Default'.
348
+ // TODO(slice-e): allow per-tenant sourceCode via tenant resolver.
349
+ sourceCode = DEFAULT_SOURCE_CODE;
350
+ }
351
+ else {
352
+ vivaMerchantId = undefined;
353
+ sourceCode = this.vivaConfig.sourceCode ?? DEFAULT_SOURCE_CODE;
354
+ }
355
+ // Use Medusa's idempotency_key if provided, else use existing data's session id
356
+ const medusaPaymentId = input.context?.idempotency_key ??
357
+ input.data?.['session_id'] ??
358
+ (0, uuid_1.v4)();
359
+ const idempotencyKey = medusaPaymentId;
360
+ // P14 dedup: check existing transaction
361
+ const repo = this.em.getRepository('VivaTransaction');
362
+ const existing = await repo.findOne({ idempotency_key: idempotencyKey });
363
+ if (existing &&
364
+ existing.status !== 'initiated' &&
365
+ existing.status !== 'failed') {
366
+ // Idempotent re-call: return existing data
367
+ const redirectUrl = existing.viva_order_code
368
+ ? buildCheckoutUrl(this.vivaConfig.environment, BigInt(existing.viva_order_code))
369
+ : null;
370
+ return {
371
+ id: medusaPaymentId,
372
+ data: {
373
+ viva_transaction_id: existing.viva_transaction_id,
374
+ order_code: existing.viva_order_code ?? null,
375
+ redirect_url: redirectUrl,
376
+ viva_status: existing.status,
377
+ },
378
+ };
379
+ }
380
+ // Compute amount in minor units.
381
+ // Medusa passes amount as a BigNumber-compatible value.
382
+ // @see references/viva-docs/md/isv-partner-program.txt:83 (P15 amounts)
383
+ const amountMinor = BigInt(Math.round(Number(input.amount) * 100));
384
+ const currencyCode = toCurrencyCode(input.currency_code);
385
+ // A4 write-pending-first: INSERT before Viva API call.
386
+ // viva_order_code is NULL until createOrder returns.
387
+ // Migration_20260425000002 makes this column nullable.
388
+ // @see references/viva-docs/md/isv-partner-program.txt:61 (A4)
389
+ const vivaTransactionId = (0, uuid_1.v4)();
390
+ // Slice D made viva_merchant_id nullable. Merchant-mode rows store NULL
391
+ // because there is no per-cart tenant merchant id — the plugin operates
392
+ // a single account.
393
+ // @see Migration_20260425000004_webhook_error_and_nullable_merchant
394
+ const storedMerchantId = vivaMerchantId ?? null;
395
+ if (!existing) {
396
+ const now = new Date();
397
+ const entity = repo.create({
398
+ viva_transaction_id: vivaTransactionId,
399
+ viva_order_code: null,
400
+ medusa_payment_id: medusaPaymentId,
401
+ viva_merchant_id: storedMerchantId,
402
+ status: 'initiated',
403
+ claim_substate: null,
404
+ amount_minor: amountMinor.toString(),
405
+ refunded_amount_minor: '0',
406
+ currency_code: currencyCode,
407
+ idempotency_key: idempotencyKey,
408
+ raw_payload: null,
409
+ created_at: now,
410
+ updated_at: now,
411
+ });
412
+ await this.em.persistAndFlush(entity);
413
+ }
414
+ // Step 5: call Viva createOrder.
415
+ // - ISV mode: merchantId from tenant resolution; URL is /isv/orders.
416
+ // - Merchant mode: merchantId omitted; URL is /checkout/v2/orders.
417
+ // `Payments` silently ignores opts.merchantId in merchant mode.
418
+ // @see references/viva-docs/md/payment-source-for-isv.txt:101
419
+ const createResult = await this.isvPayments.createOrder({
420
+ amount: amountMinor,
421
+ currencyCode,
422
+ merchantTrns: medusaPaymentId,
423
+ customerTrns: `Payment via Viva Wallet`,
424
+ sourceCode,
425
+ }, {
426
+ ...(vivaMerchantId !== undefined ? { merchantId: vivaMerchantId } : {}),
427
+ idempotencyKey,
428
+ });
429
+ const orderCode = createResult.orderCode;
430
+ // Step 6: update the pending row with the order code
431
+ const txId = existing?.viva_transaction_id ?? vivaTransactionId;
432
+ const txRow = await repo.findOne({ viva_transaction_id: txId });
433
+ if (txRow) {
434
+ txRow.viva_order_code = orderCode.toString();
435
+ await this.em.flush();
436
+ }
437
+ // Step 7: build redirect URL and return.
438
+ // Storefront reads data.redirect_url to redirect the customer.
439
+ // @see references/viva-docs/md/smart-checkout-save-payment.txt:1
440
+ const redirectUrl = buildCheckoutUrl(this.vivaConfig.environment, orderCode);
441
+ return {
442
+ id: medusaPaymentId,
443
+ data: {
444
+ viva_transaction_id: txId,
445
+ order_code: orderCode.toString(),
446
+ redirect_url: redirectUrl,
447
+ viva_status: 'initiated',
448
+ },
449
+ };
450
+ }
451
+ catch (err) {
452
+ throw toMedusaError(err);
453
+ }
454
+ }
455
+ // --------------------------------------------------------------------------
456
+ // authorizePayment
457
+ // --------------------------------------------------------------------------
458
+ /**
459
+ * Consults the DB for the latest transaction status.
460
+ * For Smart Checkout, authorization is off-band (redirect + webhook).
461
+ * Called during cart-completion; returns current DB status.
462
+ *
463
+ * @see references/viva-docs/md/payment-isv-api.txt:1 (Smart Checkout flow)
464
+ * @see references/viva-docs/md/isv-partner-program.txt:61 (P6 Smart Checkout)
465
+ */
466
+ async authorizePayment(input) {
467
+ try {
468
+ const medusaPaymentId = extractPaymentId(input.data);
469
+ const repo = this.em.getRepository('VivaTransaction');
470
+ const row = medusaPaymentId
471
+ ? await repo.findOne({ medusa_payment_id: medusaPaymentId })
472
+ : null;
473
+ if (!row) {
474
+ return { status: 'pending', data: input.data ?? {} };
475
+ }
476
+ const status = toMedusaStatus(row.status);
477
+ return {
478
+ status,
479
+ data: {
480
+ ...(input.data ?? {}),
481
+ viva_transaction_id: row.viva_transaction_id,
482
+ order_code: row.viva_order_code ?? null,
483
+ viva_status: row.status,
484
+ },
485
+ };
486
+ }
487
+ catch (err) {
488
+ throw toMedusaError(err);
489
+ }
490
+ }
491
+ // --------------------------------------------------------------------------
492
+ // capturePayment
493
+ // --------------------------------------------------------------------------
494
+ /**
495
+ * Capture is implicit on Smart Checkout completion (1796 webhook).
496
+ * This method is idempotent: if already captured, returns success.
497
+ * If not yet captured, logs a warning and returns current state.
498
+ *
499
+ * NOTE: Do NOT call Viva's capture endpoint. Capture is automatic on
500
+ * Smart Checkout completion per Viva's ISV model.
501
+ *
502
+ * @see references/viva-docs/md/payment-isv-api.txt:1 (implicit capture)
503
+ * @see references/viva-docs/md/isv-partner-program.txt:61 (P6 Smart Checkout)
504
+ */
505
+ async capturePayment(input) {
506
+ try {
507
+ const medusaPaymentId = extractPaymentId(input.data);
508
+ const repo = this.em.getRepository('VivaTransaction');
509
+ const row = medusaPaymentId
510
+ ? await repo.findOne({ medusa_payment_id: medusaPaymentId })
511
+ : null;
512
+ if (!row) {
513
+ return { data: input.data ?? {} };
514
+ }
515
+ if (row.status === 'captured') {
516
+ return {
517
+ data: {
518
+ ...(input.data ?? {}),
519
+ viva_transaction_id: row.viva_transaction_id,
520
+ order_code: row.viva_order_code ?? null,
521
+ viva_status: row.status,
522
+ },
523
+ };
524
+ }
525
+ // Not yet captured — asynchronous via webhook 1796
526
+ console.warn(`[viva] capturePayment called on transaction ${row.viva_transaction_id} ` +
527
+ `with status '${row.status}' (not 'captured'). ` +
528
+ `Smart Checkout capture happens asynchronously via Viva webhook 1796. ` +
529
+ `No Viva API call made.`);
530
+ return {
531
+ data: {
532
+ ...(input.data ?? {}),
533
+ viva_transaction_id: row.viva_transaction_id,
534
+ order_code: row.viva_order_code ?? null,
535
+ viva_status: row.status,
536
+ },
537
+ };
538
+ }
539
+ catch (err) {
540
+ throw toMedusaError(err);
541
+ }
542
+ }
543
+ // --------------------------------------------------------------------------
544
+ // refundPayment
545
+ // --------------------------------------------------------------------------
546
+ /**
547
+ * Issues a full or partial refund via Viva ISV API.
548
+ *
549
+ * P18 validation: refundAmountMinor <= capturedAmount - alreadyRefunded.
550
+ * Auth: primary OAuth2 strategy (Q5 — if Viva rejects with 401/403, reseller
551
+ * auth may be required; log warning and re-raise for now).
552
+ * Partial refund: increments refunded_amount_minor; status stays 'captured'.
553
+ * Full refund: transitions status to 'refunded' via lattice.
554
+ *
555
+ * @see references/viva-docs/md/isv-partner-program.txt:296 (P18 refund, ISV fee reversal)
556
+ * @see references/viva-docs/md/payment-isv-api.txt:1 (refund endpoint)
557
+ */
558
+ async refundPayment(input) {
559
+ try {
560
+ const medusaPaymentId = extractPaymentId(input.data);
561
+ if (!medusaPaymentId) {
562
+ throw new errors_1.VivaValidationError({
563
+ message: 'refundPayment: medusa_payment_id is required in input.data',
564
+ });
565
+ }
566
+ const repo = this.em.getRepository('VivaTransaction');
567
+ const row = await repo.findOne({ medusa_payment_id: medusaPaymentId });
568
+ if (!row) {
569
+ throw new errors_1.VivaValidationError({
570
+ message: `Transaction not found for medusa_payment_id '${medusaPaymentId}'`,
571
+ });
572
+ }
573
+ if (row.status !== 'captured') {
574
+ throw new errors_1.VivaValidationError({
575
+ message: `Cannot refund transaction '${row.viva_transaction_id}': ` +
576
+ `status is '${row.status}', expected 'captured'.`,
577
+ });
578
+ }
579
+ const capturedAmount = BigInt(row.amount_minor);
580
+ const alreadyRefunded = BigInt(row.refunded_amount_minor);
581
+ const available = capturedAmount - alreadyRefunded;
582
+ // input.amount is BigNumberInput (number | string | BigNumber).
583
+ // Convert to minor units (Medusa tracks amounts in currency major units for display).
584
+ // TODO(impl): confirm whether Medusa passes refund amount in minor or major units.
585
+ // Treating as major units (same as initiatePayment amount) for consistency.
586
+ const refundAmountMinor = input.amount !== undefined && input.amount !== null
587
+ ? BigInt(Math.round(Number(input.amount) * 100))
588
+ : available; // full refund if amount not specified
589
+ // P18 validation
590
+ if (refundAmountMinor <= 0n) {
591
+ throw new errors_1.VivaValidationError({
592
+ message: `Refund amount must be > 0, got ${refundAmountMinor}.`,
593
+ });
594
+ }
595
+ if (refundAmountMinor > available) {
596
+ throw new errors_1.VivaValidationError({
597
+ message: `Refund amount ${refundAmountMinor} exceeds available amount ${available} ` +
598
+ `(captured: ${capturedAmount}, already refunded: ${alreadyRefunded}). ` +
599
+ `Plan P18.`,
600
+ });
601
+ }
602
+ // Get the Viva TransactionId for the refund endpoint.
603
+ // The webhook handler (S8) stores this in raw_payload.TransactionId.
604
+ // @see references/viva-docs/md/payment-isv-api.txt:1 (refund uses TransactionId)
605
+ const vivaTransactionId = (row.raw_payload?.['TransactionId'] ??
606
+ row.raw_payload?.['transactionId'] ??
607
+ row.viva_transaction_id);
608
+ // Build idempotency key for the refund: combines refund id + amount to prevent duplicates
609
+ const medusaRefundId = input.data?.['medusa_refund_id'] ?? medusaPaymentId;
610
+ const refundIdempotencyKey = `refund:${medusaRefundId}:${refundAmountMinor}`;
611
+ // Branch refund path on mode:
612
+ // ISV mode → legacy/Basic refund via Payments.refundPayment (unchanged).
613
+ // Merchant mode → resolveRefundStrategy + FastRefundClient, with auto
614
+ // fallback to Standard (legacy/Basic) refund on HTTP 403.
615
+ //
616
+ // @see docs/ENDPOINTS.md §4 (Fast vs Standard matrix)
617
+ // @see docs/plans/multi-mode-v0.md §9
618
+ const rowMerchantId = row.viva_merchant_id;
619
+ if (this.vivaConfig.mode === 'isv') {
620
+ await this.isvPayments.refundPayment(vivaTransactionId, {
621
+ merchantId: rowMerchantId,
622
+ amountMinor: refundAmountMinor,
623
+ idempotencyKey: refundIdempotencyKey,
624
+ });
625
+ }
626
+ else {
627
+ await this.refundPaymentMerchantMode({
628
+ vivaTransactionId,
629
+ refundAmountMinor,
630
+ refundIdempotencyKey,
631
+ medusaRefundId,
632
+ });
633
+ }
634
+ // Update refunded_amount_minor
635
+ const newRefunded = alreadyRefunded + refundAmountMinor;
636
+ row.refunded_amount_minor = newRefunded.toString();
637
+ // Determine new status via lattice.
638
+ // Full refund: captured → refunded. Partial: stays captured.
639
+ // @see references/viva-docs/md/isv-partner-program.txt:296 (P18)
640
+ const isFullRefund = newRefunded >= capturedAmount;
641
+ if (isFullRefund) {
642
+ const transition = (0, webhooks_1.validateStatusTransition)(row.status, 'refunded');
643
+ if (transition.ok) {
644
+ row.status = transition.next;
645
+ }
646
+ }
647
+ await this.em.flush();
648
+ return {
649
+ data: {
650
+ ...(input.data ?? {}),
651
+ viva_transaction_id: row.viva_transaction_id,
652
+ order_code: row.viva_order_code ?? null,
653
+ viva_status: row.status,
654
+ refunded_amount_minor: newRefunded.toString(),
655
+ },
656
+ };
657
+ }
658
+ catch (err) {
659
+ throw toMedusaError(err);
660
+ }
661
+ }
662
+ // --------------------------------------------------------------------------
663
+ // refundPaymentMerchantMode (slice B)
664
+ // --------------------------------------------------------------------------
665
+ /**
666
+ * Merchant-mode refund routing — Fast vs Standard with auto-fallback.
667
+ *
668
+ * Flow:
669
+ * 1. retrieveTransaction → cardType (for refund-strategy decision).
670
+ * 2. resolveRefundStrategy(config.refundStrategy ?? 'auto', { cardType, CNP: true }).
671
+ * 3. If decision.kind === 'fast': call FastRefundClient.refund.
672
+ * - 403 + strategy === 'fast' → throw VivaApiError with vivaCode
673
+ * 'VIVA_FAST_REFUND_INELIGIBLE'.
674
+ * - 403 + strategy === 'auto' → fall through to Standard refund.
675
+ * - any other error → re-throw.
676
+ * 4. Otherwise call BasicAuthClient via Payments.refundPayment (Standard).
677
+ *
678
+ * Smart Checkout is always card-not-present (CNP), so `isCardNotPresent: true`.
679
+ *
680
+ * @see docs/ENDPOINTS.md §4
681
+ * @see docs/plans/multi-mode-v0.md §9
682
+ * @see references/payment-api.yaml:9255 (Fast Refund 403 semantics)
683
+ */
684
+ async refundPaymentMerchantMode(params) {
685
+ const { vivaTransactionId, refundAmountMinor, refundIdempotencyKey, medusaRefundId } = params;
686
+ const configuredStrategy = this.vivaConfig.refundStrategy ?? 'auto';
687
+ // Look up cardType from Viva to feed the strategy decision. Merchant mode
688
+ // passes no merchantId — Payments ignores opts.merchantId when mode==='merchant'.
689
+ let cardType;
690
+ try {
691
+ const tx = await this.isvPayments.retrieveTransaction(vivaTransactionId);
692
+ cardType = tx.cardType;
693
+ }
694
+ catch (e) {
695
+ // retrieveTransaction failure shouldn't block refund — log and proceed
696
+ // with cardType=undefined, which lands on 'auto-no-card-info' → Standard.
697
+ console.warn(`[viva] retrieveTransaction failed for refund '${vivaTransactionId}': ` +
698
+ `${e instanceof Error ? e.message : String(e)}. ` +
699
+ `Refund strategy will route via 'auto-no-card-info' (Standard).`);
700
+ }
701
+ const decision = (0, refunds_1.resolveRefundStrategy)(configuredStrategy, {
702
+ ...(cardType !== undefined ? { cardType } : {}),
703
+ isCardNotPresent: true, // Smart Checkout is always CNP
704
+ });
705
+ if (decision.kind === 'fast') {
706
+ try {
707
+ await this.fastRefundClient.refund({
708
+ transactionId: vivaTransactionId,
709
+ amount: refundAmountMinor,
710
+ sourceCode: this.vivaConfig.mode === 'merchant'
711
+ ? this.vivaConfig.sourceCode ?? DEFAULT_SOURCE_CODE
712
+ : DEFAULT_SOURCE_CODE,
713
+ merchantTrns: medusaRefundId,
714
+ idempotencyKey: refundIdempotencyKey,
715
+ });
716
+ return;
717
+ }
718
+ catch (err) {
719
+ // 403 → either fall back (auto) or surface VIVA_FAST_REFUND_INELIGIBLE (fast).
720
+ const is403 = err instanceof errors_1.VivaApiError && err.httpStatus === 403;
721
+ if (is403 && configuredStrategy === 'fast') {
722
+ throw new errors_1.VivaApiError({
723
+ message: `Fast Refund ineligible (HTTP 403). Configured strategy 'fast' does not fall back. ` +
724
+ `Set VIVA_REFUND_STRATEGY=auto to enable automatic Standard-refund fallback.`,
725
+ httpStatus: 403,
726
+ vivaCode: 'VIVA_FAST_REFUND_INELIGIBLE',
727
+ });
728
+ }
729
+ if (!is403) {
730
+ throw err;
731
+ }
732
+ // is403 + strategy === 'auto' → fall through to Standard refund.
733
+ }
734
+ }
735
+ // Standard refund — Payments.refundPayment routes through BasicAuthClient.
736
+ // merchantId is required by the Payments method signature but ignored at
737
+ // the wire layer for merchant mode (the legacy refund endpoint doesn't
738
+ // carry merchantId in the URL).
739
+ await this.isvPayments.refundPayment(vivaTransactionId, {
740
+ merchantId: this.vivaConfig.legacyMerchantId,
741
+ amountMinor: refundAmountMinor,
742
+ idempotencyKey: refundIdempotencyKey,
743
+ });
744
+ }
745
+ // --------------------------------------------------------------------------
746
+ // cancelPayment
747
+ // --------------------------------------------------------------------------
748
+ /**
749
+ * Cancels a Viva order (valid for unpaid orders not yet captured).
750
+ * Idempotent: skip API call if already in terminal cancelled state.
751
+ * Apply lattice: authorized → cancelled (A9 path).
752
+ *
753
+ * @see references/viva-docs/md/payment-isv-api.txt:1 (cancelOrder)
754
+ * @see references/viva-docs/md/isv-partner-program.txt:61 (P18, A9)
755
+ */
756
+ async cancelPayment(input) {
757
+ try {
758
+ const medusaPaymentId = extractPaymentId(input.data);
759
+ const repo = this.em.getRepository('VivaTransaction');
760
+ const row = medusaPaymentId
761
+ ? await repo.findOne({ medusa_payment_id: medusaPaymentId })
762
+ : null;
763
+ if (!row) {
764
+ return { data: input.data ?? {} };
765
+ }
766
+ // Idempotent: already cancelled
767
+ if (row.status === 'cancelled') {
768
+ return {
769
+ data: {
770
+ ...(input.data ?? {}),
771
+ viva_transaction_id: row.viva_transaction_id,
772
+ viva_status: 'cancelled',
773
+ },
774
+ };
775
+ }
776
+ // Validate the transition is allowed
777
+ const transition = (0, webhooks_1.validateStatusTransition)(row.status, 'cancelled');
778
+ if (!transition.ok) {
779
+ throw new errors_1.VivaValidationError({
780
+ message: `Cannot cancel transaction '${row.viva_transaction_id}': ` +
781
+ `status '${row.status}' → 'cancelled' is not allowed (${transition.reason}). ` +
782
+ `Status lattice plan P17/A9.`,
783
+ });
784
+ }
785
+ // Call Viva cancelOrder if order code is available.
786
+ // - ISV mode: pass merchantId from the stored row → DELETE /checkout/v2/orders/{oc}?merchantId={uuid}.
787
+ // - Merchant mode: omit merchantId → DELETE /checkout/v2/orders/{oc}.
788
+ // Payments silently ignores opts.merchantId in merchant mode anyway.
789
+ if (row.viva_order_code) {
790
+ const cancelOpts = this.vivaConfig.mode === 'isv'
791
+ ? { merchantId: row.viva_merchant_id }
792
+ : {};
793
+ await this.isvPayments.cancelOrder(BigInt(row.viva_order_code), cancelOpts);
794
+ }
795
+ row.status = transition.next;
796
+ await this.em.flush();
797
+ return {
798
+ data: {
799
+ ...(input.data ?? {}),
800
+ viva_transaction_id: row.viva_transaction_id,
801
+ order_code: row.viva_order_code ?? null,
802
+ viva_status: 'cancelled',
803
+ },
804
+ };
805
+ }
806
+ catch (err) {
807
+ throw toMedusaError(err);
808
+ }
809
+ }
810
+ // --------------------------------------------------------------------------
811
+ // retrievePayment
812
+ // --------------------------------------------------------------------------
813
+ /**
814
+ * Returns the serialized viva_transaction row for the given payment.
815
+ *
816
+ * @see references/viva-docs/md/isv-partner-program.txt:104
817
+ */
818
+ async retrievePayment(input) {
819
+ try {
820
+ const medusaPaymentId = extractPaymentId(input.data);
821
+ const repo = this.em.getRepository('VivaTransaction');
822
+ const row = medusaPaymentId
823
+ ? await repo.findOne({ medusa_payment_id: medusaPaymentId })
824
+ : null;
825
+ if (!row) {
826
+ return { data: {} };
827
+ }
828
+ return {
829
+ data: {
830
+ viva_transaction_id: row.viva_transaction_id,
831
+ order_code: row.viva_order_code ?? null,
832
+ medusa_payment_id: row.medusa_payment_id,
833
+ status: row.status,
834
+ amount_minor: row.amount_minor,
835
+ refunded_amount_minor: row.refunded_amount_minor,
836
+ currency_code: row.currency_code,
837
+ created_at: row.created_at.toISOString(),
838
+ updated_at: row.updated_at.toISOString(),
839
+ },
840
+ };
841
+ }
842
+ catch (err) {
843
+ throw toMedusaError(err);
844
+ }
845
+ }
846
+ // --------------------------------------------------------------------------
847
+ // getPaymentStatus
848
+ // --------------------------------------------------------------------------
849
+ /**
850
+ * Returns the current Medusa-mapped payment status from the DB.
851
+ *
852
+ * @see references/viva-docs/md/isv-partner-program.txt:104
853
+ */
854
+ async getPaymentStatus(input) {
855
+ try {
856
+ const medusaPaymentId = extractPaymentId(input.data);
857
+ const repo = this.em.getRepository('VivaTransaction');
858
+ const row = medusaPaymentId
859
+ ? await repo.findOne({ medusa_payment_id: medusaPaymentId })
860
+ : null;
861
+ if (!row) {
862
+ return { status: 'pending' };
863
+ }
864
+ return {
865
+ status: toMedusaStatus(row.status),
866
+ data: {
867
+ viva_transaction_id: row.viva_transaction_id,
868
+ viva_status: row.status,
869
+ },
870
+ };
871
+ }
872
+ catch (err) {
873
+ throw toMedusaError(err);
874
+ }
875
+ }
876
+ // --------------------------------------------------------------------------
877
+ // deletePayment
878
+ // --------------------------------------------------------------------------
879
+ /**
880
+ * Logical-only deletion — does NOT delete the Viva transaction (audit trail).
881
+ * The viva_transaction row is preserved for forensics and idempotency replay.
882
+ *
883
+ * @see references/viva-docs/md/isv-partner-program.txt:104
884
+ */
885
+ async deletePayment(input) {
886
+ try {
887
+ // Logical delete only — no Viva API call, no DB row deletion.
888
+ // Audit trail must be preserved per plan design. Mode-agnostic.
889
+ return { data: input.data ?? {} };
890
+ }
891
+ catch (err) {
892
+ throw toMedusaError(err);
893
+ }
894
+ }
895
+ // --------------------------------------------------------------------------
896
+ // updatePayment
897
+ // --------------------------------------------------------------------------
898
+ /**
899
+ * No-op for Smart Checkout: Viva orders cannot be updated after creation.
900
+ * If amount changes, caller should cancel and re-initiate.
901
+ *
902
+ * @see references/viva-docs/md/payment-isv-api.txt:1 (order immutability)
903
+ */
904
+ async updatePayment(input) {
905
+ try {
906
+ // Smart Checkout orders are immutable once created. Mode-agnostic.
907
+ // TODO(impl): log a warning if input.amount differs from the stored amount,
908
+ // advising the operator to cancel and re-initiate.
909
+ return {
910
+ data: input.data ?? {},
911
+ };
912
+ }
913
+ catch (err) {
914
+ throw toMedusaError(err);
915
+ }
916
+ }
917
+ // --------------------------------------------------------------------------
918
+ // getWebhookActionAndData
919
+ // --------------------------------------------------------------------------
920
+ /**
921
+ * Processes Viva webhook events received via the /viva/webhook route (S8).
922
+ * S8 routes the verified webhook payload here.
923
+ *
924
+ * Viva webhook event types in v1 scope:
925
+ * 1796 - Transaction Payment Created → action: 'captured'
926
+ * 1797 - Transaction Reversal Created → action: 'authorized' (refund)
927
+ * 1798 - Transaction Failed → action: 'failed'
928
+ * 4865 - Order Updated (cancellation) → action: 'canceled'
929
+ *
930
+ * NOTE: This is the minimal implementation for S6. The full webhook processing
931
+ * subscriber (status lattice updates, Retrieve Transaction API call per plan
932
+ * step 4.a) is implemented in S8.
933
+ *
934
+ * @see references/viva-docs/md/isv-partner-program.txt:104 (webhook flow)
935
+ * @see references/viva-docs/md/webhooks-for-payments.txt:248 (retrieve before update)
936
+ */
937
+ async getWebhookActionAndData(data) {
938
+ try {
939
+ const eventData = data.data;
940
+ const eventTypeId = eventData?.['EventTypeId'];
941
+ const innerEventData = eventData?.['EventData'];
942
+ const sessionId = (innerEventData?.['MerchantTrns'] ??
943
+ eventData?.['session_id']);
944
+ // Map Viva event types to Medusa actions
945
+ // TODO(impl): full webhook processing in S8 — this is a minimal stub
946
+ // @see references/viva-docs/md/isv-partner-program.txt:104 (webhook event types)
947
+ switch (eventTypeId) {
948
+ case 1796: // Transaction Payment Created
949
+ return {
950
+ action: 'captured',
951
+ data: {
952
+ session_id: sessionId ?? '',
953
+ amount: 0,
954
+ },
955
+ };
956
+ case 1797: // Transaction Reversal Created
957
+ return {
958
+ action: 'authorized',
959
+ data: {
960
+ session_id: sessionId ?? '',
961
+ amount: 0,
962
+ },
963
+ };
964
+ case 1798: // Transaction Failed
965
+ return {
966
+ action: 'failed',
967
+ data: {
968
+ session_id: sessionId ?? '',
969
+ amount: 0,
970
+ },
971
+ };
972
+ case 4865: // Order Updated (cancellation)
973
+ return {
974
+ action: 'canceled',
975
+ data: {
976
+ session_id: sessionId ?? '',
977
+ amount: 0,
978
+ },
979
+ };
980
+ default:
981
+ return { action: 'not_supported' };
982
+ }
983
+ }
984
+ catch (err) {
985
+ return { action: 'failed' };
986
+ }
987
+ }
988
+ }
989
+ exports.VivaPaymentProvider = VivaPaymentProvider;
990
+ // ---------------------------------------------------------------------------
991
+ // Helper: extract medusa_payment_id from provider data
992
+ // ---------------------------------------------------------------------------
993
+ /**
994
+ * Extracts the medusa_payment_id from the provider data bag.
995
+ * Checks common keys set by our provider in previous calls.
996
+ */
997
+ function extractPaymentId(data) {
998
+ if (!data)
999
+ return undefined;
1000
+ return (data['medusa_payment_id'] ??
1001
+ data['session_id']);
1002
+ }
1003
+ //# sourceMappingURL=service.js.map