@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.
- package/CHANGELOG.md +8 -0
- package/dist/src/compile.d.ts.map +1 -1
- package/dist/src/compile.js +43 -7
- package/dist/src/compile.js.map +1 -1
- package/dist/src/cost.d.ts +1 -1
- package/dist/src/cost.d.ts.map +1 -1
- package/dist/src/cost.js +52 -9
- package/dist/src/cost.js.map +1 -1
- package/dist/src/headers.d.ts +12 -2
- package/dist/src/headers.d.ts.map +1 -1
- package/dist/src/headers.js +27 -0
- package/dist/src/headers.js.map +1 -1
- package/dist/src/parse.js +4 -2
- package/dist/src/parse.js.map +1 -1
- package/dist/src/std-bundle.d.ts.map +1 -1
- package/dist/src/std-bundle.js +7 -5
- package/dist/src/std-bundle.js.map +1 -1
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/docs/README.md +6 -0
- package/docs/headers.md +43 -39
- package/examples/cost-table.json +36 -4
- package/examples/library.hsx +9 -2
- package/package.json +3 -3
- package/src/compile.ts +56 -10
- package/src/cost.ts +60 -16
- package/src/headers.ts +30 -0
- package/src/parse.ts +3 -2
- package/src/std-bundle.ts +7 -5
- package/src/version.ts +1 -1
- package/std/collections.hsx +45 -4
- package/std/escrow.hsx +10 -3
- package/std/financing.hsx +240 -32
- package/std/marketplace.hsx +94 -7
- package/std/money.hsx +1 -1
- package/std/reporting.hsx +1286 -0
- package/std/vehicles.hsx +34 -0
package/std/collections.hsx
CHANGED
|
@@ -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
|
-
|
|
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 {
|
|
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
|
-
|
|
33
|
-
|
|
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
|
-
|
|
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:
|
|
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: [
|
|
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:
|
|
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: [
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
}
|