@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.
Files changed (113) hide show
  1. package/README.md +148 -0
  2. package/dist/commands/agent.d.ts +31 -0
  3. package/dist/commands/agent.d.ts.map +1 -0
  4. package/dist/commands/agent.js +55 -0
  5. package/dist/commands/ask.d.ts +20 -0
  6. package/dist/commands/ask.d.ts.map +1 -0
  7. package/dist/commands/ask.js +154 -0
  8. package/dist/commands/doctor.d.ts +3 -0
  9. package/dist/commands/doctor.d.ts.map +1 -0
  10. package/dist/commands/doctor.js +44 -0
  11. package/dist/commands/health.d.ts +2 -0
  12. package/dist/commands/health.d.ts.map +1 -0
  13. package/dist/commands/health.js +28 -0
  14. package/dist/commands/local-agent.d.ts +19 -0
  15. package/dist/commands/local-agent.d.ts.map +1 -0
  16. package/dist/commands/local-agent.js +450 -0
  17. package/dist/commands/scan.d.ts +11 -0
  18. package/dist/commands/scan.d.ts.map +1 -0
  19. package/dist/commands/scan.js +119 -0
  20. package/dist/commands/webhook.d.ts +10 -0
  21. package/dist/commands/webhook.d.ts.map +1 -0
  22. package/dist/commands/webhook.js +42 -0
  23. package/dist/index.d.ts +3 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +185 -0
  26. package/dist/lib/banner.d.ts +10 -0
  27. package/dist/lib/banner.d.ts.map +1 -0
  28. package/dist/lib/banner.js +26 -0
  29. package/dist/lib/commands-i18n.d.ts +20 -0
  30. package/dist/lib/commands-i18n.d.ts.map +1 -0
  31. package/dist/lib/commands-i18n.js +157 -0
  32. package/dist/lib/composer-model.d.ts +10 -0
  33. package/dist/lib/composer-model.d.ts.map +1 -0
  34. package/dist/lib/composer-model.js +15 -0
  35. package/dist/lib/context.d.ts +12 -0
  36. package/dist/lib/context.d.ts.map +1 -0
  37. package/dist/lib/context.js +56 -0
  38. package/dist/lib/doctor-quiet.d.ts +11 -0
  39. package/dist/lib/doctor-quiet.d.ts.map +1 -0
  40. package/dist/lib/doctor-quiet.js +39 -0
  41. package/dist/lib/env.d.ts +18 -0
  42. package/dist/lib/env.d.ts.map +1 -0
  43. package/dist/lib/env.js +66 -0
  44. package/dist/lib/help.d.ts +10 -0
  45. package/dist/lib/help.d.ts.map +1 -0
  46. package/dist/lib/help.js +140 -0
  47. package/dist/lib/locale.d.ts +38 -0
  48. package/dist/lib/locale.d.ts.map +1 -0
  49. package/dist/lib/locale.js +189 -0
  50. package/dist/lib/mode.d.ts +11 -0
  51. package/dist/lib/mode.d.ts.map +1 -0
  52. package/dist/lib/mode.js +29 -0
  53. package/dist/lib/output.d.ts +7 -0
  54. package/dist/lib/output.d.ts.map +1 -0
  55. package/dist/lib/output.js +18 -0
  56. package/dist/lib/preferences.d.ts +9 -0
  57. package/dist/lib/preferences.d.ts.map +1 -0
  58. package/dist/lib/preferences.js +35 -0
  59. package/dist/lib/redact.d.ts +3 -0
  60. package/dist/lib/redact.d.ts.map +1 -0
  61. package/dist/lib/redact.js +32 -0
  62. package/dist/lib/scan-quiet.d.ts +4 -0
  63. package/dist/lib/scan-quiet.d.ts.map +1 -0
  64. package/dist/lib/scan-quiet.js +4 -0
  65. package/dist/lib/scope.d.ts +5 -0
  66. package/dist/lib/scope.d.ts.map +1 -0
  67. package/dist/lib/scope.js +61 -0
  68. package/dist/lib/session.d.ts +31 -0
  69. package/dist/lib/session.d.ts.map +1 -0
  70. package/dist/lib/session.js +101 -0
  71. package/dist/lib/shell.d.ts +18 -0
  72. package/dist/lib/shell.d.ts.map +1 -0
  73. package/dist/lib/shell.js +214 -0
  74. package/dist/lib/skill.d.ts +3 -0
  75. package/dist/lib/skill.d.ts.map +1 -0
  76. package/dist/lib/skill.js +2 -0
  77. package/dist/lib/skills.d.ts +16 -0
  78. package/dist/lib/skills.d.ts.map +1 -0
  79. package/dist/lib/skills.js +199 -0
  80. package/dist/lib/theme.d.ts +23 -0
  81. package/dist/lib/theme.d.ts.map +1 -0
  82. package/dist/lib/theme.js +40 -0
  83. package/dist/lib/ui/ascii-art.d.ts +10 -0
  84. package/dist/lib/ui/ascii-art.d.ts.map +1 -0
  85. package/dist/lib/ui/ascii-art.js +55 -0
  86. package/dist/lib/ui/layout.d.ts +19 -0
  87. package/dist/lib/ui/layout.d.ts.map +1 -0
  88. package/dist/lib/ui/layout.js +46 -0
  89. package/dist/lib/ui/logo.d.ts +3 -0
  90. package/dist/lib/ui/logo.d.ts.map +1 -0
  91. package/dist/lib/ui/logo.js +8 -0
  92. package/dist/lib/ui/prompt.d.ts +6 -0
  93. package/dist/lib/ui/prompt.d.ts.map +1 -0
  94. package/dist/lib/ui/prompt.js +75 -0
  95. package/dist/lib/ui/splash.d.ts +15 -0
  96. package/dist/lib/ui/splash.d.ts.map +1 -0
  97. package/dist/lib/ui/splash.js +121 -0
  98. package/package.json +48 -0
  99. package/skills/ekz-connect/SKILL.md +99 -0
  100. package/skills/ekz-data-layer-design/SKILL.md +199 -0
  101. package/skills/ekz-data-mongo/SKILL.md +341 -0
  102. package/skills/ekz-data-mysql/SKILL.md +245 -0
  103. package/skills/ekz-data-postgres/SKILL.md +257 -0
  104. package/skills/ekz-data-sqlite/SKILL.md +261 -0
  105. package/skills/ekz-ekwanza-provider-adapter/SKILL.md +91 -0
  106. package/skills/ekz-integration-playbook/SKILL.md +122 -0
  107. package/skills/ekz-one-time-product-payments/SKILL.md +91 -0
  108. package/skills/ekz-overage-billing/SKILL.md +68 -0
  109. package/skills/ekz-payment-core-architecture/SKILL.md +121 -0
  110. package/skills/ekz-sdk-cli/SKILL.md +82 -0
  111. package/skills/ekz-subscription-billing/SKILL.md +120 -0
  112. package/skills/ekz-ticket-invite-selling/SKILL.md +64 -0
  113. 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
+