@ekzs/cli 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +148 -0
- package/dist/commands/agent.d.ts +31 -0
- package/dist/commands/agent.d.ts.map +1 -0
- package/dist/commands/agent.js +55 -0
- package/dist/commands/ask.d.ts +20 -0
- package/dist/commands/ask.d.ts.map +1 -0
- package/dist/commands/ask.js +154 -0
- package/dist/commands/doctor.d.ts +3 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +44 -0
- package/dist/commands/health.d.ts +2 -0
- package/dist/commands/health.d.ts.map +1 -0
- package/dist/commands/health.js +28 -0
- package/dist/commands/local-agent.d.ts +19 -0
- package/dist/commands/local-agent.d.ts.map +1 -0
- package/dist/commands/local-agent.js +450 -0
- package/dist/commands/scan.d.ts +11 -0
- package/dist/commands/scan.d.ts.map +1 -0
- package/dist/commands/scan.js +119 -0
- package/dist/commands/webhook.d.ts +10 -0
- package/dist/commands/webhook.d.ts.map +1 -0
- package/dist/commands/webhook.js +42 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +185 -0
- package/dist/lib/banner.d.ts +10 -0
- package/dist/lib/banner.d.ts.map +1 -0
- package/dist/lib/banner.js +26 -0
- package/dist/lib/commands-i18n.d.ts +20 -0
- package/dist/lib/commands-i18n.d.ts.map +1 -0
- package/dist/lib/commands-i18n.js +157 -0
- package/dist/lib/composer-model.d.ts +10 -0
- package/dist/lib/composer-model.d.ts.map +1 -0
- package/dist/lib/composer-model.js +15 -0
- package/dist/lib/context.d.ts +12 -0
- package/dist/lib/context.d.ts.map +1 -0
- package/dist/lib/context.js +56 -0
- package/dist/lib/doctor-quiet.d.ts +11 -0
- package/dist/lib/doctor-quiet.d.ts.map +1 -0
- package/dist/lib/doctor-quiet.js +39 -0
- package/dist/lib/env.d.ts +18 -0
- package/dist/lib/env.d.ts.map +1 -0
- package/dist/lib/env.js +66 -0
- package/dist/lib/help.d.ts +10 -0
- package/dist/lib/help.d.ts.map +1 -0
- package/dist/lib/help.js +140 -0
- package/dist/lib/locale.d.ts +38 -0
- package/dist/lib/locale.d.ts.map +1 -0
- package/dist/lib/locale.js +189 -0
- package/dist/lib/mode.d.ts +11 -0
- package/dist/lib/mode.d.ts.map +1 -0
- package/dist/lib/mode.js +29 -0
- package/dist/lib/output.d.ts +7 -0
- package/dist/lib/output.d.ts.map +1 -0
- package/dist/lib/output.js +18 -0
- package/dist/lib/preferences.d.ts +9 -0
- package/dist/lib/preferences.d.ts.map +1 -0
- package/dist/lib/preferences.js +35 -0
- package/dist/lib/redact.d.ts +3 -0
- package/dist/lib/redact.d.ts.map +1 -0
- package/dist/lib/redact.js +32 -0
- package/dist/lib/scan-quiet.d.ts +4 -0
- package/dist/lib/scan-quiet.d.ts.map +1 -0
- package/dist/lib/scan-quiet.js +4 -0
- package/dist/lib/scope.d.ts +5 -0
- package/dist/lib/scope.d.ts.map +1 -0
- package/dist/lib/scope.js +61 -0
- package/dist/lib/session.d.ts +31 -0
- package/dist/lib/session.d.ts.map +1 -0
- package/dist/lib/session.js +101 -0
- package/dist/lib/shell.d.ts +18 -0
- package/dist/lib/shell.d.ts.map +1 -0
- package/dist/lib/shell.js +214 -0
- package/dist/lib/skill.d.ts +3 -0
- package/dist/lib/skill.d.ts.map +1 -0
- package/dist/lib/skill.js +2 -0
- package/dist/lib/skills.d.ts +16 -0
- package/dist/lib/skills.d.ts.map +1 -0
- package/dist/lib/skills.js +199 -0
- package/dist/lib/theme.d.ts +23 -0
- package/dist/lib/theme.d.ts.map +1 -0
- package/dist/lib/theme.js +40 -0
- package/dist/lib/ui/ascii-art.d.ts +10 -0
- package/dist/lib/ui/ascii-art.d.ts.map +1 -0
- package/dist/lib/ui/ascii-art.js +55 -0
- package/dist/lib/ui/layout.d.ts +19 -0
- package/dist/lib/ui/layout.d.ts.map +1 -0
- package/dist/lib/ui/layout.js +46 -0
- package/dist/lib/ui/logo.d.ts +3 -0
- package/dist/lib/ui/logo.d.ts.map +1 -0
- package/dist/lib/ui/logo.js +8 -0
- package/dist/lib/ui/prompt.d.ts +6 -0
- package/dist/lib/ui/prompt.d.ts.map +1 -0
- package/dist/lib/ui/prompt.js +75 -0
- package/dist/lib/ui/splash.d.ts +15 -0
- package/dist/lib/ui/splash.d.ts.map +1 -0
- package/dist/lib/ui/splash.js +121 -0
- package/package.json +48 -0
- package/skills/ekz-connect/SKILL.md +99 -0
- package/skills/ekz-data-layer-design/SKILL.md +199 -0
- package/skills/ekz-data-mongo/SKILL.md +341 -0
- package/skills/ekz-data-mysql/SKILL.md +245 -0
- package/skills/ekz-data-postgres/SKILL.md +257 -0
- package/skills/ekz-data-sqlite/SKILL.md +261 -0
- package/skills/ekz-ekwanza-provider-adapter/SKILL.md +91 -0
- package/skills/ekz-integration-playbook/SKILL.md +122 -0
- package/skills/ekz-one-time-product-payments/SKILL.md +91 -0
- package/skills/ekz-overage-billing/SKILL.md +68 -0
- package/skills/ekz-payment-core-architecture/SKILL.md +121 -0
- package/skills/ekz-sdk-cli/SKILL.md +82 -0
- package/skills/ekz-subscription-billing/SKILL.md +120 -0
- package/skills/ekz-ticket-invite-selling/SKILL.md +64 -0
- package/skills/ekz-webhook-normalization/SKILL.md +88 -0
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ekz-data-sqlite
|
|
3
|
+
description: >-
|
|
4
|
+
SQLite STRICT tables, json1, WAL mode, and the fit-for-purpose
|
|
5
|
+
guidance for when SQLite is and isn't appropriate for payment flows
|
|
6
|
+
(tests, embedded, single-process). Includes parity DDL for the payment
|
|
7
|
+
primitives.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# SQLite Data Layer
|
|
11
|
+
|
|
12
|
+
Use this skill when the host codebase uses SQLite (test suites, CLI
|
|
13
|
+
tools, embedded apps, local-first products). SQLite is small, fast,
|
|
14
|
+
and reliable when used inside its lane. Outside that lane, it bites.
|
|
15
|
+
|
|
16
|
+
## When SQLite is right
|
|
17
|
+
|
|
18
|
+
- **Test suites** that need a real SQL engine without a service.
|
|
19
|
+
- **CLI tools and developer tooling** with per-user data.
|
|
20
|
+
- **Single-process desktop/mobile apps** (Electron, mobile).
|
|
21
|
+
- **Local-first / offline-first** products that sync to a server later.
|
|
22
|
+
- **Edge deployments** with single-writer assumptions (Cloudflare D1,
|
|
23
|
+
Turso, LiteFS — each adds its own constraints).
|
|
24
|
+
|
|
25
|
+
## When SQLite is wrong
|
|
26
|
+
|
|
27
|
+
- **Multi-writer production workloads.** SQLite serialises writes
|
|
28
|
+
globally. A payments backend with concurrent webhook handlers will
|
|
29
|
+
contend on every transaction.
|
|
30
|
+
- **Multi-tenant SaaS with shared data.** No RLS, no per-row auth.
|
|
31
|
+
Tenant isolation is purely the application's responsibility.
|
|
32
|
+
- **Anything requiring strong durability over network.** SQLite is a
|
|
33
|
+
*library*, not a service. Distributed SQLite (LiteFS, Turso) adds back
|
|
34
|
+
durability but with replication semantics you must read carefully.
|
|
35
|
+
|
|
36
|
+
If the production engine is Postgres and SQLite is only used for tests,
|
|
37
|
+
keep schema and queries **portability-conservative** — avoid Postgres-only
|
|
38
|
+
syntax (`RETURNING`, `JSONB`, partial indexes, RLS, generated columns
|
|
39
|
+
with `STORED` semantics that differ) inside code paths the SQLite tests
|
|
40
|
+
run.
|
|
41
|
+
|
|
42
|
+
## What SQLite gives you
|
|
43
|
+
|
|
44
|
+
- One-file durability. Easy backups (copy the file).
|
|
45
|
+
- WAL mode for concurrent readers + one writer.
|
|
46
|
+
- `json1` extension for structured payloads.
|
|
47
|
+
- STRICT tables (3.37+) for real type enforcement.
|
|
48
|
+
- `RETURNING` (3.35+) — yes, Postgres-style.
|
|
49
|
+
- Full-text search (FTS5) and R-Tree spatial indexes built in.
|
|
50
|
+
|
|
51
|
+
## Bootstrap configuration
|
|
52
|
+
|
|
53
|
+
```sql
|
|
54
|
+
-- Run once when opening the DB:
|
|
55
|
+
PRAGMA journal_mode = WAL; -- readers + one writer concurrent.
|
|
56
|
+
PRAGMA synchronous = NORMAL; -- safe with WAL; FULL is overkill.
|
|
57
|
+
PRAGMA foreign_keys = ON; -- off by default. Always turn on.
|
|
58
|
+
PRAGMA busy_timeout = 5000; -- wait 5s on SQLITE_BUSY instead of failing.
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
These settings live with the connection, not the DB file — set them on
|
|
62
|
+
every open. Most drivers have a `pragmas` option for this.
|
|
63
|
+
|
|
64
|
+
## DDL conventions
|
|
65
|
+
|
|
66
|
+
```sql
|
|
67
|
+
BEGIN;
|
|
68
|
+
|
|
69
|
+
CREATE TABLE IF NOT EXISTS app_payment_requests (
|
|
70
|
+
id TEXT PRIMARY KEY, -- UUID v4 string.
|
|
71
|
+
tenant_id TEXT NOT NULL,
|
|
72
|
+
kind TEXT NOT NULL CHECK (kind IN ('order','ticket','subscription_cycle','usage_charge')),
|
|
73
|
+
amount REAL NOT NULL CHECK (amount > 0), -- see note below
|
|
74
|
+
currency TEXT NOT NULL DEFAULT 'AOA',
|
|
75
|
+
status TEXT NOT NULL DEFAULT 'pending'
|
|
76
|
+
CHECK (status IN ('pending','paid','failed','expired','canceled')),
|
|
77
|
+
business_object_kind TEXT,
|
|
78
|
+
business_object_id TEXT,
|
|
79
|
+
metadata TEXT NOT NULL DEFAULT '{}', -- JSON as TEXT.
|
|
80
|
+
due_at TEXT, -- ISO-8601 UTC string.
|
|
81
|
+
resolved_at TEXT,
|
|
82
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
83
|
+
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
|
84
|
+
) STRICT;
|
|
85
|
+
|
|
86
|
+
CREATE INDEX IF NOT EXISTS idx_app_payment_requests_due
|
|
87
|
+
ON app_payment_requests (tenant_id, status, due_at);
|
|
88
|
+
|
|
89
|
+
COMMIT;
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Rules of thumb:
|
|
93
|
+
|
|
94
|
+
- **`STRICT` tables.** Without it, SQLite's type column declarations are
|
|
95
|
+
*suggestions* and a column declared `INTEGER` will happily store `"abc"`.
|
|
96
|
+
Always add `STRICT` (3.37+).
|
|
97
|
+
- **Storage classes are limited.** STRICT allows: `INTEGER`, `REAL`, `TEXT`,
|
|
98
|
+
`BLOB`, `ANY`. No `BOOLEAN`, no `DECIMAL`, no `DATETIME`.
|
|
99
|
+
- **Money — pick one and stay consistent.** Either:
|
|
100
|
+
- `INTEGER` minor units (cents), which is exact and recommended.
|
|
101
|
+
- `TEXT` numeric string parsed by the app, exact but slower comparisons.
|
|
102
|
+
- `REAL` (floating-point), simple but accumulates errors. Acceptable
|
|
103
|
+
only for low-stakes prototypes.
|
|
104
|
+
- **Timestamps as ISO-8601 TEXT.** Always UTC, always with `Z` suffix.
|
|
105
|
+
`strftime('%Y-%m-%dT%H:%M:%fZ', 'now')` gives millisecond precision.
|
|
106
|
+
- **UUIDs as TEXT** (36 chars). Or BLOB(16) if size matters and you don't
|
|
107
|
+
query them outside the app.
|
|
108
|
+
- **`CHECK` constraints** are enforced. Use them for enums.
|
|
109
|
+
- **No native `auto-updated` timestamp.** Use a trigger.
|
|
110
|
+
|
|
111
|
+
### Updated-at trigger
|
|
112
|
+
|
|
113
|
+
```sql
|
|
114
|
+
CREATE TRIGGER tg_app_payment_requests_updated
|
|
115
|
+
AFTER UPDATE ON app_payment_requests
|
|
116
|
+
FOR EACH ROW BEGIN
|
|
117
|
+
UPDATE app_payment_requests
|
|
118
|
+
SET updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
|
|
119
|
+
WHERE id = OLD.id;
|
|
120
|
+
END;
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
The trigger recurses unless you also set
|
|
124
|
+
`PRAGMA recursive_triggers = OFF;` (default OFF in modern SQLite). Verify.
|
|
125
|
+
|
|
126
|
+
## Tenant isolation
|
|
127
|
+
|
|
128
|
+
There is no Row-Level Security. Tenant isolation is **entirely** the
|
|
129
|
+
application's job:
|
|
130
|
+
|
|
131
|
+
- Repository layer that takes `tenantId` and injects it into every
|
|
132
|
+
`WHERE`.
|
|
133
|
+
- Code review checklist item.
|
|
134
|
+
- One linting rule per project: no raw SQL on tenant-scoped tables
|
|
135
|
+
outside the repository module.
|
|
136
|
+
|
|
137
|
+
For local-first / single-tenant SQLite use, isolation isn't needed —
|
|
138
|
+
there's one user per file.
|
|
139
|
+
|
|
140
|
+
## json1 usage
|
|
141
|
+
|
|
142
|
+
SQLite ships `json1` enabled by default in any modern build:
|
|
143
|
+
|
|
144
|
+
```sql
|
|
145
|
+
-- Read a JSON value.
|
|
146
|
+
SELECT json_extract(metadata, '$.rail') AS rail
|
|
147
|
+
FROM app_payment_requests;
|
|
148
|
+
|
|
149
|
+
-- Update a JSON path.
|
|
150
|
+
UPDATE app_payment_requests
|
|
151
|
+
SET metadata = json_set(metadata, '$.normalized_status', 'paid')
|
|
152
|
+
WHERE id = ?;
|
|
153
|
+
|
|
154
|
+
-- Query a JSON path with an index via generated column.
|
|
155
|
+
ALTER TABLE app_webhook_events
|
|
156
|
+
ADD COLUMN payload_rail TEXT AS (json_extract(payload, '$.rail')) STORED;
|
|
157
|
+
CREATE INDEX idx_webhook_rail ON app_webhook_events (payload_rail);
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
`json_set` / `json_insert` / `json_replace` differ:
|
|
161
|
+
|
|
162
|
+
- `json_set`: write whether the path exists or not.
|
|
163
|
+
- `json_insert`: write only if path doesn't exist.
|
|
164
|
+
- `json_replace`: write only if path exists.
|
|
165
|
+
|
|
166
|
+
## Idempotent inserts and upserts
|
|
167
|
+
|
|
168
|
+
```sql
|
|
169
|
+
-- Webhook dedupe.
|
|
170
|
+
INSERT INTO app_webhook_events (id, provider, event_id, status, payload)
|
|
171
|
+
VALUES (?, ?, ?, 'processing', ?)
|
|
172
|
+
ON CONFLICT (provider, event_id) DO NOTHING
|
|
173
|
+
RETURNING id;
|
|
174
|
+
|
|
175
|
+
-- Upsert.
|
|
176
|
+
INSERT INTO app_usage_records (id, entitlement_id, meter, quantity, idempotency_key, occurred_at)
|
|
177
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
178
|
+
ON CONFLICT (entitlement_id, meter, idempotency_key)
|
|
179
|
+
DO UPDATE SET quantity = excluded.quantity, occurred_at = excluded.occurred_at;
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
`excluded.col` is SQLite's name for the row that was being inserted.
|
|
183
|
+
|
|
184
|
+
## Optimistic claim pattern
|
|
185
|
+
|
|
186
|
+
```sql
|
|
187
|
+
UPDATE app_scheduled_jobs
|
|
188
|
+
SET claimed_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now'),
|
|
189
|
+
claimed_by = ?,
|
|
190
|
+
updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
|
|
191
|
+
WHERE id = ?
|
|
192
|
+
AND (claimed_at IS NULL
|
|
193
|
+
OR claimed_at < strftime('%Y-%m-%dT%H:%M:%fZ', 'now', '-10 minutes'))
|
|
194
|
+
RETURNING id, kind, payload, due_at, attempts;
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
`RETURNING` lands the claim in one round trip. Empty result = lost race.
|
|
198
|
+
|
|
199
|
+
Because SQLite serialises writes globally, contention is naturally low —
|
|
200
|
+
but `busy_timeout` is essential or you'll see `SQLITE_BUSY` on contention.
|
|
201
|
+
|
|
202
|
+
## Append-only ledger
|
|
203
|
+
|
|
204
|
+
No real GRANT/REVOKE in SQLite (it's a library, not a service). Enforce
|
|
205
|
+
at the app layer + a trigger:
|
|
206
|
+
|
|
207
|
+
```sql
|
|
208
|
+
CREATE TRIGGER tg_app_ledger_no_update
|
|
209
|
+
BEFORE UPDATE ON app_ledger_entries
|
|
210
|
+
FOR EACH ROW BEGIN
|
|
211
|
+
SELECT RAISE(ABORT, 'ledger entries are append-only');
|
|
212
|
+
END;
|
|
213
|
+
|
|
214
|
+
CREATE TRIGGER tg_app_ledger_no_delete
|
|
215
|
+
BEFORE DELETE ON app_ledger_entries
|
|
216
|
+
FOR EACH ROW BEGIN
|
|
217
|
+
SELECT RAISE(ABORT, 'ledger entries are append-only');
|
|
218
|
+
END;
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## Migrations
|
|
222
|
+
|
|
223
|
+
Without a migration tool of choice, a minimal scheme:
|
|
224
|
+
|
|
225
|
+
```sql
|
|
226
|
+
-- 001_init.sql
|
|
227
|
+
CREATE TABLE IF NOT EXISTS _migrations (
|
|
228
|
+
id TEXT PRIMARY KEY,
|
|
229
|
+
applied_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
|
230
|
+
) STRICT;
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
App code on boot:
|
|
234
|
+
|
|
235
|
+
1. Open DB, set pragmas.
|
|
236
|
+
2. `BEGIN`; check `_migrations` for each file in `db/`; run each missing
|
|
237
|
+
one; insert `_migrations` row; `COMMIT`.
|
|
238
|
+
3. SQLite migrations CAN be transactional — unlike MySQL most DDL is
|
|
239
|
+
inside the transaction. Use this.
|
|
240
|
+
|
|
241
|
+
## Gotchas
|
|
242
|
+
|
|
243
|
+
- **`DEFAULT` expressions need parentheses for non-literal values.**
|
|
244
|
+
`DEFAULT now()` won't parse; `DEFAULT (strftime(...))` will.
|
|
245
|
+
- **`ALTER TABLE` is limited.** You can add columns, rename tables, rename
|
|
246
|
+
columns (3.25+). To drop/rename with constraints, copy-rebuild:
|
|
247
|
+
```sql
|
|
248
|
+
BEGIN;
|
|
249
|
+
CREATE TABLE app_payments_new (...);
|
|
250
|
+
INSERT INTO app_payments_new SELECT ... FROM app_payments;
|
|
251
|
+
DROP TABLE app_payments;
|
|
252
|
+
ALTER TABLE app_payments_new RENAME TO app_payments;
|
|
253
|
+
-- Recreate indexes and triggers.
|
|
254
|
+
COMMIT;
|
|
255
|
+
```
|
|
256
|
+
- **Boolean returns INTEGER 0/1.** No native boolean — just be consistent.
|
|
257
|
+
- **`PRAGMA` doesn't take parameters in PREPARE statements.** Set them
|
|
258
|
+
with literal values in raw SQL on connection open.
|
|
259
|
+
- **WAL files (`-wal`, `-shm`) must travel with the DB.** Backups via
|
|
260
|
+
file copy must do `PRAGMA wal_checkpoint(FULL);` first or use the
|
|
261
|
+
online backup API.
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ekz-ekwanza-provider-adapter
|
|
3
|
+
description: >-
|
|
4
|
+
e-Kwanza provider adapter: auth, config, credentials, GPO, EMIS reference,
|
|
5
|
+
Ticket API, one-time charge creation, provider identifiers, env validation,
|
|
6
|
+
and provider-specific callback verification.
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# e-Kwanza Provider Adapter
|
|
10
|
+
|
|
11
|
+
This skill owns e-Kwanza-specific behavior only. It should not own product fulfillment, ticket inventory, subscription cycles, overage settlement, or entitlements.
|
|
12
|
+
|
|
13
|
+
## API limitation
|
|
14
|
+
|
|
15
|
+
e-Kwanza provides one-time payment primitives. There is no native subscription API, no native overage billing, and no app-level inventory model. Those behaviors must be implemented locally in the application.
|
|
16
|
+
|
|
17
|
+
## Responsibilities
|
|
18
|
+
|
|
19
|
+
- Load and validate credentials.
|
|
20
|
+
- Select sandbox or production endpoints.
|
|
21
|
+
- Expose rail capabilities.
|
|
22
|
+
- Create one-time payment attempts.
|
|
23
|
+
- Return provider identifiers and customer-facing instructions.
|
|
24
|
+
- Verify provider-specific callback signatures.
|
|
25
|
+
- Keep provider details behind an adapter boundary.
|
|
26
|
+
|
|
27
|
+
## Rails
|
|
28
|
+
|
|
29
|
+
| Rail | API | Customer UX | Required capability |
|
|
30
|
+
|------|-----|-------------|---------------------|
|
|
31
|
+
| `gpo` | AppyPay charges + phone | Multicaixa Express push | `GPO_*` payment method, phone |
|
|
32
|
+
| `emis_ref` | AppyPay charges `REF_*` | ATM/home banking reference | `REF_*` payment method |
|
|
33
|
+
| `ticket` | `POST /Ticket/{notificationToken}` | SMS/QR/code in e-Kwanza wallet | notification token, phone, HMAC config |
|
|
34
|
+
|
|
35
|
+
Do not use `"reference"` as a new rail ID. If an app already stores `"reference"`, inspect whether it means `emis_ref`, `ticket`, or a UI label that can resolve to either.
|
|
36
|
+
|
|
37
|
+
## Required config
|
|
38
|
+
|
|
39
|
+
```text
|
|
40
|
+
EKWANZA_CLIENT_ID
|
|
41
|
+
EKWANZA_CLIENT_SECRET
|
|
42
|
+
EKWANZA_MERCHANT_ACCOUNT
|
|
43
|
+
EKWANZA_API_KEY
|
|
44
|
+
EKWANZA_ENV=sandbox|production
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Rail-specific:
|
|
48
|
+
|
|
49
|
+
```text
|
|
50
|
+
EKWANZA_PAYMENT_METHOD_GPO=GPO_...
|
|
51
|
+
EKWANZA_PAYMENT_METHOD_GPR=REF_... # or EKWANZA_PAYMENT_METHOD_REF
|
|
52
|
+
EKWANZA_NOTIFICATION_TOKEN=...
|
|
53
|
+
EKWANZA_COMPANY_REGISTRATION=...
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Adapter contract
|
|
57
|
+
|
|
58
|
+
Create exactly one provider attempt for one local payment attempt:
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
await client.createPayment("gpo", {
|
|
62
|
+
amountAoa: 15000,
|
|
63
|
+
merchantTransactionId: "REQ1234567890",
|
|
64
|
+
phoneNumber: "935095730",
|
|
65
|
+
});
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Persist `merchantTransactionId`, provider operation IDs, Ticket `operationCode`, reference/code, QR payload, expiration, and raw provider response.
|
|
69
|
+
|
|
70
|
+
## Ticket HMAC
|
|
71
|
+
|
|
72
|
+
Ticket-like callbacks must verify:
|
|
73
|
+
|
|
74
|
+
```text
|
|
75
|
+
message = code + operationCode + partnerRegistrationNumber + notificationToken
|
|
76
|
+
signature = HMAC_SHA256(message, EKWANZA_API_KEY)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Production behavior must fail closed when the signature is missing, invalid, or required signing config is missing.
|
|
80
|
+
|
|
81
|
+
## Configuration policy
|
|
82
|
+
|
|
83
|
+
- Do not expose a rail in UI unless its config is complete.
|
|
84
|
+
- Decide explicitly whether credentials are platform-level or merchant/account-level.
|
|
85
|
+
- Never log secrets.
|
|
86
|
+
- Keep sandbox/production URL selection explicit.
|
|
87
|
+
- Normalize provider errors into local errors that domain code can handle.
|
|
88
|
+
|
|
89
|
+
## Internal provider-config case study
|
|
90
|
+
|
|
91
|
+
One internal reference combines env fallback and tenant-owned payment-method config. That is a valid implementation choice, not a rule. In a new codebase, first decide who receives the money, then place credentials at the matching owner boundary.
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ekz-integration-playbook
|
|
3
|
+
description: >-
|
|
4
|
+
Integration playbook for unknown codebases: inspect existing models, map
|
|
5
|
+
payment primitives onto local routes, jobs, auth, persistence, and provider
|
|
6
|
+
interfaces without copying internal reference-app plans or tables.
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Integration Playbook
|
|
10
|
+
|
|
11
|
+
Use this skill when adapting the payment architecture to any codebase.
|
|
12
|
+
|
|
13
|
+
## First inspect
|
|
14
|
+
|
|
15
|
+
Before editing, answer:
|
|
16
|
+
|
|
17
|
+
- Where are users, accounts, tenants, merchants, or organizations stored?
|
|
18
|
+
- Is there already an order, invoice, cart, subscription, or usage table?
|
|
19
|
+
- Is there a payment/provider abstraction?
|
|
20
|
+
- How are statuses represented?
|
|
21
|
+
- How are webhooks authenticated, deduped, and retried?
|
|
22
|
+
- Is there a job, cron, queue, scheduled function, or worker system?
|
|
23
|
+
- Is there a ledger or audit table?
|
|
24
|
+
- What business object needs fulfillment after payment?
|
|
25
|
+
- What should happen on pending, paid, failed, expired, duplicate callback?
|
|
26
|
+
- Who receives the money: platform or merchant?
|
|
27
|
+
|
|
28
|
+
## Mapping rule
|
|
29
|
+
|
|
30
|
+
Map concepts into the target app's existing vocabulary:
|
|
31
|
+
|
|
32
|
+
```text
|
|
33
|
+
PaymentRequest -> invoice/payment_intent/order_payment/billing_request
|
|
34
|
+
PaymentAttempt -> payment_attempt/provider_attempt/charge_attempt
|
|
35
|
+
WebhookEvent -> webhook_event/provider_event/idempotency_record
|
|
36
|
+
FulfillmentAction -> fulfillment/job/outbox/domain_event
|
|
37
|
+
Entitlement -> subscription_access/quota/license/membership
|
|
38
|
+
UsageRecord -> usage_event/meter_event/activity_record
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Do not force internal reference-app table names, plan names, or routes into another app.
|
|
42
|
+
|
|
43
|
+
## Plain-language prompts
|
|
44
|
+
|
|
45
|
+
Users do not need to know the architecture names. Translate their intent:
|
|
46
|
+
|
|
47
|
+
- "payment link" -> public one-time payment checkout
|
|
48
|
+
- "product in stock" -> inventory guard plus paid-only fulfillment
|
|
49
|
+
- "store checkout" -> one-time product/order payment flow
|
|
50
|
+
- "Multicaixa Express" -> GPO rail
|
|
51
|
+
- "reference code" -> EMIS reference rail unless the existing app clearly means Ticket API
|
|
52
|
+
- "ticket/invite sale" -> reservation, paid issuance, QR/code ownership
|
|
53
|
+
- "subscription/monthly plan" -> local recurring one-time billing requests
|
|
54
|
+
- "extra usage/overage" -> usage ledger, aggregation, charge, settlement
|
|
55
|
+
|
|
56
|
+
Implement the correct architecture without requiring the user to say these internal terms.
|
|
57
|
+
|
|
58
|
+
## Suggested greenfield layout
|
|
59
|
+
|
|
60
|
+
```text
|
|
61
|
+
src/
|
|
62
|
+
payments/
|
|
63
|
+
core/
|
|
64
|
+
types.ts
|
|
65
|
+
state-machine.ts
|
|
66
|
+
idempotency.ts
|
|
67
|
+
ledger.ts
|
|
68
|
+
providers/
|
|
69
|
+
ekwanza/
|
|
70
|
+
client.ts
|
|
71
|
+
rails.ts
|
|
72
|
+
webhooks.ts
|
|
73
|
+
webhooks/
|
|
74
|
+
payment-webhook-router.ts
|
|
75
|
+
|
|
76
|
+
billing/
|
|
77
|
+
subscriptions/
|
|
78
|
+
cycles.ts
|
|
79
|
+
payment-requests.ts
|
|
80
|
+
entitlements.ts
|
|
81
|
+
scheduler.ts
|
|
82
|
+
overage/
|
|
83
|
+
usage-ledger.ts
|
|
84
|
+
aggregation.ts
|
|
85
|
+
charges.ts
|
|
86
|
+
|
|
87
|
+
commerce/
|
|
88
|
+
orders/
|
|
89
|
+
checkout.ts
|
|
90
|
+
fulfillment.ts
|
|
91
|
+
|
|
92
|
+
tickets/
|
|
93
|
+
reservations.ts
|
|
94
|
+
issuance.ts
|
|
95
|
+
qr.ts
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
For an existing app, adapt to its structure instead of creating this exact tree.
|
|
99
|
+
|
|
100
|
+
## Implementation order
|
|
101
|
+
|
|
102
|
+
1. Add provider adapter and env validation.
|
|
103
|
+
2. Add payment core primitives or map to existing models.
|
|
104
|
+
3. Add webhook normalization and idempotency.
|
|
105
|
+
4. Add one business flow at a time.
|
|
106
|
+
5. Add scheduler only for recurring/overage flows.
|
|
107
|
+
6. Add tests for duplicate callbacks and business side effects.
|
|
108
|
+
|
|
109
|
+
## What not to do
|
|
110
|
+
|
|
111
|
+
- Do not fulfill from client-side success screens.
|
|
112
|
+
- Do not trust amount, plan, product, or usage from the browser.
|
|
113
|
+
- Do not let raw provider status leak into domain logic.
|
|
114
|
+
- Do not create duplicate local lifecycle systems for the same business object.
|
|
115
|
+
- Do not expose rails with incomplete config.
|
|
116
|
+
- Do not process Ticket webhooks without fail-closed signature verification in production.
|
|
117
|
+
|
|
118
|
+
## Internal subscription case study
|
|
119
|
+
|
|
120
|
+
The internal subscription reference teaches one way to run subscriptions over one-time e-Kwanza charges: local billing cycles, due dates, grace, reminders, webhook-driven state, and entitlement updates.
|
|
121
|
+
|
|
122
|
+
Use it to learn the pattern. The target codebase remains the source of truth for naming, routes, models, policies, and entitlements.
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ekz-one-time-product-payments
|
|
3
|
+
description: >-
|
|
4
|
+
One-time product payment architecture: orders, products, invoices, checkout
|
|
5
|
+
links, payment requests, payment attempts, paid fulfillment, failed/expired
|
|
6
|
+
recovery, stock updates, and receipts.
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# One-Time Product Payments
|
|
10
|
+
|
|
11
|
+
Use this skill for products, orders, invoices, checkout links, donations, bookings, deposits, and payment collections.
|
|
12
|
+
|
|
13
|
+
## Plain-language trigger
|
|
14
|
+
|
|
15
|
+
If the user says "create a payment link", "add payment to my store", "sell this product", "25 in stock", "checkout with Multicaixa Express", or "pay with reference code", implement a one-time product/order payment flow. Do not require the user to name payment primitives.
|
|
16
|
+
|
|
17
|
+
## Pattern
|
|
18
|
+
|
|
19
|
+
```text
|
|
20
|
+
Order/Product/Invoice -> PaymentRequest -> PaymentAttempt
|
|
21
|
+
-> normalized paid webhook -> FulfillmentAction
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The business object owns what is being sold. The payment layer owns collection. Fulfillment happens after a normalized paid event.
|
|
25
|
+
|
|
26
|
+
## Minimal concepts
|
|
27
|
+
|
|
28
|
+
- Domain object: order, invoice, cart, booking, product sale, collection.
|
|
29
|
+
- Payment request: amount and reason to collect.
|
|
30
|
+
- Payment attempt: one provider try using GPO, EMIS ref, or Ticket.
|
|
31
|
+
- Fulfillment action: ship, mark paid, deliver access, send receipt, increment sold count.
|
|
32
|
+
- Webhook event: normalized and idempotent.
|
|
33
|
+
|
|
34
|
+
Use existing tables/models if present. Do not create a parallel order system unless the app has none.
|
|
35
|
+
|
|
36
|
+
## Flow
|
|
37
|
+
|
|
38
|
+
1. Validate the domain object and compute amount server-side.
|
|
39
|
+
2. Check availability, ownership, status, and currency.
|
|
40
|
+
3. Create a local `PaymentRequest`.
|
|
41
|
+
4. Create a local `PaymentAttempt`.
|
|
42
|
+
5. Call the provider adapter to create the one-time charge.
|
|
43
|
+
6. Store provider identifiers and customer-facing instructions.
|
|
44
|
+
7. Poll local status or show instructions.
|
|
45
|
+
8. On normalized paid webhook, mark request paid and run fulfillment once.
|
|
46
|
+
9. On failed/expired, mark recoverable state and allow retry if business policy permits.
|
|
47
|
+
|
|
48
|
+
## Payment link acceptance criteria
|
|
49
|
+
|
|
50
|
+
For a simple payment link request, deliver a usable vertical slice:
|
|
51
|
+
|
|
52
|
+
- admin/seller can create or configure a product/payment link
|
|
53
|
+
- public customer page shows product name, amount, stock/availability, and allowed rails
|
|
54
|
+
- customer can start payment server-side
|
|
55
|
+
- provider identifiers and instructions are persisted
|
|
56
|
+
- status page or polling reflects pending/paid/failed/expired
|
|
57
|
+
- webhook paid event marks the payment paid and fulfills exactly once
|
|
58
|
+
- stock/capacity is decremented only after paid, or reservation is released on expiry
|
|
59
|
+
- duplicate webhook is harmless
|
|
60
|
+
|
|
61
|
+
## Fulfillment guard
|
|
62
|
+
|
|
63
|
+
Fulfillment must be protected by a one-time guard:
|
|
64
|
+
|
|
65
|
+
```text
|
|
66
|
+
if payment_request.status == paid and fulfillment_action.status != completed:
|
|
67
|
+
perform domain side effect
|
|
68
|
+
mark fulfillment completed
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
This prevents duplicate webhooks from shipping, delivering, or counting the same sale twice.
|
|
72
|
+
|
|
73
|
+
## Inventory
|
|
74
|
+
|
|
75
|
+
For normal products:
|
|
76
|
+
|
|
77
|
+
- validate stock before payment start
|
|
78
|
+
- reserve if needed
|
|
79
|
+
- decrement or mark sold only after paid
|
|
80
|
+
- release reservation on expiration
|
|
81
|
+
|
|
82
|
+
For event tickets or invite inventory, use **ekz-ticket-invite-selling**.
|
|
83
|
+
|
|
84
|
+
## Tests
|
|
85
|
+
|
|
86
|
+
- client cannot alter amount
|
|
87
|
+
- paid event fulfills exactly once
|
|
88
|
+
- duplicate paid event is ignored safely
|
|
89
|
+
- failed/expired event does not fulfill
|
|
90
|
+
- sold-out object cannot start a new payment
|
|
91
|
+
- retry does not create a duplicate order
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ekz-overage-billing
|
|
3
|
+
description: >-
|
|
4
|
+
Overage billing architecture: metered usage records, usage ledgers,
|
|
5
|
+
aggregation windows, thresholds, end-of-cycle charges, settlement, and
|
|
6
|
+
one-time payment requests for usage beyond allowance.
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Overage Billing
|
|
10
|
+
|
|
11
|
+
Use this skill when charging for usage beyond a plan allowance, quota, prepaid balance, or threshold.
|
|
12
|
+
|
|
13
|
+
## Pattern
|
|
14
|
+
|
|
15
|
+
```text
|
|
16
|
+
UsageRecord -> UsageLedger -> OverageCharge/Invoice
|
|
17
|
+
-> PaymentRequest -> PaymentAttempt
|
|
18
|
+
-> paid webhook -> settle usage balance
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Concepts
|
|
22
|
+
|
|
23
|
+
- Meter: what is measured, such as seats, messages, API calls, storage, minutes, or transactions.
|
|
24
|
+
- UsageRecord: idempotent raw usage event.
|
|
25
|
+
- UsageLedger: append-only rollup/audit of usage and charges.
|
|
26
|
+
- Allowance: included quota from plan or entitlement.
|
|
27
|
+
- Aggregation window: billing period or threshold window.
|
|
28
|
+
- Overage charge: amount due for usage above allowance.
|
|
29
|
+
- Settlement: mark the billed usage window paid.
|
|
30
|
+
|
|
31
|
+
## Charging models
|
|
32
|
+
|
|
33
|
+
Choose one policy explicitly:
|
|
34
|
+
|
|
35
|
+
- End-of-cycle postpaid: aggregate usage, charge at cycle close.
|
|
36
|
+
- Threshold billing: charge when usage exceeds a monetary or unit threshold.
|
|
37
|
+
- Prepaid drawdown: deduct from balance and request top-up when low.
|
|
38
|
+
- Hybrid subscription + overage: subscription grants allowance, overage charges excess.
|
|
39
|
+
|
|
40
|
+
## Flow
|
|
41
|
+
|
|
42
|
+
1. Ingest usage with an idempotency key.
|
|
43
|
+
2. Store usage records append-only.
|
|
44
|
+
3. Aggregate by account, meter, and billing window.
|
|
45
|
+
4. Subtract included allowance or credits.
|
|
46
|
+
5. Create an overage invoice or charge when policy says due.
|
|
47
|
+
6. Create a `PaymentRequest` for the charge.
|
|
48
|
+
7. On normalized paid webhook, mark the overage window settled.
|
|
49
|
+
8. On failed/expired, apply retry, grace, throttling, or suspension policy.
|
|
50
|
+
|
|
51
|
+
## Invariants
|
|
52
|
+
|
|
53
|
+
- Usage ingestion must be idempotent.
|
|
54
|
+
- Usage records should not be mutated to hide history.
|
|
55
|
+
- Amount is computed server-side from metered usage and pricing rules.
|
|
56
|
+
- A settled usage window cannot be charged twice.
|
|
57
|
+
- Overage payment failure should affect only the policy-defined entitlement or quota.
|
|
58
|
+
- Subscription renewal and overage settlement are related but separate lifecycle events.
|
|
59
|
+
|
|
60
|
+
## Tests
|
|
61
|
+
|
|
62
|
+
- duplicate usage event does not double count
|
|
63
|
+
- usage below allowance creates no charge
|
|
64
|
+
- usage above allowance creates the expected charge
|
|
65
|
+
- duplicate paid webhook does not settle twice
|
|
66
|
+
- failed overage charge leaves the balance collectible
|
|
67
|
+
- end-of-cycle aggregation uses the correct window boundaries
|
|
68
|
+
|