@krizaka/credits 0.1.4 → 0.1.5

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 (2) hide show
  1. package/README.md +25 -7
  2. package/package.json +3 -3
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @krizaka/credits
2
2
 
3
- **One credits ledger for every Krizaka product.** It replaces three models: Yomizaka's `audit_credits` and `free_audit_used_at` columns on `accounts` (`reserveAudit` / `releaseAudit` / `refundFailedAudit`), and the Java kit's `credit_wallet` / `credit_ledger_entry` / `credit_hold` (`krizaka-billing`, whose names and events it follows). The coupons module (#46) grants capped credits through it.
3
+ **One credits ledger for every Krizaka product.** It replaces three models: Verikora's `audit_credits` and `free_audit_used_at` columns on `accounts` (`reserveAudit` / `releaseAudit` / `refundFailedAudit`), and the Java kit's `credit_wallet` / `credit_ledger_entry` / `credit_hold` (`krizaka-billing`, whose names and events it follows). The coupons module (#46) grants capped credits through it.
4
4
 
5
5
  > **The ledger is append-only and the balance is derived.** Nothing stores a balance that could drift: what is left of a pot is the sum of its lines. A database trigger refuses any `UPDATE` or `DELETE` of a line. Credits are integers, never floats.
6
6
 
@@ -15,13 +15,31 @@ const credits = createCredits({ clock, settings: () => readCreditsSettings((key)
15
15
 
16
16
  await transaction(connect, async (tx) => {
17
17
  // a coupon's free credits: 20 audits, at most 5 a day, then stop; they lapse in 30 days
18
- await credits.grant(tx, { accountId, amount: 20, capPerDay: 5, expiresAt, onExhausted: "stop", source: `coupon:${redemptionId}`, idempotencyKey: `redeem-${redemptionId}` });
18
+ await credits.grant(tx, {
19
+ accountId,
20
+ amount: 20,
21
+ capPerDay: 5,
22
+ expiresAt,
23
+ onExhausted: "stop",
24
+ source: `coupon:${redemptionId}`,
25
+ idempotencyKey: `redeem-${redemptionId}`,
26
+ });
19
27
  // credits bought (the payment webhook, in the event's transaction)
20
- await credits.purchase(tx, { accountId, amount: 10, source: `purchase:${paymentRef}`, idempotencyKey: `pay-${paymentRef}` });
28
+ await credits.purchase(tx, {
29
+ accountId,
30
+ amount: 10,
31
+ source: `purchase:${paymentRef}`,
32
+ idempotencyKey: `pay-${paymentRef}`,
33
+ });
21
34
  });
22
35
 
23
36
  // spending: it refuses with a typed code, writes nothing when it refuses, and a replay of the key spends nothing more
24
- const { draws } = await credits.consume(tx, { accountId, amount: 1, reason: `audit:${auditId}`, idempotencyKey: `audit-${auditId}` });
37
+ const { draws } = await credits.consume(tx, {
38
+ accountId,
39
+ amount: 1,
40
+ reason: `audit:${auditId}`,
41
+ idempotencyKey: `audit-${auditId}`,
42
+ });
25
43
  await credits.refund(tx, { accountId, consumed: `audit-${auditId}`, reason: "the run failed" }); // back to the same pots, once
26
44
  ```
27
45
 
@@ -33,7 +51,7 @@ await credits.refund(tx, { accountId, consumed: `audit-${auditId}`, reason: "the
33
51
  - a `stop` pot that is empty: `CREDITS_EXHAUSTED`, even if purchased credits exist;
34
52
  - a `fallback_paid` pot that is empty: it moves on to purchased credits, then answers `PAYWALL`;
35
53
  - no credits at all: `PAYWALL`.
36
- Refusals are `CreditsError`s with a `code` and the HTTP `status` a route may answer (402 / 429).
54
+ Refusals are `CreditsError`s with a `code` and the HTTP `status` a route may answer (402 / 429).
37
55
  - **Holds.** `hold` keeps credits back for work in progress (it counts against the caps and the balance, and expires after `credits.holdTtlSeconds`); `settle(holdId, { amount? })` spends what the work cost (at most the hold) and frees the rest; `release` frees it all. Once each; a replay answers the first result.
38
56
  - **`refund`** gives back what a consume (or a settled hold) spent, to the same pots, once. **`expire(grantId)`** writes off what is left of a pot (a revoked coupon: credits already spent are never taken back); **`expireDue`** sweeps the lapsed pots in a job. **`adjust`** is an administrator's signed correction, never below zero.
39
57
  - **`balance`** is `{ total, granted, purchased, held, available, grants[] }`, derived; **`history`** pages the lines newest first.
@@ -45,6 +63,6 @@ await credits.refund(tx, { accountId, consumed: `audit-${auditId}`, reason: "the
45
63
  - **Tables** (`creditsSchema`, Postgres schema `credits`): `grant`, `entry`, `hold`. The one product table it references is the owner (`owner`, the account table). An account that has credits is **anonymised, never deleted**: it is an accounting record, like an invoice.
46
64
  - `creditsLedgerContract` (`@krizaka/credits/testkit`) is the contract every store runs; `memoryCreditStore()` is for unit tests (it has no transaction, so `lock` does nothing: concurrency is proved on PostgreSQL).
47
65
 
48
- ## Adopting it (Yomizaka)
66
+ ## Adopting it (Verikora)
49
67
 
50
- `audit_credits` becomes a purchased pot (`purchase` once per account for the balance it has, key `import-<accountId>`), the free audit a granted pot with `onExhausted: "fallback_paid"`, `reserveAudit` a `consume` (a `PAYWALL` is already Yomizaka's 402), `releaseAudit` and `refundFailedAudit` a `refund` of the consume key.
68
+ `audit_credits` becomes a purchased pot (`purchase` once per account for the balance it has, key `import-<accountId>`), the free audit a granted pot with `onExhausted: "fallback_paid"`, `reserveAudit` a `consume` (a `PAYWALL` is already Verikora's 402), `releaseAudit` and `refundFailedAudit` a `refund` of the consume key.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@krizaka/credits",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "One credits ledger for every Krizaka product: grants that expire with daily and monthly caps, purchases, debits, refunds, holds. Append-only; the balance is derived. Concurrent consumption never goes negative and never spends twice. No database client, no wall clock.",
5
5
  "keywords": [
6
6
  "krizaka",
@@ -56,13 +56,13 @@
56
56
  "typescript": "^5.9.3",
57
57
  "vitest": "^4.1.11",
58
58
  "zod": "^4.6.7",
59
- "@krizaka/config": "^0.20.0"
59
+ "@krizaka/config": "^0.21.0"
60
60
  },
61
61
  "dependencies": {
62
62
  "@krizaka/intl": "^0.4.1",
63
63
  "@krizaka/db": "^0.3.1",
64
64
  "@krizaka/time": "^0.2.0",
65
- "@krizaka/settings": "^0.6.0"
65
+ "@krizaka/settings": "^0.7.0"
66
66
  },
67
67
  "peerDependencies": {
68
68
  "vitest": ">=4",