Corp/backend/payroll_core
lizerk 158b71ac04
All checks were successful
Deploy production / deploy (push) Successful in 10m13s
feat: make payroll columns configurable
2026-09-11 02:14:27 +03:00
..
2026-09-07 19:43:48 +03:00

Payroll Core

payroll_core is a deterministic, project-independent calculation package. It contains no Django, database, network, Celery or EUSRR imports. A host application resolves employees and approved inputs, calls the calculator, and persists the returned immutable snapshot.

Guarantees

  • every monetary and quantity value is Decimal; floats are rejected;
  • each displayed money line is rounded once before totals are summed;
  • calculation uses a fixed local decimal context and does not depend on the host process context;
  • rules are immutable, effective-dated and versioned;
  • inputs, rules and calculation output have canonical SHA-256 fingerprints;
  • reconciliation warnings have a separate fingerprint, so legacy checkpoints never alter calculation identity but cannot be changed unnoticed;
  • explicit line kinds carry direction, so a negative correction cannot become a double negative;
  • legacy spreadsheet totals are reconciliation checkpoints, never inputs;
  • the result is independent of the order of input lines.

Example matching the June 2026 spreadsheet

from datetime import date
from decimal import Decimal

from payroll_core import (
    DeterministicPayrollCalculator,
    ExpectedTotals,
    InputLine,
    LineKind,
    PayrollPeriod,
    PayrollRequest,
    PayrollRules,
    PointPolicy,
)

period = PayrollPeriod(date(2026, 6, 1), date(2026, 6, 30))
request = PayrollRequest(
    employee_ref="employee:42",
    period=period,
    currency="RUB",
    base_accrual=Decimal("80000"),
    base_source_ref="rate:42:revision:1",
    point_base_accrual=Decimal("95000"),
    target_points=Decimal("110"),
    actual_points=Decimal("110"),
    point_rate=Decimal("100"),
    lines=(
        InputLine(
            line_id="bonus:1",
            code="BONUS",
            label="Bonus",
            kind=LineKind.EARNING,
            amount=Decimal("15000"),
            source_ref="excel:june:row-2:bonus",
        ),
        InputLine(
            line_id="correction:1",
            code="MANUAL_CORRECTION",
            label="Manual correction",
            kind=LineKind.ADJUSTMENT_CREDIT,
            amount=Decimal("20000"),
            reason="Approved accounting correction",
            source_ref="excel:june:row-2:correction",
        ),
    ),
    expected_totals=ExpectedTotals(
        point_amount=Decimal("0"),
        gross=Decimal("115000"),
        recalculated_gross=Decimal("115000"),
        payable=Decimal("115000"),
    ),
)
rules = PayrollRules(
    ruleset_id="company-standard",
    version="2026.07.1",
    effective_from=date(2026, 1, 1),
    point_policy=PointPolicy.EXCESS_ONLY,
)

result = DeterministicPayrollCalculator().calculate(request, rules)
assert result.totals.payable == Decimal("115000.00")

The initial EXCESS_ONLY point policy pays only points above the target: max(actual - target, 0) * point_rate. It never creates an implicit penalty for a shortfall. This policy must remain in shadow/reconciliation mode until the company confirms how points work for other rows.

PROPORTIONAL_WITH_EXCESS produces a signed point adjustment. Below target it uses the complete point basis supplied by the host: point_base_accrual * actual / target - point_base_accrual. Above target it uses (actual - target) * point_rate. Keeping the basis separate from the base accrual preserves the original input lines while making the adjustment visible.

Host boundary

The core knows an employee only as an opaque employee_ref. The host adapter is responsible for authorization, rate resolution, maker-checker approval, transactions, audit, publication, acknowledgements and notifications. EUSRR maps the spreadsheet point basis to base accrual plus bonus inputs. It keeps the configured excess point price nullable: when blank, the host supplies the current in-norm display price and marks it as automatic. The core then uses the unrounded point_base_accrual / target_points formula, without persisting the derived value.