Validation Rules
This page documents the cross-cutting validation rules the system enforces across modules: ownership-holding rules, field formats, the password policy, optimistic locking, soft deletes, auto-numbering, multi-tenant isolation, and when validation runs.
For entity-specific rules (status transitions, delete blocks), see Status Lifecycles and Delete Guardrails.
Ownership holdings (100% + managing partner)
Holdings represent ownership percentages and are validated whenever holdings are saved or a partnership is activated.
| Rule | Detail |
|---|---|
| Sum to 100% | Active holding percentages must total exactly 100.00%. The check uses a tolerance of Math.abs(sum - 100) > 0.0001, so any deviation beyond rounding is rejected. |
| Exactly one managing partner | Among active holdings there must be exactly one managing partner. Zero managing partners and more than one are both rejected. |
| Inactive rows excluded | Inactive holding rows are excluded from the sum and the managing-partner count. An inactive partner cannot be designated as managing partner. |
| No implicit rebalancing | The system never auto-adjusts holdings. If a change breaks the 100% sum, the save is rejected and the user must fix it explicitly. |
Error messages (as returned by the backend):
Active holding percentages must total 100.00%. Current total: <n>%.Exactly one active managing partner is required.Only one managing partner is allowed. Currently selected: <n>.Inactive partner cannot be assigned as a Managing Partner.
The same 100% / single-managing-partner principle applies to partnership holdings and to unit shareholder holdings.
Field formats
Format validation is applied to the relevant master and transactional fields.
| Field | Format | Example |
|---|---|---|
| PAN | 10 characters: five letters, four digits, one letter | ABCDE1234F |
| Aadhaar | 12 digits; displayed masked (last 4 shown) | XXXX XXXX 1234 |
| GSTIN | 15 characters (state code + PAN + entity + check digit) | 22ABCDE1234F1Z5 |
| Percentage | 0.00–100.00, two decimal places | 33.33 |
| Currency | Positive, two decimals, Indian grouping | 1,00,000.00 |
| Phone / Mobile | International format with country code | +91 9876543210 |
| Valid email format; unique within the organization where required | user@example.com | |
| Names / text | Trimmed; typically 2–255 characters | — |
| Dates | DD/MM/YYYY; logical ordering enforced (e.g. end after start) | 08/06/2026 |
PII display rules: Aadhaar and PAN are shown with only the last 4 characters; bank account numbers are masked in the middle. Passwords are never displayed or logged.
Future-date guards: Several modules reject future dates where they make no business sense — for example, a project start date and a stock allocation date cannot be in the future.
Password policy
| Requirement | Detail |
|---|---|
| Minimum length | 8 characters |
| Composition | At least one uppercase, one lowercase, one number, and one symbol |
| Storage | Hashed with bcrypt; never stored or logged in plain text |
| Reset | Via a single-use token (see Auth Flows) |
Optimistic locking (409 Conflict)
To prevent two users from silently overwriting each other's changes, edits use optimistic locking based on the record's updatedAt timestamp.
- When you open a record, you receive its current
updatedAt. - On save, the client sends back that
updatedAt. The backend compares it to the current value in the database. - If they differ, the record changed since you loaded it. The save is rejected with HTTP 409 Conflict.
Standard message: "This record changed since you opened it. Refresh and try again."
This is enforced on edits such as Sales Invoices and partnership holdings. Resolve a 409 by refreshing the record, re-applying your change, and saving again.
Soft deletes
Deletions are soft across the system.
- Records carry a
deletedAttimestamp. "Deleting" setsdeletedAtrather than removing the row. - All standard reads filter on
deletedAt: null, so soft-deleted records disappear from lists but remain for audit and referential integrity. - Because deletes are soft, history and audit trails are preserved.
- Deletion is still blocked when a record is referenced by live downstream data — see Delete Guardrails.
Auto-numbering of codes
Human-readable identifiers are generated automatically using configurable prefixes (Settings → Naming Conventions) and zero-padded sequences.
| Entity | Pattern | Example |
|---|---|---|
| Project | PRJ-### | PRJ-001 |
| Subproject | SP-### | SP-001 |
| Quotation | QT-### | QT-001 |
| Sales Order | SO-### | SO-001 |
| Sales Invoice | SI-### | SI-001 |
| Purchase Order | PO-### | PO-001 |
| Stock | (configured prefix) | STK-057 |
| Stock allocation | <ProjectCode>-ALLOC-### | PRJ-001-ALLOC-001 |
| Split child stock | <ParentCode>-<A,B,C…> | STK-057-A, STK-057-B |
Numbering is sequential per organization. On rare collision (concurrent creation), the backend retries to obtain the next free number. Codes are unique within the organization.
Multi-tenant isolation
Every business record is scoped to an organization via organizationId.
- All queries are filtered by the caller's
organizationId; a user can never read or write another organization's data. - Uniqueness constraints (stock code, project code, party email/mobile, unit number within a subproject, etc.) are evaluated within the organization, not globally.
- Subproject-scoped access narrows this further for users with subproject roles — see RBAC Matrix.
Sensitive data protection
Personal and financial data (PAN, Aadhaar, GSTIN, contact details, bank account numbers, sensitive custom fields, and sensitive files) is protected in two ways:
- Masked by default. Responses return masked values (for example
****+ last 4 characters); the full value is shown only through a permission-gated, audited reveal. - Validated before protection. Field-format rules (PAN, Aadhaar, GSTIN, phone — see Field formats) still run on the entered value first, so masking never hides an invalid entry.
For the full masking formats, the reveal flow, file sensitivity tiers, and which roles can reveal, see Data Security.
When validation runs
| Aspect | Behaviour |
|---|---|
| Trigger | Validation runs on submit — not on blur and not while typing. |
| Focus | On failure, focus jumps to the first invalid field. |
| Error display | The most critical error per field is shown beneath the field in red, with a red field border. The error clears once the field becomes valid. |
| Server authority | The frontend validates for convenience; the backend is authoritative. A request that passes client checks can still be rejected server-side (e.g. uniqueness, holdings sum, invalid transition). |
Uniqueness constraints (summary)
| Entity | Must be unique (within organization) |
|---|---|
| Party | Email, Mobile |
| Stock | Stock code |
| Project | Project code, Project name |
| Subproject | Subproject code |
| Unit | Unit number (within its subproject) |
| Partner | Partner name; mobile/email combination |
| Partnership | Partnership code, Partnership name |
| Bank account | Account number (within organization) |
Violations return HTTP 409 Conflict with a field-level message indicating which value is already in use.