@hyperscale0/hsx 3.2.0 → 4.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 (57) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +1 -1
  3. package/dist/src/ast.d.ts +17 -2
  4. package/dist/src/ast.d.ts.map +1 -1
  5. package/dist/src/ast.js.map +1 -1
  6. package/dist/src/cli.js +1 -1
  7. package/dist/src/compile.d.ts +6 -0
  8. package/dist/src/compile.d.ts.map +1 -1
  9. package/dist/src/compile.js +1194 -97
  10. package/dist/src/compile.js.map +1 -1
  11. package/dist/src/cost.d.ts +1 -1
  12. package/dist/src/cost.d.ts.map +1 -1
  13. package/dist/src/cost.js +52 -9
  14. package/dist/src/cost.js.map +1 -1
  15. package/dist/src/headers.d.ts +2 -2
  16. package/dist/src/headers.d.ts.map +1 -1
  17. package/dist/src/headers.js +3 -2
  18. package/dist/src/headers.js.map +1 -1
  19. package/dist/src/index.d.ts +3 -2
  20. package/dist/src/index.d.ts.map +1 -1
  21. package/dist/src/index.js.map +1 -1
  22. package/dist/src/lex.d.ts +1 -1
  23. package/dist/src/lex.d.ts.map +1 -1
  24. package/dist/src/lex.js +5 -0
  25. package/dist/src/lex.js.map +1 -1
  26. package/dist/src/parse.js +91 -5
  27. package/dist/src/parse.js.map +1 -1
  28. package/dist/src/std-bundle.d.ts.map +1 -1
  29. package/dist/src/std-bundle.js +10 -9
  30. package/dist/src/std-bundle.js.map +1 -1
  31. package/dist/src/version.d.ts +2 -2
  32. package/dist/src/version.js +2 -2
  33. package/docs/README.md +51 -27
  34. package/docs/headers.md +43 -40
  35. package/examples/cost-table.json +40 -324
  36. package/examples/library.hsx +12 -57
  37. package/package.json +7 -5
  38. package/src/ast.ts +11 -1
  39. package/src/cli.ts +1 -1
  40. package/src/compile.ts +1630 -114
  41. package/src/cost.ts +60 -16
  42. package/src/headers.ts +3 -2
  43. package/src/index.ts +12 -1
  44. package/src/lex.ts +5 -0
  45. package/src/parse.ts +87 -5
  46. package/src/std-bundle.ts +10 -9
  47. package/src/version.ts +2 -2
  48. package/std/approvals.hsx +3 -3
  49. package/std/collections.hsx +45 -4
  50. package/std/escrow.hsx +17 -8
  51. package/std/financing.hsx +292 -42
  52. package/std/insurance.hsx +4 -5
  53. package/std/lending.hsx +7 -8
  54. package/std/marketplace.hsx +90 -6
  55. package/std/money.hsx +15 -49
  56. package/std/reporting.hsx +1286 -0
  57. package/std/travel.hsx +5 -6
package/std/financing.hsx CHANGED
@@ -1,12 +1,14 @@
1
1
  header financing
2
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)) {
3
+ 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 = object(escrow.hold), approval: approval = programOperator, borrower: party = party(person), capital: party = programOperator, share: percent = 0%, limits: ref<financing.limits> = object(financing.limits)) {
4
+ revisioned: true
5
+ familyRevision: 1
4
6
  summary: "Funding, profit recognition and payment order chosen by the program."
5
7
  fields {
6
8
  funds: ref<funds>, borrower: account of borrower, settlementIdentity: text, borrowerLimit: ref<limits>, portfolioLimit: ref<limits.portfolio>
7
9
  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
+ allocatedPayable: account(capital, cash, "allocatedProfitPayable"), capital: account(capital, cash, "capital"), profitIncome: account(capital, cash, "profitIncome"), loss: account(capital, claim, "loss")
11
+ downPaymentRemaining: money?, downPaymentCredit: money?, principalReturned: money?, profitReturned: money?, lossRecorded: money?
10
12
  price: money = self.funds.price
11
13
  downPayment: money = rate(self.price, down_payment)
12
14
  principal: money = subtract(self.price, self.downPayment)
@@ -17,35 +19,49 @@ instrument installments(months: integer(1, 366), profit: percent, disburse_to: e
17
19
  invariants count of { instrument: all(financing.installments), reference: "borrowerLimit", anchor: self.borrowerLimit, states: [active], limit: 366 } <= self.borrowerLimit.activePlans
18
20
  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
21
  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 }
22
+ action create {
23
+ requires self.borrower == self.funds.payer; requires self.borrowerLimit.borrower == self.funds.payer
24
+ 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 } } }]
25
+ }
21
26
  action sign {
22
- from: quoted, to: signed, input { signedOffer: text, affordabilityReference: text }
27
+ from: quoted, to: signed, actor: { party: borrower }, input { signedOffer: text, affordabilityReference: text }
23
28
  requires count of { instrument: slice, reference: "plan", anchor: self.id, states: [draft], limit: 366 } == self.months
24
29
  }
25
30
  action disburse {
26
31
  from: signed, to: active
27
- requires approval by approval
32
+ requires approval by approval protectedRequest self differentFromInitiator true
28
33
  requires self.borrowerLimit in [approved]
29
34
  requires self.portfolioLimit in [approved]
30
35
  when disburse_to is funds {
31
36
  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
37
+ invoke: [{ reference: self.id, action: fund_down_payment, input: {} }]
34
38
  }
35
39
  when disburse_to is borrower { moves self.principal from self.capital to self.borrower }
36
40
  invoke: [{ selection: { instrument: slice, reference: "plan", anchor: self.id, states: [draft], limit: 366, order: [position] }, action: activate, input: {} }]
37
41
  when disburse_to is funds { invoke: [{ reference: self.funds, action: confirm, input: {} }] }
38
42
  }
43
+ action fund_down_payment {
44
+ from: active, to: active, actor: { parent: current() }
45
+ 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 }] }]
46
+ moves self.downPaymentRemaining from self.funds.payer to self.funds.held
47
+ moves self.principal from self.capital to self.funds.held
48
+ }
39
49
  action complete {
40
50
  from: active, to: paid
41
51
  requires count of { instrument: slice, reference: "plan", anchor: self.id, states: [pending, due], limit: 366 } == 0
52
+ requires sum "receivable.balance" of { instrument: all(financing.late_charge), reference: "plan", anchor: self.id, states: [assessed, collected], limit: 366 } == "0"
53
+ requires sum "receivable.balance" of { instrument: all(financing.late_charge.cost), reference: "plan", anchor: self.id, states: [assessed, collected], limit: 366 } == "0"
42
54
  }
43
55
  action restore { from: [active, paid], to: active, actor: { parent: payment } }
44
56
  action payoff {
45
- from: active, to: paid
57
+ from: active, to: paid, actor: { party: borrower }
58
+ input { quote: ref<payoff_quote> }
59
+ requires input.quote.plan == self.id; requires input.quote.revision == self.productRevision
60
+ deadline: { at: input.quote.expiresAt }
46
61
  calculate: [
47
62
  { target: "principalReturned", op: "aggregate", selection: { instrument: slice, reference: "plan", anchor: self.id, states: [pending, due], limit: 366 }, measure: { sum: "principalReceivable.balance" } }
48
63
  ]
64
+ 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: {} }]
49
65
  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
66
  }
51
67
  action record_payoff {
@@ -54,11 +70,11 @@ instrument installments(months: integer(1, 366), profit: percent, disburse_to: e
54
70
  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
71
  }
56
72
  action write_off {
57
- from: active, to: written_off
73
+ from: active, to: written_off, actor: { parent: write_off_request }
58
74
  input { overdueSlice: ref<slice> }
59
75
  calculate: [{ target: "lossRecorded", op: "aggregate", selection: { instrument: slice, reference: "plan", anchor: self.id, states: [pending, due], limit: 366 }, measure: { sum: "principalReceivable.balance" } }]
60
76
  due: { at: input.overdueSlice.lossEligibleAt }
61
- requires approval by approval; requires input.overdueSlice.plan == self.id; requires input.overdueSlice.principalReceivable.balance > "0"
77
+ requires input.overdueSlice.plan == self.id; requires input.overdueSlice.principalReceivable.balance > "0"
62
78
  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
79
  }
64
80
  action record_payment {
@@ -67,21 +83,125 @@ instrument installments(months: integer(1, 366), profit: percent, disburse_to: e
67
83
  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
84
  }
69
85
  action cancel { from: quoted, to: cancelled }
86
+ action void { from: signed, to: cancelled, actor: { party: borrower } }
70
87
  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") }
88
+ write_off_request: {
89
+ fields { plan: ref<parent>, overdueSlice: ref<slice> }
90
+ lifecycle { states: [requested, applied, cancelled], initial: requested }
91
+ action create { requires self.overdueSlice.plan == self.plan; requires self.plan in [active] }
92
+ action apply {
93
+ from: requested, to: applied
94
+ requires approval by approval protectedRequest self differentFromInitiator true
95
+ invoke: [{ reference: self.plan, action: write_off, input: { overdueSlice: { field: self.overdueSlice } } }]
96
+ }
97
+ action cancel { from: requested, to: cancelled }
98
+ }
99
+ payoff_quote: {
100
+ fields {
101
+ plan: ref<parent>, revision: text?, effectiveAt: date?, expiresAt: date = after(self.createdAt, payoff_quote_expiry)
102
+ principal: money?, earnedProfit: money?, unearnedProfit: money?, rebate: money?, fines: money?, costs: money?, gross: money?, amount: money?
103
+ }
104
+ lifecycle { states: [quoted], initial: quoted }
105
+ action create {
106
+ requires self.plan in [active]
107
+ set: { revision: { field: self.productRevision }, effectiveAt: { field: self.now } }
108
+ invoke: [
109
+ { selection: { instrument: slice, reference: "plan", anchor: self.plan, states: [pending], limit: 366 }, action: quote_pending, input: { quote: { field: self.id } } }
110
+ { selection: { instrument: slice, reference: "plan", anchor: self.plan, states: [due], limit: 366 }, action: quote_due, input: { quote: { field: self.id } } }
111
+ { reference: self.id, action: record, input: {} }
112
+ ]
113
+ }
114
+ action record {
115
+ from: quoted, to: quoted, actor: { parent: current() }
116
+ calculate: [
117
+ { target: principal, op: aggregate, selection: { instrument: slice, reference: "plan", anchor: self.plan, states: [pending, due, paid], limit: 366 }, measure: { sum: "principalReceivable.balance" } }
118
+ { target: earnedProfit, op: aggregate, selection: { instrument: slice, reference: "plan", anchor: self.plan, states: [pending, due, paid], limit: 366 }, measure: { sum: "profitEarned.balance" } }
119
+ { target: unearnedProfit, op: aggregate, selection: { instrument: slice, reference: "plan", anchor: self.plan, states: [pending, due, paid], limit: 366 }, measure: { sum: "profitReceivable.balance" } }
120
+ { target: rebate, op: aggregate, selection: { instrument: quote_piece, reference: "quote", anchor: self.id, states: [recorded], limit: 366 }, measure: { sum: "rebate" } }
121
+ { target: fines, op: aggregate, selection: { instrument: all(financing.late_charge), reference: "plan", anchor: self.plan, states: [assessed, collected], limit: 366 }, measure: { sum: "receivable.balance" } }
122
+ { 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" } }
123
+ { target: gross, op: sum, values: [{ field: self.principal }, { field: self.earnedProfit }, { field: self.unearnedProfit }, { field: self.fines }, { field: self.costs }] }
124
+ { target: amount, op: subtract, base: { field: self.gross }, subtract: [{ field: self.rebate }] }
125
+ ]
126
+ }
127
+ }
128
+ quote_piece: {
129
+ fields { quote: ref<payoff_quote>, slice: ref<slice>, rebate: money = rate(self.slice.profitReceivable.balance, payoff_rebate) }
74
130
  lifecycle { states: [recorded], initial: recorded }
131
+ action create { actor: { parent: slice }, input { quote: ref<payoff_quote>, slice: ref<slice> }, requires unique "payoff_slice_quote" on [self.quote, self.slice] }
132
+ }
133
+ amendment: {
134
+ fields {
135
+ plan: ref<parent>, dates: list(date, months), agreement: text, waiverPercent: percent, waiverEvidence: text, maximumWaiver: percent = waiver_limit
136
+ revision: text?, acceptedAt: date?, staleReason: text?, expiresAt: date = after(self.createdAt, amendment_expiry)
137
+ fineDisposition: enum(carry, require_waiver) = assessed_fines, addedProfit: money = 0 SAR
138
+ principalSnapshot: money?, earnedProfitSnapshot: money?, unearnedProfitSnapshot: money?, finesSnapshot: money?, costsSnapshot: money?
139
+ extension: duration = max_extension, maximum: integer = max_amendments
140
+ }
141
+ lifecycle { states: [proposed, applied, declined, withdrawn, expired, superseded], initial: proposed }
142
+ action create {
143
+ actor: { party: borrower }
144
+ requires self.plan in [active]; requires self.waiverPercent <= self.maximumWaiver
145
+ set: { revision: { field: self.productRevision } }
146
+ calculate: [
147
+ { target: principalSnapshot, op: aggregate, selection: { instrument: slice, reference: "plan", anchor: self.plan, states: [pending, due, paid], limit: 366 }, measure: { sum: "principalReceivable.balance" } }
148
+ { target: earnedProfitSnapshot, op: aggregate, selection: { instrument: slice, reference: "plan", anchor: self.plan, states: [pending, due, paid], limit: 366 }, measure: { sum: "profitEarned.balance" } }
149
+ { target: unearnedProfitSnapshot, op: aggregate, selection: { instrument: slice, reference: "plan", anchor: self.plan, states: [pending, due, paid], limit: 366 }, measure: { sum: "profitReceivable.balance" } }
150
+ { target: finesSnapshot, op: aggregate, selection: { instrument: all(financing.late_charge), reference: "plan", anchor: self.plan, states: [assessed, collected], limit: 366 }, measure: { sum: "receivable.balance" } }
151
+ { 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" } }
152
+ ]
153
+ requires count of { instrument: current(), reference: "plan", anchor: self.plan, states: [applied], limit: 366 } < self.maximum
154
+ 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: {} }]
155
+ }
156
+ action accept {
157
+ from: proposed, to: applied, actor: { party: borrower }, deadline: { at: self.expiresAt }
158
+ requires self.plan in [active]; requires self.revision == self.productRevision
159
+ requires approval by approval protectedRequest self differentFromInitiator true
160
+ requires count of { instrument: current(), reference: "plan", anchor: self.plan, states: [applied], limit: 366 } < self.maximum
161
+ set: { acceptedAt: { field: self.now } }
162
+ when assessed_fines is require_waiver { requires self.waiverPercent == 10000 }
163
+ 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 } } }]
164
+ 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: {} }]
165
+ invoke: [{ selection: { instrument: slice, reference: "plan", anchor: self.plan, states: [pending, due], limit: 366, order: [position] }, action: amend, input: { amendment: { field: self.id } } }]
166
+ invoke: [{ selection: { instrument: slice, reference: "plan", anchor: self.plan, states: [paid], limit: 366 }, action: verify_fixed_date, input: { amendment: { field: self.id } } }]
167
+ }
168
+ action withdraw { from: proposed, to: withdrawn, actor: { party: borrower } }
169
+ action decline { from: proposed, to: declined, actor: { party: approval } }
170
+ action expire { from: proposed, to: expired, actor: clock, due: { at: self.expiresAt } }
171
+ action supersede { from: proposed, to: superseded, requires self.revision != self.productRevision, set: { staleReason: { literal: "economic_revision_changed" } } }
172
+ }
173
+ proposed_date: {
174
+ 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) }
175
+ lifecycle { states: [recorded], initial: recorded }
176
+ action create { actor: { parent: amendment }, input { amendment: ref<amendment>, position: integer(1, months) }, requires self.date >= self.previous }
177
+ action check_order { from: recorded, to: recorded, actor: { parent: amendment }, requires self.date > self.previous }
178
+ }
179
+ date_change: {
180
+ fields { amendment: ref<amendment>, slice: ref<slice>, oldDate: date, newDate: date }
181
+ lifecycle { states: [recorded], initial: recorded }
182
+ 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] }
183
+ }
184
+ settlement: {
185
+ fields { plan: ref<parent>, identity: text, principal: money, profit: money, loss: money, kind: enum(cash, loss), share: money?, retainedProfit: money?
186
+ allocatedPayable: account(capital, cash, "allocatedProfitPayable"), capital: account(capital, cash, "capital"), profitIncome: account(capital, cash, "profitIncome"), lossAccount: account(capital, claim, "loss") }
187
+ lifecycle { states: [recorded, refunded], initial: recorded }
75
188
  action create {
76
189
  actor: { parent: parent }
77
190
  input { plan: ref<parent>, identity: text, principal: money, profit: money, loss: money, kind: enum(cash, loss) }
78
191
  requires unique "settlement_identity" on [self.plan, self.identity]
192
+ 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 }] }]
193
+ moves self.share from self.profitIncome to self.allocatedPayable
194
+ }
195
+ action refund {
196
+ from: recorded, to: refunded, actor: { parent: payment }
197
+ moves self.share from self.allocatedPayable to self.profitIncome
79
198
  }
80
199
  }
81
200
  slice: {
201
+ revisioned: true
82
202
  fields {
83
203
  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)
204
+ 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?
85
205
  principalBase: money = divide(self.plan.principal, months)
86
206
  principalAllocated: money = multiply(self.principalBase, months)
87
207
  principalRemainder: money = subtract(self.plan.principal, self.principalAllocated)
@@ -100,8 +220,44 @@ instrument installments(months: integer(1, 366), profit: percent, disburse_to: e
100
220
  }
101
221
  lifecycle { states: [draft, pending, due, paid, written_off], initial: draft }
102
222
  action create {
223
+ actor: { parent: parent }
224
+ input { plan: ref<parent>, borrowerLimit: ref<limits>, portfolioLimit: ref<limits.portfolio>, position: integer(1, months) }
225
+ set: { dueAt: { field: self.originalDueAt } }
226
+ invoke: [{ reference: self.id, action: refresh_loss_date, input: {} }]
103
227
  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
228
  }
229
+ action quote_pending {
230
+ from: pending, to: pending, actor: { parent: payoff_quote }, input { quote: ref<payoff_quote> }
231
+ requires input.quote.plan == self.plan
232
+ invoke: [{ instrument: quote_piece, action: create, input: { quote: { field: input.quote }, slice: { field: self.id } } }]
233
+ }
234
+ action quote_due {
235
+ from: due, to: due, actor: { parent: payoff_quote }, input { quote: ref<payoff_quote> }
236
+ requires input.quote.plan == self.plan
237
+ invoke: [{ instrument: quote_piece, action: create, input: { quote: { field: input.quote }, slice: { field: self.id } } }]
238
+ }
239
+ action refresh_loss_date {
240
+ from: [draft, pending, due], to: draft, actor: { parent: current() }
241
+ calculate: [{ target: lossEligibleAt, op: shift, date: { field: self.dueAt }, milliseconds: { literal: write_off_after }, direction: after }]
242
+ }
243
+ action amend {
244
+ from: [pending, due], to: pending, actor: { parent: amendment }, input { amendment: ref<amendment> }
245
+ requires input.amendment.plan == self.plan
246
+ when allow_overdue is blocked { requires self.dueAt > self.now }
247
+ calculate: [
248
+ { target: nextDueAt, op: at, list: input.amendment.dates, position: { field: self.position } }
249
+ { target: lossEligibleAt, op: shift, date: { field: self.nextDueAt }, milliseconds: { literal: write_off_after }, direction: after }
250
+ ]
251
+ requires self.nextDueAt > self.now; requires self.nextDueAt >= self.dueAt; requires self.nextDueAt <= self.extensionLimit
252
+ set: { previousDueAt: { field: self.dueAt }, dueAt: { field: self.nextDueAt } }
253
+ invoke: [{ instrument: date_change, action: create, input: { amendment: { field: input.amendment }, slice: { field: self.id }, oldDate: { field: self.previousDueAt }, newDate: { field: self.nextDueAt } } }]
254
+ when profit_earned is by_schedule { invoke: [{ selection: { instrument: recognition, reference: "slice", anchor: self.id, states: [pending], limit: 1 }, action: reschedule, input: {} }] }
255
+ }
256
+ action verify_fixed_date {
257
+ from: paid, to: paid, actor: { parent: amendment }, input { amendment: ref<amendment> }
258
+ calculate: [{ target: nextDueAt, op: at, list: input.amendment.dates, position: { field: self.position } }]
259
+ requires self.nextDueAt == self.dueAt
260
+ }
105
261
  action activate {
106
262
  from: draft, to: pending, actor: { parent: parent }
107
263
  moves self.principal from self.debt to self.principalReceivable
@@ -113,21 +269,36 @@ instrument installments(months: integer(1, 366), profit: percent, disburse_to: e
113
269
  from: pending, to: due, actor: clock, due: { at: self.dueAt }
114
270
  }
115
271
  action collect {
116
- from: [pending, due], to: due, actor: { parent: payment }
272
+ from: [pending, due], to: preserve, actor: { parent: payment }
117
273
  input { payment: ref<payment> }
118
274
  requires input.payment.plan == self.plan
119
275
  when profit_earned is on_payment { calculate: [{ target: collectibleProfit, op: sum, values: [{ field: self.profitReceivable.balance }] }] }
120
276
  when profit_earned is by_schedule { calculate: [{ target: collectibleProfit, op: sum, values: [{ field: self.profitEarned.balance }] }] }
121
277
  when profit_earned is at_disbursement { calculate: [{ target: collectibleProfit, op: sum, values: [{ field: self.profitEarned.balance }] }] }
122
- when apply is fines_profit_principal {
123
- invoke: [{ instrument: profit_piece, action: create, input: { slice: { field: self.id }, payment: { field: input.payment }, limit: { field: self.collectibleProfit } } }, { instrument: principal_piece, action: create, input: { slice: { field: self.id }, payment: { field: input.payment }, limit: { field: self.principalReceivable.balance } } }]
124
- }
125
278
  when apply is principal_profit {
126
- invoke: [{ instrument: principal_piece, action: create, input: { slice: { field: self.id }, payment: { field: input.payment }, limit: { field: self.principalReceivable.balance } } }, { instrument: profit_piece, action: create, input: { slice: { field: self.id }, payment: { field: input.payment }, limit: { field: self.collectibleProfit } } }]
279
+ invoke: [
280
+ { instrument: principal_piece, action: create, input: { slice: { field: self.id }, payment: { field: input.payment }, limit: { field: self.principalReceivable.balance } } }
281
+ { instrument: profit_piece, action: create, input: { slice: { field: self.id }, payment: { field: input.payment }, limit: { field: self.collectibleProfit } } }
282
+ { 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: {} }
283
+ ]
127
284
  }
128
285
  }
286
+ action collect_waterfall {
287
+ from: [pending, due], to: preserve, actor: { parent: payment }, input { payment: ref<payment> }
288
+ requires input.payment.plan == self.plan
289
+ when profit_earned is on_payment { calculate: [{ target: collectibleProfit, op: sum, values: [{ field: self.profitReceivable.balance }] }] }
290
+ when profit_earned is by_schedule { calculate: [{ target: collectibleProfit, op: sum, values: [{ field: self.profitEarned.balance }] }] }
291
+ when profit_earned is at_disbursement { calculate: [{ target: collectibleProfit, op: sum, values: [{ field: self.profitEarned.balance }] }] }
292
+ invoke: [
293
+ { 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 } } }
294
+ { 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 } } }
295
+ { instrument: profit_piece, action: create, input: { slice: { field: self.id }, payment: { field: input.payment }, limit: { field: self.collectibleProfit } } }
296
+ { instrument: principal_piece, action: create, input: { slice: { field: self.id }, payment: { field: input.payment }, limit: { field: self.principalReceivable.balance } } }
297
+ { 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: {} }
298
+ ]
299
+ }
129
300
  action collect_pro_rata {
130
- from: [pending, due], to: due, actor: { parent: payment }, input { payment: ref<payment> }
301
+ from: [pending, due], to: preserve, actor: { parent: payment }, input { payment: ref<payment> }
131
302
  requires input.payment.plan == self.plan
132
303
  when profit_earned is on_payment { calculate: [{ target: collectibleProfit, op: sum, values: [{ field: self.profitReceivable.balance }] }] }
133
304
  when profit_earned is by_schedule { calculate: [{ target: collectibleProfit, op: sum, values: [{ field: self.profitEarned.balance }] }] }
@@ -141,13 +312,17 @@ instrument installments(months: integer(1, 366), profit: percent, disburse_to: e
141
312
  { target: profitPayment, op: minimum, values: [{ field: self.profitBudget }, { field: self.collectibleProfit }] }
142
313
  { target: principalPayment, op: subtract, base: { field: self.paymentBudget }, subtract: [{ field: self.profitPayment }] }
143
314
  ]
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 } } }]
315
+ invoke: [
316
+ { instrument: principal_piece, action: create, input: { slice: { field: self.id }, payment: { field: input.payment }, limit: { field: self.principalPayment } } }
317
+ { instrument: profit_piece, action: create, input: { slice: { field: self.id }, payment: { field: input.payment }, limit: { field: self.profitPayment } } }
318
+ { 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: {} }
319
+ ]
145
320
  }
146
321
  action complete {
147
322
  from: [pending, due], to: paid
148
323
  requires self.principalReceivable.balance == "0"; requires self.profitReceivable.balance == "0"; requires self.profitEarned.balance == "0"
149
324
  }
150
- action restore { from: [pending, due, paid], to: due, actor: { parent: payment } }
325
+ action restore { from: [pending, due, paid], to: pending, actor: { parent: payment } }
151
326
  action payoff {
152
327
  from: [pending, due], to: paid, actor: { parent: parent }, input { identity: text }
153
328
  set: { payoffIdentity: { field: input.identity } }
@@ -167,16 +342,18 @@ instrument installments(months: integer(1, 366), profit: percent, disburse_to: e
167
342
  fields { slice: ref<slice>, dueAt: date = after(self.slice.dueAt, 0) }
168
343
  lifecycle { states: [pending, recognised], initial: pending }
169
344
  action create { actor: { parent: slice }, input { slice: ref<slice> }, requires unique "profit_recognition" on [self.slice] }
345
+ action reschedule { from: pending, to: pending, actor: { parent: slice } }
170
346
  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
347
  }
172
348
  payment: {
349
+ revisioned: true
173
350
  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
351
  outstandingPrincipal: money?, outstandingProfit: money?, outstandingFines: money?, outstandingCosts: money?, outstandingTotal: money?, principalReturned: money?, profitReturned: money?, lossRecorded: money? }
176
352
 
177
353
  lifecycle { states: [pending, paid, refunded], initial: pending }
178
354
  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
355
  action pay {
356
+ expansionLimit: 8192
180
357
  from: pending, to: paid
181
358
  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
359
  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" } }] }
@@ -190,14 +367,13 @@ instrument installments(months: integer(1, 366), profit: percent, disburse_to: e
190
367
  requires self.plan in [active]; requires self.amount <= self.outstandingTotal
191
368
  moves self.amount from self.plan.borrower to self.held
192
369
  when apply is principal_profit {
193
- invoke: [{ selection: { instrument: slice, reference: "plan", anchor: self.plan, states: [pending, due], limit: 366, order: [position] }, action: collect, input: { payment: { field: self.id } } }]
370
+ 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 } } }]
194
371
  }
195
372
  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], limit: 366, order: [position] }, action: collect, input: { payment: { field: self.id } } }]
373
+ 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 } } }]
198
374
  }
199
375
  when apply is pro_rata {
200
- invoke: [{ selection: { instrument: slice, reference: "plan", anchor: self.plan, states: [pending, due], limit: 366, order: [position] }, action: collect_pro_rata, input: { payment: { field: self.id } } }]
376
+ 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 } } }]
201
377
  }
202
378
  invoke: [{ reference: self.id, action: record, input: {} }]
203
379
  }
@@ -210,6 +386,8 @@ instrument installments(months: integer(1, 366), profit: percent, disburse_to: e
210
386
  action refund {
211
387
  from: paid, to: refunded
212
388
  requires unique "settlement_consumption" on [self.plan, self.paymentIdentity]; requires self.plan in [active, paid]
389
+ requires count of { instrument: settlement, reference: "plan", anchor: self.plan, states: [recorded], limit: 366, where: { identity: { field: self.paymentIdentity } } } == 1
390
+ invoke: [{ selection: { instrument: settlement, reference: "plan", anchor: self.plan, states: [recorded], limit: 366, where: { identity: { field: self.paymentIdentity } } }, action: refund, input: {} }]
213
391
  when apply is fines_profit_principal {
214
392
  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
393
  }
@@ -217,9 +395,10 @@ instrument installments(months: integer(1, 366), profit: percent, disburse_to: e
217
395
  }
218
396
  }
219
397
  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) }
398
+ fields { slice: ref<slice>, payment: ref<payment>, limit: money, amount: money? }
221
399
  lifecycle { states: [paid, refunded], initial: paid }
222
400
  action create {
401
+ calculate: [{ target: amount, op: minimum, values: [{ field: self.payment.held.balance }, { field: self.slice.principalReceivable.balance }, { field: self.limit }] }]
223
402
  actor: { parent: slice }
224
403
  input { slice: ref<slice>, payment: ref<payment>, limit: money }
225
404
  requires unique "principal_receipt" on [self.slice, self.payment]
@@ -231,9 +410,10 @@ instrument installments(months: integer(1, 366), profit: percent, disburse_to: e
231
410
  }
232
411
  }
233
412
  profit_piece: {
234
- fields { slice: ref<slice>, payment: ref<payment>, limit: money, amount: money = minimum(self.payment.held.balance, self.limit) }
413
+ fields { slice: ref<slice>, payment: ref<payment>, limit: money, amount: money? }
235
414
  lifecycle { states: [paid, refunded], initial: paid }
236
415
  action create {
416
+ calculate: [{ target: amount, op: minimum, values: [{ field: self.payment.held.balance }, { field: self.limit }] }]
237
417
  actor: { parent: slice }
238
418
  input { slice: ref<slice>, payment: ref<payment>, limit: money }
239
419
  requires unique "profit_receipt" on [self.slice, self.payment]
@@ -254,16 +434,18 @@ instrument installments(months: integer(1, 366), profit: percent, disburse_to: e
254
434
  }
255
435
 
256
436
  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)) {
437
+ revisioned: true
438
+ familyRevision: 1
257
439
  summary: "Separate approved fine and evidenced recovery claims on an overdue installment."
258
440
  fields {
259
- plan: ref<on>, slice: ref<on.slice>, borrower: account of borrower, fine: money = fine, overdueAt: date = after(self.slice.dueAt, grace)
441
+ plan: ref<on>, slice: ref<on.slice>, borrower: account of borrower, fine: money = fine, overdueAt: date?
260
442
  receivable: account(self, claim), debt: account(borrower, claim, contra, "debt")
261
443
  }
262
444
  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 }
445
+ action create { 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 }
264
446
  action assess {
265
447
  from: proposed, to: assessed, due: { at: self.overdueAt }
266
- requires self.slice.principalReceivable.balance > "0"; requires approval by approval
448
+ requires self.slice.principalReceivable.balance > "0"; requires approval by approval protectedRequest self differentFromInitiator true
267
449
  moves self.fine from self.debt to self.receivable
268
450
  }
269
451
  action collect {
@@ -272,9 +454,20 @@ instrument late_charge(on: ref<financing.installments>[], grace: duration = 3d,
272
454
  requires input.payment.plan == self.plan
273
455
  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
456
  }
457
+ action payoff {
458
+ from: [assessed, collected], to: collected
459
+ requires self.plan in [paid]
460
+ moves self.receivable.balance from self.plan.borrower to fines_to
461
+ moves self.receivable.balance from self.receivable to self.debt
462
+ }
275
463
  action finish { from: assessed, to: collected, actor: { parent: current() }, requires self.receivable.balance == "0" }
276
464
  action cancel { from: proposed, to: waived }
277
- action waive { from: assessed, to: waived, moves self.receivable.balance from self.receivable to self.debt }
465
+ action waive_for_amendment {
466
+ from: [assessed, collected], to: assessed, actor: { parent: child(on, amendment) }, input { amendment: ref<on.amendment> }
467
+ requires input.amendment.plan == self.plan; requires input.amendment in [applied]
468
+ invoke: [{ instrument: waiver, action: create, input: { assessment: { field: self.id }, amendment: { field: input.amendment } } }]
469
+ }
470
+ action waive { from: [assessed, collected], to: waived, actor: { parent: waiver_request }, input { reason: text, evidence: text }, moves self.receivable.balance from self.receivable to self.debt }
278
471
  action refund {
279
472
  from: [assessed, collected], to: assessed, input { payment: ref<on.payment> }
280
473
  requires input.payment in [paid, refunded]
@@ -282,23 +475,45 @@ instrument late_charge(on: ref<financing.installments>[], grace: duration = 3d,
282
475
  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
476
  }
284
477
  records {
478
+ waiver_request: {
479
+ fields { assessment: ref<parent>, reason: text, evidence: text }
480
+ lifecycle { states: [requested, applied, cancelled], initial: requested }
481
+ action create { requires self.assessment in [assessed, collected] }
482
+ action apply {
483
+ from: requested, to: applied, input { reason: text, evidence: text }
484
+ requires approval by approval protectedRequest self differentFromInitiator true
485
+ invoke: [{ reference: self.assessment, action: waive, input: { reason: { field: input.reason }, evidence: { field: input.evidence } } }]
486
+ }
487
+ action cancel { from: requested, to: cancelled }
488
+ }
489
+ waiver: {
490
+ fields { assessment: ref<parent>, amendment: ref<on.amendment>, amount: money? }
491
+ lifecycle { states: [recorded], initial: recorded }
492
+ action create {
493
+ actor: { parent: parent }, input { assessment: ref<parent>, amendment: ref<on.amendment> }
494
+ calculate: [{ target: amount, op: rate, base: { field: self.assessment.receivable.balance }, bps: { field: self.amendment.waiverPercent }, rounding: floor }]
495
+ requires unique "amendment_waiver" on [self.assessment, self.amendment]
496
+ moves self.amount from self.assessment.receivable to self.assessment.debt
497
+ }
498
+ }
285
499
  receipt: {
286
- fields { assessment: ref<parent>, payment: ref<on.payment>, amount: money = minimum(self.payment.held.balance, self.assessment.receivable.balance) }
500
+ fields { assessment: ref<parent>, payment: ref<on.payment>, amount: money? }
287
501
  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 }
502
+ action create { 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; moves self.amount from self.assessment.receivable to self.assessment.debt }
289
503
  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
504
  }
291
505
 
292
506
  cost: {
507
+ revisioned: true
293
508
  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)
509
+ plan: ref<on>, assessment: ref<parent>, overdueAt: date?, evidence: text, evidencedCost: money, amount: money = minimum(self.evidencedCost, cap)
295
510
  receivable: account(self, claim), debt: account(borrower, claim, contra, "debt")
296
511
  }
297
512
  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 }
513
+ 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 }
299
514
  action assess {
300
515
  from: proposed, to: assessed, due: { at: self.assessment.overdueAt }
301
- requires self.assessment.slice.principalReceivable.balance > "0"; requires approval by approval
516
+ requires self.assessment.slice.principalReceivable.balance > "0"; requires approval by approval protectedRequest self differentFromInitiator true
302
517
  moves self.amount from self.debt to self.receivable
303
518
  }
304
519
  action collect {
@@ -307,9 +522,20 @@ instrument late_charge(on: ref<financing.installments>[], grace: duration = 3d,
307
522
  requires input.payment.plan == self.plan
308
523
  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
524
  }
525
+ action payoff {
526
+ from: [assessed, collected], to: collected
527
+ requires self.plan in [paid]
528
+ moves self.receivable.balance from self.plan.borrower to costs_to
529
+ moves self.receivable.balance from self.receivable to self.debt
530
+ }
310
531
  action finish { from: assessed, to: collected, actor: { parent: current() }, requires self.receivable.balance == "0" }
311
532
  action cancel { from: proposed, to: waived }
312
- action waive { from: assessed, to: waived, moves self.receivable.balance from self.receivable to self.debt }
533
+ action waive_for_amendment {
534
+ from: [assessed, collected], to: assessed, actor: { parent: child(on, amendment) }, input { amendment: ref<on.amendment> }
535
+ requires input.amendment.plan == self.plan; requires input.amendment in [applied]
536
+ invoke: [{ instrument: waiver, action: create, input: { assessment: { field: self.id }, amendment: { field: input.amendment } } }]
537
+ }
538
+ action waive { from: [assessed, collected], to: waived, actor: { parent: waiver_request }, input { reason: text, evidence: text }, moves self.receivable.balance from self.receivable to self.debt }
313
539
  action refund {
314
540
  from: [assessed, collected], to: assessed, input { payment: ref<on.payment> }
315
541
  requires input.payment in [paid, refunded]
@@ -317,10 +543,31 @@ instrument late_charge(on: ref<financing.installments>[], grace: duration = 3d,
317
543
  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
544
  }
319
545
  records {
546
+ waiver_request: {
547
+ fields { assessment: ref<parent>, reason: text, evidence: text }
548
+ lifecycle { states: [requested, applied, cancelled], initial: requested }
549
+ action create { requires self.assessment in [assessed, collected] }
550
+ action apply {
551
+ from: requested, to: applied, input { reason: text, evidence: text }
552
+ requires approval by approval protectedRequest self differentFromInitiator true
553
+ invoke: [{ reference: self.assessment, action: waive, input: { reason: { field: input.reason }, evidence: { field: input.evidence } } }]
554
+ }
555
+ action cancel { from: requested, to: cancelled }
556
+ }
557
+ waiver: {
558
+ fields { assessment: ref<parent>, amendment: ref<on.amendment>, amount: money? }
559
+ lifecycle { states: [recorded], initial: recorded }
560
+ action create {
561
+ actor: { parent: parent }, input { assessment: ref<parent>, amendment: ref<on.amendment> }
562
+ calculate: [{ target: amount, op: rate, base: { field: self.assessment.receivable.balance }, bps: { field: self.amendment.waiverPercent }, rounding: floor }]
563
+ requires unique "amendment_waiver" on [self.assessment, self.amendment]
564
+ moves self.amount from self.assessment.receivable to self.assessment.debt
565
+ }
566
+ }
320
567
  receipt: {
321
- fields { assessment: ref<parent>, payment: ref<on.payment>, amount: money = minimum(self.payment.held.balance, self.assessment.receivable.balance) }
568
+ fields { assessment: ref<parent>, payment: ref<on.payment>, amount: money? }
322
569
  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 }
570
+ action create { 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; moves self.amount from self.assessment.receivable to self.assessment.debt }
324
571
  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
572
  }
326
573
  }
@@ -329,6 +576,7 @@ instrument late_charge(on: ref<financing.installments>[], grace: duration = 3d,
329
576
  }
330
577
 
331
578
  instrument limits(per_borrower: money, portfolio: money, borrower: party = party(person), active_plans: integer = 1) {
579
+ familyRevision: 1
332
580
  summary: "One borrower ceiling and one shared portfolio ceiling."
333
581
  fields { borrower: account of borrower, limit: money = per_borrower, activePlans: integer = active_plans }
334
582
  lifecycle { states: [pending, approved], initial: pending }
@@ -345,6 +593,7 @@ instrument limits(per_borrower: money, portfolio: money, borrower: party = party
345
593
  }
346
594
 
347
595
  instrument credit_line(borrower: party, lender: party, limit: money, expires: date) {
596
+ familyRevision: 1
348
597
  summary: "A revolving facility; each draw is a separate immutable advance."
349
598
  fields { borrower: account of borrower, lender: account of lender, debt: account(borrower, claim, contra, "debt"), limit: money = limit, expiresAt: date = expires }
350
599
  lifecycle { states: [pending, active, suspended, closed], initial: pending }
@@ -356,6 +605,7 @@ instrument credit_line(borrower: party, lender: party, limit: money, expires: da
356
605
  }
357
606
 
358
607
  instrument advance(line: ref<financing.credit_line>, amount: money = runtime) {
608
+ familyRevision: 1
359
609
  summary: "A single facility draw with an immutable principal and full repayment."
360
610
  fields { line: ref<line>, amount: money = amount, principalReceivable: account(self, claim) }
361
611
  lifecycle { states: [pending, drawn, repaid, cancelled], initial: pending }
package/std/insurance.hsx CHANGED
@@ -1,11 +1,11 @@
1
1
  header insurance
2
2
 
3
- instrument cover(holder: party, insurer: party, commission: percent = 0%, covers: ref, premium: money = runtime) {
3
+ instrument cover(holder: party, insurer: party, approval: approval = programOperator, commission: percent = 0%, covers: ref, premium: money = runtime) {
4
4
  summary: "Recurring cover with separate premium slices and commission clawback."
5
5
  fields { covers: ref<covers>, holder: account of holder, insurer: account of insurer, expiresAt: date }
6
6
  lifecycle { states: [pending, active, cancelled, expired], initial: pending }
7
7
  action create {}
8
- action activate { from: pending, to: active, actor: { party: insurer } }
8
+ action activate { from: pending, to: active, requires approval by approval protectedRequest self differentFromInitiator true }
9
9
  action cancel {
10
10
  from: active, to: cancelled
11
11
  invoke: [{ selection: { instrument: slice, reference: "cover", anchor: self.id, states: [paid], limit: 366 }, action: refund, input: {} }]
@@ -17,7 +17,6 @@ instrument cover(holder: party, insurer: party, commission: percent = 0%, covers
17
17
  fields {
18
18
  cover: ref<parent>, covers: ref<covers>, premium: money = premium, startsAt: date, endsAt: date
19
19
  commission: money = rate(self.premium, commission)
20
- carrierNet: money = subtract(self.premium, self.commission)
21
20
  }
22
21
  lifecycle { states: [pending, paid, active, expired, refunded], initial: pending }
23
22
  action create {
@@ -43,12 +42,12 @@ instrument cover(holder: party, insurer: party, commission: percent = 0%, covers
43
42
  }
44
43
 
45
44
  instrument claim(cover: ref<insurance.cover>, approved_by: approval) {
46
- summary: "A carrier decision reserves one claim payment before settlement."
45
+ summary: "A claim decision reserves one claim payment before settlement."
47
46
  fields { cover: ref<cover>, amount: money, expiresAt: date, evidence: text, reservation: text? }
48
47
  lifecycle { states: [submitted, approved, paid, denied, expired], initial: submitted }
49
48
  action create { requires self.cover in [active] }
50
49
  action approve {
51
- from: submitted, to: approved, actor: { party: approved_by }
50
+ from: submitted, to: approved
52
51
  requires approval by approved_by
53
52
  deadline: { at: self.expiresAt }
54
53
  moves reserve self.amount from self.cover.insurer to self.cover.holder capture "reservation"