Quantity Limit & Days Supply Validation

In modern Pharmacy Benefit Manager (PBM) claims adjudication pipelines, quantity limit (QL) and days supply (DS) validation operate as deterministic gatekeepers that prevent over-utilization, enforce clinical appropriateness, and directly modulate downstream financial calculations. Within the broader Formulary Validation & Rule Engine Design architecture, this subsystem must execute at sub-100ms latency while maintaining strict auditability and schema-driven consistency. Automation engineers and pharmacy benefits analysts must treat QL/DS validation not as an isolated check, but as a stateful evaluation layer that feeds tier assignment, prior authorization routing, and compliance reporting.

The foundational validation step cross-references the prescribed dispensed quantity against the National Drug Code (NDC) package configuration and the plan-defined maximum allowable quantity. A robust implementation must parse manufacturer package descriptors, normalize units of measure (UOM), and calculate the implied days supply based on the prescribed SIG code. As detailed in Validating days supply against NDC package sizes, discrepancies between dispensed units and package constraints frequently trigger standardized NCPDP rejection codes. Automation teams must deploy UOM translation matrices that map packaging metadata to CMS-standardized dispensing units, ensuring fractional quantities, split-packaging scenarios, and multi-unit vials are evaluated against plan thresholds without introducing rounding drift or unit mismatch errors.

When a claim passes QL/DS validation, the adjudication engine immediately routes it to the financial calculation layer. The validated days supply directly modulates copay proration, deductible accumulation, and out-of-pocket maximum tracking. Under the Tier Mapping & Copay Calculation Logic framework, quantity adjustments can shift a claim across tier boundaries if the plan utilizes volume-based pricing, mail-order incentives, or step-dose copay structures. For instance, a 90-day maintenance fill may qualify for a preferred tier rate, while a 30-day retail fill remains on standard pricing. The validation subsystem must emit structured metadata indicating the applied limit, the tier assignment, and the exact copay derivation path to support downstream reconciliation, payer reporting, and member transparency mandates.

Exceeding established quantity or days supply thresholds does not always result in a hard rejection. Modern rule engines evaluate whether the overage qualifies for an override pathway based on clinical history, refill cadence, and plan-specific escalation matrices. When a prescriber requests quantities beyond the standard plan allowance, the engine cross-references the Generic Product Identifier (GPI) hierarchy to determine therapeutic class restrictions, then routes the transaction to Step Therapy & Prior Auth Trigger Rules for clinical justification workflows. This conditional branching ensures that legitimate clinical exceptions bypass rigid hard limits while preserving audit trails for compliance review and DIR fee reconciliation.

flowchart TD
    A["Parse SIG: dose per admin and interval value/unit"] --> B["Normalize interval unit to hours (h=1, d=24, wk=168)"]
    B --> C["Admins per day = 24 / interval hours"]
    C --> D["Daily consumption = admins per day * dose per admin"]
    D --> E["Implied days supply = dispensed qty / daily consumption"]
    E --> F{"Qty exceeds plan QL max?"}
    F -->|"yes"| R1["Reject: 75 quantity limit exceeded"]
    F -->|"no"| G{"Implied days supply exceeds plan DS max?"}
    G -->|"yes"| R2["Reject: 76 days supply exceeded"]
    G -->|"no"| P["Pass: emit 440-QU and 442-DP"]

Figure: Quantity and days-supply validation deriving implied days from hour-normalized SIG intervals before comparing against plan QL/DS maxima.

Production-Grade Python Implementation

The following Python implementation demonstrates a deterministic, type-safe validation engine aligned with NCPDP Telecommunications Standard D.0 field mappings. It leverages decimal.Decimal for financial precision, pre-compiled regex for SIG parsing, and a cached UOM normalization matrix to guarantee sub-100ms execution in high-throughput adjudication environments.

python
from __future__ import annotations
import re
import logging
from dataclasses import dataclass, field
from decimal import Decimal, ROUND_HALF_UP
from enum import Enum
from typing import Dict, Optional, Tuple
from datetime import datetime

# NCPDP Reject Code Mappings (D.0 Standard)
class NCPDPRejectCode(str, Enum):
    QUANTITY_LIMIT_EXCEEDED = "75"
    DAYS_SUPPLY_EXCEEDED = "76"
    PRIOR_AUTH_REQUIRED = "89"
    SUCCESS = "00"

@dataclass(frozen=True)
class UOMConversion:
    """Maps manufacturer UOM to CMS-standard dispensing units."""
    to_tablet: Decimal = Decimal("1.0")
    to_milliliter: Decimal = Decimal("1.0")
    to_gram: Decimal = Decimal("1.0")

@dataclass
class ClaimPayload:
    """Simulated NCPDP D.0 claim fields."""
    ndc: str
    gpi: str
    dispensed_qty: Decimal
    dispensed_uom: str
    days_supply: int
    sig_code: str
    plan_ql_max: Decimal
    plan_ds_max: int
    is_maintenance: bool

@dataclass
class ValidationResult:
    status: NCPDPRejectCode
    adjusted_qty: Decimal
    adjusted_days_supply: int
    ncpdp_field_map: Dict[str, str] = field(default_factory=dict)
    routing_flags: list[str] = field(default_factory=list)
    audit_message: str = ""

# Pre-compiled SIG parser for common dosing frequencies
SIG_PATTERN = re.compile(
    r"(?:take|apply|instill)\s+(\d+)\s*(?:tablet|capsule|drop|puff|ml|mg)?\s*"
    r"(?:every|q)\s*(\d+)\s*(hour|hr|h|day|d|week|wk)?",
    re.IGNORECASE
)

# Convert a parsed SIG interval unit to hours for a common time base.
_INTERVAL_UNIT_HOURS = {
    "hour": 1, "hr": 1, "h": 1,
    "day": 24, "d": 24,
    "week": 168, "wk": 168,
}

class QLDSValidator:
    """Deterministic QL/DS validation engine for PBM adjudication."""
    
    UOM_MATRIX: Dict[str, UOMConversion] = {
        "TAB": UOMConversion(to_tablet=Decimal("1")),
        "ML": UOMConversion(to_milliliter=Decimal("1")),
        "GM": UOMConversion(to_gram=Decimal("1")),
        "EA": UOMConversion(to_tablet=Decimal("1")),
    }

    @classmethod
    def _normalize_uom(cls, qty: Decimal, uom: str) -> Decimal:
        """Convert manufacturer UOM to CMS-standard dispensing units."""
        conv = cls.UOM_MATRIX.get(uom.upper())
        if not conv:
            raise ValueError(f"Unsupported UOM: {uom}")
        # Simplified normalization; real-world requires NDC package size lookup
        return (qty * conv.to_tablet).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)

    @classmethod
    def _derive_days_supply(cls, sig: str, qty: Decimal) -> int:
        """Parse SIG code to calculate clinically implied days supply."""
        match = SIG_PATTERN.search(sig)
        if not match:
            # Fallback to plan-provided days supply if SIG is unstructured
            return 0
        dose_per_admin = int(match.group(1))
        interval_value = int(match.group(2))
        if interval_value == 0:
            return 0
        unit = (match.group(3) or "hour").lower()
        interval_hours = interval_value * _INTERVAL_UNIT_HOURS.get(unit, 1)
        administrations_per_day = Decimal("24") / Decimal(interval_hours)
        daily_consumption = administrations_per_day * dose_per_admin
        if daily_consumption <= 0:
            return 0
        implied_days = qty / daily_consumption
        return int(implied_days.quantize(Decimal("1"), rounding=ROUND_HALF_UP))

    @classmethod
    def validate(cls, claim: ClaimPayload) -> ValidationResult:
        """Execute deterministic QL/DS validation against plan limits."""
        try:
            normalized_qty = cls._normalize_uom(claim.dispensed_qty, claim.dispensed_uom)
            implied_ds = cls._derive_days_supply(claim.sig_code, normalized_qty)
            effective_ds = implied_ds if implied_ds > 0 else claim.days_supply

            routing_flags = []
            audit_msgs = []

            # QL Validation
            if normalized_qty > claim.plan_ql_max:
                return ValidationResult(
                    status=NCPDPRejectCode.QUANTITY_LIMIT_EXCEEDED,
                    adjusted_qty=claim.plan_ql_max,
                    adjusted_days_supply=effective_ds,
                    ncpdp_field_map={"440-QU": str(claim.plan_ql_max)},
                    audit_message=f"QL exceeded: dispensed {normalized_qty} > max {claim.plan_ql_max}"
                )

            # DS Validation
            if effective_ds > claim.plan_ds_max:
                return ValidationResult(
                    status=NCPDPRejectCode.DAYS_SUPPLY_EXCEEDED,
                    adjusted_qty=normalized_qty,
                    adjusted_days_supply=claim.plan_ds_max,
                    ncpdp_field_map={"442-DP": str(claim.plan_ds_max)},
                    audit_message=f"DS exceeded: implied {effective_ds} > max {claim.plan_ds_max}"
                )

            # Override/PA routing logic
            if normalized_qty > (claim.plan_ql_max * Decimal("0.8")) and claim.is_maintenance:
                routing_flags.append("MAINTENANCE_ESCALATION")
                audit_msgs.append("High-volume maintenance fill flagged for tier review")

            return ValidationResult(
                status=NCPDPRejectCode.SUCCESS,
                adjusted_qty=normalized_qty,
                adjusted_days_supply=effective_ds,
                ncpdp_field_map={"440-QU": str(normalized_qty), "442-DP": str(effective_ds)},
                routing_flags=routing_flags,
                audit_message="; ".join(audit_msgs) if audit_msgs else "Validation passed"
            )
        except Exception as e:
            logging.error(f"QL/DS validation failure for NDC {claim.ndc}: {e}")
            return ValidationResult(
                status=NCPDPRejectCode.PRIOR_AUTH_REQUIRED,
                adjusted_qty=Decimal("0"),
                adjusted_days_supply=0,
                audit_message=f"System exception triggered PA fallback: {str(e)}"
            )

Operational Hardening & Latency Guarantees

To sustain sub-100ms adjudication latency, the validation engine must operate as a stateless microservice with pre-warmed lookup caches for NDC package descriptors, GPI therapeutic class mappings, and plan-specific QL/DS matrices. UOM normalization and SIG parsing should be memoized or compiled at service startup, avoiding runtime regex recompilation and dictionary lookups. Financial calculations downstream must consume the adjusted_qty and adjusted_days_supply fields directly from the ValidationResult object, ensuring deterministic copay derivation without re-evaluating raw claim payloads.

Compliance and auditability require immutable logging of every validation decision, including the exact NCPDP D.0 field overrides (440-QU, 442-DP), the applied plan limit, and the routing flag emitted. When integrating with external clinical data sources or formulary updates, implement schema versioning (e.g., Avro or Protobuf) to prevent drift between plan configuration and adjudication logic. For teams scaling Python-based adjudication pipelines, leveraging the decimal module for all quantity and financial operations eliminates IEEE-754 floating-point inaccuracies that historically cause reconciliation mismatches in payer reporting.