Skip to content

feat(domains): Phase 2 — transfers, nameservers, lock, info, bulk availability, domain-sync (#93)#170

Open
b3lz3but wants to merge 5 commits intocaptainpragmatic:masterfrom
b3lz3but:feat/domain-gateway-phase2
Open

feat(domains): Phase 2 — transfers, nameservers, lock, info, bulk availability, domain-sync (#93)#170
b3lz3but wants to merge 5 commits intocaptainpragmatic:masterfrom
b3lz3but:feat/domain-gateway-phase2

Conversation

@b3lz3but
Copy link
Copy Markdown
Contributor

@b3lz3but b3lz3but commented Apr 6, 2026

Summary

Phase 2 (Best tier) of the domain registrar gateway implementation from #93. Builds on PR #169 (Phase 1).

DomainOperation model

  • Async operation tracking mirroring ProvisioningTask pattern
  • Types: transfer_in, transfer_out, nameserver_update, lock_update, whois_update, domain_info
  • State lifecycle: pendingsubmittedcompleted / failed / retrying
  • Retry logic with max_retries and next_retry_at
  • Transition methods: mark_submitted(), mark_completed(), mark_failed()
  • Field named state (not status) to comply with FSM guardrail (ADR-0034)

Gateway extensions (BaseRegistrarGateway)

  • initiate_transfer() — domain transfer with idempotency
  • get_domain_info() — pull current state from registrar
  • update_nameservers() — with audit logging
  • set_lock() — lock/unlock domain at registrar
  • check_availability_bulk() — batch check with sequential fallback (overridable for native batch APIs)
  • All Phase 2 methods are non-abstract with NotImplementedError defaults — existing gateways work without changes until they opt in

Concrete implementations

Both GandiGateway and ROTLDGateway implement all Phase 2 operations:

  • Transfer initiation (Gandi /transferin, ROTLD /domain/transfer)
  • Domain info retrieval
  • Nameserver updates
  • Lock/unlock

DomainLifecycleService Phase 2

  • initiate_transfer() — two-phase: create Domain + DomainOperation records, then submit to registrar
  • update_nameservers() — update at registrar, sync local record
  • set_domain_lock() — with DomainOperation tracking
  • sync_domain_info() — pull registrar state, update all local domain fields

Management command

  • domain_sync — pull domain status from registrar APIs, detect drift
  • Usage: manage.py domain_sync [--domain example.com] [--registrar gandi] [--dry-run]

Depends on

Test plan

  • 15 new Phase 2 tests in test_gateway_phase2.py — all passing
    • DomainOperation model: can_retry, duration_seconds, state transitions
    • Gandi: transfer (success + auth failure), domain info, nameserver update, lock
    • ROTLD: transfer, domain info
    • Bulk availability: sequential fallback, graceful failure handling
    • Lifecycle service: sync_domain_info updates local record from registrar
  • 25 Phase 1 tests still pass (40 total)
  • Ruff lint + format clean
  • All pre-commit hooks pass (including FSM guardrail)
  • DCO signed

Diff stats

  • 3 new files: DomainOperation model + migration, domain_sync command, Phase 2 tests
  • 5 modified files: base.py, gandi.py, rotld.py, services.py, models.py, __init__.py

Closes #93

🤖 Generated with Claude Code

b3lz3but and others added 5 commits April 2, 2026 17:02
…tainpragmatic#121)

Add retriable: bool = False field to the Err dataclass so callers (e.g.
Django-Q tasks) can distinguish transient errors (DB timeout, lock
contention) from permanent ones (validation failure). Default False
preserves backward compatibility with all 577 existing Err() call sites.

Closes captainpragmatic#121

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Ciprian Radulescu <craps2003@gmail.com>
…aptainpragmatic#93)

Replace stubbed DomainRegistrarGateway with a production-ready gateway
abstraction following the billing gateway ABC + cloud gateway Result[T, E]
patterns. Implements the "Better" tier from issue captainpragmatic#93.

New gateway layer (apps/domains/gateways/):
- BaseRegistrarGateway ABC with circuit breaker, idempotency, retry,
  audit logging, SSRF-safe HTTP via OutboundPolicy, and shared error mapping
- GandiGateway for international domains (.com, .net, .org, .eu)
- ROTLDGateway for Romanian .ro domains with CUI/CNP registrant mapping
- RegistrarErrorCode enum + typed exception hierarchy
- RegistrarGatewayFactory following PaymentGatewayFactory pattern

Service layer migration:
- DomainLifecycleService methods now return Result[T, str] instead of
  tuple[bool, T | str], with all callers updated (views, order service, tests)
- DomainRegistrarGateway in services.py is now a backward-compatible facade
  delegating to the gateway factory

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Ciprian Radulescu <craps2003@gmail.com>
…ilability, domain-sync (captainpragmatic#93)

Best tier additions to the domain registrar gateway layer:

DomainOperation model:
- Async operation tracking mirroring ProvisioningTask pattern
- Types: transfer_in/out, nameserver_update, lock_update, whois_update, domain_info
- State lifecycle: pending → submitted → completed/failed/retrying
- Field named 'state' (not 'status') to avoid FSM guardrail false positives
- Transition methods: mark_submitted(), mark_completed(), mark_failed()

Gateway extensions (BaseRegistrarGateway):
- initiate_transfer() with idempotency
- get_domain_info() — pull current state from registrar
- update_nameservers() with audit logging
- set_lock() — lock/unlock domain at registrar
- check_availability_bulk() — batch check with sequential fallback

Concrete implementations in GandiGateway and ROTLDGateway for all Phase 2 ops.

DomainLifecycleService Phase 2:
- initiate_transfer() — two-phase: create Domain+DomainOperation, submit
- update_nameservers() — registrar + local record sync
- set_domain_lock() — with DomainOperation tracking
- sync_domain_info() — pull registrar state, update local fields

Management command:
- domain_sync — sync active domains, detect drift
  Usage: manage.py domain_sync [--domain X] [--registrar Y] [--dry-run]

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Ciprian Radulescu <craps2003@gmail.com>
DomainOperation is an operational task tracker (like ProvisioningTask)
already audited via BaseRegistrarGateway._audit_api_call(). Adding to
the allowlist satisfies the audit model coverage regression test.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Ciprian Radulescu <craps2003@gmail.com>
…et_lock

Annotate body as dict[str, Any] to avoid type narrowing conflict between
the two branches (dict[str, None] vs dict[str, list[str]]).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Ciprian Radulescu <craps2003@gmail.com>
@b3lz3but
Copy link
Copy Markdown
Contributor Author

@mostlyvirtual ready for review 🙏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement domain registrar API integrations

1 participant