@hyperscale0/hsx 1.0.125 → 1.0.130

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +2 -2
  2. package/dist/src/cli.js +1 -1
  3. package/dist/src/cli.js.map +1 -1
  4. package/dist/src/compile.d.ts.map +1 -1
  5. package/dist/src/compile.js +155 -21
  6. package/dist/src/compile.js.map +1 -1
  7. package/dist/src/examples-bundle.js +9 -9
  8. package/dist/src/examples-bundle.js.map +1 -1
  9. package/dist/src/headers.d.ts +1 -1
  10. package/dist/src/headers.d.ts.map +1 -1
  11. package/dist/src/headers.js +1 -0
  12. package/dist/src/headers.js.map +1 -1
  13. package/dist/src/parse.js +4 -1
  14. package/dist/src/parse.js.map +1 -1
  15. package/dist/src/std-bundle.d.ts.map +1 -1
  16. package/dist/src/std-bundle.js +8 -7
  17. package/dist/src/std-bundle.js.map +1 -1
  18. package/docs/README.md +102 -50
  19. package/examples/cards.hsx +4 -0
  20. package/examples/collections.hsx +5 -1
  21. package/examples/cost-table.json +80 -56
  22. package/examples/financing.hsx +1 -1
  23. package/examples/insurance.hsx +5 -4
  24. package/examples/lending.hsx +1 -1
  25. package/examples/library.hsx +1 -1
  26. package/examples/rental-deposit.hsx +1 -1
  27. package/examples/reporting.hsx +1 -1
  28. package/examples/serviced.hsx +1 -1
  29. package/package.json +4 -4
  30. package/src/cli.ts +1 -1
  31. package/src/compile.ts +223 -27
  32. package/src/examples-bundle.ts +9 -9
  33. package/src/headers.ts +1 -0
  34. package/src/parse.ts +4 -1
  35. package/src/std-bundle.ts +8 -7
  36. package/std/booking.hsx +75 -0
  37. package/std/cards.hsx +5 -3
  38. package/std/escrow.hsx +35 -9
  39. package/std/financing.hsx +158 -36
  40. package/std/insurance.hsx +9 -5
  41. package/std/marketplace.hsx +3 -3
  42. package/std/money.hsx +3 -3
  43. package/std/wallet.hsx +20 -9
@@ -0,0 +1,75 @@
1
+ header booking
2
+
3
+ // Economics: Deposit and balance payments and every refund are principal. Completion
4
+ // and any forfeit are earnings for the company, or pass the customer's cash through
5
+ // to a bound outside operator.
6
+
7
+ // Expose create, pay_deposit and pay_balance for the booking path, and
8
+ // cancel_early, cancel_middle and cancel_late for the customer. Create captures
9
+ // subject departure, price, deposit and balance, and refuses a deposit and balance
10
+ // that do not add up to the price. The balance is payable only after the deposit
11
+ // and before balance_due ahead of departure, so a cancelled booking never offers
12
+ // it. cancel_early runs until early_until before departure and returns everything
13
+ // held. cancel_middle runs from then until late_from; cancel_late runs from then
14
+ // until departure, even when the clock's complete runs late. They keep
15
+ // middle_forfeit or late_forfeit of the held money and return the rest. Both
16
+ // forfeits default to 0%, so nothing is kept unless the author sets them. At
17
+ // departure the clock completes a paid booking and pays operator, and lapses an
18
+ // unpaid one on cancel_late's terms, so nothing stays held after departure.
19
+
20
+ // Cost: create declares one owned account. Each payment declares one transfer;
21
+ // cancellation and completion move the held balance. Run hsx cost for the
22
+ // composed counts.
23
+
24
+ instrument reservation(customer: party = party(person), operator: party = programOperator, early_until: duration = 30d, late_from: duration = 15d, balance_due: duration = 15d, middle_forfeit: percent = 0%, late_forfeit: percent = 0%) {
25
+ revisioned: true
26
+ summary: "Take a deposit and a balance for a dated booking, with cancellation windows before departure."
27
+ fields {
28
+ customer: account of customer, operator: account of operator, held: account of self
29
+ departure: date, price: money = subject.price
30
+ deposit: money = subject.deposit, balance: money = subject.balance
31
+ split: money = sum(self.deposit, self.balance)
32
+ balanceAt: date = before(self.departure, balance_due)
33
+ earlyAt: date = before(self.departure, early_until)
34
+ lateAt: date = before(self.departure, late_from)
35
+ refund: money?, forfeit: money?
36
+ }
37
+ lifecycle { states: [reserved, deposit_paid, paid, completed, cancelled], initial: reserved }
38
+ action create {
39
+ subject { departure: date, price: money, deposit: money, balance: money }
40
+ actor: { party: customer }
41
+ requires self.departure > self.now
42
+ requires self.split == self.price
43
+ }
44
+ action pay_deposit {
45
+ from: reserved, to: deposit_paid, actor: { party: customer }
46
+ moves self.deposit from customer to self.held economics { purpose: principal, sourceParty: customer }
47
+ }
48
+ action pay_balance {
49
+ from: deposit_paid, to: paid, actor: { party: customer }, deadline: { at: self.balanceAt }
50
+ moves self.balance from customer to self.held economics { purpose: principal, sourceParty: customer }
51
+ }
52
+ action complete {
53
+ from: paid, to: completed, actor: clock, due: { at: self.departure }
54
+ moves self.held.balance from self.held to operator economics { recipient: operator, company: { purpose: earning, sourceParty: customer }, participant: { purpose: pass_through, sourceParty: programOperator } }
55
+ }
56
+ action cancel_early { allowZero: true,
57
+ from: [reserved, deposit_paid, paid], to: cancelled, actor: { party: customer }, deadline: { at: self.earlyAt }
58
+ moves self.held.balance from self.held to customer economics { purpose: principal, sourceParty: programOperator }
59
+ }
60
+ action cancel_middle { allowZero: true,
61
+ from: [reserved, deposit_paid, paid], to: cancelled, actor: { party: customer }, due: { at: self.earlyAt }, deadline: { at: self.lateAt }
62
+ calculate: [{ target: forfeit, op: rate, base: { field: self.held.balance }, bps: { literal: middle_forfeit }, rounding: floor }, { target: refund, op: subtract, base: { field: self.held.balance }, subtract: [{ field: self.forfeit }] }]
63
+ moves self.refund from self.held to customer economics { purpose: principal, sourceParty: programOperator }; moves self.forfeit from self.held to operator economics { recipient: operator, company: { purpose: earning, sourceParty: customer }, participant: { purpose: pass_through, sourceParty: programOperator } }
64
+ }
65
+ action cancel_late { allowZero: true,
66
+ from: [reserved, deposit_paid, paid], to: cancelled, actor: { party: customer }, due: { at: self.lateAt }, deadline: { at: self.departure }
67
+ calculate: [{ target: forfeit, op: rate, base: { field: self.held.balance }, bps: { literal: late_forfeit }, rounding: floor }, { target: refund, op: subtract, base: { field: self.held.balance }, subtract: [{ field: self.forfeit }] }]
68
+ moves self.refund from self.held to customer economics { purpose: principal, sourceParty: programOperator }; moves self.forfeit from self.held to operator economics { recipient: operator, company: { purpose: earning, sourceParty: customer }, participant: { purpose: pass_through, sourceParty: programOperator } }
69
+ }
70
+ action lapse { allowZero: true,
71
+ from: [reserved, deposit_paid], to: cancelled, actor: clock, due: { at: self.departure }
72
+ calculate: [{ target: forfeit, op: rate, base: { field: self.held.balance }, bps: { literal: late_forfeit }, rounding: floor }, { target: refund, op: subtract, base: { field: self.held.balance }, subtract: [{ field: self.forfeit }] }]
73
+ moves self.refund from self.held to customer economics { purpose: principal, sourceParty: programOperator }; moves self.forfeit from self.held to operator economics { recipient: operator, company: { purpose: earning, sourceParty: customer }, participant: { purpose: pass_through, sourceParty: programOperator } }
74
+ }
75
+ }
package/std/cards.hsx CHANGED
@@ -1,6 +1,8 @@
1
1
  header cards
2
2
 
3
3
  // Activate holder and card before creating and approving an authorization.
4
+ // Only the Product operator activates, suspends or resumes a cardholder; the
5
+ // holder freezes, unfreezes and cancels their own card.
4
6
  // Expose authorization.create, approve and capture; expose decline and reverse
5
7
  // separately if callers need those paths. Issuer declines only from pending;
6
8
  // merchant captures or reverses an approved reservation. Expiry voids approved
@@ -21,9 +23,9 @@ instrument cardholder(holder: party) {
21
23
  fields { holder: account of holder }
22
24
  lifecycle { states: [pending, active, suspended, closed], initial: pending }
23
25
  action create {}
24
- action activate { from: pending, to: active }
25
- action suspend { from: active, to: suspended }
26
- action resume { from: suspended, to: active }
26
+ action activate { from: pending, to: active, actor: { party: programOperator } }
27
+ action suspend { from: active, to: suspended, actor: { party: programOperator } }
28
+ action resume { from: suspended, to: active, actor: { party: programOperator } }
27
29
  action close { from: [active, suspended], to: closed }
28
30
  }
29
31
 
package/std/escrow.hsx CHANGED
@@ -1,13 +1,28 @@
1
1
  header escrow
2
2
 
3
3
  // Economics: Funding and unspent returns are principal. Acceptance realizes earnings for
4
- // the company or a payout to a bound outside seller; tax is pass-through.
4
+ // the company; the payer's cash passes through to a bound outside seller, less the
5
+ // fee the company earns. Tax is pass-through.
5
6
 
6
7
  // Expose create, fund, deliver and accept for the purchase path. Create captures
7
8
  // subject.price; acceptance pays payee less the configured fee, not payer.
8
9
  // Expose dispute, verify_return and refund separately for the return path.
9
- // Payer disputes delivered goods; payee verifies return evidence from disputed;
10
- // payee refunds the entire held price from return_verified by default.
10
+ // Payer disputes delivered goods; payee verifies return evidence from disputed.
11
+ // Payee refunds the whole held balance before delivery, or from return_verified
12
+ // by default after a dispute. While a financing plan funds the hold in any state
13
+ // but cancelled, both refunds refuse, so a written-off plan never hands lender
14
+ // principal to the payer. Only unwind returns the principal to capital and
15
+ // cancels the plan; after that, refund returns the rest. Before delivery the
16
+ // Product operator runs the plan's unwind_undelivered instead, which invokes
17
+ // unwind_financed here to return the rest to the payer and close the hold.
18
+ // After a paid plan's refund window, the plan's return_hold runs it from funded
19
+ // or return_verified to return the whole balance to the payer.
20
+ // Only an installments plan can run confirm and unwind_financed; without
21
+ // financing neither action exists, so no hold reaches funded without fundedAt. The plan's confirm leaves fundedAt unset, so
22
+ // refund_undelivered never comes due on a financed hold and no schedule offers it.
23
+ // Expose refund_undelivered to let the payer take the whole held price back once
24
+ // deliver_within has passed since funding without delivery. It is a payer action
25
+ // that becomes available at that time; nothing runs it automatically.
11
26
  // Acceptance expiry enters disputed, never released. Neither verify_return nor
12
27
  // refund becomes public merely because dispute is exposed. Clock actions remain
13
28
  // internal. Expose cancel for a pending hold, including a checkout-funded hold;
@@ -18,12 +33,12 @@ header escrow
18
33
  // transfer; accept expands the configured fee and tax into moves. Run hsx cost
19
34
  // on the composed program for instruction counts. The executor supplies prices.
20
35
 
21
- instrument hold(payer: party = party(person), payee: party = programOperator, accept_within: duration = 48h, fee: fee = { seller: 0%, tax: 0% }, dispute: policy = { refund_after: return_verified }) {
36
+ instrument hold(payer: party = party(person), payee: party = programOperator, accept_within: duration = 48h, deliver_within: duration = 14d, fee: fee = { seller: 0%, tax: 0% }, dispute: policy = { refund_after: return_verified }) {
22
37
  revisioned: true
23
38
  summary: "Hold the subject price through delivery, acceptance and verified return."
24
39
  fields {
25
40
  payer: account of payer, payee: account of payee
26
- held: account of self, deliveredAt: date?
41
+ held: account of self, fundedAt: date?, deliveredAt: date?
27
42
  price: money = subject.price
28
43
  }
29
44
  lifecycle { states: [pending, funded, delivered, disputed, return_verified, released, refunded, cancelled], initial: pending }
@@ -33,11 +48,11 @@ instrument hold(payer: party = party(person), payee: party = programOperator, ac
33
48
  }
34
49
  action fund {
35
50
  from: pending, to: funded, actor: { party: payer }
36
- subject { price: money }
51
+ set: { fundedAt: { field: self.now } }
37
52
  moves self.price from payer to self.held economics { purpose: principal, sourceParty: payer }
38
53
  }
39
54
  action confirm {
40
- from: pending, to: funded
55
+ from: pending, to: funded, actor: { parent: all(financing.installments) }
41
56
  requires self.held.balance == self.price
42
57
  }
43
58
  action deliver {
@@ -48,7 +63,7 @@ instrument hold(payer: party = party(person), payee: party = programOperator, ac
48
63
  from: delivered, to: released, actor: { party: payer }
49
64
  input { acceptanceReference: text }
50
65
  deadline: { at: self.deliveredAt, offset: accept_within }
51
- moves self.price from self.held to payee fee fee economics { payee: { recipient: payee, company: { purpose: earning, sourceParty: payer }, participant: { purpose: participant_payout, sourceParty: programOperator } }, fee: { purpose: earning, sourceParty: payer }, tax: { purpose: pass_through, sourceParty: programOperator } }
66
+ moves self.price from self.held to payee fee fee economics { payee: { recipient: payee, company: { purpose: earning, sourceParty: payer }, participant: { purpose: pass_through, sourceParty: programOperator } }, fee: { purpose: earning, sourceParty: payer }, tax: { purpose: pass_through, sourceParty: programOperator } }
52
67
  }
53
68
  action expire_acceptance {
54
69
  from: delivered, to: disputed, actor: clock
@@ -63,9 +78,20 @@ instrument hold(payer: party = party(person), payee: party = programOperator, ac
63
78
  input { returnEvidence: text }
64
79
  }
65
80
  action refund {
66
- from: dispute.refund_after, to: refunded, actor: { party: payee }
81
+ from: [funded, dispute.refund_after], to: refunded, actor: { party: payee }
82
+ requires count of { instrument: all(financing.installments), reference: "funds", anchor: self.id, states: [quoted, signed, active, paid, written_off], limit: 1 } == 0
83
+ moves self.held.balance from self.held to payer economics { purpose: principal, sourceParty: programOperator }
84
+ }
85
+ action refund_undelivered {
86
+ from: funded, to: refunded, actor: { party: payer }
87
+ due: { at: self.fundedAt, offset: deliver_within }
88
+ requires count of { instrument: all(financing.installments), reference: "funds", anchor: self.id, states: [quoted, signed, active, paid, written_off], limit: 1 } == 0
67
89
  moves self.price from self.held to payer economics { purpose: principal, sourceParty: programOperator }
68
90
  }
91
+ action unwind_financed { allowZero: true,
92
+ from: [funded, return_verified], to: refunded, actor: { parent: all(financing.installments) }
93
+ moves self.held.balance from self.held to payer economics { purpose: principal, sourceParty: programOperator }
94
+ }
69
95
  action cancel { allowZero: true,
70
96
  from: pending, to: cancelled, actor: { party: payer }
71
97
  moves self.held.balance from self.held to payer economics { purpose: principal, sourceParty: programOperator }
package/std/financing.hsx CHANGED
@@ -2,9 +2,14 @@ header financing
2
2
 
3
3
  // Economics: Principal advances and repayments are excluded from earnings. Claims, waivers,
4
4
  // write-offs and allocations are internal. Paid profit and company charges are
5
- // earnings; receipt refunds reverse captured cash. Outside recovery recipients
6
- // receive payouts from held company cash, or pass-through direct customer cash.
5
+ // earnings; receipt refunds reverse captured cash. Borrower cash paid to outside
6
+ // capital or recovery recipients passes through, whether held first or paid direct.
7
7
 
8
+ // Pricing is required. amortizing reads profit_rate as an annual rate on the
9
+ // declining balance: equal monthly instalments, the last absorbing rounding.
10
+ // flat_total reads it as one charge on principal for the whole term (murabaha,
11
+ // fixed-fee pay-later). 12,000 at 18% over 12 months costs 13,201.95 amortizing
12
+ // and 14,160 flat_total.
8
13
  // Expose create and sign on installments, then collect_down_payment as checkout
9
14
  // for disburse_to: funds. Checkout requires a signed plan and pending escrow;
10
15
  // it tops up only the missing down payment, without advancing lender principal.
@@ -12,13 +17,44 @@ header financing
12
17
  // the fully funded hold. Existing escrow credit counts once toward the down payment.
13
18
  // Disburse first credits an active marketplace.reservation on the same hold,
14
19
  // so list the plan in its converters; the deposit then counts as escrow credit.
15
- // Borrower-directed disbursement excludes collect_down_payment. A down_payment
20
+ // disburse_to: seller pays principal to the seller party at activation (pay later
21
+ // with the merchant paid on day one). For a return, capital refunds each paid
22
+ // payment to the borrower. Payments stay refundable while the plan is active. The
23
+ // payment that clears the last slice completes the plan when no late charge or
24
+ // recovery cost is outstanding; otherwise expose complete to close it once they
25
+ // are settled or waived. When
26
+ // the plan leaves active (paid, written off or cancelled) it sets refundUntil to
27
+ // refund_within (default 30d) later, and payment, late charge and recovery cost
28
+ // refunds stop at that time. A refund that reopens the plan clears refundUntil, which is set
29
+ // from the calculated refundWindow so a restore can clear it. Once it passes,
30
+ // nothing can move the plan's payments or late charges, so they stop counting as
31
+ // open records. After the refunds, capital runs unwind: it takes principal back to
32
+ // capital and cancels open slices with their claims. Seller mode debits the seller; funds mode
33
+ // needs the hold in return_verified and debits the hold, after which the payee's
34
+ // escrow refund returns only the down payment. Before delivery, the Product
35
+ // operator runs unwind_undelivered on a funds-mode plan whose hold is still
36
+ // funded: principal goes back to capital and the hold's unwind_financed returns
37
+ // the down payment to the payer and closes the hold as refunded, all in one
38
+ // operation, so deliver is refused afterwards. It also cancels proposed and
39
+ // assessed late charges and recovery costs: goods that never arrived earn no
40
+ // fines. Escrow refund refuses while any plan that is
41
+ // not cancelled funds the hold, written off included, so the borrower never
42
+ // receives lender principal. A funds-mode plan writes off only once its hold
43
+ // has released principal to the seller: principal still held is not a loss, and
44
+ // unwind or unwind_undelivered returns it. Both unwinds need no paid payments and no
45
+ // collected late charges; refund them first. unwind also needs no assessed
46
+ // late charges; waive them first. Once a paid plan's refund window has passed,
47
+ // its payments can no longer be refunded, so the Product operator runs
48
+ // return_hold to return an undelivered or returned hold's whole balance to the
49
+ // borrower, who has repaid the principal.
50
+ // Borrower- and seller-directed disbursement exclude collect_down_payment. A down_payment
16
51
  // there reduces principal but does not collect cash. Price is subject metadata,
17
52
  // not the borrower limit. The escrow payer and price must match the plan.
18
53
  // Expose escrow cancel to return checkout cash if financing is abandoned; plan
19
54
  // void alone cancels the agreement and does not refund its separate escrow.
20
55
  // Approve borrower and portfolio limits before disbursement. Create builds the
21
- // dated slice records; payments need a separate payment record and pay action.
56
+ // dated slice records from future, strictly increasing dates; disbursement
57
+ // refuses once a slice is due. Payments need a separate payment record and pay action.
22
58
  // Expose parent payoff separately and create a current, unexpired payoff_quote.
23
59
  // Expose late_charge.create as propose_assessment before the clock can assess;
24
60
  // provide matching plan, slice and borrower. Fine is fixed money, not a percentage.
@@ -26,19 +62,25 @@ header financing
26
62
  // payment does not settle the remaining fine. Recovery cost needs its own record.
27
63
  // Profit share credits capital's allocatedProfitPayable, not an outside agency.
28
64
  // Credit lines need adapter binding at activation; draws are ledger advances,
29
- // not evidence of outside funding. Caller-authorized limit approvals and advances
30
- // need the Product's operation authority. No clock declaration proves retries.
65
+ // not evidence of outside funding. Only the Product operator approves borrower
66
+ // and portfolio limits; caller-authorized advances need the Product's operation
67
+ // authority. No clock declaration proves retries.
31
68
 
32
69
  // Cost: installments include cash and claim moves, child-account creation and
33
70
  // bounded invocations across slices and assessments. Month and charge limits affect
34
71
  // the bound. Run hsx cost on the selected policies; profit is not a platform tariff.
35
72
 
36
- 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<escrow.hold>?, borrower: party = party(person), capital: party = programOperator, share: percent = 0%, limits: ref<financing.limits> = object(financing.limits), portfolio: ref<financing.portfolio_limit> = object(financing.portfolio_limit)) {
73
+ instrument installments(months: integer(1, 366), pricing: enum(amortizing, flat_total), profit_rate: percent, charge_limit: integer(1, 3) = 3, disburse_to: enum(funds, borrower, seller) = funds, seller: party?, profit_earned: enum(on_payment, by_schedule, at_disbursement) = on_payment, apply: enum(fines_profit_principal, principal_profit, pro_rata) = fines_profit_principal, payoff_rebate: percent = 100%, write_off_after: duration = 90d, max_extension: duration = 90d, max_amendments: integer(0, 366) = 2, waiver_limit: percent = 100%, amendment_expiry: duration = 7d, payoff_quote_expiry: duration = 1h, refund_within: duration = 30d, allow_overdue: enum(allowed, blocked) = allowed, assessed_fines: enum(carry, require_waiver) = carry, down_payment: percent = 0%, funds: ref<escrow.hold>?, borrower: party = party(person), capital: party = programOperator, share: percent = 0%, limits: ref<financing.limits> = object(financing.limits), portfolio: ref<financing.portfolio_limit> = object(financing.portfolio_limit)) {
37
74
  dependencies {
38
75
  funds {
39
76
  selector: disburse_to, is: funds
40
77
  message: "Attachment `{attachment}` disburses into a hold, but `funds` is not bound."
41
- fix: "Bind `funds: sale` to an escrow attachment. Choose `disburse_to: borrower` only if the borrower should receive the money directly."
78
+ fix: "Bind `funds: sale` to an escrow attachment. Choose `disburse_to: seller` to pay a seller on activation, or `disburse_to: borrower` only if the borrower should receive the money directly."
79
+ }
80
+ seller {
81
+ selector: disburse_to, is: seller
82
+ message: "Attachment `{attachment}` pays a seller, but `seller` is not bound."
83
+ fix: "Bind `seller: merchant` to the declared business that receives the principal."
42
84
  }
43
85
  }
44
86
  revisioned: true
@@ -48,12 +90,12 @@ instrument installments(months: integer(1, 366), charge_limit: integer(1, 3) = 3
48
90
  funds: ref<funds>?, borrower: account of borrower, settlementIdentity: text, borrowerLimit: ref<limits>, portfolioLimit: ref<portfolio>
49
91
  months: integer = months, dates: list(date, months)
50
92
  allocatedPayable: account(capital, cash, "allocatedProfitPayable"), capital: account(capital, cash, "capital"), profitIncome: account(capital, cash, "profitIncome"), loss: account(capital, claim, "loss")
51
- downPaymentRemaining: money?, downPaymentCredit: money?, principalReturned: money?, profitReturned: money?, lossRecorded: money?
93
+ downPaymentRemaining: money?, downPaymentCredit: money?, principalReturned: money?, profitReturned: money?, lossRecorded: money?, refundWindow: date?, refundUntil: date?
94
+ openSlices: integer?, openCharges: money?, openCosts: money?
52
95
  price: money = subject.price
53
96
  downPayment: money = rate(self.price, down_payment)
54
97
  principal: money = subtract(self.price, self.downPayment)
55
- profit: money = rate(self.principal, profit)
56
- total: money = sum(self.principal, self.profit)
98
+ flatProfit: money?
57
99
  }
58
100
  lifecycle { states: [quoted, signed, active, paid, written_off, cancelled], initial: quoted }
59
101
  invariants count of { instrument: all(financing.installments), reference: "borrowerLimit", anchor: self.borrowerLimit, states: [active], limit: 366 } <= self.borrowerLimit.activePlans
@@ -62,8 +104,9 @@ instrument installments(months: integer(1, 366), charge_limit: integer(1, 3) = 3
62
104
  action create {
63
105
  subject { price: money }
64
106
  requires self.borrowerLimit.borrower == self.borrower
107
+ when pricing is flat_total { calculate: [{ target: flatProfit, op: rate, base: { field: self.principal }, bps: { literal: profit_rate }, rounding: floor }] }
65
108
  when disburse_to is funds { requires self.borrower == self.funds.payer; requires self.price == self.funds.price }
66
- 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 } } }]
109
+ invoke: [{ range: { count: { literal: months }, maximum: months, bind: position }, instrument: slice, action: create, input: { plan: { field: self.id }, borrowerLimit: { field: self.borrowerLimit }, portfolioLimit: { field: self.portfolioLimit }, position: { field: position } } }, { selection: { instrument: slice, reference: "plan", anchor: self.id, states: [draft], limit: months, where: { first: { literal: 0 } } }, action: check_order, input: {} }]
67
110
  }
68
111
  action sign {
69
112
  from: quoted, to: signed, actor: { party: borrower }, input { signedOffer: text, affordabilityReference: text }
@@ -72,6 +115,7 @@ instrument installments(months: integer(1, 366), charge_limit: integer(1, 3) = 3
72
115
  action collect_down_payment { allowZero: true,
73
116
  from: signed, to: signed, actor: { party: borrower }
74
117
  when disburse_to is borrower { requires true == false }
118
+ when disburse_to is seller { requires true == false }
75
119
  when disburse_to is funds {
76
120
  requires self.funds in [pending]
77
121
  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 }] }]
@@ -87,6 +131,7 @@ instrument installments(months: integer(1, 366), charge_limit: integer(1, 3) = 3
87
131
  invoke: [{ selection: { instrument: all(marketplace.reservation), reference: "funds", anchor: self.funds, states: [active], limit: 1 }, action: credit, input: { funds: { field: self.funds }, contribution: { field: self.downPayment } } }, { reference: self.id, action: fund_down_payment, input: {} }]
88
132
  }
89
133
  when disburse_to is borrower { moves self.principal from self.capital to self.borrower economics { purpose: principal, sourceParty: capital } }
134
+ when disburse_to is seller { moves self.principal from self.capital to seller economics { purpose: principal, sourceParty: capital } }
90
135
  invoke: [{ selection: { instrument: slice, reference: "plan", anchor: self.id, states: [draft], limit: 366, order: [position] }, action: activate, input: {} }]
91
136
  when disburse_to is funds { invoke: [{ reference: self.funds, action: confirm, input: {} }] }
92
137
  }
@@ -100,11 +145,13 @@ instrument installments(months: integer(1, 366), charge_limit: integer(1, 3) = 3
100
145
  }
101
146
  action complete {
102
147
  from: active, to: paid
148
+ calculate: [{ target: refundWindow, op: shift, date: { field: self.now }, milliseconds: { literal: refund_within }, direction: after }]
149
+ set: { refundUntil: { field: self.refundWindow } }
103
150
  requires count of { instrument: slice, reference: "plan", anchor: self.id, states: [pending, due], limit: 366 } == 0
104
151
  requires sum "receivable.balance" of { instrument: all(financing.late_charge), reference: "plan", anchor: self.id, states: [assessed, collected], limit: 366 } == "0"
105
152
  requires sum "receivable.balance" of { instrument: all(financing.late_charge.cost), reference: "plan", anchor: self.id, states: [assessed, collected], limit: 366 } == "0"
106
153
  }
107
- action restore { from: [active, paid], to: active, actor: { parent: payment } }
154
+ action restore { from: [active, paid], to: active, actor: { parent: payment }, input { refundUntil: date? }, set: { refundUntil: { field: input.refundUntil } } }
108
155
  action payoff {
109
156
  from: active, to: paid, actor: { party: borrower }
110
157
  input { quote: ref<payoff_quote> }
@@ -112,7 +159,9 @@ instrument installments(months: integer(1, 366), charge_limit: integer(1, 3) = 3
112
159
  deadline: { at: input.quote.expiresAt }
113
160
  calculate: [
114
161
  { target: "principalReturned", op: "aggregate", selection: { instrument: slice, reference: "plan", anchor: self.id, states: [pending, due], limit: 366 }, measure: { sum: "principalReceivable.balance" } }
162
+ { target: refundWindow, op: shift, date: { field: self.now }, milliseconds: { literal: refund_within }, direction: after }
115
163
  ]
164
+ set: { refundUntil: { field: self.refundWindow } }
116
165
  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: {} }]
117
166
  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: {} }]
118
167
  }
@@ -124,18 +173,64 @@ instrument installments(months: integer(1, 366), charge_limit: integer(1, 3) = 3
124
173
  action write_off {
125
174
  from: active, to: written_off, actor: { parent: write_off_request }
126
175
  input { overdueSlice: ref<slice> }
127
- calculate: [{ target: "lossRecorded", op: "aggregate", selection: { instrument: slice, reference: "plan", anchor: self.id, states: [pending, due], limit: 366 }, measure: { sum: "principalReceivable.balance" } }]
176
+ calculate: [{ target: "lossRecorded", op: "aggregate", selection: { instrument: slice, reference: "plan", anchor: self.id, states: [pending, due], limit: 366 }, measure: { sum: "principalReceivable.balance" } }, { target: refundWindow, op: shift, date: { field: self.now }, milliseconds: { literal: refund_within }, direction: after }]
177
+ set: { refundUntil: { field: self.refundWindow } }
128
178
  due: { at: input.overdueSlice.lossEligibleAt }
129
179
  requires input.overdueSlice.plan == self.id; requires input.overdueSlice.principalReceivable.balance > "0"
180
+ when disburse_to is funds { requires self.funds in [released] }
130
181
  invoke: [{ selection: { instrument: slice, reference: "plan", anchor: self.id, states: [pending, due], limit: 366, order: [position] }, action: write_off, input: {} }, { instrument: settlement, action: create, input: { plan: { field: self.id }, identity: { field: self.settlementIdentity }, principal: { literal: "0" }, profit: { literal: "0" }, loss: { field: self.lossRecorded }, kind: { literal: "loss" } } }]
131
182
  }
132
183
  action record_payment {
133
184
  from: active, to: active, actor: { parent: payment }
134
185
  input { identity: text, principal: money, profit: money }
186
+ calculate: [
187
+ { target: openSlices, op: aggregate, selection: { instrument: slice, reference: "plan", anchor: self.id, states: [pending, due], limit: 366 }, measure: count }
188
+ { target: openCharges, op: aggregate, selection: { instrument: all(financing.late_charge), reference: "plan", anchor: self.id, states: [assessed, collected], limit: 366 }, measure: { sum: "receivable.balance" } }
189
+ { target: openCosts, op: aggregate, selection: { instrument: all(financing.late_charge.cost), reference: "plan", anchor: self.id, states: [assessed, collected], limit: 366 }, measure: { sum: "receivable.balance" } }
190
+ ]
135
191
  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" } } }]
192
+ invoke: [{ selection: { instrument: current(), reference: "borrowerLimit", anchor: self.borrowerLimit, states: [active], limit: 1, where: { id: { field: self.id }, openSlices: { literal: 0 }, openCharges: { literal: "0" }, openCosts: { literal: "0" } } }, action: complete, input: {} }]
136
193
  }
137
194
  action cancel { from: quoted, to: cancelled }
138
195
  action void { from: signed, to: cancelled, actor: { party: borrower } }
196
+ action unwind { allowZero: true,
197
+ from: active, to: cancelled, actor: { party: capital }
198
+ calculate: [{ target: refundWindow, op: shift, date: { field: self.now }, milliseconds: { literal: refund_within }, direction: after }]
199
+ set: { refundUntil: { field: self.refundWindow } }
200
+ when disburse_to is borrower { requires true == false }
201
+ when disburse_to is funds { requires self.funds in [return_verified] }
202
+ requires count of { instrument: payment, reference: "plan", anchor: self.id, states: [paid], limit: 366 } == 0
203
+ requires count of { instrument: all(financing.late_charge), reference: "plan", anchor: self.id, states: [assessed, collected], limit: 366 } == 0
204
+ requires count of { instrument: all(financing.late_charge.cost), reference: "plan", anchor: self.id, states: [assessed, collected], limit: 366 } == 0
205
+ when disburse_to is funds { moves self.principal from self.funds.held to self.capital economics { purpose: principal, sourceParty: capital } }
206
+ when disburse_to is seller { moves self.principal from seller to self.capital economics { purpose: principal, sourceParty: seller } }
207
+ invoke: [{ selection: { instrument: slice, reference: "plan", anchor: self.id, states: [pending, due], limit: 366, order: [position] }, action: cancel, input: {} }]
208
+ }
209
+ action unwind_undelivered { allowZero: true,
210
+ from: active, to: cancelled, actor: { party: programOperator }
211
+ calculate: [{ target: refundWindow, op: shift, date: { field: self.now }, milliseconds: { literal: refund_within }, direction: after }]
212
+ set: { refundUntil: { field: self.refundWindow } }
213
+ when disburse_to is borrower { requires true == false }
214
+ when disburse_to is seller { requires true == false }
215
+ when disburse_to is funds { requires self.funds in [funded] }
216
+ requires count of { instrument: payment, reference: "plan", anchor: self.id, states: [paid], limit: 366 } == 0
217
+ requires count of { instrument: all(financing.late_charge), reference: "plan", anchor: self.id, states: [collected], limit: 366 } == 0
218
+ requires count of { instrument: all(financing.late_charge.cost), reference: "plan", anchor: self.id, states: [collected], limit: 366 } == 0
219
+ when disburse_to is funds { moves self.principal from self.funds.held to self.capital economics { purpose: principal, sourceParty: capital } }
220
+ invoke: [{ selection: { instrument: slice, reference: "plan", anchor: self.id, states: [pending, due], limit: 366, order: [position] }, action: cancel, input: {} }]
221
+ invoke: [{ selection: { instrument: all(financing.late_charge.cost), reference: "plan", anchor: self.id, states: [proposed, assessed], limit: 366 }, action: cancel_undelivered, input: {} }, { selection: { instrument: all(financing.late_charge), reference: "plan", anchor: self.id, states: [proposed, assessed], limit: 366 }, action: cancel_undelivered, input: {} }]
222
+ when disburse_to is funds { invoke: [{ reference: self.funds, action: unwind_financed, input: {} }] }
223
+ }
224
+ action return_hold {
225
+ from: paid, to: paid, actor: { party: programOperator }
226
+ requires self.refundUntil < self.now
227
+ when disburse_to is borrower { requires true == false }
228
+ when disburse_to is seller { requires true == false }
229
+ when disburse_to is funds {
230
+ requires self.funds in [funded, return_verified]
231
+ invoke: [{ reference: self.funds, action: unwind_financed, input: {} }]
232
+ }
233
+ }
139
234
  records {
140
235
  write_off_request: {
141
236
  fields { plan: ref<parent>, overdueSlice: ref<slice> }
@@ -251,31 +346,48 @@ instrument installments(months: integer(1, 366), charge_limit: integer(1, 3) = 3
251
346
  revisioned: true
252
347
  fields {
253
348
  plan: ref<parent>, borrowerLimit: ref<limits>, portfolioLimit: ref<portfolio>, position: integer(1, months)
254
- 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?
255
- principalBase: money = divide(self.plan.principal, months)
256
- principalAllocated: money = multiply(self.principalBase, months)
257
- principalRemainder: money = subtract(self.plan.principal, self.principalAllocated)
258
- profitBase: money = divide(self.plan.profit, months)
259
- profitAllocated: money = multiply(self.profitBase, months)
260
- profitRemainder: money = subtract(self.plan.profit, self.profitAllocated)
349
+ originalDueAt: date = at(self.plan.dates, self.position), dueAt: date?, nextDueAt: date?, previousDueAt: date?, prior: integer = subtract(self.position, 1), previousPosition: integer = sum(self.prior, self.first), previousDate: date = at(self.plan.dates, self.previousPosition), extensionLimit: date = after(self.originalDueAt, max_extension), lossEligibleAt: date?
261
350
  first: integer = divide(1, self.position)
262
- principalAdjustment: money = multiply(self.principalRemainder, self.first)
263
- profitAdjustment: money = multiply(self.profitRemainder, self.first)
264
- principal: money = sum(self.principalBase, self.principalAdjustment)
265
- profit: money = sum(self.profitBase, self.profitAdjustment)
351
+ principalBase: money?, principalAllocated: money?, principalRemainder: money?, principalAdjustment: money?
352
+ profitBase: money?, profitAllocated: money?, profitRemainder: money?, profitAdjustment: money?
353
+ principal: money, profit: money, instalment: money
266
354
  principalReceivable: account(self, claim), profitReceivable: account(self, claim), profitEarned: account(self, claim)
267
355
  payoffIdentity: text?, payoffRebate: money?, payoffProfitTotal: money?, payoffProfit: money?
268
- paymentBudget: money?, principalPayment: money?, principalBasePayment: money?, principalRatio: money?, profitPayment: money?, profitBudget: money?, collectibleProfit: money?, collectibleTotal: money?, ratioDenominator: integer = sum(10000, profit)
356
+ paymentBudget: money?, principalPayment: money?, principalBasePayment: money?, principalRatio: money?, profitPayment: money?, profitBudget: money?, collectibleProfit: money?, collectibleTotal: money?, ratioDenominator: integer = sum(10000, profit_rate)
269
357
  debt: account(borrower, claim, contra, "debt"), loss: account(capital, claim, "loss")
270
358
  }
271
- lifecycle { states: [draft, pending, due, paid, written_off], initial: draft }
359
+ lifecycle { states: [draft, pending, due, paid, written_off, cancelled], initial: draft }
272
360
  action create {
273
361
  actor: { parent: parent }
274
362
  input { plan: ref<parent>, borrowerLimit: ref<limits>, portfolioLimit: ref<portfolio>, position: integer(1, months) }
275
363
  set: { dueAt: { field: self.originalDueAt } }
364
+ when pricing is flat_total {
365
+ calculate: [
366
+ { target: principalBase, op: divide, amount: { field: self.plan.principal }, divisor: { literal: months }, rounding: floor }
367
+ { target: principalAllocated, op: multiply, amount: { field: self.principalBase }, units: { literal: months } }
368
+ { target: principalRemainder, op: subtract, base: { field: self.plan.principal }, subtract: [{ field: self.principalAllocated }] }
369
+ { target: principalAdjustment, op: multiply, amount: { field: self.principalRemainder }, units: { field: self.first } }
370
+ { target: principal, op: sum, values: [{ field: self.principalBase }, { field: self.principalAdjustment }] }
371
+ { target: profitBase, op: divide, amount: { field: self.plan.flatProfit }, divisor: { literal: months }, rounding: floor }
372
+ { target: profitAllocated, op: multiply, amount: { field: self.profitBase }, units: { literal: months } }
373
+ { target: profitRemainder, op: subtract, base: { field: self.plan.flatProfit }, subtract: [{ field: self.profitAllocated }] }
374
+ { target: profitAdjustment, op: multiply, amount: { field: self.profitRemainder }, units: { field: self.first } }
375
+ { target: profit, op: sum, values: [{ field: self.profitBase }, { field: self.profitAdjustment }] }
376
+ { target: instalment, op: sum, values: [{ field: self.principal }, { field: self.profit }] }
377
+ ]
378
+ }
379
+ when pricing is amortizing {
380
+ calculate: [
381
+ { target: principal, op: annuity, amount: { field: self.plan.principal }, rate: { literal: profit_rate }, periods: { literal: months }, position: { field: self.position }, part: principal }
382
+ { target: profit, op: annuity, amount: { field: self.plan.principal }, rate: { literal: profit_rate }, periods: { literal: months }, position: { field: self.position }, part: interest }
383
+ { target: instalment, op: sum, values: [{ field: self.principal }, { field: self.profit }] }
384
+ ]
385
+ }
276
386
  invoke: [{ reference: self.id, action: refresh_loss_date, input: {} }]
277
387
  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
388
+ requires self.originalDueAt > self.now; requires self.originalDueAt >= self.previousDate
278
389
  }
390
+ action check_order { from: draft, to: draft, actor: { parent: parent }, requires self.originalDueAt > self.previousDate }
279
391
  action quote_pending {
280
392
  from: pending, to: pending, actor: { parent: payoff_quote }, input { quote: ref<payoff_quote> }
281
393
  requires input.quote.plan == self.plan
@@ -310,6 +422,7 @@ instrument installments(months: integer(1, 366), charge_limit: integer(1, 3) = 3
310
422
  }
311
423
  action activate { allowZero: true,
312
424
  from: draft, to: pending, actor: { parent: parent }
425
+ requires self.dueAt > self.now
313
426
  moves self.principal from self.debt to self.principalReceivable economics { purpose: internal, sourceParty: borrower }
314
427
  moves self.profit from self.debt to self.profitReceivable economics { purpose: internal, sourceParty: borrower }
315
428
  when profit_earned is at_disbursement { moves self.profit from self.profitReceivable to self.profitEarned economics { purpose: internal, sourceParty: programOperator } }
@@ -356,7 +469,10 @@ instrument installments(months: integer(1, 366), charge_limit: integer(1, 3) = 3
356
469
  calculate: [
357
470
  { target: collectibleTotal, op: sum, values: [{ field: self.principalReceivable.balance }, { field: self.collectibleProfit }] }
358
471
  { target: paymentBudget, op: minimum, values: [{ field: input.payment.held.balance }, { field: self.collectibleTotal }] }
359
- { target: principalRatio, op: ratio, amount: { field: self.paymentBudget }, numerator: { literal: 10000 }, denominator: { field: self.ratioDenominator }, rounding: floor }
472
+ ]
473
+ when pricing is flat_total { calculate: [{ target: principalRatio, op: ratio, amount: { field: self.paymentBudget }, numerator: { literal: 10000 }, denominator: { field: self.ratioDenominator }, rounding: floor }] }
474
+ when pricing is amortizing { calculate: [{ target: principalRatio, op: ratio, amount: { field: self.paymentBudget }, numerator: { field: self.principal }, denominator: { field: self.instalment }, rounding: floor }] }
475
+ calculate: [
360
476
  { target: principalBasePayment, op: minimum, values: [{ field: self.principalRatio }, { field: self.principalReceivable.balance }] }
361
477
  { target: profitBudget, op: subtract, base: { field: self.paymentBudget }, subtract: [{ field: self.principalBasePayment }] }
362
478
  { target: profitPayment, op: minimum, values: [{ field: self.profitBudget }, { field: self.collectibleProfit }] }
@@ -387,6 +503,10 @@ instrument installments(months: integer(1, 366), charge_limit: integer(1, 3) = 3
387
503
  from: [pending, due], to: written_off, actor: { parent: parent }
388
504
  moves self.principalReceivable.balance from self.principalReceivable to self.loss economics { purpose: internal, sourceParty: programOperator }; moves self.profitReceivable.balance from self.profitReceivable to self.debt economics { purpose: internal, sourceParty: programOperator }; moves self.profitEarned.balance from self.profitEarned to self.loss economics { purpose: internal, sourceParty: programOperator }
389
505
  }
506
+ action cancel { allowZero: true,
507
+ from: [pending, due], to: cancelled, actor: { parent: parent }
508
+ moves self.principalReceivable.balance from self.principalReceivable to self.debt economics { purpose: internal, sourceParty: programOperator }; moves self.profitReceivable.balance from self.profitReceivable to self.debt economics { purpose: internal, sourceParty: programOperator }; moves self.profitEarned.balance from self.profitEarned to self.debt economics { purpose: internal, sourceParty: programOperator }
509
+ }
390
510
  }
391
511
  recognition: {
392
512
  fields { slice: ref<slice>, dueAt: date = after(self.slice.dueAt, 0) }
@@ -434,7 +554,7 @@ instrument installments(months: integer(1, 366), charge_limit: integer(1, 3) = 3
434
554
  invoke: [{ reference: self.plan, action: record_payment, input: { identity: { field: self.paymentIdentity }, principal: { field: self.principalReturned }, profit: { field: self.profitReturned } } }]
435
555
  }
436
556
  action refund {
437
- from: paid, to: refunded, actor: { party: capital }
557
+ from: paid, to: refunded, actor: { party: capital }, deadline: { at: self.plan.refundUntil }
438
558
  requires unique "settlement_consumption" on [self.plan, self.paymentIdentity]; requires self.plan in [active, paid]
439
559
  requires count of { instrument: settlement, reference: "plan", anchor: self.plan, states: [recorded], limit: 366, where: { identity: { field: self.paymentIdentity } } } == 1
440
560
  invoke: [{ selection: { instrument: settlement, reference: "plan", anchor: self.plan, states: [recorded], limit: 366, where: { identity: { field: self.paymentIdentity } } }, action: refund, input: {} }]
@@ -467,7 +587,7 @@ instrument installments(months: integer(1, 366), charge_limit: integer(1, 3) = 3
467
587
  actor: { parent: slice }
468
588
  input { slice: ref<slice>, payment: ref<payment>, limit: money }
469
589
  requires unique "profit_receipt" on [self.slice, self.payment]
470
- moves self.amount from self.payment.held to self.slice.plan.profitIncome capture "cashReceipt" economics { recipient: capital, company: { purpose: earning, sourceParty: borrower }, participant: { purpose: participant_payout, sourceParty: programOperator } }
590
+ moves self.amount from self.payment.held to self.slice.plan.profitIncome capture "cashReceipt" economics { recipient: capital, company: { purpose: earning, sourceParty: borrower }, participant: { purpose: pass_through, sourceParty: programOperator } }
471
591
  when profit_earned is on_payment { moves self.amount from self.slice.profitReceivable to self.slice.debt economics { purpose: internal, sourceParty: programOperator } }
472
592
  when profit_earned is by_schedule { moves self.amount from self.slice.profitEarned to self.slice.debt economics { purpose: internal, sourceParty: programOperator } }
473
593
  when profit_earned is at_disbursement { moves self.amount from self.slice.profitEarned to self.slice.debt economics { purpose: internal, sourceParty: programOperator } }
@@ -519,6 +639,7 @@ instrument late_charge(on: ref<financing.installments>[], grace: duration = 3d,
519
639
  }
520
640
  action finish { from: assessed, to: collected, actor: { parent: current() }, requires self.receivable.balance == "0" }
521
641
  action cancel { from: proposed, to: waived }
642
+ action cancel_undelivered { allowZero: true, from: [proposed, assessed], to: waived, actor: { parent: on }, moves self.receivable.balance from self.receivable to self.debt economics { purpose: internal, sourceParty: programOperator } }
522
643
  action waive_for_amendment {
523
644
  from: [assessed, collected], to: assessed, actor: { parent: child(on, amendment) }, input { amendment: ref<on.amendment> }
524
645
  requires input.amendment.plan == self.plan; requires input.amendment in [applied]
@@ -526,7 +647,7 @@ instrument late_charge(on: ref<financing.installments>[], grace: duration = 3d,
526
647
  }
527
648
  action waive { allowZero: true, from: [assessed, collected], to: waived, actor: { parent: waiver_request }, input { reason: text, evidence: text }, moves self.receivable.balance from self.receivable to self.debt economics { purpose: internal, sourceParty: programOperator } }
528
649
  action refund {
529
- from: [assessed, collected], to: assessed, actor: { party: fines_to }, input { payment: ref<on.payment> }
650
+ from: [assessed, collected], to: assessed, actor: { party: fines_to }, input { payment: ref<on.payment> }, deadline: { at: self.plan.refundUntil }
530
651
  requires input.payment in [paid, refunded]
531
652
  requires input.payment.plan == self.plan
532
653
  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: {} }]
@@ -555,7 +676,7 @@ instrument late_charge(on: ref<financing.installments>[], grace: duration = 3d,
555
676
  receipt: {
556
677
  fields { cashReceipt: text?, assessment: ref<parent>, payment: ref<on.payment>, amount: money? }
557
678
  lifecycle { states: [paid, refunded], initial: paid }
558
- action create { allowZero: true, 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 capture "cashReceipt" economics { recipient: fines_to, company: { purpose: earning, sourceParty: borrower }, participant: { purpose: participant_payout, sourceParty: programOperator } }; moves self.amount from self.assessment.receivable to self.assessment.debt economics { purpose: internal, sourceParty: programOperator } }
679
+ action create { allowZero: true, 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 capture "cashReceipt" economics { recipient: fines_to, company: { purpose: earning, sourceParty: borrower }, participant: { purpose: pass_through, sourceParty: programOperator } }; moves self.amount from self.assessment.receivable to self.assessment.debt economics { purpose: internal, sourceParty: programOperator } }
559
680
  action refund { allowZero: true, from: paid, to: refunded, actor: { parent: parent }, moves self.amount from fines_to to self.payment.plan.borrower economics { recipient: fines_to, company: { purpose: earning, sourceParty: fines_to, reversalOf: self.cashReceipt }, participant: unclassified }; moves self.amount from self.assessment.debt to self.assessment.receivable economics { purpose: internal, sourceParty: borrower } }
560
681
  }
561
682
 
@@ -586,6 +707,7 @@ instrument late_charge(on: ref<financing.installments>[], grace: duration = 3d,
586
707
  }
587
708
  action finish { from: assessed, to: collected, actor: { parent: current() }, requires self.receivable.balance == "0" }
588
709
  action cancel { from: proposed, to: waived }
710
+ action cancel_undelivered { allowZero: true, from: [proposed, assessed], to: waived, actor: { parent: on }, moves self.receivable.balance from self.receivable to self.debt economics { purpose: internal, sourceParty: programOperator } }
589
711
  action waive_for_amendment {
590
712
  from: [assessed, collected], to: assessed, actor: { parent: child(on, amendment) }, input { amendment: ref<on.amendment> }
591
713
  requires input.amendment.plan == self.plan; requires input.amendment in [applied]
@@ -593,7 +715,7 @@ instrument late_charge(on: ref<financing.installments>[], grace: duration = 3d,
593
715
  }
594
716
  action waive { allowZero: true, from: [assessed, collected], to: waived, actor: { parent: waiver_request }, input { reason: text, evidence: text }, moves self.receivable.balance from self.receivable to self.debt economics { purpose: internal, sourceParty: programOperator } }
595
717
  action refund {
596
- from: [assessed, collected], to: assessed, actor: { party: costs_to }, input { payment: ref<on.payment> }
718
+ from: [assessed, collected], to: assessed, actor: { party: costs_to }, input { payment: ref<on.payment> }, deadline: { at: self.plan.refundUntil }
597
719
  requires input.payment in [paid, refunded]
598
720
  requires input.payment.plan == self.plan
599
721
  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: {} }]
@@ -622,7 +744,7 @@ instrument late_charge(on: ref<financing.installments>[], grace: duration = 3d,
622
744
  receipt: {
623
745
  fields { cashReceipt: text?, assessment: ref<parent>, payment: ref<on.payment>, amount: money? }
624
746
  lifecycle { states: [paid, refunded], initial: paid }
625
- action create { allowZero: true, 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 capture "cashReceipt" economics { recipient: costs_to, company: { purpose: earning, sourceParty: borrower }, participant: { purpose: participant_payout, sourceParty: programOperator } }; moves self.amount from self.assessment.receivable to self.assessment.debt economics { purpose: internal, sourceParty: programOperator } }
747
+ action create { allowZero: true, 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 capture "cashReceipt" economics { recipient: costs_to, company: { purpose: earning, sourceParty: borrower }, participant: { purpose: pass_through, sourceParty: programOperator } }; moves self.amount from self.assessment.receivable to self.assessment.debt economics { purpose: internal, sourceParty: programOperator } }
626
748
  action refund { allowZero: true, from: paid, to: refunded, actor: { parent: parent }, moves self.amount from costs_to to self.payment.plan.borrower economics { recipient: costs_to, company: { purpose: earning, sourceParty: costs_to, reversalOf: self.cashReceipt }, participant: unclassified }; moves self.amount from self.assessment.debt to self.assessment.receivable economics { purpose: internal, sourceParty: borrower } }
627
749
  }
628
750
  }
@@ -637,7 +759,7 @@ instrument limits(per_borrower: money, borrower: party = party(person), active_p
637
759
  fields { borrower: account of borrower, limit: money = per_borrower, activePlans: integer = active_plans }
638
760
  lifecycle { states: [pending, approved], initial: pending }
639
761
  action create { requires unique "borrower_limit" on [self.borrower] }
640
- action approve { from: pending, to: approved }
762
+ action approve { from: pending, to: approved, actor: { party: programOperator } }
641
763
  }
642
764
 
643
765
  instrument portfolio_limit(limit: money) {
@@ -647,7 +769,7 @@ instrument portfolio_limit(limit: money) {
647
769
  fields { key: text = "portfolio", limit: money = limit }
648
770
  lifecycle { states: [pending, approved], initial: pending }
649
771
  action create { requires unique "portfolio_limit" on [self.key] }
650
- action approve { from: pending, to: approved }
772
+ action approve { from: pending, to: approved, actor: { party: programOperator } }
651
773
  }
652
774
 
653
775
  instrument credit_line(borrower: party = programOperator, adapter: text, limit: money, expires: date) {