"""Evidence pack generation with manifests and signatures.""" import io from typing import Any import structlog logger = structlog.get_logger() class EvidencePackGenerator: # pylint: disable=too-few-public-methods """Generate evidence packs with manifests and signatures""" def __init__(self, storage_client: Any) -> None: self.storage = storage_client async def create_evidence_pack( # pylint: disable=too-many-locals self, taxpayer_id: str, tax_year: str, scope: str, evidence_items: list[dict[str, Any]], ) -> dict[str, Any]: """Create evidence pack with manifest and signatures""" # pylint: disable=import-outside-toplevel import hashlib import json import zipfile from datetime import datetime try: # Create ZIP buffer zip_buffer = io.BytesIO() with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zip_file: manifest: dict[str, Any] = { "taxpayer_id": taxpayer_id, "tax_year": tax_year, "scope": scope, "created_at": datetime.utcnow().isoformat(), "evidence_items": [], "signatures": {}, } # Add evidence files to ZIP for item in evidence_items: doc_id = item["doc_id"] page = item.get("page") bbox = item.get("bbox") text_hash = item.get("text_hash") # Get document content doc_content = await self.storage.get_object( bucket_name="raw-documents", object_name=f"tenants/{taxpayer_id}/raw/{doc_id}.pdf", ) if doc_content: # Add to ZIP zip_filename = f"documents/{doc_id}.pdf" zip_file.writestr(zip_filename, doc_content) # Calculate file hash file_hash = hashlib.sha256(doc_content).hexdigest() # Add to manifest manifest["evidence_items"].append( { "doc_id": doc_id, "filename": zip_filename, "page": page, "bbox": bbox, "text_hash": text_hash, "file_hash": file_hash, "file_size": len(doc_content), } ) # Sign manifest manifest_json = json.dumps(manifest, indent=2, sort_keys=True) manifest_hash = hashlib.sha256(manifest_json.encode()).hexdigest() manifest["signatures"]["manifest_hash"] = manifest_hash manifest["signatures"]["algorithm"] = "SHA-256" # Add manifest to ZIP zip_file.writestr("manifest.json", json.dumps(manifest, indent=2)) # Get ZIP content zip_content = zip_buffer.getvalue() # Store evidence pack pack_filename = f"evidence_pack_{taxpayer_id}_{tax_year}_{scope}.zip" pack_key = f"tenants/{taxpayer_id}/evidence_packs/{pack_filename}" success = await self.storage.put_object( bucket_name="evidence-packs", object_name=pack_key, data=io.BytesIO(zip_content), length=len(zip_content), content_type="application/zip", ) if success: return { "pack_filename": pack_filename, "pack_key": pack_key, "pack_size": len(zip_content), "evidence_count": len(evidence_items), "manifest_hash": manifest_hash, "s3_url": f"s3://evidence-packs/{pack_key}", } raise RuntimeError("Failed to store evidence pack") except Exception as e: # pylint: disable=broad-exception-caught logger.error("Failed to create evidence pack", error=str(e)) raise # Form configuration for UK tax forms UK_TAX_FORMS = { "SA100": { "name": "Self Assessment Tax Return", "template_path": "forms/templates/SA100.pdf", "boxes": { "1": {"description": "Your name", "type": "text"}, "2": {"description": "Your address", "type": "text"}, "3": {"description": "Your UTR", "type": "text"}, "4": {"description": "Your NI number", "type": "text"}, }, }, "SA103": { "name": "Self-employment (full)", "template_path": "forms/templates/SA103.pdf", "boxes": { "1": {"description": "Business name", "type": "text"}, "2": {"description": "Business description", "type": "text"}, "3": {"description": "Accounting period start", "type": "date"}, "4": {"description": "Accounting period end", "type": "date"}, "20": {"description": "Total turnover", "type": "currency"}, "31": { "description": "Total allowable business expenses", "type": "currency", }, "32": {"description": "Net profit", "type": "currency"}, "33": {"description": "Balancing charges", "type": "currency"}, "34": {"description": "Goods/services for own use", "type": "currency"}, "35": {"description": "Total taxable profits", "type": "currency"}, }, }, "SA105": { "name": "Property income", "template_path": "forms/templates/SA105.pdf", "boxes": { "20": {"description": "Total rents and other income", "type": "currency"}, "29": { "description": "Premiums for the grant of a lease", "type": "currency", }, "31": { "description": "Rent, rates, insurance, ground rents etc", "type": "currency", }, "32": {"description": "Property management", "type": "currency"}, "33": { "description": "Services provided, including wages", "type": "currency", }, "34": { "description": "Repairs, maintenance and renewals", "type": "currency", }, "35": { "description": "Finance costs, including interest", "type": "currency", }, "36": {"description": "Professional fees", "type": "currency"}, "37": {"description": "Costs of services provided", "type": "currency"}, "38": { "description": "Other allowable property expenses", "type": "currency", }, }, }, }