@hyperscale0/hsx 1.0.0 → 1.0.123

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 (316) hide show
  1. package/CHANGELOG.md +5 -256
  2. package/LICENSE.md +356 -0
  3. package/NOTICE.md +8 -0
  4. package/README.md +44 -44
  5. package/bin/hsx.ts +0 -7
  6. package/dist/bin/hsx.d.ts +0 -4
  7. package/dist/bin/hsx.d.ts.map +1 -1
  8. package/dist/bin/hsx.js +0 -6
  9. package/dist/bin/hsx.js.map +1 -1
  10. package/dist/src/ast.d.ts +113 -308
  11. package/dist/src/ast.d.ts.map +1 -1
  12. package/dist/src/ast.js +7 -71
  13. package/dist/src/ast.js.map +1 -1
  14. package/dist/src/attachment-economics.d.ts +5 -0
  15. package/dist/src/attachment-economics.d.ts.map +1 -0
  16. package/dist/src/attachment-economics.js +55 -0
  17. package/dist/src/attachment-economics.js.map +1 -0
  18. package/dist/src/binding-contract.d.ts +22 -0
  19. package/dist/src/binding-contract.d.ts.map +1 -0
  20. package/dist/src/binding-contract.js +74 -0
  21. package/dist/src/binding-contract.js.map +1 -0
  22. package/dist/src/cli.d.ts +4 -19
  23. package/dist/src/cli.d.ts.map +1 -1
  24. package/dist/src/cli.js +89 -276
  25. package/dist/src/cli.js.map +1 -1
  26. package/dist/src/compile.d.ts +34 -66
  27. package/dist/src/compile.d.ts.map +1 -1
  28. package/dist/src/compile.js +2949 -117
  29. package/dist/src/compile.js.map +1 -1
  30. package/dist/src/cost.d.ts +8 -75
  31. package/dist/src/cost.d.ts.map +1 -1
  32. package/dist/src/cost.js +54 -364
  33. package/dist/src/cost.js.map +1 -1
  34. package/dist/src/diagnostics.d.ts +14 -9
  35. package/dist/src/diagnostics.d.ts.map +1 -1
  36. package/dist/src/diagnostics.js +39 -416
  37. package/dist/src/diagnostics.js.map +1 -1
  38. package/dist/src/examples-bundle.d.ts +3 -0
  39. package/dist/src/examples-bundle.d.ts.map +1 -0
  40. package/dist/src/examples-bundle.js +18 -0
  41. package/dist/src/examples-bundle.js.map +1 -0
  42. package/dist/src/examples.d.ts +10 -0
  43. package/dist/src/examples.d.ts.map +1 -0
  44. package/dist/src/examples.js +2 -0
  45. package/dist/src/examples.js.map +1 -0
  46. package/dist/src/format.d.ts +8 -7
  47. package/dist/src/format.d.ts.map +1 -1
  48. package/dist/src/format.js +9 -175
  49. package/dist/src/format.js.map +1 -1
  50. package/dist/src/header-source.d.ts +3 -0
  51. package/dist/src/header-source.d.ts.map +1 -0
  52. package/dist/src/header-source.js +41 -0
  53. package/dist/src/header-source.js.map +1 -0
  54. package/dist/src/headers.d.ts +64 -0
  55. package/dist/src/headers.d.ts.map +1 -0
  56. package/dist/src/headers.js +167 -0
  57. package/dist/src/headers.js.map +1 -0
  58. package/dist/src/index.d.ts +7 -11
  59. package/dist/src/index.d.ts.map +1 -1
  60. package/dist/src/index.js +4 -9
  61. package/dist/src/index.js.map +1 -1
  62. package/dist/src/keywords.d.ts +56 -0
  63. package/dist/src/keywords.d.ts.map +1 -0
  64. package/dist/src/keywords.js +133 -0
  65. package/dist/src/keywords.js.map +1 -0
  66. package/dist/src/language.d.ts +26 -0
  67. package/dist/src/language.d.ts.map +1 -0
  68. package/dist/src/language.js +440 -0
  69. package/dist/src/language.js.map +1 -0
  70. package/dist/src/lex.d.ts +13 -28
  71. package/dist/src/lex.d.ts.map +1 -1
  72. package/dist/src/lex.js +119 -145
  73. package/dist/src/lex.js.map +1 -1
  74. package/dist/src/parse.d.ts +4 -17
  75. package/dist/src/parse.d.ts.map +1 -1
  76. package/dist/src/parse.js +484 -1036
  77. package/dist/src/parse.js.map +1 -1
  78. package/dist/src/std-bundle.d.ts.map +1 -1
  79. package/dist/src/std-bundle.js +11 -84
  80. package/dist/src/std-bundle.js.map +1 -1
  81. package/dist/src/std-library.d.ts +1 -1
  82. package/dist/src/std-library.d.ts.map +1 -1
  83. package/dist/src/std-library.js +1 -5
  84. package/dist/src/std-library.js.map +1 -1
  85. package/dist/src/tunables.d.ts +7 -0
  86. package/dist/src/tunables.d.ts.map +1 -0
  87. package/dist/src/tunables.js +27 -0
  88. package/dist/src/tunables.js.map +1 -0
  89. package/docs/README.md +528 -26
  90. package/docs/assets/brand/hsx-horizontal-white.svg +1 -0
  91. package/docs/assets/brand/hsx-horizontal.svg +1 -0
  92. package/docs/assets/brand/hsx-stacked-white.svg +1 -0
  93. package/docs/assets/brand/hsx-stacked.svg +1 -0
  94. package/docs/assets/brand/manifest.json +30 -0
  95. package/docs/assets/hsx.svg +11 -0
  96. package/examples/cards.hsx +34 -0
  97. package/examples/collections.hsx +37 -0
  98. package/examples/cost-table.json +633 -385
  99. package/examples/escrow.hsx +24 -0
  100. package/examples/financing.hsx +32 -0
  101. package/examples/insurance.hsx +30 -0
  102. package/examples/lending.hsx +57 -0
  103. package/examples/library.hsx +15 -0
  104. package/examples/marketplace.hsx +32 -0
  105. package/examples/money.hsx +19 -0
  106. package/examples/rental-deposit.hsx +60 -0
  107. package/examples/repair-approval.hsx +44 -0
  108. package/examples/reporting.hsx +28 -0
  109. package/examples/savings.hsx +21 -0
  110. package/examples/serviced.hsx +25 -0
  111. package/examples/wallet.hsx +24 -0
  112. package/package.json +25 -23
  113. package/src/ast.ts +83 -440
  114. package/src/attachment-economics.ts +103 -0
  115. package/src/binding-contract.ts +104 -0
  116. package/src/cli.ts +95 -325
  117. package/src/compile.ts +4063 -209
  118. package/src/cost.ts +71 -574
  119. package/src/diagnostics.ts +57 -437
  120. package/src/examples-bundle.ts +19 -0
  121. package/src/examples.ts +9 -0
  122. package/src/format.ts +12 -256
  123. package/src/header-source.ts +54 -0
  124. package/src/headers.ts +195 -0
  125. package/src/index.ts +18 -33
  126. package/src/keywords.ts +134 -0
  127. package/src/language.ts +629 -0
  128. package/src/lex.ts +131 -192
  129. package/src/parse.ts +489 -1195
  130. package/src/std-bundle.ts +12 -88
  131. package/src/std-library.ts +2 -7
  132. package/src/tunables.ts +36 -0
  133. package/std/cards.hsx +85 -0
  134. package/std/collections.hsx +90 -0
  135. package/std/escrow.hsx +73 -0
  136. package/std/financing.hsx +683 -0
  137. package/std/insurance.hsx +89 -0
  138. package/std/lending.hsx +138 -0
  139. package/std/marketplace.hsx +123 -0
  140. package/std/money.hsx +173 -0
  141. package/std/reporting.hsx +753 -0
  142. package/std/savings.hsx +68 -0
  143. package/std/wallet.hsx +57 -0
  144. package/LICENSE +0 -661
  145. package/LICENSING.md +0 -52
  146. package/TRADEMARKS.md +0 -35
  147. package/dist/src/emit.d.ts +0 -34
  148. package/dist/src/emit.d.ts.map +0 -1
  149. package/dist/src/emit.js +0 -137
  150. package/dist/src/emit.js.map +0 -1
  151. package/dist/src/entry-overrides.d.ts +0 -58
  152. package/dist/src/entry-overrides.d.ts.map +0 -1
  153. package/dist/src/entry-overrides.js +0 -284
  154. package/dist/src/entry-overrides.js.map +0 -1
  155. package/dist/src/ir.d.ts +0 -66
  156. package/dist/src/ir.d.ts.map +0 -1
  157. package/dist/src/ir.js +0 -15
  158. package/dist/src/ir.js.map +0 -1
  159. package/dist/src/limits.d.ts +0 -21
  160. package/dist/src/limits.d.ts.map +0 -1
  161. package/dist/src/limits.js +0 -21
  162. package/dist/src/limits.js.map +0 -1
  163. package/dist/src/lsp/server.d.ts +0 -32
  164. package/dist/src/lsp/server.d.ts.map +0 -1
  165. package/dist/src/lsp/server.js +0 -391
  166. package/dist/src/lsp/server.js.map +0 -1
  167. package/dist/src/modules.d.ts +0 -38
  168. package/dist/src/modules.d.ts.map +0 -1
  169. package/dist/src/modules.js +0 -368
  170. package/dist/src/modules.js.map +0 -1
  171. package/dist/src/typecheck.d.ts +0 -8
  172. package/dist/src/typecheck.d.ts.map +0 -1
  173. package/dist/src/typecheck.js +0 -2958
  174. package/dist/src/typecheck.js.map +0 -1
  175. package/dist/src/version.d.ts +0 -5
  176. package/dist/src/version.d.ts.map +0 -1
  177. package/dist/src/version.js +0 -5
  178. package/dist/src/version.js.map +0 -1
  179. package/docs/guide/01-first-program.md +0 -24
  180. package/docs/guide/02-money.md +0 -25
  181. package/docs/guide/03-instruments.md +0 -22
  182. package/docs/guide/04-lifecycles.md +0 -25
  183. package/docs/guide/05-fees-and-splits.md +0 -45
  184. package/docs/guide/06-schedules.md +0 -22
  185. package/docs/guide/07-composition.md +0 -46
  186. package/docs/guide/08-writing-a-module.md +0 -26
  187. package/docs/guide/09-cost.md +0 -24
  188. package/docs/guide/10-diagnostics.md +0 -27
  189. package/docs/llms-full.txt +0 -1739
  190. package/docs/llms.txt +0 -45
  191. package/docs/playground.md +0 -54
  192. package/docs/reference/cli.md +0 -35
  193. package/docs/reference/diagnostics.md +0 -619
  194. package/docs/reference/grammar.md +0 -39
  195. package/docs/reference/std/advance.md +0 -35
  196. package/docs/reference/std/cancellable_booking.md +0 -35
  197. package/docs/reference/std/captured_payment.md +0 -43
  198. package/docs/reference/std/conditional_disbursement.md +0 -33
  199. package/docs/reference/std/credit_facility.md +0 -35
  200. package/docs/reference/std/held_payment.md +0 -64
  201. package/docs/reference/std/instant_transfer.md +0 -30
  202. package/docs/reference/std/metered.md +0 -27
  203. package/docs/reference/std/pooled_split.md +0 -26
  204. package/docs/reference/std/premium_forward.md +0 -39
  205. package/docs/reference/std/reconciled_payout.md +0 -33
  206. package/docs/reference/std/recurring_collection.md +0 -25
  207. package/docs/reference/std/rotating_pool.md +0 -42
  208. package/docs/reference/std/scheduled.md +0 -51
  209. package/docs/reference/std/security_deposit.md +0 -46
  210. package/docs/reference/std/settlement_batch.md +0 -42
  211. package/docs/reference/std/swap.md +0 -46
  212. package/docs/reference/std/threshold_pool.md +0 -47
  213. package/docs/reference/std/weighted_distribution.md +0 -39
  214. package/docs/reference/types.md +0 -20
  215. package/docs/reference/udl-output.md +0 -13
  216. package/docs/sessions/2026-09-02-two-instruments.hsx +0 -21
  217. package/docs/sessions/2026-09-02-two-instruments.md +0 -41
  218. package/examples/01-first-program/README.md +0 -45
  219. package/examples/01-first-program/tip-jar.hsx +0 -17
  220. package/examples/02-imports-and-archetypes/README.md +0 -96
  221. package/examples/02-imports-and-archetypes/photo-booth.hsx +0 -37
  222. package/examples/03-diagnostics/README.md +0 -68
  223. package/examples/03-diagnostics/corner-shop-fixed.hsx +0 -22
  224. package/examples/03-diagnostics/corner-shop.hsx +0 -19
  225. package/examples/04-complete-product/README.md +0 -84
  226. package/examples/04-complete-product/study-hall.hsx +0 -67
  227. package/examples/05-watch-club/README.md +0 -9
  228. package/examples/05-watch-club/watch-club.hsx +0 -124
  229. package/examples/README.md +0 -18
  230. package/examples/advance/README.md +0 -3
  231. package/examples/advance/advance.hsx +0 -13
  232. package/examples/advance/advance.udl +0 -339
  233. package/examples/cancellable_booking/README.md +0 -3
  234. package/examples/cancellable_booking/cancellable_booking.hsx +0 -14
  235. package/examples/cancellable_booking/cancellable_booking.udl +0 -391
  236. package/examples/captured_payment/README.md +0 -3
  237. package/examples/captured_payment/captured_payment.hsx +0 -21
  238. package/examples/captured_payment/captured_payment.udl +0 -677
  239. package/examples/conditional_disbursement/README.md +0 -3
  240. package/examples/conditional_disbursement/conditional_disbursement.hsx +0 -17
  241. package/examples/conditional_disbursement/conditional_disbursement.udl +0 -412
  242. package/examples/credit_facility/README.md +0 -3
  243. package/examples/credit_facility/credit_facility.hsx +0 -27
  244. package/examples/credit_facility/credit_facility.udl +0 -1373
  245. package/examples/held_payment/README.md +0 -3
  246. package/examples/held_payment/held_payment.hsx +0 -13
  247. package/examples/held_payment/held_payment.udl +0 -507
  248. package/examples/instant_transfer/README.md +0 -3
  249. package/examples/instant_transfer/instant_transfer.hsx +0 -16
  250. package/examples/instant_transfer/instant_transfer.udl +0 -364
  251. package/examples/metered/README.md +0 -3
  252. package/examples/metered/metered.hsx +0 -13
  253. package/examples/metered/metered.udl +0 -247
  254. package/examples/pooled_split/README.md +0 -3
  255. package/examples/pooled_split/pooled_split.hsx +0 -15
  256. package/examples/pooled_split/pooled_split.udl +0 -425
  257. package/examples/premium_forward/README.md +0 -3
  258. package/examples/premium_forward/premium_forward.hsx +0 -19
  259. package/examples/premium_forward/premium_forward.udl +0 -594
  260. package/examples/reconciled_payout/README.md +0 -3
  261. package/examples/reconciled_payout/reconciled_payout.hsx +0 -13
  262. package/examples/reconciled_payout/reconciled_payout.udl +0 -303
  263. package/examples/recurring_collection/README.md +0 -3
  264. package/examples/recurring_collection/recurring_collection.hsx +0 -21
  265. package/examples/recurring_collection/recurring_collection.udl +0 -1130
  266. package/examples/rotating_pool/README.md +0 -3
  267. package/examples/rotating_pool/rotating_pool.hsx +0 -18
  268. package/examples/rotating_pool/rotating_pool.udl +0 -4352
  269. package/examples/scheduled/README.md +0 -3
  270. package/examples/scheduled/scheduled.hsx +0 -12
  271. package/examples/scheduled/scheduled.udl +0 -335
  272. package/examples/security_deposit/README.md +0 -3
  273. package/examples/security_deposit/security_deposit.hsx +0 -21
  274. package/examples/security_deposit/security_deposit.udl +0 -293
  275. package/examples/settlement_batch/README.md +0 -3
  276. package/examples/settlement_batch/settlement_batch.hsx +0 -19
  277. package/examples/settlement_batch/settlement_batch.udl +0 -753
  278. package/examples/swap/README.md +0 -3
  279. package/examples/swap/swap.hsx +0 -19
  280. package/examples/swap/swap.udl +0 -876
  281. package/examples/threshold_pool/README.md +0 -3
  282. package/examples/threshold_pool/threshold_pool.hsx +0 -16
  283. package/examples/threshold_pool/threshold_pool.udl +0 -667
  284. package/examples/weighted_distribution/README.md +0 -3
  285. package/examples/weighted_distribution/weighted_distribution.hsx +0 -20
  286. package/examples/weighted_distribution/weighted_distribution.udl +0 -337
  287. package/skills/hsx/SKILL.md +0 -483
  288. package/src/emit.ts +0 -201
  289. package/src/entry-overrides.ts +0 -428
  290. package/src/ir.ts +0 -99
  291. package/src/limits.ts +0 -20
  292. package/src/lsp/server.ts +0 -460
  293. package/src/modules.ts +0 -446
  294. package/src/typecheck.ts +0 -3876
  295. package/src/version.ts +0 -5
  296. package/std/SEMANTICS.md +0 -130
  297. package/std/settlements/advance.hsx +0 -177
  298. package/std/settlements/cancellable_booking.hsx +0 -87
  299. package/std/settlements/captured_payment.hsx +0 -144
  300. package/std/settlements/conditional_disbursement.hsx +0 -185
  301. package/std/settlements/credit_facility.hsx +0 -167
  302. package/std/settlements/held_payment.hsx +0 -578
  303. package/std/settlements/index.hsx +0 -3
  304. package/std/settlements/instant_transfer.hsx +0 -190
  305. package/std/settlements/metered.hsx +0 -67
  306. package/std/settlements/pooled_split.hsx +0 -68
  307. package/std/settlements/premium_forward.hsx +0 -257
  308. package/std/settlements/reconciled_payout.hsx +0 -77
  309. package/std/settlements/recurring_collection.hsx +0 -27
  310. package/std/settlements/rotating_pool.hsx +0 -252
  311. package/std/settlements/scheduled.hsx +0 -641
  312. package/std/settlements/security_deposit.hsx +0 -166
  313. package/std/settlements/settlement_batch.hsx +0 -122
  314. package/std/settlements/swap.hsx +0 -1114
  315. package/std/settlements/threshold_pool.hsx +0 -221
  316. package/std/settlements/weighted_distribution.hsx +0 -141
@@ -0,0 +1,683 @@
1
+ header financing
2
+
3
+ // Economics: Principal advances and repayments are excluded from earnings. Claims, waivers,
4
+ // write-offs and allocations are internal. Paid profit and company charges are
5
+ // earnings; receipt refunds reverse captured cash. Outside recovery recipients
6
+ // receive payouts from held company cash, or pass-through direct customer cash.
7
+
8
+ // Expose create and sign on installments, then collect_down_payment as checkout
9
+ // for disburse_to: funds. Checkout requires a signed plan and pending escrow;
10
+ // it tops up only the missing down payment, without advancing lender principal.
11
+ // Disburse still collects any missing down payment, activates slices, and confirms
12
+ // the fully funded hold. Existing escrow credit counts once toward the down payment.
13
+ // Disburse first credits an active marketplace.reservation on the same hold,
14
+ // so list the plan in its converters; the deposit then counts as escrow credit.
15
+ // Borrower-directed disbursement excludes collect_down_payment. A down_payment
16
+ // there reduces principal but does not collect cash. Price is subject metadata,
17
+ // not the borrower limit. The escrow payer and price must match the plan.
18
+ // Expose escrow cancel to return checkout cash if financing is abandoned; plan
19
+ // void alone cancels the agreement and does not refund its separate escrow.
20
+ // Approve borrower and portfolio limits before disbursement. Create builds the
21
+ // dated slice records; payments need a separate payment record and pay action.
22
+ // Expose parent payoff separately and create a current, unexpired payoff_quote.
23
+ // Expose late_charge.create as propose_assessment before the clock can assess;
24
+ // provide matching plan, slice and borrower. Fine is fixed money, not a percentage.
25
+ // Collection uses the minimum of payment cash and outstanding fine; a partial
26
+ // payment does not settle the remaining fine. Recovery cost needs its own record.
27
+ // Profit share credits capital's allocatedProfitPayable, not an outside agency.
28
+ // Credit lines need adapter binding at activation; draws are ledger advances,
29
+ // not evidence of outside funding. Caller-authorized limit approvals and advances
30
+ // need the Product's operation authority. No clock declaration proves retries.
31
+
32
+ // Cost: installments include cash and claim moves, child-account creation and
33
+ // bounded invocations across slices and assessments. Month and charge limits affect
34
+ // the bound. Run hsx cost on the selected policies; profit is not a platform tariff.
35
+
36
+ instrument installments(months: integer(1, 366), charge_limit: integer(1, 3) = 3, profit: percent, disburse_to: enum(funds, borrower) = funds, profit_earned: enum(on_payment, by_schedule, at_disbursement) = on_payment, apply: enum(fines_profit_principal, principal_profit, pro_rata) = fines_profit_principal, payoff_rebate: percent = 100%, write_off_after: duration = 90d, max_extension: duration = 90d, max_amendments: integer(0, 366) = 2, waiver_limit: percent = 100%, amendment_expiry: duration = 7d, payoff_quote_expiry: duration = 1h, allow_overdue: enum(allowed, blocked) = allowed, assessed_fines: enum(carry, require_waiver) = carry, down_payment: percent = 0%, funds: ref<escrow.hold>?, borrower: party = party(person), capital: party = programOperator, share: percent = 0%, limits: ref<financing.limits> = object(financing.limits), portfolio: ref<financing.portfolio_limit> = object(financing.portfolio_limit)) {
37
+ dependencies {
38
+ funds {
39
+ selector: disburse_to, is: funds
40
+ message: "Attachment `{attachment}` disburses into a hold, but `funds` is not bound."
41
+ fix: "Bind `funds: sale` to an escrow attachment. Choose `disburse_to: borrower` only if the borrower should receive the money directly."
42
+ }
43
+ }
44
+ revisioned: true
45
+ familyRevision: 1
46
+ summary: "Funding, profit recognition and payment order chosen by the program."
47
+ fields {
48
+ funds: ref<funds>?, borrower: account of borrower, settlementIdentity: text, borrowerLimit: ref<limits>, portfolioLimit: ref<portfolio>
49
+ months: integer = months, dates: list(date, months)
50
+ allocatedPayable: account(capital, cash, "allocatedProfitPayable"), capital: account(capital, cash, "capital"), profitIncome: account(capital, cash, "profitIncome"), loss: account(capital, claim, "loss")
51
+ downPaymentRemaining: money?, downPaymentCredit: money?, principalReturned: money?, profitReturned: money?, lossRecorded: money?
52
+ price: money = subject.price
53
+ downPayment: money = rate(self.price, down_payment)
54
+ principal: money = subtract(self.price, self.downPayment)
55
+ profit: money = rate(self.principal, profit)
56
+ total: money = sum(self.principal, self.profit)
57
+ }
58
+ lifecycle { states: [quoted, signed, active, paid, written_off, cancelled], initial: quoted }
59
+ invariants count of { instrument: all(financing.installments), reference: "borrowerLimit", anchor: self.borrowerLimit, states: [active], limit: 366 } <= self.borrowerLimit.activePlans
60
+ invariants sum "principalReceivable.balance" of { instrument: all(financing.installments.slice), reference: "borrowerLimit", anchor: self.borrowerLimit, states: [pending, due, paid], limit: 366 } <= self.borrowerLimit.limit
61
+ invariants sum "principalReceivable.balance" of { instrument: all(financing.installments.slice), reference: "portfolioLimit", anchor: self.portfolioLimit, states: [pending, due, paid], limit: 366 } <= self.portfolioLimit.limit
62
+ action create {
63
+ subject { price: money }
64
+ requires self.borrowerLimit.borrower == self.borrower
65
+ when disburse_to is funds { requires self.borrower == self.funds.payer; requires self.price == self.funds.price }
66
+ invoke: [{ range: { count: { literal: months }, maximum: months, bind: position }, instrument: slice, action: create, input: { plan: { field: self.id }, borrowerLimit: { field: self.borrowerLimit }, portfolioLimit: { field: self.portfolioLimit }, position: { field: position } } }]
67
+ }
68
+ action sign {
69
+ from: quoted, to: signed, actor: { party: borrower }, input { signedOffer: text, affordabilityReference: text }
70
+ requires count of { instrument: slice, reference: "plan", anchor: self.id, states: [draft], limit: 366 } == self.months
71
+ }
72
+ action collect_down_payment { allowZero: true,
73
+ from: signed, to: signed, actor: { party: borrower }
74
+ when disburse_to is borrower { requires true == false }
75
+ when disburse_to is funds {
76
+ requires self.funds in [pending]
77
+ calculate: [{ target: downPaymentCredit, op: minimum, values: [{ field: self.funds.held.balance }, { field: self.downPayment }] }, { target: downPaymentRemaining, op: subtract, base: { field: self.downPayment }, subtract: [{ field: self.downPaymentCredit }] }]
78
+ moves self.downPaymentRemaining from self.borrower to self.funds.held economics { purpose: principal, sourceParty: borrower }
79
+ }
80
+ }
81
+ action disburse { allowZero: true,
82
+ from: signed, to: active, actor: { party: capital }
83
+ requires self.borrowerLimit in [approved]
84
+ requires self.portfolioLimit in [approved]
85
+ when disburse_to is funds {
86
+ requires self.funds in [pending]
87
+ invoke: [{ selection: { instrument: all(marketplace.reservation), reference: "funds", anchor: self.funds, states: [active], limit: 1 }, action: credit, input: { funds: { field: self.funds }, contribution: { field: self.downPayment } } }, { reference: self.id, action: fund_down_payment, input: {} }]
88
+ }
89
+ when disburse_to is borrower { moves self.principal from self.capital to self.borrower economics { purpose: principal, sourceParty: capital } }
90
+ invoke: [{ selection: { instrument: slice, reference: "plan", anchor: self.id, states: [draft], limit: 366, order: [position] }, action: activate, input: {} }]
91
+ when disburse_to is funds { invoke: [{ reference: self.funds, action: confirm, input: {} }] }
92
+ }
93
+ action fund_down_payment { allowZero: true,
94
+ from: active, to: active, actor: { parent: current() }
95
+ when disburse_to is funds {
96
+ calculate: [{ target: downPaymentCredit, op: minimum, values: [{ field: self.funds.held.balance }, { field: self.downPayment }] }, { target: downPaymentRemaining, op: subtract, base: { field: self.downPayment }, subtract: [{ field: self.downPaymentCredit }] }]
97
+ moves self.downPaymentRemaining from self.funds.payer to self.funds.held economics { purpose: principal, sourceParty: borrower }
98
+ moves self.principal from self.capital to self.funds.held economics { purpose: principal, sourceParty: capital }
99
+ }
100
+ }
101
+ action complete {
102
+ from: active, to: paid
103
+ requires count of { instrument: slice, reference: "plan", anchor: self.id, states: [pending, due], limit: 366 } == 0
104
+ requires sum "receivable.balance" of { instrument: all(financing.late_charge), reference: "plan", anchor: self.id, states: [assessed, collected], limit: 366 } == "0"
105
+ requires sum "receivable.balance" of { instrument: all(financing.late_charge.cost), reference: "plan", anchor: self.id, states: [assessed, collected], limit: 366 } == "0"
106
+ }
107
+ action restore { from: [active, paid], to: active, actor: { parent: payment } }
108
+ action payoff {
109
+ from: active, to: paid, actor: { party: borrower }
110
+ input { quote: ref<payoff_quote> }
111
+ requires input.quote.plan == self.id; requires input.quote.revision == self.productRevision
112
+ deadline: { at: input.quote.expiresAt }
113
+ calculate: [
114
+ { target: "principalReturned", op: "aggregate", selection: { instrument: slice, reference: "plan", anchor: self.id, states: [pending, due], limit: 366 }, measure: { sum: "principalReceivable.balance" } }
115
+ ]
116
+ invoke: [{ selection: { instrument: all(financing.late_charge), reference: "plan", anchor: self.id, states: [assessed, collected], limit: 366, order: [overdueAt] }, action: payoff, input: {} }, { selection: { instrument: all(financing.late_charge.cost), reference: "plan", anchor: self.id, states: [assessed, collected], limit: 366, order: [overdueAt] }, action: payoff, input: {} }]
117
+ invoke: [{ selection: { instrument: slice, reference: "plan", anchor: self.id, states: [pending, due], limit: 366, order: [position] }, action: payoff, input: { identity: { field: self.settlementIdentity } } }, { reference: self.id, action: record_payoff, input: {} }]
118
+ }
119
+ action record_payoff {
120
+ from: paid, to: paid, actor: { parent: current() }
121
+ calculate: [{ target: profitReturned, op: aggregate, selection: { instrument: slice, reference: "plan", anchor: self.id, states: [paid], limit: 366, where: { payoffIdentity: { field: self.settlementIdentity } } }, measure: { sum: "payoffProfit" } }]
122
+ invoke: [{ instrument: settlement, action: create, input: { plan: { field: self.id }, identity: { field: self.settlementIdentity }, principal: { field: self.principalReturned }, profit: { field: self.profitReturned }, loss: { literal: "0" }, kind: { literal: "cash" } } }]
123
+ }
124
+ action write_off {
125
+ from: active, to: written_off, actor: { parent: write_off_request }
126
+ input { overdueSlice: ref<slice> }
127
+ calculate: [{ target: "lossRecorded", op: "aggregate", selection: { instrument: slice, reference: "plan", anchor: self.id, states: [pending, due], limit: 366 }, measure: { sum: "principalReceivable.balance" } }]
128
+ due: { at: input.overdueSlice.lossEligibleAt }
129
+ requires input.overdueSlice.plan == self.id; requires input.overdueSlice.principalReceivable.balance > "0"
130
+ invoke: [{ selection: { instrument: slice, reference: "plan", anchor: self.id, states: [pending, due], limit: 366, order: [position] }, action: write_off, input: {} }, { instrument: settlement, action: create, input: { plan: { field: self.id }, identity: { field: self.settlementIdentity }, principal: { literal: "0" }, profit: { literal: "0" }, loss: { field: self.lossRecorded }, kind: { literal: "loss" } } }]
131
+ }
132
+ action record_payment {
133
+ from: active, to: active, actor: { parent: payment }
134
+ input { identity: text, principal: money, profit: money }
135
+ invoke: [{ instrument: settlement, action: create, input: { plan: { field: self.id }, identity: { field: input.identity }, principal: { field: input.principal }, profit: { field: input.profit }, loss: { literal: "0" }, kind: { literal: "cash" } } }]
136
+ }
137
+ action cancel { from: quoted, to: cancelled }
138
+ action void { from: signed, to: cancelled, actor: { party: borrower } }
139
+ records {
140
+ write_off_request: {
141
+ fields { plan: ref<parent>, overdueSlice: ref<slice> }
142
+ lifecycle { states: [requested, applied, cancelled], initial: requested }
143
+ action create { requires self.overdueSlice.plan == self.plan; requires self.plan in [active] }
144
+ action apply {
145
+ from: requested, to: applied, actor: { party: capital }
146
+ invoke: [{ reference: self.plan, action: write_off, input: { overdueSlice: { field: self.overdueSlice } } }]
147
+ }
148
+ action cancel { from: requested, to: cancelled }
149
+ }
150
+ payoff_quote: {
151
+ fields {
152
+ plan: ref<parent>, revision: text?, effectiveAt: date?, expiresAt: date = after(self.createdAt, payoff_quote_expiry)
153
+ principal: money?, earnedProfit: money?, unearnedProfit: money?, rebate: money?, fines: money?, costs: money?, gross: money?, amount: money?
154
+ }
155
+ lifecycle { states: [quoted], initial: quoted }
156
+ action create {
157
+ requires self.plan in [active]
158
+ set: { revision: { field: self.productRevision }, effectiveAt: { field: self.now } }
159
+ invoke: [
160
+ { selection: { instrument: slice, reference: "plan", anchor: self.plan, states: [pending], limit: 366 }, action: quote_pending, input: { quote: { field: self.id } } }
161
+ { selection: { instrument: slice, reference: "plan", anchor: self.plan, states: [due], limit: 366 }, action: quote_due, input: { quote: { field: self.id } } }
162
+ { reference: self.id, action: record, input: {} }
163
+ ]
164
+ }
165
+ action record {
166
+ from: quoted, to: quoted, actor: { parent: current() }
167
+ calculate: [
168
+ { target: principal, op: aggregate, selection: { instrument: slice, reference: "plan", anchor: self.plan, states: [pending, due, paid], limit: 366 }, measure: { sum: "principalReceivable.balance" } }
169
+ { target: earnedProfit, op: aggregate, selection: { instrument: slice, reference: "plan", anchor: self.plan, states: [pending, due, paid], limit: 366 }, measure: { sum: "profitEarned.balance" } }
170
+ { target: unearnedProfit, op: aggregate, selection: { instrument: slice, reference: "plan", anchor: self.plan, states: [pending, due, paid], limit: 366 }, measure: { sum: "profitReceivable.balance" } }
171
+ { target: rebate, op: aggregate, selection: { instrument: quote_piece, reference: "quote", anchor: self.id, states: [recorded], limit: 366 }, measure: { sum: "rebate" } }
172
+ { target: fines, op: aggregate, selection: { instrument: all(financing.late_charge), reference: "plan", anchor: self.plan, states: [assessed, collected], limit: 366 }, measure: { sum: "receivable.balance" } }
173
+ { target: costs, op: aggregate, selection: { instrument: all(financing.late_charge.cost), reference: "plan", anchor: self.plan, states: [assessed, collected], limit: 366 }, measure: { sum: "receivable.balance" } }
174
+ { target: gross, op: sum, values: [{ field: self.principal }, { field: self.earnedProfit }, { field: self.unearnedProfit }, { field: self.fines }, { field: self.costs }] }
175
+ { target: amount, op: subtract, base: { field: self.gross }, subtract: [{ field: self.rebate }] }
176
+ ]
177
+ }
178
+ }
179
+ quote_piece: {
180
+ fields { quote: ref<payoff_quote>, slice: ref<slice>, rebate: money = rate(self.slice.profitReceivable.balance, payoff_rebate) }
181
+ lifecycle { states: [recorded], initial: recorded }
182
+ action create { actor: { parent: slice }, input { quote: ref<payoff_quote>, slice: ref<slice> }, requires unique "payoff_slice_quote" on [self.quote, self.slice] }
183
+ }
184
+ amendment: {
185
+ fields {
186
+ plan: ref<parent>, dates: list(date, months), agreement: text, waiverPercent: percent, waiverEvidence: text, maximumWaiver: percent = waiver_limit
187
+ revision: text?, acceptedAt: date?, staleReason: text?, expiresAt: date = after(self.createdAt, amendment_expiry)
188
+ fineDisposition: enum(carry, require_waiver) = assessed_fines, addedProfit: money = 0 SAR
189
+ principalSnapshot: money?, earnedProfitSnapshot: money?, unearnedProfitSnapshot: money?, finesSnapshot: money?, costsSnapshot: money?
190
+ extension: duration = max_extension, maximum: integer = max_amendments
191
+ }
192
+ lifecycle { states: [proposed, applied, declined, withdrawn, expired, superseded], initial: proposed }
193
+ action create {
194
+ actor: { party: capital }
195
+ requires self.plan in [active]; requires self.waiverPercent <= self.maximumWaiver
196
+ set: { revision: { field: self.productRevision } }
197
+ calculate: [
198
+ { target: principalSnapshot, op: aggregate, selection: { instrument: slice, reference: "plan", anchor: self.plan, states: [pending, due, paid], limit: 366 }, measure: { sum: "principalReceivable.balance" } }
199
+ { target: earnedProfitSnapshot, op: aggregate, selection: { instrument: slice, reference: "plan", anchor: self.plan, states: [pending, due, paid], limit: 366 }, measure: { sum: "profitEarned.balance" } }
200
+ { target: unearnedProfitSnapshot, op: aggregate, selection: { instrument: slice, reference: "plan", anchor: self.plan, states: [pending, due, paid], limit: 366 }, measure: { sum: "profitReceivable.balance" } }
201
+ { target: finesSnapshot, op: aggregate, selection: { instrument: all(financing.late_charge), reference: "plan", anchor: self.plan, states: [assessed, collected], limit: 366 }, measure: { sum: "receivable.balance" } }
202
+ { target: costsSnapshot, op: aggregate, selection: { instrument: all(financing.late_charge.cost), reference: "plan", anchor: self.plan, states: [assessed, collected], limit: 366 }, measure: { sum: "receivable.balance" } }
203
+ ]
204
+ requires count of { instrument: current(), reference: "plan", anchor: self.plan, states: [applied], limit: 366 } < self.maximum
205
+ invoke: [{ range: { count: { literal: months }, maximum: months, bind: position }, instrument: proposed_date, action: create, input: { amendment: { field: self.id }, position: { field: position } } }, { selection: { instrument: proposed_date, reference: "amendment", anchor: self.id, states: [recorded], limit: 366, where: { first: { literal: 0 } } }, action: check_order, input: {} }]
206
+ }
207
+ action accept {
208
+ from: proposed, to: applied, actor: { party: borrower }, deadline: { at: self.expiresAt }
209
+ requires self.plan in [active]; requires self.revision == self.productRevision
210
+ requires count of { instrument: current(), reference: "plan", anchor: self.plan, states: [applied], limit: 366 } < self.maximum
211
+ set: { acceptedAt: { field: self.now } }
212
+ when assessed_fines is require_waiver { requires self.waiverPercent == 10000 }
213
+ invoke: [{ selection: { instrument: all(financing.late_charge), reference: "plan", anchor: self.plan, states: [assessed, collected], limit: 366 }, action: waive_for_amendment, input: { amendment: { field: self.id } } }, { selection: { instrument: all(financing.late_charge.cost), reference: "plan", anchor: self.plan, states: [assessed, collected], limit: 366 }, action: waive_for_amendment, input: { amendment: { field: self.id } } }]
214
+ invoke: [{ selection: { instrument: all(collections.case), reference: "plan", anchor: self.plan, states: [opened, assigned], limit: 366 }, action: recall, input: {} }, { selection: { instrument: all(financing.late_charge.cost), reference: "plan", anchor: self.plan, states: [proposed], limit: 366 }, action: cancel, input: {} }, { selection: { instrument: all(financing.late_charge), reference: "plan", anchor: self.plan, states: [proposed], limit: 366 }, action: cancel, input: {} }]
215
+ invoke: [{ selection: { instrument: slice, reference: "plan", anchor: self.plan, states: [pending, due], limit: 366, order: [position] }, action: amend, input: { amendment: { field: self.id } } }]
216
+ invoke: [{ selection: { instrument: slice, reference: "plan", anchor: self.plan, states: [paid], limit: 366 }, action: verify_fixed_date, input: { amendment: { field: self.id } } }]
217
+ }
218
+ action withdraw { from: proposed, to: withdrawn, actor: { party: capital } }
219
+ action decline { from: proposed, to: declined, actor: { party: borrower } }
220
+ action expire { from: proposed, to: expired, actor: clock, due: { at: self.expiresAt } }
221
+ action supersede { from: proposed, to: superseded, requires self.revision != self.productRevision, set: { staleReason: { literal: "economic_revision_changed" } } }
222
+ }
223
+ proposed_date: {
224
+ fields { amendment: ref<amendment>, position: integer(1, months), date: date = at(self.amendment.dates, self.position), first: integer = divide(1, self.position), prior: integer = subtract(self.position, 1), previousPosition: integer = sum(self.prior, self.first), previous: date = at(self.amendment.dates, self.previousPosition) }
225
+ lifecycle { states: [recorded], initial: recorded }
226
+ action create { actor: { parent: amendment }, input { amendment: ref<amendment>, position: integer(1, months) }, requires self.date >= self.previous }
227
+ action check_order { from: recorded, to: recorded, actor: { parent: amendment }, requires self.date > self.previous }
228
+ }
229
+ date_change: {
230
+ fields { amendment: ref<amendment>, slice: ref<slice>, oldDate: date, newDate: date }
231
+ lifecycle { states: [recorded], initial: recorded }
232
+ action create { actor: { parent: slice }, input { amendment: ref<amendment>, slice: ref<slice>, oldDate: date, newDate: date }, requires unique "amended_slice" on [self.amendment, self.slice] }
233
+ }
234
+ settlement: {
235
+ fields { plan: ref<parent>, identity: text, principal: money, profit: money, loss: money, kind: enum(cash, loss), share: money?, retainedProfit: money?
236
+ allocatedPayable: account(capital, cash, "allocatedProfitPayable"), capital: account(capital, cash, "capital"), profitIncome: account(capital, cash, "profitIncome"), lossAccount: account(capital, claim, "loss") }
237
+ lifecycle { states: [recorded, refunded], initial: recorded }
238
+ action create { allowZero: true,
239
+ actor: { parent: parent }
240
+ input { plan: ref<parent>, identity: text, principal: money, profit: money, loss: money, kind: enum(cash, loss) }
241
+ requires unique "settlement_identity" on [self.plan, self.identity]
242
+ calculate: [{ target: "share", op: "rate", base: { field: self.profit }, bps: { literal: share }, rounding: "floor" }, { target: retainedProfit, op: "subtract", base: { field: self.profit }, subtract: [{ field: self.share }] }]
243
+ moves self.share from self.profitIncome to self.allocatedPayable economics { purpose: internal, sourceParty: capital }
244
+ }
245
+ action refund { allowZero: true,
246
+ from: recorded, to: refunded, actor: { parent: payment }
247
+ moves self.share from self.allocatedPayable to self.profitIncome economics { purpose: internal, sourceParty: capital }
248
+ }
249
+ }
250
+ slice: {
251
+ revisioned: true
252
+ fields {
253
+ plan: ref<parent>, borrowerLimit: ref<limits>, portfolioLimit: ref<portfolio>, position: integer(1, months)
254
+ originalDueAt: date = at(self.plan.dates, self.position), dueAt: date?, nextDueAt: date?, previousDueAt: date?, previousPosition: integer?, previousDate: date?, extensionLimit: date = after(self.originalDueAt, max_extension), lossEligibleAt: date?
255
+ principalBase: money = divide(self.plan.principal, months)
256
+ principalAllocated: money = multiply(self.principalBase, months)
257
+ principalRemainder: money = subtract(self.plan.principal, self.principalAllocated)
258
+ profitBase: money = divide(self.plan.profit, months)
259
+ profitAllocated: money = multiply(self.profitBase, months)
260
+ profitRemainder: money = subtract(self.plan.profit, self.profitAllocated)
261
+ first: integer = divide(1, self.position)
262
+ principalAdjustment: money = multiply(self.principalRemainder, self.first)
263
+ profitAdjustment: money = multiply(self.profitRemainder, self.first)
264
+ principal: money = sum(self.principalBase, self.principalAdjustment)
265
+ profit: money = sum(self.profitBase, self.profitAdjustment)
266
+ principalReceivable: account(self, claim), profitReceivable: account(self, claim), profitEarned: account(self, claim)
267
+ payoffIdentity: text?, payoffRebate: money?, payoffProfitTotal: money?, payoffProfit: money?
268
+ paymentBudget: money?, principalPayment: money?, principalBasePayment: money?, principalRatio: money?, profitPayment: money?, profitBudget: money?, collectibleProfit: money?, collectibleTotal: money?, ratioDenominator: integer = sum(10000, profit)
269
+ debt: account(borrower, claim, contra, "debt"), loss: account(capital, claim, "loss")
270
+ }
271
+ lifecycle { states: [draft, pending, due, paid, written_off], initial: draft }
272
+ action create {
273
+ actor: { parent: parent }
274
+ input { plan: ref<parent>, borrowerLimit: ref<limits>, portfolioLimit: ref<portfolio>, position: integer(1, months) }
275
+ set: { dueAt: { field: self.originalDueAt } }
276
+ invoke: [{ reference: self.id, action: refresh_loss_date, input: {} }]
277
+ requires self.plan in [quoted]; requires unique "signed_position" on [self.plan, self.position]; requires self.borrowerLimit == self.plan.borrowerLimit; requires self.portfolioLimit == self.plan.portfolioLimit
278
+ }
279
+ action quote_pending {
280
+ from: pending, to: pending, actor: { parent: payoff_quote }, input { quote: ref<payoff_quote> }
281
+ requires input.quote.plan == self.plan
282
+ invoke: [{ instrument: quote_piece, action: create, input: { quote: { field: input.quote }, slice: { field: self.id } } }]
283
+ }
284
+ action quote_due {
285
+ from: due, to: due, actor: { parent: payoff_quote }, input { quote: ref<payoff_quote> }
286
+ requires input.quote.plan == self.plan
287
+ invoke: [{ instrument: quote_piece, action: create, input: { quote: { field: input.quote }, slice: { field: self.id } } }]
288
+ }
289
+ action refresh_loss_date {
290
+ from: [draft, pending, due], to: draft, actor: { parent: current() }
291
+ calculate: [{ target: lossEligibleAt, op: shift, date: { field: self.dueAt }, milliseconds: { literal: write_off_after }, direction: after }]
292
+ }
293
+ action amend {
294
+ from: [pending, due], to: pending, actor: { parent: amendment }, input { amendment: ref<amendment> }
295
+ requires input.amendment.plan == self.plan
296
+ when allow_overdue is blocked { requires self.dueAt > self.now }
297
+ calculate: [
298
+ { target: nextDueAt, op: at, list: input.amendment.dates, position: { field: self.position } }
299
+ { target: lossEligibleAt, op: shift, date: { field: self.nextDueAt }, milliseconds: { literal: write_off_after }, direction: after }
300
+ ]
301
+ requires self.nextDueAt > self.now; requires self.nextDueAt >= self.dueAt; requires self.nextDueAt <= self.extensionLimit
302
+ set: { previousDueAt: { field: self.dueAt }, dueAt: { field: self.nextDueAt } }
303
+ invoke: [{ instrument: date_change, action: create, input: { amendment: { field: input.amendment }, slice: { field: self.id }, oldDate: { field: self.previousDueAt }, newDate: { field: self.nextDueAt } } }]
304
+ when profit_earned is by_schedule { invoke: [{ selection: { instrument: recognition, reference: "slice", anchor: self.id, states: [pending], limit: 1 }, action: reschedule, input: {} }] }
305
+ }
306
+ action verify_fixed_date {
307
+ from: paid, to: paid, actor: { parent: amendment }, input { amendment: ref<amendment> }
308
+ calculate: [{ target: nextDueAt, op: at, list: input.amendment.dates, position: { field: self.position } }]
309
+ requires self.nextDueAt == self.dueAt
310
+ }
311
+ action activate { allowZero: true,
312
+ from: draft, to: pending, actor: { parent: parent }
313
+ moves self.principal from self.debt to self.principalReceivable economics { purpose: internal, sourceParty: borrower }
314
+ moves self.profit from self.debt to self.profitReceivable economics { purpose: internal, sourceParty: borrower }
315
+ when profit_earned is at_disbursement { moves self.profit from self.profitReceivable to self.profitEarned economics { purpose: internal, sourceParty: programOperator } }
316
+ when profit_earned is by_schedule { invoke: [{ instrument: recognition, action: create, input: { slice: { field: self.id } } }] }
317
+ }
318
+ action mature {
319
+ from: pending, to: due, actor: clock, due: { at: self.dueAt }
320
+ }
321
+ action collect {
322
+ from: [pending, due], to: preserve, actor: { parent: payment }
323
+ input { payment: ref<payment> }
324
+ requires input.payment.plan == self.plan
325
+ when profit_earned is on_payment { calculate: [{ target: collectibleProfit, op: sum, values: [{ field: self.profitReceivable.balance }] }] }
326
+ when profit_earned is by_schedule { calculate: [{ target: collectibleProfit, op: sum, values: [{ field: self.profitEarned.balance }] }] }
327
+ when profit_earned is at_disbursement { calculate: [{ target: collectibleProfit, op: sum, values: [{ field: self.profitEarned.balance }] }] }
328
+ when apply is principal_profit {
329
+ invoke: [
330
+ { instrument: principal_piece, action: create, input: { slice: { field: self.id }, payment: { field: input.payment }, limit: { field: self.principalReceivable.balance } } }
331
+ { instrument: profit_piece, action: create, input: { slice: { field: self.id }, payment: { field: input.payment }, limit: { field: self.collectibleProfit } } }
332
+ { selection: { instrument: current(), reference: "plan", anchor: self.plan, states: [pending, due], limit: 1, where: { id: { field: self.id }, principalReceivable.balance: { literal: "0" }, profitReceivable.balance: { literal: "0" }, profitEarned.balance: { literal: "0" } } }, action: complete, input: {} }
333
+ ]
334
+ }
335
+ }
336
+ action collect_waterfall {
337
+ from: [pending, due], to: preserve, actor: { parent: payment }, input { payment: ref<payment> }
338
+ requires input.payment.plan == self.plan
339
+ when profit_earned is on_payment { calculate: [{ target: collectibleProfit, op: sum, values: [{ field: self.profitReceivable.balance }] }] }
340
+ when profit_earned is by_schedule { calculate: [{ target: collectibleProfit, op: sum, values: [{ field: self.profitEarned.balance }] }] }
341
+ when profit_earned is at_disbursement { calculate: [{ target: collectibleProfit, op: sum, values: [{ field: self.profitEarned.balance }] }] }
342
+ invoke: [
343
+ { selection: { instrument: all(financing.late_charge), reference: "slice", anchor: self.id, states: [assessed], limit: charge_limit, order: [overdueAt] }, action: collect, input: { payment: { field: input.payment } } }
344
+ { selection: { instrument: all(financing.late_charge.cost), reference: "plan", anchor: self.plan, states: [assessed], limit: charge_limit, order: [overdueAt], where: { assessment.slice: { field: self.id } } }, action: collect, input: { payment: { field: input.payment } } }
345
+ { instrument: profit_piece, action: create, input: { slice: { field: self.id }, payment: { field: input.payment }, limit: { field: self.collectibleProfit } } }
346
+ { instrument: principal_piece, action: create, input: { slice: { field: self.id }, payment: { field: input.payment }, limit: { field: self.principalReceivable.balance } } }
347
+ { selection: { instrument: current(), reference: "plan", anchor: self.plan, states: [pending, due], limit: 1, where: { id: { field: self.id }, principalReceivable.balance: { literal: "0" }, profitReceivable.balance: { literal: "0" }, profitEarned.balance: { literal: "0" } } }, action: complete, input: {} }
348
+ ]
349
+ }
350
+ action collect_pro_rata {
351
+ from: [pending, due], to: preserve, actor: { parent: payment }, input { payment: ref<payment> }
352
+ requires input.payment.plan == self.plan
353
+ when profit_earned is on_payment { calculate: [{ target: collectibleProfit, op: sum, values: [{ field: self.profitReceivable.balance }] }] }
354
+ when profit_earned is by_schedule { calculate: [{ target: collectibleProfit, op: sum, values: [{ field: self.profitEarned.balance }] }] }
355
+ when profit_earned is at_disbursement { calculate: [{ target: collectibleProfit, op: sum, values: [{ field: self.profitEarned.balance }] }] }
356
+ calculate: [
357
+ { target: collectibleTotal, op: sum, values: [{ field: self.principalReceivable.balance }, { field: self.collectibleProfit }] }
358
+ { target: paymentBudget, op: minimum, values: [{ field: input.payment.held.balance }, { field: self.collectibleTotal }] }
359
+ { target: principalRatio, op: ratio, amount: { field: self.paymentBudget }, numerator: { literal: 10000 }, denominator: { field: self.ratioDenominator }, rounding: floor }
360
+ { target: principalBasePayment, op: minimum, values: [{ field: self.principalRatio }, { field: self.principalReceivable.balance }] }
361
+ { target: profitBudget, op: subtract, base: { field: self.paymentBudget }, subtract: [{ field: self.principalBasePayment }] }
362
+ { target: profitPayment, op: minimum, values: [{ field: self.profitBudget }, { field: self.collectibleProfit }] }
363
+ { target: principalPayment, op: subtract, base: { field: self.paymentBudget }, subtract: [{ field: self.profitPayment }] }
364
+ ]
365
+ invoke: [
366
+ { instrument: principal_piece, action: create, input: { slice: { field: self.id }, payment: { field: input.payment }, limit: { field: self.principalPayment } } }
367
+ { instrument: profit_piece, action: create, input: { slice: { field: self.id }, payment: { field: input.payment }, limit: { field: self.profitPayment } } }
368
+ { selection: { instrument: current(), reference: "plan", anchor: self.plan, states: [pending, due], limit: 1, where: { id: { field: self.id }, principalReceivable.balance: { literal: "0" }, profitReceivable.balance: { literal: "0" }, profitEarned.balance: { literal: "0" } } }, action: complete, input: {} }
369
+ ]
370
+ }
371
+ action complete {
372
+ from: [pending, due], to: paid
373
+ requires self.principalReceivable.balance == "0"; requires self.profitReceivable.balance == "0"; requires self.profitEarned.balance == "0"
374
+ }
375
+ action restore { from: [pending, due, paid], to: pending, actor: { parent: payment } }
376
+ action payoff { allowZero: true,
377
+ from: [pending, due], to: paid, actor: { parent: parent }, input { identity: text }
378
+ set: { payoffIdentity: { field: input.identity } }
379
+ calculate: [{ target: payoffRebate, op: rate, base: { field: self.profitReceivable.balance }, bps: { literal: payoff_rebate }, rounding: floor }, { target: payoffProfitTotal, op: sum, values: [{ field: self.profitReceivable.balance }, { field: self.profitEarned.balance }] }, { target: payoffProfit, op: subtract, base: { field: self.payoffProfitTotal }, subtract: [{ field: self.payoffRebate }] }]
380
+ moves self.principalReceivable.balance from self.plan.borrower to self.plan.capital economics { purpose: principal, sourceParty: borrower }
381
+ moves self.payoffProfit from self.plan.borrower to self.plan.profitIncome economics { recipient: capital, company: { purpose: earning, sourceParty: borrower }, participant: { purpose: pass_through, sourceParty: borrower } }
382
+ moves self.principalReceivable.balance from self.principalReceivable to self.debt economics { purpose: internal, sourceParty: programOperator }
383
+ moves self.profitReceivable.balance from self.profitReceivable to self.debt economics { purpose: internal, sourceParty: programOperator }
384
+ moves self.profitEarned.balance from self.profitEarned to self.debt economics { purpose: internal, sourceParty: programOperator }
385
+ }
386
+ action write_off { allowZero: true,
387
+ from: [pending, due], to: written_off, actor: { parent: parent }
388
+ moves self.principalReceivable.balance from self.principalReceivable to self.loss economics { purpose: internal, sourceParty: programOperator }; moves self.profitReceivable.balance from self.profitReceivable to self.debt economics { purpose: internal, sourceParty: programOperator }; moves self.profitEarned.balance from self.profitEarned to self.loss economics { purpose: internal, sourceParty: programOperator }
389
+ }
390
+ }
391
+ recognition: {
392
+ fields { slice: ref<slice>, dueAt: date = after(self.slice.dueAt, 0) }
393
+ lifecycle { states: [pending, recognised], initial: pending }
394
+ action create { actor: { parent: slice }, input { slice: ref<slice> }, requires unique "profit_recognition" on [self.slice] }
395
+ action reschedule { from: pending, to: pending, actor: { parent: slice }, calculate: [{ target: dueAt, op: shift, date: { field: self.slice.dueAt }, milliseconds: { literal: 0 }, direction: after }] }
396
+ action mature { allowZero: true, from: pending, to: recognised, actor: clock, due: { at: self.dueAt }, moves self.slice.profitReceivable.balance from self.slice.profitReceivable to self.slice.profitEarned economics { purpose: internal, sourceParty: programOperator } }
397
+ }
398
+ payment: {
399
+ revisioned: true
400
+ fields { plan: ref<parent>, amount: money, paymentIdentity: text, held: account of self
401
+ outstandingPrincipal: money?, outstandingProfit: money?, outstandingFines: money?, outstandingCosts: money?, outstandingTotal: money?, principalReturned: money?, profitReturned: money?, lossRecorded: money? }
402
+
403
+ lifecycle { states: [pending, paid, refunded], initial: pending }
404
+ action create { input { plan: ref<parent>, amount: money, paymentIdentity: text } requires self.plan in [active]; requires unique "payment_identity" on [self.plan, self.paymentIdentity] }
405
+ action pay {
406
+ expansionLimit: 8192
407
+ from: pending, to: paid, actor: { party: borrower }
408
+ calculate: [{ target: outstandingPrincipal, op: aggregate, selection: { instrument: slice, reference: "plan", anchor: self.plan, states: [pending, due, paid], limit: 366, order: [position] }, measure: { sum: "principalReceivable.balance" } }]
409
+ when profit_earned is on_payment { calculate: [{ target: outstandingProfit, op: aggregate, selection: { instrument: slice, reference: "plan", anchor: self.plan, states: [pending, due, paid], limit: 366, order: [position] }, measure: { sum: "profitReceivable.balance" } }] }
410
+ when profit_earned is by_schedule { calculate: [{ target: outstandingProfit, op: aggregate, selection: { instrument: slice, reference: "plan", anchor: self.plan, states: [pending, due, paid], limit: 366, order: [position] }, measure: { sum: "profitEarned.balance" } }] }
411
+ when profit_earned is at_disbursement { calculate: [{ target: outstandingProfit, op: aggregate, selection: { instrument: slice, reference: "plan", anchor: self.plan, states: [pending, due, paid], limit: 366, order: [position] }, measure: { sum: "profitEarned.balance" } }] }
412
+ when apply is fines_profit_principal {
413
+ calculate: [{ target: outstandingFines, op: aggregate, selection: { instrument: all(financing.late_charge), reference: "plan", anchor: self.plan, states: [assessed, collected], limit: 366, order: [overdueAt] }, measure: { sum: "receivable.balance" } }, { target: outstandingCosts, op: aggregate, selection: { instrument: all(financing.late_charge.cost), reference: "plan", anchor: self.plan, states: [assessed, collected], limit: 366, order: [overdueAt] }, measure: { sum: "receivable.balance" } }, { target: outstandingTotal, op: sum, values: [{ field: self.outstandingPrincipal }, { field: self.outstandingProfit }, { field: self.outstandingFines }, { field: self.outstandingCosts }] }]
414
+ }
415
+ when apply is principal_profit { calculate: [{ target: outstandingTotal, op: sum, values: [{ field: self.outstandingPrincipal }, { field: self.outstandingProfit }] }] }
416
+ when apply is pro_rata { calculate: [{ target: outstandingTotal, op: sum, values: [{ field: self.outstandingPrincipal }, { field: self.outstandingProfit }] }] }
417
+ requires self.plan in [active]; requires self.amount <= self.outstandingTotal
418
+ moves self.amount from self.plan.borrower to self.held economics { purpose: principal, sourceParty: borrower }
419
+ when apply is principal_profit {
420
+ invoke: [{ selection: { instrument: slice, reference: "plan", anchor: self.plan, states: [pending, due], limit: 366, order: [position] }, action: collect, guard: { kind: compare, left: { field: self.held.balance }, operator: ">", right: { literal: "0" } }, input: { payment: { field: self.id } } }]
421
+ }
422
+ when apply is fines_profit_principal {
423
+ invoke: [{ selection: { instrument: slice, reference: "plan", anchor: self.plan, states: [pending, due], limit: 366, order: [position] }, action: collect_waterfall, guard: { kind: compare, left: { field: self.held.balance }, operator: ">", right: { literal: "0" } }, input: { payment: { field: self.id } } }]
424
+ }
425
+ when apply is pro_rata {
426
+ invoke: [{ selection: { instrument: slice, reference: "plan", anchor: self.plan, states: [pending, due], limit: 366, order: [position] }, action: collect_pro_rata, guard: { kind: compare, left: { field: self.held.balance }, operator: ">", right: { literal: "0" } }, input: { payment: { field: self.id } } }]
427
+ }
428
+ invoke: [{ reference: self.id, action: record, input: {} }]
429
+ }
430
+ action record {
431
+ from: paid, to: paid, actor: { parent: current() }
432
+ requires self.held.balance == "0"
433
+ calculate: [{ target: principalReturned, op: aggregate, selection: { instrument: principal_piece, reference: "payment", anchor: self.id, states: [paid], limit: 366 }, measure: { sum: "amount" } }, { target: profitReturned, op: aggregate, selection: { instrument: profit_piece, reference: "payment", anchor: self.id, states: [paid], limit: 366 }, measure: { sum: "amount" } }]
434
+ invoke: [{ reference: self.plan, action: record_payment, input: { identity: { field: self.paymentIdentity }, principal: { field: self.principalReturned }, profit: { field: self.profitReturned } } }]
435
+ }
436
+ action refund {
437
+ from: paid, to: refunded, actor: { party: capital }
438
+ requires unique "settlement_consumption" on [self.plan, self.paymentIdentity]; requires self.plan in [active, paid]
439
+ requires count of { instrument: settlement, reference: "plan", anchor: self.plan, states: [recorded], limit: 366, where: { identity: { field: self.paymentIdentity } } } == 1
440
+ invoke: [{ selection: { instrument: settlement, reference: "plan", anchor: self.plan, states: [recorded], limit: 366, where: { identity: { field: self.paymentIdentity } } }, action: refund, input: {} }]
441
+ when apply is fines_profit_principal {
442
+ invoke: [{ selection: { instrument: all(financing.late_charge), reference: "plan", anchor: self.plan, states: [assessed, collected], limit: 366, order: [overdueAt] }, action: refund, input: { payment: { field: self.id } } }, { selection: { instrument: all(financing.late_charge.cost), reference: "plan", anchor: self.plan, states: [assessed, collected], limit: 366, order: [overdueAt] }, action: refund, input: { payment: { field: self.id } } }]
443
+ }
444
+ invoke: [{ reference: self.plan, action: restore, input: {} }, { selection: { instrument: slice, reference: "plan", anchor: self.plan, states: [pending, due, paid], limit: 366 }, action: restore, input: {} }, { selection: { instrument: principal_piece, reference: "payment", anchor: self.id, states: [paid], limit: 366 }, action: refund, input: {} }, { selection: { instrument: profit_piece, reference: "payment", anchor: self.id, states: [paid], limit: 366 }, action: refund, input: {} }]
445
+ }
446
+ }
447
+ principal_piece: {
448
+ fields { slice: ref<slice>, payment: ref<payment>, limit: money, amount: money? }
449
+ lifecycle { states: [paid, refunded], initial: paid }
450
+ action create { allowZero: true,
451
+ calculate: [{ target: amount, op: minimum, values: [{ field: self.payment.held.balance }, { field: self.slice.principalReceivable.balance }, { field: self.limit }] }]
452
+ actor: { parent: slice }
453
+ input { slice: ref<slice>, payment: ref<payment>, limit: money }
454
+ requires unique "principal_receipt" on [self.slice, self.payment]
455
+ moves self.amount from self.payment.held to self.slice.plan.capital economics { purpose: principal, sourceParty: programOperator }; moves self.amount from self.slice.principalReceivable to self.slice.debt economics { purpose: internal, sourceParty: programOperator }
456
+ }
457
+ action refund { allowZero: true,
458
+ from: paid, to: refunded, actor: { parent: payment }
459
+ moves self.amount from self.slice.plan.capital to self.slice.plan.borrower economics { purpose: principal, sourceParty: capital }; moves self.amount from self.slice.debt to self.slice.principalReceivable economics { purpose: internal, sourceParty: borrower }
460
+ }
461
+ }
462
+ profit_piece: {
463
+ fields { cashReceipt: text?, slice: ref<slice>, payment: ref<payment>, limit: money, amount: money? }
464
+ lifecycle { states: [paid, refunded], initial: paid }
465
+ action create { allowZero: true,
466
+ calculate: [{ target: amount, op: minimum, values: [{ field: self.payment.held.balance }, { field: self.limit }] }]
467
+ actor: { parent: slice }
468
+ input { slice: ref<slice>, payment: ref<payment>, limit: money }
469
+ requires unique "profit_receipt" on [self.slice, self.payment]
470
+ moves self.amount from self.payment.held to self.slice.plan.profitIncome capture "cashReceipt" economics { recipient: capital, company: { purpose: earning, sourceParty: borrower }, participant: { purpose: participant_payout, sourceParty: programOperator } }
471
+ when profit_earned is on_payment { moves self.amount from self.slice.profitReceivable to self.slice.debt economics { purpose: internal, sourceParty: programOperator } }
472
+ when profit_earned is by_schedule { moves self.amount from self.slice.profitEarned to self.slice.debt economics { purpose: internal, sourceParty: programOperator } }
473
+ when profit_earned is at_disbursement { moves self.amount from self.slice.profitEarned to self.slice.debt economics { purpose: internal, sourceParty: programOperator } }
474
+ }
475
+ action refund { allowZero: true,
476
+ from: paid, to: refunded, actor: { parent: payment }
477
+ moves self.amount from self.slice.plan.profitIncome to self.slice.plan.borrower economics { recipient: capital, company: { purpose: earning, sourceParty: capital, reversalOf: self.cashReceipt }, participant: unclassified }
478
+ when profit_earned is on_payment { moves self.amount from self.slice.debt to self.slice.profitReceivable economics { purpose: internal, sourceParty: borrower } }
479
+ when profit_earned is by_schedule { moves self.amount from self.slice.debt to self.slice.profitEarned economics { purpose: internal, sourceParty: borrower } }
480
+ when profit_earned is at_disbursement { moves self.amount from self.slice.debt to self.slice.profitEarned economics { purpose: internal, sourceParty: borrower } }
481
+ }
482
+ }
483
+ }
484
+ }
485
+
486
+ instrument late_charge(on: ref<financing.installments>[], grace: duration = 3d, fine: money = 50 SAR, cap: money = 25 SAR, fines_to: party = programOperator, costs_to: party = programOperator, borrower: party = party(person)) {
487
+ parameterDiagnostics {
488
+ fine {
489
+ accepts: "a fixed amount"
490
+ percentage: "a percentage of overdue debt"
491
+ fix: "Use a fixed amount only if that is the intended policy. A percentage charge needs an authored rate calculation; do not approximate it with a cash amount."
492
+ }
493
+ }
494
+ revisioned: true
495
+ familyRevision: 1
496
+ summary: "Separate fine and evidenced recovery claims on an overdue installment."
497
+ fields {
498
+ plan: ref<on>, slice: ref<on.slice>, borrower: account of borrower, fine: money = fine, overdueAt: date?
499
+ receivable: account(self, claim), debt: account(borrower, claim, contra, "debt")
500
+ }
501
+ lifecycle { states: [proposed, assessed, collected, waived], initial: proposed }
502
+ action create { input { slice: ref<on.slice>? }, calculate: [{ target: overdueAt, op: shift, date: { field: self.slice.dueAt }, milliseconds: { literal: grace }, direction: after }], requires unique "late_assessment" on [self.slice, self.overdueAt]; requires self.borrower == self.slice.plan.borrower; requires self.plan == self.slice.plan }
503
+ action assess { allowZero: true,
504
+ from: proposed, to: assessed, actor: clock, due: { at: self.overdueAt }
505
+ requires self.slice.principalReceivable.balance > "0"
506
+ moves self.fine from self.debt to self.receivable economics { purpose: internal, sourceParty: borrower }
507
+ }
508
+ action collect {
509
+ from: [assessed, collected], to: assessed, actor: { parent: child(on, slice) }, input { payment: ref<on.payment> }
510
+ requires input.payment in [paid]
511
+ requires input.payment.plan == self.plan
512
+ invoke: [{ instrument: receipt, action: create, input: { assessment: { field: self.id }, payment: { field: input.payment } } }, { selection: { instrument: current(), reference: "plan", anchor: self.plan, states: [assessed], limit: 1, where: { id: { field: self.id }, receivable.balance: { literal: "0" } } }, action: finish, input: {} }]
513
+ }
514
+ action payoff { allowZero: true,
515
+ from: [assessed, collected], to: collected, actor: { parent: on }
516
+ requires self.plan in [paid]
517
+ moves self.receivable.balance from self.plan.borrower to fines_to economics { recipient: fines_to, company: { purpose: earning, sourceParty: borrower }, participant: { purpose: pass_through, sourceParty: borrower } }
518
+ moves self.receivable.balance from self.receivable to self.debt economics { purpose: internal, sourceParty: programOperator }
519
+ }
520
+ action finish { from: assessed, to: collected, actor: { parent: current() }, requires self.receivable.balance == "0" }
521
+ action cancel { from: proposed, to: waived }
522
+ action waive_for_amendment {
523
+ from: [assessed, collected], to: assessed, actor: { parent: child(on, amendment) }, input { amendment: ref<on.amendment> }
524
+ requires input.amendment.plan == self.plan; requires input.amendment in [applied]
525
+ invoke: [{ instrument: waiver, action: create, input: { assessment: { field: self.id }, amendment: { field: input.amendment } } }]
526
+ }
527
+ action waive { allowZero: true, from: [assessed, collected], to: waived, actor: { parent: waiver_request }, input { reason: text, evidence: text }, moves self.receivable.balance from self.receivable to self.debt economics { purpose: internal, sourceParty: programOperator } }
528
+ action refund {
529
+ from: [assessed, collected], to: assessed, actor: { party: fines_to }, input { payment: ref<on.payment> }
530
+ requires input.payment in [paid, refunded]
531
+ requires input.payment.plan == self.plan
532
+ invoke: [{ selection: { instrument: receipt, reference: "payment", anchor: input.payment, states: [paid], limit: 1, where: { assessment: { field: self.id } } }, action: refund, input: {} }, { selection: { instrument: current(), reference: "plan", anchor: self.plan, states: [assessed], limit: 1, where: { id: { field: self.id }, receivable.balance: { literal: "0" } } }, action: finish, input: {} }]
533
+ }
534
+ records {
535
+ waiver_request: {
536
+ fields { assessment: ref<parent>, reason: text, evidence: text }
537
+ lifecycle { states: [requested, applied, cancelled], initial: requested }
538
+ action create { requires self.assessment in [assessed, collected] }
539
+ action apply {
540
+ from: requested, to: applied, actor: { party: fines_to }, input { reason: text, evidence: text }
541
+ invoke: [{ reference: self.assessment, action: waive, input: { reason: { field: input.reason }, evidence: { field: input.evidence } } }]
542
+ }
543
+ action cancel { from: requested, to: cancelled }
544
+ }
545
+ waiver: {
546
+ fields { assessment: ref<parent>, amendment: ref<on.amendment>, amount: money? }
547
+ lifecycle { states: [recorded], initial: recorded }
548
+ action create { allowZero: true,
549
+ actor: { parent: parent }, input { assessment: ref<parent>, amendment: ref<on.amendment> }
550
+ calculate: [{ target: amount, op: rate, base: { field: self.assessment.receivable.balance }, bps: { field: self.amendment.waiverPercent }, rounding: floor }]
551
+ requires unique "amendment_waiver" on [self.assessment, self.amendment]
552
+ moves self.amount from self.assessment.receivable to self.assessment.debt economics { purpose: internal, sourceParty: programOperator }
553
+ }
554
+ }
555
+ receipt: {
556
+ fields { cashReceipt: text?, assessment: ref<parent>, payment: ref<on.payment>, amount: money? }
557
+ lifecycle { states: [paid, refunded], initial: paid }
558
+ action create { allowZero: true, calculate: [{ target: amount, op: minimum, values: [{ field: self.payment.held.balance }, { field: self.assessment.receivable.balance }] }], actor: { parent: parent }, input { assessment: ref<parent>, payment: ref<on.payment> }, requires input.payment.plan == self.assessment.plan; requires unique "fine_receipt" on [self.assessment, self.payment]; moves self.amount from self.payment.held to fines_to capture "cashReceipt" economics { recipient: fines_to, company: { purpose: earning, sourceParty: borrower }, participant: { purpose: participant_payout, sourceParty: programOperator } }; moves self.amount from self.assessment.receivable to self.assessment.debt economics { purpose: internal, sourceParty: programOperator } }
559
+ action refund { allowZero: true, from: paid, to: refunded, actor: { parent: parent }, moves self.amount from fines_to to self.payment.plan.borrower economics { recipient: fines_to, company: { purpose: earning, sourceParty: fines_to, reversalOf: self.cashReceipt }, participant: unclassified }; moves self.amount from self.assessment.debt to self.assessment.receivable economics { purpose: internal, sourceParty: borrower } }
560
+ }
561
+
562
+ cost: {
563
+ revisioned: true
564
+ fields {
565
+ plan: ref<on>, assessment: ref<parent>, overdueAt: date?, evidence: text, evidencedCost: money, amount: money = minimum(self.evidencedCost, cap)
566
+ receivable: account(self, claim), debt: account(borrower, claim, contra, "debt")
567
+ }
568
+ lifecycle { states: [proposed, assessed, collected, waived], initial: proposed }
569
+ action create { calculate: [{ target: overdueAt, op: shift, date: { field: self.assessment.overdueAt }, milliseconds: { literal: 0 }, direction: after }], requires unique "recovery_assessment" on [self.assessment]; requires self.plan == self.assessment.plan }
570
+ action assess { allowZero: true,
571
+ from: proposed, to: assessed, actor: clock, due: { at: self.assessment.overdueAt }
572
+ requires self.assessment.slice.principalReceivable.balance > "0"
573
+ moves self.amount from self.debt to self.receivable economics { purpose: internal, sourceParty: borrower }
574
+ }
575
+ action collect {
576
+ from: [assessed, collected], to: assessed, actor: { parent: child(on, slice) }, input { payment: ref<on.payment> }
577
+ requires input.payment in [paid]
578
+ requires input.payment.plan == self.plan
579
+ invoke: [{ instrument: receipt, action: create, input: { assessment: { field: self.id }, payment: { field: input.payment } } }, { selection: { instrument: current(), reference: "plan", anchor: self.plan, states: [assessed], limit: 1, where: { id: { field: self.id }, receivable.balance: { literal: "0" } } }, action: finish, input: {} }]
580
+ }
581
+ action payoff { allowZero: true,
582
+ from: [assessed, collected], to: collected, actor: { parent: on }
583
+ requires self.plan in [paid]
584
+ moves self.receivable.balance from self.plan.borrower to costs_to economics { recipient: costs_to, company: { purpose: earning, sourceParty: borrower }, participant: { purpose: pass_through, sourceParty: borrower } }
585
+ moves self.receivable.balance from self.receivable to self.debt economics { purpose: internal, sourceParty: programOperator }
586
+ }
587
+ action finish { from: assessed, to: collected, actor: { parent: current() }, requires self.receivable.balance == "0" }
588
+ action cancel { from: proposed, to: waived }
589
+ action waive_for_amendment {
590
+ from: [assessed, collected], to: assessed, actor: { parent: child(on, amendment) }, input { amendment: ref<on.amendment> }
591
+ requires input.amendment.plan == self.plan; requires input.amendment in [applied]
592
+ invoke: [{ instrument: waiver, action: create, input: { assessment: { field: self.id }, amendment: { field: input.amendment } } }]
593
+ }
594
+ action waive { allowZero: true, from: [assessed, collected], to: waived, actor: { parent: waiver_request }, input { reason: text, evidence: text }, moves self.receivable.balance from self.receivable to self.debt economics { purpose: internal, sourceParty: programOperator } }
595
+ action refund {
596
+ from: [assessed, collected], to: assessed, actor: { party: costs_to }, input { payment: ref<on.payment> }
597
+ requires input.payment in [paid, refunded]
598
+ requires input.payment.plan == self.plan
599
+ invoke: [{ selection: { instrument: receipt, reference: "payment", anchor: input.payment, states: [paid], limit: 1, where: { assessment: { field: self.id } } }, action: refund, input: {} }, { selection: { instrument: current(), reference: "plan", anchor: self.plan, states: [assessed], limit: 1, where: { id: { field: self.id }, receivable.balance: { literal: "0" } } }, action: finish, input: {} }]
600
+ }
601
+ records {
602
+ waiver_request: {
603
+ fields { assessment: ref<parent>, reason: text, evidence: text }
604
+ lifecycle { states: [requested, applied, cancelled], initial: requested }
605
+ action create { requires self.assessment in [assessed, collected] }
606
+ action apply {
607
+ from: requested, to: applied, actor: { party: costs_to }, input { reason: text, evidence: text }
608
+ invoke: [{ reference: self.assessment, action: waive, input: { reason: { field: input.reason }, evidence: { field: input.evidence } } }]
609
+ }
610
+ action cancel { from: requested, to: cancelled }
611
+ }
612
+ waiver: {
613
+ fields { assessment: ref<parent>, amendment: ref<on.amendment>, amount: money? }
614
+ lifecycle { states: [recorded], initial: recorded }
615
+ action create { allowZero: true,
616
+ actor: { parent: parent }, input { assessment: ref<parent>, amendment: ref<on.amendment> }
617
+ calculate: [{ target: amount, op: rate, base: { field: self.assessment.receivable.balance }, bps: { field: self.amendment.waiverPercent }, rounding: floor }]
618
+ requires unique "amendment_waiver" on [self.assessment, self.amendment]
619
+ moves self.amount from self.assessment.receivable to self.assessment.debt economics { purpose: internal, sourceParty: programOperator }
620
+ }
621
+ }
622
+ receipt: {
623
+ fields { cashReceipt: text?, assessment: ref<parent>, payment: ref<on.payment>, amount: money? }
624
+ lifecycle { states: [paid, refunded], initial: paid }
625
+ action create { allowZero: true, calculate: [{ target: amount, op: minimum, values: [{ field: self.payment.held.balance }, { field: self.assessment.receivable.balance }] }], actor: { parent: parent }, input { assessment: ref<parent>, payment: ref<on.payment> }, requires input.payment.plan == self.assessment.plan; requires unique "cost_receipt" on [self.assessment, self.payment]; moves self.amount from self.payment.held to costs_to capture "cashReceipt" economics { recipient: costs_to, company: { purpose: earning, sourceParty: borrower }, participant: { purpose: participant_payout, sourceParty: programOperator } }; moves self.amount from self.assessment.receivable to self.assessment.debt economics { purpose: internal, sourceParty: programOperator } }
626
+ action refund { allowZero: true, from: paid, to: refunded, actor: { parent: parent }, moves self.amount from costs_to to self.payment.plan.borrower economics { recipient: costs_to, company: { purpose: earning, sourceParty: costs_to, reversalOf: self.cashReceipt }, participant: unclassified }; moves self.amount from self.assessment.debt to self.assessment.receivable economics { purpose: internal, sourceParty: borrower } }
627
+ }
628
+ }
629
+ }
630
+ }
631
+ }
632
+
633
+ instrument limits(per_borrower: money, borrower: party = party(person), active_plans: integer = 1) {
634
+ familyRevision: 1
635
+ scope: product
636
+ summary: "One outstanding balance ceiling per borrower."
637
+ fields { borrower: account of borrower, limit: money = per_borrower, activePlans: integer = active_plans }
638
+ lifecycle { states: [pending, approved], initial: pending }
639
+ action create { requires unique "borrower_limit" on [self.borrower] }
640
+ action approve { from: pending, to: approved }
641
+ }
642
+
643
+ instrument portfolio_limit(limit: money) {
644
+ familyRevision: 1
645
+ scope: product
646
+ summary: "One shared ceiling on the principal the product may hold."
647
+ fields { key: text = "portfolio", limit: money = limit }
648
+ lifecycle { states: [pending, approved], initial: pending }
649
+ action create { requires unique "portfolio_limit" on [self.key] }
650
+ action approve { from: pending, to: approved }
651
+ }
652
+
653
+ instrument credit_line(borrower: party = programOperator, adapter: text, limit: money, expires: date) {
654
+ familyRevision: 1
655
+ summary: "A revolving facility from an outside lender; each draw is a separate immutable advance."
656
+ fields { borrower: account of borrower, facility: account(borrower, cash, "facility"), debt: account(borrower, claim, contra, "debt"), limit: money = limit, expiresAt: date = expires }
657
+ lifecycle { states: [pending, active, suspended, closed], initial: pending }
658
+ action create {}
659
+ action activate {
660
+ from: pending, to: active, actor: { party: borrower }
661
+ subject { adapter: adapter }
662
+ }
663
+ action suspend { from: active, to: suspended, actor: { party: borrower } }
664
+ action resume { from: suspended, to: active, actor: { party: borrower } }
665
+ action close { from: [active, suspended], to: closed, due: { at: self.expiresAt }, requires count of { instrument: all(financing.advance), reference: "line", anchor: self.id, states: [drawn], limit: 366 } == 0 }
666
+ }
667
+
668
+ instrument advance(line: ref<financing.credit_line>, amount: money = runtime) {
669
+ familyRevision: 1
670
+ summary: "A single facility draw with an immutable principal and full repayment."
671
+ fields { line: ref<line>, amount: money = amount, principalReceivable: account(self, claim) }
672
+ lifecycle { states: [pending, drawn, repaid, cancelled], initial: pending }
673
+ action create { requires self.amount <= self.line.limit }
674
+ invariants sum "principalReceivable.balance" of { instrument: all(financing.advance), reference: "line", anchor: self.line, states: [drawn], limit: 366 } <= self.line.limit
675
+ action draw {
676
+ from: pending, to: drawn
677
+ requires self.line in [active]
678
+ deadline: { at: self.line.expiresAt }
679
+ moves self.amount from self.line.facility to self.line.borrower; moves self.amount from self.line.debt to self.principalReceivable
680
+ }
681
+ action repay { from: drawn, to: repaid, moves self.amount from self.line.borrower to self.line.facility; moves self.amount from self.principalReceivable to self.line.debt }
682
+ action cancel { from: pending, to: cancelled }
683
+ }