@hyperscale0/hsx 3.1.0 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,9 @@
1
1
  header collections
2
2
 
3
3
  instrument case(on: ref<financing.installments>[], agency: approval, overdue: duration = 3d, fee: percent = 20%) {
4
+ revisioned: true
4
5
  summary: "Refer an overdue installment without creating another debt."
5
- fields { plan: ref<on>, slice: ref<on.slice>, payment: ref<on.payment>, eligibleAt: date = after(self.slice.dueAt, overdue), agencyFee: money = rate(self.payment.amount, fee) }
6
+ fields { assignedAgency: account of agency, plan: ref<on>, slice: ref<on.slice>, payment: ref<on.payment>, eligibleAt: date = after(self.slice.dueAt, overdue), agencyFee: money = rate(self.payment.amount, fee) }
6
7
  lifecycle { states: [opened, assigned, recovered, recalled, returned_uncollected], initial: opened }
7
8
  action create {
8
9
  due: { at: self.eligibleAt }
@@ -11,7 +12,7 @@ instrument case(on: ref<financing.installments>[], agency: approval, overdue: du
11
12
  requires unique "overdue_referral" on [self.slice]
12
13
  }
13
14
  action assign { from: opened, to: assigned, requires approval by agency }
14
- action recover { from: assigned, to: recovered, actor: { party: agency }, requires self.payment.plan == self.plan, invoke: [{ reference: self.payment, action: pay, input: {} }, { reference: self.id, action: pay_fee, input: {} }] }
15
+ action recover { expansionLimit: 8192, from: assigned, to: recovered, actor: { party: agency }, requires self.payment.plan == self.plan, invoke: [{ reference: self.payment, action: pay, input: {} }, { reference: self.id, action: pay_fee, input: {} }] }
15
16
  action pay_fee { from: recovered, to: recovered, actor: { parent: current() }, moves self.agencyFee from self.plan.capital to agency }
16
17
  action recall { from: [opened, assigned], to: recalled }
17
18
  action return_uncollected { from: assigned, to: returned_uncollected, actor: { party: agency } }
@@ -20,12 +21,52 @@ instrument case(on: ref<financing.installments>[], agency: approval, overdue: du
20
21
  instrument contact(case: ref<collections.case>[], agency: party, max_contacts: integer = 10, window: duration = 30d, contact_from: integer(0, 23) = 8, contact_until: integer(1, 23) = 20, timezone: text = "Asia/Riyadh") {
21
22
  constraints { contact_from: less_than(contact_until) }
22
23
  summary: "An agency contact admitted against the locked rolling count and local hours."
23
- fields { case: ref<case>, attemptedAt: date?, evidence: text, maximum: integer = max_contacts }
24
+ fields { agency: account of agency, case: ref<case>, attemptedAt: date?, evidence: text, maximum: integer = max_contacts }
24
25
  lifecycle { states: [recorded], initial: recorded }
25
26
  invariants count of { instrument: current(), reference: "case", anchor: self.case, states: [recorded], limit: 366, window: { field: attemptedAt, milliseconds: window } } <= self.maximum
26
27
  action create {
27
28
  actor: { party: agency }
28
29
  set: { attemptedAt: { field: self.now } }
29
- requires self.case in [assigned]; requires hours self.now between contact_from and contact_until timezone timezone
30
+ requires self.agency == self.case.assignedAgency; requires self.case in [assigned]; requires hours self.now between contact_from and contact_until timezone timezone
31
+ }
32
+ records {
33
+ outcome: {
34
+ fields { contact: ref<parent>, agency: account of agency, outcome: enum(reached, no_answer, promise_to_pay, disputed, wrong_contact, refused), evidence: text, recordedAt: date? }
35
+ lifecycle { states: [recorded], initial: recorded }
36
+ action create {
37
+ actor: { party: agency }
38
+ requires self.agency == self.contact.case.assignedAgency
39
+ set: { recordedAt: { field: self.now } }
40
+ }
41
+ }
42
+ }
43
+ }
44
+
45
+
46
+ instrument reminder(on: ref<financing.installments>[], operator: party = programOperator, before_days: integer(0, 366) = 3, overdue_days: integer(1, 366) = 1, max_per_day: integer(1, 10) = 1, send_from: integer(0, 23) = 8, send_until: integer(1, 24) = 20, timezone: text = "Asia/Riyadh") {
47
+ constraints { send_from: less_than(send_until) }
48
+ summary: "Send an email to the installment borrower's stored contact address."
49
+ fields { slice: ref<on.slice> }
50
+ lifecycle { states: [ready], initial: ready }
51
+ action create { requires unique "installment_reminder" on [self.slice] }
52
+ action dispatch {
53
+ from: ready, to: ready, actor: { party: operator }
54
+ invoke: [{ reference: self.id, action: send, input: {} }]
55
+ }
56
+ action before_due {
57
+ from: ready, to: ready, actor: clock
58
+ due: { at: self.slice.dueAt, localDay: { days: before_days, direction: "before", hour: send_from, timezone: timezone } }
59
+ invoke: [{ selection: { instrument: current(), reference: "slice", anchor: self.slice, states: [ready], limit: 1, where: { slice.plan.status: { literal: "active" } } }, action: send, guard: { kind: compare, left: { field: self.slice.principalReceivable.balance }, operator: ">", right: { literal: "0" } }, input: {} }]
60
+ }
61
+ action overdue {
62
+ from: ready, to: ready, actor: clock
63
+ due: { at: self.slice.dueAt, localDay: { days: overdue_days, direction: "after", hour: send_from, timezone: timezone } }
64
+ invoke: [{ selection: { instrument: current(), reference: "slice", anchor: self.slice, states: [ready], limit: 1, where: { slice.plan.status: { literal: "active" } } }, action: send, guard: { kind: compare, left: { field: self.slice.principalReceivable.balance }, operator: ">", right: { literal: "0" } }, input: {} }]
65
+ }
66
+ action send {
67
+ from: ready, to: ready, actor: { parent: current() }
68
+ requires self.slice.plan in [active]
69
+ requires self.slice.principalReceivable.balance > "0"
70
+ reminder: { installment: self.slice, recipient: self.slice.plan.borrower, dueAt: self.slice.dueAt, channel: "email", template: "payment_reminder", beforeDays: before_days, overdueDays: overdue_days, maxPerDay: max_per_day, startHour: send_from, endHour: send_until, timezone: timezone }
30
71
  }
31
72
  }
package/std/escrow.hsx CHANGED
@@ -1,20 +1,27 @@
1
1
  header escrow
2
2
 
3
3
  instrument hold(payer: party, payee: party, for: ref<marketplace.order>, accept_within: duration = 48h, fee: fee = { seller: 0%, tax: 0% }, dispute: policy = { refund_after: return_verified }) {
4
+ revisioned: true
4
5
  summary: "Hold an order's price through delivery, acceptance and verified return."
5
- fields { payer: account of payer, payee: account of payee, order: ref<for>, price: money, held: account of self, deliveredAt: date? }
6
+ fields { payer: account of payer, payee: account of payee, order: ref<for>, price: money, held: account of self, deliveredAt: date?, buyerTopUp: money? }
6
7
  lifecycle { states: [pending, funded, delivered, disputed, return_verified, released, refunded, cancelled], initial: pending }
7
8
  action create {
8
9
  requires self.payer == self.order.buyer; requires self.payee == self.order.listing.seller; requires self.price == self.order.price
9
10
  }
10
11
  action fund {
11
12
  from: pending, to: funded, actor: { party: payer }
12
- requires self.order in [placed]
13
- moves self.price from payer to self.held
13
+ requires self.order in [quoted, placed]
14
+ invoke: [{ reference: self.order, action: commit, input: {} }, { selection: { instrument: all(marketplace.reservation), reference: "listing", anchor: self.order.listing, states: [active], limit: 1, where: { buyer: { field: self.payer } } }, action: credit, input: { funds: { field: self.id }, contribution: { field: self.price } } }, { reference: self.id, action: fund_remainder, input: {} }]
15
+ }
16
+ action fund_remainder {
17
+ from: funded, to: funded, actor: { parent: current() }
18
+ calculate: [{ target: buyerTopUp, op: subtract, base: { field: self.price }, subtract: [{ field: self.held.balance }] }]
19
+ moves self.buyerTopUp from payer to self.held
14
20
  }
15
21
  action confirm {
16
22
  from: pending, to: funded
17
23
  requires self.held.balance == self.price
24
+ invoke: [{ reference: self.order, action: commit, input: {} }]
18
25
  }
19
26
  action deliver {
20
27
  from: funded, to: delivered, actor: { party: payee }
package/std/financing.hsx CHANGED
@@ -1,12 +1,13 @@
1
1
  header financing
2
2
 
3
- instrument installments(months: integer(1, 366), profit: percent, disburse_to: enum(funds, borrower) = funds, profit_earned: enum(on_payment, by_schedule, at_disbursement) = on_payment, apply: enum(fines_profit_principal, principal_profit, pro_rata) = fines_profit_principal, payoff_rebate: percent = 100%, down_payment: percent = 0%, funds: ref, approval: approval, borrower: party = party(person), capital: party = programOperator, profit_to: party = programOperator, limits: ref<financing.limits> = object(financing.limits)) {
3
+ instrument installments(months: integer(1, 366), charge_limit: integer(1, 3) = 3, profit: percent, disburse_to: enum(funds, borrower) = funds, profit_earned: enum(on_payment, by_schedule, at_disbursement) = on_payment, apply: enum(fines_profit_principal, principal_profit, pro_rata) = fines_profit_principal, payoff_rebate: percent = 100%, write_off_after: duration = 90d, max_extension: duration = 90d, max_amendments: integer(0, 366) = 2, waiver_limit: percent = 100%, amendment_expiry: duration = 7d, payoff_quote_expiry: duration = 1h, allow_overdue: enum(allowed, blocked) = allowed, assessed_fines: enum(carry, require_waiver) = carry, down_payment: percent = 0%, funds: ref, approval: approval, borrower: party = party(person), capital: party = programOperator, profit_to: party = programOperator, limits: ref<financing.limits> = object(financing.limits)) {
4
+ revisioned: true
4
5
  summary: "Funding, profit recognition and payment order chosen by the program."
5
6
  fields {
6
7
  funds: ref<funds>, borrower: account of borrower, settlementIdentity: text, borrowerLimit: ref<limits>, portfolioLimit: ref<limits.portfolio>
7
8
  months: integer = months, dates: list(date, months)
8
9
  capital: account(capital, cash, "capital"), profitIncome: account(profit_to, cash, "profitIncome"), loss: account(capital, claim, "loss")
9
- principalReturned: money?, profitReturned: money?, lossRecorded: money?
10
+ downPaymentRemaining: money?, downPaymentCredit: money?, principalReturned: money?, profitReturned: money?, lossRecorded: money?
10
11
  price: money = self.funds.price
11
12
  downPayment: money = rate(self.price, down_payment)
12
13
  principal: money = subtract(self.price, self.downPayment)
@@ -17,10 +18,14 @@ instrument installments(months: integer(1, 366), profit: percent, disburse_to: e
17
18
  invariants count of { instrument: all(financing.installments), reference: "borrowerLimit", anchor: self.borrowerLimit, states: [active], limit: 366 } <= self.borrowerLimit.activePlans
18
19
  invariants sum "principalReceivable.balance" of { instrument: all(financing.installments.slice), reference: "borrowerLimit", anchor: self.borrowerLimit, states: [pending, due, paid], limit: 366 } <= self.borrowerLimit.limit
19
20
  invariants sum "principalReceivable.balance" of { instrument: all(financing.installments.slice), reference: "portfolioLimit", anchor: self.portfolioLimit, states: [pending, due, paid], limit: 366 } <= self.portfolioLimit.limit
20
- action create { requires self.borrower == self.funds.payer; requires self.borrowerLimit.borrower == self.funds.payer }
21
+ action create {
22
+ requires self.borrower == self.funds.payer; requires self.borrowerLimit.borrower == self.funds.payer
23
+ invoke: [{ range: { count: { literal: months }, maximum: months, bind: position }, instrument: slice, action: create, input: { plan: { field: self.id }, borrowerLimit: { field: self.borrowerLimit }, portfolioLimit: { field: self.portfolioLimit }, position: { field: position } } }]
24
+ }
21
25
  action sign {
22
- from: quoted, to: signed, input { signedOffer: text, affordabilityReference: text }
26
+ from: quoted, to: signed, actor: { party: borrower }, input { signedOffer: text, affordabilityReference: text }
23
27
  requires count of { instrument: slice, reference: "plan", anchor: self.id, states: [draft], limit: 366 } == self.months
28
+ when funds has order { invoke: [{ reference: self.funds.order, action: commit, input: {} }] }
24
29
  }
25
30
  action disburse {
26
31
  from: signed, to: active
@@ -29,23 +34,35 @@ instrument installments(months: integer(1, 366), profit: percent, disburse_to: e
29
34
  requires self.portfolioLimit in [approved]
30
35
  when disburse_to is funds {
31
36
  requires self.funds in [pending]
32
- moves self.downPayment from self.funds.payer to self.funds.held
33
- moves self.principal from self.capital to self.funds.held
37
+ when funds has order { invoke: [{ selection: { instrument: all(marketplace.reservation), reference: "listing", anchor: self.funds.order.listing, states: [active], limit: 1, where: { buyer: { field: self.borrower } } }, action: credit, input: { funds: { field: self.funds }, contribution: { field: self.downPayment } } }] }
38
+ invoke: [{ reference: self.id, action: fund_down_payment, input: {} }]
34
39
  }
35
40
  when disburse_to is borrower { moves self.principal from self.capital to self.borrower }
36
41
  invoke: [{ selection: { instrument: slice, reference: "plan", anchor: self.id, states: [draft], limit: 366, order: [position] }, action: activate, input: {} }]
37
42
  when disburse_to is funds { invoke: [{ reference: self.funds, action: confirm, input: {} }] }
38
43
  }
44
+ action fund_down_payment {
45
+ from: active, to: active, actor: { parent: current() }
46
+ calculate: [{ target: downPaymentCredit, op: minimum, values: [{ field: self.funds.held.balance }, { field: self.downPayment }] }, { target: downPaymentRemaining, op: subtract, base: { field: self.downPayment }, subtract: [{ field: self.downPaymentCredit }] }]
47
+ moves self.downPaymentRemaining from self.funds.payer to self.funds.held
48
+ moves self.principal from self.capital to self.funds.held
49
+ }
39
50
  action complete {
40
51
  from: active, to: paid
41
52
  requires count of { instrument: slice, reference: "plan", anchor: self.id, states: [pending, due], limit: 366 } == 0
53
+ requires sum "receivable.balance" of { instrument: all(financing.late_charge), reference: "plan", anchor: self.id, states: [assessed, collected], limit: 366 } == "0"
54
+ requires sum "receivable.balance" of { instrument: all(financing.late_charge.cost), reference: "plan", anchor: self.id, states: [assessed, collected], limit: 366 } == "0"
42
55
  }
43
56
  action restore { from: [active, paid], to: active, actor: { parent: payment } }
44
57
  action payoff {
45
- from: active, to: paid
58
+ from: active, to: paid, actor: { party: borrower }
59
+ input { quote: ref<payoff_quote> }
60
+ requires input.quote.plan == self.id; requires input.quote.revision == self.productRevision
61
+ deadline: { at: input.quote.expiresAt }
46
62
  calculate: [
47
63
  { target: "principalReturned", op: "aggregate", selection: { instrument: slice, reference: "plan", anchor: self.id, states: [pending, due], limit: 366 }, measure: { sum: "principalReceivable.balance" } }
48
64
  ]
65
+ invoke: [{ selection: { instrument: all(financing.late_charge), reference: "plan", anchor: self.id, states: [assessed, collected], limit: 366, order: [overdueAt] }, action: payoff, input: {} }, { selection: { instrument: all(financing.late_charge.cost), reference: "plan", anchor: self.id, states: [assessed, collected], limit: 366, order: [overdueAt] }, action: payoff, input: {} }]
49
66
  invoke: [{ selection: { instrument: slice, reference: "plan", anchor: self.id, states: [pending, due], limit: 366, order: [position] }, action: payoff, input: { identity: { field: self.settlementIdentity } } }, { reference: self.id, action: record_payoff, input: {} }]
50
67
  }
51
68
  action record_payoff {
@@ -67,7 +84,93 @@ instrument installments(months: integer(1, 366), profit: percent, disburse_to: e
67
84
  invoke: [{ instrument: settlement, action: create, input: { plan: { field: self.id }, identity: { field: input.identity }, principal: { field: input.principal }, profit: { field: input.profit }, loss: { literal: "0" }, kind: { literal: "cash" } } }]
68
85
  }
69
86
  action cancel { from: quoted, to: cancelled }
87
+ action void { from: signed, to: cancelled, actor: { party: borrower } }
70
88
  records {
89
+ payoff_quote: {
90
+ fields {
91
+ plan: ref<parent>, revision: text?, effectiveAt: date?, expiresAt: date = after(self.createdAt, payoff_quote_expiry)
92
+ principal: money?, earnedProfit: money?, unearnedProfit: money?, rebate: money?, fines: money?, costs: money?, gross: money?, amount: money?
93
+ }
94
+ lifecycle { states: [quoted], initial: quoted }
95
+ action create {
96
+ requires self.plan in [active]
97
+ set: { revision: { field: self.productRevision }, effectiveAt: { field: self.now } }
98
+ invoke: [
99
+ { selection: { instrument: slice, reference: "plan", anchor: self.plan, states: [pending], limit: 366 }, action: quote_pending, input: { quote: { field: self.id } } }
100
+ { selection: { instrument: slice, reference: "plan", anchor: self.plan, states: [due], limit: 366 }, action: quote_due, input: { quote: { field: self.id } } }
101
+ { reference: self.id, action: record, input: {} }
102
+ ]
103
+ }
104
+ action record {
105
+ from: quoted, to: quoted, actor: { parent: current() }
106
+ calculate: [
107
+ { target: principal, op: aggregate, selection: { instrument: slice, reference: "plan", anchor: self.plan, states: [pending, due, paid], limit: 366 }, measure: { sum: "principalReceivable.balance" } }
108
+ { target: earnedProfit, op: aggregate, selection: { instrument: slice, reference: "plan", anchor: self.plan, states: [pending, due, paid], limit: 366 }, measure: { sum: "profitEarned.balance" } }
109
+ { target: unearnedProfit, op: aggregate, selection: { instrument: slice, reference: "plan", anchor: self.plan, states: [pending, due, paid], limit: 366 }, measure: { sum: "profitReceivable.balance" } }
110
+ { target: rebate, op: aggregate, selection: { instrument: quote_piece, reference: "quote", anchor: self.id, states: [recorded], limit: 366 }, measure: { sum: "rebate" } }
111
+ { target: fines, op: aggregate, selection: { instrument: all(financing.late_charge), reference: "plan", anchor: self.plan, states: [assessed, collected], limit: 366 }, measure: { sum: "receivable.balance" } }
112
+ { target: costs, op: aggregate, selection: { instrument: all(financing.late_charge.cost), reference: "plan", anchor: self.plan, states: [assessed, collected], limit: 366 }, measure: { sum: "receivable.balance" } }
113
+ { target: gross, op: sum, values: [{ field: self.principal }, { field: self.earnedProfit }, { field: self.unearnedProfit }, { field: self.fines }, { field: self.costs }] }
114
+ { target: amount, op: subtract, base: { field: self.gross }, subtract: [{ field: self.rebate }] }
115
+ ]
116
+ }
117
+ }
118
+ quote_piece: {
119
+ fields { quote: ref<payoff_quote>, slice: ref<slice>, rebate: money = rate(self.slice.profitReceivable.balance, payoff_rebate) }
120
+ lifecycle { states: [recorded], initial: recorded }
121
+ action create { actor: { parent: slice }, input { quote: ref<payoff_quote>, slice: ref<slice> }, requires unique "payoff_slice_quote" on [self.quote, self.slice] }
122
+ }
123
+ amendment: {
124
+ fields {
125
+ plan: ref<parent>, dates: list(date, months), agreement: text, waiverPercent: percent, waiverEvidence: text, maximumWaiver: percent = waiver_limit
126
+ revision: text?, acceptedAt: date?, staleReason: text?, expiresAt: date = after(self.createdAt, amendment_expiry)
127
+ fineDisposition: enum(carry, require_waiver) = assessed_fines, addedProfit: money = 0 SAR
128
+ principalSnapshot: money?, earnedProfitSnapshot: money?, unearnedProfitSnapshot: money?, finesSnapshot: money?, costsSnapshot: money?
129
+ extension: duration = max_extension, maximum: integer = max_amendments
130
+ }
131
+ lifecycle { states: [proposed, applied, declined, withdrawn, expired, superseded], initial: proposed }
132
+ action create {
133
+ actor: { party: borrower }
134
+ requires self.plan in [active]; requires self.waiverPercent <= self.maximumWaiver
135
+ set: { revision: { field: self.productRevision } }
136
+ calculate: [
137
+ { target: principalSnapshot, op: aggregate, selection: { instrument: slice, reference: "plan", anchor: self.plan, states: [pending, due, paid], limit: 366 }, measure: { sum: "principalReceivable.balance" } }
138
+ { target: earnedProfitSnapshot, op: aggregate, selection: { instrument: slice, reference: "plan", anchor: self.plan, states: [pending, due, paid], limit: 366 }, measure: { sum: "profitEarned.balance" } }
139
+ { target: unearnedProfitSnapshot, op: aggregate, selection: { instrument: slice, reference: "plan", anchor: self.plan, states: [pending, due, paid], limit: 366 }, measure: { sum: "profitReceivable.balance" } }
140
+ { target: finesSnapshot, op: aggregate, selection: { instrument: all(financing.late_charge), reference: "plan", anchor: self.plan, states: [assessed, collected], limit: 366 }, measure: { sum: "receivable.balance" } }
141
+ { target: costsSnapshot, op: aggregate, selection: { instrument: all(financing.late_charge.cost), reference: "plan", anchor: self.plan, states: [assessed, collected], limit: 366 }, measure: { sum: "receivable.balance" } }
142
+ ]
143
+ requires count of { instrument: current(), reference: "plan", anchor: self.plan, states: [applied], limit: 366 } < self.maximum
144
+ invoke: [{ range: { count: { literal: months }, maximum: months, bind: position }, instrument: proposed_date, action: create, input: { amendment: { field: self.id }, position: { field: position } } }, { selection: { instrument: proposed_date, reference: "amendment", anchor: self.id, states: [recorded], limit: 366, where: { first: { literal: 0 } } }, action: check_order, input: {} }]
145
+ }
146
+ action accept {
147
+ from: proposed, to: applied, actor: { party: borrower }, deadline: { at: self.expiresAt }
148
+ requires self.plan in [active]; requires self.revision == self.productRevision
149
+ requires approval by approval
150
+ requires count of { instrument: current(), reference: "plan", anchor: self.plan, states: [applied], limit: 366 } < self.maximum
151
+ set: { acceptedAt: { field: self.now } }
152
+ when assessed_fines is require_waiver { requires self.waiverPercent == 10000 }
153
+ 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 } } }]
154
+ invoke: [{ selection: { instrument: all(collections.case), reference: "plan", anchor: self.plan, states: [opened, assigned], limit: 366 }, action: recall, input: {} }, { selection: { instrument: all(financing.late_charge.cost), reference: "plan", anchor: self.plan, states: [proposed], limit: 366 }, action: cancel, input: {} }, { selection: { instrument: all(financing.late_charge), reference: "plan", anchor: self.plan, states: [proposed], limit: 366 }, action: cancel, input: {} }]
155
+ invoke: [{ selection: { instrument: slice, reference: "plan", anchor: self.plan, states: [pending, due], limit: 366, order: [position] }, action: amend, input: { amendment: { field: self.id } } }]
156
+ invoke: [{ selection: { instrument: slice, reference: "plan", anchor: self.plan, states: [paid], limit: 366 }, action: verify_fixed_date, input: { amendment: { field: self.id } } }]
157
+ }
158
+ action withdraw { from: proposed, to: withdrawn, actor: { party: borrower } }
159
+ action decline { from: proposed, to: declined, actor: { party: approval } }
160
+ action expire { from: proposed, to: expired, actor: clock, due: { at: self.expiresAt } }
161
+ action supersede { from: proposed, to: superseded, requires self.revision != self.productRevision, set: { staleReason: { literal: "economic_revision_changed" } } }
162
+ }
163
+ proposed_date: {
164
+ fields { amendment: ref<amendment>, position: integer(1, months), date: date = at(self.amendment.dates, self.position), first: integer = divide(1, self.position), prior: integer = subtract(self.position, 1), previousPosition: integer = sum(self.prior, self.first), previous: date = at(self.amendment.dates, self.previousPosition) }
165
+ lifecycle { states: [recorded], initial: recorded }
166
+ action create { actor: { parent: amendment }, input { amendment: ref<amendment>, position: integer(1, months) }, requires self.date >= self.previous }
167
+ action check_order { from: recorded, to: recorded, actor: { parent: amendment }, requires self.date > self.previous }
168
+ }
169
+ date_change: {
170
+ fields { amendment: ref<amendment>, slice: ref<slice>, oldDate: date, newDate: date }
171
+ lifecycle { states: [recorded], initial: recorded }
172
+ action create { actor: { parent: slice }, input { amendment: ref<amendment>, slice: ref<slice>, oldDate: date, newDate: date }, requires unique "amended_slice" on [self.amendment, self.slice] }
173
+ }
71
174
  settlement: {
72
175
  fields { plan: ref<parent>, identity: text, principal: money, profit: money, loss: money, kind: enum(cash, loss)
73
176
  capital: account(capital, cash, "capital"), profitIncome: account(profit_to, cash, "profitIncome"), lossAccount: account(capital, claim, "loss") }
@@ -79,9 +182,10 @@ instrument installments(months: integer(1, 366), profit: percent, disburse_to: e
79
182
  }
80
183
  }
81
184
  slice: {
185
+ revisioned: true
82
186
  fields {
83
187
  plan: ref<parent>, borrowerLimit: ref<limits>, portfolioLimit: ref<limits.portfolio>, position: integer(1, months)
84
- dueAt: date = at(self.plan.dates, self.position), lossEligibleAt: date = after(self.dueAt, 90d)
188
+ originalDueAt: date = at(self.plan.dates, self.position), dueAt: date?, nextDueAt: date?, previousDueAt: date?, previousPosition: integer?, previousDate: date?, extensionLimit: date = after(self.originalDueAt, max_extension), lossEligibleAt: date?
85
189
  principalBase: money = divide(self.plan.principal, months)
86
190
  principalAllocated: money = multiply(self.principalBase, months)
87
191
  principalRemainder: money = subtract(self.plan.principal, self.principalAllocated)
@@ -100,8 +204,44 @@ instrument installments(months: integer(1, 366), profit: percent, disburse_to: e
100
204
  }
101
205
  lifecycle { states: [draft, pending, due, paid, written_off], initial: draft }
102
206
  action create {
207
+ actor: { parent: parent }
208
+ input { plan: ref<parent>, borrowerLimit: ref<limits>, portfolioLimit: ref<limits.portfolio>, position: integer(1, months) }
209
+ set: { dueAt: { field: self.originalDueAt } }
210
+ invoke: [{ reference: self.id, action: refresh_loss_date, input: {} }]
103
211
  requires self.plan in [quoted]; requires unique "signed_position" on [self.plan, self.position]; requires self.borrowerLimit == self.plan.borrowerLimit; requires self.portfolioLimit == self.plan.portfolioLimit
104
212
  }
213
+ action quote_pending {
214
+ from: pending, to: pending, actor: { parent: payoff_quote }, input { quote: ref<payoff_quote> }
215
+ requires input.quote.plan == self.plan
216
+ invoke: [{ instrument: quote_piece, action: create, input: { quote: { field: input.quote }, slice: { field: self.id } } }]
217
+ }
218
+ action quote_due {
219
+ from: due, to: due, actor: { parent: payoff_quote }, input { quote: ref<payoff_quote> }
220
+ requires input.quote.plan == self.plan
221
+ invoke: [{ instrument: quote_piece, action: create, input: { quote: { field: input.quote }, slice: { field: self.id } } }]
222
+ }
223
+ action refresh_loss_date {
224
+ from: [draft, pending, due], to: draft, actor: { parent: current() }
225
+ calculate: [{ target: lossEligibleAt, op: shift, date: { field: self.dueAt }, milliseconds: { literal: write_off_after }, direction: after }]
226
+ }
227
+ action amend {
228
+ from: [pending, due], to: pending, actor: { parent: amendment }, input { amendment: ref<amendment> }
229
+ requires input.amendment.plan == self.plan
230
+ when allow_overdue is blocked { requires self.dueAt > self.now }
231
+ calculate: [
232
+ { target: nextDueAt, op: at, list: input.amendment.dates, position: { field: self.position } }
233
+ { target: lossEligibleAt, op: shift, date: { field: self.nextDueAt }, milliseconds: { literal: write_off_after }, direction: after }
234
+ ]
235
+ requires self.nextDueAt > self.now; requires self.nextDueAt >= self.dueAt; requires self.nextDueAt <= self.extensionLimit
236
+ set: { previousDueAt: { field: self.dueAt }, dueAt: { field: self.nextDueAt } }
237
+ invoke: [{ instrument: date_change, action: create, input: { amendment: { field: input.amendment }, slice: { field: self.id }, oldDate: { field: self.previousDueAt }, newDate: { field: self.nextDueAt } } }]
238
+ when profit_earned is by_schedule { invoke: [{ selection: { instrument: recognition, reference: "slice", anchor: self.id, states: [pending], limit: 1 }, action: reschedule, input: {} }] }
239
+ }
240
+ action verify_fixed_date {
241
+ from: paid, to: paid, actor: { parent: amendment }, input { amendment: ref<amendment> }
242
+ calculate: [{ target: nextDueAt, op: at, list: input.amendment.dates, position: { field: self.position } }]
243
+ requires self.nextDueAt == self.dueAt
244
+ }
105
245
  action activate {
106
246
  from: draft, to: pending, actor: { parent: parent }
107
247
  moves self.principal from self.debt to self.principalReceivable
@@ -113,21 +253,36 @@ instrument installments(months: integer(1, 366), profit: percent, disburse_to: e
113
253
  from: pending, to: due, actor: clock, due: { at: self.dueAt }
114
254
  }
115
255
  action collect {
116
- from: [pending, due], to: due, actor: { parent: payment }
256
+ from: [pending, due], to: preserve, actor: { parent: payment }
117
257
  input { payment: ref<payment> }
118
258
  requires input.payment.plan == self.plan
119
259
  when profit_earned is on_payment { calculate: [{ target: collectibleProfit, op: sum, values: [{ field: self.profitReceivable.balance }] }] }
120
260
  when profit_earned is by_schedule { calculate: [{ target: collectibleProfit, op: sum, values: [{ field: self.profitEarned.balance }] }] }
121
261
  when profit_earned is at_disbursement { calculate: [{ target: collectibleProfit, op: sum, values: [{ field: self.profitEarned.balance }] }] }
122
- when apply is fines_profit_principal {
123
- invoke: [{ instrument: profit_piece, action: create, input: { slice: { field: self.id }, payment: { field: input.payment }, limit: { field: self.collectibleProfit } } }, { instrument: principal_piece, action: create, input: { slice: { field: self.id }, payment: { field: input.payment }, limit: { field: self.principalReceivable.balance } } }]
124
- }
125
262
  when apply is principal_profit {
126
- invoke: [{ instrument: principal_piece, action: create, input: { slice: { field: self.id }, payment: { field: input.payment }, limit: { field: self.principalReceivable.balance } } }, { instrument: profit_piece, action: create, input: { slice: { field: self.id }, payment: { field: input.payment }, limit: { field: self.collectibleProfit } } }]
263
+ invoke: [
264
+ { instrument: principal_piece, action: create, input: { slice: { field: self.id }, payment: { field: input.payment }, limit: { field: self.principalReceivable.balance } } }
265
+ { instrument: profit_piece, action: create, input: { slice: { field: self.id }, payment: { field: input.payment }, limit: { field: self.collectibleProfit } } }
266
+ { 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: {} }
267
+ ]
127
268
  }
128
269
  }
270
+ action collect_waterfall {
271
+ from: [pending, due], to: preserve, actor: { parent: payment }, input { payment: ref<payment> }
272
+ requires input.payment.plan == self.plan
273
+ when profit_earned is on_payment { calculate: [{ target: collectibleProfit, op: sum, values: [{ field: self.profitReceivable.balance }] }] }
274
+ when profit_earned is by_schedule { calculate: [{ target: collectibleProfit, op: sum, values: [{ field: self.profitEarned.balance }] }] }
275
+ when profit_earned is at_disbursement { calculate: [{ target: collectibleProfit, op: sum, values: [{ field: self.profitEarned.balance }] }] }
276
+ invoke: [
277
+ { selection: { instrument: all(financing.late_charge), reference: "slice", anchor: self.id, states: [assessed], limit: charge_limit, order: [overdueAt] }, action: collect, input: { payment: { field: input.payment } } }
278
+ { selection: { instrument: all(financing.late_charge.cost), reference: "plan", anchor: self.plan, states: [assessed], limit: charge_limit, order: [overdueAt], where: { assessment.slice: { field: self.id } } }, action: collect, input: { payment: { field: input.payment } } }
279
+ { instrument: profit_piece, action: create, input: { slice: { field: self.id }, payment: { field: input.payment }, limit: { field: self.collectibleProfit } } }
280
+ { instrument: principal_piece, action: create, input: { slice: { field: self.id }, payment: { field: input.payment }, limit: { field: self.principalReceivable.balance } } }
281
+ { 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: {} }
282
+ ]
283
+ }
129
284
  action collect_pro_rata {
130
- from: [pending, due], to: due, actor: { parent: payment }, input { payment: ref<payment> }
285
+ from: [pending, due], to: preserve, actor: { parent: payment }, input { payment: ref<payment> }
131
286
  requires input.payment.plan == self.plan
132
287
  when profit_earned is on_payment { calculate: [{ target: collectibleProfit, op: sum, values: [{ field: self.profitReceivable.balance }] }] }
133
288
  when profit_earned is by_schedule { calculate: [{ target: collectibleProfit, op: sum, values: [{ field: self.profitEarned.balance }] }] }
@@ -141,13 +296,17 @@ instrument installments(months: integer(1, 366), profit: percent, disburse_to: e
141
296
  { target: profitPayment, op: minimum, values: [{ field: self.profitBudget }, { field: self.collectibleProfit }] }
142
297
  { target: principalPayment, op: subtract, base: { field: self.paymentBudget }, subtract: [{ field: self.profitPayment }] }
143
298
  ]
144
- invoke: [{ instrument: principal_piece, action: create, input: { slice: { field: self.id }, payment: { field: input.payment }, limit: { field: self.principalPayment } } }, { instrument: profit_piece, action: create, input: { slice: { field: self.id }, payment: { field: input.payment }, limit: { field: self.profitPayment } } }]
299
+ invoke: [
300
+ { instrument: principal_piece, action: create, input: { slice: { field: self.id }, payment: { field: input.payment }, limit: { field: self.principalPayment } } }
301
+ { instrument: profit_piece, action: create, input: { slice: { field: self.id }, payment: { field: input.payment }, limit: { field: self.profitPayment } } }
302
+ { 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: {} }
303
+ ]
145
304
  }
146
305
  action complete {
147
306
  from: [pending, due], to: paid
148
307
  requires self.principalReceivable.balance == "0"; requires self.profitReceivable.balance == "0"; requires self.profitEarned.balance == "0"
149
308
  }
150
- action restore { from: [pending, due, paid], to: due, actor: { parent: payment } }
309
+ action restore { from: [pending, due, paid], to: pending, actor: { parent: payment } }
151
310
  action payoff {
152
311
  from: [pending, due], to: paid, actor: { parent: parent }, input { identity: text }
153
312
  set: { payoffIdentity: { field: input.identity } }
@@ -167,16 +326,20 @@ instrument installments(months: integer(1, 366), profit: percent, disburse_to: e
167
326
  fields { slice: ref<slice>, dueAt: date = after(self.slice.dueAt, 0) }
168
327
  lifecycle { states: [pending, recognised], initial: pending }
169
328
  action create { actor: { parent: slice }, input { slice: ref<slice> }, requires unique "profit_recognition" on [self.slice] }
329
+ action reschedule { from: pending, to: pending, actor: { parent: slice } }
170
330
  action mature { from: pending, to: recognised, actor: clock, due: { at: self.dueAt }, moves self.slice.profitReceivable.balance from self.slice.profitReceivable to self.slice.profitEarned }
171
331
  }
172
332
  payment: {
333
+ revisioned: true
173
334
  fields { plan: ref<parent>, amount: money, paymentIdentity: text, held: account of self
174
335
  capital: account(capital, cash, "capital"), profitIncome: account(profit_to, cash, "profitIncome"), loss: account(capital, claim, "loss")
175
336
  outstandingPrincipal: money?, outstandingProfit: money?, outstandingFines: money?, outstandingCosts: money?, outstandingTotal: money?, principalReturned: money?, profitReturned: money?, lossRecorded: money? }
176
337
 
177
338
  lifecycle { states: [pending, paid, refunded], initial: pending }
178
339
  action create { input { plan: ref<parent>, amount: money, paymentIdentity: text } requires self.plan in [active]; requires unique "payment_identity" on [self.plan, self.paymentIdentity] }
340
+ // 366 slices * (1 + 3 fines * 3 + 3 costs * 3 + 2 pieces + 1 completion), plus recording.
179
341
  action pay {
342
+ expansionLimit: 8192
180
343
  from: pending, to: paid
181
344
  calculate: [{ target: outstandingPrincipal, op: aggregate, selection: { instrument: slice, reference: "plan", anchor: self.plan, states: [pending, due, paid], limit: 366, order: [position] }, measure: { sum: "principalReceivable.balance" } }]
182
345
  when profit_earned is on_payment { calculate: [{ target: outstandingProfit, op: aggregate, selection: { instrument: slice, reference: "plan", anchor: self.plan, states: [pending, due, paid], limit: 366, order: [position] }, measure: { sum: "profitReceivable.balance" } }] }
@@ -190,14 +353,13 @@ instrument installments(months: integer(1, 366), profit: percent, disburse_to: e
190
353
  requires self.plan in [active]; requires self.amount <= self.outstandingTotal
191
354
  moves self.amount from self.plan.borrower to self.held
192
355
  when apply is principal_profit {
193
- invoke: [{ selection: { instrument: slice, reference: "plan", anchor: self.plan, states: [pending, due], limit: 366, order: [position] }, action: collect, input: { payment: { field: self.id } } }]
356
+ invoke: [{ selection: { instrument: slice, reference: "plan", anchor: self.plan, states: [pending, due], limit: 366, order: [position] }, action: collect, guard: { kind: compare, left: { field: self.held.balance }, operator: ">", right: { literal: "0" } }, input: { payment: { field: self.id } } }]
194
357
  }
195
358
  when apply is fines_profit_principal {
196
- invoke: [{ selection: { instrument: all(financing.late_charge), reference: "plan", anchor: self.plan, states: [assessed, collected], limit: 366, order: [overdueAt] }, action: collect, input: { payment: { field: self.id } } }, { selection: { instrument: all(financing.late_charge.cost), reference: "plan", anchor: self.plan, states: [assessed, collected], limit: 366, order: [overdueAt] }, action: collect, input: { payment: { field: self.id } } }]
197
- invoke: [{ selection: { instrument: slice, reference: "plan", anchor: self.plan, states: [pending, due], limit: 366, order: [position] }, action: collect, input: { payment: { field: self.id } } }]
359
+ invoke: [{ selection: { instrument: slice, reference: "plan", anchor: self.plan, states: [pending, due], limit: 366, order: [position] }, action: collect_waterfall, guard: { kind: compare, left: { field: self.held.balance }, operator: ">", right: { literal: "0" } }, input: { payment: { field: self.id } } }]
198
360
  }
199
361
  when apply is pro_rata {
200
- invoke: [{ selection: { instrument: slice, reference: "plan", anchor: self.plan, states: [pending, due], limit: 366, order: [position] }, action: collect_pro_rata, input: { payment: { field: self.id } } }]
362
+ invoke: [{ selection: { instrument: slice, reference: "plan", anchor: self.plan, states: [pending, due], limit: 366, order: [position] }, action: collect_pro_rata, guard: { kind: compare, left: { field: self.held.balance }, operator: ">", right: { literal: "0" } }, input: { payment: { field: self.id } } }]
201
363
  }
202
364
  invoke: [{ reference: self.id, action: record, input: {} }]
203
365
  }
@@ -217,9 +379,10 @@ instrument installments(months: integer(1, 366), profit: percent, disburse_to: e
217
379
  }
218
380
  }
219
381
  principal_piece: {
220
- fields { slice: ref<slice>, payment: ref<payment>, limit: money, amount: money = minimum(self.payment.held.balance, self.slice.principalReceivable.balance, self.limit) }
382
+ fields { slice: ref<slice>, payment: ref<payment>, limit: money, amount: money? }
221
383
  lifecycle { states: [paid, refunded], initial: paid }
222
384
  action create {
385
+ calculate: [{ target: amount, op: minimum, values: [{ field: self.payment.held.balance }, { field: self.slice.principalReceivable.balance }, { field: self.limit }] }]
223
386
  actor: { parent: slice }
224
387
  input { slice: ref<slice>, payment: ref<payment>, limit: money }
225
388
  requires unique "principal_receipt" on [self.slice, self.payment]
@@ -231,9 +394,10 @@ instrument installments(months: integer(1, 366), profit: percent, disburse_to: e
231
394
  }
232
395
  }
233
396
  profit_piece: {
234
- fields { slice: ref<slice>, payment: ref<payment>, limit: money, amount: money = minimum(self.payment.held.balance, self.limit) }
397
+ fields { slice: ref<slice>, payment: ref<payment>, limit: money, amount: money? }
235
398
  lifecycle { states: [paid, refunded], initial: paid }
236
399
  action create {
400
+ calculate: [{ target: amount, op: minimum, values: [{ field: self.payment.held.balance }, { field: self.limit }] }]
237
401
  actor: { parent: slice }
238
402
  input { slice: ref<slice>, payment: ref<payment>, limit: money }
239
403
  requires unique "profit_receipt" on [self.slice, self.payment]
@@ -254,13 +418,14 @@ instrument installments(months: integer(1, 366), profit: percent, disburse_to: e
254
418
  }
255
419
 
256
420
  instrument late_charge(on: ref<financing.installments>[], grace: duration = 3d, fine: money = 50 SAR, cap: money = 25 SAR, fines_to: party = programOperator, costs_to: party = programOperator, approval: approval = programOperator, borrower: party = party(person)) {
421
+ revisioned: true
257
422
  summary: "Separate approved fine and evidenced recovery claims on an overdue installment."
258
423
  fields {
259
- plan: ref<on>, slice: ref<on.slice>, borrower: account of borrower, fine: money = fine, overdueAt: date = after(self.slice.dueAt, grace)
424
+ plan: ref<on>, slice: ref<on.slice>, borrower: account of borrower, fine: money = fine, overdueAt: date?
260
425
  receivable: account(self, claim), debt: account(borrower, claim, contra, "debt")
261
426
  }
262
427
  lifecycle { states: [proposed, assessed, collected, waived, refunded], initial: proposed }
263
- action create { requires unique "late_assessment" on [self.slice]; requires self.borrower == self.slice.plan.borrower; requires self.plan == self.slice.plan }
428
+ action create { calculate: [{ target: overdueAt, op: shift, date: { field: self.slice.dueAt }, milliseconds: { literal: grace }, direction: after }], requires unique "late_assessment" on [self.slice, self.overdueAt]; requires self.borrower == self.slice.plan.borrower; requires self.plan == self.slice.plan }
264
429
  action assess {
265
430
  from: proposed, to: assessed, due: { at: self.overdueAt }
266
431
  requires self.slice.principalReceivable.balance > "0"; requires approval by approval
@@ -272,9 +437,20 @@ instrument late_charge(on: ref<financing.installments>[], grace: duration = 3d,
272
437
  requires input.payment.plan == self.plan
273
438
  invoke: [{ instrument: receipt, action: create, input: { assessment: { field: self.id }, payment: { field: input.payment } } }, { selection: { instrument: current(), reference: "plan", anchor: self.plan, states: [assessed], limit: 1, where: { id: { field: self.id }, receivable.balance: { literal: "0" } } }, action: finish, input: {} }]
274
439
  }
440
+ action payoff {
441
+ from: [assessed, collected], to: collected
442
+ requires self.plan in [paid]
443
+ moves self.receivable.balance from self.plan.borrower to fines_to
444
+ moves self.receivable.balance from self.receivable to self.debt
445
+ }
275
446
  action finish { from: assessed, to: collected, actor: { parent: current() }, requires self.receivable.balance == "0" }
276
447
  action cancel { from: proposed, to: waived }
277
- action waive { from: assessed, to: waived, moves self.receivable.balance from self.receivable to self.debt }
448
+ action waive_for_amendment {
449
+ from: [assessed, collected], to: assessed, actor: { parent: child(on, amendment) }, input { amendment: ref<on.amendment> }
450
+ requires input.amendment.plan == self.plan; requires input.amendment in [applied]
451
+ invoke: [{ instrument: waiver, action: create, input: { assessment: { field: self.id }, amendment: { field: input.amendment } } }]
452
+ }
453
+ action waive { from: [assessed, collected], to: waived, input { reason: text, evidence: text }, requires approval by approval, moves self.receivable.balance from self.receivable to self.debt }
278
454
  action refund {
279
455
  from: [assessed, collected], to: assessed, input { payment: ref<on.payment> }
280
456
  requires input.payment in [paid, refunded]
@@ -282,20 +458,31 @@ instrument late_charge(on: ref<financing.installments>[], grace: duration = 3d,
282
458
  invoke: [{ selection: { instrument: receipt, reference: "payment", anchor: input.payment, states: [paid], limit: 1, where: { assessment: { field: self.id } } }, action: refund, input: {} }, { selection: { instrument: current(), reference: "plan", anchor: self.plan, states: [assessed], limit: 1, where: { id: { field: self.id }, receivable.balance: { literal: "0" } } }, action: finish, input: {} }]
283
459
  }
284
460
  records {
461
+ waiver: {
462
+ fields { assessment: ref<parent>, amendment: ref<on.amendment>, amount: money? }
463
+ lifecycle { states: [recorded], initial: recorded }
464
+ action create {
465
+ actor: { parent: parent }, input { assessment: ref<parent>, amendment: ref<on.amendment> }
466
+ calculate: [{ target: amount, op: rate, base: { field: self.assessment.receivable.balance }, bps: { field: self.amendment.waiverPercent }, rounding: floor }]
467
+ requires unique "amendment_waiver" on [self.assessment, self.amendment]
468
+ moves self.amount from self.assessment.receivable to self.assessment.debt
469
+ }
470
+ }
285
471
  receipt: {
286
- fields { assessment: ref<parent>, payment: ref<on.payment>, amount: money = minimum(self.payment.held.balance, self.assessment.receivable.balance) }
472
+ fields { assessment: ref<parent>, payment: ref<on.payment>, amount: money? }
287
473
  lifecycle { states: [paid, refunded], initial: paid }
288
- action create { actor: { parent: parent }, input { assessment: ref<parent>, payment: ref<on.payment> }, requires input.payment.plan == self.assessment.plan; requires unique "fine_receipt" on [self.assessment, self.payment]; moves self.amount from self.payment.held to fines_to; moves self.amount from self.assessment.receivable to self.assessment.debt }
474
+ action create { calculate: [{ target: amount, op: minimum, values: [{ field: self.payment.held.balance }, { field: self.assessment.receivable.balance }] }], actor: { parent: parent }, input { assessment: ref<parent>, payment: ref<on.payment> }, requires input.payment.plan == self.assessment.plan; requires unique "fine_receipt" on [self.assessment, self.payment]; moves self.amount from self.payment.held to fines_to; moves self.amount from self.assessment.receivable to self.assessment.debt }
289
475
  action refund { from: paid, to: refunded, actor: { parent: parent }, moves self.amount from fines_to to self.payment.plan.borrower; moves self.amount from self.assessment.debt to self.assessment.receivable }
290
476
  }
291
477
 
292
478
  cost: {
479
+ revisioned: true
293
480
  fields {
294
- plan: ref<on>, assessment: ref<parent>, overdueAt: date = after(self.assessment.slice.dueAt, grace), evidence: text, evidencedCost: money, amount: money = minimum(self.evidencedCost, cap)
481
+ plan: ref<on>, assessment: ref<parent>, overdueAt: date?, evidence: text, evidencedCost: money, amount: money = minimum(self.evidencedCost, cap)
295
482
  receivable: account(self, claim), debt: account(borrower, claim, contra, "debt")
296
483
  }
297
484
  lifecycle { states: [proposed, assessed, collected, waived, refunded], initial: proposed }
298
- action create { requires unique "recovery_assessment" on [self.assessment]; requires self.plan == self.assessment.plan }
485
+ action create { calculate: [{ target: overdueAt, op: shift, date: { field: self.assessment.overdueAt }, milliseconds: { literal: 0 }, direction: after }], requires unique "recovery_assessment" on [self.assessment]; requires self.plan == self.assessment.plan }
299
486
  action assess {
300
487
  from: proposed, to: assessed, due: { at: self.assessment.overdueAt }
301
488
  requires self.assessment.slice.principalReceivable.balance > "0"; requires approval by approval
@@ -307,9 +494,20 @@ instrument late_charge(on: ref<financing.installments>[], grace: duration = 3d,
307
494
  requires input.payment.plan == self.plan
308
495
  invoke: [{ instrument: receipt, action: create, input: { assessment: { field: self.id }, payment: { field: input.payment } } }, { selection: { instrument: current(), reference: "plan", anchor: self.plan, states: [assessed], limit: 1, where: { id: { field: self.id }, receivable.balance: { literal: "0" } } }, action: finish, input: {} }]
309
496
  }
497
+ action payoff {
498
+ from: [assessed, collected], to: collected
499
+ requires self.plan in [paid]
500
+ moves self.receivable.balance from self.plan.borrower to costs_to
501
+ moves self.receivable.balance from self.receivable to self.debt
502
+ }
310
503
  action finish { from: assessed, to: collected, actor: { parent: current() }, requires self.receivable.balance == "0" }
311
504
  action cancel { from: proposed, to: waived }
312
- action waive { from: assessed, to: waived, moves self.receivable.balance from self.receivable to self.debt }
505
+ action waive_for_amendment {
506
+ from: [assessed, collected], to: assessed, actor: { parent: child(on, amendment) }, input { amendment: ref<on.amendment> }
507
+ requires input.amendment.plan == self.plan; requires input.amendment in [applied]
508
+ invoke: [{ instrument: waiver, action: create, input: { assessment: { field: self.id }, amendment: { field: input.amendment } } }]
509
+ }
510
+ action waive { from: [assessed, collected], to: waived, input { reason: text, evidence: text }, requires approval by approval, moves self.receivable.balance from self.receivable to self.debt }
313
511
  action refund {
314
512
  from: [assessed, collected], to: assessed, input { payment: ref<on.payment> }
315
513
  requires input.payment in [paid, refunded]
@@ -317,10 +515,20 @@ instrument late_charge(on: ref<financing.installments>[], grace: duration = 3d,
317
515
  invoke: [{ selection: { instrument: receipt, reference: "payment", anchor: input.payment, states: [paid], limit: 1, where: { assessment: { field: self.id } } }, action: refund, input: {} }, { selection: { instrument: current(), reference: "plan", anchor: self.plan, states: [assessed], limit: 1, where: { id: { field: self.id }, receivable.balance: { literal: "0" } } }, action: finish, input: {} }]
318
516
  }
319
517
  records {
518
+ waiver: {
519
+ fields { assessment: ref<parent>, amendment: ref<on.amendment>, amount: money? }
520
+ lifecycle { states: [recorded], initial: recorded }
521
+ action create {
522
+ actor: { parent: parent }, input { assessment: ref<parent>, amendment: ref<on.amendment> }
523
+ calculate: [{ target: amount, op: rate, base: { field: self.assessment.receivable.balance }, bps: { field: self.amendment.waiverPercent }, rounding: floor }]
524
+ requires unique "amendment_waiver" on [self.assessment, self.amendment]
525
+ moves self.amount from self.assessment.receivable to self.assessment.debt
526
+ }
527
+ }
320
528
  receipt: {
321
- fields { assessment: ref<parent>, payment: ref<on.payment>, amount: money = minimum(self.payment.held.balance, self.assessment.receivable.balance) }
529
+ fields { assessment: ref<parent>, payment: ref<on.payment>, amount: money? }
322
530
  lifecycle { states: [paid, refunded], initial: paid }
323
- action create { actor: { parent: parent }, input { assessment: ref<parent>, payment: ref<on.payment> }, requires input.payment.plan == self.assessment.plan; requires unique "cost_receipt" on [self.assessment, self.payment]; moves self.amount from self.payment.held to costs_to; moves self.amount from self.assessment.receivable to self.assessment.debt }
531
+ action create { calculate: [{ target: amount, op: minimum, values: [{ field: self.payment.held.balance }, { field: self.assessment.receivable.balance }] }], actor: { parent: parent }, input { assessment: ref<parent>, payment: ref<on.payment> }, requires input.payment.plan == self.assessment.plan; requires unique "cost_receipt" on [self.assessment, self.payment]; moves self.amount from self.payment.held to costs_to; moves self.amount from self.assessment.receivable to self.assessment.debt }
324
532
  action refund { from: paid, to: refunded, actor: { parent: parent }, moves self.amount from costs_to to self.payment.plan.borrower; moves self.amount from self.assessment.debt to self.assessment.receivable }
325
533
  }
326
534
  }