@hyperscale0/hsx 1.0.336 → 1.0.338
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/dist/src/attachment-economics.d.ts +18 -0
- package/dist/src/attachment-economics.d.ts.map +1 -1
- package/dist/src/attachment-economics.js +82 -0
- package/dist/src/attachment-economics.js.map +1 -1
- package/dist/src/catalog-program.d.ts +1 -1
- package/dist/src/catalog-program.d.ts.map +1 -1
- package/dist/src/catalog-program.js +1 -1
- package/dist/src/compile.d.ts.map +1 -1
- package/dist/src/compile.js +100 -36
- package/dist/src/compile.js.map +1 -1
- package/dist/src/examples-bundle.js +1 -1
- package/dist/src/examples-bundle.js.map +1 -1
- package/dist/src/language.d.ts.map +1 -1
- package/dist/src/language.js +1 -0
- package/dist/src/language.js.map +1 -1
- package/dist/src/std-bundle.js +1 -1
- package/dist/src/std-bundle.js.map +1 -1
- package/dist/src/template-guide.d.ts.map +1 -1
- package/dist/src/template-guide.js +70 -19
- package/dist/src/template-guide.js.map +1 -1
- package/examples/camera-rental.hsx +3 -0
- package/package.json +4 -4
- package/src/attachment-economics.ts +103 -0
- package/src/catalog-program.ts +1 -1
- package/src/compile.ts +124 -51
- package/src/examples-bundle.ts +1 -1
- package/src/language.ts +1 -0
- package/src/std-bundle.ts +1 -1
- package/src/template-guide.ts +106 -23
- package/std/money.hsx +77 -23
package/src/std-bundle.ts
CHANGED
|
@@ -9,7 +9,7 @@ export const BUNDLED_STD_FILES: ReadonlyMap<string, string> = new Map<string, st
|
|
|
9
9
|
["insurance.hsx", "header insurance\n\n// Economics: Company commission is earning; refunds reverse its captured transfer.\n// Outside broker commissions and insurer premiums are pass-through.\n// Claim moves remain unclassified until their money purpose is declared.\n\n// Expose cover.create, activate and cancel separately. The adapter names a\n// registered boundary adapter (conformance_boundary in sandbox). covers is an\n// optional link to a sibling attachment; the attached object is already the\n// insured thing. claim_limit is the sum insured: approved and paid claims on one\n// cover, across every claim attachment, never total more. Activation binds adapter;\n// each slice needs a separately created unique period and an active cover to collect.\n// Collection debits holder for insurerPremium to the insurer and commission to\n// broker as two moves. Broker defaults to programOperator; bind a declared business\n// for a distinct agency. The two amounts sum to premium. Zero portions are allowed.\n// Cover.cancel invokes refund only on paid slices, before each startsAt. It returns\n// insurerPremium from insurer and commission from broker to holder. Active or\n// expired periods are excluded; insufficient recipient funds refuse the reversal.\n// Slice refund is parent-owned, not a public alias. Expose cover.cancel instead.\n// Claims require active cover. Expose claim.create, approve, deny and pay as needed.\n// Inspector approves OR denies from submitted. After approval only pay or clock\n// expiry can consume the reservation; deny is never available after approval.\n// Inspector is a bound party, not necessarily staff. These are ledger transfers;\n// outside settlement needs the boundary protocol, not a claim of provider success.\n\n// Cost: each slice collection or refund declares two transfers. Claim approval\n// reserves once; pay posts once and expiry voids once. Cover cancellation adds\n// bounded slice invocations. Run hsx cost for composed counts, not provider prices.\n\ninstrument cover(holder: party, adapter: text, commission: percent = 0%, broker: party = programOperator, covers: ref?, premium: money = runtime, claim_limit: money = runtime) {\n summary: \"Recurring cover bound to an outside insurer across an ADL boundary.\"\n fields { covers: ref<covers>?, holder: account of holder, insurer: account(adapter(adapter), cash, \"premium\"), expiresAt: date, claimLimit: money = claim_limit }\n lifecycle { states: [pending, active, cancelled, expired], initial: pending }\n action create { actor: { party: holder } }\n action activate {\n from: pending, to: active, actor: { party: programOperator }\n subject { adapter: adapter }\n }\n action cancel {\n from: active, to: cancelled, actor: { party: holder }\n invoke: [{ selection: { instrument: slice, reference: \"cover\", anchor: self.id, states: [paid], limit: 366 }, action: refund, input: {} }]\n }\n action expire { from: active, to: expired, actor: clock, due: { at: self.expiresAt } }\n records {\n slice: {\n summary: \"A whole premium period, collected once and refundable only before it starts.\"\n fields {\n cover: ref<parent>, premium: money = premium, startsAt: date, endsAt: date\n commission: money = rate(self.premium, commission)\n insurerPremium: money = subtract(self.premium, self.commission), commissionReceipt: text?\n }\n lifecycle { states: [pending, paid, active, expired, refunded], initial: pending }\n action create {\n requires self.startsAt < self.endsAt\n requires unique \"premium_period\" on [self.cover, self.startsAt, self.endsAt]\n }\n action collect {\n allowZero: true\n requires self.premium > \"0\"\n from: pending, to: paid, actor: { party: holder }\n requires self.cover in [active]\n moves self.insurerPremium from holder to self.cover.insurer economics { purpose: pass_through, sourceParty: holder }\n moves self.commission from holder to broker capture \"commissionReceipt\" economics { recipient: broker, company: { purpose: earning, sourceParty: holder }, participant: { purpose: pass_through, sourceParty: holder } }\n }\n action start { from: paid, to: active, actor: clock, due: { at: self.startsAt } }\n action expire { from: active, to: expired, actor: clock, due: { at: self.endsAt } }\n action refund {\n allowZero: true\n requires self.premium > \"0\"\n from: paid, to: refunded, actor: { parent: parent }, deadline: { at: self.startsAt }\n moves self.insurerPremium from self.cover.insurer to holder economics { purpose: pass_through, sourceParty: programOperator }\n moves self.commission from broker to holder economics { recipient: broker, company: { purpose: earning, sourceParty: broker, reversalOf: self.commissionReceipt }, participant: { purpose: pass_through, sourceParty: broker } }\n }\n }\n }\n}\n\ninstrument claim(cover: ref<insurance.cover>, inspector: party) {\n summary: \"A claim decision reserves one claim payment before settlement.\"\n fields { cover: ref<cover>, amount: money, expiresAt: date, evidence: text, reservation: text? }\n lifecycle { states: [submitted, approved, paid, denied, expired], initial: submitted }\n invariants sum \"amount\" of { instrument: all(insurance.claim), reference: \"cover\", anchor: self.cover, states: [approved, paid], limit: 366 } <= self.cover.claimLimit\n action create { requires self.cover in [active] }\n action approve {\n from: submitted, to: approved\n actor: { party: inspector }\n deadline: { at: self.expiresAt }\n moves reserve self.amount from self.cover.insurer to self.cover.holder capture \"reservation\"\n }\n action pay { from: approved, to: paid, actor: { party: inspector }, deadline: { at: self.expiresAt }, moves post self.reservation }\n action deny { from: submitted, to: denied, actor: { party: inspector } }\n action expire { from: approved, to: expired, actor: clock, due: { at: self.expiresAt }, moves void self.reservation }\n}\n"],
|
|
10
10
|
["lending.hsx", "header lending\n\n// Economics: Funding and commitment returns are principal. Distribution tax is\n// pass-through; own-account fee and claim allocations are internal. Mixed\n// principal/profit distributions and unbound source owners remain unclassified.\n\n// Round target is plan.principal from runtime subject.price, not its credit limit.\n// The operator opens the round and closes it before closesAt. Expose\n// round.create and close plus commitment.create and withdraw. A commitment\n// debits the bound investor's original active wallet before closesAt. Investor cap\n// aggregates by investor across wallets. A round still open at closesAt fails,\n// even when fully committed, and refunds those original wallets.\n// Closing needs the exact target; it funds plan.capital and does not disburse the\n// loan. Expose plan.disburse separately after signing and limit approval.\n// Distribution.create needs a recorded settlement matching the plan. Only the\n// payout consumes it: distribute_cash or distribute_loss claims the settlement\n// and marks it distributed, so the plan's unwind_paid keeps that profit, while an\n// abandoned distribution leaves the settlement refundable.\n// Prepare cash or loss first, then create shares against funded commitments of\n// that round. Distribution needs pending share weights equal to the round target.\n// Cash fee is a percentage of retained profit, VAT a percentage of that fee.\n// Principal plus retained profit less fee and VAT goes to weighted wallets;\n// floored residual goes to residual_to. Loss mode moves claim balances, not cash.\n// Child share execution is parent-owned. No report or clock proves outside payout.\n\n// Cost: round.create declares one owned account. Close transfers its balance\n// and invokes commitments; failure invokes their refunds. Distribution adds moves\n// and bounded share invocations. Run hsx cost for counts after policy selection.\n\ninstrument 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)) {\n summary: \"A threshold funding round; failed rounds return each original wallet contribution.\"\n fields {\n borrower: account of borrower, plan: ref<plan>, price: money = self.plan.principal, closesAt: date\n minimumTicket: money = minimum_ticket, maximumTicket: money = rate(self.price, investor_cap)\n held: account of self\n }\n lifecycle { states: [open, funded, failed], initial: open }\n action create { actor: { party: operator }, requires unique \"funding_plan\" on [self.plan]; requires self.borrower == self.plan.borrower }\n action close {\n from: open, to: funded, actor: { party: operator }\n requires sum \"amount\" of { instrument: commitments, reference: \"round\", anchor: self.id, states: [committed], limit: 366 } == self.price\n requires self.held.balance == self.price\n moves self.price from self.held to self.plan.capital economics { purpose: principal, sourceParty: programOperator }\n invoke: [{ selection: { instrument: commitments, reference: \"round\", anchor: self.id, states: [committed], limit: 366 }, action: fund, input: {} }]\n }\n action fail {\n from: open, to: failed, actor: clock, due: { at: self.closesAt }\n invoke: [{ selection: { instrument: commitments, reference: \"round\", anchor: self.id, states: [committed], limit: 366 }, action: refund, input: {} }]\n }\n}\n\ninstrument commitment(round: ref<lending.round>, wallet: ref<wallet.balance>, investor: party = party(person)) {\n summary: \"One investor's original-wallet contribution, capped cumulatively within the round.\"\n fields { round: ref<round>, wallet: ref<wallet>, investor: account of investor, amount: money, loss: account(investor, claim, \"loss\") }\n lifecycle { states: [committed, funded, withdrawn, refunded], initial: committed }\n 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\n invariants sum \"amount\" of { instrument: all(lending.commitment), reference: \"round\", anchor: self.round, states: [committed, funded], limit: 366 } <= self.round.price\n action create {\n actor: { party: investor }\n requires self.round in [open]\n requires self.wallet in [active]\n requires self.wallet.holder == self.investor\n requires self.amount >= self.round.minimumTicket\n deadline: { at: self.round.closesAt }\n moves self.amount from self.wallet.held to self.round.held economics { purpose: principal, sourceParty: programOperator }\n }\n action fund { from: committed, to: funded, actor: { parent: round } }\n action withdraw {\n from: committed, to: withdrawn, actor: { party: investor }\n requires self.round in [open]\n deadline: { at: self.round.closesAt }\n moves self.amount from self.round.held to self.wallet.held economics { purpose: principal, sourceParty: programOperator }\n }\n action refund {\n from: committed, to: refunded, actor: { parent: round }\n moves self.amount from self.round.held to self.wallet.held economics { purpose: principal, sourceParty: programOperator }\n }\n}\n\ninstrument distribution(round: ref<lending.round>, receipt: ref, commitments: ref<lending.commitment> = object(lending.commitment), mode: enum(cash, loss) = cash, residual_to: party = programOperator, operator: party = programOperator, fee: percent = 1%, tax: percent = 15%) {\n summary: \"Frozen weighted shares, floored once, with residual assigned to the chosen party.\"\n fields {\n round: ref<round>, source: ref<receipt>, mode: enum(cash, loss) = mode\n principal: money?, profit: money?, grossTotal: money?, charge: money?, vat: money?, net: money?\n held: account of self, losses: account(self, claim), residualLoss: account(residual_to, claim, \"loss\")\n }\n lifecycle { states: [pending, cash_ready, loss_ready, distributed], initial: pending }\n action create {\n requires self.round in [funded]; requires self.source in [recorded]; requires self.source.plan == self.round.plan\n }\n action prepare_cash {\n from: pending, to: cash_ready\n requires self.mode == \"cash\"; requires self.source.kind == \"cash\"; requires self.source in [recorded]\n calculate: [\n { target: \"principal\", op: \"sum\", values: [{ field: self.source.principal }] }\n { target: \"profit\", op: \"sum\", values: [{ field: self.source.retainedProfit }] }\n { target: \"charge\", op: \"rate\", base: { field: self.profit }, bps: { literal: fee }, rounding: \"floor\" }\n { target: \"vat\", op: \"rate\", base: { field: self.charge }, bps: { literal: tax }, rounding: \"floor\" }\n { target: \"grossTotal\", op: \"sum\", values: [{ field: self.principal }, { field: self.profit }] }\n { target: \"net\", op: \"subtract\", base: { field: self.grossTotal }, subtract: [{ field: self.charge }, { field: self.vat }] }\n ]\n }\n action prepare_loss {\n from: pending, to: loss_ready\n requires self.mode == \"loss\"; requires self.source.kind == \"loss\"; requires self.source in [recorded]\n calculate: [{ target: \"net\", op: \"sum\", values: [{ field: self.source.loss }] }]\n }\n action distribute_cash { allowZero: true,\n from: cash_ready, to: distributed, actor: { party: operator }\n requires self.source in [recorded]; requires unique \"settlement_consumption\" on [self.source.plan, self.source.identity]\n requires sum \"weight\" of { instrument: share, reference: \"distribution\", anchor: self.id, states: [pending], limit: 366 } == self.round.price\n 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 }\n 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: {} }]\n }\n action residual_cash { allowZero: true,\n from: distributed, to: distributed, actor: { parent: current() }\n moves self.held.balance from self.held to residual_to\n }\n action distribute_loss { allowZero: true,\n from: loss_ready, to: distributed, actor: { party: operator }\n requires self.source in [recorded]; requires unique \"settlement_consumption\" on [self.source.plan, self.source.identity]\n requires sum \"weight\" of { instrument: share, reference: \"distribution\", anchor: self.id, states: [pending], limit: 366 } == self.round.price\n moves self.net from self.source.lossAccount to self.losses\n 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: {} }]\n }\n action residual_loss { allowZero: true,\n from: distributed, to: distributed, actor: { parent: current() }\n moves self.losses.balance from self.losses to self.residualLoss economics { purpose: internal, sourceParty: programOperator }\n }\n records {\n share: {\n fields {\n distribution: ref<parent>, commitment: ref<commitments>\n weight: money = self.commitment.amount\n gross: money = ratio(self.distribution.net, self.weight, self.distribution.round.price)\n }\n lifecycle { states: [pending, paid], initial: pending }\n action create { requires self.distribution in [cash_ready, loss_ready]; requires self.commitment in [funded]; requires unique \"distribution_weight\" on [self.distribution, self.commitment]; requires self.commitment.round == self.distribution.round }\n action cash { allowZero: true,\n from: pending, to: paid, actor: { parent: parent }\n moves self.gross from self.distribution.held to self.commitment.wallet.held\n }\n action loss { allowZero: true, from: pending, to: paid, actor: { parent: parent }, moves self.gross from self.distribution.losses to self.commitment.loss economics { purpose: internal, sourceParty: programOperator } }\n }\n }\n}\n"],
|
|
11
11
|
["marketplace.hsx", "header marketplace\n\n// Economics: Deposit funding, escrow credit and returns are principal. Retention is\n// earning for the company or passes the buyer's cash through to a bound outside recipient.\n\n// Listing, order and reservation are separate agreements. Expose their create\n// and decision actions individually. Order fulfillment does not move escrow cash;\n// only the fulfiller marks it, the Product operator unless the seller is bound.\n// Pair an order with escrow.hold and expose its fund, deliver, accept, dispute,\n// verify_return and refund aliases. Refund needs disputed -> return_verified,\n// with payee evidence, then returns the full price to payer; accept pays payee.\n// A reservation holds its own deposit. Activate requires an active listing and\n// no placed order. Only a declared converter may invoke credit. A financing\n// plan on the same hold credits it when the lender disburses. Credit consumes the\n// deposit once, credits at most the contribution, and returns excess to buyer.\n// Expose cancel, seller_cancel or operator_cancel for the intended actors.\n// Buyer cancel and expiry use configured refund percentages; seller/operator\n// cancellation return the whole reservation balance. Converted money is in escrow,\n// so subsequent return verification and refund use the escrow aliases.\n\n// Cost: listing and order actions declare no transfers. Reservation.create\n// declares one owned account; activate declares one deposit transfer. Cancellation\n// and credit split that balance into moves. Run hsx cost for the composed counts.\n\ninstrument listing(seller: party) {\n revisioned: true\n summary: \"A seller's offer and its asking price.\"\n fields { seller: account of seller, title: text, price: money }\n lifecycle { states: [draft, active, withdrawn, sold], initial: draft }\n action create { actor: { party: seller } }\n action publish {\n from: draft, to: active, actor: { party: seller }\n }\n action withdraw { from: active, to: withdrawn, actor: { party: seller }, requires count of { instrument: all(marketplace.reservation), reference: \"listing\", anchor: self.id, states: [active], limit: 366 } == 0 }\n action sell { from: active, to: sold, actor: { party: seller }, requires count of { instrument: all(marketplace.reservation), reference: \"listing\", anchor: self.id, states: [active], limit: 366 } == 0 }\n}\n\ninstrument order(listing: ref<marketplace.listing>, buyer: party, fulfiller: party = programOperator) {\n revisioned: true\n summary: \"A buyer's order for one listing at its declared price.\"\n fields { listing: ref<listing>, buyer: account of buyer, price: money, reservations: integer?, ownReservations: integer?, blockingQuotes: integer?, ownBlockingQuotes: integer? }\n lifecycle { states: [quoted, placed, fulfilled, cancelled], initial: quoted }\n action create {\n actor: { party: buyer }\n requires self.listing in [active]\n calculate: [\n { target: blockingQuotes, op: aggregate, selection: { instrument: all(marketplace.reservation), reference: \"listing\", anchor: self.listing, states: [active], limit: 366, where: { quotePolicy: { literal: \"blocked\" } } }, measure: count }\n { target: ownBlockingQuotes, op: aggregate, selection: { instrument: all(marketplace.reservation), reference: \"listing\", anchor: self.listing, states: [active], limit: 366, where: { quotePolicy: { literal: \"blocked\" }, buyer: { field: self.buyer } } }, measure: count }\n ]\n requires self.blockingQuotes == self.ownBlockingQuotes\n requires self.price == self.listing.price\n }\n action commit {\n from: [quoted, placed], to: placed\n requires self.listing in [active]\n calculate: [\n { target: reservations, op: aggregate, selection: { instrument: all(marketplace.reservation), reference: \"listing\", anchor: self.listing, states: [active], limit: 366 }, measure: count }\n { target: ownReservations, op: aggregate, selection: { instrument: all(marketplace.reservation), reference: \"listing\", anchor: self.listing, states: [active], limit: 366, where: { buyer: { field: self.buyer } } }, measure: count }\n ]\n requires self.reservations == self.ownReservations\n }\n invariants count of { instrument: all(marketplace.order), reference: \"listing\", anchor: self.listing, states: [placed, fulfilled], limit: 366 } <= 1\n action fulfill { from: placed, to: fulfilled, actor: { party: fulfiller } }\n action cancel { from: [quoted, placed], to: cancelled, actor: { party: buyer } }\n}\n\ninstrument reservation(listing: ref<marketplace.listing>, funds: ref<escrow.hold>, converters: ref[], buyer: party, seller: party, expires_after: duration = 48h, deposit: money = 1000 SAR, cancellation_refund: percent = 100%, expiry_refund: percent = 100%, retained_to: party = programOperator, operator: party = programOperator, competing_quotes: enum(allowed, blocked) = allowed) {\n revisioned: true\n summary: \"Exclusive timed listing hold with explicit deposit credit and refund.\"\n fields {\n listing: ref<listing>, funds: ref<funds>, buyer: account of buyer, seller: account of seller, price: money, terms: text\n held: account of self, quotePolicy: enum(allowed, blocked) = competing_quotes, deposit: money = deposit, proposalExpiresAt: date = after(self.createdAt, expires_after), expiresAt: date?\n credit: money?, refund: money?, retained: money?, dispositionAt: date?, reason: text?\n }\n lifecycle { states: [proposed, active, converted, cancelled, expired], initial: proposed }\n invariants count of { instrument: all(marketplace.reservation), reference: \"listing\", anchor: self.listing, states: [active], limit: 366 } <= 1\n action create { actor: { party: buyer }, requires self.listing in [active]; requires self.seller == self.listing.seller; requires self.price == self.listing.price }\n action activate {\n from: proposed, to: active, actor: { party: buyer }, deadline: { at: self.proposalExpiresAt }\n calculate: [{ target: expiresAt, op: shift, date: { field: self.now }, milliseconds: { literal: expires_after }, direction: after }]\n requires self.listing in [active]; requires self.price == self.listing.price\n requires count of { instrument: all(marketplace.order), reference: \"listing\", anchor: self.listing, states: [placed, fulfilled], limit: 366 } == 0\n moves self.deposit from self.buyer to self.held economics { purpose: principal, sourceParty: buyer }\n }\n action credit { allowZero: true,\n from: active, to: converted, actor: { parent: converters }, deadline: { at: self.expiresAt }\n input { funds: ref<funds>, contribution: money }\n requires input.funds == self.funds; requires input.funds in [pending, funded]; requires input.funds.payer == self.buyer; requires input.funds.price == self.price\n requires input.contribution <= self.price\n calculate: [{ target: credit, op: minimum, values: [{ field: self.held.balance }, { field: input.contribution }] }, { target: refund, op: subtract, base: { field: self.held.balance }, subtract: [{ field: self.credit }] }]\n set: { dispositionAt: { field: self.now } }\n moves self.credit from self.held to input.funds.held economics { purpose: principal, sourceParty: programOperator }\n moves self.refund from self.held to self.buyer economics { purpose: principal, sourceParty: programOperator }\n }\n action cancel { allowZero: true,\n from: active, to: cancelled, actor: { party: buyer }, input { reason: text }\n calculate: [{ target: refund, op: rate, base: { field: self.held.balance }, bps: { literal: cancellation_refund }, rounding: floor }, { target: retained, op: subtract, base: { field: self.held.balance }, subtract: [{ field: self.refund }] }]\n set: { reason: { field: input.reason }, dispositionAt: { field: self.now } }\n moves self.refund from self.held to self.buyer economics { purpose: principal, sourceParty: programOperator }; moves self.retained from self.held to retained_to economics { recipient: retained_to, company: { purpose: earning, sourceParty: buyer }, participant: { purpose: pass_through, sourceParty: programOperator } }\n }\n action seller_cancel {\n from: active, to: cancelled, actor: { party: seller }, input { reason: text }\n set: { reason: { field: input.reason }, dispositionAt: { field: self.now } }\n moves self.held.balance from self.held to self.buyer economics { purpose: principal, sourceParty: programOperator }\n }\n action operator_cancel {\n from: active, to: cancelled, actor: { party: operator }, input { reason: text }\n set: { reason: { field: input.reason }, dispositionAt: { field: self.now } }\n moves self.held.balance from self.held to self.buyer economics { purpose: principal, sourceParty: programOperator }\n }\n action withdraw { from: proposed, to: cancelled, actor: { party: buyer } }\n action expire { allowZero: true,\n from: active, to: expired, actor: clock, due: { at: self.expiresAt }\n calculate: [{ target: refund, op: rate, base: { field: self.held.balance }, bps: { literal: expiry_refund }, rounding: floor }, { target: retained, op: subtract, base: { field: self.held.balance }, subtract: [{ field: self.refund }] }]\n set: { dispositionAt: { field: self.now } }\n moves self.refund from self.held to self.buyer economics { purpose: principal, sourceParty: programOperator }; moves self.retained from self.held to retained_to economics { recipient: retained_to, company: { purpose: earning, sourceParty: buyer }, participant: { purpose: pass_through, sourceParty: programOperator } }\n }\n action expire_proposal { from: proposed, to: expired, actor: clock, due: { at: self.proposalExpiresAt } }\n}\n"],
|
|
12
|
-
["money.hsx", "header money\n\n// Economics: Held funding and returns are principal. Release to the company realizes\n// earnings; release to another participant passes the payer's cash through. A late\n// fee charge is the same: earnings for the company, pass-through for another payee.\n// Generic transfers, splits, schedules, swaps, boundary payouts and metered fees\n// retain no assumed purpose.\n\n// Expose each attachment's create and caller actions explicitly. A transfer or\n// split pays once; a hold exposes fund, release and refund separately. Release\n// pays payee and refund returns payer. released_by runs release and defaults to\n// payee, so a shop that is payee keeps a deposit without acting for its customer.\n// Bind released_by: payer for an escrow-style hold that only the payer releases.\n// Only the payee refunds. A hold has no dispute or damage decision.\n// The payee proposes a schedule, which creates its count occurrences, and only\n// the payer activates or cancels it. Each due occurrence pays only while the\n// schedule is active. A due time promises no retry.\n// A pool accepts whole contributions only up to its remaining target and at most\n// 366 paid records. Expose pay to release the exact target. Clock failure refunds\n// original contributors; excess acceptance and partial contribution are absent.\n// A swap reserves both legs before exchange or voids reservations on expiry.\n// Payout instruct reserves across the selected adapter; confirm/reject require\n// matching terminal instruction evidence. An acknowledgement cannot settle it.\n// Metered bill charges a unique usage identity at the fixed unit price, only\n// under an active mandate the payer created: bills stop at its expiresAt, and at\n// most 366 billed events across every metered attachment on the mandate may sum\n// their payer debit, buyer fee and tax included, to its limit. The payer revokes\n// it to stop billing. Only the payee cancels its own unbilled usage.\n// The payer creates a late fee against its dueAt, which rename can bind to the\n// record's own due field. Once dueAt plus grace plus one period has passed, the\n// clock charges amount from payer to payee for each whole period, and the last\n// charge takes only what is left under cap; then the fee is capped. The payee runs\n// stop when the obligation is met, which ends charging. A charge the payer cannot\n// fund refuses as clock work and charging waits for its retry. Grace is at least\n// one second; there are no percentage or interest-style fees.\n\n// Cost: transfer.pay declares one transfer. Hold.create declares one owned account;\n// fund, release and refund each declare one transfer. Schedules and pools add bounded\n// child invocations. Each late fee charge declares one transfer, and cap may be at most\n// 366 times amount. Run hsx cost on the composed program for counts, not riyal prices.\n\ninstrument transfer(payer: party, payee: party, amount: money = runtime) {\n summary: \"One transfer between the bound parties.\"\n fields { amount: money = amount }\n lifecycle { states: [pending, paid, cancelled], initial: pending }\n action create { actor: { party: payer }, requires self.amount > \"0\" }\n action pay {\n from: pending, to: paid, actor: { party: payer }\n moves self.amount from payer to payee\n }\n action cancel { from: pending, to: cancelled, actor: { party: payer } }\n}\n\ninstrument hold(payer: party, payee: party, amount: money = runtime, released_by: party = payee) {\n summary: \"Hold the declared amount until release or refund.\"\n fields { payer: account of payer, payee: account of payee, amount: money = amount, price: money = self.amount, held: account of self }\n lifecycle { states: [pending, funded, released, refunded, cancelled], initial: pending }\n action create { }\n action fund {\n from: pending, to: funded, actor: { party: payer }\n moves self.amount from payer to self.held economics { purpose: principal, sourceParty: payer }\n }\n action confirm {\n from: pending, to: funded\n requires self.held.balance == self.amount\n }\n action release {\n from: funded, to: released, actor: { party: released_by }\n moves self.amount from self.held to payee economics { recipient: payee, company: { purpose: earning, sourceParty: payer }, participant: { purpose: pass_through, sourceParty: programOperator } }\n }\n action refund {\n from: funded, to: refunded, actor: { party: payee }\n moves self.amount from self.held to payer economics { purpose: principal, sourceParty: programOperator }\n }\n action cancel { from: pending, to: cancelled }\n}\n\ninstrument split(payer: party, amount: money = runtime, shares: split = { programOperator: 100% }) {\n summary: \"Split one amount; the last declared recipient receives the rounding remainder.\"\n fields { amount: money = amount }\n lifecycle { states: [pending, paid, cancelled], initial: pending }\n action create {}\n action pay { allowZero: true, requires self.amount > \"0\"; from: pending, to: paid, actor: { party: payer }, moves self.amount from payer shares shares }\n action cancel { from: pending, to: cancelled }\n}\n\ninstrument schedule(payer: party, payee: party, amount: money = runtime, count: integer(1, 366) = 12) {\n summary: \"A finite schedule whose dated pieces sum exactly to the declared total.\"\n fields { amount: money = amount, count: integer = count, dates: list(date, count) }\n lifecycle { states: [draft, active, complete, cancelled], initial: draft }\n action create {\n actor: { party: payee }\n invoke: [{ range: { count: { literal: count }, maximum: count, bind: position }, instrument: occurrence, action: create, input: { schedule: { field: self.id }, position: { field: position } } }]\n }\n action activate { from: draft, to: active, actor: { party: payer } }\n action complete {\n from: active, to: complete\n requires count of { instrument: occurrence, reference: \"schedule\", anchor: self.id, states: [pending], limit: 366 } == 0\n }\n action cancel { from: [draft, active], to: cancelled, actor: { party: payer } }\n records {\n occurrence: {\n fields {\n schedule: ref<parent>, position: integer(1, count), dueAt: date = at(self.schedule.dates, self.position)\n base: money = divide(self.schedule.amount, count)\n allocated: money = multiply(self.base, count)\n remainder: money = subtract(self.schedule.amount, self.allocated)\n first: integer = divide(1, self.position)\n adjustment: money = multiply(self.remainder, self.first)\n amount: money = sum(self.base, self.adjustment)\n }\n lifecycle { states: [pending, paid, cancelled], initial: pending }\n action create { actor: { parent: parent }, input { schedule: ref<parent>, position: integer(1, count) }, requires unique \"scheduled_position\" on [self.schedule, self.position] }\n 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 }\n action cancel { from: pending, to: cancelled, requires self.schedule in [cancelled] }\n }\n }\n}\n\ninstrument pool(payer: party, payee: party, target: money, closes: date) {\n summary: \"Contributions fund one threshold or return to their original contributor.\"\n fields { target: money = target, closesAt: date = closes, held: account of self }\n lifecycle { states: [open, paid, failed], initial: open }\n action create { }\n action pay {\n from: open, to: paid, actor: { party: payee }\n requires self.held.balance == self.target\n moves self.target from self.held to payee economics { recipient: payee, company: { purpose: earning, sourceParty: payer }, participant: { purpose: pass_through, sourceParty: programOperator } }\n }\n action fail {\n from: open, to: failed, actor: clock, due: { at: self.closesAt }\n requires self.held.balance < self.target\n invoke: [{ selection: { instrument: contribution, reference: \"pool\", anchor: self.id, states: [paid], limit: 366 }, action: refund, input: {} }]\n }\n records {\n contribution: {\n fields { pool: ref<parent>, payer: account of payer, amount: money, remaining: money? }\n lifecycle { states: [pending, paid, refunded], initial: pending }\n invariants count of { instrument: current(), reference: \"pool\", anchor: self.pool, states: [paid], limit: 366 } <= 366\n action create { requires self.pool in [open] }\n 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 } }\n action refund { from: paid, to: refunded, actor: { parent: parent }, moves self.amount from self.pool.held to self.payer economics { purpose: principal, sourceParty: programOperator } }\n }\n }\n}\n\ninstrument swap(first: party, second: party, first_amount: money, second_amount: money, expires: date) {\n summary: \"Two reservations settle together or both return.\"\n fields { firstAmount: money = first_amount, secondAmount: money = second_amount, expiresAt: date = expires, firstReceipt: text?, secondReceipt: text? }\n lifecycle { states: [pending, first_ready, second_ready, both_ready, exchanged, cancelled], initial: pending }\n action create {}\n action fund_first { from: pending, to: first_ready, actor: { party: first }, deadline: { at: self.expiresAt }, moves reserve self.firstAmount from first to second capture \"firstReceipt\" }\n action fund_second { from: first_ready, to: both_ready, actor: { party: second }, deadline: { at: self.expiresAt }, moves reserve self.secondAmount from second to first capture \"secondReceipt\" }\n action start_second { from: pending, to: second_ready, actor: { party: second }, deadline: { at: self.expiresAt }, moves reserve self.secondAmount from second to first capture \"secondReceipt\" }\n action finish_first { from: second_ready, to: both_ready, actor: { party: first }, deadline: { at: self.expiresAt }, moves reserve self.firstAmount from first to second capture \"firstReceipt\" }\n action expire_second { from: second_ready, to: cancelled, actor: clock, due: { at: self.expiresAt }, moves void self.secondReceipt }\n action exchange { from: both_ready, to: exchanged, actor: { party: first }, moves post self.firstReceipt; moves post self.secondReceipt }\n action cancel { from: pending, to: cancelled }\n action expire_first { from: first_ready, to: cancelled, actor: clock, due: { at: self.expiresAt }, moves void self.firstReceipt }\n action expire_both { from: both_ready, to: cancelled, actor: clock, due: { at: self.expiresAt }, moves void self.firstReceipt; moves void self.secondReceipt }\n}\n\ninstrument payout(payer: party, payee: party, adapter: text, amount: money = runtime) {\n summary: \"Pay the recipient when the provider confirms settlement.\"\n fields { amount: money = amount }\n lifecycle { states: [pending, instructed, settled, rejected], initial: pending }\n action create {}\n action instruct {\n from: pending, to: instructed, actor: { party: payer }\n moves reserve self.amount from payer to payee boundary adapter\n }\n action confirm {\n from: instructed, to: settled\n moves post instruct\n }\n action reject {\n from: instructed, to: rejected\n moves void instruct\n }\n}\n\ninstrument mandate(payer: party, payee: party) {\n summary: \"A payer's permission for one payee to charge up to a total until it expires.\"\n fields { payer: account of payer, payee: account of payee, limit: money, expiresAt: date }\n lifecycle { states: [active, revoked], initial: active }\n action create { actor: { party: payer }, requires self.limit > \"0\" }\n action revoke { from: active, to: revoked, actor: { party: payer } }\n}\n\ninstrument metered(payer: party, payee: party, unit_price: money, units: integer = 1, fee: fee = { seller: 0% }, mandate: ref<money.mandate> = object(money.mandate)) {\n summary: \"A unique usage event priced once at the fixed unit price.\"\n fields { usageIdentity: text, mandate: ref<mandate>, payer: account of payer, payee: account of payee, unitPrice: money = unit_price, units: integer = units, amount: money = multiply(self.unitPrice, self.units) }\n lifecycle { states: [recorded, billed, cancelled], initial: recorded }\n invariants sum \"bill_move1_debit\" of { instrument: all(money.metered), reference: \"mandate\", anchor: self.mandate, states: [billed], limit: 366 } <= self.mandate.limit\n action create { requires unique \"usage_event\" on [self.usageIdentity]; requires self.mandate.payer == self.payer; requires self.mandate.payee == self.payee }\n action bill { allowZero: true, requires self.amount > \"0\"; from: recorded, to: billed, actor: { party: payee }, deadline: { at: self.mandate.expiresAt }, requires self.mandate in [active], moves self.amount from payer to payee fee fee }\n action cancel { from: recorded, to: cancelled, actor: { party: payee } }\n}\n\ninstrument late_fee(payer: party, payee: party, amount: money, cap: money, period: duration = 1d, grace: duration = 1h) {\n summary: \"Charge a fixed fee for each period a due time stays unmet, up to a cap.\"\n fields {\n payer: account of payer, payee: account of payee, amount: money = amount, cap: money = cap, ceiling: money = multiply(self.amount, 366)\n dueAt: date, graceEndsAt: date?, firstAt: date?, nextAt: date?, followingAt: date?, charge: money?, left: money?, remaining: money?\n }\n lifecycle { states: [running, capped, stopped], initial: running }\n action create {\n actor: { party: payer }\n requires self.amount > \"0\"\n requires self.cap >= self.amount\n requires self.cap <= self.ceiling\n 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 }]\n set: { nextAt: { field: self.firstAt }, left: { field: self.cap } }\n }\n action charge {\n summary: \"Charge late fee\"\n from: running, to: running, actor: clock, due: { at: self.nextAt }\n 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 }]\n set: { left: { field: self.remaining }, nextAt: { field: self.followingAt } }\n moves self.charge from payer to payee economics { recipient: payee, company: { purpose: earning, sourceParty: payer }, participant: { purpose: pass_through, sourceParty: payer } }\n invoke: [{ selection: { instrument: current(), reference: \"payer\", anchor: self.payer, states: [running], limit: 1, where: { id: { field: self.id }, left: { literal: \"0\" } } }, action: finish, input: {} }]\n }\n action finish { from: running, to: capped, actor: { parent: current() } }\n action stop { from: running, to: stopped, actor: { party: payee } }\n}\n"],
|
|
12
|
+
["money.hsx", "header money\n\n// Economics: Held funding and returns are principal. Release to the company realizes\n// earnings; release to another participant passes the payer's cash through. A late\n// fee charge is the same: earnings for the company, pass-through for another payee.\n// Generic transfers, splits, schedules, swaps, boundary payouts and metered fees\n// retain no assumed purpose.\n\n// Expose each attachment's create and caller actions explicitly. A transfer or\n// split pays once; a hold exposes fund, release and refund separately. Release\n// pays payee and refund returns payer. released_by runs release and defaults to\n// payee, so a shop that is payee keeps a deposit without acting for its customer.\n// Bind released_by: payer for an escrow-style hold that only the payer releases.\n// Only the payee refunds. A hold has no dispute or damage decision.\n// The payee proposes a schedule and only the payer activates or cancels it; the\n// payee stops it. Without every, create makes count occurrences on the listed\n// dates and they sum to amount. With every (1 month, 2 weeks), each occurrence\n// charges amount, the first at startsAt, and paying one creates the next, so only\n// one piece waits ahead. Months step from startsAt and clamp to the month's end.\n// create refuses a startsAt in the past. An authored count ends it after count\n// pieces; without one it runs until stopped or cancelled. Each due occurrence pays\n// only while the schedule is active, and one clock drain charges at most 126\n// pieces; the next drain continues a plan that fell further behind.\n// A pool accepts whole contributions only up to its remaining target and at most\n// 366 paid records. Expose pay to release the exact target. Clock failure refunds\n// original contributors; excess acceptance and partial contribution are absent.\n// A swap reserves both legs before exchange or voids reservations on expiry.\n// Payout instruct reserves across the selected adapter; confirm/reject require\n// matching terminal instruction evidence. An acknowledgement cannot settle it.\n// Metered bill charges a unique usage identity at the fixed unit price, only\n// under an active mandate the payer created: bills stop at its expiresAt, and at\n// most 366 billed events across every metered attachment on the mandate may sum\n// their payer debit, buyer fee and tax included, to its limit. The payer revokes\n// it to stop billing. Only the payee cancels its own unbilled usage.\n// The payer creates a late fee against its dueAt, which rename can bind to the\n// record's own due field. Once dueAt plus grace plus one period has passed, the\n// clock charges amount from payer to payee for each whole period, and the last\n// charge takes only what is left under cap; then the fee is capped. The payee runs\n// stop when the obligation is met. Stop keeps what was owed: each charge due at or\n// before it still posts, and the fee settles until the last of those posts, then\n// stops. A period still running at the stop is not charged. A charge the payer cannot fund refuses as clock work and charging waits\n// for its retry; the payee runs waive to forgive what is still owed. Grace is at\n// least one second; there are no percentage or interest-style fees.\n\n// Cost: transfer.pay declares one transfer. Hold.create declares one owned account;\n// fund, release and refund each declare one transfer. Schedules and pools add bounded\n// child invocations. Each late fee charge declares one transfer, and cap may be at most\n// 366 times amount. Run hsx cost on the composed program for counts, not riyal prices.\n\ninstrument transfer(payer: party, payee: party, amount: money = runtime) {\n summary: \"One transfer between the bound parties.\"\n fields { amount: money = amount }\n lifecycle { states: [pending, paid, cancelled], initial: pending }\n action create { actor: { party: payer }, requires self.amount > \"0\" }\n action pay {\n from: pending, to: paid, actor: { party: payer }\n moves self.amount from payer to payee\n }\n action cancel { from: pending, to: cancelled, actor: { party: payer } }\n}\n\ninstrument hold(payer: party, payee: party, amount: money = runtime, released_by: party = payee) {\n summary: \"Hold the declared amount until release or refund.\"\n fields { payer: account of payer, payee: account of payee, amount: money = amount, price: money = self.amount, held: account of self }\n lifecycle { states: [pending, funded, released, refunded, cancelled], initial: pending }\n action create { }\n action fund {\n from: pending, to: funded, actor: { party: payer }\n moves self.amount from payer to self.held economics { purpose: principal, sourceParty: payer }\n }\n action confirm {\n from: pending, to: funded\n requires self.held.balance == self.amount\n }\n action release {\n from: funded, to: released, actor: { party: released_by }\n moves self.amount from self.held to payee economics { recipient: payee, company: { purpose: earning, sourceParty: payer }, participant: { purpose: pass_through, sourceParty: programOperator } }\n }\n action refund {\n from: funded, to: refunded, actor: { party: payee }\n moves self.amount from self.held to payer economics { purpose: principal, sourceParty: programOperator }\n }\n action cancel { from: pending, to: cancelled }\n}\n\ninstrument split(payer: party, amount: money = runtime, shares: split = { programOperator: 100% }) {\n summary: \"Split one amount; the last declared recipient receives the rounding remainder.\"\n fields { amount: money = amount }\n lifecycle { states: [pending, paid, cancelled], initial: pending }\n action create {}\n action pay { allowZero: true, requires self.amount > \"0\"; from: pending, to: paid, actor: { party: payer }, moves self.amount from payer shares shares }\n action cancel { from: pending, to: cancelled }\n}\n\ninstrument schedule(payer: party, payee: party, amount: money = runtime, count: integer(1, 366) = 12, every: period?) {\n summary: \"Pieces due on listed dates that sum to the total, or one amount each period from a start date.\"\n fields {\n amount: money = amount\n when every is unset { count: integer = count, dates: list(date, count) }\n when every is set { startsAt: date }\n }\n lifecycle { states: [draft, active, complete, cancelled], initial: draft }\n action create {\n actor: { party: payee }\n 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 } } }] }\n when every is set {\n requires self.startsAt >= self.now\n invoke: [{ instrument: occurrence, action: create, input: { schedule: { field: self.id }, position: { literal: 1 } } }]\n }\n }\n action activate { from: draft, to: active, actor: { party: payer } }\n action complete {\n from: active, to: complete\n requires count of { instrument: occurrence, reference: \"schedule\", anchor: self.id, states: [pending], limit: 366 } == 0\n }\n action cancel { from: [draft, active], to: cancelled, actor: { party: payer } }\n action stop { from: active, to: cancelled, actor: { party: payee } }\n records {\n occurrence: {\n fields {\n schedule: ref<parent>\n when every is unset {\n position: integer(1, count), dueAt: date = at(self.schedule.dates, self.position)\n base: money = divide(self.schedule.amount, count)\n allocated: money = multiply(self.base, count)\n remainder: money = subtract(self.schedule.amount, self.allocated)\n first: integer = divide(1, self.position)\n adjustment: money = multiply(self.remainder, self.first)\n amount: money = sum(self.base, self.adjustment)\n }\n when every is set {\n position: integer, offset: integer = subtract(self.position, 1), following: integer = sum(self.position, 1)\n dueAt: date = after(self.schedule.startsAt, every, self.offset)\n amount: money = self.schedule.amount\n }\n }\n lifecycle { states: [pending, paid, cancelled], initial: pending }\n action create {\n actor: { parent: [parent, current()] }\n input { schedule: ref<parent>, when every is unset { position: integer(1, count) } when every is set { position: integer } }\n requires unique \"scheduled_position\" on [self.schedule, self.position]\n }\n action pay {\n allowZero: true, from: pending, to: paid, actor: clock, due: { at: self.dueAt }\n requires self.schedule in [active]\n moves self.amount from payer to payee\n when every is set {\n 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 } } }] }\n when count is unset { invoke: [{ instrument: current(), action: create, input: { schedule: { field: self.schedule }, position: { field: self.following } } }] }\n }\n }\n action cancel { from: pending, to: cancelled, requires self.schedule in [cancelled] }\n }\n }\n}\n\ninstrument pool(payer: party, payee: party, target: money, closes: date) {\n summary: \"Contributions fund one threshold or return to their original contributor.\"\n fields { target: money = target, closesAt: date = closes, held: account of self }\n lifecycle { states: [open, paid, failed], initial: open }\n action create { }\n action pay {\n from: open, to: paid, actor: { party: payee }\n requires self.held.balance == self.target\n moves self.target from self.held to payee economics { recipient: payee, company: { purpose: earning, sourceParty: payer }, participant: { purpose: pass_through, sourceParty: programOperator } }\n }\n action fail {\n from: open, to: failed, actor: clock, due: { at: self.closesAt }\n requires self.held.balance < self.target\n invoke: [{ selection: { instrument: contribution, reference: \"pool\", anchor: self.id, states: [paid], limit: 366 }, action: refund, input: {} }]\n }\n records {\n contribution: {\n fields { pool: ref<parent>, payer: account of payer, amount: money, remaining: money? }\n lifecycle { states: [pending, paid, refunded], initial: pending }\n invariants count of { instrument: current(), reference: \"pool\", anchor: self.pool, states: [paid], limit: 366 } <= 366\n action create { requires self.pool in [open] }\n 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 } }\n action refund { from: paid, to: refunded, actor: { parent: parent }, moves self.amount from self.pool.held to self.payer economics { purpose: principal, sourceParty: programOperator } }\n }\n }\n}\n\ninstrument swap(first: party, second: party, first_amount: money, second_amount: money, expires: date) {\n summary: \"Two reservations settle together or both return.\"\n fields { firstAmount: money = first_amount, secondAmount: money = second_amount, expiresAt: date = expires, firstReceipt: text?, secondReceipt: text? }\n lifecycle { states: [pending, first_ready, second_ready, both_ready, exchanged, cancelled], initial: pending }\n action create {}\n action fund_first { from: pending, to: first_ready, actor: { party: first }, deadline: { at: self.expiresAt }, moves reserve self.firstAmount from first to second capture \"firstReceipt\" }\n action fund_second { from: first_ready, to: both_ready, actor: { party: second }, deadline: { at: self.expiresAt }, moves reserve self.secondAmount from second to first capture \"secondReceipt\" }\n action start_second { from: pending, to: second_ready, actor: { party: second }, deadline: { at: self.expiresAt }, moves reserve self.secondAmount from second to first capture \"secondReceipt\" }\n action finish_first { from: second_ready, to: both_ready, actor: { party: first }, deadline: { at: self.expiresAt }, moves reserve self.firstAmount from first to second capture \"firstReceipt\" }\n action expire_second { from: second_ready, to: cancelled, actor: clock, due: { at: self.expiresAt }, moves void self.secondReceipt }\n action exchange { from: both_ready, to: exchanged, actor: { party: first }, moves post self.firstReceipt; moves post self.secondReceipt }\n action cancel { from: pending, to: cancelled }\n action expire_first { from: first_ready, to: cancelled, actor: clock, due: { at: self.expiresAt }, moves void self.firstReceipt }\n action expire_both { from: both_ready, to: cancelled, actor: clock, due: { at: self.expiresAt }, moves void self.firstReceipt; moves void self.secondReceipt }\n}\n\ninstrument payout(payer: party, payee: party, adapter: text, amount: money = runtime) {\n summary: \"Pay the recipient when the provider confirms settlement.\"\n fields { amount: money = amount }\n lifecycle { states: [pending, instructed, settled, rejected], initial: pending }\n action create {}\n action instruct {\n from: pending, to: instructed, actor: { party: payer }\n moves reserve self.amount from payer to payee boundary adapter\n }\n action confirm {\n from: instructed, to: settled\n moves post instruct\n }\n action reject {\n from: instructed, to: rejected\n moves void instruct\n }\n}\n\ninstrument mandate(payer: party, payee: party) {\n summary: \"A payer's permission for one payee to charge up to a total until it expires.\"\n fields { payer: account of payer, payee: account of payee, limit: money, expiresAt: date }\n lifecycle { states: [active, revoked], initial: active }\n action create { actor: { party: payer }, requires self.limit > \"0\" }\n action revoke { from: active, to: revoked, actor: { party: payer } }\n}\n\ninstrument metered(payer: party, payee: party, unit_price: money, units: integer = 1, fee: fee = { seller: 0% }, mandate: ref<money.mandate> = object(money.mandate)) {\n summary: \"A unique usage event priced once at the fixed unit price.\"\n fields { usageIdentity: text, mandate: ref<mandate>, payer: account of payer, payee: account of payee, unitPrice: money = unit_price, units: integer = units, amount: money = multiply(self.unitPrice, self.units) }\n lifecycle { states: [recorded, billed, cancelled], initial: recorded }\n invariants sum \"bill_move1_debit\" of { instrument: all(money.metered), reference: \"mandate\", anchor: self.mandate, states: [billed], limit: 366 } <= self.mandate.limit\n action create { requires unique \"usage_event\" on [self.usageIdentity]; requires self.mandate.payer == self.payer; requires self.mandate.payee == self.payee }\n action bill { allowZero: true, requires self.amount > \"0\"; from: recorded, to: billed, actor: { party: payee }, deadline: { at: self.mandate.expiresAt }, requires self.mandate in [active], moves self.amount from payer to payee fee fee }\n action cancel { from: recorded, to: cancelled, actor: { party: payee } }\n}\n\ninstrument late_fee(payer: party, payee: party, amount: money, cap: money, period: duration = 1d, grace: duration = 1h) {\n summary: \"Charge a fixed fee for each period a due time stays unmet, up to a cap.\"\n fields {\n payer: account of payer, payee: account of payee, amount: money = amount, cap: money = cap, ceiling: money = multiply(self.amount, 366)\n dueAt: date, graceEndsAt: date?, firstAt: date?, nextAt: date?, followingAt: date?, charge: money?, left: money?, remaining: money?, stoppedAt: date?\n }\n lifecycle { states: [running, settling, capped, stopped], initial: running }\n action create {\n actor: { party: payer }\n requires self.amount > \"0\"\n requires self.cap >= self.amount\n requires self.cap <= self.ceiling\n 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 }]\n set: { nextAt: { field: self.firstAt }, left: { field: self.cap } }\n }\n action charge {\n summary: \"Charge late fee\"\n from: running, to: running, actor: clock, due: { at: self.nextAt }\n 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 }]\n set: { left: { field: self.remaining }, nextAt: { field: self.followingAt } }\n moves self.charge from payer to payee economics { recipient: payee, company: { purpose: earning, sourceParty: payer }, participant: { purpose: pass_through, sourceParty: payer } }\n invoke: [{ selection: { instrument: current(), reference: \"payer\", anchor: self.payer, states: [running], limit: 1, where: { id: { field: self.id }, left: { literal: \"0\" } } }, action: finish, input: {} }]\n }\n action collect {\n summary: \"Collect owed late fee\"\n from: settling, to: settling, actor: clock, due: { at: self.nextAt }\n 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 }]\n set: { left: { field: self.remaining }, nextAt: { field: self.followingAt } }\n moves self.charge from payer to payee economics { recipient: payee, company: { purpose: earning, sourceParty: payer }, participant: { purpose: pass_through, sourceParty: payer } }\n invoke: [\n { reference: self.id, action: settle, guard: { kind: compare, left: { field: self.nextAt }, operator: \">\", right: { field: self.stoppedAt } }, input: {} }\n { selection: { instrument: current(), reference: \"payer\", anchor: self.payer, states: [settling], limit: 1, where: { id: { field: self.id }, left: { literal: \"0\" } } }, action: finish, input: {} }\n ]\n }\n action finish { from: [running, settling], to: capped, actor: { parent: current() } }\n action stop {\n from: running, to: settling, actor: { party: payee }\n set: { stoppedAt: { field: self.now } }\n invoke: [{ reference: self.id, action: settle, guard: { kind: compare, left: { field: self.nextAt }, operator: \">\", right: { field: self.stoppedAt } }, input: {} }]\n }\n action settle { from: settling, to: stopped, actor: { parent: current() } }\n action waive { from: settling, to: stopped, actor: { party: payee } }\n}\n"],
|
|
13
13
|
["purchase.hsx", "header purchase\n\n// A financed purchase connects a repayment plan, a delivery hold and optional\n// marketplace reservations. The hold blocks refunds while checkout is active.\n// Debt cancellation returns outstanding capital before the buyer's own cash.\ninstrument checkout(plans: ref<financing.installments>[], funds: ref<escrow.hold>, borrower: party, capital: party = programOperator) {\n summary: \"Fund a purchase and return lender capital before refunding the buyer.\"\n fields {\n funds: ref<funds>, borrower: account of borrower\n downPaymentCredit: money?, downPaymentRemaining: money?\n }\n lifecycle { states: [pending, active, closed], initial: pending }\n action create { actor: { party: borrower } }\n action check {\n from: pending, to: pending, actor: { parent: plans }\n input { plan: ref<plans> }\n requires input.plan.funds == self.id\n requires input.plan.borrower == self.borrower\n requires input.plan.borrower == self.funds.payer\n requires input.plan.price == self.funds.price\n }\n action collect { allowZero: true,\n from: pending, to: pending, actor: { parent: plans }\n input { plan: ref<plans> }\n requires input.plan.funds == self.id\n requires self.funds in [pending]\n calculate: [{ target: downPaymentCredit, op: minimum, values: [{ field: self.funds.held.balance }, { field: input.plan.downPayment }] }, { target: downPaymentRemaining, op: subtract, base: { field: input.plan.downPayment }, subtract: [{ field: self.downPaymentCredit }] }]\n moves self.downPaymentRemaining from self.borrower to self.funds.held economics { purpose: principal, sourceParty: borrower }\n }\n action disburse {\n from: pending, to: active, actor: { parent: plans }\n input { plan: ref<plans> }\n requires input.plan.funds == self.id\n requires self.funds in [pending]\n invoke: [{ selection: { instrument: all(marketplace.reservation), reference: \"funds\", anchor: self.funds, states: [active], limit: 1 }, action: credit, input: { funds: { field: self.funds }, contribution: { field: input.plan.downPayment } } }, { reference: self.id, action: fund, input: { plan: { field: input.plan } } }]\n }\n action fund { allowZero: true,\n from: active, to: active, actor: { parent: current() }\n input { plan: ref<plans> }\n calculate: [{ target: downPaymentCredit, op: minimum, values: [{ field: self.funds.held.balance }, { field: input.plan.downPayment }] }, { target: downPaymentRemaining, op: subtract, base: { field: input.plan.downPayment }, subtract: [{ field: self.downPaymentCredit }] }]\n moves self.downPaymentRemaining from self.borrower to self.funds.held economics { purpose: principal, sourceParty: borrower }\n moves input.plan.principal from input.plan.capital to self.funds.held economics { purpose: principal, sourceParty: capital }\n invoke: [{ reference: self.funds, action: confirm, input: {} }]\n }\n action write_off {\n from: active, to: active, actor: { parent: plans }\n input { plan: ref<plans> }\n requires input.plan.funds == self.id\n requires self.funds in [released]\n }\n action refund { allowZero: true,\n from: active, to: closed, actor: { parent: plans }\n input { plan: ref<plans>, principal: money }\n requires input.plan.funds == self.id\n requires self.funds in [return_verified]\n moves input.principal from self.funds.held to input.plan.capital economics { purpose: principal, sourceParty: capital }\n }\n action cancel_undelivered { allowZero: true,\n from: active, to: closed, actor: { parent: plans }\n input { plan: ref<plans> }\n requires input.plan.funds == self.id\n requires self.funds in [funded]\n moves input.plan.principal from self.funds.held to input.plan.capital economics { purpose: principal, sourceParty: capital }\n invoke: [{ reference: self.funds, action: finish_refund, input: {} }]\n }\n action return_paid {\n from: active, to: closed, actor: { parent: plans }\n input { plan: ref<plans> }\n requires input.plan.funds == self.id\n requires self.funds in [return_verified]\n invoke: [{ reference: self.funds, action: finish_refund, input: {} }]\n }\n action unwind_paid_undelivered {\n from: active, to: closed, actor: { parent: plans }\n input { plan: ref<plans> }\n requires input.plan.funds == self.id\n requires self.funds in [funded]\n requires self.funds.deliverBy < self.now\n invoke: [{ reference: self.funds, action: finish_refund, input: {} }]\n }\n action unwind_paid {\n from: active, to: closed, actor: { parent: plans }\n input { plan: ref<plans> }\n requires input.plan.funds == self.id\n requires self.funds in [funded, return_verified]\n requires self.funds.deliverBy < self.now\n invoke: [{ reference: self.funds, action: finish_refund, input: {} }]\n }\n}\n"],
|
|
14
14
|
["rental.hsx", "header rental\n\n// One hire per security agreement, including ended hires.\n// Pickup makes hire payable to the operator; cancellation before pickup refunds both holds.\ninstrument hire_charge(renter: party, security: ref<rental.deposit>) {\n fields { security: ref<security>, held: account of self }\n lifecycle { states: [pending, funded, released, cancelled], initial: pending }\n action create {\n actor: { party: renter }\n requires count of { instrument: current(), reference: \"security\", anchor: self.security, states: [pending, funded, released, cancelled], limit: 1 } == 0\n }\n action fund {\n from: pending, to: funded, actor: { party: renter }\n requires self.security in [pending, funded, hired, returned]\n moves 250 SAR from renter to self.held economics { purpose: principal, sourceParty: renter }\n }\n action release {\n from: funded, to: released, actor: { party: renter }\n requires self.security in [hired, returned]\n moves self.held.balance from self.held to programOperator economics { purpose: earning, sourceParty: renter }\n }\n action refund { allowZero: true,\n from: [pending, funded], to: cancelled, actor: { parent: security }\n moves self.held.balance from self.held to renter economics { purpose: principal, sourceParty: programOperator }\n }\n action cancel {\n from: [pending, funded], to: preserve, actor: { party: renter }\n invoke: [{ reference: self.security, action: cancel, input: {} }]\n }\n}\n\n// Security stays refundable. Only the operator records pickup and damage.\ninstrument deposit(renter: party, hire: ref<rental.hire_charge>) {\n summary: \"Refund rental security on return, less an authorized damage amount.\"\n fields { renter: account of renter, held: account of self, amount: money = subject.price, refund: money? }\n lifecycle { states: [pending, funded, hired, returned, cancelled], initial: pending }\n action create { actor: { party: renter }, subject { price: money }, requires self.amount > \"0\" }\n action fund {\n from: pending, to: funded, actor: { party: renter }\n moves self.amount from self.renter to self.held economics { purpose: principal, sourceParty: renter }\n }\n action pickup { from: funded, to: hired, actor: { party: programOperator } }\n action return {\n from: hired, to: returned, actor: { party: programOperator }\n input { returnReference: text }\n moves self.held.balance from self.held to self.renter economics { purpose: principal, sourceParty: programOperator }\n }\n action withhold { allowZero: true,\n from: hired, to: returned, actor: { party: programOperator }\n input { damage: money, damageReference: text }\n requires input.damage > \"0\"\n requires input.damage <= self.held.balance\n calculate: [{ target: refund, op: subtract, base: { field: self.held.balance }, subtract: [{ field: input.damage }] }]\n moves input.damage from self.held to programOperator economics { purpose: earning, sourceParty: renter }\n moves self.refund from self.held to self.renter economics { purpose: principal, sourceParty: programOperator }\n }\n action refund_before_pickup { allowZero: true,\n from: [pending, funded], to: cancelled, actor: { party: renter }\n moves self.held.balance from self.held to self.renter economics { purpose: principal, sourceParty: programOperator }\n invoke: [{ selection: { instrument: hire, reference: \"security\", anchor: self.id, states: [pending, funded], limit: 1 }, action: refund, input: {} }]\n }\n action cancel { allowZero: true,\n from: [pending, funded], to: cancelled, actor: { party: renter }\n moves self.held.balance from self.held to self.renter economics { purpose: principal, sourceParty: programOperator }\n invoke: [{ selection: { instrument: hire, reference: \"security\", anchor: self.id, states: [pending, funded], limit: 1 }, action: refund, input: {} }]\n }\n}\n\n"],
|
|
15
15
|
["reporting.hsx", "header reporting\n\n// Attach portfolio to one or more financing plans. Its only lifecycle action is\n// create; reports use the declared report request/read roles, not a money action.\n// Aging selects active facilities and includes earned profit when deciding arrears.\n// The default-rate report measures period-end stock, not new default incidence.\n// Paid facilities are excluded and write-offs counted separately. Amounts are riyal\n// minor units; declared scale and rounding govern ratios. Missing identity joins\n// or unreconciled schedule counts refuse.\n\n// Cost: portfolio.create declares no transfers, owned accounts or invocations.\n// That zero instruction count does not price report capture, rendering, storage or\n// release. The host owns those charges and their execution.\n\ninstrument portfolio(on: ref<financing.installments>[], default_days: integer(1, 3650) = 90, retention_years: integer(1, 100) = 10, aging_first_days: integer(1, 3650) = 30, aging_second_days: integer(1, 3650) = 60, aging_third_days: integer(1, 3650) = 90, ratio_scale: integer(1, 1000000) = 10000, ratio_rounding: enum(floor, halfUp) = floor) {\n summary: \"Declared financing reports evaluated over frozen Product facts.\"\n fields {}\n lifecycle { states: [declared], initial: declared }\n action create {}\n constraints { aging_first_days: less_than(aging_second_days), aging_second_days: less_than(aging_third_days) }\n reports: [\n {\n identity: { id: \"portfolio_aging\", version: 1, description: \"Period-end outstanding principal by oldest unpaid installment. Remaining scheduled profit is distinct from accrued profit. Restructuring evidence is unavailable.\" },\n scope: { kind: \"product\", classification: \"internal\", currency: \"SAR\" },\n datasets: [\n {\n id: \"facilities\",\n source: \"instrument\",\n instruments: on,\n rowKey: \"facilityId\",\n columns: [\n {\n name: \"facilityId\",\n field: \"id\",\n type: { kind: \"text\" },\n required: true\n },\n {\n name: \"borrowerAccount\",\n field: \"borrower\",\n type: { kind: \"text\" },\n required: true\n },\n {\n name: \"state\",\n field: \"status\",\n type: { kind: \"text\" },\n required: true\n },\n {\n name: \"originalPrincipal\",\n field: \"principal\",\n type: { kind: \"money\", currency: \"SAR\" },\n required: true\n },\n {\n name: \"scheduleCount\",\n field: \"months\",\n type: { kind: \"integer\" },\n required: true\n },\n {\n name: \"recordedCreation\",\n field: \"createdAt\",\n type: { kind: \"date\" },\n required: true\n }\n ],\n expressions: [\n {\n id: \"activeState\",\n op: \"literal\",\n type: { kind: \"text\" },\n value: \"active\"\n },\n {\n id: \"paidState\",\n op: \"literal\",\n type: { kind: \"text\" },\n value: \"paid\"\n },\n {\n id: \"writtenState\",\n op: \"literal\",\n type: { kind: \"text\" },\n value: \"written_off\"\n },\n { id: \"active\", op: \"equal\", left: \"state\", right: \"activeState\" },\n { id: \"paid\", op: \"equal\", left: \"state\", right: \"paidState\" },\n { id: \"writtenOff\", op: \"equal\", left: \"state\", right: \"writtenState\" },\n { id: \"activeOrPaid\", op: \"or\", left: \"active\", right: \"paid\" },\n { id: \"financed\", op: \"or\", left: \"active\", right: \"writtenOff\" }\n ],\n selection: \"active\"\n },\n {\n id: \"schedule\",\n source: \"instrument\",\n instruments: child(on, slice),\n rowKey: \"sliceId\",\n columns: [\n {\n name: \"sliceId\",\n field: \"id\",\n type: { kind: \"text\" },\n required: true\n },\n {\n name: \"planId\",\n field: \"plan\",\n type: { kind: \"text\" },\n required: true\n },\n {\n name: \"due\",\n field: \"dueAt\",\n type: { kind: \"date\" },\n required: true\n },\n {\n name: \"principal\",\n field: \"principalReceivable.balance\",\n type: { kind: \"money\", currency: \"SAR\" },\n required: true\n },\n {\n name: \"profit\",\n field: \"profitReceivable.balance\",\n type: { kind: \"money\", currency: \"SAR\" },\n required: true\n },\n {\n name: \"earnedProfit\",\n field: \"profitEarned.balance\",\n type: { kind: \"money\", currency: \"SAR\" },\n required: true\n },\n {\n name: \"scheduledPrincipal\",\n field: \"principal\",\n type: { kind: \"money\", currency: \"SAR\" },\n required: true\n }\n ],\n expressions: [\n {\n id: \"zeroMoney\",\n op: \"literal\",\n type: { kind: \"money\", currency: \"SAR\" },\n value: \"0\"\n },\n {\n id: \"zeroDays\",\n op: \"literal\",\n type: { kind: \"integer\" },\n value: 0\n },\n { id: \"observed\", op: \"parameter\", name: \"observationAt\" },\n { id: \"scheduledOutstanding\", op: \"sum\", left: \"principal\", right: \"profit\" },\n { id: \"totalOutstanding\", op: \"sum\", left: \"scheduledOutstanding\", right: \"earnedProfit\" },\n { id: \"unpaid\", op: \"less\", left: \"zeroMoney\", right: \"totalOutstanding\" },\n { id: \"elapsed\", op: \"daysBetween\", left: \"due\", right: \"observed\" },\n { id: \"nonnegativeDays\", op: \"maximum\", left: \"elapsed\", right: \"zeroDays\" },\n { id: \"daysPastDue\", op: \"choose\", condition: \"unpaid\", yes: \"nonnegativeDays\", no: \"zeroDays\" }\n ]\n },\n {\n id: \"borrowerAccounts\",\n source: \"account\",\n instruments: [ ],\n rowKey: \"accountId\",\n columns: [\n {\n name: \"accountId\",\n field: \"id\",\n type: { kind: \"text\" },\n required: true\n },\n {\n name: \"participant\",\n field: \"ownerParticipantId\",\n type: { kind: \"text\" },\n required: false\n }\n ],\n expressions: [ ]\n },\n {\n id: \"borrowers\",\n source: \"identity\",\n instruments: [ ],\n rowKey: \"customerId\",\n columns: [\n {\n name: \"customerId\",\n field: \"id\",\n type: { kind: \"text\" },\n required: true\n },\n {\n name: \"participantId\",\n field: \"participantId\",\n type: { kind: \"text\" },\n required: true\n },\n {\n name: \"entityId\",\n field: \"entityId\",\n type: { kind: \"text\" },\n required: true\n }\n ],\n expressions: [ ]\n }\n ],\n time: { timezone: \"Asia/Riyadh\", boundary: \"startInclusiveEndExclusive\", observation: \"periodEnd\", history: \"retainedOnly\" },\n selection: { dataset: \"facilities\" },\n calculation: {\n joins: [\n {\n dataset: \"schedule\",\n local: \"facilityId\",\n foreign: \"planId\",\n cardinality: \"many\",\n missing: \"refuse\",\n aggregates: [\n { name: \"outstandingPrincipal\", op: \"sum\", value: \"principal\" },\n { name: \"remainingScheduledProfit\", op: \"sum\", value: \"profit\" },\n { name: \"scheduledPrincipalTotal\", op: \"sum\", value: \"scheduledPrincipal\" },\n { name: \"capturedScheduleCount\", op: \"count\" },\n { name: \"oldestUnpaidDays\", op: \"maximum\", value: \"daysPastDue\" }\n ]\n },\n {\n dataset: \"borrowerAccounts\",\n local: \"borrowerAccount\",\n foreign: \"accountId\",\n cardinality: \"one\",\n missing: \"refuse\",\n aggregates: [\n { name: \"borrowerParticipant\", op: \"minimum\", value: \"participant\" }\n ]\n },\n {\n dataset: \"borrowers\",\n local: \"borrowerParticipant\",\n foreign: \"participantId\",\n cardinality: \"one\",\n missing: \"refuse\",\n aggregates: [\n { name: \"borrowerEntity\", op: \"minimum\", value: \"entityId\" }\n ]\n }\n ],\n expressions: [\n {\n id: \"day0\",\n op: \"literal\",\n type: { kind: \"integer\" },\n value: 0\n },\n {\n id: \"day30\",\n op: \"literal\",\n type: { kind: \"integer\" },\n value: aging_first_days\n },\n {\n id: \"day60\",\n op: \"literal\",\n type: { kind: \"integer\" },\n value: aging_second_days\n },\n {\n id: \"day90\",\n op: \"literal\",\n type: { kind: \"integer\" },\n value: aging_third_days\n },\n {\n id: \"band0\",\n op: \"literal\",\n type: { kind: \"text\" },\n value: \"current\"\n },\n {\n id: \"band30\",\n op: \"literal\",\n type: { kind: \"text\" },\n value: \"first\"\n },\n {\n id: \"band60\",\n op: \"literal\",\n type: { kind: \"text\" },\n value: \"second\"\n },\n {\n id: \"band90\",\n op: \"literal\",\n type: { kind: \"text\" },\n value: \"third\"\n },\n {\n id: \"bandLate\",\n op: \"literal\",\n type: { kind: \"text\" },\n value: \"later\"\n },\n { id: \"current\", op: \"atMost\", left: \"oldestUnpaidDays\", right: \"day0\" },\n { id: \"first\", op: \"atMost\", left: \"oldestUnpaidDays\", right: \"day30\" },\n { id: \"second\", op: \"atMost\", left: \"oldestUnpaidDays\", right: \"day60\" },\n { id: \"third\", op: \"atMost\", left: \"oldestUnpaidDays\", right: \"day90\" },\n { id: \"lateBand\", op: \"choose\", condition: \"third\", yes: \"band90\", no: \"bandLate\" },\n { id: \"secondBand\", op: \"choose\", condition: \"second\", yes: \"band60\", no: \"lateBand\" },\n { id: \"firstBand\", op: \"choose\", condition: \"first\", yes: \"band30\", no: \"secondBand\" },\n { id: \"agingBand\", op: \"choose\", condition: \"current\", yes: \"band0\", no: \"firstBand\" }\n ],\n groupBy: [ \"agingBand\" ],\n aggregates: [\n { name: \"facilities\", op: \"count\" },\n { name: \"principalMinor\", op: \"sum\", value: \"outstandingPrincipal\" },\n { name: \"scheduledProfitMinor\", op: \"sum\", value: \"remainingScheduledProfit\" }\n ],\n resultExpressions: [ ]\n },\n validation: {\n empty: \"noEligibleFacilities\",\n required: [ \"borrowerEntity\" ],\n reconcile: [ ],\n rowReconcile: [\n { left: \"originalPrincipal\", right: \"scheduledPrincipalTotal\" },\n { left: \"scheduleCount\", right: \"capturedScheduleCount\" }\n ],\n unavailableFacts: [ ],\n },\n output: {\n profile: \"internal.v1\",\n formats: [ \"json\", \"csv\" ],\n columns: [\n {\n name: \"agingBand\",\n label: \"agingBand\",\n type: { kind: \"text\" }\n },\n {\n name: \"facilities\",\n label: \"facilities\",\n type: { kind: \"integer\" }\n },\n {\n name: \"principalMinor\",\n label: \"principalMinor\",\n type: { kind: \"money\", currency: \"SAR\" }\n },\n {\n name: \"scheduledProfitMinor\",\n label: \"scheduledProfitMinor\",\n type: { kind: \"money\", currency: \"SAR\" }\n }\n ],\n sort: [ \"agingBand\" ]\n },\n authority: {\n request: [ \"owner\", \"finance\" ],\n read: [ \"owner\", \"admin\", \"operations\", \"finance\", \"viewer\" ],\n release: [ ],\n retention: { policy: \"report_replay\", years: retention_years }\n }\n },\n {\n identity: { id: \"period_end_outstanding_principal_default_rate\", version: 1, description: \"Current active facilities with any unpaid installment at least the declared default days past due, observed at period end. Cure when arrears clear. Count and outstanding-principal weighted rates in the declared scale exclude paid and written-off facilities; write-offs are counted separately. This is a stock measure, not a new-default incidence or origination cohort rate.\" },\n scope: { kind: \"product\", classification: \"internal\", currency: \"SAR\" },\n datasets: [\n {\n id: \"facilities\",\n source: \"instrument\",\n instruments: on,\n rowKey: \"facilityId\",\n columns: [\n {\n name: \"facilityId\",\n field: \"id\",\n type: { kind: \"text\" },\n required: true\n },\n {\n name: \"borrowerAccount\",\n field: \"borrower\",\n type: { kind: \"text\" },\n required: true\n },\n {\n name: \"state\",\n field: \"status\",\n type: { kind: \"text\" },\n required: true\n },\n {\n name: \"originalPrincipal\",\n field: \"principal\",\n type: { kind: \"money\", currency: \"SAR\" },\n required: true\n },\n {\n name: \"scheduleCount\",\n field: \"months\",\n type: { kind: \"integer\" },\n required: true\n },\n {\n name: \"recordedCreation\",\n field: \"createdAt\",\n type: { kind: \"date\" },\n required: true\n }\n ],\n expressions: [\n {\n id: \"activeState\",\n op: \"literal\",\n type: { kind: \"text\" },\n value: \"active\"\n },\n {\n id: \"paidState\",\n op: \"literal\",\n type: { kind: \"text\" },\n value: \"paid\"\n },\n {\n id: \"writtenState\",\n op: \"literal\",\n type: { kind: \"text\" },\n value: \"written_off\"\n },\n { id: \"active\", op: \"equal\", left: \"state\", right: \"activeState\" },\n { id: \"paid\", op: \"equal\", left: \"state\", right: \"paidState\" },\n { id: \"writtenOff\", op: \"equal\", left: \"state\", right: \"writtenState\" },\n { id: \"activeOrPaid\", op: \"or\", left: \"active\", right: \"paid\" },\n { id: \"financed\", op: \"or\", left: \"active\", right: \"writtenOff\" }\n ],\n selection: \"financed\"\n },\n {\n id: \"schedule\",\n source: \"instrument\",\n instruments: child(on, slice),\n rowKey: \"sliceId\",\n columns: [\n {\n name: \"sliceId\",\n field: \"id\",\n type: { kind: \"text\" },\n required: true\n },\n {\n name: \"planId\",\n field: \"plan\",\n type: { kind: \"text\" },\n required: true\n },\n {\n name: \"due\",\n field: \"dueAt\",\n type: { kind: \"date\" },\n required: true\n },\n {\n name: \"principal\",\n field: \"principalReceivable.balance\",\n type: { kind: \"money\", currency: \"SAR\" },\n required: true\n },\n {\n name: \"profit\",\n field: \"profitReceivable.balance\",\n type: { kind: \"money\", currency: \"SAR\" },\n required: true\n },\n {\n name: \"earnedProfit\",\n field: \"profitEarned.balance\",\n type: { kind: \"money\", currency: \"SAR\" },\n required: true\n },\n {\n name: \"scheduledPrincipal\",\n field: \"principal\",\n type: { kind: \"money\", currency: \"SAR\" },\n required: true\n }\n ],\n expressions: [\n {\n id: \"zeroMoney\",\n op: \"literal\",\n type: { kind: \"money\", currency: \"SAR\" },\n value: \"0\"\n },\n {\n id: \"zeroDays\",\n op: \"literal\",\n type: { kind: \"integer\" },\n value: 0\n },\n { id: \"observed\", op: \"parameter\", name: \"observationAt\" },\n { id: \"scheduledOutstanding\", op: \"sum\", left: \"principal\", right: \"profit\" },\n { id: \"totalOutstanding\", op: \"sum\", left: \"scheduledOutstanding\", right: \"earnedProfit\" },\n { id: \"unpaid\", op: \"less\", left: \"zeroMoney\", right: \"totalOutstanding\" },\n { id: \"elapsed\", op: \"daysBetween\", left: \"due\", right: \"observed\" },\n { id: \"nonnegativeDays\", op: \"maximum\", left: \"elapsed\", right: \"zeroDays\" },\n { id: \"daysPastDue\", op: \"choose\", condition: \"unpaid\", yes: \"nonnegativeDays\", no: \"zeroDays\" }\n ]\n },\n {\n id: \"borrowerAccounts\",\n source: \"account\",\n instruments: [ ],\n rowKey: \"accountId\",\n columns: [\n {\n name: \"accountId\",\n field: \"id\",\n type: { kind: \"text\" },\n required: true\n },\n {\n name: \"participant\",\n field: \"ownerParticipantId\",\n type: { kind: \"text\" },\n required: false\n }\n ],\n expressions: [ ]\n },\n {\n id: \"borrowers\",\n source: \"identity\",\n instruments: [ ],\n rowKey: \"customerId\",\n columns: [\n {\n name: \"customerId\",\n field: \"id\",\n type: { kind: \"text\" },\n required: true\n },\n {\n name: \"participantId\",\n field: \"participantId\",\n type: { kind: \"text\" },\n required: true\n },\n {\n name: \"entityId\",\n field: \"entityId\",\n type: { kind: \"text\" },\n required: true\n }\n ],\n expressions: [ ]\n }\n ],\n time: { timezone: \"Asia/Riyadh\", boundary: \"startInclusiveEndExclusive\", observation: \"periodEnd\", history: \"retainedOnly\" },\n selection: { dataset: \"facilities\" },\n calculation: {\n joins: [\n {\n dataset: \"schedule\",\n local: \"facilityId\",\n foreign: \"planId\",\n cardinality: \"many\",\n missing: \"refuse\",\n aggregates: [\n { name: \"outstandingPrincipal\", op: \"sum\", value: \"principal\" },\n { name: \"remainingScheduledProfit\", op: \"sum\", value: \"profit\" },\n { name: \"scheduledPrincipalTotal\", op: \"sum\", value: \"scheduledPrincipal\" },\n { name: \"capturedScheduleCount\", op: \"count\" },\n { name: \"oldestUnpaidDays\", op: \"maximum\", value: \"daysPastDue\" }\n ]\n },\n {\n dataset: \"borrowerAccounts\",\n local: \"borrowerAccount\",\n foreign: \"accountId\",\n cardinality: \"one\",\n missing: \"refuse\",\n aggregates: [\n { name: \"borrowerParticipant\", op: \"minimum\", value: \"participant\" }\n ]\n },\n {\n dataset: \"borrowers\",\n local: \"borrowerParticipant\",\n foreign: \"participantId\",\n cardinality: \"one\",\n missing: \"refuse\",\n aggregates: [\n { name: \"borrowerEntity\", op: \"minimum\", value: \"entityId\" }\n ]\n }\n ],\n expressions: [\n {\n id: \"threshold\",\n op: \"literal\",\n type: { kind: \"integer\" },\n value: default_days\n },\n {\n id: \"zero\",\n op: \"literal\",\n type: { kind: \"integer\" },\n value: 0\n },\n {\n id: \"one\",\n op: \"literal\",\n type: { kind: \"integer\" },\n value: 1\n },\n {\n id: \"zeroMoney\",\n op: \"literal\",\n type: { kind: \"money\", currency: \"SAR\" },\n value: \"0\"\n },\n { id: \"pastThreshold\", op: \"atMost\", left: \"threshold\", right: \"oldestUnpaidDays\" },\n { id: \"isDefault\", op: \"and\", left: \"active\", right: \"pastThreshold\" },\n { id: \"activeCount\", op: \"choose\", condition: \"active\", yes: \"one\", no: \"zero\" },\n { id: \"defaultCount\", op: \"choose\", condition: \"isDefault\", yes: \"one\", no: \"zero\" },\n { id: \"writeOffCount\", op: \"choose\", condition: \"writtenOff\", yes: \"one\", no: \"zero\" },\n { id: \"activePrincipal\", op: \"choose\", condition: \"active\", yes: \"outstandingPrincipal\", no: \"zeroMoney\" },\n { id: \"defaultPrincipal\", op: \"choose\", condition: \"isDefault\", yes: \"outstandingPrincipal\", no: \"zeroMoney\" }\n ],\n groupBy: [ ],\n aggregates: [\n { name: \"activeFacilities\", op: \"sum\", value: \"activeCount\" },\n { name: \"defaultFacilities\", op: \"sum\", value: \"defaultCount\" },\n { name: \"writtenOffFacilities\", op: \"sum\", value: \"writeOffCount\" },\n { name: \"activePrincipalMinor\", op: \"sum\", value: \"activePrincipal\" },\n { name: \"defaultPrincipalMinor\", op: \"sum\", value: \"defaultPrincipal\" }\n ],\n resultExpressions: [\n { id: \"countRate\", op: \"ratio\", numerator: \"defaultFacilities\", denominator: \"activeFacilities\", scale: ratio_scale, rounding: ratio_rounding, zero: \"null\" },\n { id: \"principalRate\", op: \"ratio\", numerator: \"defaultPrincipalMinor\", denominator: \"activePrincipalMinor\", scale: ratio_scale, rounding: ratio_rounding, zero: \"null\" },\n {\n id: \"restructuringEvidence\",\n op: \"literal\",\n type: { kind: \"text\" },\n value: \"unavailable\"\n },\n {\n id: \"defaultDays\",\n op: \"literal\",\n type: { kind: \"integer\" },\n value: default_days\n },\n {\n id: \"rateScale\",\n op: \"literal\",\n type: { kind: \"integer\" },\n value: ratio_scale\n }\n ]\n },\n validation: {\n empty: \"refuse\",\n required: [ \"borrowerEntity\" ],\n reconcile: [ ],\n rowReconcile: [\n { left: \"originalPrincipal\", right: \"scheduledPrincipalTotal\" },\n { left: \"scheduleCount\", right: \"capturedScheduleCount\" }\n ],\n unavailableFacts: [ ],\n },\n output: {\n profile: \"internal.v1\",\n formats: [ \"json\", \"csv\" ],\n columns: [\n {\n name: \"activeFacilities\",\n label: \"activeFacilities\",\n type: { kind: \"integer\" }\n },\n {\n name: \"defaultFacilities\",\n label: \"defaultFacilities\",\n type: { kind: \"integer\" }\n },\n {\n name: \"writtenOffFacilities\",\n label: \"writtenOffFacilities\",\n type: { kind: \"integer\" }\n },\n {\n name: \"activePrincipalMinor\",\n label: \"activePrincipalMinor\",\n type: { kind: \"money\", currency: \"SAR\" }\n },\n {\n name: \"defaultPrincipalMinor\",\n label: \"defaultPrincipalMinor\",\n type: { kind: \"money\", currency: \"SAR\" }\n },\n {\n name: \"countRate\",\n label: \"countRate\",\n type: { kind: \"integer\" }\n },\n {\n name: \"principalRate\",\n label: \"principalRate\",\n type: { kind: \"integer\" }\n },\n {\n name: \"restructuringEvidence\",\n label: \"restructuringEvidence\",\n type: { kind: \"text\" }\n },\n {\n name: \"defaultDays\",\n label: \"defaultDays\",\n type: { kind: \"integer\" }\n },\n {\n name: \"rateScale\",\n label: \"rateScale\",\n type: { kind: \"integer\" }\n }\n ],\n sort: [ \"activeFacilities\" ]\n },\n authority: {\n request: [ \"owner\", \"finance\" ],\n read: [ \"owner\", \"admin\", \"operations\", \"finance\", \"viewer\" ],\n release: [ ],\n retention: { policy: \"report_replay\", years: retention_years }\n }\n }\n ]\n}\n"],
|
package/src/template-guide.ts
CHANGED
|
@@ -121,6 +121,7 @@ export function templateGuide(
|
|
|
121
121
|
const compiled = compiledAttachment(program, example.attachment, options);
|
|
122
122
|
const create = compiled.actions.create;
|
|
123
123
|
const economics = compiled.warnings.flatMap((warning) =>
|
|
124
|
+
warning.code === "economic_purpose_missing" &&
|
|
124
125
|
warning.message.startsWith(`${example.attachment}: `)
|
|
125
126
|
? [/economics [^}]+\}/.exec(warning.fix)![0]]
|
|
126
127
|
: [],
|
|
@@ -143,7 +144,10 @@ export function templateGuide(
|
|
|
143
144
|
})),
|
|
144
145
|
createInput: (create?.input ?? []).map((field) => ({
|
|
145
146
|
name: field.name,
|
|
146
|
-
type: inputType(
|
|
147
|
+
type: inputType(
|
|
148
|
+
field as unknown as Record<string, unknown>,
|
|
149
|
+
listBound(field.name, decl, spell),
|
|
150
|
+
),
|
|
147
151
|
})),
|
|
148
152
|
subject: manifest.subject,
|
|
149
153
|
actions,
|
|
@@ -160,23 +164,54 @@ function slot(block: BlockExpr, key: string) {
|
|
|
160
164
|
return block.entries.find((entry) => entry.key === key)?.value;
|
|
161
165
|
}
|
|
162
166
|
|
|
167
|
+
/** Each `key` row of a block with the `when` branches it sits under. */
|
|
168
|
+
function branched(
|
|
169
|
+
block: BlockExpr,
|
|
170
|
+
key: string,
|
|
171
|
+
outer: string[] = [],
|
|
172
|
+
): { value: Expr; condition: string }[] {
|
|
173
|
+
return block.entries.flatMap((entry) =>
|
|
174
|
+
entry.key.startsWith("when ") && entry.value.kind === "block"
|
|
175
|
+
? branched(entry.value, key, [...outer, entry.key])
|
|
176
|
+
: entry.key === key
|
|
177
|
+
? [
|
|
178
|
+
{
|
|
179
|
+
value: entry.value,
|
|
180
|
+
condition: outer.length
|
|
181
|
+
? `when ${outer.map((when) => when.slice(5)).join(" and ")}`
|
|
182
|
+
: "",
|
|
183
|
+
},
|
|
184
|
+
]
|
|
185
|
+
: [],
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** A block's rows with every `when` branch's rows in place, so both forms read. */
|
|
190
|
+
function flat(block: BlockExpr): Entry[] {
|
|
191
|
+
return block.entries.flatMap((entry) =>
|
|
192
|
+
entry.key.startsWith("when ") && entry.value.kind === "block"
|
|
193
|
+
? flat(entry.value)
|
|
194
|
+
: [entry],
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
163
198
|
/**
|
|
164
199
|
* The parser desugars `requires` and `moves` into blocks without source
|
|
165
|
-
* spans, so each clause is spelled from the slot's own source text.
|
|
200
|
+
* spans, so each clause is spelled from the slot's own source text. A clause
|
|
201
|
+
* under a `when` branch names its condition.
|
|
166
202
|
*/
|
|
167
203
|
function clauses(
|
|
168
|
-
|
|
204
|
+
rows: { value: Expr; condition: string }[],
|
|
169
205
|
key: "requires" | "moves",
|
|
170
206
|
spell: (expression: Expr) => string,
|
|
171
207
|
) {
|
|
172
|
-
return
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
);
|
|
208
|
+
return rows.flatMap(({ value, condition }) =>
|
|
209
|
+
spell(value)
|
|
210
|
+
.split(new RegExp(`(?:^|[;,]?\\s+)${key}\\s+`))
|
|
211
|
+
.map((clause) => clause.trim().replace(/[;,]$/, ""))
|
|
212
|
+
.filter(Boolean)
|
|
213
|
+
.map((clause) => (condition ? `${clause}, ${condition}` : clause)),
|
|
214
|
+
);
|
|
180
215
|
}
|
|
181
216
|
|
|
182
217
|
function items(expression: Expr | undefined): Expr[] {
|
|
@@ -195,7 +230,7 @@ function guideActions(
|
|
|
195
230
|
for (const entry of body.entries) {
|
|
196
231
|
if (!entry.key.startsWith("action ") || entry.value.kind !== "block")
|
|
197
232
|
continue;
|
|
198
|
-
const action = entry.value;
|
|
233
|
+
const action = { ...entry.value, entries: flat(entry.value) };
|
|
199
234
|
const actor = slot(action, "actor");
|
|
200
235
|
const party = actor?.kind === "block" ? slot(actor, "party") : undefined;
|
|
201
236
|
const parent = actor?.kind === "block" ? slot(actor, "parent") : undefined;
|
|
@@ -216,10 +251,27 @@ function guideActions(
|
|
|
216
251
|
};
|
|
217
252
|
const due = at("due");
|
|
218
253
|
const deadline = at("deadline");
|
|
219
|
-
const requires = clauses(
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
254
|
+
const requires = clauses(
|
|
255
|
+
branched(entry.value, "requires"),
|
|
256
|
+
"requires",
|
|
257
|
+
spell,
|
|
258
|
+
);
|
|
259
|
+
const moves = clauses(
|
|
260
|
+
action.entries
|
|
261
|
+
.filter((row) => row.key === "moves")
|
|
262
|
+
.map((row) => ({ value: row.value, condition: "" })),
|
|
263
|
+
"moves",
|
|
264
|
+
spell,
|
|
265
|
+
);
|
|
266
|
+
const invokes = branched(entry.value, "invoke").flatMap(
|
|
267
|
+
({ value, condition }) =>
|
|
268
|
+
items(value).flatMap((item) =>
|
|
269
|
+
item.kind === "block"
|
|
270
|
+
? [
|
|
271
|
+
`${invocation(item, spell, prefix)}${condition ? `, ${condition}` : ""}`,
|
|
272
|
+
]
|
|
273
|
+
: [],
|
|
274
|
+
),
|
|
223
275
|
);
|
|
224
276
|
const from = slot(action, "from");
|
|
225
277
|
const to = slot(action, "to");
|
|
@@ -259,7 +311,9 @@ function invocation(
|
|
|
259
311
|
) {
|
|
260
312
|
const instrument = slot(block, "instrument");
|
|
261
313
|
const action = slot(block, "action");
|
|
262
|
-
|
|
314
|
+
// current() is the record itself, already named by the prefix.
|
|
315
|
+
const own = instrument && spell(instrument) === "current()";
|
|
316
|
+
const target = `${instrument && !own ? `${prefix}${spell(instrument)}.` : own ? prefix : ""}${action ? spell(action) : "?"}`;
|
|
263
317
|
const range = slot(block, "range");
|
|
264
318
|
const count = range?.kind === "block" ? slot(range, "count") : undefined;
|
|
265
319
|
const literal =
|
|
@@ -348,7 +402,7 @@ function valueMeaning(
|
|
|
348
402
|
const fields = slot(decl.body, "fields");
|
|
349
403
|
const set =
|
|
350
404
|
fields?.kind === "block"
|
|
351
|
-
? fields.
|
|
405
|
+
? flat(fields).filter((entry) => bare.test(fieldValue(entry, spell)))
|
|
352
406
|
: [];
|
|
353
407
|
const read = new Set(set.map((entry) => entry.key));
|
|
354
408
|
const uses: string[] = [];
|
|
@@ -364,14 +418,14 @@ function valueMeaning(
|
|
|
364
418
|
: new RegExp(`\\bself\\.${field}\\b`).test(text),
|
|
365
419
|
);
|
|
366
420
|
if (own?.kind === "block" && prefix)
|
|
367
|
-
for (const entry of own
|
|
421
|
+
for (const entry of flat(own)) {
|
|
368
422
|
const value = fieldValue(entry, spell);
|
|
369
423
|
if (reads(value)) uses.push(`${prefix}${entry.key}: ${value}`);
|
|
370
424
|
}
|
|
371
425
|
for (const entry of body.entries) {
|
|
372
426
|
if (!entry.key.startsWith("action ") || entry.value.kind !== "block")
|
|
373
427
|
continue;
|
|
374
|
-
for (const part of entry.value
|
|
428
|
+
for (const part of flat(entry.value)) {
|
|
375
429
|
if (["actor", "from", "to", "summary"].includes(part.key)) continue;
|
|
376
430
|
const text = spell(part.value);
|
|
377
431
|
// A long slot, such as an invoke list, is named rather than spelled.
|
|
@@ -388,7 +442,7 @@ function valueMeaning(
|
|
|
388
442
|
const recordFields = slot(record.value, "fields");
|
|
389
443
|
const refs =
|
|
390
444
|
recordFields?.kind === "block"
|
|
391
|
-
? recordFields
|
|
445
|
+
? flat(recordFields)
|
|
392
446
|
.filter((entry) =>
|
|
393
447
|
/^ref<parent>/.test(fieldValue(entry, spell)),
|
|
394
448
|
)
|
|
@@ -573,10 +627,39 @@ function compiledAttachment(
|
|
|
573
627
|
return Object.assign(instrument, { warnings: result.warnings });
|
|
574
628
|
}
|
|
575
629
|
|
|
576
|
-
|
|
630
|
+
/**
|
|
631
|
+
* The tunable a list field's length follows, as in `dates: list(date, count)`.
|
|
632
|
+
* Such a list takes one entry per unit of the tunable, so the guide names the
|
|
633
|
+
* tunable instead of the example's value.
|
|
634
|
+
*/
|
|
635
|
+
function listBound(
|
|
636
|
+
name: string,
|
|
637
|
+
decl: InstrumentDecl,
|
|
638
|
+
spell: (expression: Expr) => string,
|
|
639
|
+
) {
|
|
640
|
+
const fields = slot(decl.body, "fields");
|
|
641
|
+
const entry =
|
|
642
|
+
fields?.kind === "block"
|
|
643
|
+
? flat(fields).find((item) => item.key === name)
|
|
644
|
+
: undefined;
|
|
645
|
+
const bound = entry
|
|
646
|
+
? /^list\(\s*[\w<>]+\s*,\s*(\w+)\s*\)$/.exec(fieldValue(entry, spell))?.[1]
|
|
647
|
+
: undefined;
|
|
648
|
+
return bound && decl.parameters.some((parameter) => parameter.key === bound)
|
|
649
|
+
? bound
|
|
650
|
+
: undefined;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function inputType(field: Record<string, unknown>, bound?: string) {
|
|
577
654
|
const type = String(field.type);
|
|
578
655
|
if (type === "list")
|
|
579
|
-
return `list of ${String(field.item)}${
|
|
656
|
+
return `list of ${String(field.item)}${
|
|
657
|
+
bound
|
|
658
|
+
? `, one per ${bound} (${String(field.maxItems)} in the example)`
|
|
659
|
+
: field.maxItems === undefined
|
|
660
|
+
? ""
|
|
661
|
+
: `, at most ${String(field.maxItems)}`
|
|
662
|
+
}`;
|
|
580
663
|
if (type === "ref" && field.target) return `ref to ${String(field.target)}`;
|
|
581
664
|
if (type === "enum" && Array.isArray(field.values))
|
|
582
665
|
return `one of ${field.values.join(", ")}`;
|
package/std/money.hsx
CHANGED
|
@@ -12,9 +12,15 @@ 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
|
|
16
|
-
//
|
|
17
|
-
//
|
|
15
|
+
// The payee proposes a schedule and only the payer activates or cancels it; the
|
|
16
|
+
// payee stops it. Without every, create makes count occurrences on the listed
|
|
17
|
+
// dates and they sum to amount. With every (1 month, 2 weeks), each occurrence
|
|
18
|
+
// charges amount, the first at startsAt, and paying one creates the next, so only
|
|
19
|
+
// one piece waits ahead. Months step from startsAt and clamp to the month's end.
|
|
20
|
+
// create refuses a startsAt in the past. An authored count ends it after count
|
|
21
|
+
// pieces; without one it runs until stopped or cancelled. Each due occurrence pays
|
|
22
|
+
// only while the schedule is active, and one clock drain charges at most 126
|
|
23
|
+
// pieces; the next drain continues a plan that fell further behind.
|
|
18
24
|
// A pool accepts whole contributions only up to its remaining target and at most
|
|
19
25
|
// 366 paid records. Expose pay to release the exact target. Clock failure refunds
|
|
20
26
|
// original contributors; excess acceptance and partial contribution are absent.
|
|
@@ -30,9 +36,11 @@ header money
|
|
|
30
36
|
// record's own due field. Once dueAt plus grace plus one period has passed, the
|
|
31
37
|
// clock charges amount from payer to payee for each whole period, and the last
|
|
32
38
|
// charge takes only what is left under cap; then the fee is capped. The payee runs
|
|
33
|
-
// stop when the obligation is met
|
|
34
|
-
//
|
|
35
|
-
//
|
|
39
|
+
// stop when the obligation is met. Stop keeps what was owed: each charge due at or
|
|
40
|
+
// before it still posts, and the fee settles until the last of those posts, then
|
|
41
|
+
// stops. A period still running at the stop is not charged. A charge the payer cannot fund refuses as clock work and charging waits
|
|
42
|
+
// for its retry; the payee runs waive to forgive what is still owed. Grace is at
|
|
43
|
+
// least one second; there are no percentage or interest-style fees.
|
|
36
44
|
|
|
37
45
|
// Cost: transfer.pay declares one transfer. Hold.create declares one owned account;
|
|
38
46
|
// fund, release and refund each declare one transfer. Schedules and pools add bounded
|
|
@@ -84,13 +92,21 @@ instrument split(payer: party, amount: money = runtime, shares: split = { progra
|
|
|
84
92
|
action cancel { from: pending, to: cancelled }
|
|
85
93
|
}
|
|
86
94
|
|
|
87
|
-
instrument schedule(payer: party, payee: party, amount: money = runtime, count: integer(1, 366) = 12) {
|
|
88
|
-
summary: "
|
|
89
|
-
fields {
|
|
95
|
+
instrument schedule(payer: party, payee: party, amount: money = runtime, count: integer(1, 366) = 12, every: period?) {
|
|
96
|
+
summary: "Pieces due on listed dates that sum to the total, or one amount each period from a start date."
|
|
97
|
+
fields {
|
|
98
|
+
amount: money = amount
|
|
99
|
+
when every is unset { count: integer = count, dates: list(date, count) }
|
|
100
|
+
when every is set { startsAt: date }
|
|
101
|
+
}
|
|
90
102
|
lifecycle { states: [draft, active, complete, cancelled], initial: draft }
|
|
91
103
|
action create {
|
|
92
104
|
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 } } }]
|
|
105
|
+
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 } } }] }
|
|
106
|
+
when every is set {
|
|
107
|
+
requires self.startsAt >= self.now
|
|
108
|
+
invoke: [{ instrument: occurrence, action: create, input: { schedule: { field: self.id }, position: { literal: 1 } } }]
|
|
109
|
+
}
|
|
94
110
|
}
|
|
95
111
|
action activate { from: draft, to: active, actor: { party: payer } }
|
|
96
112
|
action complete {
|
|
@@ -98,20 +114,41 @@ instrument schedule(payer: party, payee: party, amount: money = runtime, count:
|
|
|
98
114
|
requires count of { instrument: occurrence, reference: "schedule", anchor: self.id, states: [pending], limit: 366 } == 0
|
|
99
115
|
}
|
|
100
116
|
action cancel { from: [draft, active], to: cancelled, actor: { party: payer } }
|
|
117
|
+
action stop { from: active, to: cancelled, actor: { party: payee } }
|
|
101
118
|
records {
|
|
102
119
|
occurrence: {
|
|
103
120
|
fields {
|
|
104
|
-
schedule: ref<parent
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
121
|
+
schedule: ref<parent>
|
|
122
|
+
when every is unset {
|
|
123
|
+
position: integer(1, count), dueAt: date = at(self.schedule.dates, self.position)
|
|
124
|
+
base: money = divide(self.schedule.amount, count)
|
|
125
|
+
allocated: money = multiply(self.base, count)
|
|
126
|
+
remainder: money = subtract(self.schedule.amount, self.allocated)
|
|
127
|
+
first: integer = divide(1, self.position)
|
|
128
|
+
adjustment: money = multiply(self.remainder, self.first)
|
|
129
|
+
amount: money = sum(self.base, self.adjustment)
|
|
130
|
+
}
|
|
131
|
+
when every is set {
|
|
132
|
+
position: integer, offset: integer = subtract(self.position, 1), following: integer = sum(self.position, 1)
|
|
133
|
+
dueAt: date = after(self.schedule.startsAt, every, self.offset)
|
|
134
|
+
amount: money = self.schedule.amount
|
|
135
|
+
}
|
|
111
136
|
}
|
|
112
137
|
lifecycle { states: [pending, paid, cancelled], initial: pending }
|
|
113
|
-
action create {
|
|
114
|
-
|
|
138
|
+
action create {
|
|
139
|
+
actor: { parent: [parent, current()] }
|
|
140
|
+
input { schedule: ref<parent>, when every is unset { position: integer(1, count) } when every is set { position: integer } }
|
|
141
|
+
requires unique "scheduled_position" on [self.schedule, self.position]
|
|
142
|
+
}
|
|
143
|
+
action pay {
|
|
144
|
+
allowZero: true, from: pending, to: paid, actor: clock, due: { at: self.dueAt }
|
|
145
|
+
requires self.schedule in [active]
|
|
146
|
+
moves self.amount from payer to payee
|
|
147
|
+
when every is set {
|
|
148
|
+
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 } } }] }
|
|
149
|
+
when count is unset { invoke: [{ instrument: current(), action: create, input: { schedule: { field: self.schedule }, position: { field: self.following } } }] }
|
|
150
|
+
}
|
|
151
|
+
}
|
|
115
152
|
action cancel { from: pending, to: cancelled, requires self.schedule in [cancelled] }
|
|
116
153
|
}
|
|
117
154
|
}
|
|
@@ -201,9 +238,9 @@ instrument late_fee(payer: party, payee: party, amount: money, cap: money, perio
|
|
|
201
238
|
summary: "Charge a fixed fee for each period a due time stays unmet, up to a cap."
|
|
202
239
|
fields {
|
|
203
240
|
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?
|
|
241
|
+
dueAt: date, graceEndsAt: date?, firstAt: date?, nextAt: date?, followingAt: date?, charge: money?, left: money?, remaining: money?, stoppedAt: date?
|
|
205
242
|
}
|
|
206
|
-
lifecycle { states: [running, capped, stopped], initial: running }
|
|
243
|
+
lifecycle { states: [running, settling, capped, stopped], initial: running }
|
|
207
244
|
action create {
|
|
208
245
|
actor: { party: payer }
|
|
209
246
|
requires self.amount > "0"
|
|
@@ -220,6 +257,23 @@ instrument late_fee(payer: party, payee: party, amount: money, cap: money, perio
|
|
|
220
257
|
moves self.charge from payer to payee economics { recipient: payee, company: { purpose: earning, sourceParty: payer }, participant: { purpose: pass_through, sourceParty: payer } }
|
|
221
258
|
invoke: [{ selection: { instrument: current(), reference: "payer", anchor: self.payer, states: [running], limit: 1, where: { id: { field: self.id }, left: { literal: "0" } } }, action: finish, input: {} }]
|
|
222
259
|
}
|
|
223
|
-
action
|
|
224
|
-
|
|
260
|
+
action collect {
|
|
261
|
+
summary: "Collect owed late fee"
|
|
262
|
+
from: settling, to: settling, actor: clock, due: { at: self.nextAt }
|
|
263
|
+
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 }]
|
|
264
|
+
set: { left: { field: self.remaining }, nextAt: { field: self.followingAt } }
|
|
265
|
+
moves self.charge from payer to payee economics { recipient: payee, company: { purpose: earning, sourceParty: payer }, participant: { purpose: pass_through, sourceParty: payer } }
|
|
266
|
+
invoke: [
|
|
267
|
+
{ reference: self.id, action: settle, guard: { kind: compare, left: { field: self.nextAt }, operator: ">", right: { field: self.stoppedAt } }, input: {} }
|
|
268
|
+
{ selection: { instrument: current(), reference: "payer", anchor: self.payer, states: [settling], limit: 1, where: { id: { field: self.id }, left: { literal: "0" } } }, action: finish, input: {} }
|
|
269
|
+
]
|
|
270
|
+
}
|
|
271
|
+
action finish { from: [running, settling], to: capped, actor: { parent: current() } }
|
|
272
|
+
action stop {
|
|
273
|
+
from: running, to: settling, actor: { party: payee }
|
|
274
|
+
set: { stoppedAt: { field: self.now } }
|
|
275
|
+
invoke: [{ reference: self.id, action: settle, guard: { kind: compare, left: { field: self.nextAt }, operator: ">", right: { field: self.stoppedAt } }, input: {} }]
|
|
276
|
+
}
|
|
277
|
+
action settle { from: settling, to: stopped, actor: { parent: current() } }
|
|
278
|
+
action waive { from: settling, to: stopped, actor: { party: payee } }
|
|
225
279
|
}
|