openvibers/openvibe.tips Commits
openvibe-contracts v0.30.2: the tips.* event payload schemas; test/contracts.test.js validates every envelope and payload Tips produces against them
Changes
@@@ -41,7 +41,7 @@ from *interaction delivered*.(openvibe-sdk transactional outbox)- **OpenVibe.Network** — SSO for people, service tokens, JWKS, identity resolve (importer)- **OpenVibe.Live** — chat delivery through `/internal/tips/deliveries` (deployed in Live since `f11f809`; [docs/live-patch.diff](docs/live-patch.diff) is the original patch) until OpenVibe.Chat exposes paid messages; overlay consumer--- **OpenVibe.Shared** v1.2.1 (app icon, SSR footer, noscript nav, release manifest, legal pages) and++- **OpenVibe.Shared** v1.3.0 (app icon, SSR footer, noscript nav, release manifest, legal pages) andthe Network's `navbar.js`## Run it@@@ -50,7 +50,7 @@ from *interaction delivered*.fnm exec --using=22.22.1 npm installcp .env.example .env # OV_OAUTH_CLIENT_SECRET, TIPS_FORM_SECRET, TIPS_EVENTS_SECRET, …npm run dev # http://localhost:4610--npm test # 7 files: stub Network/Billing/Events/Live, temp DBs, random ports++npm test # every test/*.test.js: stub Network/Billing/Events/Live, temp DBs, random portsnpm run subscribe # create the billing.transaction.* and billing.receipt.* subscriptions in OpenVibe.Eventsnode scripts/import-live.js --live-db <live snapshot> --billing-db <billing snapshot> [--dry-run] [--json]```@@@ -125,12 +125,13 @@ token as a Bearer and act on their own things only. The API never reads cookies.| `POST /simulate` | `tips.simulation.run` | owner || `POST /internal/events` | Events webhook signature (`TIPS_EVENTS_SECRET`), loopback only | — |--The capabilities and the service manifest are released in openvibe-contracts v0.15.0 (3-segment ids:++The capabilities and the service manifest were released in openvibe-contracts v0.15.0 (3-segment ids:the charter's `tips.simulate` is `tips.simulation.run`; `tips.interaction.record` is new, for EXTERNALtips); the drafts stay in [docs/capabilities-proposal/](docs/capabilities-proposal/) and[docs/service-manifest-proposal.json](docs/service-manifest-proposal.json). Grants are matched with--contracts' `capabilities.grants()` (exact id or a `.*` family). Contracts has no `tips.*` event payload--schemas yet.++contracts' `capabilities.grants()` (exact id or a `.*` family). Tips pins openvibe-contracts v0.30.2,++which also carries the payload schemas of the six `tips.*` events below; `test/contracts.test.js`++validates every envelope and payload Tips produces against them.## Events@@@ -241,7 +242,7 @@ exist here (plan §12.12):4. real persistence and end-to-end workflows — ✔ against stubs; not yet against the real Billing andEvents (Billing is in shadow and has sent no events; the production database is empty; the Liveimport has not been run);--5. capability and event registration against OpenVibe.Contracts — ✔ v0.15.0 (no event payload schemas yet);++5. capability and event registration against OpenVibe.Contracts — ✔ v0.15.0, payload schemas in v0.30.2 (pinned);6. a migration/seed strategy ✔, a security/threat review (not done beyond the tests' refusals), andsitemap/robots ✔ (no feed);7. acceptance tests proving the advertised functionality — ✔ (table above).@@@ -7,6 +7,6 @@"adr": "ADR-012","authoritative": false,"deployment": "Deployed internally, not launched: openvibe-ovh /opt/openvibe.tips (release 435f32b), unit openvibe-tips, 127.0.0.1:4610 loopback only; https://openvibe.tips still serves the OpenVibe.Sites placeholder. Principal tips; Events subscription billing.transaction.* -> /internal/events; TIPS_CHAT_ADAPTER=none. Pins openvibe-contracts v0.15.0, openvibe-sdk v0.4.0, openvibe-shared v1.2.1.",-- "notes": "Runtime, API, SSR pages, overlays, Billing settlement consumer, events outbox, Live import and reconciliation are built and tested against stubs. Capabilities (17 tips.*) and the service manifest are registered in OpenVibe.Contracts v0.15.0; no tips.* event payload schemas yet. Production database is empty (0 profiles, interactions, goals, overlay tokens); the Live import (scripts/import-live.js) has never been run; 0 billing events have ever arrived because Billing runs in shadow. Live's /internal/tips/deliveries route is deployed (Live f11f809) but never called. The restore drill passed on the host 2026-09-23 05:23 UTC. Built since (not deployed yet): /metrics (loopback only) and the Billing/Events checks in /api/ready. Not done: privacy/moderation controls on the pages; an independent security review; the openvibe.tips launch. Live stays the place tips happen until the Billing cutover (blocked on the owner re-pointing the PowerChat webhook) and the launch rule in README.md holds.",++ "notes": "Runtime, API, SSR pages, overlays, Billing settlement consumer, events outbox, Live import and reconciliation are built and tested against stubs. Capabilities (17 tips.*) and the service manifest are registered in OpenVibe.Contracts v0.15.0; the six tips.* event payload schemas are in v0.30.2, which the repository pins since 2026-09-23 (production still runs v0.15.0). Production database is empty (0 profiles, interactions, goals, overlay tokens); the Live import (scripts/import-live.js) has never been run; 0 billing events have ever arrived because Billing runs in shadow. Live's /internal/tips/deliveries route is deployed (Live f11f809) but never called. The restore drill passed on the host 2026-09-23 05:23 UTC. Built since (not deployed yet): /metrics (loopback only) and the Billing/Events checks in /api/ready. Not done: privacy/moderation controls on the pages; an independent security review; the openvibe.tips launch. Live stays the place tips happen until the Billing cutover (blocked on the owner re-pointing the PowerChat webhook) and the launch rule in README.md holds.","updated": "2026-09-23"}@@@ -14,7 +14,7 @@"dotenv": "^16.4.7","express": "^4.21.2","jsonwebtoken": "^9.0.2",-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.15.0",++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.30.2","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.4.0","openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.3.0"},@@@ -983,9 +983,9 @@}},"node_modules/openvibe-contracts": {-- "version": "0.15.0",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.15.0",-- "integrity": "sha512-1+Y7Qm+eplVEwxACsga3eASmBrTNF2hQ/fmw33CQtzUZuTkA12sv93LCZo3lbvFyKbbZFZDYIHSt7gfu3QISlQ==",++ "version": "0.30.2",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.30.2",++ "integrity": "sha512-R01NgRY5V/GEYWkWUyxfl1MifSQb4dfJy11VS9RP+wsWedh2Crr9/IL7yGSn1PytwTKKL+tQl/r4ppGfH3TwLg==","license": "AGPL-3.0-only","dependencies": {"ajv": "^8.17.1",@@@ -21,7 +21,7 @@"dotenv": "^16.4.7","express": "^4.21.2","jsonwebtoken": "^9.0.2",-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.15.0",++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.30.2","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.4.0","openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.3.0"},nnew file mode 100644@@@ -0,0 +1,55 @@++'use strict';++// Every event Tips puts in its outbox is a valid events.event-envelope@1 whose payload validates++// against the tips.* payload schema released in openvibe-contracts (v0.30.2 registers all six).++const assert = require('assert');++const contracts = require('openvibe-contracts');++const { boot, check, done } = require('./helpers/app');++++const TYPES = ['tips.interaction.ready', 'tips.interaction.failed', 'tips.interaction.cancelled', 'tips.goal.updated', 'tips.overlay.delivered', 'tips.overlay.failed'];++++(async () => {++ const t = await boot({ live: true });++ const { domain, billing, live } = t;++ const alex = await t.creator('alex', { settings: { tts_enabled: true } });++ const viewer = t.network.newUser('viewer');++ billing.fund(viewer.subject, 10_000);++++ await check('the six tips.* payload schemas are in the installed contracts', async () => {++ for (const type of TYPES) assert.strictEqual(contracts.resolve(type).owner, 'tips', type);++ });++++ await check('a flow that produces every tips.* event: each envelope and payload validates', async () => {++ await t.call('POST', '/api/v1/goals', { user: alex, body: { title: 'Desk', target_amount: 1000 } });++ const tok = await t.call('POST', '/api/v1/overlay-tokens', { user: alex, body: { scopes: ['alerts', 'goals'] } });++ const s = await t.sse(`/overlay/${tok.json.secret}/events`);++ const tip = await t.call('POST', '/api/v1/checkout', { user: viewer, body: { creator: 'alex', amount: 300, message: 'hi' } });++ await s.waitFor((e) => e.event === 'alert' && e.data.interaction_id === tip.json.interaction.id);++ s.close();++ await domain.effects.drain();++ // A delivery Live refuses (tips.interaction.failed), an overlay nobody showed (tips.overlay.failed).++ live.state.fail = 422;++ await t.call('POST', '/api/v1/checkout', { user: viewer, body: { creator: 'alex', amount: 5 } });++ await domain.effects.drain();++ live.state.fail = null;++ t.clock.offset += 11 * 60 * 1000;++ domain.overlays.sweepFailed();++ t.clock.offset = 0;++ // A reversal before delivery (tips.interaction.cancelled) and one after it.++ const pending = await t.call('POST', '/api/v1/tts-requests', { user: viewer, body: { creator: 'alex', amount: 150, tts: { text: 'read me' } } });++ const txn = domain.interactions.get(pending.json.interaction.id).billing_txn_id;++ await t.deliver(billing.refund(txn));++ await t.deliver(billing.refund(domain.interactions.get(tip.json.interaction.id).billing_txn_id, 100));++++ const rows = t.outboxRows();++ for (const type of TYPES) assert.ok(rows.some((e) => e.event_type === type), `no ${type} was produced`);++ for (const e of rows) {++ const env = contracts.validate('events.event-envelope@1', e);++ assert.ok(env.valid, `${e.event_type} envelope: ${JSON.stringify(env.errors)}`);++ const p = contracts.validate(e.event_type, e.payload);++ assert.ok(p.valid, `${e.event_type} payload: ${JSON.stringify(p.errors)} in ${JSON.stringify(e.payload)}`);++ }++ });++++ await t.close();++ done();++})().catch((e) => { console.error(e); process.exit(1); });
@@ -41,7 +41,7 @@ from *interaction delivered*.
@@ -41,7 +41,7 @@ from *interaction delivered*.
(openvibe-sdk transactional outbox)
(openvibe-sdk transactional outbox)
- **OpenVibe.Network** — SSO for people, service tokens, JWKS, identity resolve (importer)
- **OpenVibe.Network** — SSO for people, service tokens, JWKS, identity resolve (importer)
- **OpenVibe.Live** — chat delivery through `/internal/tips/deliveries` (deployed in Live since `f11f809`; [docs/live-patch.diff](docs/live-patch.diff) is the original patch) until OpenVibe.Chat exposes paid messages; overlay consumer
- **OpenVibe.Live** — chat delivery through `/internal/tips/deliveries` (deployed in Live since `f11f809`; [docs/live-patch.diff](docs/live-patch.diff) is the original patch) until OpenVibe.Chat exposes paid messages; overlay consumer
-- **OpenVibe.Shared** v1.2.1 (app icon, SSR footer, noscript nav, release manifest, legal pages) and
+- **OpenVibe.Shared** v1.3.0 (app icon, SSR footer, noscript nav, release manifest, legal pages) and
the Network's `navbar.js`
the Network's `navbar.js`
## Run it
## Run it
@@ -50,7 +50,7 @@ from *interaction delivered*.
@@ -50,7 +50,7 @@ from *interaction delivered*.
fnm exec --using=22.22.1 npm install
fnm exec --using=22.22.1 npm install
cp .env.example .env # OV_OAUTH_CLIENT_SECRET, TIPS_FORM_SECRET, TIPS_EVENTS_SECRET, …
cp .env.example .env # OV_OAUTH_CLIENT_SECRET, TIPS_FORM_SECRET, TIPS_EVENTS_SECRET, …
npm run dev # http://localhost:4610
npm run dev # http://localhost:4610
-npm test # 7 files: stub Network/Billing/Events/Live, temp DBs, random ports
+npm test # every test/*.test.js: stub Network/Billing/Events/Live, temp DBs, random ports
npm run subscribe # create the billing.transaction.* and billing.receipt.* subscriptions in OpenVibe.Events
npm run subscribe # create the billing.transaction.* and billing.receipt.* subscriptions in OpenVibe.Events
node scripts/import-live.js --live-db <live snapshot> --billing-db <billing snapshot> [--dry-run] [--json]
node scripts/import-live.js --live-db <live snapshot> --billing-db <billing snapshot> [--dry-run] [--json]
```
```
@@ -125,12 +125,13 @@ token as a Bearer and act on their own things only. The API never reads cookies.
@@ -125,12 +125,13 @@ token as a Bearer and act on their own things only. The API never reads cookies.
| `POST /simulate` | `tips.simulation.run` | owner |
| `POST /simulate` | `tips.simulation.run` | owner |
| `POST /internal/events` | Events webhook signature (`TIPS_EVENTS_SECRET`), loopback only | — |
| `POST /internal/events` | Events webhook signature (`TIPS_EVENTS_SECRET`), loopback only | — |
-The capabilities and the service manifest are released in openvibe-contracts v0.15.0 (3-segment ids:
+The capabilities and the service manifest were released in openvibe-contracts v0.15.0 (3-segment ids:
the charter's `tips.simulate` is `tips.simulation.run`; `tips.interaction.record` is new, for EXTERNAL
the charter's `tips.simulate` is `tips.simulation.run`; `tips.interaction.record` is new, for EXTERNAL
tips); the drafts stay in [docs/capabilities-proposal/](docs/capabilities-proposal/) and
tips); the drafts stay in [docs/capabilities-proposal/](docs/capabilities-proposal/) and
[docs/service-manifest-proposal.json](docs/service-manifest-proposal.json). Grants are matched with
[docs/service-manifest-proposal.json](docs/service-manifest-proposal.json). Grants are matched with
-contracts' `capabilities.grants()` (exact id or a `.*` family). Contracts has no `tips.*` event payload
-schemas yet.
+contracts' `capabilities.grants()` (exact id or a `.*` family). Tips pins openvibe-contracts v0.30.2,
+which also carries the payload schemas of the six `tips.*` events below; `test/contracts.test.js`
+validates every envelope and payload Tips produces against them.
## Events
## Events
@@ -241,7 +242,7 @@ exist here (plan §12.12):
@@ -241,7 +242,7 @@ exist here (plan §12.12):
4. real persistence and end-to-end workflows — ✔ against stubs; not yet against the real Billing and
4. real persistence and end-to-end workflows — ✔ against stubs; not yet against the real Billing and
Events (Billing is in shadow and has sent no events; the production database is empty; the Live
Events (Billing is in shadow and has sent no events; the production database is empty; the Live
import has not been run);
import has not been run);
-5. capability and event registration against OpenVibe.Contracts — ✔ v0.15.0 (no event payload schemas yet);
+5. capability and event registration against OpenVibe.Contracts — ✔ v0.15.0, payload schemas in v0.30.2 (pinned);
6. a migration/seed strategy ✔, a security/threat review (not done beyond the tests' refusals), and
6. a migration/seed strategy ✔, a security/threat review (not done beyond the tests' refusals), and
sitemap/robots ✔ (no feed);
sitemap/robots ✔ (no feed);
7. acceptance tests proving the advertised functionality — ✔ (table above).
7. acceptance tests proving the advertised functionality — ✔ (table above).
@@ -7,6 +7,6 @@
@@ -7,6 +7,6 @@
"adr": "ADR-012",
"adr": "ADR-012",
"authoritative": false,
"authoritative": false,
"deployment": "Deployed internally, not launched: openvibe-ovh /opt/openvibe.tips (release 435f32b), unit openvibe-tips, 127.0.0.1:4610 loopback only; https://openvibe.tips still serves the OpenVibe.Sites placeholder. Principal tips; Events subscription billing.transaction.* -> /internal/events; TIPS_CHAT_ADAPTER=none. Pins openvibe-contracts v0.15.0, openvibe-sdk v0.4.0, openvibe-shared v1.2.1.",
"deployment": "Deployed internally, not launched: openvibe-ovh /opt/openvibe.tips (release 435f32b), unit openvibe-tips, 127.0.0.1:4610 loopback only; https://openvibe.tips still serves the OpenVibe.Sites placeholder. Principal tips; Events subscription billing.transaction.* -> /internal/events; TIPS_CHAT_ADAPTER=none. Pins openvibe-contracts v0.15.0, openvibe-sdk v0.4.0, openvibe-shared v1.2.1.",
- "notes": "Runtime, API, SSR pages, overlays, Billing settlement consumer, events outbox, Live import and reconciliation are built and tested against stubs. Capabilities (17 tips.*) and the service manifest are registered in OpenVibe.Contracts v0.15.0; no tips.* event payload schemas yet. Production database is empty (0 profiles, interactions, goals, overlay tokens); the Live import (scripts/import-live.js) has never been run; 0 billing events have ever arrived because Billing runs in shadow. Live's /internal/tips/deliveries route is deployed (Live f11f809) but never called. The restore drill passed on the host 2026-09-23 05:23 UTC. Built since (not deployed yet): /metrics (loopback only) and the Billing/Events checks in /api/ready. Not done: privacy/moderation controls on the pages; an independent security review; the openvibe.tips launch. Live stays the place tips happen until the Billing cutover (blocked on the owner re-pointing the PowerChat webhook) and the launch rule in README.md holds.",
+ "notes": "Runtime, API, SSR pages, overlays, Billing settlement consumer, events outbox, Live import and reconciliation are built and tested against stubs. Capabilities (17 tips.*) and the service manifest are registered in OpenVibe.Contracts v0.15.0; the six tips.* event payload schemas are in v0.30.2, which the repository pins since 2026-09-23 (production still runs v0.15.0). Production database is empty (0 profiles, interactions, goals, overlay tokens); the Live import (scripts/import-live.js) has never been run; 0 billing events have ever arrived because Billing runs in shadow. Live's /internal/tips/deliveries route is deployed (Live f11f809) but never called. The restore drill passed on the host 2026-09-23 05:23 UTC. Built since (not deployed yet): /metrics (loopback only) and the Billing/Events checks in /api/ready. Not done: privacy/moderation controls on the pages; an independent security review; the openvibe.tips launch. Live stays the place tips happen until the Billing cutover (blocked on the owner re-pointing the PowerChat webhook) and the launch rule in README.md holds.",
"updated": "2026-09-23"
"updated": "2026-09-23"
}
}
@@ -14,7 +14,7 @@
@@ -14,7 +14,7 @@
"dotenv": "^16.4.7",
"dotenv": "^16.4.7",
"express": "^4.21.2",
"express": "^4.21.2",
"jsonwebtoken": "^9.0.2",
"jsonwebtoken": "^9.0.2",
- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.15.0",
+ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.30.2",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.4.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.4.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.3.0"
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.3.0"
},
},
@@ -983,9 +983,9 @@
@@ -983,9 +983,9 @@
}
}
},
},
"node_modules/openvibe-contracts": {
"node_modules/openvibe-contracts": {
- "version": "0.15.0",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.15.0",
- "integrity": "sha512-1+Y7Qm+eplVEwxACsga3eASmBrTNF2hQ/fmw33CQtzUZuTkA12sv93LCZo3lbvFyKbbZFZDYIHSt7gfu3QISlQ==",
+ "version": "0.30.2",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.30.2",
+ "integrity": "sha512-R01NgRY5V/GEYWkWUyxfl1MifSQb4dfJy11VS9RP+wsWedh2Crr9/IL7yGSn1PytwTKKL+tQl/r4ppGfH3TwLg==",
"license": "AGPL-3.0-only",
"license": "AGPL-3.0-only",
"dependencies": {
"dependencies": {
"ajv": "^8.17.1",
"ajv": "^8.17.1",
@@ -21,7 +21,7 @@
@@ -21,7 +21,7 @@
"dotenv": "^16.4.7",
"dotenv": "^16.4.7",
"express": "^4.21.2",
"express": "^4.21.2",
"jsonwebtoken": "^9.0.2",
"jsonwebtoken": "^9.0.2",
- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.15.0",
+ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.30.2",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.4.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.4.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.3.0"
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.3.0"
},
},
new file mode 100644
new file mode 100644
@@ -0,0 +1,55 @@
@@ -0,0 +1,55 @@
+'use strict';
+// Every event Tips puts in its outbox is a valid events.event-envelope@1 whose payload validates
+// against the tips.* payload schema released in openvibe-contracts (v0.30.2 registers all six).
+const assert = require('assert');
+const contracts = require('openvibe-contracts');
+const { boot, check, done } = require('./helpers/app');
+
+const TYPES = ['tips.interaction.ready', 'tips.interaction.failed', 'tips.interaction.cancelled', 'tips.goal.updated', 'tips.overlay.delivered', 'tips.overlay.failed'];
+
+(async () => {
+ const t = await boot({ live: true });
+ const { domain, billing, live } = t;
+ const alex = await t.creator('alex', { settings: { tts_enabled: true } });
+ const viewer = t.network.newUser('viewer');
+ billing.fund(viewer.subject, 10_000);
+
+ await check('the six tips.* payload schemas are in the installed contracts', async () => {
+ for (const type of TYPES) assert.strictEqual(contracts.resolve(type).owner, 'tips', type);
+ });
+
+ await check('a flow that produces every tips.* event: each envelope and payload validates', async () => {
+ await t.call('POST', '/api/v1/goals', { user: alex, body: { title: 'Desk', target_amount: 1000 } });
+ const tok = await t.call('POST', '/api/v1/overlay-tokens', { user: alex, body: { scopes: ['alerts', 'goals'] } });
+ const s = await t.sse(`/overlay/${tok.json.secret}/events`);
+ const tip = await t.call('POST', '/api/v1/checkout', { user: viewer, body: { creator: 'alex', amount: 300, message: 'hi' } });
+ await s.waitFor((e) => e.event === 'alert' && e.data.interaction_id === tip.json.interaction.id);
+ s.close();
+ await domain.effects.drain();
+ // A delivery Live refuses (tips.interaction.failed), an overlay nobody showed (tips.overlay.failed).
+ live.state.fail = 422;
+ await t.call('POST', '/api/v1/checkout', { user: viewer, body: { creator: 'alex', amount: 5 } });
+ await domain.effects.drain();
+ live.state.fail = null;
+ t.clock.offset += 11 * 60 * 1000;
+ domain.overlays.sweepFailed();
+ t.clock.offset = 0;
+ // A reversal before delivery (tips.interaction.cancelled) and one after it.
+ const pending = await t.call('POST', '/api/v1/tts-requests', { user: viewer, body: { creator: 'alex', amount: 150, tts: { text: 'read me' } } });
+ const txn = domain.interactions.get(pending.json.interaction.id).billing_txn_id;
+ await t.deliver(billing.refund(txn));
+ await t.deliver(billing.refund(domain.interactions.get(tip.json.interaction.id).billing_txn_id, 100));
+
+ const rows = t.outboxRows();
+ for (const type of TYPES) assert.ok(rows.some((e) => e.event_type === type), `no ${type} was produced`);
+ for (const e of rows) {
+ const env = contracts.validate('events.event-envelope@1', e);
+ assert.ok(env.valid, `${e.event_type} envelope: ${JSON.stringify(env.errors)}`);
+ const p = contracts.validate(e.event_type, e.payload);
+ assert.ok(p.valid, `${e.event_type} payload: ${JSON.stringify(p.errors)} in ${JSON.stringify(e.payload)}`);
+ }
+ });
+
+ await t.close();
+ done();
+})().catch((e) => { console.error(e); process.exit(1); });