Restructure with Turborepo
This commit is contained in:
@@ -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
|
||||
+71
@@ -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
|
||||
+82
@@ -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]
|
||||
+90
@@ -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]
|
||||
+88
@@ -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()
|
||||
Reference in New Issue
Block a user