payment-skill 1.0.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/LICENSE +62 -0
- package/README.md +545 -0
- package/SKILL.md +99 -0
- package/SUPPORT.md +153 -0
- package/bin/payment-skill.js +2 -0
- package/dashboard.html +669 -0
- package/dist/api/bunq.d.ts +35 -0
- package/dist/api/bunq.d.ts.map +1 -0
- package/dist/api/bunq.js +164 -0
- package/dist/api/bunq.js.map +1 -0
- package/dist/api/wise.d.ts +32 -0
- package/dist/api/wise.d.ts.map +1 -0
- package/dist/api/wise.js +155 -0
- package/dist/api/wise.js.map +1 -0
- package/dist/cli.d.ts +8 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +69 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/bunq.d.ts +8 -0
- package/dist/commands/bunq.d.ts.map +1 -0
- package/dist/commands/bunq.js +193 -0
- package/dist/commands/bunq.js.map +1 -0
- package/dist/commands/config.d.ts +8 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +70 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/emergency.d.ts +8 -0
- package/dist/commands/emergency.d.ts.map +1 -0
- package/dist/commands/emergency.js +85 -0
- package/dist/commands/emergency.js.map +1 -0
- package/dist/commands/limits.d.ts +6 -0
- package/dist/commands/limits.d.ts.map +1 -0
- package/dist/commands/limits.js +125 -0
- package/dist/commands/limits.js.map +1 -0
- package/dist/commands/merchant.d.ts +6 -0
- package/dist/commands/merchant.d.ts.map +1 -0
- package/dist/commands/merchant.js +41 -0
- package/dist/commands/merchant.js.map +1 -0
- package/dist/commands/pay.d.ts +10 -0
- package/dist/commands/pay.d.ts.map +1 -0
- package/dist/commands/pay.js +112 -0
- package/dist/commands/pay.js.map +1 -0
- package/dist/commands/provider.d.ts +6 -0
- package/dist/commands/provider.d.ts.map +1 -0
- package/dist/commands/provider.js +74 -0
- package/dist/commands/provider.js.map +1 -0
- package/dist/commands/server.d.ts +8 -0
- package/dist/commands/server.d.ts.map +1 -0
- package/dist/commands/server.js +92 -0
- package/dist/commands/server.js.map +1 -0
- package/dist/commands/template.d.ts +8 -0
- package/dist/commands/template.d.ts.map +1 -0
- package/dist/commands/template.js +161 -0
- package/dist/commands/template.js.map +1 -0
- package/dist/commands/transaction.d.ts +6 -0
- package/dist/commands/transaction.d.ts.map +1 -0
- package/dist/commands/transaction.js +72 -0
- package/dist/commands/transaction.js.map +1 -0
- package/dist/commands/wise.d.ts +8 -0
- package/dist/commands/wise.d.ts.map +1 -0
- package/dist/commands/wise.js +240 -0
- package/dist/commands/wise.js.map +1 -0
- package/dist/core/config.d.ts +40 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +201 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/template-engine.d.ts +27 -0
- package/dist/core/template-engine.d.ts.map +1 -0
- package/dist/core/template-engine.js +410 -0
- package/dist/core/template-engine.js.map +1 -0
- package/dist/core/transaction.d.ts +31 -0
- package/dist/core/transaction.d.ts.map +1 -0
- package/dist/core/transaction.js +214 -0
- package/dist/core/transaction.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +36 -0
- package/dist/index.js.map +1 -0
- package/dist/server/server.d.ts +14 -0
- package/dist/server/server.d.ts.map +1 -0
- package/dist/server/server.js +120 -0
- package/dist/server/server.js.map +1 -0
- package/dist/types/index.d.ts +141 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +8 -0
- package/dist/types/index.js.map +1 -0
- package/logo.png +0 -0
- package/package.json +78 -0
- package/src/api/bunq.ts +257 -0
- package/src/api/wise.ts +204 -0
- package/src/cli.ts +67 -0
- package/src/commands/bunq.ts +223 -0
- package/src/commands/config.ts +72 -0
- package/src/commands/emergency.ts +94 -0
- package/src/commands/limits.ts +126 -0
- package/src/commands/merchant.ts +39 -0
- package/src/commands/pay.ts +109 -0
- package/src/commands/provider.ts +75 -0
- package/src/commands/server.ts +59 -0
- package/src/commands/template.ts +172 -0
- package/src/commands/transaction.ts +66 -0
- package/src/commands/wise.ts +279 -0
- package/src/core/config.ts +202 -0
- package/src/core/template-engine.ts +454 -0
- package/src/core/transaction.ts +228 -0
- package/src/index.ts +14 -0
- package/src/server/server.ts +131 -0
- package/src/types/index.ts +178 -0
- package/tsconfig.json +23 -0
- package/verified-merchants.json +63 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Payment Skill - Core Configuration Manager
|
|
3
|
+
*
|
|
4
|
+
* Manages application configuration, limits, and emergency stop state
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as fs from 'fs-extra';
|
|
8
|
+
import * as path from 'path';
|
|
9
|
+
import {
|
|
10
|
+
ProviderConfig,
|
|
11
|
+
LimitConfig,
|
|
12
|
+
TimeWindowConfig,
|
|
13
|
+
EmergencyStopState,
|
|
14
|
+
WiseConfig,
|
|
15
|
+
BunqConfig
|
|
16
|
+
} from '../types';
|
|
17
|
+
|
|
18
|
+
const CONFIG_DIR = path.join(process.env.HOME || process.env.USERPROFILE || '.', '.payment-skill');
|
|
19
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
20
|
+
const EMERGENCY_FILE = path.join(CONFIG_DIR, 'emergency.json');
|
|
21
|
+
|
|
22
|
+
export class ConfigManager {
|
|
23
|
+
private config: any = {};
|
|
24
|
+
private emergencyState: EmergencyStopState = { active: false, pendingTransactions: [] };
|
|
25
|
+
|
|
26
|
+
constructor() {
|
|
27
|
+
this.ensureConfigDir();
|
|
28
|
+
this.loadConfig();
|
|
29
|
+
this.loadEmergencyState();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
private ensureConfigDir(): void {
|
|
33
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
34
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private loadConfig(): void {
|
|
39
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
40
|
+
this.config = fs.readJsonSync(CONFIG_FILE);
|
|
41
|
+
} else {
|
|
42
|
+
this.config = this.getDefaultConfig();
|
|
43
|
+
this.saveConfig();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private loadEmergencyState(): void {
|
|
48
|
+
if (fs.existsSync(EMERGENCY_FILE)) {
|
|
49
|
+
this.emergencyState = fs.readJsonSync(EMERGENCY_FILE);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private saveConfig(): void {
|
|
54
|
+
fs.writeJsonSync(CONFIG_FILE, this.config, { spaces: 2 });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private saveEmergencyState(): void {
|
|
58
|
+
fs.writeJsonSync(EMERGENCY_FILE, this.emergencyState, { spaces: 2 });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
private getDefaultConfig(): any {
|
|
62
|
+
return {
|
|
63
|
+
version: '1.0.0',
|
|
64
|
+
providers: {},
|
|
65
|
+
limits: {
|
|
66
|
+
perTransaction: 10000,
|
|
67
|
+
daily: 50000,
|
|
68
|
+
weekly: 200000,
|
|
69
|
+
monthly: 500000,
|
|
70
|
+
maxTransactionsPerHour: 10
|
|
71
|
+
},
|
|
72
|
+
timeWindow: {
|
|
73
|
+
enabled: false,
|
|
74
|
+
start: '08:00',
|
|
75
|
+
end: '22:00',
|
|
76
|
+
timezone: 'Europe/Bucharest'
|
|
77
|
+
},
|
|
78
|
+
blockedCategories: ['gambling', 'adult', 'drugs', 'weapons', 'tobacco'],
|
|
79
|
+
verifiedMerchantsOnly: true,
|
|
80
|
+
webhookUrl: null
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Provider Management
|
|
85
|
+
setProvider(name: string, config: ProviderConfig): void {
|
|
86
|
+
this.config.providers[name] = config;
|
|
87
|
+
this.saveConfig();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
getProvider(name: string): ProviderConfig | null {
|
|
91
|
+
return this.config.providers[name] || null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
getAllProviders(): Record<string, ProviderConfig> {
|
|
95
|
+
return this.config.providers;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
removeProvider(name: string): void {
|
|
99
|
+
delete this.config.providers[name];
|
|
100
|
+
this.saveConfig();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Limits Management
|
|
104
|
+
getLimits(): LimitConfig {
|
|
105
|
+
return this.config.limits;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
setLimits(limits: Partial<LimitConfig>): void {
|
|
109
|
+
this.config.limits = { ...this.config.limits, ...limits };
|
|
110
|
+
this.saveConfig();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Time Window Management
|
|
114
|
+
getTimeWindow(): TimeWindowConfig {
|
|
115
|
+
return this.config.timeWindow;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
setTimeWindow(config: Partial<TimeWindowConfig>): void {
|
|
119
|
+
this.config.timeWindow = { ...this.config.timeWindow, ...config };
|
|
120
|
+
this.saveConfig();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Emergency Stop
|
|
124
|
+
activateEmergencyStop(reason?: string): void {
|
|
125
|
+
this.emergencyState = {
|
|
126
|
+
active: true,
|
|
127
|
+
activatedAt: new Date(),
|
|
128
|
+
reason: reason || 'Manual activation',
|
|
129
|
+
pendingTransactions: this.emergencyState.pendingTransactions
|
|
130
|
+
};
|
|
131
|
+
this.saveEmergencyState();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
deactivateEmergencyStop(): void {
|
|
135
|
+
this.emergencyState = {
|
|
136
|
+
active: false,
|
|
137
|
+
pendingTransactions: []
|
|
138
|
+
};
|
|
139
|
+
this.saveEmergencyState();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
isEmergencyStopActive(): boolean {
|
|
143
|
+
return this.emergencyState.active;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
getEmergencyStopState(): EmergencyStopState {
|
|
147
|
+
return this.emergencyState;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
addPendingTransaction(transactionId: string): void {
|
|
151
|
+
if (!this.emergencyState.pendingTransactions.includes(transactionId)) {
|
|
152
|
+
this.emergencyState.pendingTransactions.push(transactionId);
|
|
153
|
+
this.saveEmergencyState();
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
removePendingTransaction(transactionId: string): void {
|
|
158
|
+
this.emergencyState.pendingTransactions =
|
|
159
|
+
this.emergencyState.pendingTransactions.filter(id => id !== transactionId);
|
|
160
|
+
this.saveEmergencyState();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// General Config
|
|
164
|
+
getConfig(): any {
|
|
165
|
+
return this.config;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
setConfig(key: string, value: any): void {
|
|
169
|
+
this.config[key] = value;
|
|
170
|
+
this.saveConfig();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Blocked Categories
|
|
174
|
+
getBlockedCategories(): string[] {
|
|
175
|
+
return this.config.blockedCategories;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
addBlockedCategory(category: string): void {
|
|
179
|
+
if (!this.config.blockedCategories.includes(category)) {
|
|
180
|
+
this.config.blockedCategories.push(category);
|
|
181
|
+
this.saveConfig();
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
removeBlockedCategory(category: string): void {
|
|
186
|
+
this.config.blockedCategories =
|
|
187
|
+
this.config.blockedCategories.filter((c: string) => c !== category);
|
|
188
|
+
this.saveConfig();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Webhook URL
|
|
192
|
+
getWebhookUrl(): string | null {
|
|
193
|
+
return this.config.webhookUrl;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
setWebhookUrl(url: string): void {
|
|
197
|
+
this.config.webhookUrl = url;
|
|
198
|
+
this.saveConfig();
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export const configManager = new ConfigManager();
|
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Payment Skill - Template Engine
|
|
3
|
+
*
|
|
4
|
+
* Implements the configurable template-based payment flow system
|
|
5
|
+
* OpenClaw selects templates and provides parameters
|
|
6
|
+
* Payment-skill executes the predefined flow
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import * as fs from 'fs-extra';
|
|
10
|
+
import * as path from 'path';
|
|
11
|
+
import { CommandTemplate, TemplateStep, Transaction } from '../types';
|
|
12
|
+
import { WiseClient } from '../api/wise';
|
|
13
|
+
import { BunqClient } from '../api/bunq';
|
|
14
|
+
import { configManager } from './config';
|
|
15
|
+
import { transactionManager } from './transaction';
|
|
16
|
+
|
|
17
|
+
const TEMPLATES_DIR = path.join(__dirname, '../../templates');
|
|
18
|
+
|
|
19
|
+
export class TemplateEngine {
|
|
20
|
+
private templates: Map<string, CommandTemplate> = new Map();
|
|
21
|
+
|
|
22
|
+
constructor() {
|
|
23
|
+
this.loadTemplates();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
private loadTemplates(): void {
|
|
27
|
+
// Ensure templates directory exists
|
|
28
|
+
if (!fs.existsSync(TEMPLATES_DIR)) {
|
|
29
|
+
fs.mkdirSync(TEMPLATES_DIR, { recursive: true });
|
|
30
|
+
this.createDefaultTemplates();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Load all template files
|
|
34
|
+
const files = fs.readdirSync(TEMPLATES_DIR).filter(f => f.endsWith('.json'));
|
|
35
|
+
|
|
36
|
+
for (const file of files) {
|
|
37
|
+
const template = fs.readJsonSync(path.join(TEMPLATES_DIR, file));
|
|
38
|
+
this.templates.set(template.templateId, template);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private createDefaultTemplates(): void {
|
|
43
|
+
const defaultTemplates: CommandTemplate[] = [
|
|
44
|
+
{
|
|
45
|
+
templateId: 'wise_standard_transfer',
|
|
46
|
+
merchant: 'wise.com',
|
|
47
|
+
version: '1.0.0',
|
|
48
|
+
description: 'Standard Wise transfer flow with PSD2 confirmation',
|
|
49
|
+
prerequisites: {
|
|
50
|
+
apiKey: 'required',
|
|
51
|
+
webhookEndpoint: 'recommended'
|
|
52
|
+
},
|
|
53
|
+
steps: [
|
|
54
|
+
{
|
|
55
|
+
order: 1,
|
|
56
|
+
name: 'create_quote',
|
|
57
|
+
command: 'wise.createQuote',
|
|
58
|
+
params: {
|
|
59
|
+
profileId: '{{profileId}}',
|
|
60
|
+
sourceCurrency: '{{sourceCurrency}}',
|
|
61
|
+
targetCurrency: '{{targetCurrency}}',
|
|
62
|
+
sourceAmount: '{{amount}}'
|
|
63
|
+
},
|
|
64
|
+
output: {
|
|
65
|
+
quoteId: 'id',
|
|
66
|
+
rate: 'rate',
|
|
67
|
+
fee: 'fee'
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
order: 2,
|
|
72
|
+
name: 'create_transfer',
|
|
73
|
+
command: 'wise.createTransfer',
|
|
74
|
+
params: {
|
|
75
|
+
profileId: '{{profileId}}',
|
|
76
|
+
quoteId: '{{quoteId}}',
|
|
77
|
+
targetAccountId: '{{recipientId}}',
|
|
78
|
+
reference: '{{reference}}'
|
|
79
|
+
},
|
|
80
|
+
output: {
|
|
81
|
+
transferId: 'id',
|
|
82
|
+
status: 'status'
|
|
83
|
+
},
|
|
84
|
+
async: true
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
order: 3,
|
|
88
|
+
name: 'fund_transfer',
|
|
89
|
+
command: 'wise.fundTransfer',
|
|
90
|
+
params: {
|
|
91
|
+
profileId: '{{profileId}}',
|
|
92
|
+
transferId: '{{transferId}}'
|
|
93
|
+
},
|
|
94
|
+
async: true,
|
|
95
|
+
confirmation: {
|
|
96
|
+
type: 'webhook',
|
|
97
|
+
events: ['transfer.completed', 'transfer.failed'],
|
|
98
|
+
timeout: 300
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
],
|
|
102
|
+
errorHandling: {
|
|
103
|
+
retryOn: ['network_error', 'rate_limit'],
|
|
104
|
+
maxRetries: 3,
|
|
105
|
+
fallback: 'cancel_transfer'
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
templateId: 'bunq_instant_payment',
|
|
110
|
+
merchant: 'bunq.com',
|
|
111
|
+
version: '1.0.0',
|
|
112
|
+
description: 'Instant Bunq payment to IBAN',
|
|
113
|
+
prerequisites: {
|
|
114
|
+
apiKey: 'required',
|
|
115
|
+
webhookEndpoint: 'optional'
|
|
116
|
+
},
|
|
117
|
+
steps: [
|
|
118
|
+
{
|
|
119
|
+
order: 1,
|
|
120
|
+
name: 'create_payment',
|
|
121
|
+
command: 'bunq.createPayment',
|
|
122
|
+
params: {
|
|
123
|
+
userId: '{{userId}}',
|
|
124
|
+
accountId: '{{accountId}}',
|
|
125
|
+
amount: '{{amount}}',
|
|
126
|
+
currency: '{{currency}}',
|
|
127
|
+
counterpartyIban: '{{recipientIban}}',
|
|
128
|
+
counterpartyName: '{{recipientName}}',
|
|
129
|
+
description: '{{description}}'
|
|
130
|
+
},
|
|
131
|
+
output: {
|
|
132
|
+
paymentId: 'id',
|
|
133
|
+
status: 'status'
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
],
|
|
137
|
+
errorHandling: {
|
|
138
|
+
retryOn: ['network_error'],
|
|
139
|
+
maxRetries: 2
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
templateId: 'bunq_payment_request',
|
|
144
|
+
merchant: 'bunq.com',
|
|
145
|
+
version: '1.0.0',
|
|
146
|
+
description: 'Request payment from someone via Bunq',
|
|
147
|
+
prerequisites: {
|
|
148
|
+
apiKey: 'required'
|
|
149
|
+
},
|
|
150
|
+
steps: [
|
|
151
|
+
{
|
|
152
|
+
order: 1,
|
|
153
|
+
name: 'create_request',
|
|
154
|
+
command: 'bunq.createRequestInquiry',
|
|
155
|
+
params: {
|
|
156
|
+
userId: '{{userId}}',
|
|
157
|
+
accountId: '{{accountId}}',
|
|
158
|
+
amount: '{{amount}}',
|
|
159
|
+
currency: '{{currency}}',
|
|
160
|
+
counterpartyAlias: {
|
|
161
|
+
type: '{{recipientType}}',
|
|
162
|
+
value: '{{recipientValue}}'
|
|
163
|
+
},
|
|
164
|
+
description: '{{description}}'
|
|
165
|
+
},
|
|
166
|
+
output: {
|
|
167
|
+
requestId: 'id',
|
|
168
|
+
status: 'status'
|
|
169
|
+
},
|
|
170
|
+
async: true,
|
|
171
|
+
confirmation: {
|
|
172
|
+
type: 'poll',
|
|
173
|
+
timeout: 86400,
|
|
174
|
+
pollInterval: 60
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
],
|
|
178
|
+
errorHandling: {
|
|
179
|
+
retryOn: ['network_error'],
|
|
180
|
+
maxRetries: 3
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
templateId: 'stripe_connect_charge',
|
|
185
|
+
merchant: 'stripe.com',
|
|
186
|
+
version: '1.0.0',
|
|
187
|
+
description: 'Create charge through Stripe Connect',
|
|
188
|
+
prerequisites: {
|
|
189
|
+
apiKey: 'required',
|
|
190
|
+
webhookEndpoint: 'required'
|
|
191
|
+
},
|
|
192
|
+
steps: [
|
|
193
|
+
{
|
|
194
|
+
order: 1,
|
|
195
|
+
name: 'create_payment_intent',
|
|
196
|
+
command: 'stripe.paymentIntents.create',
|
|
197
|
+
params: {
|
|
198
|
+
amount: '{{amount}}',
|
|
199
|
+
currency: '{{currency}}',
|
|
200
|
+
customer: '{{customerId}}',
|
|
201
|
+
automatic_payment_methods: { enabled: true }
|
|
202
|
+
},
|
|
203
|
+
output: {
|
|
204
|
+
paymentIntentId: 'id',
|
|
205
|
+
clientSecret: 'client_secret',
|
|
206
|
+
status: 'status'
|
|
207
|
+
},
|
|
208
|
+
async: true
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
order: 2,
|
|
212
|
+
name: 'confirm_payment',
|
|
213
|
+
command: 'stripe.paymentIntents.confirm',
|
|
214
|
+
params: {
|
|
215
|
+
paymentIntentId: '{{paymentIntentId}}'
|
|
216
|
+
},
|
|
217
|
+
condition: 'requires_confirmation',
|
|
218
|
+
async: true,
|
|
219
|
+
confirmation: {
|
|
220
|
+
type: 'webhook',
|
|
221
|
+
events: ['payment_intent.succeeded', 'payment_intent.payment_failed'],
|
|
222
|
+
timeout: 600
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
],
|
|
226
|
+
errorHandling: {
|
|
227
|
+
retryOn: ['network_error', 'idempotency_error'],
|
|
228
|
+
maxRetries: 3
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
];
|
|
232
|
+
|
|
233
|
+
for (const template of defaultTemplates) {
|
|
234
|
+
const filePath = path.join(TEMPLATES_DIR, `${template.templateId}.json`);
|
|
235
|
+
fs.writeJsonSync(filePath, template, { spaces: 2 });
|
|
236
|
+
this.templates.set(template.templateId, template);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
getTemplate(templateId: string): CommandTemplate | null {
|
|
241
|
+
return this.templates.get(templateId) || null;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
getAllTemplates(): CommandTemplate[] {
|
|
245
|
+
return Array.from(this.templates.values());
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
getTemplatesForMerchant(merchant: string): CommandTemplate[] {
|
|
249
|
+
return this.getAllTemplates().filter(t => t.merchant === merchant);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async executeTemplate(
|
|
253
|
+
templateId: string,
|
|
254
|
+
params: Record<string, any>
|
|
255
|
+
): Promise<Transaction> {
|
|
256
|
+
const template = this.getTemplate(templateId);
|
|
257
|
+
if (!template) {
|
|
258
|
+
throw new Error(`Template '${templateId}' not found`);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Check emergency stop
|
|
262
|
+
if (configManager.isEmergencyStopActive()) {
|
|
263
|
+
throw new Error('Emergency stop is active. Cannot execute template.');
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Create transaction record
|
|
267
|
+
const tx = transactionManager.createTransaction(
|
|
268
|
+
template.merchant,
|
|
269
|
+
templateId,
|
|
270
|
+
parseFloat(params.amount) || 0,
|
|
271
|
+
params.currency || 'EUR',
|
|
272
|
+
{ templateId, params }
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
try {
|
|
276
|
+
// Execute each step
|
|
277
|
+
const context: Record<string, any> = { ...params };
|
|
278
|
+
|
|
279
|
+
for (const step of template.steps.sort((a, b) => a.order - b.order)) {
|
|
280
|
+
await this.executeStep(step, context, tx);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
transactionManager.updateTransactionStatus(tx.id, 'completed');
|
|
284
|
+
return tx;
|
|
285
|
+
} catch (error: any) {
|
|
286
|
+
transactionManager.updateTransactionStatus(tx.id, 'failed', error.message);
|
|
287
|
+
throw error;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
private async executeStep(
|
|
292
|
+
step: TemplateStep,
|
|
293
|
+
context: Record<string, any>,
|
|
294
|
+
tx: Transaction
|
|
295
|
+
): Promise<void> {
|
|
296
|
+
// Replace template variables with actual values
|
|
297
|
+
const resolvedParams = this.resolveParams(step.params, context);
|
|
298
|
+
|
|
299
|
+
// Execute the command
|
|
300
|
+
const result = await this.executeCommand(step.command, resolvedParams);
|
|
301
|
+
|
|
302
|
+
// Store outputs in context for subsequent steps
|
|
303
|
+
if (step.output && result) {
|
|
304
|
+
for (const [key, path] of Object.entries(step.output)) {
|
|
305
|
+
context[key] = this.getNestedValue(result, path);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Handle async steps
|
|
310
|
+
if (step.async && step.confirmation) {
|
|
311
|
+
await this.waitForConfirmation(step.confirmation, context, tx);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
private resolveParams(params: any, context: Record<string, any>): any {
|
|
316
|
+
if (typeof params === 'string') {
|
|
317
|
+
// Replace {{variable}} with context value
|
|
318
|
+
return params.replace(/\{\{(\w+)\}\}/g, (match, key) => {
|
|
319
|
+
return context[key] !== undefined ? context[key] : match;
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (Array.isArray(params)) {
|
|
324
|
+
return params.map(p => this.resolveParams(p, context));
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (typeof params === 'object' && params !== null) {
|
|
328
|
+
const resolved: any = {};
|
|
329
|
+
for (const [key, value] of Object.entries(params)) {
|
|
330
|
+
resolved[key] = this.resolveParams(value, context);
|
|
331
|
+
}
|
|
332
|
+
return resolved;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return params;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
private getNestedValue(obj: any, path: string): any {
|
|
339
|
+
return path.split('.').reduce((o, p) => o?.[p], obj);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
private async executeCommand(command: string, params: any): Promise<any> {
|
|
343
|
+
const [provider, method] = command.split('.');
|
|
344
|
+
|
|
345
|
+
switch (provider) {
|
|
346
|
+
case 'wise':
|
|
347
|
+
return this.executeWiseCommand(method, params);
|
|
348
|
+
case 'bunq':
|
|
349
|
+
return this.executeBunqCommand(method, params);
|
|
350
|
+
case 'stripe':
|
|
351
|
+
// Stripe implementation would go here
|
|
352
|
+
throw new Error('Stripe commands not yet implemented');
|
|
353
|
+
default:
|
|
354
|
+
throw new Error(`Unknown provider: ${provider}`);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
private async executeWiseCommand(method: string, params: any): Promise<any> {
|
|
359
|
+
const config = configManager.getProvider('wise');
|
|
360
|
+
if (!config) {
|
|
361
|
+
throw new Error('Wise not configured');
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const client = new WiseClient(config as any);
|
|
365
|
+
|
|
366
|
+
switch (method) {
|
|
367
|
+
case 'createQuote':
|
|
368
|
+
return client.createQuote(
|
|
369
|
+
params.profileId,
|
|
370
|
+
params.sourceCurrency,
|
|
371
|
+
params.targetCurrency,
|
|
372
|
+
parseFloat(params.sourceAmount)
|
|
373
|
+
);
|
|
374
|
+
case 'createTransfer':
|
|
375
|
+
return client.createTransfer(
|
|
376
|
+
params.profileId,
|
|
377
|
+
params.quoteId,
|
|
378
|
+
params.targetAccountId,
|
|
379
|
+
params.reference
|
|
380
|
+
);
|
|
381
|
+
case 'fundTransfer':
|
|
382
|
+
return client.fundTransfer(params.profileId, params.transferId);
|
|
383
|
+
default:
|
|
384
|
+
throw new Error(`Unknown Wise command: ${method}`);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
private async executeBunqCommand(method: string, params: any): Promise<any> {
|
|
389
|
+
const config = configManager.getProvider('bunq');
|
|
390
|
+
if (!config) {
|
|
391
|
+
throw new Error('Bunq not configured');
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const client = new BunqClient(config as any);
|
|
395
|
+
|
|
396
|
+
switch (method) {
|
|
397
|
+
case 'createPayment':
|
|
398
|
+
return client.createPayment(
|
|
399
|
+
params.userId,
|
|
400
|
+
params.accountId,
|
|
401
|
+
params.amount,
|
|
402
|
+
params.currency,
|
|
403
|
+
params.counterpartyIban,
|
|
404
|
+
params.counterpartyName,
|
|
405
|
+
params.description
|
|
406
|
+
);
|
|
407
|
+
case 'createRequestInquiry':
|
|
408
|
+
return client.createRequestInquiry(
|
|
409
|
+
params.userId,
|
|
410
|
+
params.accountId,
|
|
411
|
+
params.amount,
|
|
412
|
+
params.currency,
|
|
413
|
+
params.counterpartyAlias,
|
|
414
|
+
params.description
|
|
415
|
+
);
|
|
416
|
+
default:
|
|
417
|
+
throw new Error(`Unknown Bunq command: ${method}`);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
private async waitForConfirmation(
|
|
422
|
+
confirmation: any,
|
|
423
|
+
context: Record<string, any>,
|
|
424
|
+
tx: Transaction
|
|
425
|
+
): Promise<void> {
|
|
426
|
+
const { type, timeout, events, pollInterval } = confirmation;
|
|
427
|
+
|
|
428
|
+
if (type === 'webhook') {
|
|
429
|
+
// Webhook confirmation - wait for webhook handler to update
|
|
430
|
+
console.log(`Waiting for webhook confirmation (${timeout}s)...`);
|
|
431
|
+
// In real implementation, this would set up a promise that resolves on webhook
|
|
432
|
+
await new Promise(resolve => setTimeout(resolve, 5000));
|
|
433
|
+
} else if (type === 'poll') {
|
|
434
|
+
// Polling confirmation
|
|
435
|
+
const startTime = Date.now();
|
|
436
|
+
const timeoutMs = (timeout || 300) * 1000;
|
|
437
|
+
|
|
438
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
439
|
+
// Poll for status
|
|
440
|
+
await new Promise(resolve => setTimeout(resolve, (pollInterval || 5) * 1000));
|
|
441
|
+
|
|
442
|
+
// Check if confirmed
|
|
443
|
+
const updatedTx = transactionManager.getTransaction(tx.id);
|
|
444
|
+
if (updatedTx?.status === 'completed') {
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
throw new Error('Confirmation timeout');
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
export const templateEngine = new TemplateEngine();
|