@hyperscale0/hsx 2.2.1 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/CHANGELOG.md +23 -1
  2. package/dist/src/cli.d.ts +1 -1
  3. package/dist/src/diagnostics.d.ts.map +1 -1
  4. package/dist/src/diagnostics.js +7 -0
  5. package/dist/src/diagnostics.js.map +1 -1
  6. package/dist/src/std-bundle.js +2 -2
  7. package/dist/src/std-bundle.js.map +1 -1
  8. package/dist/src/typecheck.d.ts.map +1 -1
  9. package/dist/src/typecheck.js +36 -15
  10. package/dist/src/typecheck.js.map +1 -1
  11. package/dist/src/version.d.ts +1 -1
  12. package/dist/src/version.js +1 -1
  13. package/docs/guide/03-instruments.md +17 -0
  14. package/docs/llms-full.txt +61 -4
  15. package/docs/llms.txt +2 -2
  16. package/docs/reference/cli.md +2 -2
  17. package/docs/reference/diagnostics.md +21 -1
  18. package/docs/reference/grammar.md +2 -2
  19. package/docs/reference/std/advance.md +1 -1
  20. package/docs/reference/std/cancellable_booking.md +5 -2
  21. package/docs/reference/std/captured_payment.md +1 -1
  22. package/docs/reference/std/conditional_disbursement.md +1 -1
  23. package/docs/reference/std/credit_facility.md +1 -1
  24. package/docs/reference/std/held_payment.md +1 -1
  25. package/docs/reference/std/instant_transfer.md +1 -1
  26. package/docs/reference/std/metered.md +1 -1
  27. package/docs/reference/std/pooled_split.md +1 -1
  28. package/docs/reference/std/premium_forward.md +1 -1
  29. package/docs/reference/std/reconciled_payout.md +1 -1
  30. package/docs/reference/std/rotating_pool.md +1 -1
  31. package/docs/reference/std/scheduled.md +1 -1
  32. package/docs/reference/std/security_deposit.md +1 -1
  33. package/docs/reference/std/settlement_batch.md +1 -1
  34. package/docs/reference/std/swap.md +1 -1
  35. package/docs/reference/std/threshold_pool.md +1 -1
  36. package/docs/reference/std/weighted_distribution.md +1 -1
  37. package/docs/reference/types.md +18 -1
  38. package/docs/reference/udl-output.md +1 -1
  39. package/examples/cancellable_booking/cancellable_booking.udl +1 -0
  40. package/examples/cost-table.json +36 -4
  41. package/package.json +3 -3
  42. package/src/diagnostics.ts +7 -0
  43. package/src/std-bundle.ts +2 -2
  44. package/src/typecheck.ts +35 -15
  45. package/src/version.ts +1 -1
  46. package/std/money_flows/advance.hsx +1 -1
  47. package/std/money_flows/cancellable_booking.hsx +7 -5
package/src/std-bundle.ts CHANGED
@@ -2,8 +2,8 @@
2
2
 
3
3
  export const BUNDLED_STD_FILES: ReadonlyMap<string, string> = new Map<string, string>([
4
4
  ["SEMANTICS.md", "# Standard library money flows semantics\n\nThis document records the runtime semantics and settlement mechanics of standard library modules under `std.money_flows`. The compiler and its unit specifications under `test/` remain the authoritative implementation reference.\n\n## Direct checkout and card\n\n- `instant_transfer` stores the amount, currency, payer, and payee. Action `create` opens the payment record without moving funds. Action `pay_piece_1` and subsequent piece actions execute the money movements, transitioning to state `paid`. A payer fee executes as an `on_top` move. A payee fee carves out of the base amount. Exact, basis-point, and tiered rules generate fee fields and `feeRules`. The finest common refinement of all fee cuts produces piece fields and one partition. Floor rounding sends minor-unit remainder to the designated non-fee recipient. A derived amount adds a floor percentage field, `derivedAmounts`, a platform party account, and its own transfer piece.\n- `captured_payment` stores authorization total, reserve and reversal dates, and party accounts. The instrument moves through `created`, `authorized`, optional `partially_captured`, `settled`, `voided`, `expired`, `corrected`, or `reversed`. Action `authorize` reserves custody on the payer balance. Actions `capture` and `capture_more` consume the authorized balance using partial posts. Action `settle` posts the captured amount. Actions `void` and `expire` release the reserve. Actions for payee correction and external reversal use their declared condition ports and deadline windows. Derived fees use the same floor, partition, bearer, and position rules as direct payment. Four unused signature knobs (`capture_mode`, `correction_mode`, `negative_position`, `timeout`) are pruned.\n\n## Custody and escrow\n\n- `held_payment` reserves every refined piece in a dedicated escrow account before release. It posts release pieces, voids cancellation pieces, and keeps payer service fees outside the held principal. Release and cancellation fee sides share the finest common partition, so each stored piece has one release recipient and one cancel recipient. States track funding, release, cancellation, and abandonment steps. Abandonment refunds every still-held piece. A deadline release and a caller decision release carry the named date or port clause. Retention forms reserve one held piece with a release deadline and cancellation back to the contractor. Whole-amount mode funds principal and on-top fees in one action, then releases or cancels the principal whole. Quoted cancellation uses `cancel_charge_bps` and `cancel_offer_life` to freeze penalty quotes before confirm and retain actions.\n- `cancellable_booking` holds booking funds in escrow away from guest and host until the booking ends or cancels. Cancellation quotes a penalty against the time remaining before `starts_at`, frozen for `offer_life`. Lifecycle moves through `created`, `held`, `cancellation_quoted`, `canceled`, `settled`, and `completed`. Penalty retention moves penalty funds to host and remainder to guest; completion releases full amount to host.\n- `security_deposit` reserves the full deposit amount in escrow. A claim port posts either the whole hold or an externally decided amount bounded by the hold. The explicit remainder returns to the payer. A return port voids the whole hold. The deadline form adds machine expiry and unfunded cancellation.\n- `premium_forward` holds premium pieces in escrow, then forwards net pieces after a carrier bind decision. It can abandon unbound custody. Policy reference, renewal due, endorsement evidence, and lapse actions activate when declared. Fee partition and floor remainder rules match `held_payment`.\n\n## Subscriptions, usage, and schedules\n\n- `scheduled` executes calendar-anchored payment series between one payer and one payee across three operational modes:\n 1. Finite installment plan: Stores total, first due date, and one money field per installment. The caller supplies each piece; a partition checks their sum, not equality. States and actions unroll in order with cumulative duration offsets.\n 2. Open recurring subscription: Charges a recurring amount on interval `every` until an `until` port fires, opening one period liability at a time.\n 3. Debt obligation: Emits a parent obligation plus one child payment instrument per installment. The parent stores principal, delinquency dates, party accounts, and aggregate invariants. Child instruments bind parent fields, repay against installment caps, and support full refunds. Finite obligation count generalization applies without grace periods.\n- `metered` stores one money field for each declared meter in its rate card and a period end date. Each usage charge executes an immediate transfer directly from payer to payee without escrow custody. Closing the period transitions state to `closed` at `close_by` and makes further charges unreachable.\n\n## Lending and credit\n\n- `advance` provides upfront capital disbursed to a recipient and repaid through two distinct modes:\n 1. Carved advance: References an active escrow hold and recourse instrument through `against`. It disburses once and settles from the referenced release without minting new value. In this mode, the `fee` parameter is ignored.\n 2. Scheduled advance: Stores advance amount, fee amount, repayable total, first due date, and one repayment field per installment. A supplied `fee` derives feeAmount as a floor percentage of principal. The caller supplies principal, repayable total and repayment pieces, which partitions reconcile. `repayment_source` can differ from the capital recipient. `profit_to` splits each collection between principal returned to the funder and profit paid to its recipient. `dated: true` requires one stored signed due date per repayment. Equality, chronological ordering, partial servicing and arrears are not implied by those partitions.\n- `credit_facility` stores lender, borrower, draw destination, credit limit, currency, and expiry date. The facility transitions across `active`, `frozen`, and `closed`. Each draw creates a child instrument with draw amount, facility reference, and obligation reference. Draw admission checks facility active state and aggregate child exposure against the limit. Closure requires all child draws to reach status `resolved`. Unused signature knobs (`availability_policy`, `expiry_policy`, `close_policy`) are pruned.\n- `conditional_disbursement` emits a submitted parent instrument with cap, currency, source account, destination account, and decision port. Denial terminates the parent before child approvals. Child approval instruments store externally bounded amounts. Child actions capture port input, move through `created`, `approved`, and `paid`, and verify aggregate child exposure against the parent cap. Unused signature knobs (`reopen_policy`, `recovery_policy`) are pruned.\n\n## Multi-party distributions and pooling\n\n- `pooled_split` stores pool total, due date, and one share amount and account per recipient. Positive shares total exactly 10,000 basis points. Floor division assigns each share, and the designated remainder recipient absorbs minor-unit residues. Funding and distribution actions execute in roster order.\n- `weighted_distribution` emits a parent distribution instrument and child entitlement instruments. The parent stores source account, total amount, record date, and maximum recipient count. Child rows store recipient, weight, source, currency, and parent reference. A largest-remainder distribution algorithm pays children once. Aggregate clauses cap and total child records against the pool.\n- `threshold_pool` emits a pool parent instrument and child commitment instruments. The parent stores target amount, close date, maximum contributors, beneficiary, and currency. It transitions through `open`, `active`, `failed`, or `settled` based on aggregate commitment gates. Each commitment can cancel before collection, collect into the pool, or refund after campaign failure.\n- `rotating_pool` emits a parent pool instrument plus one contribution child instrument per member. The parent unrolls active and ready states for every cycle and checks that each member row exists exactly once. Member child instruments unroll due, funded, defaulted, guaranteed, paid, and completed states for every cycle. A funded or guaranteed contribution pays the fixed beneficiary for that cycle. Final closure requires the escrow balance to drain to zero.\n\n## Bilateral exchange, clearing, and reconciliation\n\n- `swap` stores amounts, accounts, fees, currency, and optional clawback dates for both sides. Funding is atomic across both sides. Release, settlement, cancellation, dispute, abandonment, and clawback preserve the two-sided conservation group. Side fees retain their declared bearer and position. Unwind refunds each side and applies declared penalty tiers without altering principal partitions.\n- `settlement_batch` emits a batch instrument alongside capture, credit-adjustment, and debit-adjustment child records. The batch stores close time, settlement and payout accounts, beneficiary reference, and currency. Closure freezes intake. Calculation applies signed arithmetic across capture, fee, and reversal entries. Actions instruct, acknowledge, and reconcile through the declared payout port.\n- `reconciled_payout` instructs an external payout with a tolerance window, waits for a matching bank debit statement line, and tracks unmatched amounts at settlement date as break rows. Lifecycle transitions through `created`, `instructed`, and `settled`.\n"],
5
- ["money_flows/advance.hsx", "module std.money_flows.advance\n\n// Upfront capital disbursement repaid by carving future held payment releases or over scheduled installments.\n//\n// ### Purpose\n// `advance` provides working capital, merchant cash advances, or contractor pre-funding from a funder to a recipient.\n// Repayment occurs either automatically by intercepting (carving) releases from a linked escrow hold (`against`),\n// or over calendar-anchored installment repayments (`count`, `every`, `first_due`).\n//\n// ### Selection guidance\n// - vs `credit_facility`: `advance` disburses a single upfront lump-sum principal that is repaid over time.\n// `credit_facility` establishes a reusable revolving credit line with multiple draws up to a limit,\n// where repayments restore available borrowing capacity.\n// - vs `conditional_disbursement`: `advance` expects repayment of the advanced principal plus optional fees.\n// `conditional_disbursement` disburses non-repayable grants, claims, or milestone payments against external evidence.\n// - vs `held_payment`: `held_payment` holds customer funds in escrow until delivery. An `advance` can carve repayments\n// directly out of a `held_payment`'s release using `against`.\n//\n// ### Parameters\n// - `funder`: The party providing the upfront capital.\n// - `to`: The party receiving the advance and responsible for repayment.\n// - `amount`: Total advanced principal in minor units of currency `C`.\n// - `fee`: Optional markup percentage fee charged on the advance (basis points precision).\n// - `count`: Optional number of scheduled installment repayments.\n// - `every`: Optional recurrence duration between installments (e.g. `\"P30D\"`).\n// - `first_due`: Optional date for the first installment repayment.\n// - `repayment_source`: Optional party repaying when distinct from the capital recipient.\n// - `profit_to`: Optional recipient of profit, separated from principal repayments.\n// - `dated`: Require an explicit signed date for each repayment instead of duration offsets.\n// - `against`: Optional reference to a hold instrument whose release will be carved to repay the advance.\n//\n// ### Decision ports\n// None. Repayment is driven by linked hold releases or scheduled calendar dates.\n//\n// ### Example\n// ```hsx\n// program advance_example \"Advance example\"\n// import { advance } from \"std/money_flows\"\n// party funder: business\n// party recipient: business\n// settlement advance_payment = advance {\n// funder: funder\n// to: recipient\n// amount: principal: money(SAR)\n// fee: 2.5%\n// count: 2\n// every: P30D\n// first_due: firstDueAt\n// }\n// ```\nexport instrument advance<C>(\n funder: party,\n to: party,\n amount: money<C>,\n fee: optional<percent>,\n count: optional<integer>,\n every: optional<text>,\n first_due: optional<date>,\n against: optional<ref>,\n repayment_source: optional<party>,\n profit_to: optional<party>,\n dated: optional<boolean>,\n) {\n let(amount_path): concat(\"fields.\", amount);\n let(currency_path): \"fields.currency\";\n let(funder_field): camel(concat(funder, \"_account_id\"));\n let(to_field): camel(concat(to, \"_account_id\"));\n\n agent_description: \"Reach for advance when the {funder} pays the {to} up front and is repaid afterwards, either out of a carved hold release or over scheduled repayments. Pick deposit for a refundable reservation and instant transfer when nobody repays the payer.\";\n title: sentence(instrument);\n id_prefix: prefix(instrument);\n parties {\n payer: funder;\n beneficiary: to;\n when(repayment_source) { repayment: repayment_source; }\n when(profit_to) { profit: profit_to; }\n }\n\n when(against) {\n let(hold_name): owner(against);\n summary: \"Advance to the {to} repaid by carving the {hold_name} release\";\n description: \"Advance: the {funder} disburses {amount} to the {to} and is repaid out of the {hold_name} release, which pays the {funder} in the {to}'s place\";\n fields {\n amount {\n type: money<C>;\n description: \"The advanced amount in {C} minor units, disbursed to the {to} up front\";\n }\n carve_hold_id {\n type: ref<hold_name>;\n description: \"The {hold_name} bound to this advance\";\n pattern: concat(\"^\", prefix(hold_name), \"_(sandbox|live)_[a-z0-9]{8,64}$\");\n }\n carve_recourse_1_id {\n type: ref<recourse>;\n description: \"The recourse bound to this advance\";\n pattern: \"^reco_(sandbox|live)_[a-z0-9]{8,64}$\";\n }\n }\n required: [to_field, funder_field, amount, carveHoldId, carveRecourse1Id, currency];\n lifecycle {\n states created advanced repaid;\n initial created;\n on disburse: created -> advanced;\n on settle: advanced -> repaid;\n }\n action create {\n agent_description: \"Open the advance row before any money moves. The caller supplies the {amount}, both party accounts, and the ids of the {hold_name} and the recourse that back repayment. Nothing is disbursed here.\";\n summary: concat(\"Create a \", words(instrument));\n steps: [];\n moves: [];\n }\n action disburse {\n agent_description: \"Pay the {amount} from the {funder} to the {to}. The advance must be created, the bound {hold_name} funded, and the recourse active, both matching this amount and currency. The transfer posts straight out and does not unwind.\";\n summary: \"Disburse the {amount} to the {to}\";\n requires refs: [{\n field: carveHoldId;\n match: {\n [currency_path]: currency_path;\n [amount_path]: amount_path;\n };\n statuses: [funded];\n }, {\n field: carveRecourse1Id;\n match: {\n [currency_path]: currency_path;\n [amount_path]: amount_path;\n };\n statuses: [active];\n }];\n moves: [{ amount: amount; from: payer; to: beneficiary; key: transfer; operation: create; }];\n steps: [];\n }\n action settle {\n agent_description: \"Close the advance once the {hold_name} has released to the {funder} in the {to}'s place. The advance must already be disbursed. No money moves here, the release did the repaying.\";\n summary: concat(\"Close the advance once the \", hold_name, \" has released to the \", funder);\n steps: [];\n moves: [];\n }\n }\n\n when_not(against) {\n let(repayment_role): if_eq(repayment_source, false, \"beneficiary\", \"repayment\");\n when(fee) { computes derived: { field: feeAmount; source_field: amount; rule: { kind: percentage_of; bps: basis_points(fee); }; rounding: floor; }; }\n let(repayment_fields): names(\"repayment\", count, \"Amount\");\n let(first_target): if_eq(count, 1, \"repaid\", \"repaying_1\");\n let(states_key): \"states\";\n let(first_due_field): camel(first_due);\n summary: concat(\"Advance to \", to, \" repaid over \", count, \" anchors\");\n description: concat(\"Advance: the \", funder, \" disburses \", amount, \" to the \", to, \", repaid over \", count, \" scheduled repayments plus the funder's discount\");\n fields {\n amount {\n type: money<C>;\n description: \"The advanced amount in {C} minor units, disbursed to the {to} up front\";\n }\n fee_amount {\n type: money<C>;\n when(fee) { optional: true; }\n description: concat(fee, \" of \", amount, \", the funder's discount repaid on top of the advance\");\n }\n repayable_amount {\n type: money<C>;\n description: concat(amount, \" + feeAmount: the total the repayment fields below partition exactly\");\n }\n [first_due_field] {\n type: date;\n description: concat(\"Due date of the first repayment; repayment k falls \", every, \" after its predecessor\");\n }\n for i in count {\n repayment[i]Amount { type: money<C>; description: \"Stored total for repayment {i}\"; }\n when(profit_to) {\n repayment[i]Principal { type: money<C>; description: \"Principal returned to the funder by repayment {i}\"; }\n repayment[i]Profit { type: money<C>; description: \"Profit paid to the named recipient by repayment {i}\"; }\n }\n when(dated) { repayment[i]DueAt { type: date; description: \"Signed calendar due date for repayment {i}\"; } }\n }\n }\n lifecycle {\n states created advanced;\n [states_key]: [first_target];\n initial created;\n on disburse: created -> advanced;\n on collect_repayment_1: advanced -> [first_target];\n for repayment in sub(count, 1) {\n let(index): add(repayment, 1);\n let(previous): concat(\"repaying_\", repayment);\n let(target): if_eq(index, count, \"repaid\", concat(\"repaying_\", index));\n [states_key]: [target];\n on collect_repayment_[index]: [previous] -> [target];\n }\n }\n partitions: [{\n piece_fields: repayment_fields;\n total_field: repayableAmount;\n }, {\n piece_fields: [amount, feeAmount];\n total_field: repayableAmount;\n }];\n when(profit_to) {\n partitions: { total_field: amount; piece_fields: names(\"repayment\", count, \"Principal\"); };\n partitions: { total_field: feeAmount; piece_fields: names(\"repayment\", count, \"Profit\"); };\n for i in count {\n partitions: { total_field: concat(\"repayment\", i, \"Amount\"); piece_fields: [concat(\"repayment\", i, \"Principal\"), concat(\"repayment\", i, \"Profit\")]; };\n }\n }\n action create {\n agent_description: \"Open the advance row before any money moves. The caller supplies the {amount}, the repayable total, and repayment dates. A declared fee derives the fee amount from principal. The repayment fields must partition the repayable total exactly. Nothing is disbursed here.\";\n summary: concat(\"Create a \", words(instrument));\n steps: [];\n moves: [];\n }\n action disburse {\n agent_description: \"Pay the {amount} from the {funder} to the {to} and start the repayment schedule. The advance must be created. The transfer posts straight out and does not unwind. Each repayment is collected on its own stored date anchor.\";\n summary: \"Disburse the {amount} to the {to}\";\n moves: [{ amount: amount; from: payer; to: beneficiary; key: transfer; operation: create; }];\n steps: [];\n }\n for i in count {\n let(repayment_field): concat(\"repayment\", i, \"Amount\");\n action collect_repayment_[i] {\n agent_description: \"Collect repayment {i} once on its stored due anchor, returning principal to the funder and any separately routed profit to its recipient.\";\n summary: \"Collect repayment {i} of {count}\";\n when(dated) { due: { field: concat(\"repayment\", i, \"DueAt\"); }; }\n when_not(dated) { due: { field: first_due_field; offset: scale_duration(every, sub(i, 1)); }; }\n when_not(profit_to) { moves: [{ amount: repayment_field; from: repayment_role; to: payer; key: transfer; }]; }\n when(profit_to) {\n moves: [\n { amount: concat(\"repayment\", i, \"Principal\"); from: repayment_role; to: payer; key: principal; },\n { amount: concat(\"repayment\", i, \"Profit\"); from: repayment_role; to: profit; key: profit; }\n ];\n }\n steps: [];\n }\n }\n }\n}\n"],
6
- ["money_flows/cancellable_booking.hsx", "module std.money_flows.cancellable_booking\n\n// Time-anchored reservation holding booking funds in escrow with dynamically quoted cancellation penalties.\n//\n// ### Purpose\n// `cancellable_booking` holds booking fees in escrow from a guest for a host until a scheduled start date.\n// It fits hospitality, travel reservations, studio rentals, event tickets, and appointments where guests\n// can cancel prior to start, but cancellation penalties depend on how close to the start date cancellation occurs.\n//\n// ### Selection guidance\n// - vs `held_payment`: Both hold money in escrow and both quote a cancellation before it is spent.\n// `cancellable_booking` prices the penalty against the time left before `starts_at`, so the charge moves\n// as the start approaches; `held_payment` has no start date and quotes one flat charge.\n// Choose `cancellable_booking` whenever cancellation fees are time-sensitive.\n// - vs `security_deposit`: `security_deposit` holds funds to cover damages claimed by the holder.\n// `cancellable_booking` holds the service fee itself and releases to the host upon `starts_at` or refunds net of penalty.\n//\n// ### Parameters\n// - `guest`: The booking customer paying the fee and receiving refunds.\n// - `host`: The service provider receiving the payout or retained cancellation penalty.\n// - `amount`: Total booking price in minor units of currency `C`.\n// - `starts_at`: Stored ISO 8601 date when the booking begins.\n// - `late_penalty_bps`: Penalty in basis points applied when cancelling within the late window.\n// - `late_within`: ISO 8601 duration defining the late window before `starts_at` (e.g. `\"P2D\"` for 2 days).\n// - `early_penalty_bps`: Penalty in basis points applied when cancelling before the late window.\n// - `offer_life`: ISO 8601 duration defining how long a quoted cancellation offer remains valid (e.g. `\"PT30M\"`).\n//\n// ### Decision ports\n// None. Action transitions are driven by guest API calls (`take`, `cancel`, `confirm`) and scheduled completion (`complete` due at `starts_at`).\n//\n// ### Example\n// ```hsx\n// program studio_booking \"Studio booking\"\n// import { cancellable_booking } from \"std/money_flows\"\n// party guest: person\n// party studio: business\n// settlement studio_session = cancellable_booking {\n// guest: guest\n// host: studio\n// amount: sessionPrice: money(SAR)\n// starts_at: startsAt\n// late_penalty_bps: 5000\n// late_within: \"P2D\"\n// early_penalty_bps: 1000\n// offer_life: \"PT30M\"\n// }\n// ```\n// ### Finite cancellation bands\n// Supply cancel_bands to select deposit/balance custody instead of quoted cancellation.\n// Each band has name, refund (full, balance or none), starts_at after the first band,\n// and supplier_bps when money is retained. The unpaid_band index selects a retained\n// deposit policy. Omit it to return the deposit; a full-refund band also returns it. price_value and supplier_cost fix the price; deposit_bps derives\n// the deposit. balance_before and confirmation_window are positive fixed durations\n// relative to departure. tax_bps applies to margin after the supplier share.\n// authority_type and supplier_role bind release to a consumed confirmation decision.\n// acceptance optionally supplies an aggregate gate. All actions are private until\n// expose selects them. Ordered band starts and deadlines prevent a late sweep from\n// extending an earlier band's cancellation rights. Full timeout refund returns price.\n// Seven derivations suffice for three bands; larger finite policies use up to the\n// UDL bound of 64 derived amounts. shift_date(date, duration, \"before\") subtracts a\n// fixed duration; omitting the direction adds it. Invalid dates or durations refuse.\n//\nexport instrument cancellable_booking<C>(\n guest: optional<party>,\n host: optional<party>,\n amount: optional<money<C>>,\n starts_at: optional<date>,\n late_penalty_bps: optional<integer>,\n late_within: optional<text>,\n early_penalty_bps: optional<integer>,\n offer_life: optional<text>,\n cancel_bands: optional<list<block>>,\n price_value: optional<text>, supplier_cost: optional<text>, departure: optional<text>,\n balance_before: optional<text>, confirmation_window: optional<text>, unpaid_band: optional<integer>,\n acceptance: optional<block>, deposit_bps: optional<integer>, tax_bps: optional<integer>,\n authority_type: optional<text>, supplier_role: optional<text>,\n\n) {\n when_not(cancel_bands) {\n let(guest_account_field): camel(concat(guest, \"_account_id\"));\n let(host_account_field): camel(concat(host, \"_account_id\"));\n let(reference_filter_key): \"x-hyperscale-reference-filter\";\n let(owner_id_key): \"owner.id\";\n let(owner_type_key): \"owner.type\";\n fields {\n amount { type: money<C>; description: concat(\"Booking price in minor units, held away from both the \", words(guest), \" and the \", words(host), \" until the booking ends\"); }\n currency { type: text; description: \"ISO 4217 currency code\"; minLength: 3; maxLength: 3; pattern: \"^[A-Z]{3}$\"; }\n starts_at { type: date; description: \"When the booking starts; the cancellation penalty is priced against the time left before it\"; }\n [guest_account_field] { type: account<C>; description: concat(\"The \", words(guest), \" account the booking is taken from and the refund returns to\"); [reference_filter_key]: { column: role; values: [customer_balance]; }; }\n [host_account_field] { type: account<C>; description: concat(\"The \", words(host), \" account the booking and any penalty end up in\"); [reference_filter_key]: { column: role; values: [customer_balance]; }; }\n }\n lifecycle {\n states created held cancellation_quoted canceled settled completed;\n initial created;\n on take: created -> held;\n on complete: held | cancellation_quoted -> completed;\n on cancel: held -> cancellation_quoted;\n on confirm: cancellation_quoted -> canceled;\n on retain: canceled -> settled;\n }\n parties { beneficiary: host_account_field; payer: guest_account_field; }\n required: [amount, currency, guest_account_field, host_account_field, starts_at];\n title: sentence(instrument);\n agent_description: concat(\"Reach for cancellable booking when the \", words(guest), \" pays up front and can back out at a price they see before they agree to it. The money sits in a holding account, the penalty is priced against the time left before the start, and the quote is frozen until it expires. Pick held payment when there is no cancellation price to quote.\");\n id_prefix: prefix(instrument);\n summary: concat(\"Booking the \", words(guest), \" can cancel at a price they see before they agree to it\");\n description: \"A booking whose cancellation is quoted rather than argued: the penalty is priced against the time left before the start, frozen with the terms it was read from, and paid only if the guest confirms while the offer is still alive\";\n action create {\n agent_description: concat(\"Open the booking and provision the holding account the price sits in. No money moves. The price, the currency, the start moment, and both party accounts are fixed here. Call take next to collect from the \", words(guest), \".\");\n summary: concat(\"Open a \", words(instrument), \" and its holding account\");\n moves: [];\n steps: [{ operation: \"account.escrow.provision\"; bind: { currency: { from: instance; path: fields.currency; }; [owner_id_key]: { from: instance; path: productId; }; [owner_type_key]: { from: const; value: product; }; productId: { from: instance; path: productId; }; role: { from: const; value: product_escrow; }; }; capture: { bookingHoldingAccountId: accountId; }; }];\n }\n action take {\n agent_description: concat(\"Move the whole booking price out of the \", words(guest), \" account into the holding account. This moves money. Only a created booking takes. Neither party can spend the money until the booking completes or a cancellation is confirmed.\");\n summary: concat(\"Take the booking price from the \", words(guest), \" into the holding account\");\n moves: [{ key: booking; operation: internal_transfer.create; bind: { amount: { from: instance; path: concat(\"fields.\", amount); }; currency: { from: instance; path: fields.currency; }; destinationAccountId: { from: instance; path: refs.bookingHoldingAccountId; }; productId: { from: instance; path: productId; }; sourceAccountId: { from: instance; path: concat(\"fields.\", guest_account_field); }; }; capture: { bookingTransferId: transferId; }; }];\n sandbox_failure_point: funding;\n steps: [];\n }\n action complete {\n summary: concat(\"Release the whole booking to the \", words(host), \" once the stay has started, even with a cancellation quote parked un-acted\");\n due { field: starts_at; }\n moves: [{ key: release; operation: internal_transfer.create; bind: { amount: { from: instance; path: concat(\"fields.\", amount); }; currency: { from: instance; path: fields.currency; }; destinationAccountId: { from: instance; path: concat(\"fields.\", host_account_field); }; productId: { from: instance; path: productId; }; sourceAccountId: { from: instance; path: refs.bookingHoldingAccountId; }; }; capture: { releaseTransferId: transferId; }; }];\n steps: [];\n }\n action cancel {\n agent_description: \"Price the cancellation and freeze that price. No money moves. The penalty is computed from the time left before the start, and the quote expires on its own, after which a new cancel prices it again. Call confirm to spend the quote.\";\n summary: \"Price the cancellation and hold that price open\";\n quote {\n anchorField: starts_at;\n baseField: amount;\n chargeRef: cancellationPenaltyAmount;\n charges: [{ bps: late_penalty_bps; withinOffset: late_within; }, { bps: early_penalty_bps; }];\n expires { offset: offer_life; }\n fixes: [amount, guest_account_field];\n netDestinationField: guest_account_field;\n netRef: cancellationRefundAmount;\n }\n moves: []; steps: [];\n }\n action confirm {\n agent_description: concat(\"Spend the frozen quote and return the net refund to the \", words(guest), \". This moves money and does not reverse. It requires an un-expired quote from cancel, and the penalty the refund left behind is paid to the \", words(host), \" by retain.\");\n summary: concat(\"Spend the quoted cancellation and return the net to the \", words(guest));\n commit: cancel;\n moves: [{ key: refund; operation: internal_transfer.create; bind: { amount: { from: instance; path: refs.cancellationRefundAmount; }; currency: { from: instance; path: fields.currency; }; destinationAccountId: { from: instance; path: concat(\"fields.\", guest_account_field); }; productId: { from: instance; path: productId; }; sourceAccountId: { from: instance; path: refs.bookingHoldingAccountId; }; }; capture: { cancellationTransferId: transferId; }; }];\n steps: [];\n }\n action retain {\n agent_description: concat(\"Pay the quoted cancellation penalty out of the holding account to the \", words(host), \". This moves money and does not reverse. The booking must already be canceled, so the penalty figure is the one the quote fixed.\");\n summary: concat(\"Pay the quoted penalty that the refund left behind to the \", words(host));\n moves: [{ key: penalty; operation: internal_transfer.create; bind: { amount: { from: instance; path: refs.cancellationPenaltyAmount; }; currency: { from: instance; path: fields.currency; }; destinationAccountId: { from: instance; path: concat(\"fields.\", host_account_field); }; productId: { from: instance; path: productId; }; sourceAccountId: { from: instance; path: refs.bookingHoldingAccountId; }; }; capture: { penaltyTransferId: transferId; }; }];\n steps: [];\n }\n }\n when(cancel_bands) {\n let(band_count): len(cancel_bands);\n for band in cancel_bands {\n let(refund): get(band, \"refund\");\n let(not_full): if_eq(refund, \"full\", false, true);\n let(not_balance): if_eq(refund, \"balance\", false, true);\n let(not_none): if_eq(refund, \"none\", false, true);\n when(not_full) { when(not_balance) { when(not_none) {\n unsupported { code: HSX1407; message: \"Unknown cancellation refund policy\"; fix: \"Choose full, balance or none\"; }\n } } }\n }\n let(deposit_states): names(\"deposit_\", len(cancel_bands), \"\");\n let(full_states): names(\"full_\", len(cancel_bands), \"\");\n fields {\n bookingKey { type: text; min_length: 1; }\n signedTermsReference { type: text; min_length: 1; }\n currency { type: text; const: C; }\n customerAccountId { type: account<C>; \"x-hyperscale-reference-filter\": { column: role; values: [customer_balance]; }; }\n supplierAccountId { type: account<C>; \"x-hyperscale-reference-filter\": { column: role; values: [customer_balance]; }; }\n operatorAccountId { type: account<C>; \"x-hyperscale-reference-filter\": { column: role; values: [customer_balance]; }; }\n taxAccountId { type: account<C>; \"x-hyperscale-reference-filter\": { column: role; values: [customer_balance]; }; }\n authorityId { type: ref<authority_type>; optional: true; }\n supplierRole { type: text; const: supplier_role; }\n travellerCount { type: integer; minimum: 1; maximum: 20; }\n departureAt { type: date; const: departure; }\n balanceDueAt { type: date; const: shift_date(departure, balance_before, \"before\"); }\n confirmationDeadline { type: date; const: shift_date(departure, confirmation_window); }\n price { type: money<C>; const: price_value; }\n supplierCost { type: money<C>; const: supplier_cost; }\n depositAmount { type: money<C>; optional: true; }\n balanceAmount: money<C>;\n margin: money<C>;\n operatorNet: money<C>;\n marginVat { type: money<C>; optional: true; }\n for i in len(cancel_bands) {\n let(band): at(cancel_bands, i);\n let(refund_mode): get(band, \"refund\");\n let(full_refund): if_eq(refund_mode, \"full\", true, false);\n let(balance_refund): if_eq(refund_mode, \"balance\", true, false);\n let(retains): if_eq(refund_mode, \"full\", false, true);\n let(later): if_eq(i, 1, false, true);\n when(later) { band[i]Start { type: date; const: get(band, \"starts_at\"); } }\n\n when(retains) {\n band[i]Supplier { type: money<C>; optional: true; }\n let(portions): if_eq(refund_mode, \"balance\", [\"deposit\"], [\"deposit\", \"full\"]);\n for paid in portions {\n let(is_full): if_eq(paid, \"full\", true, false);\n let(portion): if_eq(refund_mode, \"balance\", \"deposit\", paid);\n let(margin_field): concat(\"band\", i, title(portion), \"Margin\");\n let(net_field): concat(\"band\", i, title(portion), \"Net\");\n let(vat_field): concat(\"band\", i, title(portion), \"Vat\");\n [margin_field]: money<C>;\n [net_field]: money<C>;\n [vat_field] { type: money<C>; optional: true; }\n }\n }\n }\n }\n parties { payer: customerAccountId; beneficiary: supplierAccountId; fee: operatorAccountId; tax: taxAccountId; }\n distinct_parties: true;\n computes derived: { field: depositAmount; source_field: price; rule: { kind: percentage_of; bps: deposit_bps; }; rounding: floor; };\n computes derived: { field: marginVat; source_field: margin; rule: { kind: percentage_of; bps: tax_bps; }; rounding: floor; };\n for i in len(cancel_bands) {\n let(band): at(cancel_bands, i);\n let(refund_mode): get(band, \"refund\");\n let(balance_refund): if_eq(refund_mode, \"balance\", true, false);\n when(balance_refund) {\n partitions: { total_field: price; piece_fields: [balanceAmount, concat(\"band\", i, \"Supplier\"), concat(\"band\", i, \"DepositNet\"), concat(\"band\", i, \"DepositVat\")]; };\n }\n }\n partitions: { total_field: price; piece_fields: [supplierCost, operatorNet, marginVat]; };\n partitions: { total_field: price; piece_fields: [depositAmount, balanceAmount]; };\n partitions: { total_field: price; piece_fields: [supplierCost, margin]; };\n partitions: { total_field: margin; piece_fields: [operatorNet, marginVat]; };\n date order: { before_field: balanceDueAt; after_field: departureAt; operator: \"<\"; };\n date order: { before_field: departureAt; after_field: confirmationDeadline; operator: \"<\"; };\n for i in len(cancel_bands) {\n let(band): at(cancel_bands, i);\n let(refund_mode): get(band, \"refund\");\n let(full_refund): if_eq(refund_mode, \"full\", true, false);\n let(balance_refund): if_eq(refund_mode, \"balance\", true, false);\n let(retains): if_eq(refund_mode, \"full\", false, true);\n let(later): if_eq(i, 1, false, true);\n let(band_start): concat(\"band\", i, \"Start\");\n when(later) {\n date order: { before_field: band_start; after_field: departureAt; operator: \"<\"; };\n let(after_second): if_eq(i, 2, false, true);\n when(after_second) { date order: { before_field: concat(\"band\", sub(i, 1), \"Start\"); after_field: band_start; operator: \"<\"; }; }\n }\n\n when(retains) {\n let(supplier_field): concat(\"band\", i, \"Supplier\");\n computes derived: { field: supplier_field; source_field: supplierCost; rule: { kind: percentage_of; bps: get(band, \"supplier_bps\"); }; rounding: floor; };\n let(portions): if_eq(refund_mode, \"balance\", [\"deposit\"], [\"deposit\", \"full\"]);\n for paid in portions {\n let(is_full): if_eq(paid, \"full\", true, false);\n let(portion): if_eq(refund_mode, \"balance\", \"deposit\", paid);\n let(margin_field): concat(\"band\", i, title(portion), \"Margin\");\n let(net_field): concat(\"band\", i, title(portion), \"Net\");\n let(vat_field): concat(\"band\", i, title(portion), \"Vat\");\n let(retained): if_eq(paid, \"deposit\", \"depositAmount\", if_eq(refund_mode, \"balance\", \"depositAmount\", \"price\"));\n computes derived: { field: vat_field; source_field: margin_field; rule: { kind: percentage_of; bps: tax_bps; }; rounding: floor; };\n partitions: { total_field: retained; piece_fields: [supplier_field, net_field, vat_field]; };\n partitions: { total_field: retained; piece_fields: [supplier_field, margin_field]; };\n partitions: { total_field: margin_field; piece_fields: [net_field, vat_field]; };\n }\n }\n }\n agent_description: \"Hold deposit and balance in one booking account. Apply a finite dated cancellation policy and release the supplier, operator and tax partition only after the named authority confirms. Refund the entire price when confirmation times out.\";\n lifecycle {\n states created awaiting_confirmation confirmed released refunded canceled expired;\n let(states_key): \"states\";\n [states_key]: concat_lists(deposit_states, full_states);\n initial created;\n on deposit: created -> deposit_1;\n on expire: created -> expired;\n for i in len(cancel_bands) {\n let(band): at(cancel_bands, i);\n let(refund_mode): get(band, \"refund\");\n let(full_refund): if_eq(refund_mode, \"full\", true, false);\n let(balance_refund): if_eq(refund_mode, \"balance\", true, false);\n let(retains): if_eq(refund_mode, \"full\", false, true);\n let(band_name): get(band, \"name\");\n let(deposit_state): concat(\"deposit_\", i);\n let(full_state): concat(\"full_\", i);\n let(balance_action): concat(\"balance_\", band_name);\n let(cancel_deposit): concat(\"cancel_deposit_\", band_name);\n let(cancel_full): concat(\"cancel_full_\", band_name);\n on [balance_action]: deposit_state -> full_state;\n on [cancel_deposit]: deposit_state -> canceled;\n on [cancel_full]: full_state -> canceled;\n let(later): if_eq(i, 1, false, true);\n when(later) {\n let(previous_deposit): names(\"deposit_\", sub(i, 1), \"\");\n let(previous_full): names(\"full_\", sub(i, 1), \"\");\n let(open_deposit): concat(\"open_deposit_\", band_name);\n let(open_full): concat(\"open_full_\", band_name);\n on [open_deposit]: previous_deposit -> deposit_state;\n on [open_full]: previous_full -> full_state;\n }\n }\n on cancel_unpaid: deposit_states -> canceled;\n on await_confirmation: full_states -> awaiting_confirmation;\n on confirm: awaiting_confirmation -> confirmed;\n on release: confirmed -> released;\n on refund_unconfirmed: full_states | awaiting_confirmation -> refunded;\n }\n caller_parked_states: { confirmed: \"Release pays the confirmed supplier and frozen partition.\"; };\n action create {\n public: none;\n agent_description: \"Freeze the booking terms and provision its holding account.\";\n unique: { namespace: instrument; by_fields: [bookingKey]; };\n deadline: { field: balanceDueAt; };\n steps: { operation: \"account.escrow.provision\"; bind: { currency: { from: instance; path: fields.currency; }; productId: { from: instance; path: productId; }; \"owner.id\": { from: instance; path: productId; }; \"owner.type\": { from: const; value: product; }; role: { from: const; value: product_escrow; }; }; capture: { heldAccountId: accountId; }; };\n }\n action deposit {\n public: none;\n agent_description: \"Collect the derived deposit after the acceptance requirements pass.\";\n when(acceptance) { requires aggregate: acceptance; }\n deadline: { field: balanceDueAt; }; port: { allowedParties: [payer]; };\n moves: { key: deposit; operation: \"internal_transfer.create\"; bind: { amount: { from: instance; path: concat(\"fields.\", \"depositAmount\"); }; currency: { from: instance; path: fields.currency; }; productId: { from: instance; path: productId; }; sourceAccountId: { from: instance; path: \"fields.customerAccountId\"; }; destinationAccountId: { from: instance; path: \"refs.heldAccountId\"; }; }; };\n steps: [];\n }\n action expire { public: none; agent_description: \"Close an unfunded booking.\"; due: { field: balanceDueAt; }; requires_drained: { path: refs.heldAccountId; }; steps: []; }\n for i in len(cancel_bands) {\n let(band): at(cancel_bands, i);\n let(refund_mode): get(band, \"refund\");\n let(full_refund): if_eq(refund_mode, \"full\", true, false);\n let(balance_refund): if_eq(refund_mode, \"balance\", true, false);\n let(retains): if_eq(refund_mode, \"full\", false, true);\n let(band_name): get(band, \"name\");\n let(balance_action): concat(\"balance_\", band_name);\n let(next_index): add(i, 1);\n let(next_field): concat(\"band\", next_index, \"Start\");\n let(band_end): if_eq(i, band_count, \"departureAt\", next_field);\n let(later): if_eq(i, 1, false, true);\n action [balance_action] {\n public: none; agent_description: \"Collect the remaining balance once before its deadline.\";\n deadline: { field: balanceDueAt; }; port: { allowedParties: [payer]; };\n moves: { key: balance; operation: \"internal_transfer.create\"; bind: { amount: { from: instance; path: concat(\"fields.\", \"balanceAmount\"); }; currency: { from: instance; path: fields.currency; }; productId: { from: instance; path: productId; }; sourceAccountId: { from: instance; path: \"fields.customerAccountId\"; }; destinationAccountId: { from: instance; path: \"refs.heldAccountId\"; }; }; };\n steps: [];\n }\n for paid in [\"deposit\", \"full\"] {\n let(is_full): if_eq(paid, \"full\", true, false);\n when(later) {\n let(open_action): concat(\"open_\", paid, \"_\", band_name);\n action [open_action] { public: none; agent_description: \"Open the next dated cancellation band.\"; due: { field: concat(\"band\", i, \"Start\"); }; steps: []; }\n }\n let(portion): if_eq(refund_mode, \"balance\", \"deposit\", paid);\n let(cancel_action): concat(\"cancel_\", paid, \"_\", band_name);\n action [cancel_action] {\n public: none; agent_description: \"Settle this paid amount under the active cancellation band.\";\n deadline: { field: band_end; }; port: { allowedParties: [payer]; };\n when(full_refund) { moves: { key: refund; operation: \"internal_transfer.create\"; bind: { amount: { from: instance; path: concat(\"fields.\", if_eq(paid, \"deposit\", \"depositAmount\", \"price\")); }; currency: { from: instance; path: fields.currency; }; productId: { from: instance; path: productId; }; sourceAccountId: { from: instance; path: \"refs.heldAccountId\"; }; destinationAccountId: { from: instance; path: \"fields.customerAccountId\"; }; }; }; }\n when(retains) {\n when(is_full) { when(balance_refund) { moves: { key: refund; operation: \"internal_transfer.create\"; bind: { amount: { from: instance; path: concat(\"fields.\", \"balanceAmount\"); }; currency: { from: instance; path: fields.currency; }; productId: { from: instance; path: productId; }; sourceAccountId: { from: instance; path: \"refs.heldAccountId\"; }; destinationAccountId: { from: instance; path: \"fields.customerAccountId\"; }; }; }; } }\n moves: { key: supplier; operation: \"internal_transfer.create\"; bind: { amount: { from: instance; path: concat(\"fields.\", concat(\"band\", i, \"Supplier\")); }; currency: { from: instance; path: fields.currency; }; productId: { from: instance; path: productId; }; sourceAccountId: { from: instance; path: \"refs.heldAccountId\"; }; destinationAccountId: { from: instance; path: \"fields.supplierAccountId\"; }; }; };\n moves: { key: operator; operation: \"internal_transfer.create\"; bind: { amount: { from: instance; path: concat(\"fields.\", concat(\"band\", i, title(portion), \"Net\")); }; currency: { from: instance; path: fields.currency; }; productId: { from: instance; path: productId; }; sourceAccountId: { from: instance; path: \"refs.heldAccountId\"; }; destinationAccountId: { from: instance; path: \"fields.operatorAccountId\"; }; }; };\n moves: { key: vat; operation: \"internal_transfer.create\"; bind: { amount: { from: instance; path: concat(\"fields.\", concat(\"band\", i, title(portion), \"Vat\")); }; currency: { from: instance; path: fields.currency; }; productId: { from: instance; path: productId; }; sourceAccountId: { from: instance; path: \"refs.heldAccountId\"; }; destinationAccountId: { from: instance; path: \"fields.taxAccountId\"; }; }; };\n }\n steps: [];\n }\n }\n }\n action cancel_unpaid {\n public: none; agent_description: \"Settle the deposit under the declared unpaid-balance band and end the remaining obligation.\";\n due: { field: balanceDueAt; };\n when_not(unpaid_band) { moves: { key: refund; operation: \"internal_transfer.create\"; bind: { amount: { from: instance; path: \"fields.depositAmount\"; }; currency: { from: instance; path: fields.currency; }; productId: { from: instance; path: productId; }; sourceAccountId: { from: instance; path: \"refs.heldAccountId\"; }; destinationAccountId: { from: instance; path: \"fields.customerAccountId\"; }; }; }; }\n when(unpaid_band) {\n let(unpaid_policy): at(cancel_bands, unpaid_band);\n let(unpaid_full_refund): if_eq(get(unpaid_policy, \"refund\"), \"full\", true, false);\n when(unpaid_full_refund) { moves: { key: refund; operation: \"internal_transfer.create\"; bind: { amount: { from: instance; path: \"fields.depositAmount\"; }; currency: { from: instance; path: fields.currency; }; productId: { from: instance; path: productId; }; sourceAccountId: { from: instance; path: \"refs.heldAccountId\"; }; destinationAccountId: { from: instance; path: \"fields.customerAccountId\"; }; }; }; }\n when_not(unpaid_full_refund) {\n moves: { key: supplier; operation: \"internal_transfer.create\"; bind: { amount: { from: instance; path: concat(\"fields.\", concat(\"band\", unpaid_band, \"Supplier\")); }; currency: { from: instance; path: fields.currency; }; productId: { from: instance; path: productId; }; sourceAccountId: { from: instance; path: \"refs.heldAccountId\"; }; destinationAccountId: { from: instance; path: \"fields.supplierAccountId\"; }; }; };\n moves: { key: operator; operation: \"internal_transfer.create\"; bind: { amount: { from: instance; path: concat(\"fields.\", concat(\"band\", unpaid_band, \"DepositNet\")); }; currency: { from: instance; path: fields.currency; }; productId: { from: instance; path: productId; }; sourceAccountId: { from: instance; path: \"refs.heldAccountId\"; }; destinationAccountId: { from: instance; path: \"fields.operatorAccountId\"; }; }; };\n moves: { key: vat; operation: \"internal_transfer.create\"; bind: { amount: { from: instance; path: concat(\"fields.\", concat(\"band\", unpaid_band, \"DepositVat\")); }; currency: { from: instance; path: fields.currency; }; productId: { from: instance; path: productId; }; sourceAccountId: { from: instance; path: \"refs.heldAccountId\"; }; destinationAccountId: { from: instance; path: \"fields.taxAccountId\"; }; }; };\n }\n }\n steps: [];\n }\n action await_confirmation { public: none; agent_description: \"Open confirmation at the booked departure.\"; due: { field: departureAt; }; steps: []; }\n action confirm {\n public: none; agent_description: \"Consume the named supplier's approved decision for this booking before timeout.\";\n deadline: { field: confirmationDeadline; };\n input: { type: object; additionalProperties: false; properties: { authorityId: { type: string; pattern: concat(\"^\", prefix(authority_type), \"_(sandbox|live)_[a-z0-9]{8,64}$\"); }; }; required: [authorityId]; };\n updates: [authorityId];\n requires refs: { field: authorityId; statuses: [approved]; match: { instrumentInstanceId: fields.requestInstanceId; \"fields.supplierAccountId\": fields.decidingActorAccountId; \"fields.supplierRole\": fields.decidingRole; }; attests: { instrument: fields.requestInstrumentId; party: decider; action: fields.requestAction; digest: fields.inputDigest; role: fields.decidingRole; expires_at: fields.expiresAt; consume: consume; }; };\n steps: [];\n }\n action release {\n public: none; agent_description: \"Pay supplier cost, operator net and VAT after confirmation.\";\n moves: { key: supplier; operation: \"internal_transfer.create\"; bind: { amount: { from: instance; path: concat(\"fields.\", \"supplierCost\"); }; currency: { from: instance; path: fields.currency; }; productId: { from: instance; path: productId; }; sourceAccountId: { from: instance; path: \"refs.heldAccountId\"; }; destinationAccountId: { from: instance; path: \"fields.supplierAccountId\"; }; }; };\n moves: { key: operator; operation: \"internal_transfer.create\"; bind: { amount: { from: instance; path: concat(\"fields.\", \"operatorNet\"); }; currency: { from: instance; path: fields.currency; }; productId: { from: instance; path: productId; }; sourceAccountId: { from: instance; path: \"refs.heldAccountId\"; }; destinationAccountId: { from: instance; path: \"fields.operatorAccountId\"; }; }; };\n moves: { key: vat; operation: \"internal_transfer.create\"; bind: { amount: { from: instance; path: concat(\"fields.\", \"marginVat\"); }; currency: { from: instance; path: fields.currency; }; productId: { from: instance; path: productId; }; sourceAccountId: { from: instance; path: \"refs.heldAccountId\"; }; destinationAccountId: { from: instance; path: \"fields.taxAccountId\"; }; }; };\n steps: [];\n }\n action refund_unconfirmed {\n public: none; agent_description: \"Return the entire booking price when supplier confirmation times out.\";\n due: { field: confirmationDeadline; };\n moves: { key: refund; operation: \"internal_transfer.create\"; bind: { amount: { from: instance; path: concat(\"fields.\", \"price\"); }; currency: { from: instance; path: fields.currency; }; productId: { from: instance; path: productId; }; sourceAccountId: { from: instance; path: \"refs.heldAccountId\"; }; destinationAccountId: { from: instance; path: \"fields.customerAccountId\"; }; }; };\n steps: [];\n }\n }\n}\n"],
5
+ ["money_flows/advance.hsx", "module std.money_flows.advance\n\n// Upfront capital disbursement repaid by carving future held payment releases or over scheduled installments.\n//\n// ### Purpose\n// `advance` provides working capital, merchant cash advances, or contractor pre-funding from a funder to a recipient.\n// Repayment occurs either automatically by intercepting (carving) releases from a linked escrow hold (`against`),\n// or over calendar-anchored installment repayments (`count`, `every`, `first_due`).\n//\n// ### Selection guidance\n// - vs `credit_facility`: `advance` disburses a single upfront lump-sum principal that is repaid over time.\n// `credit_facility` establishes a reusable revolving credit line with multiple draws up to a limit,\n// where repayments restore available borrowing capacity.\n// - vs `conditional_disbursement`: `advance` expects repayment of the advanced principal plus optional fees.\n// `conditional_disbursement` disburses non-repayable grants, claims, or milestone payments against external evidence.\n// - vs `held_payment`: `held_payment` holds customer funds in escrow until delivery. An `advance` can carve repayments\n// directly out of a `held_payment`'s release using `against`.\n//\n// ### Parameters\n// - `funder`: The party providing the upfront capital.\n// - `to`: The party receiving the advance and responsible for repayment.\n// - `amount`: Total advanced principal in minor units of currency `C`.\n// - `fee`: Optional markup percentage fee charged on the advance (basis points precision).\n// - `count`: Optional number of scheduled installment repayments.\n// - `every`: Optional recurrence duration between installments (e.g. `\"P30D\"`).\n// - `first_due`: Optional date for the first installment repayment.\n// - `repayment_source`: Optional party repaying when distinct from the capital recipient.\n// - `profit_to`: Optional recipient of profit, separated from principal repayments.\n// - `dated`: Require an explicit signed date for each repayment instead of duration offsets.\n// - `against`: Optional reference to a hold instrument whose release will be carved to repay the advance.\n//\n// ### Decision ports\n// None. Repayment is driven by linked hold releases or scheduled calendar dates.\n//\n// ### Example\n// ```hsx\n// program advance_example \"Advance example\"\n// import { advance } from \"std/money_flows\"\n// party funder: business\n// party recipient: business\n// settlement advance_payment = advance {\n// funder: funder\n// to: recipient\n// amount: principal: money(SAR)\n// fee: 2.5%\n// count: 2\n// every: P30D\n// first_due: firstDueAt\n// }\n// ```\nexport instrument advance<C>(\n funder: party,\n to: party,\n amount: money<C>,\n fee: optional<percent>,\n count: optional<integer>,\n every: optional<text>,\n first_due: optional<date>,\n against: optional<ref>,\n repayment_source: optional<party>,\n profit_to: optional<party>,\n dated: optional<boolean>,\n) {\n let(amount_path): concat(\"fields.\", amount);\n let(currency_path): \"fields.currency\";\n let(funder_field): camel(concat(funder, \"_account_id\"));\n let(to_field): camel(concat(to, \"_account_id\"));\n\n agent_description: \"Reach for advance when the {funder} pays the {to} up front and is repaid afterwards, either out of a carved hold release or over scheduled repayments. Pick deposit for a refundable reservation and instant transfer when nobody repays the payer.\";\n title: sentence(instrument);\n id_prefix: prefix(instrument);\n parties {\n payer: funder;\n beneficiary: to;\n when(repayment_source) { repayment: repayment_source; }\n when(profit_to) { profit: profit_to; }\n }\n\n when(against) {\n let(hold_name): owner(against);\n summary: \"Advance to the {to} repaid by carving the {hold_name} release\";\n description: \"Advance: the {funder} disburses {amount} to the {to} and is repaid out of the {hold_name} release, which pays the {funder} in the {to}'s place\";\n fields {\n amount {\n type: money<C>;\n description: \"The advanced amount in {C} minor units, disbursed to the {to} up front\";\n }\n carve_hold_id {\n type: ref<hold_name>;\n description: \"The {hold_name} bound to this advance\";\n pattern: concat(\"^\", prefix(hold_name), \"_(sandbox|live)_[a-z0-9]{8,64}$\");\n }\n carve_recourse_1_id {\n type: ref<recourse>;\n description: \"The recourse bound to this advance\";\n pattern: \"^reco_(sandbox|live)_[a-z0-9]{8,64}$\";\n }\n }\n required: [to_field, funder_field, amount, carveHoldId, carveRecourse1Id, currency];\n lifecycle {\n states created advanced repaid;\n initial created;\n on disburse: created -> advanced;\n on settle: advanced -> repaid;\n }\n action create {\n agent_description: \"Open the advance row before any money moves. The caller supplies the {amount}, both party accounts, and the ids of the {hold_name} and the recourse that back repayment. Nothing is disbursed here.\";\n summary: concat(\"Create a \", words(instrument));\n steps: [];\n moves: [];\n }\n action disburse {\n agent_description: \"Pay the {amount} from the {funder} to the {to}. The advance must be created, the bound {hold_name} funded, and the recourse active, both matching this amount and currency. The transfer posts straight out and does not unwind.\";\n summary: \"Disburse the {amount} to the {to}\";\n requires refs: [{\n field: carveHoldId;\n match: {\n [currency_path]: currency_path;\n [amount_path]: amount_path;\n };\n statuses: [funded];\n }, {\n field: carveRecourse1Id;\n match: {\n [currency_path]: currency_path;\n [amount_path]: amount_path;\n };\n statuses: [active];\n }];\n moves: [{ amount: amount; from: payer; to: beneficiary; key: transfer; operation: create; }];\n steps: [];\n }\n action settle {\n agent_description: \"Close the advance once the {hold_name} has released to the {funder} in the {to}'s place. The advance must already be disbursed. No money moves here, the release did the repaying.\";\n summary: concat(\"Close the advance once the \", hold_name, \" has released to the \", funder);\n steps: [];\n moves: [];\n }\n }\n\n when_not(against) {\n let(repayment_role): if_eq(repayment_source, false, \"beneficiary\", \"repayment\");\n when(fee) { computes derived: { field: feeAmount; source_field: amount; rule: { kind: percentage_of; bps: basis_points(fee); }; rounding: floor; }; }\n let(repayment_fields): names(\"repayment\", count, \"Amount\");\n let(first_target): if_eq(count, 1, \"repaid\", \"repaying_1\");\n let(states_key): \"states\";\n let(first_due_field): camel(first_due);\n summary: concat(\"Advance to \", to, \" repaid over \", count, \" anchors\");\n description: concat(\"Advance: the \", funder, \" disburses \", amount, \" to the \", to, \", repaid over \", count, \" scheduled repayments plus the funder's discount\");\n fields {\n amount {\n type: money<C>;\n description: \"The advanced amount in {C} minor units, disbursed to the {to} up front\";\n }\n fee_amount {\n type: money<C>;\n when(fee) { optional: true; allow_zero: true; }\n description: concat(fee, \" of \", amount, \", the funder's discount repaid on top of the advance\");\n }\n repayable_amount {\n type: money<C>;\n description: concat(amount, \" + feeAmount: the total the repayment fields below partition exactly\");\n }\n [first_due_field] {\n type: date;\n description: concat(\"Due date of the first repayment; repayment k falls \", every, \" after its predecessor\");\n }\n for i in count {\n repayment[i]Amount { type: money<C>; description: \"Stored total for repayment {i}\"; }\n when(profit_to) {\n repayment[i]Principal { type: money<C>; description: \"Principal returned to the funder by repayment {i}\"; }\n repayment[i]Profit { type: money<C>; description: \"Profit paid to the named recipient by repayment {i}\"; }\n }\n when(dated) { repayment[i]DueAt { type: date; description: \"Signed calendar due date for repayment {i}\"; } }\n }\n }\n lifecycle {\n states created advanced;\n [states_key]: [first_target];\n initial created;\n on disburse: created -> advanced;\n on collect_repayment_1: advanced -> [first_target];\n for repayment in sub(count, 1) {\n let(index): add(repayment, 1);\n let(previous): concat(\"repaying_\", repayment);\n let(target): if_eq(index, count, \"repaid\", concat(\"repaying_\", index));\n [states_key]: [target];\n on collect_repayment_[index]: [previous] -> [target];\n }\n }\n partitions: [{\n piece_fields: repayment_fields;\n total_field: repayableAmount;\n }, {\n piece_fields: [amount, feeAmount];\n total_field: repayableAmount;\n }];\n when(profit_to) {\n partitions: { total_field: amount; piece_fields: names(\"repayment\", count, \"Principal\"); };\n partitions: { total_field: feeAmount; piece_fields: names(\"repayment\", count, \"Profit\"); };\n for i in count {\n partitions: { total_field: concat(\"repayment\", i, \"Amount\"); piece_fields: [concat(\"repayment\", i, \"Principal\"), concat(\"repayment\", i, \"Profit\")]; };\n }\n }\n action create {\n agent_description: \"Open the advance row before any money moves. The caller supplies the {amount}, the repayable total, and repayment dates. A declared fee derives the fee amount from principal. The repayment fields must partition the repayable total exactly. Nothing is disbursed here.\";\n summary: concat(\"Create a \", words(instrument));\n steps: [];\n moves: [];\n }\n action disburse {\n agent_description: \"Pay the {amount} from the {funder} to the {to} and start the repayment schedule. The advance must be created. The transfer posts straight out and does not unwind. Each repayment is collected on its own stored date anchor.\";\n summary: \"Disburse the {amount} to the {to}\";\n moves: [{ amount: amount; from: payer; to: beneficiary; key: transfer; operation: create; }];\n steps: [];\n }\n for i in count {\n let(repayment_field): concat(\"repayment\", i, \"Amount\");\n action collect_repayment_[i] {\n agent_description: \"Collect repayment {i} once on its stored due anchor, returning principal to the funder and any separately routed profit to its recipient.\";\n summary: \"Collect repayment {i} of {count}\";\n when(dated) { due: { field: concat(\"repayment\", i, \"DueAt\"); }; }\n when_not(dated) { due: { field: first_due_field; offset: scale_duration(every, sub(i, 1)); }; }\n when_not(profit_to) { moves: [{ amount: repayment_field; from: repayment_role; to: payer; key: transfer; }]; }\n when(profit_to) {\n moves: [\n { amount: concat(\"repayment\", i, \"Principal\"); from: repayment_role; to: payer; key: principal; },\n { amount: concat(\"repayment\", i, \"Profit\"); from: repayment_role; to: profit; key: profit; }\n ];\n }\n steps: [];\n }\n }\n }\n}\n"],
6
+ ["money_flows/cancellable_booking.hsx", "module std.money_flows.cancellable_booking\n\n// Time-anchored reservation holding booking funds in escrow with dynamically quoted cancellation penalties.\n//\n// ### Purpose\n// `cancellable_booking` holds booking fees in escrow from a guest for a host until a scheduled start date.\n// It fits hospitality, travel reservations, studio rentals, event tickets, and appointments where guests\n// can cancel prior to start, but cancellation penalties depend on how close to the start date cancellation occurs.\n//\n// ### Selection guidance\n// - vs `held_payment`: Both hold money in escrow and both quote a cancellation before it is spent.\n// `cancellable_booking` prices the penalty against the time left before `starts_at`, so the charge moves\n// as the start approaches; `held_payment` has no start date and quotes one flat charge.\n// Choose `cancellable_booking` whenever cancellation fees are time-sensitive.\n// - vs `security_deposit`: `security_deposit` holds funds to cover damages claimed by the holder.\n// `cancellable_booking` holds the service fee itself and releases to the host upon `starts_at` or refunds net of penalty.\n//\n// ### Parameters\n// - `guest`: The booking customer paying the fee and receiving refunds.\n// - `host`: The service provider receiving the payout or retained cancellation penalty.\n// - `amount`: Total booking price in minor units of currency `C`.\n// - `starts_at`: Stored ISO 8601 date when the booking begins.\n// - `late_penalty_bps`: Penalty in basis points applied when cancelling within the late window.\n// - `late_within`: ISO 8601 duration defining the late window before `starts_at` (e.g. `\"P2D\"` for 2 days).\n// - `early_penalty_bps`: Penalty in basis points applied when cancelling before the late window.\n// - `offer_life`: ISO 8601 duration defining how long a quoted cancellation offer remains valid (e.g. `\"PT30M\"`).\n//\n// ### Decision ports\n// None. Action transitions are driven by guest API calls (`take`, `cancel`, `confirm`) and scheduled completion (`complete` due at `starts_at`).\n//\n// ### Example\n// ```hsx\n// program studio_booking \"Studio booking\"\n// import { cancellable_booking } from \"std/money_flows\"\n// party guest: person\n// party studio: business\n// settlement studio_session = cancellable_booking {\n// guest: guest\n// host: studio\n// amount: sessionPrice: money(SAR)\n// starts_at: startsAt\n// late_penalty_bps: 5000\n// late_within: \"P2D\"\n// early_penalty_bps: 1000\n// offer_life: \"PT30M\"\n// }\n// ```\n// ### Finite cancellation bands\n// Supply cancel_bands to select deposit/balance custody instead of quoted cancellation.\n// Each band has name, refund (full, balance or none), starts_at after the first band,\n// and supplier_bps when money is retained. The unpaid_band index selects a retained\n// deposit policy. Omit it to return the deposit; a full-refund band also returns it. price_value and supplier_cost fix the price; deposit_bps derives\n// the deposit. balance_before and confirmation_window are positive fixed durations\n// relative to departure. tax_bps applies to margin after the supplier share.\n// authority_type and supplier_role bind release to a consumed confirmation decision.\n// collects optionally cascades named actions to linked instruments on deposit.\n// acceptance optionally supplies an aggregate gate. All actions are private until\n// expose selects them. Ordered band starts and deadlines prevent a late sweep from\n// extending an earlier band's cancellation rights. Full timeout refund returns price.\n// Seven derivations suffice for three bands; larger finite policies use up to the\n// UDL bound of 64 derived amounts. shift_date(date, duration, \"before\") subtracts a\n// fixed duration; omitting the direction adds it. Invalid dates or durations refuse.\n//\nexport instrument cancellable_booking<C>(\n guest: optional<party>,\n host: optional<party>,\n amount: optional<money<C>>,\n starts_at: optional<date>,\n late_penalty_bps: optional<integer>,\n late_within: optional<text>,\n early_penalty_bps: optional<integer>,\n offer_life: optional<text>,\n cancel_bands: optional<list<block>>,\n price_value: optional<text>, supplier_cost: optional<text>, departure: optional<text>,\n balance_before: optional<text>, confirmation_window: optional<text>, unpaid_band: optional<integer>,\n acceptance: optional<block>, deposit_bps: optional<integer>, tax_bps: optional<integer>,\n authority_type: optional<text>, supplier_role: optional<text>,\n collects: optional<json>,\n) {\n when_not(cancel_bands) {\n let(guest_account_field): camel(concat(guest, \"_account_id\"));\n let(host_account_field): camel(concat(host, \"_account_id\"));\n let(reference_filter_key): \"x-hyperscale-reference-filter\";\n let(owner_id_key): \"owner.id\";\n let(owner_type_key): \"owner.type\";\n fields {\n amount { type: money<C>; description: concat(\"Booking price in minor units, held away from both the \", words(guest), \" and the \", words(host), \" until the booking ends\"); }\n currency { type: text; description: \"ISO 4217 currency code\"; minLength: 3; maxLength: 3; pattern: \"^[A-Z]{3}$\"; }\n starts_at { type: date; description: \"When the booking starts; the cancellation penalty is priced against the time left before it\"; }\n [guest_account_field] { type: account<C>; description: concat(\"The \", words(guest), \" account the booking is taken from and the refund returns to\"); [reference_filter_key]: { column: role; values: [customer_balance]; }; }\n [host_account_field] { type: account<C>; description: concat(\"The \", words(host), \" account the booking and any penalty end up in\"); [reference_filter_key]: { column: role; values: [customer_balance]; }; }\n }\n lifecycle {\n states created held cancellation_quoted canceled settled completed;\n initial created;\n on take: created -> held;\n on complete: held | cancellation_quoted -> completed;\n on cancel: held -> cancellation_quoted;\n on confirm: cancellation_quoted -> canceled;\n on retain: canceled -> settled;\n }\n parties { beneficiary: host_account_field; payer: guest_account_field; }\n required: [amount, currency, guest_account_field, host_account_field, starts_at];\n title: sentence(instrument);\n agent_description: concat(\"Reach for cancellable booking when the \", words(guest), \" pays up front and can back out at a price they see before they agree to it. The money sits in a holding account, the penalty is priced against the time left before the start, and the quote is frozen until it expires. Pick held payment when there is no cancellation price to quote.\");\n id_prefix: prefix(instrument);\n summary: concat(\"Booking the \", words(guest), \" can cancel at a price they see before they agree to it\");\n description: \"A booking whose cancellation is quoted rather than argued: the penalty is priced against the time left before the start, frozen with the terms it was read from, and paid only if the guest confirms while the offer is still alive\";\n action create {\n agent_description: concat(\"Open the booking and provision the holding account the price sits in. No money moves. The price, the currency, the start moment, and both party accounts are fixed here. Call take next to collect from the \", words(guest), \".\");\n summary: concat(\"Open a \", words(instrument), \" and its holding account\");\n moves: [];\n steps: [{ operation: \"account.escrow.provision\"; bind: { currency: { from: instance; path: fields.currency; }; [owner_id_key]: { from: instance; path: productId; }; [owner_type_key]: { from: const; value: product; }; productId: { from: instance; path: productId; }; role: { from: const; value: product_escrow; }; }; capture: { bookingHoldingAccountId: accountId; }; }];\n }\n action take {\n agent_description: concat(\"Move the whole booking price out of the \", words(guest), \" account into the holding account. This moves money. Only a created booking takes. Neither party can spend the money until the booking completes or a cancellation is confirmed.\");\n summary: concat(\"Take the booking price from the \", words(guest), \" into the holding account\");\n moves: [{ key: booking; operation: internal_transfer.create; bind: { amount: { from: instance; path: concat(\"fields.\", amount); }; currency: { from: instance; path: fields.currency; }; destinationAccountId: { from: instance; path: refs.bookingHoldingAccountId; }; productId: { from: instance; path: productId; }; sourceAccountId: { from: instance; path: concat(\"fields.\", guest_account_field); }; }; capture: { bookingTransferId: transferId; }; }];\n sandbox_failure_point: funding;\n steps: [];\n }\n action complete {\n summary: concat(\"Release the whole booking to the \", words(host), \" once the stay has started, even with a cancellation quote parked un-acted\");\n due { field: starts_at; }\n moves: [{ key: release; operation: internal_transfer.create; bind: { amount: { from: instance; path: concat(\"fields.\", amount); }; currency: { from: instance; path: fields.currency; }; destinationAccountId: { from: instance; path: concat(\"fields.\", host_account_field); }; productId: { from: instance; path: productId; }; sourceAccountId: { from: instance; path: refs.bookingHoldingAccountId; }; }; capture: { releaseTransferId: transferId; }; }];\n steps: [];\n }\n action cancel {\n agent_description: \"Price the cancellation and freeze that price. No money moves. The penalty is computed from the time left before the start, and the quote expires on its own, after which a new cancel prices it again. Call confirm to spend the quote.\";\n summary: \"Price the cancellation and hold that price open\";\n quote {\n anchorField: starts_at;\n baseField: amount;\n chargeRef: cancellationPenaltyAmount;\n charges: [{ bps: late_penalty_bps; withinOffset: late_within; }, { bps: early_penalty_bps; }];\n expires { offset: offer_life; }\n fixes: [amount, guest_account_field];\n netDestinationField: guest_account_field;\n netRef: cancellationRefundAmount;\n }\n moves: []; steps: [];\n }\n action confirm {\n agent_description: concat(\"Spend the frozen quote and return the net refund to the \", words(guest), \". This moves money and does not reverse. It requires an un-expired quote from cancel, and the penalty the refund left behind is paid to the \", words(host), \" by retain.\");\n summary: concat(\"Spend the quoted cancellation and return the net to the \", words(guest));\n commit: cancel;\n moves: [{ key: refund; operation: internal_transfer.create; bind: { amount: { from: instance; path: refs.cancellationRefundAmount; }; currency: { from: instance; path: fields.currency; }; destinationAccountId: { from: instance; path: concat(\"fields.\", guest_account_field); }; productId: { from: instance; path: productId; }; sourceAccountId: { from: instance; path: refs.bookingHoldingAccountId; }; }; capture: { cancellationTransferId: transferId; }; }];\n steps: [];\n }\n action retain {\n agent_description: concat(\"Pay the quoted cancellation penalty out of the holding account to the \", words(host), \". This moves money and does not reverse. The booking must already be canceled, so the penalty figure is the one the quote fixed.\");\n summary: concat(\"Pay the quoted penalty that the refund left behind to the \", words(host));\n moves: [{ key: penalty; operation: internal_transfer.create; bind: { amount: { from: instance; path: refs.cancellationPenaltyAmount; }; currency: { from: instance; path: fields.currency; }; destinationAccountId: { from: instance; path: concat(\"fields.\", host_account_field); }; productId: { from: instance; path: productId; }; sourceAccountId: { from: instance; path: refs.bookingHoldingAccountId; }; }; capture: { penaltyTransferId: transferId; }; }];\n steps: [];\n }\n }\n when(cancel_bands) {\n let(band_count): len(cancel_bands);\n for band in cancel_bands {\n let(refund): get(band, \"refund\");\n let(not_full): if_eq(refund, \"full\", false, true);\n let(not_balance): if_eq(refund, \"balance\", false, true);\n let(not_none): if_eq(refund, \"none\", false, true);\n when(not_full) { when(not_balance) { when(not_none) {\n unsupported { code: HSX1407; message: \"Unknown cancellation refund policy\"; fix: \"Choose full, balance or none\"; }\n } } }\n }\n let(deposit_states): names(\"deposit_\", len(cancel_bands), \"\");\n let(full_states): names(\"full_\", len(cancel_bands), \"\");\n fields {\n bookingKey { type: text; min_length: 1; }\n signedTermsReference { type: text; min_length: 1; }\n currency { type: text; const: C; }\n customerAccountId { type: account<C>; \"x-hyperscale-reference-filter\": { column: role; values: [customer_balance]; }; }\n supplierAccountId { type: account<C>; \"x-hyperscale-reference-filter\": { column: role; values: [customer_balance]; }; }\n operatorAccountId { type: account<C>; \"x-hyperscale-reference-filter\": { column: role; values: [customer_balance]; }; }\n taxAccountId { type: account<C>; \"x-hyperscale-reference-filter\": { column: role; values: [customer_balance]; }; }\n authorityId { type: ref<authority_type>; optional: true; }\n supplierRole { type: text; const: supplier_role; }\n travellerCount { type: integer; minimum: 1; maximum: 20; }\n departureAt { type: date; const: departure; }\n balanceDueAt { type: date; const: shift_date(departure, balance_before, \"before\"); }\n confirmationDeadline { type: date; const: shift_date(departure, confirmation_window); }\n price { type: money<C>; const: price_value; }\n supplierCost { type: money<C>; const: supplier_cost; }\n depositAmount { type: money<C>; optional: true; allow_zero: true; }\n balanceAmount: money<C>;\n margin: money<C>;\n operatorNet: money<C>;\n marginVat { type: money<C>; optional: true; allow_zero: true; }\n for i in len(cancel_bands) {\n let(band): at(cancel_bands, i);\n let(refund_mode): get(band, \"refund\");\n let(full_refund): if_eq(refund_mode, \"full\", true, false);\n let(balance_refund): if_eq(refund_mode, \"balance\", true, false);\n let(retains): if_eq(refund_mode, \"full\", false, true);\n let(later): if_eq(i, 1, false, true);\n when(later) { band[i]Start { type: date; const: get(band, \"starts_at\"); } }\n\n when(retains) {\n band[i]Supplier { type: money<C>; optional: true; allow_zero: true; }\n let(portions): if_eq(refund_mode, \"balance\", [\"deposit\"], [\"deposit\", \"full\"]);\n for paid in portions {\n let(is_full): if_eq(paid, \"full\", true, false);\n let(portion): if_eq(refund_mode, \"balance\", \"deposit\", paid);\n let(margin_field): concat(\"band\", i, title(portion), \"Margin\");\n let(net_field): concat(\"band\", i, title(portion), \"Net\");\n let(vat_field): concat(\"band\", i, title(portion), \"Vat\");\n [margin_field]: money<C>;\n [net_field]: money<C>;\n [vat_field] { type: money<C>; optional: true; allow_zero: true; }\n }\n }\n }\n }\n parties { payer: customerAccountId; beneficiary: supplierAccountId; fee: operatorAccountId; tax: taxAccountId; }\n distinct_parties: true;\n computes derived: { field: depositAmount; source_field: price; rule: { kind: percentage_of; bps: deposit_bps; }; rounding: floor; };\n computes derived: { field: marginVat; source_field: margin; rule: { kind: percentage_of; bps: tax_bps; }; rounding: floor; };\n for i in len(cancel_bands) {\n let(band): at(cancel_bands, i);\n let(refund_mode): get(band, \"refund\");\n let(balance_refund): if_eq(refund_mode, \"balance\", true, false);\n when(balance_refund) {\n partitions: { total_field: price; piece_fields: [balanceAmount, concat(\"band\", i, \"Supplier\"), concat(\"band\", i, \"DepositNet\"), concat(\"band\", i, \"DepositVat\")]; };\n }\n }\n partitions: { total_field: price; piece_fields: [supplierCost, operatorNet, marginVat]; };\n partitions: { total_field: price; piece_fields: [depositAmount, balanceAmount]; };\n partitions: { total_field: price; piece_fields: [supplierCost, margin]; };\n partitions: { total_field: margin; piece_fields: [operatorNet, marginVat]; };\n date order: { before_field: balanceDueAt; after_field: departureAt; operator: \"<\"; };\n date order: { before_field: departureAt; after_field: confirmationDeadline; operator: \"<\"; };\n for i in len(cancel_bands) {\n let(band): at(cancel_bands, i);\n let(refund_mode): get(band, \"refund\");\n let(full_refund): if_eq(refund_mode, \"full\", true, false);\n let(balance_refund): if_eq(refund_mode, \"balance\", true, false);\n let(retains): if_eq(refund_mode, \"full\", false, true);\n let(later): if_eq(i, 1, false, true);\n let(band_start): concat(\"band\", i, \"Start\");\n when(later) {\n date order: { before_field: band_start; after_field: departureAt; operator: \"<\"; };\n let(after_second): if_eq(i, 2, false, true);\n when(after_second) { date order: { before_field: concat(\"band\", sub(i, 1), \"Start\"); after_field: band_start; operator: \"<\"; }; }\n }\n\n when(retains) {\n let(supplier_field): concat(\"band\", i, \"Supplier\");\n computes derived: { field: supplier_field; source_field: supplierCost; rule: { kind: percentage_of; bps: get(band, \"supplier_bps\"); }; rounding: floor; };\n let(portions): if_eq(refund_mode, \"balance\", [\"deposit\"], [\"deposit\", \"full\"]);\n for paid in portions {\n let(is_full): if_eq(paid, \"full\", true, false);\n let(portion): if_eq(refund_mode, \"balance\", \"deposit\", paid);\n let(margin_field): concat(\"band\", i, title(portion), \"Margin\");\n let(net_field): concat(\"band\", i, title(portion), \"Net\");\n let(vat_field): concat(\"band\", i, title(portion), \"Vat\");\n let(retained): if_eq(paid, \"deposit\", \"depositAmount\", if_eq(refund_mode, \"balance\", \"depositAmount\", \"price\"));\n computes derived: { field: vat_field; source_field: margin_field; rule: { kind: percentage_of; bps: tax_bps; }; rounding: floor; };\n partitions: { total_field: retained; piece_fields: [supplier_field, net_field, vat_field]; };\n partitions: { total_field: retained; piece_fields: [supplier_field, margin_field]; };\n partitions: { total_field: margin_field; piece_fields: [net_field, vat_field]; };\n }\n }\n }\n agent_description: \"Hold deposit and balance in one booking account. Apply a finite dated cancellation policy and release the supplier, operator and tax partition only after the named authority confirms. Refund the entire price when confirmation times out.\";\n lifecycle {\n states created awaiting_confirmation confirmed released refunded canceled expired;\n let(states_key): \"states\";\n [states_key]: concat_lists(deposit_states, full_states);\n initial created;\n on deposit: created -> deposit_1;\n on expire: created -> expired;\n for i in len(cancel_bands) {\n let(band): at(cancel_bands, i);\n let(refund_mode): get(band, \"refund\");\n let(full_refund): if_eq(refund_mode, \"full\", true, false);\n let(balance_refund): if_eq(refund_mode, \"balance\", true, false);\n let(retains): if_eq(refund_mode, \"full\", false, true);\n let(band_name): get(band, \"name\");\n let(deposit_state): concat(\"deposit_\", i);\n let(full_state): concat(\"full_\", i);\n let(balance_action): concat(\"balance_\", band_name);\n let(cancel_deposit): concat(\"cancel_deposit_\", band_name);\n let(cancel_full): concat(\"cancel_full_\", band_name);\n on [balance_action]: deposit_state -> full_state;\n on [cancel_deposit]: deposit_state -> canceled;\n on [cancel_full]: full_state -> canceled;\n let(later): if_eq(i, 1, false, true);\n when(later) {\n let(previous_deposit): names(\"deposit_\", sub(i, 1), \"\");\n let(previous_full): names(\"full_\", sub(i, 1), \"\");\n let(open_deposit): concat(\"open_deposit_\", band_name);\n let(open_full): concat(\"open_full_\", band_name);\n on [open_deposit]: previous_deposit -> deposit_state;\n on [open_full]: previous_full -> full_state;\n }\n }\n on cancel_unpaid: deposit_states -> canceled;\n on await_confirmation: full_states -> awaiting_confirmation;\n on confirm: awaiting_confirmation -> confirmed;\n on release: confirmed -> released;\n on refund_unconfirmed: full_states | awaiting_confirmation -> refunded;\n }\n caller_parked_states: { confirmed: \"Release pays the confirmed supplier and frozen partition.\"; };\n action create {\n public: none;\n agent_description: \"Freeze the booking terms and provision its holding account.\";\n unique: { namespace: instrument; by_fields: [bookingKey]; };\n deadline: { field: balanceDueAt; };\n steps: { operation: \"account.escrow.provision\"; bind: { currency: { from: instance; path: fields.currency; }; productId: { from: instance; path: productId; }; \"owner.id\": { from: instance; path: productId; }; \"owner.type\": { from: const; value: product; }; role: { from: const; value: product_escrow; }; }; capture: { heldAccountId: accountId; }; };\n }\n action deposit {\n public: none;\n agent_description: \"Collect the derived deposit after the acceptance requirements pass.\";\n when(collects) { cascade: collects; }\n when(acceptance) { requires aggregate: acceptance; }\n deadline: { field: balanceDueAt; }; port: { allowedParties: [payer]; };\n moves: { key: deposit; operation: \"internal_transfer.create\"; bind: { amount: { from: instance; path: concat(\"fields.\", \"depositAmount\"); }; currency: { from: instance; path: fields.currency; }; productId: { from: instance; path: productId; }; sourceAccountId: { from: instance; path: \"fields.customerAccountId\"; }; destinationAccountId: { from: instance; path: \"refs.heldAccountId\"; }; }; };\n steps: [];\n }\n action expire { public: none; agent_description: \"Close an unfunded booking.\"; due: { field: balanceDueAt; }; requires_drained: { path: refs.heldAccountId; }; steps: []; }\n for i in len(cancel_bands) {\n let(band): at(cancel_bands, i);\n let(refund_mode): get(band, \"refund\");\n let(full_refund): if_eq(refund_mode, \"full\", true, false);\n let(balance_refund): if_eq(refund_mode, \"balance\", true, false);\n let(retains): if_eq(refund_mode, \"full\", false, true);\n let(band_name): get(band, \"name\");\n let(balance_action): concat(\"balance_\", band_name);\n let(next_index): add(i, 1);\n let(next_field): concat(\"band\", next_index, \"Start\");\n let(band_end): if_eq(i, band_count, \"departureAt\", next_field);\n let(later): if_eq(i, 1, false, true);\n action [balance_action] {\n public: none; agent_description: \"Collect the remaining balance once before its deadline.\";\n deadline: { field: balanceDueAt; }; port: { allowedParties: [payer]; };\n moves: { key: balance; operation: \"internal_transfer.create\"; bind: { amount: { from: instance; path: concat(\"fields.\", \"balanceAmount\"); }; currency: { from: instance; path: fields.currency; }; productId: { from: instance; path: productId; }; sourceAccountId: { from: instance; path: \"fields.customerAccountId\"; }; destinationAccountId: { from: instance; path: \"refs.heldAccountId\"; }; }; };\n steps: [];\n }\n for paid in [\"deposit\", \"full\"] {\n let(is_full): if_eq(paid, \"full\", true, false);\n when(later) {\n let(open_action): concat(\"open_\", paid, \"_\", band_name);\n action [open_action] { public: none; agent_description: \"Open the next dated cancellation band.\"; due: { field: concat(\"band\", i, \"Start\"); }; steps: []; }\n }\n let(portion): if_eq(refund_mode, \"balance\", \"deposit\", paid);\n let(cancel_action): concat(\"cancel_\", paid, \"_\", band_name);\n action [cancel_action] {\n public: none; agent_description: \"Settle this paid amount under the active cancellation band.\";\n deadline: { field: band_end; }; port: { allowedParties: [payer]; };\n when(full_refund) { moves: { key: refund; operation: \"internal_transfer.create\"; bind: { amount: { from: instance; path: concat(\"fields.\", if_eq(paid, \"deposit\", \"depositAmount\", \"price\")); }; currency: { from: instance; path: fields.currency; }; productId: { from: instance; path: productId; }; sourceAccountId: { from: instance; path: \"refs.heldAccountId\"; }; destinationAccountId: { from: instance; path: \"fields.customerAccountId\"; }; }; }; }\n when(retains) {\n when(is_full) { when(balance_refund) { moves: { key: refund; operation: \"internal_transfer.create\"; bind: { amount: { from: instance; path: concat(\"fields.\", \"balanceAmount\"); }; currency: { from: instance; path: fields.currency; }; productId: { from: instance; path: productId; }; sourceAccountId: { from: instance; path: \"refs.heldAccountId\"; }; destinationAccountId: { from: instance; path: \"fields.customerAccountId\"; }; }; }; } }\n moves: { key: supplier; operation: \"internal_transfer.create\"; bind: { amount: { from: instance; path: concat(\"fields.\", concat(\"band\", i, \"Supplier\")); }; currency: { from: instance; path: fields.currency; }; productId: { from: instance; path: productId; }; sourceAccountId: { from: instance; path: \"refs.heldAccountId\"; }; destinationAccountId: { from: instance; path: \"fields.supplierAccountId\"; }; }; };\n moves: { key: operator; operation: \"internal_transfer.create\"; bind: { amount: { from: instance; path: concat(\"fields.\", concat(\"band\", i, title(portion), \"Net\")); }; currency: { from: instance; path: fields.currency; }; productId: { from: instance; path: productId; }; sourceAccountId: { from: instance; path: \"refs.heldAccountId\"; }; destinationAccountId: { from: instance; path: \"fields.operatorAccountId\"; }; }; };\n moves: { key: vat; operation: \"internal_transfer.create\"; bind: { amount: { from: instance; path: concat(\"fields.\", concat(\"band\", i, title(portion), \"Vat\")); }; currency: { from: instance; path: fields.currency; }; productId: { from: instance; path: productId; }; sourceAccountId: { from: instance; path: \"refs.heldAccountId\"; }; destinationAccountId: { from: instance; path: \"fields.taxAccountId\"; }; }; };\n }\n steps: [];\n }\n }\n }\n action cancel_unpaid {\n public: none; agent_description: \"Settle the deposit under the declared unpaid-balance band and end the remaining obligation.\";\n due: { field: balanceDueAt; };\n when_not(unpaid_band) { moves: { key: refund; operation: \"internal_transfer.create\"; bind: { amount: { from: instance; path: \"fields.depositAmount\"; }; currency: { from: instance; path: fields.currency; }; productId: { from: instance; path: productId; }; sourceAccountId: { from: instance; path: \"refs.heldAccountId\"; }; destinationAccountId: { from: instance; path: \"fields.customerAccountId\"; }; }; }; }\n when(unpaid_band) {\n let(unpaid_policy): at(cancel_bands, unpaid_band);\n let(unpaid_full_refund): if_eq(get(unpaid_policy, \"refund\"), \"full\", true, false);\n when(unpaid_full_refund) { moves: { key: refund; operation: \"internal_transfer.create\"; bind: { amount: { from: instance; path: \"fields.depositAmount\"; }; currency: { from: instance; path: fields.currency; }; productId: { from: instance; path: productId; }; sourceAccountId: { from: instance; path: \"refs.heldAccountId\"; }; destinationAccountId: { from: instance; path: \"fields.customerAccountId\"; }; }; }; }\n when_not(unpaid_full_refund) {\n moves: { key: supplier; operation: \"internal_transfer.create\"; bind: { amount: { from: instance; path: concat(\"fields.\", concat(\"band\", unpaid_band, \"Supplier\")); }; currency: { from: instance; path: fields.currency; }; productId: { from: instance; path: productId; }; sourceAccountId: { from: instance; path: \"refs.heldAccountId\"; }; destinationAccountId: { from: instance; path: \"fields.supplierAccountId\"; }; }; };\n moves: { key: operator; operation: \"internal_transfer.create\"; bind: { amount: { from: instance; path: concat(\"fields.\", concat(\"band\", unpaid_band, \"DepositNet\")); }; currency: { from: instance; path: fields.currency; }; productId: { from: instance; path: productId; }; sourceAccountId: { from: instance; path: \"refs.heldAccountId\"; }; destinationAccountId: { from: instance; path: \"fields.operatorAccountId\"; }; }; };\n moves: { key: vat; operation: \"internal_transfer.create\"; bind: { amount: { from: instance; path: concat(\"fields.\", concat(\"band\", unpaid_band, \"DepositVat\")); }; currency: { from: instance; path: fields.currency; }; productId: { from: instance; path: productId; }; sourceAccountId: { from: instance; path: \"refs.heldAccountId\"; }; destinationAccountId: { from: instance; path: \"fields.taxAccountId\"; }; }; };\n }\n }\n steps: [];\n }\n action await_confirmation { public: none; agent_description: \"Open confirmation at the booked departure.\"; due: { field: departureAt; }; steps: []; }\n action confirm {\n public: none; agent_description: \"Consume the named supplier's approved decision for this booking before timeout.\";\n deadline: { field: confirmationDeadline; };\n input: { type: object; additionalProperties: false; properties: { authorityId: { type: string; pattern: concat(\"^\", prefix(authority_type), \"_(sandbox|live)_[a-z0-9]{8,64}$\"); }; }; required: [authorityId]; };\n updates: [authorityId];\n requires refs: { field: authorityId; statuses: [approved]; match: { instrumentInstanceId: fields.requestInstanceId; \"fields.supplierAccountId\": fields.decidingActorAccountId; \"fields.supplierRole\": fields.decidingRole; }; attests: { instrument: fields.requestInstrumentId; party: decider; action: fields.requestAction; digest: fields.inputDigest; role: fields.decidingRole; expires_at: fields.expiresAt; consume: consume; }; };\n steps: [];\n }\n action release {\n public: none; agent_description: \"Pay supplier cost, operator net and VAT after confirmation.\";\n moves: { key: supplier; operation: \"internal_transfer.create\"; bind: { amount: { from: instance; path: concat(\"fields.\", \"supplierCost\"); }; currency: { from: instance; path: fields.currency; }; productId: { from: instance; path: productId; }; sourceAccountId: { from: instance; path: \"refs.heldAccountId\"; }; destinationAccountId: { from: instance; path: \"fields.supplierAccountId\"; }; }; };\n moves: { key: operator; operation: \"internal_transfer.create\"; bind: { amount: { from: instance; path: concat(\"fields.\", \"operatorNet\"); }; currency: { from: instance; path: fields.currency; }; productId: { from: instance; path: productId; }; sourceAccountId: { from: instance; path: \"refs.heldAccountId\"; }; destinationAccountId: { from: instance; path: \"fields.operatorAccountId\"; }; }; };\n moves: { key: vat; operation: \"internal_transfer.create\"; bind: { amount: { from: instance; path: concat(\"fields.\", \"marginVat\"); }; currency: { from: instance; path: fields.currency; }; productId: { from: instance; path: productId; }; sourceAccountId: { from: instance; path: \"refs.heldAccountId\"; }; destinationAccountId: { from: instance; path: \"fields.taxAccountId\"; }; }; };\n steps: [];\n }\n action refund_unconfirmed {\n public: none; agent_description: \"Return the entire booking price when supplier confirmation times out.\";\n due: { field: confirmationDeadline; };\n moves: { key: refund; operation: \"internal_transfer.create\"; bind: { amount: { from: instance; path: concat(\"fields.\", \"price\"); }; currency: { from: instance; path: fields.currency; }; productId: { from: instance; path: productId; }; sourceAccountId: { from: instance; path: \"refs.heldAccountId\"; }; destinationAccountId: { from: instance; path: \"fields.customerAccountId\"; }; }; };\n steps: [];\n }\n }\n}\n"],
7
7
  ["money_flows/captured_payment.hsx", "module std.money_flows.captured_payment\n\n// Two-phase authorization and capture payment flow for card and merchant processing.\n//\n// ### Purpose\n// `captured_payment` reserves funds against a payer's account and allows the payee to capture the authorized\n// balance in one or multiple slices before a reservation expiry date (`reserve_until`). It fits ecommerce checkouts,\n// card processing, hotel authorizations, and pay-at-pump fuel payments where final amounts vary or settle later.\n//\n// ### Selection guidance\n// - vs `instant_transfer`: `instant_transfer` immediately transfers money from payer to payee in a single irreversible\n// step without reservation or settlement delays. `captured_payment` separates authorization from capture, allowing\n// incremental captures, voids, amount corrections via the `correction` port, and external network reversals via `external_reversal`.\n// - vs `held_payment`: `held_payment` holds the full amount in third-party escrow pending release. `captured_payment`\n// reserves funds directly on payer balance and settles incrementally directly to payee.\n//\n// ### Parameters\n// - `payer`: Party whose account balance is reserved during authorization.\n// - `payee`: Beneficiary party capturing authorized funds.\n// - `amount`: Maximum authorized reservation in minor units of currency `C`.\n// - `reserve_until`: Expiration date for the authorization hold.\n// - `correction`: Condition port allowing post-settlement amount corrections.\n// - `external_reversal`: Condition port allowing external processor chargebacks/reversals.\n// - `derived_amount`: Optional block declaring percentage fee calculation.\n//\n// ### Decision ports\n// - `correction`: Condition allowing payee or processor to submit an amount correction after settlement.\n// - `external_reversal`: Condition allowing bank or card network to execute an external reversal.\n//\n// ### Example\n// ```hsx\n// program captured_payment_example \"Captured payment example\"\n// import { captured_payment } from \"std/money_flows\"\n// party payer: person\n// party payee: business\n// settlement card_payment = captured_payment {\n// payer: payer\n// payee: payee\n// amount: authorizedAmount: money(SAR)\n// reserve_until: reserveUntil\n// correction: port correct_capture\n// external_reversal: port reverse_capture within P14D\n// }\n// port correct_capture { allowed: [payee] }\n// port reverse_capture {\n// allowed: [payee]\n// shape: { externalReference: text }\n// }\n// ```\nexport instrument captured_payment<C>(payer: party, payee: party, amount: money<C>, reserve_until: date, correction: condition, external_reversal: condition, derived_amount: optional<block>) {\n let(correction_name): correction;\n let(reversal_name): external_reversal;\n let(reversal_window): external_reversal_within;\n let(instrument_words): words(instrument);\n when(derived_amount) {\n let(derived_field): get(derived_amount, \"field\");\n let(derived_source): get(derived_amount, \"source\");\n let(derived_rule): get(derived_amount, \"rule\");\n let(derived_bearer): get(derived_amount, \"bearer\");\n }\n agent_description: \"Reach for captured payment when the {payee} reserves against the {payer} first and then takes the money in slices before {reserve_until}. Pick instant transfer when the whole amount moves at once, and deposit when the reservation is claimed or returned whole.\";\n summary: \"Capture reservation from {payer} to {payee}\";\n title: sentence(instrument);\n description: \"Payer reservation captured by the payee in slices within a fixed window\";\n fields {\n when(derived_amount) {\n let(fee_collection_key): \"x-hyperscale-fee-collection-port\";\n let(reference_filter_key): \"x-hyperscale-reference-filter\";\n platformAccountId {\n type: account<C>;\n description: \"The product fee collection account\";\n [fee_collection_key]: true;\n [reference_filter_key]: { column: role; values: [customer_balance, product_revenue]; };\n }\n }\n amount { type: money<C>; description: \"Maximum captured amount in {C} minor units\"; }\n reserve_until { type: date; description: \"Reservation expiry that releases any uncaptured remainder\"; }\n reversalUntil { type: date; description: \"Machine-owned external reversal cutoff anchored when settlement completes\"; optional: true; }\n when(derived_amount) {\n [derived_field] { type: money<C>; description: \"Machine-computed {derived_rule} of {derived_source}; callers never supply it\"; pattern: \"^[1-9][0-9]{0,17}$\"; optional: true; }\n }\n }\n lifecycle {\n states created authorized partially_captured settled voided expired corrected reversed;\n initial created;\n on authorize: created -> authorized;\n on capture: authorized -> partially_captured;\n on capture_more: partially_captured -> partially_captured;\n on settle: authorized | partially_captured -> settled;\n on void: authorized -> voided;\n on expire: authorized -> expired;\n on settle_on_expiry: partially_captured -> settled;\n on [correction_name]: settled -> corrected;\n on [reversal_name]: settled -> reversed;\n }\n parties {\n payer: payer;\n beneficiary: payee;\n }\n when(derived_amount) {\n computes derived: { field: derived_field; source_field: derived_source; rounding: floor; rule: { kind: percentage_of; bps: derived_rule; }; };\n }\n action create {\n let(metadata_instrument): \"metadata.instrumentId\";\n let(metadata_instance): \"metadata.instrumentInstanceId\";\n let(metadata_phase): \"metadata.phase\";\n agent_description: \"Open the payment row before anything is reserved. The caller supplies the {amount}, the currency, and both party accounts. This reserves nothing, call authorize next.\";\n summary: \"Create a {instrument_words}\";\n when(derived_amount) {\n moves: [{ key: \"derived_amount\"; operation: internal_transfer.create; bind: { amount: { from: instance; path: concat(\"fields.\", derived_field); }; currency: { from: instance; path: fields.currency; }; destinationAccountId: { from: instance; path: fields.platformAccountId; }; [metadata_instrument]: { from: const; value: instrument; }; [metadata_instance]: { from: instance; path: instrumentInstanceId; }; [metadata_phase]: { from: const; value: create; }; productId: { from: instance; path: productId; }; sourceAccountId: { from: instance; path: concat(\"fields.\", camel(derived_bearer), \"AccountId\"); }; }; capture: { createDerivedAmountTransferId: transferId; }; }];\n }\n steps: [];\n }\n action authorize {\n agent_description: \"Reserve the {amount} against the {payer} in the {payee}'s favor until {reserve_until}. The payment must be created. The money is held, not paid: capture and settle post it, void and expiry release it.\";\n summary: \"Reserve the {amount} until {reserve_until}\";\n moves: [{ key: reservation; amount: amount; from: payer; to: beneficiary; operation: reserve; }];\n sandbox_failure_point: funding;\n steps: [];\n }\n action capture {\n agent_description: \"Post the first capture slice out of the live reservation. The payment must be authorized and the caller passes captureAmount in minor units. The slice settles for good and void can no longer release the reservation. Call before {reserve_until}.\";\n summary: \"Post one strict partial capture slice\";\n deadline { field: reserve_until; };\n input { type: object; additional_properties: false; properties: { captureAmount: { description: \"Positive partial capture amount in minor units\"; pattern: \"^[1-9][0-9]{0,17}$\"; type: string; }; }; required: [captureAmount]; }\n moves: [{ key: post; operation: internal_transfer.post; bind: { amount: { from: input; path: captureAmount; }; currency: { from: instance; path: fields.currency; }; postMode: { from: const; value: partial_only; }; transferId: { from: instance; path: refs.authorizeReservationTransferId; }; }; capture: { capturedAmount: postedAmount; }; }];\n sandbox_failure_point: \"release\";\n steps: [];\n }\n action capture_more {\n agent_description: \"Post another capture slice out of a partly captured reservation. Call this only after capture took the first slice. Each slice settles for good, and the uncaptured remainder still releases at {reserve_until}.\";\n summary: \"Post another strict partial capture slice\";\n deadline { field: reserve_until; };\n input { type: object; additional_properties: false; properties: { captureAmount: { description: \"Positive partial capture amount in minor units\"; pattern: \"^[1-9][0-9]{0,17}$\"; type: string; }; }; required: [captureAmount]; }\n moves: [{ key: post; operation: internal_transfer.post; bind: { amount: { from: input; path: captureAmount; }; currency: { from: instance; path: fields.currency; }; postMode: { from: const; value: partial_only; }; transferId: { from: instance; path: refs.authorizeReservationTransferId; }; }; capture: { capturedAmount: postedAmount; }; }];\n steps: [];\n }\n action settle {\n agent_description: \"Post the whole reserved remainder and finish the payment. The payment must be authorized or partly captured. The money settles and the external reversal window opens on reversalUntil.\";\n summary: \"Post the full reserved remainder and settle\";\n deadline { field: reserve_until; };\n sets(at) { field: reversalUntil; offset: reversal_window; };\n moves: [{ key: post; operation: internal_transfer.post; bind: { transferId: { from: instance; path: refs.authorizeReservationTransferId; }; }; capture: { capturedAmount: postedAmount; }; }];\n steps: [];\n }\n action void {\n agent_description: \"Release the reservation whole and cancel the payment. Only an authorized reservation with nothing captured can void. Once any slice is captured, settle it or let it expire instead.\";\n summary: \"Release an entirely uncaptured reservation\";\n moves: [{ key: void; operation: internal_transfer.void; bind: { reason: { from: const; value: \"Reservation voided before any capture\"; }; transferId: { from: instance; path: refs.authorizeReservationTransferId; }; }; }];\n steps: [];\n }\n action expire {\n summary: \"Release an uncaptured reservation at expiry\";\n due { field: reserve_until; };\n moves: [{ key: void; operation: internal_transfer.void; bind: { reason: { from: const; value: \"Uncaptured reservation expired\"; }; transferId: { from: instance; path: refs.authorizeReservationTransferId; }; }; }];\n steps: [];\n }\n action settle_on_expiry {\n summary: \"Release the uncaptured remainder and settle captured slices\";\n due { field: reserve_until; };\n sets(at) { field: reversalUntil; offset: reversal_window; };\n moves: [{ key: void; operation: internal_transfer.void; bind: { reason: { from: const; value: \"Uncaptured remainder released at expiry\"; }; transferId: { from: instance; path: refs.authorizeReservationTransferId; }; }; }];\n steps: [];\n }\n action [correction_name] {\n let(metadata_clawback): \"metadata.clawbackOfTransferId\";\n let(metadata_instrument): \"metadata.instrumentId\";\n let(metadata_instance): \"metadata.instrumentInstanceId\";\n let(metadata_phase): \"metadata.phase\";\n let(correction_capture): camel(concat(correction_name, \"_transfer_transfer_id\"));\n agent_description: \"Return the whole captured amount to the {payer} after the payment settled. Only a settled payment corrects, and only the party the product authorized may call it. The money moves back and there is no undo.\";\n summary: \"Return the full captured amount on payee correction\";\n when(correction_fields) {\n input {\n type: object;\n additional_properties: false;\n properties {\n for input_name in keys(correction_fields) {\n [input_name]: get(correction_fields, input_name);\n }\n }\n required: keys(correction_fields);\n }\n capture_input: {\n for item in keys(correction_fields) { [item]: item; }\n };\n }\n port { allowed_parties: correction_allowed; };\n moves: [{ key: transfer; operation: internal_transfer.create; bind: { amount: { from: instance; path: refs.capturedAmount; }; currency: { from: instance; path: fields.currency; }; destinationAccountId: { from: instance; path: concat(\"fields.\", payer, \"AccountId\"); }; [metadata_clawback]: { from: instance; path: refs.authorizeReservationTransferId; }; [metadata_instrument]: { from: const; value: instrument; }; [metadata_instance]: { from: instance; path: instrumentInstanceId; }; [metadata_phase]: { from: const; value: correction_name; }; productId: { from: instance; path: productId; }; sourceAccountId: { from: instance; path: concat(\"fields.\", payee, \"AccountId\"); }; }; capture: { [correction_capture]: transferId; }; }];\n steps: [];\n }\n action [reversal_name] {\n let(metadata_clawback): \"metadata.clawbackOfTransferId\";\n let(metadata_instrument): \"metadata.instrumentId\";\n let(metadata_instance): \"metadata.instrumentInstanceId\";\n let(metadata_phase): \"metadata.phase\";\n let(reversal_capture): camel(concat(reversal_name, \"_transfer_transfer_id\"));\n agent_description: \"Return the whole captured amount to the {payer} on an external reversal. The payment must be settled and the call must land before reversalUntil. The caller passes externalReference, which stays in the receipt. There is no undo.\";\n summary: \"Return the full captured amount on an external reversal\";\n deadline { field: reversalUntil; };\n when(external_reversal_fields) {\n capture_input: {\n for item in keys(external_reversal_fields) { [item]: item; }\n };\n input {\n type: object;\n additional_properties: false;\n properties {\n for input_name in keys(external_reversal_fields) {\n [input_name]: get(external_reversal_fields, input_name);\n }\n }\n required: keys(external_reversal_fields);\n }\n }\n port { allowed_parties: external_reversal_allowed; };\n moves: [{ key: transfer; operation: internal_transfer.create; bind: { amount: { from: instance; path: refs.capturedAmount; }; currency: { from: instance; path: fields.currency; }; destinationAccountId: { from: instance; path: concat(\"fields.\", payer, \"AccountId\"); }; [metadata_clawback]: { from: instance; path: refs.authorizeReservationTransferId; }; [metadata_instrument]: { from: const; value: instrument; }; [metadata_instance]: { from: instance; path: instrumentInstanceId; }; [metadata_phase]: { from: const; value: reversal_name; }; productId: { from: instance; path: productId; }; sourceAccountId: { from: instance; path: concat(\"fields.\", payee, \"AccountId\"); }; }; capture: { [reversal_capture]: transferId; }; }];\n steps: [];\n }\n}\n"],
8
8
  ["money_flows/conditional_disbursement.hsx", "module std.money_flows.conditional_disbursement\n\n// Capped disbursement from a source party to a destination party gated on stored external decision evidence.\n//\n// ### Purpose\n// `conditional_disbursement` manages evidence-contingent payouts subject to a cumulative cap.\n// It fits insurance claim settlements, grant tranches, subsidy distributions, and escrow milestones\n// where each approved payment requires explicit external evidence and the total paid must not exceed `cap`.\n//\n// ### Selection guidance\n// - vs `advance`: `conditional_disbursement` disburses non-repayable funds against external evidence\n// up to a declared cap. `advance` pays money up front with the expectation of repayment through carved\n// hold releases or scheduled installments.\n// - vs `instant_transfer`: `instant_transfer` moves money immediately with no evidence gate or cap.\n// `conditional_disbursement` requires an external decision port and evidence reference before any child amount moves.\n//\n// ### Parameters\n// - `source`: The funding party providing the money.\n// - `destination`: The beneficiary party receiving approved disbursements.\n// - `cap`: Maximum total amount that can be disbursed across all child approvals in minor units of currency `C`.\n// - `amount`: Binding name for child approval amount values.\n// - `decision`: Port conditioning approval, requiring evidence reference.\n//\n// ### Decision ports\n// - `decision`: External port providing decision evidence required to approve child disbursement amounts.\n//\n// ### Example\n// ```hsx\n// program conditional_disbursement_example \"Conditional disbursement example\"\n// import { conditional_disbursement } from \"std/money_flows\"\n// party source: business\n// party claimant: person\n// settlement claim_payment = conditional_disbursement {\n// source: source\n// destination: claimant\n// cap: policyLimit: money(SAR)\n// amount: approvedAmount: money(SAR)\n// decision: port approve_claim\n// }\n// port approve_claim {\n// allowed: [source]\n// shape: { evidenceReference: text }\n// }\n// ```\nexport instrument conditional_disbursement<C>(\n source: party,\n destination: party,\n cap: money<C>,\n amount: money<C>,\n decision: condition,\n) {\n let(child_id): concat(instrument, \"_approved_amount\");\n let(child_amount): \"amount\";\n let(parent_field): camel(concat(instrument, \"_id\"));\n let(source_field): camel(concat(source, \"_account_id\"));\n let(destination_field): camel(concat(destination, \"_account_id\"));\n let(currency_path): \"fields.currency\";\n let(source_path): concat(\"fields.\", source_field);\n let(destination_path): concat(\"fields.\", destination_field);\n\n title: sentence(instrument);\n agent_description: \"Reach for conditional disbursement when the {source} pays the {destination} only against stored external evidence and the payouts must stay under {cap}. Each approved amount is its own child row. Pick instant transfer when no evidence gates the payment.\";\n summary: \"Capped disbursement to {destination}\";\n description: \"Capped disbursement controlled by stored external evidence\";\n id_prefix: prefix(instrument);\n fields {\n cap {\n type: money<C>;\n description: \"Disbursement cap in {C} minor units\";\n }\n }\n required: [destination_field, source_field, currency, cap];\n lifecycle {\n states submitted denied;\n initial submitted;\n on deny: submitted -> denied;\n }\n parties {\n beneficiary: destination;\n payer: source;\n }\n action create {\n agent_description: \"Open the capped disbursement before any amount is approved. The caller supplies the {cap}, the currency, and the {source} and {destination} accounts. No money moves and nothing is approved yet.\";\n summary: \"Submit the capped disbursement\";\n steps: [];\n moves: [];\n }\n action deny {\n agent_description: \"Refuse the whole disbursement without moving money. Every child amount under it must still be unapproved, so call this before any approval lands. The caller passes evidenceReference, which stays in the receipt. Denial is final.\";\n summary: \"Record a denial without moving money\";\n when(decision_fields) {\n input {\n type: object;\n additional_properties: false;\n properties {\n for input_name in keys(decision_fields) {\n [input_name]: get(decision_fields, input_name);\n }\n }\n required: keys(decision_fields);\n }\n capture_input: {\n for item in keys(decision_fields) { [item]: item; }\n };\n }\n port { allowed_parties: decision_allowed; };\n requires aggregate: {\n check: { kind: all_in; };\n instrument_id: child_id;\n over: children;\n ref_field: parent_field;\n statuses: [created];\n };\n steps: [];\n moves: [];\n }\n\n instruments {\n instrument {\n id: child_id;\n generatedPrefix: true;\n title: concat(sentence(instrument), \" Approved Amount\");\n agent_description: concat(\"Reach for this child row to hold one evidence-gated amount under a \", words(instrument), \". The parent carries the {cap}, this row carries one approved amount and pays it once.\");\n summary: \"Approved amount under {instrument}\";\n description: \"One evidence-gated amount under {instrument}\";\n fields {\n [child_amount] {\n type: money<C>;\n description: \"Approved amount under the parent cap\";\n }\n currency {\n type: text;\n description: \"ISO 4217 currency code\";\n max_length: 3;\n min_length: 3;\n pattern: \"^[A-Z]{3}$\";\n }\n [parent_field] {\n type: ref<instrument>;\n description: \"The exact {instrument}\";\n pattern: concat(\"^\", prefix(instrument), \"_(sandbox|live)_[a-z0-9]{8,64}$\");\n }\n }\n required: [destination_field, source_field, child_amount, currency, parent_field];\n lifecycle {\n states created approved paid;\n initial created;\n on approve: created -> approved;\n on pay: approved -> paid;\n }\n parties {\n beneficiary: destination;\n payer: source;\n }\n action create {\n agent_description: concat(\"Create one candidate amount under the parent \", words(instrument), \". The parent must still be submitted and the currency and both accounts must match it. No money moves and the amount is not approved yet.\");\n summary: \"Create one candidate amount under the parent\";\n requires refs: {\n bind: {\n currency: currency_path;\n [destination_field]: destination_path;\n [source_field]: source_path;\n };\n field: parent_field;\n statuses: [submitted];\n unique: true;\n };\n steps: [];\n moves: [];\n }\n action approve {\n agent_description: \"Store one externally approved amount under the parent cap. The row must be created, the parent still submitted, and the already approved and paid amounts plus this one must stay within {cap}. The caller passes evidenceReference. No money moves yet.\";\n summary: \"Store one externally approved amount under the cap\";\n input: {\n additional_properties: false;\n properties: {\n evidence_reference: {\n description: \"Required decision reference retained in the operation receipt\";\n max_length: 180;\n min_length: 1;\n type: \"string\";\n };\n };\n required: [evidenceReference];\n type: \"object\";\n };\n capture_input: { decision_evidence_reference: evidenceReference; };\n port: { allowed_parties: decision_allowed; };\n requires exposure: {\n amount_field: child_amount;\n anchor_field: parent_field;\n cap_field: cap;\n cap_on_anchor: true;\n child_instrument_id: child_id;\n statuses: [approved, paid];\n };\n requires refs: {\n field: parent_field;\n match: {\n [currency_path]: currency_path;\n [destination_path]: destination_path;\n [source_path]: source_path;\n };\n statuses: [submitted];\n };\n steps: [];\n moves: [];\n }\n action pay {\n agent_description: \"Pay the stored approved amount to the {destination} once. The row must be approved and the parent still submitted. The transfer posts straight out of the {source} and does not unwind.\";\n summary: \"Pay the stored approved amount once\";\n requires refs: {\n field: parent_field;\n match: {\n [currency_path]: currency_path;\n [destination_path]: destination_path;\n [source_path]: source_path;\n };\n statuses: [submitted];\n };\n moves: [{ amount: child_amount; from: payer; to: beneficiary; key: payout; operation: create; }];\n steps: [];\n }\n }\n }\n}\n"],
9
9
  ["money_flows/credit_facility.hsx", "module std.money_flows.credit_facility\n\n// Revolving credit line providing reusable borrowing capacity up to a limit backed by scheduled obligations.\n//\n// ### Purpose\n// `credit_facility` manages revolving commercial credit, inventory financing, and overdraft facilities.\n// A borrower can draw funds multiple times up to `limit` into `draw_destination`. Each draw creates a child\n// record linked to a scheduled `obligation`. Repayments restore available borrowing capacity until `expires_at`.\n//\n// ### Selection guidance\n// - vs `advance`: `credit_facility` provides revolving, reusable credit lines where multiple draws can occur\n// and repayments restore capacity. `advance` is a single upfront lump-sum disbursement with a fixed repayment plan.\n// - vs `scheduled`: `scheduled` defines repayment installments or recurring transfers. `credit_facility` delegates\n// draw repayments to a `scheduled` obligation while tracking total facility utilization and limit compliance.\n//\n// ### Parameters\n// - `lender`: The financing institution or party providing the credit capacity.\n// - `borrower`: The borrowing party authorized to draw against the facility limit.\n// - `draw_destination`: Account or party receiving disbursed draw proceeds.\n// - `limit`: Total revolving borrowing limit in minor units of currency `C`.\n// - `expires_at`: Expiration date after which new draws cannot be opened.\n// - `obligation`: Reference to a scheduled obligation instrument handling draw repayments.\n//\n// ### Decision ports\n// None on the facility itself. Mandates and decision ports are declared on the linked `obligation` instrument.\n//\n// ### Example\n// ```hsx\n// program credit_facility_example \"Credit facility example\"\n// import { credit_facility, scheduled } from \"std/money_flows\"\n// party lender: business\n// party borrower: business\n// party draw_destination: business\n// party repayment_source: business\n// settlement repayment = scheduled {\n// mode: obligation\n// payer: repayment_source\n// payee: lender\n// debtor: borrower\n// amount: principal: money(SAR)\n// count: 2\n// every: P30D\n// first_due: firstDueAt\n// }\n// settlement facility = credit_facility {\n// lender: lender\n// borrower: borrower\n// draw_destination: draw_destination\n// limit: facilityLimit: money(SAR)\n// expires_at: expiresAt\n// obligation: repayment.obligation\n// }\n// ```\nexport instrument credit_facility<C>(\n lender: party,\n borrower: party,\n draw_destination: party,\n limit: money<C>,\n expires_at: date,\n obligation: ref,\n) {\n let(draw_id): concat(instrument, \"_draw\");\n let(parent_field): camel(concat(instrument, \"_id\"));\n let(borrower_field): camel(concat(borrower, \"_account_id\"));\n let(lender_field): camel(concat(lender, \"_account_id\"));\n let(destination_field): camel(concat(draw_destination, \"_account_id\"));\n let(currency_path): \"fields.currency\";\n let(lender_path): concat(\"fields.\", lender_field);\n let(destination_path): concat(\"fields.\", destination_field);\n let(reference_filter): \"x-hyperscale-reference-filter\";\n let(obligation_name): owner(obligation);\n let(obligation_field): camel(concat(obligation_name, \"_id\"));\n\n title: sentence(instrument);\n agent_description: \"Reach for credit facility when the {lender} gives the {borrower} reusable capacity up to {limit} and every draw is repaid through its own scheduled obligation. Capacity frees up as each obligation resolves. Pick advance for one up-front payment with no reusable limit.\";\n summary: \"Draw capacity for {borrower}\";\n description: \"Reusable capacity with repayment delegated to one scheduled obligation\";\n id_prefix: prefix(instrument);\n fields {\n [borrower_field] {\n type: account<C>;\n description: \"The {borrower} account\";\n [reference_filter]: {\n column: role;\n values: [customer_balance];\n };\n }\n limit {\n type: money<C>;\n description: \"Facility limit in {C} minor units\";\n }\n expires_at {\n type: date;\n description: \"Stored draw expiry\";\n }\n }\n required: [borrower_field, destination_field, lender_field, currency, limit, expires_at];\n lifecycle {\n states active frozen closed;\n initial active;\n on freeze: active -> frozen;\n on close: active|frozen -> closed;\n }\n parties {\n beneficiary: draw_destination;\n payer: lender;\n }\n action create {\n agent_description: \"Open the facility before any draw exists. The caller supplies the {limit}, the currency, the {borrower} and {lender} accounts, the draw destination account, and the stored expiry. No money moves.\";\n summary: \"Open the facility\";\n steps: [];\n moves: [];\n }\n action freeze {\n summary: \"Freeze new draws at expiry\";\n due: { field: expires_at; };\n steps: [];\n moves: [];\n }\n action close {\n agent_description: \"Close the facility for good. Every draw under it must already be resolved, so clear the outstanding obligations first. No money moves and a closed facility never lends again.\";\n summary: \"Close only when every admitted draw resolved\";\n requires aggregate: {\n check: { kind: all_in; };\n instrument_id: draw_id;\n over: children;\n ref_field: parent_field;\n statuses: [resolved];\n };\n steps: [];\n moves: [];\n }\n\n instruments {\n instrument {\n id: draw_id;\n generatedPrefix: true;\n title: concat(sentence(instrument), \" Draw\");\n agent_description: concat(\"Reach for this child row to take one draw against a \", words(instrument), \". The parent carries the {limit}, this row carries one drawn amount and the single obligation that repays it.\");\n summary: \"Draw from {instrument}\";\n description: \"One capacity-capped draw linked to obligation\";\n fields {\n amount {\n type: money<C>;\n description: \"One draw amount\";\n }\n currency {\n type: text;\n description: \"ISO 4217 currency code\";\n max_length: 3;\n min_length: 3;\n pattern: \"^[A-Z]{3}$\";\n }\n [parent_field] {\n type: ref<instrument>;\n description: \"The exact {instrument}\";\n pattern: concat(\"^\", prefix(instrument), \"_(sandbox|live)_[a-z0-9]{8,64}$\");\n }\n [obligation_field] {\n type: ref<obligation_name>;\n description: \"The sole repayment obligation\";\n pattern: concat(\"^\", prefix(obligation_name), \"_(sandbox|live)_[a-z0-9]{8,64}$\");\n }\n }\n required: [destination_field, lender_field, amount, currency, parent_field, obligation_field];\n lifecycle {\n states drawn resolved;\n initial drawn;\n on resolve: drawn -> resolved;\n }\n parties {\n beneficiary: draw_destination;\n payer: lender;\n }\n action create {\n agent_description: \"Draw one amount out of the facility to the draw destination account. The facility must be active, the linked obligation active and bound to no other draw, and the drawn amounts plus this one must stay within {limit}. The money leaves the {lender} straight away.\";\n summary: \"Create one draw under the locked facility capacity\";\n requires exposure: {\n amount_field: amount;\n anchor_field: parent_field;\n cap_field: limit;\n cap_on_anchor: true;\n child_instrument_id: draw_id;\n statuses: [drawn];\n };\n requires refs: [{\n bind: {\n currency: currency_path;\n [destination_field]: destination_path;\n [lender_field]: lender_path;\n };\n field: parent_field;\n statuses: [active];\n }, {\n field: obligation_field;\n statuses: [active];\n unique: true;\n }];\n moves: [{ amount: amount; from: payer; to: beneficiary; key: draw; operation: create; }];\n steps: [];\n }\n action resolve {\n agent_description: \"Free the drawn capacity back to the facility. The linked obligation must already be repaid or written off. No money moves here, the obligation did the repaying.\";\n summary: \"Release revolving capacity only after the linked obligation resolves\";\n requires refs: {\n field: obligation_field;\n statuses: [repaid, written_off];\n };\n steps: [];\n moves: [];\n }\n }\n }\n}\n"],
package/src/typecheck.ts CHANGED
@@ -52,7 +52,9 @@ const SNAKE_CASE = /^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$/;
52
52
  const CAMEL_CASE = /^[a-z][A-Za-z0-9]*$/;
53
53
  const CURRENCY = /^[A-Z]{3}$/;
54
54
  const MONEY_PATTERN = "^[1-9][0-9]{0,17}$";
55
- const OPTIONAL_MONEY_PATTERN = "^(0|[1-9][0-9]{0,17})$";
55
+ // Owner ruling 2026-09-16: amounts stay strictly positive unless the author
56
+ // marks the money field with `allow_zero: true`; `optional` no longer implies zero.
57
+ const ZERO_MONEY_PATTERN = "^(0|[1-9][0-9]{0,17})$";
56
58
  const ACCOUNT_PATTERN = "^acct_(sandbox|live)_[a-z0-9]{8,64}$";
57
59
 
58
60
  const NONE_SENTINEL = "__hsx_none__";
@@ -950,7 +952,7 @@ function publishedFieldType(
950
952
  if (schema.type !== "string") return { kind: "unknown" };
951
953
  if (
952
954
  schema.pattern === MONEY_PATTERN ||
953
- schema.pattern === OPTIONAL_MONEY_PATTERN
955
+ schema.pattern === ZERO_MONEY_PATTERN
954
956
  ) {
955
957
  const currency = schema["x-hyperscale-currency"];
956
958
  return typeof currency === "string"
@@ -2093,7 +2095,7 @@ function lowerPortShapeField(
2093
2095
  ? { kind: "number", raw: String(value), span: row.span }
2094
2096
  : { kind: "string", value: String(value), span: row.span },
2095
2097
  }));
2096
- // Port inputs require every key, but a caller-decided amount may be zero.
2098
+ // Port inputs require every key; a caller-decided amount may still be zero.
2097
2099
  const field = lowerField(
2098
2100
  {
2099
2101
  ...row,
@@ -2107,12 +2109,16 @@ function lowerPortShapeField(
2107
2109
  span: row.span,
2108
2110
  value: row.value,
2109
2111
  },
2110
- {
2111
- key: { kind: "ident", name: "optional", span: row.span },
2112
- qualifiers: [],
2113
- span: row.span,
2114
- value: { kind: "boolean", value: true, span: row.span },
2115
- },
2112
+ ...(type.kind === "money"
2113
+ ? [
2114
+ {
2115
+ key: { kind: "ident", name: "allow_zero", span: row.span },
2116
+ qualifiers: [],
2117
+ span: row.span,
2118
+ value: { kind: "boolean", value: true, span: row.span },
2119
+ } as const,
2120
+ ]
2121
+ : []),
2116
2122
  ...scalarEntries(extras),
2117
2123
  ],
2118
2124
  },
@@ -2545,7 +2551,7 @@ function argumentMatches(
2545
2551
  );
2546
2552
  case "unknown":
2547
2553
  if (expected.kind !== "ident") return false;
2548
- if (expected.name === "unknown") return true;
2554
+ if (expected.name === "unknown" || expected.name === "json") return true;
2549
2555
  return (
2550
2556
  expected.name === "block" &&
2551
2557
  (actual.kind === "block" || actual.kind === "decided_amount")
@@ -3743,6 +3749,7 @@ function lowerField(
3743
3749
  let description: string | undefined;
3744
3750
  let required = true;
3745
3751
  let extra: Record<string, JsonValue> = {};
3752
+ let allowZero = false;
3746
3753
  if (row.value.kind === "block") {
3747
3754
  const typeRow = entry(row.value, "type");
3748
3755
  if (typeRow) typeExpr = typeRow.value;
@@ -3750,12 +3757,14 @@ function lowerField(
3750
3757
  if (desc?.value.kind === "string") description = desc.value.value;
3751
3758
  const optional = entry(row.value, "optional");
3752
3759
  if (optional?.value.kind === "boolean") required = !optional.value.value;
3760
+ const zero = entry(row.value, "allow_zero");
3761
+ if (zero?.value.kind === "boolean") allowZero = zero.value.value;
3753
3762
  extra = blockToObject(
3754
3763
  {
3755
3764
  ...row.value,
3756
3765
  entries: row.value.entries.filter(
3757
3766
  (item) =>
3758
- !["type", "description", "desc", "optional"].includes(
3767
+ !["type", "description", "desc", "optional", "allow_zero"].includes(
3759
3768
  item.key.name,
3760
3769
  ),
3761
3770
  ),
@@ -3768,6 +3777,15 @@ function lowerField(
3768
3777
  // Diagnostics point at the field's own type expression, not at an alias
3769
3778
  // declaration that may live in an imported module.
3770
3779
  const declaredSpan = typeExpr.span;
3780
+ if (allowZero && type.kind !== "money") {
3781
+ diagnostics.push({
3782
+ code: "HSX1105",
3783
+ fix: `declare ${name} as money(CUR) or remove allow_zero`,
3784
+ message: `field ${name} sets allow_zero but is ${typeWords(type)}, not money`,
3785
+ severity: "error",
3786
+ span: declaredSpan,
3787
+ });
3788
+ }
3771
3789
  const seen = new Set<string>();
3772
3790
  while (
3773
3791
  typeExpr.kind === "ident" &&
@@ -3822,7 +3840,7 @@ function lowerField(
3822
3840
  ? "object"
3823
3841
  : undefined;
3824
3842
  const schema = {
3825
- ...schemaFor(type, required),
3843
+ ...schemaFor(type, allowZero),
3826
3844
  ...extra,
3827
3845
  ...(structuralType ? { type: structuralType } : {}),
3828
3846
  ...(description ? { description } : {}),
@@ -4334,7 +4352,9 @@ function bindFieldReferences(
4334
4352
  (path.includes("requiresExposure") && key === "capField") ||
4335
4353
  (path.includes("reconcile") &&
4336
4354
  path.includes("exception") &&
4337
- ["amountField", "reasonField", "refField"].includes(key ?? ""))
4355
+ ["amountField", "reasonField", "refField"].includes(key ?? "")) ||
4356
+ (path.includes("cascade") &&
4357
+ ["inputField", "refField"].includes(key ?? ""))
4338
4358
  );
4339
4359
  };
4340
4360
  const visit = (value: JsonValue, path: readonly string[] = []): void => {
@@ -5165,14 +5185,14 @@ function typeOf(
5165
5185
 
5166
5186
  function schemaFor(
5167
5187
  type: HsxType,
5168
- required: boolean,
5188
+ allowZero: boolean,
5169
5189
  ): Record<string, JsonValue> {
5170
5190
  switch (type.kind) {
5171
5191
  case "money":
5172
5192
  return {
5173
5193
  ...(type.fixedAmount
5174
5194
  ? { const: type.fixedAmount, pattern: MONEY_PATTERN }
5175
- : { pattern: required ? MONEY_PATTERN : OPTIONAL_MONEY_PATTERN }),
5195
+ : { pattern: allowZero ? ZERO_MONEY_PATTERN : MONEY_PATTERN }),
5176
5196
  type: "string",
5177
5197
  ...(type.currency && CURRENCY.test(type.currency)
5178
5198
  ? { "x-hyperscale-currency": type.currency }
package/src/version.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /** Package version reported by the command line. */
2
- export const HSX_VERSION = "2.2.1";
2
+ export const HSX_VERSION = "2.4.0";
3
3
 
4
4
  /** Canonical UDL contract version emitted by this compiler. */
5
5
  export const HSX_TARGET_UDL_VERSION = 1;
@@ -154,7 +154,7 @@ export instrument advance<C>(
154
154
  }
155
155
  fee_amount {
156
156
  type: money<C>;
157
- when(fee) { optional: true; }
157
+ when(fee) { optional: true; allow_zero: true; }
158
158
  description: concat(fee, " of ", amount, ", the funder's discount repaid on top of the advance");
159
159
  }
160
160
  repayable_amount {
@@ -53,6 +53,7 @@ module std.money_flows.cancellable_booking
53
53
  // the deposit. balance_before and confirmation_window are positive fixed durations
54
54
  // relative to departure. tax_bps applies to margin after the supplier share.
55
55
  // authority_type and supplier_role bind release to a consumed confirmation decision.
56
+ // collects optionally cascades named actions to linked instruments on deposit.
56
57
  // acceptance optionally supplies an aggregate gate. All actions are private until
57
58
  // expose selects them. Ordered band starts and deadlines prevent a late sweep from
58
59
  // extending an earlier band's cancellation rights. Full timeout refund returns price.
@@ -74,7 +75,7 @@ export instrument cancellable_booking<C>(
74
75
  balance_before: optional<text>, confirmation_window: optional<text>, unpaid_band: optional<integer>,
75
76
  acceptance: optional<block>, deposit_bps: optional<integer>, tax_bps: optional<integer>,
76
77
  authority_type: optional<text>, supplier_role: optional<text>,
77
-
78
+ collects: optional<json>,
78
79
  ) {
79
80
  when_not(cancel_bands) {
80
81
  let(guest_account_field): camel(concat(guest, "_account_id"));
@@ -182,11 +183,11 @@ export instrument cancellable_booking<C>(
182
183
  confirmationDeadline { type: date; const: shift_date(departure, confirmation_window); }
183
184
  price { type: money<C>; const: price_value; }
184
185
  supplierCost { type: money<C>; const: supplier_cost; }
185
- depositAmount { type: money<C>; optional: true; }
186
+ depositAmount { type: money<C>; optional: true; allow_zero: true; }
186
187
  balanceAmount: money<C>;
187
188
  margin: money<C>;
188
189
  operatorNet: money<C>;
189
- marginVat { type: money<C>; optional: true; }
190
+ marginVat { type: money<C>; optional: true; allow_zero: true; }
190
191
  for i in len(cancel_bands) {
191
192
  let(band): at(cancel_bands, i);
192
193
  let(refund_mode): get(band, "refund");
@@ -197,7 +198,7 @@ export instrument cancellable_booking<C>(
197
198
  when(later) { band[i]Start { type: date; const: get(band, "starts_at"); } }
198
199
 
199
200
  when(retains) {
200
- band[i]Supplier { type: money<C>; optional: true; }
201
+ band[i]Supplier { type: money<C>; optional: true; allow_zero: true; }
201
202
  let(portions): if_eq(refund_mode, "balance", ["deposit"], ["deposit", "full"]);
202
203
  for paid in portions {
203
204
  let(is_full): if_eq(paid, "full", true, false);
@@ -207,7 +208,7 @@ export instrument cancellable_booking<C>(
207
208
  let(vat_field): concat("band", i, title(portion), "Vat");
208
209
  [margin_field]: money<C>;
209
210
  [net_field]: money<C>;
210
- [vat_field] { type: money<C>; optional: true; }
211
+ [vat_field] { type: money<C>; optional: true; allow_zero: true; }
211
212
  }
212
213
  }
213
214
  }
@@ -312,6 +313,7 @@ export instrument cancellable_booking<C>(
312
313
  action deposit {
313
314
  public: none;
314
315
  agent_description: "Collect the derived deposit after the acceptance requirements pass.";
316
+ when(collects) { cascade: collects; }
315
317
  when(acceptance) { requires aggregate: acceptance; }
316
318
  deadline: { field: balanceDueAt; }; port: { allowedParties: [payer]; };
317
319
  moves: { key: deposit; operation: "internal_transfer.create"; bind: { amount: { from: instance; path: concat("fields.", "depositAmount"); }; currency: { from: instance; path: fields.currency; }; productId: { from: instance; path: productId; }; sourceAccountId: { from: instance; path: "fields.customerAccountId"; }; destinationAccountId: { from: instance; path: "refs.heldAccountId"; }; }; };