@capxul/sdk-react 4.20.0-beta.4 → 4.20.0-beta.6

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/index.mjs CHANGED
@@ -1,8 +1,8 @@
1
1
  "use client";
2
- import { _ as useCapxul, a as useCapxulAuth, c as useCapxulIdentityOrNull, d as toInvocationControls, f as toInvocationOptions, g as useCapxulClientOrNull, h as capxulKeys, i as entered, l as useCapxulSend, m as actingAccount, n as CapxulOnboardingController, o as useCapxulDestination, p as accountKey, r as CapxulProvider, s as useCapxulIdentity, t as CapxulAuthenticationController, u as useCapxulTransitions } from "./controllers-DXmEHrnw.mjs";
2
+ import { _ as useCapxul, a as useCapxulAuth, c as useCapxulIdentityOrNull, d as toInvocationControls, f as toInvocationOptions, g as useCapxulClientOrNull, h as capxulKeys, i as entered, l as useCapxulSend, m as actingAccount, n as CapxulOnboardingController, o as useCapxulDestination, p as accountKey, r as CapxulProvider, s as useCapxulIdentity, t as CapxulAuthenticationController, u as useCapxulTransitions } from "./controllers-L1UqwWS1.mjs";
3
3
  import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
4
4
  import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
5
- import { CAPXUL_OPERATIONS, CapxulError, EVM_ADDRESS_RE, Errors, HANDLE_RE, PAYMENT_DIRECTIONS, PAYMENT_STATUSES, fingerprintPaymentIntent, formatMoney, isCapxulError, isClaimed, isClaimed as isClaimed$1, isMoneyParseError, isRestoring, isRestoring as isRestoring$1, parseMoney, paymentPhase, resolveIdentityDestination, toEvmAddress, toPartyId } from "@capxul/sdk";
5
+ import { CAPXUL_OPERATIONS, CapxulError, EVM_ADDRESS_RE, Errors, HANDLE_RE, PAYMENT_DIRECTIONS, PAYMENT_STATUSES, assetSymbolFor, fingerprintPaymentIntent, formatAssetAmount, formatMoney, isCapxulError, isClaimed, isClaimed as isClaimed$1, isMoneyParseError, isRestoring, isRestoring as isRestoring$1, parseAssetAmount, parseMoney, paymentPhase, requireAsset, resolveIdentityDestination, toEvmAddress, toPartyId, toRaw, valueActivitySummary } from "@capxul/sdk";
6
6
  import { Fragment, jsx } from "react/jsx-runtime";
7
7
  //#region src/internal/require-bootstrapped-client.ts
8
8
  /**
@@ -1079,7 +1079,7 @@ function useContactsContext() {
1079
1079
  if (engine === null) throw new Error("CapxulContacts parts must be used inside <CapxulContacts>");
1080
1080
  return engine;
1081
1081
  }
1082
- function Root$6({ actor, onAdded, onFailed, children }) {
1082
+ function Root$7({ actor, onAdded, onFailed, children }) {
1083
1083
  const engine = useContacts({
1084
1084
  actor,
1085
1085
  onAdded,
@@ -1103,7 +1103,7 @@ function Add({ children }) {
1103
1103
  return /* @__PURE__ */ jsx(Fragment, { children: children(useContactsContext().add) });
1104
1104
  }
1105
1105
  /** The compound shape the app's own design system already uses (`StatusTabs`). */
1106
- const CapxulContacts = Object.assign(Root$6, {
1106
+ const CapxulContacts = Object.assign(Root$7, {
1107
1107
  Summary: Summary$3,
1108
1108
  List,
1109
1109
  Add
@@ -1141,15 +1141,8 @@ function useActivityEnabled() {
1141
1141
  }
1142
1142
  //#endregion
1143
1143
  //#region src/headless/activity/detail.ts
1144
- /**
1145
- * ONE finished document, never the array, so no consumer writes `documents[0]`:
1146
- * the payslip, else the receipt, else nothing. Those two kinds are the ones the
1147
- * Download receipt button is for. A Movement detail carries no documents.
1148
- */
1149
- function pickReceiptDocument(detail) {
1150
- if (detail === null || detail.kind !== "payment") return null;
1151
- const documents = detail.payment.documents;
1152
- return documents.find((document) => document.kind === "payslip") ?? documents.find((document) => document.kind === "receipt") ?? null;
1144
+ function detailDocuments(detail) {
1145
+ return detail === null ? [] : detail.kind === "payment" ? detail.payment.documents : detail.documents;
1153
1146
  }
1154
1147
  /**
1155
1148
  * The root actor and the whole reference are included. A payment reference can
@@ -1184,27 +1177,76 @@ function useActivityDetail(reference, actor) {
1184
1177
  const enabled = useActivityEnabled() && reference !== void 0;
1185
1178
  const detail = useQuery({
1186
1179
  queryKey: accountKey(capxulKeys.activityDetail(reference, actor), identity),
1187
- queryFn: async () => {
1180
+ queryFn: async ({ signal }) => {
1188
1181
  if (reference === void 0) throw Errors.invalidInput("reference", "required for activity.get");
1189
- return unwrapCapxulResult(await requireBootstrappedClient(client, CAPXUL_OPERATIONS.activity.get).activity.get(reference, actor === void 0 ? void 0 : { actor }));
1182
+ return unwrapCapxulResult(await requireBootstrappedClient(client, CAPXUL_OPERATIONS.activity.get).activity.get(reference, {
1183
+ ...actor === void 0 ? {} : { actor },
1184
+ signal
1185
+ }));
1190
1186
  },
1191
1187
  enabled
1192
1188
  });
1193
- const receiptDocument = pickReceiptDocument(detail.data ?? null);
1189
+ const documents = detailDocuments(detail.data ?? null);
1190
+ const [selected, setSelected] = useState(null);
1191
+ const selectedDocument = (selected === null ? void 0 : documents.find((doc) => doc.documentHash === selected.documentHash && doc.contentHash === selected.contentHash)) ?? documents.find((doc) => doc.role === "instruction") ?? documents.find((doc) => doc.role === "movement") ?? documents[0] ?? null;
1192
+ const pending = useRef(/* @__PURE__ */ new Set());
1193
+ const mounted = useRef(true);
1194
+ useEffect(() => {
1195
+ mounted.current = true;
1196
+ const reads = pending.current;
1197
+ return () => {
1198
+ mounted.current = false;
1199
+ for (const read of reads) read.abort();
1200
+ reads.clear();
1201
+ };
1202
+ }, []);
1203
+ const selectDocument = useCallback((ref) => {
1204
+ if (!documents.some((doc) => doc.documentHash === ref.documentHash && doc.contentHash === ref.contentHash)) throw Errors.invalidInput("documentHash", "document not found");
1205
+ for (const read of pending.current) read.abort();
1206
+ pending.current.clear();
1207
+ setIsOpeningDocument(false);
1208
+ setSelected({
1209
+ documentHash: ref.documentHash,
1210
+ contentHash: ref.contentHash
1211
+ });
1212
+ }, [documents]);
1194
1213
  const [isOpeningDocument, setIsOpeningDocument] = useState(false);
1195
- const openReceiptDocument = useCallback(async () => {
1196
- if (reference === void 0 || receiptDocument === null) throw Errors.invalidInput("receiptDocument", "no renderable document on this row");
1214
+ const readDocument = useCallback(async (download) => {
1215
+ const operation = download ? CAPXUL_OPERATIONS.paymentDocuments.get : CAPXUL_OPERATIONS.paymentDocuments.render;
1216
+ if (!mounted.current) throw Errors.cancelled({ operation });
1217
+ if (reference === void 0 || selectedDocument === null) throw Errors.invalidInput("documentHash", "no document on this row");
1218
+ const controller = new AbortController();
1219
+ pending.current.add(controller);
1197
1220
  setIsOpeningDocument(true);
1198
1221
  try {
1199
- return unwrapCapxulResult(await requireBootstrappedClient(client, CAPXUL_OPERATIONS.paymentDocuments.render).paymentDocuments.render(receiptDocument.documentHash));
1222
+ const bootstrapped = requireBootstrappedClient(client, download ? CAPXUL_OPERATIONS.paymentDocuments.get : CAPXUL_OPERATIONS.paymentDocuments.render);
1223
+ const pair = {
1224
+ documentHash: selectedDocument.documentHash,
1225
+ contentHash: selectedDocument.contentHash
1226
+ };
1227
+ const options = { signal: controller.signal };
1228
+ const result = download ? await bootstrapped.paymentDocuments.get(pair, options) : await bootstrapped.paymentDocuments.render(pair, options);
1229
+ if (controller.signal.aborted) throw Errors.cancelled({ operation });
1230
+ return unwrapCapxulResult(result);
1200
1231
  } finally {
1201
- setIsOpeningDocument(false);
1232
+ pending.current.delete(controller);
1233
+ if (!controller.signal.aborted) setIsOpeningDocument(pending.current.size > 0);
1202
1234
  }
1203
1235
  }, [
1204
1236
  client,
1205
- receiptDocument,
1237
+ selectedDocument,
1206
1238
  reference
1207
1239
  ]);
1240
+ const renderDocument = useCallback(async () => {
1241
+ const result = await readDocument(false);
1242
+ if (!("html" in result)) throw Errors.invalidInput("documentHash", "invalid document render");
1243
+ return result;
1244
+ }, [readDocument]);
1245
+ const downloadDocument = useCallback(async () => {
1246
+ const result = await readDocument(true);
1247
+ if (!("bytesBase64" in result)) throw Errors.invalidInput("documentHash", "invalid document download");
1248
+ return result;
1249
+ }, [readDocument]);
1208
1250
  const [typed, setTyped] = useState(null);
1209
1251
  const annotate = useCapxulAnnotateMovement();
1210
1252
  const saveMemo = useCallback(async () => {
@@ -1233,8 +1275,11 @@ function useActivityDetail(reference, actor) {
1233
1275
  status: detail.data.payment.status,
1234
1276
  ...detail.data.payment.execution === void 0 ? {} : { execution: detail.data.payment.execution }
1235
1277
  } } : {},
1236
- receiptDocument,
1237
- openReceiptDocument,
1278
+ documents,
1279
+ selectedDocument,
1280
+ selectDocument,
1281
+ renderDocument,
1282
+ downloadDocument,
1238
1283
  isOpeningDocument,
1239
1284
  memo: typed ?? detail.data?.annotation?.memo ?? "",
1240
1285
  setMemo: setTyped,
@@ -1247,6 +1292,7 @@ function useActivityDetail(reference, actor) {
1247
1292
  //#endregion
1248
1293
  //#region src/headless/activity/summary.ts
1249
1294
  const DAY_MS = 864e5;
1295
+ const display = (money) => money === null ? null : formatMoney(money);
1250
1296
  /**
1251
1297
  * `"last30days"` is the last 30 whole UTC days including today. Quantizing to
1252
1298
  * the UTC day keeps the cache key stable for a whole day instead of changing on
@@ -1257,57 +1303,6 @@ function resolveWindow(window, now) {
1257
1303
  if (window === "last30days") return { from: Math.floor(now / DAY_MS) * DAY_MS - 29 * DAY_MS };
1258
1304
  return window;
1259
1305
  }
1260
- /** An integer raw-unit string lifted into a major-unit decimal string. */
1261
- function majorUnits(raw, decimals) {
1262
- const negative = raw.startsWith("-");
1263
- const digits = negative ? raw.slice(1) : raw;
1264
- const sign = negative ? "-" : "";
1265
- if (decimals === 0) return `${sign}${digits}`;
1266
- const padded = digits.padStart(decimals + 1, "0");
1267
- const cut = padded.length - decimals;
1268
- return `${sign}${padded.slice(0, cut)}.${padded.slice(cut)}`;
1269
- }
1270
- function absoluteNet(total) {
1271
- const net = BigInt(total.net);
1272
- return net < 0n ? -net : net;
1273
- }
1274
- /**
1275
- * Buckets carry different `decimals`, so their raw integers are not comparable.
1276
- * Both are scaled to the wider precision before the magnitudes are ordered.
1277
- */
1278
- function compareNet(left, right) {
1279
- const width = Math.max(left.decimals, right.decimals);
1280
- const scaledLeft = absoluteNet(left) * 10n ** BigInt(width - left.decimals);
1281
- const scaledRight = absoluteNet(right) * 10n ** BigInt(width - right.decimals);
1282
- if (scaledLeft === scaledRight) return 0;
1283
- return scaledLeft < scaledRight ? -1 : 1;
1284
- }
1285
- /**
1286
- * Which currency `.Summary` shows: the bucket matching the primary holding's
1287
- * currency, else the bucket with the largest absolute `net`, breaking a tie on
1288
- * currency code ascending so the choice is stable across reloads.
1289
- *
1290
- * The primary holding is matched on `available.currency` — the consumer
1291
- * `CurrencyCode` — and NOT on `Holding.symbol`, which is the raw token ticker
1292
- * ("USDX") and can never equal a bucket's currency ("USD"). See the contract
1293
- * correction on #1449.
1294
- */
1295
- function pickBucket(totals, primaryCurrency) {
1296
- if (primaryCurrency !== null) {
1297
- const matched = totals.find((total) => total.currency === primaryCurrency);
1298
- if (matched !== void 0) return matched;
1299
- }
1300
- let best = null;
1301
- for (const total of totals) {
1302
- if (best === null) {
1303
- best = total;
1304
- continue;
1305
- }
1306
- const order = compareNet(total, best);
1307
- if (order > 0 || order === 0 && total.currency < best.currency) best = total;
1308
- }
1309
- return best;
1310
- }
1311
1306
  /**
1312
1307
  * The current UTC day number, which advances ON ITS OWN at UTC midnight.
1313
1308
  *
@@ -1351,47 +1346,36 @@ function useActivitySummary(actor, window) {
1351
1346
  const params = {};
1352
1347
  if (actor !== void 0) params.actor = actor;
1353
1348
  if (range !== void 0) params.window = range;
1354
- return unwrapCapxulResult(await requireBootstrappedClient(client, CAPXUL_OPERATIONS.activity.summary).activity.summary(params));
1355
- },
1356
- enabled
1357
- });
1358
- const primary = useQuery({
1359
- queryKey: accountKey([...capxulKeys.holdings(actor), "primary"], identity),
1360
- queryFn: async () => {
1361
- return unwrapCapxulResult(await requireBootstrappedClient(client, CAPXUL_OPERATIONS.holdings.primary).holdings.primary(actor === void 0 ? void 0 : { actor }));
1349
+ const result = unwrapCapxulResult(await requireBootstrappedClient(client, CAPXUL_OPERATIONS.activity.summary).activity.summary(params));
1350
+ return {
1351
+ ...result,
1352
+ valuation: valueActivitySummary(result.totals)
1353
+ };
1362
1354
  },
1363
1355
  enabled
1364
1356
  });
1365
1357
  const totals = summary.data?.totals;
1366
- const primaryCurrency = primary.data?.available.currency ?? null;
1367
- const isLoading = summary.isLoading || primary.isLoading;
1368
- const bucket = useMemo(() => totals === void 0 || isLoading ? null : pickBucket(totals, primaryCurrency), [
1369
- isLoading,
1370
- primaryCurrency,
1371
- totals
1372
- ]);
1373
- if (bucket === null) return {
1358
+ const isLoading = summary.isLoading;
1359
+ if (summary.data === void 0 || totals === void 0 || totals.length === 0 || isLoading) return {
1374
1360
  currency: null,
1375
1361
  moneyIn: null,
1376
1362
  moneyOut: null,
1377
1363
  net: null,
1378
1364
  pending: null,
1365
+ unrated: [],
1379
1366
  count: summary.data?.count ?? 0,
1380
1367
  isLoading,
1381
1368
  error: asCapxulError$1(summary.error),
1382
1369
  ...summary.data?.readWarning === void 0 ? {} : { readWarning: summary.data.readWarning }
1383
1370
  };
1384
- const display = (raw) => formatMoney({
1385
- currency: bucket.currency,
1386
- value: majorUnits(raw, bucket.decimals),
1387
- decimals: bucket.decimals
1388
- });
1371
+ const valuation = summary.data.valuation;
1389
1372
  return {
1390
- currency: bucket.currency,
1391
- moneyIn: display(bucket.in),
1392
- moneyOut: display(bucket.out),
1393
- net: display(bucket.net),
1394
- pending: display(bucket.pendingIn),
1373
+ currency: valuation.moneyIn?.currency ?? null,
1374
+ moneyIn: display(valuation.moneyIn),
1375
+ moneyOut: display(valuation.moneyOut),
1376
+ net: display(valuation.net),
1377
+ pending: display(valuation.pending),
1378
+ unrated: valuation.unrated,
1395
1379
  count: summary.data?.count ?? 0,
1396
1380
  isLoading,
1397
1381
  error: asCapxulError$1(summary.error),
@@ -1418,7 +1402,7 @@ const SIGN = Object.freeze({
1418
1402
  self: ""
1419
1403
  });
1420
1404
  function toActivityRow(item, actor) {
1421
- const amountDisplay = formatMoney(item.amount);
1405
+ const amountDisplay = formatAssetAmount(item.amount);
1422
1406
  const signedAmountDisplay = `${SIGN[item.direction]}${amountDisplay}`;
1423
1407
  if (item.kind === "movement") return {
1424
1408
  kind: "movement",
@@ -1430,7 +1414,7 @@ function toActivityRow(item, actor) {
1430
1414
  counterpartyLabel: item.counterpartyLabel,
1431
1415
  amountDisplay,
1432
1416
  signedAmountDisplay,
1433
- currency: item.amount.currency,
1417
+ currency: assetSymbolFor(item.amount.asset),
1434
1418
  createdAt: item.createdAt,
1435
1419
  classification: item.classification,
1436
1420
  phase: MOVEMENT_PHASE,
@@ -1452,7 +1436,7 @@ function toActivityRow(item, actor) {
1452
1436
  counterpartyLabel: item.counterpartyLabel,
1453
1437
  amountDisplay,
1454
1438
  signedAmountDisplay,
1455
- currency: item.amount.currency,
1439
+ currency: assetSymbolFor(item.amount.asset),
1456
1440
  createdAt: item.createdAt,
1457
1441
  ledgerKind: item.ledgerKind,
1458
1442
  paymentType: item.paymentType,
@@ -1533,7 +1517,7 @@ function buildCsv(items, actor, search) {
1533
1517
  row.counterpartyLabel,
1534
1518
  row.direction,
1535
1519
  item.amount.value,
1536
- item.amount.currency,
1520
+ assetSymbolFor(item.amount.asset),
1537
1521
  row.kind === "payment" ? row.execution?.notSent ? "not_sent" : row.execution?.status === "unresolved" ? "verifying" : row.status : "",
1538
1522
  row.reference.id,
1539
1523
  row.receipt?.transactionHash ?? ""
@@ -1789,7 +1773,7 @@ function useActivityContext() {
1789
1773
  if (engine === null) throw new Error("CapxulActivity parts must be used inside <CapxulActivity>");
1790
1774
  return engine;
1791
1775
  }
1792
- function Root$5({ actor, pageSize, children }) {
1776
+ function Root$6({ actor, pageSize, children }) {
1793
1777
  const engine = useActivity({
1794
1778
  ...actor === void 0 ? {} : { actor },
1795
1779
  ...pageSize === void 0 ? {} : { pageSize }
@@ -1840,7 +1824,7 @@ function Export({ children }) {
1840
1824
  return /* @__PURE__ */ jsx(Fragment, { children: children(useActivityContext().csv) });
1841
1825
  }
1842
1826
  /** The compound shape the app's own design system already uses (`StatusTabs`). */
1843
- const CapxulActivity = Object.assign(Root$5, {
1827
+ const CapxulActivity = Object.assign(Root$6, {
1844
1828
  Summary: Summary$2,
1845
1829
  Filters,
1846
1830
  Search,
@@ -1881,6 +1865,26 @@ function useOrgMeReading(orgId) {
1881
1865
  ]);
1882
1866
  }
1883
1867
  //#endregion
1868
+ //#region src/internal/write-names-asset.ts
1869
+ /**
1870
+ * Identity law: "A payment names an asset or inherits it from an authorized
1871
+ * fact. Missing asset refuses." The rule was recorded in the retired
1872
+ * treasury-cut handoff (removed from the current tree by the exact-asset
1873
+ * contraction; its record survives in Git history).
1874
+ *
1875
+ * A Capxul payment writer takes `PaymentMoney` — a peg currency, a value and
1876
+ * `decimals` — and settles one fixed legacy token. No `AssetId` crosses that
1877
+ * seam, so a payment composed from a named position cannot name the asset it
1878
+ * moves: a position selected as USDT authorizes a different token.
1879
+ *
1880
+ * `false` until the writer takes the command's asset. The Send and Payroll
1881
+ * composers read this to refuse a submission rather than authorize a token the
1882
+ * person did not pick.
1883
+ */
1884
+ function writeNamesAsset() {
1885
+ return false;
1886
+ }
1887
+ //#endregion
1884
1888
  //#region src/headless/send-money/signer-status.ts
1885
1889
  const NO_SUBSCRIPTION = () => () => {};
1886
1890
  function useSignerStatus(client) {
@@ -2030,6 +2034,37 @@ function useSelectedOption(options) {
2030
2034
  }, [options]);
2031
2035
  return [options.find((option) => option.id === selectedId) ?? null, select];
2032
2036
  }
2037
+ /**
2038
+ * One Send `.Asset` option per position the Account holds, taken from
2039
+ * `account.balances` — never from `account.available`, which is the Account's
2040
+ * fiat VALUATION of those positions (ADR-0028 R5/R6). Labelling the valuation
2041
+ * was the false identity this cut removes, and it collapsed USDC+USDT into one
2042
+ * "USD" row.
2043
+ *
2044
+ * The option is keyed on the ASSET id, never the symbol and never the account:
2045
+ * several positions share one account id, and a symbol is display only
2046
+ * (ADR-0028 R3). Every row is listed, so a real multi-position Account still
2047
+ * offers its holdings.
2048
+ */
2049
+ function assetSelectionsFromAccount$1(account) {
2050
+ if (account === null) return [];
2051
+ return account.balances.map((position) => {
2052
+ return {
2053
+ option: {
2054
+ id: position.asset,
2055
+ symbol: position.symbol,
2056
+ availableDisplay: formatAssetAmount({
2057
+ asset: position.asset,
2058
+ value: position.available
2059
+ })
2060
+ },
2061
+ money: {
2062
+ asset: position.asset,
2063
+ value: position.available
2064
+ }
2065
+ };
2066
+ });
2067
+ }
2033
2068
  function useSendMoney(input) {
2034
2069
  const { actor, onSent, onFailed } = input;
2035
2070
  const client = useCapxulClientOrNull();
@@ -2042,15 +2077,12 @@ function useSendMoney(input) {
2042
2077
  const treasury = useCapxulOrgTreasury(orgId, { enabled: actor.kind === "organization" });
2043
2078
  const accountQuery = actor.kind === "personal" ? balance : treasury;
2044
2079
  const account = accountQuery.data ?? null;
2045
- const assetOptions = useMemo(() => account === null ? [] : [{
2046
- id: account.id,
2047
- symbol: account.available.currency,
2048
- availableDisplay: formatMoney(account.available, { grammar: "code" })
2049
- }], [account]);
2080
+ const assetSelections = useMemo(() => assetSelectionsFromAccount$1(account), [account]);
2081
+ const assetOptions = useMemo(() => assetSelections.map((selection) => selection.option), [assetSelections]);
2050
2082
  const [selectedAssetOption, selectAsset] = useSelectedOption(assetOptions);
2051
- const selectedAssetMoney = selectedAssetOption !== null && account !== null ? account.available : null;
2083
+ const selectedAssetMoney = useMemo(() => assetSelections.find((selection) => selection.option.id === selectedAssetOption?.id)?.money ?? null, [assetSelections, selectedAssetOption]);
2052
2084
  const [amountText, setAmountText] = useState("");
2053
- const parsed = useMemo(() => selectedAssetMoney === null || amountText.trim() === "" ? null : parseMoney(amountText, selectedAssetMoney), [amountText, selectedAssetMoney]);
2085
+ const parsed = useMemo(() => selectedAssetMoney === null || amountText.trim() === "" ? null : parseAssetAmount(amountText, requireAsset(selectedAssetMoney.asset)), [amountText, selectedAssetMoney]);
2054
2086
  const parsedMoney = parsed !== null && !isMoneyParseError(parsed) ? parsed : null;
2055
2087
  const amountError = parsed !== null && isMoneyParseError(parsed) ? parsed.reason : null;
2056
2088
  const recipient = useRecipientResolution(client, JSON.stringify([
@@ -2062,7 +2094,7 @@ function useSendMoney(input) {
2062
2094
  const personalPay = useCapxulPay();
2063
2095
  const organizationPay = useCapxulOrganizationPay(orgId);
2064
2096
  const pay = actor.kind === "personal" ? personalPay : organizationPay;
2065
- const blockedReason = signerStatus !== "ready" ? "signer-not-ready" : actor.kind === "organization" && selectedSource === null ? "no-source" : selectedAssetOption === null ? "no-asset" : parsedMoney === null ? "amount-invalid" : resolvedReference === null ? "recipient-unresolved" : null;
2097
+ const blockedReason = signerStatus !== "ready" ? "signer-not-ready" : actor.kind === "organization" && selectedSource === null ? "no-source" : selectedAssetOption === null ? "no-asset" : parsedMoney === null ? "amount-invalid" : resolvedReference === null ? "recipient-unresolved" : writeNamesAsset() ? null : "asset-unnamed";
2066
2098
  const submitGate = useRef(false);
2067
2099
  const submit = useCallback(() => {
2068
2100
  if (blockedReason !== null || submitGate.current) return;
@@ -2149,7 +2181,7 @@ function useSendMoneyContext() {
2149
2181
  if (engine === null) throw new Error("CapxulSendMoney parts must be used inside <CapxulSendMoney>");
2150
2182
  return engine;
2151
2183
  }
2152
- function Root$4({ actor, onSent, onFailed, children }) {
2184
+ function Root$5({ actor, onSent, onFailed, children }) {
2153
2185
  const engine = useSendMoney({
2154
2186
  actor,
2155
2187
  onSent,
@@ -2176,7 +2208,7 @@ function Actions$1({ children }) {
2176
2208
  return /* @__PURE__ */ jsx(Fragment, { children: children(useSendMoneyContext().actions) });
2177
2209
  }
2178
2210
  /** The compound shape the app's own design system already uses (`StatusTabs`). */
2179
- const CapxulSendMoney = Object.assign(Root$4, {
2211
+ const CapxulSendMoney = Object.assign(Root$5, {
2180
2212
  Source,
2181
2213
  Asset: Asset$1,
2182
2214
  Amount,
@@ -2266,7 +2298,7 @@ function useOrgMemberContext() {
2266
2298
  if (state === null) throw new Error("CapxulOrgMember parts must be used inside <CapxulOrgMember>");
2267
2299
  return state;
2268
2300
  }
2269
- function Root$3({ orgId, children }) {
2301
+ function Root$4({ orgId, children }) {
2270
2302
  const state = useOrgMember(orgId);
2271
2303
  return /* @__PURE__ */ jsx(OrgMemberContext.Provider, {
2272
2304
  value: state,
@@ -2286,7 +2318,7 @@ function Standing({ children }) {
2286
2318
  return /* @__PURE__ */ jsx(Fragment, { children: children(useOrgMemberContext().standing) });
2287
2319
  }
2288
2320
  /** The compound shape the app's own design system already uses (`StatusTabs`). */
2289
- const CapxulOrgMember = Object.assign(Root$3, {
2321
+ const CapxulOrgMember = Object.assign(Root$4, {
2290
2322
  Can,
2291
2323
  Standing
2292
2324
  });
@@ -2446,7 +2478,7 @@ function usePart(phase) {
2446
2478
  }, [placed, phase]);
2447
2479
  return state.phase === phase ? state : null;
2448
2480
  }
2449
- function Root$2({ scope, children }) {
2481
+ function Root$3({ scope, children }) {
2450
2482
  const state = useDashboardAccess(scope);
2451
2483
  const [placed] = useState(() => /* @__PURE__ */ new Set());
2452
2484
  const value = useMemo(() => ({
@@ -2464,7 +2496,7 @@ function Root$2({ scope, children }) {
2464
2496
  });
2465
2497
  }
2466
2498
  /** Hands nothing — today's app renders `null` here, exactly as it does now. */
2467
- function Checking({ children }) {
2499
+ function Checking$1({ children }) {
2468
2500
  return usePart("checking") === null ? null : /* @__PURE__ */ jsx(Fragment, { children });
2469
2501
  }
2470
2502
  function DashboardError({ children }) {
@@ -2491,19 +2523,14 @@ function Granted({ children }) {
2491
2523
  return usePart("granted") === null ? null : /* @__PURE__ */ jsx(Fragment, { children });
2492
2524
  }
2493
2525
  /** The compound shape the app's own design system already uses (`StatusTabs`). */
2494
- const CapxulDashboardAccess = Object.assign(Root$2, {
2495
- Checking,
2526
+ const CapxulDashboardAccess = Object.assign(Root$3, {
2527
+ Checking: Checking$1,
2496
2528
  Error: DashboardError,
2497
2529
  Redirect,
2498
2530
  Granted
2499
2531
  });
2500
2532
  //#endregion
2501
2533
  //#region src/headless/payroll/money.ts
2502
- /** `{ value: "12.34", decimals: 6 }` → `12340000n`. */
2503
- function toMinorUnits(money) {
2504
- const [whole = "0", fraction = ""] = money.value.split(".");
2505
- return BigInt(`${whole}${fraction.padEnd(money.decimals, "0")}`);
2506
- }
2507
2534
  const TYPED_AMOUNT = /^(-?)(\d+)(?:\.(\d+))?$/;
2508
2535
  /**
2509
2536
  * Σ of the employer's TYPED amounts on a saved group — a DISPLAY total.
@@ -2536,6 +2563,17 @@ function fromMinorUnits(raw, asset) {
2536
2563
  decimals: asset.decimals
2537
2564
  };
2538
2565
  }
2566
+ /** Convert exact raw units to one asset quantity without a fiat denomination. */
2567
+ function assetFromMinorUnits(raw, asset) {
2568
+ const money = fromMinorUnits(raw, {
2569
+ currency: "USD",
2570
+ decimals: asset.decimals
2571
+ });
2572
+ return {
2573
+ asset: asset.asset,
2574
+ value: money.value
2575
+ };
2576
+ }
2539
2577
  //#endregion
2540
2578
  //#region src/headless/payroll/reads.ts
2541
2579
  function toRecipientOption(entry) {
@@ -2652,7 +2690,7 @@ function scopedClient(client, method, orgId) {
2652
2690
  //#endregion
2653
2691
  //#region src/headless/payroll/use-payroll.ts
2654
2692
  /** Money reads the same way everywhere on this screen: amount then code. */
2655
- const GRAMMAR$1 = { grammar: "code" };
2693
+ const GRAMMAR = { grammar: "code" };
2656
2694
  /**
2657
2695
  * One captured snapshot, or nothing — never a fabricated empty options object,
2658
2696
  * and never a DOM event a screen wired straight to `onClick`.
@@ -2668,7 +2706,7 @@ function toPayrollGroupRow(group) {
2668
2706
  name: group.name,
2669
2707
  tone: group.tone,
2670
2708
  memberCount: group.members.length,
2671
- totalDisplay: currency === void 0 ? "" : formatMoney(sumTypedAmounts(group.members.map((member) => member.amount), currency), GRAMMAR$1),
2709
+ totalDisplay: currency === void 0 ? "" : formatMoney(sumTypedAmounts(group.members.map((member) => member.amount), currency), GRAMMAR),
2672
2710
  members: group.members.map((member) => ({
2673
2711
  recipientId: member.partyId,
2674
2712
  amount: member.amount,
@@ -2758,8 +2796,8 @@ function usePayroll(orgId) {
2758
2796
  const started = orgId !== void 0;
2759
2797
  return {
2760
2798
  summary: {
2761
- walletBalanceDisplay: treasury.data === void 0 ? null : formatMoney(treasury.data.available, GRAMMAR$1),
2762
- paidThisMonthDisplay: runs.data === void 0 ? null : formatMoney(runs.data.settledThisMonth, GRAMMAR$1),
2799
+ walletBalanceDisplay: treasury.data?.available === void 0 || treasury.data.available === null ? null : formatMoney(treasury.data.available, GRAMMAR),
2800
+ paidThisMonthDisplay: runs.data === void 0 ? null : runs.data.settledThisMonth.map(formatAssetAmount).join(", "),
2763
2801
  isLoading: started && (treasury.data === void 0 && treasury.error === null || runs.data === void 0 && runs.error === null),
2764
2802
  error: treasury.error ?? runs.error
2765
2803
  },
@@ -2792,7 +2830,7 @@ function usePayrollContext() {
2792
2830
  if (engine === null) throw new Error("CapxulPayroll parts must be used inside <CapxulPayroll>");
2793
2831
  return engine;
2794
2832
  }
2795
- function Root$1({ orgId, children }) {
2833
+ function Root$2({ orgId, children }) {
2796
2834
  const engine = usePayroll(orgId);
2797
2835
  return /* @__PURE__ */ jsx(PayrollContext.Provider, {
2798
2836
  value: engine,
@@ -2810,7 +2848,7 @@ function Groups({ children }) {
2810
2848
  * There is NO `.Runs` part (ruling Q6): no shipped region has the right shape
2811
2849
  * for a run list, and a part without a production caller may not ship.
2812
2850
  */
2813
- const CapxulPayroll = Object.assign(Root$1, {
2851
+ const CapxulPayroll = Object.assign(Root$2, {
2814
2852
  Summary: Summary$1,
2815
2853
  Groups
2816
2854
  });
@@ -2820,8 +2858,14 @@ function record(value) {
2820
2858
  if (typeof value !== "object" || value === null || Array.isArray(value)) throw Errors.invalidInput("payrollDraft", "Invalid run draft");
2821
2859
  return value;
2822
2860
  }
2861
+ /**
2862
+ * A draft-scoped opaque key. `/` is in the class so `asset.id` can be the
2863
+ * canonical CAIP-19 `AssetId` (`eip155:<chainId>/erc20:<address>`): the draft
2864
+ * records WHICH asset was authorized, and a symbol is not an identity
2865
+ * (ADR-0028 R3). The field never reaches the backend — only `input` is sent.
2866
+ */
2823
2867
  function identifier(value) {
2824
- if (typeof value !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u.test(value)) throw Errors.invalidInput("payrollDraft", "Invalid draft identifier");
2868
+ if (typeof value !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}$/u.test(value)) throw Errors.invalidInput("payrollDraft", "Invalid draft identifier");
2825
2869
  return value;
2826
2870
  }
2827
2871
  function minorAmount(value) {
@@ -2837,8 +2881,9 @@ function decodeDraft(value, scope) {
2837
2881
  const items = input.items.map((raw) => {
2838
2882
  const item = record(raw);
2839
2883
  const to = record(item.to);
2884
+ const amount = record(item.amount);
2840
2885
  const partyId = toPartyId(identifier(item.partyId));
2841
- if (to.kind !== "party" || to.partyId !== partyId || item.gross !== item.net || !Array.isArray(item.adjustments) || item.adjustments.length !== 0) throw Errors.invalidInput("payrollDraft", "Invalid draft recipient");
2886
+ if (to.kind !== "party" || to.partyId !== partyId || item.gross !== item.net || typeof amount.asset !== "string" || typeof amount.value !== "string" || typeof item.currency !== "string" || typeof item.decimals !== "number" || !Number.isInteger(item.decimals) || !Array.isArray(item.adjustments) || item.adjustments.length !== 0) throw Errors.invalidInput("payrollDraft", "Invalid draft recipient");
2842
2887
  const net = minorAmount(item.net);
2843
2888
  return {
2844
2889
  to: {
@@ -2846,12 +2891,20 @@ function decodeDraft(value, scope) {
2846
2891
  partyId
2847
2892
  },
2848
2893
  partyId,
2894
+ amount: {
2895
+ asset: requireAsset(amount.asset).assetId,
2896
+ value: amount.value
2897
+ },
2849
2898
  gross: net,
2850
2899
  net,
2900
+ currency: item.currency,
2901
+ decimals: item.decimals,
2851
2902
  adjustments: []
2852
2903
  };
2853
2904
  });
2854
2905
  if (new Set(items.map((item) => item.partyId)).size !== items.length) throw Errors.invalidInput("payrollDraft", "Duplicate draft recipient");
2906
+ const exactAsset = requireAsset(identifier(asset.id));
2907
+ if (items.some((item) => item.amount.asset !== exactAsset.assetId)) throw Errors.invalidInput("payrollDraft", "Draft item asset does not match the run asset");
2855
2908
  return {
2856
2909
  version: 1,
2857
2910
  scope,
@@ -2862,6 +2915,7 @@ function decodeDraft(value, scope) {
2862
2915
  },
2863
2916
  input: {
2864
2917
  permissionId: identifier(input.permissionId),
2918
+ asset: exactAsset.assetId,
2865
2919
  period: {
2866
2920
  start: period.start,
2867
2921
  end: period.start
@@ -3065,7 +3119,6 @@ function termsSeeds(terms, asset, period) {
3065
3119
  }
3066
3120
  //#endregion
3067
3121
  //#region src/headless/payroll/use-payroll-run.ts
3068
- const GRAMMAR = { grammar: "code" };
3069
3122
  /**
3070
3123
  * The period this composer composes: an INSTANT run (ruling Q3). `authorizeRun`
3071
3124
  * stamps `start === end === runAt` at submit, and the payslip's period renders
@@ -3089,6 +3142,32 @@ const INSTANT_RUN_PERIOD = {
3089
3142
  function amountText(amounts, seeds, recipientId) {
3090
3143
  return amounts[recipientId]?.value ?? seeds.get(recipientId) ?? "";
3091
3144
  }
3145
+ function freshRunItems(input) {
3146
+ const items = [];
3147
+ for (const [index, recipientId] of input.selected.entries()) {
3148
+ const to = input.payableRefs[index];
3149
+ const amount = input.amounts[index];
3150
+ const terms = input.compensationTerms.get(recipientId);
3151
+ if (to === void 0 || amount === void 0 || terms === void 0) throw Errors.unknown();
3152
+ items.push({
3153
+ to: {
3154
+ kind: "party",
3155
+ partyId: toPartyId(recipientId)
3156
+ },
3157
+ partyId: recipientId,
3158
+ amount,
3159
+ gross: terms.rate,
3160
+ net: terms.rate,
3161
+ currency: terms.currency,
3162
+ decimals: terms.decimals,
3163
+ adjustments: []
3164
+ });
3165
+ }
3166
+ return items;
3167
+ }
3168
+ function hasRequiredCompensation(frozen, writerNamesSelectedAsset, selected, terms) {
3169
+ return frozen || !writerNamesSelectedAsset || selected.every((recipientId) => terms.has(recipientId));
3170
+ }
3092
3171
  /**
3093
3172
  * WHICH run the composer is composing, by MEANING rather than by object shape.
3094
3173
  *
@@ -3120,6 +3199,33 @@ function initialAmounts(prefill) {
3120
3199
  };
3121
3200
  return seeded;
3122
3201
  }
3202
+ /**
3203
+ * One `.Asset` option per position the treasury Account holds, taken from
3204
+ * `account.balances` — never from `account.available`, which is the Account's
3205
+ * fiat VALUATION of those positions (ADR-0028 R5/R6). The option is keyed on
3206
+ * the ASSET id: several positions share one account id, and a symbol is display
3207
+ * only (ADR-0028 R3). Every row is listed, so a real multi-position treasury
3208
+ * still offers its holdings.
3209
+ */
3210
+ function assetSelectionsFromAccount(account) {
3211
+ if (account === null) return [];
3212
+ return account.balances.map((position) => {
3213
+ return {
3214
+ option: {
3215
+ id: position.asset,
3216
+ symbol: position.symbol,
3217
+ availableDisplay: formatAssetAmount({
3218
+ asset: position.asset,
3219
+ value: position.available
3220
+ })
3221
+ },
3222
+ money: {
3223
+ asset: position.asset,
3224
+ value: position.available
3225
+ }
3226
+ };
3227
+ });
3228
+ }
3123
3229
  function usePayrollRun(input) {
3124
3230
  const { orgId, prefill, onRun, onFailed } = input;
3125
3231
  const client = useCapxulClientOrNull();
@@ -3133,17 +3239,13 @@ function usePayrollRun(input) {
3133
3239
  const terms = useOrgTerms(orgId);
3134
3240
  const [selected, setSelected] = useState(() => prefill?.recipientIds ?? []);
3135
3241
  const [amounts, setAmounts] = useState(() => initialAmounts(prefill));
3136
- const restoreCurrency = treasury.data?.available.currency;
3137
3242
  const { draftScope, currentScope, draft, draftError, restorationPending, activateDraft, restoreDraft, setDraft, recovery, draftBlocked } = useRunDraft(client, orgId, useCallback((restored) => {
3138
3243
  setSelected(restored.input.items.map((item) => item.partyId));
3139
- if (restoreCurrency !== void 0 && restoreCurrency === restored.asset.currency) setAmounts(Object.fromEntries(restored.input.items.map((item) => [item.partyId, {
3140
- value: fromMinorUnits(BigInt(item.net), {
3141
- currency: restoreCurrency,
3142
- decimals: restored.asset.decimals
3143
- }).value,
3244
+ if (requireAsset(restored.input.asset).assetId === restored.input.asset) setAmounts(Object.fromEntries(restored.input.items.map((item) => [item.partyId, {
3245
+ value: item.amount.value,
3144
3246
  prefilled: false
3145
3247
  }])));
3146
- }, [restoreCurrency]));
3248
+ }, []));
3147
3249
  const frozen = draft !== null;
3148
3250
  const composerKey = composerIdentity(orgId, prefill, draftScope, frozen);
3149
3251
  const [seededKey, setSeededKey] = useState(composerKey);
@@ -3183,11 +3285,8 @@ function usePayrollRun(input) {
3183
3285
  draftError
3184
3286
  ]);
3185
3287
  const account = treasury.data ?? null;
3186
- const assetOptions = useMemo(() => account === null ? [] : [{
3187
- id: account.id,
3188
- symbol: account.available.currency,
3189
- availableDisplay: formatMoney(account.available, GRAMMAR)
3190
- }], [account]);
3288
+ const assetSelections = useMemo(() => assetSelectionsFromAccount(account), [account]);
3289
+ const assetOptions = useMemo(() => assetSelections.map((selection) => selection.option), [assetSelections]);
3191
3290
  const [selectedAssetId, setSelectedAssetId] = useState(null);
3192
3291
  useEffect(() => {
3193
3292
  if (!assetOptions.some((option) => option.id === selectedAssetId)) setSelectedAssetId(assetOptions[0]?.id ?? null);
@@ -3196,7 +3295,7 @@ function usePayrollRun(input) {
3196
3295
  const selectAsset = useCallback((id) => {
3197
3296
  if (!submitGate.current && !frozen) setSelectedAssetId((current) => assetOptions.some((option) => option.id === id) ? id : current);
3198
3297
  }, [assetOptions, frozen]);
3199
- const assetMoney = selectedAsset !== null && account !== null ? account.available : null;
3298
+ const assetMoney = assetSelections.find((selection) => selection.option.id === selectedAsset?.id)?.money ?? null;
3200
3299
  const named = useMemo(() => new Map(roster.options.map((option) => [option.id, option])), [roster.options]);
3201
3300
  const refs = roster.refs;
3202
3301
  const payableRefs = useMemo(() => {
@@ -3208,12 +3307,19 @@ function usePayrollRun(input) {
3208
3307
  }
3209
3308
  return resolved;
3210
3309
  }, [refs, selected]);
3211
- const seeds = useMemo(() => termsSeeds(terms.data ?? [], assetMoney, INSTANT_RUN_PERIOD), [assetMoney, terms.data]);
3310
+ const seeds = useMemo(() => {
3311
+ if (assetMoney === null) return /* @__PURE__ */ new Map();
3312
+ const metadata = requireAsset(assetMoney.asset);
3313
+ return termsSeeds(terms.data ?? [], metadata.peg === null ? null : {
3314
+ currency: metadata.peg,
3315
+ decimals: metadata.decimals
3316
+ }, INSTANT_RUN_PERIOD);
3317
+ }, [assetMoney, terms.data]);
3212
3318
  const entries = useMemo(() => {
3213
3319
  if (assetMoney === null) return [];
3214
3320
  return selected.map((recipientId) => {
3215
3321
  const value = amountText(amounts, seeds, recipientId);
3216
- const parsed = value.trim() === "" ? null : parseMoney(value, assetMoney);
3322
+ const parsed = value.trim() === "" ? null : parseAssetAmount(value, requireAsset(assetMoney.asset));
3217
3323
  return {
3218
3324
  recipientId,
3219
3325
  name: named.get(recipientId)?.name ?? recipientId,
@@ -3221,7 +3327,7 @@ function usePayrollRun(input) {
3221
3327
  change: (text) => change(recipientId, text),
3222
3328
  error: parsed !== null && isMoneyParseError(parsed) ? parsed.reason : null,
3223
3329
  prefilled: amounts[recipientId]?.prefilled ?? seeds.has(recipientId),
3224
- currency: assetMoney.currency
3330
+ currency: requireAsset(assetMoney.asset).symbol
3225
3331
  };
3226
3332
  });
3227
3333
  }, [
@@ -3236,7 +3342,7 @@ function usePayrollRun(input) {
3236
3342
  if (assetMoney === null || selected.length === 0) return null;
3237
3343
  const money = [];
3238
3344
  for (const recipientId of selected) {
3239
- const parsed = parseMoney(amountText(amounts, seeds, recipientId), assetMoney);
3345
+ const parsed = parseAssetAmount(amountText(amounts, seeds, recipientId), requireAsset(assetMoney.asset));
3240
3346
  if (isMoneyParseError(parsed)) return null;
3241
3347
  money.push(parsed);
3242
3348
  }
@@ -3247,16 +3353,25 @@ function usePayrollRun(input) {
3247
3353
  seeds,
3248
3354
  selected
3249
3355
  ]);
3250
- const total = useMemo(() => parsedAmounts === null || assetMoney === null ? null : fromMinorUnits(parsedAmounts.reduce((sum, money) => sum + toMinorUnits(money), 0n), assetMoney), [assetMoney, parsedAmounts]);
3251
- const availableRaw = assetMoney === null ? null : toMinorUnits(assetMoney);
3252
- const totalRaw = total === null ? null : toMinorUnits(total);
3356
+ const total = useMemo(() => parsedAmounts === null || assetMoney === null ? null : {
3357
+ asset: assetMoney.asset,
3358
+ value: assetFromMinorUnits(parsedAmounts.reduce((sum, amount) => sum + toRaw(amount, requireAsset(amount.asset)), 0n), {
3359
+ asset: assetMoney.asset,
3360
+ decimals: requireAsset(assetMoney.asset).decimals
3361
+ }).value
3362
+ }, [assetMoney, parsedAmounts]);
3363
+ const availableRaw = assetMoney === null ? null : toRaw(assetMoney, requireAsset(assetMoney.asset));
3364
+ const totalRaw = total === null ? null : toRaw(total, requireAsset(total.asset));
3253
3365
  const funds = availableRaw === null || totalRaw === null ? "unknown" : totalRaw > availableRaw ? "insufficient" : "available";
3254
3366
  const budgets = me.data?.budgets ?? [];
3367
+ const compensationTerms = useMemo(() => new Map((terms.data ?? []).filter((row) => row.unit === "paycheck" && /^[1-9][0-9]*$/u.test(row.rate)).map((row) => [row.partyId, row])), [terms.data]);
3368
+ const writerNamesSelectedAsset = writeNamesAsset();
3255
3369
  const blockedReason = firstFalseFact({
3256
3370
  signerReady: signerStatus === "ready",
3257
3371
  ...runAuthorityFacts(draft, budgets, selectedAsset, assetMoney, funds),
3258
3372
  recipientCount: payableRefs === null ? 0 : selected.length,
3259
- amountsParse: total !== null
3373
+ amountsParse: total !== null && hasRequiredCompensation(frozen, writerNamesSelectedAsset, selected, compensationTerms),
3374
+ writeNamesAsset: writerNamesSelectedAsset
3260
3375
  });
3261
3376
  const authorize = useMutation({
3262
3377
  mutationFn: async ({ candidate, options }) => {
@@ -3278,7 +3393,7 @@ function usePayrollRun(input) {
3278
3393
  if (candidate.scope === currentScope.current) restoreDraft(candidate.scope);
3279
3394
  }
3280
3395
  });
3281
- const totalDisplay = total === null ? null : formatMoney(total, GRAMMAR);
3396
+ const totalDisplay = total === null ? null : formatAssetAmount(total);
3282
3397
  const authorizeMutate = authorize.mutate;
3283
3398
  const submit = useCallback((options) => {
3284
3399
  const attempt = toInvocationOptions(options);
@@ -3287,41 +3402,35 @@ function usePayrollRun(input) {
3287
3402
  if (permissionId === void 0 || totalDisplay === null || parsedAmounts === null) return;
3288
3403
  if (payableRefs === null) return;
3289
3404
  const recipientCount = selected.length;
3290
- const items = [];
3291
- for (const [index, recipientId] of selected.entries()) {
3292
- const to = payableRefs[index];
3293
- const parsed = parsedAmounts[index];
3294
- if (to === void 0 || parsed === void 0) return;
3295
- const raw = toMinorUnits(parsed).toString();
3296
- items.push({
3297
- to: {
3298
- kind: "party",
3299
- partyId: toPartyId(recipientId)
3300
- },
3301
- partyId: recipientId,
3302
- gross: raw,
3303
- net: raw,
3304
- adjustments: []
3405
+ let candidate;
3406
+ if (frozen) candidate = draft;
3407
+ else {
3408
+ const items = freshRunItems({
3409
+ selected,
3410
+ payableRefs,
3411
+ amounts: parsedAmounts,
3412
+ compensationTerms
3305
3413
  });
3306
- }
3307
- const runAt = Date.now();
3308
- const candidate = frozen ? draft : {
3309
- version: 1,
3310
- scope: draftScope,
3311
- asset: {
3312
- id: selectedAsset.id,
3313
- currency: assetMoney.currency,
3314
- decimals: assetMoney.decimals
3315
- },
3316
- input: {
3317
- permissionId,
3318
- period: {
3319
- start: runAt,
3320
- end: runAt
3414
+ const runAt = Date.now();
3415
+ candidate = {
3416
+ version: 1,
3417
+ scope: draftScope,
3418
+ asset: {
3419
+ id: selectedAsset.id,
3420
+ currency: requireAsset(assetMoney.asset).symbol,
3421
+ decimals: requireAsset(assetMoney.asset).decimals
3321
3422
  },
3322
- items
3323
- }
3324
- };
3423
+ input: {
3424
+ permissionId,
3425
+ asset: selectedAsset.id,
3426
+ period: {
3427
+ start: runAt,
3428
+ end: runAt
3429
+ },
3430
+ items
3431
+ }
3432
+ };
3433
+ }
3325
3434
  submitGate.current = true;
3326
3435
  authorizeMutate({
3327
3436
  candidate,
@@ -3353,6 +3462,7 @@ function usePayrollRun(input) {
3353
3462
  selectedAsset,
3354
3463
  blockedReason,
3355
3464
  budgets,
3465
+ compensationTerms,
3356
3466
  onFailed,
3357
3467
  onRun,
3358
3468
  parsedAmounts,
@@ -3401,18 +3511,25 @@ function usePayrollRun(input) {
3401
3511
  };
3402
3512
  }
3403
3513
  function remainingBalanceDisplay(available, total, asset) {
3404
- return available === null || total === null || asset === null ? null : formatMoney(fromMinorUnits(available - total, asset), GRAMMAR);
3514
+ if (available === null || total === null || asset === null) return null;
3515
+ const remaining = available - total;
3516
+ const magnitude = remaining < 0n ? -remaining : remaining;
3517
+ const formatted = formatAssetAmount(assetFromMinorUnits(magnitude, {
3518
+ asset: asset.asset,
3519
+ decimals: requireAsset(asset.asset).decimals
3520
+ }));
3521
+ return remaining < 0n ? `-${formatted}` : formatted;
3405
3522
  }
3406
3523
  /** Persisted replay keeps its authorized Budget and asset; a prior debit cannot block its own recovery. */
3407
3524
  function runAuthorityFacts(draft, budgets, asset, money, funds) {
3408
3525
  return {
3409
3526
  budgetCount: draft !== null && !budgets.some((budget) => budget.id === draft.input.permissionId) ? 0 : budgets.length,
3410
- hasAsset: asset !== null && (draft === null || draft.asset.id === asset.id && draft.asset.currency === money?.currency && draft.asset.decimals === money?.decimals),
3527
+ hasAsset: asset !== null && (draft === null || draft.asset.id === asset.id && draft.input.asset === money?.asset),
3411
3528
  funds: draft?.input.requestKey === void 0 ? funds : "available"
3412
3529
  };
3413
3530
  }
3414
3531
  /**
3415
- * The seven facts, in the ONE order the contract fixes. Exactly one code comes
3532
+ * The facts, in the ONE order the contract fixes. Exactly one code comes
3416
3533
  * out, and it is always the first thing that is false — so two problems never
3417
3534
  * race to name the button.
3418
3535
  */
@@ -3423,7 +3540,8 @@ function firstFalseFact(facts) {
3423
3540
  if (!facts.hasAsset) return "no-asset";
3424
3541
  if (facts.recipientCount === 0) return "no-recipients";
3425
3542
  if (!facts.amountsParse) return "amount-invalid";
3426
- return facts.funds === "insufficient" ? "insufficient-funds" : null;
3543
+ if (facts.funds === "insufficient") return "insufficient-funds";
3544
+ return facts.writeNamesAsset ? null : "asset-unnamed";
3427
3545
  }
3428
3546
  //#endregion
3429
3547
  //#region src/headless/payroll/payroll-run.tsx
@@ -3433,7 +3551,7 @@ function usePayrollRunContext() {
3433
3551
  if (engine === null) throw new Error("CapxulPayrollRun parts must be used inside <CapxulPayrollRun>");
3434
3552
  return engine;
3435
3553
  }
3436
- function Root({ orgId, prefill, onRun, onFailed, children }) {
3554
+ function Root$1({ orgId, prefill, onRun, onFailed, children }) {
3437
3555
  const engine = usePayrollRun({
3438
3556
  orgId,
3439
3557
  prefill,
@@ -3463,7 +3581,7 @@ function Actions({ children }) {
3463
3581
  return /* @__PURE__ */ jsx(Fragment, { children: children(engine.actions) });
3464
3582
  }
3465
3583
  /** The compound shape the app's own design system already uses (`StatusTabs`). */
3466
- const CapxulPayrollRun = Object.assign(Root, {
3584
+ const CapxulPayrollRun = Object.assign(Root$1, {
3467
3585
  Recipients,
3468
3586
  Amounts,
3469
3587
  Asset,
@@ -3471,4 +3589,230 @@ const CapxulPayrollRun = Object.assign(Root, {
3471
3589
  Actions
3472
3590
  });
3473
3591
  //#endregion
3474
- export { CapxulActivity, CapxulAuthenticationController, CapxulContacts, CapxulDashboardAccess, CapxulOnboardingController, CapxulOrgMember, CapxulPayroll, CapxulPayrollRun, CapxulProvider, CapxulSendMoney, acknowledgeOnboardingDestination, activeOnboardingRecovery, capxulKeys, clearOnboardingJourney, currentOnboardingJourneyId, entered, invalidateOnboardingJourneyObservation, isClaimed, isRestoring, loadOnboardingJourney, onboardingJourneyPosition, saveOnboardingJourney, startOnboardingJourney, useCapxul, useCapxulAccountBalance, useCapxulAccountFund, useCapxulActivity, useCapxulActivityDetail, useCapxulAnnotateMovement, useCapxulAuth, useCapxulCancelPayment, useCapxulClaimPayment, useCapxulClientOrNull, useCapxulCreateCommitment, useCapxulCreateOrg, useCapxulDestination, useCapxulHandleAvailability, useCapxulHoldings, useCapxulIdentity, useCapxulImageUpload, useCapxulInviteMember, useCapxulOrgMembers, useCapxulOrgRoles, useCapxulOrgTreasury, useCapxulOrganizationPay, useCapxulOrganizationPayBatch, useCapxulOrgs, useCapxulPay, useCapxulPayment, useCapxulPayments, useCapxulPermission, useCapxulPermissionAssign, useCapxulPermissionChange, useCapxulPermissionCreate, useCapxulPermissionReplace, useCapxulPermissionRevoke, useCapxulPermissions, useCapxulProfile, useCapxulRedirectPayment, useCapxulSend, useCapxulTransitions, useCapxulUploadImage };
3592
+ //#region src/headless/client-connection/use-client-connection.ts
3593
+ function useClientConnection(input) {
3594
+ const [view, setView] = useState(null);
3595
+ const [pending, setPending] = useState(false);
3596
+ const [error, setError] = useState(null);
3597
+ const [resendReady, setResendReady] = useState(false);
3598
+ const identityRef = useRef({
3599
+ client: input.client,
3600
+ requestId: input.requestId
3601
+ });
3602
+ identityRef.current = {
3603
+ client: input.client,
3604
+ requestId: input.requestId
3605
+ };
3606
+ const belongsHere = useCallback((client, requestId) => identityRef.current.client === client && identityRef.current.requestId === requestId, []);
3607
+ const refresh = useCallback(async (signal) => {
3608
+ const { client, requestId } = identityRef.current;
3609
+ const result = await client.auth.clientRequests.read({ requestId }, signal === void 0 ? void 0 : { signal });
3610
+ if (signal?.aborted || !belongsHere(client, requestId)) return;
3611
+ if (result.ok) {
3612
+ setView(result.value);
3613
+ setError(null);
3614
+ return;
3615
+ }
3616
+ setError(result.error);
3617
+ }, [belongsHere]);
3618
+ useEffect(() => {
3619
+ setView(null);
3620
+ setError(null);
3621
+ setPending(false);
3622
+ setResendReady(false);
3623
+ const controller = new AbortController();
3624
+ refresh(controller.signal);
3625
+ return () => {
3626
+ controller.abort();
3627
+ };
3628
+ }, [
3629
+ input.client,
3630
+ input.requestId,
3631
+ refresh
3632
+ ]);
3633
+ useEffect(() => {
3634
+ if (view?.phase !== "otp_required") {
3635
+ setResendReady(false);
3636
+ return;
3637
+ }
3638
+ const remaining = view.resendAt - Date.now();
3639
+ if (remaining <= 0) {
3640
+ setResendReady(true);
3641
+ return;
3642
+ }
3643
+ setResendReady(false);
3644
+ const timer = setTimeout(() => setResendReady(true), remaining);
3645
+ return () => clearTimeout(timer);
3646
+ }, [view]);
3647
+ const run = useCallback(async (work) => {
3648
+ const { client, requestId } = identityRef.current;
3649
+ setPending(true);
3650
+ try {
3651
+ const result = await work();
3652
+ if (!belongsHere(client, requestId)) return;
3653
+ if (!result.ok) {
3654
+ setError(result.error);
3655
+ return;
3656
+ }
3657
+ await refresh();
3658
+ } finally {
3659
+ if (belongsHere(client, requestId)) setPending(false);
3660
+ }
3661
+ }, [belongsHere, refresh]);
3662
+ return useMemo(() => {
3663
+ if (error !== null && (view === null || view.phase !== "refused")) return {
3664
+ phase: "failure",
3665
+ otp: null,
3666
+ consent: null,
3667
+ complete: null,
3668
+ failure: {
3669
+ code: error.code,
3670
+ retry: () => refresh(),
3671
+ cancel: async () => {
3672
+ if (view === null) return;
3673
+ const { client, requestId } = identityRef.current;
3674
+ await run(() => client.auth.clientRequests.cancel({
3675
+ requestId,
3676
+ revision: view.revision
3677
+ }));
3678
+ }
3679
+ }
3680
+ };
3681
+ if (view === null) return {
3682
+ phase: "checking",
3683
+ otp: null,
3684
+ consent: null,
3685
+ complete: null,
3686
+ failure: null
3687
+ };
3688
+ if (view.phase === "otp_required") return {
3689
+ phase: view.phase,
3690
+ otp: {
3691
+ email: view.email,
3692
+ pending,
3693
+ canResend: resendReady,
3694
+ verifyCode: async (code) => {
3695
+ const { client, requestId } = identityRef.current;
3696
+ await run(() => client.auth.clientRequests.verifyOtp({
3697
+ requestId,
3698
+ code
3699
+ }));
3700
+ },
3701
+ resend: async () => {
3702
+ const { client, requestId } = identityRef.current;
3703
+ await run(() => client.auth.clientRequests.sendOtp({ requestId }));
3704
+ }
3705
+ },
3706
+ consent: null,
3707
+ complete: null,
3708
+ failure: null
3709
+ };
3710
+ if (view.phase === "review") return {
3711
+ phase: view.phase,
3712
+ otp: null,
3713
+ consent: {
3714
+ verifiedEmail: view.email,
3715
+ clientLabel: view.clientLabel,
3716
+ scopeCodes: view.scopes,
3717
+ purpose: view.intent.mode,
3718
+ setupState: view.setupState,
3719
+ pending,
3720
+ accept: async () => {
3721
+ const { client, requestId } = identityRef.current;
3722
+ await run(() => client.auth.clientRequests.accept({
3723
+ requestId,
3724
+ revision: view.revision
3725
+ }));
3726
+ },
3727
+ cancel: async () => {
3728
+ const { client, requestId } = identityRef.current;
3729
+ await run(() => client.auth.clientRequests.cancel({
3730
+ requestId,
3731
+ revision: view.revision
3732
+ }));
3733
+ }
3734
+ },
3735
+ complete: null,
3736
+ failure: null
3737
+ };
3738
+ if (view.phase === "connected") return {
3739
+ phase: view.phase,
3740
+ otp: null,
3741
+ consent: null,
3742
+ complete: { outcome: "connected" },
3743
+ failure: null
3744
+ };
3745
+ if (view.phase === "refused") return {
3746
+ phase: view.phase,
3747
+ otp: null,
3748
+ consent: null,
3749
+ complete: null,
3750
+ failure: {
3751
+ code: view.code,
3752
+ ...view.field === void 0 ? {} : { field: view.field },
3753
+ retry: () => refresh(),
3754
+ cancel: async () => void 0
3755
+ }
3756
+ };
3757
+ return {
3758
+ phase: view.phase,
3759
+ otp: null,
3760
+ consent: null,
3761
+ complete: null,
3762
+ failure: null
3763
+ };
3764
+ }, [
3765
+ error,
3766
+ pending,
3767
+ refresh,
3768
+ resendReady,
3769
+ run,
3770
+ view
3771
+ ]);
3772
+ }
3773
+ //#endregion
3774
+ //#region src/headless/client-connection/client-connection.tsx
3775
+ const ConnectionContext = createContext(null);
3776
+ function useConnection() {
3777
+ const engine = useContext(ConnectionContext);
3778
+ if (engine === null) throw new Error("CapxulClientConnection parts must be used inside <CapxulClientConnection>");
3779
+ return engine;
3780
+ }
3781
+ function Root({ client, requestId, children }) {
3782
+ const engine = useClientConnection({
3783
+ client,
3784
+ requestId
3785
+ });
3786
+ return /* @__PURE__ */ jsx(ConnectionContext.Provider, {
3787
+ value: engine,
3788
+ children
3789
+ });
3790
+ }
3791
+ function Checking({ children }) {
3792
+ return useConnection().phase === "checking" ? /* @__PURE__ */ jsx(Fragment, { children }) : null;
3793
+ }
3794
+ function Otp({ children }) {
3795
+ const otp = useConnection().otp;
3796
+ return otp === null ? null : /* @__PURE__ */ jsx(Fragment, { children: children(otp) });
3797
+ }
3798
+ function Consent({ children }) {
3799
+ const consent = useConnection().consent;
3800
+ return consent === null ? null : /* @__PURE__ */ jsx(Fragment, { children: children(consent) });
3801
+ }
3802
+ function Complete({ children }) {
3803
+ const complete = useConnection().complete;
3804
+ return complete === null ? null : /* @__PURE__ */ jsx(Fragment, { children: children(complete) });
3805
+ }
3806
+ function Failure({ children }) {
3807
+ const failure = useConnection().failure;
3808
+ return failure === null ? null : /* @__PURE__ */ jsx(Fragment, { children: children(failure) });
3809
+ }
3810
+ const CapxulClientConnection = Object.assign(Root, {
3811
+ Checking,
3812
+ Otp,
3813
+ Consent,
3814
+ Complete,
3815
+ Failure
3816
+ });
3817
+ //#endregion
3818
+ export { CapxulActivity, CapxulAuthenticationController, CapxulClientConnection, CapxulContacts, CapxulDashboardAccess, CapxulOnboardingController, CapxulOrgMember, CapxulPayroll, CapxulPayrollRun, CapxulProvider, CapxulSendMoney, acknowledgeOnboardingDestination, activeOnboardingRecovery, capxulKeys, clearOnboardingJourney, currentOnboardingJourneyId, entered, invalidateOnboardingJourneyObservation, isClaimed, isRestoring, loadOnboardingJourney, onboardingJourneyPosition, saveOnboardingJourney, startOnboardingJourney, useCapxul, useCapxulAccountBalance, useCapxulAccountFund, useCapxulActivity, useCapxulActivityDetail, useCapxulAnnotateMovement, useCapxulAuth, useCapxulCancelPayment, useCapxulClaimPayment, useCapxulClientOrNull, useCapxulCreateCommitment, useCapxulCreateOrg, useCapxulDestination, useCapxulHandleAvailability, useCapxulHoldings, useCapxulIdentity, useCapxulImageUpload, useCapxulInviteMember, useCapxulOrgMembers, useCapxulOrgRoles, useCapxulOrgTreasury, useCapxulOrganizationPay, useCapxulOrganizationPayBatch, useCapxulOrgs, useCapxulPay, useCapxulPayment, useCapxulPayments, useCapxulPermission, useCapxulPermissionAssign, useCapxulPermissionChange, useCapxulPermissionCreate, useCapxulPermissionReplace, useCapxulPermissionRevoke, useCapxulPermissions, useCapxulProfile, useCapxulRedirectPayment, useCapxulSend, useCapxulTransitions, useCapxulUploadImage };