@hyperscale0/hsx 1.0.130 → 1.0.134
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/compile.d.ts.map +1 -1
- package/dist/src/compile.js +77 -13
- package/dist/src/compile.js.map +1 -1
- package/dist/src/examples-bundle.js +4 -4
- package/dist/src/examples-bundle.js.map +1 -1
- package/dist/src/std-bundle.js +9 -9
- package/dist/src/std-bundle.js.map +1 -1
- package/docs/README.md +48 -22
- package/examples/cards.hsx +3 -1
- package/examples/marketplace.hsx +3 -1
- package/examples/rental-deposit.hsx +2 -0
- package/examples/wallet.hsx +1 -1
- package/package.json +4 -4
- package/src/compile.ts +100 -13
- package/src/examples-bundle.ts +4 -4
- package/src/std-bundle.ts +9 -9
- package/std/cards.hsx +16 -9
- package/std/escrow.hsx +32 -6
- package/std/financing.hsx +155 -68
- package/std/insurance.hsx +3 -3
- package/std/lending.hsx +16 -8
- package/std/marketplace.hsx +4 -3
- package/std/money.hsx +28 -14
- package/std/savings.hsx +3 -2
- package/std/wallet.hsx +10 -5
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"std-bundle.js","sourceRoot":"","sources":["../../src/std-bundle.ts"],"names":[],"mappings":"AAAA,oEAAoE;AAEpE,MAAM,CAAC,MAAM,iBAAiB,GAAgC,IAAI,GAAG,CAAiB;IACpF,CAAC,aAAa,EAAE,soLAAsoL,CAAC;IACvpL,CAAC,WAAW,EAAE,
|
|
1
|
+
{"version":3,"file":"std-bundle.js","sourceRoot":"","sources":["../../src/std-bundle.ts"],"names":[],"mappings":"AAAA,oEAAoE;AAEpE,MAAM,CAAC,MAAM,iBAAiB,GAAgC,IAAI,GAAG,CAAiB;IACpF,CAAC,aAAa,EAAE,soLAAsoL,CAAC;IACvpL,CAAC,WAAW,EAAE,6wMAA6wM,CAAC;IAC5xM,CAAC,iBAAiB,EAAE,2sNAA2sN,CAAC;IAChuN,CAAC,YAAY,EAAE,yjQAAyjQ,CAAC;IACzkQ,CAAC,eAAe,EAAE,6nrFAA6nrF,CAAC;IAChprF,CAAC,eAAe,EAAE,41LAA41L,CAAC;IAC/2L,CAAC,aAAa,EAAE,03UAA03U,CAAC;IAC34U,CAAC,iBAAiB,EAAE,syTAAsyT,CAAC;IAC3zT,CAAC,WAAW,EAAE,4zYAA4zY,CAAC;IAC30Y,CAAC,eAAe,EAAE,+q2BAA+q2B,CAAC;IACls2B,CAAC,aAAa,EAAE,ohKAAohK,CAAC;IACriK,CAAC,YAAY,EAAE,2zIAA2zI,CAAC;CAC50I,CAAC,CAAC"}
|
package/docs/README.md
CHANGED
|
@@ -147,6 +147,19 @@ Creation accepts `{}`. Every normalized field, including adapter requirements,
|
|
|
147
147
|
is optional at creation. Only a named action requires its subject metadata.
|
|
148
148
|
Use `attach` inside the object block to configure its financial instruments.
|
|
149
149
|
|
|
150
|
+
A required instrument field with no fixed value is a local creation value by
|
|
151
|
+
default. It becomes create `input` on that agreement and never an object field,
|
|
152
|
+
so two agreements on one object keep separate amounts and a wallet spend never
|
|
153
|
+
prefills a later fee. Only a rename such as `rename { dueAt: dueAt }` shares
|
|
154
|
+
it as record metadata: the value then comes from the object field as a create
|
|
155
|
+
subject requirement. An authored object field of the same name does not share
|
|
156
|
+
it; write `rename { price: price }` when the agreement should read that field.
|
|
157
|
+
Later actions still read `self.amount` from the agreement either way. An
|
|
158
|
+
attachment whose create needs input must expose `create`, or compilation fails,
|
|
159
|
+
because a public action on a fresh attachment creates it first with no input.
|
|
160
|
+
A rename binds only fields of the attached instrument itself; renaming a record
|
|
161
|
+
field such as `insurance.cover`'s slice `premium` reports `subject_field_unknown`.
|
|
162
|
+
|
|
150
163
|
## Headers and tunables
|
|
151
164
|
|
|
152
165
|
The header defines the available policies. The company chooses percentages, amounts, durations,
|
|
@@ -266,7 +279,9 @@ maxAge 1d` requires a recent completed provider check.
|
|
|
266
279
|
`moves post self.receipt` posts it and `moves void self.receipt` releases it.
|
|
267
280
|
`moves amount from payer shares shares` expands a declared split. Optional `fee`,
|
|
268
281
|
`capture`, `key` and `boundary` modifiers describe a move. `boundary` applies
|
|
269
|
-
only to reservations. Fees settle with create moves, never reservations.
|
|
282
|
+
only to reservations. Fees settle with create moves, never reservations. A fee move
|
|
283
|
+
also derives `<action>_<move>_debit`, the payer's whole debit: amount plus fee and
|
|
284
|
+
tax when the buyer pays, the amount alone when the seller pays. Repeated clauses keep
|
|
270
285
|
their declaration order. The JSON-like clause form remains accepted and lowers
|
|
271
286
|
to the same [UDL clauses](https://github.com/hyperscale0/hyperscale-udl/blob/main/spec/README.md).
|
|
272
287
|
Use `economics { purpose: earning, sourceParty: merchant }` on a move to
|
|
@@ -407,21 +422,21 @@ Read the instrument's states, time gates and moves before promising a money
|
|
|
407
422
|
outcome. A compiler pass does not prove that required actions are exposed,
|
|
408
423
|
adapters are bound, participants have funds, or the flow can finish.
|
|
409
424
|
|
|
410
|
-
| Instrument | Behavior
|
|
411
|
-
| ------------------------------------------ |
|
|
412
|
-
| `financing.installments` | `share` allocates collected profit to a cash payable owned by `capital`. It does not itself pay operator income.
|
|
413
|
-
| `financing.limits` | `per_borrower` caps outstanding principal. Both borrower and portfolio limits must be approved before disbursement; attaching them does not approve them.
|
|
414
|
-
| `lending.round` | The target is the linked plan's principal. Closing requires commitments and held funds to equal that amount.
|
|
415
|
-
| `lending.distribution` | Cash distribution needs an eligible recorded settlement, `prepare_cash`, one share record per funded commitment, then `distribute_cash`. Attaching it moves nothing.
|
|
416
|
-
| `insurance.cover.slice` | `collect` credits premium net of commission to the insurer account. The tenant retains commission. `refund` returns both portions; claims reserve insurer funds. External settlement uses the boundary protocol.
|
|
417
|
-
| `financing.installments`, `savings.circle` | Supply explicit date lists when creating agreements. A term count does not generate a monthly calendar. Savings supports at most 60 distinct member seats.
|
|
418
|
-
| `escrow.hold` | `fund` takes the whole price. Financing into pending escrow collects the remaining down payment and adds capital principal at disbursement; `fund` is not a down-payment checkout.
|
|
419
|
-
| `escrow.hold` | Acceptance timeout enters `disputed` without paying the seller. Delivery and return verification belong to `payee`; rebinding it also changes who receives accepted funds.
|
|
420
|
-
| `escrow.hold` | `deliver_within` (default `14d`) starts at funding. After it passes without delivery, an exposed `refund_undelivered` lets the payer take the whole held price back. Nothing runs it automatically.
|
|
421
|
-
| `wallet.balance` | `topup_fee` (default `0%`) takes a floored percentage of each top-up for the Company. At `1%`, a 100.00 top-up credits 99.00 and earns 1.00.
|
|
422
|
-
| `booking.reservation` | The balance is payable only from `deposit_paid`, so a cancelled booking never takes it. `middle_forfeit` and `late_forfeit` default to `0%`, so every cancellation returns all held money until the author sets them. At departure the clock completes a paid booking and lapses an unpaid one on `late_forfeit`.
|
|
423
|
-
| `financing.late_charge` | `fine` is a fixed money amount, not a percentage of overdue principal.
|
|
424
|
-
| `cards.card` | `spend_limit` is a per-authorization ceiling, not a monthly aggregate.
|
|
425
|
+
| Instrument | Behavior |
|
|
426
|
+
| ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
427
|
+
| `financing.installments` | `share` allocates collected profit to a cash payable owned by `capital`. It does not itself pay operator income. |
|
|
428
|
+
| `financing.limits` | `per_borrower` caps outstanding principal. Both borrower and portfolio limits must be approved before disbursement; attaching them does not approve them. |
|
|
429
|
+
| `lending.round` | The target is the linked plan's principal. Closing requires commitments and held funds to equal that amount. |
|
|
430
|
+
| `lending.distribution` | Cash distribution needs an eligible recorded settlement, `prepare_cash`, one share record per funded commitment, then `distribute_cash`. Attaching it moves nothing. |
|
|
431
|
+
| `insurance.cover.slice` | `collect` credits premium net of commission to the insurer account. The tenant retains commission. `refund` returns both portions; claims reserve insurer funds. External settlement uses the boundary protocol. |
|
|
432
|
+
| `financing.installments`, `savings.circle` | Supply explicit date lists when creating agreements. A term count does not generate a monthly calendar. Savings supports at most 60 distinct member seats. |
|
|
433
|
+
| `escrow.hold` | `fund` takes the whole price. Financing into pending escrow collects the remaining down payment and adds capital principal at disbursement; `fund` is not a down-payment checkout. |
|
|
434
|
+
| `escrow.hold` | Acceptance timeout enters `disputed` without paying the seller. Delivery and return verification belong to `payee`; rebinding it also changes who receives accepted funds. |
|
|
435
|
+
| `escrow.hold` | `deliver_within` (default `14d`) starts at funding. After it passes without delivery, an exposed `refund_undelivered` lets the payer take the whole held price back. Nothing runs it automatically. |
|
|
436
|
+
| `wallet.balance` | `topup_fee` (default `0%`) takes a floored percentage of each top-up for the Company. At `1%`, a 100.00 top-up credits 99.00 and earns 1.00. |
|
|
437
|
+
| `booking.reservation` | The balance is payable only from `deposit_paid`, so a cancelled booking never takes it. `middle_forfeit` and `late_forfeit` default to `0%`, so every cancellation returns all held money until the author sets them. At departure the clock completes a paid booking and lapses an unpaid one on `late_forfeit`. |
|
|
438
|
+
| `financing.late_charge` | `fine` is a fixed money amount, not a percentage of overdue principal. |
|
|
439
|
+
| `cards.card` | `spend_limit` is a per-authorization ceiling, not a monthly aggregate. Bind `person` to the cardholder's `holder`, such as `person: actor`; create refuses any other party. The person freezes, unfreezes and cancels; the issuer uses `issuer_freeze`, `issuer_unfreeze` and `issuer_cancel`, and only `issuer_unfreeze` lifts a block. |
|
|
425
440
|
|
|
426
441
|
A seller-owned marketplace object can declare `entryActions: [create_order]`
|
|
427
442
|
when `create_order` exposes an attachment's create action with its buyer bound
|
|
@@ -432,6 +447,14 @@ Without entry actions, the company runs a two-customer sale from its Product
|
|
|
432
447
|
key: create the record with `onBehalfOf` naming the seller, then run the buyer's
|
|
433
448
|
steps with `onBehalfOf` naming the buyer.
|
|
434
449
|
|
|
450
|
+
The same rule admits the other std creates whose starting party can be a
|
|
451
|
+
customer: `financing.limits` and `financing.installments` (borrower),
|
|
452
|
+
`insurance.cover` (holder), `wallet.balance` (holder) and `savings.membership`
|
|
453
|
+
(member). Several customers can each start one on the same record. A plan,
|
|
454
|
+
commitment or other reference resolves the sibling that binds the same
|
|
455
|
+
accounts to the parties both share, so each customer's plan finds its own
|
|
456
|
+
escrow and each investor's commitment finds their own wallet.
|
|
457
|
+
|
|
435
458
|
`financing.limits` and `financing.portfolio_limit` declare `scope: product`.
|
|
436
459
|
Create and approve the portfolio limit once per Product and the borrower limit
|
|
437
460
|
once per borrower. Plans on later records resolve those shared references.
|
|
@@ -443,8 +466,10 @@ The account primitive creates it automatically and shares it across the Product.
|
|
|
443
466
|
Zero-tax escrow releases need no additional party setup.
|
|
444
467
|
|
|
445
468
|
The financing, collections and lending samples expose payment creation, payment,
|
|
446
|
-
refund and payoff quotes through the object's existing action routes.
|
|
447
|
-
|
|
469
|
+
refund and payoff quotes through the object's existing action routes. Record
|
|
470
|
+
actions are private: the parent still runs them, but a caller sees one only
|
|
471
|
+
after the program exposes it. A child alias names the full attachment path, for
|
|
472
|
+
example:
|
|
448
473
|
|
|
449
474
|
```hsx
|
|
450
475
|
expose enrolment.tuition.payment.create as create_payment
|
|
@@ -466,9 +491,10 @@ allocation order and payoff rebate; aliases do not change those terms.
|
|
|
466
491
|
|
|
467
492
|
The [lending sample](../examples/lending.hsx) also exposes share creation. Callers
|
|
468
493
|
still need dates, agreement inputs, funded wallets and eligible settlements.
|
|
469
|
-
|
|
470
|
-
the object
|
|
471
|
-
|
|
494
|
+
Every agreement reference is an optional creation input on an exposed create.
|
|
495
|
+
With one matching agreement on the object the engine picks it; with several,
|
|
496
|
+
such as a plan's slices or a lending round's settlements, the caller passes the
|
|
497
|
+
chosen instance ID under the reference's name.
|
|
472
498
|
Insurance needs premium-slice records and actions. Savings needs dated
|
|
473
499
|
contribution records. Read each header's creation and lifecycle requirements.
|
|
474
500
|
|
|
@@ -531,7 +557,7 @@ An attachment can declare the purpose of an imported action's money:
|
|
|
531
557
|
|
|
532
558
|
```hsx
|
|
533
559
|
attach visit = wallet.spend {
|
|
534
|
-
wallet: balance, payee: operator
|
|
560
|
+
wallet: balance, payee: operator, holder: owner
|
|
535
561
|
economics pay { purpose: earning, sourceParty: owner }
|
|
536
562
|
expose pay as charge_visit
|
|
537
563
|
}
|
package/examples/cards.hsx
CHANGED
|
@@ -18,10 +18,12 @@ object employee "Employee card" {
|
|
|
18
18
|
}
|
|
19
19
|
attach card = cards.card {
|
|
20
20
|
expose create as create_card
|
|
21
|
-
holder: holder, spend_limit: 5000 SAR
|
|
21
|
+
holder: holder, person: owner, spend_limit: 5000 SAR
|
|
22
22
|
expose activate as activate_card
|
|
23
23
|
expose freeze as freeze_card
|
|
24
24
|
expose unfreeze as unfreeze_card
|
|
25
|
+
expose issuer_freeze as block_card
|
|
26
|
+
expose issuer_unfreeze as unblock_card
|
|
25
27
|
}
|
|
26
28
|
attach payment = cards.authorization {
|
|
27
29
|
expose create as create_payment
|
package/examples/marketplace.hsx
CHANGED
|
@@ -7,14 +7,16 @@ object project "Project" {
|
|
|
7
7
|
fields { brief: text, deliverable: text, price: money }
|
|
8
8
|
columns: [brief, deliverable, price]
|
|
9
9
|
attach offer = marketplace.listing {
|
|
10
|
+
rename { price: price }
|
|
10
11
|
expose create as create_offer
|
|
11
12
|
seller: owner
|
|
12
13
|
expose publish as publish_offer
|
|
13
14
|
expose withdraw as withdraw_offer
|
|
14
15
|
}
|
|
15
16
|
attach order = marketplace.order {
|
|
17
|
+
rename { price: price }
|
|
16
18
|
expose create as create_order
|
|
17
|
-
listing: offer, buyer: actor
|
|
19
|
+
listing: offer, buyer: actor, fulfiller: owner
|
|
18
20
|
expose commit as place_order
|
|
19
21
|
expose fulfill as finish_order
|
|
20
22
|
}
|
|
@@ -45,6 +45,8 @@ object rental "Equipment rental" {
|
|
|
45
45
|
fields { equipment: text }
|
|
46
46
|
columns: [equipment, dueAt]
|
|
47
47
|
attach security = deposit {
|
|
48
|
+
// dueAt is a column, so it stays shared object metadata.
|
|
49
|
+
rename { dueAt: dueAt }
|
|
48
50
|
expose create as agree_rental
|
|
49
51
|
expose fund as pay_deposit
|
|
50
52
|
expose return_on_time as return_equipment
|
package/examples/wallet.hsx
CHANGED
|
@@ -15,7 +15,7 @@ object membership "Membership" {
|
|
|
15
15
|
}
|
|
16
16
|
attach visit = wallet.spend {
|
|
17
17
|
expose create as create_visit
|
|
18
|
-
wallet: balance, payee: operator
|
|
18
|
+
wallet: balance, payee: operator, holder: owner
|
|
19
19
|
economics pay { purpose: earning, sourceParty: owner }
|
|
20
20
|
expose reserve as reserve_visit
|
|
21
21
|
expose pay as charge_visit
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperscale0/hsx",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.134",
|
|
4
4
|
"description": "The strictly typed HSX language: compile total financial programs into canonical UDL.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"hsx",
|
|
@@ -84,8 +84,8 @@
|
|
|
84
84
|
"examples:bundle": "bun scripts/bundle-examples.ts"
|
|
85
85
|
},
|
|
86
86
|
"dependencies": {
|
|
87
|
-
"@hyperscale0/adl": "1.0.
|
|
88
|
-
"@hyperscale0/udl": "1.0.
|
|
87
|
+
"@hyperscale0/adl": "1.0.134",
|
|
88
|
+
"@hyperscale0/udl": "1.0.134",
|
|
89
89
|
"fast-sha256": "1.3.0"
|
|
90
90
|
},
|
|
91
91
|
"devDependencies": {
|
|
@@ -97,5 +97,5 @@
|
|
|
97
97
|
"Amir Ayub",
|
|
98
98
|
"Sara AlBakaawi"
|
|
99
99
|
],
|
|
100
|
-
"gitHead": "
|
|
100
|
+
"gitHead": "3449c1fe1ea4ee1ae40edc3775ab7bebf890e91b"
|
|
101
101
|
}
|
package/src/compile.ts
CHANGED
|
@@ -457,6 +457,9 @@ export function compile(
|
|
|
457
457
|
UdlSubjectRequirement,
|
|
458
458
|
{ source: string; span: Span; message: string }
|
|
459
459
|
>();
|
|
460
|
+
// Instruments that lost a std action to an empty all() parent. Their
|
|
461
|
+
// lifecycles drop the states only that action reached.
|
|
462
|
+
const trimmed = new Set<string>();
|
|
460
463
|
const used = new Set<string>();
|
|
461
464
|
for (const use of program.decls.filter((d) => d.kind === "use")) {
|
|
462
465
|
if (used.has(use.name))
|
|
@@ -551,6 +554,15 @@ export function compile(
|
|
|
551
554
|
const assignments = new Map<string, AssignmentDecl>();
|
|
552
555
|
const attachmentSubjects = new Map<string, string>();
|
|
553
556
|
const names = new Set<string>();
|
|
557
|
+
// A record's create turns public only through a program-level expose,
|
|
558
|
+
// which applies after lowering, so creation input needs the list first.
|
|
559
|
+
const exposedCreates = new Set(
|
|
560
|
+
program.decls.flatMap((decl) =>
|
|
561
|
+
decl.kind === "expose" && decl.target.endsWith(".create")
|
|
562
|
+
? [decl.target.split(".").slice(0, -1).join("_")]
|
|
563
|
+
: [],
|
|
564
|
+
),
|
|
565
|
+
);
|
|
554
566
|
for (const decl of program.decls) {
|
|
555
567
|
if (decl.kind === "expose" || decl.kind === "hide" || decl.kind === "use")
|
|
556
568
|
continue;
|
|
@@ -2832,6 +2844,15 @@ export function compile(
|
|
|
2832
2844
|
} as Omit<UdlCalculation, "target">)
|
|
2833
2845
|
: undefined;
|
|
2834
2846
|
const charges = vat ? [charge, vat] : [charge];
|
|
2847
|
+
// A payer's limit reads this: a buyer pays the charges on
|
|
2848
|
+
// top, a seller pays them out of the amount.
|
|
2849
|
+
derived("debit", "Payer debit", {
|
|
2850
|
+
op: "sum",
|
|
2851
|
+
values:
|
|
2852
|
+
paidBy === "buyer"
|
|
2853
|
+
? [transfer.amount, ...charges]
|
|
2854
|
+
: [transfer.amount],
|
|
2855
|
+
} as Omit<UdlCalculation, "target">);
|
|
2835
2856
|
const net =
|
|
2836
2857
|
paidBy === "seller"
|
|
2837
2858
|
? derived("net", "Amount after fee", {
|
|
@@ -2908,7 +2929,7 @@ export function compile(
|
|
|
2908
2929
|
}
|
|
2909
2930
|
if (actionFeeCalculations.length)
|
|
2910
2931
|
a.calculate = [...(a.calculate ?? []), ...actionFeeCalculations];
|
|
2911
|
-
if (attachmentInfo
|
|
2932
|
+
if (attachmentInfo) {
|
|
2912
2933
|
if (attachmentInfo.exposed.has(name)) {
|
|
2913
2934
|
if (automatic(a.actor))
|
|
2914
2935
|
fail(
|
|
@@ -2975,6 +2996,13 @@ export function compile(
|
|
|
2975
2996
|
message: `${id}.${name}.subject.${requirement.field.name}`,
|
|
2976
2997
|
});
|
|
2977
2998
|
}
|
|
2999
|
+
// A parent list may name a list parameter such as `[parent, on]`.
|
|
3000
|
+
if (
|
|
3001
|
+
typeof a.actor === "object" &&
|
|
3002
|
+
"parent" in a.actor &&
|
|
3003
|
+
Array.isArray(a.actor.parent)
|
|
3004
|
+
)
|
|
3005
|
+
a.actor.parent = a.actor.parent.flat();
|
|
2978
3006
|
// A parent actor over an empty all() names no instrument that could
|
|
2979
3007
|
// invoke the action. A std action then does not exist in this program;
|
|
2980
3008
|
// in user code the all() is a mistake.
|
|
@@ -2993,6 +3021,7 @@ export function compile(
|
|
|
2993
3021
|
);
|
|
2994
3022
|
}
|
|
2995
3023
|
delete lifecycle.transitions[name];
|
|
3024
|
+
trimmed.add(id);
|
|
2996
3025
|
continue;
|
|
2997
3026
|
}
|
|
2998
3027
|
inst.actions[name] = a;
|
|
@@ -3282,8 +3311,7 @@ export function compile(
|
|
|
3282
3311
|
.slice(firstAttachedInstrument)
|
|
3283
3312
|
.filter(
|
|
3284
3313
|
(instrument) =>
|
|
3285
|
-
instrument.id === instId ||
|
|
3286
|
-
instrument.actions.create?.publicAction,
|
|
3314
|
+
instrument.id === instId || exposedCreates.has(instrument.id),
|
|
3287
3315
|
)) {
|
|
3288
3316
|
if (!createdInst.actions.create) continue;
|
|
3289
3317
|
const owned = new Set(
|
|
@@ -3296,9 +3324,15 @@ export function compile(
|
|
|
3296
3324
|
}
|
|
3297
3325
|
const create = createdInst.actions.create;
|
|
3298
3326
|
for (const field of createdInst.fields) {
|
|
3327
|
+
// The engine resolves a lone candidate itself; with several (a
|
|
3328
|
+
// plan's slices) the caller names one, as late_charge does.
|
|
3329
|
+
if (field.type === "ref" && field.targetKind === "instrument") {
|
|
3330
|
+
if (!create.input.some((input) => input.name === field.name))
|
|
3331
|
+
create.input.push({ ...field, optional: true });
|
|
3332
|
+
continue;
|
|
3333
|
+
}
|
|
3299
3334
|
if (
|
|
3300
3335
|
field.type === "account" ||
|
|
3301
|
-
(field.type === "ref" && field.targetKind === "instrument") ||
|
|
3302
3336
|
(field.type === "list" &&
|
|
3303
3337
|
field.item === "ref" &&
|
|
3304
3338
|
field.targetKind === "instrument") ||
|
|
@@ -3308,8 +3342,7 @@ export function compile(
|
|
|
3308
3342
|
create.input.some((input) => input.name === field.name)
|
|
3309
3343
|
)
|
|
3310
3344
|
continue;
|
|
3311
|
-
create.subject
|
|
3312
|
-
const existing = create.subject.requirements.find(
|
|
3345
|
+
const existing = create.subject?.requirements.find(
|
|
3313
3346
|
(item) => item.field.name === field.name,
|
|
3314
3347
|
);
|
|
3315
3348
|
if (existing) {
|
|
@@ -3322,11 +3355,16 @@ export function compile(
|
|
|
3322
3355
|
);
|
|
3323
3356
|
continue;
|
|
3324
3357
|
}
|
|
3358
|
+
// A creation value belongs to this agreement. Only a rename shares
|
|
3359
|
+
// it as object metadata; a matching authored field name does not,
|
|
3360
|
+
// so two agreements never read each other's amount.
|
|
3325
3361
|
const objectField = renames.get(field.name);
|
|
3326
|
-
|
|
3327
|
-
field
|
|
3328
|
-
|
|
3329
|
-
}
|
|
3362
|
+
if (!objectField) {
|
|
3363
|
+
create.input.push({ ...field });
|
|
3364
|
+
continue;
|
|
3365
|
+
}
|
|
3366
|
+
const requirement = { field, objectField };
|
|
3367
|
+
create.subject ??= { requirements: [], adapters: [] };
|
|
3330
3368
|
create.subject.requirements.push(requirement);
|
|
3331
3369
|
requirementOrigins.set(requirement, {
|
|
3332
3370
|
source: declarationSources.get(template) ?? "program",
|
|
@@ -3345,6 +3383,20 @@ export function compile(
|
|
|
3345
3383
|
);
|
|
3346
3384
|
if (!found) {
|
|
3347
3385
|
const renameEntry = renameEntries.get(oldName) ?? entry;
|
|
3386
|
+
const record = document.instruments
|
|
3387
|
+
.slice(firstAttachedInstrument)
|
|
3388
|
+
.find(
|
|
3389
|
+
(instrument) =>
|
|
3390
|
+
instrument.id.startsWith(`${instId}_`) &&
|
|
3391
|
+
instrument.fields.some((field) => field.name === oldName),
|
|
3392
|
+
);
|
|
3393
|
+
if (record)
|
|
3394
|
+
failWithCode(
|
|
3395
|
+
renameEntry,
|
|
3396
|
+
"subject_field_unknown",
|
|
3397
|
+
`rename source '${oldName}' belongs to the ${record.id.slice(instId.length + 1)} record of ${targetTemplate}; rename binds only fields of ${targetTemplate} itself`,
|
|
3398
|
+
`remove this rename; supply ${oldName} as input when the record is created, or set it as a tunable where the header offers one`,
|
|
3399
|
+
);
|
|
3348
3400
|
const declared = attachedInst
|
|
3349
3401
|
? Object.values(attachedInst.actions).flatMap(
|
|
3350
3402
|
(action) =>
|
|
@@ -3889,9 +3941,21 @@ export function compile(
|
|
|
3889
3941
|
targets.some((target) => kin(target, candidate)),
|
|
3890
3942
|
);
|
|
3891
3943
|
});
|
|
3892
|
-
// A count over no matchable member is zero.
|
|
3944
|
+
// A count over no matchable member is zero, in a requirement or a calculation.
|
|
3893
3945
|
for (const inst of document.instruments)
|
|
3894
|
-
for (const action of Object.values(inst.actions))
|
|
3946
|
+
for (const action of Object.values(inst.actions)) {
|
|
3947
|
+
if (action.calculate)
|
|
3948
|
+
action.calculate = action.calculate.map((calculation) =>
|
|
3949
|
+
calculation.op === "aggregate" &&
|
|
3950
|
+
calculation.measure === "count" &&
|
|
3951
|
+
!matchable(calculation.selection, inst, action).length
|
|
3952
|
+
? {
|
|
3953
|
+
target: calculation.target,
|
|
3954
|
+
op: "sum",
|
|
3955
|
+
values: [{ literal: 0 }],
|
|
3956
|
+
}
|
|
3957
|
+
: calculation,
|
|
3958
|
+
);
|
|
3895
3959
|
action.requires = action.requires.map((requirement) =>
|
|
3896
3960
|
requirement.kind === "aggregate" &&
|
|
3897
3961
|
requirement.measure === "count" &&
|
|
@@ -3904,6 +3968,7 @@ export function compile(
|
|
|
3904
3968
|
}
|
|
3905
3969
|
: requirement,
|
|
3906
3970
|
);
|
|
3971
|
+
}
|
|
3907
3972
|
const checkSelection = (
|
|
3908
3973
|
selection: UdlSelection,
|
|
3909
3974
|
at: Located,
|
|
@@ -4076,7 +4141,7 @@ export function compile(
|
|
|
4076
4141
|
)
|
|
4077
4142
|
)
|
|
4078
4143
|
continue;
|
|
4079
|
-
let specialized =
|
|
4144
|
+
let specialized = trimmed.has(inst.id);
|
|
4080
4145
|
for (const [key, action] of Object.entries(inst.actions)) {
|
|
4081
4146
|
const constant = (value: import("@hyperscale0/udl").UdlValue) => {
|
|
4082
4147
|
if ("literal" in value) return value.literal;
|
|
@@ -4194,6 +4259,28 @@ export function compile(
|
|
|
4194
4259
|
"give each exposed action a distinct public name",
|
|
4195
4260
|
);
|
|
4196
4261
|
}
|
|
4262
|
+
// A public action on a fresh attachment creates it first with no input
|
|
4263
|
+
// (in the engine executor), so creation that needs input must be public.
|
|
4264
|
+
for (const kind of document.objects)
|
|
4265
|
+
for (const attachment of kind.attachments) {
|
|
4266
|
+
if (attachment.parent) continue;
|
|
4267
|
+
const instrument = byId.get(attachment.instrument)!;
|
|
4268
|
+
const create = instrument.actions.create;
|
|
4269
|
+
const needed = create?.input.filter((field) => !field.optional) ?? [];
|
|
4270
|
+
const entry = Object.values(instrument.actions).find(
|
|
4271
|
+
(action) => action.publicAction,
|
|
4272
|
+
);
|
|
4273
|
+
if (!needed.length || create!.publicAction || !entry) continue;
|
|
4274
|
+
fail(
|
|
4275
|
+
objects
|
|
4276
|
+
.get(kind.id)!
|
|
4277
|
+
.body.entries.find((row) =>
|
|
4278
|
+
new RegExp(`^attach\\s+${attachment.name}\\s*=`).test(row.key),
|
|
4279
|
+
) ?? objects.get(kind.id)!,
|
|
4280
|
+
`${attachment.name} starts from ${entry.publicAction}, but its create needs ${needed.map((field) => field.name).join(", ")} and is not exposed`,
|
|
4281
|
+
`expose create on ${attachment.name} so the caller supplies ${needed.length === 1 ? "it" : "them"}, or rename { ${needed[0]!.name}: ${needed[0]!.name} } to read object metadata`,
|
|
4282
|
+
);
|
|
4283
|
+
}
|
|
4197
4284
|
for (const kind of document.objects) {
|
|
4198
4285
|
for (const name of kind.entryActions ?? []) {
|
|
4199
4286
|
const eligible = kind.attachments.some((attachment) => {
|
package/src/examples-bundle.ts
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
// Generated by open/hsx/scripts/bundle-examples.ts. Do not edit by hand.
|
|
2
2
|
import type { HsxExample } from "./examples.ts";
|
|
3
3
|
export const examples: HsxExample[] = [
|
|
4
|
-
{"id":"cards","title":"Team expense cards","summary":"A company issues expense cards to employees and reserves each supplier payment before capture.","headers":["cards"],"source":"// A company issues expense cards to employees and reserves each supplier payment before capture.\nprogram team_expense_cards \"Team expense cards\"\nuse cards\n// supplier is a business this program declares, so plan needs {\"partyBindings\":{\"supplier\":\"<partyId>\"}}.\n// That partyId is an organization customer's: create the Product from this source without the payment\n// and receipt attachments, add the customer with customer.create, activate its access, open its\n// customer_balance account, then compose this source with the binding.\nparty supplier: business\n\nobject employee \"Employee card\" {\n fields { employeeName: text, department: text }\n columns: [employeeName, department]\n attach holder = cards.cardholder {\n expose create as create_holder\n holder: owner\n expose activate as activate_employee\n expose suspend as suspend_employee\n }\n attach card = cards.card {\n expose create as create_card\n holder: holder, spend_limit: 5000 SAR\n expose activate as activate_card\n expose freeze as freeze_card\n expose unfreeze as unfreeze_card\n }\n attach payment = cards.authorization {\n expose create as create_payment\n card: card, merchant: supplier\n expose approve as approve_payment\n expose capture as capture_payment\n expose reverse as reverse_payment\n }\n attach receipt = cards.transaction {\n expose create as create_receipt\n authorization: payment\n expose refund as refund_payment\n }\n}\n"},
|
|
4
|
+
{"id":"cards","title":"Team expense cards","summary":"A company issues expense cards to employees and reserves each supplier payment before capture.","headers":["cards"],"source":"// A company issues expense cards to employees and reserves each supplier payment before capture.\nprogram team_expense_cards \"Team expense cards\"\nuse cards\n// supplier is a business this program declares, so plan needs {\"partyBindings\":{\"supplier\":\"<partyId>\"}}.\n// That partyId is an organization customer's: create the Product from this source without the payment\n// and receipt attachments, add the customer with customer.create, activate its access, open its\n// customer_balance account, then compose this source with the binding.\nparty supplier: business\n\nobject employee \"Employee card\" {\n fields { employeeName: text, department: text }\n columns: [employeeName, department]\n attach holder = cards.cardholder {\n expose create as create_holder\n holder: owner\n expose activate as activate_employee\n expose suspend as suspend_employee\n }\n attach card = cards.card {\n expose create as create_card\n holder: holder, person: owner, spend_limit: 5000 SAR\n expose activate as activate_card\n expose freeze as freeze_card\n expose unfreeze as unfreeze_card\n expose issuer_freeze as block_card\n expose issuer_unfreeze as unblock_card\n }\n attach payment = cards.authorization {\n expose create as create_payment\n card: card, merchant: supplier\n expose approve as approve_payment\n expose capture as capture_payment\n expose reverse as reverse_payment\n }\n attach receipt = cards.transaction {\n expose create as create_receipt\n authorization: payment\n expose refund as refund_payment\n }\n}\n"},
|
|
5
5
|
{"id":"collections","title":"Small business collections","summary":"A business lender refers overdue instalments to an agency with limits on collection contacts.","headers":["collections","financing"],"source":"// A business lender refers overdue instalments to an agency with limits on collection contacts.\nprogram business_collections \"Small business collections\"\nuse collections\nuse financing\n// agency is a business this program declares, so plan needs {\"partyBindings\":{\"agency\":\"<partyId>\"}}.\n// That partyId is an organization customer's: create the Product from this source without the recovery\n// and contact attachments, add the customer with customer.create, activate its access, open its\n// customer_balance account, then compose this source with the binding.\nparty agency: business\n\nobject facility \"Business loan\" {\n fields { businessName: text, purpose: text, price: money }\n columns: [businessName, purpose, price]\n attach allowance = financing.limits { expose create as create_allowance, expose approve as approve_borrower, borrower: owner, per_borrower: 100000 SAR }\n attach budget = financing.portfolio_limit { expose create as create_budget, expose approve as approve_budget, limit: 2000000 SAR }\n attach loan = financing.installments {\n expose create as create_loan\n borrower: owner, capital: operator\n months: 12, pricing: amortizing, profit_rate: 6%, disburse_to: borrower\n limits: allowance, portfolio: budget\n expose sign as sign_loan\n expose disburse as release_loan\n expose payoff as repay_early\n }\n attach recovery = collections.case {\n expose create as create_recovery\n on: loan, agency: agency\n expose assign as assign_case\n expose recover as recover_payment\n expose recall as recall_case\n }\n attach contact = collections.contact {\n expose create as create_contact\n case: recovery, agency: agency\n }\n}\n\nexpose facility.loan.payment.create as create_payment\nexpose facility.loan.payment.pay as pay_instalment\nexpose facility.loan.payment.refund as refund_payment\nexpose facility.loan.payoff_quote.create as request_payoff_quote\n"},
|
|
6
6
|
{"id":"escrow","title":"Used device sales","summary":"Buyers fund a used device purchase, then accept delivery or raise a dispute before release.","headers":["escrow"],"source":"// Buyers fund a used device purchase, then accept delivery or raise a dispute before release.\nprogram used_device_sales \"Used device sales\"\nuse escrow\n\nobject device \"Device\" {\n fields {\n model: text\n condition: text\n serialNumber: text\n price: money\n }\n columns: [model, condition, price]\n attach sale = escrow.hold {\n expose create as create_sale\n payer: actor, payee: owner\n expose fund as pay_for_device\n expose deliver as confirm_delivery\n expose accept as accept_device\n expose dispute as report_problem\n expose verify_return as verify_return\n expose refund as refund_buyer\n expose cancel as cancel_sale\n }\n}\n"},
|
|
7
7
|
{"id":"financing","title":"Tuition instalments","summary":"A tuition lender funds a student's fees over three interest-free instalments with borrowing limits.","headers":["financing"],"source":"// A tuition lender funds a student's fees over three interest-free instalments with borrowing limits.\nprogram tuition_instalments \"Tuition instalments\"\nuse financing\n\nobject enrolment \"Enrolment\" {\n fields { student: text, course: text, price: money }\n columns: [student, course, price]\n attach allowance = financing.limits {\n expose create as create_allowance\n borrower: owner, per_borrower: 12000 SAR\n expose approve as approve_student\n }\n attach budget = financing.portfolio_limit {\n expose create as create_budget\n limit: 500000 SAR\n expose approve as approve_budget\n }\n attach tuition = financing.installments {\n expose create as create_tuition\n borrower: owner, capital: operator\n months: 3, pricing: flat_total, profit_rate: 0%, disburse_to: borrower\n limits: allowance, portfolio: budget\n expose sign as sign_tuition_plan\n expose disburse as fund_tuition\n expose payoff as repay_early\n }\n}\n\nexpose enrolment.tuition.payment.create as create_payment\nexpose enrolment.tuition.payment.pay as pay_instalment\nexpose enrolment.tuition.payment.refund as refund_payment\nexpose enrolment.tuition.payoff_quote.create as request_payoff_quote\n"},
|
|
8
8
|
{"id":"insurance","title":"Device insurance","summary":"A device retailer collects the purchase price and attaches cover with a claim review process.","headers":["insurance","money"],"source":"// A device retailer collects the purchase price and attaches cover with a claim review process.\nprogram device_insurance \"Device insurance\"\nuse insurance\nuse money\n\nobject device \"Insured device\" {\n fields { model: text, serialNumber: text, purchaseDate: date }\n columns: [model, serialNumber, purchaseDate]\n attach purchase = money.transfer {\n expose create as create_purchase\n payer: owner, payee: operator\n economics pay { purpose: earning, sourceParty: owner }\n expose pay as pay_for_device\n }\n // conformance_boundary is the sandbox insurer adapter; swap in the insurer's registered adapter to go live.\n // Premiums net of commission go to its account. The default broker is the retailer, which keeps the commission.\n // claim_limit is the sum insured, set per cover at creation.\n attach protection = insurance.cover {\n expose create as create_protection\n holder: owner, adapter: \"conformance_boundary\", commission: 10%, covers: purchase\n expose activate as activate_cover\n expose cancel as cancel_cover\n }\n attach claim = insurance.claim {\n expose create as create_claim\n cover: protection, inspector: operator\n expose approve as approve_claim\n expose deny as deny_claim\n expose pay as pay_claim\n }\n}\n"},
|
|
9
9
|
{"id":"lending","title":"Community business lending","summary":"A lending composition with limit approvals, wallet commitments and cash distribution transitions.","headers":["lending","financing","wallet"],"source":"// A lending composition with limit approvals, wallet commitments and cash distribution transitions.\nprogram community_business_lending \"Community business lending\"\nuse lending\nuse financing\nuse wallet\n\nobject business \"Business funding\" {\n fields { businessName: text, purpose: text, price: money }\n columns: [businessName, purpose, price]\n attach allowance = financing.limits {\n expose create as create_allowance\n borrower: owner, per_borrower: 100000 SAR\n expose approve as approve_borrower\n }\n attach budget = financing.portfolio_limit {\n expose create as create_budget\n limit: 2000000 SAR\n expose approve as approve_budget\n }\n attach loan = financing.installments {\n expose create as create_loan\n borrower: owner, capital: operator\n months: 12, pricing: amortizing, profit_rate: 6%, disburse_to: borrower\n limits: allowance, portfolio: budget\n expose sign as sign_loan\n expose disburse as release_loan\n expose payoff as repay_early\n }\n attach investor_balance = wallet.balance {\n expose create as create_investor_balance\n holder: actor\n expose activate as open_wallet\n expose topup as add_funds\n }\n attach funding = lending.round {\n borrower: owner, plan: loan\n expose create as open_funding\n expose close as close_funding\n }\n attach investment = lending.commitment {\n round: funding, wallet: investor_balance, investor: actor\n expose create as invest\n expose withdraw as withdraw_investment\n }\n attach returns = lending.distribution {\n expose create as create_returns\n round: funding, receipt: loan.settlement\n expose prepare_cash as prepare_returns\n expose distribute_cash as distribute_returns\n }\n}\n\nexpose business.loan.payment.create as create_payment\nexpose business.loan.payment.pay as pay_instalment\nexpose business.loan.payment.refund as refund_payment\nexpose business.loan.payoff_quote.create as request_payoff_quote\nexpose business.returns.share.create as create_return_share\n"},
|
|
10
10
|
{"id":"library","title":"Financed car sales","summary":"A minimal financed car sale connects escrow to a three-instalment plan and lending limits.","headers":["escrow","financing"],"source":"// A minimal financed car sale connects escrow to a three-instalment plan and lending limits.\nprogram cars \"Financed car sales\"\nuse escrow\nuse financing\nobject car \"Cars\" {\n fields { make: text, model: text, year: integer }\n attach sale = escrow.hold { expose create as create_sale, payer: actor, payee: owner, expose fund as sell }\n attach limits = financing.limits { expose create as create_limits, borrower: actor, per_borrower: 60000 SAR }\n attach ceiling = financing.portfolio_limit { expose create as create_ceiling, limit: 1500000 SAR }\n attach plan = financing.installments {\n borrower: actor, capital: operator, share: 25%\n months: 3, pricing: flat_total, profit_rate: 2.5%, funds: sale, limits: limits, portfolio: ceiling\n expose create as finance\n }\n}\n"},
|
|
11
|
-
{"id":"marketplace","title":"Freelance marketplace","summary":"Freelancers publish offers and clients place orders with funds held until work is accepted.","headers":["marketplace","escrow"],"source":"// Freelancers publish offers and clients place orders with funds held until work is accepted.\nprogram freelance_marketplace \"Freelance marketplace\"\nuse marketplace\nuse escrow\n\nobject project \"Project\" {\n fields { brief: text, deliverable: text, price: money }\n columns: [brief, deliverable, price]\n attach offer = marketplace.listing {\n expose create as create_offer\n seller: owner\n expose publish as publish_offer\n expose withdraw as withdraw_offer\n }\n attach order = marketplace.order {\n expose create as create_order\n listing: offer, buyer: actor\n expose commit as place_order\n expose fulfill as finish_order\n }\n attach payment = escrow.hold {\n expose create as create_payment\n payer: actor, payee: owner\n expose fund as fund_project\n expose deliver as submit_work\n expose accept as accept_work\n expose dispute as dispute_work\n expose verify_return as verify_return\n expose refund as refund_client\n expose cancel as cancel_payment\n }\n}\n"},
|
|
11
|
+
{"id":"marketplace","title":"Freelance marketplace","summary":"Freelancers publish offers and clients place orders with funds held until work is accepted.","headers":["marketplace","escrow"],"source":"// Freelancers publish offers and clients place orders with funds held until work is accepted.\nprogram freelance_marketplace \"Freelance marketplace\"\nuse marketplace\nuse escrow\n\nobject project \"Project\" {\n fields { brief: text, deliverable: text, price: money }\n columns: [brief, deliverable, price]\n attach offer = marketplace.listing {\n rename { price: price }\n expose create as create_offer\n seller: owner\n expose publish as publish_offer\n expose withdraw as withdraw_offer\n }\n attach order = marketplace.order {\n rename { price: price }\n expose create as create_order\n listing: offer, buyer: actor, fulfiller: owner\n expose commit as place_order\n expose fulfill as finish_order\n }\n attach payment = escrow.hold {\n expose create as create_payment\n payer: actor, payee: owner\n expose fund as fund_project\n expose deliver as submit_work\n expose accept as accept_work\n expose dispute as dispute_work\n expose verify_return as verify_return\n expose refund as refund_client\n expose cancel as cancel_payment\n }\n}\n"},
|
|
12
12
|
{"id":"money","title":"Tutoring studio","summary":"A tutoring studio collects a fixed fee for each lesson and can cancel unpaid bookings.","headers":["money"],"source":"// A tutoring studio collects a fixed fee for each lesson and can cancel unpaid bookings.\nprogram tutoring_studio \"Tutoring studio\"\nuse money\n\nobject lesson \"Lesson\" {\n fields {\n student: text\n tutor: text\n startsAt: date\n }\n columns: [student, tutor, startsAt]\n attach payment = money.transfer {\n expose create as create_payment\n payer: actor, payee: operator, amount: 150 SAR\n economics pay { purpose: earning, sourceParty: actor }\n expose pay as pay_for_lesson\n expose cancel as cancel_payment\n }\n}\n"},
|
|
13
|
-
{"id":"rental-deposit","title":"Equipment rental","summary":"A business rents equipment with a 1000 SAR deposit and a single 50 SAR late fee. On-time return refunds 1000 SAR; late return pays the operator 50 SAR and refunds 950 SAR.","headers":[],"source":"// A business rents equipment with a 1000 SAR deposit and a single 50 SAR late fee.\n// On-time return refunds 1000 SAR; late return pays the operator 50 SAR and refunds 950 SAR.\nprogram equipment_rental \"Equipment rental\"\nparty renter: business\n\n// The renter is one declared business, so this instrument takes no parameters.\ninstrument deposit {\n summary: \"Hold a rental deposit, then return it less any fixed late fee.\"\n fields {\n renter: account of renter\n held: account of self\n amount: money = 1000 SAR\n lateFee: money = 50 SAR\n dueAt: date\n refund: money?\n }\n lifecycle { states: [pending, funded, returned, cancelled], initial: pending }\n action create {\n actor: { party: renter }\n requires self.dueAt > self.now\n }\n action fund {\n from: pending, to: funded, actor: { party: renter }\n deadline: { at: self.dueAt }\n moves self.amount from self.renter to self.held\n }\n action return_on_time {\n from: funded, to: returned, actor: { party: programOperator }\n deadline: { at: self.dueAt }\n subject { returnReference: text }\n moves self.held.balance from self.held to self.renter\n }\n action return_late {\n from: funded, to: returned, actor: { party: programOperator }\n due: { at: self.dueAt }\n subject { returnReference: text }\n calculate: [{ target: refund, op: subtract, base: { field: self.held.balance }, subtract: [{ field: self.lateFee }] }]\n moves self.lateFee from self.held to programOperator\n moves self.refund from self.held to self.renter\n }\n action cancel { from: pending, to: cancelled, actor: { party: renter } }\n}\n\nobject rental \"Equipment rental\" {\n fields { equipment: text }\n columns: [equipment, dueAt]\n attach security = deposit {\n expose create as agree_rental\n expose fund as pay_deposit\n expose return_on_time as return_equipment\n expose return_late as return_equipment_late\n expose cancel as cancel_rental\n }\n}\n\nhide deposit.create\nhide deposit.fund\nhide deposit.return_on_time\nhide deposit.return_late\nhide deposit.cancel\n"},
|
|
13
|
+
{"id":"rental-deposit","title":"Equipment rental","summary":"A business rents equipment with a 1000 SAR deposit and a single 50 SAR late fee. On-time return refunds 1000 SAR; late return pays the operator 50 SAR and refunds 950 SAR.","headers":[],"source":"// A business rents equipment with a 1000 SAR deposit and a single 50 SAR late fee.\n// On-time return refunds 1000 SAR; late return pays the operator 50 SAR and refunds 950 SAR.\nprogram equipment_rental \"Equipment rental\"\nparty renter: business\n\n// The renter is one declared business, so this instrument takes no parameters.\ninstrument deposit {\n summary: \"Hold a rental deposit, then return it less any fixed late fee.\"\n fields {\n renter: account of renter\n held: account of self\n amount: money = 1000 SAR\n lateFee: money = 50 SAR\n dueAt: date\n refund: money?\n }\n lifecycle { states: [pending, funded, returned, cancelled], initial: pending }\n action create {\n actor: { party: renter }\n requires self.dueAt > self.now\n }\n action fund {\n from: pending, to: funded, actor: { party: renter }\n deadline: { at: self.dueAt }\n moves self.amount from self.renter to self.held\n }\n action return_on_time {\n from: funded, to: returned, actor: { party: programOperator }\n deadline: { at: self.dueAt }\n subject { returnReference: text }\n moves self.held.balance from self.held to self.renter\n }\n action return_late {\n from: funded, to: returned, actor: { party: programOperator }\n due: { at: self.dueAt }\n subject { returnReference: text }\n calculate: [{ target: refund, op: subtract, base: { field: self.held.balance }, subtract: [{ field: self.lateFee }] }]\n moves self.lateFee from self.held to programOperator\n moves self.refund from self.held to self.renter\n }\n action cancel { from: pending, to: cancelled, actor: { party: renter } }\n}\n\nobject rental \"Equipment rental\" {\n fields { equipment: text }\n columns: [equipment, dueAt]\n attach security = deposit {\n // dueAt is a column, so it stays shared object metadata.\n rename { dueAt: dueAt }\n expose create as agree_rental\n expose fund as pay_deposit\n expose return_on_time as return_equipment\n expose return_late as return_equipment_late\n expose cancel as cancel_rental\n }\n}\n\nhide deposit.create\nhide deposit.fund\nhide deposit.return_on_time\nhide deposit.return_late\nhide deposit.cancel\n"},
|
|
14
14
|
{"id":"repair-approval","title":"Repair approvals","summary":"A repair shop records inspection and approval evidence before it can complete a job.","headers":[],"source":"// A repair shop records inspection and approval evidence before it can complete a job.\nprogram repair_shop \"Repair approvals\"\n\ninstrument assessment {\n summary: \"Inspect the repair, collect approval, then record completion.\"\n fields { inspectedAt: date?, approvedAt: date? }\n lifecycle { states: [submitted, inspected, approved, completed, rejected], initial: submitted }\n action create {}\n action inspect {\n from: submitted, to: inspected, actor: { party: programOperator }\n subject { inspectionNotes: text }\n set: { inspectedAt: { field: self.now } }\n }\n action approve {\n from: inspected, to: approved, actor: { party: programOperator }\n subject { approvalReference: text }\n set: { approvedAt: { field: self.now } }\n }\n action reject {\n from: inspected, to: rejected, actor: { party: programOperator }\n }\n action complete {\n from: approved, to: completed, actor: { party: programOperator }\n subject { completionNotes: text }\n }\n}\n\nobject repair \"Repair\" {\n fields { item: text, problem: text }\n columns: [item, problem]\n attach review = assessment {\n expose create as create_review\n expose inspect as inspect_repair\n expose approve as approve_repair\n expose reject as reject_repair\n expose complete as complete_repair\n }\n}\n\nhide assessment.create\nhide assessment.inspect\nhide assessment.approve\nhide assessment.reject\nhide assessment.complete\n"},
|
|
15
15
|
{"id":"reporting","title":"Equipment loan reports","summary":"An equipment lender keeps an ageing report over its financed purchases and recorded repayments.","headers":["reporting","financing"],"source":"// An equipment lender keeps an ageing report over its financed purchases and recorded repayments.\nprogram equipment_loan_reports \"Equipment loan reports\"\nuse reporting\nuse financing\n\nobject purchase \"Equipment loan\" {\n fields { businessName: text, equipment: text, price: money }\n columns: [businessName, equipment, price]\n attach allowance = financing.limits {\n expose create as create_allowance\n borrower: owner, per_borrower: 200000 SAR\n expose approve as approve_business\n }\n attach budget = financing.portfolio_limit {\n expose create as create_budget\n limit: 5000000 SAR\n expose approve as approve_budget\n }\n attach loan = financing.installments {\n expose create as create_loan\n borrower: owner, capital: operator\n months: 12, pricing: amortizing, profit_rate: 6%, disburse_to: borrower\n limits: allowance, portfolio: budget\n expose sign as sign_purchase_loan\n expose disburse as fund_purchase\n }\n attach reports = reporting.portfolio { expose create as create_reports, on: loan }\n}\n"},
|
|
16
16
|
{"id":"savings","title":"Savings circle","summary":"Six members contribute to a shared pot and receive it in their agreed calendar order.","headers":["savings"],"source":"// Six members contribute to a shared pot and receive it in their agreed calendar order.\nprogram neighbourhood_savings \"Savings circle\"\nuse savings\n\nobject group \"Savings group\" {\n fields { groupName: text, purpose: text }\n columns: [groupName, purpose]\n attach circle = savings.circle {\n expose create as create_circle\n contribution: 500 SAR, members: 6, starts: 2027-01-01\n expose disband as disband_circle\n expose close as close_circle\n }\n attach membership = savings.membership {\n expose create as create_membership\n circle: circle, member: actor\n expose activate as confirm_membership\n expose withdraw as leave_circle\n }\n // Contributions and each member's payout follow the declared calendar.\n}\n"},
|
|
17
17
|
{"id":"serviced","title":"Car financing","summary":"A car finance company services loans with late charges, payment reminders and early repayment.","headers":["escrow","financing","collections"],"source":"// A car finance company services loans with late charges, payment reminders and early repayment.\nprogram car_financing \"Car financing\"\nuse escrow\nuse financing\nuse collections\nobject car \"Car\" {\n fields { make: text, model: text, year: integer, vin: text, price: money }\n columns: [make, model, year, vin, price]\n attach sale = escrow.hold { expose create as create_sale, payer: actor, payee: operator, expose fund as purchase }\n attach limits = financing.limits { expose create as create_limits, borrower: actor, per_borrower: 250000 SAR }\n attach ceiling = financing.portfolio_limit { expose create as create_ceiling, limit: 10000000 SAR }\n attach plan = financing.installments {\n borrower: actor, capital: operator\n months: 36, pricing: flat_total, profit_rate: 8.5%, down_payment: 10%\n apply: fines_profit_principal, payoff_rebate: 100%, write_off_after: 90d\n funds: sale, limits: limits, portfolio: ceiling\n expose create as finance, expose sign as sign_contract, expose payoff as payoff\n }\n attach late = financing.late_charge {\n expose create as create_late\n on: plan, grace: 3d, fine: 50 SAR, cap: 25 SAR\n fines_to: operator, costs_to: operator, borrower: actor\n }\n attach reminders = collections.reminder { expose create as create_reminders, on: plan, operator: operator, before_days: 3, overdue_days: 1 }\n}\n"},
|
|
18
|
-
{"id":"wallet","title":"Workshop credits","summary":"Members prepay for workshop visits and the studio reserves each spend before charging it.","headers":["wallet"],"source":"// Members prepay for workshop visits and the studio reserves each spend before charging it.\nprogram workshop_credits \"Workshop credits\"\nuse wallet\n\nobject membership \"Membership\" {\n fields { memberName: text, membershipNumber: text }\n columns: [memberName, membershipNumber]\n attach balance = wallet.balance {\n expose create as create_balance\n holder: owner\n expose activate as open_balance\n expose topup as add_credit\n expose withdraw as withdraw_credit\n expose close as close_balance\n }\n attach visit = wallet.spend {\n expose create as create_visit\n wallet: balance, payee: operator\n economics pay { purpose: earning, sourceParty: owner }\n expose reserve as reserve_visit\n expose pay as charge_visit\n expose cancel as cancel_visit\n }\n}\n"},
|
|
18
|
+
{"id":"wallet","title":"Workshop credits","summary":"Members prepay for workshop visits and the studio reserves each spend before charging it.","headers":["wallet"],"source":"// Members prepay for workshop visits and the studio reserves each spend before charging it.\nprogram workshop_credits \"Workshop credits\"\nuse wallet\n\nobject membership \"Membership\" {\n fields { memberName: text, membershipNumber: text }\n columns: [memberName, membershipNumber]\n attach balance = wallet.balance {\n expose create as create_balance\n holder: owner\n expose activate as open_balance\n expose topup as add_credit\n expose withdraw as withdraw_credit\n expose close as close_balance\n }\n attach visit = wallet.spend {\n expose create as create_visit\n wallet: balance, payee: operator, holder: owner\n economics pay { purpose: earning, sourceParty: owner }\n expose reserve as reserve_visit\n expose pay as charge_visit\n expose cancel as cancel_visit\n }\n}\n"},
|
|
19
19
|
];
|