paybridge 0.5.0 → 0.7.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.
@@ -0,0 +1,179 @@
1
+ "use strict";
2
+ /**
3
+ * Ramp Network crypto on/off-ramp provider
4
+ * @see https://docs.ramp.network/
5
+ */
6
+ var __importDefault = (this && this.__importDefault) || function (mod) {
7
+ return (mod && mod.__esModule) ? mod : { "default": mod };
8
+ };
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.RampProvider = void 0;
11
+ const crypto_1 = __importDefault(require("crypto"));
12
+ const base_1 = require("./base");
13
+ const fetch_1 = require("../utils/fetch");
14
+ class RampProvider extends base_1.CryptoRampProvider {
15
+ constructor(config) {
16
+ super();
17
+ this.name = 'ramp';
18
+ this.hostApiKey = config.hostApiKey;
19
+ this.webhookSecret = config.webhookSecret;
20
+ this.sandbox = config.sandbox;
21
+ this.widgetUrl = this.sandbox
22
+ ? 'https://ri-widget-staging.firebaseapp.com'
23
+ : 'https://buy.ramp.network';
24
+ this.apiUrl = 'https://api.ramp.network/api/host-api/v3';
25
+ }
26
+ async getQuote(_direction, fiatAmount, _fiatCurrency, _cryptoAsset, _network) {
27
+ return {
28
+ fiatAmount,
29
+ cryptoAmount: 0,
30
+ rate: 0,
31
+ feeFixed: 0,
32
+ feePercent: 2.9,
33
+ feeTotal: (fiatAmount * 2.9) / 100,
34
+ expiresAt: new Date(Date.now() + 5 * 60 * 1000).toISOString(),
35
+ };
36
+ }
37
+ async createOnRamp(params) {
38
+ (0, base_1.validateWalletAddress)(params.destinationWallet, params.network);
39
+ const quote = await this.getQuote('on', params.fiatAmount, params.fiatCurrency, params.asset, params.network);
40
+ const swapAsset = `${params.asset}_${params.network}`;
41
+ const queryParams = new URLSearchParams({
42
+ hostApiKey: this.hostApiKey,
43
+ swapAsset: swapAsset.toUpperCase(),
44
+ fiatCurrency: params.fiatCurrency.toUpperCase(),
45
+ fiatValue: params.fiatAmount.toString(),
46
+ userAddress: params.destinationWallet,
47
+ userEmailAddress: params.customer.email,
48
+ finalUrl: params.urls.success,
49
+ });
50
+ const checkoutUrl = `${this.widgetUrl}?${queryParams}`;
51
+ return {
52
+ id: `ramp_on_${params.reference}`,
53
+ direction: 'on',
54
+ status: 'pending',
55
+ quote,
56
+ checkoutUrl,
57
+ createdAt: new Date().toISOString(),
58
+ expiresAt: quote.expiresAt,
59
+ };
60
+ }
61
+ async createOffRamp(params) {
62
+ if (params.sourceWallet) {
63
+ (0, base_1.validateWalletAddress)(params.sourceWallet, params.network);
64
+ }
65
+ const quote = await this.getQuote('off', params.cryptoAmount, params.fiatCurrency, params.asset, params.network);
66
+ const swapAsset = `${params.asset}_${params.network}`;
67
+ const queryParams = new URLSearchParams({
68
+ hostApiKey: this.hostApiKey,
69
+ swapAsset: swapAsset.toUpperCase(),
70
+ defaultFlow: 'OFFRAMP',
71
+ });
72
+ const checkoutUrl = `${this.widgetUrl}?${queryParams}`;
73
+ return {
74
+ id: `ramp_off_${params.reference}`,
75
+ direction: 'off',
76
+ status: 'pending',
77
+ quote,
78
+ checkoutUrl,
79
+ createdAt: new Date().toISOString(),
80
+ expiresAt: quote.expiresAt,
81
+ };
82
+ }
83
+ async getRamp(id) {
84
+ const response = await (0, fetch_1.timedFetch)(`${this.apiUrl}/${id}`);
85
+ if (!response.ok) {
86
+ throw new Error(`Ramp Network getRamp failed: ${response.status}`);
87
+ }
88
+ const data = (await response.json());
89
+ const direction = data.type === 'ONRAMP' ? 'on' : 'off';
90
+ const status = this.mapRampStatus(data.status);
91
+ const quote = {
92
+ fiatAmount: data.fiatValue || 0,
93
+ cryptoAmount: data.cryptoAmount || 0,
94
+ rate: data.assetExchangeRate || 0,
95
+ feeFixed: 0,
96
+ feePercent: 0,
97
+ feeTotal: data.appliedFee || 0,
98
+ expiresAt: new Date(Date.now() + 5 * 60 * 1000).toISOString(),
99
+ };
100
+ return {
101
+ id: data.id,
102
+ direction,
103
+ status,
104
+ quote,
105
+ txHash: data.cryptoTxHash,
106
+ createdAt: data.createdAt,
107
+ raw: data,
108
+ };
109
+ }
110
+ parseWebhook(body, _headers) {
111
+ const event = typeof body === 'string' ? JSON.parse(body) : body;
112
+ return {
113
+ type: event.type,
114
+ data: event.purchase || event,
115
+ raw: event,
116
+ };
117
+ }
118
+ /**
119
+ * Ramp Network webhook verification (HMAC placeholder).
120
+ *
121
+ * TODO(verify): Ramp Network uses ECDSA secp256k1 for webhook signatures.
122
+ * This implementation uses HMAC-SHA256 as a placeholder. Production deployments
123
+ * should implement ECDSA verification using Ramp's public key.
124
+ */
125
+ verifyWebhook(body, headers) {
126
+ if (!this.webhookSecret)
127
+ return false;
128
+ const signature = headers?.['x-body-signature'];
129
+ if (!signature)
130
+ return false;
131
+ const rawBody = typeof body === 'string' ? body : body.toString('utf8');
132
+ const computedSig = crypto_1.default
133
+ .createHmac('sha256', this.webhookSecret)
134
+ .update(rawBody)
135
+ .digest('hex');
136
+ try {
137
+ const computedBuffer = Buffer.from(computedSig, 'hex');
138
+ const expectedBuffer = Buffer.from(signature, 'hex');
139
+ if (computedBuffer.length !== expectedBuffer.length) {
140
+ return false;
141
+ }
142
+ return crypto_1.default.timingSafeEqual(computedBuffer, expectedBuffer);
143
+ }
144
+ catch {
145
+ return false;
146
+ }
147
+ }
148
+ getCapabilities() {
149
+ return {
150
+ supportedAssets: ['BTC', 'ETH', 'USDT', 'USDC', 'DAI', 'MATIC'],
151
+ supportedNetworks: ['BTC', 'ETH', 'POLYGON', 'BSC'],
152
+ supportedFiat: ['USD', 'EUR', 'GBP', 'ZAR', 'AUD', 'CAD'],
153
+ country: 'GLOBAL',
154
+ kycRequired: true,
155
+ onRampLimits: { min: 30, max: 30000 },
156
+ offRampLimits: { min: 50, max: 30000 },
157
+ fees: {
158
+ onRampPercent: 2.9,
159
+ offRampPercent: 1.9,
160
+ },
161
+ };
162
+ }
163
+ mapRampStatus(rampStatus) {
164
+ const statusMap = {
165
+ INITIALIZED: 'pending',
166
+ PAYMENT_STARTED: 'pending',
167
+ PAYMENT_IN_PROGRESS: 'pending',
168
+ PAYMENT_EXECUTED: 'pending',
169
+ FIAT_SENT: 'pending',
170
+ FIAT_RECEIVED: 'pending',
171
+ RELEASING: 'pending',
172
+ RELEASED: 'completed',
173
+ EXPIRED: 'expired',
174
+ CANCELLED: 'expired',
175
+ };
176
+ return statusMap[rampStatus] || 'pending';
177
+ }
178
+ }
179
+ exports.RampProvider = RampProvider;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Transak crypto on/off-ramp provider
3
+ * @see https://docs.transak.com/
4
+ */
5
+ import { CryptoRampProvider } from './base';
6
+ import { OnRampParams, OffRampParams, RampQuote, RampResult, CryptoRampCapabilities } from './types';
7
+ interface TransakConfig {
8
+ apiKey: string;
9
+ apiSecret: string;
10
+ sandbox: boolean;
11
+ webhookSecret?: string;
12
+ }
13
+ export declare class TransakProvider extends CryptoRampProvider {
14
+ readonly name = "transak";
15
+ private apiKey;
16
+ private apiSecret;
17
+ private sandbox;
18
+ private widgetUrl;
19
+ private apiUrl;
20
+ private webhookSecret?;
21
+ constructor(config: TransakConfig);
22
+ getQuote(direction: 'on' | 'off', fiatAmount: number, fiatCurrency: string, cryptoAsset: string, _network: string): Promise<RampQuote>;
23
+ createOnRamp(params: OnRampParams): Promise<RampResult>;
24
+ createOffRamp(params: OffRampParams): Promise<RampResult>;
25
+ getRamp(id: string): Promise<RampResult>;
26
+ parseWebhook(body: any, _headers?: any): any;
27
+ verifyWebhook(body: string | Buffer, headers?: any): boolean;
28
+ getCapabilities(): CryptoRampCapabilities;
29
+ private signWidgetUrl;
30
+ private mapTransakStatus;
31
+ }
32
+ export {};
@@ -0,0 +1,212 @@
1
+ "use strict";
2
+ /**
3
+ * Transak crypto on/off-ramp provider
4
+ * @see https://docs.transak.com/
5
+ */
6
+ var __importDefault = (this && this.__importDefault) || function (mod) {
7
+ return (mod && mod.__esModule) ? mod : { "default": mod };
8
+ };
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.TransakProvider = void 0;
11
+ const crypto_1 = __importDefault(require("crypto"));
12
+ const base_1 = require("./base");
13
+ const fetch_1 = require("../utils/fetch");
14
+ class TransakProvider extends base_1.CryptoRampProvider {
15
+ constructor(config) {
16
+ super();
17
+ this.name = 'transak';
18
+ this.apiKey = config.apiKey;
19
+ this.apiSecret = config.apiSecret;
20
+ this.sandbox = config.sandbox;
21
+ this.webhookSecret = config.webhookSecret;
22
+ this.widgetUrl = this.sandbox
23
+ ? 'https://global-stg.transak.com'
24
+ : 'https://global.transak.com';
25
+ this.apiUrl = this.sandbox
26
+ ? 'https://api-stg.transak.com'
27
+ : 'https://api.transak.com';
28
+ }
29
+ async getQuote(direction, fiatAmount, fiatCurrency, cryptoAsset, _network) {
30
+ const params = new URLSearchParams({
31
+ fiatCurrency: fiatCurrency.toUpperCase(),
32
+ cryptoCurrency: cryptoAsset.toUpperCase(),
33
+ fiatAmount: fiatAmount.toString(),
34
+ paymentMethod: 'credit_debit_card',
35
+ isBuyOrSell: direction === 'on' ? 'BUY' : 'SELL',
36
+ });
37
+ const url = `${this.apiUrl}/api/v2/currencies/price?${params}`;
38
+ const response = await (0, fetch_1.timedFetch)(url, {
39
+ headers: {
40
+ 'api-secret': this.apiSecret,
41
+ },
42
+ });
43
+ if (!response.ok) {
44
+ throw new Error(`Transak quote failed: ${response.status}`);
45
+ }
46
+ const data = (await response.json());
47
+ const cryptoAmount = data.response?.cryptoAmount || 0;
48
+ const totalFee = data.response?.totalFee || 0;
49
+ const rate = fiatAmount > 0 ? cryptoAmount / fiatAmount : 0;
50
+ return {
51
+ fiatAmount,
52
+ cryptoAmount,
53
+ rate,
54
+ feeFixed: 0,
55
+ feePercent: (totalFee / fiatAmount) * 100,
56
+ feeTotal: totalFee,
57
+ expiresAt: new Date(Date.now() + 5 * 60 * 1000).toISOString(),
58
+ };
59
+ }
60
+ async createOnRamp(params) {
61
+ (0, base_1.validateWalletAddress)(params.destinationWallet, params.network);
62
+ const quote = await this.getQuote('on', params.fiatAmount, params.fiatCurrency, params.asset, params.network);
63
+ const queryParams = new URLSearchParams({
64
+ apiKey: this.apiKey,
65
+ fiatAmount: params.fiatAmount.toString(),
66
+ fiatCurrency: params.fiatCurrency.toUpperCase(),
67
+ cryptoCurrencyCode: params.asset.toUpperCase(),
68
+ network: params.network.toUpperCase(),
69
+ walletAddress: params.destinationWallet,
70
+ email: params.customer.email,
71
+ partnerOrderId: params.reference,
72
+ redirectURL: params.urls.success,
73
+ });
74
+ const signature = this.signWidgetUrl(queryParams.toString());
75
+ queryParams.append('signature', signature);
76
+ const checkoutUrl = `${this.widgetUrl}?${queryParams}`;
77
+ return {
78
+ id: `transak_on_${params.reference}`,
79
+ direction: 'on',
80
+ status: 'pending',
81
+ quote,
82
+ checkoutUrl,
83
+ createdAt: new Date().toISOString(),
84
+ expiresAt: quote.expiresAt,
85
+ };
86
+ }
87
+ async createOffRamp(params) {
88
+ if (params.sourceWallet) {
89
+ (0, base_1.validateWalletAddress)(params.sourceWallet, params.network);
90
+ }
91
+ const quote = await this.getQuote('off', params.cryptoAmount, params.fiatCurrency, params.asset, params.network);
92
+ const queryParams = new URLSearchParams({
93
+ apiKey: this.apiKey,
94
+ productsAvailed: 'SELL',
95
+ cryptoCurrencyCode: params.asset.toUpperCase(),
96
+ network: params.network.toUpperCase(),
97
+ fiatCurrency: params.fiatCurrency.toUpperCase(),
98
+ partnerOrderId: params.reference,
99
+ });
100
+ if (params.urls?.success) {
101
+ queryParams.append('redirectURL', params.urls.success);
102
+ }
103
+ const signature = this.signWidgetUrl(queryParams.toString());
104
+ queryParams.append('signature', signature);
105
+ const checkoutUrl = `${this.widgetUrl}?${queryParams}`;
106
+ return {
107
+ id: `transak_off_${params.reference}`,
108
+ direction: 'off',
109
+ status: 'pending',
110
+ quote,
111
+ checkoutUrl,
112
+ createdAt: new Date().toISOString(),
113
+ expiresAt: quote.expiresAt,
114
+ };
115
+ }
116
+ async getRamp(id) {
117
+ const response = await (0, fetch_1.timedFetch)(`${this.apiUrl}/api/v2/orders/${id}`, {
118
+ headers: {
119
+ 'api-secret': this.apiSecret,
120
+ },
121
+ });
122
+ if (!response.ok) {
123
+ throw new Error(`Transak getRamp failed: ${response.status}`);
124
+ }
125
+ const data = (await response.json());
126
+ const order = data.response;
127
+ const direction = order.isBuyOrSell === 'BUY' ? 'on' : 'off';
128
+ const status = this.mapTransakStatus(order.status);
129
+ const quote = {
130
+ fiatAmount: order.fiatAmount || 0,
131
+ cryptoAmount: order.cryptoAmount || 0,
132
+ rate: order.conversionPrice || 0,
133
+ feeFixed: 0,
134
+ feePercent: 0,
135
+ feeTotal: order.totalFee || 0,
136
+ expiresAt: new Date(Date.now() + 5 * 60 * 1000).toISOString(),
137
+ };
138
+ return {
139
+ id: order.id,
140
+ direction,
141
+ status,
142
+ quote,
143
+ txHash: order.transactionHash,
144
+ createdAt: order.createdAt,
145
+ raw: data,
146
+ };
147
+ }
148
+ parseWebhook(body, _headers) {
149
+ const event = typeof body === 'string' ? JSON.parse(body) : body;
150
+ return {
151
+ type: event.eventName,
152
+ data: event.data,
153
+ raw: event,
154
+ };
155
+ }
156
+ verifyWebhook(body, headers) {
157
+ if (!this.webhookSecret)
158
+ return false;
159
+ const signature = headers?.['x-transak-signature'];
160
+ if (!signature)
161
+ return false;
162
+ const rawBody = typeof body === 'string' ? body : body.toString('utf8');
163
+ const computedSig = crypto_1.default
164
+ .createHmac('sha256', this.webhookSecret)
165
+ .update(rawBody)
166
+ .digest('hex');
167
+ try {
168
+ const computedBuffer = Buffer.from(computedSig, 'hex');
169
+ const expectedBuffer = Buffer.from(signature, 'hex');
170
+ if (computedBuffer.length !== expectedBuffer.length) {
171
+ return false;
172
+ }
173
+ return crypto_1.default.timingSafeEqual(computedBuffer, expectedBuffer);
174
+ }
175
+ catch {
176
+ return false;
177
+ }
178
+ }
179
+ getCapabilities() {
180
+ return {
181
+ supportedAssets: ['BTC', 'ETH', 'USDT', 'USDC', 'MATIC', 'BNB'],
182
+ supportedNetworks: ['BTC', 'ETH', 'POLYGON', 'BSC', 'TRON'],
183
+ supportedFiat: ['USD', 'EUR', 'GBP', 'ZAR', 'INR', 'AUD'],
184
+ country: 'GLOBAL',
185
+ kycRequired: true,
186
+ onRampLimits: { min: 30, max: 50000 },
187
+ offRampLimits: { min: 50, max: 50000 },
188
+ fees: {
189
+ onRampPercent: 2.0,
190
+ offRampPercent: 1.5,
191
+ },
192
+ };
193
+ }
194
+ signWidgetUrl(queryString) {
195
+ return crypto_1.default
196
+ .createHmac('sha256', this.apiSecret)
197
+ .update(queryString)
198
+ .digest('base64');
199
+ }
200
+ mapTransakStatus(transakStatus) {
201
+ const statusMap = {
202
+ AWAITING_PAYMENT_FROM_USER: 'pending',
203
+ PROCESSING: 'pending',
204
+ COMPLETED: 'completed',
205
+ FAILED: 'failed',
206
+ EXPIRED: 'expired',
207
+ CANCELLED: 'expired',
208
+ };
209
+ return statusMap[transakStatus] || 'pending';
210
+ }
211
+ }
212
+ exports.TransakProvider = TransakProvider;
package/dist/index.js CHANGED
@@ -32,6 +32,9 @@ const flutterwave_1 = require("./providers/flutterwave");
32
32
  const adyen_1 = require("./providers/adyen");
33
33
  const mercadopago_1 = require("./providers/mercadopago");
34
34
  const razorpay_1 = require("./providers/razorpay");
35
+ const mollie_1 = require("./providers/mollie");
36
+ const square_1 = require("./providers/square");
37
+ const pesapal_1 = require("./providers/pesapal");
35
38
  __exportStar(require("./types"), exports);
36
39
  __exportStar(require("./utils/currency"), exports);
37
40
  __exportStar(require("./utils/fetch"), exports);
@@ -168,6 +171,38 @@ class PayBridge {
168
171
  sandbox,
169
172
  webhookSecret,
170
173
  });
174
+ case 'mollie':
175
+ if (!credentials.apiKey) {
176
+ throw new Error('Mollie requires apiKey (test_* or live_*)');
177
+ }
178
+ return new mollie_1.MollieProvider({
179
+ apiKey: credentials.apiKey,
180
+ sandbox,
181
+ webhookSecret,
182
+ });
183
+ case 'square':
184
+ if (!credentials.apiKey || !credentials.locationId) {
185
+ throw new Error('Square requires apiKey (access token) and locationId');
186
+ }
187
+ return new square_1.SquareProvider({
188
+ accessToken: credentials.apiKey,
189
+ locationId: credentials.locationId,
190
+ notificationUrl: credentials.notificationUrl,
191
+ sandbox,
192
+ webhookSecret,
193
+ });
194
+ case 'pesapal':
195
+ if (!credentials.apiKey || !credentials.secretKey) {
196
+ throw new Error('Pesapal requires apiKey (consumer_key) and secretKey (consumer_secret)');
197
+ }
198
+ return new pesapal_1.PesapalProvider({
199
+ consumerKey: credentials.apiKey,
200
+ consumerSecret: credentials.secretKey,
201
+ notificationId: credentials.notificationId,
202
+ username: credentials.username,
203
+ sandbox,
204
+ webhookSecret,
205
+ });
171
206
  default:
172
207
  throw new Error(`Unknown provider: ${provider}`);
173
208
  }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Mollie payment provider
3
+ * EU-focused payment gateway supporting 9 currencies
4
+ * @see https://docs.mollie.com/reference
5
+ */
6
+ import { PaymentProvider } from './base';
7
+ import { CreatePaymentParams, PaymentResult, CreateSubscriptionParams, SubscriptionResult, RefundParams, RefundResult, WebhookEvent } from '../types';
8
+ import { ProviderCapabilities } from '../routing-types';
9
+ interface MollieConfig {
10
+ apiKey: string;
11
+ webhookSecret?: string;
12
+ sandbox?: boolean;
13
+ }
14
+ export declare class MollieProvider extends PaymentProvider {
15
+ readonly name = "mollie";
16
+ readonly supportedCurrencies: string[];
17
+ private apiKey;
18
+ private webhookSecret?;
19
+ private sandbox;
20
+ private baseUrl;
21
+ constructor(config: MollieConfig);
22
+ private apiRequest;
23
+ createPayment(params: CreatePaymentParams): Promise<PaymentResult>;
24
+ createSubscription(_params: CreateSubscriptionParams): Promise<SubscriptionResult>;
25
+ getPayment(id: string): Promise<PaymentResult>;
26
+ refund(params: RefundParams): Promise<RefundResult>;
27
+ parseWebhook(body: any, _headers?: any): WebhookEvent;
28
+ /**
29
+ * Mollie webhooks have no signature verification scheme.
30
+ * Security comes from:
31
+ * 1. Validating source IP (caller's responsibility)
32
+ * 2. Calling getPayment(id) to verify actual status
33
+ *
34
+ * Always returns true to indicate no validation error (Mollie design limitation).
35
+ */
36
+ verifyWebhook(_body: string | Buffer, _headers?: any): boolean;
37
+ getCapabilities(): ProviderCapabilities;
38
+ }
39
+ export {};
@@ -0,0 +1,178 @@
1
+ "use strict";
2
+ /**
3
+ * Mollie payment provider
4
+ * EU-focused payment gateway supporting 9 currencies
5
+ * @see https://docs.mollie.com/reference
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.MollieProvider = void 0;
9
+ const base_1 = require("./base");
10
+ const fetch_1 = require("../utils/fetch");
11
+ let webhookSecretWarned = false;
12
+ class MollieProvider extends base_1.PaymentProvider {
13
+ constructor(config) {
14
+ super();
15
+ this.name = 'mollie';
16
+ this.supportedCurrencies = ['EUR', 'USD', 'GBP', 'CHF', 'CAD', 'AUD', 'DKK', 'SEK', 'NOK'];
17
+ this.baseUrl = 'https://api.mollie.com/v2';
18
+ this.apiKey = config.apiKey;
19
+ this.webhookSecret = config.webhookSecret;
20
+ this.sandbox = config.sandbox ?? this.apiKey.startsWith('test_');
21
+ if (this.webhookSecret && !webhookSecretWarned) {
22
+ console.warn('[PayBridge:Mollie] Mollie has no webhook signature scheme. Webhook validation relies on getPayment() round-trip. Validate by source IP if possible.');
23
+ webhookSecretWarned = true;
24
+ }
25
+ }
26
+ async apiRequest(method, path, data) {
27
+ const url = `${this.baseUrl}${path}`;
28
+ const response = await (0, fetch_1.timedFetchOrThrow)(url, {
29
+ method,
30
+ headers: {
31
+ Authorization: `Bearer ${this.apiKey}`,
32
+ 'Content-Type': 'application/json',
33
+ },
34
+ body: data ? JSON.stringify(data) : undefined,
35
+ });
36
+ return (await response.json());
37
+ }
38
+ async createPayment(params) {
39
+ this.validateCurrency(params.currency);
40
+ const requestBody = {
41
+ amount: {
42
+ value: params.amount.toFixed(2),
43
+ currency: params.currency,
44
+ },
45
+ description: params.description || params.reference,
46
+ redirectUrl: params.urls.success,
47
+ cancelUrl: params.urls.cancel,
48
+ webhookUrl: params.urls.webhook,
49
+ metadata: {
50
+ reference: params.reference,
51
+ ...params.metadata,
52
+ },
53
+ };
54
+ const response = await this.apiRequest('POST', '/payments', requestBody);
55
+ return {
56
+ id: response.id,
57
+ checkoutUrl: response._links.checkout.href,
58
+ status: 'pending',
59
+ amount: params.amount,
60
+ currency: params.currency.toUpperCase(),
61
+ reference: params.reference,
62
+ provider: 'mollie',
63
+ createdAt: response.createdAt || new Date().toISOString(),
64
+ raw: response,
65
+ };
66
+ }
67
+ async createSubscription(_params) {
68
+ throw new Error('Mollie subscriptions require Customer + Mandate setup; not yet supported by paybridge. Use the Mollie Customers API directly or choose another provider.');
69
+ }
70
+ async getPayment(id) {
71
+ const response = await this.apiRequest('GET', `/payments/${id}`);
72
+ let status = 'pending';
73
+ if (response.status === 'paid') {
74
+ status = 'completed';
75
+ }
76
+ else if (response.status === 'failed' || response.status === 'expired') {
77
+ status = 'failed';
78
+ }
79
+ else if (response.status === 'canceled') {
80
+ status = 'cancelled';
81
+ }
82
+ else if (response.status === 'open' || response.status === 'pending') {
83
+ status = 'pending';
84
+ }
85
+ const currency = (response.amount?.currency || 'EUR').toUpperCase();
86
+ const amount = response.amount?.value ? parseFloat(response.amount.value) : 0;
87
+ return {
88
+ id: response.id,
89
+ checkoutUrl: response._links?.checkout?.href || '',
90
+ status,
91
+ amount,
92
+ currency,
93
+ reference: response.metadata?.reference || response.id,
94
+ provider: 'mollie',
95
+ createdAt: response.createdAt || new Date().toISOString(),
96
+ raw: response,
97
+ };
98
+ }
99
+ async refund(params) {
100
+ const refundData = {
101
+ description: params.reason || 'Refund',
102
+ };
103
+ if (params.amount !== undefined) {
104
+ const currency = 'EUR';
105
+ refundData.amount = {
106
+ value: params.amount.toFixed(2),
107
+ currency,
108
+ };
109
+ }
110
+ const response = await this.apiRequest('POST', `/payments/${params.paymentId}/refunds`, refundData);
111
+ const currency = (response.amount?.currency || 'EUR').toUpperCase();
112
+ const amount = response.amount?.value ? parseFloat(response.amount.value) : 0;
113
+ let refundStatus = 'pending';
114
+ if (response.status === 'refunded') {
115
+ refundStatus = 'completed';
116
+ }
117
+ else if (response.status === 'failed') {
118
+ refundStatus = 'failed';
119
+ }
120
+ return {
121
+ id: response.id,
122
+ status: refundStatus,
123
+ amount,
124
+ currency,
125
+ paymentId: params.paymentId,
126
+ createdAt: response.createdAt || new Date().toISOString(),
127
+ raw: response,
128
+ };
129
+ }
130
+ parseWebhook(body, _headers) {
131
+ let paymentId;
132
+ if (typeof body === 'string') {
133
+ const parsed = new URLSearchParams(body);
134
+ paymentId = parsed.get('id') || '';
135
+ }
136
+ else {
137
+ paymentId = body.id || '';
138
+ }
139
+ return {
140
+ type: 'payment.pending',
141
+ payment: {
142
+ id: paymentId,
143
+ checkoutUrl: '',
144
+ status: 'pending',
145
+ amount: 0,
146
+ currency: 'EUR',
147
+ reference: paymentId,
148
+ provider: 'mollie',
149
+ createdAt: new Date().toISOString(),
150
+ },
151
+ raw: body,
152
+ };
153
+ }
154
+ /**
155
+ * Mollie webhooks have no signature verification scheme.
156
+ * Security comes from:
157
+ * 1. Validating source IP (caller's responsibility)
158
+ * 2. Calling getPayment(id) to verify actual status
159
+ *
160
+ * Always returns true to indicate no validation error (Mollie design limitation).
161
+ */
162
+ verifyWebhook(_body, _headers) {
163
+ return true;
164
+ }
165
+ getCapabilities() {
166
+ return {
167
+ fees: {
168
+ fixed: 0.25,
169
+ percent: 1.8,
170
+ currency: 'EUR',
171
+ },
172
+ currencies: this.supportedCurrencies,
173
+ country: 'EU',
174
+ avgLatencyMs: 350,
175
+ };
176
+ }
177
+ }
178
+ exports.MollieProvider = MollieProvider;