@portel/photon-core 1.4.0 → 2.1.0

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 (169) hide show
  1. package/README.md +123 -0
  2. package/dist/auto-ui.d.ts +103 -0
  3. package/dist/auto-ui.d.ts.map +1 -0
  4. package/dist/auto-ui.js +275 -0
  5. package/dist/auto-ui.js.map +1 -0
  6. package/dist/base.d.ts +9 -2
  7. package/dist/base.d.ts.map +1 -1
  8. package/dist/base.js +23 -10
  9. package/dist/base.js.map +1 -1
  10. package/dist/cli-ui-renderer.d.ts +31 -0
  11. package/dist/cli-ui-renderer.d.ts.map +1 -0
  12. package/dist/cli-ui-renderer.js +224 -0
  13. package/dist/cli-ui-renderer.js.map +1 -0
  14. package/dist/dependency-manager.d.ts.map +1 -1
  15. package/dist/dependency-manager.js +0 -1
  16. package/dist/dependency-manager.js.map +1 -1
  17. package/dist/design-system/index.d.ts +21 -0
  18. package/dist/design-system/index.d.ts.map +1 -0
  19. package/dist/design-system/index.js +27 -0
  20. package/dist/design-system/index.js.map +1 -0
  21. package/dist/design-system/tokens.d.ts +149 -0
  22. package/dist/design-system/tokens.d.ts.map +1 -0
  23. package/dist/design-system/tokens.js +413 -0
  24. package/dist/design-system/tokens.js.map +1 -0
  25. package/dist/design-system/transaction-ui.d.ts +70 -0
  26. package/dist/design-system/transaction-ui.d.ts.map +1 -0
  27. package/dist/design-system/transaction-ui.js +982 -0
  28. package/dist/design-system/transaction-ui.js.map +1 -0
  29. package/dist/generator.d.ts +58 -8
  30. package/dist/generator.d.ts.map +1 -1
  31. package/dist/generator.js +9 -4
  32. package/dist/generator.js.map +1 -1
  33. package/dist/index.d.ts +10 -7
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +48 -44
  36. package/dist/index.js.map +1 -1
  37. package/dist/io.d.ts +395 -0
  38. package/dist/io.d.ts.map +1 -0
  39. package/dist/io.js +304 -0
  40. package/dist/io.js.map +1 -0
  41. package/dist/path-resolver.d.ts.map +1 -1
  42. package/dist/path-resolver.js +2 -1
  43. package/dist/path-resolver.js.map +1 -1
  44. package/dist/rendering/components.d.ts +29 -0
  45. package/dist/rendering/components.d.ts.map +1 -0
  46. package/dist/rendering/components.js +773 -0
  47. package/dist/rendering/components.js.map +1 -0
  48. package/dist/rendering/field-analyzer.d.ts +48 -0
  49. package/dist/rendering/field-analyzer.d.ts.map +1 -0
  50. package/dist/rendering/field-analyzer.js +270 -0
  51. package/dist/rendering/field-analyzer.js.map +1 -0
  52. package/dist/rendering/field-renderers.d.ts +64 -0
  53. package/dist/rendering/field-renderers.d.ts.map +1 -0
  54. package/dist/rendering/field-renderers.js +317 -0
  55. package/dist/rendering/field-renderers.js.map +1 -0
  56. package/dist/rendering/index.d.ts +28 -0
  57. package/dist/rendering/index.d.ts.map +1 -0
  58. package/dist/rendering/index.js +60 -0
  59. package/dist/rendering/index.js.map +1 -0
  60. package/dist/rendering/layout-selector.d.ts +48 -0
  61. package/dist/rendering/layout-selector.d.ts.map +1 -0
  62. package/dist/rendering/layout-selector.js +347 -0
  63. package/dist/rendering/layout-selector.js.map +1 -0
  64. package/dist/rendering/template-engine.d.ts +41 -0
  65. package/dist/rendering/template-engine.d.ts.map +1 -0
  66. package/dist/rendering/template-engine.js +236 -0
  67. package/dist/rendering/template-engine.js.map +1 -0
  68. package/dist/schema-extractor.d.ts +30 -0
  69. package/dist/schema-extractor.d.ts.map +1 -1
  70. package/dist/schema-extractor.js +205 -12
  71. package/dist/schema-extractor.js.map +1 -1
  72. package/dist/stateful.d.ts +63 -0
  73. package/dist/stateful.d.ts.map +1 -1
  74. package/dist/stateful.js +222 -0
  75. package/dist/stateful.js.map +1 -1
  76. package/dist/types.d.ts +9 -1
  77. package/dist/types.d.ts.map +1 -1
  78. package/dist/types.js.map +1 -1
  79. package/dist/ucp/ap2/handlers.d.ts +242 -0
  80. package/dist/ucp/ap2/handlers.d.ts.map +1 -0
  81. package/dist/ucp/ap2/handlers.js +482 -0
  82. package/dist/ucp/ap2/handlers.js.map +1 -0
  83. package/dist/ucp/ap2/mandates.d.ts +95 -0
  84. package/dist/ucp/ap2/mandates.d.ts.map +1 -0
  85. package/dist/ucp/ap2/mandates.js +234 -0
  86. package/dist/ucp/ap2/mandates.js.map +1 -0
  87. package/dist/ucp/ap2/types.d.ts +305 -0
  88. package/dist/ucp/ap2/types.d.ts.map +1 -0
  89. package/dist/ucp/ap2/types.js +8 -0
  90. package/dist/ucp/ap2/types.js.map +1 -0
  91. package/dist/ucp/capabilities/checkout.d.ts +118 -0
  92. package/dist/ucp/capabilities/checkout.d.ts.map +1 -0
  93. package/dist/ucp/capabilities/checkout.js +344 -0
  94. package/dist/ucp/capabilities/checkout.js.map +1 -0
  95. package/dist/ucp/capabilities/identity.d.ts +130 -0
  96. package/dist/ucp/capabilities/identity.d.ts.map +1 -0
  97. package/dist/ucp/capabilities/identity.js +290 -0
  98. package/dist/ucp/capabilities/identity.js.map +1 -0
  99. package/dist/ucp/capabilities/order.d.ts +142 -0
  100. package/dist/ucp/capabilities/order.d.ts.map +1 -0
  101. package/dist/ucp/capabilities/order.js +383 -0
  102. package/dist/ucp/capabilities/order.js.map +1 -0
  103. package/dist/ucp/index.d.ts +18 -0
  104. package/dist/ucp/index.d.ts.map +1 -0
  105. package/dist/ucp/index.js +19 -0
  106. package/dist/ucp/index.js.map +1 -0
  107. package/dist/ucp/manifest.d.ts +62 -0
  108. package/dist/ucp/manifest.d.ts.map +1 -0
  109. package/dist/ucp/manifest.js +180 -0
  110. package/dist/ucp/manifest.js.map +1 -0
  111. package/dist/ucp/types.d.ts +327 -0
  112. package/dist/ucp/types.d.ts.map +1 -0
  113. package/dist/ucp/types.js +8 -0
  114. package/dist/ucp/types.js.map +1 -0
  115. package/package.json +3 -4
  116. package/src/auto-ui.ts +413 -0
  117. package/src/base.ts +22 -9
  118. package/src/cli-ui-renderer.ts +264 -0
  119. package/src/dependency-manager.ts +0 -1
  120. package/src/design-system/index.ts +30 -0
  121. package/src/design-system/tokens.ts +451 -0
  122. package/src/design-system/transaction-ui.ts +1038 -0
  123. package/src/generator.ts +68 -8
  124. package/src/index.ts +159 -101
  125. package/src/io.ts +493 -0
  126. package/src/path-resolver.ts +2 -1
  127. package/src/rendering/components.ts +785 -0
  128. package/src/rendering/field-analyzer.ts +299 -0
  129. package/src/rendering/field-renderers.ts +356 -0
  130. package/src/rendering/index.ts +63 -0
  131. package/src/rendering/layout-selector.ts +390 -0
  132. package/src/rendering/template-engine.ts +254 -0
  133. package/src/schema-extractor.ts +225 -12
  134. package/src/stateful.ts +301 -0
  135. package/src/types.ts +10 -1
  136. package/src/ucp/ap2/handlers.ts +779 -0
  137. package/src/ucp/ap2/mandates.ts +354 -0
  138. package/src/ucp/ap2/types.ts +441 -0
  139. package/src/ucp/capabilities/checkout.ts +497 -0
  140. package/src/ucp/capabilities/identity.ts +425 -0
  141. package/src/ucp/capabilities/order.ts +549 -0
  142. package/src/ucp/index.ts +27 -0
  143. package/src/ucp/manifest.ts +257 -0
  144. package/src/ucp/types.ts +454 -0
  145. package/dist/cli-formatter.d.ts +0 -92
  146. package/dist/cli-formatter.d.ts.map +0 -1
  147. package/dist/cli-formatter.js +0 -486
  148. package/dist/cli-formatter.js.map +0 -1
  149. package/dist/elicit.d.ts +0 -93
  150. package/dist/elicit.d.ts.map +0 -1
  151. package/dist/elicit.js +0 -373
  152. package/dist/elicit.js.map +0 -1
  153. package/dist/mcp-client.d.ts +0 -218
  154. package/dist/mcp-client.d.ts.map +0 -1
  155. package/dist/mcp-client.js +0 -424
  156. package/dist/mcp-client.js.map +0 -1
  157. package/dist/mcp-sdk-transport.d.ts +0 -88
  158. package/dist/mcp-sdk-transport.d.ts.map +0 -1
  159. package/dist/mcp-sdk-transport.js +0 -360
  160. package/dist/mcp-sdk-transport.js.map +0 -1
  161. package/dist/photon-config.d.ts +0 -86
  162. package/dist/photon-config.d.ts.map +0 -1
  163. package/dist/photon-config.js +0 -156
  164. package/dist/photon-config.js.map +0 -1
  165. package/src/cli-formatter.ts +0 -579
  166. package/src/elicit.ts +0 -438
  167. package/src/mcp-client.ts +0 -561
  168. package/src/mcp-sdk-transport.ts +0 -449
  169. package/src/photon-config.ts +0 -201
@@ -0,0 +1,779 @@
1
+ /**
2
+ * AP2 Payment Handler Implementations
3
+ *
4
+ * Adapters for various payment providers that implement the AP2 PaymentHandler interface.
5
+ */
6
+
7
+ import * as crypto from 'crypto';
8
+ import {
9
+ PaymentMandate,
10
+ PaymentHandler,
11
+ PaymentMethodType,
12
+ PaymentMethodData,
13
+ RiskPayload
14
+ } from './types.js';
15
+ import { Money } from '../types.js';
16
+
17
+ // ============================================================================
18
+ // Base Payment Handler
19
+ // ============================================================================
20
+
21
+ export interface PaymentHandlerConfig {
22
+ merchantId: string;
23
+ environment: 'sandbox' | 'production';
24
+ }
25
+
26
+ export abstract class BasePaymentHandler implements PaymentHandler {
27
+ abstract name: string;
28
+ abstract supportedMethods: PaymentMethodType[];
29
+ abstract supportedCurrencies: string[];
30
+
31
+ protected config: PaymentHandlerConfig;
32
+
33
+ constructor(config: PaymentHandlerConfig) {
34
+ this.config = config;
35
+ }
36
+
37
+ abstract tokenize(params: {
38
+ mandate: PaymentMandate;
39
+ paymentMethod: PaymentMethodData;
40
+ }): Promise<{ token: string; expiresAt: string }>;
41
+
42
+ abstract authorize(params: {
43
+ token: string;
44
+ amount: Money;
45
+ mandate: PaymentMandate;
46
+ riskPayload?: RiskPayload;
47
+ }): Promise<{
48
+ authorizationId: string;
49
+ status: 'authorized' | 'declined' | 'pending';
50
+ declineReason?: string;
51
+ }>;
52
+
53
+ abstract capture(params: {
54
+ authorizationId: string;
55
+ amount?: Money;
56
+ }): Promise<{
57
+ captureId: string;
58
+ status: 'captured' | 'failed';
59
+ }>;
60
+
61
+ abstract refund(params: {
62
+ captureId: string;
63
+ amount?: Money;
64
+ reason?: string;
65
+ }): Promise<{
66
+ refundId: string;
67
+ status: 'refunded' | 'pending' | 'failed';
68
+ }>;
69
+
70
+ abstract void(authorizationId: string): Promise<{
71
+ status: 'voided' | 'failed';
72
+ }>;
73
+
74
+ protected generateId(prefix: string): string {
75
+ return `${prefix}_${crypto.randomUUID()}`;
76
+ }
77
+ }
78
+
79
+ // ============================================================================
80
+ // Mock Payment Handler (for testing/development)
81
+ // ============================================================================
82
+
83
+ export interface MockPaymentHandlerConfig extends PaymentHandlerConfig {
84
+ /** Simulate processing delay in ms */
85
+ processingDelayMs?: number;
86
+ /** Failure rate (0-1) for testing error handling */
87
+ failureRate?: number;
88
+ /** Specific error to return */
89
+ simulatedError?: string;
90
+ }
91
+
92
+ interface MockToken {
93
+ mandate: PaymentMandate;
94
+ paymentMethod: PaymentMethodData;
95
+ expiresAt: string;
96
+ }
97
+
98
+ interface MockAuthorization {
99
+ token: string;
100
+ amount: Money;
101
+ mandate: PaymentMandate;
102
+ status: 'authorized' | 'captured' | 'voided' | 'refunded';
103
+ captureId?: string;
104
+ }
105
+
106
+ /**
107
+ * Mock payment handler for testing and development.
108
+ * Simulates payment processing without actual charges.
109
+ */
110
+ export class MockPaymentHandler extends BasePaymentHandler {
111
+ name = 'mock';
112
+ supportedMethods: PaymentMethodType[] = ['CARD', 'BANK_TRANSFER', 'WALLET'];
113
+ supportedCurrencies = ['USD', 'EUR', 'GBP'];
114
+
115
+ private mockConfig: MockPaymentHandlerConfig;
116
+ private tokens = new Map<string, MockToken>();
117
+ private authorizations = new Map<string, MockAuthorization>();
118
+
119
+ constructor(config: MockPaymentHandlerConfig) {
120
+ super(config);
121
+ this.mockConfig = config;
122
+ }
123
+
124
+ async tokenize(params: {
125
+ mandate: PaymentMandate;
126
+ paymentMethod: PaymentMethodData;
127
+ }): Promise<{ token: string; expiresAt: string }> {
128
+ await this.maybeDelay();
129
+
130
+ if (this.shouldFail()) {
131
+ throw new Error(this.mockConfig.simulatedError || 'Simulated tokenization failure');
132
+ }
133
+
134
+ const token = this.generateId('tok');
135
+ const expiresAt = new Date(Date.now() + 15 * 60 * 1000).toISOString(); // 15 minutes
136
+
137
+ this.tokens.set(token, {
138
+ mandate: params.mandate,
139
+ paymentMethod: params.paymentMethod,
140
+ expiresAt
141
+ });
142
+
143
+ return { token, expiresAt };
144
+ }
145
+
146
+ async authorize(params: {
147
+ token: string;
148
+ amount: Money;
149
+ mandate: PaymentMandate;
150
+ riskPayload?: RiskPayload;
151
+ }): Promise<{
152
+ authorizationId: string;
153
+ status: 'authorized' | 'declined' | 'pending';
154
+ declineReason?: string;
155
+ }> {
156
+ await this.maybeDelay();
157
+
158
+ const tokenData = this.tokens.get(params.token);
159
+ if (!tokenData) {
160
+ return {
161
+ authorizationId: this.generateId('auth'),
162
+ status: 'declined',
163
+ declineReason: 'Invalid or expired token'
164
+ };
165
+ }
166
+
167
+ if (this.shouldFail()) {
168
+ return {
169
+ authorizationId: this.generateId('auth'),
170
+ status: 'declined',
171
+ declineReason: this.mockConfig.simulatedError || 'Simulated authorization failure'
172
+ };
173
+ }
174
+
175
+ const authorizationId = this.generateId('auth');
176
+
177
+ this.authorizations.set(authorizationId, {
178
+ token: params.token,
179
+ amount: params.amount,
180
+ mandate: params.mandate,
181
+ status: 'authorized'
182
+ });
183
+
184
+ // Delete used token
185
+ this.tokens.delete(params.token);
186
+
187
+ return {
188
+ authorizationId,
189
+ status: 'authorized'
190
+ };
191
+ }
192
+
193
+ async capture(params: {
194
+ authorizationId: string;
195
+ amount?: Money;
196
+ }): Promise<{
197
+ captureId: string;
198
+ status: 'captured' | 'failed';
199
+ }> {
200
+ await this.maybeDelay();
201
+
202
+ const auth = this.authorizations.get(params.authorizationId);
203
+ if (!auth || auth.status !== 'authorized') {
204
+ return {
205
+ captureId: this.generateId('cap'),
206
+ status: 'failed'
207
+ };
208
+ }
209
+
210
+ if (this.shouldFail()) {
211
+ return {
212
+ captureId: this.generateId('cap'),
213
+ status: 'failed'
214
+ };
215
+ }
216
+
217
+ const captureId = this.generateId('cap');
218
+ auth.status = 'captured';
219
+ auth.captureId = captureId;
220
+
221
+ return {
222
+ captureId,
223
+ status: 'captured'
224
+ };
225
+ }
226
+
227
+ async refund(params: {
228
+ captureId: string;
229
+ amount?: Money;
230
+ reason?: string;
231
+ }): Promise<{
232
+ refundId: string;
233
+ status: 'refunded' | 'pending' | 'failed';
234
+ }> {
235
+ await this.maybeDelay();
236
+
237
+ // Find authorization by capture ID
238
+ let foundAuth: MockAuthorization | undefined;
239
+ for (const auth of this.authorizations.values()) {
240
+ if (auth.captureId === params.captureId) {
241
+ foundAuth = auth;
242
+ break;
243
+ }
244
+ }
245
+
246
+ if (!foundAuth || foundAuth.status !== 'captured') {
247
+ return {
248
+ refundId: this.generateId('ref'),
249
+ status: 'failed'
250
+ };
251
+ }
252
+
253
+ if (this.shouldFail()) {
254
+ return {
255
+ refundId: this.generateId('ref'),
256
+ status: 'failed'
257
+ };
258
+ }
259
+
260
+ foundAuth.status = 'refunded';
261
+
262
+ return {
263
+ refundId: this.generateId('ref'),
264
+ status: 'refunded'
265
+ };
266
+ }
267
+
268
+ async void(authorizationId: string): Promise<{
269
+ status: 'voided' | 'failed';
270
+ }> {
271
+ await this.maybeDelay();
272
+
273
+ const auth = this.authorizations.get(authorizationId);
274
+ if (!auth || auth.status !== 'authorized') {
275
+ return { status: 'failed' };
276
+ }
277
+
278
+ if (this.shouldFail()) {
279
+ return { status: 'failed' };
280
+ }
281
+
282
+ auth.status = 'voided';
283
+ return { status: 'voided' };
284
+ }
285
+
286
+ private async maybeDelay(): Promise<void> {
287
+ if (this.mockConfig.processingDelayMs) {
288
+ await new Promise(resolve => setTimeout(resolve, this.mockConfig.processingDelayMs));
289
+ }
290
+ }
291
+
292
+ private shouldFail(): boolean {
293
+ if (this.mockConfig.simulatedError) return true;
294
+ if (this.mockConfig.failureRate && Math.random() < this.mockConfig.failureRate) return true;
295
+ return false;
296
+ }
297
+
298
+ /** Clear all stored data (for testing) */
299
+ clear(): void {
300
+ this.tokens.clear();
301
+ this.authorizations.clear();
302
+ }
303
+ }
304
+
305
+ // ============================================================================
306
+ // Stripe Payment Handler
307
+ // ============================================================================
308
+
309
+ export interface StripeConfig extends PaymentHandlerConfig {
310
+ secretKey: string;
311
+ webhookSecret?: string;
312
+ }
313
+
314
+ /**
315
+ * Stripe payment handler adapter.
316
+ * Requires stripe package to be installed.
317
+ */
318
+ export class StripePaymentHandler extends BasePaymentHandler {
319
+ name = 'stripe';
320
+ supportedMethods: PaymentMethodType[] = ['CARD', 'BANK_TRANSFER', 'WALLET'];
321
+ supportedCurrencies = ['USD', 'EUR', 'GBP', 'CAD', 'AUD', 'JPY'];
322
+
323
+ private stripeConfig: StripeConfig;
324
+ private stripe: any; // Stripe SDK instance
325
+
326
+ constructor(config: StripeConfig) {
327
+ super(config);
328
+ this.stripeConfig = config;
329
+ }
330
+
331
+ /**
332
+ * Initialize Stripe SDK (lazy loading)
333
+ */
334
+ private async getStripe(): Promise<any> {
335
+ if (!this.stripe) {
336
+ try {
337
+ // Dynamic import to avoid hard dependency
338
+ const { default: Stripe } = await import('stripe' as any);
339
+ this.stripe = new Stripe(this.stripeConfig.secretKey, {
340
+ apiVersion: '2024-12-18.acacia'
341
+ });
342
+ } catch {
343
+ throw new Error('Stripe package not installed. Run: npm install stripe');
344
+ }
345
+ }
346
+ return this.stripe;
347
+ }
348
+
349
+ async tokenize(params: {
350
+ mandate: PaymentMandate;
351
+ paymentMethod: PaymentMethodData;
352
+ }): Promise<{ token: string; expiresAt: string }> {
353
+ const stripe = await this.getStripe();
354
+
355
+ // Create a SetupIntent to tokenize the payment method
356
+ const setupIntent = await stripe.setupIntents.create({
357
+ payment_method_types: this.mapMethodToStripe(params.paymentMethod.supportedMethods),
358
+ metadata: {
359
+ mandateId: params.mandate.paymentMandateContents.paymentMandateId
360
+ }
361
+ });
362
+
363
+ return {
364
+ token: setupIntent.client_secret,
365
+ expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString() // 24 hours
366
+ };
367
+ }
368
+
369
+ async authorize(params: {
370
+ token: string;
371
+ amount: Money;
372
+ mandate: PaymentMandate;
373
+ riskPayload?: RiskPayload;
374
+ }): Promise<{
375
+ authorizationId: string;
376
+ status: 'authorized' | 'declined' | 'pending';
377
+ declineReason?: string;
378
+ }> {
379
+ try {
380
+ const stripe = await this.getStripe();
381
+
382
+ // Create PaymentIntent with capture_method = manual for auth-only
383
+ const paymentIntent = await stripe.paymentIntents.create({
384
+ amount: Math.round(params.amount.amount * 100), // Stripe uses cents
385
+ currency: params.amount.currency.toLowerCase(),
386
+ capture_method: 'manual',
387
+ metadata: {
388
+ mandateId: params.mandate.paymentMandateContents.paymentMandateId
389
+ }
390
+ });
391
+
392
+ return {
393
+ authorizationId: paymentIntent.id,
394
+ status: paymentIntent.status === 'requires_capture' ? 'authorized' : 'pending'
395
+ };
396
+
397
+ } catch (error: any) {
398
+ return {
399
+ authorizationId: this.generateId('auth'),
400
+ status: 'declined',
401
+ declineReason: error.message || 'Stripe authorization failed'
402
+ };
403
+ }
404
+ }
405
+
406
+ async capture(params: {
407
+ authorizationId: string;
408
+ amount?: Money;
409
+ }): Promise<{
410
+ captureId: string;
411
+ status: 'captured' | 'failed';
412
+ }> {
413
+ try {
414
+ const stripe = await this.getStripe();
415
+
416
+ const captureParams: any = {};
417
+ if (params.amount) {
418
+ captureParams.amount_to_capture = Math.round(params.amount.amount * 100);
419
+ }
420
+
421
+ const paymentIntent = await stripe.paymentIntents.capture(
422
+ params.authorizationId,
423
+ captureParams
424
+ );
425
+
426
+ return {
427
+ captureId: paymentIntent.latest_charge || params.authorizationId,
428
+ status: paymentIntent.status === 'succeeded' ? 'captured' : 'failed'
429
+ };
430
+
431
+ } catch {
432
+ return {
433
+ captureId: this.generateId('cap'),
434
+ status: 'failed'
435
+ };
436
+ }
437
+ }
438
+
439
+ async refund(params: {
440
+ captureId: string;
441
+ amount?: Money;
442
+ reason?: string;
443
+ }): Promise<{
444
+ refundId: string;
445
+ status: 'refunded' | 'pending' | 'failed';
446
+ }> {
447
+ try {
448
+ const stripe = await this.getStripe();
449
+
450
+ const refundParams: any = {
451
+ charge: params.captureId,
452
+ reason: params.reason || 'requested_by_customer'
453
+ };
454
+
455
+ if (params.amount) {
456
+ refundParams.amount = Math.round(params.amount.amount * 100);
457
+ }
458
+
459
+ const refund = await stripe.refunds.create(refundParams);
460
+
461
+ return {
462
+ refundId: refund.id,
463
+ status: refund.status === 'succeeded' ? 'refunded' : 'pending'
464
+ };
465
+
466
+ } catch {
467
+ return {
468
+ refundId: this.generateId('ref'),
469
+ status: 'failed'
470
+ };
471
+ }
472
+ }
473
+
474
+ async void(authorizationId: string): Promise<{
475
+ status: 'voided' | 'failed';
476
+ }> {
477
+ try {
478
+ const stripe = await this.getStripe();
479
+ await stripe.paymentIntents.cancel(authorizationId);
480
+ return { status: 'voided' };
481
+ } catch {
482
+ return { status: 'failed' };
483
+ }
484
+ }
485
+
486
+ private mapMethodToStripe(method: PaymentMethodType): string[] {
487
+ switch (method) {
488
+ case 'CARD': return ['card'];
489
+ case 'BANK_TRANSFER': return ['us_bank_account', 'sepa_debit'];
490
+ case 'WALLET': return ['apple_pay', 'google_pay'];
491
+ default: return ['card'];
492
+ }
493
+ }
494
+
495
+ /**
496
+ * Verify Stripe webhook signature
497
+ */
498
+ verifyWebhook(payload: string, signature: string): any {
499
+ if (!this.stripeConfig.webhookSecret) {
500
+ throw new Error('Webhook secret not configured');
501
+ }
502
+
503
+ if (!this.stripe) {
504
+ throw new Error('Stripe not initialized');
505
+ }
506
+
507
+ return this.stripe.webhooks.constructEvent(
508
+ payload,
509
+ signature,
510
+ this.stripeConfig.webhookSecret
511
+ );
512
+ }
513
+ }
514
+
515
+ // ============================================================================
516
+ // Adyen Payment Handler
517
+ // ============================================================================
518
+
519
+ export interface AdyenConfig extends PaymentHandlerConfig {
520
+ apiKey: string;
521
+ merchantAccount: string;
522
+ liveEndpointUrlPrefix?: string;
523
+ }
524
+
525
+ /**
526
+ * Adyen payment handler adapter.
527
+ * Requires @adyen/api-library package.
528
+ */
529
+ export class AdyenPaymentHandler extends BasePaymentHandler {
530
+ name = 'adyen';
531
+ supportedMethods: PaymentMethodType[] = ['CARD', 'BANK_TRANSFER', 'WALLET'];
532
+ supportedCurrencies = ['USD', 'EUR', 'GBP', 'CAD', 'AUD', 'JPY', 'CNY'];
533
+
534
+ private adyenConfig: AdyenConfig;
535
+ private client: any;
536
+ private checkout: any;
537
+
538
+ constructor(config: AdyenConfig) {
539
+ super(config);
540
+ this.adyenConfig = config;
541
+ }
542
+
543
+ /**
544
+ * Initialize Adyen client (lazy loading)
545
+ */
546
+ private async getCheckout(): Promise<any> {
547
+ if (!this.checkout) {
548
+ try {
549
+ // Dynamic import to avoid hard dependency
550
+ const { Client, CheckoutAPI, Config } = await import('@adyen/api-library' as any);
551
+
552
+ const config = new Config();
553
+ config.apiKey = this.adyenConfig.apiKey;
554
+ config.merchantAccount = this.adyenConfig.merchantAccount;
555
+
556
+ if (this.config.environment === 'production') {
557
+ config.environment = 'LIVE';
558
+ if (this.adyenConfig.liveEndpointUrlPrefix) {
559
+ config.liveEndpointUrlPrefix = this.adyenConfig.liveEndpointUrlPrefix;
560
+ }
561
+ } else {
562
+ config.environment = 'TEST';
563
+ }
564
+
565
+ this.client = new Client({ config });
566
+ this.checkout = new CheckoutAPI(this.client);
567
+ } catch {
568
+ throw new Error('Adyen package not installed. Run: npm install @adyen/api-library');
569
+ }
570
+ }
571
+ return this.checkout;
572
+ }
573
+
574
+ async tokenize(params: {
575
+ mandate: PaymentMandate;
576
+ paymentMethod: PaymentMethodData;
577
+ }): Promise<{ token: string; expiresAt: string }> {
578
+ // Adyen tokenization typically happens client-side
579
+ // Server-side we just return a session ID
580
+ const sessionId = this.generateId('adyen_session');
581
+ return {
582
+ token: sessionId,
583
+ expiresAt: new Date(Date.now() + 15 * 60 * 1000).toISOString()
584
+ };
585
+ }
586
+
587
+ async authorize(params: {
588
+ token: string;
589
+ amount: Money;
590
+ mandate: PaymentMandate;
591
+ riskPayload?: RiskPayload;
592
+ }): Promise<{
593
+ authorizationId: string;
594
+ status: 'authorized' | 'declined' | 'pending';
595
+ declineReason?: string;
596
+ }> {
597
+ try {
598
+ const checkout = await this.getCheckout();
599
+
600
+ const paymentRequest = {
601
+ amount: {
602
+ value: Math.round(params.amount.amount * 100),
603
+ currency: params.amount.currency
604
+ },
605
+ reference: params.mandate.paymentMandateContents.paymentMandateId,
606
+ merchantAccount: this.adyenConfig.merchantAccount,
607
+ metadata: {
608
+ mandateId: params.mandate.paymentMandateContents.paymentMandateId
609
+ }
610
+ };
611
+
612
+ const response = await checkout.PaymentsApi.payments(paymentRequest);
613
+
614
+ return {
615
+ authorizationId: response.pspReference,
616
+ status: this.mapAdyenStatus(response.resultCode),
617
+ declineReason: response.refusalReason
618
+ };
619
+
620
+ } catch (error: any) {
621
+ return {
622
+ authorizationId: this.generateId('auth'),
623
+ status: 'declined',
624
+ declineReason: error.message || 'Adyen authorization failed'
625
+ };
626
+ }
627
+ }
628
+
629
+ async capture(params: {
630
+ authorizationId: string;
631
+ amount?: Money;
632
+ }): Promise<{
633
+ captureId: string;
634
+ status: 'captured' | 'failed';
635
+ }> {
636
+ try {
637
+ const checkout = await this.getCheckout();
638
+
639
+ const captureRequest: any = {
640
+ merchantAccount: this.adyenConfig.merchantAccount,
641
+ originalReference: params.authorizationId,
642
+ reference: `capture_${this.generateId('cap')}`
643
+ };
644
+
645
+ if (params.amount) {
646
+ captureRequest.amount = {
647
+ value: Math.round(params.amount.amount * 100),
648
+ currency: params.amount.currency
649
+ };
650
+ }
651
+
652
+ const response = await checkout.ModificationsApi.captureAuthorisedPayment(
653
+ params.authorizationId,
654
+ captureRequest
655
+ );
656
+
657
+ return {
658
+ captureId: response.pspReference,
659
+ status: response.status === 'received' ? 'captured' : 'failed'
660
+ };
661
+
662
+ } catch {
663
+ return {
664
+ captureId: this.generateId('cap'),
665
+ status: 'failed'
666
+ };
667
+ }
668
+ }
669
+
670
+ async refund(params: {
671
+ captureId: string;
672
+ amount?: Money;
673
+ reason?: string;
674
+ }): Promise<{
675
+ refundId: string;
676
+ status: 'refunded' | 'pending' | 'failed';
677
+ }> {
678
+ try {
679
+ const checkout = await this.getCheckout();
680
+
681
+ const refundRequest: any = {
682
+ merchantAccount: this.adyenConfig.merchantAccount,
683
+ originalReference: params.captureId,
684
+ reference: `refund_${this.generateId('ref')}`
685
+ };
686
+
687
+ if (params.amount) {
688
+ refundRequest.amount = {
689
+ value: Math.round(params.amount.amount * 100),
690
+ currency: params.amount.currency
691
+ };
692
+ }
693
+
694
+ const response = await checkout.ModificationsApi.refundCapturedPayment(
695
+ params.captureId,
696
+ refundRequest
697
+ );
698
+
699
+ return {
700
+ refundId: response.pspReference,
701
+ status: response.status === 'received' ? 'pending' : 'failed'
702
+ };
703
+
704
+ } catch {
705
+ return {
706
+ refundId: this.generateId('ref'),
707
+ status: 'failed'
708
+ };
709
+ }
710
+ }
711
+
712
+ async void(authorizationId: string): Promise<{
713
+ status: 'voided' | 'failed';
714
+ }> {
715
+ try {
716
+ const checkout = await this.getCheckout();
717
+
718
+ const cancelRequest = {
719
+ merchantAccount: this.adyenConfig.merchantAccount,
720
+ originalReference: authorizationId,
721
+ reference: `cancel_${this.generateId('void')}`
722
+ };
723
+
724
+ const response = await checkout.ModificationsApi.cancelAuthorisedPayment(
725
+ authorizationId,
726
+ cancelRequest
727
+ );
728
+
729
+ return {
730
+ status: response.status === 'received' ? 'voided' : 'failed'
731
+ };
732
+
733
+ } catch {
734
+ return { status: 'failed' };
735
+ }
736
+ }
737
+
738
+ private mapAdyenStatus(resultCode: string): 'authorized' | 'declined' | 'pending' {
739
+ switch (resultCode) {
740
+ case 'Authorised':
741
+ return 'authorized';
742
+ case 'Pending':
743
+ case 'Received':
744
+ return 'pending';
745
+ default:
746
+ return 'declined';
747
+ }
748
+ }
749
+ }
750
+
751
+ // ============================================================================
752
+ // Payment Handler Factory
753
+ // ============================================================================
754
+
755
+ export type PaymentHandlerType = 'mock' | 'stripe' | 'adyen';
756
+
757
+ export interface CreatePaymentHandlerParams {
758
+ type: PaymentHandlerType;
759
+ config: PaymentHandlerConfig & Record<string, any>;
760
+ }
761
+
762
+ /**
763
+ * Factory function to create payment handlers
764
+ */
765
+ export function createPaymentHandler(params: CreatePaymentHandlerParams): PaymentHandler {
766
+ switch (params.type) {
767
+ case 'mock':
768
+ return new MockPaymentHandler(params.config as MockPaymentHandlerConfig);
769
+
770
+ case 'stripe':
771
+ return new StripePaymentHandler(params.config as StripeConfig);
772
+
773
+ case 'adyen':
774
+ return new AdyenPaymentHandler(params.config as AdyenConfig);
775
+
776
+ default:
777
+ throw new Error(`Unknown payment handler type: ${params.type}`);
778
+ }
779
+ }