""" e-conomic Export Service for Time Tracking Module ================================================== ๐Ÿšจ KRITISK: Denne service skal respektere safety flags. Eksporterer ordrer til e-conomic som draft orders. Safety Flags: - TIMETRACKING_ECONOMIC_READ_ONLY = True (default) - TIMETRACKING_ECONOMIC_DRY_RUN = True (default) """ import logging from typing import Optional, Dict, Any import aiohttp from fastapi import HTTPException from app.core.config import settings from app.core.database import execute_query, execute_update from app.timetracking.backend.models import ( TModuleEconomicExportRequest, TModuleEconomicExportResult ) from app.timetracking.backend.audit import audit logger = logging.getLogger(__name__) class EconomicExportService: """ e-conomic integration for Time Tracking Module. ๐Ÿ”’ SAFETY-FIRST service - all writes controlled by flags. """ def __init__(self): self.api_url = settings.ECONOMIC_API_URL self.app_secret_token = settings.ECONOMIC_APP_SECRET_TOKEN self.agreement_grant_token = settings.ECONOMIC_AGREEMENT_GRANT_TOKEN # Safety flags self.read_only = settings.TIMETRACKING_ECONOMIC_READ_ONLY self.dry_run = settings.TIMETRACKING_ECONOMIC_DRY_RUN self.export_type = settings.TIMETRACKING_EXPORT_TYPE # Log safety status if self.read_only: logger.warning("๐Ÿ”’ TIMETRACKING e-conomic READ-ONLY mode: Enabled") if self.dry_run: logger.warning("๐Ÿƒ TIMETRACKING e-conomic DRY-RUN mode: Enabled") if not self.read_only: logger.error("โš ๏ธ WARNING: TIMETRACKING e-conomic READ-ONLY disabled!") def _get_headers(self) -> Dict[str, str]: """Get e-conomic API headers""" return { 'X-AppSecretToken': self.app_secret_token, 'X-AgreementGrantToken': self.agreement_grant_token, 'Content-Type': 'application/json' } def _check_write_permission(self, operation: str) -> bool: """ Check om write operation er tilladt. Returns: True hvis operationen mรฅ udfรธres, False hvis blokeret """ if self.read_only: logger.error(f"๐Ÿšซ BLOCKED: {operation} - READ_ONLY mode enabled") return False if self.dry_run: logger.warning(f"๐Ÿƒ DRY-RUN: {operation} - Would execute but not sending") return False logger.warning(f"โš ๏ธ EXECUTING WRITE: {operation}") return True async def test_connection(self) -> bool: """Test e-conomic connection""" try: logger.info("๐Ÿ” Testing e-conomic connection...") async with aiohttp.ClientSession() as session: async with session.get( f"{self.api_url}/self", headers=self._get_headers(), timeout=aiohttp.ClientTimeout(total=10) ) as response: if response.status == 200: logger.info("โœ… e-conomic connection successful") return True else: logger.error(f"โŒ e-conomic connection failed: {response.status}") return False except Exception as e: logger.error(f"โŒ e-conomic connection error: {e}") return False async def export_order( self, request: TModuleEconomicExportRequest, user_id: Optional[int] = None ) -> TModuleEconomicExportResult: """ Eksporter ordre til e-conomic som draft order. Args: request: Export request med order_id user_id: ID pรฅ brugeren der eksporterer Returns: Export result med success status """ try: # Hent ordre med linjer order_query = """ SELECT o.*, c.name as customer_name, c.vtiger_id as customer_vtiger_id FROM tmodule_orders o JOIN tmodule_customers c ON o.customer_id = c.id WHERE o.id = %s """ order = execute_query(order_query, (request.order_id,), fetchone=True) if not order: raise HTTPException(status_code=404, detail="Order not found") # Check if already exported if order['economic_draft_id'] and not request.force: raise HTTPException( status_code=400, detail=f"Order already exported (draft ID: {order['economic_draft_id']}). Use force=true to re-export." ) # Hent ordre-linjer lines_query = """ SELECT * FROM tmodule_order_lines WHERE order_id = %s ORDER BY line_number """ lines = execute_query(lines_query, (request.order_id,)) if not lines: raise HTTPException( status_code=400, detail="Order has no lines" ) # Log export start audit.log_export_started( order_id=request.order_id, user_id=user_id ) # Check safety flags operation = f"Export order {request.order_id} to e-conomic" if not self._check_write_permission(operation): # Dry-run or read-only mode result = TModuleEconomicExportResult( success=True, dry_run=True, order_id=request.order_id, economic_draft_id=None, economic_order_number=None, message=f"DRY-RUN: Would export order {order['order_number']} to e-conomic", details={ "order_number": order['order_number'], "customer_name": order['customer_name'], "total_amount": float(order['total_amount']), "line_count": len(lines), "read_only": self.read_only, "dry_run": self.dry_run } ) # Log dry-run completion audit.log_export_completed( order_id=request.order_id, economic_draft_id=None, economic_order_number=None, dry_run=True, user_id=user_id ) return result # REAL EXPORT (kun hvis safety flags er disabled) logger.warning(f"โš ๏ธ REAL EXPORT STARTING for order {request.order_id}") # TODO: Implementer rigtig e-conomic API call her # Denne kode vil kun kรธre hvis READ_ONLY og DRY_RUN begge er False # Build e-conomic draft order payload economic_payload = { "date": order['order_date'].isoformat(), "currency": "DKK", "exchangeRate": 100, "netAmount": float(order['subtotal']), "grossAmount": float(order['total_amount']), "vatAmount": float(order['vat_amount']), "notes": { "heading": f"Tidsregistrering - {order['order_number']}", "textLine1": order.get('notes', '') }, "lines": [ { "lineNumber": line['line_number'], "description": line['description'], "quantity": float(line['quantity']), "unitNetPrice": float(line['unit_price']), "totalNetAmount": float(line['line_total']) } for line in lines ] } # Call e-conomic API async with aiohttp.ClientSession() as session: async with session.post( f"{self.api_url}/orders/drafts", headers=self._get_headers(), json=economic_payload, timeout=aiohttp.ClientTimeout(total=30) ) as response: if response.status not in [200, 201]: error_text = await response.text() logger.error(f"โŒ e-conomic export failed: {response.status} - {error_text}") # Log failed export audit.log_export_failed( order_id=request.order_id, error=error_text, user_id=user_id ) raise HTTPException( status_code=response.status, detail=f"e-conomic API error: {error_text}" ) result_data = await response.json() economic_draft_id = result_data.get('draftOrderNumber') economic_order_number = result_data.get('orderNumber', str(economic_draft_id)) # Update order med e-conomic IDs execute_update( """UPDATE tmodule_orders SET economic_draft_id = %s, economic_order_number = %s, exported_at = CURRENT_TIMESTAMP, exported_by = %s, status = 'exported' WHERE id = %s""", (economic_draft_id, economic_order_number, user_id, request.order_id) ) # Log successful export audit.log_export_completed( order_id=request.order_id, economic_draft_id=economic_draft_id, economic_order_number=economic_order_number, dry_run=False, user_id=user_id ) logger.info(f"โœ… Exported order {request.order_id} โ†’ e-conomic draft {economic_draft_id}") return TModuleEconomicExportResult( success=True, dry_run=False, order_id=request.order_id, economic_draft_id=economic_draft_id, economic_order_number=economic_order_number, message=f"Successfully exported to e-conomic draft {economic_draft_id}", details=result_data ) except HTTPException: raise except Exception as e: logger.error(f"โŒ Error exporting order: {e}") # Log failed export audit.log_export_failed( order_id=request.order_id, error=str(e), user_id=user_id ) raise HTTPException(status_code=500, detail=str(e)) # Singleton instance economic_service = EconomicExportService()