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.
| Code | Account Type | Category | Withdrawal Notes |
QBK | Qualified Brokerage | Qualified Plan | Employer-sponsored plan distribution; subject to 20% mandatory fed withholding on eligible rollover distributions |
QDA | Qualified Direct Access | Qualified Plan | Same as QBK; plan custodian may require additional documentation |
QFL | Qualified Full-Service Lease | Qualified Plan | Fee-based qualified account; distribution may trigger plan-level approvals |
QIS | Qualified Institutional Services | Qualified Plan | Institutional qualified plan; higher documentation requirements |
QMF | Qualified Mutual Fund | Qualified Plan | MF-only qualified plan; restricted to mutual fund liquidation for distribution |
QMM | Qualified Money Market | Qualified Plan | Cash-equivalent qualified plan; direct cash distribution |
QOI | Qualified Outside Investment | Qualified Plan | Outside investment in qualified wrapper; may require in-kind distribution |
R1H | Roth IRA - Type 1 | Roth IRA | Qualified distributions are tax-free (5-year rule + age 59.5). Non-qualified: earnings taxable. |
R2H | Roth IRA - Type 2 | Roth IRA | Same as R1H; alternate custodial arrangement |
RDH | Roth IRA - Direct | Roth IRA | Direct Roth; contributions can be withdrawn tax/penalty-free anytime |
RIH | Traditional IRA - Household | Traditional IRA | Subject to RMD after age 73. Early withdrawal penalty < 59.5 unless exception applies. |
RIS | Traditional IRA - Services | Traditional IRA | Same as RIH; institutional service wrapper |
RM2 | IRA Managed - Type 2 | IRA Managed | Advisory-managed IRA; distribution may require advisor approval. SLA = 3 for managed accounts. |
RMH | IRA Managed - Household | IRA Managed | Same managed rules; household-level tracking |
RMO | IRA Managed - Outside | IRA Managed | Outside managed IRA; additional compliance review |
RMP | IRA Managed - Pension | IRA Managed | Pension IRA in managed wrapper; pension distribution rules may layer on top |
RMS | IRA Managed - Services | IRA Managed | Service-level managed IRA |
ROI | Rollover IRA | Rollover IRA | Rolled over from employer plan; subject to Traditional IRA distribution rules |
RP1 | Pension IRA - Type 1 | Pension IRA | Pension rollover; may have pension-specific distribution requirements |
RP2 | Pension IRA - Type 2 | Pension IRA | Same as RP1; alternate arrangement |
RPH | Pension IRA - Household | Pension IRA | Household-tracked pension IRA |
RRH | Roth IRA - Rollover Household | Roth Rollover | Roth rollover from employer plan; separate 5-year clock per rollover |
RRX | Roth IRA - Rollover Extended | Roth Rollover | Extended Roth rollover arrangement |
RS2 | SEP IRA - Type 2 | SEP IRA | Simplified Employee Pension; employer contributions only; standard IRA withdrawal rules |
RSM | SEP IRA - Managed | SEP IRA | Advisory-managed SEP; advisor approval for distributions |
RSX | SEP IRA - Extended | SEP IRA | Extended SEP arrangement |
RWH | IRA Withdrawal - Household | IRA Withdrawal | Designated withdrawal IRA; typically in distribution phase |
RWP | IRA Withdrawal - Pension | IRA Withdrawal | Pension 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).
| Code | Account Type | Category |
BBK | Brokerage | Standard Brokerage NLZ currently supports only these 3 |
BFL | Full-Service Lease Brokerage |
BMM | Money Market Brokerage |
BDA | Direct Access Brokerage |
BIS | Institutional Services Brokerage | Other Brokerage |
BMF | Mutual Fund Brokerage |
BOI | Outside Investment Brokerage |
A1H | Advisory - Type 1 Household | Advisory / Managed Accounts |
A2H | Advisory - Type 2 Household |
ADH | Advisory - Direct Household |
AIH | Advisory - Institutional Household |
AIS | Advisory - Institutional Services |
AM2 | Advisory Managed - Type 2 |
AMH | Advisory Managed - Household |
AMI | Advisory Managed - Institutional |
AMO | Advisory Managed - Outside |
AMP | Advisory Managed - Pension |
AMS | Advisory Managed - Services |
AP1 | Advisory Platform - Type 1 |
AP2 | Advisory Platform - Type 2 |
APH | Advisory Platform - Household |
ARH | Advisory Rollover - Household |
ARX | Advisory Rollover - Extended |
AS2 | Advisory Services - Type 2 |
ASI | Advisory Services - Institutional |
ASM | Advisory Services - Managed |
ASX | Advisory Services - Extended |
AWH | Advisory Withdrawal - Household |
AWP | Advisory 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
| Rule | Logic | Source (OLZ Reference) | Action on Fail |
| R1: Amount | Amount must be numeric and > 0 | Existing in NLZ EligibilityService | Ineligible |
| R2: Account class | Must be in the 57-code known set (28 retirement + 29 non-retirement). Unknown = blocked. | CmRequestFactory.IsRetirement() | Ineligible — "Unknown account type" |
| R3: IsRetirement flag | Set 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.5 | If 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 + RetirementAgeFiftyFiveOrMore | Warning + 10% penalty flag |
| R7b: RMD | If 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, FairMarketValue | Info — 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 HeldFiveYearsOrMore | Info — taxable flag |
| R7d: Excess contribution | If TransferReason = excess contribution return, validate ExcessAmt and NetIncomeAttributable. Must be before tax filing deadline. | CmRequest.ExcessAmt, ExcessContributionType | Validation error if after deadline |
| R8: Margin | If IsUseMargin = true + ACHD: check MarginCashAvailable balance. Set SLA = 2. | RttRequestFactory.GetSla() line 79-83 | Ineligible if insufficient margin |
| R9: Managed | If 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 Field | Source | Used For |
IraType | BetaAcctMaster.IRAType | Determine IRA category (Traditional, Roth, SEP, Rollover). RTT SLA routing for ZH/ZE/ZG types. |
FairMarketValue | BetaAccountIraDetails.IRAMktValueYE | RMD calculation: prior year-end fair market value / life expectancy factor |
FeeSchedule | BetaAccountIraDetails.FeeSchedule | Determine if closing fees apply on full distribution |
IsAnnualFeesPaid | BetaAccountIraDetails.FeeRecords != 0 | Block distribution if annual IRA fees unpaid |
AccountOpeningDate | BetaAcctMaster.OpeningDate | 5-year rule for Roth distributions |
BetaIraHistory | MasterData.ActivityHistory | Contribution/distribution history: ConDistIndicator, Amount, TaxYear, ContributionLimit, SourceCode |
DateOfBirth | BetaCustomerRelationship.DateofBirth | Age-based rules: early withdrawal (<59.5), RMD (≥73), catch-up contributions |
OrigOwnerBirthDate / DeathDate | CmRequest fields | Inherited IRA distribution rules (different from original owner) |
StateTaxWithholdingIndicator | BetaAcctMaster | State tax on-file indicator for distributions |
FederalTaxWithholdingIndicator | BetaAcctMaster | Fed tax on-file indicator for distributions |
FullDistribution | BalanceService | Full account liquidation amount for total distribution |
SelectedNetGrossDistribution | CmRequest.AmntNetOrGross | "N" = NET (taxes deducted from amount), other = GROSS (taxes deducted on top) |
IRA Distribution Type Matrix
| IRA Type | Codes | RMD Required? | Early Penalty? | 5-Year Rule? | Tax Treatment |
| Traditional IRA | RIH, RIS | Yes (age 73+) | Yes (<59.5) | No | Fully taxable as ordinary income |
| Roth IRA | R1H, R2H, RDH | No (owner lifetime) | Earnings only (<59.5) | Yes | Contributions tax-free; earnings taxable if non-qualified |
| Roth Rollover | RRH, RRX | No (owner lifetime) | Earnings only (<59.5) | Yes (per rollover) | Separate 5-year clock per employer rollover |
| SEP IRA | RS2, RSM, RSX | Yes (age 73+) | Yes (<59.5) | No | Fully taxable as ordinary income |
| Rollover IRA | ROI | Yes (age 73+) | Yes (<59.5) | No | Fully taxable as ordinary income |
| Pension IRA | RP1, RP2, RPH | Yes (age 73+) | Yes (<59.5) | No | Fully taxable; pension-specific rules may layer |
| IRA Managed | RM2, RMH, RMO, RMP, RMS | Depends on IRA type | Depends | Depends | Same as underlying IRA; SLA = 3 for managed |
| IRA Withdrawal Phase | RWH, RWP | Typically yes | Unlikely (in dist phase) | No | Fully taxable |
| Qualified Plans | QBK, QDA, QFL, QIS, QMF, QMM, QOI | Plan-specific | Yes (<59.5) | No | 20% 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
| Scenario | Fed Tax | State Tax | Net Calculation |
| Non-retirement (BBK, BFL, BMM, A** codes) | None | None | NetAmount = Amount (no withholding) |
| Qualified plan (Q** codes) eligible rollover | Mandatory 20% | Per state rules | NetAmount = Amount - (Amount * 0.20) - StateTax |
| IRA distribution (R** codes) with fed elected | FedTaxPercent (min 10% if elected) | Per request or on-file | Depends on NET/GROSS election |
| IRA distribution with no withholding | $0 (investor opted out) | May still apply in mandatory states | NetAmount = Amount - StateTax |
| Roth qualified distribution | $0 (tax-free) | $0 | NetAmount = Amount |
NET vs GROSS Distribution
| Election | Meaning | Example ($10,000 request, 20% fed, 5% state) |
| NET (AmntNetOrGross = "N") | Investor receives the requested amount; taxes are added on top | GrossAmount = $13,333. FedTax = $2,667. StateTax = $667. NetToInvestor = $10,000. |
| GROSS (AmntNetOrGross != "N") | Total distribution is the requested amount; taxes are deducted | GrossAmount = $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 Type | Transaction Type | PECO Lookup Field | Used For |
| ACHD | ACHD | "Net Distribution PECO Code" | One-time ACH withdrawal/distribution |
| ACHC | ACHC | "Contribution PECO Code" | One-time ACH deposit/contribution |
| INS_PER_U / INS_PER_C | ACHD | "Net Distribution PECO Code" | Periodic distribution (recurring withdrawal) |
| INS_PER_U / INS_PER_C | ACHC | "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
| Condition | SLA | Meaning | OLZ Source |
| Margin + ACHD | 2 | Margin withdrawal requires ops review | RttRequestFactory.cs line 79-83 |
| Managed account + periodic (INS_PER_C/R) | 3 | Managed periodic distribution needs advisor sign-off | RttRequestFactory.cs line 86-99 |
| IRA type ZH/ZE/ZG + ACHD/ACHC/INS_PER | 3 | IRA distributions require additional compliance review | RttRequestFactory.cs line 102-107 |
| Advisory ops + ACHD/ACHC/INS_PER | 1 | Advisory operations team handles | RttRequestFactory.cs line 109 |
| Default | 0 | Standard processing, no special review | Default path |
7. Implementation Summary: 5 Steps to Build in NLZ
| Step | NLZ Service | NLZ Repo | What to Build | OLZ Reference |
| 1 | EligibilityService | cam-movemoney-process-api | Expand 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) |
| 2 | IRA Rules (within EligibilityService or new IraDistributionService) | cam-movemoney-process-api | Inline 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 |
| 3 | TaxWithholdingService (NEW) | cam-movemoney-process-api | Build 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) |
| 4 | BETA request builder | cam-movemoney-system-api | Map 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() |
| 5 | RTT status + SLA | cam-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() |