Skip to content

Instantly share code, notes, and snippets.

@noaelad
Created February 10, 2026 00:43
Show Gist options
  • Select an option

  • Save noaelad/1e5105f880a928dba2f1f9a560957ca9 to your computer and use it in GitHub Desktop.

Select an option

Save noaelad/1e5105f880a928dba2f1f9a560957ca9 to your computer and use it in GitHub Desktop.
TAXPLAT-565: Tax Attribute Migration Research

TAXPLAT-565: Tax Attribute Migration Research

Tax#allow_negative_amount

Current Behavior

  • Not a DB column - delegated from PayrollReferenceData::Tax (JSON reference data)
  • Type: T::Boolean, most taxes have false

Taxes with allow_negative_amount: true

Federal (5 taxes) - All Employer Social Security Credits (COVID-related):

  • 00-000-0000-ER_CRED-001 - Emergency Leave, Caring for Self
  • 00-000-0000-ER_CRED-002 - Emergency Leave, Caring for Others
  • 00-000-0000-ER_CRED-003 - Family Medical Leave
  • 00-000-0000-ER_CRED-004 - Employee Retention
  • 00-000-0000-ER_CRED-005 - COVID-19 Relief

State (5 taxes across 4 states):

  • IL: 17-000-0000-ER_CRED-000 (Employer Credit)
  • GA: 13-000-0000-ER_CRED-000 (Employer Credit)
  • NM: 35-000-0000-WC-000 (Workers' Compensation) - employee-level, see below
  • MO: 29-000-0000-SITCD-000-CD05 (Compensation Deduction) - company-level, see below
  • MO: 29-000-0000-SITCD-000-CD2 (Compensation Deduction) - company-level, see below

Pattern: All are credits/deductions that legitimately go negative. Different record types handle them differently.

MO SITCD - Company-Level Tax

MO SITCD is NOT a PayrollItemTax - it's a CompanyTax:

  • Created in CompanyTaxes::CompanyTaxHandling::CompensationDeductionTax
  • Employers retain 0.5-2% of collected SIT as compensation for withholding
  • Always negative amounts (reduces what employer owes to state)
  • employee_ids: [] - not tied to specific employees

Why this matters for YTD validation:

  • PayrollItemTax#ytd_values_are_positive validation only runs on PayrollItemTax records
  • CompanyTax is a different model with no YTD validation
  • SITCD bypasses the YTD check entirely by being company-level

Two tax record types:

Model Level Examples Has YTD validation?
PayrollItemTax Employee SIT, FICA, NM WC Yes
CompanyTax Company MO SITCD, MCTMT No

NM Workers' Compensation - Explained

  • 35-000-0000-WC-000 has allow_negative_amount: true
  • OR WC (41-000-0000-WC-000) has allow_negative_amount: false
  • Both are employee-side WC taxes (employer_tax: false)
  • Git history: Original commit (02c0fdfb9e733, Feb 2017) shows NM WC was true and OR WC was null from initial import.

Why NM WC needs negative amounts: Found hardcoded exception in PayrollItemTax YTD/QTD validation (lines 169, 203) with comment:

"NM workers comp is often withheld with a check date AFTER the relevant EOQ. So if you have a liability on July 10, and we want to remove it (because, say, you actually quit on June 20), then we want to make a June 30 negative liability (which is illegal according to this rule)"

Three levels of negative amount handling:

  1. allow_negative_amount: true → allows negative current-period amounts
  2. credit_tax? (ER_CRED) → also exempt from YTD/QTD positive checks
  3. Hardcoded NM WC exception → also exempt from YTD/QTD checks (timing edge case)

Conclusion: NM WC's allow_negative_amount: true is intentional - needed for backdated corrections when check dates cross quarter boundaries. Not a compliance question.

Usage in Codebase

allow_negative_amount flag usages:

  1. PayrollTax#allow_negative_amounts? - Combined with payroll type checks (correction/recon/reversal)
  2. PayrollItemTax#allow_negative_amounts? - Same pattern
  3. PayrollTax#can_negative_tax_be_marked_as_prepaid? - Direct check on tax
  4. UpdatePrepaidPayrollTaxes - Clamps negative amounts to 0 unless flag is true

credit_tax? method usages (separate from allow_negative_amount):

  1. PayrollItemTax YTD/QTD validation - Credit taxes exempt from positive check
  2. PayrollItem#allow_negative_employer_tax? - Additional escape hatch
  3. FEIN Change - Credit taxes excluded from paystubs
  4. Bookkeeping - Credit taxes excluded from employer tax ledger entries

Key insight: credit_tax? is hardcoded to tax_type_code == 'ER_CRED'. It provides stricter handling than just allow_negative_amount.

Terminology Assessment

  • Name is clear and descriptive
  • Accurately reflects behavior (allowing negative amounts on tax records)

Recommendation

Can this be derived instead of stored? Partially.

9/10 taxes with allow_negative_amount: true follow the pattern: tax_type_code in ['ER_CRED', 'SITCD']. The exception is NM WC, which needs negative amounts for timing edge cases (not because it's a credit).

Option A: STE type check + hardcode

def allows_negative_amount?
  tax_type_code.in?(['ER_CRED', 'SITCD']) || tax_id == Tax::NM_WC_TAX_ID
end
  • Pros: Explicit, auditable, matches existing credit_tax? pattern
  • Cons: Requires code change for new credit types

Option B: Keep as explicit attribute

  • Pros: Flexible, no hardcodes needed
  • Cons: Another attribute to maintain in reference data

Recommendation: Option A (derive from type + hardcode NM WC). The attribute is only used in 3 places, NM WC is already hardcoded elsewhere, and deriving simplifies reference data.

Tax#tax_group

Current Behavior

  • Not a DB column - delegated from PayrollReferenceData::Tax (JSON reference data)
  • Source: zp_tax_group in state JSON files, tax_group in federal JSON file
  • Type: String - contains a unique_tax_id of another tax (or itself)

What tax_group Represents

Groups related taxes together for various purposes. A tax's tax_group points to the "primary" tax in its family.

Distribution Overview

Total taxes: 7,087

Category Count %
Self-referential (tax_group == unique_tax_id) 6,097 86%
Pointing elsewhere 990 14%

Federal: 58 of 63 (92%) point elsewhere State: 932 of 7,024 (13%) point elsewhere

Pattern Breakdown (990 grouped taxes)

Pattern Count % Why grouped
Ohio Vol (voluntary) 669 67.6% Single exemption controls both mandatory and voluntary
NonRes/Res 242 24.4% Same tax, different residency status
State FUTA credits 51 5.2% Report under unified FUTA
ER_CRED (credits) 7 0.7% Offset FICA liabilities
ER/EE pairs 2 0.2% Sync exemptions, sum amounts
Other variants 19 1.9% Zone, district, ordinal suffixes

Pattern Examples

1. Ohio Vol (voluntary) - 669 taxes

39-000-1037396-CITY-000-Vol → 39-000-1037396-CITY-000

Ohio cities with optional enrollment. The -Vol variant groups under the base city tax.

2. NonRes/Res - 242 taxes

18-001-0000-CNTY-000-NonRes → 18-001-0000-CNTY-000

Indiana county tax for non-residents groups under the base county tax. Indiana is systematic: 184 of 186 taxes (98.9%) follow this pattern.

3. State FUTA credits - 51 taxes

00-000-0000-ER_FUTA-000-AL → 00-000-0000-ER_FUTA-000

Each state's FUTA credit reduction groups under the base federal FUTA. All 50 states + DC follow this pattern.

4. ER_CRED (credits) - 7 taxes

00-000-0000-ER_CRED-001 → 00-000-0000-FICA-000

Federal COVID emergency leave credit (caring for self) groups under FICA because it offsets FICA liability. All 5 federal + 2 state credits follow this pattern.

5. ER/EE pairs - 2 taxes

00-000-0000-ER_FICA-000 → 00-000-0000-FICA-000

Employer Social Security groups under Employee Social Security. Same for Medicare. Enables synced exemptions and combined reporting.

6. Other variants - 19 taxes

21-111-0000-OLTS-000-Ord → 21-111-0000-OLTS-000

Kentucky occupational license tax with ordinal suffix. Also includes zone/district variants in NY, OH.

State Distribution

State Total Taxes Grouped % Primary Pattern
Ohio 1,683 671 39.9% Vol (voluntary)
Indiana 186 184 98.9% NonRes/Res
Michigan 51 48 94.1% NonRes/Res
Pennsylvania 4,592 3 0.07% Minimal grouping
Kentucky 271 6 2.2% Mixed

Notable: Pennsylvania has the most granular tax database (65% of all state taxes) but uses grouping the least.

Key Observations

  1. No multi-level indirection - All tax_group references are 1-level deep
  2. ER/EE pairs are rare - Only 2 taxes (FICA, MEDI) despite code being designed for this pattern

Usage in Codebase

Primary uses (21 files reference tax_group):

# Usage What it does Why tax_group matters
1 Tax Exemptions UI shows one tax per group; exempting one auto-exempts others Keeps grouped taxes exempt as one unit
2 Historical Tax Liability Stores liabilities at tax_group level; payment splitting matches payroll_item_taxes by tax_group Matches payments to tax family
3 Cash Requirements Report Groups taxes via total_tax_amount_for_ee/er(tax_group) One line per tax family

1. Tax Exemptions

The exemption UI deduplicates taxes by tax_group so users see one entry per tax family (e.g., "Social Security" rather than separate EE/ER entries). When a user exempts one tax, the TaxChangesGroupable concern automatically creates matching exemptions for all other taxes in the same group via an after_save callback.

Key code:

  • UI deduplication: packs/.../tax_exemption/app/public/tax_exemption/graphql_resolvers/read/get_applicable_taxes_for_employee.rb:55-60 - groups by tax_group, picks first exemptable tax
  • Auto-sync callback: packs/.../tax_exemption/app/models/concerns/tax_changes_groupable.rb:15-38 - mirrors exemptions to related taxes
  • Find related taxes: components/deprecated_legacy_reference_data/lib/payroll_reference_data/tax_exemptions.rb:10-14 - PayrollReferenceData::Tax.from_tax_group(tax.tax_group)

2. Historical Tax Liability

The historical tax liability system uses tax_group throughout its entire lifecycle to consolidate related taxes (like Social Security EE and ER) into a single liability unit. The HistoricalTaxLiability table stores one record per tax_group per company, with the tax_id pointing to the "default" tax (typically the employee variant). All amount calculations and payment matching then filter by tax_group to capture all related taxes.

Liability creation: When converting historical payrolls, the system extracts unique tax_group values, then creates ONE HistoricalTaxLiability per group using find_default_tax(tax_group) to pick the canonical tax.

Amount calculations: Methods like total_ytd_liability, paid_liability, and unpaid_liability all filter payroll taxes by matching tax_group, ensuring both EE and ER portions contribute to the same liability.

Payment matching: When building debits, the system finds undebited payroll taxes by tax_group, groups them by check date, and creates debit entries that map payments to specific historical payrolls.

Key code:

  • Liability creation: packs/.../historical_payrolls/app/services/historical_companies/convert_historical_payrolls.rb:67-89 - extracts tax_groups, creates one liability per group
  • Default tax resolution: packs/.../historical_payrolls/app/services/historical_payrolls/historical_tax_liability_finder.rb:37-39 - find_default_tax(tax_group)
  • Amount calculations: packs/.../historical_payrolls/app/models/historical_tax_liability.rb:137,163,177 - filters by tax_group in total_ytd_liability, paid_liability, unpaid_liability
  • Finding undebited taxes: packs/.../historical_payrolls/app/services/payments/debits/historical/undebited_payroll_taxes_finder.rb:47 - payroll_tax.tax.tax_group == tax.tax_group
  • Debit building: packs/.../historical_payrolls/app/services/payments/debits/historical/historical_tax_liability_debit_builder.rb:45-47 - groups by tax_group

3. Cash Requirements Report

The Cash Requirements Report outputs one line per tax family by iterating over unique tax_group values and summing employee/employer amounts separately. This is the only caller of total_tax_amount_for_ee/er(tax_group) methods.

Key code:

  • CSV report: packs/.../payroll_reports/app/services/customer_reports/reports/cash_requirements.rb:180-187 - iterates payroll_item_taxes.map(&:tax_group).uniq
  • PDF report: packs/.../reporting_toolkit/reports/app/views/customer_reports/pdf/cash_requirements.arb:188-190
  • Calculator methods: packs/.../running_payroll/app/services/calculators/payroll_calculator.rb:178-191 - total_tax_amount_for_ee/er

Design Intent vs Actual Usage

The code was designed for the EE/ER pattern (e.g., Social Security EE + ER in same group), but only 2 of 990 grouped taxes actually follow this pattern:

Pattern Count % Both apply to same employee?
Ohio Vol (voluntary) 669 67.6% No - mutually exclusive
NonRes/Res 242 24.4% No - mutually exclusive
State FUTA variants 51 5.2% No - one per state
ER_CRED credits 7 0.7% No - only when credit applies
EE/ER pairs 2 0.2% Yes - both on every payroll
Other 19 1.9% Varies

Implication: The code handles the dominant patterns (Vol, NonRes/Res) correctly despite being designed for the rare pattern (EE/ER).

Res/NonRes Pattern Deep Dive

Examined whether the Res/NonRes pattern causes issues with code designed for EE/ER:

Example: Walker City Tax

  • 26-000-1615688-CITY-000-Res (exemptable: true, employer_tax: false)
  • 26-000-1615688-CITY-000-NonRes (exemptable: true, employer_tax: false)
  • Both point to tax_group 26-000-1615688-CITY-000

Key finding: The code is robust because all lookups use tax_group matching:

# Historical liability lookup (undebited_payroll_taxes_finder.rb:47)
payroll_taxes.select { |pt| pt.tax.tax_group == tax.tax_group }

This finds ALL taxes in the group, not just the one stored on the liability.

Operation How it works Res/NonRes safe?
Store liability Against one "default" tax N/A
Calculate amounts WHERE tax.tax_group == X ✅ Finds both
Find undebited taxes WHERE tax.tax_group == X ✅ Finds both
Payment splitting WHERE tax.tax_group == X ✅ Finds both

Why it works: An employee can only be Res OR NonRes, never both. So grouping them doesn't cause double-counting.

Minor issues (cosmetic):

  • find_default_tax picks arbitrarily when both are employer_tax: false
  • Comment says "we choose the EE tax" but for city taxes, both are EE-side
  • UI might show "NonRes" variant when data is actually for "Res"

Conclusion: tax_group abstraction designed for EE/ER accidentally works for Res/NonRes because the lookup pattern is general enough.

Recommendation - WIP

Seems like a lot of the current grouping was not done by-hand by a human, but rather ported over from a previous implementation (https://github.com/Gusto/zenpayroll/pull/42334). The usages are a little sus (particularly Historical Tax Liability), I'm not convinced they're doing the right thing for all the patterns that exist. I'm not sure if there's something here that's worth preserving the ability to manage by hand, or if we should work towards deriving this attribute. Will circle back.

Tax#exemptable

Current Behavior

The "exemptable" attribute controlls whether a tax exemption can be created for a specific tax. Only 69 taxes (out of ~7K, so 1%) are non-exemptable, everything else is exemptable.

Distribution

Category Exemptable Non-Exemptable % Exemptable
Federal 5 58 8%
State 7,013 11 99.8%
Total ~7,018 ~69 99%

Federal exemptable taxes (only 5):

  • FIT-000 (Federal Income Tax)
  • FICA-000 (Social Security - Employee)
  • ER_FICA-000 (Social Security - Employer)
  • MEDI-000 (Medicare - Employee)
  • ER_FUTA-000 (Federal Unemployment - base only, no state modifier)

Federal non-exemptable (58):

  • MEDI2-000 (Additional Medicare)
  • ER_MEDI-000 (Employer Medicare)
  • All 51 ER_FUTA-000-XX state credit variants
  • All 5 ER_CRED-00X COVID credits

State non-exemptable (11):

  • 13-000-0000-ER_CRED-000 (GA - Employer Credit)
  • 17-000-0000-ER_CRED-000 (IL - Employer Credit)
  • 29-000-0000-SITCD-000-CD05 (MO - SIT Compensation Deduction)
  • 29-000-0000-SITCD-000-CD2 (MO - SIT Compensation Deduction)
  • 34-000-0000-ER_SUTA_SC-048 (NJ - SUTA Surcharge)
  • 41-000-0000-ER_TRANS-001 (OR - Employer Transit)
  • 41-000-0000-ER_TRANS-002 (OR - Employer Transit)
  • 41-000-0000-ER_TRANS-003 (OR - Employer Transit)
  • 41-000-0000-ER_TRANS-004 (OR - Employer Transit)
  • 41-000-0000-ER_TRANS-005 (OR - Employer Transit)
  • 41-000-0000-ER_TRANS-006 (OR - Employer Transit)

Usage in Codebase

# Usage What it does Key code
1 Exemption validation Prevents creating exemptions for non-exemptable taxes exempt_tax.rb:80, company_exempt_tax.rb:60
2 UI filtering Only shows exemptable taxes in exemption UI get_applicable_taxes_for_employee.rb:57
3 GraphQL exposure Exposes exemptable field on Tax type objects/tax.rb

Special Business Rules

Hardcoded validations in CompanyExemptTax:

  1. Companies cannot be FICA-exempt (company_exempt_tax.rb:65-68)

    def cannot_be_fica_exempt
      errors.add(:tax, 'Companies cannot be exempt from FICA taxes') if tax&.fica_tax?
    end
  2. Only non-profits can be FUTA-exempt at company level (company_exempt_tax.rb:71-74)

    def only_non_profit_can_be_futa_exempt
      if tax&.federal_unemployment_tax? && company.tax_payer_type != TAX_PAYER_TYPE_NON_PROFIT
        errors.add(:tax, 'Only Non-Profits can be FUTA exempt')
      end
    end
  3. Employee FICA exemptions exist for specific scenarios (documented in get_tax_exemptability_for_employee.rb:8-43):

    • Students working at same institution
    • Nonresident aliens (F1, J1 visa holders)
    • H-1B visa holders under totalization agreements

History

Origin (May 2012) - Commit b8201698b70bb by Edward Kim first added exemptable:

Tax Original (2012) Current
FIT false true
FICA true true
Medicare true true
FUTA true true
CA SIT false true

FIT changed from non-exemptable to exemptable sometime before the JSON migration.

Non-Exemptable Pattern Analysis

Federal non-exemptable (58 taxes):

Tax Why non-exemptable
MEDI2-000 (Additional Medicare) Surcharge on high earners, calculated on base
ER_MEDI-000 (Employer Medicare) Employer side not exemptable (unlike ER_FICA)
51 ER_FUTA-000-XX state credits Adjustments to base FUTA, not standalone taxes
5 ER_CRED-00X COVID credits Credits reduce liability, can't "exempt" a credit

State non-exemptable (11 taxes):

Tax State Why non-exemptable
ER_CRED-000 GA, IL Credits, not taxes
SITCD-000-CD05, SITCD-000-CD2 MO Compensation deduction (employer keeps portion)
ER_SUTA_SC-048 NJ Surcharge on base SUTA
ER_TRANS-001 to -006 OR Transit district taxes

Categories of non-exemptable taxes:

  1. Credits (ER_CRED, SITCD) - Can't exempt what reduces your liability
  2. Surcharges (MEDI2, ER_SUTA_SC) - Calculated on base tax, follows base
  3. Adjustments (ER_FUTA-XX state credits) - Not standalone, modifies base
  4. Employer-only non-exemptable (ER_MEDI, ER_TRANS) - EE side exemptable, ER side not

Employer-Only Non-Exemptable Pattern

Some taxes have an asymmetry where the employee side is exemptable but the employer side is not:

Tax EE Side ER Side
Social Security exemptable exemptable
Medicare exemptable non-exemptable
OR Transit exemptable (TRANS-000) non-exemptable (ER_TRANS-001 to -006)

This pattern appears intentional - the employer portion of these taxes cannot be exempted even when the employee portion can be. Worth confirming with compliance whether this is correct.

Interesting slack conversation about the conditions that lead to employee-exemptability https://gustohq.slack.com/archives/C057108USN8/p1717709170059109

Recommendation

Open Questions

  • (capture things to ask compliance partners)

References

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