National Phase Entry Date Logic: Implementation Guide for IP Docketing Systems
National phase entry date logic is the deterministic rule set that converts a single international Patent Cooperation Treaty (PCT) priority date into the correct, calendar-adjusted filing deadline for every designated national or regional office. This guide closes the gap between the treaty’s abstract “30 or 31 months from priority” language and the byte-level date value a docketing system must emit — where an off-by-one-day error or a silently missed weekend shift causes irreversible abandonment or forces a costly restoration petition.
The problem is narrow but unforgiving: the same priority date resolves to different deadlines in different offices, month arithmetic does not behave like day arithmetic, and the “corresponding date” rule, holiday closures, and timezone boundaries each introduce their own failure surface. This module treats that calculation as a pure, testable function anchored to the earliest valid priority date, sitting downstream of the broader Automated Deadline Calculation & Rule Engines framework and alongside the PCT 30/31 Month Deadline Calculators that share its rule matrix.
Compliance & Scope Boundaries
This engine computes statutory windows; it does not practice law and must never auto-file. Several hard boundaries are non-negotiable and belong in code review before anything ships:
- No autonomous filing or petition submission. Restoration of the right of priority under PCT Rule 26bis.3 and reinstatement under Rule 49ter.2 require substantive attorney judgment (the “due care” or “unintentional” standard). The engine may flag eligibility windows but must route them to a human. The related PCT National Phase Entry Rules framework defines where that human gate lives in the workflow.
- Computation is advisory, not authoritative. Every emitted date is a decision-support artifact. The controlling deadline is whatever the designated office recognizes; the engine’s output must always be traceable back to the exact statute, rule version, and calendar snapshot that produced it.
- Source-of-truth deference. The applicable month count is governed by the current WIPO PCT Contracting States table, not by hardcoded assumptions. A contracting state can file or withdraw a reservation that changes its window, so the rule data is treated as versioned input, never as a literal baked into logic.
- Data-access constraints. When the priority date or designated-office list is pulled from an upstream portal, respect that portal’s rate limits and terms — see WIPO PATENTSCOPE Integration for polling etiquette and Security & Access Control Boundaries for who may read or override a computed deadline.
Prerequisites & Dependency Map
The calculation function has a small, explicit dependency surface. Pin every item so a rule change is a reviewable diff rather than an ambient drift.
| Dependency | Minimum version | Role |
|---|---|---|
| Python | 3.11 | Native zoneinfo, datetime.UTC, structural pattern matching |
pydantic |
2.5 | Request/response validation and coercion |
python-dateutil |
2.8 | relativedelta calendar-month arithmetic |
tzdata |
2024.1+ | IANA zone database on platforms without a system copy |
Upstream data inputs that must be resolved before this function runs:
- Earliest valid priority date — the resolved anchor from the priority chain, already de-duplicated and stripped of withdrawn claims. Sourced from portal sync (see USPTO Data Schema Mapping and EPO Register Sync Architecture).
- Designated office list — ISO 3166-1 alpha-2 / regional codes (
EP,EA,AP,OA) drawn from the PCT request. - Jurisdiction rule file — a version-pinned mapping of office code → statutory months and shift policy, cited to WIPO/USPTO/EPO documentation.
- Holiday calendar dataset — per-office closure days, pinned to a release tag.
# npe_jurisdiction_rules.yaml
# Source of truth: WIPO PCT Contracting States table (re-verify each release).
# https://www.wipo.int/pct/en/pct_contracting_states.html
rule_version: "2026.07.0"
jurisdictions:
US: # 35 U.S.C. 371(b); 37 CFR 1.7 for weekend/holiday shift
base_months: 30
business_day_shift: "following"
restoration_available: true
EP: # PCT Art. 39(1)(b) as applied under Rule 159(1) EPC
base_months: 31
business_day_shift: "following"
restoration_available: true
JP: # PCT Art. 22(1); 30-month standard entry
base_months: 30
business_day_shift: "following"
restoration_available: false
CN: # PCT Art. 22(1); no reservation on file
base_months: 30
business_day_shift: "following"
restoration_available: true
Step-by-Step Implementation
The engine is a deterministic pipeline anchored to the priority date. Each step below is independently verifiable — run its snippet in isolation and assert the intermediate value before composing the whole.
Step 1 — Normalize the priority anchor to UTC
Docketing systems ingest dates in mixed representations (naive local, fixed-offset, or named-zone). Collapse them to a single UTC instant before any arithmetic so that later timezone conversion has one unambiguous origin.
from datetime import datetime, UTC
def normalize_anchor(raw: datetime) -> datetime:
"""Collapse any datetime to a UTC-aware instant.
A naive datetime is *assumed* to already be UTC (docketing convention);
aware datetimes are converted. This makes the anchor comparable and
prevents off-by-one-day drift when we later shift into office-local time.
"""
if raw.tzinfo is None:
return raw.replace(tzinfo=UTC)
return raw.astimezone(UTC)
# Verify: a naive value is tagged, an offset value is converted.
assert normalize_anchor(datetime(2023, 4, 15)).isoformat() == "2023-04-15T00:00:00+00:00"
Step 2 — Resolve the statutory window per office
Look each office up in the pinned rule file. Reject unknown codes loudly — a silent default is a malpractice vector. Most PCT contracting states use 30 months under Article 22(1); the EPO is the primary office at 31 months under Article 39(1)(b) as applied by Rule 159(1) EPC.
def resolve_window(office: str, rules: dict[str, dict]) -> int:
"""Return the statutory month count for a designated office.
Never fall back to a default: an unmapped office must halt the pipeline
so a human confirms the window against the current WIPO table.
"""
entry = rules.get(office)
if entry is None:
raise ValueError(f"Unmapped designated office: {office!r} — verify WIPO table")
return int(entry["base_months"])
Step 3 — Apply calendar-month arithmetic with the corresponding-date rule
Naive day-count addition (timedelta) is wrong for month windows. relativedelta implements the treaty’s “corresponding date” behavior: when the target month lacks the anchor day, it clamps to that month’s last valid day (e.g. an August 31 anchor + 6 months lands on the last day of February, not March 3).
from datetime import date
from dateutil.relativedelta import relativedelta
def add_statutory_months(anchor: date, months: int) -> date:
"""PCT-style month arithmetic with automatic month-end clamping."""
return anchor + relativedelta(months=months)
# Verify the clamp: 2023-08-31 + 6 months = 2024-02-29 (leap year), not March.
assert add_statutory_months(date(2023, 8, 31), 6) == date(2024, 2, 29)
Step 4 — Shift off weekends and office closures
A raw statutory date that lands on a Saturday, Sunday, or an office closure day rolls forward to the next business day (following policy). Both the USPTO (37 CFR 1.7) and the EPO (closures published in the Official Journal) apply this rule, so the shift must consult a per-office holiday set.
from datetime import date, timedelta
def shift_to_business_day(target: date, closures: frozenset[date]) -> tuple[date, bool]:
"""Roll forward to the next open day; return (date, was_adjusted)."""
adjusted = False
while target.weekday() >= 5 or target in closures: # 5=Sat, 6=Sun
target += timedelta(days=1)
adjusted = True
return target, adjusted
Step 5 — Emit the deadline with audit metadata
The output is never a bare date. It carries the applied month count, the shift flag, the rule version, and a hash that lets a compliance dashboard reconstruct exactly which inputs produced it — the same discipline enforced across the PCT 30/31 Month Deadline Calculators module.
import hashlib
def build_audit_hash(anchor: date, office: str, deadline: date, rule_version: str) -> str:
payload = f"{anchor.isoformat()}|{office}|{deadline.isoformat()}|{rule_version}"
return hashlib.sha256(payload.encode()).hexdigest()
API Contract & Schema
Docketing platforms consume this logic through a stateless, idempotent endpoint. Strict Pydantic v2 validation rejects malformed input before any arithmetic, and the idempotency key deduplicates retries during bulk portal synchronization.
import uuid
from datetime import datetime, date, UTC
from typing import Literal
from pydantic import BaseModel, Field, field_validator
from dateutil.relativedelta import relativedelta
class NPERequest(BaseModel):
pct_application_number: str = Field(pattern=r"^PCT/[A-Z]{2}\d{4}/\d{6}$")
priority_date: datetime
designated_offices: list[str] = Field(min_length=1, max_length=100)
restoration_requested: bool = False
grace_period_applied: bool = False
idempotency_key: str = Field(default_factory=lambda: str(uuid.uuid4()))
@field_validator("priority_date")
@classmethod
def to_utc(cls, v: datetime) -> datetime:
return v.replace(tzinfo=UTC) if v.tzinfo is None else v.astimezone(UTC)
@field_validator("designated_offices")
@classmethod
def upper_codes(cls, v: list[str]) -> list[str]:
if not all(2 <= len(c) <= 3 and c.isalpha() for c in v):
raise ValueError("Office codes must be 2–3 alphabetic characters")
return [c.upper() for c in v]
class JurisdictionalDeadline(BaseModel):
office_code: str
calculated_deadline: date
statutory_months: int
calendar_adjusted: bool
compliance_status: Literal["ACTIVE", "RESTORATION_PENDING", "REVIEW_REQUIRED"]
rule_version: str
audit_hash: str
def compute_deadlines(
req: NPERequest, rules: dict[str, dict], closures: dict[str, frozenset[date]]
) -> list[JurisdictionalDeadline]:
anchor = req.priority_date.date()
results: list[JurisdictionalDeadline] = []
for office in req.designated_offices:
months = resolve_window(office, rules)
raw = anchor + relativedelta(months=months)
deadline, adjusted = shift_to_business_day(raw, closures.get(office, frozenset()))
status = "REVIEW_REQUIRED" if (req.restoration_requested or req.grace_period_applied) else "ACTIVE"
results.append(JurisdictionalDeadline(
office_code=office,
calculated_deadline=deadline,
statutory_months=months,
calendar_adjusted=adjusted,
compliance_status=status,
rule_version=rules["rule_version"] if "rule_version" in rules else "unknown",
audit_hash=build_audit_hash(anchor, office, deadline, str(months)),
))
return results
The response is an array of per-office deadlines, each self-describing. A caller replaying the same idempotency_key receives the identical payload without re-triggering downstream reminder webhooks.
Edge Cases & Failure Modes
The happy path is trivial; the value of this module is in the failures it refuses to hide.
- Corresponding-date clamping. A February 29 anchor or a 31st-of-the-month anchor is where naive arithmetic silently produces the wrong month.
relativedeltaclamps correctly, but any hand-rolled fallback must be unit-tested against these specific inputs. - Holiday collision after the weekend shift. Rolling off a Sunday can land on a Monday that is itself a closure (e.g. a USPTO federal holiday). The shift loop must re-test the condition, not shift a single day — hence the
whilein Step 4. - Translation and fee windows decoupled from entry. Under Rule 159(1) EPC the entry act is due at 31 months, but translation and renewal-fee obligations follow separate schedules. Modeling a single date per office hides those secondary deadlines; emit them as distinct records rather than folding them into the entry date.
- Reservation drift / schema deprecation. A rule file that lags a WIPO reservation change produces confidently wrong output. Pin
rule_version, alert on any office whose file entry predates the latest WIPO table release, and fail closed on unmapped offices. - Timezone off-by-one. If the anchor is stored in the filing office’s local time but the system defaults to UTC, an entry recorded near midnight can shift a calendar day. Normalizing to UTC once (Step 1) and converting to office-local only at display time prevents this.
- Retry storms during bulk import. Wrap portal-sourced inputs in exponential backoff and a circuit breaker so a flaky upstream does not amplify into a cascade; the idempotency key guarantees replays are safe.
Verification & Regression Testing
Anchor the engine to known-good dates and run the suite on every rule-file change. These assertions are the contract:
from datetime import date, datetime, UTC
RULES = {
"rule_version": "2026.07.0",
"US": {"base_months": 30}, "EP": {"base_months": 31}, "JP": {"base_months": 30},
}
def test_us_vs_ep_window_diverges():
req = NPERequest(
pct_application_number="PCT/US2023/012345",
priority_date=datetime(2023, 4, 15, tzinfo=UTC),
designated_offices=["US", "EP"],
)
out = {d.office_code: d for d in compute_deadlines(req, RULES, {})}
assert out["US"].calculated_deadline == date(2025, 10, 15) # +30 months, a Wednesday → no shift
assert out["EP"].calculated_deadline == date(2025, 11, 17) # +31 months = Sat 2025-11-15 → shift to Monday
assert out["EP"].calendar_adjusted is True
def test_month_end_clamp():
req = NPERequest(
pct_application_number="PCT/US2021/099999",
priority_date=datetime(2021, 8, 31, tzinfo=UTC),
designated_offices=["EP"],
)
# 2021-08-31 + 31 months = 2024-03-31 (Sunday) → shift to 2024-04-01
assert compute_deadlines(req, RULES, {})[0].calculated_deadline == date(2024, 4, 1)
def test_unmapped_office_halts():
req = NPERequest(
pct_application_number="PCT/US2023/012345",
priority_date=datetime(2023, 4, 15, tzinfo=UTC),
designated_offices=["ZZ"],
)
try:
compute_deadlines(req, RULES, {})
assert False, "expected ValueError for unmapped office"
except ValueError:
pass
The US case demonstrates the weekend shift; the clamp case pins the corresponding-date rule; the unmapped-office case proves the engine fails closed rather than guessing.
Operational Action Summary
Operational Action: Before go-live, treat the jurisdiction rule file as code and gate it through peer review by patent counsel; pin rule_version and refuse any deadline whose rule entry predates the current WIPO Contracting States table release.
Operational Action: Log every computation — inputs, resolved window, shift flag, output, and audit hash — to append-only storage, and route every record with restoration_requested or grace_period_applied set to a paralegal review queue before emission.
Operational Action: Deploy the calculator as a stateless service with idempotency-key deduplication, exponential backoff on portal reads, and nightly regression tests against the known-date suite above to detect rule drift or holiday-dataset staleness.
Frequently Asked Questions
What happens if the 30-month PCT deadline falls on a weekend in the US?
following shift, and the calendar_adjusted flag records that the emitted date differs from the raw statutory date.
Why is the EPO window 31 months when most offices use 30?
base_months value rather than a global constant, so no single default is assumed.
Can the engine calculate a restoration or reinstatement deadline automatically?
REVIEW_REQUIRED and routed to a human.
How do I avoid off-by-one-day errors from timezones?
What should happen when a designated office is not in the rule file?
For authoritative statutory references, practitioners should consult the WIPO PCT Treaty Articles, the USPTO MPEP § 1893.03, and the WIPO PCT Contracting States table for the current per-office windows. Python implementations should rely on dateutil.relativedelta and the standard-library zoneinfo module for calendar-aware, timezone-correct arithmetic. Practitioners moving between offices will also want the sibling How to Map PCT Rule 47 Deadlines to National Phase walkthrough.