spartaxx-documents
v1.0.2
Published
Spartaxx client Documents module (Required / Submitted / Upload) — reusable across all Spartaxx internal applications
Maintainers
Readme
spartaxx-documents
Client Documents module — Required / Submitted / Upload — extracted from the Customer Service V2 web app so any Spartaxx internal application can mount it.
The package owns no endpoint knowledge. Every URL, controller, method, query id and package id is passed in by the host, so a second app points it at its own environment instead of forking the code.
Installation
npm install spartaxx-documentsPeer dependencies (most hosts already have all of these):
npm install react react-dom axios formik yup uuid \
react-bootstrap react-select react-toastify react-icons react-lottie \
lucide-react react-infinite-scroll-componentreact-lottie is optional — it is only used by the "No Signature Required" empty state.
Bootstrap 5 CSS must be loaded by the host; the module uses Bootstrap utility classes.
Import the stylesheet once, anywhere in your app:
import "spartaxx-documents/styles.css";Quick start
import { DocumentsModule, type DocumentsConfig } from "spartaxx-documents";
import "spartaxx-documents/styles.css";
const documentsConfig: DocumentsConfig = {
api: {
baseUrl: process.env.REACT_APP_API_BASE_URL!,
pfbaBaseUrl: process.env.REACT_APP_PFBA_API_BASE_URL!,
paperwiseBaseUrl: process.env.REACT_APP_PAPERWISE_API_BASE_URL!,
clientPackagePortalUrl: process.env.REACT_APP_CLIENT_PACKAGE_PORTAL_BASE_URL,
},
controllers: {
dynamicQuery: "api/DynamicQuery",
clientPackage: "clientpackage/clientpackage",
paperwiseDocuments: "pwdocuments",
hub: "hub/hub",
hubShort: "hub",
},
methods: {
executeQuery: "ExecuteQuery",
buildClientPackage: "BuildClientPackage",
previewClientPackage: "PreviewClientPackage",
downloadClientPackage: "DownloadClientPackage",
sendClientPackageEmail: "SendClientPackageEmail",
sendClientPackageEmailPdf: "SendClientPackageEmailPdf",
sendClientPackageUsMail: "SendClientPackageUsMail",
sendClientPackageSms: "SendClientPackageSms",
getPwDocuments: "GetPWDocuments",
uploadPaperwiseDocument: "UploadPWDocuments",
encrypt: "encrypt",
},
queryIds: {
taxYears: 87,
properties: 269,
documentTypes: 96,
correspondenceLog: 145,
},
};
export const ClientDocuments = ({ rowData }) => (
<DocumentsModule config={documentsConfig} rowData={rowData} />
);That is the minimum. rowData may be omitted, in which case the client is decoded from the
?nekoT= URL token, exactly as the Customer Service app does today.
Two variants
| Variant | Use it for | Renders |
|---|---|---|
| variant="embedded" (default) | Mounting inside an existing page shell | bg-white rounded-3 shadow-sm p-1 documents-shell |
| variant="iframe" | A standalone full-page route loaded in an iframe | Full-height shell, plus postMessage and query-param context |
The iframe variant accepts context by whichever of these arrives first:
postMessage({ type: "CS_DOCUMENTS_INIT", payload })from the parent frame (the constant is exported asDOCUMENTS_INIT_MESSAGE);?rowData=/?cookieResponse=URI-encoded JSON params;- the
?nekoT=token.
Query string and hash are both checked, since some hosts route by hash. Pass
allowedOrigins to restrict which parents may post context — the original module accepted
messages from any origin.
Prop reference
DocumentsModule
| Prop | Type | Default | Notes |
|---|---|---|---|
| config | DocumentsConfig | required | Endpoints, ids. See below. |
| variant | "embedded" \| "iframe" | "embedded" | |
| rowData | DocumentsRowData \| null | decoded from token | ClientId / ClientNumber; either casing accepted. |
| session | DocumentsSession \| null | resolved by adapter | Pre-resolved user. Supplies UserId and Email. |
| sessionAdapter | DocumentsSessionAdapter | URL-token adapter | How to decode the token / resolve the user. |
| token | string | ?nekoT= from the URL | Explicit token override. |
| initialTab | "Required" \| "Submitted" \| "Upload" | "Required" | |
| logger | DocumentsLogger | no-op | Same call shape as CS LogService. |
| renderCorrespondence | ((rowData) => ReactNode) | false | built-in log | Omit for the built-in correspondence log. Pass a function to substitute your own, or false to hide the link. |
| onSendToClient | (docs: SubmittedDocument[]) => Promise<void> \| void | — | Submitted tab's "Send to Client". See caveat below. |
| onUploadSuccess | () => void | — | Fires after the module has already refreshed Submitted. |
| allowedOrigins | string[] | any origin | variant="iframe" only. |
| client | DocumentsClient | built from config | Reuse one client across mounts. |
config
| Field | Purpose |
|---|---|
| api.baseUrl | Client-package + hub API: preview, download, send, encrypt. |
| api.pfbaBaseUrl | Dynamic-query API: tax years, properties, document types, logs. |
| api.paperwiseBaseUrl | Paperwise: submitted-document listing and upload. |
| api.clientPackagePortalUrl | Root used to build the signing URL. |
| api.clientPortalApiBaseUrl | Optional; only getRequiredDocumentUrl uses it. |
| api.requiredDocumentUrl | Fallback URL for the same call. |
| controllers.*, methods.* | Path segments, joined as {base}/{controller}/{method}. |
| queryIds.* | The six dynamic queries the module runs. |
| paperwise.fileCabinetId | Fallback Paperwise cabinet. UAT 1015, production 1001. Correspondence rows carry their own FileCabinetId and that wins, so this only covers rows that omit it. Default 1001. |
| paperwise.environmentId | Paperwise environment. Default "1001". |
| packageIds.requiredDocuments | Parent package for the Required tree. Default 450314. |
| packageIds.clientPackage | Parent package for preview/send. Default 450337. |
| defaultTaxYear | Pre-selected year. Defaults to the current year. |
| uploadIpAddress | Stamped on upload payloads. Defaults to "0.0.0.0". |
| uploadBatchCodePrefix | Batch code prefix. Defaults to "CSV2_Upload_". |
Environment-specific values.
packageIdsandpaperwise.fileCabinetIddiffer per environment: the Customer Service source carriedUAT 450337 / PROD 450333as a comment, and the Paperwise cabinet is1015in UAT versus1001in production. Set both explicitly per environment rather than relying on the defaults, which are the production values.PAPERWISE_FILE_CABINET_IDSis exported for convenience.
Correspondence log
The Required tab's Correspondence Logs link opens the client's send history: what went out, by which channel, to whom, the accounts behind each send, the stored Paperwise document, and a Resend action that rebuilds the package and re-emails it.
This is built in — no host wiring needed. It is also usable on its own:
import { CorrespondenceLogs, DocumentsProvider } from "spartaxx-documents";
<DocumentsProvider config={documentsConfig}>
<CorrespondenceLogs rowData={rowData} mode="embedded" />
</DocumentsProvider>| Prop | Type | Default | Notes |
|---|---|---|---|
| rowData | DocumentsRowData | null | required | Needs ClientId and ClientNumber. |
| mode | "drawer" | "embedded" | "drawer" | "embedded" wraps it in a titled card. |
| session | DocumentsSession | null | resolved by adapter | Supplies the resending user. |
| initialLogs | CorrespondenceLogEntry[] | — | Seed the first page to skip a request. Replaces the Redux cache the CS version read from. |
To substitute your own view, or remove the link entirely:
<DocumentsModule config={cfg} renderCorrespondence={(rowData) => <MyLogs {...rowData} />} />
<DocumentsModule config={cfg} renderCorrespondence={false} />Styling note
The log keeps the original Tailwind utility markup, which in the Customer Service app only
renders because public/index.html loads the Tailwind Play CDN. To avoid pushing that
dependency onto every consumer, the package ships the exact utility subset it needs,
scoped under .spartaxx-correspondence so it cannot restyle a host's own .flex or
.bg-white. It is generated, not hand-maintained:
npm run build:correspondence-cssRe-run that after changing any className in CorrespondenceLogs.tsx. Tailwind's
preflight is off, so no global reset is injected into the host.
Session and logging
The module never imports the host's auth code. It asks a DocumentsSessionAdapter:
interface DocumentsSessionAdapter {
decodeToken?: (token: string) => unknown | Promise<unknown>;
getSession?: () => DocumentsSession | null | Promise<DocumentsSession | null>;
}The default adapter reproduces the Spartaxx flow: base64-decode ?nekoT=, take its Guid,
read the cookie stored under that GUID. Supply your own to plug in a different scheme:
<DocumentsModule
config={documentsConfig}
sessionAdapter={{
decodeToken: (token) => myDecode(token),
getSession: () => store.getState().auth.user,
}}
/>Or skip it entirely by passing rowData and session as props.
Logging is the same idea — an all-optional interface shaped like CS LogService, so that app
can pass its logger straight through:
<DocumentsModule config={documentsConfig} logger={LogService} />Headless use
Every API call is available without React:
import { createDocumentsClient, constructPackagePayload } from "spartaxx-documents";
const client = createDocumentsClient(documentsConfig, logger);
const years = await client.getTaxYears();
const tree = await client.buildRequiredDocuments({ clientId, clientNumber, userId });
await client.sendClientPackage("email", constructPackagePayload({ /* … */ }));constructPackagePayload is pure — no config, no network — so hosts that assemble client
packages elsewhere can import it on its own.
Error contract, kept identical to the original module so the UI behaves the same:
- dynamic-query reads swallow failures, report via
logger.logError, and resolve to[](a failed read shows as an empty list, not an error); - writes and package operations reject, so callers can toast.
What changed in the extraction
Behaviour is a faithful port. These are the deliberate differences:
| Change | Why |
|---|---|
| react-router-dom is no longer needed | The tabs read the token from window.location instead of useSearchParams. Nothing is lost: the host patches the token with history.replaceState, which never notified react-router anyway. Pass token if you need reactivity. |
| Correspondence is a render slot | Keeps the 990-line CorrespondenceDrawer and its own config couplings out of the package. The link disappears if you don't pass renderCorrespondence. |
| defaultTaxYear defaults to the current year | The original hardcoded 2026 in five places. |
| ipAddress defaults to "0.0.0.0" | The original shipped a hardcoded office IP. Set uploadIpAddress to restore a real value. |
| Parent package ids are config | Were hardcoded 450314 / 450337 inline. |
| One shared IndeterminateCheckbox | Was duplicated in the Required and Submitted tabs. |
| Inline style objects became CSS classes | Overridable by hosts; see the classes appended to documents.css. |
| Correspondence date-range picker removed | Unreachable in the original: setIsOpen(true) lived only in handleOpen, which was never called, so it could never open — and the query hardcoded DeliveryFromDate/DeliveryToDate to null regardless. Dropping it also removes the react-datepicker dependency and ~150 lines of picker CSS overrides. |
| Correspondence document-viewer modal removed | Sat behind {false && …} with a hardcoded iframe URL. |
| Correspondence Redux cache became initialLogs | The package carries no Redux dependency. |
| Paperwise cabinet is taken from the row | The original hardcoded fileCabinetId: 1001 — the production cabinet — while running in UAT, where it is 1015, so document opens were requesting the wrong cabinet there. The client now prefers the FileCabinetId the correspondence row itself carries, falling back to config.paperwise.fileCabinetId. |
| Dropped dead code | escAgentList (read from sessionStorage, never used), preparedFileBase64, preparedFileSignature, getFileSignature, and the four unused service exports. |
| getSubmittedDocuments etc. take config | Were module-level constants read from the host's apiConfig. |
Known caveat carried over: the Submitted tab's "Send to Client" button had no API call in
the original — it only showed a success toast. That is preserved. Pass onSendToClient to
give it real behaviour; without it, the toast still appears and nothing is sent.
Wiring this into the Customer Service web app
The web app still contains its own copy at web/src/components/Documents. Nothing in
web/ has been changed — this package stands alone. To switch it over:
1. Install the package
Build a tarball and install that. Prefer this over npm link — see the Testing section
for why.
cd packages/spartaxx-documents
npm run build && npm pack # -> spartaxx-documents-1.0.0.tgz
cd ../../web
npm install ../packages/spartaxx-documents/spartaxx-documents-1.0.0.tgz2. Import the stylesheet once
In web/src/index.tsx:
import "spartaxx-documents/styles.css";3. Map apiConfig onto DocumentsConfig
Every key below already exists in web/src/config/apiConfig.ts, so this is a
straight copy. Save it as web/src/config/documentsConfig.ts:
import apiConfig from "./apiConfig";
import type { DocumentsConfig } from "spartaxx-documents";
export const documentsConfig: DocumentsConfig = {
api: {
baseUrl: apiConfig.api.baseUrl,
pfbaBaseUrl: apiConfig.api.pfbaBaseUrl,
paperwiseBaseUrl: apiConfig.api.paperwiseBaseUrl,
clientPortalApiBaseUrl: apiConfig.api.clientPortalApiBaseUrl,
clientPackagePortalUrl: apiConfig.urls.clientPackagePortal,
requiredDocumentUrl: apiConfig.urls.requiredDocument,
},
controllers: {
dynamicQuery: apiConfig.api.controllers.dynamicQuery,
clientPackage: apiConfig.api.controllers.clientPackage,
paperwiseDocuments: apiConfig.api.controllers.paperwiseDocuments,
hub: apiConfig.api.controllers.hub,
hubShort: apiConfig.api.controllers.hubShort,
},
methods: {
executeQuery: apiConfig.api.methods.executeQuery,
buildClientPackage: apiConfig.api.methods.buildClientPackage,
previewClientPackage: apiConfig.api.methods.previewClientPackage,
downloadClientPackage: apiConfig.api.methods.downloadClientPackage,
sendClientPackageEmail: apiConfig.api.methods.sendClientPackageEmail,
sendClientPackageEmailPdf: apiConfig.api.methods.sendClientPackageEmailPdf,
sendClientPackageUsMail: apiConfig.api.methods.sendClientPackageUsMail,
sendClientPackageSms: apiConfig.api.methods.sendClientPackageSms,
getPwDocuments: apiConfig.api.methods.getPwDocuments,
uploadPaperwiseDocument: apiConfig.api.methods.uploadPaperwiseDocument,
getPwDocumentFile: apiConfig.api.methods.getPwDocumentFile,
encrypt: apiConfig.api.methods.encrypt,
},
queryIds: {
taxYears: apiConfig.api.queryIds.q87,
properties: apiConfig.api.queryIds.q269,
documentTypes: apiConfig.api.queryIds.q96,
correspondenceLog: apiConfig.api.queryIds.q145,
correspondenceAccounts: apiConfig.api.queryIds.q146,
servicePackages: apiConfig.api.queryIds.q149,
},
// Environment-specific — see the warning under the config table.
packageIds: { requiredDocuments: 450314, clientPackage: 450337 },
paperwise: { fileCabinetId: 1015, environmentId: "1001" }, // 1015 UAT / 1001 prod
defaultTaxYear: 2026,
};4. Prove it on a throwaway route first
Add a temporary route before touching the real mounts:
import { DocumentsModule } from "spartaxx-documents";
import { documentsConfig } from "./config/documentsConfig";
import LogService from "./services/logger";
<Route
path="/documents-test"
element={<DocumentsModule config={documentsConfig} logger={LogService} />}
/>Open it with a real ?nekoT= token from a live Customer Service session — the module
decodes the client from it, exactly as the current code does.
5. Swap the real mounts
Replace <Documents /> and the /cs-documents route's <IframeDocuments /> with
<DocumentsModule config={documentsConfig} logger={LogService} /> and
<DocumentsModule variant="iframe" config={documentsConfig} logger={LogService} />.
The correspondence log is built in, so no renderCorrespondence is needed.
6. Repoint the two outside consumers
These import the module's service directly and will break when the folder is deleted:
web/src/components/Overview/Overview.tsx→buildClientPackageweb/src/Utills/RightSidebar/drawers/CorrespondenceDrawer.tsx→buildClientPackage,constructPackagePayload
constructPackagePayload is exported directly; buildClientPackage comes off
createDocumentsClient(documentsConfig).
7. Delete the originals
web/src/components/Documents and web/src/common/constants/deliveryOptions.ts (folded
into the package as deliveryOptions + client.sendClientPackage). Optionally also
web/src/Utills/RightSidebar/drawers/CorrespondenceDrawer.tsx once nothing else uses it —
note the drawer is still referenced by RightDrawerContent.tsx for the standalone
correspondence panel, so check before removing it.
Testing
Three levels, cheapest first. Run them in this order when validating a change.
1. Static — types and public surface
npm run type-check # tsc --noEmit over src/, strict
npm run build # then:
npm run verify:dist # compiles .verify/consumer.tsx against the built .d.tsverify:dist is the one that catches a broken public API — it exercises
DocumentsModule, DocumentsProvider, both variants, a custom session adapter and the
headless client, all through the package's own entry point.
2. Interactive — the playground
npm run dev # http://localhost:5174Enter a ClientId and ClientNumber from a real UAT client, press Mount module, and
click through all three tabs. The toolbar also toggles the embedded/iframe variants and
renderCorrespondence, so you can confirm the Correspondence Logs link disappears when no
renderer is passed. Every logger call is echoed to the browser console.
Two things the playground does deliberately:
- API calls are proxied through the dev server (
/uat-hub,/uat-cs,/uat-pw→ the UAT hosts), so CORS never applies. Retarget withPLAYGROUND_HUB_API,PLAYGROUND_CS_API,PLAYGROUND_PW_APIenv vars. - Session comes from the form, not a cookie. The default adapter reads a cookie keyed by
the token's GUID, and that cookie is scoped to the Customer Service domain — it does not
exist on localhost. Passing
rowDataandsessionas props is the supported way to bypass token resolution, and this is what a host app with its own auth would do.
It imports from src/, so edits hot-reload.
3. Integration — install it into a host app
Build a real tarball and install that. Prefer this over npm link for the CRA-based
web app: npm link exposes this package's own node_modules, which contains a second
copy of React, and Webpack will then fail at runtime with "Invalid hook call / more than one
copy of React". A tarball install pulls no devDependencies, so the duplicate cannot occur.
# in packages/spartaxx-documents
npm run build && npm pack # -> spartaxx-documents-1.0.0.tgz
# in web/
npm install ../packages/spartaxx-documents/spartaxx-documents-1.0.0.tgzThen import the stylesheet once, build a DocumentsConfig from apiConfig, and render
<DocumentsModule … /> on a throwaway route before touching the real ones. See
Wiring this into the Customer Service web app above.
To re-test after a code change: npm run build && npm pack again, then re-run the
npm install line in web/ (npm caches by path, so reinstalling the same filename does
pick up the new contents).
Development
npm install
npm run type-check # tsc --noEmit, strict
npm run build # vite lib build -> dist/{index.js,index.mjs,index.d.ts,style.css}.verify/ and playground/ are development-only: both are excluded from src/, from
npm run type-check, and from the published files (the tarball is dist/ + README, 27
files).
dist/index.esm.js is intentionally not whitespace-minified — Vite preserves
/* @__PURE__ */ annotations in lib mode so the consuming bundler can tree-shake. The CJS
The ESM build is named .esm.js, not .mjs, on purpose: Webpack applies strict-ESM
fullySpecified resolution to .mjs, which breaks bare subpath imports such as
react-icons/fi (react-icons v4 ships no exports map). A .js file in a package without
"type": "module" is resolved non-strictly, and bundlers still parse it as ESM because they
follow the module/exports.import fields. Renaming it back to .mjs breaks
react-scripts build in the Customer Service app.
build is fully minified.
