@hyperscale0/hsx 2.4.1 → 3.0.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 (249) hide show
  1. package/CHANGELOG.md +2 -408
  2. package/README.md +3 -92
  3. package/dist/src/ast.d.ts +99 -310
  4. package/dist/src/ast.d.ts.map +1 -1
  5. package/dist/src/ast.js +16 -69
  6. package/dist/src/ast.js.map +1 -1
  7. package/dist/src/cli.d.ts +6 -19
  8. package/dist/src/cli.d.ts.map +1 -1
  9. package/dist/src/cli.js +76 -315
  10. package/dist/src/cli.js.map +1 -1
  11. package/dist/src/compile.d.ts +23 -73
  12. package/dist/src/compile.d.ts.map +1 -1
  13. package/dist/src/compile.js +1113 -148
  14. package/dist/src/compile.js.map +1 -1
  15. package/dist/src/cost.d.ts +8 -77
  16. package/dist/src/cost.d.ts.map +1 -1
  17. package/dist/src/cost.js +13 -422
  18. package/dist/src/cost.js.map +1 -1
  19. package/dist/src/format.d.ts +8 -7
  20. package/dist/src/format.d.ts.map +1 -1
  21. package/dist/src/format.js +9 -175
  22. package/dist/src/format.js.map +1 -1
  23. package/dist/src/headers.d.ts +35 -0
  24. package/dist/src/headers.d.ts.map +1 -0
  25. package/dist/src/headers.js +108 -0
  26. package/dist/src/headers.js.map +1 -0
  27. package/dist/src/index.d.ts +6 -12
  28. package/dist/src/index.d.ts.map +1 -1
  29. package/dist/src/index.js +4 -9
  30. package/dist/src/index.js.map +1 -1
  31. package/dist/src/lex.d.ts +8 -28
  32. package/dist/src/lex.d.ts.map +1 -1
  33. package/dist/src/lex.js +88 -146
  34. package/dist/src/lex.js.map +1 -1
  35. package/dist/src/parse.d.ts +4 -17
  36. package/dist/src/parse.d.ts.map +1 -1
  37. package/dist/src/parse.js +402 -1040
  38. package/dist/src/parse.js.map +1 -1
  39. package/dist/src/std-bundle.d.ts.map +1 -1
  40. package/dist/src/std-bundle.js +12 -20
  41. package/dist/src/std-bundle.js.map +1 -1
  42. package/dist/src/std-library.d.ts +1 -1
  43. package/dist/src/std-library.d.ts.map +1 -1
  44. package/dist/src/std-library.js +1 -5
  45. package/dist/src/std-library.js.map +1 -1
  46. package/dist/src/tunables.d.ts +7 -0
  47. package/dist/src/tunables.d.ts.map +1 -0
  48. package/dist/src/tunables.js +26 -0
  49. package/dist/src/tunables.js.map +1 -0
  50. package/dist/src/version.d.ts +2 -4
  51. package/dist/src/version.d.ts.map +1 -1
  52. package/dist/src/version.js +2 -4
  53. package/dist/src/version.js.map +1 -1
  54. package/docs/README.md +179 -41
  55. package/docs/headers.md +44 -0
  56. package/examples/cost-table.json +99 -751
  57. package/examples/library.hsx +59 -0
  58. package/package.json +9 -17
  59. package/src/ast.ts +82 -441
  60. package/src/cli.ts +77 -362
  61. package/src/compile.ts +1535 -247
  62. package/src/cost.ts +28 -671
  63. package/src/format.ts +12 -256
  64. package/src/headers.ts +126 -0
  65. package/src/index.ts +7 -39
  66. package/src/lex.ts +99 -195
  67. package/src/parse.ts +403 -1209
  68. package/src/std-bundle.ts +12 -20
  69. package/src/std-library.ts +2 -7
  70. package/src/tunables.ts +31 -0
  71. package/src/version.ts +2 -5
  72. package/std/approvals.hsx +17 -0
  73. package/std/cards.hsx +63 -0
  74. package/std/collections.hsx +31 -0
  75. package/std/escrow.hsx +43 -0
  76. package/std/financing.hsx +372 -0
  77. package/std/insurance.hsx +59 -0
  78. package/std/lending.hsx +115 -0
  79. package/std/marketplace.hsx +24 -0
  80. package/std/money.hsx +195 -0
  81. package/std/savings.hsx +44 -0
  82. package/std/travel.hsx +98 -0
  83. package/std/wallet.hsx +41 -0
  84. package/dist/src/diagnostics.d.ts +0 -13
  85. package/dist/src/diagnostics.d.ts.map +0 -1
  86. package/dist/src/diagnostics.js +0 -587
  87. package/dist/src/diagnostics.js.map +0 -1
  88. package/dist/src/emit.d.ts +0 -51
  89. package/dist/src/emit.d.ts.map +0 -1
  90. package/dist/src/emit.js +0 -192
  91. package/dist/src/emit.js.map +0 -1
  92. package/dist/src/entry-overrides.d.ts +0 -58
  93. package/dist/src/entry-overrides.d.ts.map +0 -1
  94. package/dist/src/entry-overrides.js +0 -284
  95. package/dist/src/entry-overrides.js.map +0 -1
  96. package/dist/src/ir.d.ts +0 -73
  97. package/dist/src/ir.d.ts.map +0 -1
  98. package/dist/src/ir.js +0 -15
  99. package/dist/src/ir.js.map +0 -1
  100. package/dist/src/limits.d.ts +0 -23
  101. package/dist/src/limits.d.ts.map +0 -1
  102. package/dist/src/limits.js +0 -23
  103. package/dist/src/limits.js.map +0 -1
  104. package/dist/src/lsp/server.d.ts +0 -32
  105. package/dist/src/lsp/server.d.ts.map +0 -1
  106. package/dist/src/lsp/server.js +0 -391
  107. package/dist/src/lsp/server.js.map +0 -1
  108. package/dist/src/modules.d.ts +0 -38
  109. package/dist/src/modules.d.ts.map +0 -1
  110. package/dist/src/modules.js +0 -372
  111. package/dist/src/modules.js.map +0 -1
  112. package/dist/src/typecheck.d.ts +0 -8
  113. package/dist/src/typecheck.d.ts.map +0 -1
  114. package/dist/src/typecheck.js +0 -4160
  115. package/dist/src/typecheck.js.map +0 -1
  116. package/docs/guide/01-first-program.md +0 -24
  117. package/docs/guide/02-money.md +0 -25
  118. package/docs/guide/03-instruments.md +0 -93
  119. package/docs/guide/04-lifecycles.md +0 -40
  120. package/docs/guide/05-fees-and-splits.md +0 -45
  121. package/docs/guide/06-schedules.md +0 -26
  122. package/docs/guide/07-composition.md +0 -52
  123. package/docs/guide/08-writing-a-module.md +0 -85
  124. package/docs/guide/09-cost.md +0 -24
  125. package/docs/guide/10-diagnostics.md +0 -27
  126. package/docs/llms-full.txt +0 -3684
  127. package/docs/llms.txt +0 -44
  128. package/docs/piece-plans.md +0 -120
  129. package/docs/playground.md +0 -55
  130. package/docs/reference/cli.md +0 -36
  131. package/docs/reference/diagnostics.md +0 -901
  132. package/docs/reference/grammar.md +0 -38
  133. package/docs/reference/std/advance.md +0 -112
  134. package/docs/reference/std/cancellable_booking.md +0 -159
  135. package/docs/reference/std/captured_payment.md +0 -115
  136. package/docs/reference/std/conditional_disbursement.md +0 -101
  137. package/docs/reference/std/credit_facility.md +0 -108
  138. package/docs/reference/std/held_payment.md +0 -173
  139. package/docs/reference/std/instant_transfer.md +0 -97
  140. package/docs/reference/std/metered.md +0 -89
  141. package/docs/reference/std/pooled_split.md +0 -95
  142. package/docs/reference/std/premium_forward.md +0 -119
  143. package/docs/reference/std/reconciled_payout.md +0 -103
  144. package/docs/reference/std/rotating_pool.md +0 -130
  145. package/docs/reference/std/scheduled.md +0 -136
  146. package/docs/reference/std/security_deposit.md +0 -138
  147. package/docs/reference/std/settlement_batch.md +0 -123
  148. package/docs/reference/std/swap.md +0 -133
  149. package/docs/reference/std/threshold_pool.md +0 -138
  150. package/docs/reference/std/weighted_distribution.md +0 -135
  151. package/docs/reference/types.md +0 -61
  152. package/docs/reference/udl-output.md +0 -13
  153. package/examples/01-first-program/README.md +0 -45
  154. package/examples/01-first-program/tip-jar.hsx +0 -17
  155. package/examples/02-imports-and-modules/README.md +0 -96
  156. package/examples/02-imports-and-modules/photo-booth.hsx +0 -37
  157. package/examples/03-diagnostics/README.md +0 -68
  158. package/examples/03-diagnostics/corner-shop-fixed.hsx +0 -22
  159. package/examples/03-diagnostics/corner-shop.hsx +0 -19
  160. package/examples/04-complete-product/README.md +0 -84
  161. package/examples/04-complete-product/study-hall.hsx +0 -67
  162. package/examples/05-authored-instrument/README.md +0 -5
  163. package/examples/05-authored-instrument/payment.hsx +0 -37
  164. package/examples/05-watch-club/README.md +0 -6
  165. package/examples/05-watch-club/watch-club.hsx +0 -123
  166. package/examples/README.md +0 -48
  167. package/examples/advance/README.md +0 -3
  168. package/examples/advance/advance.hsx +0 -13
  169. package/examples/advance/advance.udl +0 -367
  170. package/examples/cancellable_booking/README.md +0 -3
  171. package/examples/cancellable_booking/cancellable_booking.hsx +0 -14
  172. package/examples/cancellable_booking/cancellable_booking.udl +0 -413
  173. package/examples/captured_payment/README.md +0 -3
  174. package/examples/captured_payment/captured_payment.hsx +0 -17
  175. package/examples/captured_payment/captured_payment.udl +0 -686
  176. package/examples/conditional_disbursement/README.md +0 -3
  177. package/examples/conditional_disbursement/conditional_disbursement.hsx +0 -15
  178. package/examples/conditional_disbursement/conditional_disbursement.udl +0 -418
  179. package/examples/credit_facility/README.md +0 -3
  180. package/examples/credit_facility/credit_facility.hsx +0 -24
  181. package/examples/credit_facility/credit_facility.udl +0 -1396
  182. package/examples/held_payment/README.md +0 -3
  183. package/examples/held_payment/held_payment.hsx +0 -13
  184. package/examples/held_payment/held_payment.udl +0 -569
  185. package/examples/instant_transfer/README.md +0 -3
  186. package/examples/instant_transfer/instant_transfer.hsx +0 -16
  187. package/examples/instant_transfer/instant_transfer.udl +0 -376
  188. package/examples/metered/README.md +0 -3
  189. package/examples/metered/metered.hsx +0 -13
  190. package/examples/metered/metered.udl +0 -253
  191. package/examples/pooled_split/README.md +0 -3
  192. package/examples/pooled_split/pooled_split.hsx +0 -15
  193. package/examples/pooled_split/pooled_split.udl +0 -432
  194. package/examples/premium_forward/README.md +0 -3
  195. package/examples/premium_forward/premium_forward.hsx +0 -19
  196. package/examples/premium_forward/premium_forward.udl +0 -604
  197. package/examples/reconciled_payout/README.md +0 -3
  198. package/examples/reconciled_payout/reconciled_payout.hsx +0 -13
  199. package/examples/reconciled_payout/reconciled_payout.udl +0 -312
  200. package/examples/rotating_pool/README.md +0 -3
  201. package/examples/rotating_pool/rotating_pool.hsx +0 -18
  202. package/examples/rotating_pool/rotating_pool.udl +0 -4370
  203. package/examples/scheduled/README.md +0 -3
  204. package/examples/scheduled/scheduled.hsx +0 -12
  205. package/examples/scheduled/scheduled.udl +0 -355
  206. package/examples/security_deposit/README.md +0 -3
  207. package/examples/security_deposit/security_deposit.hsx +0 -21
  208. package/examples/security_deposit/security_deposit.udl +0 -320
  209. package/examples/settlement_batch/README.md +0 -3
  210. package/examples/settlement_batch/settlement_batch.hsx +0 -19
  211. package/examples/settlement_batch/settlement_batch.udl +0 -757
  212. package/examples/swap/README.md +0 -3
  213. package/examples/swap/swap.hsx +0 -19
  214. package/examples/swap/swap.udl +0 -892
  215. package/examples/threshold_pool/README.md +0 -3
  216. package/examples/threshold_pool/threshold_pool.hsx +0 -16
  217. package/examples/threshold_pool/threshold_pool.udl +0 -680
  218. package/examples/weighted_distribution/README.md +0 -3
  219. package/examples/weighted_distribution/weighted_distribution.hsx +0 -20
  220. package/examples/weighted_distribution/weighted_distribution.udl +0 -348
  221. package/skills/hsx/SKILL.md +0 -449
  222. package/src/diagnostics.ts +0 -620
  223. package/src/emit.ts +0 -282
  224. package/src/entry-overrides.ts +0 -428
  225. package/src/ir.ts +0 -113
  226. package/src/limits.ts +0 -22
  227. package/src/lsp/server.ts +0 -460
  228. package/src/modules.ts +0 -452
  229. package/src/typecheck.ts +0 -5515
  230. package/std/SEMANTICS.md +0 -44
  231. package/std/money_flows/advance.hsx +0 -235
  232. package/std/money_flows/cancellable_booking.hsx +0 -402
  233. package/std/money_flows/captured_payment.hsx +0 -220
  234. package/std/money_flows/conditional_disbursement.hsx +0 -226
  235. package/std/money_flows/credit_facility.hsx +0 -214
  236. package/std/money_flows/held_payment.hsx +0 -812
  237. package/std/money_flows/index.hsx +0 -4
  238. package/std/money_flows/instant_transfer.hsx +0 -231
  239. package/std/money_flows/metered.hsx +0 -104
  240. package/std/money_flows/pooled_split.hsx +0 -109
  241. package/std/money_flows/premium_forward.hsx +0 -307
  242. package/std/money_flows/reconciled_payout.hsx +0 -119
  243. package/std/money_flows/rotating_pool.hsx +0 -305
  244. package/std/money_flows/scheduled.hsx +0 -599
  245. package/std/money_flows/security_deposit.hsx +0 -248
  246. package/std/money_flows/settlement_batch.hsx +0 -173
  247. package/std/money_flows/swap.hsx +0 -1192
  248. package/std/money_flows/threshold_pool.hsx +0 -307
  249. package/std/money_flows/weighted_distribution.hsx +0 -237
@@ -1,12 +1,7 @@
1
1
  import { BUNDLED_STD_FILES } from "./std-bundle.ts";
2
-
3
2
  export interface StandardLibrary {
4
- readonly source: (specifier: string, name: string) => string | undefined;
3
+ source(header: string): string | undefined;
5
4
  }
6
-
7
5
  export const bundledStandardLibrary: StandardLibrary = {
8
- source(specifier: string, name: string): string | undefined {
9
- if (specifier !== "std/money_flows") return undefined;
10
- return BUNDLED_STD_FILES.get(`money_flows/${name}.hsx`);
11
- },
6
+ source: (header) => BUNDLED_STD_FILES.get(`${header}.hsx`),
12
7
  };
@@ -0,0 +1,31 @@
1
+ import type { Expr } from "./ast.ts";
2
+
3
+ /** Bounds use UDL units: minor units, basis points and milliseconds. */
4
+ export function tunableBounds(
5
+ type: Expr,
6
+ ): { minimum: number | string; maximum: number | string } | undefined {
7
+ const name =
8
+ type.kind === "call" || type.kind === "type"
9
+ ? type.name
10
+ : type.kind === "name"
11
+ ? type.value
12
+ : "";
13
+ if (name === "money") return { minimum: "0", maximum: "999999999999999999" };
14
+ if (name === "percent") return { minimum: 0, maximum: 10000 };
15
+ if (name === "duration")
16
+ return { minimum: 1, maximum: Number.MAX_SAFE_INTEGER };
17
+ if (name !== "integer") return;
18
+ if (type.kind === "call") {
19
+ const numbers = type.args.map((arg) =>
20
+ arg.kind === "number" ? Number(arg.value) : NaN,
21
+ );
22
+ if (
23
+ numbers.length !== 2 ||
24
+ numbers.some((value) => !Number.isSafeInteger(value)) ||
25
+ numbers[0]! > numbers[1]!
26
+ )
27
+ throw new Error("integer tunable needs a minimum and maximum");
28
+ return { minimum: numbers[0]!, maximum: numbers[1]! };
29
+ }
30
+ return { minimum: 0, maximum: Number.MAX_SAFE_INTEGER };
31
+ }
package/src/version.ts CHANGED
@@ -1,5 +1,2 @@
1
- /** Package version reported by the command line. */
2
- export const HSX_VERSION = "2.4.1";
3
-
4
- /** Canonical UDL contract version emitted by this compiler. */
5
- export const HSX_TARGET_UDL_VERSION = 1;
1
+ export const HSX_VERSION = "3.0.0";
2
+ export const HSX_TARGET_UDL_VERSION = 3;
@@ -0,0 +1,17 @@
1
+ header approvals
2
+
3
+ instrument decision(for: ref, approved_by: party, action: text = "approve") {
4
+ summary: "One authenticated decision bound to an immutable target and expiry."
5
+ fields { target: ref<for>, expiresAt: date }
6
+ lifecycle { states: [pending, approved, declined, expired], initial: pending }
7
+ action create {}
8
+ action approve {
9
+ from: pending, to: approved, actor: { party: approved_by }, deadline: { at: self.expiresAt }
10
+ approval: { target: self.target, action: action, party: approved_by, expires: self.expiresAt, input: "material", decision: "approved" }
11
+ }
12
+ action decline {
13
+ from: pending, to: declined, actor: { party: approved_by }, deadline: { at: self.expiresAt }
14
+ approval: { target: self.target, action: action, party: approved_by, expires: self.expiresAt, input: "material", decision: "declined" }
15
+ }
16
+ action expire { from: pending, to: expired, actor: clock, due: { at: self.expiresAt } }
17
+ }
package/std/cards.hsx ADDED
@@ -0,0 +1,63 @@
1
+ header cards
2
+
3
+ instrument cardholder(holder: party) {
4
+ fields { holder: account of holder }
5
+ lifecycle { states: [pending, active, suspended, closed], initial: pending }
6
+ action create {}
7
+ action activate { from: pending, to: active }
8
+ action suspend { from: active, to: suspended }
9
+ action resume { from: suspended, to: active }
10
+ action close { from: [active, suspended], to: closed }
11
+ }
12
+
13
+ instrument card(holder: ref<cards.cardholder>, spend_limit: money) {
14
+ fields { holder: ref<holder>, spendLimit: money = spend_limit }
15
+ lifecycle { states: [issued, active, frozen, cancelled], initial: issued }
16
+ action create { requires self.holder in [active] }
17
+ action activate { from: issued, to: active, requires self.holder in [active] }
18
+ action freeze { from: active, to: frozen }
19
+ action unfreeze { from: frozen, to: active, requires self.holder in [active] }
20
+ action cancel { from: [issued, active, frozen], to: cancelled }
21
+ }
22
+
23
+ instrument authorization(card: ref<cards.card>, merchant: party, approval: approval) {
24
+ fields { card: ref<card>, merchant: account of merchant, amount: money, expiresAt: date, reservation: text? }
25
+ lifecycle { states: [pending, approved, captured, declined, reversed, expired], initial: pending }
26
+ action create {
27
+ requires self.card in [active]
28
+ requires self.amount <= self.card.spendLimit
29
+ }
30
+ action approve {
31
+ from: pending, to: approved, deadline: { at: self.expiresAt }
32
+ requires approval by approval; requires self.card in [active]
33
+ moves reserve self.amount from self.card.holder.holder to merchant capture "reservation"
34
+ }
35
+ action capture { from: approved, to: captured, deadline: { at: self.expiresAt }, moves post self.reservation }
36
+ action reverse { from: approved, to: reversed, moves void self.reservation }
37
+ action decline { from: pending, to: declined, requires approval by approval for "approve" is "declined" }
38
+ action expire { from: approved, to: expired, actor: clock, due: { at: self.expiresAt }, moves void self.reservation }
39
+ action abandon { from: pending, to: expired, actor: clock, due: { at: self.expiresAt } }
40
+ }
41
+
42
+ instrument transaction(authorization: ref<cards.authorization>) {
43
+ fields { authorization: ref<authorization>, amount: money = self.authorization.amount }
44
+ lifecycle { states: [posted, refunded], initial: posted }
45
+ action create { requires self.authorization in [captured]; requires unique "captured_authorization" on [self.authorization] }
46
+ action refund {
47
+ from: posted, to: refunded, input { reason: text }
48
+ moves self.amount from self.authorization.merchant to self.authorization.card.holder.holder
49
+ }
50
+ }
51
+
52
+ instrument dispute(transaction: ref<cards.transaction>, approved_by: approval, within: duration = 90d) {
53
+ fields { transaction: ref<transaction>, evidence: text, expiresAt: date = after(self.createdAt, within) }
54
+ lifecycle { states: [filed, reviewing, won, lost], initial: filed }
55
+ action create { requires self.transaction in [posted]; requires unique "disputed_transaction" on [self.transaction] }
56
+ action review { from: filed, to: reviewing }
57
+ action win {
58
+ from: reviewing, to: won, deadline: { at: self.expiresAt }
59
+ requires approval by approved_by
60
+ invoke: [{ reference: self.transaction, action: refund, input: { reason: { field: self.evidence } } }]
61
+ }
62
+ action lose { from: [filed, reviewing], to: lost, actor: clock, due: { at: self.expiresAt } }
63
+ }
@@ -0,0 +1,31 @@
1
+ header collections
2
+
3
+ instrument case(on: ref<financing.installments>[], agency: approval, overdue: duration = 3d, fee: percent = 20%) {
4
+ summary: "Refer an overdue installment without creating another debt."
5
+ fields { plan: ref<on>, slice: ref<on.slice>, payment: ref<on.payment>, eligibleAt: date = after(self.slice.dueAt, overdue), agencyFee: money = rate(self.payment.amount, fee) }
6
+ lifecycle { states: [opened, assigned, recovered, recalled, returned_uncollected], initial: opened }
7
+ action create {
8
+ due: { at: self.eligibleAt }
9
+ requires self.plan == self.slice.plan
10
+ requires self.slice.principalReceivable.balance > "0"
11
+ requires unique "overdue_referral" on [self.slice]
12
+ }
13
+ action assign { from: opened, to: assigned, requires approval by agency }
14
+ action recover { from: assigned, to: recovered, actor: { party: agency }, requires self.payment.plan == self.plan, invoke: [{ reference: self.payment, action: pay, input: {} }, { reference: self.id, action: pay_fee, input: {} }] }
15
+ action pay_fee { from: recovered, to: recovered, actor: { parent: current() }, moves self.agencyFee from self.plan.capital to agency }
16
+ action recall { from: [opened, assigned], to: recalled }
17
+ action return_uncollected { from: assigned, to: returned_uncollected, actor: { party: agency } }
18
+ }
19
+
20
+ instrument contact(case: ref<collections.case>[], agency: party, max_contacts: integer = 10, window: duration = 30d, contact_from: integer(0, 23) = 8, contact_until: integer(1, 23) = 20, timezone: text = "Asia/Riyadh") {
21
+ constraints { contact_from: less_than(contact_until) }
22
+ summary: "An agency contact admitted against the locked rolling count and local hours."
23
+ fields { case: ref<case>, attemptedAt: date?, evidence: text, maximum: integer = max_contacts }
24
+ lifecycle { states: [recorded], initial: recorded }
25
+ invariants count of { instrument: current(), reference: "case", anchor: self.case, states: [recorded], limit: 366, window: { field: attemptedAt, milliseconds: window } } <= self.maximum
26
+ action create {
27
+ actor: { party: agency }
28
+ set: { attemptedAt: { field: self.now } }
29
+ requires self.case in [assigned]; requires hours self.now between contact_from and contact_until timezone timezone
30
+ }
31
+ }
package/std/escrow.hsx ADDED
@@ -0,0 +1,43 @@
1
+ header escrow
2
+
3
+ instrument hold(payer: party, payee: party, for: ref<marketplace.order>, accept_within: duration = 48h, fee: fee = { seller: 0%, tax: 0% }, dispute: policy = { refund_after: return_verified }) {
4
+ summary: "Hold an order's price through delivery, acceptance and verified return."
5
+ fields { payer: account of payer, payee: account of payee, order: ref<for>, price: money, held: account of self, deliveredAt: date? }
6
+ lifecycle { states: [pending, funded, delivered, disputed, return_verified, released, refunded, cancelled], initial: pending }
7
+ action create {
8
+ requires self.payer == self.order.buyer; requires self.payee == self.order.listing.seller; requires self.price == self.order.price
9
+ }
10
+ action fund {
11
+ from: pending, to: funded, actor: { party: payer }
12
+ requires self.order in [placed]
13
+ moves self.price from payer to self.held
14
+ }
15
+ action confirm {
16
+ from: pending, to: funded
17
+ requires self.held.balance == self.price
18
+ }
19
+ action deliver {
20
+ from: funded, to: delivered, actor: { party: payee }
21
+ set: { deliveredAt: { field: self.now } }
22
+ }
23
+ action accept {
24
+ from: delivered, to: released, actor: { party: payer }
25
+ input { acceptanceReference: text, titleReference: text, coverReference: text }
26
+ deadline: { at: self.deliveredAt, offset: accept_within }
27
+ moves self.price from self.held to payee fee fee
28
+ }
29
+ action expire_acceptance {
30
+ from: delivered, to: disputed, actor: clock
31
+ due: { at: self.deliveredAt, offset: accept_within }
32
+ }
33
+ action dispute {
34
+ from: delivered, to: disputed, actor: { party: payer }
35
+ deadline: { at: self.deliveredAt, offset: accept_within }
36
+ }
37
+ action verify_return { from: disputed, to: return_verified, input { returnReference: text } }
38
+ action refund {
39
+ from: dispute.refund_after, to: refunded
40
+ moves self.price from self.held to payer
41
+ }
42
+ action cancel { from: pending, to: cancelled }
43
+ }
@@ -0,0 +1,372 @@
1
+ header financing
2
+
3
+ instrument installments(months: integer(1, 366), 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%, down_payment: percent = 0%, funds: ref, approval: approval, borrower: party = party(person), capital: party = programOperator, profit_to: party = programOperator, limits: ref<financing.limits> = object(financing.limits)) {
4
+ summary: "Funding, profit recognition and payment order chosen by the program."
5
+ fields {
6
+ funds: ref<funds>, borrower: account of borrower, settlementIdentity: text, borrowerLimit: ref<limits>, portfolioLimit: ref<limits.portfolio>
7
+ months: integer = months, dates: list(date, months)
8
+ capital: account(capital, cash, "capital"), profitIncome: account(profit_to, cash, "profitIncome"), loss: account(capital, claim, "loss")
9
+ principalReturned: money?, profitReturned: money?, lossRecorded: money?
10
+ price: money = self.funds.price
11
+ downPayment: money = rate(self.price, down_payment)
12
+ principal: money = subtract(self.price, self.downPayment)
13
+ profit: money = rate(self.principal, profit)
14
+ total: money = sum(self.principal, self.profit)
15
+ }
16
+ lifecycle { states: [quoted, signed, active, paid, written_off, cancelled], initial: quoted }
17
+ invariants count of { instrument: all(financing.installments), reference: "borrowerLimit", anchor: self.borrowerLimit, states: [active], limit: 366 } <= self.borrowerLimit.activePlans
18
+ invariants sum "principalReceivable.balance" of { instrument: all(financing.installments.slice), reference: "borrowerLimit", anchor: self.borrowerLimit, states: [pending, due, paid], limit: 366 } <= self.borrowerLimit.limit
19
+ invariants sum "principalReceivable.balance" of { instrument: all(financing.installments.slice), reference: "portfolioLimit", anchor: self.portfolioLimit, states: [pending, due, paid], limit: 366 } <= self.portfolioLimit.limit
20
+ action create { requires self.borrower == self.funds.payer; requires self.borrowerLimit.borrower == self.funds.payer }
21
+ action sign {
22
+ from: quoted, to: signed, input { signedOffer: text, affordabilityReference: text }
23
+ requires count of { instrument: slice, reference: "plan", anchor: self.id, states: [draft], limit: 366 } == self.months
24
+ }
25
+ action disburse {
26
+ from: signed, to: active
27
+ requires approval by approval
28
+ requires self.borrowerLimit in [approved]
29
+ requires self.portfolioLimit in [approved]
30
+ when disburse_to is funds {
31
+ requires self.funds in [pending]
32
+ moves self.downPayment from self.funds.payer to self.funds.held
33
+ moves self.principal from self.capital to self.funds.held
34
+ }
35
+ when disburse_to is borrower { moves self.principal from self.capital to self.borrower }
36
+ invoke: [{ selection: { instrument: slice, reference: "plan", anchor: self.id, states: [draft], limit: 366, order: [position] }, action: activate, input: {} }]
37
+ when disburse_to is funds { invoke: [{ reference: self.funds, action: confirm, input: {} }] }
38
+ }
39
+ action complete {
40
+ from: active, to: paid
41
+ requires count of { instrument: slice, reference: "plan", anchor: self.id, states: [pending, due], limit: 366 } == 0
42
+ }
43
+ action restore { from: [active, paid], to: active, actor: { parent: payment } }
44
+ action payoff {
45
+ from: active, to: paid
46
+ calculate: [
47
+ { target: "principalReturned", op: "aggregate", selection: { instrument: slice, reference: "plan", anchor: self.id, states: [pending, due], limit: 366 }, measure: { sum: "principalReceivable.balance" } }
48
+ ]
49
+ 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: {} }]
50
+ }
51
+ action record_payoff {
52
+ from: paid, to: paid, actor: { parent: current() }
53
+ 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" } }]
54
+ 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" } } }]
55
+ }
56
+ action write_off {
57
+ from: active, to: written_off
58
+ input { overdueSlice: ref<slice> }
59
+ calculate: [{ target: "lossRecorded", op: "aggregate", selection: { instrument: slice, reference: "plan", anchor: self.id, states: [pending, due], limit: 366 }, measure: { sum: "principalReceivable.balance" } }]
60
+ due: { at: input.overdueSlice.lossEligibleAt }
61
+ requires approval by approval; requires input.overdueSlice.plan == self.id; requires input.overdueSlice.principalReceivable.balance > "0"
62
+ 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" } } }]
63
+ }
64
+ action record_payment {
65
+ from: active, to: active, actor: { parent: payment }
66
+ input { identity: text, principal: money, profit: money }
67
+ 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" } } }]
68
+ }
69
+ action cancel { from: quoted, to: cancelled }
70
+ records {
71
+ settlement: {
72
+ fields { plan: ref<parent>, identity: text, principal: money, profit: money, loss: money, kind: enum(cash, loss)
73
+ capital: account(capital, cash, "capital"), profitIncome: account(profit_to, cash, "profitIncome"), lossAccount: account(capital, claim, "loss") }
74
+ lifecycle { states: [recorded], initial: recorded }
75
+ action create {
76
+ actor: { parent: parent }
77
+ input { plan: ref<parent>, identity: text, principal: money, profit: money, loss: money, kind: enum(cash, loss) }
78
+ requires unique "settlement_identity" on [self.plan, self.identity]
79
+ }
80
+ }
81
+ slice: {
82
+ fields {
83
+ plan: ref<parent>, borrowerLimit: ref<limits>, portfolioLimit: ref<limits.portfolio>, position: integer(1, months)
84
+ dueAt: date = at(self.plan.dates, self.position), lossEligibleAt: date = after(self.dueAt, 90d)
85
+ principalBase: money = divide(self.plan.principal, months)
86
+ principalAllocated: money = multiply(self.principalBase, months)
87
+ principalRemainder: money = subtract(self.plan.principal, self.principalAllocated)
88
+ profitBase: money = divide(self.plan.profit, months)
89
+ profitAllocated: money = multiply(self.profitBase, months)
90
+ profitRemainder: money = subtract(self.plan.profit, self.profitAllocated)
91
+ first: integer = divide(1, self.position)
92
+ principalAdjustment: money = multiply(self.principalRemainder, self.first)
93
+ profitAdjustment: money = multiply(self.profitRemainder, self.first)
94
+ principal: money = sum(self.principalBase, self.principalAdjustment)
95
+ profit: money = sum(self.profitBase, self.profitAdjustment)
96
+ principalReceivable: account(self, claim), profitReceivable: account(self, claim), profitEarned: account(self, claim)
97
+ payoffIdentity: text?, payoffRebate: money?, payoffProfitTotal: money?, payoffProfit: money?
98
+ paymentBudget: money?, principalPayment: money?, principalBasePayment: money?, principalRatio: money?, profitPayment: money?, profitBudget: money?, collectibleProfit: money?, collectibleTotal: money?, ratioDenominator: integer = sum(10000, profit)
99
+ debt: account(borrower, claim, contra, "debt"), loss: account(capital, claim, "loss")
100
+ }
101
+ lifecycle { states: [draft, pending, due, paid, written_off], initial: draft }
102
+ action create {
103
+ 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
104
+ }
105
+ action activate {
106
+ from: draft, to: pending, actor: { parent: parent }
107
+ moves self.principal from self.debt to self.principalReceivable
108
+ moves self.profit from self.debt to self.profitReceivable
109
+ when profit_earned is at_disbursement { moves self.profit from self.profitReceivable to self.profitEarned }
110
+ when profit_earned is by_schedule { invoke: [{ instrument: recognition, action: create, input: { slice: { field: self.id } } }] }
111
+ }
112
+ action mature {
113
+ from: pending, to: due, actor: clock, due: { at: self.dueAt }
114
+ }
115
+ action collect_principal {
116
+ from: [pending, due, paid], to: due, actor: { parent: payment }
117
+ input { payment: ref<payment> }
118
+ requires input.payment.plan == self.plan
119
+ invoke: [{ instrument: principal_piece, action: create, input: { slice: { field: self.id }, payment: { field: input.payment }, limit: { field: self.principalReceivable.balance } } }]
120
+ }
121
+ action collect_profit {
122
+ from: [pending, due, paid], to: due, actor: { parent: payment }
123
+ input { payment: ref<payment> }
124
+ requires input.payment.plan == self.plan
125
+ when profit_earned is on_payment { invoke: [{ instrument: profit_piece, action: create, input: { slice: { field: self.id }, payment: { field: input.payment }, limit: { field: self.profitReceivable.balance } } }] }
126
+ when profit_earned is by_schedule { invoke: [{ instrument: profit_piece, action: create, input: { slice: { field: self.id }, payment: { field: input.payment }, limit: { field: self.profitEarned.balance } } }] }
127
+ when profit_earned is at_disbursement { invoke: [{ instrument: profit_piece, action: create, input: { slice: { field: self.id }, payment: { field: input.payment }, limit: { field: self.profitEarned.balance } } }] }
128
+ }
129
+ action collect_pro_rata {
130
+ from: [pending, due, paid], to: due, actor: { parent: payment }, input { payment: ref<payment> }
131
+ requires input.payment.plan == self.plan
132
+ when profit_earned is on_payment { calculate: [{ target: collectibleProfit, op: sum, values: [{ field: self.profitReceivable.balance }] }] }
133
+ when profit_earned is by_schedule { calculate: [{ target: collectibleProfit, op: sum, values: [{ field: self.profitEarned.balance }] }] }
134
+ when profit_earned is at_disbursement { calculate: [{ target: collectibleProfit, op: sum, values: [{ field: self.profitEarned.balance }] }] }
135
+ calculate: [
136
+ { target: collectibleTotal, op: sum, values: [{ field: self.principalReceivable.balance }, { field: self.collectibleProfit }] }
137
+ { target: paymentBudget, op: minimum, values: [{ field: input.payment.held.balance }, { field: self.collectibleTotal }] }
138
+ { target: principalRatio, op: ratio, amount: { field: self.paymentBudget }, numerator: { literal: 10000 }, denominator: { field: self.ratioDenominator }, rounding: floor }
139
+ { target: principalBasePayment, op: minimum, values: [{ field: self.principalRatio }, { field: self.principalReceivable.balance }] }
140
+ { target: profitBudget, op: subtract, base: { field: self.paymentBudget }, subtract: [{ field: self.principalBasePayment }] }
141
+ { target: profitPayment, op: minimum, values: [{ field: self.profitBudget }, { field: self.collectibleProfit }] }
142
+ { target: principalPayment, op: subtract, base: { field: self.paymentBudget }, subtract: [{ field: self.profitPayment }] }
143
+ ]
144
+ invoke: [{ instrument: principal_piece, action: create, input: { slice: { field: self.id }, payment: { field: input.payment }, limit: { field: self.principalPayment } } }, { instrument: profit_piece, action: create, input: { slice: { field: self.id }, payment: { field: input.payment }, limit: { field: self.profitPayment } } }]
145
+ }
146
+ action complete {
147
+ from: [pending, due], to: paid
148
+ requires self.principalReceivable.balance == "0"; requires self.profitReceivable.balance == "0"; requires self.profitEarned.balance == "0"
149
+ }
150
+ action restore { from: [pending, due, paid], to: due, actor: { parent: payment } }
151
+ action payoff {
152
+ from: [pending, due], to: paid, actor: { parent: parent }, input { identity: text }
153
+ set: { payoffIdentity: { field: input.identity } }
154
+ 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 }] }]
155
+ moves self.principalReceivable.balance from self.plan.borrower to self.plan.capital
156
+ moves self.payoffProfit from self.plan.borrower to self.plan.profitIncome
157
+ moves self.principalReceivable.balance from self.principalReceivable to self.debt
158
+ moves self.profitReceivable.balance from self.profitReceivable to self.debt
159
+ moves self.profitEarned.balance from self.profitEarned to self.debt
160
+ }
161
+ action write_off {
162
+ from: [pending, due], to: written_off, actor: { parent: parent }
163
+ moves self.principalReceivable.balance from self.principalReceivable to self.loss; moves self.profitReceivable.balance from self.profitReceivable to self.debt; moves self.profitEarned.balance from self.profitEarned to self.loss
164
+ }
165
+ }
166
+ recognition: {
167
+ fields { slice: ref<slice>, dueAt: date = after(self.slice.dueAt, 0) }
168
+ lifecycle { states: [pending, recognised], initial: pending }
169
+ action create { actor: { parent: slice }, input { slice: ref<slice> }, requires unique "profit_recognition" on [self.slice] }
170
+ action mature { from: pending, to: recognised, actor: clock, due: { at: self.dueAt }, moves self.slice.profitReceivable.balance from self.slice.profitReceivable to self.slice.profitEarned }
171
+ }
172
+ payment: {
173
+ fields { plan: ref<parent>, amount: money, paymentIdentity: text, held: account of self
174
+ capital: account(capital, cash, "capital"), profitIncome: account(profit_to, cash, "profitIncome"), loss: account(capital, claim, "loss")
175
+ outstandingPrincipal: money?, outstandingProfit: money?, outstandingFines: money?, outstandingCosts: money?, outstandingTotal: money?, principalReturned: money?, profitReturned: money?, lossRecorded: money? }
176
+
177
+ lifecycle { states: [pending, paid, refunded], initial: pending }
178
+ action create { input { plan: ref<parent>, amount: money, paymentIdentity: text } requires self.plan in [active]; requires unique "payment_identity" on [self.plan, self.paymentIdentity] }
179
+ action pay {
180
+ from: pending, to: paid
181
+ 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" } }]
182
+ 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" } }] }
183
+ 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" } }] }
184
+ 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" } }] }
185
+ when apply is fines_profit_principal {
186
+ 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 }] }]
187
+ }
188
+ when apply is principal_profit { calculate: [{ target: outstandingTotal, op: sum, values: [{ field: self.outstandingPrincipal }, { field: self.outstandingProfit }] }] }
189
+ when apply is pro_rata { calculate: [{ target: outstandingTotal, op: sum, values: [{ field: self.outstandingPrincipal }, { field: self.outstandingProfit }] }] }
190
+ requires self.plan in [active]; requires self.amount <= self.outstandingTotal
191
+ moves self.amount from self.plan.borrower to self.held
192
+ when apply is principal_profit {
193
+ invoke: [{ selection: { instrument: slice, reference: "plan", anchor: self.plan, states: [pending, due, paid], limit: 366, order: [position] }, action: collect_principal, input: { payment: { field: self.id } } }, { selection: { instrument: slice, reference: "plan", anchor: self.plan, states: [pending, due, paid], limit: 366, order: [position] }, action: collect_profit, input: { payment: { field: self.id } } }]
194
+ }
195
+ when apply is fines_profit_principal {
196
+ invoke: [{ selection: { instrument: all(financing.late_charge), reference: "plan", anchor: self.plan, states: [assessed, collected], limit: 366, order: [overdueAt] }, action: collect, 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: collect, input: { payment: { field: self.id } } }]
197
+ invoke: [{ selection: { instrument: slice, reference: "plan", anchor: self.plan, states: [pending, due, paid], limit: 366, order: [position] }, action: collect_profit, input: { payment: { field: self.id } } }, { selection: { instrument: slice, reference: "plan", anchor: self.plan, states: [pending, due, paid], limit: 366, order: [position] }, action: collect_principal, input: { payment: { field: self.id } } }]
198
+ }
199
+ when apply is pro_rata {
200
+ invoke: [{ selection: { instrument: slice, reference: "plan", anchor: self.plan, states: [pending, due, paid], limit: 366, order: [position] }, action: collect_pro_rata, input: { payment: { field: self.id } } }]
201
+ }
202
+ invoke: [{ reference: self.id, action: record, input: {} }]
203
+ }
204
+ action record {
205
+ from: paid, to: paid, actor: { parent: current() }
206
+ requires self.held.balance == "0"
207
+ 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" } }]
208
+ invoke: [{ reference: self.plan, action: record_payment, input: { identity: { field: self.paymentIdentity }, principal: { field: self.principalReturned }, profit: { field: self.profitReturned } } }]
209
+ }
210
+ action refund {
211
+ from: paid, to: refunded
212
+ requires unique "settlement_consumption" on [self.plan, self.paymentIdentity]; requires self.plan in [active, paid]
213
+ when apply is fines_profit_principal {
214
+ 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 } } }]
215
+ }
216
+ 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: {} }]
217
+ }
218
+ }
219
+ principal_piece: {
220
+ fields { slice: ref<slice>, payment: ref<payment>, limit: money, amount: money = minimum(self.payment.held.balance, self.slice.principalReceivable.balance, self.limit) }
221
+ lifecycle { states: [paid, refunded], initial: paid }
222
+ action create {
223
+ actor: { parent: slice }
224
+ input { slice: ref<slice>, payment: ref<payment>, limit: money }
225
+ requires unique "principal_receipt" on [self.slice, self.payment]
226
+ moves self.amount from self.payment.held to self.slice.plan.capital; moves self.amount from self.slice.principalReceivable to self.slice.debt
227
+ }
228
+ action refund {
229
+ from: paid, to: refunded, actor: { parent: payment }
230
+ moves self.amount from self.slice.plan.capital to self.slice.plan.funds.payer; moves self.amount from self.slice.debt to self.slice.principalReceivable
231
+ }
232
+ }
233
+ profit_piece: {
234
+ fields { slice: ref<slice>, payment: ref<payment>, limit: money, amount: money = minimum(self.payment.held.balance, self.limit) }
235
+ lifecycle { states: [paid, refunded], initial: paid }
236
+ action create {
237
+ actor: { parent: slice }
238
+ input { slice: ref<slice>, payment: ref<payment>, limit: money }
239
+ requires unique "profit_receipt" on [self.slice, self.payment]
240
+ moves self.amount from self.payment.held to self.slice.plan.profitIncome
241
+ when profit_earned is on_payment { moves self.amount from self.slice.profitReceivable to self.slice.debt }
242
+ when profit_earned is by_schedule { moves self.amount from self.slice.profitEarned to self.slice.debt }
243
+ when profit_earned is at_disbursement { moves self.amount from self.slice.profitEarned to self.slice.debt }
244
+ }
245
+ action refund {
246
+ from: paid, to: refunded, actor: { parent: payment }
247
+ moves self.amount from self.slice.plan.profitIncome to self.slice.plan.borrower
248
+ when profit_earned is on_payment { moves self.amount from self.slice.debt to self.slice.profitReceivable }
249
+ when profit_earned is by_schedule { moves self.amount from self.slice.debt to self.slice.profitEarned }
250
+ when profit_earned is at_disbursement { moves self.amount from self.slice.debt to self.slice.profitEarned }
251
+ }
252
+ }
253
+ }
254
+ }
255
+
256
+ 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, approval: approval = programOperator, borrower: party = party(person)) {
257
+ summary: "Separate approved fine and evidenced recovery claims on an overdue installment."
258
+ fields {
259
+ plan: ref<on>, slice: ref<on.slice>, borrower: account of borrower, fine: money = fine, overdueAt: date = after(self.slice.dueAt, grace)
260
+ receivable: account(self, claim), debt: account(borrower, claim, contra, "debt")
261
+ }
262
+ lifecycle { states: [proposed, assessed, collected, waived, refunded], initial: proposed }
263
+ action create { requires unique "late_assessment" on [self.slice]; requires self.borrower == self.slice.plan.borrower; requires self.plan == self.slice.plan }
264
+ action assess {
265
+ from: proposed, to: assessed, due: { at: self.overdueAt }
266
+ requires self.slice.principalReceivable.balance > "0"; requires approval by approval
267
+ moves self.fine from self.debt to self.receivable
268
+ }
269
+ action collect {
270
+ from: [assessed, collected], to: assessed, input { payment: ref<on.payment> }
271
+ requires input.payment in [paid]
272
+ requires input.payment.plan == self.plan
273
+ 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: {} }]
274
+ }
275
+ action finish { from: assessed, to: collected, actor: { parent: current() }, requires self.receivable.balance == "0" }
276
+ action cancel { from: proposed, to: waived }
277
+ action waive { from: assessed, to: waived, moves self.receivable.balance from self.receivable to self.debt }
278
+ action refund {
279
+ from: [assessed, collected], to: assessed, input { payment: ref<on.payment> }
280
+ requires input.payment in [paid, refunded]
281
+ requires input.payment.plan == self.plan
282
+ 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: {} }]
283
+ }
284
+ records {
285
+ receipt: {
286
+ fields { assessment: ref<parent>, payment: ref<on.payment>, amount: money = minimum(self.payment.held.balance, self.assessment.receivable.balance) }
287
+ lifecycle { states: [paid, refunded], initial: paid }
288
+ action create { 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; moves self.amount from self.assessment.receivable to self.assessment.debt }
289
+ action refund { from: paid, to: refunded, actor: { parent: parent }, moves self.amount from fines_to to self.payment.plan.borrower; moves self.amount from self.assessment.debt to self.assessment.receivable }
290
+ }
291
+
292
+ cost: {
293
+ fields {
294
+ plan: ref<on>, assessment: ref<parent>, overdueAt: date = after(self.assessment.slice.dueAt, grace), evidence: text, evidencedCost: money, amount: money = minimum(self.evidencedCost, cap)
295
+ receivable: account(self, claim), debt: account(borrower, claim, contra, "debt")
296
+ }
297
+ lifecycle { states: [proposed, assessed, collected, waived, refunded], initial: proposed }
298
+ action create { requires unique "recovery_assessment" on [self.assessment]; requires self.plan == self.assessment.plan }
299
+ action assess {
300
+ from: proposed, to: assessed, due: { at: self.assessment.overdueAt }
301
+ requires self.assessment.slice.principalReceivable.balance > "0"; requires approval by approval
302
+ moves self.amount from self.debt to self.receivable
303
+ }
304
+ action collect {
305
+ from: [assessed, collected], to: assessed, input { payment: ref<on.payment> }
306
+ requires input.payment in [paid]
307
+ requires input.payment.plan == self.plan
308
+ 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: {} }]
309
+ }
310
+ action finish { from: assessed, to: collected, actor: { parent: current() }, requires self.receivable.balance == "0" }
311
+ action cancel { from: proposed, to: waived }
312
+ action waive { from: assessed, to: waived, moves self.receivable.balance from self.receivable to self.debt }
313
+ action refund {
314
+ from: [assessed, collected], to: assessed, input { payment: ref<on.payment> }
315
+ requires input.payment in [paid, refunded]
316
+ requires input.payment.plan == self.plan
317
+ 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: {} }]
318
+ }
319
+ records {
320
+ receipt: {
321
+ fields { assessment: ref<parent>, payment: ref<on.payment>, amount: money = minimum(self.payment.held.balance, self.assessment.receivable.balance) }
322
+ lifecycle { states: [paid, refunded], initial: paid }
323
+ action create { 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; moves self.amount from self.assessment.receivable to self.assessment.debt }
324
+ action refund { from: paid, to: refunded, actor: { parent: parent }, moves self.amount from costs_to to self.payment.plan.borrower; moves self.amount from self.assessment.debt to self.assessment.receivable }
325
+ }
326
+ }
327
+ }
328
+ }
329
+ }
330
+
331
+ instrument limits(per_borrower: money, portfolio: money, borrower: party = party(person), active_plans: integer = 1) {
332
+ summary: "One borrower ceiling and one shared portfolio ceiling."
333
+ fields { borrower: account of borrower, limit: money = per_borrower, activePlans: integer = active_plans }
334
+ lifecycle { states: [pending, approved], initial: pending }
335
+ action create { requires unique "borrower_limit" on [self.borrower] }
336
+ action approve { from: pending, to: approved }
337
+ records {
338
+ portfolio: {
339
+ fields { key: text = "portfolio", limit: money = portfolio }
340
+ lifecycle { states: [pending, approved], initial: pending }
341
+ action create { requires unique "portfolio_limit" on [self.key] }
342
+ action approve { from: pending, to: approved }
343
+ }
344
+ }
345
+ }
346
+
347
+ instrument credit_line(borrower: party, lender: party, limit: money, expires: date) {
348
+ summary: "A revolving facility; each draw is a separate immutable advance."
349
+ fields { borrower: account of borrower, lender: account of lender, debt: account(borrower, claim, contra, "debt"), limit: money = limit, expiresAt: date = expires }
350
+ lifecycle { states: [pending, active, suspended, closed], initial: pending }
351
+ action create {}
352
+ action activate { from: pending, to: active, actor: { party: lender } }
353
+ action suspend { from: active, to: suspended, actor: { party: lender } }
354
+ action resume { from: suspended, to: active, actor: { party: lender } }
355
+ 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 }
356
+ }
357
+
358
+ instrument advance(line: ref<financing.credit_line>, amount: money = runtime) {
359
+ summary: "A single facility draw with an immutable principal and full repayment."
360
+ fields { line: ref<line>, amount: money = amount, principalReceivable: account(self, claim) }
361
+ lifecycle { states: [pending, drawn, repaid, cancelled], initial: pending }
362
+ action create { requires self.amount <= self.line.limit }
363
+ invariants sum "principalReceivable.balance" of { instrument: all(financing.advance), reference: "line", anchor: self.line, states: [drawn], limit: 366 } <= self.line.limit
364
+ action draw {
365
+ from: pending, to: drawn
366
+ requires self.line in [active]
367
+ deadline: { at: self.line.expiresAt }
368
+ moves self.amount from self.line.lender to self.line.borrower; moves self.amount from self.line.debt to self.principalReceivable
369
+ }
370
+ action repay { from: drawn, to: repaid, moves self.amount from self.line.borrower to self.line.lender; moves self.amount from self.principalReceivable to self.line.debt }
371
+ action cancel { from: pending, to: cancelled }
372
+ }