@hyperscale0/hsx 1.0.128 → 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.
package/std/booking.hsx CHANGED
@@ -21,7 +21,7 @@ header booking
21
21
  // cancellation and completion move the held balance. Run hsx cost for the
22
22
  // composed counts.
23
23
 
24
- instrument reservation(customer: party = party(person), operator: party = programOperator, early_until: duration = 30d, late_from: duration = 15d, balance_due: duration = 14d, middle_forfeit: percent = 0%, late_forfeit: percent = 0%) {
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
25
  revisioned: true
26
26
  summary: "Take a deposit and a balance for a dated booking, with cancellation windows before departure."
27
27
  fields {
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
@@ -9,10 +9,17 @@ header escrow
9
9
  // Expose dispute, verify_return and refund separately for the return path.
10
10
  // Payer disputes delivered goods; payee verifies return evidence from disputed.
11
11
  // Payee refunds the whole held balance before delivery, or from return_verified
12
- // by default after a dispute. While a signed, active or paid financing plan funds
13
- // the hold, both refunds refuse: the plan's unwind returns the lender's principal
14
- // first, then refund returns the rest. Unwind needs return_verified, so a
15
- // financed hold cannot be refunded before delivery.
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.
16
23
  // Expose refund_undelivered to let the payer take the whole held price back once
17
24
  // deliver_within has passed since funding without delivery. It is a payer action
18
25
  // that becomes available at that time; nothing runs it automatically.
@@ -41,14 +48,12 @@ instrument hold(payer: party = party(person), payee: party = programOperator, ac
41
48
  }
42
49
  action fund {
43
50
  from: pending, to: funded, actor: { party: payer }
44
- subject { price: money }
45
51
  set: { fundedAt: { field: self.now } }
46
52
  moves self.price from payer to self.held economics { purpose: principal, sourceParty: payer }
47
53
  }
48
54
  action confirm {
49
- from: pending, to: funded
55
+ from: pending, to: funded, actor: { parent: all(financing.installments) }
50
56
  requires self.held.balance == self.price
51
- set: { fundedAt: { field: self.now } }
52
57
  }
53
58
  action deliver {
54
59
  from: funded, to: delivered, actor: { party: payee }
@@ -74,15 +79,19 @@ instrument hold(payer: party = party(person), payee: party = programOperator, ac
74
79
  }
75
80
  action refund {
76
81
  from: [funded, dispute.refund_after], to: refunded, actor: { party: payee }
77
- requires count of { instrument: all(financing.installments), reference: "funds", anchor: self.id, states: [signed, active, paid], limit: 1 } == 0
82
+ requires count of { instrument: all(financing.installments), reference: "funds", anchor: self.id, states: [quoted, signed, active, paid, written_off], limit: 1 } == 0
78
83
  moves self.held.balance from self.held to payer economics { purpose: principal, sourceParty: programOperator }
79
84
  }
80
85
  action refund_undelivered {
81
86
  from: funded, to: refunded, actor: { party: payer }
82
87
  due: { at: self.fundedAt, offset: deliver_within }
83
- requires count of { instrument: all(financing.installments), reference: "funds", anchor: self.id, states: [signed, active, paid], limit: 1 } == 0
88
+ requires count of { instrument: all(financing.installments), reference: "funds", anchor: self.id, states: [quoted, signed, active, paid, written_off], limit: 1 } == 0
84
89
  moves self.price from self.held to payer economics { purpose: principal, sourceParty: programOperator }
85
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
+ }
86
95
  action cancel { allowZero: true,
87
96
  from: pending, to: cancelled, actor: { party: payer }
88
97
  moves self.held.balance from self.held to payer economics { purpose: principal, sourceParty: programOperator }
package/std/financing.hsx CHANGED
@@ -19,12 +19,34 @@ header financing
19
19
  // so list the plan in its converters; the deposit then counts as escrow credit.
20
20
  // disburse_to: seller pays principal to the seller party at activation (pay later
21
21
  // with the merchant paid on day one). For a return, capital refunds each paid
22
- // payment to the borrower, then runs unwind: it takes principal back to capital and
23
- // cancels open slices with their claims. Seller mode debits the seller; funds mode
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
24
33
  // needs the hold in return_verified and debits the hold, after which the payee's
25
- // escrow refund returns only the down payment. Escrow refund refuses while a live
26
- // plan funds the hold. Unwind needs no assessed or collected late charges; waive
27
- // them first.
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.
28
50
  // Borrower- and seller-directed disbursement exclude collect_down_payment. A down_payment
29
51
  // there reduces principal but does not collect cash. Price is subject metadata,
30
52
  // not the borrower limit. The escrow payer and price must match the plan.
@@ -40,14 +62,15 @@ header financing
40
62
  // payment does not settle the remaining fine. Recovery cost needs its own record.
41
63
  // Profit share credits capital's allocatedProfitPayable, not an outside agency.
42
64
  // Credit lines need adapter binding at activation; draws are ledger advances,
43
- // not evidence of outside funding. Caller-authorized limit approvals and advances
44
- // 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.
45
68
 
46
69
  // Cost: installments include cash and claim moves, child-account creation and
47
70
  // bounded invocations across slices and assessments. Month and charge limits affect
48
71
  // the bound. Run hsx cost on the selected policies; profit is not a platform tariff.
49
72
 
50
- 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, 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)) {
51
74
  dependencies {
52
75
  funds {
53
76
  selector: disburse_to, is: funds
@@ -67,7 +90,8 @@ instrument installments(months: integer(1, 366), pricing: enum(amortizing, flat_
67
90
  funds: ref<funds>?, borrower: account of borrower, settlementIdentity: text, borrowerLimit: ref<limits>, portfolioLimit: ref<portfolio>
68
91
  months: integer = months, dates: list(date, months)
69
92
  allocatedPayable: account(capital, cash, "allocatedProfitPayable"), capital: account(capital, cash, "capital"), profitIncome: account(capital, cash, "profitIncome"), loss: account(capital, claim, "loss")
70
- 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?
71
95
  price: money = subject.price
72
96
  downPayment: money = rate(self.price, down_payment)
73
97
  principal: money = subtract(self.price, self.downPayment)
@@ -121,11 +145,13 @@ instrument installments(months: integer(1, 366), pricing: enum(amortizing, flat_
121
145
  }
122
146
  action complete {
123
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 } }
124
150
  requires count of { instrument: slice, reference: "plan", anchor: self.id, states: [pending, due], limit: 366 } == 0
125
151
  requires sum "receivable.balance" of { instrument: all(financing.late_charge), reference: "plan", anchor: self.id, states: [assessed, collected], limit: 366 } == "0"
126
152
  requires sum "receivable.balance" of { instrument: all(financing.late_charge.cost), reference: "plan", anchor: self.id, states: [assessed, collected], limit: 366 } == "0"
127
153
  }
128
- 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 } } }
129
155
  action payoff {
130
156
  from: active, to: paid, actor: { party: borrower }
131
157
  input { quote: ref<payoff_quote> }
@@ -133,7 +159,9 @@ instrument installments(months: integer(1, 366), pricing: enum(amortizing, flat_
133
159
  deadline: { at: input.quote.expiresAt }
134
160
  calculate: [
135
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 }
136
163
  ]
164
+ set: { refundUntil: { field: self.refundWindow } }
137
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: {} }]
138
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: {} }]
139
167
  }
@@ -145,20 +173,30 @@ instrument installments(months: integer(1, 366), pricing: enum(amortizing, flat_
145
173
  action write_off {
146
174
  from: active, to: written_off, actor: { parent: write_off_request }
147
175
  input { overdueSlice: ref<slice> }
148
- 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 } }
149
178
  due: { at: input.overdueSlice.lossEligibleAt }
150
179
  requires input.overdueSlice.plan == self.id; requires input.overdueSlice.principalReceivable.balance > "0"
180
+ when disburse_to is funds { requires self.funds in [released] }
151
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" } } }]
152
182
  }
153
183
  action record_payment {
154
184
  from: active, to: active, actor: { parent: payment }
155
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
+ ]
156
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: {} }]
157
193
  }
158
194
  action cancel { from: quoted, to: cancelled }
159
195
  action void { from: signed, to: cancelled, actor: { party: borrower } }
160
196
  action unwind { allowZero: true,
161
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 } }
162
200
  when disburse_to is borrower { requires true == false }
163
201
  when disburse_to is funds { requires self.funds in [return_verified] }
164
202
  requires count of { instrument: payment, reference: "plan", anchor: self.id, states: [paid], limit: 366 } == 0
@@ -168,6 +206,31 @@ instrument installments(months: integer(1, 366), pricing: enum(amortizing, flat_
168
206
  when disburse_to is seller { moves self.principal from seller to self.capital economics { purpose: principal, sourceParty: seller } }
169
207
  invoke: [{ selection: { instrument: slice, reference: "plan", anchor: self.id, states: [pending, due], limit: 366, order: [position] }, action: cancel, input: {} }]
170
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
+ }
171
234
  records {
172
235
  write_off_request: {
173
236
  fields { plan: ref<parent>, overdueSlice: ref<slice> }
@@ -491,7 +554,7 @@ instrument installments(months: integer(1, 366), pricing: enum(amortizing, flat_
491
554
  invoke: [{ reference: self.plan, action: record_payment, input: { identity: { field: self.paymentIdentity }, principal: { field: self.principalReturned }, profit: { field: self.profitReturned } } }]
492
555
  }
493
556
  action refund {
494
- from: paid, to: refunded, actor: { party: capital }
557
+ from: paid, to: refunded, actor: { party: capital }, deadline: { at: self.plan.refundUntil }
495
558
  requires unique "settlement_consumption" on [self.plan, self.paymentIdentity]; requires self.plan in [active, paid]
496
559
  requires count of { instrument: settlement, reference: "plan", anchor: self.plan, states: [recorded], limit: 366, where: { identity: { field: self.paymentIdentity } } } == 1
497
560
  invoke: [{ selection: { instrument: settlement, reference: "plan", anchor: self.plan, states: [recorded], limit: 366, where: { identity: { field: self.paymentIdentity } } }, action: refund, input: {} }]
@@ -576,6 +639,7 @@ instrument late_charge(on: ref<financing.installments>[], grace: duration = 3d,
576
639
  }
577
640
  action finish { from: assessed, to: collected, actor: { parent: current() }, requires self.receivable.balance == "0" }
578
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 } }
579
643
  action waive_for_amendment {
580
644
  from: [assessed, collected], to: assessed, actor: { parent: child(on, amendment) }, input { amendment: ref<on.amendment> }
581
645
  requires input.amendment.plan == self.plan; requires input.amendment in [applied]
@@ -583,7 +647,7 @@ instrument late_charge(on: ref<financing.installments>[], grace: duration = 3d,
583
647
  }
584
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 } }
585
649
  action refund {
586
- 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 }
587
651
  requires input.payment in [paid, refunded]
588
652
  requires input.payment.plan == self.plan
589
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: {} }]
@@ -643,6 +707,7 @@ instrument late_charge(on: ref<financing.installments>[], grace: duration = 3d,
643
707
  }
644
708
  action finish { from: assessed, to: collected, actor: { parent: current() }, requires self.receivable.balance == "0" }
645
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 } }
646
711
  action waive_for_amendment {
647
712
  from: [assessed, collected], to: assessed, actor: { parent: child(on, amendment) }, input { amendment: ref<on.amendment> }
648
713
  requires input.amendment.plan == self.plan; requires input.amendment in [applied]
@@ -650,7 +715,7 @@ instrument late_charge(on: ref<financing.installments>[], grace: duration = 3d,
650
715
  }
651
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 } }
652
717
  action refund {
653
- 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 }
654
719
  requires input.payment in [paid, refunded]
655
720
  requires input.payment.plan == self.plan
656
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: {} }]
@@ -694,7 +759,7 @@ instrument limits(per_borrower: money, borrower: party = party(person), active_p
694
759
  fields { borrower: account of borrower, limit: money = per_borrower, activePlans: integer = active_plans }
695
760
  lifecycle { states: [pending, approved], initial: pending }
696
761
  action create { requires unique "borrower_limit" on [self.borrower] }
697
- action approve { from: pending, to: approved }
762
+ action approve { from: pending, to: approved, actor: { party: programOperator } }
698
763
  }
699
764
 
700
765
  instrument portfolio_limit(limit: money) {
@@ -704,7 +769,7 @@ instrument portfolio_limit(limit: money) {
704
769
  fields { key: text = "portfolio", limit: money = limit }
705
770
  lifecycle { states: [pending, approved], initial: pending }
706
771
  action create { requires unique "portfolio_limit" on [self.key] }
707
- action approve { from: pending, to: approved }
772
+ action approve { from: pending, to: approved, actor: { party: programOperator } }
708
773
  }
709
774
 
710
775
  instrument credit_line(borrower: party = programOperator, adapter: text, limit: money, expires: date) {
package/std/wallet.hsx CHANGED
@@ -6,7 +6,7 @@ header wallet
6
6
 
7
7
  // Expose balance.create, activate, topup and withdraw as needed. Holder moves
8
8
  // cash only while active. Freeze blocks new topups, withdrawals and spend reserves;
9
- // close requires no held cash. Freeze/unfreeze use caller operation authority.
9
+ // close requires no held cash. Only the Product operator freezes or unfreezes.
10
10
  // A separate spend record checks its amount against spendLimit at creation.
11
11
  // Expose reserve, pay and cancel for the bound payee. Reserve requires an active
12
12
  // wallet before expiresAt; pay posts the whole reservation, cancel/expiry void it.
@@ -43,8 +43,8 @@ instrument balance(holder: party, spend_limit: money = 100000 SAR, topup_fee: pe
43
43
  input { amount: money }
44
44
  moves input.amount from self.held to holder economics { purpose: prepaid_credit, sourceParty: programOperator }
45
45
  }
46
- action freeze { from: active, to: frozen, }
47
- action unfreeze { from: frozen, to: active, }
46
+ action freeze { from: active, to: frozen, actor: { party: programOperator } }
47
+ action unfreeze { from: frozen, to: active, actor: { party: programOperator } }
48
48
  action close { from: [pending, active], to: closed, requires self.held.balance == "0" }
49
49
  }
50
50