diff --git a/backend/app/api/v1/finance.py b/backend/app/api/v1/finance.py new file mode 100644 index 00000000..2ece9cd3 --- /dev/null +++ b/backend/app/api/v1/finance.py @@ -0,0 +1,190 @@ +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 finance as finance_crud +from app.crud import study as study_crud +from app.schemas.finance import FinanceCreate, FinanceRead, FinanceStatusUpdate, FinanceUpdate + +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 + + +def _with_month(item: FinanceRead) -> FinanceRead: + item.month = item.occur_date.strftime("%Y-%m") + return item + + +@router.post( + "/items", + response_model=FinanceRead, + status_code=status.HTTP_201_CREATED, + dependencies=[Depends(require_study_roles(["PM", "CRA"]))], +) +async def create_finance_item( + study_id: uuid.UUID, + item_in: FinanceCreate, + db: AsyncSession = Depends(get_db_session), + current_user=Depends(get_current_user), +) -> FinanceRead: + await _ensure_study_exists(db, study_id) + try: + item = await finance_crud.create_item(db, study_id, item_in, created_by=current_user.id) + 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="finance", + entity_id=item.id, + action="CREATE_FINANCE_ITEM", + detail=f"Finance {item.id} created", + operator_id=current_user.id, + operator_role=current_user.role, + ) + return _with_month(FinanceRead.model_validate(item)) + + +@router.get( + "/items", + response_model=list[FinanceRead], + dependencies=[Depends(require_study_member())], +) +async def list_finance_items( + study_id: uuid.UUID, + status_filter: str | None = None, + category: str | None = None, + site_id: uuid.UUID | None = None, + subject_id: uuid.UUID | 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[FinanceRead]: + await _ensure_study_exists(db, study_id) + items = await finance_crud.list_items( + db, + study_id, + status=status_filter, + category=category, + site_id=site_id, + subject_id=subject_id, + date_from=date_from, + date_to=date_to, + skip=skip, + limit=limit, + ) + return [_with_month(FinanceRead.model_validate(i)) for i in items] + + +@router.get( + "/items/{item_id}", + response_model=FinanceRead, + dependencies=[Depends(require_study_member())], +) +async def get_finance_item( + study_id: uuid.UUID, + item_id: uuid.UUID, + db: AsyncSession = Depends(get_db_session), +) -> FinanceRead: + await _ensure_study_exists(db, study_id) + item = await finance_crud.get_item(db, item_id) + if not item or item.study_id != study_id: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Finance item not found") + return _with_month(FinanceRead.model_validate(item)) + + +@router.patch( + "/items/{item_id}", + response_model=FinanceRead, + dependencies=[Depends(require_study_roles(["PM", "CRA"]))], +) +async def update_finance_item( + study_id: uuid.UUID, + item_id: uuid.UUID, + item_in: FinanceUpdate, + db: AsyncSession = Depends(get_db_session), + current_user=Depends(get_current_user), +) -> FinanceRead: + await _ensure_study_exists(db, study_id) + item = await finance_crud.get_item(db, item_id) + if not item or item.study_id != study_id: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Finance item not found") + try: + updated = await finance_crud.update_item_draft(db, item, item_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="finance", + entity_id=item_id, + action="UPDATE_FINANCE_ITEM", + detail=f"Finance {item_id} updated", + operator_id=current_user.id, + operator_role=current_user.role, + ) + return _with_month(FinanceRead.model_validate(updated)) + + +@router.post( + "/items/{item_id}/status", + response_model=FinanceRead, + dependencies=[Depends(require_study_member())], +) +async def change_finance_status( + study_id: uuid.UUID, + item_id: uuid.UUID, + status_in: FinanceStatusUpdate, + db: AsyncSession = Depends(get_db_session), + current_user=Depends(get_current_user), +) -> FinanceRead: + await _ensure_study_exists(db, study_id) + item = await finance_crud.get_item(db, item_id) + if not item or item.study_id != study_id: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Finance item not found") + + # permission by status target + from app.crud import member as member_crud + member = await member_crud.get_member(db, study_id, current_user.id) + member_role = member.role_in_study if member else None + + target = status_in.status + if target in {"SUBMITTED"}: + if current_user.role != "ADMIN" and member_role not in {"PM", "CRA"}: + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Insufficient permissions") + elif target in {"APPROVED", "REJECTED", "PAID"}: + if current_user.role != "ADMIN" and member_role not in {"PM"}: + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Insufficient permissions") + else: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Unsupported status") + + old_status = item.status + try: + updated = await finance_crud.change_status(db, item, status_in, operator_id=current_user.id) + except ValueError as exc: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc + detail = f"Finance {item_id} {old_status} -> {status_in.status}" + action = "FINANCE_STATUS_CHANGE" + await audit_crud.log_action( + db, + study_id=study_id, + entity_type="finance", + entity_id=item_id, + action=action, + detail=detail, + operator_id=current_user.id, + operator_role=current_user.role, + ) + return _with_month(FinanceRead.model_validate(updated)) diff --git a/backend/app/api/v1/finance_dashboard.py b/backend/app/api/v1/finance_dashboard.py new file mode 100644 index 00000000..6e7ecaa6 --- /dev/null +++ b/backend/app/api/v1/finance_dashboard.py @@ -0,0 +1,32 @@ +import uuid +from datetime import date + +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 finance as finance_crud +from app.crud import study as study_crud +from app.schemas.finance import FinanceSummaryRead + +router = APIRouter() + + +@router.get("/summary", response_model=FinanceSummaryRead, dependencies=[Depends(require_study_member())]) +async def finance_summary( + study_id: uuid.UUID, + date_from: date | None = None, + date_to: date | None = None, + db: AsyncSession = Depends(get_db_session), +) -> FinanceSummaryRead: + # ensure study exists + await study_crud.get(db, study_id) + row = await finance_crud.summary(db, study_id, date_from=date_from, date_to=date_to) + return FinanceSummaryRead( + total_amount=row.total_amount, + paid_amount=row.paid_amount, + approved_amount=row.approved_amount, + submitted_amount=row.submitted_amount, + count_total=row.count_total, + count_paid=row.count_paid, + ) diff --git a/backend/app/api/v1/router.py b/backend/app/api/v1/router.py index 27b2d420..4a068472 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, imp_products, imp_batches, imp_inventory, imp_transactions +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, finance, finance_dashboard api_router = APIRouter() api_router.include_router(auth.router, prefix="/auth", tags=["auth"]) @@ -24,3 +24,5 @@ api_router.include_router(imp_products.router, prefix="/studies/{study_id}/imp/p 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"]) +api_router.include_router(finance.router, prefix="/studies/{study_id}/finance", tags=["finance"]) +api_router.include_router(finance_dashboard.router, prefix="/studies/{study_id}/finance", tags=["finance"]) diff --git a/backend/app/crud/finance.py b/backend/app/crud/finance.py new file mode 100644 index 00000000..aab82b5f --- /dev/null +++ b/backend/app/crud/finance.py @@ -0,0 +1,174 @@ +import uuid +from datetime import date, datetime, timezone +from decimal import Decimal +from typing import Sequence + +from sqlalchemy import Numeric, func, select, update as sa_update, case +from sqlalchemy.ext.asyncio import AsyncSession + +from app.models.finance import FinanceItem +from app.models.site import Site +from app.models.subject import Subject +from app.schemas.finance import FinanceCreate, FinanceStatusUpdate, FinanceUpdate + +VALID_TRANSITIONS = { + "DRAFT": {"SUBMITTED"}, + "SUBMITTED": {"APPROVED", "REJECTED"}, + "APPROVED": {"PAID"}, + "REJECTED": set(), + "PAID": set(), +} + + +async def _validate_site_subject(db: AsyncSession, study_id: uuid.UUID, site_id: uuid.UUID | None, subject_id: uuid.UUID | None): + subj_site = None + if site_id: + result = await db.execute(select(Site).where(Site.id == site_id)) + site = result.scalar_one_or_none() + if not site or site.study_id != study_id: + raise ValueError("Site not found in study") + if subject_id: + result = await db.execute(select(Subject).where(Subject.id == subject_id)) + subj = result.scalar_one_or_none() + if not subj or subj.study_id != study_id: + raise ValueError("Subject not found in study") + subj_site = subj.site_id + if site_id and subj_site and site_id != subj_site: + raise ValueError("Site and subject mismatch") + + +async def create_item(db: AsyncSession, study_id: uuid.UUID, item_in: FinanceCreate, *, created_by: uuid.UUID) -> FinanceItem: + await _validate_site_subject(db, study_id, item_in.site_id, item_in.subject_id) + item = FinanceItem( + study_id=study_id, + site_id=item_in.site_id, + subject_id=item_in.subject_id, + visit_id=item_in.visit_id, + category=item_in.category, + title=item_in.title, + description=item_in.description, + currency=item_in.currency, + amount=item_in.amount, + occur_date=item_in.occur_date, + status="DRAFT", + created_by=created_by, + ) + db.add(item) + await db.commit() + await db.refresh(item) + return item + + +async def get_item(db: AsyncSession, item_id: uuid.UUID) -> FinanceItem | None: + result = await db.execute(select(FinanceItem).where(FinanceItem.id == item_id)) + return result.scalar_one_or_none() + + +async def list_items( + db: AsyncSession, + study_id: uuid.UUID, + *, + status: str | None = None, + category: str | None = None, + site_id: uuid.UUID | None = None, + subject_id: uuid.UUID | None = None, + date_from: date | None = None, + date_to: date | None = None, + skip: int = 0, + limit: int = 100, +) -> Sequence[FinanceItem]: + stmt = select(FinanceItem).where(FinanceItem.study_id == study_id) + if status: + stmt = stmt.where(FinanceItem.status == status) + if category: + stmt = stmt.where(FinanceItem.category == category) + if site_id: + stmt = stmt.where(FinanceItem.site_id == site_id) + if subject_id: + stmt = stmt.where(FinanceItem.subject_id == subject_id) + if date_from: + stmt = stmt.where(FinanceItem.occur_date >= date_from) + if date_to: + stmt = stmt.where(FinanceItem.occur_date <= date_to) + stmt = stmt.offset(skip).limit(limit) + result = await db.execute(stmt) + return result.scalars().all() + + +async def update_item_draft(db: AsyncSession, item: FinanceItem, item_in: FinanceUpdate) -> FinanceItem: + if item.status != "DRAFT": + raise ValueError("Only DRAFT items can be edited") + update_data = item_in.model_dump(exclude_unset=True) + if update_data: + await db.execute( + sa_update(FinanceItem) + .where(FinanceItem.id == item.id) + .values(**update_data) + ) + await db.commit() + await db.refresh(item) + return item + + +async def change_status(db: AsyncSession, item: FinanceItem, status_in: FinanceStatusUpdate, *, operator_id: uuid.UUID) -> FinanceItem: + target = status_in.status + allowed = VALID_TRANSITIONS.get(item.status, set()) + if target not in allowed: + raise ValueError(f"Invalid status transition {item.status} -> {target}") + update_data = {"status": target} + now = datetime.now(timezone.utc) + if target == "SUBMITTED": + update_data["submitted_at"] = now + if target == "APPROVED": + update_data["approved_at"] = now + update_data["approver_id"] = operator_id + update_data["reject_reason"] = None + if target == "REJECTED": + if not status_in.reject_reason: + raise ValueError("reject_reason required") + update_data["rejected_at"] = now + update_data["approver_id"] = operator_id + update_data["reject_reason"] = status_in.reject_reason + if target == "PAID": + if item.status != "APPROVED": + raise ValueError("Only APPROVED can transition to PAID") + update_data["paid_at"] = now + update_data["payer_id"] = operator_id + + await db.execute( + sa_update(FinanceItem) + .where(FinanceItem.id == item.id) + .values(**update_data) + ) + await db.commit() + await db.refresh(item) + return item + + +async def summary( + db: AsyncSession, + study_id: uuid.UUID, + *, + date_from: date | None = None, + date_to: date | None = None, +): + stmt = select( + func.coalesce(func.sum(FinanceItem.amount), 0).label("total_amount"), + func.coalesce(func.sum(case((FinanceItem.status == "PAID", FinanceItem.amount), else_=0)), 0).label( + "paid_amount" + ), + func.coalesce(func.sum(case((FinanceItem.status == "APPROVED", FinanceItem.amount), else_=0)), 0).label( + "approved_amount" + ), + func.coalesce(func.sum(case((FinanceItem.status == "SUBMITTED", FinanceItem.amount), else_=0)), 0).label( + "submitted_amount" + ), + func.count().label("count_total"), + func.coalesce(func.sum(case((FinanceItem.status == "PAID", 1), else_=0)), 0).label("count_paid"), + ).where(FinanceItem.study_id == study_id) + if date_from: + stmt = stmt.where(FinanceItem.occur_date >= date_from) + if date_to: + stmt = stmt.where(FinanceItem.occur_date <= date_to) + result = await db.execute(stmt) + return result.one() diff --git a/backend/app/db/base.py b/backend/app/db/base.py index 400b6871..d726c9e3 100644 --- a/backend/app/db/base.py +++ b/backend/app/db/base.py @@ -20,3 +20,4 @@ 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 +from app.models.finance import FinanceItem # noqa: F401 diff --git a/backend/app/models/finance.py b/backend/app/models/finance.py new file mode 100644 index 00000000..0799d935 --- /dev/null +++ b/backend/app/models/finance.py @@ -0,0 +1,37 @@ +import uuid +from datetime import datetime, date + +from sqlalchemy import Date, DateTime, ForeignKey, Numeric, String, Text, func +from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.orm import Mapped, mapped_column + +from app.db.base_class import Base + + +class FinanceItem(Base): + __tablename__ = "finance_items" + + 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 | None] = mapped_column(UUID(as_uuid=True), ForeignKey("sites.id"), index=True, nullable=True) + subject_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("subjects.id"), index=True, nullable=True) + visit_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), nullable=True) + category: Mapped[str] = mapped_column(String(50), nullable=False) + title: Mapped[str] = mapped_column(String(255), nullable=False) + description: Mapped[str | None] = mapped_column(Text, nullable=True) + currency: Mapped[str] = mapped_column(String(10), nullable=False, default="CNY") + amount: Mapped[float] = mapped_column(Numeric(12, 2), nullable=False) + occur_date: Mapped[date] = mapped_column(Date, nullable=False) + status: Mapped[str] = mapped_column(String(20), nullable=False, default="DRAFT") + submitted_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) + approved_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) + rejected_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) + paid_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) + approver_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=True) + payer_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=True) + reject_reason: 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()) + updated_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now() + ) diff --git a/backend/app/schemas/finance.py b/backend/app/schemas/finance.py new file mode 100644 index 00000000..9f85239e --- /dev/null +++ b/backend/app/schemas/finance.py @@ -0,0 +1,72 @@ +import uuid +from datetime import date, datetime +from decimal import Decimal +from typing import Optional + +from pydantic import BaseModel, ConfigDict, Field + + +class FinanceCreate(BaseModel): + site_id: Optional[uuid.UUID] = None + subject_id: Optional[uuid.UUID] = None + visit_id: Optional[uuid.UUID] = None + category: str + title: str + description: Optional[str] = None + currency: str = "CNY" + amount: Decimal = Field(gt=0) + occur_date: date + + +class FinanceUpdate(BaseModel): + site_id: Optional[uuid.UUID] = None + subject_id: Optional[uuid.UUID] = None + visit_id: Optional[uuid.UUID] = None + category: Optional[str] = None + title: Optional[str] = None + description: Optional[str] = None + currency: Optional[str] = None + amount: Optional[Decimal] = Field(default=None, gt=0) + occur_date: Optional[date] = None + + +class FinanceStatusUpdate(BaseModel): + status: str + reject_reason: Optional[str] = None + + +class FinanceRead(BaseModel): + id: uuid.UUID + study_id: uuid.UUID + site_id: Optional[uuid.UUID] + subject_id: Optional[uuid.UUID] + visit_id: Optional[uuid.UUID] + category: str + title: str + description: Optional[str] + currency: str + amount: Decimal + occur_date: date + status: str + submitted_at: Optional[datetime] + approved_at: Optional[datetime] + rejected_at: Optional[datetime] + paid_at: Optional[datetime] + approver_id: Optional[uuid.UUID] + payer_id: Optional[uuid.UUID] + reject_reason: Optional[str] + created_by: uuid.UUID + created_at: datetime + updated_at: datetime + month: Optional[str] = None + + model_config = ConfigDict(from_attributes=True) + + +class FinanceSummaryRead(BaseModel): + total_amount: Decimal + paid_amount: Decimal + approved_amount: Decimal + submitted_amount: Decimal + count_total: int + count_paid: int diff --git a/pg_data/base/16384/1247 b/pg_data/base/16384/1247 index 72fafe15..03075762 100644 Binary files a/pg_data/base/16384/1247 and b/pg_data/base/16384/1247 differ diff --git a/pg_data/base/16384/1249 b/pg_data/base/16384/1249 index bce07316..8458b67f 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 e02f2446..f850c80c 100644 Binary files a/pg_data/base/16384/1259 and b/pg_data/base/16384/1259 differ diff --git a/pg_data/base/16384/1259_fsm b/pg_data/base/16384/1259_fsm index 640ad1e4..68410320 100644 Binary files a/pg_data/base/16384/1259_fsm and b/pg_data/base/16384/1259_fsm differ diff --git a/pg_data/base/16384/24576 b/pg_data/base/16384/24576 index e9fa5c88..8e609549 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 a8a1ac54..c3dc2c0f 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 c2010eaa..bfd5d1a3 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 476f1057..7931a7ee 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 80256cf9..e0af6671 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 50380616..c72b6ed0 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 ee89163b..91934de1 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 35917a8b..541c24c7 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 1fd27c3b..36334ead 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 ac662c86..182cbafa 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 529f1f2e..6ea7f320 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 afbc08f1..fd4a993f 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 e28e1ca9..d7a2be6e 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 95052094..95b2d18b 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 7148a6a5..a07c9622 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 47dc58f5..a264ab19 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 ab424b56..0b277f7d 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 73c2e864..891b20c1 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 6fa2b197..7420b78e 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 952d719b..2bbf5fee 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 500e2ebd..834fba40 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 61e025fc..221b491d 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 606db57a..e3bf6c78 100644 Binary files a/pg_data/base/16384/24782 and b/pg_data/base/16384/24782 differ diff --git a/pg_data/base/16384/24924 b/pg_data/base/16384/24924 index 6d17cf9d..176b0afe 100644 Binary files a/pg_data/base/16384/24924 and b/pg_data/base/16384/24924 differ diff --git a/pg_data/base/16384/24931 b/pg_data/base/16384/24931 index 6cc02b05..aa4eb077 100644 Binary files a/pg_data/base/16384/24931 and b/pg_data/base/16384/24931 differ diff --git a/pg_data/base/16384/24933 b/pg_data/base/16384/24933 index c8425784..395490e7 100644 Binary files a/pg_data/base/16384/24933 and b/pg_data/base/16384/24933 differ diff --git a/pg_data/base/16384/24940 b/pg_data/base/16384/24940 index 518afb50..b24fca79 100644 Binary files a/pg_data/base/16384/24940 and b/pg_data/base/16384/24940 differ diff --git a/pg_data/base/16384/24941 b/pg_data/base/16384/24941 index 6d17cf9d..2293e563 100644 Binary files a/pg_data/base/16384/24941 and b/pg_data/base/16384/24941 differ diff --git a/pg_data/base/16384/24946 b/pg_data/base/16384/24946 index 66efecf8..68531ae4 100644 Binary files a/pg_data/base/16384/24946 and b/pg_data/base/16384/24946 differ diff --git a/pg_data/base/16384/24948 b/pg_data/base/16384/24948 index f6d0780a..8a82cdd4 100644 Binary files a/pg_data/base/16384/24948 and b/pg_data/base/16384/24948 differ diff --git a/pg_data/base/16384/24960 b/pg_data/base/16384/24960 index 806ea29a..d28249cd 100644 Binary files a/pg_data/base/16384/24960 and b/pg_data/base/16384/24960 differ diff --git a/pg_data/base/16384/24961 b/pg_data/base/16384/24961 index 7b406d53..15c09352 100644 Binary files a/pg_data/base/16384/24961 and b/pg_data/base/16384/24961 differ diff --git a/pg_data/base/16384/24962 b/pg_data/base/16384/24962 index 6d17cf9d..68eb8e29 100644 Binary files a/pg_data/base/16384/24962 and b/pg_data/base/16384/24962 differ diff --git a/pg_data/base/16384/24966 b/pg_data/base/16384/24966 index 1682f996..a61db372 100644 Binary files a/pg_data/base/16384/24966 and b/pg_data/base/16384/24966 differ diff --git a/pg_data/base/16384/24968 b/pg_data/base/16384/24968 index ea2255b1..7365f1b5 100644 Binary files a/pg_data/base/16384/24968 and b/pg_data/base/16384/24968 differ diff --git a/pg_data/base/16384/24985 b/pg_data/base/16384/24985 index 21fe7e10..cbae2a37 100644 Binary files a/pg_data/base/16384/24985 and b/pg_data/base/16384/24985 differ diff --git a/pg_data/base/16384/24986 b/pg_data/base/16384/24986 index e604d1db..ceecb83c 100644 Binary files a/pg_data/base/16384/24986 and b/pg_data/base/16384/24986 differ diff --git a/pg_data/base/16384/24987 b/pg_data/base/16384/24987 index a343e8cb..c9df7829 100644 Binary files a/pg_data/base/16384/24987 and b/pg_data/base/16384/24987 differ diff --git a/pg_data/base/16384/24988 b/pg_data/base/16384/24988 index 6d17cf9d..bba0517f 100644 Binary files a/pg_data/base/16384/24988 and b/pg_data/base/16384/24988 differ diff --git a/pg_data/base/16384/24994 b/pg_data/base/16384/24994 index 22c4ce71..d3bbc862 100644 Binary files a/pg_data/base/16384/24994 and b/pg_data/base/16384/24994 differ diff --git a/pg_data/base/16384/25021 b/pg_data/base/16384/25021 index 19c7a04e..ee678594 100644 Binary files a/pg_data/base/16384/25021 and b/pg_data/base/16384/25021 differ diff --git a/pg_data/base/16384/25022 b/pg_data/base/16384/25022 index 7583e2bd..5399d6fe 100644 Binary files a/pg_data/base/16384/25022 and b/pg_data/base/16384/25022 differ diff --git a/pg_data/base/16384/25023 b/pg_data/base/16384/25023 index bbfce87c..24ea8fa3 100644 Binary files a/pg_data/base/16384/25023 and b/pg_data/base/16384/25023 differ diff --git a/pg_data/base/16384/25024 b/pg_data/base/16384/25024 new file mode 100644 index 00000000..6d17cf9d Binary files /dev/null and b/pg_data/base/16384/25024 differ diff --git a/pg_data/base/16384/25029 b/pg_data/base/16384/25029 new file mode 100644 index 00000000..e69de29b diff --git a/pg_data/base/16384/25030 b/pg_data/base/16384/25030 new file mode 100644 index 00000000..83045ffd Binary files /dev/null and b/pg_data/base/16384/25030 differ diff --git a/pg_data/base/16384/25031 b/pg_data/base/16384/25031 new file mode 100644 index 00000000..82f9720d Binary files /dev/null and b/pg_data/base/16384/25031 differ diff --git a/pg_data/base/16384/25063 b/pg_data/base/16384/25063 new file mode 100644 index 00000000..5882b7c1 Binary files /dev/null and b/pg_data/base/16384/25063 differ diff --git a/pg_data/base/16384/25064 b/pg_data/base/16384/25064 new file mode 100644 index 00000000..349d61d5 Binary files /dev/null and b/pg_data/base/16384/25064 differ diff --git a/pg_data/base/16384/25065 b/pg_data/base/16384/25065 new file mode 100644 index 00000000..c0fda325 Binary files /dev/null and b/pg_data/base/16384/25065 differ diff --git a/pg_data/base/16384/2579 b/pg_data/base/16384/2579 index 42ed45f9..d17ef732 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 5498754b..e62a3e48 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 7a826be1..0c15ef82 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 958ca612..b25a2783 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 ca87abbf..27ef5469 100644 Binary files a/pg_data/base/16384/2610 and b/pg_data/base/16384/2610 differ diff --git a/pg_data/base/16384/2619 b/pg_data/base/16384/2619 index f25c7ed6..87f1ded9 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 13acbd8c..c14fd0b3 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 21e59260..5e6aef6f 100644 Binary files a/pg_data/base/16384/2620 and b/pg_data/base/16384/2620 differ diff --git a/pg_data/base/16384/2620_fsm b/pg_data/base/16384/2620_fsm index 565f0792..81a6f1ed 100644 Binary files a/pg_data/base/16384/2620_fsm and b/pg_data/base/16384/2620_fsm differ diff --git a/pg_data/base/16384/2656 b/pg_data/base/16384/2656 index d9ce84be..9c2ebb58 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 68bf4c83..cab6b01f 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 fc438476..13bdfc49 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 663f3eea..eadd482f 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 47907766..da21f7cc 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 a31f5484..173a088c 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 e6f487cc..a8f23495 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 c1f505fc..6b042946 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 d36c7b2d..7a5cfbdc 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 b5edf783..60973054 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 e9d7eafd..3de00dc3 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 7d5de125..79a297a1 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 479374e1..1fe16301 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 f9ba870f..674a67a3 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 6d989590..21ef1839 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 1c712e4b..e8f2446e 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 2ecb0711..a3bb06df 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 ebe71656..d87b4f64 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 2a3ea377..6669d545 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 9185622a..a5634941 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 e2c938c0..86708d61 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 c0fd76d2..12834ae9 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 11a7d23d..228f0e5b 100644 Binary files a/pg_data/global/pg_control and b/pg_data/global/pg_control differ diff --git a/pg_data/global/pg_internal.init b/pg_data/global/pg_internal.init index 0ff1bca7..12740877 100644 Binary files a/pg_data/global/pg_internal.init and b/pg_data/global/pg_internal.init differ diff --git a/pg_data/pg_subtrans/0000 b/pg_data/pg_subtrans/0000 index 6d17cf9d..ffe9102f 100644 Binary files a/pg_data/pg_subtrans/0000 and b/pg_data/pg_subtrans/0000 differ diff --git a/pg_data/pg_wal/000000010000000000000001 b/pg_data/pg_wal/000000010000000000000001 index 312f8e4f..7118edd0 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 cadbc6f0..ff1e92c4 100644 Binary files a/pg_data/pg_xact/0000 and b/pg_data/pg_xact/0000 differ