8.5 KiB
Pharmacy Integration Platform (PIP) — State Log
Goal
Build a Python/FastAPI backend that abstracts pharmacy ERPs behind a unified REST API. Currently implementing Phase 3 (Inventory & Medication APIs).
Constraints & Preferences
- Backend only, no UI/mobile. Python 3.13, FastAPI, SQLAlchemy async, Alembic, PostgreSQL, Redis, RabbitMQ.
- Clean Architecture, SOLID, Repository Pattern, DI, DDD where valuable, Event Driven, CQRS where needed.
- Versioned API at
/api/v1/. JWT+OAuth2 auth. RBAC with role hierarchy and scopes. API Keys for third parties. - No code secrets. Structured JSON logs. OpenTelemetry, Prometheus, Grafana.
- Docker Compose with health checks; Kubernetes-migratable.
- Monitoring (Prometheus, Grafana, OTEL Collector) omitted from Docker stack; remote monitoring stack exists. PLAN.md provided for external integration.
- Incremental phases — wait for validation before advancing.
Progress
Done
- Phase 1 complete: full project structure, domain entities, ORM models, repositories, auth, security, API endpoints, middleware, Docker, CLI, Alembic, tests scaffolding
- Phase 2 complete: Connector Framework + SDK + Concrete Connectors + API Endpoints + Event Publishers + Tests
- Phase 3 (medication/inventory): Medication & Inventory API endpoints — all E2E validated:
- Medications: CRUD, search by name/ingredient/nregistro/atc, nregistro lookup, duplicate 409
- Catalog: per-pharmacy upsert, list, get
- Inventory: per-pharmacy upsert with history tracking, list, get, history endpoint
- Reservations: create, idempotency key, list, get, confirm, cancel, insufficient stock 422
In Progress
- Phase 3 remaining items: Sync Job management, Audit logging integration, API Key auth flow, Rate limiting
Blocked
- (none)
Key Decisions
- Monitoring removed from Docker stack;
PLAN.mdprovides instructions for external Prometheus/Grafana integration docker-compose.runtime.ymlis the lean compose file for local dev- Connector Registry uses singleton pattern with lazy loading via
importlibfor builtin connector types - SDK architecture:
SDKConnectorbase class composes auth handler + retry policy + circuit breaker + httpx client; concrete connectors extend it _BUILTIN_CONNECTORSregistry maps ERPType values tomodule.Classstrings for dynamic import- Circuit breaker: 5 failures → open → 30s recovery → half_open (1 probe call) → close on success / reopen on failure
- OAuth2 handler fetches client_credentials tokens with 30s expiry buffer
- Replaced
passlibwith directbcryptlibrary due to Python 3.13 incompatibility - Connector events published to RabbitMQ with
connector.{event}routing keys viaConnectorService._publish_event() test_connectionsets connector status to ACTIVE on success, ERROR on failure;deployrefuses if already active- Catalog entries are per-pharmacy, keyed by
(pharmacy_id, medication_id)— upsert pattern - Inventory records use same composite key pattern as catalog — upsert with history tracking
- Reservations support idempotency via
idempotency_key, auto-expire, stock reservation/release on confirm/cancel - Event publishing uses
event_type.replace(".", "_")to map dotted event types to RabbitMQ routing keys - Auth token endpoint expects JSON body (
LoginRequest), not OAuth2 form data InventoryHistoryEntityhasinventory_record_idfield — must be set from service when creating history records- Reservation
expires_atusestimedelta(not timestamp arithmetic — float type error with Python 3.13 datetime)
Critical Bugs Fixed (this session)
inventory_repository.py:add_history()was usingentity.id(None) instead ofentity.inventory_record_idfor the FKinventory_service.py:upsert_inventory_record()wasn't passinginventory_record_id=upserted.idtoInventoryHistoryEntityinventory_service.py:_release_reserved_stock()wasn't passinginventory_record_ideitherinventory_service.py:create_reservation()usednow.__class__(now.timestamp() + ttl_minutes * 60, tz=timezone.utc)which fails with TypeError on float — replaced withnow + timedelta(minutes=ttl_minutes)
Next Steps
- Implement Sync Job management (create, list, get, trigger, cancel sync jobs)
- Add Audit logging integration
- Implement API Key auth flow
- Add Rate limiting
- Replace
Base.metadata.create_allwith Alembic migrations - Add more connector implementations (NixFara, Unycop, CSV, FTP, SQL)
- Enhance test coverage
- Set up CI/CD pipeline
Critical Context
-
bcryptlibrary used directly (not via passlib) — must keepbcrypt>=4.2.0in dependencies -
OTel collector reference in docker-compose points to
otel-collector:4317which doesn't exist in runtime stack — harmless warning logs -
System router has
/systemprefix; health at/api/v1/system/health -
Auth: register at
/api/v1/auth/register, token at/api/v1/auth/token(JSON body, not form data) -
Generic exception handler in
main.pyswallows all errors as{"error": {"code": "INTERNAL_ERROR", "message": "Internal server error"}}— no traceback in response; check container logs or test repo/service directly for debugging -
Inventory endpoint uses
PUT /pharmacies/{pharmacy_id}/inventoryfor upsert -
Reservation endpoints:
POST .../reservations(create),POST .../reservations/{id}/confirm,POST .../reservations/{id}/cancel## Recent changes (OTel migration in progress) -
Observability now lives on srv84-macos. The in-stack otel-collector / prometheus / grafana services have been stripped from
pip-platform/docker-compose.yml, the orphanedpip-platform/monitoring/otel-collector-config.yamlhas been deleted, andpip-platform/docker-compose.runtime.ymlwas previously pointing at a non-existentotel-collector:4317— fixed tohost.docker.localhost:4317to route through Grafana Alloy. See PLAN.md for the external-integration rationale. -
pip-platform/monitoring/prometheus/prometheus.ymlis now documentation-only — kept as a reference; the active scraper is the Prometheus on srv84-macos. -
Monitoring stack (~/Projects/Monitoring/): Telegram bot token was externalized from
alertmanager/config.yml(was plaintext literal) to${TG_BOT_TOKEN}resolved by anenvsubstentrypoint on the remote.OTel→Alloyswap is local-canonical; srv84-macos merge still pending (see PLAN.md).
Relevant Files
Domain
pip-platform/src/domain/entities/entities.py— All domain entities (includinginventory_record_idonInventoryHistoryEntity)pip-platform/src/domain/entities/enums.py— All enums
Repositories
pip-platform/src/infrastructure/database/repositories/medication_repository.py— Medication CRUD + searchpip-platform/src/infrastructure/database/repositories/medication_catalog_repository.py— Per-pharmacy catalog upsertpip-platform/src/infrastructure/database/repositories/inventory_repository.py— Inventory upsert + historypip-platform/src/infrastructure/database/repositories/reservation_repository.py— Reservation CRUD + idempotencypip-platform/src/infrastructure/database/repositories/__init__.py— All repo exports
Services
pip-platform/src/services/medication/medication_service.py— Medication + catalog business logic + eventspip-platform/src/services/inventory/inventory_service.py— Inventory + reservation business logic (stock reservation/release, confirm, cancel, idempotency) + eventspip-platform/src/services/connector_service.py— Connector lifecycle + event publishing
API Endpoints
pip-platform/src/api/v1/endpoints/medications.py— Medication CRUD + searchpip-platform/src/api/v1/endpoints/catalog.py— Per-pharmacy catalog list/get/upsertpip-platform/src/api/v1/endpoints/inventory.py— Per-pharmacy inventory list/get/upsert + historypip-platform/src/api/v1/endpoints/reservations.py— Per-pharmacy reservation create/confirm/cancel/list/getpip-platform/src/api/v1/endpoints/connectors.py— Connector CRUD + test/deploy/deactivate/configspip-platform/src/api/v1/endpoints/erp_systems.py— ERP System CRUDpip-platform/src/api/v1/router.py— All routers wired
Infrastructure
pip-platform/src/infrastructure/messaging/rabbitmq.py— RabbitMQ client with routing keyspip-platform/src/connectors/registry.py— ConnectorRegistry singletonpip-platform/src/connectors/sdk/base_sdk.py— SDKConnector base classpip-platform/src/services/auth/security.py— Fixed bcrypt usage (no passlib)
Docker
pip-platform/docker-compose.runtime.yml— Lean runtime stack