Salary API Reference
Complete API contracts for the salary management module, including routes, auth, DTOs, responses, errors, and integration notes.
Salary - API Reference
Audience: Frontend engineers, mobile/panel engineers, backend engineers, QA, and the accounting integration's developers (for the read-only vocabulary the accounting API mirrors).
Scope: Admin (/api/salary/**) and mobile portal (/api/mobile/staff/{salary,payslips,assets}) routes owned by the salary management module. The accounting-facing /api/integration/** surface is documented alongside the integration module.
1. Documentation Evidence
| Area | Files Inspected | What Was Verified |
|---|---|---|
| Controllers | All 17 apps/api/src/modules/salary/**/*.controller.ts | Every route, method, guard, permission |
| Schema | packages/db/src/schema/school/salary-*.ts | Enums and constraints surfaced in responses |
| Error codes | apps/api/src/common/types/error-codes.ts | All 101 SALARY_* codes plus STAFF_SALARY_MANAGED_BY_SALARY_MODULE |
| Permission catalog | packages/db/src/authorization/permission-catalog.ts | All 13 salary permission modules |
| Module composition | apps/api/src/config/swagger/swagger-documents.ts, apps/api/src/modules/mobile/mobile.module.ts | Which document each route ships in |
2. Module Summary
| Field | Value |
|---|---|
| Module name | Salary |
| Module slug | salary |
| Primary actors | Payroll/office (permissioned, superadmin-granted), every staff member (own salary/payslips/assets), the accounting application, the daily sweep and BullMQ workers |
| API surfaces | admin, mobile (portal) |
| Base route prefixes | /api/salary/..., /api/mobile/staff/salary, /api/mobile/staff/payslips, /api/mobile/staff/assets |
| Auth model | JwtAuthGuard + RoleGuard + @Permissions("Salary*_ACTION") (admin); JwtAuthGuard + RoleGuard, self-resolved (portal) |
| Persistence | PostgreSQL (packages/db), BullMQ (QueueName.SALARY) |
| Runtime source of truth | salary_assignments (current pay) and salary_payslips (a settled month) |
| Sibling docs | Features/flows, Backend |
3. Concepts and Terminology
| Term | Meaning | Source |
|---|---|---|
| Assignment | One row in a staff member's pay history; status + change_type | salary-assignments.ts, salary-contract.ts:SALARY_ASSIGNMENT_STATUSES |
| Effective date | Resolved server-side from one of six client modes | salary-effective-date.util.ts |
| Period | One Bikram Sambat month | salary-periods.ts |
| Payslip | One staff member's calculated month within a period | salary-payslips.ts |
| Run | A background bulk or approval operation | salary-periods.ts:salaryRuns |
| Flag | A payslip-level condition surfaced before approval | salary-contract.ts:SALARY_FLAGS |
4. Admin Routes — Catalog and Structures
| Method | Path | Permission | Purpose |
|---|---|---|---|
| GET / POST / GET :id / PATCH :id / DELETE :id | /api/salary/categories | SalaryComponents_* | Component categories CRUD |
| GET / POST / GET :id / PATCH :id / DELETE :id | /api/salary/components | SalaryComponents_* | Components CRUD |
| GET | /api/salary/structures | SalaryStructures_READ | Paginated structure list |
| POST | /api/salary/structures | SalaryStructures_CREATE | Create a draft structure |
| GET | /api/salary/structures/:id | SalaryStructures_READ | Structure detail |
| GET | /api/salary/structures/:id/versions/:versionId | SalaryStructures_READ | One version's lines |
| PATCH | /api/salary/structures/:id | SalaryStructures_UPDATE | Edit a draft structure |
| PUT | /api/salary/structures/:id/lines | SalaryStructures_UPDATE | Replace a draft's lines |
| PUT | /api/salary/structures/:id/designations | SalaryStructures_UPDATE | Set which designations use it |
| POST | /api/salary/structures/:id/activate | SalaryStructures_UPDATE | draft → active, creates version 1 |
| POST | /api/salary/structures/:id/revise | SalaryStructures_UPDATE | New version effective on a future date |
| POST | /api/salary/structures/:id/deactivate | SalaryStructures_UPDATE | active → inactive |
| POST | /api/salary/structures/:id/reactivate | SalaryStructures_UPDATE | inactive → active |
| POST | /api/salary/structures/:id/archive | SalaryStructures_UPDATE | Terminal |
| POST | /api/salary/structures/:id/copy | SalaryStructures_CREATE | Duplicate as a new draft |
| DELETE | /api/salary/structures/:id | SalaryStructures_DELETE | Delete a draft |
| GET | /api/salary/designations | SalaryStructures_READ | List designation defaults |
| PUT | /api/salary/designations/:designationId/default | SalaryStructures_UPDATE | Set a designation's default structure |
5. Admin Routes — Assignments, Employees, One-Time Items, Bulk
| Method | Path | Permission | Purpose |
|---|---|---|---|
| GET | /api/salary/employees | SalaryAssignments_READ | Cross-staff assignment list |
| GET | /api/salary/employees/:staffId | SalaryAssignments_READ | One staff member's assignment history |
| POST | /api/salary/employees/:staffId/assignments/preview | SalaryAssignments_READ | Preview a change before committing |
| POST | /api/salary/employees/:staffId/assignments | SalaryAssignments_CREATE | Commit a change (see §9 for accepted change types) |
| POST | /api/salary/employees/:staffId/assignments/:assignmentId/cancel | SalaryAssignments_DELETE | Cancel a scheduled assignment |
| GET | /api/salary/one-time-items | SalaryAssignments_READ | List one-time items |
| POST | /api/salary/one-time-items | SalaryAssignments_CREATE | Create a one-time item |
| POST | /api/salary/one-time-items/:id/cancel | SalaryAssignments_DELETE | Cancel one |
| POST | /api/salary/bulk/preview | SalaryAssignments_READ | Preview a bulk operation across a staff scope |
| POST | /api/salary/bulk | SalaryAssignments_CREATE | Launch a bulk operation as a salary_run |
6. Admin Routes — Periods, Payslips, Runs
| Method | Path | Permission | Purpose |
|---|---|---|---|
| GET / POST | /api/salary/periods | SalaryPeriods_READ/_CREATE | List / create a period |
| GET | /api/salary/periods/:id | SalaryPeriods_READ | Period detail |
| POST | /api/salary/periods/:id/prepare | SalaryPeriods_UPDATE | Compute payslips |
| POST | /api/salary/periods/:id/recalculate | SalaryPeriods_UPDATE | Recompute (e.g. after a late change) |
| POST | /api/salary/periods/:id/approve | SalaryApproval_UPDATE | Approve (inline or background) |
| POST | /api/salary/periods/:id/reopen | SalaryApproval_UPDATE | Reopen approved/sent |
| POST | /api/salary/periods/:id/send | SalaryApproval_UPDATE | Release to staff |
| POST | /api/salary/periods/:id/cancel | SalaryApproval_DELETE | Cancel a period |
| GET / POST | /api/salary/periods/:periodId/exclusions | SalaryPeriods_READ/_UPDATE | List / add an exclusion |
| DELETE | /api/salary/periods/:periodId/exclusions/:staffId | SalaryPeriods_UPDATE | Revoke an exclusion |
| GET | /api/salary/periods/:periodId/run | SalaryPeriods_READ | The period's active run (data: null if it has never run) |
| GET | /api/salary/periods/:periodId/run/items | SalaryPeriods_READ | The run's items |
| POST | /api/salary/periods/:periodId/run/retry | SalaryPeriods_UPDATE | Retry failed/exception items |
| POST | /api/salary/periods/:periodId/run/cancel | SalaryPeriods_UPDATE | Cancel the run |
| GET | /api/salary/payslips | SalaryPeriods_READ | Cross-period payslip list |
| GET | /api/salary/payslips/:id | SalaryPeriods_READ | Payslip detail |
| POST | /api/salary/payslips/:id/recalculate | SalaryPeriods_UPDATE | Recalculate one payslip |
| POST / DELETE | /api/salary/payslips/:id/overrides / .../overrides/:componentId | SalaryPeriods_UPDATE | Add / clear a manual line override |
| POST | /api/salary/payslips/:id/acknowledge | SalaryApproval_UPDATE | Acknowledge an acknowledgeable flag. Refused on the caller's own payslip (SALARY_SELF_CHANGE_FORBIDDEN), and the acknowledger counts as a maker of the period, so with distinct approval on they cannot also approve it |
| POST | /api/salary/payslips/:id/correct | SalaryApproval_UPDATE | Correct a sent payslip |
| POST | /api/salary/payslips/:id/refresh-bank | SalaryApproval_UPDATE | Re-pull current bank details |
| GET | /api/salary/runs | SalaryAssignments_READ | Cross-run list |
| GET | /api/salary/runs/:id | SalaryAssignments_READ | Run detail |
| GET | /api/salary/runs/:id/items | SalaryAssignments_READ | Run items |
| POST | /api/salary/runs/:id/retry | SalaryAssignments_UPDATE | Retry |
| POST | /api/salary/runs/:id/cancel | SalaryAssignments_UPDATE | Cancel |
7. Admin Routes — Payments, Reports, Settings
| Method | Path | Permission | Purpose |
|---|---|---|---|
| POST | /api/salary/payslips/:id/payment | SalaryPayments_UPDATE | Record a manual payment state change |
| GET | /api/salary/inbound-events | SalarySync_READ | The accounting application's reported payment events — carries @RequiresAccountingIntegration(), so it answers 404 ACCOUNTING_INTEGRATION_DISABLED while the school's accounting-integration switch is off (see the integration module's docs); the manual payment route above is never gated |
| GET | /api/salary/dashboard | SalaryDashboard_READ | Cross-staff counts and status |
| GET | /api/salary/history | SalaryDashboard_READ | The salary change log |
| GET | /api/salary/exports/payslips | SalaryExports_READ | CSV export of payslips |
| GET / PATCH | /api/salary/settings | SalarySettings_* | Global settings |
| PATCH | /api/salary/settings/financial | SalaryFinancialSettings_UPDATE | Payment authority, distinct approver, SSF rates, retirement cap, accounting client, citizenship sharing |
| GET | /api/salary/notification-settings | SalarySettings_READ | Notification matrix override |
| PUT | /api/salary/notification-settings/:eventKey | SalarySettings_UPDATE | Override one event |
| POST | /api/salary/tax-slabs/copy | SalaryTaxSettings_CREATE | Copy a fiscal year's slabs forward |
| GET | /api/salary/tax-slabs | SalaryTaxSettings_READ | List slabs |
| PUT / DELETE | /api/salary/tax-slabs/:fiscalYearBs/:assessment | SalaryTaxSettings_* | Set / remove one fiscal year's slabs for an assessment |
8. Mobile/Portal Routes
| Method | Path | Permission | Purpose |
|---|---|---|---|
| GET | /api/mobile/staff/salary | none (self-resolved) | My current salary assignment, or null if unassigned |
| GET | /api/mobile/staff/payslips | none (self-resolved) | My own released payslips, newest first |
| GET | /api/mobile/staff/payslips/:id | none (self-resolved) | One of my own released payslips |
| GET | /api/mobile/staff/assets | none (self-resolved) | The assets currently assigned to me |
SalaryPortalAssignmentDto (GET staff/salary, or null when the caller has no effective assignment): effectiveFrom (SalaryDateDto), structureName (nullable), designationName/departmentName (nullable — the caller's own designation and department label, read straight off their staff row rather than any live lookup; a designation or department that is itself deactivated still returns its name), lines (SalaryPortalLineDto[]: code, name, kind, monthlyPaisa), totals (SalaryPortalTotalsDto: basicPaisa/allowancesPaisa/grossPaisa), scheduled (nullable SalaryPortalScheduledDto: effectiveFrom — a pending future change, when one exists).
SalaryPortalPayslipSummaryDto (GET staff/payslips list items): identity/period fields, grossEarningsPaisa/leaveDeductionPaisa/totalDeductionsPaisa/netPayPaisa, paymentStatus, paidOn (nullable). SalaryPortalPayslipDetailDto (GET staff/payslips/:id) extends it with employerContributionsPaisa (the employer's SSF share paid on top of gross — not part of netPayPaisa, which is computed from earning and deduction lines only), the day/divisor and leave breakdown (periodDays/employedDays/divisorDays/leaveDays/leavePaidDays/leaveUnpaidDays/leaveDeductionDays), lines (SalaryPortalPayslipLineDto[]: code/name/kind/amountPaisa — kind is earning, deduction, or employer_contribution), leaves (SalaryPortalPayslipLeaveDto[]: leaveTypeLabel/unpaidDaysInPeriod/deductionDays), and bank (nullable SalaryPortalPayslipBankDto: bankName, accountLastFour — the last four digits only). Never an override reason, an event, or an accounting field — the portal shows what was paid, not why.
9. Accepted Change Types
POST /api/salary/employees/:staffId/assignments accepts a changeType from SALARY_CLIENT_CHANGE_TYPES: initial, revision, increment, decrement, promotion, demotion, correction, stop, resume. initial is the only type accepted for a staff member with no current effective assignment; every other type requires one (SALARY_ASSIGNMENT_NO_CURRENT otherwise). Every type except initial requires a reason string. A promotion additionally accepts a designationId identifying the new designation.
10. Error Codes
error-codes.ts declares 101 SALARY_* codes covering assignment lifecycle (SALARY_ASSIGNMENT_*), catalogue (SALARY_CATEGORY_*, SALARY_COMPONENT_*), structures (SALARY_STRUCTURE_*), periods (SALARY_PERIOD_*), payslips (SALARY_PAYSLIP_*), payments (SALARY_PAYMENT_*), runs (SALARY_RUN_*), inbound sync (SALARY_INBOUND_*), bulk (SALARY_BULK_*), and tax slabs (SALARY_TAX_SLABS_*). The ones worth calling out specifically:
| Code | Meaning |
|---|---|
SALARY_SELF_CHANGE_FORBIDDEN | A payroll admin tried to change or approve their own salary |
SALARY_ASSIGNMENT_NO_CURRENT | A non-initial change named for a staff member with no current assignment |
SALARY_ASSIGNMENT_DATE_IN_LOCKED_PERIOD | The resolved effective date falls in a locked period |
SALARY_PAYSLIP_STALE | Approval attempted against a payslip whose inputs changed since preparation |
SALARY_PERIOD_POPULATION_CHANGED | The eligible staff population moved since preparation |
SALARY_PERIOD_DISTINCT_APPROVER_REQUIRED | Same user prepared and is attempting to approve, where the school requires distinct approvers |
SALARY_PERIOD_NOT_CONTIGUOUS / _EARLIER_UNAPPROVED / _LATER_IN_PROGRESS | Period ordering violations |
SALARY_PAYMENT_TRANSITION_INVALID | The requested payment status change is not on SALARY_PAYMENT_TRANSITIONS |
SALARY_PAYMENT_AUTHORITY_MANUAL / _EXTERNAL / _NEEDS_CLIENT | Payment authority mismatch |
SALARY_VERSION_CONFLICT | Optimistic version mismatch |
SALARY_FLAG_NOT_ACKNOWLEDGEABLE | Attempted to acknowledge a flag not on SALARY_ACKNOWLEDGEABLE_FLAGS |
STAFF_HAS_ACTIVE_PAY_OR_ASSETS | Staff deletion refused — pay or assets still active (shared with assets) |
STAFF_SALARY_MANAGED_BY_SALARY_MODULE | The legacy people salary fields are read-only once the salary module manages this staff member's pay |
11. Integration Notes
The accounting application never calls /api/salary/** directly except the inbound events read (GET /api/salary/inbound-events, gated by SalarySync_READ for an admin, or the client's own scope for the accounting application via integration). It reads the salary change feed at the grain its scopes allow (payroll:read for the vocabulary and period/payslip lifecycle, payroll:identity:read for PAN/SSF/CIT numbers, payroll:bank:read for the bank snapshot on an approved payslip) and writes payment state through payroll:write. See the integration module's docs for the client-facing request/response shapes, INTEGRATION_SCOPE_GRANT_FORBIDDEN, and INTEGRATION_EVENT_SCOPE.
12. DTOs — Catalog, Structures, Designations
CreateSalaryCategoryDto: name (string, required, ≤80) · kind (required, SalaryComponentKind: earning/deduction/employer_contribution) · description (optional, nullable, ≤500) · sortOrder (optional int, ≥0, default 0). UpdateSalaryCategoryDto: same minus kind (immutable) minus name/sortOrder/description becoming optional, plus status (optional, SalaryCatalogStatus: active/inactive/archived) and version (required int ≥1 — a stale value is 409 SALARY_VERSION_CONFLICT). SalaryCategoryDto (response) adds publicId, isSystem, componentCount, createdAt/updatedAt.
CreateSalaryComponentDto: code (required, ^[A-Z][A-Z0-9_]{1,29}$) · name (required, ≤80) · categoryId (required, uuid) · frequency (required, SalaryFrequency: monthly/one_time) · defaultValueType (optional nullable, SalaryValueType: flat/percent) · defaultAmountPaisa (optional nullable int, 0-9,999,999,999) · defaultPercentBp (optional nullable int, 0-10000) · defaultPercentBase (optional nullable, SalaryPercentBase: basic/gross_earnings) · taxable/prorated/leaveDeductible (all required booleans) · description (optional nullable, ≤500) · sortOrder (optional int ≥0). UpdateSalaryComponentDto: every field optional (code/frequency immutable, absent entirely), plus required version. SalaryComponentDto (response) adds publicId, category (SalaryCategoryRefDto: publicId/name/kind), role (SalaryComponentRole), isSystem, usage ({structures, assignments, oneTimeItems} counts — non-zero usage is what SALARY_COMPONENT_IN_USE protects), createdAt/updatedAt.
CreateSalaryStructureDto: name (required, ≤150) · description (optional nullable, ≤2000) · effectiveFrom (required, ISO date string) · designationIds (optional array of uuid). UpdateSalaryStructureDto: name/description/effectiveFrom all optional, plus required version. PutSalaryStructureLinesDto: lines (required array, 1-40, each SalaryStructureLineInputDto: componentId uuid required, valueType required enum, amountPaisa/percentBp/percentBase per the value-shape rule, sortOrder optional) plus version. PutSalaryStructureDesignationsDto: designationIds (required array of uuid) plus version. ReviseSalaryStructureDto: effectiveFrom (required date, must be future), changeNote (required, 3-2000), lines (same shape as above, 1-40), version. CopySalaryStructureDto: name only (required, ≤150) — copies as a fresh draft, no version needed since nothing existing is touched. Every structure-lifecycle action (activate/deactivate/reactivate/archive) takes only { version } (SalaryStructureVersionOnlyDto).
Structure response DTOs nest SalaryStructureLineDto (adds the resolved component ref and monthlyPaisa), SalaryStructureVersionSummaryDto (versionNo, effectiveFrom as SalaryDateDto, changeNote, createdByName), and SalaryStructureMonthlyDto (basicPaisa/earningsPaisa/deductionsPaisa — the structure's own monthly total, not any one employee's).
SalaryDesignationDto (GET salary/designations): publicId, name, department ({publicId, name}), isTeaching, defaultStructure (nullable — {publicId, name, currentVersionNo, monthly: {basicPaisa, earningsPaisa}}), applicableStructures (array of {publicId, name, status}), staffCount, salariedCount, onDefaultCount. PutSalaryDesignationDefaultDto: structureId (nullable uuid — null clears the default; setting one requires the structure already declare that designation applicable, or the write is refused by the composite FK, translated to a validation error).
13. DTOs — Assignments, Employees, One-Time Items, Bulk
AssignmentEffectiveDateDto (nested in every change): mode (required, one of immediate/ad_date/bs_date/bs_month/plus_days/next_period) plus exactly the field its mode needs — date (AD, ad_date), dateBs (BS, bs_date), bsYear+bsMonth (bs_month), days (1-366, plus_days); immediate/next_period take no extra field. See the feature doc §5 for what each mode resolves to.
AssignmentChangeDto (the body of both preview and, extended, commit): changeType (required, SALARY_CLIENT_CHANGE_TYPES: initial/revision/increment/decrement/promotion/demotion/correction/stop/resume) · effectiveDate (required, above) · structureId (optional nullable uuid) · lines (optional array of AssignmentLineInputDto: componentId uuid required, valueType required, amountPaisa/percentBp/percentBase optional nullable per value-shape) · designationId (optional nullable uuid — required when changeType is promotion/demotion; a missing, unknown, or inactive designation both answer SALARY_PROMOTION_DESIGNATION_INVALID) · taxAssessment (optional, single/couple) · ssfEnrolled (optional bool) · reason (optional, 3-500 — required in practice for every changeType except initial, enforced server-side, not by the DTO) · replaceScheduled (optional bool — supersede an already-pending scheduled change instead of refusing with SALARY_ASSIGNMENT_SCHEDULED_EXISTS).
CommitAssignmentChangeDto extends the above with effectiveFrom (required, the exact AD date preview returned — echoed back, not recomputed, so a client cannot silently commit against a different date than what it previewed) and basis (required AssignmentBasisDto: currentAssignmentId/currentVersion/scheduledAssignmentId/scheduledVersion, all nullable — the exact snapshot preview returned). Commit re-validates from scratch: it re-runs the same evaluateChange preview uses (self-dealing, permission, designation, locked-period, and every other blocking check) under the period+staff locks, so nothing preview missed can slip through a race; separately, it compares the request's basis against what it just re-read and throws SALARY_VERSION_CONFLICT on any mismatch — a concurrent change between preview and commit is caught even though the blocking-check re-run alone might not catch it (the assignment could still be structurally valid, just not the one the client's screen was showing).
CreateSalaryOneTimeItemDto: staffId (required uuid) · componentId (required uuid, must be a one_time-frequency component) · bsYear (required, 2000-2090) · bsMonth (required, 1-12) · valueType (required) · amountPaisa/percentBp (optional nullable int, ≥1 — per the schema's salary_one_time_items_amount_bounded, percentBp up to 100,000, i.e. 1000%) · reason (required, 3-500) · idempotencyKey (optional uuid — paired with the caller's own user id server-side for replay safety).
SalaryBulkScopeDto (who the action applies to — every field optional, combined with AND where more than one is given): staffIds (≤2000 uuids) · departmentIds/designationIds (int arrays) · structureId (uuid — filters to staff currently on this structure) · employmentStatuses (string array). SalaryBulkActionDto: kind (required, apply_structure/adjust_salary/one_time_item/profile_import/reschedule), plus per-kind fields: apply_structure needs structureId + optional includeOverrides; adjust_salary needs mode (percent/flat) and either basisPoints or amountPaisa, plus optional componentId (default BASIC); one_time_item needs valueType, percentBp or (implicitly) a flat amount, bsYear, bsMonth, reason; reschedule needs assignmentIds (≤2000 uuids); every action-needing-a-date kind (apply_structure/adjust_salary/reschedule) also needs effectiveDate. Every kind accepts an optional reason (3–500 characters); the run records it on every assignment row it writes and on every scheduled change it cancels, and falls back to "Applied by a bulk salary run." only when the operator gave none. BulkPreviewDto: action + scope (required, nested) plus page/size (default 1/20, size capped 100 — preview is paginated, unlike the run itself). BulkCommitDto extends it with staffIds (required — the exact eligible set the client confirmed, checked against a fresh eligibility re-evaluation; any id no longer eligible refuses the whole commit) and requestKey (required uuid, per-requester idempotency for the whole bulk operation).
BulkPreviewResultDto: resolvedEffectiveFrom (nullable SalaryDateDto — the server-resolved date, or null when the action kind needs none) · counts (a free-form {total, eligible, <reason>: n, ...} record — one key per distinct ineligibility reason seen on the page) · rows (the current page, each SalaryBulkRowDto: staffId, name, employeeCode, eligible, reasons (string array — values from SALARY_BULK_INELIGIBLE_REASONS: already_on_structure/individual_override/missing_profile_salary/inactive_employment/pending_scheduled_change/date_before_current/date_in_locked_period/no_current_assignment/self), current (nullable {basicPaisa, grossPaisa}), proposed (nullable, same shape)) · eligibleStaffIds (the complete eligible set across every page, not just the one shown — what a client passes to BulkCommitDto.staffIds).
proposed is computed only for apply_structure and adjust_salary, only for rows that are eligible AND already have a current assignment, and only for the page actually being returned (not the whole eligible set, to bound the work a single preview call does). For apply_structure it is the target structure version's own monthly totals, unconditionally the same for every eligible row. For adjust_salary it re-reads that staff member's current lines, applies the delta (a flat amount, or a percentage of the current value of the named component — default BASIC) to that one component only, floors at zero, and totals the result; a row whose target component has no current flat line (e.g. it is presently a percentage line) gets proposed: null even if otherwise eligible. one_time_item, profile_import, and reschedule always return proposed: null for every row — those actions do not change the recurring monthly total in a way this preview computes. salary/bulk/salary-bulk-proposal.util.ts is the shared arithmetic both preview and the run handler call, so the number shown in preview is guaranteed to be the number the run would actually write.
14. DTOs — Periods, Payslips, Runs
CreateSalaryPeriodDto: bsYear (required, 2000-2090) · bsMonth (required, 1-12) · notes (optional, 1-500). PrepareSalaryPeriodDto is just { version }. CancelSalaryPeriodDto: { version, reason } (reason required, 3-500). Approve/reopen/send take { version } alone (not separately listed — same SalaryVersionDto shape).
SalaryPeriodDto (response) carries, beyond the identity fields: label/bsYear/bsMonth/fiscalYearBs/fiscalMonth, startDate/endDate (SalaryDateDto), calendarDays, status, settings (nullable SalaryPeriodSettingsDto — paymentAuthority/requireDistinctApprover/leaveDivisor/ssfEnabled/tdsEnabled, null until prepared), counts (SalaryPeriodCountsDto: payslips/draft/approved/cancelled/blocked/paid), totals (SalaryPeriodTotalsDto: the five payisa sums), activeRun (nullable SalaryRunSummaryDto), sync (SalaryPeriodSyncDto: lastInboundAt, varianceCount, externallyUpdated, manuallyUpdated), actorIsMaker (nullable bool — non-null only when the period requires a distinct approver, tells the caller's own UI whether to grey out Approve), the five lifecycle timestamp/actor pairs, reopenedCount, notes, allowedActions (string array — a UI hint from state, never a permission check), version. SalaryPeriodActionResultDto wraps {period, runId} — runId is non-null only when the action ran in the background.
SetSalaryOverrideDto: componentId (required uuid, must be deduction/employer_contribution) · amountPaisa (required int ≥0) · reason (required, 3-500) · version. AcknowledgeSalaryFlagDto: flag (required, must be in SALARY_ACKNOWLEDGEABLE_FLAGS — today only employment_inactive_with_pay) · reason (required, 3-500) · version. CreateSalaryExclusionDto: staffId (required uuid) · reason (required, 3-500) — no version (exclusions are their own row, not an edit of the payslip).
SalaryPayslipSummaryDto: identity/period fields, status (draft/approved/cancelled), revision, the five money totals, paymentStatus, paidAmountPaisa, flags/blockingFlags (string arrays — blockingFlags is flags minus whatever is in acknowledgedFlags), isSelf (true when the payslip is the caller's own — every mutating route on it then refuses regardless of permission), allowedActions, version. SalaryPayslipDetailDto adds the day/divisor breakdown, taxAssessment/ssfEnrolled, the five tax figures, acknowledgedFlags, lines (SalaryPayslipLineDto — component ref, origin (SalaryLineOrigin), value shape, grossPaisa/leaveDeductionPaisa/computedAmountPaisa/amountPaisa, overridden/overrideReason, sourcePayslipId for a leave-adjustment line), segments, leaves (SalaryPayslipLeaveDto), events (SalaryPayslipEventDto), and bank (SalaryPayslipBankDto, optional — the key is present on the response only for a caller holding StaffSalary_READ; a caller without that permission never sees the key at all, regardless of the payslip's approval state).
SalaryRunDto: kind/status/periodId (nullable), the four item counters, requestedBy (nullable ref), startedAt/finishedAt/lastProgressAt, errorCode/errorMessage, allowedActions (retry/cancel by state), createdAt. SalaryRunItemDto: staffId/employeeName/employeeCode, status (SalaryRunItemStatus), outcome (nullable, e.g. "prepared"), attempts, errorCode/errorMessage, resultId (nullable — the public id of whatever row the item created, e.g. a new assignment or payslip).
15. DTOs — Payments, Reports, Settings
RecordSalaryPaymentDto: status (required, SalaryPaymentStatus) · paidAmountPaisa (optional int ≥0 — defaults to net pay for paid, required for partially_paid) · paidOn (optional AD date — defaults to today for paid/partially_paid) · paymentReference (optional, 1-128) · note (required, 3-500) · requestKey (required uuid — per-actor idempotency: the same key with the same body from the same actor replays the stored result; a different body or a different actor reusing the key is refused) · version.
SalaryDashboardDto (GET salary/dashboard): employees ({assigned, unassigned, stopped}), scheduledChanges ({pending, failedActivation}), currentPeriod (nullable {id, periodLabel, status}), pendingLeaveAdjustments (array of {staffId, employeeName, periodLabel}), exceptions (array of {staffId, employeeName, reason}, reason one of missing_bank_details/missing_pan/stopped_with_pending_adjustment).
ListSalaryHistoryQueryDto (GET salary/history): staffId (optional uuid — omit for every staff member), from/to (optional date strings), kind (optional, assignment/one_time_item). SalaryHistoryItemDto: kind, id, staffId, employeeName, changeType (nullable — set only for an assignment row), componentName (nullable — set only for a one_time_item row), effectiveFromOrBsMonth (a single overloaded field: an AD date string for an assignment, a BS-month label for a one-time item — read kind first to know which), status, reason (nullable), createdBy, createdAt.
SalaryPayslipsExportQueryDto (GET salary/exports/payslips): periodId (required uuid) — the CSV always exports one period's payslips at a time; there is no cross-period export.
SalarySettingsDto / PatchSalarySettingsDto: the patchable subset is deliberately narrow — roundingMode, roundingUnitPaisa, leaveDivisor — everything else (SSF, TDS, retirement cap, payment authority, distinct approver, citizenship sharing) is on the separate PatchSalaryFinancialSettingsDto behind SalaryFinancialSettings_UPDATE: paymentAuthority, payrollClientId (nullable uuid), requireDistinctApprover (bool), ssfEnabled (bool), ssfEmployeeRateBp/ssfEmployerRateBp (0-10000), employerSsfTaxable (bool), tdsEnabled (bool), retirementCapPaisa (0-999,999,999,999), retirementCapNumerator/retirementCapDenominator (1-100 each), shareCitizenshipWithAccounting (bool) — every field optional except version.
PutSalaryNotificationSettingDto: isEnabled (required bool), audiences (required array, subset of SALARY_NOTIFICATION_AUDIENCES), channels (required array, subset of SALARY_NOTIFICATION_CHANNELS), version. The response additionally carries allowedAudiences/allowedChannels — the matrix's own ceiling for that event key, so the UI can grey out an audience/channel the event will never actually notify even if selected.
PutSalaryTaxSlabsBodyDto (PUT salary/tax-slabs/:fiscalYearBs/:assessment): slabs (required array, 1-20, each {upperBoundPaisa (nullable, the open top slab), rateBp (0-10000), waivedForSsf (bool, legal only on seq 1)} — the array's own order becomes seq 1..n; the whole group replaces what existed, and the deferred trigger salary_tax_slabs_check refuses at commit if the replacement is not contiguous/increasing/single-open-top). CopySalaryTaxSlabsDto: fromFiscalYearBs/toFiscalYearBs (both required, 2000-2089) — copies a whole group forward.
16. Error Codes by Route Group
Every route group's mutating actions can also return the shared codes: VALIDATION_FAILED (a DTO field fails its decorator), PERMISSION_INSUFFICIENT (the guard's own message when @Permissions() is not held), and — for anything read by public id — the group's own _NOT_FOUND. This table lists what is specific to each group, and what a consumer should do about it.
| Route group | Codes it can return | Consumer guidance |
|---|---|---|
| Categories, Components | SALARY_CATEGORY_NAME_TAKEN/_NOT_FOUND/_IN_USE/_SYSTEM, SALARY_COMPONENT_CODE_TAKEN/_NAME_TAKEN/_NOT_FOUND/_IN_USE/_SYSTEM/_INACTIVE/_INVALID/_KIND_MISMATCH | _IN_USE/_SYSTEM on delete: retire (status: inactive/archived) instead of deleting. _KIND_MISMATCH: the category's kind and the component's kind must match — pick a category of the right kind rather than retrying. |
| Structures, Versions, Designation Defaults | SALARY_STRUCTURE_NAME_TAKEN/_NOT_FOUND/_NOT_DRAFT/_NOT_ACTIVE/_NOT_APPLICABLE/_STATUS_TRANSITION_INVALID/_BASIC_LINE_REQUIRED/_REVISION_DATE_INVALID/_VERSION_NOT_FOUND, SALARY_STRUCTURE_LINE_DUPLICATE/_INVALID, SALARY_DESIGNATION_NOT_FOUND | _BASIC_LINE_REQUIRED on activate: add exactly one role=basic line first. _NOT_APPLICABLE on setting a designation default: call PUT structures/:id/designations to declare it applicable before defaulting to it. _REVISION_DATE_INVALID: the revision's effectiveFrom must be strictly after today. |
| Assignments / Employees | SALARY_ASSIGNMENT_* (13 codes — activation failure, already-exists, already-stopped, basic-required, date-before-current, date-in-locked-period, lines-invalid, not-found, not-scheduled, not-stopped, no-current, reason-required, scheduled-exists), SALARY_SELF_CHANGE_FORBIDDEN, SALARY_STAFF_UPDATE_PERMISSION_REQUIRED, SALARY_PROMOTION_DESIGNATION_INVALID, SALARY_STAFF_NOT_FOUND, SALARY_VERSION_CONFLICT | _NO_CURRENT: use changeType: "initial" for this staff member's first assignment. _DATE_IN_LOCKED_PERIOD: choose a later effective date, or wait for the period to release. _SCHEDULED_EXISTS: pass replaceScheduled: true to supersede the pending change instead of refusing. SALARY_VERSION_CONFLICT on commit: re-run preview and resubmit with the fresh basis. |
| One-Time Items | SALARY_ONE_TIME_ITEM_ALREADY_CANCELLED/_COMPONENT_INVALID/_NOT_FOUND/_PERIOD_LOCKED | _PERIOD_LOCKED: the owning month's period is already preparing or later — add the item to next month instead, or reopen if this month must be corrected. |
| Bulk | SALARY_BULK_SCOPE_EMPTY/_STAFF_INVALID/_TOO_LARGE | _TOO_LARGE: split the scope — the 2000-row cap applies per staffIds/assignmentIds array. _STAFF_INVALID on commit: an id in staffIds is not in the fresh eligible set — re-run preview. |
| Periods | SALARY_PERIOD_* (13 codes — bank-changed, blocking-flags, calendar-missing, distinct-approver-required, earlier-unapproved, exists, invalid-month, later-in-progress, not-contiguous, not-ended, not-found, population-changed, status-invalid) | _BLOCKING_FLAGS: read the payslip's blockingFlags; acknowledge the acknowledgeable ones, fix the rest, then retry approval. _POPULATION_CHANGED: recalculate the period before approving again. _EARLIER_UNAPPROVED/_LATER_IN_PROGRESS: process periods in month order. |
| Payslips | SALARY_PAYSLIP_NEGATIVE_NET/_NOT_CORRECTABLE/_NOT_DRAFT/_NOT_FOUND/_REVISION_MISMATCH/_STALE, SALARY_OVERRIDE_NOT_ALLOWED/_NOT_FOUND, SALARY_FLAG_NOT_ACKNOWLEDGEABLE, SALARY_EXCLUSION_EXISTS/_NOT_FOUND | _STALE: recalculate before approving — the inputs changed since the last preparation. _NEGATIVE_NET: fix the assignment or add a leave/one-time adjustment before the net can go positive; approval will not override this. |
| Payments | SALARY_PAYMENT_AMOUNT_INVALID/_AUTHORITY_EXTERNAL/_AUTHORITY_MANUAL/_AUTHORITY_NEEDS_CLIENT/_PERIOD_NOT_SENT/_TRANSITION_INVALID, SALARY_REQUEST_KEY_REUSED | _AUTHORITY_MANUAL/_EXTERNAL: check salary/settings → paymentAuthority before offering the manual-payment screen at all. _TRANSITION_INVALID: read SALARY_PAYMENT_TRANSITIONS (feature doc §11) before offering a status option in the UI. |
| Inbound Sync | SALARY_INBOUND_CLIENT_NOT_DESIGNATED/_EVENT_ID_REUSED/_EVENT_NOT_FOUND/_PAYLOAD_INVALID/_TIMESTAMP_INVALID | These surface to the accounting application's own client, not the admin panel — see the integration module's docs for its retry guidance. |
| Runs | SALARY_RUN_NOT_CANCELLABLE/_NOT_FOUND/_NOT_RETRYABLE | _NOT_RETRYABLE: only a failed/completed_with_exceptions run can be retried; a completed run has nothing left to retry. |
| Tax Slabs | SALARY_TAX_SLABS_INVALID/_MISSING/_SOURCE_NOT_FOUND | _MISSING surfaces during TDS calculation, not on a settings write — it means the fiscal year in question has no slabs configured at all; configure them before the period containing that fiscal year is prepared. |
| Structure/Designation shared | SALARY_STRUCTURE_NOT_FOUND also applies to designation-default writes | — |
Salary Backend Documentation
Backend architecture, data model, the calculation engine, services, queues, and operational behavior for the salary management module.
Assets Features and Flows
Complete feature list, actor journeys, state flows, business rules, edge cases, and diagrams for the asset management module.