# 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.md` provides instructions for external Prometheus/Grafana integration - `docker-compose.runtime.yml` is the lean compose file for local dev - Connector Registry uses singleton pattern with lazy loading via `importlib` for builtin connector types - SDK architecture: `SDKConnector` base class composes auth handler + retry policy + circuit breaker + httpx client; concrete connectors extend it - `_BUILTIN_CONNECTORS` registry maps ERPType values to `module.Class` strings 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 `passlib` with direct `bcrypt` library due to Python 3.13 incompatibility - Connector events published to RabbitMQ with `connector.{event}` routing keys via `ConnectorService._publish_event()` - `test_connection` sets connector status to ACTIVE on success, ERROR on failure; `deploy` refuses 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 - `InventoryHistoryEntity` has `inventory_record_id` field — must be set from service when creating history records - Reservation `expires_at` uses `timedelta` (not timestamp arithmetic — float type error with Python 3.13 datetime) ## Critical Bugs Fixed (this session) 1. `inventory_repository.py:add_history()` was using `entity.id` (None) instead of `entity.inventory_record_id` for the FK 2. `inventory_service.py:upsert_inventory_record()` wasn't passing `inventory_record_id=upserted.id` to `InventoryHistoryEntity` 3. `inventory_service.py:_release_reserved_stock()` wasn't passing `inventory_record_id` either 4. `inventory_service.py:create_reservation()` used `now.__class__(now.timestamp() + ttl_minutes * 60, tz=timezone.utc)` which fails with TypeError on float — replaced with `now + 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_all` with Alembic migrations - Add more connector implementations (NixFara, Unycop, CSV, FTP, SQL) - Enhance test coverage - Set up CI/CD pipeline ## Critical Context - `bcrypt` library used directly (not via passlib) — must keep `bcrypt>=4.2.0` in dependencies - OTel collector reference in docker-compose points to `otel-collector:4317` which doesn't exist in runtime stack — harmless warning logs - System router has `/system` prefix; 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.py` swallows 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}/inventory` for 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 orphaned `pip-platform/monitoring/otel-collector-config.yaml` has been deleted, and `pip-platform/docker-compose.runtime.yml` was previously pointing at a non-existent `otel-collector:4317` — fixed to `host.docker.localhost:4317` to route through Grafana Alloy. See PLAN.md for the external-integration rationale. - **`pip-platform/monitoring/prometheus/prometheus.yml` is 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 an `envsubst` entrypoint on the remote. `OTel→Alloy` swap is local-canonical; srv84-macos merge still pending (see PLAN.md). ## Relevant Files ### Domain - `pip-platform/src/domain/entities/entities.py` — All domain entities (including `inventory_record_id` on `InventoryHistoryEntity`) - `pip-platform/src/domain/entities/enums.py` — All enums ### Repositories - `pip-platform/src/infrastructure/database/repositories/medication_repository.py` — Medication CRUD + search - `pip-platform/src/infrastructure/database/repositories/medication_catalog_repository.py` — Per-pharmacy catalog upsert - `pip-platform/src/infrastructure/database/repositories/inventory_repository.py` — Inventory upsert + history - `pip-platform/src/infrastructure/database/repositories/reservation_repository.py` — Reservation CRUD + idempotency - `pip-platform/src/infrastructure/database/repositories/__init__.py` — All repo exports ### Services - `pip-platform/src/services/medication/medication_service.py` — Medication + catalog business logic + events - `pip-platform/src/services/inventory/inventory_service.py` — Inventory + reservation business logic (stock reservation/release, confirm, cancel, idempotency) + events - `pip-platform/src/services/connector_service.py` — Connector lifecycle + event publishing ### API Endpoints - `pip-platform/src/api/v1/endpoints/medications.py` — Medication CRUD + search - `pip-platform/src/api/v1/endpoints/catalog.py` — Per-pharmacy catalog list/get/upsert - `pip-platform/src/api/v1/endpoints/inventory.py` — Per-pharmacy inventory list/get/upsert + history - `pip-platform/src/api/v1/endpoints/reservations.py` — Per-pharmacy reservation create/confirm/cancel/list/get - `pip-platform/src/api/v1/endpoints/connectors.py` — Connector CRUD + test/deploy/deactivate/configs - `pip-platform/src/api/v1/endpoints/erp_systems.py` — ERP System CRUD - `pip-platform/src/api/v1/router.py` — All routers wired ### Infrastructure - `pip-platform/src/infrastructure/messaging/rabbitmq.py` — RabbitMQ client with routing keys - `pip-platform/src/connectors/registry.py` — ConnectorRegistry singleton - `pip-platform/src/connectors/sdk/base_sdk.py` — SDKConnector base class - `pip-platform/src/services/auth/security.py` — Fixed bcrypt usage (no passlib) ### Docker - `pip-platform/docker-compose.runtime.yml` — Lean runtime stack