Employee Module Documentation
Overview
The Employee module manages business staff members, separating "system user" (Firebase authentication) from "employee" (business entity). This enables transaction attribution, commission tracking, PIN-based manager approvals, and role-specific permissions for staff who may or may not have system access.
Architecture
Hexagonal Structure
apps/backend/src/employees/
├── employees.module.ts # Module registration
├── domain/
│ └── employees-repository.domain.ts # Repository interface (port) + EMPLOYEES_REPOSITORY token
├── application/
│ └── employees.service.ts # Business logic & use cases
├── infrastructure/
│ └── employees.repository.ts # Kysely implementation (adapter)
└── interfaces/
├── employees.controller.ts # REST endpoints (adapter)
├── dtos/
│ ├── create-employee.dto.ts # Create validation + Swagger schema
│ ├── update-employee.dto.ts # Update validation + Swagger schema
│ ├── set-approval-pin.dto.ts # PIN set validation
│ └── verify-pin.dto.ts # PIN verify validation
└── query/
└── paginate-employees.query.ts # Pagination/filter/sort query (OffsetPaginationMixin + SortMixin)
Dependency Injection
The module uses Symbol-based DI tokens for proper hexagonal boundaries:
EMPLOYEES_REPOSITORYSymbol defined in domain layer- Service injects via
@Inject(EMPLOYEES_REPOSITORY)withIEmployeesRepositoryinterface type - Module registers:
{ provide: EMPLOYEES_REPOSITORY, useClass: EmployeesRepository }
Guards & Permissions
@UseGuards(RolesGuard)— enforces RBAC@PermissionResource(PolicyResource.Employee)— class-level resource binding@PermissionAction(PolicyAction.Create/Read/Update/Delete)— per-endpoint action
Database Schema
Table: employee
| Section | Fields |
|---|---|
| Identity | id, businessId, locationId, userId |
| Identification | employeeNumber, fullName, email, phone |
| Job Info | position, department, employmentType, hireDate, terminationDate |
| Approval | approvalPinHash, canApproveReturns/Discounts/Voids, canOverridePrices |
| Compensation | commissionRate, hourlyRate, salaryAmount, tipPoolShare |
| Restaurant | serverNumber, canTakeOrders, canCloseTables, maxTableCapacity |
| Retail | salesTargetMonthly, canProcessReturns |
| Status | isActive, isManager, notes |
| Audit | createdAt, createdBy, updatedAt, updatedBy |
Key Indexes
(businessId)— business scoping(businessId, isActive)— active employee queries(businessId, position)— position filtering(businessId, employeeNumber)UNIQUE — employee number per business(businessId, userId)UNIQUE — one employee per user per business
API Endpoints
All endpoints require Bearer authentication and businessId context.
CRUD
| Method | Endpoint | Description |
|---|---|---|
POST | /employees | Create employee (businessId + createdBy in body) |
GET | /employees?businessId=<id> | Paginated list with filters and sorting |
GET | /employees/:id?businessId=<id> | Get by ID |
PATCH | /employees/:id | Update (businessId + updatedBy in body) |
DELETE | /employees/:id?businessId=<id> | Soft delete (sets isActive=false) |
Lookups
| Method | Endpoint | Description |
|---|---|---|
GET | /employees/active?businessId=<id> | All active employees |
GET | /employees/managers?businessId=<id> | All active managers |
GET | /employees/by-number/:number?businessId=<id> | Find by employee number |
PIN Management
| Method | Endpoint | Description |
|---|---|---|
POST | /employees/:id/set-pin?businessId=<id> | Set 4-6 digit approval PIN |
POST | /employees/:id/verify-pin?businessId=<id> | Verify PIN → { valid: boolean } |
DELETE | /employees/:id/pin?businessId=<id> | Remove PIN |
Pagination Parameters
| Param | Type | Default | Description |
|---|---|---|---|
size | number | 10 | Page size |
page | number | 1 | Page number |
orderBy | string | fullName | Sort field: fullName, hireDate, position, employeeNumber |
order | string | asc | Sort direction: asc, desc |
search | string | — | Search across fullName, email, employeeNumber |
isActive | boolean | — | Filter by active status |
position | string | — | Filter by position |
department | string | — | Filter by department |
locationId | UUID | — | Filter by location |
isManager | boolean | — | Filter by manager flag |
Usage Examples
Creating an Employee
// Via service (internal)
const employee = await employeesService.createEmployee({
businessId,
fullName: "John Doe",
employeeNumber: "EMP-001",
position: "Server",
canTakeOrders: true,
isActive: true,
createdBy: userId,
});
// Via API
POST /employees
{
"businessId": "...",
"fullName": "John Doe",
"employeeNumber": "EMP-001",
"position": "Server",
"canTakeOrders": true,
"createdBy": "..."
}
Setting Approval PIN
await employeesService.setApprovalPin(employeeId, businessId, userId, "1234");
const isValid = await employeesService.verifyPin(employeeId, businessId, "1234");
Integration with Other Modules
The EmployeesService is exported and used by:
| Module | Usage |
|---|---|
| Restaurant Orders | order.waiter_id → validates active + canTakeOrders |
| Retail Sales | sale.salesperson_id, sale.approved_by_employee_id |
| Cash Register | session.opened_by_employee_id, session.closed_by_employee_id |
| Discounts | Employee approval for discount applications |
| Store Credit | Employee context resolution |
| Data Import | Bulk employee upsert |
Backward-Compatible Methods
For cross-module consumers, the service exposes convenience methods with simpler signatures:
findById(id)— throws NotFoundException (no businessId required)findByUserId(businessId, userId)— returns null if not linkedfindByEmployeeNumber(businessId, number)— throws NotFoundExceptionfindAll(businessId, options)— legacy paginated listcreate(businessId, userId, data)— legacy createupdate(id, userId, data)— legacy update
Security
PIN Management
- Algorithm: bcrypt (cost factor 12)
- Storage: Only hashed in
approvalPinHashcolumn - Format: 4-6 digits (validated via regex)
- Logging: All operations logged (raw PINs never logged)
Permission Flags
| Flag | Purpose |
|---|---|
canApproveReturns | Approve return requests |
canApproveDiscounts | Approve discount applications |
canApproveVoids | Approve void transactions |
canOverridePrices | Override item prices |
canTakeOrders | Take restaurant orders |
canCloseTables | Close restaurant tables |
canProcessReturns | Process retail returns |
Multi-Tenancy
- All queries scoped by
businessId - Unique constraints per-business (employeeNumber, userId)
- One employee record per user per business
- Foreign keys use
ON DELETE SET NULLto preserve transaction history
Design Decisions
- Soft deletes only — employees are never physically deleted to preserve transaction attribution
- Symbol-based DI — ensures proper hexagonal boundaries between layers
- Backward-compatible methods — external consumers use simpler signatures while the controller uses the standard pattern
- Business-scoped repository — all mutations require businessId for multi-tenancy safety
Version: 2.0.0 Last Updated: 2026-03-24 Status: Production Ready