@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.
- package/README.md +123 -0
- package/dist/auto-ui.d.ts +103 -0
- package/dist/auto-ui.d.ts.map +1 -0
- package/dist/auto-ui.js +275 -0
- package/dist/auto-ui.js.map +1 -0
- package/dist/base.d.ts +9 -2
- package/dist/base.d.ts.map +1 -1
- package/dist/base.js +23 -10
- package/dist/base.js.map +1 -1
- package/dist/cli-ui-renderer.d.ts +31 -0
- package/dist/cli-ui-renderer.d.ts.map +1 -0
- package/dist/cli-ui-renderer.js +224 -0
- package/dist/cli-ui-renderer.js.map +1 -0
- package/dist/dependency-manager.d.ts.map +1 -1
- package/dist/dependency-manager.js +0 -1
- package/dist/dependency-manager.js.map +1 -1
- package/dist/design-system/index.d.ts +21 -0
- package/dist/design-system/index.d.ts.map +1 -0
- package/dist/design-system/index.js +27 -0
- package/dist/design-system/index.js.map +1 -0
- package/dist/design-system/tokens.d.ts +149 -0
- package/dist/design-system/tokens.d.ts.map +1 -0
- package/dist/design-system/tokens.js +413 -0
- package/dist/design-system/tokens.js.map +1 -0
- package/dist/design-system/transaction-ui.d.ts +70 -0
- package/dist/design-system/transaction-ui.d.ts.map +1 -0
- package/dist/design-system/transaction-ui.js +982 -0
- package/dist/design-system/transaction-ui.js.map +1 -0
- package/dist/generator.d.ts +58 -8
- package/dist/generator.d.ts.map +1 -1
- package/dist/generator.js +9 -4
- package/dist/generator.js.map +1 -1
- package/dist/index.d.ts +10 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +48 -44
- package/dist/index.js.map +1 -1
- package/dist/io.d.ts +395 -0
- package/dist/io.d.ts.map +1 -0
- package/dist/io.js +304 -0
- package/dist/io.js.map +1 -0
- package/dist/path-resolver.d.ts.map +1 -1
- package/dist/path-resolver.js +2 -1
- package/dist/path-resolver.js.map +1 -1
- package/dist/rendering/components.d.ts +29 -0
- package/dist/rendering/components.d.ts.map +1 -0
- package/dist/rendering/components.js +773 -0
- package/dist/rendering/components.js.map +1 -0
- package/dist/rendering/field-analyzer.d.ts +48 -0
- package/dist/rendering/field-analyzer.d.ts.map +1 -0
- package/dist/rendering/field-analyzer.js +270 -0
- package/dist/rendering/field-analyzer.js.map +1 -0
- package/dist/rendering/field-renderers.d.ts +64 -0
- package/dist/rendering/field-renderers.d.ts.map +1 -0
- package/dist/rendering/field-renderers.js +317 -0
- package/dist/rendering/field-renderers.js.map +1 -0
- package/dist/rendering/index.d.ts +28 -0
- package/dist/rendering/index.d.ts.map +1 -0
- package/dist/rendering/index.js +60 -0
- package/dist/rendering/index.js.map +1 -0
- package/dist/rendering/layout-selector.d.ts +48 -0
- package/dist/rendering/layout-selector.d.ts.map +1 -0
- package/dist/rendering/layout-selector.js +347 -0
- package/dist/rendering/layout-selector.js.map +1 -0
- package/dist/rendering/template-engine.d.ts +41 -0
- package/dist/rendering/template-engine.d.ts.map +1 -0
- package/dist/rendering/template-engine.js +236 -0
- package/dist/rendering/template-engine.js.map +1 -0
- package/dist/schema-extractor.d.ts +30 -0
- package/dist/schema-extractor.d.ts.map +1 -1
- package/dist/schema-extractor.js +205 -12
- package/dist/schema-extractor.js.map +1 -1
- package/dist/stateful.d.ts +63 -0
- package/dist/stateful.d.ts.map +1 -1
- package/dist/stateful.js +222 -0
- package/dist/stateful.js.map +1 -1
- package/dist/types.d.ts +9 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/ucp/ap2/handlers.d.ts +242 -0
- package/dist/ucp/ap2/handlers.d.ts.map +1 -0
- package/dist/ucp/ap2/handlers.js +482 -0
- package/dist/ucp/ap2/handlers.js.map +1 -0
- package/dist/ucp/ap2/mandates.d.ts +95 -0
- package/dist/ucp/ap2/mandates.d.ts.map +1 -0
- package/dist/ucp/ap2/mandates.js +234 -0
- package/dist/ucp/ap2/mandates.js.map +1 -0
- package/dist/ucp/ap2/types.d.ts +305 -0
- package/dist/ucp/ap2/types.d.ts.map +1 -0
- package/dist/ucp/ap2/types.js +8 -0
- package/dist/ucp/ap2/types.js.map +1 -0
- package/dist/ucp/capabilities/checkout.d.ts +118 -0
- package/dist/ucp/capabilities/checkout.d.ts.map +1 -0
- package/dist/ucp/capabilities/checkout.js +344 -0
- package/dist/ucp/capabilities/checkout.js.map +1 -0
- package/dist/ucp/capabilities/identity.d.ts +130 -0
- package/dist/ucp/capabilities/identity.d.ts.map +1 -0
- package/dist/ucp/capabilities/identity.js +290 -0
- package/dist/ucp/capabilities/identity.js.map +1 -0
- package/dist/ucp/capabilities/order.d.ts +142 -0
- package/dist/ucp/capabilities/order.d.ts.map +1 -0
- package/dist/ucp/capabilities/order.js +383 -0
- package/dist/ucp/capabilities/order.js.map +1 -0
- package/dist/ucp/index.d.ts +18 -0
- package/dist/ucp/index.d.ts.map +1 -0
- package/dist/ucp/index.js +19 -0
- package/dist/ucp/index.js.map +1 -0
- package/dist/ucp/manifest.d.ts +62 -0
- package/dist/ucp/manifest.d.ts.map +1 -0
- package/dist/ucp/manifest.js +180 -0
- package/dist/ucp/manifest.js.map +1 -0
- package/dist/ucp/types.d.ts +327 -0
- package/dist/ucp/types.d.ts.map +1 -0
- package/dist/ucp/types.js +8 -0
- package/dist/ucp/types.js.map +1 -0
- package/package.json +3 -4
- package/src/auto-ui.ts +413 -0
- package/src/base.ts +22 -9
- package/src/cli-ui-renderer.ts +264 -0
- package/src/dependency-manager.ts +0 -1
- package/src/design-system/index.ts +30 -0
- package/src/design-system/tokens.ts +451 -0
- package/src/design-system/transaction-ui.ts +1038 -0
- package/src/generator.ts +68 -8
- package/src/index.ts +159 -101
- package/src/io.ts +493 -0
- package/src/path-resolver.ts +2 -1
- package/src/rendering/components.ts +785 -0
- package/src/rendering/field-analyzer.ts +299 -0
- package/src/rendering/field-renderers.ts +356 -0
- package/src/rendering/index.ts +63 -0
- package/src/rendering/layout-selector.ts +390 -0
- package/src/rendering/template-engine.ts +254 -0
- package/src/schema-extractor.ts +225 -12
- package/src/stateful.ts +301 -0
- package/src/types.ts +10 -1
- package/src/ucp/ap2/handlers.ts +779 -0
- package/src/ucp/ap2/mandates.ts +354 -0
- package/src/ucp/ap2/types.ts +441 -0
- package/src/ucp/capabilities/checkout.ts +497 -0
- package/src/ucp/capabilities/identity.ts +425 -0
- package/src/ucp/capabilities/order.ts +549 -0
- package/src/ucp/index.ts +27 -0
- package/src/ucp/manifest.ts +257 -0
- package/src/ucp/types.ts +454 -0
- package/dist/cli-formatter.d.ts +0 -92
- package/dist/cli-formatter.d.ts.map +0 -1
- package/dist/cli-formatter.js +0 -486
- package/dist/cli-formatter.js.map +0 -1
- package/dist/elicit.d.ts +0 -93
- package/dist/elicit.d.ts.map +0 -1
- package/dist/elicit.js +0 -373
- package/dist/elicit.js.map +0 -1
- package/dist/mcp-client.d.ts +0 -218
- package/dist/mcp-client.d.ts.map +0 -1
- package/dist/mcp-client.js +0 -424
- package/dist/mcp-client.js.map +0 -1
- package/dist/mcp-sdk-transport.d.ts +0 -88
- package/dist/mcp-sdk-transport.d.ts.map +0 -1
- package/dist/mcp-sdk-transport.js +0 -360
- package/dist/mcp-sdk-transport.js.map +0 -1
- package/dist/photon-config.d.ts +0 -86
- package/dist/photon-config.d.ts.map +0 -1
- package/dist/photon-config.js +0 -156
- package/dist/photon-config.js.map +0 -1
- package/src/cli-formatter.ts +0 -579
- package/src/elicit.ts +0 -438
- package/src/mcp-client.ts +0 -561
- package/src/mcp-sdk-transport.ts +0 -449
- 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
|
+
}
|