stable-ci 0.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/LICENSE +21 -0
- package/README.md +346 -0
- package/dist/adapters/configured-http.js +173 -0
- package/dist/adapters/demo.js +150 -0
- package/dist/adapters/http-demo.js +232 -0
- package/dist/config.js +67 -0
- package/dist/core/demo-payment-app.js +254 -0
- package/dist/core/runner.js +16 -0
- package/dist/index.js +183 -0
- package/dist/invariants/builtins.js +138 -0
- package/dist/providers/bvnk.js +60 -0
- package/dist/providers/generic.js +13 -0
- package/dist/providers/types.js +1 -0
- package/dist/reporters/junit.js +65 -0
- package/dist/scenarios/builtins.js +106 -0
- package/dist/types.js +1 -0
- package/package.json +57 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 saldfsdk
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
# stable-ci
|
|
2
|
+
|
|
3
|
+
**Break your stablecoin integration before production.**
|
|
4
|
+
|
|
5
|
+
`stable-ci` injects deterministic payment failures into stablecoin payment integrations and verifies that your application and ledger still converge to the expected financial state.
|
|
6
|
+
|
|
7
|
+
It is designed for CI: define your payment policy, run failure scenarios against your integration, and fail the build when money moves incorrectly.
|
|
8
|
+
|
|
9
|
+
## What it catches
|
|
10
|
+
|
|
11
|
+
`stable-ci` currently tests:
|
|
12
|
+
|
|
13
|
+
- Duplicate webhooks
|
|
14
|
+
- Out-of-order webhooks
|
|
15
|
+
- Missing webhooks
|
|
16
|
+
- Provider timeout after broadcast
|
|
17
|
+
- Late blockchain confirmation
|
|
18
|
+
- Retry while settlement is unknown
|
|
19
|
+
- Invalid webhook signatures
|
|
20
|
+
- Underpayments
|
|
21
|
+
- Overpayments
|
|
22
|
+
- Late payments
|
|
23
|
+
|
|
24
|
+
It verifies outcomes such as:
|
|
25
|
+
|
|
26
|
+
- exactly-once ledger posting
|
|
27
|
+
- credited amount
|
|
28
|
+
- final application state
|
|
29
|
+
- retry behavior
|
|
30
|
+
- webhook acceptance
|
|
31
|
+
- recovery after missing events
|
|
32
|
+
|
|
33
|
+
## Example
|
|
34
|
+
|
|
35
|
+
A broken overpayment handler:
|
|
36
|
+
|
|
37
|
+
```text
|
|
38
|
+
FAIL overpayment
|
|
39
|
+
ledger_delta_equals_settlement_amount
|
|
40
|
+
Expected USD 100.00, ledger credited USD 140.00.
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Instead of only asking whether an API request succeeded, `stable-ci` checks whether the resulting financial state is correct.
|
|
44
|
+
|
|
45
|
+
## Install
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npm install --save-dev stable-ci
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Or run it directly:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
npx stable-ci --help
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Quick start
|
|
58
|
+
|
|
59
|
+
Create `stable-ci.yml`:
|
|
60
|
+
|
|
61
|
+
```yaml
|
|
62
|
+
provider: bvnk
|
|
63
|
+
|
|
64
|
+
webhookSecret: your-test-webhook-secret
|
|
65
|
+
|
|
66
|
+
target:
|
|
67
|
+
name: payment-app
|
|
68
|
+
baseUrl: http://127.0.0.1:4310
|
|
69
|
+
endpoints:
|
|
70
|
+
reset: /reset
|
|
71
|
+
webhook: /webhook
|
|
72
|
+
state: /state
|
|
73
|
+
reconcile: /reconcile
|
|
74
|
+
|
|
75
|
+
payment:
|
|
76
|
+
id: pay_test_001
|
|
77
|
+
amount: 100
|
|
78
|
+
asset: USDC
|
|
79
|
+
|
|
80
|
+
scenarios:
|
|
81
|
+
- duplicate_webhook
|
|
82
|
+
- out_of_order_webhook
|
|
83
|
+
- missing_webhook
|
|
84
|
+
- invalid_signature
|
|
85
|
+
- underpayment
|
|
86
|
+
- overpayment
|
|
87
|
+
- late_payment
|
|
88
|
+
|
|
89
|
+
expectations:
|
|
90
|
+
underpayment:
|
|
91
|
+
applicationStatus: manual_review
|
|
92
|
+
ledgerEntries: 0
|
|
93
|
+
credit: none
|
|
94
|
+
|
|
95
|
+
overpayment:
|
|
96
|
+
applicationStatus: completed
|
|
97
|
+
ledgerEntries: 1
|
|
98
|
+
credit: exact_expected
|
|
99
|
+
|
|
100
|
+
late_payment:
|
|
101
|
+
applicationStatus: manual_review
|
|
102
|
+
ledgerEntries: 0
|
|
103
|
+
credit: none
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Run:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
npx stable-ci run --config stable-ci.yml
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Example output:
|
|
113
|
+
|
|
114
|
+
```text
|
|
115
|
+
Stablecoin Reliability CI
|
|
116
|
+
=========================
|
|
117
|
+
Adapter: payment-app [bvnk]
|
|
118
|
+
|
|
119
|
+
PASS duplicate_webhook
|
|
120
|
+
PASS out_of_order_webhook
|
|
121
|
+
PASS missing_webhook
|
|
122
|
+
PASS invalid_signature
|
|
123
|
+
PASS underpayment
|
|
124
|
+
FAIL overpayment
|
|
125
|
+
PASS late_payment
|
|
126
|
+
|
|
127
|
+
6 passed, 1 failed
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
A failed scenario causes a non-zero exit code, so it can fail CI.
|
|
131
|
+
|
|
132
|
+
## Expected outcomes
|
|
133
|
+
|
|
134
|
+
Different applications may intentionally handle payment exceptions differently.
|
|
135
|
+
|
|
136
|
+
For example, an overpayment may credit only the requested amount:
|
|
137
|
+
|
|
138
|
+
```yaml
|
|
139
|
+
expectations:
|
|
140
|
+
overpayment:
|
|
141
|
+
applicationStatus: completed
|
|
142
|
+
ledgerEntries: 1
|
|
143
|
+
credit: exact_expected
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
While another application may intentionally credit the entire received amount:
|
|
147
|
+
|
|
148
|
+
```yaml
|
|
149
|
+
expectations:
|
|
150
|
+
overpayment:
|
|
151
|
+
applicationStatus: completed
|
|
152
|
+
ledgerEntries: 1
|
|
153
|
+
credit: received_amount
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
`stable-ci` tests against your declared financial policy instead of assuming that every integration has the same correct outcome.
|
|
157
|
+
|
|
158
|
+
## GitHub Actions
|
|
159
|
+
|
|
160
|
+
This repository includes a composite GitHub Action.
|
|
161
|
+
|
|
162
|
+
```yaml
|
|
163
|
+
steps:
|
|
164
|
+
- uses: actions/checkout@v4
|
|
165
|
+
|
|
166
|
+
- uses: saldfsdk/stable-ci@v0.1.0
|
|
167
|
+
with:
|
|
168
|
+
config: stable-ci.yml
|
|
169
|
+
junit: reports/stable-ci.xml
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
The action runs the configured scenarios and writes a JUnit report.
|
|
173
|
+
|
|
174
|
+
## JUnit
|
|
175
|
+
|
|
176
|
+
You can generate a JUnit XML report directly:
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
npx stable-ci run \
|
|
180
|
+
--config stable-ci.yml \
|
|
181
|
+
--junit reports/stable-ci.xml
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
JUnit reports are written even when reliability scenarios fail.
|
|
185
|
+
|
|
186
|
+
## JSON output
|
|
187
|
+
|
|
188
|
+
For machine-readable output:
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
npx stable-ci test --profile safe --json
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## Built-in scenarios
|
|
195
|
+
|
|
196
|
+
List all scenarios:
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
npx stable-ci scenarios
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Current scenarios include:
|
|
203
|
+
|
|
204
|
+
- `duplicate_webhook`
|
|
205
|
+
- `out_of_order_webhook`
|
|
206
|
+
- `missing_webhook`
|
|
207
|
+
- `provider_timeout_after_broadcast`
|
|
208
|
+
- `late_chain_confirmation`
|
|
209
|
+
- `retry_after_unknown_settlement`
|
|
210
|
+
- `invalid_signature`
|
|
211
|
+
- `underpayment`
|
|
212
|
+
- `overpayment`
|
|
213
|
+
- `late_payment`
|
|
214
|
+
|
|
215
|
+
## Provider support
|
|
216
|
+
|
|
217
|
+
### BVNK
|
|
218
|
+
|
|
219
|
+
The current BVNK adapter models payment webhook behavior including:
|
|
220
|
+
|
|
221
|
+
- signed webhook delivery
|
|
222
|
+
- `statusChanged`
|
|
223
|
+
- `transactionConfirmed`
|
|
224
|
+
- `transactionLate`
|
|
225
|
+
- underpayments
|
|
226
|
+
- overpayments
|
|
227
|
+
- late payments
|
|
228
|
+
|
|
229
|
+
Provider payload fixtures are based on publicly documented behavior and are intended for reliability testing.
|
|
230
|
+
|
|
231
|
+
`stable-ci` is not affiliated with or endorsed by BVNK.
|
|
232
|
+
|
|
233
|
+
### Generic
|
|
234
|
+
|
|
235
|
+
A generic provider adapter is also included for provider-independent testing.
|
|
236
|
+
|
|
237
|
+
## Architecture
|
|
238
|
+
|
|
239
|
+
```text
|
|
240
|
+
stable-ci.yml
|
|
241
|
+
|
|
|
242
|
+
v
|
|
243
|
+
Scenario Runner
|
|
244
|
+
|
|
|
245
|
+
v
|
|
246
|
+
Fault Injection
|
|
247
|
+
|
|
|
248
|
+
v
|
|
249
|
+
Provider / Webhook / Application
|
|
250
|
+
|
|
|
251
|
+
v
|
|
252
|
+
Observed State
|
|
253
|
+
|
|
|
254
|
+
v
|
|
255
|
+
Expected Outcome + Invariant Engine
|
|
256
|
+
|
|
|
257
|
+
+---- PASS
|
|
258
|
+
|
|
|
259
|
+
+---- FAIL
|
|
260
|
+
|
|
|
261
|
+
+-- CLI
|
|
262
|
+
+-- JSON
|
|
263
|
+
+-- JUnit
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
## Why stable-ci?
|
|
267
|
+
|
|
268
|
+
Payment integrations can fail even when individual API calls appear successful.
|
|
269
|
+
|
|
270
|
+
Examples include:
|
|
271
|
+
|
|
272
|
+
- the same webhook being delivered twice
|
|
273
|
+
- lifecycle events arriving out of order
|
|
274
|
+
- a payment being confirmed after an application timeout
|
|
275
|
+
- retrying while settlement is still unknown
|
|
276
|
+
- an invalid webhook being accepted
|
|
277
|
+
- an underpayment being treated as fully paid
|
|
278
|
+
- an overpayment being credited incorrectly
|
|
279
|
+
- an expired payment being revived after late funds arrive
|
|
280
|
+
|
|
281
|
+
These are not only API correctness problems.
|
|
282
|
+
|
|
283
|
+
They are **money movement correctness** problems.
|
|
284
|
+
|
|
285
|
+
`stable-ci` is designed to test the final financial outcome, not only whether a request returned HTTP 200.
|
|
286
|
+
|
|
287
|
+
## Current status
|
|
288
|
+
|
|
289
|
+
`stable-ci` is an early-stage developer tool.
|
|
290
|
+
|
|
291
|
+
The current version focuses on deterministic local and CI testing.
|
|
292
|
+
|
|
293
|
+
It does not yet provide full production blockchain observation or hosted monitoring.
|
|
294
|
+
|
|
295
|
+
Planned areas include:
|
|
296
|
+
|
|
297
|
+
- additional provider adapters
|
|
298
|
+
- richer CI reporting
|
|
299
|
+
- blockchain/RPC observers
|
|
300
|
+
- provider sandbox drift detection
|
|
301
|
+
- hosted reliability testing
|
|
302
|
+
|
|
303
|
+
## Development
|
|
304
|
+
|
|
305
|
+
Install dependencies:
|
|
306
|
+
|
|
307
|
+
```bash
|
|
308
|
+
npm install
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
Run all tests and build:
|
|
312
|
+
|
|
313
|
+
```bash
|
|
314
|
+
npm run check
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
Run the local safe demo:
|
|
318
|
+
|
|
319
|
+
```bash
|
|
320
|
+
npm run build
|
|
321
|
+
node dist/index.js test-http --profile safe
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
Expected result:
|
|
325
|
+
|
|
326
|
+
```text
|
|
327
|
+
10 passed, 0 failed
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
Run the intentionally unsafe demo:
|
|
331
|
+
|
|
332
|
+
```bash
|
|
333
|
+
node dist/index.js test-http --profile unsafe
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
Expected result:
|
|
337
|
+
|
|
338
|
+
```text
|
|
339
|
+
0 passed, 10 failed
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
The unsafe profile intentionally contains broken payment-handling behavior so that the reliability checks can demonstrate what they detect.
|
|
343
|
+
|
|
344
|
+
## License
|
|
345
|
+
|
|
346
|
+
MIT
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { createBvnkProvider } from '../providers/bvnk.js';
|
|
3
|
+
import { createGenericProvider } from '../providers/generic.js';
|
|
4
|
+
const stateSchema = z.object({
|
|
5
|
+
applicationStatus: z.enum([
|
|
6
|
+
'none',
|
|
7
|
+
'pending',
|
|
8
|
+
'completed',
|
|
9
|
+
'failed',
|
|
10
|
+
'manual_review',
|
|
11
|
+
]),
|
|
12
|
+
webhookDeliveries: z.number(),
|
|
13
|
+
ledgerEntries: z.number(),
|
|
14
|
+
creditedAmount: z.number(),
|
|
15
|
+
retryAttempts: z.number().default(0),
|
|
16
|
+
settlementWasUnknown: z.boolean().default(false),
|
|
17
|
+
recovered: z.boolean().default(false),
|
|
18
|
+
});
|
|
19
|
+
function getProvider(config) {
|
|
20
|
+
if (config.provider === 'bvnk') {
|
|
21
|
+
return createBvnkProvider(config.webhookSecret ?? 'stable-ci-local-secret');
|
|
22
|
+
}
|
|
23
|
+
return createGenericProvider();
|
|
24
|
+
}
|
|
25
|
+
async function postJson(baseUrl, path, body = {}) {
|
|
26
|
+
const response = await fetch(baseUrl + path, {
|
|
27
|
+
method: 'POST',
|
|
28
|
+
headers: {
|
|
29
|
+
'content-type': 'application/json',
|
|
30
|
+
},
|
|
31
|
+
body: JSON.stringify(body),
|
|
32
|
+
});
|
|
33
|
+
if (!response.ok) {
|
|
34
|
+
throw new Error('HTTP ' + response.status + ' from ' + path);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
async function sendWebhook(config, provider, eventId, status, actualAmount, eventType) {
|
|
38
|
+
const rendered = provider.render({
|
|
39
|
+
eventId,
|
|
40
|
+
paymentId: config.payment.id,
|
|
41
|
+
status,
|
|
42
|
+
amount: config.payment.amount,
|
|
43
|
+
asset: config.payment.asset,
|
|
44
|
+
actualAmount,
|
|
45
|
+
eventType,
|
|
46
|
+
});
|
|
47
|
+
const response = await fetch(config.target.baseUrl +
|
|
48
|
+
config.target.endpoints.webhook, {
|
|
49
|
+
method: 'POST',
|
|
50
|
+
headers: rendered.headers,
|
|
51
|
+
body: rendered.body,
|
|
52
|
+
});
|
|
53
|
+
if (!response.ok) {
|
|
54
|
+
throw new Error('Webhook returned HTTP ' + response.status);
|
|
55
|
+
}
|
|
56
|
+
return response.status;
|
|
57
|
+
}
|
|
58
|
+
async function sendInvalidSignatureWebhook(config, provider) {
|
|
59
|
+
const rendered = provider.render({
|
|
60
|
+
eventId: 'evt_invalid_signature',
|
|
61
|
+
paymentId: config.payment.id,
|
|
62
|
+
status: 'completed',
|
|
63
|
+
amount: config.payment.amount,
|
|
64
|
+
asset: config.payment.asset,
|
|
65
|
+
});
|
|
66
|
+
const response = await fetch(config.target.baseUrl +
|
|
67
|
+
config.target.endpoints.webhook, {
|
|
68
|
+
method: 'POST',
|
|
69
|
+
headers: {
|
|
70
|
+
...rendered.headers,
|
|
71
|
+
'x-signature': 'invalid-signature',
|
|
72
|
+
},
|
|
73
|
+
body: rendered.body,
|
|
74
|
+
});
|
|
75
|
+
return response.ok;
|
|
76
|
+
}
|
|
77
|
+
async function getState(config) {
|
|
78
|
+
const response = await fetch(config.target.baseUrl +
|
|
79
|
+
config.target.endpoints.state);
|
|
80
|
+
if (!response.ok) {
|
|
81
|
+
throw new Error('Failed to read target application state.');
|
|
82
|
+
}
|
|
83
|
+
return stateSchema.parse(await response.json());
|
|
84
|
+
}
|
|
85
|
+
export function createConfiguredHttpAdapter(config) {
|
|
86
|
+
const provider = getProvider(config);
|
|
87
|
+
return {
|
|
88
|
+
name: config.target.name + ' [' + provider.name + ']',
|
|
89
|
+
async runScenario(scenario) {
|
|
90
|
+
const baseUrl = config.target.baseUrl;
|
|
91
|
+
const endpoints = config.target.endpoints;
|
|
92
|
+
const payment = config.payment;
|
|
93
|
+
await postJson(baseUrl, endpoints.reset);
|
|
94
|
+
let providerStatus = 'completed';
|
|
95
|
+
let chainStatus = 'confirmed';
|
|
96
|
+
let webhookAccepted;
|
|
97
|
+
let receivedAmount;
|
|
98
|
+
switch (scenario) {
|
|
99
|
+
case 'duplicate_webhook':
|
|
100
|
+
await sendWebhook(config, provider, 'evt_complete_1', 'completed');
|
|
101
|
+
await sendWebhook(config, provider, 'evt_complete_1', 'completed');
|
|
102
|
+
break;
|
|
103
|
+
case 'out_of_order_webhook':
|
|
104
|
+
await sendWebhook(config, provider, 'evt_complete_1', 'completed');
|
|
105
|
+
await sendWebhook(config, provider, 'evt_pending_2', 'pending');
|
|
106
|
+
break;
|
|
107
|
+
case 'missing_webhook':
|
|
108
|
+
if (endpoints.reconcile) {
|
|
109
|
+
await postJson(baseUrl, endpoints.reconcile, {
|
|
110
|
+
paymentId: payment.id,
|
|
111
|
+
chainStatus: 'confirmed',
|
|
112
|
+
amount: payment.amount,
|
|
113
|
+
asset: payment.asset,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
break;
|
|
117
|
+
case 'invalid_signature':
|
|
118
|
+
if (provider.name !== 'bvnk') {
|
|
119
|
+
throw new Error('invalid_signature currently requires the BVNK provider.');
|
|
120
|
+
}
|
|
121
|
+
webhookAccepted =
|
|
122
|
+
await sendInvalidSignatureWebhook(config, provider);
|
|
123
|
+
providerStatus = 'failed';
|
|
124
|
+
chainStatus = 'not_broadcast';
|
|
125
|
+
break;
|
|
126
|
+
case 'underpayment':
|
|
127
|
+
if (provider.name !== 'bvnk') {
|
|
128
|
+
throw new Error('underpayment currently requires the BVNK provider.');
|
|
129
|
+
}
|
|
130
|
+
receivedAmount = payment.amount * 0.6;
|
|
131
|
+
providerStatus = 'underpaid';
|
|
132
|
+
await sendWebhook(config, provider, 'evt_underpayment_1', 'underpaid', receivedAmount, 'transactionConfirmed');
|
|
133
|
+
break;
|
|
134
|
+
case 'overpayment':
|
|
135
|
+
if (provider.name !== 'bvnk') {
|
|
136
|
+
throw new Error('overpayment currently requires the BVNK provider.');
|
|
137
|
+
}
|
|
138
|
+
receivedAmount = payment.amount * 1.4;
|
|
139
|
+
providerStatus = 'completed';
|
|
140
|
+
await sendWebhook(config, provider, 'evt_overpayment_1', 'completed', receivedAmount, 'transactionConfirmed');
|
|
141
|
+
break;
|
|
142
|
+
case 'late_payment':
|
|
143
|
+
if (provider.name !== 'bvnk') {
|
|
144
|
+
throw new Error('late_payment currently requires the BVNK provider.');
|
|
145
|
+
}
|
|
146
|
+
receivedAmount = payment.amount;
|
|
147
|
+
providerStatus = 'expired';
|
|
148
|
+
await sendWebhook(config, provider, 'evt_late_payment_1', 'expired', receivedAmount, 'transactionLate');
|
|
149
|
+
break;
|
|
150
|
+
default:
|
|
151
|
+
throw new Error('Scenario is not supported by the configured HTTP adapter yet: ' +
|
|
152
|
+
scenario);
|
|
153
|
+
}
|
|
154
|
+
const state = await getState(config);
|
|
155
|
+
return {
|
|
156
|
+
scenario,
|
|
157
|
+
paymentId: payment.id,
|
|
158
|
+
expectedAmount: payment.amount,
|
|
159
|
+
receivedAmount,
|
|
160
|
+
providerStatus,
|
|
161
|
+
chainStatus,
|
|
162
|
+
webhookDeliveries: state.webhookDeliveries,
|
|
163
|
+
ledgerEntries: state.ledgerEntries,
|
|
164
|
+
creditedAmount: state.creditedAmount,
|
|
165
|
+
applicationStatus: state.applicationStatus,
|
|
166
|
+
retryAttempts: state.retryAttempts,
|
|
167
|
+
settlementWasUnknown: state.settlementWasUnknown,
|
|
168
|
+
recovered: state.recovered,
|
|
169
|
+
webhookAccepted,
|
|
170
|
+
};
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
const baseObservation = (scenario) => ({
|
|
2
|
+
scenario,
|
|
3
|
+
paymentId: 'pay_demo_001',
|
|
4
|
+
expectedAmount: 100,
|
|
5
|
+
providerStatus: 'completed',
|
|
6
|
+
chainStatus: 'confirmed',
|
|
7
|
+
webhookDeliveries: 1,
|
|
8
|
+
ledgerEntries: 1,
|
|
9
|
+
creditedAmount: 100,
|
|
10
|
+
applicationStatus: 'completed',
|
|
11
|
+
retryAttempts: 0,
|
|
12
|
+
settlementWasUnknown: false,
|
|
13
|
+
recovered: false,
|
|
14
|
+
});
|
|
15
|
+
function safeObservation(scenario) {
|
|
16
|
+
const o = baseObservation(scenario);
|
|
17
|
+
switch (scenario) {
|
|
18
|
+
case 'duplicate_webhook':
|
|
19
|
+
return {
|
|
20
|
+
...o,
|
|
21
|
+
webhookDeliveries: 2,
|
|
22
|
+
};
|
|
23
|
+
case 'out_of_order_webhook':
|
|
24
|
+
return {
|
|
25
|
+
...o,
|
|
26
|
+
webhookDeliveries: 3,
|
|
27
|
+
};
|
|
28
|
+
case 'missing_webhook':
|
|
29
|
+
return {
|
|
30
|
+
...o,
|
|
31
|
+
webhookDeliveries: 0,
|
|
32
|
+
recovered: true,
|
|
33
|
+
};
|
|
34
|
+
case 'provider_timeout_after_broadcast':
|
|
35
|
+
case 'late_chain_confirmation':
|
|
36
|
+
case 'retry_after_unknown_settlement':
|
|
37
|
+
return {
|
|
38
|
+
...o,
|
|
39
|
+
providerStatus: 'timeout',
|
|
40
|
+
webhookDeliveries: 0,
|
|
41
|
+
settlementWasUnknown: true,
|
|
42
|
+
recovered: true,
|
|
43
|
+
};
|
|
44
|
+
case 'invalid_signature':
|
|
45
|
+
return {
|
|
46
|
+
...o,
|
|
47
|
+
providerStatus: 'failed',
|
|
48
|
+
chainStatus: 'not_broadcast',
|
|
49
|
+
applicationStatus: 'none',
|
|
50
|
+
webhookDeliveries: 0,
|
|
51
|
+
ledgerEntries: 0,
|
|
52
|
+
creditedAmount: 0,
|
|
53
|
+
webhookAccepted: false,
|
|
54
|
+
};
|
|
55
|
+
case 'late_payment':
|
|
56
|
+
return {
|
|
57
|
+
...o,
|
|
58
|
+
providerStatus: 'expired',
|
|
59
|
+
receivedAmount: 100,
|
|
60
|
+
applicationStatus: 'manual_review',
|
|
61
|
+
ledgerEntries: 0,
|
|
62
|
+
creditedAmount: 0,
|
|
63
|
+
};
|
|
64
|
+
case 'overpayment':
|
|
65
|
+
return {
|
|
66
|
+
...o,
|
|
67
|
+
receivedAmount: 140,
|
|
68
|
+
};
|
|
69
|
+
case 'underpayment':
|
|
70
|
+
return {
|
|
71
|
+
...o,
|
|
72
|
+
providerStatus: 'underpaid',
|
|
73
|
+
receivedAmount: 60,
|
|
74
|
+
applicationStatus: 'manual_review',
|
|
75
|
+
ledgerEntries: 0,
|
|
76
|
+
creditedAmount: 0,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function unsafeObservation(scenario) {
|
|
81
|
+
const safe = safeObservation(scenario);
|
|
82
|
+
switch (scenario) {
|
|
83
|
+
case 'duplicate_webhook':
|
|
84
|
+
return {
|
|
85
|
+
...safe,
|
|
86
|
+
ledgerEntries: 2,
|
|
87
|
+
creditedAmount: 200,
|
|
88
|
+
};
|
|
89
|
+
case 'out_of_order_webhook':
|
|
90
|
+
return {
|
|
91
|
+
...safe,
|
|
92
|
+
applicationStatus: 'pending',
|
|
93
|
+
};
|
|
94
|
+
case 'missing_webhook':
|
|
95
|
+
return {
|
|
96
|
+
...safe,
|
|
97
|
+
applicationStatus: 'none',
|
|
98
|
+
ledgerEntries: 0,
|
|
99
|
+
creditedAmount: 0,
|
|
100
|
+
};
|
|
101
|
+
case 'provider_timeout_after_broadcast':
|
|
102
|
+
case 'late_chain_confirmation':
|
|
103
|
+
case 'retry_after_unknown_settlement':
|
|
104
|
+
return {
|
|
105
|
+
...safe,
|
|
106
|
+
retryAttempts: 1,
|
|
107
|
+
ledgerEntries: 2,
|
|
108
|
+
creditedAmount: 200,
|
|
109
|
+
};
|
|
110
|
+
case 'invalid_signature':
|
|
111
|
+
return {
|
|
112
|
+
...safe,
|
|
113
|
+
applicationStatus: 'completed',
|
|
114
|
+
webhookDeliveries: 1,
|
|
115
|
+
ledgerEntries: 1,
|
|
116
|
+
creditedAmount: 100,
|
|
117
|
+
webhookAccepted: true,
|
|
118
|
+
};
|
|
119
|
+
case 'late_payment':
|
|
120
|
+
return {
|
|
121
|
+
...safe,
|
|
122
|
+
applicationStatus: 'completed',
|
|
123
|
+
ledgerEntries: 1,
|
|
124
|
+
creditedAmount: 100,
|
|
125
|
+
};
|
|
126
|
+
case 'overpayment':
|
|
127
|
+
return {
|
|
128
|
+
...safe,
|
|
129
|
+
receivedAmount: 140,
|
|
130
|
+
creditedAmount: 140,
|
|
131
|
+
};
|
|
132
|
+
case 'underpayment':
|
|
133
|
+
return {
|
|
134
|
+
...safe,
|
|
135
|
+
applicationStatus: 'completed',
|
|
136
|
+
ledgerEntries: 1,
|
|
137
|
+
creditedAmount: 100,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
export function createDemoAdapter(profile) {
|
|
142
|
+
return {
|
|
143
|
+
name: 'demo:' + profile,
|
|
144
|
+
async runScenario(scenario) {
|
|
145
|
+
return profile === 'safe'
|
|
146
|
+
? safeObservation(scenario)
|
|
147
|
+
: unsafeObservation(scenario);
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
}
|