116 lines
8.5 KiB
Markdown
116 lines
8.5 KiB
Markdown
# 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
|