Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions lending-poc/.env.example

This file was deleted.

1 change: 1 addition & 0 deletions lending-poc/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ build/
.mypy_cache/
.pytest_cache/
.ruff_cache/
venv/
2 changes: 1 addition & 1 deletion lending-poc/app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class Settings(BaseSettings):
APP_NAME: str = "lending-poc"
APP_VERSION: str = "0.1.0"
DEBUG: bool = False
DATABASE_URL: str = "postgresql+asyncpg://postgres:postgres@localhost:5432/lending_poc"
DATABASE_URL: str
LOG_LEVEL: str = "INFO"


Expand Down
File renamed without changes.
4 changes: 2 additions & 2 deletions lending-poc/alembic.ini → lending-poc/db/alembic.ini
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[alembic]
script_location = alembic
prepend_sys_path = .
script_location = %(here)s/migrations
prepend_sys_path = %(here)s/..
sqlalchemy.url = driver://user:pass@localhost/dbname

[loggers]
Expand Down
12 changes: 12 additions & 0 deletions lending-poc/db/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8", extra="ignore")

DEBUG: bool = False
DATABASE_URL: str
ENCRYPTION_KEY: str = ""


settings = Settings()
26 changes: 26 additions & 0 deletions lending-poc/db/database.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from collections.abc import AsyncGenerator

from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from sqlalchemy.orm import DeclarativeBase

from db.config import settings

engine = create_async_engine(settings.DATABASE_URL, echo=settings.DEBUG)

async_session = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)


class Base(DeclarativeBase):
pass


async def get_db() -> AsyncGenerator[AsyncSession, None]:
session = async_session()
try:
yield session
await session.commit()
except Exception:
await session.rollback()
raise
finally:
await session.close()
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
from alembic import context
from sqlalchemy.ext.asyncio import create_async_engine

from app.config import settings
from app.database import Base
from db.config import settings
from db.database import Base
import db.models # noqa: F401 (registers models on Base.metadata for autogenerate)

config = context.config

Expand Down
41 changes: 41 additions & 0 deletions lending-poc/db/migrations/versions/0001_add_cases.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
"""add cases

Revision ID: 0001_add_cases
Revises:
Create Date: 2026-08-10 11:51:33.283278

"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa

# revision identifiers, used by Alembic.
revision: str = "0001_add_cases"
down_revision: Union[str, None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
op.execute("CREATE EXTENSION IF NOT EXISTS vector")
op.create_table(
"cases",
sa.Column("id", sa.UUID(), nullable=False),
sa.Column("applicant_ref", sa.String(), nullable=False),
sa.Column(
"status",
sa.Enum("RECEIVED", "RUNNING", "PASS", "FAIL", "NEEDS_REVIEW", name="case_status"),
nullable=False,
),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
sa.PrimaryKeyConstraint("id"),
)
op.create_index(op.f("ix_cases_applicant_ref"), "cases", ["applicant_ref"], unique=True)


def downgrade() -> None:
op.drop_index(op.f("ix_cases_applicant_ref"), table_name="cases")
op.drop_table("cases")
op.execute("DROP TYPE IF EXISTS case_status")
43 changes: 43 additions & 0 deletions lending-poc/db/migrations/versions/0002_add_documents.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"""add documents

Revision ID: 0002_add_documents
Revises: 0001_add_cases
Create Date: 2026-08-10 11:51:33.283278

"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql

# revision identifiers, used by Alembic.
revision: str = "0002_add_documents"
down_revision: Union[str, None] = "0001_add_cases"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
op.create_table(
"documents",
sa.Column("id", sa.UUID(), nullable=False),
sa.Column("case_id", sa.UUID(), nullable=False),
sa.Column(
"doc_type",
sa.Enum("AADHAAR", "PAN", "ADDRESS_PROOF", "SALARY_SLIP", "BANK_STATEMENT", name="doc_type"),
nullable=False,
),
sa.Column("extracted_fields", postgresql.JSONB(astext_type=sa.Text()), nullable=False),
sa.Column("source_file_ref", sa.String(), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
sa.ForeignKeyConstraint(["case_id"], ["cases.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
)
op.create_index(op.f("ix_documents_case_id"), "documents", ["case_id"], unique=False)


def downgrade() -> None:
op.drop_index(op.f("ix_documents_case_id"), table_name="documents")
op.drop_table("documents")
op.execute("DROP TYPE IF EXISTS doc_type")
49 changes: 49 additions & 0 deletions lending-poc/db/migrations/versions/0003_add_golden_records.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"""add golden_records

Revision ID: 0003_add_golden_records
Revises: 0002_add_documents
Create Date: 2026-08-10 11:51:33.283278

"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa
import pgvector.sqlalchemy

import db.models.types

# revision identifiers, used by Alembic.
revision: str = "0003_add_golden_records"
down_revision: Union[str, None] = "0002_add_documents"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
op.create_table(
"golden_records",
sa.Column("id", sa.UUID(), nullable=False),
sa.Column("case_id", sa.UUID(), nullable=False),
sa.Column("name", sa.String(), nullable=True),
sa.Column("address", sa.String(), nullable=True),
sa.Column("address_embedding", pgvector.sqlalchemy.vector.VECTOR(dim=384), nullable=True),
sa.Column("aadhaar_number", db.models.types.EncryptedString(), nullable=True),
sa.Column("pan_number", db.models.types.EncryptedString(), nullable=True),
sa.Column("date_of_birth", sa.Date(), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
sa.Column(
"updated_at",
sa.DateTime(timezone=True),
server_default=sa.text("now()"),
onupdate=sa.text("now()"),
nullable=False,
),
sa.ForeignKeyConstraint(["case_id"], ["cases.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("case_id"),
)


def downgrade() -> None:
op.drop_table("golden_records")
46 changes: 46 additions & 0 deletions lending-poc/db/migrations/versions/0004_add_pipeline_results.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"""add pipeline_results

Revision ID: 0004_add_pipeline_results
Revises: 0003_add_golden_records
Create Date: 2026-08-10 11:51:33.283278

"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql

# revision identifiers, used by Alembic.
revision: str = "0004_add_pipeline_results"
down_revision: Union[str, None] = "0003_add_golden_records"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
op.create_table(
"pipeline_results",
sa.Column("id", sa.UUID(), nullable=False),
sa.Column("case_id", sa.UUID(), nullable=False),
sa.Column("overall_score", sa.Float(), nullable=False),
sa.Column("decision", sa.Enum("PASS", "FAIL", "NEEDS_REVIEW", name="decision"), nullable=False),
sa.Column("reasons", postgresql.JSONB(astext_type=sa.Text()), nullable=True),
sa.Column("reviewer", sa.String(), nullable=True),
sa.Column(
"review_status", sa.Enum("PENDING", "APPROVED", "REJECTED", name="review_status"), nullable=True
),
sa.Column("reviewer_remarks", sa.Text(), nullable=True),
sa.Column("reviewed_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
sa.ForeignKeyConstraint(["case_id"], ["cases.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
)
op.create_index(op.f("ix_pipeline_results_case_id"), "pipeline_results", ["case_id"], unique=False)


def downgrade() -> None:
op.drop_index(op.f("ix_pipeline_results_case_id"), table_name="pipeline_results")
op.drop_table("pipeline_results")
op.execute("DROP TYPE IF EXISTS decision")
op.execute("DROP TYPE IF EXISTS review_status")
61 changes: 61 additions & 0 deletions lending-poc/db/migrations/versions/0005_add_validation_results.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"""add validation_results

Revision ID: 0005_add_validation_results
Revises: 0004_add_pipeline_results
Create Date: 2026-08-10 11:51:33.283278

"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql

# revision identifiers, used by Alembic.
revision: str = "0005_add_validation_results"
down_revision: Union[str, None] = "0004_add_pipeline_results"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
op.create_table(
"validation_results",
sa.Column("id", sa.UUID(), nullable=False),
sa.Column("case_id", sa.UUID(), nullable=False),
sa.Column("document_id", sa.UUID(), nullable=True),
sa.Column(
"check_type",
sa.Enum(
"NAME",
"ADDRESS",
"AADHAAR",
"PAN",
"DOB",
"EMPLOYER",
"SALARY_DATE",
"SALARY_CREDIT_COUNT",
"MANDATORY_PRESENCE",
name="check_type",
),
nullable=False,
),
sa.Column("passed", sa.Boolean(), nullable=False),
sa.Column("score", sa.Float(), nullable=False),
sa.Column("evidence", postgresql.JSONB(astext_type=sa.Text()), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
sa.ForeignKeyConstraint(["case_id"], ["cases.id"], ondelete="CASCADE"),
sa.ForeignKeyConstraint(["document_id"], ["documents.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
)
op.create_index(op.f("ix_validation_results_case_id"), "validation_results", ["case_id"], unique=False)
op.create_index(
op.f("ix_validation_results_document_id"), "validation_results", ["document_id"], unique=False
)


def downgrade() -> None:
op.drop_index(op.f("ix_validation_results_document_id"), table_name="validation_results")
op.drop_index(op.f("ix_validation_results_case_id"), table_name="validation_results")
op.drop_table("validation_results")
op.execute("DROP TYPE IF EXISTS check_type")
19 changes: 19 additions & 0 deletions lending-poc/db/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from db.models.case import Case, CaseStatus
from db.models.document import Document
from db.models.enums import CheckType, Decision, DocType
from db.models.golden_record import GoldenRecord
from db.models.pipeline_result import PipelineResult, ReviewStatus
from db.models.validation_result import ValidationResult

__all__ = [
"Case",
"CaseStatus",
"CheckType",
"Decision",
"Document",
"DocType",
"GoldenRecord",
"PipelineResult",
"ReviewStatus",
"ValidationResult",
]
42 changes: 42 additions & 0 deletions lending-poc/db/models/case.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import uuid
from datetime import datetime
from enum import Enum

from sqlalchemy import DateTime, Enum as SAEnum, String, func
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import Mapped, mapped_column, relationship

from db.database import Base


class CaseStatus(str, Enum):
RECEIVED = "RECEIVED"
RUNNING = "RUNNING"
PASS = "PASS"
FAIL = "FAIL"
NEEDS_REVIEW = "NEEDS_REVIEW"


class Case(Base):
__tablename__ = "cases"

id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
applicant_ref: Mapped[str] = mapped_column(String, unique=True, index=True, nullable=False)
status: Mapped[CaseStatus] = mapped_column(
SAEnum(CaseStatus, name="case_status"), nullable=False, default=CaseStatus.RECEIVED
)
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()
)

documents: Mapped[list["Document"]] = relationship(back_populates="case", cascade="all, delete-orphan")
golden_record: Mapped["GoldenRecord | None"] = relationship(
back_populates="case", cascade="all, delete-orphan", uselist=False
)
validation_results: Mapped[list["ValidationResult"]] = relationship(
back_populates="case", cascade="all, delete-orphan"
)
pipeline_results: Mapped[list["PipelineResult"]] = relationship(
back_populates="case", cascade="all, delete-orphan"
)
Loading