First implementation API for PIP (Proyecto: Pharmacy Integration Platform)
This commit is contained in:
@@ -0,0 +1,210 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
from uuid import UUID
|
||||
|
||||
from src.core.exceptions import ConflictException, NotFoundException, ValidationException
|
||||
from src.core.logging import get_logger
|
||||
from src.domain.entities import (
|
||||
InventoryHistoryEntity,
|
||||
InventoryRecordEntity,
|
||||
ReservationEntity,
|
||||
ReservationStatus,
|
||||
)
|
||||
from src.domain.interfaces import IInventoryRepository, IReservationRepository
|
||||
from src.infrastructure.messaging.rabbitmq import rabbitmq_client
|
||||
|
||||
logger = get_logger("inventory_service")
|
||||
|
||||
RESERVATION_DEFAULT_TTL_MINUTES = 30
|
||||
|
||||
|
||||
class InventoryService:
|
||||
def __init__(
|
||||
self,
|
||||
inventory_repo: IInventoryRepository,
|
||||
reservation_repo: IReservationRepository,
|
||||
) -> None:
|
||||
self._inventory_repo = inventory_repo
|
||||
self._reservation_repo = reservation_repo
|
||||
|
||||
async def get_inventory_record(self, pharmacy_id: UUID, medication_id: UUID) -> InventoryRecordEntity | None:
|
||||
return await self._inventory_repo.get_record(pharmacy_id, medication_id)
|
||||
|
||||
async def list_inventory(self, pharmacy_id: UUID, *, offset: int = 0, limit: int = 50) -> tuple[list[InventoryRecordEntity], int]:
|
||||
return await self._inventory_repo.list_by_pharmacy(pharmacy_id, offset=offset, limit=limit)
|
||||
|
||||
async def upsert_inventory_record(self, entity: InventoryRecordEntity) -> InventoryRecordEntity:
|
||||
existing = await self._inventory_repo.get_record(entity.pharmacy_id, entity.medication_id)
|
||||
previous_stock = existing.stock if existing else 0
|
||||
|
||||
upserted = await self._inventory_repo.upsert(entity)
|
||||
|
||||
delta = entity.stock - previous_stock
|
||||
if delta != 0:
|
||||
history = InventoryHistoryEntity(
|
||||
inventory_record_id=upserted.id,
|
||||
pharmacy_id=entity.pharmacy_id,
|
||||
medication_id=entity.medication_id,
|
||||
previous_stock=previous_stock,
|
||||
new_stock=entity.stock,
|
||||
delta=delta,
|
||||
reason=entity.source or "upsert",
|
||||
source=entity.source,
|
||||
)
|
||||
await self._inventory_repo.add_history(history)
|
||||
|
||||
await self._publish_event("inventory.updated", upserted.id, {
|
||||
"pharmacy_id": str(entity.pharmacy_id),
|
||||
"medication_id": str(entity.medication_id),
|
||||
"previous_stock": previous_stock,
|
||||
"new_stock": entity.stock,
|
||||
"delta": delta,
|
||||
})
|
||||
return upserted
|
||||
|
||||
async def get_inventory_history(self, pharmacy_id: UUID, medication_id: UUID, *, offset: int = 0, limit: int = 50) -> list[InventoryHistoryEntity]:
|
||||
records = await self._inventory_repo.get_history(pharmacy_id, medication_id, offset=offset, limit=limit)
|
||||
return list(records)
|
||||
|
||||
async def create_reservation(
|
||||
self,
|
||||
pharmacy_id: UUID,
|
||||
medication_id: UUID,
|
||||
quantity: int,
|
||||
*,
|
||||
user_id: UUID | None = None,
|
||||
external_reference: str | None = None,
|
||||
idempotency_key: str | None = None,
|
||||
ttl_minutes: int = RESERVATION_DEFAULT_TTL_MINUTES,
|
||||
) -> ReservationEntity:
|
||||
if idempotency_key:
|
||||
existing = await self._reservation_repo.get_by_idempotency_key(idempotency_key)
|
||||
if existing:
|
||||
return existing
|
||||
|
||||
record = await self._inventory_repo.get_record(pharmacy_id, medication_id)
|
||||
if not record:
|
||||
raise NotFoundException("InventoryRecord", f"{pharmacy_id}/{medication_id}")
|
||||
|
||||
if record.available_stock < quantity:
|
||||
raise ValidationException(
|
||||
f"Insufficient available stock. Requested: {quantity}, Available: {record.available_stock}",
|
||||
details={"available_stock": record.available_stock, "requested": quantity},
|
||||
)
|
||||
|
||||
record.reserved_stock += quantity
|
||||
record.available_stock = record.stock - record.reserved_stock
|
||||
await self._inventory_repo.upsert(record)
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
entity = ReservationEntity(
|
||||
pharmacy_id=pharmacy_id,
|
||||
medication_id=medication_id,
|
||||
user_id=user_id,
|
||||
external_reference=external_reference,
|
||||
quantity=quantity,
|
||||
status=ReservationStatus.PENDING,
|
||||
expires_at=now + __import__("datetime").timedelta(minutes=ttl_minutes),
|
||||
idempotency_key=idempotency_key,
|
||||
)
|
||||
created = await self._reservation_repo.create(entity)
|
||||
|
||||
await self._publish_event("reservation.created", created.id, {
|
||||
"pharmacy_id": str(pharmacy_id),
|
||||
"medication_id": str(medication_id),
|
||||
"quantity": quantity,
|
||||
})
|
||||
return created
|
||||
|
||||
async def confirm_reservation(self, reservation_id: UUID) -> ReservationEntity:
|
||||
entity = await self._reservation_repo.get_by_id(reservation_id)
|
||||
if not entity:
|
||||
raise NotFoundException("Reservation", str(reservation_id))
|
||||
|
||||
if entity.status != ReservationStatus.PENDING:
|
||||
raise ValidationException(f"Reservation cannot be confirmed. Current status: {entity.status}")
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
if entity.expires_at and entity.expires_at < now:
|
||||
entity.status = ReservationStatus.EXPIRED
|
||||
await self._reservation_repo.update(entity)
|
||||
await self._release_reserved_stock(entity)
|
||||
await self._publish_event("reservation.expired", entity.id, {"pharmacy_id": str(entity.pharmacy_id)})
|
||||
raise ValidationException("Reservation has expired")
|
||||
|
||||
entity.status = ReservationStatus.CONFIRMED
|
||||
entity.confirmed_at = now
|
||||
updated = await self._reservation_repo.update(entity)
|
||||
|
||||
await self._publish_event("reservation.confirmed", updated.id, {
|
||||
"pharmacy_id": str(entity.pharmacy_id),
|
||||
"medication_id": str(entity.medication_id),
|
||||
"quantity": entity.quantity,
|
||||
})
|
||||
return updated
|
||||
|
||||
async def cancel_reservation(self, reservation_id: UUID) -> ReservationEntity:
|
||||
entity = await self._reservation_repo.get_by_id(reservation_id)
|
||||
if not entity:
|
||||
raise NotFoundException("Reservation", str(reservation_id))
|
||||
|
||||
if entity.status not in (ReservationStatus.PENDING, ReservationStatus.CONFIRMED):
|
||||
raise ValidationException(f"Reservation cannot be cancelled. Current status: {entity.status}")
|
||||
|
||||
entity.status = ReservationStatus.CANCELLED
|
||||
entity.cancelled_at = datetime.now(timezone.utc)
|
||||
updated = await self._reservation_repo.update(entity)
|
||||
|
||||
await self._release_reserved_stock(entity)
|
||||
|
||||
await self._publish_event("reservation.cancelled", updated.id, {
|
||||
"pharmacy_id": str(entity.pharmacy_id),
|
||||
"medication_id": str(entity.medication_id),
|
||||
"quantity": entity.quantity,
|
||||
})
|
||||
return updated
|
||||
|
||||
async def get_reservation(self, reservation_id: UUID) -> ReservationEntity | None:
|
||||
return await self._reservation_repo.get_by_id(reservation_id)
|
||||
|
||||
async def list_reservations(self, pharmacy_id: UUID, *, status: str | None = None, offset: int = 0, limit: int = 50) -> tuple[list[ReservationEntity], int]:
|
||||
return await self._reservation_repo.list_by_pharmacy(pharmacy_id, status=status, offset=offset, limit=limit)
|
||||
|
||||
async def expire_pending_reservations(self) -> int:
|
||||
pass
|
||||
|
||||
async def _release_reserved_stock(self, reservation: ReservationEntity) -> None:
|
||||
record = await self._inventory_repo.get_record(reservation.pharmacy_id, reservation.medication_id)
|
||||
if not record:
|
||||
return
|
||||
|
||||
record.reserved_stock = max(0, record.reserved_stock - reservation.quantity)
|
||||
record.available_stock = record.stock - record.reserved_stock
|
||||
updated = await self._inventory_repo.upsert(record)
|
||||
|
||||
history = InventoryHistoryEntity(
|
||||
inventory_record_id=updated.id,
|
||||
pharmacy_id=reservation.pharmacy_id,
|
||||
medication_id=reservation.medication_id,
|
||||
previous_stock=record.stock + reservation.quantity if reservation.status == ReservationStatus.PENDING else record.stock,
|
||||
new_stock=record.stock,
|
||||
delta=-reservation.quantity,
|
||||
reason=f"reservation_{reservation.status}",
|
||||
source="inventory_service",
|
||||
)
|
||||
await self._inventory_repo.add_history(history)
|
||||
|
||||
async def _publish_event(self, event_type: str, resource_id: UUID, payload: dict[str, Any]) -> None:
|
||||
try:
|
||||
routing_key = event_type.replace(".", "_")
|
||||
full_payload = {
|
||||
"event_type": event_type,
|
||||
"resource_id": str(resource_id),
|
||||
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||
**payload,
|
||||
}
|
||||
await rabbitmq_client.publish(routing_key, full_payload)
|
||||
except Exception as exc:
|
||||
logger.warning("inventory_event_publish_failed", extra={"event_type": event_type, "error": str(exc)})
|
||||
Reference in New Issue
Block a user