Payroll & Statutory Compliance (PAY)
Module code PAY · Release R1 (MVP); Form 16, 24Q data, journal export and VDA in R2 · Packaging Core · Requirements
FR-PAY-*,BR-PAY-*,RULE-PAY-*in the PRD and BRD · Related Attendance, Leave, Claims, Exit, Core HR
:::caution Statutory figures are examples Every rate, ceiling, slab and threshold on this page is an example to verify against the current notification before use. In the product they are reference data in a versioned statutory pack published by the platform (see Platform), never constants in code. :::
1. Overview and scope
PAY turns compensation, attendance and one-time inputs into correct, reproducible net pay and the statutory outputs that go with it. It is the main reason SMEs buy the product and the riskiest module in the MVP. The design rests on four ideas:
- Salary structures (templates of components and formulas) are separate from each employee's compensation record (effective-dated CTC + structure version).
- A pure, deterministic calculation library (
packages/payroll-engine) runs gross-to-net from a frozen input snapshot. It has golden tests. - A full pay-run lifecycle with validation, review, approval, lock, payment and reversal. Locked runs are immutable.
- Statutory reference data (EPF, ESI, PT, LWF, TDS slabs, minimum wages) is versioned by effective date and state.
In scope (R1)
- Components, structures, compensation assignment and revision (with arrears), CTC breakup.
- Pay schedules and periods, including an attendance cut-off different from the pay month.
- Regular and off-cycle runs, salary hold, inputs (arrears, variable pay, bonus, other deductions, OT, extra days, reimbursements, LOP reversal).
- Statutory: EPF (incl. EPS, EDLI, admin charges, VPF), ESI, PT by state, LWF by state, gratuity provision, TDS with regime choice, declarations and proofs.
- Advance salary and loans with a recovery ledger.
- Payslips (PDF, optional password, publish and notify), payroll register, statutory files (EPFO ECR, ESIC contribution upload, PT and LWF challan data), bank transfer files.
R2: F&F run type (with Exit), Form 16 Part B, 24Q return data, accounting journal export (Tally XML, Zoho Books), VDA, statutory bonus calculator.
Out of scope: direct portal filing or automation (legal and fragility risk), payment execution through bank APIs (later H2H), non-India payroll.
2. Learned from the reference product
Source: Spryple analysis, §7, §16, §19 and §21.5.
- [Observed] Pay groups are chosen by CTC range; each group lists earnings and deductions as a percentage of CTC or Basic, or flat, and components can be renamed for the payslip (SP-PAY-05, SP-PAY-10, SP-PAY-11; A/M: Payroll).
- [Observed] A pay schedule can use the same or different payroll and leave windows (e.g. 25th to 24th) and a salary basis of actual days in month or organisation working days (SP-PAY-04).
- [Observed] Statutory configuration covers EPF with EDLI and admin charges, ESI with state-wise numbers and an "include employer contribution in CTC" toggle, PT slabs by state; outputs are reports (EPF download, ESI PDF/Excel, PT by state), plus a bank PDF and a bank-wise summary (SP-PAY-02, SP-PAY-03, SP-PAY-13, SP-PAY-15).
- [Observed] The documented TDS flow is a manually typed amount per employee (SP-PAY-16), although the employee menu shows "IT Declaration" and "Form 16" items [Observed-SS §21.5].
- [Observed] Advance salary needs at least one payslip, is capped at gross, is approved by the Finance Manager and recovered over instalments with cooling-off months (SP-PAY-07; E: Payroll).
- [Inferred] No run lifecycle (lock, approve, rollback), variance checks or off-cycle runs are documented, and gratuity appears as an employee deduction on a sample payslip (§7.3, §21.5).
3. Our design decisions
| ID | Decision [Proposed] | Reason |
|---|---|---|
| D-PAY-01 | Salary structure = versioned template of components with formulas. Compensation = per employee, effective-dated (valid_from/valid_to), pointing to a structure version and an annual CTC. Structure suggestions by grade, not by CTC band. | A promotion at the same CTC can change structure; increments do not need a new "group". |
| D-PAY-02 | Components have typed flags: taxable, PF wage, ESI wage, PT wage, gratuity wage, prorate, part of CTC, payslip label, rounding, statutory code. Formulas use a safe expression language with references to other components and inputs, evaluated in dependency order. | One engine handles all tenants; no per-tenant code. |
| D-PAY-03 | A pay run freezes a snapshot per employee: compensation version, payable days, inputs, statutory rate versions, declaration version, and the hash of all of it. Outputs are stored as pay_run_line rows. | Exact reproduction of what was paid; legal defensibility. |
| D-PAY-04 | Run lifecycle: Draft → Calculated → InReview → Approved → Locked → Paid, with Reversed via a reversal run. Preparer and approver must differ when segregation of duties is on. | Controls the reference product lacks. |
| D-PAY-05 | Back-dated compensation changes and post-lock attendance changes become arrears or LOP-reversal inputs in the next run, recomputed from the old periods' snapshots. | Locked runs stay untouched; employees still get the correct money. |
| D-PAY-06 | A TDS engine projects annual tax under the chosen regime, using declarations (estimates) before the proof window and verified proofs after. The manual TDS override remains as an audited input. | Parity with the hidden IT Declaration menu, and correctness. |
| D-PAY-07 | Statutory outputs are portal-ready files generated by versioned adapters, not reports. | The portal formats are what finance actually needs. |
| D-PAY-08 | Gratuity is an employer provision (accrual), not a payslip deduction; payment happens in F&F. | Correct treatment; avoids the reference product's confusing display. |
| D-PAY-09 | Money is stored as integer paise (bigint … _paise, see Data model) and computed with a decimal library, never floating point. Rounding is per component and per statutory rule. | Paise-level errors are visible to every employee. |
| D-PAY-10 | Only one non-reversed regular run per employee per period. Off-cycle runs are separate run types. | Prevents double pay. |
4. Domain model
Notes:
pay_component.kind:EARNING,DEDUCTION,EMPLOYER_CONTRIBUTION,REIMBURSEMENT,INFORMATION.calc_type:FIXED,PERCENT,FORMULA,INPUT,STATUTORY.pay_run.run_type:REGULAR,OFF_CYCLE,ARREARS,BONUS,FNF(R2),REVERSAL.pay_run_employee.status:INCLUDED,HELD,EXCLUDED,ERROR.pay_input.input_type:ARREAR,VARIABLE_PAY,BONUS,OTHER_DEDUCTION,REIMBURSEMENT,OT_HOURS,EXTRA_DAYS,LOP_REVERSAL,LOP_ADDITIONAL,TDS_OVERRIDE,LEAVE_ENCASHMENT,NOTICE_PAY,NOTICE_RECOVERY.pay_statutory_raterows are copied from the platform's statutory pack (tenant_idset when a tenant overrides or accepts a pack). PAN and Aadhaar stay in Core HR with envelope encryption; PAY reads them only for file generation.pay_bank_file.debit_account_idpoints to a legal-entity bank account in ORG. Employee bank accounts are in EMP.
5. State machines
5.1 Pay run
Rules: Locked requires the attendance period to be locked (Attendance). Payslips are published to employees at Locked or at a scheduled publish time. A reversal creates a REVERSAL run whose lines negate the original; a corrected OFF_CYCLE run follows.
5.2 Tax declaration
5.3 Advance salary or loan
6. Key business logic
6.1 CTC breakup at assignment
function breakup(annual_ctc, structureVersion, statutoryContext):
monthly_ctc = annual_ctc / 12
values = {}
for line in topologicalOrder(structureVersion.lines, by formula references):
if line.is_balancing: continue
values[line.component] = evaluate(line.formula, values, monthly_ctc)
# e.g. BASIC = 0.50 * CTC ; HRA = 0.40 * BASIC ; CONV = 1600
employer = employerContributions(values, statutoryContext) # EPF ER, ESI ER if part_of_ctc
balancing = monthly_ctc - sum(earnings in values) - sum(employer where part_of_ctc)
if balancing < 0: error "structure exceeds CTC"
values[balancing component] = balancing # e.g. SPECIAL_ALLOWANCE
# Labour Codes wage check (optional, configurable): if excluded allowances exceed
# 50% of total remuneration, the excess counts as wages for PF and gratuity. Verify.
return values
6.2 Payable days
function payableDays(employee, period, schedule):
att_window = [period.att_window_start, period.att_window_end] # e.g. 26th to 25th
basis_days = case schedule.salary_basis:
CALENDAR -> days in period
FIXED -> schedule.fixed_basis_days # e.g. 30 or 26
WORKING_DAYS -> days in period minus week-offs and holidays for the employee
employed = days in period within [joining_date, last_working_date]
if salary_basis == WORKING_DAYS: employed = working days within that range
unpaid = 0
for day in ATT days in att_window (locked):
unpaid += (1 - day.paid_fraction) # absences, unpaid halves, penalised week-offs
for days the employee was employed
unpaid += LVE LOP days in att_window not already counted as unpaid in ATT
# Days after the attendance cut-off (att_window_end + 1 .. period_end) are deemed
# present now and trued up next period through LOP_ADDITIONAL inputs.
# Proportional week-off deduction (reference payroll rule, configurable):
if policy.prop_weekoff.enabled and unpaid > policy.prop_weekoff.lop_threshold:
weekoffs = week-offs in att_window
unpaid += weekoffs * unpaid / working_days_in_att_window
unpaid += sum(LOP_ADDITIONAL inputs) - sum(LOP_REVERSAL inputs as days, when the policy
settles reversals as days rather than as amounts)
payable = clamp(employed - unpaid, 0, basis_days)
return basis_days, payable, unpaid
Example: CALENDAR basis, 30-day month, employee joined on the 11th, 2 LOP days in the window. employed = 20, unpaid = 2, payable = 18, proration factor 18 / 30 = 0.6.
6.3 Gross-to-net order of operations
The engine is a pure function calculate(snapshot) -> lines. The order matters because later steps use earlier results.
function calculate(s): # s = frozen input snapshot
# 1. Earnings from the compensation record
for c in s.compensation.lines where kind == EARNING:
amt = c.monthly_amount
if c.prorate: amt = amt * s.payable_days / s.basis_days
line(c, amt)
# 2. Mid-period compensation change: split the period by valid_from and prorate
# each slice by its own payable days (handled by the snapshot builder).
# 3. Variable earnings from inputs
line(OT, s.ot_hours * hourlyRate(s) * s.ot_multiplier)
line(EXTRA_DAYS, s.extra_days * dailyRate(s))
line(ARREAR, VARIABLE_PAY, BONUS, LEAVE_ENCASHMENT, NOTICE_PAY inputs)
gross = sum(earning lines)
# 4. Reimbursements (non-taxable, outside gross for statutory bases)
line(REIMBURSEMENT inputs)
# 5. Statutory wage bases
pf_wage = sum(lines with pf_wage) ; pf_base = min(pf_wage, ceiling) if capped else pf_wage
esi_wage = sum(lines with esi_wage)
pt_wage = sum(lines with pt_wage)
# 6. Employee statutory deductions
line(EPF_EE, round(pf_base * epf_ee_rate)) # example 12%
line(VPF, round(pf_wage * vpf_percent))
if esiCovered(s, esi_wage): line(ESI_EE, roundUp(esi_wage * esi_ee_rate)) # example 0.75%
line(PT, ptSlab(s.pt_state, pt_wage, s.period)) # state slab, month rules
line(LWF_EE, lwf(s.lwf_state, s.period)) # only in due months
# 7. Income tax (TDS) - see 6.5
line(TDS, s.tds_override if present else tds(s, lines))
# 8. Other deductions in priority order
for d in [LOAN_RECOVERY, ADVANCE_RECOVERY, OTHER_DEDUCTION inputs, NOTICE_RECOVERY]:
line(d)
# 9. Net and the net-pay floor
net = gross + reimbursements - sum(deduction lines)
if net < policy.min_net_pay:
reduce non-statutory deductions in reverse priority until net >= floor
carry the unrecovered amounts to the next period ; add warning NET_FLOOR_APPLIED
# 10. Employer contributions (cost, not deducted)
line(EPF_ER_EPF, EPS, EDLI, EPF_ADMIN) # see 6.4
if esiCovered: line(ESI_ER, roundUp(esi_wage * esi_er_rate)) # example 3.25%
line(LWF_ER) ; line(GRATUITY_PROVISION, gratuity_wage * provision_rate)
# 11. Rounding: per component rule, then net rounded per policy (e.g. nearest rupee)
# 12. YTD: ytd_amount on each line = previous locked YTD + amount
return lines, gross, net, employer_cost
6.4 EPF split (example rates, verify)
pf_base = min(pf_wage, 15000) when pf_wage_capped else pf_wage # ceiling example
epf_ee = 12% of pf_base
eps = 8.33% of min(pf_base, 15000) # pension, capped; zero if not EPS-eligible
epf_er = 12% of pf_base - eps # employer share to the EPF account
edli = 0.5% of min(pf_base, 15000)
admin = 0.5% of pf_base (subject to a minimum per establishment)
6.5 TDS projection
function tds(s, lines):
months_left = remaining months in FY including this one
recurring = taxable monthly earnings from compensation (unprorated)
projected = YTD taxable (incl. previous employer per declaration)
+ taxable lines this month
+ recurring * (months_left - 1)
if regime == OLD:
projected -= exemptions (HRA per rent declared or proved, LTA, etc.)
projected -= deductions (chapter VI-A items capped per item, PT paid, standard deduction)
else: # NEW regime
projected -= standard deduction for the year # example, verify
tax = slabTax(regime, FY, projected) - rebate + surcharge
tax = tax * (1 + cess_rate) # example 4%
remaining = max(0, tax - TDS deducted YTD - previous employer TDS)
# One-time items (bonus, arrears): tax on the increment is deducted in full this month
# when policy.tds.one_time_full_deduction is true.
return round(remaining / months_left)
Declarations are used until proof_window_start; afterwards only verified proofs count. Regime is the employee's declared choice; the default is the statutory default regime. Section references follow the Income-tax Act in force for the year (the 2025 Act applies from FY 2026-27; verify mappings).
6.6 ESI coverage (example rule, verify)
esiCovered(s, esi_wage):
if employee was covered at the start of the current contribution period
(April to September or October to March): covered for the whole period
else covered when esi_wage <= ceiling (example 21000; higher for persons with disability)
6.7 Arrears from back-dated changes
on pay.compensation.changed with valid_from in a locked period:
for each locked period P from valid_from to last locked period:
old = locked pay_run_employee for P
new = calculate(old.input_snapshot with compensation replaced by the new version)
diff = new.lines - old.lines per component (earnings and statutory)
create pay_input ARREAR per earning component and statutory differences
for the next open period, source = ARREARS, referencing P
Post-lock attendance or leave changes arrive as att.adjustment.created or lve.request.cancelled and become LOP_REVERSAL (money owed to the employee) or LOP_ADDITIONAL inputs.
6.8 Pre-approval validation
Blocking (must fix or exclude the employee): missing bank account for bank-transfer mode; negative net after the floor; PF member without UAN at file generation; compensation changed after calculation; attendance period not locked; employee with zero payable days but non-zero earnings.
Warnings (must be acknowledged): net pay variance above variance_threshold_percent vs last period; new joiners and leavers; ESI coverage changes; gross below the configured state minimum wage for the employee's skill category (minimum-wage table from the statutory pack); TDS override present; held salaries.
6.9 Statutory and bank file generation
Generators are adapters with a version: EPFO_ECR (text format with UAN rows), ESIC_MC (monthly contribution upload), PT_<STATE> (challan or return data), LWF_<STATE>, TDS_24Q (R2), FORM16_PARTB (R2). Bank adapters: GENERIC_NEFT_CSV plus bank-specific formats. Every file is generated from locked run lines only, stored in object storage with Object Lock, and listed with its adapter version so a format change never alters a past file.
7. Configuration
| Setting | Type | Default | Description |
|---|---|---|---|
schedule.frequency | enum | MONTHLY | Only monthly in R1 |
schedule.pay_day | integer | last day | Day salaries are credited |
schedule.attendance_cutoff_day | integer | null | Null means same window as the pay month; e.g. 25 gives a 26th to 25th window |
schedule.salary_basis | enum | CALENDAR | CALENDAR, FIXED, WORKING_DAYS |
schedule.fixed_basis_days | integer | 30 | Used with FIXED |
epf.enabled | boolean | true | EPF applies |
epf.wage_ceiling_applies | boolean | true | Contributions on capped wages by default |
epf.employer_in_ctc | boolean | true | Employer PF is part of CTC |
epf.include_admin_in_ctc | boolean | false | Admin and EDLI charges in CTC |
esi.enabled | boolean | true | ESI applies |
esi.employer_in_ctc | boolean | true | Reference toggle [Observed SP-PAY-03] |
pt.enabled | boolean | true | PT by the employee's work state |
lwf.enabled | boolean | true | LWF by state |
gratuity.provision_enabled | boolean | true | Monthly provision posted as employer cost |
gratuity.provision_rate | number | 0.0481 | Example 15/26/12 of gratuity wage; verify actuarially |
tds.default_regime | enum | NEW | Statutory default regime (verify) |
tds.declaration_window | date range | April | When employees declare |
tds.proof_window | date range | January to February | When proofs are accepted |
tds.one_time_full_deduction | boolean | true | Tax on one-time items deducted in the same month |
ot.included_in_salary | boolean | true | Reference payroll rule [Observed SP-PAY-07] |
ot.rate_basis | enum | GROSS | BASIC_DA or GROSS for the hourly rate |
extra_days.rate_basis | enum | GROSS | Daily rate base for extra days |
absent_is_lop | boolean | true | Reference payroll rule [Observed SP-PAY-07] |
prop_weekoff.enabled | boolean | false | Proportional week-off deduction |
prop_weekoff.lop_threshold | number | 5 | LOP days before it applies |
statutory_independent_of_lop | boolean | false | Compute PF and ESI on full wages despite LOP [Observed SP-PAY-07] |
min_net_pay | number | 0 | Net floor before deductions are deferred |
rounding.net | enum | NEAREST_RUPEE | NONE, NEAREST_RUPEE, UP, DOWN |
variance_threshold_percent | number | 20 | Warning above this change vs last period |
sod.enabled | boolean | true | Preparer cannot approve; compensation editor cannot approve the run |
payslip.password | enum | NONE | NONE, PAN_DOB, CUSTOM |
payslip.publish | enum | ON_LOCK | ON_LOCK or SCHEDULED |
advance.min_payslips | integer | 1 | [Observed SP-PAY-07] |
advance.max_percent_of_gross | number | 100 | Cap on advance amount |
advance.max_instalments | integer | 6 | Recovery instalments |
advance.cooling_off_months | integer | 3 | Gap between advances [Observed SP-PAY-07] |
freeze_window | day range | 25 to 5 | Platform change-freeze window shown to payroll users |
8. Screens by persona
| Persona | Web | Mobile |
|---|---|---|
| Employee | Payslips (FY and month), CTC and revision history, tax regime and declarations, proofs upload, tax projection, Form 16 (R2), advance request and recovery status | Payslips, declarations status, advance request |
| Finance admin | Components, structures with formula editor and preview, statutory registrations, statutory pack acceptance with change log, schedules, bank accounts, payslip template | — |
| Payroll manager | Assign and revise compensation with breakup preview, inputs grid (bulk upload), run console (calculate, warnings, variance, hold, approve, lock), payslip publish, statutory files, bank files, advance approvals, proof verification | Run status and approval (approver only) |
| Org owner | Payroll cost dashboard by department and month, run approval when configured | Approval |
| Auditor | Read-only runs, files and audit trail | — |
9. Events
| Event | Emitted when | Main consumers |
|---|---|---|
pay.compensation.changed | A compensation record is created or revised | PAY arrears calculator, NTF (revision letter), RPT |
pay.input.created | A one-time input is added (any source) | PAY run invalidation |
pay.run.created | A run is created | RPT |
pay.run.calculated | Calculation completes | NTF (preparer), RPT |
pay.run.submitted | Sent for review | NTF (approver task) |
pay.run.approved | Approved | NTF |
pay.run.locked | Locked; snapshots final | ATT (close period), CLM (mark payouts paid-in-run), EXT (F&F settled), NTF, accounting export |
pay.payslip.published | Payslips visible to employees | NTF (email, push) |
pay.statutory_file.generated | A statutory file is ready | NTF, RPT |
pay.bank_file.generated | A bank file is ready | NTF |
pay.run.paid | Bank payment confirmed | CLM, EXT, NTF |
pay.run.reversed | A reversal run is locked | ATT, CLM, NTF |
pay.declaration.submitted | Employee submits declarations or proofs | NTF (payroll task) |
pay.loan.approved | Advance or loan approved | NTF |
Consumed: att.period.locked, att.period.unlocked, att.adjustment.created, att.overtime.approved, lve.request.cancelled, lve.encashment.approved, lve.cycle.closed, clm.claim.approved (reimbursement input when payout mode is payroll), ext.fnf.initiated (create F&F run), emp.employee.joined, emp.job.changed (PT state, structure suggestion), plt.statutory_pack.published.
10. API surface
Detailed in API endpoints.
| Resource | Main operations |
|---|---|
/v1/pay/components | CRUD |
/v1/pay/structures and /v1/pay/structures/:id/versions | CRUD, publish, POST /:id/preview with a CTC |
/v1/pay/compensations | POST assign or revise (with breakup), GET history per employee |
/v1/pay/employee-statutory | GET, PUT UAN, ESIC, regime, VPF |
/v1/pay/schedules and /v1/pay/periods | CRUD, GET periods |
/v1/pay/inputs | POST single or bulk upload, GET, DELETE while the run is not locked |
/v1/pay/runs | POST create, POST /:id/calculate, POST /:id/submit, POST /:id/approve, POST /:id/lock, POST /:id/reverse, GET /:id/employees, GET /:id/variance |
/v1/pay/runs/:id/employees/:employeeId/hold | POST hold or release |
/v1/pay/payslips | GET (SELF, ORG), GET /:id/pdf, POST /publish |
/v1/pay/statutory-files | POST generate for run and scheme, GET download |
/v1/pay/bank-files | POST generate, POST /:id/confirm, GET download |
/v1/pay/tax-declarations and /v1/pay/tax-proofs | POST, PATCH, POST /:id/verify |
/v1/pay/loans | POST request, GET, ledger |
11. Permissions
Salary data is sensitive: list views mask amounts unless the viewer has pay.compensation.read at the right scope.
| Permission | Typical scopes | Default roles |
|---|---|---|
pay.payslip.read | SELF, ORG | employee (SELF), payroll_manager, auditor |
pay.compensation.read | SELF, LOCATION, ORG | employee (SELF), payroll_manager, finance_admin, org_owner |
pay.compensation.manage | ORG, LOCATION | payroll_manager |
pay.component.manage | ORG | finance_admin |
pay.structure.manage | ORG | finance_admin |
pay.statutory.manage | ORG | finance_admin |
pay.input.manage | ORG, LOCATION | payroll_manager |
pay.run.prepare | ORG, LOCATION | payroll_manager |
pay.run.approve | ORG | finance_admin, org_owner |
pay.run.lock | ORG | payroll_manager, finance_admin |
pay.run.reverse | ORG | finance_admin (MFA step-up, audited) |
pay.file.generate | ORG | payroll_manager |
pay.declaration.submit | SELF | employee |
pay.declaration.verify | ORG | payroll_manager |
pay.loan.request | SELF | employee |
pay.loan.approve | ORG | payroll_manager |
pay.report.export | ORG | payroll_manager, finance_admin, auditor |
12. Reports
| Report | Contents | Reference |
|---|---|---|
| Payroll register | Every employee and component for a run; totals | [Observed] SP-PAY-13 |
| Bank statement and bank-wise summary | Net pay by bank and debit account, with statutory totals | [Observed] SP-PAY-15 |
| EPF, ESI, PT, LWF registers | Contribution by employee for the period, plus the generated file | [Observed] SP-PAY-13, files [Proposed] |
| Payable days report | Basis, employed, unpaid and payable days with the source breakdown | [Observed] SP-PAY-13 |
| Variance report | Employee and component changes vs the previous period | [Proposed] |
| Pay structure and compensation list | Employee, structure, CTC, effective date | [Observed] §21.5 |
| CTC and revision history | Per employee | [Observed] E: Payroll |
| Tax projection and declarations | Projected tax, declared vs proved | [Proposed] |
| Advance and loan register | Principal, recovered, balance, status | [Observed] SP-PAY-13 (empty in the docs) |
| Payroll cost by department and month | Gross, employer cost, trend | [Observed] SA dashboard |
| Arrears register | Arrear inputs with source period | [Observed] SP-PAY-09 |
| Form 16 and 24Q data | R2 | [Proposed] |
13. Release and priority
| Capability | Release | MoSCoW |
|---|---|---|
| Components, structures, compensation, CTC breakup, revision with arrears | R1 | Must |
| Pay schedule with attendance cut-off; payable days | R1 | Must |
| Run lifecycle with validation, variance, hold, SoD, lock, reversal | R1 | Must |
| EPF, ESI, PT, LWF, gratuity provision | R1 | Must |
| TDS engine with regime, declarations and proofs | R1 | Must |
| ECR, ESIC, PT and LWF files; generic bank file | R1 | Must |
| Payslips with publish and notify; password option | R1 | Must |
| Advance salary and loans with recovery ledger | R1 | Should |
| Off-cycle and bonus runs | R1 | Should |
| Bank-specific file formats | R1 (top 3 banks), R2 (more) | Should |
| F&F run type | R2 | Must |
| Form 16 Part B, 24Q data | R2 | Must |
| Accounting journal export (Tally, Zoho Books) | R2 | Should |
| VDA (CPI-linked DA) | R2 | Could |
| Statutory bonus calculator | R2 | Could |
| Minimum-wage check by state and skill | R1 (warning) | Should |
14. Open questions
- Which banks do pilot customers use? This sets the R1 bank-format list.
- Should PF on arrears be computed on the arrear month's ceiling or the original month's? The default is the original month's rules.
- Do we support multiple legal entities (separate PF and ESI codes) in one tenant in R1? The data model allows it; UI scope is to be confirmed.
- Where are previous-employer income and TDS captured for mid-year joiners: declaration only, or also an import? The default is both.
- What is the R1 approach to the Labour Codes wage definition: warning only, or automatic adjustment of PF and gratuity wages? The default is a configurable warning plus an opt-in adjustment.
- Should the payroll engine move to a separate worker service before GA? The trigger in the architecture is a run longer than 10 minutes for the largest tenant.