@capxul/sdk-react 4.20.0-beta.5 → 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/README.md +15 -0
- package/dist/index.d.mts +15 -11
- package/dist/index.mjs +216 -171
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -56,6 +56,8 @@ export function Providers({ children }: { children: React.ReactNode }) {
|
|
|
56
56
|
engine hook stays module-scoped. Both actor paths ship: a personal actor omits
|
|
57
57
|
`.Source` and spends the personal Account; an organization actor holds the
|
|
58
58
|
Budget the payment is drawn from and spends the treasury Account.
|
|
59
|
+
The submitted amount uses the selected registry AssetId and precision. This
|
|
60
|
+
includes 18-decimal WETH.
|
|
59
61
|
- `CapxulContacts` is the compound address-book component. One root owns the
|
|
60
62
|
book behaviour and draws nothing. The `.Summary`, `.List`, and `.Add` region
|
|
61
63
|
parts each hand the screen one finished slice, and a `.List` row carries its
|
|
@@ -75,6 +77,9 @@ export function Providers({ children }: { children: React.ReactNode }) {
|
|
|
75
77
|
and an amount the caller passed in `prefill` wins over it. Both engine hooks
|
|
76
78
|
stay module-scoped.
|
|
77
79
|
|
|
80
|
+
Activity groups use AssetId. The SDK resolves symbols and precision only for
|
|
81
|
+
display and CSV output. Assets with the same peg remain separate.
|
|
82
|
+
|
|
78
83
|
Payroll confirmation restores a saved run before enabling submission. An
|
|
79
84
|
uncertain run keeps its period, amounts, recipients, and SDK request key across
|
|
80
85
|
close/reopen and reload. Its fields remain fixed until Core confirms success or terminal closure.
|
|
@@ -98,6 +103,16 @@ The testing harness uses the real public provider and controllers and imports
|
|
|
98
103
|
its client factory only through `@capxul/sdk/testing`; it owns no renderer or
|
|
99
104
|
parallel identity state.
|
|
100
105
|
|
|
106
|
+
## Activity documents
|
|
107
|
+
|
|
108
|
+
`CapxulActivity.Detail` exposes `documents`, `selectedDocument`, and
|
|
109
|
+
`selectDocument({ documentHash, contentHash })`. Selection defaults to the
|
|
110
|
+
instruction, then the Movement Memo. `renderDocument()` returns safe semantic
|
|
111
|
+
HTML. `downloadDocument()` returns exact bytes for a deliberate download.
|
|
112
|
+
The host owns the panel and download action. Do not log these results.
|
|
113
|
+
Account or Organization changes clear selection and cancel pending reads.
|
|
114
|
+
These methods replace the receipt-only selector and action.
|
|
115
|
+
|
|
101
116
|
## Documentation
|
|
102
117
|
|
|
103
118
|
- [Maintainer context](./CONTEXT.md)
|
package/dist/index.d.mts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { A as entered, C as CreateOrganizationFailure, D as OrganizationDetails, E as InvocationOptions, F as useCapxulTransitions, M as useCapxulDestination, N as useCapxulIdentity, O as ProfileDetails, P as useCapxulSend, S as CapxulSend, T as Destination, _ as ReadyDestination, a as AuthenticationSlots, b as Slot, c as ControllerAction, d as OnboardingControllerProps, f as OrgFailure, g as ProfileSlotProps, h as PendingAuthState, i as AuthenticatedState, j as useCapxulAuth, k as SendResult, l as FaultedState, m as OtpPendingState, n as AccountProgress, o as CapxulAuthenticationController, p as OrgProgress, r as ActionResult, s as CapxulOnboardingController, t as AccountFailure, u as NavigationAction, v as RetryAction, w as CreateOrganizationSubmission, x as CapxulAuth, y as SignedOutState } from "./controllers-BWmKRFy6.mjs";
|
|
2
2
|
import { ReactNode, RefCallback } from "react";
|
|
3
3
|
import { QueryClient, UseMutationResult, UseQueryResult } from "@tanstack/react-query";
|
|
4
|
-
import { Account, AccountRequirement, ActivityAnnotation, ActivityAnnotationInput, ActivityDetail, ActivityKind, ActivityListParams, ActivityPage, ActivityPhase, ActivityRange, ActivityReference, ActorReference, AddressBookEntry, BudgetId, CapxulClient, CapxulError, CapxulResult, CapxulSigner, CreateOrgInput, CurrentHoldings, HostObservability, IdentityDestination, IdentityState, InviteMemberInput, MemberView, Money, MoneyParseErrorReason, OrgId, OrgView, OrganizationPaymentBatchInput, OrganizationPaymentInput, PartyId, Payment, PaymentDirection, PaymentDocumentRef, PaymentDocumentRender, PaymentStatus, PaymentTiming, PaymentType, PaymentsPayInput, PayrollGroupId, PayrollGroupInput, PayrollRunId, PayrollRunStatus, Permission, PermissionMethods, PermissionReadResult, Profile, RoleView, SubmittedPermissionExecution, isClaimed, isRestoring } from "@capxul/sdk";
|
|
4
|
+
import { Account, AccountRequirement, ActivityAnnotation, ActivityAnnotationInput, ActivityDetail, ActivityKind, ActivityListParams, ActivityPage, ActivityPhase, ActivityRange, ActivityReference, ActivitySummaryTotal, ActorReference, AddressBookEntry, AssetAmount, AssetId, BudgetId, CapxulClient, CapxulError, CapxulResult, CapxulSigner, CreateOrgInput, CurrentHoldings, HostObservability, IdentityDestination, IdentityState, InviteMemberInput, MemberView, Money, MoneyParseErrorReason, OrgId, OrgView, OrganizationAccess, OrganizationPaymentBatchInput, OrganizationPaymentInput, PartyId, Payment, PaymentDirection, PaymentDocumentDownload, PaymentDocumentReadRef, PaymentDocumentRef, PaymentDocumentRender, PaymentStatus, PaymentTiming, PaymentType, PaymentsPayInput, PayrollGroupId, PayrollGroupInput, PayrollRunId, PayrollRunStatus, Permission, PermissionMethods, PermissionReadResult, Profile, RoleView, SubmittedPermissionExecution, isClaimed, isRestoring } from "@capxul/sdk";
|
|
5
5
|
//#region src/provider.d.ts
|
|
6
6
|
type CapxulProviderSharedProps = {
|
|
7
7
|
/** Bring your own QueryClient; otherwise the provider creates one. */
|
|
@@ -82,7 +82,7 @@ declare function useCapxulAccountBalance(options?: UseCapxulAccountBalanceOption
|
|
|
82
82
|
//#region src/hooks/use-capxul-account-fund.d.ts
|
|
83
83
|
type UseCapxulAccountFundReturn = UseMutationResult<{
|
|
84
84
|
readonly txHash: string;
|
|
85
|
-
}, CapxulError,
|
|
85
|
+
}, CapxulError, AssetAmount>;
|
|
86
86
|
declare function useCapxulAccountFund(): UseCapxulAccountFundReturn;
|
|
87
87
|
//#endregion
|
|
88
88
|
//#region src/hooks/use-capxul-money.d.ts
|
|
@@ -148,7 +148,7 @@ declare function useCapxulHoldings(params?: UseCapxulHoldingsParams): UseQueryRe
|
|
|
148
148
|
* List the Organizations that the current user can access. Binds directly to
|
|
149
149
|
* the locked `capxul.orgs()` SDK method.
|
|
150
150
|
*/
|
|
151
|
-
type UseCapxulOrgsReturn = UseQueryResult<readonly
|
|
151
|
+
type UseCapxulOrgsReturn = UseQueryResult<readonly OrganizationAccess[], CapxulError>;
|
|
152
152
|
type UseCapxulOrgsOptions = {
|
|
153
153
|
/**
|
|
154
154
|
* Gate the query on auth readiness. `client.orgs()` is a session-scoped
|
|
@@ -525,8 +525,8 @@ type ActivityRow = {
|
|
|
525
525
|
*/
|
|
526
526
|
type SummaryWindow = "all" | "last30days" | ActivityRange;
|
|
527
527
|
/**
|
|
528
|
-
* Finished
|
|
529
|
-
*
|
|
528
|
+
* Finished valuation strings in the display currency. Unrated assets remain
|
|
529
|
+
* separate. All four strings are `null` and `count` is `0`
|
|
530
530
|
* when the window holds no activity — we say there is nothing, the app says it
|
|
531
531
|
* in its own words.
|
|
532
532
|
*/
|
|
@@ -537,6 +537,7 @@ interface ActivitySummarySlice {
|
|
|
537
537
|
readonly moneyOut: string | null;
|
|
538
538
|
readonly net: string | null;
|
|
539
539
|
readonly pending: string | null;
|
|
540
|
+
readonly unrated: readonly ActivitySummaryTotal[];
|
|
540
541
|
/** A count is not money, so it is not formatted. */
|
|
541
542
|
readonly count: number;
|
|
542
543
|
readonly isLoading: boolean;
|
|
@@ -600,9 +601,12 @@ interface ActivityLoadMoreSlice {
|
|
|
600
601
|
interface ActivityDetailSlice {
|
|
601
602
|
/** Current Payment state from the existing detail read; absent for Movements. */
|
|
602
603
|
readonly payment?: Pick<Payment, "status" | "execution">;
|
|
603
|
-
|
|
604
|
-
readonly
|
|
605
|
-
readonly
|
|
604
|
+
readonly documents: readonly PaymentDocumentRef[];
|
|
605
|
+
readonly selectedDocument: PaymentDocumentRef | null;
|
|
606
|
+
readonly selectDocument: (ref: PaymentDocumentReadRef) => void;
|
|
607
|
+
readonly renderDocument: () => Promise<PaymentDocumentRender>;
|
|
608
|
+
/** Return exact bytes on a deliberate action. The host owns the download URL. */
|
|
609
|
+
readonly downloadDocument: () => Promise<PaymentDocumentDownload>;
|
|
606
610
|
readonly isOpeningDocument: boolean;
|
|
607
611
|
readonly memo: string;
|
|
608
612
|
readonly setMemo: (text: string) => void;
|
|
@@ -708,7 +712,7 @@ type SentPayment = {
|
|
|
708
712
|
/** The backend payment row. */
|
|
709
713
|
readonly payment: Payment;
|
|
710
714
|
/** What we parsed and sent — `.value` is the comma-stripped major-unit string. */
|
|
711
|
-
readonly amount:
|
|
715
|
+
readonly amount: AssetAmount;
|
|
712
716
|
/** The trimmed text the person typed — the app's success toast interpolates it. */
|
|
713
717
|
readonly recipient: string;
|
|
714
718
|
};
|
|
@@ -741,7 +745,7 @@ interface SendMoneySourceSlice {
|
|
|
741
745
|
readonly isPending: boolean;
|
|
742
746
|
}
|
|
743
747
|
interface SendMoneyAssetOption {
|
|
744
|
-
readonly id:
|
|
748
|
+
readonly id: AssetId;
|
|
745
749
|
readonly symbol: string;
|
|
746
750
|
readonly availableDisplay: string;
|
|
747
751
|
}
|
|
@@ -1010,7 +1014,7 @@ interface PayrollRunAmountEntry {
|
|
|
1010
1014
|
/** True while the value is one we seeded and the employer has not edited. */
|
|
1011
1015
|
readonly prefilled: boolean;
|
|
1012
1016
|
/** The treasury asset's currency. Rows stay absent until this value exists. */
|
|
1013
|
-
readonly currency:
|
|
1017
|
+
readonly currency: string;
|
|
1014
1018
|
}
|
|
1015
1019
|
interface PayrollRunAmountsSlice {
|
|
1016
1020
|
readonly entries: readonly PayrollRunAmountEntry[];
|
package/dist/index.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
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, formatAssetAmount, formatMoney, isCapxulError, isClaimed, isClaimed as isClaimed$1, isMoneyParseError, isRestoring, isRestoring as isRestoring$1, parseMoney, paymentPhase,
|
|
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
|
/**
|
|
@@ -1141,15 +1141,8 @@ function useActivityEnabled() {
|
|
|
1141
1141
|
}
|
|
1142
1142
|
//#endregion
|
|
1143
1143
|
//#region src/headless/activity/detail.ts
|
|
1144
|
-
|
|
1145
|
-
|
|
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,
|
|
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
|
|
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
|
|
1196
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1232
|
+
pending.current.delete(controller);
|
|
1233
|
+
if (!controller.signal.aborted) setIsOpeningDocument(pending.current.size > 0);
|
|
1202
1234
|
}
|
|
1203
1235
|
}, [
|
|
1204
1236
|
client,
|
|
1205
|
-
|
|
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
|
-
|
|
1237
|
-
|
|
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,52 +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 `primaryCurrency` when
|
|
1287
|
-
* one is given, else the bucket with the largest absolute `net`, breaking a tie
|
|
1288
|
-
* on currency code ascending so the choice is stable across reloads.
|
|
1289
|
-
*/
|
|
1290
|
-
function pickBucket(totals, primaryCurrency) {
|
|
1291
|
-
if (primaryCurrency !== null) {
|
|
1292
|
-
const matched = totals.find((total) => total.currency === primaryCurrency);
|
|
1293
|
-
if (matched !== void 0) return matched;
|
|
1294
|
-
}
|
|
1295
|
-
let best = null;
|
|
1296
|
-
for (const total of totals) {
|
|
1297
|
-
if (best === null) {
|
|
1298
|
-
best = total;
|
|
1299
|
-
continue;
|
|
1300
|
-
}
|
|
1301
|
-
const order = compareNet(total, best);
|
|
1302
|
-
if (order > 0 || order === 0 && total.currency < best.currency) best = total;
|
|
1303
|
-
}
|
|
1304
|
-
return best;
|
|
1305
|
-
}
|
|
1306
1306
|
/**
|
|
1307
1307
|
* The current UTC day number, which advances ON ITS OWN at UTC midnight.
|
|
1308
1308
|
*
|
|
@@ -1346,42 +1346,36 @@ function useActivitySummary(actor, window) {
|
|
|
1346
1346
|
const params = {};
|
|
1347
1347
|
if (actor !== void 0) params.actor = actor;
|
|
1348
1348
|
if (range !== void 0) params.window = range;
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
queryKey: accountKey([...capxulKeys.holdings(actor), "primary"], identity),
|
|
1355
|
-
queryFn: async () => {
|
|
1356
|
-
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
|
+
};
|
|
1357
1354
|
},
|
|
1358
1355
|
enabled
|
|
1359
1356
|
});
|
|
1360
1357
|
const totals = summary.data?.totals;
|
|
1361
|
-
const isLoading = summary.isLoading
|
|
1362
|
-
|
|
1363
|
-
if (bucket === null) return {
|
|
1358
|
+
const isLoading = summary.isLoading;
|
|
1359
|
+
if (summary.data === void 0 || totals === void 0 || totals.length === 0 || isLoading) return {
|
|
1364
1360
|
currency: null,
|
|
1365
1361
|
moneyIn: null,
|
|
1366
1362
|
moneyOut: null,
|
|
1367
1363
|
net: null,
|
|
1368
1364
|
pending: null,
|
|
1365
|
+
unrated: [],
|
|
1369
1366
|
count: summary.data?.count ?? 0,
|
|
1370
1367
|
isLoading,
|
|
1371
1368
|
error: asCapxulError$1(summary.error),
|
|
1372
1369
|
...summary.data?.readWarning === void 0 ? {} : { readWarning: summary.data.readWarning }
|
|
1373
1370
|
};
|
|
1374
|
-
const
|
|
1375
|
-
currency: bucket.currency,
|
|
1376
|
-
value: majorUnits(raw, bucket.decimals),
|
|
1377
|
-
decimals: bucket.decimals
|
|
1378
|
-
});
|
|
1371
|
+
const valuation = summary.data.valuation;
|
|
1379
1372
|
return {
|
|
1380
|
-
currency:
|
|
1381
|
-
moneyIn: display(
|
|
1382
|
-
moneyOut: display(
|
|
1383
|
-
net: display(
|
|
1384
|
-
pending: display(
|
|
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,
|
|
1385
1379
|
count: summary.data?.count ?? 0,
|
|
1386
1380
|
isLoading,
|
|
1387
1381
|
error: asCapxulError$1(summary.error),
|
|
@@ -1408,7 +1402,7 @@ const SIGN = Object.freeze({
|
|
|
1408
1402
|
self: ""
|
|
1409
1403
|
});
|
|
1410
1404
|
function toActivityRow(item, actor) {
|
|
1411
|
-
const amountDisplay =
|
|
1405
|
+
const amountDisplay = formatAssetAmount(item.amount);
|
|
1412
1406
|
const signedAmountDisplay = `${SIGN[item.direction]}${amountDisplay}`;
|
|
1413
1407
|
if (item.kind === "movement") return {
|
|
1414
1408
|
kind: "movement",
|
|
@@ -1420,7 +1414,7 @@ function toActivityRow(item, actor) {
|
|
|
1420
1414
|
counterpartyLabel: item.counterpartyLabel,
|
|
1421
1415
|
amountDisplay,
|
|
1422
1416
|
signedAmountDisplay,
|
|
1423
|
-
currency: item.amount.
|
|
1417
|
+
currency: assetSymbolFor(item.amount.asset),
|
|
1424
1418
|
createdAt: item.createdAt,
|
|
1425
1419
|
classification: item.classification,
|
|
1426
1420
|
phase: MOVEMENT_PHASE,
|
|
@@ -1442,7 +1436,7 @@ function toActivityRow(item, actor) {
|
|
|
1442
1436
|
counterpartyLabel: item.counterpartyLabel,
|
|
1443
1437
|
amountDisplay,
|
|
1444
1438
|
signedAmountDisplay,
|
|
1445
|
-
currency: item.amount.
|
|
1439
|
+
currency: assetSymbolFor(item.amount.asset),
|
|
1446
1440
|
createdAt: item.createdAt,
|
|
1447
1441
|
ledgerKind: item.ledgerKind,
|
|
1448
1442
|
paymentType: item.paymentType,
|
|
@@ -1523,7 +1517,7 @@ function buildCsv(items, actor, search) {
|
|
|
1523
1517
|
row.counterpartyLabel,
|
|
1524
1518
|
row.direction,
|
|
1525
1519
|
item.amount.value,
|
|
1526
|
-
item.amount.
|
|
1520
|
+
assetSymbolFor(item.amount.asset),
|
|
1527
1521
|
row.kind === "payment" ? row.execution?.notSent ? "not_sent" : row.execution?.status === "unresolved" ? "verifying" : row.status : "",
|
|
1528
1522
|
row.reference.id,
|
|
1529
1523
|
row.receipt?.transactionHash ?? ""
|
|
@@ -1873,18 +1867,19 @@ function useOrgMeReading(orgId) {
|
|
|
1873
1867
|
//#endregion
|
|
1874
1868
|
//#region src/internal/write-names-asset.ts
|
|
1875
1869
|
/**
|
|
1876
|
-
* Identity law
|
|
1877
|
-
*
|
|
1878
|
-
*
|
|
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).
|
|
1879
1874
|
*
|
|
1880
1875
|
* A Capxul payment writer takes `PaymentMoney` — a peg currency, a value and
|
|
1881
1876
|
* `decimals` — and settles one fixed legacy token. No `AssetId` crosses that
|
|
1882
1877
|
* seam, so a payment composed from a named position cannot name the asset it
|
|
1883
1878
|
* moves: a position selected as USDT authorizes a different token.
|
|
1884
1879
|
*
|
|
1885
|
-
* `false` until
|
|
1886
|
-
*
|
|
1887
|
-
*
|
|
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.
|
|
1888
1883
|
*/
|
|
1889
1884
|
function writeNamesAsset() {
|
|
1890
1885
|
return false;
|
|
@@ -2054,7 +2049,6 @@ function useSelectedOption(options) {
|
|
|
2054
2049
|
function assetSelectionsFromAccount$1(account) {
|
|
2055
2050
|
if (account === null) return [];
|
|
2056
2051
|
return account.balances.map((position) => {
|
|
2057
|
-
const peg = positionPeg(position.asset);
|
|
2058
2052
|
return {
|
|
2059
2053
|
option: {
|
|
2060
2054
|
id: position.asset,
|
|
@@ -2064,10 +2058,9 @@ function assetSelectionsFromAccount$1(account) {
|
|
|
2064
2058
|
value: position.available
|
|
2065
2059
|
})
|
|
2066
2060
|
},
|
|
2067
|
-
money:
|
|
2068
|
-
|
|
2069
|
-
value: position.available
|
|
2070
|
-
decimals: position.decimals
|
|
2061
|
+
money: {
|
|
2062
|
+
asset: position.asset,
|
|
2063
|
+
value: position.available
|
|
2071
2064
|
}
|
|
2072
2065
|
};
|
|
2073
2066
|
});
|
|
@@ -2089,7 +2082,7 @@ function useSendMoney(input) {
|
|
|
2089
2082
|
const [selectedAssetOption, selectAsset] = useSelectedOption(assetOptions);
|
|
2090
2083
|
const selectedAssetMoney = useMemo(() => assetSelections.find((selection) => selection.option.id === selectedAssetOption?.id)?.money ?? null, [assetSelections, selectedAssetOption]);
|
|
2091
2084
|
const [amountText, setAmountText] = useState("");
|
|
2092
|
-
const parsed = useMemo(() => selectedAssetMoney === null || amountText.trim() === "" ? null :
|
|
2085
|
+
const parsed = useMemo(() => selectedAssetMoney === null || amountText.trim() === "" ? null : parseAssetAmount(amountText, requireAsset(selectedAssetMoney.asset)), [amountText, selectedAssetMoney]);
|
|
2093
2086
|
const parsedMoney = parsed !== null && !isMoneyParseError(parsed) ? parsed : null;
|
|
2094
2087
|
const amountError = parsed !== null && isMoneyParseError(parsed) ? parsed.reason : null;
|
|
2095
2088
|
const recipient = useRecipientResolution(client, JSON.stringify([
|
|
@@ -2538,11 +2531,6 @@ const CapxulDashboardAccess = Object.assign(Root$3, {
|
|
|
2538
2531
|
});
|
|
2539
2532
|
//#endregion
|
|
2540
2533
|
//#region src/headless/payroll/money.ts
|
|
2541
|
-
/** `{ value: "12.34", decimals: 6 }` → `12340000n`. */
|
|
2542
|
-
function toMinorUnits(money) {
|
|
2543
|
-
const [whole = "0", fraction = ""] = money.value.split(".");
|
|
2544
|
-
return BigInt(`${whole}${fraction.padEnd(money.decimals, "0")}`);
|
|
2545
|
-
}
|
|
2546
2534
|
const TYPED_AMOUNT = /^(-?)(\d+)(?:\.(\d+))?$/;
|
|
2547
2535
|
/**
|
|
2548
2536
|
* Σ of the employer's TYPED amounts on a saved group — a DISPLAY total.
|
|
@@ -2575,6 +2563,17 @@ function fromMinorUnits(raw, asset) {
|
|
|
2575
2563
|
decimals: asset.decimals
|
|
2576
2564
|
};
|
|
2577
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
|
+
}
|
|
2578
2577
|
//#endregion
|
|
2579
2578
|
//#region src/headless/payroll/reads.ts
|
|
2580
2579
|
function toRecipientOption(entry) {
|
|
@@ -2691,7 +2690,7 @@ function scopedClient(client, method, orgId) {
|
|
|
2691
2690
|
//#endregion
|
|
2692
2691
|
//#region src/headless/payroll/use-payroll.ts
|
|
2693
2692
|
/** Money reads the same way everywhere on this screen: amount then code. */
|
|
2694
|
-
const GRAMMAR
|
|
2693
|
+
const GRAMMAR = { grammar: "code" };
|
|
2695
2694
|
/**
|
|
2696
2695
|
* One captured snapshot, or nothing — never a fabricated empty options object,
|
|
2697
2696
|
* and never a DOM event a screen wired straight to `onClick`.
|
|
@@ -2707,7 +2706,7 @@ function toPayrollGroupRow(group) {
|
|
|
2707
2706
|
name: group.name,
|
|
2708
2707
|
tone: group.tone,
|
|
2709
2708
|
memberCount: group.members.length,
|
|
2710
|
-
totalDisplay: currency === void 0 ? "" : formatMoney(sumTypedAmounts(group.members.map((member) => member.amount), currency), GRAMMAR
|
|
2709
|
+
totalDisplay: currency === void 0 ? "" : formatMoney(sumTypedAmounts(group.members.map((member) => member.amount), currency), GRAMMAR),
|
|
2711
2710
|
members: group.members.map((member) => ({
|
|
2712
2711
|
recipientId: member.partyId,
|
|
2713
2712
|
amount: member.amount,
|
|
@@ -2797,8 +2796,8 @@ function usePayroll(orgId) {
|
|
|
2797
2796
|
const started = orgId !== void 0;
|
|
2798
2797
|
return {
|
|
2799
2798
|
summary: {
|
|
2800
|
-
walletBalanceDisplay: treasury.data === void 0 ? null : formatMoney(treasury.data.available, GRAMMAR
|
|
2801
|
-
paidThisMonthDisplay: runs.data === void 0 ? null :
|
|
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(", "),
|
|
2802
2801
|
isLoading: started && (treasury.data === void 0 && treasury.error === null || runs.data === void 0 && runs.error === null),
|
|
2803
2802
|
error: treasury.error ?? runs.error
|
|
2804
2803
|
},
|
|
@@ -2882,8 +2881,9 @@ function decodeDraft(value, scope) {
|
|
|
2882
2881
|
const items = input.items.map((raw) => {
|
|
2883
2882
|
const item = record(raw);
|
|
2884
2883
|
const to = record(item.to);
|
|
2884
|
+
const amount = record(item.amount);
|
|
2885
2885
|
const partyId = toPartyId(identifier(item.partyId));
|
|
2886
|
-
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");
|
|
2887
2887
|
const net = minorAmount(item.net);
|
|
2888
2888
|
return {
|
|
2889
2889
|
to: {
|
|
@@ -2891,12 +2891,20 @@ function decodeDraft(value, scope) {
|
|
|
2891
2891
|
partyId
|
|
2892
2892
|
},
|
|
2893
2893
|
partyId,
|
|
2894
|
+
amount: {
|
|
2895
|
+
asset: requireAsset(amount.asset).assetId,
|
|
2896
|
+
value: amount.value
|
|
2897
|
+
},
|
|
2894
2898
|
gross: net,
|
|
2895
2899
|
net,
|
|
2900
|
+
currency: item.currency,
|
|
2901
|
+
decimals: item.decimals,
|
|
2896
2902
|
adjustments: []
|
|
2897
2903
|
};
|
|
2898
2904
|
});
|
|
2899
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");
|
|
2900
2908
|
return {
|
|
2901
2909
|
version: 1,
|
|
2902
2910
|
scope,
|
|
@@ -2907,6 +2915,7 @@ function decodeDraft(value, scope) {
|
|
|
2907
2915
|
},
|
|
2908
2916
|
input: {
|
|
2909
2917
|
permissionId: identifier(input.permissionId),
|
|
2918
|
+
asset: exactAsset.assetId,
|
|
2910
2919
|
period: {
|
|
2911
2920
|
start: period.start,
|
|
2912
2921
|
end: period.start
|
|
@@ -3110,7 +3119,6 @@ function termsSeeds(terms, asset, period) {
|
|
|
3110
3119
|
}
|
|
3111
3120
|
//#endregion
|
|
3112
3121
|
//#region src/headless/payroll/use-payroll-run.ts
|
|
3113
|
-
const GRAMMAR = { grammar: "code" };
|
|
3114
3122
|
/**
|
|
3115
3123
|
* The period this composer composes: an INSTANT run (ruling Q3). `authorizeRun`
|
|
3116
3124
|
* stamps `start === end === runAt` at submit, and the payslip's period renders
|
|
@@ -3134,6 +3142,32 @@ const INSTANT_RUN_PERIOD = {
|
|
|
3134
3142
|
function amountText(amounts, seeds, recipientId) {
|
|
3135
3143
|
return amounts[recipientId]?.value ?? seeds.get(recipientId) ?? "";
|
|
3136
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
|
+
}
|
|
3137
3171
|
/**
|
|
3138
3172
|
* WHICH run the composer is composing, by MEANING rather than by object shape.
|
|
3139
3173
|
*
|
|
@@ -3176,7 +3210,6 @@ function initialAmounts(prefill) {
|
|
|
3176
3210
|
function assetSelectionsFromAccount(account) {
|
|
3177
3211
|
if (account === null) return [];
|
|
3178
3212
|
return account.balances.map((position) => {
|
|
3179
|
-
const peg = positionPeg(position.asset);
|
|
3180
3213
|
return {
|
|
3181
3214
|
option: {
|
|
3182
3215
|
id: position.asset,
|
|
@@ -3186,10 +3219,9 @@ function assetSelectionsFromAccount(account) {
|
|
|
3186
3219
|
value: position.available
|
|
3187
3220
|
})
|
|
3188
3221
|
},
|
|
3189
|
-
money:
|
|
3190
|
-
|
|
3191
|
-
value: position.available
|
|
3192
|
-
decimals: position.decimals
|
|
3222
|
+
money: {
|
|
3223
|
+
asset: position.asset,
|
|
3224
|
+
value: position.available
|
|
3193
3225
|
}
|
|
3194
3226
|
};
|
|
3195
3227
|
});
|
|
@@ -3207,17 +3239,13 @@ function usePayrollRun(input) {
|
|
|
3207
3239
|
const terms = useOrgTerms(orgId);
|
|
3208
3240
|
const [selected, setSelected] = useState(() => prefill?.recipientIds ?? []);
|
|
3209
3241
|
const [amounts, setAmounts] = useState(() => initialAmounts(prefill));
|
|
3210
|
-
const restoreCurrency = treasury.data?.available.currency;
|
|
3211
3242
|
const { draftScope, currentScope, draft, draftError, restorationPending, activateDraft, restoreDraft, setDraft, recovery, draftBlocked } = useRunDraft(client, orgId, useCallback((restored) => {
|
|
3212
3243
|
setSelected(restored.input.items.map((item) => item.partyId));
|
|
3213
|
-
if (
|
|
3214
|
-
value:
|
|
3215
|
-
currency: restoreCurrency,
|
|
3216
|
-
decimals: restored.asset.decimals
|
|
3217
|
-
}).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,
|
|
3218
3246
|
prefilled: false
|
|
3219
3247
|
}])));
|
|
3220
|
-
}, [
|
|
3248
|
+
}, []));
|
|
3221
3249
|
const frozen = draft !== null;
|
|
3222
3250
|
const composerKey = composerIdentity(orgId, prefill, draftScope, frozen);
|
|
3223
3251
|
const [seededKey, setSeededKey] = useState(composerKey);
|
|
@@ -3279,12 +3307,19 @@ function usePayrollRun(input) {
|
|
|
3279
3307
|
}
|
|
3280
3308
|
return resolved;
|
|
3281
3309
|
}, [refs, selected]);
|
|
3282
|
-
const seeds = useMemo(() =>
|
|
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]);
|
|
3283
3318
|
const entries = useMemo(() => {
|
|
3284
3319
|
if (assetMoney === null) return [];
|
|
3285
3320
|
return selected.map((recipientId) => {
|
|
3286
3321
|
const value = amountText(amounts, seeds, recipientId);
|
|
3287
|
-
const parsed = value.trim() === "" ? null :
|
|
3322
|
+
const parsed = value.trim() === "" ? null : parseAssetAmount(value, requireAsset(assetMoney.asset));
|
|
3288
3323
|
return {
|
|
3289
3324
|
recipientId,
|
|
3290
3325
|
name: named.get(recipientId)?.name ?? recipientId,
|
|
@@ -3292,7 +3327,7 @@ function usePayrollRun(input) {
|
|
|
3292
3327
|
change: (text) => change(recipientId, text),
|
|
3293
3328
|
error: parsed !== null && isMoneyParseError(parsed) ? parsed.reason : null,
|
|
3294
3329
|
prefilled: amounts[recipientId]?.prefilled ?? seeds.has(recipientId),
|
|
3295
|
-
currency: assetMoney.
|
|
3330
|
+
currency: requireAsset(assetMoney.asset).symbol
|
|
3296
3331
|
};
|
|
3297
3332
|
});
|
|
3298
3333
|
}, [
|
|
@@ -3307,7 +3342,7 @@ function usePayrollRun(input) {
|
|
|
3307
3342
|
if (assetMoney === null || selected.length === 0) return null;
|
|
3308
3343
|
const money = [];
|
|
3309
3344
|
for (const recipientId of selected) {
|
|
3310
|
-
const parsed =
|
|
3345
|
+
const parsed = parseAssetAmount(amountText(amounts, seeds, recipientId), requireAsset(assetMoney.asset));
|
|
3311
3346
|
if (isMoneyParseError(parsed)) return null;
|
|
3312
3347
|
money.push(parsed);
|
|
3313
3348
|
}
|
|
@@ -3318,17 +3353,25 @@ function usePayrollRun(input) {
|
|
|
3318
3353
|
seeds,
|
|
3319
3354
|
selected
|
|
3320
3355
|
]);
|
|
3321
|
-
const total = useMemo(() => parsedAmounts === null || assetMoney === null ? null :
|
|
3322
|
-
|
|
3323
|
-
|
|
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));
|
|
3324
3365
|
const funds = availableRaw === null || totalRaw === null ? "unknown" : totalRaw > availableRaw ? "insufficient" : "available";
|
|
3325
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();
|
|
3326
3369
|
const blockedReason = firstFalseFact({
|
|
3327
3370
|
signerReady: signerStatus === "ready",
|
|
3328
3371
|
...runAuthorityFacts(draft, budgets, selectedAsset, assetMoney, funds),
|
|
3329
3372
|
recipientCount: payableRefs === null ? 0 : selected.length,
|
|
3330
|
-
amountsParse: total !== null,
|
|
3331
|
-
writeNamesAsset:
|
|
3373
|
+
amountsParse: total !== null && hasRequiredCompensation(frozen, writerNamesSelectedAsset, selected, compensationTerms),
|
|
3374
|
+
writeNamesAsset: writerNamesSelectedAsset
|
|
3332
3375
|
});
|
|
3333
3376
|
const authorize = useMutation({
|
|
3334
3377
|
mutationFn: async ({ candidate, options }) => {
|
|
@@ -3350,7 +3393,7 @@ function usePayrollRun(input) {
|
|
|
3350
3393
|
if (candidate.scope === currentScope.current) restoreDraft(candidate.scope);
|
|
3351
3394
|
}
|
|
3352
3395
|
});
|
|
3353
|
-
const totalDisplay = total === null ? null :
|
|
3396
|
+
const totalDisplay = total === null ? null : formatAssetAmount(total);
|
|
3354
3397
|
const authorizeMutate = authorize.mutate;
|
|
3355
3398
|
const submit = useCallback((options) => {
|
|
3356
3399
|
const attempt = toInvocationOptions(options);
|
|
@@ -3359,41 +3402,35 @@ function usePayrollRun(input) {
|
|
|
3359
3402
|
if (permissionId === void 0 || totalDisplay === null || parsedAmounts === null) return;
|
|
3360
3403
|
if (payableRefs === null) return;
|
|
3361
3404
|
const recipientCount = selected.length;
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
const
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
kind: "party",
|
|
3371
|
-
partyId: toPartyId(recipientId)
|
|
3372
|
-
},
|
|
3373
|
-
partyId: recipientId,
|
|
3374
|
-
gross: raw,
|
|
3375
|
-
net: raw,
|
|
3376
|
-
adjustments: []
|
|
3405
|
+
let candidate;
|
|
3406
|
+
if (frozen) candidate = draft;
|
|
3407
|
+
else {
|
|
3408
|
+
const items = freshRunItems({
|
|
3409
|
+
selected,
|
|
3410
|
+
payableRefs,
|
|
3411
|
+
amounts: parsedAmounts,
|
|
3412
|
+
compensationTerms
|
|
3377
3413
|
});
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
decimals: assetMoney.decimals
|
|
3387
|
-
},
|
|
3388
|
-
input: {
|
|
3389
|
-
permissionId,
|
|
3390
|
-
period: {
|
|
3391
|
-
start: runAt,
|
|
3392
|
-
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
|
|
3393
3422
|
},
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3423
|
+
input: {
|
|
3424
|
+
permissionId,
|
|
3425
|
+
asset: selectedAsset.id,
|
|
3426
|
+
period: {
|
|
3427
|
+
start: runAt,
|
|
3428
|
+
end: runAt
|
|
3429
|
+
},
|
|
3430
|
+
items
|
|
3431
|
+
}
|
|
3432
|
+
};
|
|
3433
|
+
}
|
|
3397
3434
|
submitGate.current = true;
|
|
3398
3435
|
authorizeMutate({
|
|
3399
3436
|
candidate,
|
|
@@ -3425,6 +3462,7 @@ function usePayrollRun(input) {
|
|
|
3425
3462
|
selectedAsset,
|
|
3426
3463
|
blockedReason,
|
|
3427
3464
|
budgets,
|
|
3465
|
+
compensationTerms,
|
|
3428
3466
|
onFailed,
|
|
3429
3467
|
onRun,
|
|
3430
3468
|
parsedAmounts,
|
|
@@ -3473,13 +3511,20 @@ function usePayrollRun(input) {
|
|
|
3473
3511
|
};
|
|
3474
3512
|
}
|
|
3475
3513
|
function remainingBalanceDisplay(available, total, asset) {
|
|
3476
|
-
|
|
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;
|
|
3477
3522
|
}
|
|
3478
3523
|
/** Persisted replay keeps its authorized Budget and asset; a prior debit cannot block its own recovery. */
|
|
3479
3524
|
function runAuthorityFacts(draft, budgets, asset, money, funds) {
|
|
3480
3525
|
return {
|
|
3481
3526
|
budgetCount: draft !== null && !budgets.some((budget) => budget.id === draft.input.permissionId) ? 0 : budgets.length,
|
|
3482
|
-
hasAsset: asset !== null && (draft === null || draft.asset.id === asset.id && draft.asset
|
|
3527
|
+
hasAsset: asset !== null && (draft === null || draft.asset.id === asset.id && draft.input.asset === money?.asset),
|
|
3483
3528
|
funds: draft?.input.requestKey === void 0 ? funds : "available"
|
|
3484
3529
|
};
|
|
3485
3530
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@capxul/sdk-react",
|
|
3
|
-
"version": "4.20.0-beta.
|
|
3
|
+
"version": "4.20.0-beta.6",
|
|
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": "4.20.0-beta.
|
|
29
|
+
"@capxul/sdk": "4.20.0-beta.6"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@tanstack/react-query": "^5.66.9",
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
"vite": "npm:@voidzero-dev/vite-plus-core@0.3.0",
|
|
44
44
|
"vite-plus": "0.3.0",
|
|
45
45
|
"vitest": "4.1.11",
|
|
46
|
+
"@capxul/config": "0.4.0-beta.0",
|
|
46
47
|
"@capxul/errors": "0.3.0",
|
|
47
48
|
"@capxul/types": "0.3.0",
|
|
48
49
|
"@capxul/typescript-config": "0.0.0"
|