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,51 @@
from .entities import (
APIClientEntity,
APIKeyEntity,
AuditLogEntity,
ConnectorConfigEntity,
ConnectorEntity,
ConnectorStatus,
ConnectorVersionEntity,
ERPType,
ERPSystemEntity,
InventoryHistoryEntity,
InventoryRecordEntity,
MedicationCatalogEntryEntity,
MedicationEntity,
PharmacyEntity,
PharmacyStatus,
ReservationEntity,
ReservationStatus,
SyncJobEntity,
SyncJobStatus,
SyncLogEntity,
SyncStrategy,
TechnicalUserEntity,
UserRole,
)
__all__ = [
"APIClientEntity",
"APIKeyEntity",
"AuditLogEntity",
"ConnectorConfigEntity",
"ConnectorEntity",
"ConnectorStatus",
"ConnectorVersionEntity",
"ERPType",
"ERPSystemEntity",
"InventoryHistoryEntity",
"InventoryRecordEntity",
"MedicationCatalogEntryEntity",
"MedicationEntity",
"PharmacyEntity",
"PharmacyStatus",
"ReservationEntity",
"ReservationStatus",
"SyncJobEntity",
"SyncJobStatus",
"SyncLogEntity",
"SyncStrategy",
"TechnicalUserEntity",
"UserRole",
]
@@ -0,0 +1,317 @@
import uuid
from datetime import datetime
from enum import Enum
from pydantic import BaseModel, Field
class PharmacyStatus(str, Enum):
ACTIVE = "active"
INACTIVE = "inactive"
SUSPENDED = "suspended"
PENDING = "pending"
class ERPType(str, Enum):
FARMATIC = "farmatic"
NIXFARMA = "nixfarma"
UNYCOP = "unycop"
CUSTOM_REST = "custom_rest"
CUSTOM_SOAP = "custom_soap"
CSV = "csv"
FTP = "ftp"
SQL = "sql"
MOCK = "mock"
class ConnectorStatus(str, Enum):
ACTIVE = "active"
INACTIVE = "inactive"
ERROR = "error"
PENDING = "pending"
UPDATING = "updating"
class SyncStrategy(str, Enum):
FULL = "full"
INCREMENTAL = "incremental"
EVENT_DRIVEN = "event_driven"
SCHEDULED = "scheduled"
HYBRID = "hybrid"
class SyncJobStatus(str, Enum):
PENDING = "pending"
RUNNING = "running"
COMPLETED = "completed"
FAILED = "failed"
CANCELLED = "cancelled"
class ReservationStatus(str, Enum):
PENDING = "pending"
CONFIRMED = "confirmed"
CANCELLED = "cancelled"
EXPIRED = "expired"
class UserRole(str, Enum):
SUPERADMIN = "superadmin"
ADMIN = "admin"
OPERATOR = "operator"
VIEWER = "viewer"
API_CLIENT = "api_client"
class PharmacyEntity(BaseModel):
id: uuid.UUID | None = None
code: str
name: str
address: str | None = None
city: str | None = None
province: str | None = None
postal_code: str | None = None
country: str = "ES"
latitude: float | None = None
longitude: float | None = None
phone: str | None = None
email: str | None = None
opening_hours: dict | None = None
status: PharmacyStatus = PharmacyStatus.PENDING
erp_type: ERPType | None = None
erp_version: str | None = None
connector_id: uuid.UUID | None = None
sync_strategy: SyncStrategy = SyncStrategy.INCREMENTAL
sync_interval_minutes: int = 30
last_sync_at: datetime | None = None
last_sync_status: SyncJobStatus | None = None
metadata_: dict | None = Field(default=None, alias="metadata")
created_at: datetime | None = None
updated_at: datetime | None = None
model_config = {"populate_by_name": True}
class ERPSystemEntity(BaseModel):
id: uuid.UUID | None = None
name: str
erp_type: ERPType
version: str | None = None
base_url: str | None = None
auth_config: dict | None = None
status: ConnectorStatus = ConnectorStatus.PENDING
metadata_: dict | None = Field(default=None, alias="metadata")
created_at: datetime | None = None
updated_at: datetime | None = None
model_config = {"populate_by_name": True}
class ConnectorEntity(BaseModel):
id: uuid.UUID | None = None
erp_system_id: uuid.UUID
pharmacy_id: uuid.UUID
name: str
connector_type: ERPType
version: str = "1.0.0"
config: dict | None = None
status: ConnectorStatus = ConnectorStatus.PENDING
last_heartbeat_at: datetime | None = None
last_error: str | None = None
metadata_: dict | None = Field(default=None, alias="metadata")
created_at: datetime | None = None
updated_at: datetime | None = None
model_config = {"populate_by_name": True}
class ConnectorVersionEntity(BaseModel):
id: uuid.UUID | None = None
connector_id: uuid.UUID | None = None
connector_type: ERPType
version: str
changelog: str | None = None
download_url: str | None = None
checksum: str | None = None
is_latest: bool = False
is_required: bool = False
released_at: datetime | None = None
created_at: datetime | None = None
class ConnectorConfigEntity(BaseModel):
id: uuid.UUID | None = None
connector_id: uuid.UUID
key: str
value: str
encrypted: bool = False
description: str | None = None
created_at: datetime | None = None
updated_at: datetime | None = None
class MedicationEntity(BaseModel):
id: uuid.UUID | None = None
nregistro: str
name: str
active_ingredient: str | None = None
dosage: str | None = None
form: str | None = None
atc_code: str | None = None
prescription_required: bool = False
manufacturer: str | None = None
metadata_: dict | None = Field(default=None, alias="metadata")
created_at: datetime | None = None
updated_at: datetime | None = None
model_config = {"populate_by_name": True}
class MedicationCatalogEntryEntity(BaseModel):
id: uuid.UUID | None = None
pharmacy_id: uuid.UUID
medication_id: uuid.UUID
price: float | None = None
stock: int = 0
is_available: bool = False
last_verified_at: datetime | None = None
metadata_: dict | None = Field(default=None, alias="metadata")
created_at: datetime | None = None
updated_at: datetime | None = None
model_config = {"populate_by_name": True}
class InventoryRecordEntity(BaseModel):
id: uuid.UUID | None = None
pharmacy_id: uuid.UUID
medication_id: uuid.UUID
stock: int = 0
reserved_stock: int = 0
available_stock: int = 0
min_stock: int | None = None
max_stock: int | None = None
last_updated_at: datetime | None = None
source: str | None = None
batch_id: str | None = None
metadata_: dict | None = Field(default=None, alias="metadata")
created_at: datetime | None = None
updated_at: datetime | None = None
model_config = {"populate_by_name": True}
class InventoryHistoryEntity(BaseModel):
id: uuid.UUID | None = None
inventory_record_id: uuid.UUID | None = None
pharmacy_id: uuid.UUID
medication_id: uuid.UUID
previous_stock: int
new_stock: int
delta: int
reason: str | None = None
source: str | None = None
created_at: datetime | None = None
class ReservationEntity(BaseModel):
id: uuid.UUID | None = None
pharmacy_id: uuid.UUID
medication_id: uuid.UUID
user_id: uuid.UUID | None = None
external_reference: str | None = None
quantity: int
status: ReservationStatus = ReservationStatus.PENDING
expires_at: datetime | None = None
confirmed_at: datetime | None = None
cancelled_at: datetime | None = None
idempotency_key: str | None = None
metadata_: dict | None = Field(default=None, alias="metadata")
created_at: datetime | None = None
updated_at: datetime | None = None
model_config = {"populate_by_name": True}
class SyncJobEntity(BaseModel):
id: uuid.UUID | None = None
pharmacy_id: uuid.UUID
connector_id: uuid.UUID
strategy: SyncStrategy
status: SyncJobStatus = SyncJobStatus.PENDING
started_at: datetime | None = None
completed_at: datetime | None = None
items_total: int | None = None
items_processed: int | None = None
items_failed: int | None = None
error_message: str | None = None
metadata_: dict | None = Field(default=None, alias="metadata")
created_at: datetime | None = None
updated_at: datetime | None = None
model_config = {"populate_by_name": True}
class SyncLogEntity(BaseModel):
id: uuid.UUID | None = None
sync_job_id: uuid.UUID
level: str = "INFO"
message: str
details: dict | None = None
created_at: datetime | None = None
class AuditLogEntity(BaseModel):
id: uuid.UUID | None = None
actor_id: uuid.UUID | None = None
actor_type: str | None = None
action: str
resource_type: str
resource_id: str | None = None
old_value: dict | None = None
new_value: dict | None = None
ip_address: str | None = None
user_agent: str | None = None
request_id: str | None = None
created_at: datetime | None = None
class TechnicalUserEntity(BaseModel):
id: uuid.UUID | None = None
username: str
email: str | None = None
full_name: str | None = None
role: UserRole = UserRole.VIEWER
is_active: bool = True
scopes: list[str] = Field(default_factory=list)
last_login_at: datetime | None = None
created_at: datetime | None = None
updated_at: datetime | None = None
class APIClientEntity(BaseModel):
id: uuid.UUID | None = None
name: str
description: str | None = None
owner_id: uuid.UUID | None = None
is_active: bool = True
scopes: list[str] = Field(default_factory=list)
rate_limit: int | None = None
allowed_ips: list[str] = Field(default_factory=list)
metadata_: dict | None = Field(default=None, alias="metadata")
created_at: datetime | None = None
updated_at: datetime | None = None
model_config = {"populate_by_name": True}
class APIKeyEntity(BaseModel):
id: uuid.UUID | None = None
api_client_id: uuid.UUID
key_prefix: str
key_hash: bytes
name: str
is_active: bool = True
expires_at: datetime | None = None
last_used_at: datetime | None = None
created_at: datetime | None = None
@@ -0,0 +1,35 @@
from .repositories import (
IAPIClientRepository,
IAPIKeyRepository,
IAuditLogRepository,
IConnectorConfigRepository,
IConnectorRepository,
IConnectorVersionRepository,
IERPSystemRepository,
IInventoryRepository,
IMedicationCatalogRepository,
IMedicationRepository,
IPharmacyRepository,
IReservationRepository,
ISyncJobRepository,
ISyncLogRepository,
ITechnicalUserRepository,
)
__all__ = [
"IAPIClientRepository",
"IAPIKeyRepository",
"IAuditLogRepository",
"IConnectorConfigRepository",
"IConnectorRepository",
"IConnectorVersionRepository",
"IERPSystemRepository",
"IInventoryRepository",
"IMedicationCatalogRepository",
"IMedicationRepository",
"IPharmacyRepository",
"IReservationRepository",
"ISyncJobRepository",
"ISyncLogRepository",
"ITechnicalUserRepository",
]
@@ -0,0 +1,249 @@
import uuid
from abc import ABC, abstractmethod
from collections.abc import Sequence
from typing import Any
from src.domain.entities import (
APIClientEntity,
APIKeyEntity,
AuditLogEntity,
ConnectorConfigEntity,
ConnectorEntity,
ConnectorVersionEntity,
ERPSystemEntity,
InventoryHistoryEntity,
InventoryRecordEntity,
MedicationCatalogEntryEntity,
MedicationEntity,
PharmacyEntity,
ReservationEntity,
SyncJobEntity,
SyncLogEntity,
TechnicalUserEntity,
)
class IPharmacyRepository(ABC):
@abstractmethod
async def get_by_id(self, pharmacy_id: uuid.UUID) -> PharmacyEntity | None: ...
@abstractmethod
async def get_by_code(self, code: str) -> PharmacyEntity | None: ...
@abstractmethod
async def list_pharmacies(
self,
*,
status: str | None = None,
erp_type: str | None = None,
offset: int = 0,
limit: int = 50,
) -> tuple[Sequence[PharmacyEntity], int]: ...
@abstractmethod
async def create(self, entity: PharmacyEntity) -> PharmacyEntity: ...
@abstractmethod
async def update(self, entity: PharmacyEntity) -> PharmacyEntity: ...
@abstractmethod
async def delete(self, pharmacy_id: uuid.UUID) -> bool: ...
class IERPSystemRepository(ABC):
@abstractmethod
async def get_by_id(self, erp_id: uuid.UUID) -> ERPSystemEntity | None: ...
@abstractmethod
async def list_erp_systems(self, *, erp_type: str | None = None, offset: int = 0, limit: int = 50) -> tuple[Sequence[ERPSystemEntity], int]: ...
@abstractmethod
async def create(self, entity: ERPSystemEntity) -> ERPSystemEntity: ...
@abstractmethod
async def update(self, entity: ERPSystemEntity) -> ERPSystemEntity: ...
@abstractmethod
async def delete(self, erp_id: uuid.UUID) -> bool: ...
class IConnectorRepository(ABC):
@abstractmethod
async def get_by_id(self, connector_id: uuid.UUID) -> ConnectorEntity | None: ...
@abstractmethod
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]: ...
@abstractmethod
async def create(self, entity: ConnectorEntity) -> ConnectorEntity: ...
@abstractmethod
async def update(self, entity: ConnectorEntity) -> ConnectorEntity: ...
@abstractmethod
async def delete(self, connector_id: uuid.UUID) -> bool: ...
class IConnectorVersionRepository(ABC):
@abstractmethod
async def get_latest(self, connector_type: str) -> ConnectorVersionEntity | None: ...
@abstractmethod
async def list_versions(self, *, connector_type: str | None = None, offset: int = 0, limit: int = 50) -> tuple[Sequence[ConnectorVersionEntity], int]: ...
@abstractmethod
async def create(self, entity: ConnectorVersionEntity) -> ConnectorVersionEntity: ...
class IConnectorConfigRepository(ABC):
@abstractmethod
async def list_by_connector(self, connector_id: uuid.UUID) -> Sequence[ConnectorConfigEntity]: ...
@abstractmethod
async def get_by_key(self, connector_id: uuid.UUID, key: str) -> ConnectorConfigEntity | None: ...
@abstractmethod
async def upsert(self, entity: ConnectorConfigEntity) -> ConnectorConfigEntity: ...
@abstractmethod
async def delete(self, config_id: uuid.UUID) -> bool: ...
class IMedicationRepository(ABC):
@abstractmethod
async def get_by_id(self, medication_id: uuid.UUID) -> MedicationEntity | None: ...
@abstractmethod
async def get_by_nregistro(self, nregistro: str) -> MedicationEntity | None: ...
@abstractmethod
async def search(self, query: str, *, offset: int = 0, limit: int = 50) -> tuple[Sequence[MedicationEntity], int]: ...
@abstractmethod
async def create(self, entity: MedicationEntity) -> MedicationEntity: ...
@abstractmethod
async def update(self, entity: MedicationEntity) -> MedicationEntity: ...
@abstractmethod
async def delete(self, medication_id: uuid.UUID) -> bool: ...
class IMedicationCatalogRepository(ABC):
@abstractmethod
async def get_entry(self, pharmacy_id: uuid.UUID, medication_id: uuid.UUID) -> MedicationCatalogEntryEntity | None: ...
@abstractmethod
async def list_by_pharmacy(self, pharmacy_id: uuid.UUID, *, offset: int = 0, limit: int = 50) -> tuple[Sequence[MedicationCatalogEntryEntity], int]: ...
@abstractmethod
async def upsert(self, entity: MedicationCatalogEntryEntity) -> MedicationCatalogEntryEntity: ...
class IInventoryRepository(ABC):
@abstractmethod
async def get_record(self, pharmacy_id: uuid.UUID, medication_id: uuid.UUID) -> InventoryRecordEntity | None: ...
@abstractmethod
async def list_by_pharmacy(self, pharmacy_id: uuid.UUID, *, offset: int = 0, limit: int = 50) -> tuple[Sequence[InventoryRecordEntity], int]: ...
@abstractmethod
async def upsert(self, entity: InventoryRecordEntity) -> InventoryRecordEntity: ...
@abstractmethod
async def add_history(self, entity: InventoryHistoryEntity) -> InventoryHistoryEntity: ...
@abstractmethod
async def get_history(self, pharmacy_id: uuid.UUID, medication_id: uuid.UUID, *, offset: int = 0, limit: int = 50) -> Sequence[InventoryHistoryEntity]: ...
class IReservationRepository(ABC):
@abstractmethod
async def get_by_id(self, reservation_id: uuid.UUID) -> ReservationEntity | None: ...
@abstractmethod
async def get_by_idempotency_key(self, key: str) -> ReservationEntity | None: ...
@abstractmethod
async def list_by_pharmacy(self, pharmacy_id: uuid.UUID, *, status: str | None = None, offset: int = 0, limit: int = 50) -> tuple[Sequence[ReservationEntity], int]: ...
@abstractmethod
async def create(self, entity: ReservationEntity) -> ReservationEntity: ...
@abstractmethod
async def update(self, entity: ReservationEntity) -> ReservationEntity: ...
class ISyncJobRepository(ABC):
@abstractmethod
async def get_by_id(self, job_id: uuid.UUID) -> SyncJobEntity | None: ...
@abstractmethod
async def list_by_pharmacy(self, pharmacy_id: uuid.UUID, *, status: str | None = None, offset: int = 0, limit: int = 50) -> tuple[Sequence[SyncJobEntity], int]: ...
@abstractmethod
async def create(self, entity: SyncJobEntity) -> SyncJobEntity: ...
@abstractmethod
async def update(self, entity: SyncJobEntity) -> SyncJobEntity: ...
class ISyncLogRepository(ABC):
@abstractmethod
async def add(self, entity: SyncLogEntity) -> SyncLogEntity: ...
@abstractmethod
async def list_by_job(self, job_id: uuid.UUID, *, level: str | None = None, offset: int = 0, limit: int = 100) -> Sequence[SyncLogEntity]: ...
class IAuditLogRepository(ABC):
@abstractmethod
async def add(self, entity: AuditLogEntity) -> AuditLogEntity: ...
@abstractmethod
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]: ...
class ITechnicalUserRepository(ABC):
@abstractmethod
async def get_by_id(self, user_id: uuid.UUID) -> TechnicalUserEntity | None: ...
@abstractmethod
async def get_by_username(self, username: str) -> TechnicalUserEntity | None: ...
@abstractmethod
async def list_users(self, *, role: str | None = None, is_active: bool | None = None, offset: int = 0, limit: int = 50) -> tuple[Sequence[TechnicalUserEntity], int]: ...
@abstractmethod
async def create(self, entity: TechnicalUserEntity, password_hash: str) -> TechnicalUserEntity: ...
@abstractmethod
async def update(self, entity: TechnicalUserEntity) -> TechnicalUserEntity: ...
class IAPIClientRepository(ABC):
@abstractmethod
async def get_by_id(self, client_id: uuid.UUID) -> APIClientEntity | None: ...
@abstractmethod
async def list_clients(self, *, is_active: bool | None = None, offset: int = 0, limit: int = 50) -> tuple[Sequence[APIClientEntity], int]: ...
@abstractmethod
async def create(self, entity: APIClientEntity) -> APIClientEntity: ...
@abstractmethod
async def update(self, entity: APIClientEntity) -> APIClientEntity: ...
class IAPIKeyRepository(ABC):
@abstractmethod
async def get_by_prefix(self, key_prefix: str) -> APIKeyEntity | None: ...
@abstractmethod
async def list_by_client(self, api_client_id: uuid.UUID) -> Sequence[APIKeyEntity]: ...
@abstractmethod
async def create(self, entity: APIKeyEntity) -> APIKeyEntity: ...
@abstractmethod
async def update_last_used(self, key_id: uuid.UUID) -> None: ...