qualia-framework 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (261) hide show
  1. package/README.md +50 -0
  2. package/bin/cli.js +519 -0
  3. package/framework/agents/architecture-strategist.md +53 -0
  4. package/framework/agents/backend-agent.md +150 -0
  5. package/framework/agents/code-simplicity-reviewer.md +86 -0
  6. package/framework/agents/frontend-agent.md +111 -0
  7. package/framework/agents/kieran-typescript-reviewer.md +96 -0
  8. package/framework/agents/performance-oracle.md +111 -0
  9. package/framework/agents/qualia-codebase-mapper.md +760 -0
  10. package/framework/agents/qualia-debugger.md +1203 -0
  11. package/framework/agents/qualia-executor.md +881 -0
  12. package/framework/agents/qualia-integration-checker.md +423 -0
  13. package/framework/agents/qualia-phase-researcher.md +453 -0
  14. package/framework/agents/qualia-plan-checker.md +699 -0
  15. package/framework/agents/qualia-planner.md +1241 -0
  16. package/framework/agents/qualia-project-researcher.md +602 -0
  17. package/framework/agents/qualia-research-synthesizer.md +236 -0
  18. package/framework/agents/qualia-roadmapper.md +605 -0
  19. package/framework/agents/qualia-verifier.md +685 -0
  20. package/framework/agents/team-orchestrator.md +228 -0
  21. package/framework/agents/teams/full-stack-team.md +48 -0
  22. package/framework/agents/teams/optimize-team.md +53 -0
  23. package/framework/agents/teams/review-team.md +62 -0
  24. package/framework/agents/teams/ship-team.md +86 -0
  25. package/framework/agents/test-agent.md +182 -0
  26. package/framework/askpass.sh +2 -0
  27. package/framework/commands/design.md +53 -0
  28. package/framework/commands/quick-db.md +22 -0
  29. package/framework/config/retention.json +35 -0
  30. package/framework/core/PRINCIPLES.md +77 -0
  31. package/framework/hooks/auto-format.sh +45 -0
  32. package/framework/hooks/block-env-edit.sh +42 -0
  33. package/framework/hooks/branch-guard.sh +46 -0
  34. package/framework/hooks/confirm-delete.sh +56 -0
  35. package/framework/hooks/migration-validate.sh +68 -0
  36. package/framework/hooks/notification-speak.sh +15 -0
  37. package/framework/hooks/pre-commit.sh +80 -0
  38. package/framework/hooks/pre-compact.sh +55 -0
  39. package/framework/hooks/pre-deploy-gate.sh +151 -0
  40. package/framework/hooks/qualia-colors.sh +32 -0
  41. package/framework/hooks/retention-cleanup.sh +43 -0
  42. package/framework/hooks/save-session-state.sh +153 -0
  43. package/framework/hooks/session-context-loader.sh +28 -0
  44. package/framework/hooks/session-learn.sh +30 -0
  45. package/framework/knowledge/claudecode-bible.md +1384 -0
  46. package/framework/knowledge/client-prefs.md +22 -0
  47. package/framework/knowledge/common-fixes.md +25 -0
  48. package/framework/knowledge/deployment-map.md +35 -0
  49. package/framework/knowledge/email-signature.html +1 -0
  50. package/framework/knowledge/employees.md +8 -0
  51. package/framework/knowledge/learned-patterns.md +51 -0
  52. package/framework/knowledge/optimization-research-2026.md +137 -0
  53. package/framework/knowledge/qualia-context.md +67 -0
  54. package/framework/knowledge/supabase-patterns.md +50 -0
  55. package/framework/knowledge/voice-agent-patterns.md +46 -0
  56. package/framework/qualia-engine/VERSION +1 -0
  57. package/framework/qualia-engine/bin/qualia-tools.js +2160 -0
  58. package/framework/qualia-engine/bin/qualia-tools.test.js +1054 -0
  59. package/framework/qualia-engine/references/checkpoints.md +775 -0
  60. package/framework/qualia-engine/references/continuation-format.md +249 -0
  61. package/framework/qualia-engine/references/decimal-phase-calculation.md +65 -0
  62. package/framework/qualia-engine/references/design-quality.md +56 -0
  63. package/framework/qualia-engine/references/git-integration.md +254 -0
  64. package/framework/qualia-engine/references/git-planning-commit.md +50 -0
  65. package/framework/qualia-engine/references/model-profile-resolution.md +32 -0
  66. package/framework/qualia-engine/references/model-profiles.md +73 -0
  67. package/framework/qualia-engine/references/phase-argument-parsing.md +61 -0
  68. package/framework/qualia-engine/references/planning-config.md +195 -0
  69. package/framework/qualia-engine/references/questioning.md +141 -0
  70. package/framework/qualia-engine/references/tdd.md +263 -0
  71. package/framework/qualia-engine/references/ui-brand.md +160 -0
  72. package/framework/qualia-engine/references/verification-patterns.md +612 -0
  73. package/framework/qualia-engine/templates/DEBUG.md +159 -0
  74. package/framework/qualia-engine/templates/DESIGN.md +81 -0
  75. package/framework/qualia-engine/templates/UAT.md +247 -0
  76. package/framework/qualia-engine/templates/codebase/architecture.md +255 -0
  77. package/framework/qualia-engine/templates/codebase/concerns.md +310 -0
  78. package/framework/qualia-engine/templates/codebase/conventions.md +307 -0
  79. package/framework/qualia-engine/templates/codebase/integrations.md +280 -0
  80. package/framework/qualia-engine/templates/codebase/stack.md +186 -0
  81. package/framework/qualia-engine/templates/codebase/structure.md +285 -0
  82. package/framework/qualia-engine/templates/codebase/testing.md +480 -0
  83. package/framework/qualia-engine/templates/config.json +35 -0
  84. package/framework/qualia-engine/templates/context.md +283 -0
  85. package/framework/qualia-engine/templates/continue-here.md +78 -0
  86. package/framework/qualia-engine/templates/debug-subagent-prompt.md +91 -0
  87. package/framework/qualia-engine/templates/discovery.md +146 -0
  88. package/framework/qualia-engine/templates/milestone-archive.md +123 -0
  89. package/framework/qualia-engine/templates/milestone.md +115 -0
  90. package/framework/qualia-engine/templates/phase-prompt.md +567 -0
  91. package/framework/qualia-engine/templates/planner-subagent-prompt.md +117 -0
  92. package/framework/qualia-engine/templates/project.md +184 -0
  93. package/framework/qualia-engine/templates/projects/ai-agent.md +156 -0
  94. package/framework/qualia-engine/templates/projects/mobile-app.md +181 -0
  95. package/framework/qualia-engine/templates/projects/voice-agent.md +134 -0
  96. package/framework/qualia-engine/templates/projects/website.md +137 -0
  97. package/framework/qualia-engine/templates/requirements.md +231 -0
  98. package/framework/qualia-engine/templates/research-project/ARCHITECTURE.md +204 -0
  99. package/framework/qualia-engine/templates/research-project/FEATURES.md +147 -0
  100. package/framework/qualia-engine/templates/research-project/PITFALLS.md +200 -0
  101. package/framework/qualia-engine/templates/research-project/STACK.md +120 -0
  102. package/framework/qualia-engine/templates/research-project/SUMMARY.md +170 -0
  103. package/framework/qualia-engine/templates/research.md +552 -0
  104. package/framework/qualia-engine/templates/roadmap.md +202 -0
  105. package/framework/qualia-engine/templates/state.md +176 -0
  106. package/framework/qualia-engine/templates/summary-complex.md +59 -0
  107. package/framework/qualia-engine/templates/summary-minimal.md +41 -0
  108. package/framework/qualia-engine/templates/summary-standard.md +48 -0
  109. package/framework/qualia-engine/templates/summary.md +246 -0
  110. package/framework/qualia-engine/templates/user-setup.md +311 -0
  111. package/framework/qualia-engine/templates/verification-report.md +322 -0
  112. package/framework/qualia-engine/workflows/add-phase.md +179 -0
  113. package/framework/qualia-engine/workflows/add-todo.md +157 -0
  114. package/framework/qualia-engine/workflows/audit-milestone.md +241 -0
  115. package/framework/qualia-engine/workflows/check-todos.md +176 -0
  116. package/framework/qualia-engine/workflows/complete-milestone.md +858 -0
  117. package/framework/qualia-engine/workflows/diagnose-issues.md +219 -0
  118. package/framework/qualia-engine/workflows/discovery-phase.md +289 -0
  119. package/framework/qualia-engine/workflows/discuss-phase.md +534 -0
  120. package/framework/qualia-engine/workflows/execute-phase.md +559 -0
  121. package/framework/qualia-engine/workflows/execute-plan.md +438 -0
  122. package/framework/qualia-engine/workflows/help.md +470 -0
  123. package/framework/qualia-engine/workflows/insert-phase.md +220 -0
  124. package/framework/qualia-engine/workflows/list-phase-assumptions.md +178 -0
  125. package/framework/qualia-engine/workflows/map-codebase.md +327 -0
  126. package/framework/qualia-engine/workflows/new-milestone.md +363 -0
  127. package/framework/qualia-engine/workflows/new-project.md +1037 -0
  128. package/framework/qualia-engine/workflows/pause-work.md +122 -0
  129. package/framework/qualia-engine/workflows/plan-milestone-gaps.md +256 -0
  130. package/framework/qualia-engine/workflows/plan-phase.md +422 -0
  131. package/framework/qualia-engine/workflows/progress.md +354 -0
  132. package/framework/qualia-engine/workflows/quick.md +252 -0
  133. package/framework/qualia-engine/workflows/remove-phase.md +326 -0
  134. package/framework/qualia-engine/workflows/research-phase.md +74 -0
  135. package/framework/qualia-engine/workflows/resume-project.md +306 -0
  136. package/framework/qualia-engine/workflows/set-profile.md +80 -0
  137. package/framework/qualia-engine/workflows/settings.md +145 -0
  138. package/framework/qualia-engine/workflows/transition.md +556 -0
  139. package/framework/qualia-engine/workflows/update.md +197 -0
  140. package/framework/qualia-engine/workflows/verify-phase.md +195 -0
  141. package/framework/qualia-engine/workflows/verify-work.md +625 -0
  142. package/framework/rules/context7.md +11 -0
  143. package/framework/rules/deployment.md +29 -0
  144. package/framework/rules/frontend.md +33 -0
  145. package/framework/rules/security.md +12 -0
  146. package/framework/rules/speed.md +20 -0
  147. package/framework/scripts/__pycache__/say.cpython-314.pyc +0 -0
  148. package/framework/scripts/apply-retention.sh +120 -0
  149. package/framework/scripts/bootstrap-pop-os.sh +354 -0
  150. package/framework/scripts/claude-voice +13 -0
  151. package/framework/scripts/cleanup.sh +131 -0
  152. package/framework/scripts/cowork-mode.sh +141 -0
  153. package/framework/scripts/generate-project-claude-md.sh +153 -0
  154. package/framework/scripts/load-test-webhook.js +172 -0
  155. package/framework/scripts/say.py +236 -0
  156. package/framework/scripts/showcase-video-recorder/ffmpeg-builder.js +167 -0
  157. package/framework/scripts/showcase-video-recorder/playwright-helpers.js +216 -0
  158. package/framework/scripts/speak.py +55 -0
  159. package/framework/scripts/speak.sh +18 -0
  160. package/framework/scripts/status.sh +138 -0
  161. package/framework/scripts/sync-to-framework.sh +65 -0
  162. package/framework/scripts/voice-hotkey.py +227 -0
  163. package/framework/scripts/voice-input.sh +51 -0
  164. package/framework/skills/animate/SKILL.md +202 -0
  165. package/framework/skills/bolder/SKILL.md +144 -0
  166. package/framework/skills/browser-qa/SKILL.md +536 -0
  167. package/framework/skills/clarify/SKILL.md +179 -0
  168. package/framework/skills/colorize/SKILL.md +170 -0
  169. package/framework/skills/critique/SKILL.md +126 -0
  170. package/framework/skills/deep-research/SKILL.md +271 -0
  171. package/framework/skills/delight/SKILL.md +329 -0
  172. package/framework/skills/deploy/SKILL.md +261 -0
  173. package/framework/skills/deploy-verify/SKILL.md +377 -0
  174. package/framework/skills/deploy-verify/scripts/canary-check.sh +206 -0
  175. package/framework/skills/deploy-verify/scripts/check-console-errors.js +147 -0
  176. package/framework/skills/deploy-verify/scripts/check-cwv.js +139 -0
  177. package/framework/skills/deploy-verify/scripts/project-detect.sh +84 -0
  178. package/framework/skills/deploy-verify/scripts/verify.sh +548 -0
  179. package/framework/skills/design-quieter/SKILL.md +130 -0
  180. package/framework/skills/distill/SKILL.md +149 -0
  181. package/framework/skills/docs-lookup/SKILL.md +78 -0
  182. package/framework/skills/fcm-notifications/SKILL.md +125 -0
  183. package/framework/skills/financial-ledger/SKILL.md +1039 -0
  184. package/framework/skills/frontend-master/NOTICE.md +4 -0
  185. package/framework/skills/frontend-master/SKILL.md +127 -0
  186. package/framework/skills/frontend-master/reference/color-and-contrast.md +132 -0
  187. package/framework/skills/frontend-master/reference/interaction-design.md +123 -0
  188. package/framework/skills/frontend-master/reference/motion-design.md +99 -0
  189. package/framework/skills/frontend-master/reference/responsive-design.md +114 -0
  190. package/framework/skills/frontend-master/reference/spatial-design.md +100 -0
  191. package/framework/skills/frontend-master/reference/typography.md +131 -0
  192. package/framework/skills/frontend-master/reference/ux-writing.md +107 -0
  193. package/framework/skills/harden/SKILL.md +357 -0
  194. package/framework/skills/i18n-rtl/SKILL.md +752 -0
  195. package/framework/skills/learn/SKILL.md +71 -0
  196. package/framework/skills/memory/SKILL.md +50 -0
  197. package/framework/skills/mobile-expo/SKILL.md +864 -0
  198. package/framework/skills/mobile-expo/references/store-checklist.md +550 -0
  199. package/framework/skills/nestjs-backend/README.md +73 -0
  200. package/framework/skills/nestjs-backend/SKILL.md +446 -0
  201. package/framework/skills/nestjs-backend/references/templates.md +1173 -0
  202. package/framework/skills/normalize/SKILL.md +79 -0
  203. package/framework/skills/onboard/SKILL.md +242 -0
  204. package/framework/skills/polish/SKILL.md +209 -0
  205. package/framework/skills/pr/SKILL.md +66 -0
  206. package/framework/skills/qualia/SKILL.md +153 -0
  207. package/framework/skills/qualia-add-todo/SKILL.md +68 -0
  208. package/framework/skills/qualia-audit-milestone/SKILL.md +92 -0
  209. package/framework/skills/qualia-check-todos/SKILL.md +55 -0
  210. package/framework/skills/qualia-complete-milestone/SKILL.md +108 -0
  211. package/framework/skills/qualia-debug/SKILL.md +149 -0
  212. package/framework/skills/qualia-design/SKILL.md +203 -0
  213. package/framework/skills/qualia-discuss-phase/SKILL.md +72 -0
  214. package/framework/skills/qualia-execute-phase/SKILL.md +86 -0
  215. package/framework/skills/qualia-help/SKILL.md +67 -0
  216. package/framework/skills/qualia-idk/SKILL.md +352 -0
  217. package/framework/skills/qualia-list-phase-assumptions/SKILL.md +67 -0
  218. package/framework/skills/qualia-new-milestone/SKILL.md +72 -0
  219. package/framework/skills/qualia-new-project/SKILL.md +92 -0
  220. package/framework/skills/qualia-optimize/SKILL.md +417 -0
  221. package/framework/skills/qualia-pause-work/SKILL.md +96 -0
  222. package/framework/skills/qualia-plan-milestone-gaps/SKILL.md +57 -0
  223. package/framework/skills/qualia-plan-phase/SKILL.md +101 -0
  224. package/framework/skills/qualia-progress/SKILL.md +53 -0
  225. package/framework/skills/qualia-quick/SKILL.md +89 -0
  226. package/framework/skills/qualia-research-phase/SKILL.md +88 -0
  227. package/framework/skills/qualia-resume-work/SKILL.md +62 -0
  228. package/framework/skills/qualia-review/SKILL.md +263 -0
  229. package/framework/skills/qualia-start/SKILL.md +182 -0
  230. package/framework/skills/qualia-verify-work/SKILL.md +105 -0
  231. package/framework/skills/qualia-workflow/SKILL.md +130 -0
  232. package/framework/skills/rag/SKILL.md +750 -0
  233. package/framework/skills/responsive/SKILL.md +231 -0
  234. package/framework/skills/retro/SKILL.md +284 -0
  235. package/framework/skills/sakani-conventions/SKILL.md +136 -0
  236. package/framework/skills/sakani-conventions/evals/evals.json +23 -0
  237. package/framework/skills/sakani-conventions/references/entities.md +365 -0
  238. package/framework/skills/sakani-conventions/references/error-codes.md +95 -0
  239. package/framework/skills/seo-master/SKILL.md +490 -0
  240. package/framework/skills/seo-master/references/checklist.md +199 -0
  241. package/framework/skills/seo-master/references/structured-data.md +609 -0
  242. package/framework/skills/ship/SKILL.md +202 -0
  243. package/framework/skills/stack-researcher/SKILL.md +215 -0
  244. package/framework/skills/status/SKILL.md +154 -0
  245. package/framework/skills/status/scripts/health-check.sh +562 -0
  246. package/framework/skills/subscription-payments/SKILL.md +250 -0
  247. package/framework/skills/supabase/SKILL.md +973 -0
  248. package/framework/skills/supabase/references/templates.md +159 -0
  249. package/framework/skills/team/SKILL.md +67 -0
  250. package/framework/skills/test-runner/SKILL.md +202 -0
  251. package/framework/skills/voice-agent/SKILL.md +407 -0
  252. package/framework/skills/zoho-workflow/SKILL.md +51 -0
  253. package/framework/statusline-command.sh +117 -0
  254. package/package.json +24 -0
  255. package/profiles/fawzi.json +16 -0
  256. package/profiles/hasan.json +16 -0
  257. package/profiles/moayad.json +16 -0
  258. package/templates/CLAUDE-owner.md +52 -0
  259. package/templates/CLAUDE.md.hbs +58 -0
  260. package/templates/env.claude.template +12 -0
  261. package/templates/settings.json +141 -0
@@ -0,0 +1,1039 @@
1
+ ---
2
+ name: financial-ledger
3
+ description: "Financial ledger and accounting patterns — append-only ledgers, idempotent financial writes, FIFO credit settlement, auto-settlement jobs, statement generation, audit trails, and monetary value handling. Use whenever implementing financial tracking, payment recording, dues/billing systems, credit management, settlement logic, or any code that touches money. This is critical for correctness — financial code has zero tolerance for bugs. Triggers on: ledger, financial, dues, billing, settlement, credit, payment recording, monetary, accounting, audit trail, statement, invoice, FIFO, idempotency."
4
+ tags: [finance, ledger, accounting, payments]
5
+ ---
6
+
7
+ # Financial Ledger Patterns
8
+
9
+ This skill covers production-grade financial ledger and accounting patterns. The patterns here are specialized for property management but the core principles (append-only ledger, idempotency, FIFO settlement) are universally reusable across any fintech application.
10
+
11
+ ## Core Principle: Append-Only Ledger
12
+
13
+ The ledger is **never modified after creation**. No UPDATE, no DELETE. Period.
14
+
15
+ This is the foundational principle of financial accounting. Banks use it. Accounting standards mandate it. Your ledger must too.
16
+
17
+ - **Corrections use REVERSAL entries** that negate the original, creating a clear audit trail
18
+ - **Every entry has**:
19
+ - `timestamp` — when the entry was created
20
+ - `actor_user_id` — who created it
21
+ - `request_id` — idempotency key to prevent duplicates
22
+ - `status` — PENDING, COMPLETED, FAILED (for async operations)
23
+ - **Queries construct current state** by summing entries, never by querying a "current balance" field
24
+ - **Immutability is guaranteed** by database constraints and application logic
25
+
26
+ ### Why Append-Only?
27
+
28
+ 1. **Audit trail is automatic** — no need to reconstruct "what happened"
29
+ 2. **Regulatory compliance** — many jurisdictions require immutable financial records
30
+ 3. **Bug safety** — if you accidentally record the wrong amount, you can reverse it with full visibility
31
+ 4. **Debugging** — "why is the balance wrong?" becomes a simple query of ledger entries
32
+ 5. **Concurrency safety** — append operations are naturally conflict-free
33
+
34
+ ## Ledger Entry Types (Property Management Example)
35
+
36
+ Define entry types for your domain. This example uses property management, but adapt the types to your context:
37
+
38
+ | Type | Who Creates | Validation | Side Effects |
39
+ |------|-------------|-----------|---------------|
40
+ | `OPENING_BALANCE` | System/Admin | One per unit, only at account creation | Sets initial balance, immutable |
41
+ | `CREDIT_TOPUP` | Resident/Admin | Amount > 0, linked to payment method | Increases unit credit balance |
42
+ | `MANUAL_UNIT_INCOME` | Building Manager | Amount > 0, requires description | Income source recorded for audit |
43
+ | `EXPENSE` | Building Manager | Amount > 0, category, description | Building operating cost, splits across units |
44
+ | `SUBSCRIPTION_FEE` | System | Fixed amount, per building, monthly | Platform revenue, creates obligation |
45
+ | `AUTOSETTLEMENT_APPLIED` | Settlement job | System-generated, never manual | Links credit to obligations, triggers FIFO logic |
46
+ | `REVERSAL` | Admin (rare) | References original entry_id, same amount | Negates previous entry, visible in audit |
47
+
48
+ ### Entry Validation Rules
49
+
50
+ ```typescript
51
+ interface LedgerEntry {
52
+ id: string;
53
+ unit_id: string;
54
+ building_id: string;
55
+ type: 'OPENING_BALANCE' | 'CREDIT_TOPUP' | 'MANUAL_UNIT_INCOME' | 'EXPENSE' | 'SUBSCRIPTION_FEE' | 'AUTOSETTLEMENT_APPLIED' | 'REVERSAL';
56
+ amount: number; // Integer cents in production
57
+ currency: string; // ISO 4217 (e.g., 'JOD')
58
+ description: string;
59
+ actor_user_id: string;
60
+ created_at: string; // ISO timestamp
61
+ request_id: string; // Idempotency key (UUID v4)
62
+ status: 'PENDING' | 'COMPLETED' | 'FAILED';
63
+ metadata: Record<string, any>; // Type-specific details
64
+ }
65
+ ```
66
+
67
+ **Validation per type:**
68
+ - `OPENING_BALANCE`: Exactly one per unit in entire history
69
+ - `CREDIT_TOPUP`: amount > 0, currency matches building
70
+ - `MANUAL_UNIT_INCOME`: amount > 0, description required, BM authorization
71
+ - `EXPENSE`: amount > 0, category in valid list, affects shared obligation pool
72
+ - `SUBSCRIPTION_FEE`: amount matches subscription plan, deterministic billing date
73
+ - `AUTOSETTLEMENT_APPLIED`: system-generated only, amount <= available credit
74
+ - `REVERSAL`: references valid entry_id, same amount and currency, created within 30 days of original
75
+
76
+ ## Monetary Values
77
+
78
+ Never, ever, store money as a floating-point number. This is non-negotiable.
79
+
80
+ ```typescript
81
+ interface Money {
82
+ amount: number; // Integer cents in production (e.g., 1000 = $10.00 or 1000 fils)
83
+ currency: string; // ISO 4217 code (e.g., 'USD', 'JOD', 'EUR')
84
+ }
85
+
86
+ // DO NOT:
87
+ const balance = 100.50; // ❌ Floating point is imprecise
88
+ const balance = 100; // ❌ Missing currency
89
+
90
+ // DO:
91
+ const balance: Money = { amount: 10050, currency: 'USD' }; // ✓ Integer cents
92
+ const balance: Money = { amount: 100, currency: 'JOD' }; // ✓ Fils (smallest unit)
93
+ ```
94
+
95
+ ### Money Handling Rules
96
+
97
+ 1. **Store as integer cents** (or smallest currency unit)
98
+ - 1 USD = 100 cents
99
+ - 1 JOD = 1000 fils
100
+ - Always use the smallest unit to avoid floating-point errors
101
+
102
+ 2. **Always include currency alongside amount**
103
+ - You cannot compare JOD with USD without exchange rates
104
+ - Store and query together: `WHERE amount > 0 AND currency = 'JOD'`
105
+
106
+ 3. **Never compare money with `===`**
107
+ ```typescript
108
+ // ❌ Wrong:
109
+ if (balance === 10050) { }
110
+
111
+ // ✓ Correct:
112
+ function moneyEqual(a: Money, b: Money): boolean {
113
+ return a.amount === b.amount && a.currency === b.currency;
114
+ }
115
+ ```
116
+
117
+ 4. **Round only at display boundaries**
118
+ - All calculations use integers (cents/fils)
119
+ - Only format to decimals when printing or displaying to users
120
+ - Never round mid-calculation
121
+
122
+ 5. **Use Decimal library for complex calculations** (if needed)
123
+ ```typescript
124
+ import Decimal from 'decimal.js';
125
+
126
+ const tax = new Decimal('100').times('0.16'); // ✓ Precise
127
+ const cents = tax.toNumber(); // Convert to integer cents only at boundary
128
+ ```
129
+
130
+ 6. **Validate on input**
131
+ ```typescript
132
+ function validateMoney(m: Money): boolean {
133
+ // amount must be integer
134
+ if (!Number.isInteger(m.amount)) return false;
135
+ // amount must be >= 0 for most operations
136
+ if (m.amount < 0) return false;
137
+ // currency must be valid ISO code
138
+ if (!isValidCurrency(m.currency)) return false;
139
+ return true;
140
+ }
141
+ ```
142
+
143
+ ## Idempotency Pattern
144
+
145
+ Every financial write **must be idempotent**. This prevents double-charges, double-credits, and double-settlements.
146
+
147
+ ### The Pattern
148
+
149
+ ```typescript
150
+ interface CreateLedgerEntryDto {
151
+ unitId: string;
152
+ type: string;
153
+ amount: number;
154
+ currency: string;
155
+ description: string;
156
+ idempotencyKey: string; // UUID v4, generated by client
157
+ }
158
+
159
+ async createLedgerEntry(entry: CreateLedgerEntryDto) {
160
+ // Step 1: Check for existing entry with same idempotency key
161
+ const existing = await this.supabase
162
+ .from('ledger_entry')
163
+ .select('*')
164
+ .eq('request_id', entry.idempotencyKey)
165
+ .eq('unit_id', entry.unitId)
166
+ .single();
167
+
168
+ if (existing.data) {
169
+ // Return the original — do not create duplicate
170
+ return { data: existing.data, isDuplicate: true };
171
+ }
172
+
173
+ // Step 2: Create new entry with idempotency key stored
174
+ const created = await this.supabase
175
+ .from('ledger_entry')
176
+ .insert({
177
+ unit_id: entry.unitId,
178
+ type: entry.type,
179
+ amount: entry.amount,
180
+ currency: entry.currency,
181
+ description: entry.description,
182
+ request_id: entry.idempotencyKey,
183
+ actor_user_id: this.currentUser.id,
184
+ created_at: new Date().toISOString(),
185
+ status: 'COMPLETED',
186
+ })
187
+ .select()
188
+ .single();
189
+
190
+ // Step 3: Return created entry
191
+ return { data: created.data, isDuplicate: false };
192
+ }
193
+ ```
194
+
195
+ ### Idempotency Key Rules
196
+
197
+ - **Client generates it** (UUID v4)
198
+ - **Server stores it** as `request_id` with UNIQUE constraint
199
+ - **Duplicate request** → return original result (HTTP 200, not 409)
200
+ - **Retry-safe** — network timeout? Retry safely without side effects
201
+ - **Long retention** — keep for at least 24 hours (longer for critical operations like settlements)
202
+
203
+ ### Client Usage
204
+
205
+ ```typescript
206
+ async function topupCredit(unitId: string, amount: number, currency: string) {
207
+ const idempotencyKey = uuidv4(); // Generate once, reuse on retry
208
+
209
+ try {
210
+ const response = await api.post('/ledger/create', {
211
+ unitId,
212
+ type: 'CREDIT_TOPUP',
213
+ amount,
214
+ currency,
215
+ idempotencyKey,
216
+ }, {
217
+ headers: { 'Idempotency-Key': idempotencyKey },
218
+ });
219
+ return response.data;
220
+ } catch (error) {
221
+ if (isNetworkError(error)) {
222
+ // Safe to retry with same idempotencyKey
223
+ return topupCredit(unitId, amount, currency);
224
+ }
225
+ throw error;
226
+ }
227
+ }
228
+ ```
229
+
230
+ ## FIFO Credit Settlement
231
+
232
+ When a unit has available credit and unpaid obligations, apply credit using **First-In-First-Out (FIFO)** logic.
233
+
234
+ ### The Algorithm
235
+
236
+ ```typescript
237
+ async settleObligations(
238
+ unitId: string,
239
+ availableCredit: number,
240
+ currency: string
241
+ ): Promise<Settlement[]> {
242
+ // Step 1: Get unpaid obligations, sorted by due date (oldest first)
243
+ const obligations = await this.supabase
244
+ .from('dues_obligation')
245
+ .select('*')
246
+ .eq('unit_id', unitId)
247
+ .eq('status', 'UNPAID')
248
+ .order('due_date', { ascending: true });
249
+
250
+ let remainingCredit = availableCredit;
251
+ const settlements: Settlement[] = [];
252
+
253
+ // Step 2: For each obligation (oldest first), apply credit
254
+ for (const obligation of obligations.data) {
255
+ // All-or-nothing: settle fully or skip entirely
256
+ if (remainingCredit >= obligation.amount) {
257
+ remainingCredit -= obligation.amount;
258
+ settlements.push({
259
+ obligationId: obligation.id,
260
+ amount: obligation.amount,
261
+ currency,
262
+ settledAt: new Date().toISOString(),
263
+ });
264
+ }
265
+ // If credit < obligation amount, skip (no partial settlement)
266
+ }
267
+
268
+ // Step 3: Execute all settlements atomically (single transaction)
269
+ return await this.executeSettlements(unitId, settlements);
270
+ }
271
+
272
+ async executeSettlements(
273
+ unitId: string,
274
+ settlements: Settlement[]
275
+ ): Promise<Settlement[]> {
276
+ // Wrap in transaction to ensure atomicity
277
+ const { data, error } = await this.supabase.rpc('settle_obligations', {
278
+ unit_id: unitId,
279
+ settlements_json: JSON.stringify(settlements),
280
+ });
281
+
282
+ if (error) throw error;
283
+
284
+ // Create ledger entries for each settlement
285
+ for (const settlement of settlements) {
286
+ await this.createLedgerEntry({
287
+ unitId,
288
+ type: 'AUTOSETTLEMENT_APPLIED',
289
+ amount: settlement.amount,
290
+ currency: settlement.currency,
291
+ description: `Settled obligation ${settlement.obligationId}`,
292
+ idempotencyKey: `settlement-${settlement.obligationId}-${new Date().toISOString()}`,
293
+ });
294
+ }
295
+
296
+ return settlements;
297
+ }
298
+ ```
299
+
300
+ ### FIFO Rules
301
+
302
+ 1. **Sort by due_date ASC** — oldest obligations first
303
+ 2. **All-or-nothing** — settle obligation fully or not at all
304
+ 3. **No partial settlement** — if credit < obligation amount, skip that obligation entirely
305
+ 4. **Atomic execution** — all settlements succeed or all fail (no partial state)
306
+ 5. **One pass** — process each obligation once per settlement run
307
+
308
+ ### Why FIFO?
309
+
310
+ - **Fair** — obligations are settled in order they became due
311
+ - **Predictable** — behavior is deterministic and auditable
312
+ - **Legal** — matches accounting standards (payment application rules)
313
+ - **Simple** — no complex allocation logic
314
+
315
+ ## Auto-Settlement Job
316
+
317
+ Run settlement logic automatically on a schedule.
318
+
319
+ ### Job Specification
320
+
321
+ ```typescript
322
+ interface SettlementJob {
323
+ jobId: string;
324
+ scheduledAt: string; // ISO timestamp
325
+ completedAt?: string;
326
+ status: 'PENDING' | 'RUNNING' | 'COMPLETED' | 'FAILED';
327
+ buildingId: string;
328
+ unitsProcessed: number;
329
+ settlementsApplied: number;
330
+ errors: Array<{ unitId: string; error: string }>;
331
+ idempotencyKey: string; // Prevent duplicate runs
332
+ }
333
+ ```
334
+
335
+ ### Implementation Pattern
336
+
337
+ ```typescript
338
+ async function runAutoSettlementJob(jobIdempotencyKey: string) {
339
+ // Check if job already ran (idempotency)
340
+ const existing = await db.settlementJob.findUnique({
341
+ where: { idempotencyKey: jobIdempotencyKey },
342
+ });
343
+ if (existing) return existing;
344
+
345
+ // Create job record
346
+ const job = await db.settlementJob.create({
347
+ data: {
348
+ idempotencyKey: jobIdempotencyKey,
349
+ status: 'RUNNING',
350
+ scheduledAt: new Date(),
351
+ unitsProcessed: 0,
352
+ settlementsApplied: 0,
353
+ errors: [],
354
+ },
355
+ });
356
+
357
+ // Get all buildings with active subscription
358
+ const buildings = await db.building.findMany({
359
+ where: { subscriptionStatus: 'ACTIVE' },
360
+ });
361
+
362
+ let settled = 0;
363
+ const errors = [];
364
+
365
+ for (const building of buildings) {
366
+ try {
367
+ // Get all units in building with available credit
368
+ const units = await db.unit.findMany({
369
+ where: { buildingId: building.id },
370
+ include: { creditBalance: true },
371
+ });
372
+
373
+ for (const unit of units) {
374
+ const credit = unit.creditBalance.amount;
375
+ if (credit > 0) {
376
+ const settlements = await settleObligations(
377
+ unit.id,
378
+ credit,
379
+ building.currency
380
+ );
381
+ settled += settlements.length;
382
+ }
383
+ }
384
+ } catch (error) {
385
+ errors.push({
386
+ buildingId: building.id,
387
+ error: error.message,
388
+ });
389
+ }
390
+ }
391
+
392
+ // Update job record
393
+ return await db.settlementJob.update({
394
+ where: { id: job.id },
395
+ data: {
396
+ status: 'COMPLETED',
397
+ completedAt: new Date(),
398
+ unitsProcessed: buildings.length,
399
+ settlementsApplied: settled,
400
+ errors,
401
+ },
402
+ });
403
+ }
404
+ ```
405
+
406
+ ### Schedule Configuration
407
+
408
+ - **Frequency**: Daily
409
+ - **Time**: 07:00 Asia/Amman (UTC+3)
410
+ - **Idempotency**: Use date as key (`settlement-${YYYY-MM-DD}`) to prevent duplicate runs
411
+ - **Observability**: Log every settlement, track job status, alert on failures
412
+ - **Timeout**: 5 minutes max (prevent long-running processes)
413
+
414
+ ### Cron Expression
415
+
416
+ ```
417
+ 0 7 * * * # Daily at 07:00 (UTC+3)
418
+ ```
419
+
420
+ ### Implementation (Supabase Edge Function)
421
+
422
+ ```typescript
423
+ // Run as Supabase scheduled function
424
+ import { serve } from 'https://deno.land/std@0.131.0/http/server.ts';
425
+
426
+ serve(async (req) => {
427
+ const today = new Date().toISOString().split('T')[0];
428
+ const idempotencyKey = `settlement-${today}`;
429
+
430
+ try {
431
+ const result = await runAutoSettlementJob(idempotencyKey);
432
+ return new Response(JSON.stringify(result), { status: 200 });
433
+ } catch (error) {
434
+ console.error('Settlement job failed:', error);
435
+ return new Response(JSON.stringify({ error: error.message }), { status: 500 });
436
+ }
437
+ });
438
+ ```
439
+
440
+ ## Late Payment Tracking
441
+
442
+ Track when obligations become late.
443
+
444
+ ### Status Transitions
445
+
446
+ - **UNPAID** → **LATE**: Transition occurs at **cycle boundary** (when new dues cycle starts)
447
+ - **LATE** → **SETTLED**: When credit is applied via FIFO settlement
448
+ - **UNPAID** → **SETTLED**: Direct settlement (no LATE status)
449
+
450
+ ### Cycle Boundary Logic
451
+
452
+ ```typescript
453
+ async function markLateObligations() {
454
+ // Get the current billing cycle
455
+ const cycle = await db.billingCycle.findFirst({
456
+ where: { status: 'ACTIVE' },
457
+ });
458
+
459
+ // Mark obligations from previous cycles as LATE
460
+ await db.obligation.updateMany({
461
+ where: {
462
+ dueCycleId: { not: cycle.id },
463
+ status: 'UNPAID',
464
+ },
465
+ data: { status: 'LATE' },
466
+ });
467
+ }
468
+ ```
469
+
470
+ ### Batch Job Schedule
471
+
472
+ - Runs at cycle boundary (e.g., 1st of each month at 01:00)
473
+ - Idempotent: safe to re-run
474
+ - Creates audit log entries for each status change
475
+
476
+ ### Queries
477
+
478
+ ```typescript
479
+ // Get unpaid obligations (includes LATE)
480
+ const unpaid = await db.obligation.findMany({
481
+ where: { status: { in: ['UNPAID', 'LATE'] } },
482
+ });
483
+
484
+ // Get only LATE obligations
485
+ const late = await db.obligation.findMany({
486
+ where: { status: 'LATE' },
487
+ });
488
+ ```
489
+
490
+ ## Statement Generation
491
+
492
+ Generate monthly financial statements per building.
493
+
494
+ ### Statement Structure
495
+
496
+ ```typescript
497
+ interface BuildingStatement {
498
+ statementId: string;
499
+ buildingId: string;
500
+ month: string; // YYYY-MM
501
+ generatedAt: string;
502
+ version: number;
503
+ versionToken: string; // For cache invalidation
504
+
505
+ // Summary
506
+ openingBalance: Money;
507
+ closingBalance: Money;
508
+ totalIncome: Money;
509
+ totalExpenses: Money;
510
+
511
+ // Details
512
+ ledgerEntries: LedgerEntry[];
513
+ settlementDetails: Settlement[];
514
+ outstandingObligations: Obligation[];
515
+
516
+ // Audit
517
+ hash: string; // SHA-256 of statement content
518
+ signedAt?: string;
519
+ }
520
+ ```
521
+
522
+ ### Generation Process
523
+
524
+ ```typescript
525
+ async function generateStatement(
526
+ buildingId: string,
527
+ month: string // 'YYYY-MM'
528
+ ): Promise<BuildingStatement> {
529
+ // Check cache first
530
+ const cached = await cache.get(`statement-${buildingId}-${month}`);
531
+ if (cached) return cached;
532
+
533
+ // Fetch ledger entries for the month
534
+ const [startDate, endDate] = getMonthRange(month);
535
+ const ledgerEntries = await db.ledgerEntry.findMany({
536
+ where: {
537
+ buildingId,
538
+ createdAt: { gte: startDate, lte: endDate },
539
+ },
540
+ orderBy: { createdAt: 'asc' },
541
+ });
542
+
543
+ // Calculate opening balance (end of previous month)
544
+ const openingBalance = await getClosingBalance(
545
+ buildingId,
546
+ new Date(startDate.getTime() - 1000)
547
+ );
548
+
549
+ // Calculate closing balance (end of current month)
550
+ const closingBalance = await getClosingBalance(buildingId, endDate);
551
+
552
+ // Build statement
553
+ const statement: BuildingStatement = {
554
+ statementId: uuidv4(),
555
+ buildingId,
556
+ month,
557
+ generatedAt: new Date().toISOString(),
558
+ version: 1,
559
+ versionToken: uuidv4(),
560
+ openingBalance,
561
+ closingBalance,
562
+ totalIncome: sumByType(ledgerEntries, ['CREDIT_TOPUP', 'MANUAL_UNIT_INCOME']),
563
+ totalExpenses: sumByType(ledgerEntries, ['EXPENSE', 'SUBSCRIPTION_FEE']),
564
+ ledgerEntries,
565
+ settlementDetails: await getSettlementsForPeriod(buildingId, startDate, endDate),
566
+ outstandingObligations: await getOutstandingAt(buildingId, endDate),
567
+ hash: hashStatement(statement),
568
+ };
569
+
570
+ // Cache for 7 days
571
+ await cache.set(`statement-${buildingId}-${month}`, statement, 7 * 24 * 60 * 60);
572
+
573
+ return statement;
574
+ }
575
+ ```
576
+
577
+ ### Versioning
578
+
579
+ ```typescript
580
+ interface StatementVersion {
581
+ statementId: string;
582
+ version: number;
583
+ versionToken: string; // UUID for cache busting
584
+ hash: string; // Immutability check
585
+ createdAt: string;
586
+ pdfUrl?: string; // Pre-generated PDF
587
+ }
588
+
589
+ // Generate new version if content changes
590
+ async function getOrGenerateStatementPDF(
591
+ buildingId: string,
592
+ month: string
593
+ ): Promise<{ url: string; version: number }> {
594
+ const statement = await generateStatement(buildingId, month);
595
+
596
+ // Check if PDF already generated for this version
597
+ let pdfVersion = await db.statementPdf.findFirst({
598
+ where: {
599
+ statementId: statement.statementId,
600
+ version: statement.version,
601
+ },
602
+ });
603
+
604
+ if (!pdfVersion) {
605
+ // Generate PDF (background job)
606
+ const pdfUrl = await generateStatementPDF(statement);
607
+ pdfVersion = await db.statementPdf.create({
608
+ data: {
609
+ statementId: statement.statementId,
610
+ version: statement.version,
611
+ versionToken: statement.versionToken,
612
+ pdfUrl,
613
+ },
614
+ });
615
+ }
616
+
617
+ return { url: pdfVersion.pdfUrl, version: pdfVersion.version };
618
+ }
619
+ ```
620
+
621
+ ## Audit Trail
622
+
623
+ Maintain an immutable audit log of all financial operations.
624
+
625
+ ### Audit Log Schema
626
+
627
+ ```typescript
628
+ interface AuditLogEntry {
629
+ id: string;
630
+ timestamp: string; // ISO 8601
631
+ eventType: string; // LEDGER_CREATED, SETTLEMENT_APPLIED, STATEMENT_GENERATED, etc.
632
+ actorUserId: string;
633
+ buildingId: string;
634
+ unitId?: string;
635
+ requestId: string; // Idempotency key
636
+
637
+ // Sanitized payload (no PII)
638
+ payload: {
639
+ ledgerEntryId?: string;
640
+ entryType?: string;
641
+ amount?: number;
642
+ currency?: string;
643
+ settlementId?: string;
644
+ obligationIds?: string[];
645
+ // No personal documents, IDs, or sensitive data
646
+ };
647
+
648
+ ipAddress: string;
649
+ userAgent: string;
650
+ status: 'SUCCESS' | 'FAILURE';
651
+ errorMessage?: string;
652
+ }
653
+ ```
654
+
655
+ ### Recording
656
+
657
+ ```typescript
658
+ async function auditLog(entry: AuditLogEntry) {
659
+ // Always append, never modify
660
+ await db.auditLog.create({ data: entry });
661
+
662
+ // For critical events, also log to external system (e.g., Sentry, CloudWatch)
663
+ if (['SETTLEMENT_APPLIED', 'REVERSAL_CREATED'].includes(entry.eventType)) {
664
+ logger.critical('Financial operation', entry);
665
+ }
666
+ }
667
+ ```
668
+
669
+ ### Retention
670
+
671
+ - **Minimum**: 7 years (regulatory requirement in many jurisdictions)
672
+ - **Immutable**: No DELETE or UPDATE
673
+ - **Queryable**: Indexed by building, unit, actor, event_type, timestamp
674
+ - **PII-free**: No document content, national IDs, or sensitive personal data
675
+
676
+ ### Queries
677
+
678
+ ```typescript
679
+ // Find all operations by a user in a time range
680
+ const userActions = await db.auditLog.findMany({
681
+ where: {
682
+ actorUserId,
683
+ timestamp: { gte: startDate, lte: endDate },
684
+ },
685
+ orderBy: { timestamp: 'desc' },
686
+ });
687
+
688
+ // Find all settlements in a building
689
+ const settlements = await db.auditLog.findMany({
690
+ where: {
691
+ buildingId,
692
+ eventType: 'SETTLEMENT_APPLIED',
693
+ },
694
+ });
695
+
696
+ // Compliance: Get all audit logs for a 7-year period
697
+ const complianceData = await db.auditLog.findMany({
698
+ where: {
699
+ timestamp: { gte: sevenYearsAgo },
700
+ },
701
+ });
702
+ ```
703
+
704
+ ## Testing Financial Logic
705
+
706
+ Financial code has zero tolerance for bugs. Testing is mandatory, not optional.
707
+
708
+ ### Unit Tests
709
+
710
+ ```typescript
711
+ describe('FIFO Settlement', () => {
712
+ it('settles oldest obligation first', async () => {
713
+ // Arrange
714
+ const unit = await createTestUnit();
715
+ const oldObligation = await createObligation(unit.id, 100, daysAgo(30));
716
+ const newObligation = await createObligation(unit.id, 50, daysAgo(1));
717
+ const credit = 100;
718
+
719
+ // Act
720
+ const settlements = await settleObligations(unit.id, credit, 'JOD');
721
+
722
+ // Assert
723
+ expect(settlements).toHaveLength(1);
724
+ expect(settlements[0].obligationId).toBe(oldObligation.id);
725
+ expect(settlements[0].amount).toBe(100);
726
+ });
727
+
728
+ it('skips if credit is insufficient for full settlement', async () => {
729
+ const unit = await createTestUnit();
730
+ const obligation = await createObligation(unit.id, 100, daysAgo(30));
731
+ const credit = 50; // Less than obligation
732
+
733
+ const settlements = await settleObligations(unit.id, credit, 'JOD');
734
+
735
+ expect(settlements).toHaveLength(0); // No partial settlement
736
+ });
737
+
738
+ it('settles multiple obligations atomically', async () => {
739
+ const unit = await createTestUnit();
740
+ await createObligation(unit.id, 50, daysAgo(30));
741
+ await createObligation(unit.id, 50, daysAgo(15));
742
+ const credit = 100;
743
+
744
+ const settlements = await settleObligations(unit.id, credit, 'JOD');
745
+
746
+ expect(settlements).toHaveLength(2);
747
+ expect(settlements[0].amount).toBe(50);
748
+ expect(settlements[1].amount).toBe(50);
749
+ });
750
+ });
751
+
752
+ describe('Idempotency', () => {
753
+ it('returns same result on duplicate request', async () => {
754
+ const key = uuidv4();
755
+
756
+ const first = await createLedgerEntry({
757
+ unitId: 'unit-1',
758
+ type: 'CREDIT_TOPUP',
759
+ amount: 1000,
760
+ currency: 'JOD',
761
+ idempotencyKey: key,
762
+ });
763
+
764
+ const duplicate = await createLedgerEntry({
765
+ unitId: 'unit-1',
766
+ type: 'CREDIT_TOPUP',
767
+ amount: 1000,
768
+ currency: 'JOD',
769
+ idempotencyKey: key,
770
+ });
771
+
772
+ expect(duplicate.id).toBe(first.id);
773
+ expect(duplicate.createdAt).toBe(first.createdAt);
774
+ });
775
+ });
776
+
777
+ describe('Money Handling', () => {
778
+ it('compares money correctly', () => {
779
+ const a = { amount: 10050, currency: 'USD' };
780
+ const b = { amount: 10050, currency: 'USD' };
781
+
782
+ expect(moneyEqual(a, b)).toBe(true);
783
+ });
784
+
785
+ it('rejects different currencies', () => {
786
+ const a = { amount: 10050, currency: 'USD' };
787
+ const b = { amount: 10050, currency: 'JOD' };
788
+
789
+ expect(moneyEqual(a, b)).toBe(false);
790
+ });
791
+ });
792
+
793
+ describe('Reversal', () => {
794
+ it('negates original entry', async () => {
795
+ const original = await createLedgerEntry({
796
+ unitId: 'unit-1',
797
+ type: 'CREDIT_TOPUP',
798
+ amount: 1000,
799
+ currency: 'JOD',
800
+ idempotencyKey: uuidv4(),
801
+ });
802
+
803
+ const reversal = await createReversal(original.id);
804
+
805
+ const balance = await getUnitBalance('unit-1');
806
+ expect(balance).toBe(0);
807
+ });
808
+ });
809
+ ```
810
+
811
+ ### Integration Tests
812
+
813
+ ```typescript
814
+ describe('Auto-Settlement Job', () => {
815
+ it('settles all units with available credit', async () => {
816
+ const building = await createTestBuilding();
817
+ const [unit1, unit2] = await createTestUnits(building.id, 2);
818
+
819
+ // Unit 1: has credit and obligations
820
+ await createLedgerEntry({
821
+ unitId: unit1.id,
822
+ type: 'CREDIT_TOPUP',
823
+ amount: 200,
824
+ currency: 'JOD',
825
+ idempotencyKey: uuidv4(),
826
+ });
827
+ await createObligation(unit1.id, 100, daysAgo(30));
828
+
829
+ // Unit 2: no credit
830
+ await createObligation(unit2.id, 100, daysAgo(30));
831
+
832
+ const result = await runAutoSettlementJob(`settlement-${today()}`);
833
+
834
+ expect(result.status).toBe('COMPLETED');
835
+ expect(result.settlementsApplied).toBe(1);
836
+ });
837
+ });
838
+ ```
839
+
840
+ ### Test Data Cleanup
841
+
842
+ ```typescript
843
+ afterEach(async () => {
844
+ // Clean up in reverse order of creation (foreign keys)
845
+ await db.settlement.deleteMany({ where: { unitId: testUnitId } });
846
+ await db.obligation.deleteMany({ where: { unitId: testUnitId } });
847
+ await db.ledgerEntry.deleteMany({ where: { unitId: testUnitId } });
848
+ await db.unit.delete({ where: { id: testUnitId } });
849
+ });
850
+ ```
851
+
852
+ ## Common Mistakes (and How to Avoid Them)
853
+
854
+ ### ❌ Mistake 1: UPDATE on Ledger Entries
855
+
856
+ ```typescript
857
+ // WRONG:
858
+ await db.ledgerEntry.update({
859
+ where: { id: 'entry-123' },
860
+ data: { amount: 1050 }, // Oops, typo fixed
861
+ });
862
+ ```
863
+
864
+ **Why it's wrong**: Destroys audit trail, hides the original error, violates accounting standards.
865
+
866
+ **Solution**: Create a REVERSAL entry.
867
+
868
+ ```typescript
869
+ // RIGHT:
870
+ await createLedgerEntry({
871
+ unitId: entry.unitId,
872
+ type: 'REVERSAL',
873
+ amount: entry.amount,
874
+ currency: entry.currency,
875
+ description: `Reversal of entry ${entry.id} (typo)`,
876
+ metadata: { reversalOf: entry.id },
877
+ idempotencyKey: uuidv4(),
878
+ });
879
+
880
+ // Then create the correct entry:
881
+ await createLedgerEntry({
882
+ unitId: entry.unitId,
883
+ type: entry.type,
884
+ amount: 1050, // Corrected
885
+ currency: entry.currency,
886
+ description: `Corrected amount for ${entry.description}`,
887
+ idempotencyKey: uuidv4(),
888
+ });
889
+ ```
890
+
891
+ ### ❌ Mistake 2: Missing Idempotency Key
892
+
893
+ ```typescript
894
+ // WRONG:
895
+ async function topupCredit(unitId: string, amount: number) {
896
+ // No idempotency key! Network retry = double charge
897
+ return await db.ledgerEntry.create({
898
+ unitId,
899
+ type: 'CREDIT_TOPUP',
900
+ amount,
901
+ });
902
+ }
903
+ ```
904
+
905
+ **Solution**: Always generate and store idempotency key.
906
+
907
+ ```typescript
908
+ // RIGHT:
909
+ async function topupCredit(
910
+ unitId: string,
911
+ amount: number,
912
+ idempotencyKey: string // Required parameter
913
+ ) {
914
+ const existing = await db.ledgerEntry.findFirst({
915
+ where: { requestId: idempotencyKey },
916
+ });
917
+ if (existing) return existing;
918
+
919
+ return await db.ledgerEntry.create({
920
+ unitId,
921
+ type: 'CREDIT_TOPUP',
922
+ amount,
923
+ requestId: idempotencyKey,
924
+ });
925
+ }
926
+ ```
927
+
928
+ ### ❌ Mistake 3: Partial Settlement
929
+
930
+ ```typescript
931
+ // WRONG:
932
+ let remaining = availableCredit;
933
+ for (const obligation of obligations) {
934
+ const settled = Math.min(remaining, obligation.amount);
935
+ settlements.push(settled); // Partial settlement!
936
+ remaining -= settled;
937
+ }
938
+ ```
939
+
940
+ **Why it's wrong**: FIFO rule is all-or-nothing. Partial settlement breaks the principle and creates confusion.
941
+
942
+ **Solution**: Skip if not enough.
943
+
944
+ ```typescript
945
+ // RIGHT:
946
+ for (const obligation of obligations) {
947
+ if (remainingCredit >= obligation.amount) {
948
+ settlements.push(obligation.amount);
949
+ remainingCredit -= obligation.amount;
950
+ }
951
+ // If not enough, skip entirely
952
+ }
953
+ ```
954
+
955
+ ### ❌ Mistake 4: Storing Amount Without Currency
956
+
957
+ ```typescript
958
+ // WRONG:
959
+ const balance = 10050;
960
+ const newBalance = balance + 500;
961
+ // Is this JOD? USD? What's the unit?
962
+ ```
963
+
964
+ **Solution**: Always pair amount with currency.
965
+
966
+ ```typescript
967
+ // RIGHT:
968
+ const balance: Money = { amount: 10050, currency: 'JOD' };
969
+ const addition: Money = { amount: 500, currency: 'JOD' };
970
+ const newBalance = addMoney(balance, addition);
971
+
972
+ function addMoney(a: Money, b: Money): Money {
973
+ if (a.currency !== b.currency) {
974
+ throw new Error(`Cannot add ${a.currency} and ${b.currency}`);
975
+ }
976
+ return { amount: a.amount + b.amount, currency: a.currency };
977
+ }
978
+ ```
979
+
980
+ ### ❌ Mistake 5: Using JavaScript Floating Point
981
+
982
+ ```typescript
983
+ // WRONG:
984
+ const amount = 10.05; // JavaScript floating point ❌
985
+ const result = amount + 0.01; // Might be 10.060000000000001
986
+ if (result === 10.06) { } // Fails due to precision
987
+ ```
988
+
989
+ **Solution**: Use integer cents/fils.
990
+
991
+ ```typescript
992
+ // RIGHT:
993
+ const amount = 1005; // 10.05 JOD in fils
994
+ const result = amount + 1; // Always precise
995
+ if (result === 1006) { } // ✓ Works
996
+ ```
997
+
998
+ ### ❌ Mistake 6: Missing Transaction Wrapping
999
+
1000
+ ```typescript
1001
+ // WRONG:
1002
+ const settlements = [];
1003
+ for (const obligation of obligations) {
1004
+ // If this fails halfway through, some are settled, some aren't
1005
+ await settleObligation(obligation);
1006
+ settlements.push(obligation);
1007
+ }
1008
+ ```
1009
+
1010
+ **Solution**: Wrap in a transaction.
1011
+
1012
+ ```typescript
1013
+ // RIGHT:
1014
+ await db.$transaction(async (tx) => {
1015
+ for (const obligation of obligations) {
1016
+ await tx.settlement.create({
1017
+ obligationId: obligation.id,
1018
+ settledAt: new Date(),
1019
+ });
1020
+ }
1021
+ // All succeed or all fail
1022
+ });
1023
+ ```
1024
+
1025
+ ---
1026
+
1027
+ ## Summary
1028
+
1029
+ Financial code is special. It requires:
1030
+
1031
+ 1. **Append-only ledgers** — immutability, full audit trail
1032
+ 2. **Idempotent writes** — prevent duplicates and double-charges
1033
+ 3. **Integer money** — never floating point
1034
+ 4. **FIFO settlement** — deterministic, fair, auditable
1035
+ 5. **Comprehensive testing** — edge cases, idempotency, reversals
1036
+ 6. **Immutable audit logs** — 7+ year retention
1037
+ 7. **Transactions** — atomic multi-step operations
1038
+
1039
+ Every principle here comes from real banking failures. Follow them rigorously.