@capxul/sdk-react 2.5.5 → 2.6.1

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/README.md CHANGED
@@ -68,7 +68,10 @@ export function Providers({ children }: { children: React.ReactNode }) {
68
68
  `.Actions`. A screen places any subset — the pay-now sheet places all five
69
69
  run parts, the group sheet places two. `.Actions.blockedReason` names the
70
70
  first false fact as a `CapxulPayrollRunBlockedReason` code, never a
71
- sentence. Both engine hooks stay module-scoped.
71
+ sentence. `.Amounts` prefills a row from the organization's active engagement
72
+ terms when the run's period can state that rate — the value stays editable,
73
+ and an amount the caller passed in `prefill` wins over it. Both engine hooks
74
+ stay module-scoped.
72
75
 
73
76
  The packed npm package exposes `@capxul/sdk-react` and
74
77
  `@capxul/sdk-react/testing`. The workspace-only `@capxul/sdk-react/headless`
@@ -117,6 +117,13 @@ const capxulKeys = {
117
117
  "payroll",
118
118
  "groups"
119
119
  ],
120
+ payrollTerms: (orgId) => [
121
+ "capxul",
122
+ "org",
123
+ orgId ?? "pending",
124
+ "payroll",
125
+ "terms"
126
+ ],
120
127
  activity: ["capxul", "activity"],
121
128
  activityDetail: (reference, actor = void 0) => [
122
129
  "capxul",
@@ -489,7 +496,9 @@ const MAX_CAPXUL_QUERY_RETRIES = 2;
489
496
  */
490
497
  function shouldRetryCapxulQuery(failureCount, error) {
491
498
  if (failureCount >= MAX_CAPXUL_QUERY_RETRIES) return false;
492
- return isCapxulError(error) && RETRYABLE_QUERY_ERROR_CODES.has(error.code);
499
+ if (!isCapxulError(error)) return false;
500
+ if (error.chain !== void 0) return error.chain.retryable;
501
+ return RETRYABLE_QUERY_ERROR_CODES.has(error.code);
493
502
  }
494
503
  /**
495
504
  * The default query client used when the host injects none. Exported so a test
package/dist/index.d.mts CHANGED
@@ -1287,6 +1287,7 @@ declare const capxulKeys: {
1287
1287
  contactsList: (scope: string, includeHidden: boolean) => readonly ["capxul", "contacts", string, "all" | "visible"];
1288
1288
  payrollRuns: (orgId: OrgId | undefined) => readonly ["capxul", "org", "pending" | OrgId, "payroll", "runs"];
1289
1289
  payrollGroups: (orgId: OrgId | undefined) => readonly ["capxul", "org", "pending" | OrgId, "payroll", "groups"];
1290
+ payrollTerms: (orgId: OrgId | undefined) => readonly ["capxul", "org", "pending" | OrgId, "payroll", "terms"];
1290
1291
  activity: readonly ["capxul", "activity"];
1291
1292
  activityDetail: (reference: ActivityReference | undefined, actor?: ActorReference | undefined) => readonly ["capxul", "activity", "self" | ActorReference, "pending" | ActivityReference];
1292
1293
  activitySummary: (actor?: ActorReference, window?: ActivityRange) => readonly ["capxul", "activity", "summary", "self" | ActorReference, "all" | ActivityRange];
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  "use client";
2
- import { a as useCapxulAuth, c as useCapxulIdentityOrNull, d as capxulKeys, f as useCapxulClientOrNull, i as entered, l as useCapxulSend, n as CapxulOnboardingController, o as useCapxulDestination, p as useCapxul, r as CapxulProvider, s as useCapxulIdentity, t as CapxulAuthenticationController, u as useCapxulTransitions } from "./controllers-DpScp8c5.mjs";
2
+ import { a as useCapxulAuth, c as useCapxulIdentityOrNull, d as capxulKeys, f as useCapxulClientOrNull, i as entered, l as useCapxulSend, n as CapxulOnboardingController, o as useCapxulDestination, p as useCapxul, r as CapxulProvider, s as useCapxulIdentity, t as CapxulAuthenticationController, u as useCapxulTransitions } from "./controllers-ClPgcB4L.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
5
  import { CapxulError, Errors, PAYMENT_DIRECTIONS, PAYMENT_STATUSES, fingerprintPaymentIntent, formatMoney, isCapxulError, isClaimed, isClaimed as isClaimed$1, isMoneyParseError, isRestoring, isRestoring as isRestoring$1, parseMoney, paymentPhase, resolveIdentityDestination } from "@capxul/sdk";
@@ -2315,6 +2315,22 @@ function usePayrollRuns(orgId) {
2315
2315
  enabled: client !== null && orgId !== void 0
2316
2316
  });
2317
2317
  }
2318
+ /**
2319
+ * The active engagement terms of the org's employed people (#1638) — the rows
2320
+ * ruling Q3 lets `.Amounts` prefill a rate from.
2321
+ *
2322
+ * The read is gated on `canSpend` server-side, exactly as the groups read is.
2323
+ * A member who may not spend therefore reads an error here and composes with no
2324
+ * seed, which is the same composer they had before this read existed.
2325
+ */
2326
+ function useOrgTerms(orgId) {
2327
+ const client = useCapxulClientOrNull();
2328
+ return useQuery({
2329
+ queryKey: capxulKeys.payrollTerms(orgId),
2330
+ queryFn: async () => unwrapCapxulResult(await scopedClient(client, "org.payroll.terms", orgId).payroll.terms()),
2331
+ enabled: client !== null && orgId !== void 0
2332
+ });
2333
+ }
2318
2334
  function useOrgTreasury(orgId) {
2319
2335
  const client = useCapxulClientOrNull();
2320
2336
  return useQuery({
@@ -2500,6 +2516,79 @@ const CapxulPayroll = Object.assign(Root$1, {
2500
2516
  Groups
2501
2517
  });
2502
2518
  //#endregion
2519
+ //#region src/headless/payroll/terms-prefill.ts
2520
+ /**
2521
+ * Is this period exactly one UTC calendar month, CLOSED at both ends?
2522
+ *
2523
+ * Closed because that is the convention `periodStart` / `periodEnd` already
2524
+ * carry: an instant run stores `periodStart === periodEnd`, one instant rather
2525
+ * than a half-open window of zero length. One convention, the shipped one — the
2526
+ * first run that actually carries a period will assert it here.
2527
+ *
2528
+ * A `month` rate states what one month costs and nothing else, so anything that
2529
+ * is not exactly a month leaves the rate underivable and seeds nothing. Half a
2530
+ * month is not half the rate — that would be proration, a policy no layer of
2531
+ * this item owns.
2532
+ */
2533
+ function spansOneMonth(period) {
2534
+ const at = new Date(period.start);
2535
+ const first = Date.UTC(at.getUTCFullYear(), at.getUTCMonth(), 1);
2536
+ const next = Date.UTC(at.getUTCFullYear(), at.getUTCMonth() + 1, 1);
2537
+ return period.start === first && period.end === next - 1;
2538
+ }
2539
+ /** A stored `rate` this module can count. Nothing decodes the row before us. */
2540
+ const MINOR_UNITS = /^\d+$/u;
2541
+ /**
2542
+ * The typed-amount text a rate becomes: a plain decimal, NOT a display string.
2543
+ * `null` when the stored rate is not minor units at all.
2544
+ *
2545
+ * The text is what the employer sees in the amount input and what the run
2546
+ * submits, so it must survive a round trip back through `parseMoney`.
2547
+ * `formatMoney` appends a currency code, which never would.
2548
+ *
2549
+ * The shape check is not belt-and-braces. `engagementTerms.rate` is a bare
2550
+ * `v.string()`, the SDK maps the row rather than decoding it, and this function
2551
+ * runs inside a render — so an unhandled `BigInt("")` would take the whole
2552
+ * composer down instead of one seed. `sumTypedAmounts` in this same folder
2553
+ * skips what it cannot read for the same reason.
2554
+ */
2555
+ function typedAmount(terms) {
2556
+ if (!MINOR_UNITS.test(terms.rate)) return null;
2557
+ const value = fromMinorUnits(BigInt(terms.rate), terms).value;
2558
+ return value.includes(".") ? value.replace(/\.?0+$/u, "") : value;
2559
+ }
2560
+ /**
2561
+ * The seed text per party, for the recipients whose terms row states a rate
2562
+ * this run can pay.
2563
+ *
2564
+ * Three facts stop a seed, and each one stops it SILENTLY: a recipient with no
2565
+ * seed gets the empty field they had before this read existed, never an error
2566
+ * on a value they did not type.
2567
+ *
2568
+ * the unit `paycheck` always states a rate. `month` states one only over
2569
+ * a month (ruling Q3). `hour`, `week` and `year` state a rate
2570
+ * for a period the run does not carry.
2571
+ * the currency `.Amounts` is denominated in the TREASURY asset's currency and
2572
+ * the one-option dropdown cannot be changed. A rate in another
2573
+ * currency is not that money, and converting it would be Capxul
2574
+ * computing pay.
2575
+ * the text a rate that is not minor units at all, or that does not parse
2576
+ * back to positive money — a zero rate, or one counted in finer
2577
+ * units than the asset — would seed a field that is wrong the
2578
+ * instant it appears.
2579
+ */
2580
+ function termsSeeds(terms, asset, period) {
2581
+ const seeds = /* @__PURE__ */ new Map();
2582
+ if (asset === null) return seeds;
2583
+ for (const row of terms) {
2584
+ if (!(row.unit === "paycheck" || row.unit === "month" && spansOneMonth(period)) || row.currency !== asset.currency) continue;
2585
+ const text = typedAmount(row);
2586
+ if (text === null || isMoneyParseError(parseMoney(text, asset))) continue;
2587
+ seeds.set(row.partyId, text);
2588
+ }
2589
+ return seeds;
2590
+ }
2591
+ //#endregion
2503
2592
  //#region src/headless/payroll/use-payroll-run.ts
2504
2593
  const GRAMMAR = { grammar: "code" };
2505
2594
  /**
@@ -2510,6 +2599,29 @@ const GRAMMAR = { grammar: "code" };
2510
2599
  */
2511
2600
  const CURRENCY_UNREAD = "";
2512
2601
  /**
2602
+ * The period this composer composes: an INSTANT run (ruling Q3). `authorizeRun`
2603
+ * stamps `start === end === runAt` at submit, and the payslip's period renders
2604
+ * that date.
2605
+ *
2606
+ * WHICH instant it is cannot change a prefill answer, because no single instant
2607
+ * spans a calendar month. So the seed asks its question with the SHAPE the run
2608
+ * will submit, rather than with a live clock that would rebuild the seed map on
2609
+ * every render. A later period-bearing run passes its own window and reaches
2610
+ * the `month` branch without a second rule.
2611
+ */
2612
+ const INSTANT_RUN_PERIOD = {
2613
+ start: 0,
2614
+ end: 0
2615
+ };
2616
+ /**
2617
+ * The text one `.Amounts` row shows: the employer's own draft, else the terms
2618
+ * seed. Read in BOTH places a value is needed — the rendered entries and the
2619
+ * parsed totals — so the summary can never state a number the fields do not.
2620
+ */
2621
+ function amountText(amounts, seeds, recipientId) {
2622
+ return amounts[recipientId]?.value ?? seeds.get(recipientId) ?? "";
2623
+ }
2624
+ /**
2513
2625
  * WHICH run the composer is composing, by MEANING rather than by object shape.
2514
2626
  *
2515
2627
  * `recipientIds` order is meaning — it is the order `.Amounts` rows appear in.
@@ -2547,6 +2659,7 @@ function usePayrollRun(input) {
2547
2659
  const groups = usePayrollGroups(orgId);
2548
2660
  const treasury = useOrgTreasury(orgId);
2549
2661
  const me = useOrgMe(orgId);
2662
+ const terms = useOrgTerms(orgId);
2550
2663
  const [selected, setSelected] = useState(() => prefill?.recipientIds ?? []);
2551
2664
  const [amounts, setAmounts] = useState(() => initialAmounts(prefill));
2552
2665
  const composerKey = composerIdentity(orgId, prefill);
@@ -2593,9 +2706,9 @@ function usePayrollRun(input) {
2593
2706
  }
2594
2707
  return resolved;
2595
2708
  }, [refs, selected]);
2709
+ const seeds = useMemo(() => termsSeeds(terms.data ?? [], assetMoney, INSTANT_RUN_PERIOD), [assetMoney, terms.data]);
2596
2710
  const entries = useMemo(() => selected.map((recipientId) => {
2597
- const draft = amounts[recipientId];
2598
- const value = draft?.value ?? "";
2711
+ const value = amountText(amounts, seeds, recipientId);
2599
2712
  const parsed = assetMoney === null || value.trim() === "" ? null : parseMoney(value, assetMoney);
2600
2713
  return {
2601
2714
  recipientId,
@@ -2603,7 +2716,7 @@ function usePayrollRun(input) {
2603
2716
  value,
2604
2717
  change: (text) => change(recipientId, text),
2605
2718
  error: parsed !== null && isMoneyParseError(parsed) ? parsed.reason : null,
2606
- prefilled: draft?.prefilled ?? false,
2719
+ prefilled: amounts[recipientId]?.prefilled ?? seeds.has(recipientId),
2607
2720
  currency: assetMoney?.currency ?? CURRENCY_UNREAD
2608
2721
  };
2609
2722
  }), [
@@ -2611,13 +2724,14 @@ function usePayrollRun(input) {
2611
2724
  assetMoney,
2612
2725
  change,
2613
2726
  named,
2727
+ seeds,
2614
2728
  selected
2615
2729
  ]);
2616
2730
  const parsedAmounts = useMemo(() => {
2617
2731
  if (assetMoney === null || selected.length === 0) return null;
2618
2732
  const money = [];
2619
2733
  for (const recipientId of selected) {
2620
- const parsed = parseMoney(amounts[recipientId]?.value ?? "", assetMoney);
2734
+ const parsed = parseMoney(amountText(amounts, seeds, recipientId), assetMoney);
2621
2735
  if (isMoneyParseError(parsed)) return null;
2622
2736
  money.push(parsed);
2623
2737
  }
@@ -2625,6 +2739,7 @@ function usePayrollRun(input) {
2625
2739
  }, [
2626
2740
  amounts,
2627
2741
  assetMoney,
2742
+ seeds,
2628
2743
  selected
2629
2744
  ]);
2630
2745
  const total = useMemo(() => parsedAmounts === null || assetMoney === null ? null : fromMinorUnits(parsedAmounts.reduce((sum, money) => sum + toMinorUnits(money), 0n), assetMoney), [assetMoney, parsedAmounts]);
@@ -1,4 +1,4 @@
1
- import { n as CapxulOnboardingController, r as CapxulProvider, t as CapxulAuthenticationController } from "../controllers-DpScp8c5.mjs";
1
+ import { n as CapxulOnboardingController, r as CapxulProvider, t as CapxulAuthenticationController } from "../controllers-ClPgcB4L.mjs";
2
2
  import "react";
3
3
  import { jsx } from "react/jsx-runtime";
4
4
  import { createCapxulTestClient } from "@capxul/sdk/testing";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capxul/sdk-react",
3
- "version": "2.5.5",
3
+ "version": "2.6.1",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/Xelmar-tech/infrastructure.git",
@@ -26,7 +26,7 @@
26
26
  "access": "public"
27
27
  },
28
28
  "dependencies": {
29
- "@capxul/sdk": "2.5.5"
29
+ "@capxul/sdk": "2.6.1"
30
30
  },
31
31
  "devDependencies": {
32
32
  "@tanstack/react-query": "^5.66.9",