Skoolsewa - School Management System Docs
Developer ResourcesSalary

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

AreaFiles InspectedWhat Was Verified
ControllersAll 17 apps/api/src/modules/salary/**/*.controller.tsEvery route, method, guard, permission
Schemapackages/db/src/schema/school/salary-*.tsEnums and constraints surfaced in responses
Error codesapps/api/src/common/types/error-codes.tsAll 101 SALARY_* codes plus STAFF_SALARY_MANAGED_BY_SALARY_MODULE
Permission catalogpackages/db/src/authorization/permission-catalog.tsAll 13 salary permission modules
Module compositionapps/api/src/config/swagger/swagger-documents.ts, apps/api/src/modules/mobile/mobile.module.tsWhich document each route ships in

2. Module Summary

FieldValue
Module nameSalary
Module slugsalary
Primary actorsPayroll/office (permissioned, superadmin-granted), every staff member (own salary/payslips/assets), the accounting application, the daily sweep and BullMQ workers
API surfacesadmin, mobile (portal)
Base route prefixes/api/salary/..., /api/mobile/staff/salary, /api/mobile/staff/payslips, /api/mobile/staff/assets
Auth modelJwtAuthGuard + RoleGuard + @Permissions("Salary*_ACTION") (admin); JwtAuthGuard + RoleGuard, self-resolved (portal)
PersistencePostgreSQL (packages/db), BullMQ (QueueName.SALARY)
Runtime source of truthsalary_assignments (current pay) and salary_payslips (a settled month)
Sibling docsFeatures/flows, Backend

3. Concepts and Terminology

TermMeaningSource
AssignmentOne row in a staff member's pay history; status + change_typesalary-assignments.ts, salary-contract.ts:SALARY_ASSIGNMENT_STATUSES
Effective dateResolved server-side from one of six client modessalary-effective-date.util.ts
PeriodOne Bikram Sambat monthsalary-periods.ts
PayslipOne staff member's calculated month within a periodsalary-payslips.ts
RunA background bulk or approval operationsalary-periods.ts:salaryRuns
FlagA payslip-level condition surfaced before approvalsalary-contract.ts:SALARY_FLAGS

4. Admin Routes — Catalog and Structures

MethodPathPermissionPurpose
GET / POST / GET :id / PATCH :id / DELETE :id/api/salary/categoriesSalaryComponents_*Component categories CRUD
GET / POST / GET :id / PATCH :id / DELETE :id/api/salary/componentsSalaryComponents_*Components CRUD
GET/api/salary/structuresSalaryStructures_READPaginated structure list
POST/api/salary/structuresSalaryStructures_CREATECreate a draft structure
GET/api/salary/structures/:idSalaryStructures_READStructure detail
GET/api/salary/structures/:id/versions/:versionIdSalaryStructures_READOne version's lines
PATCH/api/salary/structures/:idSalaryStructures_UPDATEEdit a draft structure
PUT/api/salary/structures/:id/linesSalaryStructures_UPDATEReplace a draft's lines
PUT/api/salary/structures/:id/designationsSalaryStructures_UPDATESet which designations use it
POST/api/salary/structures/:id/activateSalaryStructures_UPDATEdraft → active, creates version 1
POST/api/salary/structures/:id/reviseSalaryStructures_UPDATENew version effective on a future date
POST/api/salary/structures/:id/deactivateSalaryStructures_UPDATEactive → inactive
POST/api/salary/structures/:id/reactivateSalaryStructures_UPDATEinactive → active
POST/api/salary/structures/:id/archiveSalaryStructures_UPDATETerminal
POST/api/salary/structures/:id/copySalaryStructures_CREATEDuplicate as a new draft
DELETE/api/salary/structures/:idSalaryStructures_DELETEDelete a draft
GET/api/salary/designationsSalaryStructures_READList designation defaults
PUT/api/salary/designations/:designationId/defaultSalaryStructures_UPDATESet a designation's default structure

5. Admin Routes — Assignments, Employees, One-Time Items, Bulk

MethodPathPermissionPurpose
GET/api/salary/employeesSalaryAssignments_READCross-staff assignment list
GET/api/salary/employees/:staffIdSalaryAssignments_READOne staff member's assignment history
POST/api/salary/employees/:staffId/assignments/previewSalaryAssignments_READPreview a change before committing
POST/api/salary/employees/:staffId/assignmentsSalaryAssignments_CREATECommit a change (see §9 for accepted change types)
POST/api/salary/employees/:staffId/assignments/:assignmentId/cancelSalaryAssignments_DELETECancel a scheduled assignment
GET/api/salary/one-time-itemsSalaryAssignments_READList one-time items
POST/api/salary/one-time-itemsSalaryAssignments_CREATECreate a one-time item
POST/api/salary/one-time-items/:id/cancelSalaryAssignments_DELETECancel one
POST/api/salary/bulk/previewSalaryAssignments_READPreview a bulk operation across a staff scope
POST/api/salary/bulkSalaryAssignments_CREATELaunch a bulk operation as a salary_run

6. Admin Routes — Periods, Payslips, Runs

MethodPathPermissionPurpose
GET / POST/api/salary/periodsSalaryPeriods_READ/_CREATEList / create a period
GET/api/salary/periods/:idSalaryPeriods_READPeriod detail
POST/api/salary/periods/:id/prepareSalaryPeriods_UPDATECompute payslips
POST/api/salary/periods/:id/recalculateSalaryPeriods_UPDATERecompute (e.g. after a late change)
POST/api/salary/periods/:id/approveSalaryApproval_UPDATEApprove (inline or background)
POST/api/salary/periods/:id/reopenSalaryApproval_UPDATEReopen approved/sent
POST/api/salary/periods/:id/sendSalaryApproval_UPDATERelease to staff
POST/api/salary/periods/:id/cancelSalaryApproval_DELETECancel a period
GET / POST/api/salary/periods/:periodId/exclusionsSalaryPeriods_READ/_UPDATEList / add an exclusion
DELETE/api/salary/periods/:periodId/exclusions/:staffIdSalaryPeriods_UPDATERevoke an exclusion
GET/api/salary/periods/:periodId/runSalaryPeriods_READThe period's active run (data: null if it has never run)
GET/api/salary/periods/:periodId/run/itemsSalaryPeriods_READThe run's items
POST/api/salary/periods/:periodId/run/retrySalaryPeriods_UPDATERetry failed/exception items
POST/api/salary/periods/:periodId/run/cancelSalaryPeriods_UPDATECancel the run
GET/api/salary/payslipsSalaryPeriods_READCross-period payslip list
GET/api/salary/payslips/:idSalaryPeriods_READPayslip detail
POST/api/salary/payslips/:id/recalculateSalaryPeriods_UPDATERecalculate one payslip
POST / DELETE/api/salary/payslips/:id/overrides / .../overrides/:componentIdSalaryPeriods_UPDATEAdd / clear a manual line override
POST/api/salary/payslips/:id/acknowledgeSalaryApproval_UPDATEAcknowledge 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/correctSalaryApproval_UPDATECorrect a sent payslip
POST/api/salary/payslips/:id/refresh-bankSalaryApproval_UPDATERe-pull current bank details
GET/api/salary/runsSalaryAssignments_READCross-run list
GET/api/salary/runs/:idSalaryAssignments_READRun detail
GET/api/salary/runs/:id/itemsSalaryAssignments_READRun items
POST/api/salary/runs/:id/retrySalaryAssignments_UPDATERetry
POST/api/salary/runs/:id/cancelSalaryAssignments_UPDATECancel

7. Admin Routes — Payments, Reports, Settings

MethodPathPermissionPurpose
POST/api/salary/payslips/:id/paymentSalaryPayments_UPDATERecord a manual payment state change
GET/api/salary/inbound-eventsSalarySync_READThe 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/dashboardSalaryDashboard_READCross-staff counts and status
GET/api/salary/historySalaryDashboard_READThe salary change log
GET/api/salary/exports/payslipsSalaryExports_READCSV export of payslips
GET / PATCH/api/salary/settingsSalarySettings_*Global settings
PATCH/api/salary/settings/financialSalaryFinancialSettings_UPDATEPayment authority, distinct approver, SSF rates, retirement cap, accounting client, citizenship sharing
GET/api/salary/notification-settingsSalarySettings_READNotification matrix override
PUT/api/salary/notification-settings/:eventKeySalarySettings_UPDATEOverride one event
POST/api/salary/tax-slabs/copySalaryTaxSettings_CREATECopy a fiscal year's slabs forward
GET/api/salary/tax-slabsSalaryTaxSettings_READList slabs
PUT / DELETE/api/salary/tax-slabs/:fiscalYearBs/:assessmentSalaryTaxSettings_*Set / remove one fiscal year's slabs for an assessment

8. Mobile/Portal Routes

MethodPathPermissionPurpose
GET/api/mobile/staff/salarynone (self-resolved)My current salary assignment, or null if unassigned
GET/api/mobile/staff/payslipsnone (self-resolved)My own released payslips, newest first
GET/api/mobile/staff/payslips/:idnone (self-resolved)One of my own released payslips
GET/api/mobile/staff/assetsnone (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:

CodeMeaning
SALARY_SELF_CHANGE_FORBIDDENA payroll admin tried to change or approve their own salary
SALARY_ASSIGNMENT_NO_CURRENTA non-initial change named for a staff member with no current assignment
SALARY_ASSIGNMENT_DATE_IN_LOCKED_PERIODThe resolved effective date falls in a locked period
SALARY_PAYSLIP_STALEApproval attempted against a payslip whose inputs changed since preparation
SALARY_PERIOD_POPULATION_CHANGEDThe eligible staff population moved since preparation
SALARY_PERIOD_DISTINCT_APPROVER_REQUIREDSame user prepared and is attempting to approve, where the school requires distinct approvers
SALARY_PERIOD_NOT_CONTIGUOUS / _EARLIER_UNAPPROVED / _LATER_IN_PROGRESSPeriod ordering violations
SALARY_PAYMENT_TRANSITION_INVALIDThe requested payment status change is not on SALARY_PAYMENT_TRANSITIONS
SALARY_PAYMENT_AUTHORITY_MANUAL / _EXTERNAL / _NEEDS_CLIENTPayment authority mismatch
SALARY_VERSION_CONFLICTOptimistic version mismatch
SALARY_FLAG_NOT_ACKNOWLEDGEABLEAttempted to acknowledge a flag not on SALARY_ACKNOWLEDGEABLE_FLAGS
STAFF_HAS_ACTIVE_PAY_OR_ASSETSStaff deletion refused — pay or assets still active (shared with assets)
STAFF_SALARY_MANAGED_BY_SALARY_MODULEThe 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 groupCodes it can returnConsumer guidance
Categories, ComponentsSALARY_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 DefaultsSALARY_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 / EmployeesSALARY_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 ItemsSALARY_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.
BulkSALARY_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.
PeriodsSALARY_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.
PayslipsSALARY_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.
PaymentsSALARY_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 SyncSALARY_INBOUND_CLIENT_NOT_DESIGNATED/_EVENT_ID_REUSED/_EVENT_NOT_FOUND/_PAYLOAD_INVALID/_TIMESTAMP_INVALIDThese surface to the accounting application's own client, not the admin panel — see the integration module's docs for its retry guidance.
RunsSALARY_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 SlabsSALARY_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 sharedSALARY_STRUCTURE_NOT_FOUND also applies to designation-default writes—