@hyperscale0/hsx 1.0.338 → 1.0.339

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 (43) hide show
  1. package/dist/src/ast.d.ts +1 -0
  2. package/dist/src/ast.d.ts.map +1 -1
  3. package/dist/src/ast.js.map +1 -1
  4. package/dist/src/attachment-economics.d.ts +19 -0
  5. package/dist/src/attachment-economics.d.ts.map +1 -1
  6. package/dist/src/attachment-economics.js +101 -3
  7. package/dist/src/attachment-economics.js.map +1 -1
  8. package/dist/src/binding-contract.d.ts.map +1 -1
  9. package/dist/src/binding-contract.js +10 -4
  10. package/dist/src/binding-contract.js.map +1 -1
  11. package/dist/src/compile.d.ts.map +1 -1
  12. package/dist/src/compile.js +36 -8
  13. package/dist/src/compile.js.map +1 -1
  14. package/dist/src/examples-bundle.js +1 -1
  15. package/dist/src/examples-bundle.js.map +1 -1
  16. package/dist/src/headers.d.ts.map +1 -1
  17. package/dist/src/headers.js +3 -1
  18. package/dist/src/headers.js.map +1 -1
  19. package/dist/src/parse.js +13 -1
  20. package/dist/src/parse.js.map +1 -1
  21. package/dist/src/std-bundle.js +6 -6
  22. package/dist/src/std-bundle.js.map +1 -1
  23. package/dist/src/template-guide.d.ts.map +1 -1
  24. package/dist/src/template-guide.js +2 -1
  25. package/dist/src/template-guide.js.map +1 -1
  26. package/docs/README.md +5 -2
  27. package/examples/camera-rental.hsx +4 -2
  28. package/package.json +4 -4
  29. package/src/ast.ts +7 -1
  30. package/src/attachment-economics.ts +127 -2
  31. package/src/binding-contract.ts +13 -6
  32. package/src/compile.ts +63 -9
  33. package/src/examples-bundle.ts +1 -1
  34. package/src/headers.ts +6 -1
  35. package/src/parse.ts +16 -1
  36. package/src/std-bundle.ts +6 -6
  37. package/src/template-guide.ts +2 -1
  38. package/std/collections.hsx +4 -4
  39. package/std/financing.hsx +461 -266
  40. package/std/insurance.hsx +21 -5
  41. package/std/lending.hsx +13 -9
  42. package/std/money.hsx +88 -32
  43. package/std/reporting.hsx +6 -6
package/std/financing.hsx CHANGED
@@ -5,6 +5,17 @@ header financing
5
5
  // earnings; receipt refunds reverse captured cash. Borrower cash paid to outside
6
6
  // capital or recovery recipients passes through, whether held first or paid direct.
7
7
 
8
+ // Terms: loan_kind names the product and caps the term. max_term defaults to
9
+ // the longest term the library allows for that kind, and a company may lower it
10
+ // but never raise it:
11
+ // mortgage 360 monthly instalments
12
+ // auto 84
13
+ // personal 84 (the default kind)
14
+ // short_term 12 (pay later and other short consumer credit)
15
+ // months is the instalment count of one plan. Set it in the program to fix the
16
+ // term, or leave it runtime and supply it at create; create refuses a plan
17
+ // longer than max_term.
18
+
8
19
  // Pricing is required. amortizing reads profit_rate as an annual rate on the
9
20
  // declining balance: equal monthly instalments, the last absorbing rounding.
10
21
  // flat_total reads it as one charge on principal for the whole term (murabaha,
@@ -43,20 +54,21 @@ header financing
43
54
  // payments and no collected late charges; refund them first. unwind needs no
44
55
  // assessed late charges; waive them first. When goods never arrive on a paid plan, the
45
56
  // Product operator runs unwind_paid once the hold's deliverBy has passed, so an
46
- // early payoff never cuts the seller's delivery window short. It refunds every
47
- // payment, share, profit piece, fine and cost receipt and reverses the payoff's
48
- // profit, fines and costs, cancels the plan, and returns the hold's whole price
49
- // to the borrower; capital keeps the principal the borrower repaid. A settlement
50
- // a lending.distribution has paid out keeps its profit and share, since investors
51
- // hold that profit; only the other settlements' profit is refunded. unwind_paid
52
- // selects at most max_history (default and most 366) of each record kind, so
53
- // a payment refuses when it would leave more paid payments than max_history
54
- // less one, keeping a settlement for the payoff, or more paid profit pieces,
55
- // fine receipts or cost receipts than max_history; a fine or a recovery cost
56
- // refuses past max_history unwaived fines or costs on the plan, counted across
57
- // every late charge attachment, so a waived fine's cost keeps its own slot. A company lowers it to keep unwinds smaller. If any
58
- // source account is short, the whole action refuses and nothing moves; a
59
- // company that wants a partial refund writes that rule in its own program.
57
+ // early payoff never cuts the seller's delivery window short. It refunds the
58
+ // profit of every payment, every share, fine and cost receipt, reverses the
59
+ // payoff's profit, fines and costs, cancels the plan, and returns the hold's
60
+ // whole price to the borrower; capital keeps the principal the borrower repaid.
61
+ // A settlement a lending.distribution has paid out keeps its profit and share,
62
+ // since investors hold that profit; only the other settlements' profit is
63
+ // refunded. Each refund is its own transfer, so unwind_paid moves the plan to
64
+ // unwinding and refunds 64 payments and 16 fine and 16 cost receipts per
65
+ // operation; the plan's unwind_step clock runs the next batch a second later
66
+ // until none remain, then the plan cancels and the hold refunds. max_history
67
+ // (default and most 366) caps the late charges and recovery costs on one plan,
68
+ // counted across every late charge attachment. A company lowers it to keep
69
+ // payoffs and amendments smaller. If any source account is short, the whole
70
+ // action refuses and nothing moves; a company that wants a partial refund
71
+ // writes that rule in its own program.
60
72
  // returned_goods decides the split on returned goods, before or after payoff.
61
73
  // keep_profit, the default, keeps the profit and fines paid so far with the
62
74
  // company: unwind runs with payments still paid, and on a paid plan the Product
@@ -75,8 +87,13 @@ header financing
75
87
  // Expose parent payoff separately and create a current, unexpired payoff_quote.
76
88
  // Expose late_charge.create as propose_assessment before the clock can assess;
77
89
  // provide matching plan, slice and borrower. Fine is fixed money, not a percentage.
78
- // Collection uses the minimum of payment cash and outstanding fine; a partial
79
- // payment does not settle the remaining fine. Recovery cost needs its own record,
90
+ // A payment under fines_profit_principal first takes assessed fines and then
91
+ // recovery costs, oldest first and at most charge_limit (default 12) of each,
92
+ // then profit and principal slice by slice in due order. A payment that would
93
+ // leave cash unapplied refuses, so a plan with more open charges than
94
+ // charge_limit takes several payments. Collection uses the minimum of payment
95
+ // cash and outstanding fine; a partial payment does not settle the remaining
96
+ // fine. Recovery cost needs its own record,
80
97
  // which only the Product operator creates; only it cancels a proposed fine or cost.
81
98
  // Profit share credits capital's allocatedProfitPayable, not an outside agency.
82
99
  // Credit lines need adapter binding at activation; draws are ledger advances,
@@ -85,11 +102,16 @@ header financing
85
102
  // advance on it. Only the Product operator creates the portfolio limit and
86
103
  // approves borrower and portfolio limits. No clock declaration proves retries.
87
104
 
88
- // Cost: installments include cash and claim moves, child-account creation and
89
- // bounded invocations across slices and assessments. Month and charge limits affect
90
- // the bound. Run hsx cost on the selected policies; profit is not a platform tariff.
105
+ // Cost: the plan holds one principal, unearned profit, earned profit, fine and
106
+ // cost claim, so disbursement, payoff, write-off, cancellation and refunds post
107
+ // one transfer per party and purpose, whatever the term. Slices, pieces and
108
+ // charges are state. Only receipts carry their own transfers: a payment posts at
109
+ // most 2 per collected charge, so charge_limit bounds it, and unwind_paid
110
+ // batches its refunds. Run hsx cost on the selected policies; every action of a
111
+ // 360-month plan stays under one 253-transfer batch. Profit is not a platform
112
+ // tariff.
91
113
 
92
- instrument installments(months: integer(1, 366), max_history: integer(2, 366) = 366, pricing: enum(amortizing, flat_total), profit_rate: percent, charge_limit: integer(1, 3) = 3, disburse_to: enum(funds, borrower, seller) = funds, seller: party?, 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, refund_within: duration = 30d, returned_goods: enum(keep_profit, refund_all) = keep_profit, allow_overdue: enum(allowed, blocked) = allowed, assessed_fines: enum(carry, require_waiver) = carry, down_payment: percent = 0%, funds: ref?, borrower: party = party(person), capital: party = programOperator, share: percent = 0%, limits: ref<financing.limits> = object(financing.limits), portfolio: ref<financing.portfolio_limit> = object(financing.portfolio_limit)) {
114
+ instrument installments(loan_kind: enum(mortgage, auto, personal, short_term) = personal, max_term: integer(1, 360) = 360, months: integer(1, 360) = runtime, max_history: integer(2, 366) = 366, pricing: enum(amortizing, flat_total), profit_rate: percent, charge_limit: integer(1, 24) = 12, disburse_to: enum(funds, borrower, seller) = funds, seller: party?, 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, refund_within: duration = 30d, returned_goods: enum(keep_profit, refund_all) = keep_profit, allow_overdue: enum(allowed, blocked) = allowed, assessed_fines: enum(carry, require_waiver) = carry, down_payment: percent = 0%, funds: ref?, borrower: party = party(person), capital: party = programOperator, share: percent = 0%, limits: ref<financing.limits> = object(financing.limits), portfolio: ref<financing.portfolio_limit> = object(financing.portfolio_limit)) {
93
115
  dependencies {
94
116
  funds {
95
117
  selector: disburse_to, is: funds
@@ -102,35 +124,41 @@ instrument installments(months: integer(1, 366), max_history: integer(2, 366) =
102
124
  fix: "Bind `seller: merchant` to the declared business that receives the principal."
103
125
  }
104
126
  }
127
+ ceilings { max_term { by: loan_kind, mortgage: 360, auto: 84, personal: 84, short_term: 12 } }
128
+ constraints { months: at_most(max_term) }
105
129
  revisioned: true
106
130
  familyRevision: 1
107
131
  summary: "Funding, profit recognition and payment order chosen by the program."
108
132
  fields {
109
133
  funds: ref<funds>?, borrower: account of borrower, settlementIdentity: text, borrowerLimit: ref<limits>, portfolioLimit: ref<portfolio>
110
- months: integer = months, dates: list(date, months), historyLimit: integer = max_history, paymentLimit: integer = subtract(max_history, 1)
134
+ months: integer = months, maxTerm: integer = max_term, dates: list(date, max_term), historyLimit: integer = max_history
111
135
  allocatedPayable: account(capital, cash, "allocatedProfitPayable"), capital: account(capital, cash, "capital"), profitIncome: account(capital, cash, "profitIncome"), loss: account(capital, claim, "loss")
136
+ principalReceivable: account(self, claim), profitReceivable: account(self, claim), profitEarned: account(self, claim), fineReceivable: account(self, claim), costReceivable: account(self, claim), debt: account(borrower, claim, contra, "debt")
112
137
  principalOutstanding: money?, principalRepaid: money?, principalReturned: money?, profitReturned: money?, lossRecorded: money?, refundWindow: date?, refundUntil: date?
138
+ scheduledPrincipal: money?, scheduledProfit: money?, payoffRebate: money?, payoffProfitTotal: money?, payoffProfit: money?, payoffReceipt: text?
139
+ unwindAt: date?, paymentsLeft: integer?, finesLeft: integer?, costsLeft: integer?
113
140
  openSlices: integer?, openCharges: money?, openCosts: money?, payoffRefunds: integer?
114
141
  price: money = subject.price
115
142
  downPayment: money = rate(self.price, down_payment)
116
143
  principal: money = subtract(self.price, self.downPayment)
117
144
  flatProfit: money?
118
145
  }
119
- lifecycle { states: [quoted, signed, active, paid, written_off, cancelled], initial: quoted }
146
+ lifecycle { states: [quoted, signed, active, paid, unwinding, written_off, cancelled], initial: quoted }
120
147
  invariants count of { instrument: all(financing.installments), reference: "borrowerLimit", anchor: self.borrowerLimit, states: [active], limit: 366 } <= self.borrowerLimit.activePlans
121
- invariants sum "principalReceivable.balance" of { instrument: all(financing.installments.slice), reference: "borrowerLimit", anchor: self.borrowerLimit, states: [pending, due, paid], limit: 366 } <= self.borrowerLimit.limit
122
- invariants sum "principalReceivable.balance" of { instrument: all(financing.installments.slice), reference: "portfolioLimit", anchor: self.portfolioLimit, states: [pending, due, paid], limit: 366 } <= self.portfolioLimit.limit
148
+ invariants sum "principalReceivable.balance" of { instrument: all(financing.installments), reference: "borrowerLimit", anchor: self.borrowerLimit, states: [active], limit: 366 } <= self.borrowerLimit.limit
149
+ invariants sum "principalReceivable.balance" of { instrument: all(financing.installments), reference: "portfolioLimit", anchor: self.portfolioLimit, states: [active], limit: 366 } <= self.portfolioLimit.limit
123
150
  action create {
124
151
  subject { price: money }
125
152
  actor: { party: borrower }
126
153
  requires self.borrowerLimit.borrower == self.borrower
154
+ requires self.months <= self.maxTerm
127
155
  when pricing is flat_total { calculate: [{ target: flatProfit, op: rate, base: { field: self.principal }, bps: { literal: profit_rate }, rounding: floor }] }
128
156
  when disburse_to is funds { invoke: [{ reference: self.funds, action: check, input: { plan: { field: self.id } } }] }
129
- 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 } } }, { selection: { instrument: slice, reference: "plan", anchor: self.id, states: [draft], limit: months, where: { first: { literal: 0 } } }, action: check_order, input: {} }]
157
+ invoke: [{ range: { count: { field: self.months }, maximum: max_term, bind: position }, instrument: slice, action: create, input: { plan: { field: self.id }, borrowerLimit: { field: self.borrowerLimit }, portfolioLimit: { field: self.portfolioLimit }, position: { field: position } } }, { selection: { instrument: slice, reference: "plan", anchor: self.id, states: [draft], limit: max_term, where: { first: { literal: 0 } } }, action: check_order, input: {} }]
130
158
  }
131
159
  action sign {
132
160
  from: quoted, to: signed, actor: { party: borrower }, input { signedOffer: text, affordabilityReference: text }
133
- requires count of { instrument: slice, reference: "plan", anchor: self.id, states: [draft], limit: 366 } == self.months
161
+ requires count of { instrument: slice, reference: "plan", anchor: self.id, states: [draft], limit: max_term } == self.months
134
162
  }
135
163
  action collect_down_payment { allowZero: true,
136
164
  from: signed, to: signed, actor: { party: borrower }
@@ -142,55 +170,86 @@ instrument installments(months: integer(1, 366), max_history: integer(2, 366) =
142
170
  from: signed, to: active, actor: { party: capital }
143
171
  requires self.borrowerLimit in [approved]
144
172
  requires self.portfolioLimit in [approved]
173
+ calculate: [
174
+ { target: scheduledPrincipal, op: aggregate, selection: { instrument: slice, reference: "plan", anchor: self.id, states: [draft], limit: max_term }, measure: { sum: "principal" } }
175
+ { target: scheduledProfit, op: aggregate, selection: { instrument: slice, reference: "plan", anchor: self.id, states: [draft], limit: max_term }, measure: { sum: "profit" } }
176
+ ]
145
177
  when disburse_to is funds { invoke: [{ reference: self.funds, action: disburse, input: { plan: { field: self.id } } }] }
146
178
  when disburse_to is borrower { moves self.principal from self.capital to self.borrower economics { purpose: principal, sourceParty: capital } }
147
179
  when disburse_to is seller { moves self.principal from self.capital to seller economics { purpose: principal, sourceParty: capital } }
148
- invoke: [{ selection: { instrument: slice, reference: "plan", anchor: self.id, states: [draft], limit: 366, order: [position] }, action: activate, input: {} }]
180
+ moves self.scheduledPrincipal from self.debt to self.principalReceivable economics { purpose: internal, sourceParty: borrower }
181
+ moves self.scheduledProfit from self.debt to self.profitReceivable economics { purpose: internal, sourceParty: borrower }
182
+ when profit_earned is at_disbursement { moves self.scheduledProfit from self.profitReceivable to self.profitEarned economics { purpose: internal, sourceParty: programOperator } }
183
+ invoke: [{ selection: { instrument: slice, reference: "plan", anchor: self.id, states: [draft], limit: max_term, order: [position] }, action: activate, input: {} }]
149
184
  }
150
185
  action complete {
151
186
  from: active, to: paid
152
187
  calculate: [{ target: refundWindow, op: shift, date: { field: self.now }, milliseconds: { literal: refund_within }, direction: after }]
153
188
  set: { refundUntil: { field: self.refundWindow } }
154
- requires count of { instrument: slice, reference: "plan", anchor: self.id, states: [pending, due], limit: 366 } == 0
155
- requires sum "receivable.balance" of { instrument: all(financing.late_charge), reference: "plan", anchor: self.id, states: [assessed, collected], limit: 366 } == "0"
156
- requires sum "receivable.balance" of { instrument: all(financing.late_charge.cost), reference: "plan", anchor: self.id, states: [assessed, collected], limit: 366 } == "0"
189
+ requires count of { instrument: slice, reference: "plan", anchor: self.id, states: [pending, due], limit: max_term } == 0
190
+ requires self.fineReceivable.balance == "0"
191
+ requires self.costReceivable.balance == "0"
157
192
  }
158
193
  action restore { from: [active, paid], to: active, actor: { parent: payment }, input { refundUntil: date? }, set: { refundUntil: { field: input.refundUntil } } }
159
- action payoff {
194
+ action payoff { allowZero: true,
160
195
  from: active, to: paid, actor: { party: borrower }
161
196
  input { quote: ref<payoff_quote> }
162
197
  requires input.quote.plan == self.id; requires input.quote.revision == self.productRevision
163
198
  deadline: { at: input.quote.expiresAt }
164
199
  calculate: [
165
- { target: "principalReturned", op: "aggregate", selection: { instrument: slice, reference: "plan", anchor: self.id, states: [pending, due], limit: 366 }, measure: { sum: "principalReceivable.balance" } }
200
+ { target: principalReturned, op: sum, values: [{ field: self.principalReceivable.balance }] }
201
+ { target: payoffRebate, op: rate, base: { field: self.profitReceivable.balance }, bps: { literal: payoff_rebate }, rounding: floor }
202
+ { target: payoffProfitTotal, op: sum, values: [{ field: self.profitReceivable.balance }, { field: self.profitEarned.balance }] }
203
+ { target: payoffProfit, op: subtract, base: { field: self.payoffProfitTotal }, subtract: [{ field: self.payoffRebate }] }
166
204
  { target: refundWindow, op: shift, date: { field: self.now }, milliseconds: { literal: refund_within }, direction: after }
167
205
  ]
168
206
  set: { refundUntil: { field: self.refundWindow } }
169
- 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: {} }]
170
- 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: {} }]
207
+ moves self.principalReturned from self.borrower to self.capital economics { purpose: principal, sourceParty: borrower }
208
+ moves self.payoffProfit from self.borrower to self.profitIncome capture "payoffReceipt" economics { recipient: capital, company: { purpose: earning, sourceParty: borrower }, participant: { purpose: pass_through, sourceParty: borrower } }
209
+ moves self.principalReturned from self.principalReceivable to self.debt economics { purpose: internal, sourceParty: programOperator }
210
+ moves self.profitReceivable.balance from self.profitReceivable to self.debt economics { purpose: internal, sourceParty: programOperator }
211
+ moves self.profitEarned.balance from self.profitEarned to self.debt economics { purpose: internal, sourceParty: programOperator }
212
+ // Each attachment's oldest open charge settles that attachment's charges in
213
+ // one cash transfer; four attachments per kind are enough for any program
214
+ // and settle_payoff refuses if a charge is still open.
215
+ invoke: [
216
+ { selection: { instrument: all(financing.late_charge), reference: "plan", anchor: self.id, states: [assessed], limit: 1, batch: true, order: [overdueAt] }, action: payoff, input: {} }
217
+ { selection: { instrument: all(financing.late_charge), reference: "plan", anchor: self.id, states: [assessed], limit: 1, batch: true, order: [overdueAt] }, action: payoff, input: {} }
218
+ { selection: { instrument: all(financing.late_charge), reference: "plan", anchor: self.id, states: [assessed], limit: 1, batch: true, order: [overdueAt] }, action: payoff, input: {} }
219
+ { selection: { instrument: all(financing.late_charge), reference: "plan", anchor: self.id, states: [assessed], limit: 1, batch: true, order: [overdueAt] }, action: payoff, input: {} }
220
+ { selection: { instrument: all(financing.late_charge.cost), reference: "plan", anchor: self.id, states: [assessed], limit: 1, batch: true, order: [overdueAt] }, action: payoff, input: {} }
221
+ { selection: { instrument: all(financing.late_charge.cost), reference: "plan", anchor: self.id, states: [assessed], limit: 1, batch: true, order: [overdueAt] }, action: payoff, input: {} }
222
+ { selection: { instrument: all(financing.late_charge.cost), reference: "plan", anchor: self.id, states: [assessed], limit: 1, batch: true, order: [overdueAt] }, action: payoff, input: {} }
223
+ { selection: { instrument: all(financing.late_charge.cost), reference: "plan", anchor: self.id, states: [assessed], limit: 1, batch: true, order: [overdueAt] }, action: payoff, input: {} }
224
+ ]
225
+ invoke: [{ selection: { instrument: slice, reference: "plan", anchor: self.id, states: [pending, due], limit: max_term, order: [position] }, action: payoff, input: { identity: { field: self.settlementIdentity } } }, { reference: self.id, action: settle_payoff, input: {} }]
171
226
  }
172
- action record_payoff {
227
+ action settle_payoff {
173
228
  from: paid, to: paid, actor: { parent: current() }
174
- 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" } }]
175
- 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" } } }]
229
+ requires self.fineReceivable.balance == "0"
230
+ requires self.costReceivable.balance == "0"
231
+ invoke: [{ instrument: settlement, action: create, input: { plan: { field: self.id }, identity: { field: self.settlementIdentity }, principal: { field: self.principalReturned }, profit: { field: self.payoffProfit }, loss: { literal: "0" }, kind: { literal: "cash" } } }]
176
232
  }
177
- action write_off {
233
+ action write_off { allowZero: true,
178
234
  from: active, to: written_off, actor: { parent: write_off_request }
179
235
  input { overdueSlice: ref<slice> }
180
- calculate: [{ target: "lossRecorded", op: "aggregate", selection: { instrument: slice, reference: "plan", anchor: self.id, states: [pending, due], limit: 366 }, measure: { sum: "principalReceivable.balance" } }, { target: refundWindow, op: shift, date: { field: self.now }, milliseconds: { literal: refund_within }, direction: after }]
236
+ calculate: [{ target: lossRecorded, op: sum, values: [{ field: self.principalReceivable.balance }] }, { target: refundWindow, op: shift, date: { field: self.now }, milliseconds: { literal: refund_within }, direction: after }]
181
237
  set: { refundUntil: { field: self.refundWindow } }
182
238
  due: { at: input.overdueSlice.lossEligibleAt }
183
- requires input.overdueSlice.plan == self.id; requires input.overdueSlice.principalReceivable.balance > "0"
239
+ requires input.overdueSlice.plan == self.id; requires input.overdueSlice.openPrincipal > "0"
240
+ moves self.lossRecorded from self.principalReceivable to self.loss economics { purpose: internal, sourceParty: programOperator }
241
+ moves self.profitReceivable.balance from self.profitReceivable to self.debt economics { purpose: internal, sourceParty: programOperator }
242
+ moves self.profitEarned.balance from self.profitEarned to self.loss economics { purpose: internal, sourceParty: programOperator }
184
243
  when disburse_to is funds { invoke: [{ reference: self.funds, action: write_off, input: { plan: { field: self.id } } }] }
185
- 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" } } }]
244
+ invoke: [{ selection: { instrument: slice, reference: "plan", anchor: self.id, states: [pending, due], limit: max_term, 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" } } }]
186
245
  }
187
246
  action record_payment {
188
247
  from: active, to: active, actor: { parent: payment }
189
248
  input { identity: text, principal: money, profit: money }
190
249
  calculate: [
191
- { target: openSlices, op: aggregate, selection: { instrument: slice, reference: "plan", anchor: self.id, states: [pending, due], limit: 366 }, measure: count }
192
- { target: openCharges, op: aggregate, selection: { instrument: all(financing.late_charge), reference: "plan", anchor: self.id, states: [assessed, collected], limit: 366 }, measure: { sum: "receivable.balance" } }
193
- { target: openCosts, op: aggregate, selection: { instrument: all(financing.late_charge.cost), reference: "plan", anchor: self.id, states: [assessed, collected], limit: 366 }, measure: { sum: "receivable.balance" } }
250
+ { target: openSlices, op: aggregate, selection: { instrument: slice, reference: "plan", anchor: self.id, states: [pending, due], limit: max_term }, measure: count }
251
+ { target: openCharges, op: sum, values: [{ field: self.fineReceivable.balance }] }
252
+ { target: openCosts, op: sum, values: [{ field: self.costReceivable.balance }] }
194
253
  ]
195
254
  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" } } }]
196
255
  invoke: [{ selection: { instrument: current(), reference: "borrowerLimit", anchor: self.borrowerLimit, states: [active], limit: 1, where: { id: { field: self.id }, openSlices: { literal: 0 }, openCharges: { literal: "0" }, openCosts: { literal: "0" } } }, action: complete, input: {} }]
@@ -201,24 +260,27 @@ instrument installments(months: integer(1, 366), max_history: integer(2, 366) =
201
260
  from: active, to: cancelled, actor: { party: capital }
202
261
  calculate: [
203
262
  { target: refundWindow, op: shift, date: { field: self.now }, milliseconds: { literal: refund_within }, direction: after }
204
- { target: principalOutstanding, op: aggregate, selection: { instrument: slice, reference: "plan", anchor: self.id, states: [pending, due], limit: 366 }, measure: { sum: "principalReceivable.balance" } }
263
+ { target: principalOutstanding, op: sum, values: [{ field: self.principalReceivable.balance }] }
205
264
  { target: principalRepaid, op: subtract, base: { field: self.principal }, subtract: [{ field: self.principalOutstanding }] }
206
265
  ]
207
266
  set: { refundUntil: { field: self.refundWindow } }
208
267
  when disburse_to is borrower { requires true == false }
209
268
  when returned_goods is refund_all {
210
- requires count of { instrument: payment, reference: "plan", anchor: self.id, states: [paid], limit: 366 } == 0
211
- requires count of { instrument: all(financing.late_charge), reference: "plan", anchor: self.id, states: [collected], limit: 366 } == 0
212
- requires count of { instrument: all(financing.late_charge.cost), reference: "plan", anchor: self.id, states: [collected], limit: 366 } == 0
269
+ requires count of { instrument: payment, reference: "plan", anchor: self.id, states: [paid], limit: 1, batch: true } == 0
270
+ requires count of { instrument: all(financing.late_charge), reference: "plan", anchor: self.id, states: [collected], limit: 1, batch: true } == 0
271
+ requires count of { instrument: all(financing.late_charge.cost), reference: "plan", anchor: self.id, states: [collected], limit: 1, batch: true } == 0
213
272
  }
214
- requires count of { instrument: all(financing.late_charge), reference: "plan", anchor: self.id, states: [assessed], limit: 366 } == 0
215
- requires count of { instrument: all(financing.late_charge.cost), reference: "plan", anchor: self.id, states: [assessed], limit: 366 } == 0
273
+ requires count of { instrument: all(financing.late_charge), reference: "plan", anchor: self.id, states: [assessed], limit: 1, batch: true } == 0
274
+ requires count of { instrument: all(financing.late_charge.cost), reference: "plan", anchor: self.id, states: [assessed], limit: 1, batch: true } == 0
216
275
  when disburse_to is funds { invoke: [{ reference: self.funds, action: refund, input: { plan: { field: self.id }, principal: { field: self.principalOutstanding } } }] }
217
276
  when disburse_to is seller {
218
277
  moves self.principalOutstanding from seller to self.capital economics { purpose: principal, sourceParty: seller }
219
278
  moves self.principalRepaid from seller to self.borrower economics { purpose: principal, sourceParty: seller }
220
279
  }
221
- invoke: [{ selection: { instrument: slice, reference: "plan", anchor: self.id, states: [pending, due], limit: 366, order: [position] }, action: cancel, input: {} }]
280
+ moves self.principalOutstanding from self.principalReceivable to self.debt economics { purpose: internal, sourceParty: programOperator }
281
+ moves self.profitReceivable.balance from self.profitReceivable to self.debt economics { purpose: internal, sourceParty: programOperator }
282
+ moves self.profitEarned.balance from self.profitEarned to self.debt economics { purpose: internal, sourceParty: programOperator }
283
+ invoke: [{ selection: { instrument: slice, reference: "plan", anchor: self.id, states: [pending, due], limit: max_term, order: [position] }, action: cancel, input: {} }]
222
284
  }
223
285
  action unwind_undelivered { allowZero: true,
224
286
  from: active, to: cancelled, actor: { party: programOperator }
@@ -226,10 +288,15 @@ instrument installments(months: integer(1, 366), max_history: integer(2, 366) =
226
288
  set: { refundUntil: { field: self.refundWindow } }
227
289
  when disburse_to is borrower { requires true == false }
228
290
  when disburse_to is seller { requires true == false }
229
- requires count of { instrument: payment, reference: "plan", anchor: self.id, states: [paid], limit: 366 } == 0
230
- requires count of { instrument: all(financing.late_charge), reference: "plan", anchor: self.id, states: [collected], limit: 366 } == 0
231
- requires count of { instrument: all(financing.late_charge.cost), reference: "plan", anchor: self.id, states: [collected], limit: 366 } == 0
232
- invoke: [{ selection: { instrument: slice, reference: "plan", anchor: self.id, states: [pending, due], limit: 366, order: [position] }, action: cancel, input: {} }]
291
+ requires count of { instrument: payment, reference: "plan", anchor: self.id, states: [paid], limit: 1, batch: true } == 0
292
+ requires count of { instrument: all(financing.late_charge), reference: "plan", anchor: self.id, states: [collected], limit: 1, batch: true } == 0
293
+ requires count of { instrument: all(financing.late_charge.cost), reference: "plan", anchor: self.id, states: [collected], limit: 1, batch: true } == 0
294
+ moves self.principalReceivable.balance from self.principalReceivable to self.debt economics { purpose: internal, sourceParty: programOperator }
295
+ moves self.profitReceivable.balance from self.profitReceivable to self.debt economics { purpose: internal, sourceParty: programOperator }
296
+ moves self.profitEarned.balance from self.profitEarned to self.debt economics { purpose: internal, sourceParty: programOperator }
297
+ moves self.fineReceivable.balance from self.fineReceivable to self.debt economics { purpose: internal, sourceParty: programOperator }
298
+ moves self.costReceivable.balance from self.costReceivable to self.debt economics { purpose: internal, sourceParty: programOperator }
299
+ invoke: [{ selection: { instrument: slice, reference: "plan", anchor: self.id, states: [pending, due], limit: max_term, order: [position] }, action: cancel, input: {} }]
233
300
  invoke: [{ selection: { instrument: all(financing.late_charge.cost), reference: "plan", anchor: self.id, states: [proposed, assessed], limit: 366 }, action: cancel_undelivered, input: {} }, { selection: { instrument: all(financing.late_charge), reference: "plan", anchor: self.id, states: [proposed, assessed], limit: 366 }, action: cancel_undelivered, input: {} }]
234
301
  when disburse_to is funds { invoke: [{ reference: self.funds, action: cancel_undelivered, input: { plan: { field: self.id } } }] }
235
302
  }
@@ -244,22 +311,61 @@ instrument installments(months: integer(1, 366), max_history: integer(2, 366) =
244
311
  }
245
312
  }
246
313
  action unwind_paid {
247
- from: paid, to: cancelled, actor: { party: programOperator }
314
+ from: paid, to: unwinding, actor: { party: programOperator }
248
315
  when disburse_to is borrower { requires true == false }
249
316
  when disburse_to is seller { requires true == false }
250
- invoke: [{ selection: { instrument: settlement, reference: "plan", anchor: self.id, states: [recorded], limit: max_history }, action: unwind, input: {} }, { selection: { instrument: profit_piece, reference: "plan", anchor: self.id, states: [paid], limit: max_history, where: { payment.status: { literal: "refunded" } } }, action: refund, input: {} }, { selection: { instrument: payment, reference: "plan", anchor: self.id, states: [paid], limit: max_history }, action: unwind, input: {} }, { reference: self.id, action: unwind_payoff_profit, input: {} }]
251
- invoke: [{ selection: { instrument: all(financing.late_charge.receipt), reference: "plan", anchor: self.id, states: [paid], limit: max_history }, action: refund, input: {} }, { selection: { instrument: all(financing.late_charge.cost.receipt), reference: "plan", anchor: self.id, states: [paid], limit: max_history }, action: refund, input: {} }, { selection: { instrument: all(financing.late_charge), reference: "plan", anchor: self.id, states: [collected], limit: max_history }, action: unwind_payoff, input: {} }, { selection: { instrument: all(financing.late_charge.cost), reference: "plan", anchor: self.id, states: [collected], limit: max_history }, action: unwind_payoff, input: {} }]
252
- invoke: [{ selection: { instrument: slice, reference: "plan", anchor: self.id, states: [pending, due, paid], limit: 366, order: [position] }, action: cancel, input: {} }]
253
- invoke: [{ selection: { instrument: all(financing.late_charge.cost), reference: "plan", anchor: self.id, states: [proposed, assessed, collected], limit: max_history }, action: cancel_undelivered, input: {} }, { selection: { instrument: all(financing.late_charge), reference: "plan", anchor: self.id, states: [proposed, assessed, collected], limit: max_history }, action: cancel_undelivered, input: {} }]
317
+ when disburse_to is funds {
318
+ requires self.funds.funds.deliverBy < self.now
319
+ when returned_goods is keep_profit { requires self.funds.funds in [funded] }
320
+ when returned_goods is refund_all { requires self.funds.funds in [funded, return_verified] }
321
+ }
322
+ invoke: [{ reference: self.id, action: unwind_batch, input: {} }]
323
+ }
324
+ action unwind_step {
325
+ from: unwinding, to: unwinding, actor: clock, due: { at: self.unwindAt }
326
+ invoke: [{ reference: self.id, action: unwind_batch, input: {} }]
327
+ }
328
+ // One batch stays under one 253-transfer operation: 64 payments at two
329
+ // transfers, 16 fine and 16 cost receipts at one, and the closing transfers.
330
+ action unwind_batch {
331
+ from: unwinding, to: unwinding, actor: { parent: current() }
332
+ calculate: [{ target: unwindAt, op: shift, date: { field: self.now }, milliseconds: { literal: 1s }, direction: after }]
333
+ invoke: [
334
+ { selection: { instrument: payment, reference: "plan", anchor: self.id, states: [paid], limit: 64, batch: true }, action: unwind_paid, input: {} }
335
+ { selection: { instrument: all(financing.late_charge.receipt), reference: "plan", anchor: self.id, states: [paid], limit: 16, batch: true }, action: unwind, input: {} }
336
+ { selection: { instrument: all(financing.late_charge.cost.receipt), reference: "plan", anchor: self.id, states: [paid], limit: 16, batch: true }, action: unwind, input: {} }
337
+ { reference: self.id, action: unwind_check, input: {} }
338
+ ]
339
+ }
340
+ action unwind_check {
341
+ from: unwinding, to: unwinding, actor: { parent: current() }
342
+ calculate: [
343
+ { target: paymentsLeft, op: aggregate, selection: { instrument: payment, reference: "plan", anchor: self.id, states: [paid], limit: 1, batch: true }, measure: count }
344
+ { target: finesLeft, op: aggregate, selection: { instrument: all(financing.late_charge.receipt), reference: "plan", anchor: self.id, states: [paid], limit: 1, batch: true }, measure: count }
345
+ { target: costsLeft, op: aggregate, selection: { instrument: all(financing.late_charge.cost.receipt), reference: "plan", anchor: self.id, states: [paid], limit: 1, batch: true }, measure: count }
346
+ ]
347
+ invoke: [{ selection: { instrument: current(), reference: "borrowerLimit", anchor: self.borrowerLimit, states: [unwinding], limit: 1, where: { id: { field: self.id }, paymentsLeft: { literal: 0 }, finesLeft: { literal: 0 }, costsLeft: { literal: 0 } } }, action: finish_unwind, input: {} }]
348
+ }
349
+ action finish_unwind { allowZero: true,
350
+ from: unwinding, to: cancelled, actor: { parent: current() }
351
+ calculate: [{ target: payoffRefunds, op: aggregate, selection: { instrument: settlement, reference: "plan", anchor: self.id, states: [recorded], limit: 1, where: { identity: { field: self.settlementIdentity } } }, measure: count }]
352
+ moves self.principalReceivable.balance from self.principalReceivable to self.debt economics { purpose: internal, sourceParty: programOperator }
353
+ moves self.profitReceivable.balance from self.profitReceivable to self.debt economics { purpose: internal, sourceParty: programOperator }
354
+ moves self.profitEarned.balance from self.profitEarned to self.debt economics { purpose: internal, sourceParty: programOperator }
355
+ moves self.fineReceivable.balance from self.fineReceivable to self.debt economics { purpose: internal, sourceParty: programOperator }
356
+ moves self.costReceivable.balance from self.costReceivable to self.debt economics { purpose: internal, sourceParty: programOperator }
357
+ invoke: [{ selection: { instrument: settlement, reference: "plan", anchor: self.id, states: [recorded], limit: 1, where: { identity: { field: self.settlementIdentity } } }, action: unwind, input: {} }, { reference: self.id, action: unwind_payoff_profit, guard: { kind: compare, left: { field: self.payoffRefunds }, operator: "==", right: { literal: 1 } }, input: {} }]
358
+ invoke: [{ selection: { instrument: all(financing.late_charge), reference: "plan", anchor: self.id, states: [collected], limit: 4, where: { payoffLed: { literal: 1 } } }, action: unwind_payoff, input: {} }, { selection: { instrument: all(financing.late_charge.cost), reference: "plan", anchor: self.id, states: [collected], limit: 4, where: { payoffLed: { literal: 1 } } }, action: unwind_payoff, input: {} }]
359
+ invoke: [{ selection: { instrument: slice, reference: "plan", anchor: self.id, states: [pending, due, paid], limit: max_term, order: [position] }, action: cancel, input: {} }]
360
+ invoke: [{ selection: { instrument: all(financing.late_charge.cost), reference: "plan", anchor: self.id, states: [proposed, assessed, collected], limit: 366 }, action: cancel_undelivered, input: {} }, { selection: { instrument: all(financing.late_charge), reference: "plan", anchor: self.id, states: [proposed, assessed, collected], limit: 366 }, action: cancel_undelivered, input: {} }]
254
361
  when disburse_to is funds {
255
362
  when returned_goods is keep_profit { invoke: [{ reference: self.funds, action: unwind_paid_undelivered, input: { plan: { field: self.id } } }] }
256
363
  when returned_goods is refund_all { invoke: [{ reference: self.funds, action: unwind_paid, input: { plan: { field: self.id } } }] }
257
364
  }
258
365
  }
259
- action unwind_payoff_profit {
366
+ action unwind_payoff_profit { allowZero: true,
260
367
  from: cancelled, to: cancelled, actor: { parent: current() }
261
- calculate: [{ target: payoffRefunds, op: aggregate, selection: { instrument: settlement, reference: "plan", anchor: self.id, states: [refunded], limit: 1, where: { identity: { field: self.settlementIdentity } } }, measure: count }]
262
- invoke: [{ selection: { instrument: slice, reference: "plan", anchor: self.id, states: [paid], limit: 366, where: { payoffIdentity: { field: self.settlementIdentity } } }, action: unwind_payoff, guard: { kind: compare, left: { field: self.payoffRefunds }, operator: "==", right: { literal: 1 } }, input: {} }]
368
+ moves self.payoffProfit from self.profitIncome to self.borrower economics { recipient: capital, company: { purpose: earning, sourceParty: capital, reversalOf: self.payoffReceipt }, participant: unclassified }
263
369
  }
264
370
  records {
265
371
  write_off_request: {
@@ -281,37 +387,25 @@ instrument installments(months: integer(1, 366), max_history: integer(2, 366) =
281
387
  action create {
282
388
  requires self.plan in [active]
283
389
  set: { revision: { field: self.productRevision }, effectiveAt: { field: self.now } }
284
- invoke: [
285
- { selection: { instrument: slice, reference: "plan", anchor: self.plan, states: [pending], limit: 366 }, action: quote_pending, input: { quote: { field: self.id } } }
286
- { selection: { instrument: slice, reference: "plan", anchor: self.plan, states: [due], limit: 366 }, action: quote_due, input: { quote: { field: self.id } } }
287
- { reference: self.id, action: record, input: {} }
288
- ]
289
- }
290
- action record {
291
- from: quoted, to: quoted, actor: { parent: current() }
292
390
  calculate: [
293
- { target: principal, op: aggregate, selection: { instrument: slice, reference: "plan", anchor: self.plan, states: [pending, due, paid], limit: 366 }, measure: { sum: "principalReceivable.balance" } }
294
- { target: earnedProfit, op: aggregate, selection: { instrument: slice, reference: "plan", anchor: self.plan, states: [pending, due, paid], limit: 366 }, measure: { sum: "profitEarned.balance" } }
295
- { target: unearnedProfit, op: aggregate, selection: { instrument: slice, reference: "plan", anchor: self.plan, states: [pending, due, paid], limit: 366 }, measure: { sum: "profitReceivable.balance" } }
296
- { target: rebate, op: aggregate, selection: { instrument: quote_piece, reference: "quote", anchor: self.id, states: [recorded], limit: 366 }, measure: { sum: "rebate" } }
297
- { target: fines, op: aggregate, selection: { instrument: all(financing.late_charge), reference: "plan", anchor: self.plan, states: [assessed, collected], limit: 366 }, measure: { sum: "receivable.balance" } }
298
- { 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" } }
391
+ { target: principal, op: sum, values: [{ field: self.plan.principalReceivable.balance }] }
392
+ { target: earnedProfit, op: sum, values: [{ field: self.plan.profitEarned.balance }] }
393
+ { target: unearnedProfit, op: sum, values: [{ field: self.plan.profitReceivable.balance }] }
394
+ { target: rebate, op: rate, base: { field: self.unearnedProfit }, bps: { literal: payoff_rebate }, rounding: floor }
395
+ { target: fines, op: sum, values: [{ field: self.plan.fineReceivable.balance }] }
396
+ { target: costs, op: sum, values: [{ field: self.plan.costReceivable.balance }] }
299
397
  { target: gross, op: sum, values: [{ field: self.principal }, { field: self.earnedProfit }, { field: self.unearnedProfit }, { field: self.fines }, { field: self.costs }] }
300
398
  { target: amount, op: subtract, base: { field: self.gross }, subtract: [{ field: self.rebate }] }
301
399
  ]
302
400
  }
303
401
  }
304
- quote_piece: {
305
- fields { quote: ref<payoff_quote>, slice: ref<slice>, rebate: money = rate(self.slice.profitReceivable.balance, payoff_rebate) }
306
- lifecycle { states: [recorded], initial: recorded }
307
- action create { actor: { parent: slice }, input { quote: ref<payoff_quote>, slice: ref<slice> }, requires unique "payoff_slice_quote" on [self.quote, self.slice] }
308
- }
309
402
  amendment: {
310
403
  fields {
311
- plan: ref<parent>, dates: list(date, months), agreement: text, waiverPercent: percent, waiverEvidence: text, maximumWaiver: percent = waiver_limit
404
+ plan: ref<parent>, dates: list(date, max_term), agreement: text, waiverPercent: percent, waiverEvidence: text, maximumWaiver: percent = waiver_limit
312
405
  revision: text?, acceptedAt: date?, staleReason: text?, expiresAt: date = after(self.createdAt, amendment_expiry)
313
406
  fineDisposition: enum(carry, require_waiver) = assessed_fines, addedProfit: money = 0 SAR
314
407
  principalSnapshot: money?, earnedProfitSnapshot: money?, unearnedProfitSnapshot: money?, finesSnapshot: money?, costsSnapshot: money?
408
+ finesWaived: money?, costsWaived: money?
315
409
  extension: duration = max_extension, maximum: integer = max_amendments
316
410
  }
317
411
  lifecycle { states: [proposed, applied, declined, withdrawn, expired, superseded], initial: proposed }
@@ -320,14 +414,14 @@ instrument installments(months: integer(1, 366), max_history: integer(2, 366) =
320
414
  requires self.plan in [active]; requires self.waiverPercent <= self.maximumWaiver
321
415
  set: { revision: { field: self.productRevision } }
322
416
  calculate: [
323
- { target: principalSnapshot, op: aggregate, selection: { instrument: slice, reference: "plan", anchor: self.plan, states: [pending, due, paid], limit: 366 }, measure: { sum: "principalReceivable.balance" } }
324
- { target: earnedProfitSnapshot, op: aggregate, selection: { instrument: slice, reference: "plan", anchor: self.plan, states: [pending, due, paid], limit: 366 }, measure: { sum: "profitEarned.balance" } }
325
- { target: unearnedProfitSnapshot, op: aggregate, selection: { instrument: slice, reference: "plan", anchor: self.plan, states: [pending, due, paid], limit: 366 }, measure: { sum: "profitReceivable.balance" } }
326
- { target: finesSnapshot, op: aggregate, selection: { instrument: all(financing.late_charge), reference: "plan", anchor: self.plan, states: [assessed, collected], limit: 366 }, measure: { sum: "receivable.balance" } }
327
- { 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" } }
417
+ { target: principalSnapshot, op: sum, values: [{ field: self.plan.principalReceivable.balance }] }
418
+ { target: earnedProfitSnapshot, op: sum, values: [{ field: self.plan.profitEarned.balance }] }
419
+ { target: unearnedProfitSnapshot, op: sum, values: [{ field: self.plan.profitReceivable.balance }] }
420
+ { target: finesSnapshot, op: sum, values: [{ field: self.plan.fineReceivable.balance }] }
421
+ { target: costsSnapshot, op: sum, values: [{ field: self.plan.costReceivable.balance }] }
328
422
  ]
329
423
  requires count of { instrument: current(), reference: "plan", anchor: self.plan, states: [applied], limit: 366 } < self.maximum
330
- 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: {} }]
424
+ invoke: [{ range: { count: { field: self.plan.months }, maximum: max_term, 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: max_term, where: { first: { literal: 0 } } }, action: check_order, input: {} }]
331
425
  }
332
426
  action accept {
333
427
  from: proposed, to: applied, actor: { party: borrower }, deadline: { at: self.expiresAt }
@@ -335,10 +429,20 @@ instrument installments(months: integer(1, 366), max_history: integer(2, 366) =
335
429
  requires count of { instrument: current(), reference: "plan", anchor: self.plan, states: [applied], limit: 366 } < self.maximum
336
430
  set: { acceptedAt: { field: self.now } }
337
431
  when assessed_fines is require_waiver { requires self.waiverPercent == 10000 }
338
- 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 } } }]
432
+ 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 } } }, { reference: self.id, action: record_waivers, input: {} }]
339
433
  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_for_amendment, input: {} }, { selection: { instrument: all(financing.late_charge), reference: "plan", anchor: self.plan, states: [proposed], limit: 366 }, action: cancel_for_amendment, input: {} }]
340
- invoke: [{ selection: { instrument: slice, reference: "plan", anchor: self.plan, states: [pending, due], limit: 366, order: [position] }, action: amend, input: { amendment: { field: self.id } } }]
341
- invoke: [{ selection: { instrument: slice, reference: "plan", anchor: self.plan, states: [paid], limit: 366 }, action: verify_fixed_date, input: { amendment: { field: self.id } } }]
434
+ invoke: [{ selection: { instrument: slice, reference: "plan", anchor: self.plan, states: [pending, due], limit: max_term, order: [position] }, action: amend, input: { amendment: { field: self.id } } }]
435
+ invoke: [{ selection: { instrument: slice, reference: "plan", anchor: self.plan, states: [paid], limit: max_term }, action: verify_fixed_date, input: { amendment: { field: self.id } } }]
436
+ }
437
+ // The waivers are state on each charge; the plan's claims drop once per kind.
438
+ action record_waivers { allowZero: true,
439
+ from: applied, to: applied, actor: { parent: current() }
440
+ calculate: [
441
+ { target: finesWaived, op: aggregate, selection: { instrument: all(financing.late_charge.waiver), reference: "amendment", anchor: self.id, states: [recorded], limit: 366 }, measure: { sum: "amount" } }
442
+ { target: costsWaived, op: aggregate, selection: { instrument: all(financing.late_charge.cost.waiver), reference: "amendment", anchor: self.id, states: [recorded], limit: 366 }, measure: { sum: "amount" } }
443
+ ]
444
+ moves self.finesWaived from self.plan.fineReceivable to self.plan.debt economics { purpose: internal, sourceParty: programOperator }
445
+ moves self.costsWaived from self.plan.costReceivable to self.plan.debt economics { purpose: internal, sourceParty: programOperator }
342
446
  }
343
447
  action withdraw { from: proposed, to: withdrawn, actor: { party: capital } }
344
448
  action decline { from: proposed, to: declined, actor: { party: borrower } }
@@ -346,9 +450,9 @@ instrument installments(months: integer(1, 366), max_history: integer(2, 366) =
346
450
  action supersede { from: proposed, to: superseded, requires self.revision != self.productRevision, set: { staleReason: { literal: "economic_revision_changed" } } }
347
451
  }
348
452
  proposed_date: {
349
- 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) }
453
+ fields { amendment: ref<amendment>, position: integer(1, max_term), 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) }
350
454
  lifecycle { states: [recorded], initial: recorded }
351
- action create { actor: { parent: amendment }, input { amendment: ref<amendment>, position: integer(1, months) }, requires self.date >= self.previous }
455
+ action create { actor: { parent: amendment }, input { amendment: ref<amendment>, position: integer(1, max_term) }, requires self.date >= self.previous }
352
456
  action check_order { from: recorded, to: recorded, actor: { parent: amendment }, requires self.date > self.previous }
353
457
  }
354
458
  date_change: {
@@ -374,42 +478,46 @@ instrument installments(months: integer(1, 366), max_history: integer(2, 366) =
374
478
  action distribute { from: recorded, to: distributed, actor: { parent: all(lending.distribution) } }
375
479
  // profitIncome is one account per capital party, so refunding profit a
376
480
  // distribution already paid out would spend other plans' profit. unwind_paid
377
- // unwinds only settlements still recorded; each marks its payment refunded,
378
- // and the plan refunds the profit pieces and payoff profit behind them.
481
+ // unwinds only settlements still recorded, and each payment refunds its
482
+ // profit only when its settlement was one of them.
379
483
  action unwind { allowZero: true,
380
- from: recorded, to: refunded, actor: { parent: parent }
484
+ from: recorded, to: refunded, actor: { parent: [parent, payment] }
381
485
  moves self.share from self.allocatedPayable to self.profitIncome economics { purpose: internal, sourceParty: capital }
382
- invoke: [{ selection: { instrument: payment, reference: "plan", anchor: self.plan, states: [paid], limit: 1, where: { paymentIdentity: { field: self.identity } } }, action: unwind, input: {} }]
383
486
  }
384
487
  }
385
488
  slice: {
386
489
  revisioned: true
387
490
  fields {
388
- plan: ref<parent>, borrowerLimit: ref<limits>, portfolioLimit: ref<portfolio>, position: integer(1, months)
491
+ plan: ref<parent>, borrowerLimit: ref<limits>, portfolioLimit: ref<portfolio>, position: integer(1, max_term)
389
492
  originalDueAt: date = at(self.plan.dates, self.position), dueAt: date?, nextDueAt: date?, previousDueAt: date?, prior: integer = subtract(self.position, 1), previousPosition: integer = sum(self.prior, self.first), previousDate: date = at(self.plan.dates, self.previousPosition), extensionLimit: date = after(self.originalDueAt, max_extension), lossEligibleAt: date?
390
493
  first: integer = divide(1, self.position)
391
494
  principalBase: money?, principalAllocated: money?, principalRemainder: money?, principalAdjustment: money?
392
495
  profitBase: money?, profitAllocated: money?, profitRemainder: money?, profitAdjustment: money?
393
496
  principal: money, profit: money, instalment: money
394
- principalReceivable: account(self, claim), profitReceivable: account(self, claim), profitEarned: account(self, claim)
395
- payoffIdentity: text?, payoffRebate: money?, payoffProfitTotal: money?, payoffProfit: money?, payoffReceipt: text?
396
- paymentBudget: money?, principalPayment: money?, principalBasePayment: money?, principalRatio: money?, profitPayment: money?, profitBudget: money?, collectibleProfit: money?, collectibleTotal: money?, ratioDenominator: integer = sum(10000, profit_rate)
397
- debt: account(borrower, claim, contra, "debt"), loss: account(capital, claim, "loss")
497
+ openPrincipal: money?, openProfit: money?, earnedProfit: money?, payoffIdentity: text?
498
+ collectibleProfit: money?, collectibleTotal: money?, paymentBudget: money?, principalRatio: money?, principalBasePayment: money?, profitBudget: money?
499
+ principalTake: money?, profitTake: money?, afterTake: money?, openProfitTake: money?, earnedProfitTake: money?, taken: money?
500
+ ratioDenominator: integer = sum(10000, profit_rate)
398
501
  }
399
502
  lifecycle { states: [draft, pending, due, paid, written_off, cancelled], initial: draft }
400
503
  action create {
401
504
  actor: { parent: parent }
402
- input { plan: ref<parent>, borrowerLimit: ref<limits>, portfolioLimit: ref<portfolio>, position: integer(1, months) }
505
+ input { plan: ref<parent>, borrowerLimit: ref<limits>, portfolioLimit: ref<portfolio>, position: integer(1, max_term) }
403
506
  set: { dueAt: { field: self.originalDueAt } }
507
+ calculate: [
508
+ { target: openPrincipal, op: sum, values: [{ literal: "0" }] }
509
+ { target: openProfit, op: sum, values: [{ literal: "0" }] }
510
+ { target: earnedProfit, op: sum, values: [{ literal: "0" }] }
511
+ ]
404
512
  when pricing is flat_total {
405
513
  calculate: [
406
- { target: principalBase, op: divide, amount: { field: self.plan.principal }, divisor: { literal: months }, rounding: floor }
407
- { target: principalAllocated, op: multiply, amount: { field: self.principalBase }, units: { literal: months } }
514
+ { target: principalBase, op: divide, amount: { field: self.plan.principal }, divisor: { field: self.plan.months }, rounding: floor }
515
+ { target: principalAllocated, op: multiply, amount: { field: self.principalBase }, units: { field: self.plan.months } }
408
516
  { target: principalRemainder, op: subtract, base: { field: self.plan.principal }, subtract: [{ field: self.principalAllocated }] }
409
517
  { target: principalAdjustment, op: multiply, amount: { field: self.principalRemainder }, units: { field: self.first } }
410
518
  { target: principal, op: sum, values: [{ field: self.principalBase }, { field: self.principalAdjustment }] }
411
- { target: profitBase, op: divide, amount: { field: self.plan.flatProfit }, divisor: { literal: months }, rounding: floor }
412
- { target: profitAllocated, op: multiply, amount: { field: self.profitBase }, units: { literal: months } }
519
+ { target: profitBase, op: divide, amount: { field: self.plan.flatProfit }, divisor: { field: self.plan.months }, rounding: floor }
520
+ { target: profitAllocated, op: multiply, amount: { field: self.profitBase }, units: { field: self.plan.months } }
413
521
  { target: profitRemainder, op: subtract, base: { field: self.plan.flatProfit }, subtract: [{ field: self.profitAllocated }] }
414
522
  { target: profitAdjustment, op: multiply, amount: { field: self.profitRemainder }, units: { field: self.first } }
415
523
  { target: profit, op: sum, values: [{ field: self.profitBase }, { field: self.profitAdjustment }] }
@@ -418,8 +526,8 @@ instrument installments(months: integer(1, 366), max_history: integer(2, 366) =
418
526
  }
419
527
  when pricing is amortizing {
420
528
  calculate: [
421
- { target: principal, op: annuity, amount: { field: self.plan.principal }, rate: { literal: profit_rate }, periods: { literal: months }, position: { field: self.position }, part: principal }
422
- { target: profit, op: annuity, amount: { field: self.plan.principal }, rate: { literal: profit_rate }, periods: { literal: months }, position: { field: self.position }, part: interest }
529
+ { target: principal, op: annuity, amount: { field: self.plan.principal }, rate: { literal: profit_rate }, periods: { field: self.plan.months }, position: { field: self.position }, part: principal }
530
+ { target: profit, op: annuity, amount: { field: self.plan.principal }, rate: { literal: profit_rate }, periods: { field: self.plan.months }, position: { field: self.position }, part: interest }
423
531
  { target: instalment, op: sum, values: [{ field: self.principal }, { field: self.profit }] }
424
532
  ]
425
533
  }
@@ -428,16 +536,6 @@ instrument installments(months: integer(1, 366), max_history: integer(2, 366) =
428
536
  requires self.originalDueAt > self.now; requires self.originalDueAt >= self.previousDate
429
537
  }
430
538
  action check_order { from: draft, to: draft, actor: { parent: parent }, requires self.originalDueAt > self.previousDate }
431
- action quote_pending {
432
- from: pending, to: pending, actor: { parent: payoff_quote }, input { quote: ref<payoff_quote> }
433
- requires input.quote.plan == self.plan
434
- invoke: [{ instrument: quote_piece, action: create, input: { quote: { field: input.quote }, slice: { field: self.id } } }]
435
- }
436
- action quote_due {
437
- from: due, to: due, actor: { parent: payoff_quote }, input { quote: ref<payoff_quote> }
438
- requires input.quote.plan == self.plan
439
- invoke: [{ instrument: quote_piece, action: create, input: { quote: { field: input.quote }, slice: { field: self.id } } }]
440
- }
441
539
  action refresh_loss_date {
442
540
  from: [draft, pending, due], to: draft, actor: { parent: current() }
443
541
  calculate: [{ target: lossEligibleAt, op: shift, date: { field: self.dueAt }, milliseconds: { literal: write_off_after }, direction: after }]
@@ -460,96 +558,122 @@ instrument installments(months: integer(1, 366), max_history: integer(2, 366) =
460
558
  calculate: [{ target: nextDueAt, op: at, list: input.amendment.dates, position: { field: self.position } }]
461
559
  requires self.nextDueAt == self.dueAt
462
560
  }
463
- action activate { allowZero: true,
561
+ // The plan posts the claims; a slice keeps its share as state.
562
+ action activate {
464
563
  from: draft, to: pending, actor: { parent: parent }
465
564
  requires self.dueAt > self.now
466
- moves self.principal from self.debt to self.principalReceivable economics { purpose: internal, sourceParty: borrower }
467
- moves self.profit from self.debt to self.profitReceivable economics { purpose: internal, sourceParty: borrower }
468
- when profit_earned is at_disbursement { moves self.profit from self.profitReceivable to self.profitEarned economics { purpose: internal, sourceParty: programOperator } }
469
- when profit_earned is by_schedule { invoke: [{ instrument: recognition, action: create, input: { slice: { field: self.id } } }] }
565
+ calculate: [{ target: openPrincipal, op: sum, values: [{ field: self.principal }] }]
566
+ when profit_earned is on_payment { calculate: [{ target: openProfit, op: sum, values: [{ field: self.profit }] }] }
567
+ when profit_earned is by_schedule {
568
+ calculate: [{ target: openProfit, op: sum, values: [{ field: self.profit }] }]
569
+ invoke: [{ instrument: recognition, action: create, input: { slice: { field: self.id } } }]
570
+ }
571
+ when profit_earned is at_disbursement { calculate: [{ target: earnedProfit, op: sum, values: [{ field: self.profit }] }] }
470
572
  }
471
573
  action mature {
472
574
  from: pending, to: due, actor: clock, due: { at: self.dueAt }
473
575
  }
576
+ action recognise {
577
+ from: [pending, due, paid, written_off, cancelled], to: preserve, actor: { parent: recognition }
578
+ calculate: [{ target: earnedProfit, op: sum, values: [{ field: self.earnedProfit }, { field: self.openProfit }] }]
579
+ invoke: [{ reference: self.id, action: clear_unearned, input: {} }]
580
+ }
581
+ action clear_unearned {
582
+ from: [pending, due, paid, written_off, cancelled], to: preserve, actor: { parent: current() }
583
+ calculate: [{ target: openProfit, op: sum, values: [{ literal: "0" }] }]
584
+ }
474
585
  action collect {
475
586
  from: [pending, due], to: preserve, actor: { parent: payment }
476
587
  input { payment: ref<payment> }
477
588
  requires input.payment.plan == self.plan
478
- when profit_earned is on_payment { calculate: [{ target: collectibleProfit, op: sum, values: [{ field: self.profitReceivable.balance }] }] }
479
- when profit_earned is by_schedule { calculate: [{ target: collectibleProfit, op: sum, values: [{ field: self.profitEarned.balance }] }] }
480
- when profit_earned is at_disbursement { calculate: [{ target: collectibleProfit, op: sum, values: [{ field: self.profitEarned.balance }] }] }
481
- when apply is principal_profit {
482
- invoke: [
483
- { instrument: principal_piece, action: create, input: { slice: { field: self.id }, payment: { field: input.payment }, limit: { field: self.principalReceivable.balance } } }
484
- { instrument: profit_piece, action: create, input: { plan: { field: self.plan }, slice: { field: self.id }, payment: { field: input.payment }, limit: { field: self.collectibleProfit } } }
485
- { 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: {} }
486
- ]
487
- }
589
+ when profit_earned is on_payment { calculate: [{ target: collectibleProfit, op: sum, values: [{ field: self.openProfit }] }] }
590
+ when profit_earned is by_schedule { calculate: [{ target: collectibleProfit, op: sum, values: [{ field: self.earnedProfit }] }] }
591
+ when profit_earned is at_disbursement { calculate: [{ target: collectibleProfit, op: sum, values: [{ field: self.earnedProfit }] }] }
592
+ calculate: [
593
+ { target: principalTake, op: minimum, values: [{ field: input.payment.unapplied }, { field: self.openPrincipal }] }
594
+ { target: afterTake, op: subtract, base: { field: input.payment.unapplied }, subtract: [{ field: self.principalTake }] }
595
+ { target: profitTake, op: minimum, values: [{ field: self.afterTake }, { field: self.collectibleProfit }] }
596
+ ]
597
+ invoke: [{ reference: self.id, action: take, input: { payment: { field: input.payment } } }]
488
598
  }
489
599
  action collect_waterfall {
490
600
  from: [pending, due], to: preserve, actor: { parent: payment }, input { payment: ref<payment> }
491
601
  requires input.payment.plan == self.plan
492
- when profit_earned is on_payment { calculate: [{ target: collectibleProfit, op: sum, values: [{ field: self.profitReceivable.balance }] }] }
493
- when profit_earned is by_schedule { calculate: [{ target: collectibleProfit, op: sum, values: [{ field: self.profitEarned.balance }] }] }
494
- when profit_earned is at_disbursement { calculate: [{ target: collectibleProfit, op: sum, values: [{ field: self.profitEarned.balance }] }] }
495
- invoke: [
496
- { 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 } } }
497
- { 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 } } }
498
- { instrument: profit_piece, action: create, input: { plan: { field: self.plan }, slice: { field: self.id }, payment: { field: input.payment }, limit: { field: self.collectibleProfit } } }
499
- { instrument: principal_piece, action: create, input: { slice: { field: self.id }, payment: { field: input.payment }, limit: { field: self.principalReceivable.balance } } }
500
- { 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: {} }
602
+ when profit_earned is on_payment { calculate: [{ target: collectibleProfit, op: sum, values: [{ field: self.openProfit }] }] }
603
+ when profit_earned is by_schedule { calculate: [{ target: collectibleProfit, op: sum, values: [{ field: self.earnedProfit }] }] }
604
+ when profit_earned is at_disbursement { calculate: [{ target: collectibleProfit, op: sum, values: [{ field: self.earnedProfit }] }] }
605
+ calculate: [
606
+ { target: profitTake, op: minimum, values: [{ field: input.payment.unapplied }, { field: self.collectibleProfit }] }
607
+ { target: afterTake, op: subtract, base: { field: input.payment.unapplied }, subtract: [{ field: self.profitTake }] }
608
+ { target: principalTake, op: minimum, values: [{ field: self.afterTake }, { field: self.openPrincipal }] }
501
609
  ]
610
+ invoke: [{ reference: self.id, action: take, input: { payment: { field: input.payment } } }]
502
611
  }
503
612
  action collect_pro_rata {
504
613
  from: [pending, due], to: preserve, actor: { parent: payment }, input { payment: ref<payment> }
505
614
  requires input.payment.plan == self.plan
506
- when profit_earned is on_payment { calculate: [{ target: collectibleProfit, op: sum, values: [{ field: self.profitReceivable.balance }] }] }
507
- when profit_earned is by_schedule { calculate: [{ target: collectibleProfit, op: sum, values: [{ field: self.profitEarned.balance }] }] }
508
- when profit_earned is at_disbursement { calculate: [{ target: collectibleProfit, op: sum, values: [{ field: self.profitEarned.balance }] }] }
615
+ when profit_earned is on_payment { calculate: [{ target: collectibleProfit, op: sum, values: [{ field: self.openProfit }] }] }
616
+ when profit_earned is by_schedule { calculate: [{ target: collectibleProfit, op: sum, values: [{ field: self.earnedProfit }] }] }
617
+ when profit_earned is at_disbursement { calculate: [{ target: collectibleProfit, op: sum, values: [{ field: self.earnedProfit }] }] }
509
618
  calculate: [
510
- { target: collectibleTotal, op: sum, values: [{ field: self.principalReceivable.balance }, { field: self.collectibleProfit }] }
511
- { target: paymentBudget, op: minimum, values: [{ field: input.payment.held.balance }, { field: self.collectibleTotal }] }
619
+ { target: collectibleTotal, op: sum, values: [{ field: self.openPrincipal }, { field: self.collectibleProfit }] }
620
+ { target: paymentBudget, op: minimum, values: [{ field: input.payment.unapplied }, { field: self.collectibleTotal }] }
512
621
  ]
513
622
  when pricing is flat_total { calculate: [{ target: principalRatio, op: ratio, amount: { field: self.paymentBudget }, numerator: { literal: 10000 }, denominator: { field: self.ratioDenominator }, rounding: floor }] }
514
623
  when pricing is amortizing { calculate: [{ target: principalRatio, op: ratio, amount: { field: self.paymentBudget }, numerator: { field: self.principal }, denominator: { field: self.instalment }, rounding: floor }] }
515
624
  calculate: [
516
- { target: principalBasePayment, op: minimum, values: [{ field: self.principalRatio }, { field: self.principalReceivable.balance }] }
625
+ { target: principalBasePayment, op: minimum, values: [{ field: self.principalRatio }, { field: self.openPrincipal }] }
517
626
  { target: profitBudget, op: subtract, base: { field: self.paymentBudget }, subtract: [{ field: self.principalBasePayment }] }
518
- { target: profitPayment, op: minimum, values: [{ field: self.profitBudget }, { field: self.collectibleProfit }] }
519
- { target: principalPayment, op: subtract, base: { field: self.paymentBudget }, subtract: [{ field: self.profitPayment }] }
627
+ { target: profitTake, op: minimum, values: [{ field: self.profitBudget }, { field: self.collectibleProfit }] }
628
+ { target: principalTake, op: subtract, base: { field: self.paymentBudget }, subtract: [{ field: self.profitTake }] }
629
+ ]
630
+ invoke: [{ reference: self.id, action: take, input: { payment: { field: input.payment } } }]
631
+ }
632
+ // A collect action chose principalTake and profitTake; take records them
633
+ // as pieces, lowers the slice's open amounts and the payment's unapplied
634
+ // cash. The payment posts the cash and claim transfers once.
635
+ action take {
636
+ from: [pending, due], to: preserve, actor: { parent: current() }, input { payment: ref<payment> }
637
+ when profit_earned is on_payment { calculate: [{ target: openProfitTake, op: sum, values: [{ field: self.profitTake }] }, { target: earnedProfitTake, op: sum, values: [{ literal: "0" }] }] }
638
+ when profit_earned is by_schedule { calculate: [{ target: openProfitTake, op: sum, values: [{ literal: "0" }] }, { target: earnedProfitTake, op: sum, values: [{ field: self.profitTake }] }] }
639
+ when profit_earned is at_disbursement { calculate: [{ target: openProfitTake, op: sum, values: [{ literal: "0" }] }, { target: earnedProfitTake, op: sum, values: [{ field: self.profitTake }] }] }
640
+ calculate: [
641
+ { target: taken, op: sum, values: [{ field: self.principalTake }, { field: self.profitTake }] }
642
+ { target: openPrincipal, op: subtract, base: { field: self.openPrincipal }, subtract: [{ field: self.principalTake }] }
643
+ { target: openProfit, op: subtract, base: { field: self.openProfit }, subtract: [{ field: self.openProfitTake }] }
644
+ { target: earnedProfit, op: subtract, base: { field: self.earnedProfit }, subtract: [{ field: self.earnedProfitTake }] }
520
645
  ]
521
646
  invoke: [
522
- { instrument: principal_piece, action: create, input: { slice: { field: self.id }, payment: { field: input.payment }, limit: { field: self.principalPayment } } }
523
- { instrument: profit_piece, action: create, input: { plan: { field: self.plan }, slice: { field: self.id }, payment: { field: input.payment }, limit: { field: self.profitPayment } } }
524
- { 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: {} }
647
+ { instrument: principal_piece, action: create, guard: { kind: compare, left: { field: self.principalTake }, operator: ">", right: { literal: "0" } }, input: { plan: { field: self.plan }, slice: { field: self.id }, payment: { field: input.payment }, amount: { field: self.principalTake } } }
648
+ { instrument: profit_piece, action: create, guard: { kind: compare, left: { field: self.profitTake }, operator: ">", right: { literal: "0" } }, input: { plan: { field: self.plan }, slice: { field: self.id }, payment: { field: input.payment }, amount: { field: self.profitTake } } }
649
+ { reference: input.payment, action: consume, input: { amount: { field: self.taken } } }
650
+ { selection: { instrument: current(), reference: "plan", anchor: self.plan, states: [pending, due], limit: 1, where: { id: { field: self.id }, openPrincipal: { literal: "0" }, openProfit: { literal: "0" }, earnedProfit: { literal: "0" } } }, action: complete, input: {} }
525
651
  ]
526
652
  }
527
653
  action complete {
528
654
  from: [pending, due], to: paid
529
- requires self.principalReceivable.balance == "0"; requires self.profitReceivable.balance == "0"; requires self.profitEarned.balance == "0"
655
+ requires self.openPrincipal == "0"; requires self.openProfit == "0"; requires self.earnedProfit == "0"
530
656
  }
531
- action restore { from: [pending, due, paid], to: pending, actor: { parent: payment } }
532
- action payoff { allowZero: true,
657
+ action unpay {
658
+ from: [pending, due, paid], to: pending, actor: { parent: [principal_piece, profit_piece] }
659
+ input { principal: money, profit: money }
660
+ calculate: [{ target: openPrincipal, op: sum, values: [{ field: self.openPrincipal }, { field: input.principal }] }]
661
+ when profit_earned is on_payment { calculate: [{ target: openProfit, op: sum, values: [{ field: self.openProfit }, { field: input.profit }] }] }
662
+ when profit_earned is by_schedule { calculate: [{ target: earnedProfit, op: sum, values: [{ field: self.earnedProfit }, { field: input.profit }] }] }
663
+ when profit_earned is at_disbursement { calculate: [{ target: earnedProfit, op: sum, values: [{ field: self.earnedProfit }, { field: input.profit }] }] }
664
+ }
665
+ action payoff {
533
666
  from: [pending, due], to: paid, actor: { parent: parent }, input { identity: text }
534
667
  set: { payoffIdentity: { field: input.identity } }
535
- 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 }] }]
536
- moves self.principalReceivable.balance from self.plan.borrower to self.plan.capital economics { purpose: principal, sourceParty: borrower }
537
- moves self.payoffProfit from self.plan.borrower to self.plan.profitIncome capture "payoffReceipt" economics { recipient: capital, company: { purpose: earning, sourceParty: borrower }, participant: { purpose: pass_through, sourceParty: borrower } }
538
- moves self.principalReceivable.balance from self.principalReceivable to self.debt economics { purpose: internal, sourceParty: programOperator }
539
- moves self.profitReceivable.balance from self.profitReceivable to self.debt economics { purpose: internal, sourceParty: programOperator }
540
- moves self.profitEarned.balance from self.profitEarned to self.debt economics { purpose: internal, sourceParty: programOperator }
668
+ calculate: [{ target: openPrincipal, op: sum, values: [{ literal: "0" }] }, { target: openProfit, op: sum, values: [{ literal: "0" }] }, { target: earnedProfit, op: sum, values: [{ literal: "0" }] }]
541
669
  }
542
- action unwind_payoff { allowZero: true,
543
- from: paid, to: paid, actor: { parent: parent }
544
- moves self.payoffProfit from self.plan.profitIncome to self.plan.borrower economics { recipient: capital, company: { purpose: earning, sourceParty: capital, reversalOf: self.payoffReceipt }, participant: unclassified }
545
- }
546
- action write_off { allowZero: true,
670
+ action write_off {
547
671
  from: [pending, due], to: written_off, actor: { parent: parent }
548
- moves self.principalReceivable.balance from self.principalReceivable to self.loss economics { purpose: internal, sourceParty: programOperator }; moves self.profitReceivable.balance from self.profitReceivable to self.debt economics { purpose: internal, sourceParty: programOperator }; moves self.profitEarned.balance from self.profitEarned to self.loss economics { purpose: internal, sourceParty: programOperator }
672
+ calculate: [{ target: openPrincipal, op: sum, values: [{ literal: "0" }] }, { target: openProfit, op: sum, values: [{ literal: "0" }] }, { target: earnedProfit, op: sum, values: [{ literal: "0" }] }]
549
673
  }
550
- action cancel { allowZero: true,
674
+ action cancel {
551
675
  from: [pending, due, paid], to: cancelled, actor: { parent: parent }
552
- moves self.principalReceivable.balance from self.principalReceivable to self.debt economics { purpose: internal, sourceParty: programOperator }; moves self.profitReceivable.balance from self.profitReceivable to self.debt economics { purpose: internal, sourceParty: programOperator }; moves self.profitEarned.balance from self.profitEarned to self.debt economics { purpose: internal, sourceParty: programOperator }
676
+ calculate: [{ target: openPrincipal, op: sum, values: [{ literal: "0" }] }, { target: openProfit, op: sum, values: [{ literal: "0" }] }, { target: earnedProfit, op: sum, values: [{ literal: "0" }] }]
553
677
  }
554
678
  }
555
679
  recognition: {
@@ -557,100 +681,127 @@ instrument installments(months: integer(1, 366), max_history: integer(2, 366) =
557
681
  lifecycle { states: [pending, recognised], initial: pending }
558
682
  action create { actor: { parent: slice }, input { slice: ref<slice> }, requires unique "profit_recognition" on [self.slice] }
559
683
  action reschedule { from: pending, to: pending, actor: { parent: slice }, calculate: [{ target: dueAt, op: shift, date: { field: self.slice.dueAt }, milliseconds: { literal: 0 }, direction: after }] }
560
- action mature { allowZero: true, from: pending, to: recognised, actor: clock, due: { at: self.dueAt }, moves self.slice.profitReceivable.balance from self.slice.profitReceivable to self.slice.profitEarned economics { purpose: internal, sourceParty: programOperator } }
684
+ action mature { allowZero: true,
685
+ from: pending, to: recognised, actor: clock, due: { at: self.dueAt }
686
+ moves self.slice.openProfit from self.slice.plan.profitReceivable to self.slice.plan.profitEarned economics { purpose: internal, sourceParty: programOperator }
687
+ invoke: [{ reference: self.slice, action: recognise, input: {} }]
688
+ }
561
689
  }
562
690
  payment: {
563
691
  revisioned: true
564
- fields { plan: ref<parent>, amount: money, paymentIdentity: text, held: account of self
565
- outstandingPrincipal: money?, outstandingProfit: money?, outstandingFines: money?, outstandingCosts: money?, outstandingTotal: money?, principalReturned: money?, profitReturned: money?, lossRecorded: money? }
692
+ fields { plan: ref<parent>, amount: money, paymentIdentity: text, held: account of self, cashReceipt: text?
693
+ outstandingPrincipal: money?, outstandingProfit: money?, outstandingFines: money?, outstandingCosts: money?, outstandingTotal: money?, unapplied: money?, principalReturned: money?, profitReturned: money?, settled: integer? }
566
694
 
567
695
  lifecycle { states: [pending, paid, refunded], initial: pending }
568
696
  action create { input { plan: ref<parent>, amount: money, paymentIdentity: text } requires self.plan in [active]; requires unique "payment_identity" on [self.plan, self.paymentIdentity] }
569
697
  action pay {
570
698
  from: pending, to: paid, actor: { party: borrower }
571
- 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" } }]
572
- 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" } }] }
573
- 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" } }] }
574
- 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" } }] }
699
+ calculate: [{ target: outstandingPrincipal, op: sum, values: [{ field: self.plan.principalReceivable.balance }] }]
700
+ when profit_earned is on_payment { calculate: [{ target: outstandingProfit, op: sum, values: [{ field: self.plan.profitReceivable.balance }] }] }
701
+ when profit_earned is by_schedule { calculate: [{ target: outstandingProfit, op: sum, values: [{ field: self.plan.profitEarned.balance }] }] }
702
+ when profit_earned is at_disbursement { calculate: [{ target: outstandingProfit, op: sum, values: [{ field: self.plan.profitEarned.balance }] }] }
575
703
  when apply is fines_profit_principal {
576
- 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 }] }]
704
+ calculate: [{ target: outstandingFines, op: sum, values: [{ field: self.plan.fineReceivable.balance }] }, { target: outstandingCosts, op: sum, values: [{ field: self.plan.costReceivable.balance }] }, { target: outstandingTotal, op: sum, values: [{ field: self.outstandingPrincipal }, { field: self.outstandingProfit }, { field: self.outstandingFines }, { field: self.outstandingCosts }] }]
577
705
  }
578
706
  when apply is principal_profit { calculate: [{ target: outstandingTotal, op: sum, values: [{ field: self.outstandingPrincipal }, { field: self.outstandingProfit }] }] }
579
707
  when apply is pro_rata { calculate: [{ target: outstandingTotal, op: sum, values: [{ field: self.outstandingPrincipal }, { field: self.outstandingProfit }] }] }
580
708
  requires self.plan in [active]; requires self.amount <= self.outstandingTotal
581
709
  moves self.amount from self.plan.borrower to self.held economics { purpose: principal, sourceParty: borrower }
710
+ when apply is fines_profit_principal {
711
+ invoke: [{ selection: { instrument: all(financing.late_charge), reference: "plan", anchor: self.plan, states: [assessed], limit: charge_limit, order: [overdueAt] }, action: collect, guard: { kind: compare, left: { field: self.held.balance }, operator: ">", right: { literal: "0" } }, input: { payment: { field: self.id } } }, { selection: { instrument: all(financing.late_charge.cost), reference: "plan", anchor: self.plan, states: [assessed], limit: charge_limit, order: [overdueAt] }, action: collect, guard: { kind: compare, left: { field: self.held.balance }, operator: ">", right: { literal: "0" } }, input: { payment: { field: self.id } } }]
712
+ }
713
+ invoke: [{ reference: self.id, action: allocate, input: {} }, { reference: self.id, action: record, input: {} }]
714
+ }
715
+ action allocate {
716
+ from: paid, to: paid, actor: { parent: current() }
717
+ calculate: [{ target: unapplied, op: sum, values: [{ field: self.held.balance }] }]
582
718
  when apply is principal_profit {
583
- 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 } } }]
719
+ invoke: [{ selection: { instrument: slice, reference: "plan", anchor: self.plan, states: [pending, due], limit: max_term, order: [position] }, action: collect, guard: { kind: compare, left: { field: self.unapplied }, operator: ">", right: { literal: "0" } }, input: { payment: { field: self.id } } }]
584
720
  }
585
721
  when apply is fines_profit_principal {
586
- 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 } } }]
722
+ invoke: [{ selection: { instrument: slice, reference: "plan", anchor: self.plan, states: [pending, due], limit: max_term, order: [position] }, action: collect_waterfall, guard: { kind: compare, left: { field: self.unapplied }, operator: ">", right: { literal: "0" } }, input: { payment: { field: self.id } } }]
587
723
  }
588
724
  when apply is pro_rata {
589
- 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 } } }]
725
+ invoke: [{ selection: { instrument: slice, reference: "plan", anchor: self.plan, states: [pending, due], limit: max_term, order: [position] }, action: collect_pro_rata, guard: { kind: compare, left: { field: self.unapplied }, operator: ">", right: { literal: "0" } }, input: { payment: { field: self.id } } }]
590
726
  }
591
- invoke: [{ reference: self.id, action: record, input: {} }]
592
727
  }
593
- action record {
728
+ action consume {
729
+ from: paid, to: paid, actor: { parent: slice }, input { amount: money }
730
+ calculate: [{ target: unapplied, op: subtract, base: { field: self.unapplied }, subtract: [{ field: input.amount }] }]
731
+ }
732
+ action record { allowZero: true,
594
733
  from: paid, to: paid, actor: { parent: current() }
595
- requires self.held.balance == "0"
596
- requires count of { instrument: current(), reference: "plan", anchor: self.plan, states: [paid], limit: 366 } <= self.plan.paymentLimit
597
- requires count of { instrument: profit_piece, reference: "plan", anchor: self.plan, states: [paid], limit: 366 } <= self.plan.historyLimit
598
- requires count of { instrument: all(financing.late_charge.receipt), reference: "plan", anchor: self.plan, states: [paid], limit: 366 } <= self.plan.historyLimit
599
- requires count of { instrument: all(financing.late_charge.cost.receipt), reference: "plan", anchor: self.plan, states: [paid], limit: 366 } <= self.plan.historyLimit
600
- 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" } }]
734
+ requires self.unapplied == "0"
735
+ calculate: [{ target: principalReturned, op: aggregate, selection: { instrument: principal_piece, reference: "payment", anchor: self.id, states: [paid], limit: max_term }, measure: { sum: "amount" } }, { target: profitReturned, op: aggregate, selection: { instrument: profit_piece, reference: "payment", anchor: self.id, states: [paid], limit: max_term }, measure: { sum: "amount" } }]
736
+ moves self.principalReturned from self.held to self.plan.capital economics { purpose: principal, sourceParty: programOperator }
737
+ moves self.profitReturned from self.held to self.plan.profitIncome capture "cashReceipt" economics { recipient: capital, company: { purpose: earning, sourceParty: borrower }, participant: { purpose: pass_through, sourceParty: programOperator } }
738
+ moves self.principalReturned from self.plan.principalReceivable to self.plan.debt economics { purpose: internal, sourceParty: programOperator }
739
+ when profit_earned is on_payment { moves self.profitReturned from self.plan.profitReceivable to self.plan.debt economics { purpose: internal, sourceParty: programOperator } }
740
+ when profit_earned is by_schedule { moves self.profitReturned from self.plan.profitEarned to self.plan.debt economics { purpose: internal, sourceParty: programOperator } }
741
+ when profit_earned is at_disbursement { moves self.profitReturned from self.plan.profitEarned to self.plan.debt economics { purpose: internal, sourceParty: programOperator } }
601
742
  invoke: [{ reference: self.plan, action: record_payment, input: { identity: { field: self.paymentIdentity }, principal: { field: self.principalReturned }, profit: { field: self.profitReturned } } }]
602
743
  }
603
- action unwind { from: paid, to: refunded, actor: { parent: [parent, settlement] } }
604
- action refund {
744
+ // unwind_paid refunds this payment's profit only when its settlement is
745
+ // still recorded; a distributed settlement keeps it with investors.
746
+ action unwind_paid {
747
+ from: paid, to: refunded, actor: { parent: parent }
748
+ calculate: [{ target: settled, op: aggregate, selection: { instrument: settlement, reference: "plan", anchor: self.plan, states: [recorded], limit: 1, where: { identity: { field: self.paymentIdentity } } }, measure: count }]
749
+ invoke: [{ selection: { instrument: settlement, reference: "plan", anchor: self.plan, states: [recorded], limit: 1, where: { identity: { field: self.paymentIdentity } } }, action: unwind, input: {} }, { reference: self.id, action: unwind_profit, guard: { kind: compare, left: { field: self.settled }, operator: "==", right: { literal: 1 } }, input: {} }]
750
+ }
751
+ action unwind_profit { allowZero: true,
752
+ from: refunded, to: refunded, actor: { parent: current() }
753
+ moves self.profitReturned from self.plan.profitIncome to self.plan.borrower economics { recipient: capital, company: { purpose: earning, sourceParty: capital, reversalOf: self.cashReceipt }, participant: unclassified }
754
+ }
755
+ action refund { allowZero: true,
605
756
  from: paid, to: refunded, actor: { party: capital }, deadline: { at: self.plan.refundUntil }
606
757
  requires unique "settlement_consumption" on [self.plan, self.paymentIdentity]; requires self.plan in [active, paid]
607
- requires count of { instrument: settlement, reference: "plan", anchor: self.plan, states: [recorded], limit: 366, where: { identity: { field: self.paymentIdentity } } } == 1
608
- invoke: [{ selection: { instrument: settlement, reference: "plan", anchor: self.plan, states: [recorded], limit: 366, where: { identity: { field: self.paymentIdentity } } }, action: refund, input: {} }]
758
+ requires count of { instrument: settlement, reference: "plan", anchor: self.plan, states: [recorded], limit: 1, where: { identity: { field: self.paymentIdentity } } } == 1
759
+ moves self.principalReturned from self.plan.capital to self.plan.borrower economics { purpose: principal, sourceParty: capital }
760
+ moves self.profitReturned from self.plan.profitIncome to self.plan.borrower economics { recipient: capital, company: { purpose: earning, sourceParty: capital, reversalOf: self.cashReceipt }, participant: unclassified }
761
+ moves self.principalReturned from self.plan.debt to self.plan.principalReceivable economics { purpose: internal, sourceParty: borrower }
762
+ when profit_earned is on_payment { moves self.profitReturned from self.plan.debt to self.plan.profitReceivable economics { purpose: internal, sourceParty: borrower } }
763
+ when profit_earned is by_schedule { moves self.profitReturned from self.plan.debt to self.plan.profitEarned economics { purpose: internal, sourceParty: borrower } }
764
+ when profit_earned is at_disbursement { moves self.profitReturned from self.plan.debt to self.plan.profitEarned economics { purpose: internal, sourceParty: borrower } }
765
+ invoke: [{ selection: { instrument: settlement, reference: "plan", anchor: self.plan, states: [recorded], limit: 1, where: { identity: { field: self.paymentIdentity } } }, action: refund, input: {} }]
609
766
  when apply is fines_profit_principal {
610
- 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 } } }]
767
+ invoke: [{ selection: { instrument: all(financing.late_charge.receipt), reference: "payment", anchor: self.id, states: [paid], limit: charge_limit }, action: refund, input: {} }, { selection: { instrument: all(financing.late_charge.cost.receipt), reference: "payment", anchor: self.id, states: [paid], limit: charge_limit }, action: refund, input: {} }]
611
768
  }
612
- 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: {} }]
769
+ invoke: [{ reference: self.plan, action: restore, input: {} }, { selection: { instrument: principal_piece, reference: "payment", anchor: self.id, states: [paid], limit: max_term }, action: refund, input: {} }, { selection: { instrument: profit_piece, reference: "payment", anchor: self.id, states: [paid], limit: max_term }, action: refund, input: {} }]
613
770
  }
614
771
  }
615
772
  principal_piece: {
616
- fields { slice: ref<slice>, payment: ref<payment>, limit: money, amount: money? }
773
+ fields { plan: ref<parent>, slice: ref<slice>, payment: ref<payment>, amount: money }
617
774
  lifecycle { states: [paid, refunded], initial: paid }
618
- action create { allowZero: true,
619
- calculate: [{ target: amount, op: minimum, values: [{ field: self.payment.held.balance }, { field: self.slice.principalReceivable.balance }, { field: self.limit }] }]
775
+ action create {
620
776
  actor: { parent: slice }
621
- input { slice: ref<slice>, payment: ref<payment>, limit: money }
777
+ input { plan: ref<parent>, slice: ref<slice>, payment: ref<payment>, amount: money }
622
778
  requires unique "principal_receipt" on [self.slice, self.payment]
623
- moves self.amount from self.payment.held to self.slice.plan.capital economics { purpose: principal, sourceParty: programOperator }; moves self.amount from self.slice.principalReceivable to self.slice.debt economics { purpose: internal, sourceParty: programOperator }
624
779
  }
625
- action refund { allowZero: true,
780
+ action refund {
626
781
  from: paid, to: refunded, actor: { parent: payment }
627
- moves self.amount from self.slice.plan.capital to self.slice.plan.borrower economics { purpose: principal, sourceParty: capital }; moves self.amount from self.slice.debt to self.slice.principalReceivable economics { purpose: internal, sourceParty: borrower }
782
+ invoke: [{ reference: self.slice, action: unpay, input: { principal: { field: self.amount }, profit: { literal: "0" } } }]
628
783
  }
629
784
  }
630
785
  profit_piece: {
631
- fields { cashReceipt: text?, plan: ref<parent>, slice: ref<slice>, payment: ref<payment>, limit: money, amount: money? }
786
+ fields { plan: ref<parent>, slice: ref<slice>, payment: ref<payment>, amount: money }
632
787
  lifecycle { states: [paid, refunded], initial: paid }
633
- action create { allowZero: true,
634
- calculate: [{ target: amount, op: minimum, values: [{ field: self.payment.held.balance }, { field: self.limit }] }]
788
+ action create {
635
789
  actor: { parent: slice }
636
- input { plan: ref<parent>, slice: ref<slice>, payment: ref<payment>, limit: money }
790
+ input { plan: ref<parent>, slice: ref<slice>, payment: ref<payment>, amount: money }
637
791
  requires unique "profit_receipt" on [self.slice, self.payment]
638
- moves self.amount from self.payment.held to self.slice.plan.profitIncome capture "cashReceipt" economics { recipient: capital, company: { purpose: earning, sourceParty: borrower }, participant: { purpose: pass_through, sourceParty: programOperator } }
639
- when profit_earned is on_payment { moves self.amount from self.slice.profitReceivable to self.slice.debt economics { purpose: internal, sourceParty: programOperator } }
640
- when profit_earned is by_schedule { moves self.amount from self.slice.profitEarned to self.slice.debt economics { purpose: internal, sourceParty: programOperator } }
641
- when profit_earned is at_disbursement { moves self.amount from self.slice.profitEarned to self.slice.debt economics { purpose: internal, sourceParty: programOperator } }
642
792
  }
643
- action refund { allowZero: true,
644
- from: paid, to: refunded, actor: { parent: [payment, parent] }
645
- moves self.amount from self.slice.plan.profitIncome to self.slice.plan.borrower economics { recipient: capital, company: { purpose: earning, sourceParty: capital, reversalOf: self.cashReceipt }, participant: unclassified }
646
- when profit_earned is on_payment { moves self.amount from self.slice.debt to self.slice.profitReceivable economics { purpose: internal, sourceParty: borrower } }
647
- when profit_earned is by_schedule { moves self.amount from self.slice.debt to self.slice.profitEarned economics { purpose: internal, sourceParty: borrower } }
648
- when profit_earned is at_disbursement { moves self.amount from self.slice.debt to self.slice.profitEarned economics { purpose: internal, sourceParty: borrower } }
793
+ action refund {
794
+ from: paid, to: refunded, actor: { parent: payment }
795
+ invoke: [{ reference: self.slice, action: unpay, input: { principal: { literal: "0" }, profit: { field: self.amount } } }]
649
796
  }
650
797
  }
651
798
  }
652
799
  }
653
800
 
801
+ // A late charge and its recovery costs keep their open amount as state. The
802
+ // plan's fineReceivable and costReceivable hold the claims, so assessment,
803
+ // waiver and refund post one claim transfer each, and payoff and amendment post
804
+ // one per attachment or kind, not one per charge.
654
805
  instrument late_charge(on: ref<financing.installments>[], grace: duration = 3d, fine: money = 50 SAR, cap: money = 25 SAR, fines_to: party = programOperator, costs_to: party = programOperator, borrower: party = party(person)) {
655
806
  parameterDiagnostics {
656
807
  fine {
@@ -663,48 +814,66 @@ instrument late_charge(on: ref<financing.installments>[], grace: duration = 3d,
663
814
  familyRevision: 1
664
815
  summary: "Separate fine and evidenced recovery claims on an overdue installment."
665
816
  fields {
666
- plan: ref<on>, slice: ref<on.slice>, borrower: account of borrower, fine: money = fine, overdueAt: date?, payoffAmount: money?, payoffReceipt: text?
667
- receivable: account(self, claim), debt: account(borrower, claim, contra, "debt")
817
+ plan: ref<on>, slice: ref<on.slice>, borrower: account of borrower, fine: money = fine, overdueAt: date?, payoffAmount: money?, payoffReceipt: text?, payoffLed: integer?
818
+ open: money?, take: money?
668
819
  }
669
820
  lifecycle { states: [proposed, assessed, collected, waived], initial: proposed }
670
- action create { input { slice: ref<on.slice>? }, calculate: [{ target: overdueAt, op: shift, date: { field: self.slice.dueAt }, milliseconds: { literal: grace }, direction: after }, { target: payoffAmount, op: sum, values: [{ literal: "0" }] }], requires unique "late_assessment" on [self.slice, self.overdueAt]; requires self.borrower == self.slice.plan.borrower; requires self.plan == self.slice.plan; requires count of { instrument: all(financing.late_charge), reference: "plan", anchor: self.plan, states: [proposed, assessed, collected], limit: 366 } < self.plan.historyLimit }
821
+ action create { input { slice: ref<on.slice>? }, calculate: [{ target: overdueAt, op: shift, date: { field: self.slice.dueAt }, milliseconds: { literal: grace }, direction: after }, { target: payoffAmount, op: sum, values: [{ literal: "0" }] }, { target: open, op: sum, values: [{ literal: "0" }] }], requires unique "late_assessment" on [self.slice, self.overdueAt]; requires self.borrower == self.slice.plan.borrower; requires self.plan == self.slice.plan; requires count of { instrument: all(financing.late_charge), reference: "plan", anchor: self.plan, states: [proposed, assessed, collected], limit: 366 } < self.plan.historyLimit }
671
822
  action assess { allowZero: true,
672
823
  from: proposed, to: assessed, actor: clock, due: { at: self.overdueAt }
673
- requires self.slice.principalReceivable.balance > "0"
674
- moves self.fine from self.debt to self.receivable economics { purpose: internal, sourceParty: borrower }
824
+ requires self.slice.openPrincipal > "0"
825
+ calculate: [{ target: open, op: sum, values: [{ field: self.fine }] }]
826
+ moves self.fine from self.plan.debt to self.plan.fineReceivable economics { purpose: internal, sourceParty: borrower }
675
827
  }
676
828
  action collect {
677
- from: [assessed, collected], to: assessed, actor: { parent: child(on, slice) }, input { payment: ref<on.payment> }
829
+ from: [assessed, collected], to: assessed, actor: { parent: child(on, payment) }, input { payment: ref<on.payment> }
678
830
  requires input.payment in [paid]
679
831
  requires input.payment.plan == self.plan
680
- invoke: [{ instrument: receipt, action: create, input: { plan: { field: self.plan }, 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: {} }]
832
+ calculate: [{ target: take, op: minimum, values: [{ field: input.payment.held.balance }, { field: self.open }] }]
833
+ invoke: [{ instrument: receipt, action: create, guard: { kind: compare, left: { field: self.take }, operator: ">", right: { literal: "0" } }, input: { plan: { field: self.plan }, assessment: { field: self.id }, payment: { field: input.payment }, amount: { field: self.take } } }, { reference: self.id, action: reduce, input: {} }, { selection: { instrument: current(), reference: "plan", anchor: self.plan, states: [assessed], limit: 1, where: { id: { field: self.id }, open: { literal: "0" } } }, action: finish, input: {} }]
681
834
  }
835
+ // The oldest open charge of an attachment settles them all in one transfer.
682
836
  action payoff { allowZero: true,
683
- from: [assessed, collected], to: collected, actor: { parent: on }
837
+ from: assessed, to: collected, actor: { parent: on }
684
838
  requires self.plan in [paid]
685
- calculate: [{ target: payoffAmount, op: sum, values: [{ field: self.receivable.balance }] }]
839
+ calculate: [{ target: payoffAmount, op: aggregate, selection: { instrument: current(), reference: "plan", anchor: self.plan, states: [assessed], limit: 366 }, measure: { sum: "open" } }]
840
+ set: { payoffLed: { literal: 1 } }
686
841
  moves self.payoffAmount from self.plan.borrower to fines_to capture "payoffReceipt" economics { recipient: fines_to, company: { purpose: earning, sourceParty: borrower }, participant: { purpose: pass_through, sourceParty: borrower } }
687
- moves self.payoffAmount from self.receivable to self.debt economics { purpose: internal, sourceParty: programOperator }
842
+ moves self.payoffAmount from self.plan.fineReceivable to self.plan.debt economics { purpose: internal, sourceParty: programOperator }
843
+ invoke: [{ selection: { instrument: current(), reference: "plan", anchor: self.plan, states: [assessed], limit: 366 }, action: close, input: {} }, { reference: self.id, action: close, input: {} }]
688
844
  }
845
+ action close { from: [assessed, collected], to: collected, actor: { parent: current() }, calculate: [{ target: open, op: sum, values: [{ literal: "0" }] }] }
689
846
  action unwind_payoff { allowZero: true,
690
847
  from: collected, to: collected, actor: { parent: on }
848
+ set: { payoffLed: { literal: 2 } }
691
849
  moves self.payoffAmount from fines_to to self.plan.borrower economics { recipient: fines_to, company: { purpose: earning, sourceParty: fines_to, reversalOf: self.payoffReceipt }, participant: unclassified }
692
850
  }
693
- action finish { from: assessed, to: collected, actor: { parent: current() }, requires self.receivable.balance == "0" }
851
+ action finish { from: assessed, to: collected, actor: { parent: current() }, requires self.open == "0" }
852
+ action reduce { from: [assessed, collected, waived], to: preserve, actor: { parent: current() }, calculate: [{ target: open, op: subtract, base: { field: self.open }, subtract: [{ field: self.take }] }] }
694
853
  action cancel { from: proposed, to: waived, actor: { party: programOperator } }
695
854
  action cancel_for_amendment { from: proposed, to: waived, actor: { parent: child(on, amendment) } }
696
- action cancel_undelivered { allowZero: true, from: [proposed, assessed, collected], to: waived, actor: { parent: on }, moves self.receivable.balance from self.receivable to self.debt economics { purpose: internal, sourceParty: programOperator } }
855
+ action cancel_undelivered { from: [proposed, assessed, collected], to: waived, actor: { parent: on }, calculate: [{ target: open, op: sum, values: [{ literal: "0" }] }] }
697
856
  action waive_for_amendment {
698
857
  from: [assessed, collected], to: assessed, actor: { parent: child(on, amendment) }, input { amendment: ref<on.amendment> }
699
858
  requires input.amendment.plan == self.plan; requires input.amendment in [applied]
700
- invoke: [{ instrument: waiver, action: create, input: { assessment: { field: self.id }, amendment: { field: input.amendment } } }]
859
+ calculate: [{ target: take, op: rate, base: { field: self.open }, bps: { field: input.amendment.waiverPercent }, rounding: floor }]
860
+ invoke: [{ instrument: waiver, action: create, input: { assessment: { field: self.id }, amendment: { field: input.amendment }, amount: { field: self.take } } }, { reference: self.id, action: reduce, input: {} }]
861
+ }
862
+ action waive { allowZero: true,
863
+ from: [assessed, collected], to: waived, actor: { parent: waiver_request }, input { reason: text, evidence: text }
864
+ calculate: [{ target: take, op: sum, values: [{ field: self.open }] }]
865
+ moves self.take from self.plan.fineReceivable to self.plan.debt economics { purpose: internal, sourceParty: programOperator }
866
+ invoke: [{ reference: self.id, action: reduce, input: {} }]
701
867
  }
702
- action waive { allowZero: true, from: [assessed, collected], to: waived, actor: { parent: waiver_request }, input { reason: text, evidence: text }, moves self.receivable.balance from self.receivable to self.debt economics { purpose: internal, sourceParty: programOperator } }
703
868
  action refund {
704
- from: [assessed, collected], to: assessed, actor: { party: fines_to }, input { payment: ref<on.payment> }, deadline: { at: self.plan.refundUntil }
869
+ from: [assessed, collected], to: preserve, actor: { party: fines_to }, input { payment: ref<on.payment> }, deadline: { at: self.plan.refundUntil }
705
870
  requires input.payment in [paid, refunded]
706
871
  requires input.payment.plan == self.plan
707
- 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: {} }]
872
+ invoke: [{ selection: { instrument: receipt, reference: "payment", anchor: input.payment, states: [paid], limit: 1, where: { assessment: { field: self.id } } }, action: refund, input: {} }]
873
+ }
874
+ action reopen {
875
+ from: [assessed, collected], to: assessed, actor: { parent: receipt }, input { amount: money }
876
+ calculate: [{ target: open, op: sum, values: [{ field: self.open }, { field: input.amount }] }]
708
877
  }
709
878
  records {
710
879
  waiver_request: {
@@ -718,67 +887,89 @@ instrument late_charge(on: ref<financing.installments>[], grace: duration = 3d,
718
887
  action cancel { from: requested, to: cancelled }
719
888
  }
720
889
  waiver: {
721
- fields { assessment: ref<parent>, amendment: ref<on.amendment>, amount: money? }
890
+ fields { assessment: ref<parent>, amendment: ref<on.amendment>, amount: money }
722
891
  lifecycle { states: [recorded], initial: recorded }
723
- action create { allowZero: true,
724
- actor: { parent: parent }, input { assessment: ref<parent>, amendment: ref<on.amendment> }
725
- calculate: [{ target: amount, op: rate, base: { field: self.assessment.receivable.balance }, bps: { field: self.amendment.waiverPercent }, rounding: floor }]
892
+ action create {
893
+ actor: { parent: parent }, input { assessment: ref<parent>, amendment: ref<on.amendment>, amount: money }
726
894
  requires unique "amendment_waiver" on [self.assessment, self.amendment]
727
- moves self.amount from self.assessment.receivable to self.assessment.debt economics { purpose: internal, sourceParty: programOperator }
728
895
  }
729
896
  }
730
897
  receipt: {
731
- fields { cashReceipt: text?, plan: ref<on>, assessment: ref<parent>, payment: ref<on.payment>, amount: money? }
898
+ fields { cashReceipt: text?, plan: ref<on>, assessment: ref<parent>, payment: ref<on.payment>, amount: money }
732
899
  lifecycle { states: [paid, refunded], initial: paid }
733
- action create { allowZero: true, calculate: [{ target: amount, op: minimum, values: [{ field: self.payment.held.balance }, { field: self.assessment.receivable.balance }] }], actor: { parent: parent }, input { plan: ref<on>, assessment: ref<parent>, payment: ref<on.payment> }, requires input.payment.plan == self.assessment.plan; requires unique "fine_receipt" on [self.assessment, self.payment]; moves self.amount from self.payment.held to fines_to capture "cashReceipt" economics { recipient: fines_to, company: { purpose: earning, sourceParty: borrower }, participant: { purpose: pass_through, sourceParty: programOperator } }; moves self.amount from self.assessment.receivable to self.assessment.debt economics { purpose: internal, sourceParty: programOperator } }
734
- action refund { allowZero: true, from: paid, to: refunded, actor: { parent: [parent, on] }, moves self.amount from fines_to to self.payment.plan.borrower economics { recipient: fines_to, company: { purpose: earning, sourceParty: fines_to, reversalOf: self.cashReceipt }, participant: unclassified }; moves self.amount from self.assessment.debt to self.assessment.receivable economics { purpose: internal, sourceParty: borrower } }
900
+ action create { actor: { parent: parent }, input { plan: ref<on>, assessment: ref<parent>, payment: ref<on.payment>, amount: money }, requires input.payment.plan == self.assessment.plan; requires unique "fine_receipt" on [self.assessment, self.payment]; moves self.amount from self.payment.held to fines_to capture "cashReceipt" economics { recipient: fines_to, company: { purpose: earning, sourceParty: borrower }, participant: { purpose: pass_through, sourceParty: programOperator } }; moves self.amount from self.plan.fineReceivable to self.plan.debt economics { purpose: internal, sourceParty: programOperator } }
901
+ action refund {
902
+ from: paid, to: refunded, actor: { parent: [parent, child(on, payment)] }
903
+ moves self.amount from fines_to to self.payment.plan.borrower economics { recipient: fines_to, company: { purpose: earning, sourceParty: fines_to, reversalOf: self.cashReceipt }, participant: unclassified }
904
+ moves self.amount from self.plan.debt to self.plan.fineReceivable economics { purpose: internal, sourceParty: borrower }
905
+ invoke: [{ reference: self.assessment, action: reopen, input: { amount: { field: self.amount } } }]
906
+ }
907
+ // unwind_paid returns the cash; the plan cancels the claims itself.
908
+ action unwind { from: paid, to: refunded, actor: { parent: on }, moves self.amount from fines_to to self.payment.plan.borrower economics { recipient: fines_to, company: { purpose: earning, sourceParty: fines_to, reversalOf: self.cashReceipt }, participant: unclassified } }
735
909
  }
736
910
 
737
911
  cost: {
738
912
  revisioned: true
739
913
  fields {
740
- plan: ref<on>, assessment: ref<parent>, overdueAt: date?, evidence: text, evidencedCost: money, amount: money = minimum(self.evidencedCost, cap), payoffAmount: money?, payoffReceipt: text?
741
- receivable: account(self, claim), debt: account(borrower, claim, contra, "debt")
914
+ plan: ref<on>, assessment: ref<parent>, overdueAt: date?, evidence: text, evidencedCost: money, amount: money = minimum(self.evidencedCost, cap), payoffAmount: money?, payoffReceipt: text?, payoffLed: integer?
915
+ open: money?, take: money?
742
916
  }
743
917
  lifecycle { states: [proposed, assessed, collected, waived], initial: proposed }
744
- action create { actor: { party: programOperator }, calculate: [{ target: overdueAt, op: shift, date: { field: self.assessment.overdueAt }, milliseconds: { literal: 0 }, direction: after }, { target: payoffAmount, op: sum, values: [{ literal: "0" }] }], requires unique "recovery_assessment" on [self.assessment]; requires self.plan == self.assessment.plan; requires count of { instrument: all(financing.late_charge.cost), reference: "plan", anchor: self.plan, states: [proposed, assessed, collected], limit: 366 } < self.plan.historyLimit }
918
+ action create { actor: { party: programOperator }, calculate: [{ target: overdueAt, op: shift, date: { field: self.assessment.overdueAt }, milliseconds: { literal: 0 }, direction: after }, { target: payoffAmount, op: sum, values: [{ literal: "0" }] }, { target: open, op: sum, values: [{ literal: "0" }] }], requires unique "recovery_assessment" on [self.assessment]; requires self.plan == self.assessment.plan; requires count of { instrument: all(financing.late_charge.cost), reference: "plan", anchor: self.plan, states: [proposed, assessed, collected], limit: 366 } < self.plan.historyLimit }
745
919
  action assess { allowZero: true,
746
920
  from: proposed, to: assessed, actor: clock, due: { at: self.assessment.overdueAt }
747
- requires self.assessment.slice.principalReceivable.balance > "0"
748
- moves self.amount from self.debt to self.receivable economics { purpose: internal, sourceParty: borrower }
921
+ requires self.assessment.slice.openPrincipal > "0"
922
+ calculate: [{ target: open, op: sum, values: [{ field: self.amount }] }]
923
+ moves self.amount from self.plan.debt to self.plan.costReceivable economics { purpose: internal, sourceParty: borrower }
749
924
  }
750
925
  action collect {
751
- from: [assessed, collected], to: assessed, actor: { parent: child(on, slice) }, input { payment: ref<on.payment> }
926
+ from: [assessed, collected], to: assessed, actor: { parent: child(on, payment) }, input { payment: ref<on.payment> }
752
927
  requires input.payment in [paid]
753
928
  requires input.payment.plan == self.plan
754
- invoke: [{ instrument: receipt, action: create, input: { plan: { field: self.plan }, 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: {} }]
929
+ calculate: [{ target: take, op: minimum, values: [{ field: input.payment.held.balance }, { field: self.open }] }]
930
+ invoke: [{ instrument: receipt, action: create, guard: { kind: compare, left: { field: self.take }, operator: ">", right: { literal: "0" } }, input: { plan: { field: self.plan }, assessment: { field: self.id }, payment: { field: input.payment }, amount: { field: self.take } } }, { reference: self.id, action: reduce, input: {} }, { selection: { instrument: current(), reference: "plan", anchor: self.plan, states: [assessed], limit: 1, where: { id: { field: self.id }, open: { literal: "0" } } }, action: finish, input: {} }]
755
931
  }
756
932
  action payoff { allowZero: true,
757
- from: [assessed, collected], to: collected, actor: { parent: on }
933
+ from: assessed, to: collected, actor: { parent: on }
758
934
  requires self.plan in [paid]
759
- calculate: [{ target: payoffAmount, op: sum, values: [{ field: self.receivable.balance }] }]
935
+ calculate: [{ target: payoffAmount, op: aggregate, selection: { instrument: current(), reference: "plan", anchor: self.plan, states: [assessed], limit: 366 }, measure: { sum: "open" } }]
936
+ set: { payoffLed: { literal: 1 } }
760
937
  moves self.payoffAmount from self.plan.borrower to costs_to capture "payoffReceipt" economics { recipient: costs_to, company: { purpose: earning, sourceParty: borrower }, participant: { purpose: pass_through, sourceParty: borrower } }
761
- moves self.payoffAmount from self.receivable to self.debt economics { purpose: internal, sourceParty: programOperator }
938
+ moves self.payoffAmount from self.plan.costReceivable to self.plan.debt economics { purpose: internal, sourceParty: programOperator }
939
+ invoke: [{ selection: { instrument: current(), reference: "plan", anchor: self.plan, states: [assessed], limit: 366 }, action: close, input: {} }, { reference: self.id, action: close, input: {} }]
762
940
  }
941
+ action close { from: [assessed, collected], to: collected, actor: { parent: current() }, calculate: [{ target: open, op: sum, values: [{ literal: "0" }] }] }
763
942
  action unwind_payoff { allowZero: true,
764
943
  from: collected, to: collected, actor: { parent: on }
944
+ set: { payoffLed: { literal: 2 } }
765
945
  moves self.payoffAmount from costs_to to self.plan.borrower economics { recipient: costs_to, company: { purpose: earning, sourceParty: costs_to, reversalOf: self.payoffReceipt }, participant: unclassified }
766
946
  }
767
- action finish { from: assessed, to: collected, actor: { parent: current() }, requires self.receivable.balance == "0" }
947
+ action finish { from: assessed, to: collected, actor: { parent: current() }, requires self.open == "0" }
948
+ action reduce { from: [assessed, collected, waived], to: preserve, actor: { parent: current() }, calculate: [{ target: open, op: subtract, base: { field: self.open }, subtract: [{ field: self.take }] }] }
768
949
  action cancel { from: proposed, to: waived, actor: { party: programOperator } }
769
950
  action cancel_for_amendment { from: proposed, to: waived, actor: { parent: child(on, amendment) } }
770
- action cancel_undelivered { allowZero: true, from: [proposed, assessed, collected], to: waived, actor: { parent: on }, moves self.receivable.balance from self.receivable to self.debt economics { purpose: internal, sourceParty: programOperator } }
951
+ action cancel_undelivered { from: [proposed, assessed, collected], to: waived, actor: { parent: on }, calculate: [{ target: open, op: sum, values: [{ literal: "0" }] }] }
771
952
  action waive_for_amendment {
772
953
  from: [assessed, collected], to: assessed, actor: { parent: child(on, amendment) }, input { amendment: ref<on.amendment> }
773
954
  requires input.amendment.plan == self.plan; requires input.amendment in [applied]
774
- invoke: [{ instrument: waiver, action: create, input: { assessment: { field: self.id }, amendment: { field: input.amendment } } }]
955
+ calculate: [{ target: take, op: rate, base: { field: self.open }, bps: { field: input.amendment.waiverPercent }, rounding: floor }]
956
+ invoke: [{ instrument: waiver, action: create, input: { assessment: { field: self.id }, amendment: { field: input.amendment }, amount: { field: self.take } } }, { reference: self.id, action: reduce, input: {} }]
957
+ }
958
+ action waive { allowZero: true,
959
+ from: [assessed, collected], to: waived, actor: { parent: waiver_request }, input { reason: text, evidence: text }
960
+ calculate: [{ target: take, op: sum, values: [{ field: self.open }] }]
961
+ moves self.take from self.plan.costReceivable to self.plan.debt economics { purpose: internal, sourceParty: programOperator }
962
+ invoke: [{ reference: self.id, action: reduce, input: {} }]
775
963
  }
776
- action waive { allowZero: true, from: [assessed, collected], to: waived, actor: { parent: waiver_request }, input { reason: text, evidence: text }, moves self.receivable.balance from self.receivable to self.debt economics { purpose: internal, sourceParty: programOperator } }
777
964
  action refund {
778
- from: [assessed, collected], to: assessed, actor: { party: costs_to }, input { payment: ref<on.payment> }, deadline: { at: self.plan.refundUntil }
965
+ from: [assessed, collected], to: preserve, actor: { party: costs_to }, input { payment: ref<on.payment> }, deadline: { at: self.plan.refundUntil }
779
966
  requires input.payment in [paid, refunded]
780
967
  requires input.payment.plan == self.plan
781
- 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: {} }]
968
+ invoke: [{ selection: { instrument: receipt, reference: "payment", anchor: input.payment, states: [paid], limit: 1, where: { assessment: { field: self.id } } }, action: refund, input: {} }]
969
+ }
970
+ action reopen {
971
+ from: [assessed, collected], to: assessed, actor: { parent: receipt }, input { amount: money }
972
+ calculate: [{ target: open, op: sum, values: [{ field: self.open }, { field: input.amount }] }]
782
973
  }
783
974
  records {
784
975
  waiver_request: {
@@ -792,20 +983,24 @@ instrument late_charge(on: ref<financing.installments>[], grace: duration = 3d,
792
983
  action cancel { from: requested, to: cancelled }
793
984
  }
794
985
  waiver: {
795
- fields { assessment: ref<parent>, amendment: ref<on.amendment>, amount: money? }
986
+ fields { assessment: ref<parent>, amendment: ref<on.amendment>, amount: money }
796
987
  lifecycle { states: [recorded], initial: recorded }
797
- action create { allowZero: true,
798
- actor: { parent: parent }, input { assessment: ref<parent>, amendment: ref<on.amendment> }
799
- calculate: [{ target: amount, op: rate, base: { field: self.assessment.receivable.balance }, bps: { field: self.amendment.waiverPercent }, rounding: floor }]
988
+ action create {
989
+ actor: { parent: parent }, input { assessment: ref<parent>, amendment: ref<on.amendment>, amount: money }
800
990
  requires unique "amendment_waiver" on [self.assessment, self.amendment]
801
- moves self.amount from self.assessment.receivable to self.assessment.debt economics { purpose: internal, sourceParty: programOperator }
802
991
  }
803
992
  }
804
993
  receipt: {
805
- fields { cashReceipt: text?, plan: ref<on>, assessment: ref<parent>, payment: ref<on.payment>, amount: money? }
994
+ fields { cashReceipt: text?, plan: ref<on>, assessment: ref<parent>, payment: ref<on.payment>, amount: money }
806
995
  lifecycle { states: [paid, refunded], initial: paid }
807
- action create { allowZero: true, calculate: [{ target: amount, op: minimum, values: [{ field: self.payment.held.balance }, { field: self.assessment.receivable.balance }] }], actor: { parent: parent }, input { plan: ref<on>, assessment: ref<parent>, payment: ref<on.payment> }, requires input.payment.plan == self.assessment.plan; requires unique "cost_receipt" on [self.assessment, self.payment]; moves self.amount from self.payment.held to costs_to capture "cashReceipt" economics { recipient: costs_to, company: { purpose: earning, sourceParty: borrower }, participant: { purpose: pass_through, sourceParty: programOperator } }; moves self.amount from self.assessment.receivable to self.assessment.debt economics { purpose: internal, sourceParty: programOperator } }
808
- action refund { allowZero: true, from: paid, to: refunded, actor: { parent: [parent, on] }, moves self.amount from costs_to to self.payment.plan.borrower economics { recipient: costs_to, company: { purpose: earning, sourceParty: costs_to, reversalOf: self.cashReceipt }, participant: unclassified }; moves self.amount from self.assessment.debt to self.assessment.receivable economics { purpose: internal, sourceParty: borrower } }
996
+ action create { actor: { parent: parent }, input { plan: ref<on>, assessment: ref<parent>, payment: ref<on.payment>, amount: money }, requires input.payment.plan == self.assessment.plan; requires unique "cost_receipt" on [self.assessment, self.payment]; moves self.amount from self.payment.held to costs_to capture "cashReceipt" economics { recipient: costs_to, company: { purpose: earning, sourceParty: borrower }, participant: { purpose: pass_through, sourceParty: programOperator } }; moves self.amount from self.plan.costReceivable to self.plan.debt economics { purpose: internal, sourceParty: programOperator } }
997
+ action refund {
998
+ from: paid, to: refunded, actor: { parent: [parent, child(on, payment)] }
999
+ moves self.amount from costs_to to self.payment.plan.borrower economics { recipient: costs_to, company: { purpose: earning, sourceParty: costs_to, reversalOf: self.cashReceipt }, participant: unclassified }
1000
+ moves self.amount from self.plan.debt to self.plan.costReceivable economics { purpose: internal, sourceParty: borrower }
1001
+ invoke: [{ reference: self.assessment, action: reopen, input: { amount: { field: self.amount } } }]
1002
+ }
1003
+ action unwind { from: paid, to: refunded, actor: { parent: on }, moves self.amount from costs_to to self.payment.plan.borrower economics { recipient: costs_to, company: { purpose: earning, sourceParty: costs_to, reversalOf: self.cashReceipt }, participant: unclassified } }
809
1004
  }
810
1005
  }
811
1006
  }