ACH Withdrawal: Account Types, IRA Rules & Tax Withholding

Detailed implementation plan for ACHD support in NLZ cam-movemoney-process-api

1. Account Type Classification — Retirement vs Non-Retirement

Source: OLZ CmRequestFactory.IsRetirement(). The account class code determines whether the account is retirement (distribution rules apply) or non-retirement (standard withdrawal). Unknown codes return null and are blocked.

28 Retirement Account Codes

Withdrawals from retirement accounts are distributions subject to IRA rules, PECO codes, RMD validation, and mandatory tax withholding reporting.
CodeAccount TypeCategoryWithdrawal Notes
QBKQualified BrokerageQualified PlanEmployer-sponsored plan distribution; subject to 20% mandatory fed withholding on eligible rollover distributions
QDAQualified Direct AccessQualified PlanSame as QBK; plan custodian may require additional documentation
QFLQualified Full-Service LeaseQualified PlanFee-based qualified account; distribution may trigger plan-level approvals
QISQualified Institutional ServicesQualified PlanInstitutional qualified plan; higher documentation requirements
QMFQualified Mutual FundQualified PlanMF-only qualified plan; restricted to mutual fund liquidation for distribution
QMMQualified Money MarketQualified PlanCash-equivalent qualified plan; direct cash distribution
QOIQualified Outside InvestmentQualified PlanOutside investment in qualified wrapper; may require in-kind distribution
R1HRoth IRA - Type 1Roth IRAQualified distributions are tax-free (5-year rule + age 59.5). Non-qualified: earnings taxable.
R2HRoth IRA - Type 2Roth IRASame as R1H; alternate custodial arrangement
RDHRoth IRA - DirectRoth IRADirect Roth; contributions can be withdrawn tax/penalty-free anytime
RIHTraditional IRA - HouseholdTraditional IRASubject to RMD after age 73. Early withdrawal penalty < 59.5 unless exception applies.
RISTraditional IRA - ServicesTraditional IRASame as RIH; institutional service wrapper
RM2IRA Managed - Type 2IRA ManagedAdvisory-managed IRA; distribution may require advisor approval. SLA = 3 for managed accounts.
RMHIRA Managed - HouseholdIRA ManagedSame managed rules; household-level tracking
RMOIRA Managed - OutsideIRA ManagedOutside managed IRA; additional compliance review
RMPIRA Managed - PensionIRA ManagedPension IRA in managed wrapper; pension distribution rules may layer on top
RMSIRA Managed - ServicesIRA ManagedService-level managed IRA
ROIRollover IRARollover IRARolled over from employer plan; subject to Traditional IRA distribution rules
RP1Pension IRA - Type 1Pension IRAPension rollover; may have pension-specific distribution requirements
RP2Pension IRA - Type 2Pension IRASame as RP1; alternate arrangement
RPHPension IRA - HouseholdPension IRAHousehold-tracked pension IRA
RRHRoth IRA - Rollover HouseholdRoth RolloverRoth rollover from employer plan; separate 5-year clock per rollover
RRXRoth IRA - Rollover ExtendedRoth RolloverExtended Roth rollover arrangement
RS2SEP IRA - Type 2SEP IRASimplified Employee Pension; employer contributions only; standard IRA withdrawal rules
RSMSEP IRA - ManagedSEP IRAAdvisory-managed SEP; advisor approval for distributions
RSXSEP IRA - ExtendedSEP IRAExtended SEP arrangement
RWHIRA Withdrawal - HouseholdIRA WithdrawalDesignated withdrawal IRA; typically in distribution phase
RWPIRA Withdrawal - PensionIRA WithdrawalPension IRA in withdrawal phase

29 Non-Retirement Account Codes

Standard brokerage withdrawals. No IRA rules, no RMD, no special PECO codes. Tax reporting is 1099 (not 1099-R).
CodeAccount TypeCategory
BBKBrokerageStandard Brokerage
NLZ currently supports only these 3
BFLFull-Service Lease Brokerage
BMMMoney Market Brokerage
BDADirect Access Brokerage
BISInstitutional Services BrokerageOther Brokerage
BMFMutual Fund Brokerage
BOIOutside Investment Brokerage
A1HAdvisory - Type 1 HouseholdAdvisory / Managed Accounts
A2HAdvisory - Type 2 Household
ADHAdvisory - Direct Household
AIHAdvisory - Institutional Household
AISAdvisory - Institutional Services
AM2Advisory Managed - Type 2
AMHAdvisory Managed - Household
AMIAdvisory Managed - Institutional
AMOAdvisory Managed - Outside
AMPAdvisory Managed - Pension
AMSAdvisory Managed - Services
AP1Advisory Platform - Type 1
AP2Advisory Platform - Type 2
APHAdvisory Platform - Household
ARHAdvisory Rollover - Household
ARXAdvisory Rollover - Extended
AS2Advisory Services - Type 2
ASIAdvisory Services - Institutional
ASMAdvisory Services - Managed
ASXAdvisory Services - Extended
AWHAdvisory Withdrawal - Household
AWPAdvisory Withdrawal - Pension

2. Step 1 — Expand EligibilityService for ACHD

Current state: NLZ EligibilityService.ValidateAccountClassCode() only allows BBK, BFL, BMM. All 57 codes must be accepted. Retirement accounts need additional distribution-specific rules.

New Inline Eligibility Rules for ACHD (Withdrawal)

flowchart TD START(["EligibilityService.EvaluateEligibility()"]) R1["Rule 1: Validate amount > 0 and numeric"] R2{"Rule 2: Account class code\nin known set? (57 codes)"} R2F(["FAIL: Unknown account type"]) R3["Rule 3: Determine IsRetirement\n28 retirement codes vs 29 non-retirement"] R4{"Rule 4: BORD restrictions check"} R4F(["FAIL: BORD restricted"]) R5{"Rule 5: House account check"} R5F(["FAIL: House account"]) R6{"Rule 6: Is this ACHD\n(withdrawal/distribution)?"} R6N["Non-retirement path:\nStandard withdrawal rules"] R7{"Rule 7: IRA distribution checks\n(retirement only)"} R7A["7a: Age validation\n< 59.5 = early withdrawal penalty\nunless exception applies"] R7B["7b: RMD check\nAge 73+ Traditional/SEP/Rollover:\nMust take Required Minimum Distribution"] R7C["7c: 5-year rule (Roth)\nR1H/R2H/RDH/RRH/RRX:\nEarnings taxable if < 5 years"] R7D["7d: Excess contribution return\nExcessAmt + ExcessContributionType\nMust be returned by tax deadline"] R8{"Rule 8: Margin check\nIsUseMargin = true?"} R8M["Margin withdrawal:\nSLA = 2, additional validation\nCheck margin balance available"] R9{"Rule 9: Managed account?\nProgramTypeId in managed set?"} R9M["Managed account:\nSLA = 3 for periodic distributions\nMay require advisor approval"] R10["Rule 10: High-dollar threshold"] PASS(["ELIGIBLE"]) START --> R1 --> R2 R2 -->|No| R2F R2 -->|Yes| R3 --> R4 R4 -->|Restricted| R4F R4 -->|Clear| R5 R5 -->|House account| R5F R5 -->|Not house| R6 R6 -->|ACHC deposit| R6N --> R10 R6 -->|ACHD withdrawal| R7 R7 --> R7A --> R7B --> R7C --> R7D --> R8 R8 -->|Yes| R8M --> R10 R8 -->|No| R9 R9 -->|Yes| R9M --> R10 R9 -->|No| R10 --> PASS style START fill:#e8ecff,stroke:#4f6bed style PASS fill:#e6faf5,stroke:#00b894 style R2F fill:#fde8e6,stroke:#e74c3c style R4F fill:#fde8e6,stroke:#e74c3c style R5F fill:#fde8e6,stroke:#e74c3c style R7 fill:#fff8ec,stroke:#f39c12 style R8M fill:#f0eeff,stroke:#6c5ce7 style R9M fill:#f0eeff,stroke:#6c5ce7

Rule Details

RuleLogicSource (OLZ Reference)Action on Fail
R1: AmountAmount must be numeric and > 0Existing in NLZ EligibilityServiceIneligible
R2: Account classMust be in the 57-code known set (28 retirement + 29 non-retirement). Unknown = blocked.CmRequestFactory.IsRetirement()Ineligible — "Unknown account type"
R3: IsRetirement flagSet IsRetirement = true for 28 retirement codes, false for 29 non-retirement. Null for unknown (already blocked by R2).CmRequestFactory.IsRetirement()Routing flag, not a fail
R7a: Age < 59.5If retirement + age < 59.5 + no exception code (hardship, disability, SEPP, first-time home), flag early withdrawal. Not a hard stop — investor may proceed with penalty acknowledgement.FICO handled via Customer.DateOfBirth + RetirementAgeFiftyFiveOrMoreWarning + 10% penalty flag
R7b: RMDIf Traditional/SEP/Rollover IRA + age ≥ 73 + no distribution taken this year (check BetaIraHistory.ConDistIndicator), flag RMD requirement. IRA types: RIH, RIS, ROI, RS2, RSM, RSX, RP1, RP2, RPH.FICO via IraType, BetaIraHistory, FairMarketValueInfo — RMD amount suggested
R7c: 5-year rule (Roth)Roth accounts (R1H, R2H, RDH, RRH, RRX): if account age < 5 years, earnings portion is taxable. Check AccountOpeningDate vs current date.FICO via HeldFiveYearsOrMoreInfo — taxable flag
R7d: Excess contributionIf TransferReason = excess contribution return, validate ExcessAmt and NetIncomeAttributable. Must be before tax filing deadline.CmRequest.ExcessAmt, ExcessContributionTypeValidation error if after deadline
R8: MarginIf IsUseMargin = true + ACHD: check MarginCashAvailable balance. Set SLA = 2.RttRequestFactory.GetSla() line 79-83Ineligible if insufficient margin
R9: ManagedIf ProgramTypeId in {10,11,12,13,14,16,17,20,21,22,23,24}: managed account. Set SLA = 3 for periodic distributions (INS_PER).RttRequestFactory.IsManagedAccount()SLA routing, may require advisor approval

3. Step 2 — IRA-Specific Inline Rules for NLZ

OLZ delegated all IRA logic to FICO via external HTTP calls, passing a rich RulesEngineRequest with IRA details, IRA history, balances, and customer data. NLZ must implement these rules inline. Below is the complete data the NLZ inline engine needs.

IRA Data Requirements (from OLZ RulesRequestFactory)

Data FieldSourceUsed For
IraTypeBetaAcctMaster.IRATypeDetermine IRA category (Traditional, Roth, SEP, Rollover). RTT SLA routing for ZH/ZE/ZG types.
FairMarketValueBetaAccountIraDetails.IRAMktValueYERMD calculation: prior year-end fair market value / life expectancy factor
FeeScheduleBetaAccountIraDetails.FeeScheduleDetermine if closing fees apply on full distribution
IsAnnualFeesPaidBetaAccountIraDetails.FeeRecords != 0Block distribution if annual IRA fees unpaid
AccountOpeningDateBetaAcctMaster.OpeningDate5-year rule for Roth distributions
BetaIraHistoryMasterData.ActivityHistoryContribution/distribution history: ConDistIndicator, Amount, TaxYear, ContributionLimit, SourceCode
DateOfBirthBetaCustomerRelationship.DateofBirthAge-based rules: early withdrawal (<59.5), RMD (≥73), catch-up contributions
OrigOwnerBirthDate / DeathDateCmRequest fieldsInherited IRA distribution rules (different from original owner)
StateTaxWithholdingIndicatorBetaAcctMasterState tax on-file indicator for distributions
FederalTaxWithholdingIndicatorBetaAcctMasterFed tax on-file indicator for distributions
FullDistributionBalanceServiceFull account liquidation amount for total distribution
SelectedNetGrossDistributionCmRequest.AmntNetOrGross"N" = NET (taxes deducted from amount), other = GROSS (taxes deducted on top)

IRA Distribution Type Matrix

IRA TypeCodesRMD Required?Early Penalty?5-Year Rule?Tax Treatment
Traditional IRARIH, RISYes (age 73+)Yes (<59.5)NoFully taxable as ordinary income
Roth IRAR1H, R2H, RDHNo (owner lifetime)Earnings only (<59.5)YesContributions tax-free; earnings taxable if non-qualified
Roth RolloverRRH, RRXNo (owner lifetime)Earnings only (<59.5)Yes (per rollover)Separate 5-year clock per employer rollover
SEP IRARS2, RSM, RSXYes (age 73+)Yes (<59.5)NoFully taxable as ordinary income
Rollover IRAROIYes (age 73+)Yes (<59.5)NoFully taxable as ordinary income
Pension IRARP1, RP2, RPHYes (age 73+)Yes (<59.5)NoFully taxable; pension-specific rules may layer
IRA ManagedRM2, RMH, RMO, RMP, RMSDepends on IRA typeDependsDependsSame as underlying IRA; SLA = 3 for managed
IRA Withdrawal PhaseRWH, RWPTypically yesUnlikely (in dist phase)NoFully taxable
Qualified PlansQBK, QDA, QFL, QIS, QMF, QMM, QOIPlan-specificYes (<59.5)No20% mandatory withholding on eligible rollover distributions

4. Step 3 — Tax Withholding Computation (TaxWithholdingService)

Must be built from scratch. OLZ has the fields (FedTaxIndicator, FedTaxPercent, StateTaxIndicator, StateTaxPercent, CalculatedNetAmount) but they were never populated by the investor-ach-api handler. The NLZ cam-movemoney-process-api needs a new TaxWithholdingService.

Tax Withholding Decision Flow

flowchart TD START(["TaxWithholdingService.Calculate()"]) RET{"IsRetirement?"} NONRET["Non-retirement: no withholding\nCalculatedNetAmount = Amount"] QUAL{"Qualified plan?\n(QBK/QDA/QFL/QIS/QMF/QMM/QOI)"} MAND["Mandatory 20% fed withholding\non eligible rollover distributions"] IRA["IRA distribution"] FED{"Fed tax indicator\nfrom request or on-file?"} FED_REQ["Use request values:\nFedTaxPercent, FedTaxAmount"] FED_FILE["Use on-file values:\nFederalTaxWithholdingIndicator\nFederalTaxWithholdingPercentage\nfrom BetaAcctMaster"] FED_NONE["No fed withholding\nMin 10% default for IRA if elected"] STATE{"State tax indicator\nfrom request or on-file?"} STATE_REQ["Use request values:\nStateTaxPercent, StateTaxAmount"] STATE_FILE["Use on-file values:\nStateTaxWithholdingIndicator\nStateTaxWithholdingPercentage"] STATE_NONE["No state withholding\nSome states mandate withholding"] NETGROSS{"AmntNetOrGross"} NET["NET distribution:\nCalculatedNetAmount = Amount\nGrossAmount = Amount + taxes"] GROSS["GROSS distribution:\nGrossAmount = Amount\nCalculatedNetAmount = Amount - taxes"] RESULT(["Return: GrossAmount,\nCalculatedNetAmount,\nFedTaxAmount, StateTaxAmount"]) START --> RET RET -->|No| NONRET --> RESULT RET -->|Yes| QUAL QUAL -->|Yes| MAND --> STATE QUAL -->|No| IRA --> FED FED -->|Request provided| FED_REQ --> STATE FED -->|On-file| FED_FILE --> STATE FED -->|None| FED_NONE --> STATE STATE -->|Request provided| STATE_REQ --> NETGROSS STATE -->|On-file| STATE_FILE --> NETGROSS STATE -->|None| STATE_NONE --> NETGROSS NETGROSS -->|N = NET| NET --> RESULT NETGROSS -->|G = GROSS| GROSS --> RESULT style START fill:#e8ecff,stroke:#4f6bed style RESULT fill:#e6faf5,stroke:#00b894 style MAND fill:#fde8e6,stroke:#e74c3c style NONRET fill:#e6faf5,stroke:#00b894

Tax Calculation Logic

ScenarioFed TaxState TaxNet Calculation
Non-retirement (BBK, BFL, BMM, A** codes)NoneNoneNetAmount = Amount (no withholding)
Qualified plan (Q** codes) eligible rolloverMandatory 20%Per state rulesNetAmount = Amount - (Amount * 0.20) - StateTax
IRA distribution (R** codes) with fed electedFedTaxPercent (min 10% if elected)Per request or on-fileDepends on NET/GROSS election
IRA distribution with no withholding$0 (investor opted out)May still apply in mandatory statesNetAmount = Amount - StateTax
Roth qualified distribution$0 (tax-free)$0NetAmount = Amount

NET vs GROSS Distribution

ElectionMeaningExample ($10,000 request, 20% fed, 5% state)
NET (AmntNetOrGross = "N")Investor receives the requested amount; taxes are added on topGrossAmount = $13,333. FedTax = $2,667. StateTax = $667. NetToInvestor = $10,000.
GROSS (AmntNetOrGross != "N")Total distribution is the requested amount; taxes are deductedGrossAmount = $10,000. FedTax = $2,000. StateTax = $500. NetToInvestor = $7,500.

5. Step 4 — BETA PECO Code Mapping

BETA uses PECO (Processing Event Code) to determine how to process the transaction. OLZ BetaRequestFactory.GetIrasPecoCode() maps request type + transaction type to the correct PECO lookup field.
Request TypeTransaction TypePECO Lookup FieldUsed For
ACHDACHD"Net Distribution PECO Code"One-time ACH withdrawal/distribution
ACHCACHC"Contribution PECO Code"One-time ACH deposit/contribution
INS_PER_U / INS_PER_CACHD"Net Distribution PECO Code"Periodic distribution (recurring withdrawal)
INS_PER_U / INS_PER_CACHC"Contribution PECO Code"Periodic contribution (recurring deposit)

NLZ Implementation

The NLZ cam-movemoney-system-api BETA request builder (ICamMovemoneySystemClient.PostBetaTransactionAsync) must include the WithdrawalSourceCode field mapped from the BetaLookup response using the correct PECO lookup field based on transaction type.

The AchType field sent to BETA is already "ACHD" in OLZ for all transactions. NLZ must ensure the same. The PECO code differentiates how BETA handles contributions vs distributions internally.

6. Step 5 — RTT SLA Routing for ACHD

OLZ RttRequestFactory.GetSla() has a specific SLA assignment matrix for ACHD based on account attributes. NLZ must port this into the RTT update logic.

SLA Assignment Matrix

flowchart TD START(["GetSla() for ACHD"]) M{"IsUseMargin = true?"} M_YES["SLA = 2\nMargin withdrawal"] MAN{"IsManagedAccount?\nProgramTypeId in\n10,11,12,13,14,16,17,20,21,22,23,24"} MAN_PER{"Request type\nINS_PER_C or INS_PER_R?"} MAN_YES["SLA = 3\nManaged + Periodic distribution"] MAN_NO["SLA = 0\nManaged + one-time"] IRA{"IRA Type?\nZH / ZE / ZG"} IRA_YES["SLA = 3\nIRA distribution"] ADV{"IsAdvisoryOps?"} ADV_YES["SLA = 1\nAdvisory ops distribution"] DEFAULT["SLA = 0\nStandard"] START --> M M -->|Yes| M_YES M -->|No| MAN MAN -->|Yes| MAN_PER MAN_PER -->|Yes| MAN_YES MAN_PER -->|No| MAN_NO MAN -->|No| IRA IRA -->|Yes| IRA_YES IRA -->|No| ADV ADV -->|Yes| ADV_YES ADV -->|No| DEFAULT style START fill:#e8ecff,stroke:#4f6bed style M_YES fill:#f0eeff,stroke:#6c5ce7 style MAN_YES fill:#f0eeff,stroke:#6c5ce7 style IRA_YES fill:#fff8ec,stroke:#f39c12 style ADV_YES fill:#fff8ec,stroke:#f39c12 style DEFAULT fill:#e6faf5,stroke:#00b894
ConditionSLAMeaningOLZ Source
Margin + ACHD2Margin withdrawal requires ops reviewRttRequestFactory.cs line 79-83
Managed account + periodic (INS_PER_C/R)3Managed periodic distribution needs advisor sign-offRttRequestFactory.cs line 86-99
IRA type ZH/ZE/ZG + ACHD/ACHC/INS_PER3IRA distributions require additional compliance reviewRttRequestFactory.cs line 102-107
Advisory ops + ACHD/ACHC/INS_PER1Advisory operations team handlesRttRequestFactory.cs line 109
Default0Standard processing, no special reviewDefault path

7. Implementation Summary: 5 Steps to Build in NLZ

StepNLZ ServiceNLZ RepoWhat to BuildOLZ Reference
1EligibilityServicecam-movemoney-process-apiExpand validClassCodes to 57. Add IsRetirement flag. Add ACHD-specific rules: age check, RMD, 5-year, excess contribution, margin, managed account.CmRequestFactory.IsRetirement(), RttRequestFactory.GetSla(), RulesRequestFactory (IRA data)
2IRA Rules (within EligibilityService or new IraDistributionService)cam-movemoney-process-apiInline IRA distribution validation: RMD calculation (FMV / life expectancy), Roth 5-year rule, early withdrawal penalty, excess contribution deadline.FICO rules engine (external) + RulesRequestFactory IRA fields
3TaxWithholdingService (NEW)cam-movemoney-process-apiBuild from scratch: Fed/State tax calculation, NET vs GROSS election, qualified plan 20% mandatory, IRA default 10%, state mandatory withholding. Compute CalculatedNetAmount.CmRequest fields (FedTax*, StateTax*, AmntNetOrGross) + BetaAcctMaster (on-file withholding indicators)
4BETA request buildercam-movemoney-system-apiMap WithdrawalSourceCode from BetaLookup using "Net Distribution PECO Code" for ACHD. Ensure AchType = ACHD. Handle INS_PER_U/C with inner ACHD/ACHC switch.BetaRequestFactory.GetIrasPecoCode()
5RTT status + SLAcam-movemoney-process-api (RttService / AchCommonService)Port SLA matrix: margin=2, managed+periodic=3, IRA(ZH/ZE/ZG)=3, advisory=1. Add IraType + ProgramTypeId to RTT request payload.RttRequestFactory.GetSla(), IsManagedAccount()