@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.
- package/dist/src/ast.d.ts +1 -0
- package/dist/src/ast.d.ts.map +1 -1
- package/dist/src/ast.js.map +1 -1
- package/dist/src/attachment-economics.d.ts +19 -0
- package/dist/src/attachment-economics.d.ts.map +1 -1
- package/dist/src/attachment-economics.js +101 -3
- package/dist/src/attachment-economics.js.map +1 -1
- package/dist/src/binding-contract.d.ts.map +1 -1
- package/dist/src/binding-contract.js +10 -4
- package/dist/src/binding-contract.js.map +1 -1
- package/dist/src/compile.d.ts.map +1 -1
- package/dist/src/compile.js +36 -8
- package/dist/src/compile.js.map +1 -1
- package/dist/src/examples-bundle.js +1 -1
- package/dist/src/examples-bundle.js.map +1 -1
- package/dist/src/headers.d.ts.map +1 -1
- package/dist/src/headers.js +3 -1
- package/dist/src/headers.js.map +1 -1
- package/dist/src/parse.js +13 -1
- package/dist/src/parse.js.map +1 -1
- package/dist/src/std-bundle.js +6 -6
- package/dist/src/std-bundle.js.map +1 -1
- package/dist/src/template-guide.d.ts.map +1 -1
- package/dist/src/template-guide.js +2 -1
- package/dist/src/template-guide.js.map +1 -1
- package/docs/README.md +5 -2
- package/examples/camera-rental.hsx +4 -2
- package/package.json +4 -4
- package/src/ast.ts +7 -1
- package/src/attachment-economics.ts +127 -2
- package/src/binding-contract.ts +13 -6
- package/src/compile.ts +63 -9
- package/src/examples-bundle.ts +1 -1
- package/src/headers.ts +6 -1
- package/src/parse.ts +16 -1
- package/src/std-bundle.ts +6 -6
- package/src/template-guide.ts +2 -1
- package/std/collections.hsx +4 -4
- package/std/financing.hsx +461 -266
- package/std/insurance.hsx +21 -5
- package/std/lending.hsx +13 -9
- package/std/money.hsx +88 -32
- 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
|
|
47
|
-
// payment, share,
|
|
48
|
-
// profit, fines and costs, cancels the plan, and returns the hold's
|
|
49
|
-
// to the borrower; capital keeps the principal the borrower repaid.
|
|
50
|
-
// a lending.distribution has paid out keeps its profit and share,
|
|
51
|
-
// hold that profit; only the other settlements' profit is
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
// every late charge attachment
|
|
58
|
-
// source account is short, the whole
|
|
59
|
-
//
|
|
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
|
-
//
|
|
79
|
-
//
|
|
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:
|
|
89
|
-
//
|
|
90
|
-
//
|
|
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,
|
|
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,
|
|
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
|
|
122
|
-
invariants sum "principalReceivable.balance" of { instrument: all(financing.installments
|
|
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: {
|
|
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:
|
|
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
|
-
|
|
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:
|
|
155
|
-
requires
|
|
156
|
-
requires
|
|
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:
|
|
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
|
-
|
|
170
|
-
|
|
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
|
|
227
|
+
action settle_payoff {
|
|
173
228
|
from: paid, to: paid, actor: { parent: current() }
|
|
174
|
-
|
|
175
|
-
|
|
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:
|
|
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.
|
|
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:
|
|
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:
|
|
192
|
-
{ target: openCharges, op:
|
|
193
|
-
{ target: openCosts, op:
|
|
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:
|
|
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:
|
|
211
|
-
requires count of { instrument: all(financing.late_charge), reference: "plan", anchor: self.id, states: [collected], limit:
|
|
212
|
-
requires count of { instrument: all(financing.late_charge.cost), reference: "plan", anchor: self.id, states: [collected], limit:
|
|
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:
|
|
215
|
-
requires count of { instrument: all(financing.late_charge.cost), reference: "plan", anchor: self.id, states: [assessed], limit:
|
|
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
|
-
|
|
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:
|
|
230
|
-
requires count of { instrument: all(financing.late_charge), reference: "plan", anchor: self.id, states: [collected], limit:
|
|
231
|
-
requires count of { instrument: all(financing.late_charge.cost), reference: "plan", anchor: self.id, states: [collected], limit:
|
|
232
|
-
|
|
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:
|
|
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
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
|
|
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:
|
|
294
|
-
{ target: earnedProfit, op:
|
|
295
|
-
{ target: unearnedProfit, op:
|
|
296
|
-
{ target: rebate, op:
|
|
297
|
-
{ target: fines, op:
|
|
298
|
-
{ target: costs, op:
|
|
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,
|
|
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:
|
|
324
|
-
{ target: earnedProfitSnapshot, op:
|
|
325
|
-
{ target: unearnedProfitSnapshot, op:
|
|
326
|
-
{ target: finesSnapshot, op:
|
|
327
|
-
{ target: costsSnapshot, op:
|
|
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: {
|
|
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:
|
|
341
|
-
invoke: [{ selection: { instrument: slice, reference: "plan", anchor: self.plan, states: [paid], limit:
|
|
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,
|
|
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,
|
|
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
|
|
378
|
-
//
|
|
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,
|
|
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
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
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,
|
|
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: {
|
|
407
|
-
{ target: principalAllocated, op: multiply, amount: { field: self.principalBase }, units: {
|
|
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: {
|
|
412
|
-
{ target: profitAllocated, op: multiply, amount: { field: self.profitBase }, units: {
|
|
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: {
|
|
422
|
-
{ target: profit, op: annuity, amount: { field: self.plan.principal }, rate: { literal: profit_rate }, periods: {
|
|
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
|
-
|
|
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
|
-
|
|
467
|
-
|
|
468
|
-
when profit_earned is
|
|
469
|
-
|
|
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.
|
|
479
|
-
when profit_earned is by_schedule { calculate: [{ target: collectibleProfit, op: sum, values: [{ field: self.
|
|
480
|
-
when profit_earned is at_disbursement { calculate: [{ target: collectibleProfit, op: sum, values: [{ field: self.
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
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.
|
|
493
|
-
when profit_earned is by_schedule { calculate: [{ target: collectibleProfit, op: sum, values: [{ field: self.
|
|
494
|
-
when profit_earned is at_disbursement { calculate: [{ target: collectibleProfit, op: sum, values: [{ field: self.
|
|
495
|
-
|
|
496
|
-
{
|
|
497
|
-
{
|
|
498
|
-
{
|
|
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.
|
|
507
|
-
when profit_earned is by_schedule { calculate: [{ target: collectibleProfit, op: sum, values: [{ field: self.
|
|
508
|
-
when profit_earned is at_disbursement { calculate: [{ target: collectibleProfit, op: sum, values: [{ field: self.
|
|
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.
|
|
511
|
-
{ target: paymentBudget, op: minimum, values: [{ field: input.payment.
|
|
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.
|
|
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:
|
|
519
|
-
{ target:
|
|
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 },
|
|
523
|
-
{ instrument: profit_piece, action: create, input: { plan: { field: self.plan }, slice: { field: self.id }, payment: { field: input.payment },
|
|
524
|
-
{
|
|
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.
|
|
655
|
+
requires self.openPrincipal == "0"; requires self.openProfit == "0"; requires self.earnedProfit == "0"
|
|
530
656
|
}
|
|
531
|
-
action
|
|
532
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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 {
|
|
674
|
+
action cancel {
|
|
551
675
|
from: [pending, due, paid], to: cancelled, actor: { parent: parent }
|
|
552
|
-
|
|
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,
|
|
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?,
|
|
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:
|
|
572
|
-
when profit_earned is on_payment { calculate: [{ target: outstandingProfit, op:
|
|
573
|
-
when profit_earned is by_schedule { calculate: [{ target: outstandingProfit, op:
|
|
574
|
-
when profit_earned is at_disbursement { calculate: [{ target: outstandingProfit, op:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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.
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
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
|
-
|
|
604
|
-
|
|
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:
|
|
608
|
-
|
|
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: "
|
|
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:
|
|
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>,
|
|
773
|
+
fields { plan: ref<parent>, slice: ref<slice>, payment: ref<payment>, amount: money }
|
|
617
774
|
lifecycle { states: [paid, refunded], initial: paid }
|
|
618
|
-
action create {
|
|
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>,
|
|
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 {
|
|
780
|
+
action refund {
|
|
626
781
|
from: paid, to: refunded, actor: { parent: payment }
|
|
627
|
-
|
|
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 {
|
|
786
|
+
fields { plan: ref<parent>, slice: ref<slice>, payment: ref<payment>, amount: money }
|
|
632
787
|
lifecycle { states: [paid, refunded], initial: paid }
|
|
633
|
-
action create {
|
|
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>,
|
|
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 {
|
|
644
|
-
from: paid, to: refunded, actor: { parent:
|
|
645
|
-
|
|
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
|
-
|
|
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.
|
|
674
|
-
|
|
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,
|
|
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
|
-
|
|
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:
|
|
837
|
+
from: assessed, to: collected, actor: { parent: on }
|
|
684
838
|
requires self.plan in [paid]
|
|
685
|
-
calculate: [{ target: payoffAmount, op:
|
|
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.
|
|
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.
|
|
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 {
|
|
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
|
-
|
|
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:
|
|
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: {} }
|
|
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 {
|
|
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 {
|
|
734
|
-
action refund {
|
|
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
|
-
|
|
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.
|
|
748
|
-
|
|
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,
|
|
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
|
-
|
|
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:
|
|
933
|
+
from: assessed, to: collected, actor: { parent: on }
|
|
758
934
|
requires self.plan in [paid]
|
|
759
|
-
calculate: [{ target: payoffAmount, op:
|
|
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.
|
|
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.
|
|
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 {
|
|
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
|
-
|
|
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:
|
|
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: {} }
|
|
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 {
|
|
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 {
|
|
808
|
-
action refund {
|
|
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
|
}
|