- Add src/core/ (config, errors, events, http, observability, storage, query) - Add src/Spreadsheet/ engine with native POI backend and Excel bridge - Add Envelope C spreadsheet flow and tender bidding envelope modules - Consolidate components into src/components/, remove legacy Component/ and Hooks/ - Add registration state/validation, auth session guard, profile edit modules - Add unit tests across axios, redux, bidding, registration, encryption, core - Add CI workflow, docs (architecture, audit, DSC API spec), keystore example - Remove tracked keystore binary; ignore scripts.zip Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
7.8 KiB
ProcureLinc — Architecture Guide (as-built)
This is the as-built companion to ARCHITECTURE_AUDIT.md.
The audit is the blueprint (what was wrong, what to build); this document is the
map of what now exists and the rules for working within it. Read this before
adding a feature or "fixing" something that looks odd — several things that look
like smells are deliberate, documented below under Deferred / do not touch blindly.
1. Layering
Presentation src/Screens/**, src/components/** (UI only)
│
Application src/Services/**, feature hooks (use-cases, orchestration)
│
Domain slices, reducers, pattern configs (business rules, pure)
│
Infrastructure src/core/** (http, storage, crypto, events)
│
Platform react-native, native modules
Dependencies point inward. src/core/** must never import from Screens,
Services, or Redux. It reaches outward only through injection/events
(see §3).
2. The core/ layer — the single source of infrastructure truth
| Module | Responsibility | Rule |
|---|---|---|
core/config/env.ts |
All hosts/base URLs, selected by build type | No hardcoded URLs anywhere else. |
core/http/client.ts |
THE axios instance; request<T>() |
Throws AppError; never returns a sentinel. All new network code uses this. |
core/http/AppError.ts |
Normalized typed error | Catch with isAppError(); branch on .kind. |
core/observability/logger.ts |
The only sanctioned log sink | Never console.* in app code. DEV-only; release-stripped. |
core/observability/redact.ts |
PII/secret masking | Every log payload is redacted; extend SENSITIVE_KEY_PATTERN when adding sensitive fields. |
core/storage/secureStorage.ts |
Keychain-backed persist storage for the auth session | Token is never in plaintext AsyncStorage. |
core/events/appEvents.ts |
Typed cross-layer event bus | How infra signals up without importing the store. |
core/query/queryClient.ts |
Module-singleton React Query client | Never construct a QueryClient in a component. |
core/errors/ErrorBoundary.tsx |
App-root render-error guard | Wraps the tree in App.tsx. |
core/platform/polyfills.ts |
Buffer / TextEncoder | Imported first in index.js; nothing may depend on load order otherwise. |
3. Hard rules (enforced by review; some by CI)
- Networking: use
request()fromcore/http/client. It injects the JWT, handles 401 (emitsauth/unauthorized), times out, and throwsAppError.src/Axios/Axios.ts(apiCall) is a legacy compatibility adapter that preserves the old never-throws{success:false}envelope for un-migrated call sites; its contract is pinned by tests. Do not add newapiCallsites. - Auth token: read only via
selectAuthJwt(state). The transport reads it through a provider registered inRedux/Store.ts— neverimport { store }intocore. - Logout: call
performLogout()fromServices/Auth/session.ts. It clears redux + keychain + the query cache in one place.AuthSessionGuard(mounted inApp.tsx) resets navigation to Login whenever the session ends. - Post-auth routing:
Services/Auth/registrationRouting.tsis the single decision function. Splash and Login both call it. Do not re-implement. - Logging:
logger.<level>(scope, message, data?). Never log key material, plaintext bids, OTPs, or full PII (the redactor is a safety net, not a licence). - Theme/colors: use
useTheme().Constant/Color.tsxis deprecated (see §6). There is one brand orange, sourced fromtheme/colors. - Components: one root —
src/components/**.src/Component/no longer exists. - Navigation: route param shapes live in
src/Navigation/types.ts(RootStackParamList).NavigationStringis theas constroute-name registry. - Forward/Reverse bidding: every behavioural difference lives in
Screens/Bidding/biddingPattern.ts. The screen/card contain zeroif (pattern === ...).
4. Feature anatomy (pattern to copy)
The tender-bidding and registration features show the intended shape — logic is extracted out of the screen into pure, testable units:
Screens/Tender/tenderBidding/
TenderBidding.tsx presentation + state only
envelopeTypes.ts types
envelopeCrypto.ts AES/RSA (never logs key material)
envelopePayload.ts pure payload build + validation ← unit-tested
envelopeSubmission.ts API calls
components/* memoized, module-scope sub-components
Screens/Ragistration/
Ragistration.tsx orchestration + render
registrationState.ts typed useReducer (was ~20 useStates) ← unit-tested
registrationValidation.ts pure per-stage validation ← unit-tested
When you build a feature: keep the screen thin; put decisions in pure functions; test the pure functions.
5. Testing & CI
npm test(jest). 75 tests across 11 suites, concentrated on the money/crypto/auth/storage paths:envelopePayload,biddingPattern,registrationState/registrationValidation,registrationRouting,Tokenslice,apiCallenvelope contract,AppError,redact,appEvents, and thesecureStoragekeychain-migration path.- CI (
.github/workflows/ci.yml) runs install → typecheck → lint → test, all blocking.tsc --noEmitis green;eslint . --quiet(errors) is green. Style warnings still exist and are being burned down; tighten to--max-warnings=0once clean. - New pure logic should ship with tests. UI can follow with RNTL later.
6. Deferred / do not touch blindly
These look like leftovers but were left deliberately. Read before "fixing".
Constant/Color.tsxis deprecated but still imported by ~20 files for neutral grays. Its brand tokens are now sourced from the theme, but its neutral values differ fromtheme/colorsin several places. A blind find/replace touseTheme()will change appearance — migrate screen-by-screen with visual review.EnvelopeCExcelPreview.tsxstill uses raw axios (responseType: 'arraybuffer') andreact-native-fswith'ascii'write encoding. The RNFS →react-native-blob-utildedupe and the axios →core/httpmigration are deferred because this sealed-bid document flow can't be runtime-verified here and the encodings aren't 1:1. Needs a device.- Sealed-bid crypto is AES-ECB with the symmetric key echoed to the server
(
randomKeyGenerated). This is a backend-contract weakness (audit §11); fixing it requires a coordinated backend change, not a client edit. ResetPasswordsends the new password as a URL query param. Migrated tocore/http(so it's redacted in logs) but the query-param placement is a backend contract — see theTODO(backend)at the call site.PurchaseTenderTile"details" tap navigates to'TenderDetail', an unregistered route (a pre-existing latent bug). The intended destination is ambiguous (the tile already renders an in-tile modal), so it awaits a product decision rather than a guessed target.
7. Open work (gated on device or backend/ops)
- Device/emulator needed: full Auth/App two-stack navigator split
(
AuthSessionGuard+registrationRoutingare the groundwork); release minify/shrink + App Bundle + ABI splits; verifying the Envelope C flow. - Backend/ops needed: HTTPS for
eproc-backend.lincpay(then remove its cleartext exemption innetwork_security_config.xmland add pinning); rotate the previously-committed keystore + Sonar token and purge git history. - Incremental: finish
Constant/Color→useTheme; per-screen typed navigation adoption (useNavigation<NativeStackNavigationProp<RootStackParamList>>); broaden accessibility labels; i18n scaffolding.