@capxul/sdk-react 2.5.4 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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.
|
|
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
|
-
|
|
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-
|
|
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
|
|
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:
|
|
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
|
|
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]);
|
package/dist/testing/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as CapxulOnboardingController, r as CapxulProvider, t as CapxulAuthenticationController } from "../controllers-
|
|
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.
|
|
3
|
+
"version": "2.6.0",
|
|
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.
|
|
29
|
+
"@capxul/sdk": "2.6.0"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@tanstack/react-query": "^5.66.9",
|