[FEAT] Full Payroll Module - PayrollRun, PayrollItem Entities & PayrollRunService (#2453)

Merged
RE
2mo ago
+1957-0

Summary

This PR implements the foundational Full Payroll Module for Ever Gauzy, resolving issue #2453. It introduces contracts, database entities, and a business-logic service covering the full payroll lifecycle.

Fixes #2453

What's Changed

1. packages/contracts/src/lib/payroll.model.ts (new)

  • Enums: PayrollRunStatusEnum (DRAFT → PENDING_APPROVAL → APPROVED → PROCESSING → PAID / CANCELLED), PayrollFrequencyEnum (WEEKLY / BI_WEEKLY / SEMI_MONTHLY / MONTHLY / QUARTERLY / ANNUALLY), PayrollItemTypeEnum (BASIC_SALARY, ALLOWANCE, BONUS, COMMISSION, OVERTIME, TAX_DEDUCTION, SOCIAL_SECURITY, HEALTH_INSURANCE, LOAN_DEDUCTION, ADVANCE_DEDUCTION, LEAVE_DEDUCTION, OTHER_ADDITION, OTHER_DEDUCTION), PayrollItemCategoryEnum (EARNING / DEDUCTION)
    • Interfaces: IPayrollRun, IPayrollRunCreateInput, IPayrollRunUpdateInput, IPayrollRunFindInput, IPayrollItem, IPayrollItemCreateInput, IPayrollItemFindInput, IPayrollSummary, IPayrollStatistics

2. packages/core/src/lib/payroll-run/payroll-run.entity.ts (new)

  • TypeORM entity mapped to payroll_run table
    • Extends TenantOrganizationBaseEntity
    • Stores: periodStart, periodEnd, payDate, frequency, status, currency, totals (gross/deductions/net), optional notes
    • @OneToMany relation to PayrollItem with cascade

3. packages/core/src/lib/payroll-item/payroll-item.entity.ts (new)

  • TypeORM entity mapped to payroll_item table
    • Extends TenantOrganizationBaseEntity
    • Stores: type, category, description, amount, quantity, unitPrice, taxable
    • @ManyToOne to PayrollRun (cascade delete) and Employee (SET NULL)

4. packages/core/src/lib/payroll-run/payroll-run.service.ts (new)

  • NestJS @Injectable() service
    • Methods: create(), findAll() (with filters), findOne(), update(), submitForApproval(), approve(), process() (recalculates totals), cancel(), getSummaryByRun(), getStatistics()
    • Status transition guards (throws BadRequestException on invalid transitions)
    • Automatic totals recalculation on process()

How It Works

DRAFT → PENDING_APPROVAL → APPROVED → PAID
                                  ↘ CANCELLED (any pre-PAID state)
  1. HR creates a payroll run (create) — starts as DRAFT
    1. Add payroll items (earnings + deductions) per employee
    1. Submit for approval (submitForApproval)
    1. Manager/finance approves (approve)
    1. System processes and marks as paid (process) — totals recalculated automatically

Checklist

  • Follows existing TypeORM entity conventions (TenantOrganizationBaseEntity)
  • - [x] Uses @gauzy/contracts for all interface imports
  • - [x] Status lifecycle enforced with guards in service layer
  • - [x] All commits reference issue (#2453)
  • - [x] PR targets ever-co/ever-gauzy:develop

Summary by cubic

Introduces the payroll module with PayrollRun, PayrollItem, PayrollRunService, and a /payroll-run REST controller covering the full run lifecycle, totals, per-employee summaries, and statistics. Implements #2453 with new ORG_PAYROLL_VIEW/ORG_PAYROLL_EDIT/ORG_PAYROLL_APPROVE permissions (default-granted to SUPER_ADMIN and ADMIN only) and additive migrations that create both tables and reload role permissions on existing tenants.

  • New Features

    • Contracts in @gauzy/contracts: enums and interfaces for runs, items, summary, and stats; entities registered in coreEntities and module wired into AppModule; endpoints for CRUD, submit, approve, process, cancel, summaries, statistics, and line-item add/remove.
    • Entities PayrollRun and PayrollItem with relations and numeric(14,2) money columns; PayrollRunService handles CRUD, status transitions, totals recomputed in integer cents, filtered queries, summaries, and statistics.
  • Bug Fixes

    • Hardened workflow: update() strips status and totals; cancel() is idempotent and blocks paid runs; all lookups are scoped by tenant and organization.
    • Pagination: findAll() treats page as a page number and converts it to an offset.
    • Contracts: made IPayrollItem.employeeId optional to match the entity; IPayrollItemCreateInput still requires it.