@hyperscale0/hsx 1.0.337 → 1.0.339

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/dist/src/ast.d.ts +1 -0
  2. package/dist/src/ast.d.ts.map +1 -1
  3. package/dist/src/ast.js.map +1 -1
  4. package/dist/src/attachment-economics.d.ts +37 -0
  5. package/dist/src/attachment-economics.d.ts.map +1 -1
  6. package/dist/src/attachment-economics.js +183 -3
  7. package/dist/src/attachment-economics.js.map +1 -1
  8. package/dist/src/binding-contract.d.ts.map +1 -1
  9. package/dist/src/binding-contract.js +10 -4
  10. package/dist/src/binding-contract.js.map +1 -1
  11. package/dist/src/catalog-program.d.ts +1 -1
  12. package/dist/src/catalog-program.d.ts.map +1 -1
  13. package/dist/src/catalog-program.js +1 -1
  14. package/dist/src/compile.d.ts.map +1 -1
  15. package/dist/src/compile.js +133 -41
  16. package/dist/src/compile.js.map +1 -1
  17. package/dist/src/examples-bundle.js +1 -1
  18. package/dist/src/examples-bundle.js.map +1 -1
  19. package/dist/src/headers.d.ts.map +1 -1
  20. package/dist/src/headers.js +3 -1
  21. package/dist/src/headers.js.map +1 -1
  22. package/dist/src/language.d.ts.map +1 -1
  23. package/dist/src/language.js +1 -0
  24. package/dist/src/language.js.map +1 -1
  25. package/dist/src/parse.js +13 -1
  26. package/dist/src/parse.js.map +1 -1
  27. package/dist/src/std-bundle.js +6 -6
  28. package/dist/src/std-bundle.js.map +1 -1
  29. package/dist/src/template-guide.d.ts.map +1 -1
  30. package/dist/src/template-guide.js +49 -18
  31. package/dist/src/template-guide.js.map +1 -1
  32. package/docs/README.md +5 -2
  33. package/examples/camera-rental.hsx +6 -1
  34. package/package.json +4 -4
  35. package/src/ast.ts +7 -1
  36. package/src/attachment-economics.ts +230 -2
  37. package/src/binding-contract.ts +13 -6
  38. package/src/catalog-program.ts +1 -1
  39. package/src/compile.ts +185 -58
  40. package/src/examples-bundle.ts +1 -1
  41. package/src/headers.ts +6 -1
  42. package/src/language.ts +1 -0
  43. package/src/parse.ts +16 -1
  44. package/src/std-bundle.ts +6 -6
  45. package/src/template-guide.ts +74 -22
  46. package/std/collections.hsx +4 -4
  47. package/std/financing.hsx +461 -266
  48. package/std/insurance.hsx +21 -5
  49. package/std/lending.hsx +13 -9
  50. package/std/money.hsx +148 -38
  51. package/std/reporting.hsx +6 -6
package/std/insurance.hsx CHANGED
@@ -16,6 +16,9 @@ header insurance
16
16
  // Cover.cancel invokes refund only on paid slices, before each startsAt. It returns
17
17
  // insurerPremium from insurer and commission from broker to holder. Active or
18
18
  // expired periods are excluded; insufficient recipient funds refuse the reversal.
19
+ // Each refund is two transfers, so cancel refunds 126 paid slices per operation.
20
+ // A cover with more reads cancelling, and its cancel_step clock refunds the next
21
+ // 126 a second later until none remain; then it reads cancelled.
19
22
  // Slice refund is parent-owned, not a public alias. Expose cover.cancel instead.
20
23
  // Claims require active cover. Expose claim.create, approve, deny and pay as needed.
21
24
  // Inspector approves OR denies from submitted. After approval only pay or clock
@@ -25,21 +28,34 @@ header insurance
25
28
 
26
29
  // Cost: each slice collection or refund declares two transfers. Claim approval
27
30
  // reserves once; pay posts once and expiry voids once. Cover cancellation adds
28
- // bounded slice invocations. Run hsx cost for composed counts, not provider prices.
31
+ // at most 252 transfers per step. Run hsx cost for composed counts, not provider prices.
29
32
 
30
33
  instrument cover(holder: party, adapter: text, commission: percent = 0%, broker: party = programOperator, covers: ref?, premium: money = runtime, claim_limit: money = runtime) {
31
34
  summary: "Recurring cover bound to an outside insurer across an ADL boundary."
32
- fields { covers: ref<covers>?, holder: account of holder, insurer: account(adapter(adapter), cash, "premium"), expiresAt: date, claimLimit: money = claim_limit }
33
- lifecycle { states: [pending, active, cancelled, expired], initial: pending }
35
+ fields { covers: ref<covers>?, holder: account of holder, insurer: account(adapter(adapter), cash, "premium"), expiresAt: date, claimLimit: money = claim_limit, cancelAt: date?, slicesLeft: integer? }
36
+ lifecycle { states: [pending, active, cancelling, cancelled, expired], initial: pending }
34
37
  action create { actor: { party: holder } }
35
38
  action activate {
36
39
  from: pending, to: active, actor: { party: programOperator }
37
40
  subject { adapter: adapter }
38
41
  }
39
42
  action cancel {
40
- from: active, to: cancelled, actor: { party: holder }
41
- invoke: [{ selection: { instrument: slice, reference: "cover", anchor: self.id, states: [paid], limit: 366 }, action: refund, input: {} }]
43
+ from: active, to: cancelling, actor: { party: holder }
44
+ invoke: [{ reference: self.id, action: cancel_batch, input: {} }]
42
45
  }
46
+ action cancel_step { from: cancelling, to: preserve, actor: clock, due: { at: self.cancelAt }, invoke: [{ reference: self.id, action: cancel_batch, input: {} }] }
47
+ action cancel_batch {
48
+ from: cancelling, to: preserve, actor: { parent: current() }
49
+ calculate: [{ target: cancelAt, op: shift, date: { field: self.now }, milliseconds: { literal: 1000 }, direction: after }]
50
+ invoke: [{ selection: { instrument: slice, reference: "cover", anchor: self.id, states: [paid], limit: 126, batch: true, order: [startsAt] }, action: refund, input: {} }]
51
+ invoke: [{ reference: self.id, action: cancel_check, input: {} }]
52
+ }
53
+ action cancel_check {
54
+ from: cancelling, to: preserve, actor: { parent: current() }
55
+ calculate: [{ target: slicesLeft, op: aggregate, selection: { instrument: slice, reference: "cover", anchor: self.id, states: [paid], limit: 1, batch: true }, measure: count }]
56
+ invoke: [{ reference: self.id, action: finish_cancel, guard: { kind: compare, left: { field: self.slicesLeft }, operator: "==", right: { literal: 0 } }, input: {} }]
57
+ }
58
+ action finish_cancel { from: cancelling, to: cancelled, actor: { parent: current() } }
43
59
  action expire { from: active, to: expired, actor: clock, due: { at: self.expiresAt } }
44
60
  records {
45
61
  slice: {
package/std/lending.hsx CHANGED
@@ -26,6 +26,9 @@ header lending
26
26
  // Cost: round.create declares one owned account. Close transfers its balance
27
27
  // and invokes commitments; failure invokes their refunds. Distribution adds moves
28
28
  // and bounded share invocations. Run hsx cost for counts after policy selection.
29
+ // A round takes at most 248 commitments, so failure refunds them in 248 ledger
30
+ // transfers and a cash distribution pays every share in 253, the most one
31
+ // ledger request takes.
29
32
 
30
33
  instrument round(borrower: party, plan: ref<financing.installments> = object(financing.installments), operator: party = programOperator, minimum_ticket: money = 100 SAR, investor_cap: percent = 20%, commitments: ref<lending.commitment> = object(lending.commitment)) {
31
34
  summary: "A threshold funding round; failed rounds return each original wallet contribution."
@@ -38,14 +41,14 @@ instrument round(borrower: party, plan: ref<financing.installments> = object(fin
38
41
  action create { actor: { party: operator }, requires unique "funding_plan" on [self.plan]; requires self.borrower == self.plan.borrower }
39
42
  action close {
40
43
  from: open, to: funded, actor: { party: operator }
41
- requires sum "amount" of { instrument: commitments, reference: "round", anchor: self.id, states: [committed], limit: 366 } == self.price
44
+ requires sum "amount" of { instrument: commitments, reference: "round", anchor: self.id, states: [committed], limit: 248 } == self.price
42
45
  requires self.held.balance == self.price
43
46
  moves self.price from self.held to self.plan.capital economics { purpose: principal, sourceParty: programOperator }
44
- invoke: [{ selection: { instrument: commitments, reference: "round", anchor: self.id, states: [committed], limit: 366 }, action: fund, input: {} }]
47
+ invoke: [{ selection: { instrument: commitments, reference: "round", anchor: self.id, states: [committed], limit: 248 }, action: fund, input: {} }]
45
48
  }
46
49
  action fail {
47
50
  from: open, to: failed, actor: clock, due: { at: self.closesAt }
48
- invoke: [{ selection: { instrument: commitments, reference: "round", anchor: self.id, states: [committed], limit: 366 }, action: refund, input: {} }]
51
+ invoke: [{ selection: { instrument: commitments, reference: "round", anchor: self.id, states: [committed], limit: 248 }, action: refund, input: {} }]
49
52
  }
50
53
  }
51
54
 
@@ -53,8 +56,9 @@ instrument commitment(round: ref<lending.round>, wallet: ref<wallet.balance>, in
53
56
  summary: "One investor's original-wallet contribution, capped cumulatively within the round."
54
57
  fields { round: ref<round>, wallet: ref<wallet>, investor: account of investor, amount: money, loss: account(investor, claim, "loss") }
55
58
  lifecycle { states: [committed, funded, withdrawn, refunded], initial: committed }
56
- invariants sum "amount" of { instrument: all(lending.commitment), reference: "round", anchor: self.round, states: [committed, funded], limit: 366, where: { investor: { field: self.investor } } } <= self.round.maximumTicket
57
- invariants sum "amount" of { instrument: all(lending.commitment), reference: "round", anchor: self.round, states: [committed, funded], limit: 366 } <= self.round.price
59
+ invariants sum "amount" of { instrument: all(lending.commitment), reference: "round", anchor: self.round, states: [committed, funded], limit: 248, where: { investor: { field: self.investor } } } <= self.round.maximumTicket
60
+ invariants sum "amount" of { instrument: all(lending.commitment), reference: "round", anchor: self.round, states: [committed, funded], limit: 248 } <= self.round.price
61
+ invariants count of { instrument: all(lending.commitment), reference: "round", anchor: self.round, states: [committed, funded], limit: 248 } <= 248
58
62
  action create {
59
63
  actor: { party: investor }
60
64
  requires self.round in [open]
@@ -108,9 +112,9 @@ instrument distribution(round: ref<lending.round>, receipt: ref, commitments: re
108
112
  action distribute_cash { allowZero: true,
109
113
  from: cash_ready, to: distributed, actor: { party: operator }
110
114
  requires self.source in [recorded]; requires unique "settlement_consumption" on [self.source.plan, self.source.identity]
111
- requires sum "weight" of { instrument: share, reference: "distribution", anchor: self.id, states: [pending], limit: 366 } == self.round.price
115
+ requires sum "weight" of { instrument: share, reference: "distribution", anchor: self.id, states: [pending], limit: 248 } == self.round.price
112
116
  moves self.principal from self.source.capital to self.held; moves self.profit from self.source.profitIncome to self.held; moves self.charge from self.held to programOperator economics { purpose: internal, sourceParty: programOperator }; moves self.vat from self.held to programTax economics { purpose: pass_through, sourceParty: programOperator }
113
- invoke: [{ reference: self.source, action: distribute, input: {} }, { selection: { instrument: share, reference: "distribution", anchor: self.id, states: [pending], limit: 366 }, action: cash, input: {} }, { reference: self.id, action: residual_cash, input: {} }]
117
+ invoke: [{ reference: self.source, action: distribute, input: {} }, { selection: { instrument: share, reference: "distribution", anchor: self.id, states: [pending], limit: 248 }, action: cash, input: {} }, { reference: self.id, action: residual_cash, input: {} }]
114
118
  }
115
119
  action residual_cash { allowZero: true,
116
120
  from: distributed, to: distributed, actor: { parent: current() }
@@ -119,9 +123,9 @@ instrument distribution(round: ref<lending.round>, receipt: ref, commitments: re
119
123
  action distribute_loss { allowZero: true,
120
124
  from: loss_ready, to: distributed, actor: { party: operator }
121
125
  requires self.source in [recorded]; requires unique "settlement_consumption" on [self.source.plan, self.source.identity]
122
- requires sum "weight" of { instrument: share, reference: "distribution", anchor: self.id, states: [pending], limit: 366 } == self.round.price
126
+ requires sum "weight" of { instrument: share, reference: "distribution", anchor: self.id, states: [pending], limit: 248 } == self.round.price
123
127
  moves self.net from self.source.lossAccount to self.losses
124
- invoke: [{ reference: self.source, action: distribute, input: {} }, { selection: { instrument: share, reference: "distribution", anchor: self.id, states: [pending], limit: 366 }, action: loss, input: {} }, { reference: self.id, action: residual_loss, input: {} }]
128
+ invoke: [{ reference: self.source, action: distribute, input: {} }, { selection: { instrument: share, reference: "distribution", anchor: self.id, states: [pending], limit: 248 }, action: loss, input: {} }, { reference: self.id, action: residual_loss, input: {} }]
125
129
  }
126
130
  action residual_loss { allowZero: true,
127
131
  from: distributed, to: distributed, actor: { parent: current() }
package/std/money.hsx CHANGED
@@ -12,12 +12,22 @@ header money
12
12
  // payee, so a shop that is payee keeps a deposit without acting for its customer.
13
13
  // Bind released_by: payer for an escrow-style hold that only the payer releases.
14
14
  // Only the payee refunds. A hold has no dispute or damage decision.
15
- // The payee proposes a schedule, which creates its count occurrences, and only
16
- // the payer activates or cancels it. Each due occurrence pays only while the
17
- // schedule is active. A due time promises no retry.
15
+ // The payee proposes a schedule and only the payer activates or cancels it; the
16
+ // payee stops it. Without every, count is required and create makes count
17
+ // occurrences on the listed dates that sum to amount. With every (1 month, 2
18
+ // weeks), each occurrence charges amount, the first at startsAt, and paying one
19
+ // creates the next, so only one piece waits ahead. Months step from startsAt and
20
+ // clamp to the month's end. create refuses a startsAt in the past. An authored
21
+ // count ends it after count pieces; without one it runs until stopped or
22
+ // cancelled. The schedule keeps count as a field only when it is set, so reads
23
+ // name a piece "2 of 12" or, open-ended, "#2". Each due occurrence pays
24
+ // only while the schedule is active, and one clock drain posts at most the 253
25
+ // ledger transfers one request takes, 253 one-transfer pieces; the next drain
26
+ // continues a plan that fell further behind.
18
27
  // A pool accepts whole contributions only up to its remaining target and at most
19
- // 366 paid records. Expose pay to release the exact target. Clock failure refunds
20
- // original contributors; excess acceptance and partial contribution are absent.
28
+ // 253 paid records, so a failure refunds every contributor in one ledger request.
29
+ // Expose pay to release the exact target. Clock failure refunds original
30
+ // contributors; excess acceptance and partial contribution are absent.
21
31
  // A swap reserves both legs before exchange or voids reservations on expiry.
22
32
  // Payout instruct reserves across the selected adapter; confirm/reject require
23
33
  // matching terminal instruction evidence. An acknowledgement cannot settle it.
@@ -26,13 +36,30 @@ header money
26
36
  // most 366 billed events across every metered attachment on the mandate may sum
27
37
  // their payer debit, buyer fee and tax included, to its limit. The payer revokes
28
38
  // it to stop billing. Only the payee cancels its own unbilled usage.
29
- // The payer creates a late fee against its dueAt, which rename can bind to the
30
- // record's own due field. Once dueAt plus grace plus one period has passed, the
31
- // clock charges amount from payer to payee for each whole period, and the last
32
- // charge takes only what is left under cap; then the fee is capped. The payee runs
33
- // stop when the obligation is met, which ends charging. A charge the payer cannot
34
- // fund refuses as clock work and charging waits for its retry. Grace is at least
35
- // one second; there are no percentage or interest-style fees.
39
+ // The company starts a late fee against its dueAt, so late tracking never waits
40
+ // on the payer; rename can bind dueAt to the record's own due field. Once dueAt
41
+ // plus grace plus one period has passed, the clock charges amount from payer to
42
+ // payee for each whole period, and the last charge takes only what is left under
43
+ // cap; then the fee is capped. Charge k is due at dueAt plus grace plus k
44
+ // periods. create copies dueAt. When any action on the record changes the bound
45
+ // field, follow runs on the fee in the same request and reads the field as it
46
+ // stands; the payee's reschedule runs follow directly. Follow keeps the charges
47
+ // already posted and moves the rest later, never earlier, because fees run from
48
+ // the dueAt set at create; past ones post on the next clock run. The clock never
49
+ // runs twice at one instant, so follow also needs the next charge after the last
50
+ // posted one. A move that breaks either rule refuses reschedule, while another
51
+ // action's change leaves the fee where it was and still runs. Period counts
52
+ // whole seconds. The payee runs stop when the obligation is met. Stop keeps what
53
+ // was owed: each charge due at or before it still posts, and the fee settles
54
+ // until the last of those posts, then stops. A period still running at the stop
55
+ // is not charged. A charge the payer cannot fund refuses as clock work and
56
+ // charging waits for its retry; the payee runs waive to forgive what is still
57
+ // owed. Grace is at least one second; there are no percentage or interest-style
58
+ // fees. Bind a hold's late_fee to the late fee on its object and refund also
59
+ // runs stop on that fee while it is running, so returning the deposit ends both.
60
+ // The link sits on refund because every good return refunds, and a fee already
61
+ // capped or stopped is skipped rather than blocking the refund. The hold links
62
+ // the fee at create, so the fee is agreed first, with the same payer and payee.
36
63
 
37
64
  // Cost: transfer.pay declares one transfer. Hold.create declares one owned account;
38
65
  // fund, release and refund each declare one transfer. Schedules and pools add bounded
@@ -51,11 +78,19 @@ instrument transfer(payer: party, payee: party, amount: money = runtime) {
51
78
  action cancel { from: pending, to: cancelled, actor: { party: payer } }
52
79
  }
53
80
 
54
- instrument hold(payer: party, payee: party, amount: money = runtime, released_by: party = payee) {
81
+ instrument hold(payer: party, payee: party, amount: money = runtime, released_by: party = payee, late_fee: ref<money.late_fee>?) {
55
82
  summary: "Hold the declared amount until release or refund."
56
- fields { payer: account of payer, payee: account of payee, amount: money = amount, price: money = self.amount, held: account of self }
83
+ fields {
84
+ payer: account of payer, payee: account of payee, amount: money = amount, price: money = self.amount, held: account of self
85
+ when late_fee is set { lateFee: ref<late_fee> }
86
+ }
57
87
  lifecycle { states: [pending, funded, released, refunded, cancelled], initial: pending }
58
- action create { }
88
+ action create {
89
+ when late_fee is set {
90
+ requires self.lateFee.payer == self.payer
91
+ requires self.lateFee.payee == self.payee
92
+ }
93
+ }
59
94
  action fund {
60
95
  from: pending, to: funded, actor: { party: payer }
61
96
  moves self.amount from payer to self.held economics { purpose: principal, sourceParty: payer }
@@ -71,6 +106,7 @@ instrument hold(payer: party, payee: party, amount: money = runtime, released_by
71
106
  action refund {
72
107
  from: funded, to: refunded, actor: { party: payee }
73
108
  moves self.amount from self.held to payer economics { purpose: principal, sourceParty: programOperator }
109
+ when late_fee is set { invoke: [{ selection: { instrument: late_fee, reference: "payer", anchor: self.payer, states: [running], limit: 1, where: { id: { field: self.lateFee } } }, action: stop, input: {} }] }
74
110
  }
75
111
  action cancel { from: pending, to: cancelled }
76
112
  }
@@ -84,13 +120,31 @@ instrument split(payer: party, amount: money = runtime, shares: split = { progra
84
120
  action cancel { from: pending, to: cancelled }
85
121
  }
86
122
 
87
- instrument schedule(payer: party, payee: party, amount: money = runtime, count: integer(1, 366) = 12) {
88
- summary: "A finite schedule whose dated pieces sum exactly to the declared total."
89
- fields { amount: money = amount, count: integer = count, dates: list(date, count) }
123
+ instrument schedule(payer: party, payee: party, amount: money = runtime, count: integer(1, 366)?, every: period?) {
124
+ summary: "Pieces due on listed dates that sum to the total, or one amount each period from a start date until stopped, or for count periods."
125
+ dependencies {
126
+ count {
127
+ selector: every, is: unset
128
+ message: "Attachment `{attachment}` charges on a list of dates, so it needs `count`, the number of dates."
129
+ fix: "Add `count: 3` for three dated pieces, or add `every: 1 month` to charge each month until stopped or cancelled."
130
+ }
131
+ }
132
+ fields {
133
+ amount: money = amount
134
+ when every is unset { count: integer = count, dates: list(date, count) }
135
+ when every is set {
136
+ startsAt: date
137
+ when count is set { count: integer = count }
138
+ }
139
+ }
90
140
  lifecycle { states: [draft, active, complete, cancelled], initial: draft }
91
141
  action create {
92
142
  actor: { party: payee }
93
- invoke: [{ range: { count: { literal: count }, maximum: count, bind: position }, instrument: occurrence, action: create, input: { schedule: { field: self.id }, position: { field: position } } }]
143
+ when every is unset { invoke: [{ range: { count: { literal: count }, maximum: count, bind: position }, instrument: occurrence, action: create, input: { schedule: { field: self.id }, position: { field: position } } }] }
144
+ when every is set {
145
+ requires self.startsAt >= self.now
146
+ invoke: [{ instrument: occurrence, action: create, input: { schedule: { field: self.id }, position: { literal: 1 } } }]
147
+ }
94
148
  }
95
149
  action activate { from: draft, to: active, actor: { party: payer } }
96
150
  action complete {
@@ -98,20 +152,41 @@ instrument schedule(payer: party, payee: party, amount: money = runtime, count:
98
152
  requires count of { instrument: occurrence, reference: "schedule", anchor: self.id, states: [pending], limit: 366 } == 0
99
153
  }
100
154
  action cancel { from: [draft, active], to: cancelled, actor: { party: payer } }
155
+ action stop { from: active, to: cancelled, actor: { party: payee } }
101
156
  records {
102
157
  occurrence: {
103
158
  fields {
104
- schedule: ref<parent>, position: integer(1, count), dueAt: date = at(self.schedule.dates, self.position)
105
- base: money = divide(self.schedule.amount, count)
106
- allocated: money = multiply(self.base, count)
107
- remainder: money = subtract(self.schedule.amount, self.allocated)
108
- first: integer = divide(1, self.position)
109
- adjustment: money = multiply(self.remainder, self.first)
110
- amount: money = sum(self.base, self.adjustment)
159
+ schedule: ref<parent>
160
+ when every is unset {
161
+ position: integer(1, count), dueAt: date = at(self.schedule.dates, self.position)
162
+ base: money = divide(self.schedule.amount, count)
163
+ allocated: money = multiply(self.base, count)
164
+ remainder: money = subtract(self.schedule.amount, self.allocated)
165
+ first: integer = divide(1, self.position)
166
+ adjustment: money = multiply(self.remainder, self.first)
167
+ amount: money = sum(self.base, self.adjustment)
168
+ }
169
+ when every is set {
170
+ position: integer, offset: integer = subtract(self.position, 1), following: integer = sum(self.position, 1)
171
+ dueAt: date = after(self.schedule.startsAt, every, self.offset)
172
+ amount: money = self.schedule.amount
173
+ }
111
174
  }
112
175
  lifecycle { states: [pending, paid, cancelled], initial: pending }
113
- action create { actor: { parent: parent }, input { schedule: ref<parent>, position: integer(1, count) }, requires unique "scheduled_position" on [self.schedule, self.position] }
114
- action pay { allowZero: true, from: pending, to: paid, actor: clock, due: { at: self.dueAt }, requires self.schedule in [active], moves self.amount from payer to payee }
176
+ action create {
177
+ actor: { parent: [parent, current()] }
178
+ input { schedule: ref<parent>, when every is unset { position: integer(1, count) } when every is set { position: integer } }
179
+ requires unique "scheduled_position" on [self.schedule, self.position]
180
+ }
181
+ action pay {
182
+ allowZero: true, from: pending, to: paid, actor: clock, due: { at: self.dueAt }
183
+ requires self.schedule in [active]
184
+ moves self.amount from payer to payee
185
+ when every is set {
186
+ when count is set { invoke: [{ instrument: current(), action: create, guard: { kind: compare, left: { field: self.position }, operator: "<", right: { literal: count } }, input: { schedule: { field: self.schedule }, position: { field: self.following } } }] }
187
+ when count is unset { invoke: [{ instrument: current(), action: create, input: { schedule: { field: self.schedule }, position: { field: self.following } } }] }
188
+ }
189
+ }
115
190
  action cancel { from: pending, to: cancelled, requires self.schedule in [cancelled] }
116
191
  }
117
192
  }
@@ -130,13 +205,13 @@ instrument pool(payer: party, payee: party, target: money, closes: date) {
130
205
  action fail {
131
206
  from: open, to: failed, actor: clock, due: { at: self.closesAt }
132
207
  requires self.held.balance < self.target
133
- invoke: [{ selection: { instrument: contribution, reference: "pool", anchor: self.id, states: [paid], limit: 366 }, action: refund, input: {} }]
208
+ invoke: [{ selection: { instrument: contribution, reference: "pool", anchor: self.id, states: [paid], limit: 253 }, action: refund, input: {} }]
134
209
  }
135
210
  records {
136
211
  contribution: {
137
212
  fields { pool: ref<parent>, payer: account of payer, amount: money, remaining: money? }
138
213
  lifecycle { states: [pending, paid, refunded], initial: pending }
139
- invariants count of { instrument: current(), reference: "pool", anchor: self.pool, states: [paid], limit: 366 } <= 366
214
+ invariants count of { instrument: current(), reference: "pool", anchor: self.pool, states: [paid], limit: 253 } <= 253
140
215
  action create { requires self.pool in [open] }
141
216
  action pay { from: pending, to: paid, actor: { party: payer }, deadline: { at: self.pool.closesAt }, requires self.pool in [open], calculate: [{ target: remaining, op: subtract, base: { field: self.pool.target }, subtract: [{ field: self.pool.held.balance }] }], requires self.amount <= self.remaining, moves self.amount from self.payer to self.pool.held economics { purpose: principal, sourceParty: payer } }
142
217
  action refund { from: paid, to: refunded, actor: { parent: parent }, moves self.amount from self.pool.held to self.payer economics { purpose: principal, sourceParty: programOperator } }
@@ -201,25 +276,60 @@ instrument late_fee(payer: party, payee: party, amount: money, cap: money, perio
201
276
  summary: "Charge a fixed fee for each period a due time stays unmet, up to a cap."
202
277
  fields {
203
278
  payer: account of payer, payee: account of payee, amount: money = amount, cap: money = cap, ceiling: money = multiply(self.amount, 366)
204
- dueAt: date, graceEndsAt: date?, firstAt: date?, nextAt: date?, followingAt: date?, charge: money?, left: money?, remaining: money?
279
+ dueAt: date, graceEndsAt: date?, firstAt: date?, nextAt: date?, followingAt: date?, charge: money?, left: money?, remaining: money?, stoppedAt: date?
280
+ stride: integer?, strideAt: date?, elapsed: integer?, lastAt: date?
205
281
  }
206
- lifecycle { states: [running, capped, stopped], initial: running }
282
+ lifecycle { states: [running, settling, capped, stopped], initial: running }
207
283
  action create {
208
- actor: { party: payer }
284
+ actor: { party: programOperator }
209
285
  requires self.amount > "0"
210
286
  requires self.cap >= self.amount
211
287
  requires self.cap <= self.ceiling
212
- calculate: [{ target: graceEndsAt, op: shift, date: { field: self.dueAt }, milliseconds: { literal: grace }, direction: after }, { target: firstAt, op: shift, date: { field: self.graceEndsAt }, milliseconds: { literal: period }, direction: after }]
213
- set: { nextAt: { field: self.firstAt }, left: { field: self.cap } }
288
+ calculate: [{ target: graceEndsAt, op: shift, date: { field: self.dueAt }, milliseconds: { literal: grace }, direction: after }, { target: firstAt, op: shift, date: { field: self.graceEndsAt }, milliseconds: { literal: period }, direction: after }, { target: stride, op: divide, amount: { literal: period }, divisor: { literal: 1000 }, rounding: floor }, { target: strideAt, op: step, date: { field: self.graceEndsAt }, period: { literal: "PT1S" }, times: { field: self.stride } }, { target: elapsed, op: sum, values: [{ literal: 0 }] }]
289
+ requires self.strideAt == self.firstAt
290
+ set: { nextAt: { field: self.firstAt }, left: { field: self.cap }, lastAt: { literal: "1970-01-01T00:00:00.000Z" } }
214
291
  }
215
292
  action charge {
216
293
  summary: "Charge late fee"
217
294
  from: running, to: running, actor: clock, due: { at: self.nextAt }
295
+ calculate: [{ target: charge, op: minimum, values: [{ field: self.amount }, { field: self.left }] }, { target: remaining, op: subtract, base: { field: self.left }, subtract: [{ field: self.charge }] }, { target: followingAt, op: shift, date: { field: self.nextAt }, milliseconds: { literal: period }, direction: after }, { target: elapsed, op: sum, values: [{ field: self.elapsed }, { field: self.stride }] }]
296
+ set: { left: { field: self.remaining }, nextAt: { field: self.followingAt }, lastAt: { field: self.nextAt } }
297
+ moves self.charge from payer to payee economics { recipient: payee, company: { purpose: earning, sourceParty: payer }, participant: { purpose: pass_through, sourceParty: payer } }
298
+ invoke: [{ selection: { instrument: current(), reference: "payer", anchor: self.payer, states: [running], limit: 1, where: { id: { field: self.id }, left: { literal: "0" } } }, action: finish, input: {} }]
299
+ }
300
+ action reschedule {
301
+ summary: "Move the due time"
302
+ from: running, to: running, actor: { party: payee }
303
+ subject { dueAt: date }
304
+ invoke: [{ reference: self.id, action: follow, input: {} }]
305
+ }
306
+ action follow {
307
+ summary: "Follow the due time"
308
+ from: running, to: running, follows: dueAt
309
+ subject { dueAt: date }
310
+ // Fees run from the dueAt set at create, so the date only moves later.
311
+ requires subject.dueAt >= self.dueAt
312
+ calculate: [{ target: graceEndsAt, op: shift, date: { field: subject.dueAt }, milliseconds: { literal: grace }, direction: after }, { target: firstAt, op: shift, date: { field: self.graceEndsAt }, milliseconds: { literal: period }, direction: after }, { target: followingAt, op: step, date: { field: self.firstAt }, period: { literal: "PT1S" }, times: { field: self.elapsed } }]
313
+ requires self.followingAt > self.lastAt
314
+ set: { dueAt: { field: subject.dueAt }, nextAt: { field: self.followingAt } }
315
+ }
316
+ action collect {
317
+ summary: "Collect owed late fee"
318
+ from: settling, to: settling, actor: clock, due: { at: self.nextAt }
218
319
  calculate: [{ target: charge, op: minimum, values: [{ field: self.amount }, { field: self.left }] }, { target: remaining, op: subtract, base: { field: self.left }, subtract: [{ field: self.charge }] }, { target: followingAt, op: shift, date: { field: self.nextAt }, milliseconds: { literal: period }, direction: after }]
219
320
  set: { left: { field: self.remaining }, nextAt: { field: self.followingAt } }
220
321
  moves self.charge from payer to payee economics { recipient: payee, company: { purpose: earning, sourceParty: payer }, participant: { purpose: pass_through, sourceParty: payer } }
221
- invoke: [{ selection: { instrument: current(), reference: "payer", anchor: self.payer, states: [running], limit: 1, where: { id: { field: self.id }, left: { literal: "0" } } }, action: finish, input: {} }]
322
+ invoke: [
323
+ { reference: self.id, action: settle, guard: { kind: compare, left: { field: self.nextAt }, operator: ">", right: { field: self.stoppedAt } }, input: {} }
324
+ { selection: { instrument: current(), reference: "payer", anchor: self.payer, states: [settling], limit: 1, where: { id: { field: self.id }, left: { literal: "0" } } }, action: finish, input: {} }
325
+ ]
326
+ }
327
+ action finish { from: [running, settling], to: capped, actor: { parent: current() } }
328
+ action stop {
329
+ from: running, to: settling, actor: { party: payee }
330
+ set: { stoppedAt: { field: self.now } }
331
+ invoke: [{ reference: self.id, action: settle, guard: { kind: compare, left: { field: self.nextAt }, operator: ">", right: { field: self.stoppedAt } }, input: {} }]
222
332
  }
223
- action finish { from: running, to: capped, actor: { parent: current() } }
224
- action stop { from: running, to: stopped, actor: { party: payee } }
333
+ action settle { from: settling, to: stopped, actor: { parent: current() } }
334
+ action waive { from: settling, to: stopped, actor: { party: payee } }
225
335
  }
package/std/reporting.hsx CHANGED
@@ -119,19 +119,19 @@ instrument portfolio(on: ref<financing.installments>[], default_days: integer(1,
119
119
  },
120
120
  {
121
121
  name: "principal",
122
- field: "principalReceivable.balance",
122
+ field: "openPrincipal",
123
123
  type: { kind: "money", currency: "SAR" },
124
124
  required: true
125
125
  },
126
126
  {
127
127
  name: "profit",
128
- field: "profitReceivable.balance",
128
+ field: "openProfit",
129
129
  type: { kind: "money", currency: "SAR" },
130
130
  required: true
131
131
  },
132
132
  {
133
133
  name: "earnedProfit",
134
- field: "profitEarned.balance",
134
+ field: "earnedProfit",
135
135
  type: { kind: "money", currency: "SAR" },
136
136
  required: true
137
137
  },
@@ -468,19 +468,19 @@ instrument portfolio(on: ref<financing.installments>[], default_days: integer(1,
468
468
  },
469
469
  {
470
470
  name: "principal",
471
- field: "principalReceivable.balance",
471
+ field: "openPrincipal",
472
472
  type: { kind: "money", currency: "SAR" },
473
473
  required: true
474
474
  },
475
475
  {
476
476
  name: "profit",
477
- field: "profitReceivable.balance",
477
+ field: "openProfit",
478
478
  type: { kind: "money", currency: "SAR" },
479
479
  required: true
480
480
  },
481
481
  {
482
482
  name: "earnedProfit",
483
- field: "profitEarned.balance",
483
+ field: "earnedProfit",
484
484
  type: { kind: "money", currency: "SAR" },
485
485
  required: true
486
486
  },