Spaces:
Sleeping
Sleeping
| # Payroll System Implementation Summary | |
| ## Overview | |
| Complete implementation of a robust weekly payroll generation system for the SwiftOps field service management platform. The system automatically aggregates worker attendance (timesheets) and work completed (ticket assignments) to generate accurate weekly compensation records. | |
| ## Architecture Decision | |
| **Approach:** Two separate background jobs (Weekly Batch Generation) | |
| - **Daily Job:** Generate timesheets from ticket_assignments (already implemented) | |
| - **Weekly Job:** Aggregate timesheets β Generate payroll records (newly implemented) | |
| **Rationale:** | |
| - Clean separation of concerns | |
| - No race conditions (timesheets finalized before payroll generation) | |
| - Easy to debug and maintain | |
| - Idempotent operations (safe to re-run) | |
| ## Implementation Components | |
| ### 1. Data Model (`src/app/models/user_payroll.py`) | |
| **Complete SQLAlchemy model matching schema.sql** | |
| **Key Fields:** | |
| - `user_id`, `project_id`: Links to worker and project | |
| - `period_start_date`, `period_end_date`: Weekly period (Monday-Sunday) | |
| - `tickets_closed`: Count from ticket_assignments | |
| - `hours_worked`, `days_worked`: Aggregated from timesheets | |
| - `flat_rate_amount`: Weekly base rate from project_role | |
| - `ticket_earnings`: Calculated earnings from tickets (base + commission) | |
| - `bonus_amount`, `deductions`: Manual adjustments | |
| - `total_amount`: Calculated total compensation | |
| - `is_paid`: Payment status flag | |
| - `version`: Optimistic locking for concurrent updates | |
| **Business Methods:** | |
| - `calculate_total()`: Computes total_amount from components | |
| - `mark_as_paid()`: Records payment details | |
| - `recalculate_from_data()`: Recalculates from fresh data (timesheet corrections) | |
| - `can_recalculate`: Property to check if recalculation is allowed | |
| **Constraints:** | |
| - Unique constraint: (user_id, project_id, period_start_date, period_end_date) | |
| - All amounts must be non-negative | |
| - Period end >= period start | |
| - Cannot recalculate paid payrolls | |
| ### 2. Pydantic Schemas (`src/app/schemas/payroll.py`) | |
| **Complete request/response models with validators** | |
| **Request Schemas:** | |
| - `PayrollGenerateRequest`: Generate for specific user/period (validates Monday-Sunday) | |
| - `PayrollBatchGenerateRequest`: Generate for all workers in a week | |
| - `PayrollUpdateRequest`: Update amounts before payment | |
| - `PayrollMarkPaidRequest`: Mark as paid with payment details | |
| **Response Schemas:** | |
| - `PayrollResponse`: Complete payroll with computed fields (can_recalculate, net_earnings, etc.) | |
| - `PayrollListResponse`: Paginated list | |
| - `PayrollGenerateResponse`: Generation result with success/skip reason | |
| - `PayrollBatchGenerateResponse`: Batch summary with errors | |
| **Validators:** | |
| - period_start_date must be Monday (weekday 0) | |
| - period_end_date must be Sunday (weekday 6) | |
| - Period must be exactly 7 days | |
| - payment_method must be valid (mobile_money, bank_transfer, cash, cheque) | |
| ### 3. Business Logic (`src/app/services/payroll_service.py`) | |
| **Comprehensive service layer with robust error handling** | |
| **Core Methods:** | |
| #### `generate_payroll_for_period()` | |
| - Generates payroll for specific user and period | |
| - Checks for existing payroll (idempotent) | |
| - Aggregates data from timesheets and ticket_assignments | |
| - Gets compensation rates from project_roles | |
| - Calculates earnings: `flat_rate + (base_amount * tickets * (1 + commission%))` | |
| - Creates payroll record with detailed calculation_notes | |
| - Returns success/skip reason | |
| #### `generate_weekly_payroll_batch()` | |
| - Generates payroll for all active project_team members | |
| - Processes each worker independently (continues on error) | |
| - Collects errors for review | |
| - Returns comprehensive summary | |
| - Safe for background job (no user context required) | |
| #### `recalculate_payroll()` | |
| - Recalculates payroll from fresh timesheet/ticket data | |
| - Only works on unpaid payrolls | |
| - Preserves manually added bonuses/deductions | |
| - Increments version (optimistic locking) | |
| - Updates calculation_notes with timestamp | |
| #### `mark_as_paid()` | |
| - Marks payroll as paid | |
| - Records payment method, reference, paid_by user | |
| - Prevents recalculation after payment | |
| - Permanent audit trail | |
| **Helper Methods:** | |
| - `get_week_bounds()`: Calculates Monday-Sunday for any date | |
| - `get_previous_week_bounds()`: Gets last completed week | |
| - `aggregate_timesheet_data()`: Sums hours_worked, counts days_present | |
| - `aggregate_ticket_data()`: Counts completed ticket assignments | |
| - `get_compensation_rates()`: Gets rates from project_team + project_role | |
| - `calculate_ticket_earnings()`: Formula: base * count + commission | |
| **Authorization:** | |
| - Platform admins: Full access | |
| - Project managers: Full access to their projects | |
| - Dispatchers: View only | |
| - Workers: View their own payroll only | |
| **Error Handling:** | |
| - Transaction safety (commit per operation, rollback on error) | |
| - Comprehensive logging | |
| - HTTPException with appropriate status codes | |
| - Graceful degradation (batch continues on individual errors) | |
| ### 4. REST API (`src/app/api/v1/payroll.py`) | |
| **Complete API endpoints with comprehensive documentation** | |
| **Endpoints:** | |
| #### `POST /payroll/generate` | |
| - Generate payroll for specific user/period | |
| - Validates Monday-Sunday period | |
| - Returns PayrollGenerateResponse with success/skip reason | |
| - Audit logging | |
| #### `POST /payroll/generate-batch` | |
| - Generate payroll for all workers in a week | |
| - Optional project_id filter | |
| - Returns batch summary with errors | |
| - Audit logging | |
| #### `GET /payroll/{payroll_id}` | |
| - Get payroll by ID | |
| - Authorization: admins/managers view all, workers view own | |
| - Returns complete payroll with joined data | |
| #### `GET /payroll` | |
| - List payrolls with filtering and pagination | |
| - Filters: user_id, project_id, is_paid, period dates | |
| - Sorting: period_start_date, total_amount, created_at, paid_at | |
| - Authorization: workers auto-filtered to own records | |
| #### `POST /payroll/{payroll_id}/recalculate` | |
| - Manually trigger recalculation | |
| - Only works on unpaid payrolls | |
| - Returns updated payroll | |
| - Audit logging | |
| #### `POST /payroll/{payroll_id}/mark-paid` | |
| - Mark payroll as paid | |
| - Requires payment_method and optional payment_reference | |
| - Returns updated payroll | |
| - Audit logging | |
| **All endpoints include:** | |
| - Comprehensive OpenAPI documentation | |
| - Authorization via `@require_permission("manage_payroll")` | |
| - Error handling with appropriate HTTP status codes | |
| - Audit logging via AuditService | |
| ### 5. Router Registration (`src/app/api/v1/router.py`) | |
| **Integrated into main API router** | |
| - Imported payroll module | |
| - Registered with prefix="/payroll" and tags=["Payroll"] | |
| - Positioned after timesheets router (logical flow) | |
| ### 6. Timesheet Integration (`src/app/services/timesheet_service.py`) | |
| **Auto-recalculation on timesheet correction** | |
| **Implementation in `update_timesheet()`:** | |
| 1. Update timesheet fields | |
| 2. Commit changes | |
| 3. Find payroll for this period (if exists and unpaid) | |
| 4. Trigger recalculation | |
| 5. Continue even if recalculation fails (logged warning) | |
| **Benefits:** | |
| - Payroll stays in sync with attendance data | |
| - No manual recalculation needed | |
| - Only affects unpaid payrolls (safe) | |
| - Non-blocking (timesheet update succeeds even if payroll recalc fails) | |
| ### 7. Background Job (`src/app/tasks/payroll_tasks.py`) | |
| **Simple cron-based approach (no Redis/Celery)** | |
| **Main Function: `generate_weekly_payroll_job()`** | |
| - Runs Monday morning (e.g., 6 AM) | |
| - Processes previous week (Monday-Sunday) | |
| - Uses PayrollService.generate_weekly_payroll_batch() | |
| - Comprehensive logging to file + console | |
| - Dry-run mode for testing | |
| - Returns summary dict | |
| **Schedule Options:** | |
| **Cron (Linux/Mac):** | |
| ```bash | |
| # Every Monday at 6 AM | |
| 0 6 * * 1 cd /path/to/project && python -m app.tasks.payroll_tasks | |
| ``` | |
| **Windows Task Scheduler:** | |
| - Trigger: Weekly, Monday, 6:00 AM | |
| - Action: `python -m app.tasks.payroll_tasks` | |
| - Working Directory: Project root | |
| **CLI Interface:** | |
| ```bash | |
| # Generate for previous week (default) | |
| python -m app.tasks.payroll_tasks | |
| # Dry run (test without committing) | |
| python -m app.tasks.payroll_tasks --dry-run | |
| # Specific date | |
| python -m app.tasks.payroll_tasks --date 2024-12-09 | |
| # Specific project | |
| python -m app.tasks.payroll_tasks --project-id <uuid> | |
| # Ad-hoc week (catch-up) | |
| python -m app.tasks.payroll_tasks --start 2024-12-02 --end 2024-12-08 | |
| ``` | |
| **Safety Features:** | |
| - Transaction-safe (commits per user, rollback on error) | |
| - Idempotent (safe to run multiple times) | |
| - Continues on error (doesn't fail entire batch) | |
| - Comprehensive logging to file | |
| - Dry-run mode for testing | |
| - Ad-hoc mode for catch-up | |
| ## Data Flow | |
| ### Weekly Payroll Generation (Automated) | |
| ``` | |
| Monday 6 AM: Background Job Runs | |
| β | |
| Calculate Previous Week (Mon-Sun) | |
| β | |
| For Each Active Project Team Member: | |
| β | |
| 1. Aggregate Timesheets | |
| - Sum hours_worked (PRESENT days) | |
| - Count days_worked (PRESENT count) | |
| β | |
| 2. Aggregate Ticket Assignments | |
| - Count completed assignments in period | |
| β | |
| 3. Get Compensation Rates | |
| - flat_rate_amount from project_role | |
| - base_amount, commission_percentage from project_role | |
| β | |
| 4. Calculate Earnings | |
| - flat_rate (weekly base) | |
| - ticket_earnings = (base * tickets) + commission | |
| - total_amount = flat_rate + ticket_earnings + bonus - deductions | |
| β | |
| 5. Create/Update Payroll Record | |
| - Skip if already paid | |
| - Update if unpaid (recalculate) | |
| - Create if doesn't exist | |
| β | |
| Summary: X generated, Y skipped, Z errors | |
| ``` | |
| ### Timesheet Correction Flow | |
| ``` | |
| Manager Corrects Timesheet | |
| β | |
| TimesheetService.update_timesheet() | |
| β | |
| Update timesheet fields | |
| β | |
| Commit changes | |
| β | |
| Find payroll for this period (if exists and unpaid) | |
| β | |
| If found: Trigger PayrollService.recalculate_payroll() | |
| β | |
| - Re-aggregate timesheets | |
| - Re-aggregate tickets | |
| - Recalculate earnings | |
| - Preserve bonuses/deductions | |
| - Increment version | |
| - Update calculation_notes | |
| β | |
| Payroll now reflects corrected timesheet | |
| ``` | |
| ## Calculation Formula | |
| ### Total Compensation | |
| ``` | |
| total_amount = flat_rate_amount + ticket_earnings + bonus_amount - deductions | |
| ``` | |
| ### Ticket Earnings | |
| ``` | |
| base_earnings = base_amount * tickets_closed | |
| commission = base_earnings * (commission_percentage / 100) | |
| ticket_earnings = base_earnings + commission | |
| ``` | |
| ### Example Calculation | |
| ``` | |
| Worker: John Doe | |
| Role: Technician | |
| Period: Dec 09-15, 2024 | |
| From project_role: | |
| - flat_rate_amount: KES 5,000 (weekly base) | |
| - base_amount: KES 500 per ticket | |
| - commission_percentage: 10% | |
| From timesheets (Dec 09-15): | |
| - days_worked: 5 (Monday-Friday present) | |
| - hours_worked: 40 hours | |
| From ticket_assignments (Dec 09-15): | |
| - tickets_closed: 12 tickets | |
| Calculation: | |
| flat_rate = 5,000 | |
| base_earnings = 500 * 12 = 6,000 | |
| commission = 6,000 * 0.10 = 600 | |
| ticket_earnings = 6,000 + 600 = 6,600 | |
| bonus_amount = 0 (none added) | |
| deductions = 0 (none added) | |
| total_amount = 5,000 + 6,600 + 0 - 0 = KES 11,600 | |
| ``` | |
| ## Key Features | |
| ### 1. Robustness | |
| - β Transaction safety (atomic operations) | |
| - β Optimistic locking (version column) | |
| - β Comprehensive error handling | |
| - β Idempotent operations (safe to re-run) | |
| - β Graceful degradation (continues on error) | |
| - β Audit logging (full trail) | |
| ### 2. Data Integrity | |
| - β Unique constraint (no duplicate payrolls) | |
| - β Validation (Monday-Sunday, non-negative amounts) | |
| - β Cannot modify paid payrolls | |
| - β Auto-recalculation on timesheet correction | |
| - β Preserves manual adjustments (bonuses/deductions) | |
| ### 3. Performance | |
| - β Batch processing (all workers at once) | |
| - β Efficient queries (joinedload, filters) | |
| - β Pagination support (API) | |
| - β Indexed columns (period dates, user_id, project_id) | |
| ### 4. Maintainability | |
| - β Clean separation of concerns (Model/Schema/Service/API) | |
| - β Comprehensive documentation (docstrings) | |
| - β Logging (debug, info, error levels) | |
| - β Simple approach (no external dependencies) | |
| ### 5. Flexibility | |
| - β Manual generation (ad-hoc periods) | |
| - β Project-specific filtering | |
| - β Dry-run mode (testing) | |
| - β CLI interface (operations) | |
| - β API endpoints (programmatic access) | |
| ## Testing Checklist | |
| ### Unit Tests | |
| - [ ] PayrollService.generate_payroll_for_period() | |
| - [ ] PayrollService.generate_weekly_payroll_batch() | |
| - [ ] PayrollService.recalculate_payroll() | |
| - [ ] PayrollService.aggregate_timesheet_data() | |
| - [ ] PayrollService.aggregate_ticket_data() | |
| - [ ] PayrollService.calculate_ticket_earnings() | |
| - [ ] UserPayroll.calculate_total() | |
| - [ ] UserPayroll.mark_as_paid() | |
| - [ ] UserPayroll.recalculate_from_data() | |
| ### Integration Tests | |
| - [ ] Generate payroll for worker with timesheets and tickets | |
| - [ ] Generate batch payroll for multiple workers | |
| - [ ] Recalculate payroll after timesheet correction | |
| - [ ] Mark payroll as paid | |
| - [ ] Prevent recalculation of paid payroll | |
| - [ ] Handle worker with no timesheets (0 days worked) | |
| - [ ] Handle worker with no tickets (flat rate only) | |
| - [ ] Handle worker with no role (0 compensation) | |
| - [ ] Idempotency (re-run generation) | |
| - [ ] Optimistic locking (concurrent updates) | |
| ### API Tests | |
| - [ ] POST /payroll/generate (success, skip, error) | |
| - [ ] POST /payroll/generate-batch (success, errors) | |
| - [ ] GET /payroll/{id} (authorization) | |
| - [ ] GET /payroll (filtering, pagination, sorting) | |
| - [ ] POST /payroll/{id}/recalculate (unpaid only) | |
| - [ ] POST /payroll/{id}/mark-paid (once only) | |
| ### Background Job Tests | |
| - [ ] Run with default (previous week) | |
| - [ ] Run with specific date | |
| - [ ] Run with project filter | |
| - [ ] Run in dry-run mode | |
| - [ ] Ad-hoc week generation | |
| - [ ] Error handling (DB connection, invalid data) | |
| ## Production Deployment | |
| ### 1. Database Migration | |
| ```sql | |
| -- Verify table exists | |
| SELECT * FROM user_payroll LIMIT 1; | |
| -- Check indexes | |
| \d user_payroll | |
| -- Verify constraints | |
| SELECT conname, contype FROM pg_constraint WHERE conrelid = 'user_payroll'::regclass; | |
| ``` | |
| ### 2. Schedule Background Job | |
| **Linux/Mac (Cron):** | |
| ```bash | |
| # Edit crontab | |
| crontab -e | |
| # Add line (Monday 6 AM) | |
| 0 6 * * 1 cd /var/www/swiftops-backend && /usr/bin/python3 -m app.tasks.payroll_tasks >> /var/log/payroll_jobs.log 2>&1 | |
| ``` | |
| **Windows (Task Scheduler):** | |
| 1. Open Task Scheduler | |
| 2. Create Task: "Weekly Payroll Generation" | |
| 3. Trigger: Weekly, Monday, 6:00 AM | |
| 4. Action: Start Program | |
| - Program: `python.exe` | |
| - Arguments: `-m app.tasks.payroll_tasks` | |
| - Working Directory: `D:\atomio\swiftops-backend` | |
| 5. Settings: Run whether user is logged on or not | |
| ### 3. Monitoring | |
| - Check logs: `payroll_job_YYYYMMDD_HHMMSS.log` | |
| - Monitor errors in batch summary | |
| - Alert on job failure (email/SMS) | |
| - Track metrics: generated count, skipped count, errors | |
| ### 4. First Run | |
| ```bash | |
| # Dry run first (test) | |
| python -m app.tasks.payroll_tasks --dry-run | |
| # Run for specific week (backfill) | |
| python -m app.tasks.payroll_tasks --start 2024-12-02 --end 2024-12-08 | |
| # Verify results | |
| # Check database for generated payrolls | |
| # Review logs for errors | |
| ``` | |
| ## Maintenance | |
| ### Weekly Operations | |
| 1. Monday morning: Job runs automatically | |
| 2. Review logs for errors | |
| 3. Check skipped workers (investigate why) | |
| 4. Fix any errors (missing roles, invalid data) | |
| 5. Re-run for specific workers if needed | |
| ### Manual Operations | |
| ```bash | |
| # Generate for specific worker | |
| curl -X POST http://localhost:8000/api/v1/payroll/generate \ | |
| -H "Authorization: Bearer $TOKEN" \ | |
| -H "Content-Type: application/json" \ | |
| -d '{ | |
| "user_id": "uuid", | |
| "project_id": "uuid", | |
| "period_start_date": "2024-12-09", | |
| "period_end_date": "2024-12-15" | |
| }' | |
| # List unpaid payrolls | |
| curl http://localhost:8000/api/v1/payroll?is_paid=false | |
| # Mark as paid | |
| curl -X POST http://localhost:8000/api/v1/payroll/{id}/mark-paid \ | |
| -H "Authorization: Bearer $TOKEN" \ | |
| -H "Content-Type: application/json" \ | |
| -d '{ | |
| "payment_method": "mobile_money", | |
| "payment_reference": "ABC123XYZ" | |
| }' | |
| ``` | |
| ## Future Enhancements | |
| ### Phase 2 (Optional) | |
| - [ ] Email notifications to workers (payroll generated) | |
| - [ ] PDF payslip generation | |
| - [ ] Payment gateway integration (M-Pesa B2C) | |
| - [ ] Payroll approval workflow | |
| - [ ] Payroll adjustments (corrections after payment) | |
| - [ ] Payroll reports (project, period, worker) | |
| - [ ] Payroll analytics (cost trends, efficiency) | |
| ### Phase 3 (Advanced) | |
| - [ ] Multi-currency support | |
| - [ ] Tax calculations (PAYE, NHIF, NSSF) | |
| - [ ] Payroll export (accounting systems) | |
| - [ ] Payroll forecasting | |
| - [ ] Bonus calculation rules engine | |
| - [ ] Overtime tracking and compensation | |
| ## Summary | |
| **Implementation Status:** β COMPLETE | |
| **Files Created/Modified:** | |
| 1. β `src/app/models/user_payroll.py` (NEW) | |
| 2. β `src/app/schemas/payroll.py` (UPDATED) | |
| 3. β `src/app/services/payroll_service.py` (UPDATED) | |
| 4. β `src/app/api/v1/payroll.py` (UPDATED) | |
| 5. β `src/app/api/v1/router.py` (UPDATED) | |
| 6. β `src/app/services/timesheet_service.py` (UPDATED) | |
| 7. β `src/app/tasks/payroll_tasks.py` (UPDATED) | |
| 8. β `src/app/models/project.py` (UPDATED - added payrolls relationship) | |
| **Key Achievements:** | |
| - β Robust weekly payroll generation from timesheets | |
| - β Automatic recalculation on timesheet correction | |
| - β Simple background job (no Redis/Celery) | |
| - β Comprehensive error handling and logging | |
| - β Idempotent operations (production-safe) | |
| - β Complete REST API with documentation | |
| - β Authorization and audit logging | |
| - β Optimistic locking for concurrency | |
| - β CLI interface for operations | |
| **Production-Ready Features:** | |
| - Transaction safety | |
| - Data integrity constraints | |
| - Comprehensive logging | |
| - Error recovery | |
| - Dry-run mode | |
| - Manual override capabilities | |
| - Complete audit trail | |
| The system is now ready for deployment and testing! π | |