Skip to content
Open
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
2 changes: 1 addition & 1 deletion lending-poc/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
from fastapi import FastAPI
from sqlalchemy import text

from app.api.cases import router as cases_router
from app.api.health import router as health_router
from app.config import logger, settings
from app.database import async_session, engine
from cross_document_validation.api import router as cases_router


@asynccontextmanager
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
from sqlalchemy.ext.asyncio import AsyncSession

from app.database import get_db
from app.schemas.case import CaseCreateRequest, CaseCreateResponse, ValidationResultOut
from app.services.case_parsing import parse_case
from app.services.persistence import save_pipeline_result
from app.services.pipeline import run_pipeline
from app.utils.json_safe import json_safe
from cross_document_validation.schemas.case import CaseCreateRequest, CaseCreateResponse, ValidationResultOut
from cross_document_validation.services.case_parsing import parse_case
from cross_document_validation.services.persistence import save_pipeline_result
from cross_document_validation.services.pipeline import run_pipeline
from cross_document_validation.utils.json_safe import json_safe

router = APIRouter(tags=["cases"])

Expand Down
Empty file.
Empty file.
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
from calendar import monthrange
from datetime import date, timedelta

from app.matching.fuzzy import employer_similarity
from app.services import validation_config as cfg
from app.services.dto import (
from cross_document_validation.matching.fuzzy import employer_similarity
from cross_document_validation.services import validation_config as cfg
from cross_document_validation.services.dto import (
BankStatementDoc,
BankTransaction,
CaseInput,
Expand Down Expand Up @@ -135,7 +135,12 @@ def _validate_salary_slip(
score=0.0,
document_id=slip.doc_id,
failure_reason="no_matching_credit_in_window",
evidence={"window": window},
evidence={
"source_text": slip.salary_month.strftime("%B %Y"),
"target_text": None,
"match_type": "DATE_MATCH",
"window": window,
},
)

txn, score = selection
Expand All @@ -145,7 +150,12 @@ def _validate_salary_slip(
passed=True,
score=score,
document_id=slip.doc_id,
evidence={"matched_transaction": txn},
evidence={
"source_text": slip.salary_month.strftime("%B %Y"),
"target_text": txn.txn_date.strftime("%Y-%m") if txn.txn_date else None,
"match_type": "DATE_MATCH",
"matched_transaction": txn,
},
)


Expand Down Expand Up @@ -187,6 +197,11 @@ def _employer_match_for_slip(
score=score,
document_id=slip.doc_id,
failure_reason=None if passed else "employer_narration_mismatch",
evidence={
"source_text": slip.employer_name,
"target_text": matched_txn.narration,
"match_type": "FUZZY",
},
)


Expand All @@ -206,9 +221,12 @@ def _salary_credit_count(
check_type=CheckType.SALARY_CREDIT_COUNT,
passed=(matched_slips == total_slips),
score=confidence_score,
document_id=bank_statement.doc_id,
evidence={
"source_value": total_slips,
"target_value": matched_slips,
"match_type": "COUNT_MATCH",
"stmt_duration": stmt_duration,
"no_of_matches": matched_slips,
"total_slips": total_slips,
"confidence_score": confidence_score,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from datetime import date, datetime

from app.services.dto import (
from cross_document_validation.services.dto import (
AadhaarDoc,
AddressProofDoc,
BankStatementDoc,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Final PASS / FAIL / NEEDS_REVIEW logic."""

from app.services import validation_config as cfg
from app.services.dto import CheckType, Decision, DecisionResult, ScoreResult, ValidationResult
from cross_document_validation.services import validation_config as cfg
from cross_document_validation.services.dto import CheckType, Decision, DecisionResult, ScoreResult, ValidationResult

MANDATORY_CHECK_TYPES = {CheckType.NAME, CheckType.AADHAAR, CheckType.PAN, CheckType.DOB}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@
rather than silently adopting an unrelated "fuller" name.
"""

from app.matching.embeddings import get_address_embedding
from app.matching.fuzzy import name_similarity
from app.services import validation_config as cfg
from app.services.dto import CaseInput, GoldenRecord
from cross_document_validation.matching.embeddings import get_address_embedding
from cross_document_validation.matching.fuzzy import name_similarity
from cross_document_validation.services import validation_config as cfg
from cross_document_validation.services.dto import CaseInput, GoldenRecord

FULLER_NAME_RELATEDNESS_THRESHOLD = cfg.NAME_MATCH_THRESHOLD

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
the Golden Record at all, regardless of why they're missing.
"""

from app.matching import exact, fuzzy
from app.matching.embeddings import address_similarity
from app.services import validation_config as cfg
from app.services.dto import CaseInput, CheckType, GoldenRecord, ValidationResult
from cross_document_validation.matching import exact, fuzzy
from cross_document_validation.matching.embeddings import address_similarity
from cross_document_validation.services import validation_config as cfg
from cross_document_validation.services.dto import CaseInput, CheckType, GoldenRecord, ValidationResult

MANDATORY_GOLDEN_FIELDS = {
CheckType.NAME: "name",
Expand All @@ -31,7 +31,9 @@ def check_mandatory_presence(golden: GoldenRecord) -> list[ValidationResult]:
return results


def _exact_result_to_validation(check_type: CheckType, outcome, document_id: str) -> ValidationResult:
def _exact_result_to_validation(
check_type: CheckType, outcome, document_id: str, source_text, target_text
) -> ValidationResult:
passed = outcome.result == exact.MatchResult.MATCH
score = 100.0 if passed else (50.0 if outcome.result == exact.MatchResult.INCONCLUSIVE else 0.0)
return ValidationResult(
Expand All @@ -40,6 +42,7 @@ def _exact_result_to_validation(check_type: CheckType, outcome, document_id: str
score=score,
document_id=document_id,
failure_reason=None if passed else outcome.reason,
evidence={"source_text": source_text, "target_text": target_text, "match_type": "EXACT"},
)


Expand All @@ -64,6 +67,7 @@ def validate_document_against_golden(
score=score,
document_id=document_id,
failure_reason=None if passed else "name_below_threshold",
evidence={"source_text": golden.name, "target_text": doc_name, "match_type": "FUZZY"},
)
)

Expand All @@ -78,20 +82,31 @@ def validate_document_against_golden(
score=score,
document_id=document_id,
failure_reason=None if passed else "address_below_threshold",
evidence={"source_text": golden.address, "target_text": doc_address, "match_type": "SEMANTIC"},
)
)

if doc_aadhaar is not None:
outcome = exact.aadhaar_match(golden.aadhaar_number, doc_aadhaar)
results.append(_exact_result_to_validation(CheckType.AADHAAR, outcome, document_id))
results.append(
_exact_result_to_validation(
CheckType.AADHAAR, outcome, document_id, golden.aadhaar_number, doc_aadhaar
)
)

if doc_pan is not None:
outcome = exact.pan_match(golden.pan_number, doc_pan)
results.append(_exact_result_to_validation(CheckType.PAN, outcome, document_id))
results.append(
_exact_result_to_validation(CheckType.PAN, outcome, document_id, golden.pan_number, doc_pan)
)

if doc_dob is not None:
outcome = exact.dob_match(golden.date_of_birth, doc_dob)
results.append(_exact_result_to_validation(CheckType.DOB, outcome, document_id))
results.append(
_exact_result_to_validation(
CheckType.DOB, outcome, document_id, golden.date_of_birth, doc_dob
)
)

return results

Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
"""Persists one pipeline run (CaseInput + PipelineResult) to the database.

The in-memory dataclasses in app.services.dto reference documents by their
string doc_id (e.g. "AADHAAR", "SALARY_SLIP-0"). This module inserts the
Document rows first and keeps a doc_id -> Document.id map so
ValidationResult.document_id (also a doc_id string) can be resolved to the
The in-memory dataclasses in cross_document_validation.services.dto reference
documents by their string doc_id (e.g. "AADHAAR", "SALARY_SLIP-0"). This
module inserts the Document rows first and keeps a doc_id -> Document.id map
so ValidationResult.document_id (also a doc_id string) can be resolved to the
real foreign key.
"""

import uuid

from sqlalchemy.ext.asyncio import AsyncSession

from app.services.dto import CaseInput, Decision, DocType
from app.services.dto import PipelineResult as PipelineResultDTO
from app.utils.json_safe import json_safe
from cross_document_validation.services.dto import CaseInput, Decision, DocType
from cross_document_validation.services.dto import PipelineResult as PipelineResultDTO
from cross_document_validation.utils.json_safe import json_safe
from db.models.case import Case, CaseStatus
from db.models.document import Document
from db.models.golden_record import GoldenRecord as GoldenRecordModel
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
Decision in one call, with a simple in-memory audit log.
"""

from app.services import business_validation, decision_engine, golden_record, scoring
from app.services import validation_config as cfg
from app.services.dto import CaseInput, Decision, DecisionResult, PipelineResult
from app.services.identity_validation import run_identity_validation
from cross_document_validation.services import business_validation, decision_engine, golden_record, scoring
from cross_document_validation.services import validation_config as cfg
from cross_document_validation.services.dto import CaseInput, Decision, DecisionResult, PipelineResult
from cross_document_validation.services.identity_validation import run_identity_validation


def run_pipeline(case: CaseInput) -> PipelineResult:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

from collections import defaultdict

from app.services import validation_config as cfg
from app.services.dto import ScoreResult, ValidationResult
from cross_document_validation.services import validation_config as cfg
from cross_document_validation.services.dto import ScoreResult, ValidationResult


def compute_score(validation_results: list[ValidationResult]) -> ScoreResult:
Expand Down
Empty file.

This file was deleted.

27 changes: 25 additions & 2 deletions lending-poc/docs/cases_api.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,14 +92,37 @@ Notes:
"passed": true,
"score": 94.5,
"document_id": "PAN",
"evidence": null
"evidence": {
"source_text": "Sneha Sunil Lokhande",
"target_text": "Sneha Lokhande",
"match_type": "FUZZY"
}
},
{
"check_type": "SALARY_DATE",
"passed": false,
"score": 0.0,
"document_id": "SALARY_SLIP-3",
"evidence": { "window": ["2026-05-27", "2026-07-31"] }
"evidence": {
"source_text": "June 2026",
"target_text": null,
"match_type": "DATE_MATCH",
"window": ["2026-05-27", "2026-07-31"]
}
},
{
"check_type": "SALARY_CREDIT_COUNT",
"passed": false,
"score": 75.0,
"document_id": "BANK_STATEMENT",
"evidence": {
"source_value": 4,
"target_value": 3,
"match_type": "COUNT_MATCH",
"stmt_duration": { "start": "2026-04-01", "end": "2026-07-04" },
"total_slips": 4,
"confidence_score": 75.0
}
}
]
}
Expand Down
Loading