Restructure with Turborepo
Run Tests on Branches / Backend Tests (push) Successful in 3m38s
Run Tests on Branches / Frontend Tests (push) Successful in 3m28s

This commit is contained in:
Antoni Nuñez Romeu
2026-07-06 15:51:53 +02:00
parent f66cafbbc3
commit 190b3d163d
277 changed files with 53253 additions and 0 deletions
@@ -0,0 +1,9 @@
from .models import *
from .session import async_session_factory, engine, get_db_session, get_db_session_no_commit
__all__ = [
"async_session_factory",
"engine",
"get_db_session",
"get_db_session_no_commit",
]
@@ -0,0 +1,41 @@
from .models import (
APIClientModel,
APIKeyModel,
AuditLogModel,
Base,
BaseMixin,
ConnectorConfigModel,
ConnectorModel,
ConnectorVersionModel,
ERPSystemModel,
InventoryHistoryModel,
InventoryRecordModel,
MedicationCatalogEntryModel,
MedicationModel,
PharmacyModel,
ReservationModel,
SyncJobModel,
SyncLogModel,
TechnicalUserModel,
)
__all__ = [
"APIClientModel",
"APIKeyModel",
"AuditLogModel",
"Base",
"BaseMixin",
"ConnectorConfigModel",
"ConnectorModel",
"ConnectorVersionModel",
"ERPSystemModel",
"InventoryHistoryModel",
"InventoryRecordModel",
"MedicationCatalogEntryModel",
"MedicationModel",
"PharmacyModel",
"ReservationModel",
"SyncJobModel",
"SyncLogModel",
"TechnicalUserModel",
]
@@ -0,0 +1,349 @@
import uuid
from datetime import datetime
from sqlalchemy import (
Boolean,
DateTime,
Enum,
Float,
ForeignKey,
Index,
Integer,
LargeBinary,
String,
Text,
UniqueConstraint,
func,
)
from sqlalchemy.dialects.postgresql import JSONB, UUID
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
class Base(DeclarativeBase):
pass
class BaseMixin:
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False)
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False)
class PharmacyModel(Base, BaseMixin):
__tablename__ = "pharmacies"
code: Mapped[str] = mapped_column(String(50), unique=True, nullable=False, index=True)
name: Mapped[str] = mapped_column(String(255), nullable=False)
address: Mapped[str | None] = mapped_column(String(500))
city: Mapped[str | None] = mapped_column(String(100))
province: Mapped[str | None] = mapped_column(String(100))
postal_code: Mapped[str | None] = mapped_column(String(20))
country: Mapped[str] = mapped_column(String(2), default="ES")
latitude: Mapped[float | None] = mapped_column(Float)
longitude: Mapped[float | None] = mapped_column(Float)
phone: Mapped[str | None] = mapped_column(String(50))
email: Mapped[str | None] = mapped_column(String(255))
opening_hours: Mapped[dict | None] = mapped_column(JSONB)
status: Mapped[str] = mapped_column(String(20), default="pending", index=True)
erp_type: Mapped[str | None] = mapped_column(String(50))
erp_version: Mapped[str | None] = mapped_column(String(50))
connector_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("connectors.id"), nullable=True)
sync_strategy: Mapped[str] = mapped_column(String(20), default="incremental")
sync_interval_minutes: Mapped[int] = mapped_column(Integer, default=30)
last_sync_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
last_sync_status: Mapped[str | None] = mapped_column(String(20))
metadata_: Mapped[dict | None] = mapped_column("metadata", JSONB)
connector = relationship("ConnectorModel", back_populates="pharmacies", foreign_keys=[connector_id])
inventory_records = relationship("InventoryRecordModel", back_populates="pharmacy", cascade="all, delete-orphan")
reservations = relationship("ReservationModel", back_populates="pharmacy", cascade="all, delete-orphan")
catalog_entries = relationship("MedicationCatalogEntryModel", back_populates="pharmacy", cascade="all, delete-orphan")
sync_jobs = relationship("SyncJobModel", back_populates="pharmacy", cascade="all, delete-orphan")
__table_args__ = (
Index("ix_pharmacies_location", "latitude", "longitude"),
Index("ix_pharmacies_status_erp", "status", "erp_type"),
)
class ERPSystemModel(Base, BaseMixin):
__tablename__ = "erp_systems"
name: Mapped[str] = mapped_column(String(255), nullable=False)
erp_type: Mapped[str] = mapped_column(String(50), nullable=False, index=True)
version: Mapped[str | None] = mapped_column(String(50))
base_url: Mapped[str | None] = mapped_column(String(500))
auth_config: Mapped[dict | None] = mapped_column(JSONB)
status: Mapped[str] = mapped_column(String(20), default="pending", index=True)
metadata_: Mapped[dict | None] = mapped_column("metadata", JSONB)
connectors = relationship("ConnectorModel", back_populates="erp_system", cascade="all, delete-orphan")
class ConnectorModel(Base, BaseMixin):
__tablename__ = "connectors"
erp_system_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("erp_systems.id"), nullable=False, index=True)
pharmacy_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("pharmacies.id"), nullable=False, index=True)
name: Mapped[str] = mapped_column(String(255), nullable=False)
connector_type: Mapped[str] = mapped_column(String(50), nullable=False, index=True)
version: Mapped[str] = mapped_column(String(20), default="1.0.0")
config: Mapped[dict | None] = mapped_column(JSONB)
status: Mapped[str] = mapped_column(String(20), default="pending", index=True)
last_heartbeat_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
last_error: Mapped[str | None] = mapped_column(Text)
metadata_: Mapped[dict | None] = mapped_column("metadata", JSONB)
erp_system = relationship("ERPSystemModel", back_populates="connectors")
pharmacies = relationship("PharmacyModel", back_populates="connector", foreign_keys="PharmacyModel.connector_id")
configs = relationship("ConnectorConfigModel", back_populates="connector", cascade="all, delete-orphan")
connector_versions = relationship("ConnectorVersionModel", back_populates="connector", cascade="all, delete-orphan")
__table_args__ = (
UniqueConstraint("erp_system_id", "pharmacy_id", name="uq_connector_erp_pharmacy"),
)
class ConnectorVersionModel(Base, BaseMixin):
__tablename__ = "connector_versions"
connector_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("connectors.id"), nullable=False, index=True)
connector_type: Mapped[str] = mapped_column(String(50), nullable=False, index=True)
version: Mapped[str] = mapped_column(String(20), nullable=False)
changelog: Mapped[str | None] = mapped_column(Text)
download_url: Mapped[str | None] = mapped_column(String(500))
checksum: Mapped[str | None] = mapped_column(String(128))
is_latest: Mapped[bool] = mapped_column(Boolean, default=False, index=True)
is_required: Mapped[bool] = mapped_column(Boolean, default=False)
released_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
connector = relationship("ConnectorModel", back_populates="connector_versions")
__table_args__ = (
UniqueConstraint("connector_type", "version", name="uq_connector_version"),
)
class ConnectorConfigModel(Base, BaseMixin):
__tablename__ = "connector_configs"
connector_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("connectors.id"), nullable=False, index=True)
key: Mapped[str] = mapped_column(String(100), nullable=False)
value: Mapped[str] = mapped_column(Text, nullable=False)
encrypted: Mapped[bool] = mapped_column(Boolean, default=False)
description: Mapped[str | None] = mapped_column(String(500))
connector = relationship("ConnectorModel", back_populates="configs")
__table_args__ = (
UniqueConstraint("connector_id", "key", name="uq_connector_config_key"),
)
class MedicationModel(Base, BaseMixin):
__tablename__ = "medications"
nregistro: Mapped[str] = mapped_column(String(50), unique=True, nullable=False, index=True)
name: Mapped[str] = mapped_column(String(500), nullable=False, index=True)
active_ingredient: Mapped[str | None] = mapped_column(String(500))
dosage: Mapped[str | None] = mapped_column(String(100))
form: Mapped[str | None] = mapped_column(String(100))
atc_code: Mapped[str | None] = mapped_column(String(20), index=True)
prescription_required: Mapped[bool] = mapped_column(Boolean, default=False)
manufacturer: Mapped[str | None] = mapped_column(String(255))
metadata_: Mapped[dict | None] = mapped_column("metadata", JSONB)
catalog_entries = relationship("MedicationCatalogEntryModel", back_populates="medication", cascade="all, delete-orphan")
inventory_records = relationship("InventoryRecordModel", back_populates="medication", cascade="all, delete-orphan")
reservations = relationship("ReservationModel", back_populates="medication", cascade="all, delete-orphan")
class MedicationCatalogEntryModel(Base, BaseMixin):
__tablename__ = "medication_catalog"
pharmacy_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("pharmacies.id"), nullable=False, index=True)
medication_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("medications.id"), nullable=False, index=True)
price: Mapped[float | None] = mapped_column(Float)
stock: Mapped[int] = mapped_column(Integer, default=0)
is_available: Mapped[bool] = mapped_column(Boolean, default=False, index=True)
last_verified_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
metadata_: Mapped[dict | None] = mapped_column("metadata", JSONB)
pharmacy = relationship("PharmacyModel", back_populates="catalog_entries")
medication = relationship("MedicationModel", back_populates="catalog_entries")
__table_args__ = (
UniqueConstraint("pharmacy_id", "medication_id", name="uq_catalog_pharmacy_medication"),
)
class InventoryRecordModel(Base, BaseMixin):
__tablename__ = "inventory_records"
pharmacy_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("pharmacies.id"), nullable=False, index=True)
medication_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("medications.id"), nullable=False, index=True)
stock: Mapped[int] = mapped_column(Integer, default=0)
reserved_stock: Mapped[int] = mapped_column(Integer, default=0)
available_stock: Mapped[int] = mapped_column(Integer, default=0)
min_stock: Mapped[int | None] = mapped_column(Integer)
max_stock: Mapped[int | None] = mapped_column(Integer)
last_updated_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), index=True)
source: Mapped[str | None] = mapped_column(String(100))
batch_id: Mapped[str | None] = mapped_column(String(100))
metadata_: Mapped[dict | None] = mapped_column("metadata", JSONB)
pharmacy = relationship("PharmacyModel", back_populates="inventory_records")
medication = relationship("MedicationModel", back_populates="inventory_records")
history = relationship("InventoryHistoryModel", back_populates="inventory_record", cascade="all, delete-orphan")
__table_args__ = (
UniqueConstraint("pharmacy_id", "medication_id", name="uq_inventory_pharmacy_medication"),
)
class InventoryHistoryModel(Base):
__tablename__ = "inventory_history"
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
inventory_record_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("inventory_records.id"), nullable=False, index=True)
pharmacy_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("pharmacies.id"), nullable=False, index=True)
medication_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("medications.id"), nullable=False, index=True)
previous_stock: Mapped[int] = mapped_column(Integer, nullable=False)
new_stock: Mapped[int] = mapped_column(Integer, nullable=False)
delta: Mapped[int] = mapped_column(Integer, nullable=False)
reason: Mapped[str | None] = mapped_column(String(255))
source: Mapped[str | None] = mapped_column(String(100))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False, index=True)
inventory_record = relationship("InventoryRecordModel", back_populates="history")
class ReservationModel(Base, BaseMixin):
__tablename__ = "reservations"
pharmacy_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("pharmacies.id"), nullable=False, index=True)
medication_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("medications.id"), nullable=False, index=True)
user_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("technical_users.id"), nullable=True, index=True)
external_reference: Mapped[str | None] = mapped_column(String(255))
quantity: Mapped[int] = mapped_column(Integer, nullable=False)
status: Mapped[str] = mapped_column(String(20), default="pending", index=True)
expires_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), index=True)
confirmed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
cancelled_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
idempotency_key: Mapped[str | None] = mapped_column(String(255), unique=True, index=True)
metadata_: Mapped[dict | None] = mapped_column("metadata", JSONB)
pharmacy = relationship("PharmacyModel", back_populates="reservations")
medication = relationship("MedicationModel", back_populates="reservations")
user = relationship("TechnicalUserModel", back_populates="reservations")
__table_args__ = (
Index("ix_reservations_pharmacy_status", "pharmacy_id", "status"),
Index("ix_reservations_expires", "expires_at"),
)
class SyncJobModel(Base, BaseMixin):
__tablename__ = "sync_jobs"
pharmacy_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("pharmacies.id"), nullable=False, index=True)
connector_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("connectors.id"), nullable=False, index=True)
strategy: Mapped[str] = mapped_column(String(20), nullable=False)
status: Mapped[str] = mapped_column(String(20), default="pending", index=True)
started_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
completed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
items_total: Mapped[int | None] = mapped_column(Integer)
items_processed: Mapped[int | None] = mapped_column(Integer)
items_failed: Mapped[int | None] = mapped_column(Integer)
error_message: Mapped[str | None] = mapped_column(Text)
metadata_: Mapped[dict | None] = mapped_column("metadata", JSONB)
pharmacy = relationship("PharmacyModel", back_populates="sync_jobs")
logs = relationship("SyncLogModel", back_populates="sync_job", cascade="all, delete-orphan")
class SyncLogModel(Base):
__tablename__ = "sync_logs"
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
sync_job_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("sync_jobs.id"), nullable=False, index=True)
level: Mapped[str] = mapped_column(String(20), default="INFO", nullable=False)
message: Mapped[str] = mapped_column(Text, nullable=False)
details: Mapped[dict | None] = mapped_column(JSONB)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False, index=True)
sync_job = relationship("SyncJobModel", back_populates="logs")
class AuditLogModel(Base):
__tablename__ = "audit_logs"
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
actor_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), index=True)
actor_type: Mapped[str | None] = mapped_column(String(50))
action: Mapped[str] = mapped_column(String(100), nullable=False, index=True)
resource_type: Mapped[str] = mapped_column(String(100), nullable=False, index=True)
resource_id: Mapped[str | None] = mapped_column(String(255), index=True)
old_value: Mapped[dict | None] = mapped_column(JSONB)
new_value: Mapped[dict | None] = mapped_column(JSONB)
ip_address: Mapped[str | None] = mapped_column(String(45))
user_agent: Mapped[str | None] = mapped_column(String(500))
request_id: Mapped[str | None] = mapped_column(String(100), index=True)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False, index=True)
__table_args__ = (
Index("ix_audit_logs_action_resource", "action", "resource_type"),
Index("ix_audit_logs_actor_created", "actor_id", "created_at"),
)
class TechnicalUserModel(Base, BaseMixin):
__tablename__ = "technical_users"
username: Mapped[str] = mapped_column(String(100), unique=True, nullable=False, index=True)
email: Mapped[str | None] = mapped_column(String(255))
full_name: Mapped[str | None] = mapped_column(String(255))
password_hash: Mapped[str] = mapped_column(String(255), nullable=False)
role: Mapped[str] = mapped_column(String(20), default="viewer", index=True)
is_active: Mapped[bool] = mapped_column(Boolean, default=True, index=True)
scopes: Mapped[dict | None] = mapped_column(JSONB, default=list)
last_login_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
reservations = relationship("ReservationModel", back_populates="user")
api_clients = relationship("APIClientModel", back_populates="owner")
class APIClientModel(Base, BaseMixin):
__tablename__ = "api_clients"
name: Mapped[str] = mapped_column(String(255), nullable=False)
description: Mapped[str | None] = mapped_column(Text)
owner_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("technical_users.id"), nullable=True, index=True)
is_active: Mapped[bool] = mapped_column(Boolean, default=True, index=True)
scopes: Mapped[dict | None] = mapped_column(JSONB, default=list)
rate_limit: Mapped[int | None] = mapped_column(Integer)
allowed_ips: Mapped[dict | None] = mapped_column(JSONB, default=list)
metadata_: Mapped[dict | None] = mapped_column("metadata", JSONB)
owner = relationship("TechnicalUserModel", back_populates="api_clients")
api_keys = relationship("APIKeyModel", back_populates="api_client", cascade="all, delete-orphan")
class APIKeyModel(Base):
__tablename__ = "api_keys"
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
api_client_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("api_clients.id"), nullable=False, index=True)
key_prefix: Mapped[str] = mapped_column(String(10), nullable=False, index=True)
key_hash: Mapped[bytes] = mapped_column(LargeBinary(64), nullable=False)
name: Mapped[str] = mapped_column(String(255), nullable=False)
is_active: Mapped[bool] = mapped_column(Boolean, default=True, index=True)
expires_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
last_used_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False)
api_client = relationship("APIClientModel", back_populates="api_keys")
__table_args__ = (
Index("ix_api_keys_prefix_active", "key_prefix", "is_active"),
)
@@ -0,0 +1,32 @@
from .pharmacy_repository import PharmacyRepository
from .technical_user_repository import TechnicalUserRepository
from .audit_log_repository import AuditLogRepository
from .api_client_repository import APIClientRepository, APIKeyRepository
from .connector_repository import ConnectorRepository
from .erp_system_repository import ERPSystemRepository
from .connector_config_repository import ConnectorConfigRepository
from .connector_version_repository import ConnectorVersionRepository
from .medication_repository import MedicationRepository
from .medication_catalog_repository import MedicationCatalogRepository
from .inventory_repository import InventoryRepository
from .reservation_repository import ReservationRepository
from .sync_job_repository import SyncJobRepository
from .sync_log_repository import SyncLogRepository
__all__ = [
"APIClientRepository",
"APIKeyRepository",
"AuditLogRepository",
"ConnectorConfigRepository",
"ConnectorRepository",
"ConnectorVersionRepository",
"ERPSystemRepository",
"InventoryRepository",
"MedicationCatalogRepository",
"MedicationRepository",
"PharmacyRepository",
"ReservationRepository",
"SyncJobRepository",
"SyncLogRepository",
"TechnicalUserRepository",
]
@@ -0,0 +1,116 @@
import uuid
from collections.abc import Sequence
from typing import Any
from sqlalchemy import delete, func, select, update
from sqlalchemy.ext.asyncio import AsyncSession
from src.domain.entities import APIClientEntity, APIKeyEntity
from src.domain.interfaces import IAPIClientRepository, IAPIKeyRepository
from src.infrastructure.database.models import APIClientModel, APIKeyModel
class APIClientRepository(IAPIClientRepository):
def __init__(self, session: AsyncSession) -> None:
self._session = session
async def get_by_id(self, client_id: uuid.UUID) -> APIClientEntity | None:
stmt = select(APIClientModel).where(APIClientModel.id == client_id)
result = await self._session.execute(stmt)
m = result.scalar_one_or_none()
if not m:
return None
return APIClientEntity(
id=m.id, name=m.name, description=m.description, owner_id=m.owner_id,
is_active=m.is_active, scopes=m.scopes or [], rate_limit=m.rate_limit,
allowed_ips=m.allowed_ips or [], metadata_=m.metadata_, created_at=m.created_at, updated_at=m.updated_at,
)
async def list_clients(self, *, is_active: bool | None = None, offset: int = 0, limit: int = 50) -> tuple[Sequence[APIClientEntity], int]:
filters: list[Any] = []
if is_active is not None:
filters.append(APIClientModel.is_active == is_active)
count_stmt = select(func.count()).select_from(APIClientModel)
if filters:
count_stmt = count_stmt.where(*filters)
total = (await self._session.execute(count_stmt)).scalar() or 0
stmt = select(APIClientModel).order_by(APIClientModel.name).offset(offset).limit(limit)
if filters:
stmt = stmt.where(*filters)
result = await self._session.execute(stmt)
models = result.scalars().all()
entities = [
APIClientEntity(id=m.id, name=m.name, description=m.description, owner_id=m.owner_id,
is_active=m.is_active, scopes=m.scopes or [], rate_limit=m.rate_limit,
allowed_ips=m.allowed_ips or [], metadata_=m.metadata_, created_at=m.created_at, updated_at=m.updated_at)
for m in models
]
return entities, total
async def create(self, entity: APIClientEntity) -> APIClientEntity:
model = APIClientModel(
name=entity.name, description=entity.description, owner_id=entity.owner_id,
is_active=entity.is_active, scopes=entity.scopes, rate_limit=entity.rate_limit,
allowed_ips=entity.allowed_ips, metadata_=entity.metadata_,
)
self._session.add(model)
await self._session.flush()
return await self.get_by_id(model.id) # type: ignore[return-value]
async def update(self, entity: APIClientEntity) -> APIClientEntity:
stmt = update(APIClientModel).where(APIClientModel.id == entity.id).values(
name=entity.name, description=entity.description, is_active=entity.is_active,
scopes=entity.scopes, rate_limit=entity.rate_limit, allowed_ips=entity.allowed_ips,
metadata_=entity.metadata_,
)
await self._session.execute(stmt)
await self._session.flush()
return await self.get_by_id(entity.id) # type: ignore[return-value]
class APIKeyRepository(IAPIKeyRepository):
def __init__(self, session: AsyncSession) -> None:
self._session = session
async def get_by_prefix(self, key_prefix: str) -> APIKeyEntity | None:
stmt = select(APIKeyModel).where(APIKeyModel.key_prefix == key_prefix, APIKeyModel.is_active == True) # noqa: E712
result = await self._session.execute(stmt)
m = result.scalar_one_or_none()
if not m:
return None
return APIKeyEntity(
id=m.id, api_client_id=m.api_client_id, key_prefix=m.key_prefix,
key_hash=m.key_hash, name=m.name, is_active=m.is_active,
expires_at=m.expires_at, last_used_at=m.last_used_at, created_at=m.created_at,
)
async def list_by_client(self, api_client_id: uuid.UUID) -> Sequence[APIKeyEntity]:
stmt = select(APIKeyModel).where(APIKeyModel.api_client_id == api_client_id).order_by(APIKeyModel.created_at.desc())
result = await self._session.execute(stmt)
models = result.scalars().all()
return [
APIKeyEntity(
id=m.id, api_client_id=m.api_client_id, key_prefix=m.key_prefix,
key_hash=m.key_hash, name=m.name, is_active=m.is_active,
expires_at=m.expires_at, last_used_at=m.last_used_at, created_at=m.created_at,
)
for m in models
]
async def create(self, entity: APIKeyEntity) -> APIKeyEntity:
model = APIKeyModel(
api_client_id=entity.api_client_id, key_prefix=entity.key_prefix,
key_hash=entity.key_hash, name=entity.name, is_active=entity.is_active,
expires_at=entity.expires_at,
)
self._session.add(model)
await self._session.flush()
return APIKeyEntity(
id=model.id, api_client_id=model.api_client_id, key_prefix=model.key_prefix,
key_hash=model.key_hash, name=model.name, is_active=model.is_active,
expires_at=model.expires_at, created_at=model.created_at,
)
async def update_last_used(self, key_id: uuid.UUID) -> None:
stmt = update(APIKeyModel).where(APIKeyModel.id == key_id).values(last_used_at=func.now())
await self._session.execute(stmt)
@@ -0,0 +1,78 @@
import uuid
from collections.abc import Sequence
from typing import Any
from sqlalchemy import delete, func, select
from sqlalchemy.ext.asyncio import AsyncSession
from src.domain.entities import AuditLogEntity
from src.domain.interfaces import IAuditLogRepository
from src.infrastructure.database.models import AuditLogModel
def _model_to_entity(m: AuditLogModel) -> AuditLogEntity:
return AuditLogEntity(
id=m.id,
actor_id=m.actor_id,
actor_type=m.actor_type,
action=m.action,
resource_type=m.resource_type,
resource_id=m.resource_id,
old_value=m.old_value,
new_value=m.new_value,
ip_address=m.ip_address,
user_agent=m.user_agent,
request_id=m.request_id,
created_at=m.created_at,
)
class AuditLogRepository(IAuditLogRepository):
def __init__(self, session: AsyncSession) -> None:
self._session = session
async def add(self, entity: AuditLogEntity) -> AuditLogEntity:
model = AuditLogModel(
actor_id=entity.actor_id,
actor_type=entity.actor_type,
action=entity.action,
resource_type=entity.resource_type,
resource_id=entity.resource_id,
old_value=entity.old_value,
new_value=entity.new_value,
ip_address=entity.ip_address,
user_agent=entity.user_agent,
request_id=entity.request_id,
)
self._session.add(model)
await self._session.flush()
return _model_to_entity(model)
async def list_logs(
self,
*,
actor_id: uuid.UUID | None = None,
action: str | None = None,
resource_type: str | None = None,
offset: int = 0,
limit: int = 50,
) -> tuple[Sequence[AuditLogEntity], int]:
filters: list[Any] = []
if actor_id:
filters.append(AuditLogModel.actor_id == actor_id)
if action:
filters.append(AuditLogModel.action == action)
if resource_type:
filters.append(AuditLogModel.resource_type == resource_type)
count_stmt = select(func.count()).select_from(AuditLogModel)
if filters:
count_stmt = count_stmt.where(*filters)
total = (await self._session.execute(count_stmt)).scalar() or 0
stmt = select(AuditLogModel).order_by(AuditLogModel.created_at.desc()).offset(offset).limit(limit)
if filters:
stmt = stmt.where(*filters)
result = await self._session.execute(stmt)
models = result.scalars().all()
return [_model_to_entity(m) for m in models], total
@@ -0,0 +1,71 @@
import uuid
from collections.abc import Sequence
from typing import Any
from sqlalchemy import delete, func, select
from sqlalchemy.ext.asyncio import AsyncSession
from src.domain.entities import ConnectorConfigEntity
from src.domain.interfaces import IConnectorConfigRepository
from src.infrastructure.database.models import ConnectorConfigModel
def _model_to_entity(m: ConnectorConfigModel) -> ConnectorConfigEntity:
return ConnectorConfigEntity(
id=m.id,
connector_id=m.connector_id,
key=m.key,
value=m.value,
encrypted=m.encrypted,
description=m.description,
created_at=m.created_at,
updated_at=m.updated_at,
)
class ConnectorConfigRepository(IConnectorConfigRepository):
def __init__(self, session: AsyncSession) -> None:
self._session = session
async def list_by_connector(self, connector_id: uuid.UUID) -> Sequence[ConnectorConfigEntity]:
stmt = select(ConnectorConfigModel).where(ConnectorConfigModel.connector_id == connector_id).order_by(ConnectorConfigModel.key)
result = await self._session.execute(stmt)
models = result.scalars().all()
return [_model_to_entity(m) for m in models]
async def get_by_key(self, connector_id: uuid.UUID, key: str) -> ConnectorConfigEntity | None:
stmt = select(ConnectorConfigModel).where(
ConnectorConfigModel.connector_id == connector_id,
ConnectorConfigModel.key == key,
)
result = await self._session.execute(stmt)
model = result.scalar_one_or_none()
return _model_to_entity(model) if model else None
async def upsert(self, entity: ConnectorConfigEntity) -> ConnectorConfigEntity:
existing = await self.get_by_key(entity.connector_id, entity.key)
if existing:
stmt = (
ConnectorConfigModel.__table__.update()
.where(ConnectorConfigModel.id == existing.id)
.values(value=entity.value, encrypted=entity.encrypted, description=entity.description)
)
await self._session.execute(stmt)
await self._session.flush()
return await self.get_by_key(entity.connector_id, entity.key) # type: ignore[return-value]
else:
model = ConnectorConfigModel(
connector_id=entity.connector_id,
key=entity.key,
value=entity.value,
encrypted=entity.encrypted,
description=entity.description,
)
self._session.add(model)
await self._session.flush()
return _model_to_entity(model)
async def delete(self, config_id: uuid.UUID) -> bool:
stmt = delete(ConnectorConfigModel).where(ConnectorConfigModel.id == config_id)
result = await self._session.execute(stmt)
return result.rowcount > 0
@@ -0,0 +1,101 @@
import uuid
from collections.abc import Sequence
from typing import Any
from sqlalchemy import delete, func, select, update
from sqlalchemy.ext.asyncio import AsyncSession
from src.domain.entities import ConnectorEntity, ConnectorStatus
from src.domain.interfaces import IConnectorRepository
from src.infrastructure.database.models import ConnectorModel
def _model_to_entity(m: ConnectorModel) -> ConnectorEntity:
return ConnectorEntity(
id=m.id,
erp_system_id=m.erp_system_id,
pharmacy_id=m.pharmacy_id,
name=m.name,
connector_type=m.connector_type,
version=m.version,
config=m.config,
status=m.status,
last_heartbeat_at=m.last_heartbeat_at,
last_error=m.last_error,
metadata_=m.metadata_,
created_at=m.created_at,
updated_at=m.updated_at,
)
def _entity_to_model_data(e: ConnectorEntity) -> dict[str, Any]:
return {
"erp_system_id": e.erp_system_id,
"pharmacy_id": e.pharmacy_id,
"name": e.name,
"connector_type": e.connector_type if isinstance(e.connector_type, str) else e.connector_type.value,
"version": e.version,
"config": e.config,
"status": e.status if isinstance(e.status, str) else e.status.value,
"last_heartbeat_at": e.last_heartbeat_at,
"last_error": e.last_error,
"metadata_": e.metadata_,
}
class ConnectorRepository(IConnectorRepository):
def __init__(self, session: AsyncSession) -> None:
self._session = session
async def get_by_id(self, connector_id: uuid.UUID) -> ConnectorEntity | None:
stmt = select(ConnectorModel).where(ConnectorModel.id == connector_id)
result = await self._session.execute(stmt)
model = result.scalar_one_or_none()
return _model_to_entity(model) if model else None
async def list_connectors(
self,
*,
erp_system_id: uuid.UUID | None = None,
pharmacy_id: uuid.UUID | None = None,
status: str | None = None,
offset: int = 0,
limit: int = 50,
) -> tuple[Sequence[ConnectorEntity], int]:
filters: list[Any] = []
if erp_system_id:
filters.append(ConnectorModel.erp_system_id == erp_system_id)
if pharmacy_id:
filters.append(ConnectorModel.pharmacy_id == pharmacy_id)
if status:
filters.append(ConnectorModel.status == status)
count_stmt = select(func.count()).select_from(ConnectorModel)
if filters:
count_stmt = count_stmt.where(*filters)
total = (await self._session.execute(count_stmt)).scalar() or 0
stmt = select(ConnectorModel).order_by(ConnectorModel.name).offset(offset).limit(limit)
if filters:
stmt = stmt.where(*filters)
result = await self._session.execute(stmt)
models = result.scalars().all()
return [_model_to_entity(m) for m in models], total
async def create(self, entity: ConnectorEntity) -> ConnectorEntity:
model = ConnectorModel(**_entity_to_model_data(entity))
self._session.add(model)
await self._session.flush()
return _model_to_entity(model)
async def update(self, entity: ConnectorEntity) -> ConnectorEntity:
data = _entity_to_model_data(entity)
stmt = update(ConnectorModel).where(ConnectorModel.id == entity.id).values(**data)
await self._session.execute(stmt)
await self._session.flush()
return await self.get_by_id(entity.id) # type: ignore[return-value]
async def delete(self, connector_id: uuid.UUID) -> bool:
stmt = delete(ConnectorModel).where(ConnectorModel.id == connector_id)
result = await self._session.execute(stmt)
return result.rowcount > 0
@@ -0,0 +1,82 @@
from collections.abc import Sequence
from typing import Any
import uuid
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
from src.domain.entities import ConnectorVersionEntity
from src.domain.interfaces import IConnectorVersionRepository
from src.infrastructure.database.models import ConnectorVersionModel
def _model_to_entity(m: ConnectorVersionModel) -> ConnectorVersionEntity:
return ConnectorVersionEntity(
id=m.id,
connector_id=m.connector_id,
connector_type=m.connector_type,
version=m.version,
changelog=m.changelog,
download_url=m.download_url,
checksum=m.checksum,
is_latest=m.is_latest,
is_required=m.is_required,
released_at=m.released_at,
created_at=m.created_at,
)
class ConnectorVersionRepository(IConnectorVersionRepository):
def __init__(self, session: AsyncSession) -> None:
self._session = session
async def get_latest(self, connector_type: str) -> ConnectorVersionEntity | None:
stmt = (
select(ConnectorVersionModel)
.where(ConnectorVersionModel.connector_type == connector_type, ConnectorVersionModel.is_latest == True) # noqa: E712
.order_by(ConnectorVersionModel.released_at.desc())
.limit(1)
)
result = await self._session.execute(stmt)
model = result.scalar_one_or_none()
return _model_to_entity(model) if model else None
async def list_versions(
self,
*,
connector_type: str | None = None,
offset: int = 0,
limit: int = 50,
) -> tuple[Sequence[ConnectorVersionEntity], int]:
filters: list[Any] = []
if connector_type:
filters.append(ConnectorVersionModel.connector_type == connector_type)
count_stmt = select(func.count()).select_from(ConnectorVersionModel)
if filters:
count_stmt = count_stmt.where(*filters)
total = (await self._session.execute(count_stmt)).scalar() or 0
stmt = select(ConnectorVersionModel).order_by(ConnectorVersionModel.released_at.desc()).offset(offset).limit(limit)
if filters:
stmt = stmt.where(*filters)
result = await self._session.execute(stmt)
models = result.scalars().all()
return [_model_to_entity(m) for m in models], total
async def create(self, entity: ConnectorVersionEntity) -> ConnectorVersionEntity:
model = ConnectorVersionModel(
connector_id=entity.connector_id or uuid.uuid4(),
connector_type=entity.connector_type if isinstance(entity.connector_type, str) else entity.connector_type.value,
version=entity.version,
changelog=entity.changelog,
download_url=entity.download_url,
checksum=entity.checksum,
is_latest=entity.is_latest,
is_required=entity.is_required,
released_at=entity.released_at,
)
self._session.add(model)
await self._session.flush()
return _model_to_entity(model)
@@ -0,0 +1,89 @@
import uuid
from collections.abc import Sequence
from typing import Any
from sqlalchemy import delete, func, select, update
from sqlalchemy.ext.asyncio import AsyncSession
from src.domain.entities import ERPSystemEntity
from src.domain.interfaces import IERPSystemRepository
from src.infrastructure.database.models import ERPSystemModel
def _model_to_entity(m: ERPSystemModel) -> ERPSystemEntity:
return ERPSystemEntity(
id=m.id,
name=m.name,
erp_type=m.erp_type,
version=m.version,
base_url=m.base_url,
auth_config=m.auth_config,
status=m.status,
metadata_=m.metadata_,
created_at=m.created_at,
updated_at=m.updated_at,
)
def _entity_to_model_data(e: ERPSystemEntity) -> dict[str, Any]:
return {
"name": e.name,
"erp_type": e.erp_type if isinstance(e.erp_type, str) else e.erp_type.value,
"version": e.version,
"base_url": e.base_url,
"auth_config": e.auth_config,
"status": e.status if isinstance(e.status, str) else e.status.value,
"metadata_": e.metadata_,
}
class ERPSystemRepository(IERPSystemRepository):
def __init__(self, session: AsyncSession) -> None:
self._session = session
async def get_by_id(self, erp_id: uuid.UUID) -> ERPSystemEntity | None:
stmt = select(ERPSystemModel).where(ERPSystemModel.id == erp_id)
result = await self._session.execute(stmt)
model = result.scalar_one_or_none()
return _model_to_entity(model) if model else None
async def list_erp_systems(
self,
*,
erp_type: str | None = None,
offset: int = 0,
limit: int = 50,
) -> tuple[Sequence[ERPSystemEntity], int]:
filters: list[Any] = []
if erp_type:
filters.append(ERPSystemModel.erp_type == erp_type)
count_stmt = select(func.count()).select_from(ERPSystemModel)
if filters:
count_stmt = count_stmt.where(*filters)
total = (await self._session.execute(count_stmt)).scalar() or 0
stmt = select(ERPSystemModel).order_by(ERPSystemModel.name).offset(offset).limit(limit)
if filters:
stmt = stmt.where(*filters)
result = await self._session.execute(stmt)
models = result.scalars().all()
return [_model_to_entity(m) for m in models], total
async def create(self, entity: ERPSystemEntity) -> ERPSystemEntity:
model = ERPSystemModel(**_entity_to_model_data(entity))
self._session.add(model)
await self._session.flush()
return _model_to_entity(model)
async def update(self, entity: ERPSystemEntity) -> ERPSystemEntity:
data = _entity_to_model_data(entity)
stmt = update(ERPSystemModel).where(ERPSystemModel.id == entity.id).values(**data)
await self._session.execute(stmt)
await self._session.flush()
return await self.get_by_id(entity.id) # type: ignore[return-value]
async def delete(self, erp_id: uuid.UUID) -> bool:
stmt = delete(ERPSystemModel).where(ERPSystemModel.id == erp_id)
result = await self._session.execute(stmt)
return result.rowcount > 0
@@ -0,0 +1,142 @@
import uuid
from collections.abc import Sequence
from typing import Any
from sqlalchemy import func, select, update
from sqlalchemy.ext.asyncio import AsyncSession
from src.domain.entities import InventoryHistoryEntity, InventoryRecordEntity
from src.domain.interfaces import IInventoryRepository
from src.infrastructure.database.models import InventoryHistoryModel, InventoryRecordModel
def _record_model_to_entity(m: InventoryRecordModel) -> InventoryRecordEntity:
return InventoryRecordEntity(
id=m.id,
pharmacy_id=m.pharmacy_id,
medication_id=m.medication_id,
stock=m.stock,
reserved_stock=m.reserved_stock,
available_stock=m.available_stock,
min_stock=m.min_stock,
max_stock=m.max_stock,
last_updated_at=m.last_updated_at,
source=m.source,
batch_id=m.batch_id,
metadata_=m.metadata_,
created_at=m.created_at,
updated_at=m.updated_at,
)
def _record_entity_to_model_data(e: InventoryRecordEntity) -> dict[str, Any]:
return {
"pharmacy_id": e.pharmacy_id,
"medication_id": e.medication_id,
"stock": e.stock,
"reserved_stock": e.reserved_stock,
"available_stock": e.available_stock,
"min_stock": e.min_stock,
"max_stock": e.max_stock,
"last_updated_at": e.last_updated_at,
"source": e.source,
"batch_id": e.batch_id,
"metadata_": e.metadata_,
}
def _history_model_to_entity(m: InventoryHistoryModel) -> InventoryHistoryEntity:
return InventoryHistoryEntity(
id=m.id,
pharmacy_id=m.pharmacy_id,
medication_id=m.medication_id,
previous_stock=m.previous_stock,
new_stock=m.new_stock,
delta=m.delta,
reason=m.reason,
source=m.source,
created_at=m.created_at,
)
class InventoryRepository(IInventoryRepository):
def __init__(self, session: AsyncSession) -> None:
self._session = session
async def get_record(self, pharmacy_id: uuid.UUID, medication_id: uuid.UUID) -> InventoryRecordEntity | None:
stmt = select(InventoryRecordModel).where(
InventoryRecordModel.pharmacy_id == pharmacy_id,
InventoryRecordModel.medication_id == medication_id,
)
result = await self._session.execute(stmt)
model = result.scalar_one_or_none()
return _record_model_to_entity(model) if model else None
async def list_by_pharmacy(self, pharmacy_id: uuid.UUID, *, offset: int = 0, limit: int = 50) -> tuple[Sequence[InventoryRecordEntity], int]:
count_stmt = select(func.count()).select_from(InventoryRecordModel).where(
InventoryRecordModel.pharmacy_id == pharmacy_id
)
total = (await self._session.execute(count_stmt)).scalar() or 0
stmt = (
select(InventoryRecordModel)
.where(InventoryRecordModel.pharmacy_id == pharmacy_id)
.order_by(InventoryRecordModel.last_updated_at.desc().nullslast())
.offset(offset)
.limit(limit)
)
result = await self._session.execute(stmt)
models = result.scalars().all()
return [_record_model_to_entity(m) for m in models], total
async def upsert(self, entity: InventoryRecordEntity) -> InventoryRecordEntity:
existing = await self.get_record(entity.pharmacy_id, entity.medication_id)
if existing:
data = _record_entity_to_model_data(entity)
stmt = (
update(InventoryRecordModel)
.where(
InventoryRecordModel.pharmacy_id == entity.pharmacy_id,
InventoryRecordModel.medication_id == entity.medication_id,
)
.values(**data)
)
await self._session.execute(stmt)
await self._session.flush()
return await self.get_record(entity.pharmacy_id, entity.medication_id) # type: ignore[return-value]
else:
model = InventoryRecordModel(**_record_entity_to_model_data(entity))
self._session.add(model)
await self._session.flush()
return _record_model_to_entity(model)
async def add_history(self, entity: InventoryHistoryEntity) -> InventoryHistoryEntity:
model = InventoryHistoryModel(
inventory_record_id=entity.inventory_record_id,
pharmacy_id=entity.pharmacy_id,
medication_id=entity.medication_id,
previous_stock=entity.previous_stock,
new_stock=entity.new_stock,
delta=entity.delta,
reason=entity.reason,
source=entity.source,
)
self._session.add(model)
await self._session.flush()
return _history_model_to_entity(model)
async def get_history(self, pharmacy_id: uuid.UUID, medication_id: uuid.UUID, *, offset: int = 0, limit: int = 50) -> Sequence[InventoryHistoryEntity]:
record = await self.get_record(pharmacy_id, medication_id)
if not record:
return []
stmt = (
select(InventoryHistoryModel)
.where(InventoryHistoryModel.inventory_record_id == record.id)
.order_by(InventoryHistoryModel.created_at.desc())
.offset(offset)
.limit(limit)
)
result = await self._session.execute(stmt)
models = result.scalars().all()
return [_history_model_to_entity(m) for m in models]
@@ -0,0 +1,90 @@
import uuid
from collections.abc import Sequence
from typing import Any
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
from src.domain.entities import MedicationCatalogEntryEntity
from src.domain.interfaces import IMedicationCatalogRepository
from src.infrastructure.database.models import MedicationCatalogEntryModel
def _model_to_entity(m: MedicationCatalogEntryModel) -> MedicationCatalogEntryEntity:
return MedicationCatalogEntryEntity(
id=m.id,
pharmacy_id=m.pharmacy_id,
medication_id=m.medication_id,
price=m.price,
stock=m.stock,
is_available=m.is_available,
last_verified_at=m.last_verified_at,
metadata_=m.metadata_,
created_at=m.created_at,
updated_at=m.updated_at,
)
def _entity_to_model_data(e: MedicationCatalogEntryEntity) -> dict[str, Any]:
return {
"pharmacy_id": e.pharmacy_id,
"medication_id": e.medication_id,
"price": e.price,
"stock": e.stock,
"is_available": e.is_available,
"last_verified_at": e.last_verified_at,
"metadata_": e.metadata_,
}
class MedicationCatalogRepository(IMedicationCatalogRepository):
def __init__(self, session: AsyncSession) -> None:
self._session = session
async def get_entry(self, pharmacy_id: uuid.UUID, medication_id: uuid.UUID) -> MedicationCatalogEntryEntity | None:
stmt = select(MedicationCatalogEntryModel).where(
MedicationCatalogEntryModel.pharmacy_id == pharmacy_id,
MedicationCatalogEntryModel.medication_id == medication_id,
)
result = await self._session.execute(stmt)
model = result.scalar_one_or_none()
return _model_to_entity(model) if model else None
async def list_by_pharmacy(self, pharmacy_id: uuid.UUID, *, offset: int = 0, limit: int = 50) -> tuple[Sequence[MedicationCatalogEntryEntity], int]:
count_stmt = select(func.count()).select_from(MedicationCatalogEntryModel).where(
MedicationCatalogEntryModel.pharmacy_id == pharmacy_id
)
total = (await self._session.execute(count_stmt)).scalar() or 0
stmt = (
select(MedicationCatalogEntryModel)
.where(MedicationCatalogEntryModel.pharmacy_id == pharmacy_id)
.order_by(MedicationCatalogEntryModel.is_available.desc(), MedicationCatalogEntryModel.price.asc())
.offset(offset)
.limit(limit)
)
result = await self._session.execute(stmt)
models = result.scalars().all()
return [_model_to_entity(m) for m in models], total
async def upsert(self, entity: MedicationCatalogEntryEntity) -> MedicationCatalogEntryEntity:
existing = await self.get_entry(entity.pharmacy_id, entity.medication_id)
if existing:
from sqlalchemy import update
data = _entity_to_model_data(entity)
stmt = (
update(MedicationCatalogEntryModel)
.where(
MedicationCatalogEntryModel.pharmacy_id == entity.pharmacy_id,
MedicationCatalogEntryModel.medication_id == entity.medication_id,
)
.values(**data)
)
await self._session.execute(stmt)
await self._session.flush()
return await self.get_entry(entity.pharmacy_id, entity.medication_id) # type: ignore[return-value]
else:
model = MedicationCatalogEntryModel(**_entity_to_model_data(entity))
self._session.add(model)
await self._session.flush()
return _model_to_entity(model)
@@ -0,0 +1,101 @@
import uuid
from collections.abc import Sequence
from typing import Any
from sqlalchemy import delete, func, or_, select, update
from sqlalchemy.ext.asyncio import AsyncSession
from src.domain.entities import MedicationEntity
from src.domain.interfaces import IMedicationRepository
from src.infrastructure.database.models import MedicationModel
def _model_to_entity(m: MedicationModel) -> MedicationEntity:
return MedicationEntity(
id=m.id,
nregistro=m.nregistro,
name=m.name,
active_ingredient=m.active_ingredient,
dosage=m.dosage,
form=m.form,
atc_code=m.atc_code,
prescription_required=m.prescription_required,
manufacturer=m.manufacturer,
metadata_=m.metadata_,
created_at=m.created_at,
updated_at=m.updated_at,
)
def _entity_to_model_data(e: MedicationEntity) -> dict[str, Any]:
return {
"nregistro": e.nregistro,
"name": e.name,
"active_ingredient": e.active_ingredient,
"dosage": e.dosage,
"form": e.form,
"atc_code": e.atc_code,
"prescription_required": e.prescription_required,
"manufacturer": e.manufacturer,
"metadata_": e.metadata_,
}
class MedicationRepository(IMedicationRepository):
def __init__(self, session: AsyncSession) -> None:
self._session = session
async def get_by_id(self, medication_id: uuid.UUID) -> MedicationEntity | None:
stmt = select(MedicationModel).where(MedicationModel.id == medication_id)
result = await self._session.execute(stmt)
model = result.scalar_one_or_none()
return _model_to_entity(model) if model else None
async def get_by_nregistro(self, nregistro: str) -> MedicationEntity | None:
stmt = select(MedicationModel).where(MedicationModel.nregistro == nregistro)
result = await self._session.execute(stmt)
model = result.scalar_one_or_none()
return _model_to_entity(model) if model else None
async def search(self, query: str, *, offset: int = 0, limit: int = 50) -> tuple[Sequence[MedicationEntity], int]:
pattern = f"%{query}%"
filters = [
or_(
MedicationModel.name.ilike(pattern),
MedicationModel.active_ingredient.ilike(pattern),
MedicationModel.nregistro.ilike(pattern),
MedicationModel.atc_code.ilike(pattern),
)
]
count_stmt = select(func.count()).select_from(MedicationModel).where(*filters)
total = (await self._session.execute(count_stmt)).scalar() or 0
stmt = (
select(MedicationModel)
.where(*filters)
.order_by(MedicationModel.name)
.offset(offset)
.limit(limit)
)
result = await self._session.execute(stmt)
models = result.scalars().all()
return [_model_to_entity(m) for m in models], total
async def create(self, entity: MedicationEntity) -> MedicationEntity:
model = MedicationModel(**_entity_to_model_data(entity))
self._session.add(model)
await self._session.flush()
return _model_to_entity(model)
async def update(self, entity: MedicationEntity) -> MedicationEntity:
data = _entity_to_model_data(entity)
stmt = update(MedicationModel).where(MedicationModel.id == entity.id).values(**data)
await self._session.execute(stmt)
await self._session.flush()
return await self.get_by_id(entity.id) # type: ignore[return-value]
async def delete(self, medication_id: uuid.UUID) -> bool:
stmt = delete(MedicationModel).where(MedicationModel.id == medication_id)
result = await self._session.execute(stmt)
return result.rowcount > 0
@@ -0,0 +1,126 @@
import uuid
from collections.abc import Sequence
from typing import Any
from sqlalchemy import delete, func, select, update
from sqlalchemy.ext.asyncio import AsyncSession
from src.domain.entities import PharmacyEntity
from src.domain.interfaces import IPharmacyRepository
from src.infrastructure.database.models import PharmacyModel
def _model_to_entity(m: PharmacyModel) -> PharmacyEntity:
return PharmacyEntity(
id=m.id,
code=m.code,
name=m.name,
address=m.address,
city=m.city,
province=m.province,
postal_code=m.postal_code,
country=m.country,
latitude=m.latitude,
longitude=m.longitude,
phone=m.phone,
email=m.email,
opening_hours=m.opening_hours,
status=m.status,
erp_type=m.erp_type,
erp_version=m.erp_version,
connector_id=m.connector_id,
sync_strategy=m.sync_strategy,
sync_interval_minutes=m.sync_interval_minutes,
last_sync_at=m.last_sync_at,
last_sync_status=m.last_sync_status,
metadata_=m.metadata_,
created_at=m.created_at,
updated_at=m.updated_at,
)
def _entity_to_model_data(e: PharmacyEntity) -> dict[str, Any]:
return {
"code": e.code,
"name": e.name,
"address": e.address,
"city": e.city,
"province": e.province,
"postal_code": e.postal_code,
"country": e.country,
"latitude": e.latitude,
"longitude": e.longitude,
"phone": e.phone,
"email": e.email,
"opening_hours": e.opening_hours,
"status": e.status,
"erp_type": e.erp_type,
"erp_version": e.erp_version,
"connector_id": e.connector_id,
"sync_strategy": e.sync_strategy,
"sync_interval_minutes": e.sync_interval_minutes,
"last_sync_at": e.last_sync_at,
"last_sync_status": e.last_sync_status,
"metadata_": e.metadata_,
}
class PharmacyRepository(IPharmacyRepository):
def __init__(self, session: AsyncSession) -> None:
self._session = session
async def get_by_id(self, pharmacy_id: uuid.UUID) -> PharmacyEntity | None:
stmt = select(PharmacyModel).where(PharmacyModel.id == pharmacy_id)
result = await self._session.execute(stmt)
model = result.scalar_one_or_none()
return _model_to_entity(model) if model else None
async def get_by_code(self, code: str) -> PharmacyEntity | None:
stmt = select(PharmacyModel).where(PharmacyModel.code == code)
result = await self._session.execute(stmt)
model = result.scalar_one_or_none()
return _model_to_entity(model) if model else None
async def list_pharmacies(
self,
*,
status: str | None = None,
erp_type: str | None = None,
offset: int = 0,
limit: int = 50,
) -> tuple[Sequence[PharmacyEntity], int]:
filters: list[Any] = []
if status:
filters.append(PharmacyModel.status == status)
if erp_type:
filters.append(PharmacyModel.erp_type == erp_type)
count_stmt = select(func.count()).select_from(PharmacyModel)
if filters:
count_stmt = count_stmt.where(*filters)
total = (await self._session.execute(count_stmt)).scalar() or 0
stmt = select(PharmacyModel).order_by(PharmacyModel.name).offset(offset).limit(limit)
if filters:
stmt = stmt.where(*filters)
result = await self._session.execute(stmt)
models = result.scalars().all()
return [_model_to_entity(m) for m in models], total
async def create(self, entity: PharmacyEntity) -> PharmacyEntity:
model = PharmacyModel(**_entity_to_model_data(entity))
self._session.add(model)
await self._session.flush()
return _model_to_entity(model)
async def update(self, entity: PharmacyEntity) -> PharmacyEntity:
data = _entity_to_model_data(entity)
stmt = update(PharmacyModel).where(PharmacyModel.id == entity.id).values(**data)
await self._session.execute(stmt)
await self._session.flush()
return await self.get_by_id(entity.id) # type: ignore[return-value]
async def delete(self, pharmacy_id: uuid.UUID) -> bool:
stmt = delete(PharmacyModel).where(PharmacyModel.id == pharmacy_id)
result = await self._session.execute(stmt)
return result.rowcount > 0
@@ -0,0 +1,94 @@
import uuid
from collections.abc import Sequence
from typing import Any
from sqlalchemy import delete, func, select, update
from sqlalchemy.ext.asyncio import AsyncSession
from src.domain.entities import ReservationEntity
from src.domain.interfaces import IReservationRepository
from src.infrastructure.database.models import ReservationModel
def _model_to_entity(m: ReservationModel) -> ReservationEntity:
return ReservationEntity(
id=m.id,
pharmacy_id=m.pharmacy_id,
medication_id=m.medication_id,
user_id=m.user_id,
external_reference=m.external_reference,
quantity=m.quantity,
status=m.status,
expires_at=m.expires_at,
confirmed_at=m.confirmed_at,
cancelled_at=m.cancelled_at,
idempotency_key=m.idempotency_key,
metadata_=m.metadata_,
created_at=m.created_at,
updated_at=m.updated_at,
)
def _entity_to_model_data(e: ReservationEntity) -> dict[str, Any]:
return {
"pharmacy_id": e.pharmacy_id,
"medication_id": e.medication_id,
"user_id": e.user_id,
"external_reference": e.external_reference,
"quantity": e.quantity,
"status": e.status,
"expires_at": e.expires_at,
"confirmed_at": e.confirmed_at,
"cancelled_at": e.cancelled_at,
"idempotency_key": e.idempotency_key,
"metadata_": e.metadata_,
}
class ReservationRepository(IReservationRepository):
def __init__(self, session: AsyncSession) -> None:
self._session = session
async def get_by_id(self, reservation_id: uuid.UUID) -> ReservationEntity | None:
stmt = select(ReservationModel).where(ReservationModel.id == reservation_id)
result = await self._session.execute(stmt)
model = result.scalar_one_or_none()
return _model_to_entity(model) if model else None
async def get_by_idempotency_key(self, key: str) -> ReservationEntity | None:
stmt = select(ReservationModel).where(ReservationModel.idempotency_key == key)
result = await self._session.execute(stmt)
model = result.scalar_one_or_none()
return _model_to_entity(model) if model else None
async def list_by_pharmacy(self, pharmacy_id: uuid.UUID, *, status: str | None = None, offset: int = 0, limit: int = 50) -> tuple[Sequence[ReservationEntity], int]:
filters: list[Any] = [ReservationModel.pharmacy_id == pharmacy_id]
if status:
filters.append(ReservationModel.status == status)
count_stmt = select(func.count()).select_from(ReservationModel).where(*filters)
total = (await self._session.execute(count_stmt)).scalar() or 0
stmt = (
select(ReservationModel)
.where(*filters)
.order_by(ReservationModel.created_at.desc())
.offset(offset)
.limit(limit)
)
result = await self._session.execute(stmt)
models = result.scalars().all()
return [_model_to_entity(m) for m in models], total
async def create(self, entity: ReservationEntity) -> ReservationEntity:
model = ReservationModel(**_entity_to_model_data(entity))
self._session.add(model)
await self._session.flush()
return _model_to_entity(model)
async def update(self, entity: ReservationEntity) -> ReservationEntity:
data = _entity_to_model_data(entity)
stmt = update(ReservationModel).where(ReservationModel.id == entity.id).values(**data)
await self._session.execute(stmt)
await self._session.flush()
return await self.get_by_id(entity.id) # type: ignore[return-value]
@@ -0,0 +1,95 @@
import uuid
from collections.abc import Sequence
from typing import Any
from sqlalchemy import func, select, update
from sqlalchemy.ext.asyncio import AsyncSession
from src.domain.entities import SyncJobEntity, SyncJobStatus
from src.domain.interfaces import ISyncJobRepository
from src.infrastructure.database.models import SyncJobModel
def _model_to_entity(m: SyncJobModel) -> SyncJobEntity:
return SyncJobEntity(
id=m.id,
pharmacy_id=m.pharmacy_id,
connector_id=m.connector_id,
strategy=m.strategy,
status=m.status,
started_at=m.started_at,
completed_at=m.completed_at,
items_total=m.items_total,
items_processed=m.items_processed,
items_failed=m.items_failed,
error_message=m.error_message,
metadata_=m.metadata_,
created_at=m.created_at,
updated_at=m.updated_at,
)
def _entity_to_model_data(e: SyncJobEntity) -> dict[str, Any]:
return {
"pharmacy_id": e.pharmacy_id,
"connector_id": e.connector_id,
"strategy": e.strategy,
"status": e.status,
"started_at": e.started_at,
"completed_at": e.completed_at,
"items_total": e.items_total,
"items_processed": e.items_processed,
"items_failed": e.items_failed,
"error_message": e.error_message,
"metadata_": e.metadata_,
}
class SyncJobRepository(ISyncJobRepository):
def __init__(self, session: AsyncSession) -> None:
self._session = session
async def get_by_id(self, job_id: uuid.UUID) -> SyncJobEntity | None:
stmt = select(SyncJobModel).where(SyncJobModel.id == job_id)
result = await self._session.execute(stmt)
model = result.scalar_one_or_none()
return _model_to_entity(model) if model else None
async def list_by_pharmacy(
self,
pharmacy_id: uuid.UUID,
*,
status: str | None = None,
offset: int = 0,
limit: int = 50,
) -> tuple[Sequence[SyncJobEntity], int]:
filters: list[Any] = [SyncJobModel.pharmacy_id == pharmacy_id]
if status:
filters.append(SyncJobModel.status == status)
count_stmt = select(func.count()).select_from(SyncJobModel).where(*filters)
total = (await self._session.execute(count_stmt)).scalar() or 0
stmt = (
select(SyncJobModel)
.where(*filters)
.order_by(SyncJobModel.created_at.desc())
.offset(offset)
.limit(limit)
)
result = await self._session.execute(stmt)
models = result.scalars().all()
return [_model_to_entity(m) for m in models], total
async def create(self, entity: SyncJobEntity) -> SyncJobEntity:
model = SyncJobModel(**_entity_to_model_data(entity))
self._session.add(model)
await self._session.flush()
return _model_to_entity(model)
async def update(self, entity: SyncJobEntity) -> SyncJobEntity:
data = _entity_to_model_data(entity)
stmt = update(SyncJobModel).where(SyncJobModel.id == entity.id).values(**data)
await self._session.execute(stmt)
await self._session.flush()
return await self.get_by_id(entity.id) # type: ignore[return-value]
@@ -0,0 +1,53 @@
import uuid
from collections.abc import Sequence
from typing import Any
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from src.domain.entities import SyncLogEntity
from src.domain.interfaces import ISyncLogRepository
from src.infrastructure.database.models import SyncLogModel
def _model_to_entity(m: SyncLogModel) -> SyncLogEntity:
return SyncLogEntity(
id=m.id,
sync_job_id=m.sync_job_id,
level=m.level,
message=m.message,
details=m.details,
created_at=m.created_at,
)
class SyncLogRepository(ISyncLogRepository):
def __init__(self, session: AsyncSession) -> None:
self._session = session
async def add(self, entity: SyncLogEntity) -> SyncLogEntity:
model = SyncLogModel(
sync_job_id=entity.sync_job_id,
level=entity.level,
message=entity.message,
details=entity.details,
)
self._session.add(model)
await self._session.flush()
return _model_to_entity(model)
async def list_by_job(
self,
job_id: uuid.UUID,
*,
level: str | None = None,
offset: int = 0,
limit: int = 100,
) -> Sequence[SyncLogEntity]:
stmt = select(SyncLogModel).where(SyncLogModel.sync_job_id == job_id)
if level:
stmt = stmt.where(SyncLogModel.level == level)
stmt = stmt.order_by(SyncLogModel.created_at.desc()).offset(offset).limit(limit)
result = await self._session.execute(stmt)
models = result.scalars().all()
return [_model_to_entity(m) for m in models]
@@ -0,0 +1,88 @@
import uuid
from collections.abc import Sequence
from typing import Any
from sqlalchemy import delete, func, select, update
from sqlalchemy.ext.asyncio import AsyncSession
from src.domain.entities import TechnicalUserEntity
from src.domain.interfaces import ITechnicalUserRepository
from src.infrastructure.database.models import TechnicalUserModel
def _model_to_entity(m: TechnicalUserModel) -> TechnicalUserEntity:
return TechnicalUserEntity(
id=m.id,
username=m.username,
email=m.email,
full_name=m.full_name,
role=m.role,
is_active=m.is_active,
scopes=m.scopes or [],
last_login_at=m.last_login_at,
created_at=m.created_at,
updated_at=m.updated_at,
)
class TechnicalUserRepository(ITechnicalUserRepository):
def __init__(self, session: AsyncSession) -> None:
self._session = session
async def get_by_id(self, user_id: uuid.UUID) -> TechnicalUserEntity | None:
stmt = select(TechnicalUserModel).where(TechnicalUserModel.id == user_id)
result = await self._session.execute(stmt)
model = result.scalar_one_or_none()
return _model_to_entity(model) if model else None
async def get_by_username(self, username: str) -> TechnicalUserEntity | None:
stmt = select(TechnicalUserModel).where(TechnicalUserModel.username == username)
result = await self._session.execute(stmt)
model = result.scalar_one_or_none()
return _model_to_entity(model) if model else None
async def list_users(self, *, role: str | None = None, is_active: bool | None = None, offset: int = 0, limit: int = 50) -> tuple[Sequence[TechnicalUserEntity], int]:
filters: list[Any] = []
if role:
filters.append(TechnicalUserModel.role == role)
if is_active is not None:
filters.append(TechnicalUserModel.is_active == is_active)
count_stmt = select(func.count()).select_from(TechnicalUserModel)
if filters:
count_stmt = count_stmt.where(*filters)
total = (await self._session.execute(count_stmt)).scalar() or 0
stmt = select(TechnicalUserModel).order_by(TechnicalUserModel.username).offset(offset).limit(limit)
if filters:
stmt = stmt.where(*filters)
result = await self._session.execute(stmt)
models = result.scalars().all()
return [_model_to_entity(m) for m in models], total
async def create(self, entity: TechnicalUserEntity, password_hash: str) -> TechnicalUserEntity:
model = TechnicalUserModel(
username=entity.username,
email=entity.email,
full_name=entity.full_name,
password_hash=password_hash,
role=entity.role,
is_active=entity.is_active,
scopes=entity.scopes,
)
self._session.add(model)
await self._session.flush()
return _model_to_entity(model)
async def update(self, entity: TechnicalUserEntity) -> TechnicalUserEntity:
stmt = update(TechnicalUserModel).where(TechnicalUserModel.id == entity.id).values(
email=entity.email,
full_name=entity.full_name,
role=entity.role,
is_active=entity.is_active,
scopes=entity.scopes,
last_login_at=entity.last_login_at,
)
await self._session.execute(stmt)
await self._session.flush()
return await self.get_by_id(entity.id) # type: ignore[return-value]
@@ -0,0 +1,40 @@
from collections.abc import AsyncGenerator
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from src.infrastructure.config.settings import settings
engine = create_async_engine(
settings.DATABASE_URL,
pool_size=settings.DATABASE_POOL_SIZE,
max_overflow=settings.DATABASE_MAX_OVERFLOW,
pool_recycle=settings.DATABASE_POOL_RECYCLE,
echo=settings.DEBUG,
pool_pre_ping=True,
)
async_session_factory = async_sessionmaker(
engine,
class_=AsyncSession,
expire_on_commit=False,
)
async def get_db_session() -> AsyncGenerator[AsyncSession, None]:
async with async_session_factory() as session:
try:
yield session
await session.commit()
except Exception:
await session.rollback()
raise
finally:
await session.close()
async def get_db_session_no_commit() -> AsyncGenerator[AsyncSession, None]:
async with async_session_factory() as session:
try:
yield session
finally:
await session.close()