A safety case does not automatically become a submission. It travels through a sequence of decisions — each reading specific fields, each able to stop the case before an XML is ever produced. This guide walks that sequence three ways at once: the why (learning), the logic (in practice), and the build (how to approach it). If you are new to the single-case side, start with our guide to ICSR case processing.
One case can owe several submissions — one XML per obligated authority — or none at all, feeding only aggregate reports like the PSUR.
Scope of this guide
This is a software design perspective on the reporting-rules engine, not a regulatory reference. Timelines shown reflect FDA (21 CFR 314.80 / 312.32), EU (GVP VI/IX; CT Reg 536/2014) and India (NDCT 2019 / PvPI) as commonly implemented. Post-market and clinical-trial pathways are kept separate throughout. India trial-reporting clocks must be confirmed against the current rule text before go-live; the client QPPV owns the final reportability determination.
The mental model
Five gates, in sequence
Each gate asks one question and reads specific case fields. If any gate says stop, no submission XML is produced — but the data still lives in the database and may feed aggregate reporting.
The validity gate before everything
A case is only reportable if it holds the four minimums: an identifiable patient, an identifiable reporter, at least one suspect drug, and at least one adverse event. Miss any one and nothing transmits — chase the missing data instead.
Data model
The fields the engine reads
Every gate consumes specific case fields. Each maps to an E2B(R3) data element, so the developer can wire straight to the XML later. (For the difference between the two formats, see E2B(R2) vs E2B(R3).)
| Field | E2B(R3) | Used by |
|---|---|---|
| Worldwide unique case ID | C.1.1 | Gate 3 · dedup |
| Date report first received (Day 0) | C.1.4 | Gate 5 |
| Date of most recent info | C.1.5 | Gate 4 · 5 |
| Report type (initial / follow-up) | C.1.7 | Gate 3 |
| Nullification / amendment flag | C.1.11.1 | Gate 3 |
| Case type (spontaneous / trial / literature) | C.1.3 | Gate 1 · 2 |
| Seriousness (case level) | E.i.3.1 | Gate 1 · 4 · 5 |
| Seriousness criteria (death, LT, hosp…) | E.i.3.2a-f | Gate 1 · 4 |
| Reaction / event (MedDRA PT) | E.i.2.1b | Gate 1 · 4 |
| Expectedness (listed vs unlisted) | derived | Gate 1 · 4 |
| Causality / relatedness | G.k.9.i | Gate 1 · 4 |
| Suspect / concomitant drug role | G.k.1 | Gate 1 · 2 · 4 |
| Product ↔ authorisation | G.k.2.x | Gate 2 |
| Country of occurrence | E.i.9 | Gate 2 |
| Country of primary source | C.2.r.3 | Gate 2 |
Case category filter
is this a normal case at all?◇ Learning · why this runs before everything else
Not every case is a plain patient took drug, had reaction. Some cases carry a special category that changes which rules even apply — before you ever get to seriousness or expectedness. This gate classifies the case first, so Gate 1 onward evaluates it with the right rule set instead of the generic one.
◆ In practice · the categories and why each is special
Each of these categories has a genuinely different regulatory path, not just a label:
| Category | Why it's routed differently |
|---|---|
| Combination product (drug + device) | FDA's combination-product postmarketing safety reporting rule folds in report types beyond the standard drug ICSR — 15-day reports, 5-day reports, and device malfunction reports alongside death/serious-injury reports. A device malfunction with no adverse event can still be reportable on its own timeline, which a plain drug-ICSR rule set would miss entirely. |
| Incomplete ICSR | Missing one of the four validity minimums (patient, reporter, suspect drug, event). The decision here isn't reject — it's whether this rule set evaluates incomplete cases at all (e.g. to trigger a follow-up request) or holds them until complete. |
| Invalid ICSR | Fails the validity minimums outright. Normally nothing transmits, but some partner/distribution agreements still want visibility into invalid cases for their own records — hence a separate toggle rather than a hard stop. |
| Occupational exposure | The exposed person is a handler or healthcare worker, not the patient. Most authorities do not treat this as a standard ICSR unless an adverse event actually resulted — so exposure-only occupational cases usually need a different (often non-expedited) path. |
| Pregnancy exposure only (no AE) | Exposure during pregnancy with no adverse outcome yet is commonly tracked as a pregnancy follow-up / registry case rather than filed as an ICSR — the case is opened to await outcome, not submitted as a safety report today. |
| Pregnancy exposure with AE | An adverse outcome did occur (e.g. congenital anomaly), so this returns to normal seriousness-based reportability — a congenital anomaly is itself one of the standard seriousness criteria. |
Two controls per category, not one
A per-destination include/exclude flag decides whether this rule set even evaluates that category. A separate override flag decides whether this specific rule set's answer beats a broader default set upstream (e.g. a base rule set or organisation-wide policy). Keeping these as two independent switches — not one — is what lets a partner-specific template diverge from the company-wide default for exactly one category, without touching anything else.
▹ Building it · how to approach it
Run a classifier immediately after case lock, before Gate 1: derive the category (or categories — combination product and pregnancy can co-occur) from the case data, then look up two booleans per destination-and-category pair: include and override. If include=false for a category on this destination, the case skips Gate 1 for that destination entirely and is logged as not evaluated — category excluded, not silently dropped. If include=true, hand the case to Gate 1 onward — but tag which category triggered it, since combination-product and pregnancy cases carry extra sub-rules (malfunction-only timing, registry routing) that a plain drug case doesn't.
- Store category rules as data per destination, exactly like the other gates — never branch in code per category name.
- A case can match more than one category (e.g. combination product and pregnancy-with-AE); evaluate every matching category's sub-rules, don't just pick one.
- Log the category match and the include/override decision in the same evaluation log used by the other gates, so an auditor sees why a case was or wasn't evaluated for a destination.
A general pattern, not a single vendor's design
This gate is a general pattern seen across pharmacovigilance safety systems — it isn't tied to one specific vendor's design, and the categories above are described in our own words based on the underlying FDA/ICH/WHO reporting concepts (combination-product PMSR reporting, ICSR validity minimums, pregnancy and occupational-exposure reporting practice), not copied from any single product's interface.
Reportability
submit at all?◇ Learning · why each field
The regulator cares about one thing: is this a new, important safety signal? Three fields decide it.
- Seriousness — did something bad enough happen? Death, life-threatening, hospitalisation, disability, congenital anomaly, or other medically important condition.
- Expectedness — is this reaction already in the product's reference document (label / SmPC / IB)? Already listed = expected. Not listed = unexpected = new information.
- Causality — is the drug reasonably suspected? For spontaneous post-market cases the bar is low; for trials you need a reasonable possibility that cannot be ruled out.
◆ In practice · the decision logic
The classic reportable combination is Serious + Unexpected + (Related where required) — the SUSAR of clinical trials, and the 15-day Alert of post-marketing.
| Case type | Expedited when… | Else |
|---|---|---|
| Spontaneous (post-market) | Serious AND Unexpected (causality assumed via suspect drug) | Aggregate only (PSUR/PBRER) |
| Clinical trial (IND / CTR) | Serious AND Unexpected AND reasonable causal link = SUSAR | Expected serious → DSUR |
| Literature | Serious AND Unexpected | Tracked, aggregate |
| Solicited (PSP, registry) | Like spontaneous, but causality affirmatively assessed | Aggregate |
▹ Building it · how to approach it
Treat reportability as one self-contained decision that takes a case plus an authority's profile and returns a clear verdict with a reason attached — never scattered through screen logic. Expectedness is a comparison of each reaction against the product's reference safety list, loaded per product — never a list baked into the program.
- First the validity gate — if the four minimums aren't all present, stop immediately with an invalid-case reason.
- Then read three things: is it serious, is any reaction unexpected against the product's reference document, and is it causally related.
- For a trial case all three must hold (a SUSAR); for a spontaneous post-market case, causality is presumed from the suspect-drug role, so the relatedness gate is a per-authority toggle — off for post-market, on for trials.
- Always hand back a reason, not just yes or no — serious & unexpected versus aggregate only — because the audit trail lives on it.
Routing
which authorities?◇ Learning · why each field
One case can owe several submissions — one XML per obligated authority. The question, per authority: given where it happened, where the product is sold, and what kind of case it is — do we owe this authority a report?
- Country of occurrence — where the event physically happened.
- Product ↔ authorisation — is this product actually marketed in that country? No authorisation usually = no obligation.
- Case type — trials route to trial authorities, spontaneous to the PV system.
◆ In practice · the where-to-report matrix
Build it as a matrix of (authority) × (conditions) → (yes/no + format + gateway + timeline). Keep post-market and clinical-trial as two separate matrices — different databases, different clocks.
Post-market ICSRs — spontaneous / solicited / literature
| Authority | Report when | Serious expedited | Non-serious / serious-expected |
|---|---|---|---|
FDA FAERS via FDA ESG | US-approved application (NDA/ANDA/BLA); serious + unexpected → expedited | 15 cal. days | Periodic (PADER / PBRER) — no 90-day ICSR clock |
EMA EudraVigilance EVPM | Authorised in EEA; serious worldwide, non-serious EEA-occurring only | 15 cal. days | 90 days non-serious EEA; non-EEA not sent |
| India CDSCO / PvPI | Product marketed in India (MAH PV obligation) | Per PvPI | Periodic (PSUR per CDSCO schedule) |
Clinical-trial SUSARs — IND / EU CT Regulation
| Authority | Report when | Fatal / life-threatening | Other SUSAR |
|---|---|---|---|
FDA IND safety report 21 CFR 312.32 | IND open; serious + unexpected + reasonable possibility drug caused it | 7 cal. days (initial) | 15 cal. days |
EMA EudraVigilance EVCTM / CTIS | Trial conducted/authorised in EEA under CT Reg (EU) 536/2014 | 7 cal. days | 15 cal. days |
India CDSCO + Ethics Committee NDCT 2019 | Trial in India; SAE/SUSAR related to investigational product | Death: initial intimation 24 h; analysis to DCGI/EC within 14 days of death | Confirm exact NDCT clause & each recipient's clock before go-live |
India timelines are the highest-risk cell
The 24 h / 14-day chain is the widely-cited NDCT Rules 2019 death-reporting flow, but the rule distinguishes trial-death from other SAEs, and DCGI, the Ethics Committee, the sponsor and other investigators each carry different obligations. Do not hardcode a single value — pin every clock to the exact Third-Schedule clause and get the client QPPV / regulatory sign-off.
▹ Building it · implementation
Routing walks the list of authorities, tests each one's conditions, and collects the destinations that qualify. Each rule is stored as data, not written into the program: a set of conditions (product marketed in the US, spontaneous, serious, unexpected) paired with an action (use the E2B(R3) format with the FDA profile, send through the FDA gateway, set a 15-day clock). Design principle: the engine is a matcher; the rules are data. Adding a partner or a country means adding a rule entry, not editing the engine.
Report type
initial · follow-up · nullification · amendment◇ Learning · why each field
Every case has one worldwide case ID (C.1.1) that never changes. What changes is the version.
- Initial — first time this case is sent to that authority.
- Follow-up — same case ID, new info arrived, message number increments.
- Nullification — please delete this case, it was invalid/duplicate.
- Amendment — case valid, but a field was wrong; here's the correction.
▹ Building it · how to approach it
The decision keys off the submission history for this case-and-authority pair. If the case has never been sent to that authority it is an initial report; if it is being withdrawn, a nullification; if a field is being corrected, an amendment; otherwise a follow-up — which then goes to the significance check before anything is generated. Keep a running submission history per case per authority; the worldwide case identifier stays constant everywhere while a separate message counter steps up on every follow-up.
Significance
send a follow-up or not?The gate most systems get wrong
A follow-up does not automatically create an XML. Only a medically meaningful change earns a resubmission — a typo fix updates the database and stops there.
▹ Building it · how to approach it
Significance is a comparison of the current case against the version last submitted to that authority. Each field carries a rule for what counts: a reaction added or removed, seriousness moving up, a newly met seriousness criterion, causality shifting toward related, a suspect drug added or changed, an outcome worsening. If any changed, generate the follow-up; if nothing on that list moved, it is not significant.
- Freeze a copy of each case exactly as it was submitted, so the comparison is against what the authority actually received — not an in-between edit.
- Record which field triggered significance. Auditors ask both why you resubmitted and why you didn't.
Downgrades — the other half of significance
Significance is not only about things getting worse
A follow-up can also make a case less serious or expected — and that is still a reportable change. An engine that only watches for upgrades will silently miss these.
◇ Learning · what a downgrade means
New information can lower a case's status, not just raise it. Three common downgrades:
- Downgrade to Non-Serious — the case no longer meets any seriousness criterion (e.g. the hospitalisation turned out to be a routine visit).
- Downgrade to Serious-Expected — still serious, but the reaction is now found to be listed in the reference document, so it is no longer a 15-day / expedited unexpected case.
- Downgrade to SAE — a SUSAR that loses its unexpected or causal basis and becomes an ordinary serious adverse event.
| Downgrade rule | Trigger (vs last submitted) | Engine action |
|---|---|---|
| Downgrade to Non-Serious | All seriousness criteria now cleared | Follow-up XML; drops out of expedited path → aggregate |
| Downgrade to Serious-Expected | Reaction now listed in reference doc | Follow-up XML; no longer expedited-unexpected |
| Downgrade to SAE | SUSAR loses unexpectedness or causality | Follow-up XML; SUSAR tag removed |
▹ Building it · how to approach it
Downgrades are just significance rules that fire in the opposite direction — build them as first-class entries in the same significance list, not as an afterthought. Each still produces a follow-up transmission and carries its own reason code (DOWNGRADE_TO_NONSERIOUS, DOWNGRADE_TO_SERIOUS_EXPECTED, DOWNGRADE_TO_SAE). A downgrade can change which onward path the case takes — a case that drops to non-serious may leave the expedited queue entirely and feed only aggregate reports from that point on. Record the transition so the audit trail explains why the expedited clock stopped.
Timeline clock
by when?◇ Learning · Day 0
The clock starts at Day 0 = the date anyone in the company first received the four minimum criteria — not when the case was entered or locked. From Day 0 you count calendar days.
▹ Building it · how to approach it
The due date is the start date plus the applicable number of days, then rolled forward to the next working day if it lands on a weekend or holiday.
- Run a separate clock per destination — the FDA and EMA windows can differ for the same case.
- Drive escalation off how many days remain: amber when the window is closing, red once it's overdue.
- The EMA measures compliance from receipt to the moment it hits the gateway — so capture the gateway timestamp, not just when you generated the file.
Study-level due-date override
The authority default is not always the final word
A specific study can carry protocol-agreed or agency-agreed timelines that differ from the standard clock, handled as a per-registration due-date override (a 7-day and a 15-day override on the study registration). The engine must let a study beat the profile default.
◆ In practice · when this matters
By default a case due date follows the FDA or EMA rule for its criteria. But a trial's protocol or its agreement with the agency may set different expedited windows for that study only. Without an override, the system would apply the generic clock and either over- or under-report against what was actually agreed for that trial.
| Clock resolution order | Wins when |
|---|---|
| 1 · Study due-date override (7-day / 15-day) | Override is set Yes on the study registration |
| 2 · Authority profile default | No study override present |
▹ Building it · how to approach it
Add a thin override layer in front of the timeline lookup. When resolving the window for a destination, first check whether the case's study registration has an override flag set; if it does, take the study's 7-day / 15-day values, otherwise fall back to the authority profile. Keep it as data on the registration — a boolean plus the two day-values — never a code branch per study, and record which source supplied the window (study override vs profile) in the evaluation log.
After the gates
Generate → validate → transmit → ACK
Only destinations that pass all five gates hit the generator. Each runs the region profile, three validation layers, gateway dispatch, and acknowledgement tracking.
Nullflavor handling lives here
For each mandatory element with missing data, apply the correct nullFlavor per the region profile's rule (e.g. NI / MSK / ASKU / UNK / NA / NASK — the allowed set is element-specific per the ICH/regional RIG, not a fixed four-item list). Every gate decision and every submission is written to the audit trail.
Implementation blueprint
How to build it
The five gates describe what the engine decides. This part describes how a developer turns that into a working system. The single most important idea: keep deciding and doing in two separate layers.
Decide, then do — never mix them
One layer looks at a case and produces a list of decisions (who is owed a report, when, initial or follow-up). A second, separate layer takes those decisions and acts (builds the XML, validates, transmits). Mixing the two is what makes safety systems impossible to test or audit — keep a clean line between them.
The decision layer — three moving parts
Everything the decision layer needs breaks into three pieces. Build these and the five gates have something to run on.
1 · A single picture of the case
Before any rule runs, gather the case into one consistent picture — the facts every gate will read. Work out the tricky derived facts (is each reaction expected, is each drug related) once, here, and let every gate read the same answer. If you recompute per authority, two authorities can silently disagree; compute once and the engine stays predictable. This picture also carries the validity check — patient, reporter, suspect drug and event all present — and if that fails, nothing else runs.
| The picture holds | E2B(R3) source |
|---|---|
| Worldwide case identifier & prior history | C.1.1 + history |
| Clock start date, case type | C.1.4 · C.1.3 |
| Seriousness & its criteria | E.i.3.1 · E.i.3.2a-f |
| Reactions, and each one's expectedness | E.i.2.1b + reference doc |
| Each suspect drug's causality | G.k.9.i |
| Products & where they're registered | products + registrations |
| Country of occurrence | E.i.9 |
| Valid-case check (the four minimums) | derived — if it fails, stop |
2 · Finding the destinations two different ways
The gates decide the what; first you need the who. Destinations come from two sources that work on completely different principles — build both.
Authorities are worked out, not flagged
Don't put an FDA? checkbox on the case. Instead, give every country a home authority. Then look at where the product is registered: the country of each registration points to its authority. If several countries share one authority (Germany and France both point to the EMA), that's a single report, not one each. This one design choice is what most people get wrong.
Partners work the opposite way. A partner (a licensee, a co-marketer, a study site) isn't tied to a country. It carries a list of what it cares about — certain products, a study, a substance, or a region — and a case reaches that partner only if it matches that list. So the two sources are:
| Authorities (regulators) | Partners (companies) | |
|---|---|---|
| How the destination is found | country of registration → its authority | case matches the partner's own list |
| Matched by | where the product is registered | product / study / substance / region |
| Typical rules | strict regulatory logic | often looser (send everything serious) |
| Produces | a Submission | a Distribution |
Watch this case
If the same destination is owed both as a regulator submission and as a partner distribution, produce two separate records — don't merge them into one. They're driven by different obligations and audited separately.
3 · The rules held as data, not code
The difference between FDA, EMA and India is not different code — it's the same five gates reading different settings. Hold each authority's settings as editable data, so changing a number (say the EMA's 90-day non-serious window, or an expedited clock) is an edit, never a re-release. The settings that differ:
| Setting | FDA | EMA | India |
|---|---|---|---|
| Seriousness required? | yes | yes (no for EEA non-serious) | yes |
| Unexpectedness required? | yes (15-day path) | not for the serious trigger | — |
| Causality required? | presumed post-market · required for trials | presumed post-market · required for trial SUSAR | required for trial SUSAR |
| Post-market expedited window | 15 days (serious+unexpected) | 15 days worldwide serious | per PvPI / NDCT |
| Fatal SUSAR window (trial) | 7 days | 7 days | 24 h initial · 14 d analysis* |
| Post-market non-expedited | Periodic (PADER/PBRER) — no 90-day ICSR clock | 90 days non-serious EEA · non-EEA not sent | Periodic (PSUR) |
| Extra recipient | — | — | Ethics Committee (trial SAE) |
*India trial figures are the commonly-cited NDCT Rules 2019 death-reporting chain and must be confirmed against the current rule text per recipient before go-live.
The execution layer — turning the plan into transmissions
Only destinations that cleared all five gates arrive here, each already carrying its report type, format, gateway and due date. This layer just acts, in order: build the E2B(R3) file using that authority's regional profile, validate it (structure first, then the ICH conformance rules, then any regional rules — fail at the earliest layer), transmit through the right gateway, and track the acknowledgement, re-queuing anything that fails.
Always keep the why
Every destination the engine looked at — matched or not — should leave a record with the reason (serious & unexpected, aggregate only, not significant, no registration). Months later, why didn't partner B get case 245? has a one-line answer instead of a forensic investigation.
Can the gates change? Yes — and here's the judgement
Five gates is the natural shape, but it's a design choice, not a law. A team could merge reportability and routing into one pass, or split significance into its own service. The advice from the safety side: keep these five as distinct, nameable decisions even if the code combines them, because each one is a question an auditor will ask independently — was it reportable, where was it owed, was it new or a follow-up, was the follow-up meaningful, and was it on time. If a proposed design can't answer those five separately, it has merged something that should stay visible.
What a developer actually needs to build
Not a sequence to memorise — a small set of parts. Build them and wire them decision-layer-first:
Country → authority map
Every country names its home regulator. Unblocks authority routing.
Editable rule settings
FDA / EMA / India values held as data, seeded from the table above.
One shared gate-runner
Runs the five gates over the case picture with a given rule set — reused by both authorities and partners.
Partner lists
Each partner's products / studies / regions, and how loosely it reports.
Submission & distribution records
The output of the decision layer — what the execution layer picks up.
The evaluation log
Every destination checked, with its reason. Your audit lifeline.
Then connect them: build the case picture, run the shared gate-runner for each authority and each partner, write the records and the log, and hand the cleared records to the existing AS2 sender. Everything above is data or a reusable check — adding a fourth authority tomorrow is new settings, not new code.
Extensibility
Designing for partners
The whole point: support FDA, EMA and India from day one, and add any new authority just by adding its settings — no engine rewrite. Everything an authority needs sits in one profile, one per authority (or per client-and-authority pair):
| Part of the profile | What it holds |
|---|---|
| Reportability settings | whether causality is required, and where expectedness is read from |
| Routing rules | the conditions that lead to a format, gateway and timeline |
| Report-type handling | how nullification and amendment are treated |
| Significance settings | which changes count as significant |
| Timeline settings | the 7 / 15 / 90 windows, the Day 0 source, the working-day calendar |
| Region profile | the E2B(R3) regional elements and nullflavor rules |
Six rules for a clean, testable engine
Engine is just a matcher
Takes a case and a profile, returns decisions. No regulation baked inside.
Rules are versioned data
Every profile versioned; you can see which set produced a decision.
Reason codes always
Every decision returns a why, not just a verdict.
Predictable & replayable
Same case + same profile version = same decisions, always.
Frozen snapshots
Submitted versions kept as-is for the significance comparison.
Decide, then do
Gates decide; generation and transmission run separately.
Template rule sets — two starting points every engine needs
Not every destination needs a full custom rule set
Ship two template rule sets — one permissive starting point and one deliberately empty — that solve two very common real-world needs. Build both as assignable templates so a new partner or destination is a configuration choice, not a code change.
| Template | What it is | Use it when |
|---|---|---|
| General Distribution Rule Set | A permissive starter that inherits a base rule set (e.g. the EMA rule set) plus a few extra rules — e.g. initial-case-submission rules. A worked example, not an exhaustive list. | Sending to a partner, licensee or another safety system where you need more flexibility than a strict agency rule set gives. |
| Placeholder Rule Set (Empty) | A valid rule set with no rules attached. Assigning it guarantees the engine produces zero transmissions for that destination. | You want a product/registration matched to a destination but must be certain nothing is auto-generated (hold, manual-only, or not-yet-live destinations). |
▹ Building it · how to build both
- Inheritance / base rule set — a rule set can name another as its base and inherit all its rules, then add or override its own. Resolve rules at the lowest level of inheritance: a child rule beats the base; an override to blank clears an inherited value.
- The empty set is a feature, not a gap — assigning an empty rule set must deterministically yield no transmissions; the engine treats matched a rule set with zero rules as a valid, logged no-destination outcome, not an error.
One Last Time (OLT) — the report when a registration goes inactive
◇ Learning · why OLT exists
When a product or study registration is being made inactive, there can still be one final obligation to send an outstanding case to that authority before the door closes. Treat One Last Time as a standard rule (as in the EMA rule set), not an optional extra.
▹ Building it · how to approach it
Tie OLT to the registration's lifecycle. When a registration transitions to inactive, run one final evaluation pass over open cases for that destination: anything still owed generates its last transmission, then the destination stops matching future cases. Carry a distinct reason code (ONE_LAST_TIME) so the audit trail shows the report was a lifecycle-close, not an ordinary follow-up.
The golden test set — proving FDA / EMA / India logic
A bank of representative cases with the expected decision per authority. Run every profile change against it. When you add a partner, you add rows — the engine code never changes.
| Test case | FDA | EMA | India |
|---|---|---|---|
| US, marketed, serious + unexpected, spontaneous | Initial · 15d | 15d if EEA | per rule |
| EEA, non-serious, spontaneous | none/90d | 90d generate | none |
| Non-EEA, non-serious | 90d if US | not sent | — |
| Trial SUSAR, fatal, unexpected, related, India site | 7d IND | 7d CT Reg | 24h + 14d, DCGI+EC* |
| Follow-up: reporter phone changed only | no XML | no XML | no XML |
| Follow-up: new death added | 15d f/up | 15d f/up | per rule |
| Follow-up: serious → non-serious (downgrade) | f/up + drop expedited | f/up + drop expedited | per rule |
| Follow-up: SUSAR loses unexpectedness (→ SAE) | f/up, SUSAR tag off | f/up, SUSAR tag off | per rule |
| Registration going inactive, case still open | OLT report | OLT report | per rule |
| Destination assigned empty rule set | no XML | no XML | no XML |
| Duplicate case found | Nullify | Nullify | per rule |
*India trial-death reporting (24 h initial intimation, 14-day analysis to DCGI/EC) per NDCT Rules 2019 — confirm the exact clause and per-recipient clock before go-live. All per rule cells resolve from the India profile once its clauses are pinned.
Revision card
One line per gate
Reportability
Serious + Unexpected (+ Related for trials) = report. Miss the 4 minimums = not even valid.
Routing
Marketed there + owed there = an XML there. One case → many authorities.
Report type
Seen this case ID at this authority before? No = Initial, Yes = Follow-up / Null / Amend.
Significance
Did something medical change? Yes = follow-up XML. Typo = DB only.
Timeline
Day 0 = first receipt of the 4 minimums. 7 / 15 / 90 calendar days.
The reporting-rules engine is where individual case processing meets regulatory obligation. Get the five gates clean and config-driven, and adding a fourth or fifth authority becomes new settings, not new code. For the reports that sit on top of individual cases, see our guide to aggregate reporting.
Frequently asked questions
Common questions
Add one field to your Product record: linked_partners — a multi-select list of partners.
That's it. When a case comes in, look at its suspect product(s), read that product's linked_partners list, and those are your candidate partners. No separate table, no lookup elsewhere — the product itself tells you.
Product: Cholecap
├── name, strength, form... (existing fields)
└── linked_partners: [Partner A, Partner C]Do the same for Study.linked_partners if you also route by study (for trial cases).
Rule of thumb
If a case has 2 suspect products and each links to different partners, the case is a candidate for both — union all linked_partners[] across all suspect products on the case, then dedupe.
Use the same 7 fields for every partner — don't invent new ones per partner. Only the values change; the fields stay identical.
This is what makes the system maintainable — one evaluator function works for all partners.
| # | Parameter | What it checks on the case | Typical use |
|---|---|---|---|
| 1 | always_send | nothing — sends every matched case | Partner wants everything, no filter |
| 2 | require_seriousness | is the case serious? | Partner wants only serious cases |
| 3 | require_unexpectedness | is any reaction unlisted/new? | Partner wants only new safety info |
| 4 | min_seriousness_criteria | does it include death/LT/hosp/etc.? | Partner wants only the worst cases |
| 5 | require_causality | is the drug considered related? | Partner wants only related cases |
| 6 | case_type_filter | spontaneous / trial / literature? | Partner only wants trial cases |
| 7 | country_filter | where did the event happen? | Partner only wants cases from certain countries |
Plus one more for timing:
| # | Parameter | What it checks | Note |
|---|---|---|---|
| 8 | due_in_days | how many days after case receipt is the partner owed this | Every partner needs this, even always_send partners |
Why the same 7 parameters for everyone
If Partner A needs require_seriousness=true and Partner B needs require_causality=true, they're both just switches on the same form — you're not writing custom logic per partner, you're filling in a checklist. This means one config screen works for every partner you ever add, and one evaluator function checks all of them the same way.
Simple mental model
Each partner is just this checklist, filled in differently:
Partner rule =
always_send?
OR (seriousness ✓/✗)
AND (unexpectedness ✓/✗)
AND (criteria list)
AND (causality ✓/✗)
AND (case type list)
AND (country list)Turn a switch ON only if the partner actually requires it. Leave it OFF/empty if they don't care about that condition.
Expectedness is not a property of a reaction — it's a property of a reaction against a specific product's reference document (label / SmPC / IB). Nausea can be expected for Drug A and unexpected for Drug B. So it can never be a list baked into the code.
Store it as a per-product, versioned list of listed reactions (MedDRA PT codes). The engine's job is a lookup, not a judgement:
is_unexpected(reaction, product):
ref = load_reference_doc(product, as_of=case.receipt_date) # version live then
return reaction.pt_code not in ref.listed_pt_codesVersion by date, not latest
A case must be assessed against the reference doc that was active on its receipt date — not today's. If you always load the latest label, re-running an old case gives a different answer and your audit trail breaks. Load the version as-of the case date.
No. created_at is when the row was inserted; Day 0 is the calendar date anyone at the company first received the four minimum criteria (patient, reporter, suspect drug, event). Those can be days apart — a call-centre logs a case on Monday but only enters it Thursday. The clock started Monday.
So Day 0 is its own stored field (C.1.4), captured at intake, never derived from system timestamps. For a follow-up, Day 0 resets to the date of the new information (C.1.5).
due_date(destination, day0):
raw = day0 + days(destination.due_in_days) # 7 / 15 / 90
return next_working_day(raw, destination.calendar) # roll off weekend/holidayRun a separate clock per destination — FDA and EMA windows can differ for the same case — and drive amber/red escalation off days remaining, not a fixed status.
Diff the current case against the version last submitted to that authority — not against the previous DB edit. That's why you freeze a snapshot at every transmission: the comparison must be against what the regulator actually received.
Each field carries a rule for whether its change is significant. If any significant field moved, generate the follow-up; if not, it's a DB-only update.
is_significant(current, last_submitted):
for field, rule in significance_rules:
if rule.changed(current[field], last_submitted[field]):
return True, field # record WHICH field triggered it
return False, NoneDowngrades count too
Significance isn't only things got worse. Serious → non-serious, or unexpected → now-listed, are also significant and each generates a follow-up (with its own reason code). An engine that only watches for upgrades silently misses these. Store the triggering field either way — auditors ask both why you resubmitted and why you didn't.
Don't put submission state on the case row. A case has many outbound records, one per destination, each with its own report type, clock, status and reason. The worldwide case ID (C.1.1) stays constant across all of them; a per-transmission message counter steps up on each follow-up.
Case (id, worldwide_case_id, receipt_date, ...)
└── has many Submission (→ a regulatory agency)
└── has many Distribution (→ a partner / licensee)
Submission (case_id, authority, report_type,
due_date, msg_number, status, reason_code)This is what lets US + EEA serious + India become three independent rows with three clocks, and lets you answer what's overdue? with one query instead of walking the case.
Never flag authorities on the case (FDA? ✓). Instead give every country a home authority, resolve each product registration's country to its authority, then group by authority. Germany and France both resolve to EMA → that's one submission, not two.
resolve_authorities(case):
hits = [country_to_authority[r.country]
for r in case.active_registrations]
return dedupe(hits) # {EMA, FDA} not {DE, FR, US}Tie-break deterministically
When several registrations feed one authority, pick a stable winner (earliest registration date → lowest registration number → earliest created date). Without a fixed order the same case can produce different output on different runs — which fails replayability.
Model them as two separate output types, because they come from different obligations and are audited separately.
| Submission | Distribution | |
|---|---|---|
| Goes to | a regulatory agency | a partner / licensee / study site / MAH |
| Destination found by | country of registration → authority | case matches the partner's own list |
| Rules | strict regulatory logic | often looser (send everything serious) |
If the same destination is owed both ways, write two records — don't merge
One regulatory Submission and one partner Distribution to the same company are driven by different obligations and audited on separate trails. Merging them loses the why for one of them.
The difference between FDA, EMA and India is not different code — it's the same gates reading different settings. Hold every authority/partner profile as versioned data rows, and make the engine a pure matcher: it takes a case picture plus a profile version and returns decisions. Changing EMA's 90-day window becomes an edit, never a release.
Stamp the profile version on every decision
Same case + same profile version must always yield the same decisions (replayable). Persist which version produced each Submission so an auditor can reconstruct exactly why a report went out the way it did — even after the rules later changed.
Practical test: if adding a fourth authority tomorrow needs new code (not just new settings rows), the rules have leaked into the engine — pull them back out into data.
