Owner: Finance / Payroll Ops (per company) | Jurisdiction: Sultanate of Oman | Review: On any labour-law / rate change
SmartPRO computes Omani payroll and generates the WPS Salary Information File (SIF) required by Oman's Wage Protection System (Ministry of Labour). The run is transactional (all-or-nothing) and locked after payment so a period can't be silently re-run.
The statutory figures below are quoted from the code as of this writing β re-verify against current Oman regulations each review cycle. SmartPRO is the source of these numbers, not the law.
Statutory figures (from code)
| Item | Value | Notes |
|---|---|---|
| PASI β employee | 7% of basic | Omani nationals only; deducted from net |
| PASI β employer | 11.5% of basic | tracked for reporting, not deducted from the employee |
| Overtime | 1.25Γ hourly; 208 monthly-hours basis | OT is pre-computed in ot_records, not recalculated at pay time |
| VAT | 5% | on service/processing fees, not on wages |
| Gratuity (EOSB) | RD 53/2023 cutoff 2023-07-31 | 15β30 days/yr before, 30 days/yr after; daily wage = basic Γ· 30 |
| Min attendance to run | 50% of expected workdays | below this raises a reconciliation warning |
| Omanisation | sector targets 5β60% (default 35%) | reported, not blocked at the pay run |
Monthly payroll cycle
Close & reconcile attendance
Confirm attendance sessions for the period are **closed**. The run needs β₯ 50% of expected workdays; below that, `executeMonthlyPayroll` raises a reconciliation warning that must be explicitly acknowledged.
Execute the run (draft)
`executeRun({ companyId, month, year })`. Per employee, inside one DB transaction: gross = basic + housing/transport/other allowances + overtime (`ot_records`) + KPI commission; deductions = **PASI 7% of basic (Omanis)** + loans + absence + other; net = gross β deductions. Any error rolls the whole run back β no partial payroll.
Review the draft
Inspect line items for outliers β unexpected zero-PASI (nationality misclassified), missing overtime, loan balances. Remember the 11.5% employer PASI share is informational and not deducted.
Approve (separation of duties)
`approveRun(runId)` β **must be a different user than the preparer**; the procedure rejects self-approval. Stamps `approvedAt` / `approvedByUserId` and writes a governance audit record. Status β `approved`.
Generate payslips
`generatePayslips(runId, employeeIds)` renders HTML payslips to storage (export is audited).
Generate the WPS SIF
`generateWpsBankFile(runId)` β **Excel `.xlsx`** in the Bank Muscat WPS template: an employer header row (CR number, payer CR, bank short name, payer account, year/month, totals, record count) then one row per employee with 15 SIF fields β ID type/number, name, BIC + 16-digit account (from IBAN), salary frequency, working days, net, basic, extra hours, extra income, deductions (excl. PASI), **Social Security deductions (PASI, Omanis only)**, notes.
Upload to bank & mark paid
Upload the SIF to your WPS-enabled bank portal before the **MOL deadline (10th of the following month)**. Then `markPaid(runId)` (must be `approved` first) β status `paid`, `paidAt` stamped, run **locked**.
Run state machine
draft βexecuteRunββΊ processing ββΊ approved ββΊ paid (locked)
β² β
different user ββββββββ βββΊ wps_generated / ready_for_upload
approved, paid, and locked all block re-execution (BLOCKED_RERUN_STATUSES).
WPS compliance reminder
The wpsComplianceReminder job (server/jobs/wpsComplianceReminder.ts) fires on the 25th of each month at 02:00 UTC, emailing each company's payroll officer (wps_configurations.contact_email) to validate and submit the SIF before the 10th-of-next-month MOL deadline, and writes an in-app notification for every HR-admin.
| Control | Effect |
|---|---|
DISABLE_WPS_REMINDER_JOB=1 |
Halts the reminder job |
RESEND_API_KEY |
Required for email delivery (notifications still post in-app) |
Compliance notes
- PASI eligibility is derived from each employee's
nationality(matchesomani/oman/ΨΉΩ Ψ§ΩΩ). Expats get 0% PASI. - Omanisation ratio is shown on the Compliance Dashboard via
computeOmanizationRate(sector targets 5β60%, default 35%); a red badge = below 80% of target. It is reported, not enforced at the pay run. - Gratuity / EOSB is a termination event, not part of the monthly run β
estimateGratuityArticle61({ basicSalaryOmr, hireDate, endDate })splits service across the 2023-07-31 cutoff. Treat output as an estimate, not legal advice.
Failure modes
| Symptom | Cause | Fix |
|---|---|---|
| SIF generation fails | Employee missing IBAN / bank code | Fill in HR β Employees β [name] β Banking |
| Bank rejects the SIF | Bank's BIC not in the short-name map | Add the BICβshort-name mapping in server/lib/wpsSifService.ts |
| Employee shows 0 PASI unexpectedly | Nationality not detected as Omani | Correct nationality, re-run (only if not yet approved) |
| Can't re-run a period | Status is approved/paid/locked |
Make a correction in the next period β locked is by design |
| Preparer can't approve own run | Separation of duties | A second Finance/Company admin must approve |
| Attendance < 50% blocks the run | Reconciliation gate | Fix attendance, or run with the acknowledge flag + documented justification |
Key files
| File | Purpose |
|---|---|
server/lib/payrollExecuteMonthly.ts |
executeMonthlyPayroll β the transactional run |
server/lib/payrollExecution.ts |
PASI / overtime / rounding helpers |
server/lib/wpsSifService.ts |
generateSIFFile β Excel WPS SIF |
server/routers/payroll.ts |
executeRun / approveRun / markPaid / generatePayslips / generateWpsBankFile |
server/lib/billingEngine.ts |
gratuity (Article 61) + VAT |
shared/omanization.ts |
computeOmanizationRate + sector targets |
server/jobs/wpsComplianceReminder.ts |
monthly WPS reminder job |