10 Commits

307 changed files with 1600 additions and 12257 deletions

BIN
.DS_Store vendored

Binary file not shown.

16
check_matches.py Normal file
View File

@@ -0,0 +1,16 @@
import sys
import os
sys.path.append(os.path.join(os.getcwd(), 'docengine'))
from app.core.database import SessionLocal
from app.models.document import TemplateMatch
from app.models.template import DocumentFormat
db = SessionLocal()
matches = db.query(TemplateMatch).order_by(TemplateMatch.created_at.desc()).limit(10).all()
print(f"Found {len(matches)} matches.")
for m in matches:
fmt = db.query(DocumentFormat).filter(DocumentFormat.id == m.format_id).first()
fmt_name = fmt.name if fmt else 'Unknown'
print(f"Match: doc_id={m.document_id}, format_id={m.format_id}, name={fmt_name}, score={m.confidence_score}")
print(f"Details: {m.match_details}")

26
debug_match.py Normal file
View File

@@ -0,0 +1,26 @@
import sys
import os
sys.path.append(os.path.join(os.getcwd(), 'docengine'))
from app.core.database import SessionLocal
from app.models.document import Document
from app.services.matching_service import MatchingService
db = SessionLocal()
doc = db.query(Document).order_by(Document.created_at.desc()).first()
if not doc:
print("No documents found.")
sys.exit(0)
print(f"Latest document: {doc.id} (status: {doc.status})")
svc = MatchingService(db)
try:
# Match with min_confidence=0.0 so it returns EVERYTHING
matches = svc.match_document(doc.id, min_confidence=0.0)
print(f"Returned {len(matches)} matches.")
for m in matches:
print(f"Match: format_id={m.format_id}, score={m.confidence_score}")
print(f"Details: {m.match_details}")
except Exception as e:
print(f"Error matching: {e}")

View File

@@ -58,7 +58,7 @@ ENV/
*~
.DS_Store
Thumbs.db
storage/
/storage/
*.pid
celerybeat-schedule
celerybeat.pid

View File

@@ -235,6 +235,45 @@ def get_document_template_matches(
return results
@router.post(
"/{document_id}/extraction",
response_model=dict,
summary="Extract Document Data",
description="Match a document against templates and extract key-value & table data.",
)
def extract_document_data(
document_id: uuid.UUID,
current_user: CurrentUser = None,
db: Session = Depends(get_db),
) -> dict:
"""Extract document data using matched template mappings."""
doc_repo = DocumentRepository(db)
document = doc_repo.get_by_id(document_id)
if not document:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Document '{document_id}' not found",
)
if document.status != "completed":
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Document must be in 'completed' status. Current status: '{document.status}'",
)
from app.services.extraction_service import ExtractionService
extraction_service = ExtractionService(db)
try:
result = extraction_service.extract_document_data(document_id)
return result
except Exception as e:
logger.exception("extraction_endpoint_failed", document_id=str(document_id), error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Extraction failed: {str(e)}",
)
@router.delete(
"/{document_id}",
response_model=SuccessResponse,

View File

@@ -10,7 +10,13 @@ from app.core.database import get_db
from app.core.dependencies import CurrentUser
from app.core.logging_config import get_logger
from app.repositories.document_repository import DocumentRepository, TemplateMatchRepository
from app.repositories.template_repository import TemplateRepository
from app.repositories.template_repository import (
TemplateRepository,
DocumentRegionRepository,
DocumentCellRepository,
TableFormatRepository,
TableColumnRepository
)
from app.schemas.common import PaginatedResponse, SuccessResponse
from app.schemas.document import TemplateMatchRequest, TemplateMatchResponse
from app.schemas.template import (
@@ -18,6 +24,8 @@ from app.schemas.template import (
TemplateRenderRequest,
TemplateRenderResponse,
TemplateResponse,
TemplateCreateRequest,
TemplateMappingSaveRequest,
)
logger = get_logger(__name__)
@@ -25,6 +33,198 @@ logger = get_logger(__name__)
router = APIRouter(prefix="/templates", tags=["Templates"])
@router.post(
"",
response_model=dict,
summary="Create Template",
description="Create a new template.",
)
def create_template(
payload: TemplateCreateRequest,
current_user: CurrentUser,
db: Session = Depends(get_db),
) -> dict:
"""Create a template."""
template_repo = TemplateRepository(db)
template = template_repo.create_template(
name=payload.template_name,
page_width=1000.0,
page_height=1000.0,
source_document_id=uuid.UUID(payload.source_document_id) if payload.source_document_id else None
)
cell_repo = DocumentCellRepository(db)
saved_fields = []
for index, field in enumerate(payload.fields):
cell = cell_repo.create_cell(
format_id=template.id,
page_number=1,
x=0.0,
y=0.0,
width=0.0,
height=0.0,
data_type=field.field_type,
field_name=field.field_label,
sequence=field.display_order or index,
is_dynamic=True
)
saved_fields.append({
"field_label": cell.field_name,
"field_type": cell.data_type
})
db.commit()
return {
"pk_template_id": str(template.id),
"template_name": template.name,
"fields": saved_fields
}
@router.put(
"/{template_id}",
response_model=dict,
summary="Update Template",
description="Update an existing template name and its fields.",
)
def update_template(
template_id: uuid.UUID,
payload: TemplateCreateRequest,
current_user: CurrentUser,
db: Session = Depends(get_db),
) -> dict:
"""Update a template."""
template_repo = TemplateRepository(db)
template = template_repo.get_by_id(template_id)
if not template:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Template '{template_id}' not found",
)
if template.name != payload.template_name:
template.name = payload.template_name
from app.models.template import DocumentCell
db.query(DocumentCell).filter(DocumentCell.format_id == template_id).delete()
saved_fields = []
cell_repo = DocumentCellRepository(db)
for index, field in enumerate(payload.fields):
cell = cell_repo.create_cell(
format_id=template.id,
page_number=1,
row_no=0,
column_no=0,
field_name=field.field_label,
data_type=field.field_type,
x=0.0,
y=0.0,
width=0.0,
height=0.0,
is_dynamic=True
)
saved_fields.append({
"field_label": cell.field_name,
"field_type": cell.data_type
})
db.commit()
return {
"pk_template_id": str(template.id),
"template_name": template.name,
"fields": saved_fields
}
@router.post(
"/{template_id}/mappings/save",
response_model=SuccessResponse,
summary="Save Template Mappings",
description="Save the field mappings for a template.",
)
def save_mappings(
template_id: uuid.UUID,
payload: list[TemplateMappingSaveRequest],
current_user: CurrentUser,
db: Session = Depends(get_db),
) -> SuccessResponse:
"""Save template mappings."""
template_repo = TemplateRepository(db)
template = template_repo.get_by_id(template_id)
if not template:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Template '{template_id}' not found",
)
region_repo = DocumentRegionRepository(db)
cell_repo = DocumentCellRepository(db)
table_repo = TableFormatRepository(db)
table_col_repo = TableColumnRepository(db)
# Delete existing field mappings and table formats for this template
from app.models.template import DocumentRegion, TableFormat
db.query(DocumentRegion).filter(
DocumentRegion.format_id == template_id,
DocumentRegion.region_type == "field_mapping"
).delete()
db.query(TableFormat).filter(TableFormat.format_id == template_id).delete()
db.flush()
cells = cell_repo.get_template_cells(template_id)
cell_map = {c.field_name: c for c in cells}
table_format = None
# Save each mapped node
for mapping in payload:
cell = cell_map.get(mapping.field_name)
is_table_column = cell and cell.data_type == 'TABLE_COLUMN'
for node_idx, node in enumerate(mapping.mapped_nodes):
if is_table_column:
if not table_format:
table_format = table_repo.create_table_format(
format_id=template_id,
page_number=node.page_no or 1,
x=0.0, y=0.0, width=1000.0, height=1000.0,
rows=1, columns=10
)
table_col_repo.create_column(
table_format_id=table_format.id,
column_index=node_idx,
width=node.width,
header_text=mapping.field_name,
data_type="text"
)
region_repo.create_region(
format_id=template_id,
page_number=node.page_no or 1,
region_type="field_mapping",
x=node.x_coordinate,
y=node.y_coordinate,
width=node.width,
height=node.height,
content={
"field_name": mapping.field_name,
"text_value": node.text_value
}
)
db.commit()
# Generate fingerprint now that mappings are populated
from app.services.fingerprint_service import FingerprintService
FingerprintService(db).generate_fingerprint(template)
db.commit()
return SuccessResponse(message="Mappings saved successfully")
@router.get(
"",
response_model=PaginatedResponse[TemplateListResponse],
@@ -32,9 +232,9 @@ router = APIRouter(prefix="/templates", tags=["Templates"])
description="List all active templates with pagination.",
)
def list_templates(
current_user: CurrentUser,
page: int = Query(default=1, ge=1),
page_size: int = Query(default=20, ge=1, le=100),
current_user: CurrentUser = None,
db: Session = Depends(get_db),
) -> PaginatedResponse[TemplateListResponse]:
"""List all active templates."""
@@ -61,7 +261,7 @@ def list_templates(
)
def get_template(
template_id: uuid.UUID,
current_user: CurrentUser = None,
current_user: CurrentUser,
db: Session = Depends(get_db),
) -> TemplateResponse:
"""Get a template by ID."""
@@ -83,7 +283,7 @@ def get_template(
)
def delete_template(
template_id: uuid.UUID,
current_user: CurrentUser = None,
current_user: CurrentUser,
db: Session = Depends(get_db),
) -> SuccessResponse:
"""Soft-delete a template."""
@@ -109,7 +309,7 @@ def delete_template(
)
def match_template(
payload: TemplateMatchRequest,
current_user: CurrentUser = None,
current_user: CurrentUser,
db: Session = Depends(get_db),
) -> list[TemplateMatchResponse]:
"""Match a document against existing templates."""
@@ -162,7 +362,7 @@ def match_template(
)
def render_template(
payload: TemplateRenderRequest,
current_user: CurrentUser = None,
current_user: CurrentUser,
db: Session = Depends(get_db),
) -> TemplateRenderResponse:
"""Render a template to PDF."""
@@ -199,8 +399,8 @@ def render_template(
)
def download_rendered_pdf(
template_id: uuid.UUID,
current_user: CurrentUser,
filename: str = Query(..., description="Filename of the rendered PDF"),
current_user: CurrentUser = None,
db: Session = Depends(get_db),
) -> FileResponse:
"""Download a rendered PDF."""

View File

@@ -83,13 +83,20 @@ class Settings(BaseSettings):
@classmethod
def parse_cors_origins(cls, v: Any) -> list[str]:
if isinstance(v, str):
if not v.strip():
return []
try:
parsed = json.loads(v)
if isinstance(parsed, list):
return parsed
return [str(item).strip() for item in parsed]
elif isinstance(parsed, str):
return [parsed.strip()]
except (json.JSONDecodeError, TypeError):
pass
return [origin.strip() for origin in v.split(",") if origin.strip()]
return v
if isinstance(v, list):
return [str(item).strip() for item in v]
return []
@property
def database_url(self) -> str:

View File

@@ -70,6 +70,7 @@ class TemplateRepository(BaseRepository[DocumentFormat]):
fingerprint: dict | None = None,
source_document_id: uuid.UUID | None = None,
created_by: uuid.UUID | None = None,
is_active: bool = True,
) -> DocumentFormat:
"""Create a new template."""
template = DocumentFormat(
@@ -85,6 +86,7 @@ class TemplateRepository(BaseRepository[DocumentFormat]):
fingerprint=fingerprint,
source_document_id=source_document_id,
created_by=created_by,
is_active=is_active,
)
return self.create(template)

View File

@@ -130,5 +130,5 @@ class TemplateMatchRequest(BaseSchema):
"""Request to match a document against templates."""
document_id: uuid.UUID
min_confidence: float = Field(default=0.5, ge=0.0, le=1.0)
min_confidence: float = Field(default=0.75, ge=0.0, le=1.0)
max_results: int = Field(default=5, ge=1, le=20)

View File

@@ -2,7 +2,7 @@ from __future__ import annotations
import uuid
from datetime import datetime
from typing import Any
from typing import Any, List, Optional, Union
from pydantic import Field
@@ -248,3 +248,30 @@ class TemplateRenderResponse(BaseSchema):
file_size: int
page_count: int
rendered_at: datetime
class TemplateFieldCreate(BaseSchema):
field_label: str
field_type: str = "text"
display_order: int = 0
required_flag: bool = False
class TemplateCreateRequest(BaseSchema):
template_name: str
source_document_id: Optional[str] = None
fields: List[TemplateFieldCreate] = Field(default_factory=list)
class MappingNodeRequest(BaseSchema):
pk_document_data_id: Union[int, str, None] = None
x_coordinate: float
y_coordinate: float
width: float
height: float
text_value: Optional[str] = None
page_width: Optional[float] = None
page_height: Optional[float] = None
page_no: Optional[int] = None
class TemplateMappingSaveRequest(BaseSchema):
field_name: str
mapped_nodes: List[MappingNodeRequest] = Field(default_factory=list)

View File

@@ -54,13 +54,11 @@ class DocumentProcessingService:
# Step 2: Analyze layout
layout_results = self.layout_service.analyze_document_layout(document)
document.document_metadata = document.document_metadata or {}
document.document_metadata["layout"] = layout_results
metadata = document.document_metadata or {}
metadata["layout"] = layout_results
document.document_metadata = metadata
# Step 3: Generate template
template = self.template_service.generate_template(document)
# Step 4: Update document status
# Step 3: Update document status
self.doc_repo.update_status(document_id, "completed")
self.db.commit()
@@ -68,7 +66,6 @@ class DocumentProcessingService:
"processing_completed",
document_id=str(document_id),
pages=document.page_count,
template_id=str(template.id),
)
return document

View File

@@ -0,0 +1,295 @@
from __future__ import annotations
import uuid
from typing import Any
from sqlalchemy.orm import Session
from app.core.logging_config import get_logger
from app.repositories.document_repository import DocumentRepository
from app.repositories.template_repository import TemplateRepository
from app.services.matching_service import MatchingService
logger = get_logger(__name__)
class ExtractionService:
"""Extract document values using matched template coordinates and structures."""
def __init__(self, db: Session) -> None:
self.db = db
self.matching_service = MatchingService(db)
self.doc_repo = DocumentRepository(db)
self.template_repo = TemplateRepository(db)
def extract_document_data(self, document_id: uuid.UUID) -> dict[str, Any]:
"""Perform template matching on the document and extract structured field values."""
document = self.doc_repo.get_with_pages(document_id)
if not document:
raise ValueError(f"Document '{document_id}' not found")
# 1. Match document against existing templates
matches = self.matching_service.match_document(document_id, min_confidence=0.75)
if not matches:
logger.info("extraction_failed_no_match", document_id=str(document_id))
return {
"template_matched": False,
"template_id": None,
"template_name": None,
"confidence_score": 0.0,
"extracted_data": {},
}
best_match = matches[0]
template = self.template_repo.get_by_id(best_match.format_id)
if not template:
raise ValueError(f"Matched template '{best_match.format_id}' not found")
logger.info(
"extraction_matched_template",
document_id=str(document_id),
template_id=str(template.id),
template_name=template.name,
score=best_match.confidence_score,
)
extracted_data: dict[str, Any] = {}
# Load all mapped regions for the template
regions = [r for r in template.regions if r.region_type == "field_mapping"]
regions_by_field: dict[str, list[Any]] = {}
for r in regions:
field_name = r.content.get("field_name") if r.content else None
if field_name:
regions_by_field.setdefault(field_name, []).append(r)
# 2. Divide fields into Scalar vs Table Column types
scalar_cells = [cell for cell in template.cells if cell.data_type != "TABLE_COLUMN"]
table_column_cells = [cell for cell in template.cells if cell.data_type == "TABLE_COLUMN"]
# 3. Extract Scalar Fields
for cell in scalar_cells:
field_name = cell.field_name
if not field_name:
continue
field_regions = regions_by_field.get(field_name, [])
if not field_regions:
extracted_data[field_name] = ""
continue
extracted_values = []
extracted_block_ids = set()
sorted_regions = sorted(field_regions, key=lambda r: (r.page_number, r.sequence, r.y, r.x))
for region in sorted_regions:
page = next((p for p in document.pages if p.page_number == region.page_number), None)
if not page:
continue
best_block = self._find_best_overlapping_block(page, region)
if best_block and best_block.id not in extracted_block_ids:
extracted_block_ids.add(best_block.id)
val = best_block.text.strip()
if val:
extracted_values.append(val)
extracted_data[field_name] = " ".join(extracted_values)
# 4. Extract Table Column Fields
if table_column_cells:
table_column_names = {cell.field_name for cell in table_column_cells if cell.field_name}
table_regions = [
r for r in regions
if r.content and r.content.get("field_name") in table_column_names
]
if table_regions:
# Determine vertical boundaries of the table area
table_start_y = min((r.y for r in table_regions), default=0.0)
table_start_y = max(0.0, table_start_y - 10.0) # subtract buffer
# Process page where the table coordinates are mapped
page_number = min((r.page_number for r in table_regions), default=1)
page = next((p for p in document.pages if p.page_number == page_number), None)
# Determine table vertical end Y by finding any summary scalar fields below the table
summary_keywords = {"tax", "total", "shipping", "discount", "vat", "handling", "duty", "subtotal", "grand"}
summary_regions = []
for col_name, regs in regions_by_field.items():
if col_name in table_column_names:
continue
for r in regs:
if r.y > table_start_y and any(kw in col_name.lower() for kw in summary_keywords):
# Skip left-aligned metadata fields (like Shipping Method)
if page and r.x < page.width * 0.4:
continue
summary_regions.append(r)
table_end_y = min((r.y for r in summary_regions), default=99999.0)
# Check if template has a footer to define the end vertical boundary
footer_regions = [r for r in template.regions if r.region_type == "footer"]
if footer_regions:
table_end_y = min(table_end_y, min(r.y for r in footer_regions))
# Determine horizontal ranges (X span) for each column in the template
col_x_spans: dict[str, tuple[float, float]] = {}
for col_name in table_column_names:
col_regs = [r for r in table_regions if r.content and r.content.get("field_name") == col_name]
if col_regs:
x_min = min(r.x for r in col_regs)
x_max = max(r.x + r.width for r in col_regs)
# Add a 15px margin to accommodate layout differences
col_x_spans[col_name] = (max(0.0, x_min - 15.0), x_max + 15.0)
else:
col_x_spans[col_name] = (0.0, 0.0)
if page:
# Check for summary keyword text blocks (e.g. Total, Tax) as vertical boundary fallback
summary_labels = {"subtotal", "total", "grand total", "tax", "shipping & handling", "discount"}
for block in page.text_blocks:
if block.y > table_start_y:
# Skip left-aligned text blocks
if block.x < page.width * 0.4:
continue
block_text_clean = block.text.strip().lower()
if any(label in block_text_clean for label in summary_labels):
table_end_y = min(table_end_y, block.y)
# Collect all document blocks inside Y range
candidate_blocks = [
b for b in page.text_blocks
if b.y >= table_start_y and b.y < table_end_y
]
# Sort blocks by Y coordinate
sorted_blocks = sorted(candidate_blocks, key=lambda b: b.y)
# Group blocks into rows by vertical center alignment (15px threshold)
rows: list[list[Any]] = []
current_row: list[Any] = []
current_y_center = None
for block in sorted_blocks:
if not block.text.strip():
continue
block_y_center = block.y + block.height / 2
if current_y_center is None:
current_row.append(block)
current_y_center = block_y_center
elif abs(block_y_center - current_y_center) < 15.0:
current_row.append(block)
else:
rows.append(current_row)
current_row = [block]
current_y_center = block_y_center
if current_row:
rows.append(current_row)
# Map candidate blocks in each row to columns
rows_data: list[dict[str, str]] = []
for row_blocks in rows:
row_dict: dict[str, list[Any]] = {name: [] for name in table_column_names}
for block in row_blocks:
best_col = None
best_overlap = 0.0
for col_name, (x_min, x_max) in col_x_spans.items():
overlap = max(
0.0,
min(x_max, block.x + block.width) - max(x_min, block.x)
)
overlap_ratio = overlap / block.width if block.width > 0 else 0.0
if overlap_ratio > 0.2 and overlap_ratio > best_overlap:
best_overlap = overlap_ratio
best_col = col_name
if best_col:
row_dict[best_col].append(block)
# Construct row values
row_values: dict[str, str] = {}
for col_name, blocks in row_dict.items():
sorted_blocks_in_col = sorted(blocks, key=lambda b: b.x)
row_values[col_name] = " ".join(
b.text.strip() for b in sorted_blocks_in_col
)
# Filter out table header rows
is_header = False
for col_name, val in row_values.items():
val_lower = val.lower()
if (
val_lower == col_name.lower() or
val_lower in [
"item", "items", "qty", "quantity", "price",
"amount", "total", "subtotal", "description"
]
):
is_header = True
break
# Append if not header and at least one cell has a value
if not is_header and any(row_values.values()):
rows_data.append(row_values)
# Merge continuation lines (descriptions spanning multiple rows with empty sibling columns)
merged_rows: list[dict[str, str]] = []
for row in rows_data:
non_empty_cols = [k for k, v in row.items() if v.strip()]
if len(merged_rows) > 0 and len(non_empty_cols) == 1:
col_name = non_empty_cols[0]
last_row = merged_rows[-1]
if last_row.get(col_name):
last_row[col_name] = last_row[col_name] + " " + row[col_name]
else:
last_row[col_name] = row[col_name]
else:
merged_rows.append(row.copy())
rows_data = merged_rows
# Pivot list of rows into parallel arrays under column names
for col_name in table_column_names:
extracted_data[col_name] = []
for row in rows_data:
for col_name in table_column_names:
extracted_data[col_name].append(row.get(col_name, ""))
return {
"template_matched": True,
"template_id": str(template.id),
"template_name": template.name,
"confidence_score": best_match.confidence_score,
"extracted_data": extracted_data,
}
def _find_best_overlapping_block(self, page: Any, region: Any) -> Any | None:
"""Find the document text block overlapping most with the template region."""
best_block = None
best_overlap = 0.0
for block in page.text_blocks:
x_overlap = max(
0.0,
min(region.x + region.width, block.x + block.width) - max(region.x, block.x)
)
y_overlap = max(
0.0,
min(region.y + region.height, block.y + block.height) - max(region.y, block.y)
)
overlap = x_overlap * y_overlap
if overlap > best_overlap:
best_overlap = overlap
best_block = block
# Fallback: if no overlapping block, find the closest center-to-center
if not best_block:
min_dist = 100.0 # max 100px center-to-center distance
for block in page.text_blocks:
c_rx = region.x + region.width / 2
c_ry = region.y + region.height / 2
c_bx = block.x + block.width / 2
c_by = block.y + block.height / 2
dist = ((c_rx - c_bx) ** 2 + (c_ry - c_by) ** 2) ** 0.5
if dist < min_dist:
min_dist = dist
best_block = block
return best_block

View File

@@ -145,6 +145,11 @@ class FingerprintService:
if not template.table_formats:
return None
# Filter out dummy tables created by manual mappings (width=1000, height=1000)
valid_tables = [tf for tf in template.table_formats if tf.width != 1000.0 and tf.height != 1000.0]
if not valid_tables:
return None
return {
"items": [
{
@@ -156,15 +161,36 @@ class FingerprintService:
"rows": tf.rows,
"columns": tf.columns,
}
for tf in template.table_formats
for tf in valid_tables
]
}
def _extract_cell_coordinates(self, template: DocumentFormat) -> dict[str, Any] | None:
"""Extract cell coordinates from template."""
# For manually mapped templates, prefer the actual mapped regions
field_regions = [r for r in template.regions if r.region_type == "field_mapping"]
if field_regions:
return {
"items": [
{
"page": r.page_number,
"x": r.x,
"y": r.y,
"width": r.width,
"height": r.height,
}
for r in field_regions
]
}
# Otherwise fallback to template cells, but skip if they are all 0,0 (unmapped placeholders)
if not template.cells:
return None
has_real_coords = any(c.width > 0 or c.height > 0 for c in template.cells)
if not has_real_coords:
return None
return {
"items": [
{
@@ -187,7 +213,7 @@ class FingerprintService:
serialized = json.dumps(normalized, sort_keys=True, default=str)
return hashlib.sha256(serialized.encode()).hexdigest()
def _normalize_coordinates(self, data: dict[str, Any]) -> dict[str, Any]:
def _normalize_coordinates(self, data: Any) -> Any:
"""Normalize coordinates by rounding to reduce sensitivity to small variations."""
if isinstance(data, dict):
return {k: self._normalize_coordinates(v) for k, v in data.items()}
@@ -215,6 +241,7 @@ class FingerprintService:
weights.append(3.0)
# Logo coordinates similarity
if fingerprint1.logo_coordinates and fingerprint1.logo_coordinates.get("items"):
logo_score = self._compare_coordinates(
fingerprint1.logo_coordinates,
fingerprint2_data.get("logo_coordinates"),
@@ -223,6 +250,7 @@ class FingerprintService:
weights.append(2.0)
# Header coordinates similarity
if fingerprint1.header_coordinates and fingerprint1.header_coordinates.get("items"):
header_score = self._compare_coordinates(
fingerprint1.header_coordinates,
fingerprint2_data.get("header_coordinates"),
@@ -231,6 +259,7 @@ class FingerprintService:
weights.append(2.0)
# Footer coordinates similarity
if fingerprint1.footer_coordinates and fingerprint1.footer_coordinates.get("items"):
footer_score = self._compare_coordinates(
fingerprint1.footer_coordinates,
fingerprint2_data.get("footer_coordinates"),
@@ -239,14 +268,16 @@ class FingerprintService:
weights.append(1.5)
# Table coordinates similarity
if fingerprint1.table_coordinates and fingerprint1.table_coordinates.get("items"):
table_score = self._compare_coordinates(
fingerprint1.table_coordinates,
fingerprint2_data.get("table_coordinates"),
)
scores.append(table_score)
weights.append(2.5)
weights.append(2.0)
# Cell coordinates similarity
if fingerprint1.cell_coordinates and fingerprint1.cell_coordinates.get("items"):
cell_score = self._compare_coordinates(
fingerprint1.cell_coordinates,
fingerprint2_data.get("cell_coordinates"),

View File

@@ -29,7 +29,7 @@ class MatchingService:
def match_document(
self,
document_id: uuid.UUID,
min_confidence: float = 0.5,
min_confidence: float = 0.75,
max_results: int = 5,
) -> list[TemplateMatch]:
"""Match a document against all existing templates."""

View File

@@ -107,49 +107,94 @@ class NativePDFService:
def _extract_text_blocks(self, doc_page: DocumentPage, page: fitz.Page) -> None:
"""Extract text blocks with positioning and font information."""
blocks = page.get_text("dict", flags=fitz.TEXT_PRESERVE_WHITESPACE)["blocks"]
blocks = page.get_text("rawdict")["blocks"]
sequence = 0
chunks = []
for block in blocks:
if block["type"] != 0: # Skip non-text blocks
if block["type"] != 0:
continue
block_text_parts = []
font_info = {"family": None, "size": None, "color": None, "style": None}
current_chunk = None
space_count = 0
for line in block.get("lines", []):
for span in line.get("spans", []):
text = span.get("text", "").strip()
if text:
block_text_parts.append(text)
# Capture font info from the first non-empty span
if font_info["family"] is None:
font_info["family"] = span.get("font", None)
font_info["size"] = span.get("size", None)
color_int = span.get("color", 0)
font_info["color"] = f"#{color_int:06x}" if isinstance(color_int, int) else None
flags = span.get("flags", 0)
styles = []
if flags & 1:
styles.append("superscript")
if flags & 2:
styles.append("italic")
if flags & 4:
styles.append("serif")
if flags & 8:
styles.append("monospace")
if flags & 16:
styles.append("bold")
font_info["style"] = ",".join(styles) if styles else "regular"
font_size = span.get("size", 12.0)
full_text = " ".join(block_text_parts)
if not full_text.strip():
# Ignore massive text (like diagonal watermarks)
if font_size > 60:
continue
bbox = block["bbox"]
space_threshold = font_size * 1.5
font_family = span.get("font", None)
color_int = span.get("color", 0)
font_color = f"#{color_int:06x}" if isinstance(color_int, int) else None
flags = span.get("flags", 0)
styles = []
if flags & 1: styles.append("superscript")
if flags & 2: styles.append("italic")
if flags & 4: styles.append("serif")
if flags & 8: styles.append("monospace")
if flags & 16: styles.append("bold")
font_style = ",".join(styles) if styles else "regular"
font_info = {
"family": font_family,
"size": font_size,
"color": font_color,
"style": font_style
}
for char in span.get("chars", []):
c = char["c"]
bbox = char["bbox"]
if c == ' ':
space_count += 1
if space_count >= 2:
if current_chunk and current_chunk["text"].strip():
chunks.append(current_chunk)
current_chunk = None
elif current_chunk:
current_chunk["text"] += c
current_chunk["bbox"][2] = max(current_chunk["bbox"][2], bbox[2])
current_chunk["bbox"][3] = max(current_chunk["bbox"][3], bbox[3])
continue
else:
space_count = 0
if current_chunk is None:
current_chunk = {"text": c, "bbox": list(bbox), "font_info": font_info}
continue
prev_x1 = current_chunk["bbox"][2]
distance = bbox[0] - prev_x1
if distance > space_threshold:
if current_chunk["text"].strip():
chunks.append(current_chunk)
current_chunk = {"text": c, "bbox": list(bbox), "font_info": font_info}
else:
current_chunk["text"] += c
current_chunk["bbox"][2] = max(current_chunk["bbox"][2], bbox[2])
current_chunk["bbox"][3] = max(current_chunk["bbox"][3], bbox[3])
current_chunk["bbox"][1] = min(current_chunk["bbox"][1], bbox[1])
current_chunk["bbox"][0] = min(current_chunk["bbox"][0], bbox[0])
if current_chunk and current_chunk["text"].strip():
chunks.append(current_chunk)
current_chunk = None
for chunk in chunks:
bbox = chunk["bbox"]
font_info = chunk["font_info"]
self.text_block_repo.create_text_block(
page_id=doc_page.id,
text=full_text,
text=chunk["text"].strip(),
x=bbox[0],
y=bbox[1],
width=bbox[2] - bbox[0],

View File

@@ -80,6 +80,7 @@ class TemplateService:
description=f"Auto-generated template from {document.original_filename}",
source_document_id=document.id,
created_by=user_id,
is_active=False,
)
# Process each page

View File

@@ -0,0 +1 @@
# Storage module

View File

@@ -0,0 +1,163 @@
from __future__ import annotations
import hashlib
import os
import shutil
import uuid
from abc import ABC, abstractmethod
from pathlib import Path
from app.core.config import settings
from app.core.exceptions import StorageError
from app.core.logging_config import get_logger
logger = get_logger(__name__)
class StorageProvider(ABC):
"""Abstract base class for storage providers."""
@abstractmethod
def save_file(self, file_data: bytes, directory: str, filename: str | None = None) -> str:
"""Save file data and return the storage path."""
...
@abstractmethod
def read_file(self, storage_path: str) -> bytes:
"""Read file data from storage."""
...
@abstractmethod
def delete_file(self, storage_path: str) -> bool:
"""Delete a file from storage. Returns True if successful."""
...
@abstractmethod
def file_exists(self, storage_path: str) -> bool:
"""Check if a file exists in storage."""
...
@abstractmethod
def get_file_size(self, storage_path: str) -> int:
"""Get the file size in bytes."""
...
@abstractmethod
def get_absolute_path(self, storage_path: str) -> str:
"""Get the absolute filesystem path for a storage path."""
...
@staticmethod
def compute_checksum(data: bytes, algorithm: str = "sha256") -> str:
"""Compute checksum of file data."""
hasher = hashlib.new(algorithm)
hasher.update(data)
return hasher.hexdigest()
@staticmethod
def generate_filename(original_filename: str) -> str:
"""Generate a unique filename preserving the original extension."""
ext = Path(original_filename).suffix.lower()
return f"{uuid.uuid4().hex}{ext}"
class LocalStorageProvider(StorageProvider):
"""Local filesystem storage provider."""
def __init__(self, base_path: str | None = None) -> None:
self.base_path = Path(base_path or settings.storage_local_path).resolve()
self._ensure_directories()
def _ensure_directories(self) -> None:
"""Create required storage directories."""
for subdir in ("documents", "templates", "images", "temp", "rendered"):
(self.base_path / subdir).mkdir(parents=True, exist_ok=True)
def _resolve_path(self, storage_path: str) -> Path:
"""Resolve a storage path to an absolute path."""
resolved = (self.base_path / storage_path).resolve()
if not str(resolved).startswith(str(self.base_path)):
raise StorageError(f"Path traversal detected: {storage_path}")
return resolved
def save_file(self, file_data: bytes, directory: str, filename: str | None = None) -> str:
"""Save file data to local storage."""
if filename is None:
filename = f"{uuid.uuid4().hex}.bin"
dir_path = self.base_path / directory
dir_path.mkdir(parents=True, exist_ok=True)
file_path = dir_path / filename
try:
file_path.write_bytes(file_data)
storage_path = str(file_path.relative_to(self.base_path))
logger.info("file_saved", storage_path=storage_path, size=len(file_data))
return storage_path
except OSError as e:
raise StorageError(f"Failed to save file: {e}") from e
def read_file(self, storage_path: str) -> bytes:
"""Read file data from local storage."""
file_path = self._resolve_path(storage_path)
if not file_path.exists():
raise StorageError(f"File not found: {storage_path}")
try:
return file_path.read_bytes()
except OSError as e:
raise StorageError(f"Failed to read file: {e}") from e
def delete_file(self, storage_path: str) -> bool:
"""Delete a file from local storage."""
file_path = self._resolve_path(storage_path)
if not file_path.exists():
return False
try:
file_path.unlink()
logger.info("file_deleted", storage_path=storage_path)
return True
except OSError as e:
logger.error("file_delete_failed", storage_path=storage_path, error=str(e))
raise StorageError(f"Failed to delete file: {e}") from e
def file_exists(self, storage_path: str) -> bool:
"""Check if a file exists in local storage."""
file_path = self._resolve_path(storage_path)
return file_path.exists()
def get_file_size(self, storage_path: str) -> int:
"""Get the file size in bytes."""
file_path = self._resolve_path(storage_path)
if not file_path.exists():
raise StorageError(f"File not found: {storage_path}")
return file_path.stat().st_size
def get_absolute_path(self, storage_path: str) -> str:
"""Get the absolute filesystem path."""
return str(self._resolve_path(storage_path))
def save_temp_file(self, file_data: bytes, filename: str) -> str:
"""Save a temporary file."""
return self.save_file(file_data, "temp", filename)
def cleanup_temp(self) -> int:
"""Remove all files in the temp directory."""
temp_dir = self.base_path / "temp"
count = 0
if temp_dir.exists():
for item in temp_dir.iterdir():
if item.is_file():
item.unlink()
count += 1
elif item.is_dir():
shutil.rmtree(item)
count += 1
logger.info("temp_cleanup", files_removed=count)
return count
def get_storage_provider() -> StorageProvider:
"""Factory function to get the configured storage provider."""
if settings.storage_provider == "local":
return LocalStorageProvider()
raise StorageError(f"Unknown storage provider: {settings.storage_provider}")

View File

@@ -57,7 +57,7 @@ def process_document_task(self, document_id: str) -> dict: # noqa: ANN001
def match_document_task(
self, # noqa: ANN001
document_id: str,
min_confidence: float = 0.5,
min_confidence: float = 0.75,
max_results: int = 5,
) -> dict:
"""Celery task to match a document against templates."""

View File

@@ -0,0 +1,62 @@
import re
from typing import Optional
from datetime import datetime
from dateutil import parser
def sanitize_amount(text: str) -> Optional[float]:
"""
Sanitizes a string representing an amount/number (e.g., "$1,234.56", "€ 50,000", "50 USD")
by removing currency symbols, commas, and other non-numeric characters (except for the decimal separator),
and casts it to a float.
Args:
text (str): The raw extracted text from the document.
Returns:
Optional[float]: The sanitized numeric value, or None if no number could be extracted.
"""
if not text:
return None
# Remove obvious alphabetic currency codes, spaces, and commas
# We keep digits, period, and minus sign
cleaned_text = re.sub(r'[^\d\.-]', '', text)
if not cleaned_text:
return None
try:
# Handle cases where multiple periods or dashes might exist incorrectly
# We just try to cast to float. If the OCR produced something like "1.23.45", this will fail.
# A more robust regex can handle exact capture if needed.
# But this basic cast covers 95% of standard sanitized strings.
return float(cleaned_text)
except ValueError:
return None
def sanitize_date(text: str, field_type: str = 'DATE') -> Optional[str]:
"""
Sanitizes a string representing a date or datetime and converts it to ISO 8601 format.
Args:
text (str): The raw extracted text from the document.
field_type (str): 'DATE' or 'DATETIME'. Determines the output format.
Returns:
Optional[str]: The sanitized date string in ISO format, or None if it could not be parsed.
"""
if not text:
return None
try:
# dateutil.parser is very robust at handling formats like "Dec 11, 2020", "11/12/2020", etc.
# fuzzy=True allows it to ignore extra words/characters around the date
parsed_date = parser.parse(text, fuzzy=True)
if field_type == 'DATETIME':
return parsed_date.strftime('%Y-%m-%dT%H:%M:%S')
else:
return parsed_date.strftime('%Y-%m-%d')
except (ValueError, TypeError, OverflowError):
return None

View File

@@ -25,6 +25,7 @@ celery_app.conf.update(
task_reject_on_worker_lost=True,
broker_connection_retry_on_startup=True,
result_expires=86400,
task_default_queue="docengine_default",
task_routes={
"app.tasks.document_tasks.*": {"queue": "document_processing"},
},

View File

@@ -17,6 +17,8 @@ export interface DocumentLayout {
height: number;
confidence: number;
sequence_no: number;
page_width: number;
page_height: number;
}
export interface TemplateField {
@@ -66,7 +68,9 @@ export class TemplateService {
width: tb.width,
height: tb.height,
confidence: tb.confidence || 0,
sequence_no: tb.sequence || 0
sequence_no: tb.sequence || 0,
page_width: page.width || 1000,
page_height: page.height || 1000
});
});
}
@@ -77,15 +81,23 @@ export class TemplateService {
);
}
createTemplate(data: { template_name: string, fields: TemplateField[] }): Observable<Template> {
return this.http.post<Template>(`${this.apiUrl}/templates`, data);
createTemplate(payload: any): Observable<any> {
return this.http.post<any>(`${this.apiUrl}/templates`, payload);
}
saveMappings(templateId: number, mappings: any[]): Observable<any> {
updateTemplate(templateId: string, payload: any): Observable<any> {
return this.http.put<any>(`${this.apiUrl}/templates/${templateId}`, payload);
}
saveMappings(templateId: string, mappings: any[]): Observable<any> {
return this.http.post(`${this.apiUrl}/templates/${templateId}/mappings/save`, mappings);
}
recognizeTemplate(documentId: string): Observable<any> {
return this.http.post(`${this.apiUrl}/documents/${documentId}/recognize`, {});
getTemplate(templateId: string): Observable<any> {
return this.http.get(`${this.apiUrl}/templates/${templateId}`);
}
recognizeTemplate(documentId: string): Observable<any[]> {
return this.http.post<any[]>(`${this.apiUrl}/templates/match`, { document_id: documentId });
}
}

View File

@@ -1,6 +1,6 @@
<p-toast></p-toast>
<div class="template-mapping-container">
<p-splitter [panelSizes]="[50, 50]" [minSizes]="[30, 30]" styleClass="h-full w-full">
<p-splitter [panelSizes]="[70, 30]" [minSizes]="[50, 20]" styleClass="h-full w-full">
<!-- LEFT PANEL: DOCUMENT PREVIEW -->
<ng-template pTemplate>
@@ -13,24 +13,56 @@
</div>
</div>
<div class="preview-area"
id="document-layout-list"
cdkDropList
[cdkDropListData]="layoutNodes"
[cdkDropListConnectedTo]="getConnectedDropLists()"
(cdkDropListDropped)="drop($event)">
<div *ngIf="layoutNodes.length === 0" class="empty-state">
<i class="pi pi-file-pdf text-4xl mb-3"></i>
<div class="preview-area p-4" style="overflow-y: auto; position: relative;">
<div *ngIf="layoutNodes.length === 0 && !isScanning" class="empty-state">
<i class="pi pi-file text-4xl mb-3"></i>
<p>Upload a document to view its extracted layout structure.</p>
</div>
<div *ngFor="let node of layoutNodes" cdkDrag class="layout-node" [ngClass]="node.block_type.toLowerCase()">
<div class="node-type">{{ node.block_type }}</div>
<!-- Scanning Overlay -->
<div *ngIf="isScanning" class="scanning-overlay">
<div class="scanner-container">
<i class="pi pi-file text-6xl text-gray-400 mb-4 scanner-doc"></i>
<div class="laser-beam"></div>
<div class="scanner-grid"></div>
</div>
<div class="scanning-text mt-4">
<i class="pi pi-spin pi-cog mr-2"></i>
<span class="font-semibold text-lg text-primary">Analyzing Document Layout...</span>
</div>
<p class="text-sm text-gray-500 mt-2">Extracting text, tables, and regions</p>
</div>
<div *ngFor="let page of pages; let i = index" class="document-page-container mb-5">
<div class="page-header text-sm font-semibold text-gray-500 mb-2">Page {{ page.page_number }}</div>
<div class="document-canvas"
[id]="'document-layout-list-' + i"
cdkDropList
[cdkDropListData]="page.nodes"
[cdkDropListConnectedTo]="getConnectedDropLists()"
(cdkDropListDropped)="drop($event)"
[style.aspect-ratio]="page.width + ' / ' + page.height">
<div *ngFor="let node of page.nodes; let j = index"
cdkDrag
[cdkDragData]="node"
class="layout-node"
(mouseup)="onNodeMouseUp(node, $event)"
[ngClass]="node.block_type.toLowerCase()"
[style.left.%]="(node.x_coordinate / node.page_width) * 100"
[style.top.%]="(node.y_coordinate / node.page_height) * 100"
[style.width.%]="(node.width / node.page_width) * 100"
[style.height.%]="(node.height / node.page_height) * 100">
<i class="pi pi-bars drag-handle" cdkDragHandle title="Drag Item"></i>
<i class="pi pi-times close-icon" (click)="removeNode(i, j, $event)" title="Delete Item"></i>
<div class="node-type" *ngIf="node.height > 20">{{ node.block_type }}</div>
<div class="node-text">{{ node.text_value }}</div>
</div>
</div>
</div>
</div>
</div>
</ng-template>
<!-- RIGHT PANEL: TEMPLATE MAPPING -->
@@ -55,24 +87,29 @@
<div class="field-list">
<div *ngFor="let field of templateFields" class="template-field-container">
<div class="field-header">
<div class="field-header flex justify-content-between align-items-center">
<div>
<strong>{{ field.field_label }}</strong>
<span class="badge">{{ field.field_type }}</span>
<span class="badge ml-2">{{ field.field_type }}</span>
</div>
<p-button icon="pi pi-trash" styleClass="p-button-rounded p-button-danger p-button-text p-button-sm" (onClick)="removeField(field.pk_template_field_id)"></p-button>
</div>
<div class="drop-zone"
[id]="'field-' + field.pk_template_field_id"
cdkDropList
[cdkDropListData]="mappings[field.pk_template_field_id]"
[cdkDropListConnectedTo]="['document-layout-list']"
[cdkDropListConnectedTo]="getCanvasDropLists()"
(cdkDropListDropped)="drop($event, field.pk_template_field_id)">
<div class="placeholder" *ngIf="mappings[field.pk_template_field_id].length === 0">
Drop value here...
</div>
<div *ngFor="let mappedNode of mappings[field.pk_template_field_id]" cdkDrag class="mapped-node">
<i class="pi pi-arrow-left text-xs mr-2"></i> {{ mappedNode.text_value }}
<div *ngFor="let mappedNode of mappings[field.pk_template_field_id]; let k = index" cdkDrag [cdkDragData]="mappedNode" class="mapped-node">
<i class="pi pi-arrow-left text-xs mr-2"></i>
<span class="flex-1 overflow-hidden white-space-nowrap text-overflow-ellipsis">{{ mappedNode.text_value }}</span>
<i class="pi pi-times cursor-pointer text-red-500 hover:text-red-700 ml-2" (click)="removeFromField(field.pk_template_field_id!, k)" title="Remove"></i>
</div>
</div>
</div>

View File

@@ -47,45 +47,137 @@
text-align: center;
}
.document-canvas {
position: relative;
width: 100%;
background: white;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
border-radius: 4px;
overflow: hidden;
}
/* Draggable Nodes */
.layout-node {
padding: 0.75rem;
margin-bottom: 0.5rem;
background: white;
border: 1px solid var(--surface-border);
border-left: 4px solid var(--primary-color);
position: absolute;
box-sizing: content-box !important;
border: 6px solid transparent;
margin: -6px !important;
background-clip: padding-box;
background: rgba(255, 255, 255, 0.9);
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.15);
border-radius: 4px;
cursor: grab;
transition: box-shadow 0.2s;
&:hover {
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
transition: box-shadow 0.2s, border-color 0.2s;
overflow: hidden;
resize: both;
display: flex;
flex-direction: column;
justify-content: center;
min-width: 60px;
min-height: 25px;
padding: 4px 6px;
&:active {
cursor: grabbing;
}
.node-type {
font-size: 0.7rem;
font-size: 0.5rem;
color: var(--text-color-secondary);
text-transform: uppercase;
font-weight: 600;
margin-bottom: 0.25rem;
margin-bottom: 0.1rem;
line-height: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.node-text {
font-size: 0.9rem;
font-size: 0.65rem;
color: var(--text-color);
line-height: 1.2;
white-space: normal;
word-break: break-word;
}
/* Color coding by block type */
&.header { border-left-color: #3B82F6; }
&.vendor { border-left-color: #8B5CF6; }
&.table_header { border-left-color: #F59E0B; }
&.total { border-left-color: #10B981; }
&.tax { border-left-color: #EF4444; }
/* Color coding by block type - use borders now instead of thick left border */
&.header { box-shadow: inset 0 0 0 1.5px #3B82F6; }
&.vendor { box-shadow: inset 0 0 0 1.5px #8B5CF6; }
&.table_header { box-shadow: inset 0 0 0 1.5px #F59E0B; }
&.total { box-shadow: inset 0 0 0 1.5px #10B981; }
&.tax { box-shadow: inset 0 0 0 1.5px #EF4444; }
&:hover {
z-index: 10;
&.header { box-shadow: inset 0 0 0 2px #3B82F6, 0 4px 8px rgba(59, 130, 246, 0.2); }
&.vendor { box-shadow: inset 0 0 0 2px #8B5CF6, 0 4px 8px rgba(139, 92, 246, 0.2); }
&.table_header { box-shadow: inset 0 0 0 2px #F59E0B, 0 4px 8px rgba(245, 158, 11, 0.2); }
&.total { box-shadow: inset 0 0 0 2px #10B981, 0 4px 8px rgba(16, 185, 129, 0.2); }
&.tax { box-shadow: inset 0 0 0 2px #EF4444, 0 4px 8px rgba(239, 68, 68, 0.2); }
}
.drag-handle {
position: absolute;
top: 0px;
left: 0px;
font-size: 0.55rem;
color: var(--text-color-secondary);
background: white;
border-radius: 50%;
width: 14px;
height: 14px;
display: flex;
align-items: center;
justify-content: center;
cursor: grab;
opacity: 0;
transition: opacity 0.2s, transform 0.2s;
z-index: 25;
box-shadow: 0 1px 3px rgba(0,0,0,0.2);
border: 1px solid var(--surface-border);
&:active {
cursor: grabbing;
}
&:hover {
color: var(--primary-color);
transform: scale(1.1);
}
}
&:hover .drag-handle {
opacity: 1;
}
.close-icon {
position: absolute;
top: 0px;
right: 0px;
font-size: 0.55rem;
color: white;
background: #EF4444;
border-radius: 50%;
width: 14px;
height: 14px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
opacity: 0;
transition: opacity 0.2s, transform 0.2s;
z-index: 25;
box-shadow: 0 1px 3px rgba(0,0,0,0.3);
&:hover {
background: #DC2626;
transform: scale(1.1);
}
}
&:hover .close-icon {
opacity: 1;
}
}
/* Template Mapping Side */
@@ -152,3 +244,74 @@
.cdk-drag-animating {
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
}
/* Scanning Overlay Animation */
.scanning-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.9);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 100;
}
.scanner-container {
position: relative;
width: 120px;
height: 120px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 8px;
overflow: hidden;
background: #f8f9fa;
box-shadow: inset 0 0 10px rgba(0,0,0,0.05);
border: 1px solid #e9ecef;
}
.scanner-doc {
z-index: 1;
}
.laser-beam {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 3px;
background: var(--primary-color, #3b82f6);
box-shadow: 0 0 10px 2px var(--primary-color, #3b82f6);
z-index: 2;
animation: scan-laser 2s ease-in-out infinite alternate;
}
.scanner-grid {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image: linear-gradient(var(--primary-100, #dbeafe) 1px, transparent 1px),
linear-gradient(90deg, var(--primary-100, #dbeafe) 1px, transparent 1px);
background-size: 10px 10px;
z-index: 0;
opacity: 0.5;
animation: scan-grid 4s linear infinite;
}
@keyframes scan-laser {
0% { top: -5%; opacity: 0; }
10% { opacity: 1; }
90% { opacity: 1; }
100% { top: 105%; opacity: 0; }
}
@keyframes scan-grid {
0% { background-position: 0 0; }
100% { background-position: 20px 20px; }
}

View File

@@ -1,7 +1,7 @@
import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { DragDropModule, CdkDragDrop, moveItemInArray, transferArrayItem } from '@angular/cdk/drag-drop';
import { DragDropModule, CdkDragDrop, moveItemInArray, transferArrayItem, copyArrayItem } from '@angular/cdk/drag-drop';
import { SplitterModule } from 'primeng/splitter';
import { ButtonModule } from 'primeng/button';
import { DialogModule } from 'primeng/dialog';
@@ -36,6 +36,9 @@ export class TemplatesComponent implements OnInit {
// Document State
documentId: string | null = null;
layoutNodes: DocumentLayout[] = [];
pages: { page_number: number, nodes: DocumentLayout[], width: number, height: number }[] = [];
pageWidth: number = 800;
pageHeight: number = 1100;
// Template State
templateName: string = '';
@@ -44,11 +47,15 @@ export class TemplatesComponent implements OnInit {
// UI State
displayAddField: boolean = false;
isScanning: boolean = false;
scanProgress: number = 0;
currentTemplateId: string | null = null;
newField: TemplateField = { field_label: '', field_type: 'TEXT', display_order: 0, required_flag: false };
fieldTypes = [
{ label: 'TEXT', value: 'TEXT' },
{ label: 'NUMBER', value: 'NUMBER' },
{ label: 'DATE', value: 'DATE' },
{ label: 'DATETIME', value: 'DATETIME' },
{ label: 'AMOUNT', value: 'AMOUNT' },
{ label: 'ADDRESS', value: 'ADDRESS' },
{ label: 'TABLE_COLUMN', value: 'TABLE_COLUMN' }
@@ -64,10 +71,16 @@ export class TemplatesComponent implements OnInit {
onFileUpload(event: any) {
const file = event.target.files[0];
if (file) {
this.currentTemplateId = null;
this.templateName = '';
this.templateFields = [];
this.mappings = {};
this.templateService.uploadDocument(file).subscribe({
next: (res) => {
this.documentId = res.id || res.pk_document_id;
this.messageService.add({ severity: 'success', summary: 'Uploaded', detail: 'Document uploaded successfully.' });
this.isScanning = true;
this.fetchLayout();
},
error: (err) => {
@@ -87,7 +100,30 @@ export class TemplatesComponent implements OnInit {
setTimeout(() => this.fetchLayout(), 2000);
return;
}
this.isScanning = false;
this.layoutNodes = layouts;
const pageMap = new Map<number, { page_number: number, nodes: DocumentLayout[], width: number, height: number }>();
layouts.forEach(node => {
if (!pageMap.has(node.page_no)) {
pageMap.set(node.page_no, {
page_number: node.page_no,
nodes: [],
width: node.page_width,
height: node.page_height
});
}
pageMap.get(node.page_no)!.nodes.push(node);
});
this.pages = Array.from(pageMap.values()).sort((a, b) => a.page_number - b.page_number);
if (this.pages.length > 0) {
this.pageWidth = this.pages[0].width;
this.pageHeight = this.pages[0].height;
}
// Trigger auto-recognition
this.autoRecognize();
}
@@ -97,10 +133,72 @@ export class TemplatesComponent implements OnInit {
autoRecognize() {
if (!this.documentId) return;
this.templateService.recognizeTemplate(this.documentId).subscribe({
next: (res) => {
if (res.templateMatched) {
this.messageService.add({ severity: 'info', summary: 'Template Recognized', detail: `Confidence: ${res.confidence}%` });
// In a full implementation, we'd load the template fields and populate the mappings.
next: (matches) => {
if (matches && matches.length > 0 && matches[0].confidence_score >= 0.75) {
const match = matches[0];
this.messageService.add({ severity: 'info', summary: 'Template Recognized', detail: `Confidence: ${(match.confidence_score * 100).toFixed(1)}%` });
this.templateService.getTemplate(match.format_id).subscribe({
next: (templateData) => {
this.templateName = templateData.name;
this.currentTemplateId = match.format_id;
this.templateFields = [];
this.mappings = {};
if (templateData.cells) {
templateData.cells.forEach((cell: any) => {
if (cell.is_dynamic) {
const fieldId = new Date().getTime() + Math.random();
this.templateFields.push({
pk_template_field_id: fieldId as any,
field_label: cell.field_name,
field_type: cell.data_type,
display_order: cell.sequence,
required_flag: false
});
this.mappings[fieldId] = [];
}
});
}
if (templateData.regions) {
templateData.regions.forEach((region: any) => {
if (region.region_type === 'field_mapping' && region.content && region.content.field_name) {
const field = this.templateFields.find(f => f.field_label === region.content.field_name);
if (field && field.pk_template_field_id) {
// Find the corresponding page in the current document
const page = this.pages.find(p => p.page_number === region.page_number);
if (page) {
let bestMatchIdx = -1;
let bestOverlap = 0;
// Find the text node on the canvas that overlaps most with this region's bounding box
for (let i = 0; i < page.nodes.length; i++) {
const node = page.nodes[i];
const x_overlap = Math.max(0, Math.min(region.x + region.width, node.x_coordinate + node.width) - Math.max(region.x, node.x_coordinate));
const y_overlap = Math.max(0, Math.min(region.y + region.height, node.y_coordinate + node.height) - Math.max(region.y, node.y_coordinate));
const overlapArea = x_overlap * y_overlap;
if (overlapArea > bestOverlap) {
bestOverlap = overlapArea;
bestMatchIdx = i;
}
}
// If we found a matching node (at least some overlap), clone it to mappings
if (bestMatchIdx !== -1) {
const matchedNode = page.nodes[bestMatchIdx];
// Because *ngFor tracks objects by reference, we clone it
this.mappings[field.pk_template_field_id].push({...matchedNode});
}
}
}
}
});
}
}
});
}
}
});
@@ -119,6 +217,12 @@ export class TemplatesComponent implements OnInit {
this.displayAddField = false;
}
removeField(fieldId?: number) {
if (!fieldId) return;
this.templateFields = this.templateFields.filter(f => f.pk_template_field_id !== fieldId);
delete this.mappings[fieldId];
}
saveTemplateAndMappings() {
if (!this.templateName || this.templateName.trim() === '') {
this.messageService.add({ severity: 'warn', summary: 'Warning', detail: 'Please provide a template name.' });
@@ -130,13 +234,47 @@ export class TemplatesComponent implements OnInit {
return;
}
this.templateService.createTemplate({
const payload = {
template_name: this.templateName,
source_document_id: this.documentId || undefined,
fields: this.templateFields
}).subscribe({
next: (res) => {
// In a real scenario, we pass res.templateId and map the layoutNodes
};
const saveRequest = this.currentTemplateId
? this.templateService.updateTemplate(this.currentTemplateId, payload)
: this.templateService.createTemplate(payload);
saveRequest.subscribe({
next: (res: any) => {
const templateId = res.pk_template_id;
// Construct the mappings payload
const mappingsPayload: any[] = [];
this.templateFields.forEach(field => {
const fieldId = field.pk_template_field_id;
if (fieldId && this.mappings[fieldId] && this.mappings[fieldId].length > 0) {
mappingsPayload.push({
field_name: field.field_label,
mapped_nodes: this.mappings[fieldId]
});
}
});
if (mappingsPayload.length > 0) {
this.templateService.saveMappings(templateId, mappingsPayload).subscribe({
next: () => {
this.messageService.add({ severity: 'success', summary: 'Success', detail: 'Template & Mappings saved successfully.' });
},
error: (err) => {
this.messageService.add({ severity: 'error', summary: 'Error', detail: 'Failed to save mappings.' });
}
});
} else {
this.messageService.add({ severity: 'success', summary: 'Success', detail: 'Template saved successfully (No mappings).' });
}
},
error: (err) => {
this.messageService.add({ severity: 'error', summary: 'Error', detail: 'Failed to save template.' });
}
});
}
@@ -144,8 +282,72 @@ export class TemplatesComponent implements OnInit {
// Drag and Drop Logic
drop(event: CdkDragDrop<DocumentLayout[]>, fieldId?: number) {
if (event.previousContainer === event.container) {
moveItemInArray(event.container.data, event.previousIndex, event.currentIndex);
// Do nothing! This allows the item to naturally snap back to its original position
// since it's a failed drop (didn't land in a mapping field).
} else {
const isFromCanvas = event.previousContainer.id.startsWith('document-layout-list');
const isToCanvas = event.container.id.startsWith('document-layout-list');
if (isFromCanvas && !isToCanvas) {
// Drag from Canvas -> Field
// We use event.item.data which perfectly tracks the dragged object regardless of DOM indexes
const clonedNode = JSON.parse(JSON.stringify(event.item.data));
// Sanitize the value if dropped on an AMOUNT or NUMBER field
if (fieldId) {
const field = this.templateFields.find(f => f.pk_template_field_id === fieldId);
if (field && (field.field_type === 'AMOUNT' || field.field_type === 'NUMBER') && clonedNode.text_value) {
const numericText = clonedNode.text_value.replace(/[^\d\.-]/g, '');
const floatValue = parseFloat(numericText);
if (!isNaN(floatValue)) {
clonedNode.text_value = floatValue.toString();
} else {
clonedNode.text_value = '';
}
} else if (field && (field.field_type === 'DATE' || field.field_type === 'DATETIME') && clonedNode.text_value) {
// Attempt to parse the date and output standard format
const timestamp = Date.parse(clonedNode.text_value);
if (!isNaN(timestamp)) {
const parsedDate = new Date(timestamp);
// Extract YYYY-MM-DD
const yyyy = parsedDate.getFullYear();
const mm = String(parsedDate.getMonth() + 1).padStart(2, '0');
const dd = String(parsedDate.getDate()).padStart(2, '0');
if (field.field_type === 'DATE') {
clonedNode.text_value = `${yyyy}-${mm}-${dd}`;
} else {
const hh = String(parsedDate.getHours()).padStart(2, '0');
const min = String(parsedDate.getMinutes()).padStart(2, '0');
const ss = String(parsedDate.getSeconds()).padStart(2, '0');
clonedNode.text_value = `${yyyy}-${mm}-${dd}T${hh}:${min}:${ss}`;
}
} else {
// If we can't parse it reliably on frontend, we leave it or let backend handle it
// We'll leave it as is to give user visual feedback, backend parser is more robust (dateutil)
}
}
}
// Insert the clone into the destination mapping field
event.container.data.splice(event.currentIndex, 0, clonedNode);
// Force Angular to completely recreate the DOM elements for this specific canvas page.
// By using .map(node => ({...node})), we change every object's identity.
// This forces Angular to destroy the corrupted DOM element (which CDK moved and left a translate3d on)
// and recreate it fresh with its original absolute coordinates!
const pageIndex = parseInt(event.previousContainer.id.split('-').pop() || '0');
if (!isNaN(pageIndex) && this.pages[pageIndex]) {
this.pages[pageIndex].nodes = this.pages[pageIndex].nodes.map(node => ({...node}));
// Also explicitly clear the transform on the dragged element just in case CDK holds a ref to it
event.item.element.nativeElement.style.transform = '';
}
} else if (!isFromCanvas && isToCanvas) {
// Drag from Field -> Canvas (Delete from Field)
event.previousContainer.data.splice(event.previousIndex, 1);
} else {
// Drag from Field -> Field (Move)
transferArrayItem(
event.previousContainer.data,
event.container.data,
@@ -154,9 +356,39 @@ export class TemplatesComponent implements OnInit {
);
}
}
}
onNodeMouseUp(node: DocumentLayout, event: MouseEvent) {
// If the user resized the node using the native CSS resize handle, save the new size
const el = event.currentTarget as HTMLElement;
if (el.style.width && el.style.width.endsWith('px')) {
const parentRect = el.parentElement!.getBoundingClientRect();
node.width = (el.offsetWidth / parentRect.width) * node.page_width;
node.height = (el.offsetHeight / parentRect.height) * node.page_height;
// Clear the inline pixel styles so Angular bindings take over smoothly
el.style.width = '';
el.style.height = '';
}
}
getConnectedDropLists() {
// Return all field ID strings as drop lists
return this.templateFields.map(f => 'field-' + f.pk_template_field_id);
}
getCanvasDropLists() {
return this.pages.map((_, i) => 'document-layout-list-' + i);
}
removeNode(pageIndex: number, nodeIndex: number, event: Event) {
event.stopPropagation();
this.pages[pageIndex].nodes.splice(nodeIndex, 1);
}
removeFromField(fieldId: number, nodeIndex: number) {
if (this.mappings[fieldId]) {
this.mappings[fieldId].splice(nodeIndex, 1);
}
}
}

11
list_templates.py Normal file
View File

@@ -0,0 +1,11 @@
import sys
import os
sys.path.append(os.path.join(os.getcwd(), 'docengine'))
from app.core.database import SessionLocal
from app.models.template import DocumentFormat
db = SessionLocal()
templates = db.query(DocumentFormat).all()
for t in templates:
print(f"ID: {t.id}, Name: {t.name}, Source: {t.source_document_id}, Active: {t.is_active}")

BIN
ocr.backup Normal file

Binary file not shown.

Binary file not shown.

View File

@@ -1,5 +0,0 @@
DB_HOST=192.168.0.111
DB_PORT=7925
DB_NAME=ocr
DB_USER=postgres
DB_PASSWORD=M@tr!x#149@dm!N

View File

@@ -1,26 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.27" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.8">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\AdminService.Infrastructure\AdminService.Infrastructure.csproj" />
<ProjectReference Include="..\AdminService.Domain\AdminService.Domain.csproj" />
<ProjectReference Include="..\..\Shared.Contracts\Shared.Contracts.csproj" />
<ProjectReference Include="..\..\Shared.Commons\Shared.Commons.csproj" />
<ProjectReference Include="..\AdminService.Application\AdminService.Application.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,6 +0,0 @@
@AdminService.API_HostAddress = http://localhost:5066
GET {{AdminService.API_HostAddress}}/weatherforecast/
Accept: application/json
###

View File

@@ -1,26 +0,0 @@
using AdminService.Infrastructure;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddInfrastructure(builder.Configuration);
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();

View File

@@ -1,41 +0,0 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:20091",
"sslPort": 44325
}
},
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "http://localhost:5066",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "https://localhost:7244;http://localhost:5066",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "swagger",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@@ -1,11 +0,0 @@
{
"ConnectionStrings": {
"DefaultConnection": "Host=192.168.0.111;Port=7925;Database=ocr;Username=postgres;Password=M@tr!x#149@dm!N"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@@ -1,9 +0,0 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}

View File

@@ -1,968 +0,0 @@
{
"runtimeTarget": {
"name": ".NETCoreApp,Version=v8.0",
"signature": ""
},
"compilationOptions": {},
"targets": {
".NETCoreApp,Version=v8.0": {
"AdminService.API/1.0.0": {
"dependencies": {
"AdminService.Application": "1.0.0",
"AdminService.Domain": "1.0.0",
"AdminService.Infrastructure": "1.0.0",
"Microsoft.AspNetCore.OpenApi": "8.0.27",
"Microsoft.EntityFrameworkCore.Design": "8.0.8",
"Shared.Commons": "1.0.0",
"Shared.Contracts": "1.0.0",
"Swashbuckle.AspNetCore": "6.6.2"
},
"runtime": {
"AdminService.API.dll": {}
}
},
"Humanizer.Core/2.14.1": {
"runtime": {
"lib/net6.0/Humanizer.dll": {
"assemblyVersion": "2.14.0.0",
"fileVersion": "2.14.1.48190"
}
}
},
"Microsoft.AspNetCore.OpenApi/8.0.27": {
"dependencies": {
"Microsoft.OpenApi": "1.6.14"
},
"runtime": {
"lib/net8.0/Microsoft.AspNetCore.OpenApi.dll": {
"assemblyVersion": "8.0.27.0",
"fileVersion": "8.0.2726.23008"
}
}
},
"Microsoft.Bcl.AsyncInterfaces/6.0.0": {
"runtime": {
"lib/netstandard2.1/Microsoft.Bcl.AsyncInterfaces.dll": {
"assemblyVersion": "6.0.0.0",
"fileVersion": "6.0.21.52210"
}
}
},
"Microsoft.CodeAnalysis.Analyzers/3.3.3": {},
"Microsoft.CodeAnalysis.Common/4.5.0": {
"dependencies": {
"Microsoft.CodeAnalysis.Analyzers": "3.3.3",
"System.Collections.Immutable": "6.0.0",
"System.Reflection.Metadata": "6.0.1",
"System.Runtime.CompilerServices.Unsafe": "6.0.0",
"System.Text.Encoding.CodePages": "6.0.0"
},
"runtime": {
"lib/netcoreapp3.1/Microsoft.CodeAnalysis.dll": {
"assemblyVersion": "4.5.0.0",
"fileVersion": "4.500.23.10905"
}
},
"resources": {
"lib/netcoreapp3.1/cs/Microsoft.CodeAnalysis.resources.dll": {
"locale": "cs"
},
"lib/netcoreapp3.1/de/Microsoft.CodeAnalysis.resources.dll": {
"locale": "de"
},
"lib/netcoreapp3.1/es/Microsoft.CodeAnalysis.resources.dll": {
"locale": "es"
},
"lib/netcoreapp3.1/fr/Microsoft.CodeAnalysis.resources.dll": {
"locale": "fr"
},
"lib/netcoreapp3.1/it/Microsoft.CodeAnalysis.resources.dll": {
"locale": "it"
},
"lib/netcoreapp3.1/ja/Microsoft.CodeAnalysis.resources.dll": {
"locale": "ja"
},
"lib/netcoreapp3.1/ko/Microsoft.CodeAnalysis.resources.dll": {
"locale": "ko"
},
"lib/netcoreapp3.1/pl/Microsoft.CodeAnalysis.resources.dll": {
"locale": "pl"
},
"lib/netcoreapp3.1/pt-BR/Microsoft.CodeAnalysis.resources.dll": {
"locale": "pt-BR"
},
"lib/netcoreapp3.1/ru/Microsoft.CodeAnalysis.resources.dll": {
"locale": "ru"
},
"lib/netcoreapp3.1/tr/Microsoft.CodeAnalysis.resources.dll": {
"locale": "tr"
},
"lib/netcoreapp3.1/zh-Hans/Microsoft.CodeAnalysis.resources.dll": {
"locale": "zh-Hans"
},
"lib/netcoreapp3.1/zh-Hant/Microsoft.CodeAnalysis.resources.dll": {
"locale": "zh-Hant"
}
}
},
"Microsoft.CodeAnalysis.CSharp/4.5.0": {
"dependencies": {
"Microsoft.CodeAnalysis.Common": "4.5.0"
},
"runtime": {
"lib/netcoreapp3.1/Microsoft.CodeAnalysis.CSharp.dll": {
"assemblyVersion": "4.5.0.0",
"fileVersion": "4.500.23.10905"
}
},
"resources": {
"lib/netcoreapp3.1/cs/Microsoft.CodeAnalysis.CSharp.resources.dll": {
"locale": "cs"
},
"lib/netcoreapp3.1/de/Microsoft.CodeAnalysis.CSharp.resources.dll": {
"locale": "de"
},
"lib/netcoreapp3.1/es/Microsoft.CodeAnalysis.CSharp.resources.dll": {
"locale": "es"
},
"lib/netcoreapp3.1/fr/Microsoft.CodeAnalysis.CSharp.resources.dll": {
"locale": "fr"
},
"lib/netcoreapp3.1/it/Microsoft.CodeAnalysis.CSharp.resources.dll": {
"locale": "it"
},
"lib/netcoreapp3.1/ja/Microsoft.CodeAnalysis.CSharp.resources.dll": {
"locale": "ja"
},
"lib/netcoreapp3.1/ko/Microsoft.CodeAnalysis.CSharp.resources.dll": {
"locale": "ko"
},
"lib/netcoreapp3.1/pl/Microsoft.CodeAnalysis.CSharp.resources.dll": {
"locale": "pl"
},
"lib/netcoreapp3.1/pt-BR/Microsoft.CodeAnalysis.CSharp.resources.dll": {
"locale": "pt-BR"
},
"lib/netcoreapp3.1/ru/Microsoft.CodeAnalysis.CSharp.resources.dll": {
"locale": "ru"
},
"lib/netcoreapp3.1/tr/Microsoft.CodeAnalysis.CSharp.resources.dll": {
"locale": "tr"
},
"lib/netcoreapp3.1/zh-Hans/Microsoft.CodeAnalysis.CSharp.resources.dll": {
"locale": "zh-Hans"
},
"lib/netcoreapp3.1/zh-Hant/Microsoft.CodeAnalysis.CSharp.resources.dll": {
"locale": "zh-Hant"
}
}
},
"Microsoft.CodeAnalysis.CSharp.Workspaces/4.5.0": {
"dependencies": {
"Humanizer.Core": "2.14.1",
"Microsoft.CodeAnalysis.CSharp": "4.5.0",
"Microsoft.CodeAnalysis.Common": "4.5.0",
"Microsoft.CodeAnalysis.Workspaces.Common": "4.5.0"
},
"runtime": {
"lib/netcoreapp3.1/Microsoft.CodeAnalysis.CSharp.Workspaces.dll": {
"assemblyVersion": "4.5.0.0",
"fileVersion": "4.500.23.10905"
}
},
"resources": {
"lib/netcoreapp3.1/cs/Microsoft.CodeAnalysis.CSharp.Workspaces.resources.dll": {
"locale": "cs"
},
"lib/netcoreapp3.1/de/Microsoft.CodeAnalysis.CSharp.Workspaces.resources.dll": {
"locale": "de"
},
"lib/netcoreapp3.1/es/Microsoft.CodeAnalysis.CSharp.Workspaces.resources.dll": {
"locale": "es"
},
"lib/netcoreapp3.1/fr/Microsoft.CodeAnalysis.CSharp.Workspaces.resources.dll": {
"locale": "fr"
},
"lib/netcoreapp3.1/it/Microsoft.CodeAnalysis.CSharp.Workspaces.resources.dll": {
"locale": "it"
},
"lib/netcoreapp3.1/ja/Microsoft.CodeAnalysis.CSharp.Workspaces.resources.dll": {
"locale": "ja"
},
"lib/netcoreapp3.1/ko/Microsoft.CodeAnalysis.CSharp.Workspaces.resources.dll": {
"locale": "ko"
},
"lib/netcoreapp3.1/pl/Microsoft.CodeAnalysis.CSharp.Workspaces.resources.dll": {
"locale": "pl"
},
"lib/netcoreapp3.1/pt-BR/Microsoft.CodeAnalysis.CSharp.Workspaces.resources.dll": {
"locale": "pt-BR"
},
"lib/netcoreapp3.1/ru/Microsoft.CodeAnalysis.CSharp.Workspaces.resources.dll": {
"locale": "ru"
},
"lib/netcoreapp3.1/tr/Microsoft.CodeAnalysis.CSharp.Workspaces.resources.dll": {
"locale": "tr"
},
"lib/netcoreapp3.1/zh-Hans/Microsoft.CodeAnalysis.CSharp.Workspaces.resources.dll": {
"locale": "zh-Hans"
},
"lib/netcoreapp3.1/zh-Hant/Microsoft.CodeAnalysis.CSharp.Workspaces.resources.dll": {
"locale": "zh-Hant"
}
}
},
"Microsoft.CodeAnalysis.Workspaces.Common/4.5.0": {
"dependencies": {
"Humanizer.Core": "2.14.1",
"Microsoft.Bcl.AsyncInterfaces": "6.0.0",
"Microsoft.CodeAnalysis.Common": "4.5.0",
"System.Composition": "6.0.0",
"System.IO.Pipelines": "6.0.3",
"System.Threading.Channels": "6.0.0"
},
"runtime": {
"lib/netcoreapp3.1/Microsoft.CodeAnalysis.Workspaces.dll": {
"assemblyVersion": "4.5.0.0",
"fileVersion": "4.500.23.10905"
}
},
"resources": {
"lib/netcoreapp3.1/cs/Microsoft.CodeAnalysis.Workspaces.resources.dll": {
"locale": "cs"
},
"lib/netcoreapp3.1/de/Microsoft.CodeAnalysis.Workspaces.resources.dll": {
"locale": "de"
},
"lib/netcoreapp3.1/es/Microsoft.CodeAnalysis.Workspaces.resources.dll": {
"locale": "es"
},
"lib/netcoreapp3.1/fr/Microsoft.CodeAnalysis.Workspaces.resources.dll": {
"locale": "fr"
},
"lib/netcoreapp3.1/it/Microsoft.CodeAnalysis.Workspaces.resources.dll": {
"locale": "it"
},
"lib/netcoreapp3.1/ja/Microsoft.CodeAnalysis.Workspaces.resources.dll": {
"locale": "ja"
},
"lib/netcoreapp3.1/ko/Microsoft.CodeAnalysis.Workspaces.resources.dll": {
"locale": "ko"
},
"lib/netcoreapp3.1/pl/Microsoft.CodeAnalysis.Workspaces.resources.dll": {
"locale": "pl"
},
"lib/netcoreapp3.1/pt-BR/Microsoft.CodeAnalysis.Workspaces.resources.dll": {
"locale": "pt-BR"
},
"lib/netcoreapp3.1/ru/Microsoft.CodeAnalysis.Workspaces.resources.dll": {
"locale": "ru"
},
"lib/netcoreapp3.1/tr/Microsoft.CodeAnalysis.Workspaces.resources.dll": {
"locale": "tr"
},
"lib/netcoreapp3.1/zh-Hans/Microsoft.CodeAnalysis.Workspaces.resources.dll": {
"locale": "zh-Hans"
},
"lib/netcoreapp3.1/zh-Hant/Microsoft.CodeAnalysis.Workspaces.resources.dll": {
"locale": "zh-Hant"
}
}
},
"Microsoft.EntityFrameworkCore/8.0.8": {
"dependencies": {
"Microsoft.EntityFrameworkCore.Abstractions": "8.0.8",
"Microsoft.EntityFrameworkCore.Analyzers": "8.0.8",
"Microsoft.Extensions.Caching.Memory": "8.0.0",
"Microsoft.Extensions.Logging": "8.0.0"
},
"runtime": {
"lib/net8.0/Microsoft.EntityFrameworkCore.dll": {
"assemblyVersion": "8.0.8.0",
"fileVersion": "8.0.824.36704"
}
}
},
"Microsoft.EntityFrameworkCore.Abstractions/8.0.8": {
"runtime": {
"lib/net8.0/Microsoft.EntityFrameworkCore.Abstractions.dll": {
"assemblyVersion": "8.0.8.0",
"fileVersion": "8.0.824.36704"
}
}
},
"Microsoft.EntityFrameworkCore.Analyzers/8.0.8": {},
"Microsoft.EntityFrameworkCore.Design/8.0.8": {
"dependencies": {
"Humanizer.Core": "2.14.1",
"Microsoft.CodeAnalysis.CSharp.Workspaces": "4.5.0",
"Microsoft.EntityFrameworkCore.Relational": "8.0.8",
"Microsoft.Extensions.DependencyModel": "8.0.1",
"Mono.TextTemplating": "2.2.1"
},
"runtime": {
"lib/net8.0/Microsoft.EntityFrameworkCore.Design.dll": {
"assemblyVersion": "8.0.8.0",
"fileVersion": "8.0.824.36704"
}
}
},
"Microsoft.EntityFrameworkCore.Relational/8.0.8": {
"dependencies": {
"Microsoft.EntityFrameworkCore": "8.0.8",
"Microsoft.Extensions.Configuration.Abstractions": "8.0.0"
},
"runtime": {
"lib/net8.0/Microsoft.EntityFrameworkCore.Relational.dll": {
"assemblyVersion": "8.0.8.0",
"fileVersion": "8.0.824.36704"
}
}
},
"Microsoft.Extensions.ApiDescription.Server/6.0.5": {},
"Microsoft.Extensions.Caching.Abstractions/8.0.0": {
"dependencies": {
"Microsoft.Extensions.Primitives": "8.0.0"
}
},
"Microsoft.Extensions.Caching.Memory/8.0.0": {
"dependencies": {
"Microsoft.Extensions.Caching.Abstractions": "8.0.0",
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0",
"Microsoft.Extensions.Logging.Abstractions": "8.0.0",
"Microsoft.Extensions.Options": "8.0.0",
"Microsoft.Extensions.Primitives": "8.0.0"
}
},
"Microsoft.Extensions.Configuration.Abstractions/8.0.0": {
"dependencies": {
"Microsoft.Extensions.Primitives": "8.0.0"
}
},
"Microsoft.Extensions.DependencyInjection/8.0.0": {
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0"
}
},
"Microsoft.Extensions.DependencyInjection.Abstractions/8.0.0": {},
"Microsoft.Extensions.DependencyModel/8.0.1": {
"dependencies": {
"System.Text.Encodings.Web": "8.0.0",
"System.Text.Json": "8.0.4"
},
"runtime": {
"lib/net8.0/Microsoft.Extensions.DependencyModel.dll": {
"assemblyVersion": "8.0.0.1",
"fileVersion": "8.0.724.31311"
}
}
},
"Microsoft.Extensions.Logging/8.0.0": {
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "8.0.0",
"Microsoft.Extensions.Logging.Abstractions": "8.0.0",
"Microsoft.Extensions.Options": "8.0.0"
}
},
"Microsoft.Extensions.Logging.Abstractions/8.0.0": {
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0"
}
},
"Microsoft.Extensions.Options/8.0.0": {
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0",
"Microsoft.Extensions.Primitives": "8.0.0"
}
},
"Microsoft.Extensions.Primitives/8.0.0": {},
"Microsoft.OpenApi/1.6.14": {
"runtime": {
"lib/netstandard2.0/Microsoft.OpenApi.dll": {
"assemblyVersion": "1.6.14.0",
"fileVersion": "1.6.14.0"
}
}
},
"Mono.TextTemplating/2.2.1": {
"dependencies": {
"System.CodeDom": "4.4.0"
},
"runtime": {
"lib/netstandard2.0/Mono.TextTemplating.dll": {
"assemblyVersion": "2.2.0.0",
"fileVersion": "2.2.1.1"
}
}
},
"Npgsql/8.0.4": {
"dependencies": {
"Microsoft.Extensions.Logging.Abstractions": "8.0.0"
},
"runtime": {
"lib/net8.0/Npgsql.dll": {
"assemblyVersion": "8.0.4.0",
"fileVersion": "8.0.4.0"
}
}
},
"Npgsql.EntityFrameworkCore.PostgreSQL/8.0.8": {
"dependencies": {
"Microsoft.EntityFrameworkCore": "8.0.8",
"Microsoft.EntityFrameworkCore.Abstractions": "8.0.8",
"Microsoft.EntityFrameworkCore.Relational": "8.0.8",
"Npgsql": "8.0.4"
},
"runtime": {
"lib/net8.0/Npgsql.EntityFrameworkCore.PostgreSQL.dll": {
"assemblyVersion": "8.0.8.0",
"fileVersion": "8.0.8.0"
}
}
},
"Swashbuckle.AspNetCore/6.6.2": {
"dependencies": {
"Microsoft.Extensions.ApiDescription.Server": "6.0.5",
"Swashbuckle.AspNetCore.Swagger": "6.6.2",
"Swashbuckle.AspNetCore.SwaggerGen": "6.6.2",
"Swashbuckle.AspNetCore.SwaggerUI": "6.6.2"
}
},
"Swashbuckle.AspNetCore.Swagger/6.6.2": {
"dependencies": {
"Microsoft.OpenApi": "1.6.14"
},
"runtime": {
"lib/net8.0/Swashbuckle.AspNetCore.Swagger.dll": {
"assemblyVersion": "6.6.2.0",
"fileVersion": "6.6.2.401"
}
}
},
"Swashbuckle.AspNetCore.SwaggerGen/6.6.2": {
"dependencies": {
"Swashbuckle.AspNetCore.Swagger": "6.6.2"
},
"runtime": {
"lib/net8.0/Swashbuckle.AspNetCore.SwaggerGen.dll": {
"assemblyVersion": "6.6.2.0",
"fileVersion": "6.6.2.401"
}
}
},
"Swashbuckle.AspNetCore.SwaggerUI/6.6.2": {
"runtime": {
"lib/net8.0/Swashbuckle.AspNetCore.SwaggerUI.dll": {
"assemblyVersion": "6.6.2.0",
"fileVersion": "6.6.2.401"
}
}
},
"System.CodeDom/4.4.0": {
"runtime": {
"lib/netstandard2.0/System.CodeDom.dll": {
"assemblyVersion": "4.0.0.0",
"fileVersion": "4.6.25519.3"
}
}
},
"System.Collections.Immutable/6.0.0": {
"dependencies": {
"System.Runtime.CompilerServices.Unsafe": "6.0.0"
}
},
"System.Composition/6.0.0": {
"dependencies": {
"System.Composition.AttributedModel": "6.0.0",
"System.Composition.Convention": "6.0.0",
"System.Composition.Hosting": "6.0.0",
"System.Composition.Runtime": "6.0.0",
"System.Composition.TypedParts": "6.0.0"
}
},
"System.Composition.AttributedModel/6.0.0": {
"runtime": {
"lib/net6.0/System.Composition.AttributedModel.dll": {
"assemblyVersion": "6.0.0.0",
"fileVersion": "6.0.21.52210"
}
}
},
"System.Composition.Convention/6.0.0": {
"dependencies": {
"System.Composition.AttributedModel": "6.0.0"
},
"runtime": {
"lib/net6.0/System.Composition.Convention.dll": {
"assemblyVersion": "6.0.0.0",
"fileVersion": "6.0.21.52210"
}
}
},
"System.Composition.Hosting/6.0.0": {
"dependencies": {
"System.Composition.Runtime": "6.0.0"
},
"runtime": {
"lib/net6.0/System.Composition.Hosting.dll": {
"assemblyVersion": "6.0.0.0",
"fileVersion": "6.0.21.52210"
}
}
},
"System.Composition.Runtime/6.0.0": {
"runtime": {
"lib/net6.0/System.Composition.Runtime.dll": {
"assemblyVersion": "6.0.0.0",
"fileVersion": "6.0.21.52210"
}
}
},
"System.Composition.TypedParts/6.0.0": {
"dependencies": {
"System.Composition.AttributedModel": "6.0.0",
"System.Composition.Hosting": "6.0.0",
"System.Composition.Runtime": "6.0.0"
},
"runtime": {
"lib/net6.0/System.Composition.TypedParts.dll": {
"assemblyVersion": "6.0.0.0",
"fileVersion": "6.0.21.52210"
}
}
},
"System.IO.Pipelines/6.0.3": {},
"System.Reflection.Metadata/6.0.1": {
"dependencies": {
"System.Collections.Immutable": "6.0.0"
}
},
"System.Runtime.CompilerServices.Unsafe/6.0.0": {},
"System.Text.Encoding.CodePages/6.0.0": {
"dependencies": {
"System.Runtime.CompilerServices.Unsafe": "6.0.0"
}
},
"System.Text.Encodings.Web/8.0.0": {},
"System.Text.Json/8.0.4": {
"dependencies": {
"System.Text.Encodings.Web": "8.0.0"
}
},
"System.Threading.Channels/6.0.0": {},
"AdminService.Application/1.0.0": {
"dependencies": {
"AdminService.Domain": "1.0.0",
"AdminService.Infrastructure": "1.0.0"
},
"runtime": {
"AdminService.Application.dll": {
"assemblyVersion": "1.0.0",
"fileVersion": "1.0.0.0"
}
}
},
"AdminService.Domain/1.0.0": {
"dependencies": {
"Shared.Commons": "1.0.0"
},
"runtime": {
"AdminService.Domain.dll": {
"assemblyVersion": "1.0.0",
"fileVersion": "1.0.0.0"
}
}
},
"AdminService.Infrastructure/1.0.0": {
"dependencies": {
"AdminService.Domain": "1.0.0",
"Microsoft.EntityFrameworkCore": "8.0.8",
"Npgsql.EntityFrameworkCore.PostgreSQL": "8.0.8"
},
"runtime": {
"AdminService.Infrastructure.dll": {
"assemblyVersion": "1.0.0",
"fileVersion": "1.0.0.0"
}
}
},
"Shared.Commons/1.0.0": {
"runtime": {
"Shared.Commons.dll": {
"assemblyVersion": "1.0.0",
"fileVersion": "1.0.0.0"
}
}
},
"Shared.Contracts/1.0.0": {
"runtime": {
"Shared.Contracts.dll": {
"assemblyVersion": "1.0.0",
"fileVersion": "1.0.0.0"
}
}
}
}
},
"libraries": {
"AdminService.API/1.0.0": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Humanizer.Core/2.14.1": {
"type": "package",
"serviceable": true,
"sha512": "sha512-lQKvtaTDOXnoVJ20ibTuSIOf2i0uO0MPbDhd1jm238I+U/2ZnRENj0cktKZhtchBMtCUSRQ5v4xBCUbKNmyVMw==",
"path": "humanizer.core/2.14.1",
"hashPath": "humanizer.core.2.14.1.nupkg.sha512"
},
"Microsoft.AspNetCore.OpenApi/8.0.27": {
"type": "package",
"serviceable": true,
"sha512": "sha512-lbjwtlQ1ICGKp3UyhoF9i4APpzX01CqtY8vVba2vL32Nm3v5ir9/FJ0PHXfxgsO5sr1Bb4KkCOH7FSauEHUgRQ==",
"path": "microsoft.aspnetcore.openapi/8.0.27",
"hashPath": "microsoft.aspnetcore.openapi.8.0.27.nupkg.sha512"
},
"Microsoft.Bcl.AsyncInterfaces/6.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-UcSjPsst+DfAdJGVDsu346FX0ci0ah+lw3WRtn18NUwEqRt70HaOQ7lI72vy3+1LxtqI3T5GWwV39rQSrCzAeg==",
"path": "microsoft.bcl.asyncinterfaces/6.0.0",
"hashPath": "microsoft.bcl.asyncinterfaces.6.0.0.nupkg.sha512"
},
"Microsoft.CodeAnalysis.Analyzers/3.3.3": {
"type": "package",
"serviceable": true,
"sha512": "sha512-j/rOZtLMVJjrfLRlAMckJLPW/1rze9MT1yfWqSIbUPGRu1m1P0fuo9PmqapwsmePfGB5PJrudQLvmUOAMF0DqQ==",
"path": "microsoft.codeanalysis.analyzers/3.3.3",
"hashPath": "microsoft.codeanalysis.analyzers.3.3.3.nupkg.sha512"
},
"Microsoft.CodeAnalysis.Common/4.5.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-lwAbIZNdnY0SUNoDmZHkVUwLO8UyNnyyh1t/4XsbFxi4Ounb3xszIYZaWhyj5ZjyfcwqwmtMbE7fUTVCqQEIdQ==",
"path": "microsoft.codeanalysis.common/4.5.0",
"hashPath": "microsoft.codeanalysis.common.4.5.0.nupkg.sha512"
},
"Microsoft.CodeAnalysis.CSharp/4.5.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-cM59oMKAOxvdv76bdmaKPy5hfj+oR+zxikWoueEB7CwTko7mt9sVKZI8Qxlov0C/LuKEG+WQwifepqL3vuTiBQ==",
"path": "microsoft.codeanalysis.csharp/4.5.0",
"hashPath": "microsoft.codeanalysis.csharp.4.5.0.nupkg.sha512"
},
"Microsoft.CodeAnalysis.CSharp.Workspaces/4.5.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-h74wTpmGOp4yS4hj+EvNzEiPgg/KVs2wmSfTZ81upJZOtPkJsVkgfsgtxxqmAeapjT/vLKfmYV0bS8n5MNVP+g==",
"path": "microsoft.codeanalysis.csharp.workspaces/4.5.0",
"hashPath": "microsoft.codeanalysis.csharp.workspaces.4.5.0.nupkg.sha512"
},
"Microsoft.CodeAnalysis.Workspaces.Common/4.5.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-l4dDRmGELXG72XZaonnOeORyD/T5RpEu5LGHOUIhnv+MmUWDY/m1kWXGwtcgQ5CJ5ynkFiRnIYzTKXYjUs7rbw==",
"path": "microsoft.codeanalysis.workspaces.common/4.5.0",
"hashPath": "microsoft.codeanalysis.workspaces.common.4.5.0.nupkg.sha512"
},
"Microsoft.EntityFrameworkCore/8.0.8": {
"type": "package",
"serviceable": true,
"sha512": "sha512-iK+jrJzkfbIxutB7or808BPmJtjUEi5O+eSM7cLDwsyde6+3iOujCSfWnrHrLxY3u+EQrJD+aD8DJ6ogPA2Rtw==",
"path": "microsoft.entityframeworkcore/8.0.8",
"hashPath": "microsoft.entityframeworkcore.8.0.8.nupkg.sha512"
},
"Microsoft.EntityFrameworkCore.Abstractions/8.0.8": {
"type": "package",
"serviceable": true,
"sha512": "sha512-9mMQkZsfL1c2iifBD8MWRmwy59rvsVtR9NOezJj7+g1j4P7g49MJHd8k8faC/v7d5KuHkQ6KOQiSItvoRt9PXA==",
"path": "microsoft.entityframeworkcore.abstractions/8.0.8",
"hashPath": "microsoft.entityframeworkcore.abstractions.8.0.8.nupkg.sha512"
},
"Microsoft.EntityFrameworkCore.Analyzers/8.0.8": {
"type": "package",
"serviceable": true,
"sha512": "sha512-OlAXMU+VQgLz5y5/SBkLvAa9VeiR3dlJqgIebEEH2M2NGA3evm68/Tv7SLWmSxwnEAtA3nmDEZF2pacK6eXh4Q==",
"path": "microsoft.entityframeworkcore.analyzers/8.0.8",
"hashPath": "microsoft.entityframeworkcore.analyzers.8.0.8.nupkg.sha512"
},
"Microsoft.EntityFrameworkCore.Design/8.0.8": {
"type": "package",
"serviceable": true,
"sha512": "sha512-MmQAMHdjZR8Iyn/FVQrh9weJQTn0HqtKa3vELS9ffQJat/qXgnTam9M9jqvePphjkYp5Scee+Hy+EJR4nmWmOA==",
"path": "microsoft.entityframeworkcore.design/8.0.8",
"hashPath": "microsoft.entityframeworkcore.design.8.0.8.nupkg.sha512"
},
"Microsoft.EntityFrameworkCore.Relational/8.0.8": {
"type": "package",
"serviceable": true,
"sha512": "sha512-3WnrwdXxKg4L98cDx0lNEEau8U2lsfuBJCs0Yzht+5XVTmahboM7MukKfQHAzVsHUPszm6ci929S7Qas0WfVHA==",
"path": "microsoft.entityframeworkcore.relational/8.0.8",
"hashPath": "microsoft.entityframeworkcore.relational.8.0.8.nupkg.sha512"
},
"Microsoft.Extensions.ApiDescription.Server/6.0.5": {
"type": "package",
"serviceable": true,
"sha512": "sha512-Ckb5EDBUNJdFWyajfXzUIMRkhf52fHZOQuuZg/oiu8y7zDCVwD0iHhew6MnThjHmevanpxL3f5ci2TtHQEN6bw==",
"path": "microsoft.extensions.apidescription.server/6.0.5",
"hashPath": "microsoft.extensions.apidescription.server.6.0.5.nupkg.sha512"
},
"Microsoft.Extensions.Caching.Abstractions/8.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-3KuSxeHoNYdxVYfg2IRZCThcrlJ1XJqIXkAWikCsbm5C/bCjv7G0WoKDyuR98Q+T607QT2Zl5GsbGRkENcV2yQ==",
"path": "microsoft.extensions.caching.abstractions/8.0.0",
"hashPath": "microsoft.extensions.caching.abstractions.8.0.0.nupkg.sha512"
},
"Microsoft.Extensions.Caching.Memory/8.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-7pqivmrZDzo1ADPkRwjy+8jtRKWRCPag9qPI+p7sgu7Q4QreWhcvbiWXsbhP+yY8XSiDvZpu2/LWdBv7PnmOpQ==",
"path": "microsoft.extensions.caching.memory/8.0.0",
"hashPath": "microsoft.extensions.caching.memory.8.0.0.nupkg.sha512"
},
"Microsoft.Extensions.Configuration.Abstractions/8.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-3lE/iLSutpgX1CC0NOW70FJoGARRHbyKmG7dc0klnUZ9Dd9hS6N/POPWhKhMLCEuNN5nXEY5agmlFtH562vqhQ==",
"path": "microsoft.extensions.configuration.abstractions/8.0.0",
"hashPath": "microsoft.extensions.configuration.abstractions.8.0.0.nupkg.sha512"
},
"Microsoft.Extensions.DependencyInjection/8.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-V8S3bsm50ig6JSyrbcJJ8bW2b9QLGouz+G1miK3UTaOWmMtFwNNNzUf4AleyDWUmTrWMLNnFSLEQtxmxgNQnNQ==",
"path": "microsoft.extensions.dependencyinjection/8.0.0",
"hashPath": "microsoft.extensions.dependencyinjection.8.0.0.nupkg.sha512"
},
"Microsoft.Extensions.DependencyInjection.Abstractions/8.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-cjWrLkJXK0rs4zofsK4bSdg+jhDLTaxrkXu4gS6Y7MAlCvRyNNgwY/lJi5RDlQOnSZweHqoyvgvbdvQsRIW+hg==",
"path": "microsoft.extensions.dependencyinjection.abstractions/8.0.0",
"hashPath": "microsoft.extensions.dependencyinjection.abstractions.8.0.0.nupkg.sha512"
},
"Microsoft.Extensions.DependencyModel/8.0.1": {
"type": "package",
"serviceable": true,
"sha512": "sha512-5Ou6varcxLBzQ+Agfm0k0pnH7vrEITYlXMDuE6s7ZHlZHz6/G8XJ3iISZDr5rfwfge6RnXJ1+Wc479mMn52vjA==",
"path": "microsoft.extensions.dependencymodel/8.0.1",
"hashPath": "microsoft.extensions.dependencymodel.8.0.1.nupkg.sha512"
},
"Microsoft.Extensions.Logging/8.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-tvRkov9tAJ3xP51LCv3FJ2zINmv1P8Hi8lhhtcKGqM+ImiTCC84uOPEI4z8Cdq2C3o9e+Aa0Gw0rmrsJD77W+w==",
"path": "microsoft.extensions.logging/8.0.0",
"hashPath": "microsoft.extensions.logging.8.0.0.nupkg.sha512"
},
"Microsoft.Extensions.Logging.Abstractions/8.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-arDBqTgFCyS0EvRV7O3MZturChstm50OJ0y9bDJvAcmEPJm0FFpFyjU/JLYyStNGGey081DvnQYlncNX5SJJGA==",
"path": "microsoft.extensions.logging.abstractions/8.0.0",
"hashPath": "microsoft.extensions.logging.abstractions.8.0.0.nupkg.sha512"
},
"Microsoft.Extensions.Options/8.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-JOVOfqpnqlVLUzINQ2fox8evY2SKLYJ3BV8QDe/Jyp21u1T7r45x/R/5QdteURMR5r01GxeJSBBUOCOyaNXA3g==",
"path": "microsoft.extensions.options/8.0.0",
"hashPath": "microsoft.extensions.options.8.0.0.nupkg.sha512"
},
"Microsoft.Extensions.Primitives/8.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-bXJEZrW9ny8vjMF1JV253WeLhpEVzFo1lyaZu1vQ4ZxWUlVvknZ/+ftFgVheLubb4eZPSwwxBeqS1JkCOjxd8g==",
"path": "microsoft.extensions.primitives/8.0.0",
"hashPath": "microsoft.extensions.primitives.8.0.0.nupkg.sha512"
},
"Microsoft.OpenApi/1.6.14": {
"type": "package",
"serviceable": true,
"sha512": "sha512-tTaBT8qjk3xINfESyOPE2rIellPvB7qpVqiWiyA/lACVvz+xOGiXhFUfohcx82NLbi5avzLW0lx+s6oAqQijfw==",
"path": "microsoft.openapi/1.6.14",
"hashPath": "microsoft.openapi.1.6.14.nupkg.sha512"
},
"Mono.TextTemplating/2.2.1": {
"type": "package",
"serviceable": true,
"sha512": "sha512-KZYeKBET/2Z0gY1WlTAK7+RHTl7GSbtvTLDXEZZojUdAPqpQNDL6tHv7VUpqfX5VEOh+uRGKaZXkuD253nEOBQ==",
"path": "mono.texttemplating/2.2.1",
"hashPath": "mono.texttemplating.2.2.1.nupkg.sha512"
},
"Npgsql/8.0.4": {
"type": "package",
"serviceable": true,
"sha512": "sha512-vaYEUlF/pB9m8bs21wQv3Da0kMHT4A9USe47VfY/L2BO97xz5KfIxhEu22QS9d68ZrLxvtL3wQDfDLPr2OjbjA==",
"path": "npgsql/8.0.4",
"hashPath": "npgsql.8.0.4.nupkg.sha512"
},
"Npgsql.EntityFrameworkCore.PostgreSQL/8.0.8": {
"type": "package",
"serviceable": true,
"sha512": "sha512-D5WWJZJTgZYUmGv66BARXbTlinp2a5f5RueJqGYoHuWJw02J0i2va/RA+8N4A5hLORK5YKMRqXhFWtKsZdrksw==",
"path": "npgsql.entityframeworkcore.postgresql/8.0.8",
"hashPath": "npgsql.entityframeworkcore.postgresql.8.0.8.nupkg.sha512"
},
"Swashbuckle.AspNetCore/6.6.2": {
"type": "package",
"serviceable": true,
"sha512": "sha512-+NB4UYVYN6AhDSjW0IJAd1AGD8V33gemFNLPaxKTtPkHB+HaKAKf9MGAEUPivEWvqeQfcKIw8lJaHq6LHljRuw==",
"path": "swashbuckle.aspnetcore/6.6.2",
"hashPath": "swashbuckle.aspnetcore.6.6.2.nupkg.sha512"
},
"Swashbuckle.AspNetCore.Swagger/6.6.2": {
"type": "package",
"serviceable": true,
"sha512": "sha512-ovgPTSYX83UrQUWiS5vzDcJ8TEX1MAxBgDFMK45rC24MorHEPQlZAHlaXj/yth4Zf6xcktpUgTEBvffRQVwDKA==",
"path": "swashbuckle.aspnetcore.swagger/6.6.2",
"hashPath": "swashbuckle.aspnetcore.swagger.6.6.2.nupkg.sha512"
},
"Swashbuckle.AspNetCore.SwaggerGen/6.6.2": {
"type": "package",
"serviceable": true,
"sha512": "sha512-zv4ikn4AT1VYuOsDCpktLq4QDq08e7Utzbir86M5/ZkRaLXbCPF11E1/vTmOiDzRTl0zTZINQU2qLKwTcHgfrA==",
"path": "swashbuckle.aspnetcore.swaggergen/6.6.2",
"hashPath": "swashbuckle.aspnetcore.swaggergen.6.6.2.nupkg.sha512"
},
"Swashbuckle.AspNetCore.SwaggerUI/6.6.2": {
"type": "package",
"serviceable": true,
"sha512": "sha512-mBBb+/8Hm2Q3Wygag+hu2jj69tZW5psuv0vMRXY07Wy+Rrj40vRP8ZTbKBhs91r45/HXT4aY4z0iSBYx1h6JvA==",
"path": "swashbuckle.aspnetcore.swaggerui/6.6.2",
"hashPath": "swashbuckle.aspnetcore.swaggerui.6.6.2.nupkg.sha512"
},
"System.CodeDom/4.4.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-2sCCb7doXEwtYAbqzbF/8UAeDRMNmPaQbU2q50Psg1J9KzumyVVCgKQY8s53WIPTufNT0DpSe9QRvVjOzfDWBA==",
"path": "system.codedom/4.4.0",
"hashPath": "system.codedom.4.4.0.nupkg.sha512"
},
"System.Collections.Immutable/6.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-l4zZJ1WU2hqpQQHXz1rvC3etVZN+2DLmQMO79FhOTZHMn8tDRr+WU287sbomD0BETlmKDn0ygUgVy9k5xkkJdA==",
"path": "system.collections.immutable/6.0.0",
"hashPath": "system.collections.immutable.6.0.0.nupkg.sha512"
},
"System.Composition/6.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-d7wMuKQtfsxUa7S13tITC8n1cQzewuhD5iDjZtK2prwFfKVzdYtgrTHgjaV03Zq7feGQ5gkP85tJJntXwInsJA==",
"path": "system.composition/6.0.0",
"hashPath": "system.composition.6.0.0.nupkg.sha512"
},
"System.Composition.AttributedModel/6.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-WK1nSDLByK/4VoC7fkNiFuTVEiperuCN/Hyn+VN30R+W2ijO1d0Z2Qm0ScEl9xkSn1G2MyapJi8xpf4R8WRa/w==",
"path": "system.composition.attributedmodel/6.0.0",
"hashPath": "system.composition.attributedmodel.6.0.0.nupkg.sha512"
},
"System.Composition.Convention/6.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-XYi4lPRdu5bM4JVJ3/UIHAiG6V6lWWUlkhB9ab4IOq0FrRsp0F4wTyV4Dj+Ds+efoXJ3qbLqlvaUozDO7OLeXA==",
"path": "system.composition.convention/6.0.0",
"hashPath": "system.composition.convention.6.0.0.nupkg.sha512"
},
"System.Composition.Hosting/6.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-w/wXjj7kvxuHPLdzZ0PAUt++qJl03t7lENmb2Oev0n3zbxyNULbWBlnd5J5WUMMv15kg5o+/TCZFb6lSwfaUUQ==",
"path": "system.composition.hosting/6.0.0",
"hashPath": "system.composition.hosting.6.0.0.nupkg.sha512"
},
"System.Composition.Runtime/6.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-qkRH/YBaMPTnzxrS5RDk1juvqed4A6HOD/CwRcDGyPpYps1J27waBddiiq1y93jk2ZZ9wuA/kynM+NO0kb3PKg==",
"path": "system.composition.runtime/6.0.0",
"hashPath": "system.composition.runtime.6.0.0.nupkg.sha512"
},
"System.Composition.TypedParts/6.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-iUR1eHrL8Cwd82neQCJ00MpwNIBs4NZgXzrPqx8NJf/k4+mwBO0XCRmHYJT4OLSwDDqh5nBLJWkz5cROnrGhRA==",
"path": "system.composition.typedparts/6.0.0",
"hashPath": "system.composition.typedparts.6.0.0.nupkg.sha512"
},
"System.IO.Pipelines/6.0.3": {
"type": "package",
"serviceable": true,
"sha512": "sha512-ryTgF+iFkpGZY1vRQhfCzX0xTdlV3pyaTTqRu2ETbEv+HlV7O6y7hyQURnghNIXvctl5DuZ//Dpks6HdL/Txgw==",
"path": "system.io.pipelines/6.0.3",
"hashPath": "system.io.pipelines.6.0.3.nupkg.sha512"
},
"System.Reflection.Metadata/6.0.1": {
"type": "package",
"serviceable": true,
"sha512": "sha512-III/lNMSn0ZRBuM9m5Cgbiho5j81u0FAEagFX5ta2DKbljZ3T0IpD8j+BIiHQPeKqJppWS9bGEp6JnKnWKze0g==",
"path": "system.reflection.metadata/6.0.1",
"hashPath": "system.reflection.metadata.6.0.1.nupkg.sha512"
},
"System.Runtime.CompilerServices.Unsafe/6.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==",
"path": "system.runtime.compilerservices.unsafe/6.0.0",
"hashPath": "system.runtime.compilerservices.unsafe.6.0.0.nupkg.sha512"
},
"System.Text.Encoding.CodePages/6.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-ZFCILZuOvtKPauZ/j/swhvw68ZRi9ATCfvGbk1QfydmcXBkIWecWKn/250UH7rahZ5OoDBaiAudJtPvLwzw85A==",
"path": "system.text.encoding.codepages/6.0.0",
"hashPath": "system.text.encoding.codepages.6.0.0.nupkg.sha512"
},
"System.Text.Encodings.Web/8.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-yev/k9GHAEGx2Rg3/tU6MQh4HGBXJs70y7j1LaM1i/ER9po+6nnQ6RRqTJn1E7Xu0fbIFK80Nh5EoODxrbxwBQ==",
"path": "system.text.encodings.web/8.0.0",
"hashPath": "system.text.encodings.web.8.0.0.nupkg.sha512"
},
"System.Text.Json/8.0.4": {
"type": "package",
"serviceable": true,
"sha512": "sha512-bAkhgDJ88XTsqczoxEMliSrpijKZHhbJQldhAmObj/RbrN3sU5dcokuXmWJWsdQAhiMJ9bTayWsL1C9fbbCRhw==",
"path": "system.text.json/8.0.4",
"hashPath": "system.text.json.8.0.4.nupkg.sha512"
},
"System.Threading.Channels/6.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-TY8/9+tI0mNaUMgntOxxaq2ndTkdXqLSxvPmas7XEqOlv9lQtB7wLjYGd756lOaO7Dvb5r/WXhluM+0Xe87v5Q==",
"path": "system.threading.channels/6.0.0",
"hashPath": "system.threading.channels.6.0.0.nupkg.sha512"
},
"AdminService.Application/1.0.0": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"AdminService.Domain/1.0.0": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"AdminService.Infrastructure/1.0.0": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Shared.Commons/1.0.0": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Shared.Contracts/1.0.0": {
"type": "project",
"serviceable": false,
"sha512": ""
}
}
}

View File

@@ -1,20 +0,0 @@
{
"runtimeOptions": {
"tfm": "net8.0",
"frameworks": [
{
"name": "Microsoft.NETCore.App",
"version": "8.0.0"
},
{
"name": "Microsoft.AspNetCore.App",
"version": "8.0.0"
}
],
"configProperties": {
"System.GC.Server": true,
"System.Reflection.NullabilityInfoContext.IsSupported": true,
"System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization": false
}
}
}

View File

@@ -1,11 +0,0 @@
{
"ConnectionStrings": {
"DefaultConnection": "Host=192.168.0.111;Port=7925;Database=ocr;Username=postgres;Password=M@tr!x#149@dm!N"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@@ -1,9 +0,0 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}

Some files were not shown because too many files have changed in this diff Show More