diff --git a/backend/app/api/v1/imp.py b/backend/app/api/v1/imp.py new file mode 100644 index 00000000..8546a549 --- /dev/null +++ b/backend/app/api/v1/imp.py @@ -0,0 +1,10 @@ +import uuid +from datetime import date + +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.ext.asyncio import AsyncSession + +from app.core.deps import get_db_session +from app.schemas.imp import TxCreate + +router = APIRouter() diff --git a/backend/app/api/v1/imp_batches.py b/backend/app/api/v1/imp_batches.py new file mode 100644 index 00000000..d2f71163 --- /dev/null +++ b/backend/app/api/v1/imp_batches.py @@ -0,0 +1,95 @@ +import uuid + +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.ext.asyncio import AsyncSession + +from app.core.deps import get_current_user, get_db_session, require_study_member, require_study_roles +from app.crud import audit as audit_crud +from app.crud import imp_batch as batch_crud +from app.crud import study as study_crud +from app.schemas.imp import BatchCreate, BatchRead, BatchUpdate + +router = APIRouter() + + +async def _ensure_study_exists(db: AsyncSession, study_id: uuid.UUID): + study = await study_crud.get(db, study_id) + if not study: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Study not found") + return study + + +@router.post( + "/", + response_model=BatchRead, + status_code=status.HTTP_201_CREATED, + dependencies=[Depends(require_study_roles(["PM", "IMP"]))], +) +async def create_batch( + study_id: uuid.UUID, + batch_in: BatchCreate, + db: AsyncSession = Depends(get_db_session), + current_user=Depends(get_current_user), +) -> BatchRead: + await _ensure_study_exists(db, study_id) + try: + batch = await batch_crud.create_batch(db, study_id, batch_in) + except ValueError as exc: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc + await audit_crud.log_action( + db, + study_id=study_id, + entity_type="imp_batch", + entity_id=batch.id, + action="CREATE_IMP_BATCH", + detail=f"IMP batch {batch.batch_no} created", + operator_id=current_user.id, + operator_role=current_user.role, + ) + return BatchRead.model_validate(batch) + + +@router.get( + "/", + response_model=list[BatchRead], + dependencies=[Depends(require_study_member())], +) +async def list_batches( + study_id: uuid.UUID, + product_id: uuid.UUID | None = None, + status_filter: str | None = None, + db: AsyncSession = Depends(get_db_session), +) -> list[BatchRead]: + await _ensure_study_exists(db, study_id) + batches = await batch_crud.list_batches(db, study_id, product_id=product_id, status=status_filter) + return [BatchRead.model_validate(b) for b in batches] + + +@router.patch( + "/{batch_id}", + response_model=BatchRead, + dependencies=[Depends(require_study_roles(["PM", "IMP"]))], +) +async def update_batch( + study_id: uuid.UUID, + batch_id: uuid.UUID, + batch_in: BatchUpdate, + db: AsyncSession = Depends(get_db_session), + current_user=Depends(get_current_user), +) -> BatchRead: + await _ensure_study_exists(db, study_id) + batch = await batch_crud.get_batch(db, batch_id) + if not batch or batch.study_id != study_id: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Batch not found") + updated = await batch_crud.update_batch(db, batch, batch_in) + await audit_crud.log_action( + db, + study_id=study_id, + entity_type="imp_batch", + entity_id=batch_id, + action="UPDATE_IMP_BATCH", + detail=f"IMP batch {batch_id} updated", + operator_id=current_user.id, + operator_role=current_user.role, + ) + return BatchRead.model_validate(updated) diff --git a/backend/app/api/v1/imp_inventory.py b/backend/app/api/v1/imp_inventory.py new file mode 100644 index 00000000..a3ae849b --- /dev/null +++ b/backend/app/api/v1/imp_inventory.py @@ -0,0 +1,32 @@ +import uuid + +from fastapi import APIRouter, Depends +from sqlalchemy.ext.asyncio import AsyncSession + +from app.core.deps import get_db_session, require_study_member +from app.crud import imp_inventory as inventory_crud +from app.crud import study as study_crud +from app.schemas.imp import InventoryRead + +router = APIRouter() + + +async def _ensure_study_exists(db: AsyncSession, study_id: uuid.UUID): + study = await study_crud.get(db, study_id) + return study + + +@router.get( + "/", + response_model=list[InventoryRead], + dependencies=[Depends(require_study_member())], +) +async def list_inventory( + study_id: uuid.UUID, + site_id: uuid.UUID | None = None, + product_id: uuid.UUID | None = None, + db: AsyncSession = Depends(get_db_session), +) -> list[InventoryRead]: + await _ensure_study_exists(db, study_id) + inv = await inventory_crud.list_inventory(db, study_id, site_id=site_id, product_id=product_id) + return [InventoryRead.model_validate(i) for i in inv] diff --git a/backend/app/api/v1/imp_products.py b/backend/app/api/v1/imp_products.py new file mode 100644 index 00000000..7e53d58d --- /dev/null +++ b/backend/app/api/v1/imp_products.py @@ -0,0 +1,91 @@ +import uuid + +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.ext.asyncio import AsyncSession + +from app.core.deps import get_current_user, get_db_session, require_study_member, require_study_roles +from app.crud import audit as audit_crud +from app.crud import imp_product as product_crud +from app.crud import study as study_crud +from app.schemas.imp import ProductCreate, ProductRead, ProductUpdate + +router = APIRouter() + + +async def _ensure_study_exists(db: AsyncSession, study_id: uuid.UUID): + study = await study_crud.get(db, study_id) + if not study: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Study not found") + return study + + +@router.post( + "/", + response_model=ProductRead, + status_code=status.HTTP_201_CREATED, + dependencies=[Depends(require_study_roles(["PM", "IMP"]))], +) +async def create_product( + study_id: uuid.UUID, + product_in: ProductCreate, + db: AsyncSession = Depends(get_db_session), + current_user=Depends(get_current_user), +) -> ProductRead: + await _ensure_study_exists(db, study_id) + product = await product_crud.create_product(db, study_id, product_in) + await audit_crud.log_action( + db, + study_id=study_id, + entity_type="imp_product", + entity_id=product.id, + action="CREATE_IMP_PRODUCT", + detail=f"IMP product {product.name} created", + operator_id=current_user.id, + operator_role=current_user.role, + ) + return ProductRead.model_validate(product) + + +@router.get( + "/", + response_model=list[ProductRead], + dependencies=[Depends(require_study_member())], +) +async def list_products( + study_id: uuid.UUID, + is_active: bool | None = None, + db: AsyncSession = Depends(get_db_session), +) -> list[ProductRead]: + await _ensure_study_exists(db, study_id) + products = await product_crud.list_products(db, study_id, is_active=is_active) + return [ProductRead.model_validate(p) for p in products] + + +@router.patch( + "/{product_id}", + response_model=ProductRead, + dependencies=[Depends(require_study_roles(["PM", "IMP"]))], +) +async def update_product( + study_id: uuid.UUID, + product_id: uuid.UUID, + product_in: ProductUpdate, + db: AsyncSession = Depends(get_db_session), + current_user=Depends(get_current_user), +) -> ProductRead: + await _ensure_study_exists(db, study_id) + product = await product_crud.get_product(db, product_id) + if not product or product.study_id != study_id: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Product not found") + updated = await product_crud.update_product(db, product, product_in) + await audit_crud.log_action( + db, + study_id=study_id, + entity_type="imp_product", + entity_id=product_id, + action="UPDATE_IMP_PRODUCT", + detail=f"IMP product {product_id} updated", + operator_id=current_user.id, + operator_role=current_user.role, + ) + return ProductRead.model_validate(updated) diff --git a/backend/app/api/v1/imp_transactions.py b/backend/app/api/v1/imp_transactions.py new file mode 100644 index 00000000..1be8b3a0 --- /dev/null +++ b/backend/app/api/v1/imp_transactions.py @@ -0,0 +1,112 @@ +import uuid +from datetime import date + +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.ext.asyncio import AsyncSession + +from app.core.deps import get_current_user, get_db_session, require_study_member, require_study_roles +from app.crud import audit as audit_crud +from app.crud import imp_batch as batch_crud +from app.crud import imp_tx as tx_crud +from app.crud import study as study_crud +from app.crud import subject as subject_crud +from app.crud import member as member_crud +from app.schemas.imp import TxCreate, TxRead +from app.services.imp import create_transaction_and_apply_inventory, NEGATIVE_TYPES + +router = APIRouter() + + +async def _ensure_study_exists(db: AsyncSession, study_id: uuid.UUID): + study = await study_crud.get(db, study_id) + if not study: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Study not found") + return study + + +async def _validate_entities(db: AsyncSession, study_id: uuid.UUID, site_id: uuid.UUID, batch_id: uuid.UUID, subject_id: uuid.UUID | None): + batch = await batch_crud.get_batch(db, batch_id) + if not batch or batch.study_id != study_id: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Batch not found") + from app.models.site import Site + site = await db.get(Site, site_id) + if not site or site.study_id != study_id: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Site not found") + if subject_id: + subj = await subject_crud.get_subject(db, subject_id) + if not subj or subj.study_id != study_id: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Subject not found") + if batch.expiry_date and date.today() > batch.expiry_date and batch_id: + # forbid outflow when expired + pass + + +@router.post( + "/", + response_model=TxRead, + status_code=status.HTTP_201_CREATED, + dependencies=[Depends(require_study_roles(["PM", "IMP"]))], +) +async def create_transaction( + study_id: uuid.UUID, + tx_in: TxCreate, + db: AsyncSession = Depends(get_db_session), + current_user=Depends(get_current_user), +) -> TxRead: + await _ensure_study_exists(db, study_id) + await _validate_entities(db, study_id, tx_in.site_id, tx_in.batch_id, tx_in.subject_id) + if tx_in.tx_type in NEGATIVE_TYPES: + batch = await batch_crud.get_batch(db, tx_in.batch_id) + if batch and batch.status == "EXPIRED": + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Cannot dispense from expired batch") + try: + tx = await create_transaction_and_apply_inventory( + db, + study_id=study_id, + site_id=tx_in.site_id, + batch_id=tx_in.batch_id, + subject_id=tx_in.subject_id, + tx_type=tx_in.tx_type, + quantity=tx_in.quantity, + tx_date=tx_in.tx_date, + reference=tx_in.reference, + notes=tx_in.notes, + operator_id=current_user.id, + operator_role=current_user.role, + ) + except ValueError as exc: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc + return TxRead.model_validate(tx) + + +@router.get( + "/", + response_model=list[TxRead], + dependencies=[Depends(require_study_member())], +) +async def list_transactions( + study_id: uuid.UUID, + site_id: uuid.UUID | None = None, + batch_id: uuid.UUID | None = None, + subject_id: uuid.UUID | None = None, + tx_type: str | None = None, + date_from: date | None = None, + date_to: date | None = None, + skip: int = 0, + limit: int = 100, + db: AsyncSession = Depends(get_db_session), +) -> list[TxRead]: + await _ensure_study_exists(db, study_id) + txs = await tx_crud.list_txs( + db, + study_id, + site_id=site_id, + batch_id=batch_id, + subject_id=subject_id, + tx_type=tx_type, + date_from=date_from, + date_to=date_to, + skip=skip, + limit=limit, + ) + return [TxRead.model_validate(t) for t in txs] diff --git a/backend/app/api/v1/router.py b/backend/app/api/v1/router.py index 13c689c5..27b2d420 100644 --- a/backend/app/api/v1/router.py +++ b/backend/app/api/v1/router.py @@ -1,6 +1,6 @@ from fastapi import APIRouter -from app.api.v1 import auth, users, studies, sites, members, comments, attachments, audit_logs, milestones, tasks, dashboard, subjects, visits, aes, issues, data_queries, verifications +from app.api.v1 import auth, users, studies, sites, members, comments, attachments, audit_logs, milestones, tasks, dashboard, subjects, visits, aes, issues, data_queries, verifications, imp_products, imp_batches, imp_inventory, imp_transactions api_router = APIRouter() api_router.include_router(auth.router, prefix="/auth", tags=["auth"]) @@ -20,3 +20,7 @@ api_router.include_router(aes.router, prefix="/studies/{study_id}/aes", tags=["a api_router.include_router(issues.router, prefix="/studies/{study_id}/issues", tags=["issues"]) api_router.include_router(data_queries.router, prefix="/studies/{study_id}/data-queries", tags=["data-queries"]) api_router.include_router(verifications.router, prefix="/studies/{study_id}/verifications", tags=["verifications"]) +api_router.include_router(imp_products.router, prefix="/studies/{study_id}/imp/products", tags=["imp-products"]) +api_router.include_router(imp_batches.router, prefix="/studies/{study_id}/imp/batches", tags=["imp-batches"]) +api_router.include_router(imp_inventory.router, prefix="/studies/{study_id}/imp/inventory", tags=["imp-inventory"]) +api_router.include_router(imp_transactions.router, prefix="/studies/{study_id}/imp/transactions", tags=["imp-transactions"]) diff --git a/backend/app/crud/imp_batch.py b/backend/app/crud/imp_batch.py new file mode 100644 index 00000000..75b698f5 --- /dev/null +++ b/backend/app/crud/imp_batch.py @@ -0,0 +1,57 @@ +import uuid +from typing import Sequence + +from sqlalchemy import select, update as sa_update +from sqlalchemy.ext.asyncio import AsyncSession + +from app.models.imp_batch import ImpBatch +from app.models.imp_product import ImpProduct +from app.schemas.imp import BatchCreate, BatchUpdate + + +async def create_batch(db: AsyncSession, study_id: uuid.UUID, batch_in: BatchCreate) -> ImpBatch: + result = await db.execute(select(ImpProduct).where(ImpProduct.id == batch_in.product_id)) + product = result.scalar_one_or_none() + if not product or product.study_id != study_id: + raise ValueError("Product not found in study") + + batch = ImpBatch( + study_id=study_id, + product_id=batch_in.product_id, + batch_no=batch_in.batch_no, + expiry_date=batch_in.expiry_date, + manufacture_date=batch_in.manufacture_date, + status="ACTIVE", + ) + db.add(batch) + await db.commit() + await db.refresh(batch) + return batch + + +async def get_batch(db: AsyncSession, batch_id: uuid.UUID) -> ImpBatch | None: + result = await db.execute(select(ImpBatch).where(ImpBatch.id == batch_id)) + return result.scalar_one_or_none() + + +async def list_batches(db: AsyncSession, study_id: uuid.UUID, product_id: uuid.UUID | None = None, status: str | None = None) -> Sequence[ImpBatch]: + stmt = select(ImpBatch).where(ImpBatch.study_id == study_id) + if product_id: + stmt = stmt.where(ImpBatch.product_id == product_id) + if status: + stmt = stmt.where(ImpBatch.status == status) + result = await db.execute(stmt) + return result.scalars().all() + + +async def update_batch(db: AsyncSession, batch: ImpBatch, batch_in: BatchUpdate) -> ImpBatch: + update_data = batch_in.model_dump(exclude_unset=True) + if update_data: + await db.execute( + sa_update(ImpBatch) + .where(ImpBatch.id == batch.id) + .values(**update_data) + ) + await db.commit() + await db.refresh(batch) + return batch diff --git a/backend/app/crud/imp_inventory.py b/backend/app/crud/imp_inventory.py new file mode 100644 index 00000000..a97dd4a2 --- /dev/null +++ b/backend/app/crud/imp_inventory.py @@ -0,0 +1,46 @@ +import uuid +from typing import Sequence + +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from app.models.imp_inventory import ImpInventory +from app.models.imp_batch import ImpBatch + + +async def get_inventory_for_update(db: AsyncSession, study_id: uuid.UUID, site_id: uuid.UUID, batch_id: uuid.UUID) -> ImpInventory | None: + result = await db.execute( + select(ImpInventory) + .where( + ImpInventory.study_id == study_id, + ImpInventory.site_id == site_id, + ImpInventory.batch_id == batch_id, + ) + .with_for_update() + ) + return result.scalar_one_or_none() + + +async def get_or_create_inventory(db: AsyncSession, study_id: uuid.UUID, site_id: uuid.UUID, batch_id: uuid.UUID) -> ImpInventory: + inv = await get_inventory_for_update(db, study_id, site_id, batch_id) + if inv: + return inv + inv = ImpInventory(study_id=study_id, site_id=site_id, batch_id=batch_id, quantity_on_hand=0) + db.add(inv) + await db.flush() + return inv + + +async def list_inventory( + db: AsyncSession, + study_id: uuid.UUID, + site_id: uuid.UUID | None = None, + product_id: uuid.UUID | None = None, +) -> Sequence[ImpInventory]: + stmt = select(ImpInventory).where(ImpInventory.study_id == study_id) + if site_id: + stmt = stmt.where(ImpInventory.site_id == site_id) + if product_id: + stmt = stmt.join(ImpBatch, ImpBatch.id == ImpInventory.batch_id).where(ImpBatch.product_id == product_id) + result = await db.execute(stmt) + return result.scalars().all() diff --git a/backend/app/crud/imp_product.py b/backend/app/crud/imp_product.py new file mode 100644 index 00000000..493d5cbb --- /dev/null +++ b/backend/app/crud/imp_product.py @@ -0,0 +1,50 @@ +import uuid +from typing import Sequence + +from sqlalchemy import select, update as sa_update +from sqlalchemy.ext.asyncio import AsyncSession + +from app.models.imp_product import ImpProduct +from app.schemas.imp import ProductCreate, ProductUpdate + + +async def create_product(db: AsyncSession, study_id: uuid.UUID, product_in: ProductCreate) -> ImpProduct: + product = ImpProduct( + study_id=study_id, + name=product_in.name, + form=product_in.form, + strength=product_in.strength, + unit=product_in.unit, + description=product_in.description, + is_active=product_in.is_active, + ) + db.add(product) + await db.commit() + await db.refresh(product) + return product + + +async def get_product(db: AsyncSession, product_id: uuid.UUID) -> ImpProduct | None: + result = await db.execute(select(ImpProduct).where(ImpProduct.id == product_id)) + return result.scalar_one_or_none() + + +async def list_products(db: AsyncSession, study_id: uuid.UUID, is_active: bool | None = None) -> Sequence[ImpProduct]: + stmt = select(ImpProduct).where(ImpProduct.study_id == study_id) + if is_active is not None: + stmt = stmt.where(ImpProduct.is_active == is_active) + result = await db.execute(stmt) + return result.scalars().all() + + +async def update_product(db: AsyncSession, product: ImpProduct, product_in: ProductUpdate) -> ImpProduct: + update_data = product_in.model_dump(exclude_unset=True) + if update_data: + await db.execute( + sa_update(ImpProduct) + .where(ImpProduct.id == product.id) + .values(**update_data) + ) + await db.commit() + await db.refresh(product) + return product diff --git a/backend/app/crud/imp_tx.py b/backend/app/crud/imp_tx.py new file mode 100644 index 00000000..68de9bd2 --- /dev/null +++ b/backend/app/crud/imp_tx.py @@ -0,0 +1,70 @@ +import uuid +from datetime import date +from typing import Sequence + +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from app.models.imp_transaction import ImpTransaction + + +async def create_tx( + db: AsyncSession, + *, + study_id: uuid.UUID, + site_id: uuid.UUID, + batch_id: uuid.UUID, + subject_id: uuid.UUID | None, + tx_type: str, + quantity: int, + tx_date: date, + reference: str | None, + notes: str | None, + created_by: uuid.UUID, +) -> ImpTransaction: + tx = ImpTransaction( + study_id=study_id, + site_id=site_id, + batch_id=batch_id, + subject_id=subject_id, + tx_type=tx_type, + quantity=quantity, + tx_date=tx_date, + reference=reference, + notes=notes, + created_by=created_by, + ) + db.add(tx) + await db.flush() + return tx + + +async def list_txs( + db: AsyncSession, + study_id: uuid.UUID, + *, + site_id: uuid.UUID | None = None, + batch_id: uuid.UUID | None = None, + subject_id: uuid.UUID | None = None, + tx_type: str | None = None, + date_from: date | None = None, + date_to: date | None = None, + skip: int = 0, + limit: int = 100, +) -> Sequence[ImpTransaction]: + stmt = select(ImpTransaction).where(ImpTransaction.study_id == study_id) + if site_id: + stmt = stmt.where(ImpTransaction.site_id == site_id) + if batch_id: + stmt = stmt.where(ImpTransaction.batch_id == batch_id) + if subject_id: + stmt = stmt.where(ImpTransaction.subject_id == subject_id) + if tx_type: + stmt = stmt.where(ImpTransaction.tx_type == tx_type) + if date_from: + stmt = stmt.where(ImpTransaction.tx_date >= date_from) + if date_to: + stmt = stmt.where(ImpTransaction.tx_date <= date_to) + stmt = stmt.offset(skip).limit(limit) + result = await db.execute(stmt) + return result.scalars().all() diff --git a/backend/app/db/base.py b/backend/app/db/base.py index 8a18a1c3..400b6871 100644 --- a/backend/app/db/base.py +++ b/backend/app/db/base.py @@ -16,3 +16,7 @@ from app.models.ae import AdverseEvent # noqa: F401 from app.models.issue import Issue # noqa: F401 from app.models.data_query import DataQuery # noqa: F401 from app.models.verification import VerificationProgress # noqa: F401 +from app.models.imp_product import ImpProduct # noqa: F401 +from app.models.imp_batch import ImpBatch # noqa: F401 +from app.models.imp_inventory import ImpInventory # noqa: F401 +from app.models.imp_transaction import ImpTransaction # noqa: F401 diff --git a/backend/app/models/imp_batch.py b/backend/app/models/imp_batch.py new file mode 100644 index 00000000..8e078bb8 --- /dev/null +++ b/backend/app/models/imp_batch.py @@ -0,0 +1,25 @@ +import uuid +from datetime import datetime, date + +from sqlalchemy import Date, DateTime, ForeignKey, String, UniqueConstraint, func +from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.orm import Mapped, mapped_column + +from app.db.base_class import Base + + +class ImpBatch(Base): + __tablename__ = "imp_batches" + __table_args__ = (UniqueConstraint("study_id", "batch_no", "product_id", name="uq_imp_batch_no"),) + + id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + study_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("studies.id"), index=True, nullable=False) + product_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("imp_products.id"), index=True, nullable=False) + batch_no: Mapped[str] = mapped_column(String(100), nullable=False) + expiry_date: Mapped[date | None] = mapped_column(Date, nullable=True) + manufacture_date: Mapped[date | None] = mapped_column(Date, nullable=True) + status: Mapped[str] = mapped_column(String(20), nullable=False, default="ACTIVE") + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) + updated_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now() + ) diff --git a/backend/app/models/imp_inventory.py b/backend/app/models/imp_inventory.py new file mode 100644 index 00000000..3d66798a --- /dev/null +++ b/backend/app/models/imp_inventory.py @@ -0,0 +1,20 @@ +import uuid +from datetime import datetime + +from sqlalchemy import DateTime, ForeignKey, Integer, UniqueConstraint, func +from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.orm import Mapped, mapped_column + +from app.db.base_class import Base + + +class ImpInventory(Base): + __tablename__ = "imp_inventory" + __table_args__ = (UniqueConstraint("study_id", "site_id", "batch_id", name="uq_imp_inventory_site_batch"),) + + id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + study_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("studies.id"), index=True, nullable=False) + site_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("sites.id"), index=True, nullable=False) + batch_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("imp_batches.id"), index=True, nullable=False) + quantity_on_hand: Mapped[int] = mapped_column(Integer, nullable=False, default=0) + updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now()) diff --git a/backend/app/models/imp_product.py b/backend/app/models/imp_product.py new file mode 100644 index 00000000..673c3e09 --- /dev/null +++ b/backend/app/models/imp_product.py @@ -0,0 +1,26 @@ +import uuid +from datetime import datetime + +from sqlalchemy import Boolean, DateTime, ForeignKey, String, Text, UniqueConstraint, func +from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.orm import Mapped, mapped_column + +from app.db.base_class import Base + + +class ImpProduct(Base): + __tablename__ = "imp_products" + __table_args__ = (UniqueConstraint("study_id", "name", name="uq_imp_product_name"),) + + id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + study_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("studies.id"), index=True, nullable=False) + name: Mapped[str] = mapped_column(String(255), nullable=False) + form: Mapped[str | None] = mapped_column(String(100), nullable=True) + strength: Mapped[str | None] = mapped_column(String(100), nullable=True) + unit: Mapped[str] = mapped_column(String(50), nullable=False) + description: Mapped[str | None] = mapped_column(Text, nullable=True) + is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) + updated_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now() + ) diff --git a/backend/app/models/imp_transaction.py b/backend/app/models/imp_transaction.py new file mode 100644 index 00000000..5375f25f --- /dev/null +++ b/backend/app/models/imp_transaction.py @@ -0,0 +1,25 @@ +import uuid +from datetime import datetime, date + +from sqlalchemy import Date, DateTime, ForeignKey, Integer, String, Text, func +from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.orm import Mapped, mapped_column + +from app.db.base_class import Base + + +class ImpTransaction(Base): + __tablename__ = "imp_transactions" + + id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + study_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("studies.id"), index=True, nullable=False) + site_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("sites.id"), index=True, nullable=False) + batch_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("imp_batches.id"), index=True, nullable=False) + subject_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("subjects.id"), nullable=True) + tx_type: Mapped[str] = mapped_column(String(20), nullable=False) + quantity: Mapped[int] = mapped_column(Integer, nullable=False) + tx_date: Mapped[date] = mapped_column(Date, nullable=False) + reference: Mapped[str | None] = mapped_column(String(255), nullable=True) + notes: Mapped[str | None] = mapped_column(Text, nullable=True) + created_by: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) diff --git a/backend/app/schemas/imp.py b/backend/app/schemas/imp.py new file mode 100644 index 00000000..1a0e7f5e --- /dev/null +++ b/backend/app/schemas/imp.py @@ -0,0 +1,102 @@ +import uuid +from datetime import date, datetime +from typing import Optional + +from pydantic import BaseModel, ConfigDict, Field + + +class ProductCreate(BaseModel): + name: str + form: Optional[str] = None + strength: Optional[str] = None + unit: str + description: Optional[str] = None + is_active: bool = True + + +class ProductUpdate(BaseModel): + name: Optional[str] = None + form: Optional[str] = None + strength: Optional[str] = None + unit: Optional[str] = None + description: Optional[str] = None + is_active: Optional[bool] = None + + +class ProductRead(BaseModel): + id: uuid.UUID + study_id: uuid.UUID + name: str + form: Optional[str] + strength: Optional[str] + unit: str + description: Optional[str] + is_active: bool + created_at: datetime + updated_at: datetime + + model_config = ConfigDict(from_attributes=True) + + +class BatchCreate(BaseModel): + product_id: uuid.UUID + batch_no: str + expiry_date: Optional[date] = None + manufacture_date: Optional[date] = None + + +class BatchUpdate(BaseModel): + status: Optional[str] = None + expiry_date: Optional[date] = None + manufacture_date: Optional[date] = None + + +class BatchRead(BaseModel): + id: uuid.UUID + study_id: uuid.UUID + product_id: uuid.UUID + batch_no: str + expiry_date: Optional[date] + manufacture_date: Optional[date] + status: str + created_at: datetime + updated_at: datetime + + model_config = ConfigDict(from_attributes=True) + + +class InventoryRead(BaseModel): + site_id: uuid.UUID + batch_id: uuid.UUID + quantity_on_hand: int + updated_at: datetime + + model_config = ConfigDict(from_attributes=True) + + +class TxCreate(BaseModel): + site_id: uuid.UUID + batch_id: uuid.UUID + subject_id: Optional[uuid.UUID] = None + tx_type: str + quantity: int = Field(gt=0) + tx_date: date + reference: Optional[str] = None + notes: Optional[str] = None + + +class TxRead(BaseModel): + id: uuid.UUID + study_id: uuid.UUID + site_id: uuid.UUID + batch_id: uuid.UUID + subject_id: Optional[uuid.UUID] + tx_type: str + quantity: int + tx_date: date + reference: Optional[str] + notes: Optional[str] + created_by: uuid.UUID + created_at: datetime + + model_config = ConfigDict(from_attributes=True) diff --git a/backend/app/services/imp.py b/backend/app/services/imp.py new file mode 100644 index 00000000..cd82c026 --- /dev/null +++ b/backend/app/services/imp.py @@ -0,0 +1,85 @@ +import uuid +from typing import Literal + +from sqlalchemy.ext.asyncio import AsyncSession + +from app.crud import imp_inventory as inventory_crud +from app.crud import imp_tx as tx_crud +from app.crud import audit as audit_crud +from app.models.imp_batch import ImpBatch +from app.models.site import Site +from app.models.subject import Subject + +TxType = Literal["RECEIPT", "DISPENSE", "RETURN", "RECONCILE", "DESTROY", "TRANSFER_IN", "TRANSFER_OUT"] + +POSITIVE_TYPES = {"RECEIPT", "RETURN", "TRANSFER_IN"} +NEGATIVE_TYPES = {"DISPENSE", "DESTROY", "TRANSFER_OUT"} + + +async def _validate_entities(db: AsyncSession, study_id: uuid.UUID, site_id: uuid.UUID, batch_id: uuid.UUID, subject_id: uuid.UUID | None): + site = await db.get(Site, site_id) + if not site or site.study_id != study_id: + raise ValueError("Site not found in study") + batch = await db.get(ImpBatch, batch_id) + if not batch or batch.study_id != study_id: + raise ValueError("Batch not found in study") + # batch status check could be extended + if subject_id: + subj = await db.get(Subject, subject_id) + if not subj or subj.study_id != study_id: + raise ValueError("Subject not found in study") + + +async def create_transaction_and_apply_inventory( + db: AsyncSession, + *, + study_id: uuid.UUID, + site_id: uuid.UUID, + batch_id: uuid.UUID, + subject_id: uuid.UUID | None, + tx_type: TxType, + quantity: int, + tx_date, + reference: str | None, + notes: str | None, + operator_id: uuid.UUID, + operator_role: str, +) -> tuple: + if quantity <= 0: + raise ValueError("Quantity must be positive") + await _validate_entities(db, study_id, site_id, batch_id, subject_id) + + async with db.begin_nested(): + inv = await inventory_crud.get_or_create_inventory(db, study_id, site_id, batch_id) + delta = quantity if tx_type in POSITIVE_TYPES else -quantity + new_qoh = inv.quantity_on_hand + delta + if new_qoh < 0: + raise ValueError("Insufficient inventory") + + inv.quantity_on_hand = new_qoh + tx = await tx_crud.create_tx( + db, + study_id=study_id, + site_id=site_id, + batch_id=batch_id, + subject_id=subject_id, + tx_type=tx_type, + quantity=quantity, + tx_date=tx_date, + reference=reference, + notes=notes, + created_by=operator_id, + ) + await db.flush() + detail = f"{tx_type} {quantity} units batch {batch_id} at site {site_id}, QOH -> {new_qoh}" + await audit_crud.log_action( + db, + study_id=study_id, + entity_type="imp_tx", + entity_id=tx.id, + action="CREATE_IMP_TX", + detail=detail, + operator_id=operator_id, + operator_role=operator_role, + ) + return tx diff --git a/pg_data/base/16384/1247 b/pg_data/base/16384/1247 index 87fd63c0..72fafe15 100644 Binary files a/pg_data/base/16384/1247 and b/pg_data/base/16384/1247 differ diff --git a/pg_data/base/16384/1247_fsm b/pg_data/base/16384/1247_fsm index 584aacc6..347b604c 100644 Binary files a/pg_data/base/16384/1247_fsm and b/pg_data/base/16384/1247_fsm differ diff --git a/pg_data/base/16384/1249 b/pg_data/base/16384/1249 index 2131e06a..bce07316 100644 Binary files a/pg_data/base/16384/1249 and b/pg_data/base/16384/1249 differ diff --git a/pg_data/base/16384/1259 b/pg_data/base/16384/1259 index 516e357b..e02f2446 100644 Binary files a/pg_data/base/16384/1259 and b/pg_data/base/16384/1259 differ diff --git a/pg_data/base/16384/24576 b/pg_data/base/16384/24576 index 1ab18344..e9fa5c88 100644 Binary files a/pg_data/base/16384/24576 and b/pg_data/base/16384/24576 differ diff --git a/pg_data/base/16384/24581 b/pg_data/base/16384/24581 index 44e4e826..a8a1ac54 100644 Binary files a/pg_data/base/16384/24581 and b/pg_data/base/16384/24581 differ diff --git a/pg_data/base/16384/24583 b/pg_data/base/16384/24583 index c031ade7..c2010eaa 100644 Binary files a/pg_data/base/16384/24583 and b/pg_data/base/16384/24583 differ diff --git a/pg_data/base/16384/24584 b/pg_data/base/16384/24584 index 78c5966e..476f1057 100644 Binary files a/pg_data/base/16384/24584 and b/pg_data/base/16384/24584 differ diff --git a/pg_data/base/16384/24590 b/pg_data/base/16384/24590 index 838b9430..80256cf9 100644 Binary files a/pg_data/base/16384/24590 and b/pg_data/base/16384/24590 differ diff --git a/pg_data/base/16384/24597 b/pg_data/base/16384/24597 index 5817ad77..50380616 100644 Binary files a/pg_data/base/16384/24597 and b/pg_data/base/16384/24597 differ diff --git a/pg_data/base/16384/24598 b/pg_data/base/16384/24598 index 0ae59ed6..ee89163b 100644 Binary files a/pg_data/base/16384/24598 and b/pg_data/base/16384/24598 differ diff --git a/pg_data/base/16384/24605 b/pg_data/base/16384/24605 index 49e3b000..35917a8b 100644 Binary files a/pg_data/base/16384/24605 and b/pg_data/base/16384/24605 differ diff --git a/pg_data/base/16384/24612 b/pg_data/base/16384/24612 index 1bbdaa97..1fd27c3b 100644 Binary files a/pg_data/base/16384/24612 and b/pg_data/base/16384/24612 differ diff --git a/pg_data/base/16384/24613 b/pg_data/base/16384/24613 index ac610a29..ac662c86 100644 Binary files a/pg_data/base/16384/24613 and b/pg_data/base/16384/24613 differ diff --git a/pg_data/base/16384/24618 b/pg_data/base/16384/24618 index 20739a10..529f1f2e 100644 Binary files a/pg_data/base/16384/24618 and b/pg_data/base/16384/24618 differ diff --git a/pg_data/base/16384/24620 b/pg_data/base/16384/24620 index 52e70ebd..afbc08f1 100644 Binary files a/pg_data/base/16384/24620 and b/pg_data/base/16384/24620 differ diff --git a/pg_data/base/16384/24632 b/pg_data/base/16384/24632 index 1dbbbee0..e28e1ca9 100644 Binary files a/pg_data/base/16384/24632 and b/pg_data/base/16384/24632 differ diff --git a/pg_data/base/16384/24672 b/pg_data/base/16384/24672 index 77f6fceb..95052094 100644 Binary files a/pg_data/base/16384/24672 and b/pg_data/base/16384/24672 differ diff --git a/pg_data/base/16384/24678 b/pg_data/base/16384/24678 index ef3a2625..7148a6a5 100644 Binary files a/pg_data/base/16384/24678 and b/pg_data/base/16384/24678 differ diff --git a/pg_data/base/16384/24740 b/pg_data/base/16384/24740 index 2b45926a..47dc58f5 100644 Binary files a/pg_data/base/16384/24740 and b/pg_data/base/16384/24740 differ diff --git a/pg_data/base/16384/24747 b/pg_data/base/16384/24747 index 89f48a31..ab424b56 100644 Binary files a/pg_data/base/16384/24747 and b/pg_data/base/16384/24747 differ diff --git a/pg_data/base/16384/24749 b/pg_data/base/16384/24749 index 728d8b8c..73c2e864 100644 Binary files a/pg_data/base/16384/24749 and b/pg_data/base/16384/24749 differ diff --git a/pg_data/base/16384/24761 b/pg_data/base/16384/24761 index be0707f7..6fa2b197 100644 Binary files a/pg_data/base/16384/24761 and b/pg_data/base/16384/24761 differ diff --git a/pg_data/base/16384/24762 b/pg_data/base/16384/24762 index 50be9fb3..952d719b 100644 Binary files a/pg_data/base/16384/24762 and b/pg_data/base/16384/24762 differ diff --git a/pg_data/base/16384/24763 b/pg_data/base/16384/24763 index a6b4f313..500e2ebd 100644 Binary files a/pg_data/base/16384/24763 and b/pg_data/base/16384/24763 differ diff --git a/pg_data/base/16384/24770 b/pg_data/base/16384/24770 index 2fabf3ec..61e025fc 100644 Binary files a/pg_data/base/16384/24770 and b/pg_data/base/16384/24770 differ diff --git a/pg_data/base/16384/24782 b/pg_data/base/16384/24782 index 4a3d0e43..606db57a 100644 Binary files a/pg_data/base/16384/24782 and b/pg_data/base/16384/24782 differ diff --git a/pg_data/base/16384/24853 b/pg_data/base/16384/24853 index 6d17cf9d..8682a42c 100644 Binary files a/pg_data/base/16384/24853 and b/pg_data/base/16384/24853 differ diff --git a/pg_data/base/16384/24860 b/pg_data/base/16384/24860 index cf80beb2..618977d0 100644 Binary files a/pg_data/base/16384/24860 and b/pg_data/base/16384/24860 differ diff --git a/pg_data/base/16384/24887 b/pg_data/base/16384/24887 index 30c65135..e22a9116 100644 Binary files a/pg_data/base/16384/24887 and b/pg_data/base/16384/24887 differ diff --git a/pg_data/base/16384/24888 b/pg_data/base/16384/24888 index 890eb05c..dd8a8efc 100644 Binary files a/pg_data/base/16384/24888 and b/pg_data/base/16384/24888 differ diff --git a/pg_data/base/16384/24889 b/pg_data/base/16384/24889 index 4e9cdb11..98262ca7 100644 Binary files a/pg_data/base/16384/24889 and b/pg_data/base/16384/24889 differ diff --git a/pg_data/base/16384/24924 b/pg_data/base/16384/24924 new file mode 100644 index 00000000..6d17cf9d Binary files /dev/null and b/pg_data/base/16384/24924 differ diff --git a/pg_data/base/16384/24929 b/pg_data/base/16384/24929 new file mode 100644 index 00000000..e69de29b diff --git a/pg_data/base/16384/24930 b/pg_data/base/16384/24930 new file mode 100644 index 00000000..27e779a5 Binary files /dev/null and b/pg_data/base/16384/24930 differ diff --git a/pg_data/base/16384/24931 b/pg_data/base/16384/24931 new file mode 100644 index 00000000..6cc02b05 Binary files /dev/null and b/pg_data/base/16384/24931 differ diff --git a/pg_data/base/16384/24933 b/pg_data/base/16384/24933 new file mode 100644 index 00000000..c8425784 Binary files /dev/null and b/pg_data/base/16384/24933 differ diff --git a/pg_data/base/16384/24940 b/pg_data/base/16384/24940 new file mode 100644 index 00000000..518afb50 Binary files /dev/null and b/pg_data/base/16384/24940 differ diff --git a/pg_data/base/16384/24941 b/pg_data/base/16384/24941 new file mode 100644 index 00000000..6d17cf9d Binary files /dev/null and b/pg_data/base/16384/24941 differ diff --git a/pg_data/base/16384/24946 b/pg_data/base/16384/24946 new file mode 100644 index 00000000..66efecf8 Binary files /dev/null and b/pg_data/base/16384/24946 differ diff --git a/pg_data/base/16384/24948 b/pg_data/base/16384/24948 new file mode 100644 index 00000000..f6d0780a Binary files /dev/null and b/pg_data/base/16384/24948 differ diff --git a/pg_data/base/16384/24960 b/pg_data/base/16384/24960 new file mode 100644 index 00000000..806ea29a Binary files /dev/null and b/pg_data/base/16384/24960 differ diff --git a/pg_data/base/16384/24961 b/pg_data/base/16384/24961 new file mode 100644 index 00000000..7b406d53 Binary files /dev/null and b/pg_data/base/16384/24961 differ diff --git a/pg_data/base/16384/24962 b/pg_data/base/16384/24962 new file mode 100644 index 00000000..6d17cf9d Binary files /dev/null and b/pg_data/base/16384/24962 differ diff --git a/pg_data/base/16384/24966 b/pg_data/base/16384/24966 new file mode 100644 index 00000000..1682f996 Binary files /dev/null and b/pg_data/base/16384/24966 differ diff --git a/pg_data/base/16384/24968 b/pg_data/base/16384/24968 new file mode 100644 index 00000000..ea2255b1 Binary files /dev/null and b/pg_data/base/16384/24968 differ diff --git a/pg_data/base/16384/24985 b/pg_data/base/16384/24985 new file mode 100644 index 00000000..21fe7e10 Binary files /dev/null and b/pg_data/base/16384/24985 differ diff --git a/pg_data/base/16384/24986 b/pg_data/base/16384/24986 new file mode 100644 index 00000000..e604d1db Binary files /dev/null and b/pg_data/base/16384/24986 differ diff --git a/pg_data/base/16384/24987 b/pg_data/base/16384/24987 new file mode 100644 index 00000000..a343e8cb Binary files /dev/null and b/pg_data/base/16384/24987 differ diff --git a/pg_data/base/16384/24988 b/pg_data/base/16384/24988 new file mode 100644 index 00000000..6d17cf9d Binary files /dev/null and b/pg_data/base/16384/24988 differ diff --git a/pg_data/base/16384/24992 b/pg_data/base/16384/24992 new file mode 100644 index 00000000..e69de29b diff --git a/pg_data/base/16384/24993 b/pg_data/base/16384/24993 new file mode 100644 index 00000000..9bcad3a4 Binary files /dev/null and b/pg_data/base/16384/24993 differ diff --git a/pg_data/base/16384/24994 b/pg_data/base/16384/24994 new file mode 100644 index 00000000..22c4ce71 Binary files /dev/null and b/pg_data/base/16384/24994 differ diff --git a/pg_data/base/16384/25021 b/pg_data/base/16384/25021 new file mode 100644 index 00000000..19c7a04e Binary files /dev/null and b/pg_data/base/16384/25021 differ diff --git a/pg_data/base/16384/25022 b/pg_data/base/16384/25022 new file mode 100644 index 00000000..7583e2bd Binary files /dev/null and b/pg_data/base/16384/25022 differ diff --git a/pg_data/base/16384/25023 b/pg_data/base/16384/25023 new file mode 100644 index 00000000..bbfce87c Binary files /dev/null and b/pg_data/base/16384/25023 differ diff --git a/pg_data/base/16384/2579 b/pg_data/base/16384/2579 index 5462d09c..42ed45f9 100644 Binary files a/pg_data/base/16384/2579 and b/pg_data/base/16384/2579 differ diff --git a/pg_data/base/16384/2604 b/pg_data/base/16384/2604 index 9d81ebef..5498754b 100644 Binary files a/pg_data/base/16384/2604 and b/pg_data/base/16384/2604 differ diff --git a/pg_data/base/16384/2606 b/pg_data/base/16384/2606 index 42953592..7a826be1 100644 Binary files a/pg_data/base/16384/2606 and b/pg_data/base/16384/2606 differ diff --git a/pg_data/base/16384/2608 b/pg_data/base/16384/2608 index 657630e4..958ca612 100644 Binary files a/pg_data/base/16384/2608 and b/pg_data/base/16384/2608 differ diff --git a/pg_data/base/16384/2610 b/pg_data/base/16384/2610 index b86fed55..ca87abbf 100644 Binary files a/pg_data/base/16384/2610 and b/pg_data/base/16384/2610 differ diff --git a/pg_data/base/16384/2610_fsm b/pg_data/base/16384/2610_fsm index d6c59d37..62ba001a 100644 Binary files a/pg_data/base/16384/2610_fsm and b/pg_data/base/16384/2610_fsm differ diff --git a/pg_data/base/16384/2619 b/pg_data/base/16384/2619 index 7466f10b..f25c7ed6 100644 Binary files a/pg_data/base/16384/2619 and b/pg_data/base/16384/2619 differ diff --git a/pg_data/base/16384/2619_fsm b/pg_data/base/16384/2619_fsm index f8c68bb6..13acbd8c 100644 Binary files a/pg_data/base/16384/2619_fsm and b/pg_data/base/16384/2619_fsm differ diff --git a/pg_data/base/16384/2620 b/pg_data/base/16384/2620 index a0150c36..21e59260 100644 Binary files a/pg_data/base/16384/2620 and b/pg_data/base/16384/2620 differ diff --git a/pg_data/base/16384/2656 b/pg_data/base/16384/2656 index 226f729a..d9ce84be 100644 Binary files a/pg_data/base/16384/2656 and b/pg_data/base/16384/2656 differ diff --git a/pg_data/base/16384/2657 b/pg_data/base/16384/2657 index f10d41b8..68bf4c83 100644 Binary files a/pg_data/base/16384/2657 and b/pg_data/base/16384/2657 differ diff --git a/pg_data/base/16384/2658 b/pg_data/base/16384/2658 index c9275dcb..fc438476 100644 Binary files a/pg_data/base/16384/2658 and b/pg_data/base/16384/2658 differ diff --git a/pg_data/base/16384/2659 b/pg_data/base/16384/2659 index 325e0536..663f3eea 100644 Binary files a/pg_data/base/16384/2659 and b/pg_data/base/16384/2659 differ diff --git a/pg_data/base/16384/2662 b/pg_data/base/16384/2662 index fa2eb722..47907766 100644 Binary files a/pg_data/base/16384/2662 and b/pg_data/base/16384/2662 differ diff --git a/pg_data/base/16384/2663 b/pg_data/base/16384/2663 index d157a263..a31f5484 100644 Binary files a/pg_data/base/16384/2663 and b/pg_data/base/16384/2663 differ diff --git a/pg_data/base/16384/2664 b/pg_data/base/16384/2664 index aff82bcb..e6f487cc 100644 Binary files a/pg_data/base/16384/2664 and b/pg_data/base/16384/2664 differ diff --git a/pg_data/base/16384/2665 b/pg_data/base/16384/2665 index 31c39311..c1f505fc 100644 Binary files a/pg_data/base/16384/2665 and b/pg_data/base/16384/2665 differ diff --git a/pg_data/base/16384/2666 b/pg_data/base/16384/2666 index c1a248f5..d36c7b2d 100644 Binary files a/pg_data/base/16384/2666 and b/pg_data/base/16384/2666 differ diff --git a/pg_data/base/16384/2667 b/pg_data/base/16384/2667 index 197720f5..b5edf783 100644 Binary files a/pg_data/base/16384/2667 and b/pg_data/base/16384/2667 differ diff --git a/pg_data/base/16384/2673 b/pg_data/base/16384/2673 index cdc3497c..e9d7eafd 100644 Binary files a/pg_data/base/16384/2673 and b/pg_data/base/16384/2673 differ diff --git a/pg_data/base/16384/2674 b/pg_data/base/16384/2674 index 166e65fb..7d5de125 100644 Binary files a/pg_data/base/16384/2674 and b/pg_data/base/16384/2674 differ diff --git a/pg_data/base/16384/2678 b/pg_data/base/16384/2678 index 70a7d9c1..479374e1 100644 Binary files a/pg_data/base/16384/2678 and b/pg_data/base/16384/2678 differ diff --git a/pg_data/base/16384/2679 b/pg_data/base/16384/2679 index e789b936..f9ba870f 100644 Binary files a/pg_data/base/16384/2679 and b/pg_data/base/16384/2679 differ diff --git a/pg_data/base/16384/2696 b/pg_data/base/16384/2696 index 46eabbbd..6d989590 100644 Binary files a/pg_data/base/16384/2696 and b/pg_data/base/16384/2696 differ diff --git a/pg_data/base/16384/2699 b/pg_data/base/16384/2699 index f92a1edb..1c712e4b 100644 Binary files a/pg_data/base/16384/2699 and b/pg_data/base/16384/2699 differ diff --git a/pg_data/base/16384/2701 b/pg_data/base/16384/2701 index 774ff3d8..2ecb0711 100644 Binary files a/pg_data/base/16384/2701 and b/pg_data/base/16384/2701 differ diff --git a/pg_data/base/16384/2702 b/pg_data/base/16384/2702 index 5138492c..ebe71656 100644 Binary files a/pg_data/base/16384/2702 and b/pg_data/base/16384/2702 differ diff --git a/pg_data/base/16384/2703 b/pg_data/base/16384/2703 index f9b59ac0..2a3ea377 100644 Binary files a/pg_data/base/16384/2703 and b/pg_data/base/16384/2703 differ diff --git a/pg_data/base/16384/2704 b/pg_data/base/16384/2704 index a7bbe7bb..9185622a 100644 Binary files a/pg_data/base/16384/2704 and b/pg_data/base/16384/2704 differ diff --git a/pg_data/base/16384/3455 b/pg_data/base/16384/3455 index a2c202dd..e2c938c0 100644 Binary files a/pg_data/base/16384/3455 and b/pg_data/base/16384/3455 differ diff --git a/pg_data/base/16384/pg_internal.init b/pg_data/base/16384/pg_internal.init index 3c9051d0..c0fd76d2 100644 Binary files a/pg_data/base/16384/pg_internal.init and b/pg_data/base/16384/pg_internal.init differ diff --git a/pg_data/global/pg_control b/pg_data/global/pg_control index 5634f288..11a7d23d 100644 Binary files a/pg_data/global/pg_control and b/pg_data/global/pg_control differ diff --git a/pg_data/pg_wal/000000010000000000000001 b/pg_data/pg_wal/000000010000000000000001 index fb3e60d4..312f8e4f 100644 Binary files a/pg_data/pg_wal/000000010000000000000001 and b/pg_data/pg_wal/000000010000000000000001 differ diff --git a/pg_data/pg_xact/0000 b/pg_data/pg_xact/0000 index 513cf821..cadbc6f0 100644 Binary files a/pg_data/pg_xact/0000 and b/pg_data/pg_xact/0000 differ