Generating DEA Form 41 Destruction Records

Assemble a DEA Form 41 destruction record from witnessed-destruction ledger events, bind it to a snapshot hash, and attest the non-retrievable standard.

A DEA Form 41 is only as trustworthy as the events it summarizes. The form asks a registrant to certify an inventory of controlled substances destroyed — each item with its NDC, quantity, and form — along with the method, date, place, and the two witnesses who observed the destruction. If that certification is typed by hand from memory or from a mutable spreadsheet, it can drift from what actually happened; if it is assembled deterministically from witnessed-destruction events already committed to an append-only ledger, the form becomes a rendering of the record rather than a re-entry of it. This recipe assembles a Form 41 destruction record from ledger events and binds it to a snapshot hash so the certified figures cannot be altered after signing. It sits within Controlled Substance Waste & Destruction Logging, which defines the witnessed-waste and destruction events this recipe consumes.

The design target is a single function: take the witnessed-destruction events for a destruction batch, verify each carries two distinct witnesses and a non-retrievable method attestation, aggregate them into the Form 41 line items, and freeze the result under a snapshot hash. When that holds, the Form 41 an inspector receives is provably the same set of destructions the ledger recorded.

Assembling a DEA Form 41 record from witnessed-destruction ledger events Witnessed-destruction events from the append-only ledger are filtered to one destruction batch. Each event is verified to carry two distinct witnesses and a non-retrievable method attestation; an event failing either check is rejected. Verified events are aggregated into Form 41 line items grouped by NDC and form, then the assembled record is bound to a SHA-256 snapshot hash that certifies the inventory of items destroyed, method, date, and place. Ledger events witnessed destruction Verify each 2 witnesses · non-retrievable Aggregate line items by NDC + form Form 41 record + SHA-256 snapshot hash Rejected missing witness / method

Problem framing

The parent topic establishes that destruction is a witnessed disposition under 21 CFR Part 1317 and that DEA Form 41 is the registrant record of what was destroyed. What it leaves open is the mechanical question this page answers: given a set of witnessed-destruction events in the ledger, how do you produce a Form 41 whose figures are guaranteed to match those events and cannot be edited afterward? The problem has three parts — select the events belonging to one destruction batch, verify each meets the Form 41 evidentiary bar (two distinct witnesses, a non-retrievable method, a quantity with explicit units), and freeze the aggregate under a hash so the signed form is bound to an exact ledger state. A reconstruction of ledger state at an arbitrary past instant, which the same snapshot technique enables, is treated in Point-in-Time Inventory Reconstruction.

Prerequisites & environment

  • Python 3.11+ — uses dataclasses(frozen=True), enum.Enum, Decimal, and union-type syntax.
  • Standard library only: dataclasses, enum, hashlib, json, decimal, logging, datetime. Add pytest for the test block.
  • Regulatory context you should already hold: 21 CFR Part 1317 (controlled-substance disposal and the non-retrievable standard), the DEA Form 41 fields (inventory of substances destroyed, method, date, place, and two witnessing signatures), and 45 CFR § 164.312(b) (audit controls). The witnessed-destruction event shape comes from the parent topic.
  • You are consuming events, not creating destructions: this recipe assumes witnessed-destruction events already exist in the append-only ledger and assembles a certifiable record from them.

Implementation: assembling a snapshot-bound Form 41

The assembler takes the witnessed-destruction events for a batch, rejects any that fail the evidentiary checks, groups the survivors into line items by NDC and dosage form, and binds the whole record to a SHA-256 snapshot hash computed over the sorted, canonicalized content. The snapshot hash is what makes the certified form tamper-evident: re-hashing the stored record must reproduce it, or the record was altered after assembly.

python
from __future__ import annotations

import hashlib
import json
import logging
from collections import defaultdict
from dataclasses import dataclass, field, asdict
from datetime import datetime, timezone
from decimal import Decimal
from enum import Enum

logger = logging.getLogger("cs_ledger.form41")


class DestructionMethod(str, Enum):
    """Only methods accepted as non-retrievable under 21 CFR Part 1317."""
    INCINERATION = "INCINERATION"
    CHEMICAL_DIGESTION = "CHEMICAL_DIGESTION"
    ENCAPSULATION = "ENCAPSULATION"
    REVERSE_DISTRIBUTION = "REVERSE_DISTRIBUTION"  # transfer to authorized registrant


class Form41Error(ValueError):
    """Raised when a destruction event cannot support a Form 41 certification."""


@dataclass(frozen=True)
class WitnessedDestructionEvent:
    """A committed destruction event sourced from the append-only ledger."""
    event_id: str
    ndc: str
    dosage_form: str            # e.g. "tablet", "injectable_ml"
    quantity: str               # Decimal string
    unit: str                   # explicit units, e.g. "mg", "mL", "tablet"
    schedule: str
    method: DestructionMethod
    non_retrievable: bool       # method attestation
    actor_id: str
    witness_1_id: str
    witness_2_id: str
    destroyed_at: str           # ISO 8601 UTC
    place: str
    record_hash: str            # the event's own chained hash


@dataclass(frozen=True)
class Form41LineItem:
    ndc: str
    dosage_form: str
    unit: str
    total_quantity: str
    event_ids: tuple[str, ...]


@dataclass(frozen=True)
class Form41Record:
    batch_id: str
    line_items: tuple[Form41LineItem, ...]
    methods: tuple[str, ...]
    place: str
    date_range: tuple[str, str]
    witnesses: tuple[str, ...]
    snapshot_hash: str
    assembled_at: str = field(
        default_factory=lambda: datetime.now(timezone.utc).isoformat()
    )


def _verify(event: WitnessedDestructionEvent) -> None:
    """Enforce the Form 41 evidentiary bar for one destruction event."""
    if event.witness_1_id == event.witness_2_id:
        raise Form41Error(f"{event.event_id}: two DISTINCT witnesses required.")
    if event.actor_id in (event.witness_1_id, event.witness_2_id):
        raise Form41Error(f"{event.event_id}: actor cannot also be a witness.")
    if not event.non_retrievable:
        raise Form41Error(
            f"{event.event_id}: method {event.method.value} lacks non-retrievable "
            "attestation under 21 CFR Part 1317."
        )
    if Decimal(event.quantity) <= 0 or not event.unit:
        raise Form41Error(f"{event.event_id}: quantity and explicit unit required.")


def assemble_form_41(
    batch_id: str, events: list[WitnessedDestructionEvent]
) -> Form41Record:
    """Build a snapshot-bound Form 41 record from witnessed-destruction events."""
    if not events:
        raise Form41Error("No destruction events supplied for the batch.")
    for event in events:
        _verify(event)

    # Aggregate quantities by (NDC, dosage_form, unit); mixed units never sum.
    grouped: dict[tuple[str, str, str], list[WitnessedDestructionEvent]] = defaultdict(list)
    for event in events:
        grouped[(event.ndc, event.dosage_form, event.unit)].append(event)

    line_items = tuple(
        Form41LineItem(
            ndc=ndc,
            dosage_form=form,
            unit=unit,
            total_quantity=str(sum((Decimal(e.quantity) for e in evs), Decimal("0"))),
            event_ids=tuple(sorted(e.event_id for e in evs)),
        )
        for (ndc, form, unit), evs in sorted(grouped.items())
    )

    dates = sorted(e.destroyed_at for e in events)
    places = {e.place for e in events}
    if len(places) != 1:
        raise Form41Error("All events in a batch must share one place of destruction.")
    witnesses = tuple(sorted({w for e in events
                              for w in (e.witness_1_id, e.witness_2_id)}))
    methods = tuple(sorted({e.method.value for e in events}))

    # Snapshot hash binds the certified content to an exact ledger state.
    canonical = json.dumps(
        {"batch_id": batch_id,
         "line_items": [asdict(li) for li in line_items],
         "methods": methods, "place": places.pop(),
         "date_range": [dates[0], dates[-1]],
         "source_hashes": sorted(e.record_hash for e in events)},
        sort_keys=True,
    )
    snapshot_hash = hashlib.sha256(canonical.encode("utf-8")).hexdigest()

    record = Form41Record(
        batch_id=batch_id, line_items=line_items, methods=methods,
        place=next(iter({e.place for e in events})),
        date_range=(dates[0], dates[-1]), witnesses=witnesses,
        snapshot_hash=snapshot_hash,
    )
    logger.info(
        "form41_assembled",
        extra={"batch_id": batch_id, "line_items": len(line_items),
               "witnesses": len(witnesses), "snapshot_hash": snapshot_hash},
    )
    return record

Grouping by (ndc, dosage_form, unit) — never by NDC alone — is what prevents the silent unit error that plagues hand-typed forms: 500 mg and 500 mL must never land in the same total. Sorting every collection before hashing makes the snapshot hash deterministic, so the same set of events always yields the same certification digest regardless of input order.

Verification & testing

Two properties must be proven: that an event failing the evidentiary bar is rejected, and that the snapshot hash is stable and unit-safe. The block below exercises both, plus the aggregation.

python
import pytest


def _event(**over):
    base = dict(
        event_id="d1", ndc="00409120230", dosage_form="injectable_ml",
        quantity="2", unit="mL", schedule="II",
        method=DestructionMethod.INCINERATION, non_retrievable=True,
        actor_id="rph-1", witness_1_id="w-2", witness_2_id="w-3",
        destroyed_at="2026-07-15T14:00:00+00:00", place="Room B-1",
        record_hash="a" * 64,
    )
    base.update(over)
    return WitnessedDestructionEvent(**base)


def test_two_distinct_witnesses_required():
    with pytest.raises(Form41Error):
        assemble_form_41("b1", [_event(witness_2_id="w-2")])   # witnesses identical


def test_non_retrievable_attestation_required():
    with pytest.raises(Form41Error):
        assemble_form_41("b1", [_event(non_retrievable=False)])


def test_quantities_aggregate_and_units_do_not_cross():
    rec = assemble_form_41("b1", [
        _event(event_id="d1", quantity="2", unit="mL"),
        _event(event_id="d2", quantity="3", unit="mL"),
        _event(event_id="d3", quantity="5", unit="mg", dosage_form="tablet"),
    ])
    totals = {(li.ndc, li.unit): li.total_quantity for li in rec.line_items}
    assert totals[("00409120230", "mL")] == "5"     # 2 + 3 mL summed
    assert totals[("00409120230", "mg")] == "5"     # mg kept separate


def test_snapshot_hash_is_stable_and_order_independent():
    a = assemble_form_41("b1", [_event(event_id="d1"), _event(event_id="d2")])
    b = assemble_form_41("b1", [_event(event_id="d2"), _event(event_id="d1")])
    assert a.snapshot_hash == b.snapshot_hash        # order-independent

The emitted log line is the compliance artifact tying the assembled form back to the ledger. It carries the batch, counts, and snapshot hash but no patient data:

text
INFO cs_ledger.form41 form41_assembled \
     batch_id=b1 line_items=2 witnesses=2 snapshot_hash=4e9a...1f7c

Re-running assemble_form_41 over the same stored events and comparing the resulting snapshot_hash to the digest printed on the certified form is the inspection check: a mismatch proves the form no longer reflects the ledger, which is exactly the tamper signal a 21 CFR Part 1317 destruction record must be able to surface.

Gotchas & compliance pitfalls

  • The two-witness omission is the classic failure. A destruction observed by one person — or by two records of the same identity — cannot support a Form 41 certification. The _verify step rejects identical witnesses and an actor doubling as a witness; do not “fix” a single-witness destruction by copying the name into both fields, which is precisely the falsification the distinctness check exists to catch.
  • non_retrievable is an attestation, not a default. A method value alone does not prove the drug was rendered irretrievable; the boolean attestation records that the operator certified the non-retrievable end state. Assembling a form from events where that attestation is absent would certify something the ledger never established, so it is rejected.
  • Quantity units must be explicit and never cross-summed. Milligrams, milliliters, and dosage units are different quantities; summing across them produces a meaningless total that an auditor will flag immediately. Grouping by unit — and storing quantities as Decimal strings — keeps each line item coherent and avoids floating-point drift on partial-unit destructions.
  • Snapshot binding must cover source hashes. Hashing only the aggregated totals would let someone swap the underlying events while keeping the same sums. Including the sorted record_hash of every source event in the snapshot ties the certification to the exact ledger entries, so substituting events changes the snapshot hash.
  • One place, one batch. A Form 41 certifies destruction at a place; mixing destructions performed at different locations into one record misrepresents where the disposal occurred. The assembler refuses a batch spanning multiple places rather than silently picking one.

Frequently Asked Questions

Does generating the record file the Form 41 with the DEA?

No — this recipe produces the certifiable record, not a submission. Registrants generally retain the completed Form 41 as part of their required records and produce it on inspection or when the destruction involves DEA-witnessed or authorized processes; on-site destructions are recorded and kept rather than routinely mailed in. The value of assembling it from the ledger is that the retained record is provably consistent with the witnessed-destruction events, so it withstands inspection without manual reconciliation.

Why bind the record to a snapshot hash instead of just storing it?

Because a stored document can be edited, but a stored document plus a hash of its exact content cannot be edited undetectably. The snapshot hash — computed over the sorted line items and the source events’ own hashes — lets anyone recompute it later and prove the certified figures match the ledger state at assembly time. It is the same immutability guarantee the append-only ledger provides, extended to the derived form.

How does reverse distribution appear on the form?

As a destruction method value of REVERSE_DISTRIBUTION with its own non-retrievable attestation supplied by the authorized registrant that performed the destruction. The transfer out of the registrant’s custody is a disposition in the ledger, and the destruction attestation arrives from the receiving party; the assembler treats it like any other method provided the two-witness and non-retrievable checks are satisfied by the sourced events.

What keeps patient information off the Form 41?

The witnessed-destruction events carry only drug identity, quantity, method, and staff identities — never patient identifiers — so the assembled form inherits that boundary. This keeps the destruction record on the operational-compliance side of the 45 CFR § 164.502 segregation wall, exactly as the waste and vault-access records do, and means the certified Form 41 can be produced for a DEA inspection without exposing PHI.