Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save jeje-andal/528c8384bcc4b1467bd04647c25dc94c to your computer and use it in GitHub Desktop.

Select an option

Save jeje-andal/528c8384bcc4b1467bd04647c25dc94c to your computer and use it in GitHub Desktop.
PR Review: AK.Server Payroll Formula Fixes (69)

πŸ“‹ PR Review Metadata

Field Value
File C:\Documents\Notes\AI Answer\PR-Review-Gitea-AKServer-Payroll-69-20260130-000000.md
PR https://git.andalsoftware.com/Andal.Kharisma/AK.Server/pulls/69
Decision REQUEST CHANGES
Files 4 changed
Risk MEDIUM-HIGH

Use this path for next step: C:\Documents\Notes\AI Answer\PR-Review-Gitea-AKServer-Payroll-69-20260130-000000.md


PR #69 Review Report: Payroll Formula Calculation Refactoring

Platform: Gitea (git.andalsoftware.com) Repository: Andal.Kharisma/AK.Server Stack: Backend (C# .NET 8) Module: Payroll Review Date: 2026-01-30


Executive Summary

This PR refactors the payroll formula calculation system to handle edge cases more gracefully by returning 0 instead of throwing exceptions for employees without complete job configurations. While the intent is valid, the implementation introduces potential null reference exceptions, inconsistent error handling, and a significant breaking behavioral change that could mask configuration errors.

Overall Assessment: The PR contains important bug fixes (comparison logic, logging) and performance improvements (compiled regex), but requires additional hardening before merge.

Key Concerns:

  1. Potential NullReferenceException in EmpSalaryTriggerByAttService (new code path)
  2. Incomplete null handling in CUSTOM_FIELD case
  3. Silent failure pattern may mask real configuration issues
  4. Missing null-check guard for GetSecondaryPSDetailByDate return value

File-by-File Analysis

1. src/Persistence/Helper/RulesFormulaHelper.cs

Changes:

  • ATT() method now returns 0 instead of throwing for missing configurations
  • _getVariableTable() returns null instead of throwing exceptions
  • ProcessFormulaString() refactored to use compiled regex patterns
  • Added NormalizeFormulaTableCalls() helper method

Critical Findings:

πŸ”΄ Issue 1: Inconsistent Error Handling in _getVariableTable()

The method now returns null for most cases but still throws exceptions for some:

// These cases now return null (good):
case FormulaVariableTable.JOB_POSITION:
case FormulaVariableTable.JOB_TITLE:
case FormulaVariableTable.JOB_GRADE:
case FormulaVariableTable.ORGANIZATION:
case FormulaVariableTable.DEFAULT:
    return null;

// These cases STILL throw exceptions (inconsistent):
case FormulaVariableTable.WORKING_SHIFT:
    if (roster == null) throw new Exception();
case FormulaVariableTable.TAX_STATUS:
    if (taxStatusEmployee == null) throw new Exception();

Recommendation: Standardize error handling. Either all cases should return null, or document why certain cases require exceptions.

πŸ”΄ Issue 2: Missing Null Check in CUSTOM_FIELD Case

case FormulaVariableTable.CUSTOM_FIELD:
{
    var customFieldEmployee = employee.CustomFieldEmployees?
        .FirstOrDefault(cfe => cfe.CustomFieldId == customFieldId && ...);
    // Missing: if (customFieldEmployee == null) return null;
    if (customFieldEmployee.CustomField.Type == ...) // NRE risk

Recommendation: Add null check before accessing CustomField property.

🟑 Issue 3: Silent Failure Pattern

The ATT() method now silently returns 0 for:

  • Employees without active job positions/titles/grades
  • Variable IDs not configured in tables

While this handles edge cases gracefully, it removes visibility into configuration problems. Consider adding warning logs:

if (id == null)
{
    _logger.LogWarning("Employee {EmployeeId} has no active {VariableTable} for formula table {TableName}",
        _employee?.Id, formulaTableText.VariableTable, formulaTableName);
    return 0;
}

🟒 Positive: Compiled Regex Performance

The new regex approach uses RegexOptions.Compiled which improves performance for repeated evaluations:

private static readonly Regex _atnPattern = new Regex(
    @"ATN\s*\(\s*(?<tableName>[^,]+)\s*,\s*(?<determinant>[^)]+)\)",
    RegexOptions.IgnoreCase | RegexOptions.Compiled);

🟑 Concern: Regex Pattern Robustness

The pattern [^)]+ for determinant matching is greedy and could match unbalanced parentheses:

// This could incorrectly match:
ATN(Table, IF(condition, value1, value2)) + 100
// Pattern might capture: "Table, IF(condition, value1" as tableName

Recommendation: Add test cases for nested parentheses in formulas.


2. src/Payroll/GraphQL/Services/Implementations/RulesFormulaService.cs

Changes:

  • Fixed comparison logic to use distinct values
  • Fixed logging to use string.Join instead of .ToString()

🟒 Correct Fix: Distinct Value Comparison

The fix properly handles multiple method calls:

// Before (buggy):
if (context.ThrownInactives.SequenceEqual(methods))  // Could fail with duplicates

// After (correct):
var distinctThrownInvalids = context.ThrownInactives.Distinct().OrderBy(x => x).ToList();
var distinctMethods = methods.Distinct().OrderBy(x => x).ToList();
if (distinctThrownInvalids.SequenceEqual(distinctMethods))

This correctly identifies when ALL unique method types have thrown exceptions.

🟒 Correct Fix: Logging

// Before:
_logger.LogError("these formulas threw an error: " + context.ThrownInactives.ToString());
// Result: "these formulas threw an error: System.Collections.Generic.List`1[System.String]"

// After:
_logger.LogError("these formulas threw an error: " + string.Join(", ", context.ThrownInactives));
// Result: "these formulas threw an error: ATT, ATN"

3. src/Payroll/GraphQL/Services/Implementations/EmpSalaryTriggerByAttService.cs

Changes:

  • Changed from GetSecondaryPSDetailByPeriod to GetSecondaryPSDetailByDate
  • Simplified date range logic

πŸ”΄ Critical: Potential NullReferenceException

// New code:
var secondaryPaymentScheduleDetail = _generalPaymentScheduleService.GetSecondaryPSDetailByDate(paymentSchedule, roster[0].ActualDate);
var rostersForFormulaAllEmployees = await _attendanceRepository.GetRosterByEmployeeIdAsync(_context, employeeIds,
    secondaryPaymentScheduleDetail.FromDate,  // NRE if null
    secondaryPaymentScheduleDetail.ToDate);

GetSecondaryPSDetailByDate can return null:

// From GeneralPaymentScheduleService:
public AttPaymentSchedulesDetail? GetSecondaryPSDetailByDate(...)
{
    return paymentSchedule.AttendancePaymentSchedule.AttPaymentSchedulesDetails
        .FirstOrDefault(x => x.FromDate <= processDate && x.ToDate >= ...);
    // Returns null if no matching period
}

Recommendation: Add null check:

var secondaryPaymentScheduleDetail = _generalPaymentScheduleService.GetSecondaryPSDetailByDate(paymentSchedule, roster[0].ActualDate);
if (secondaryPaymentScheduleDetail == null)
{
    _logger.LogError("No secondary payment schedule found for date {Date}", roster[0].ActualDate);
    return false;
}

🟑 Concern: Date Range Logic Change

The new logic uses different date ranges for rosters vs employee salaries:

  • Rosters: secondaryPaymentScheduleDetail dates
  • EmployeeSalaries: primaryPaymentScheduleDetail dates

Question: Is this decoupling intentional? Please document the rationale.


4. src/Payroll.UnitTest/UnitTest/RulesFormulaTest.cs

Changes:

  • Test expectation changed from exception to returning 0
  • Fixed Assert.Equal parameter order

🟑 Issue: Misleading Test Name

[Theory]
[MemberData(nameof(Data.TestATTExceptionAssertion), MemberType = typeof(Data))]
public void TestDeterminantTextExceptionAssertion(string formula, ...)  // Name no longer accurate
{
    // Now expects result = 0, not an exception
    var result = _rulesFormulaService.Calculate(...);
    Assert.Equal(0, result);
}

Recommendation: Rename test to reflect actual behavior:

public void TestATT_ReturnsZero_WhenEmployeeConfigurationMissing(...)

🟒 Correct Fix: Assert.Equal Parameter Order

// Before (wrong order - actual, expected):
Assert.Equal(result, expected);

// After (correct order - expected, actual):
Assert.Equal(expected, result);

Breaking Change Analysis

Behavioral Change: Exception to Return Value

Scenario Before After Impact
Employee no active job position Throws AllFormulasInvalidException Returns 0 Breaking
Employee no active job title Throws AllFormulasInvalidException Returns 0 Breaking
Employee no active job grade Throws AllFormulasInvalidException Returns 0 Breaking
Variable ID not in table Throws exception Returns 0 Breaking

Impact Assessment:

  • Any code catching AllFormulasInvalidException for ATT scenarios will no longer execute
  • Configuration errors will now result in silent 0 calculations
  • Payroll reports may show 0 amounts without indicating configuration issues

Mitigation Recommendation: Consider adding a warning log or optional strict mode:

public decimal ATT(string formulaTableName)
{
    // ... existing code ...
    if (id == null)
    {
        #if STRICT_FORMULA_VALIDATION
        throw new AllFormulasInvalidException($"No active configuration for {formulaTableText.VariableTable}");
        #else
        _logger.LogWarning("ATT returning 0 due to missing configuration for employee {EmployeeId}", _employee?.Id);
        return 0;
        #endif
    }
}

Database & Performance Analysis

N+1 Query Risk

The _getVariableTable method accesses navigation properties:

employee.JobPositionEmployees?.Where(...).Select(jpe => jpe)

Risk: If this is called in a loop for multiple employees without proper Include(), it could cause N+1 queries.

Recommendation: Verify that calling code uses proper eager loading:

_context.Employees
    .Include(e => e.JobPositionEmployees)
    .ThenInclude(jpe => jpe.JobPosition)
    .ThenInclude(jp => jp.JobTitle)
    // etc.

Regex Performance

Compiled regex patterns are beneficial for high-volume scenarios:

  • Before: Regex recreated on each call
  • After: Regex compiled once at class load

Benefit: ~10-30% performance improvement for formula parsing under load.


Test Coverage Assessment

Coverage Gaps Identified

Scenario Covered Priority
Employee with no active job position Yes (renamed test needed) High
Employee with no active job title No High
Employee with no active job grade No High
Variable ID not in formula table Yes High
Null secondary payment schedule No Critical
CUSTOM_FIELD null reference No Medium
Malformed formula with regex No Medium
Multiple ATT/ATN calls in formula Yes (via distinct fix) Medium

Recommended Additional Tests

[Theory]
[InlineData("ATT(Table, JOB_POSITION)", null)] // No job position
[InlineData("ATT(Table, JOB_TITLE)", null)]   // No job title
[InlineData("ATT(Table, JOB_GRADE)", null)]   // No job grade
public void ATT_ReturnsZero_ForMissingEmployeeConfiguration(...)

[Fact]
public void GetSecondaryPSDetailByDate_NullHandling()
{
    // Test null return from GetSecondaryPSDetailByDate
}

[Fact]
public void ProcessFormulaString_NestedParentheses()
{
    // Test formula: ATN(Table, IF(condition, val1, val2))
}

Security Considerations

No direct security concerns identified. However:

  • Formula evaluation uses CodingSeb.ExpressionEvaluator which should be monitored for security updates
  • User-input formulas should still be validated/sanitized before evaluation
  • The regex patterns don't appear vulnerable to ReDoS (no nested quantifiers with backtracking)

Recommendations Summary

Must Fix (Blocking Merge)

  1. Add null check in EmpSalaryTriggerByAttService:

    if (secondaryPaymentScheduleDetail == null) return false;
  2. Add null check in CUSTOM_FIELD case:

    if (customFieldEmployee == null) return null;
  3. Rename misleading test: TestDeterminantTextExceptionAssertion -> TestATT_ReturnsZero_ForMissingConfiguration

Should Fix (High Priority)

  1. Add warning logs for silent failures: Log when returning 0 due to missing configuration

  2. Standardize error handling: Either all cases in _getVariableTable return null, or document exception cases

  3. Add test coverage:

    • Null secondary payment schedule
    • CUSTOM_FIELD null scenario
    • Nested parentheses in formulas

Consider (Medium Priority)

  1. Document behavioral change: Update API documentation to reflect new return-0 behavior

  2. Consider feature flag: Allow gradual migration from exception-based to return-0 behavior


Quality Score

Category Score Notes
Code Quality 7/10 Good refactoring, but missing null checks
Test Coverage 6/10 Tests updated but gaps remain
Performance 8/10 Compiled regex is improvement
Maintainability 7/10 Cleaner code but inconsistent error handling
Breaking Changes 4/10 Significant behavioral change needs documentation

Overall Score: 6.4/10


Final Decision

REQUEST CHANGES

This PR contains valuable improvements (bug fixes, performance) but requires the following before merge:

  1. Add null check for secondaryPaymentScheduleDetail in EmpSalaryTriggerByAttService
  2. Add null check for customFieldEmployee in _getVariableTable CUSTOM_FIELD case
  3. Rename test method to reflect actual behavior
  4. Add warning logs when returning 0 for missing configurations

Once these items are addressed, the PR can be approved for merge.


Review complete. File path shown in metadata block above.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment