from datetime import datetime from threading import Thread from flask import current_app from app.extensions import db from app.models.company import Company from app.models.setting import AppSetting from app.models.sync_log import SyncLog from app.services.company_service import CompanyService from app.services.invoice_service import InvoiceService from app.services.ksef_service import KSeFService from app.services.notification_service import NotificationService from app.services.settings_service import SettingsService from app.services.redis_service import RedisService class SyncService: def __init__(self, company=None): self.company = company or CompanyService.get_current_company() self.ksef = KSeFService(company_id=self.company.id if self.company else None) self.invoice_service = InvoiceService() self.notification_service = NotificationService(company_id=self.company.id if self.company else None) def _run(self, sync_type='manual', existing_log=None): log = existing_log or SyncLog( company_id=self.company.id if self.company else None, sync_type=sync_type, status='started', started_at=datetime.utcnow(), message='Rozpoczęto synchronizację', ) db.session.add(log) db.session.commit() since_raw = SettingsService.get('ksef.last_sync_at', company_id=self.company.id if self.company else None) since = datetime.fromisoformat(since_raw) if since_raw else None created = updated = errors = 0 try: documents = self.ksef.list_documents(since=since) log.total = len(documents) db.session.commit() for idx, document in enumerate(documents, start=1): invoice, was_created = self.invoice_service.upsert_from_ksef(document, self.company) if was_created: created += 1 self.notification_service.notify_new_invoice(invoice) else: updated += 1 log.processed = idx log.created = created log.updated = updated log.message = f'Przetworzono {idx}/{len(documents)}' db.session.commit() log.status = 'finished' log.message = 'Synchronizacja zakończona' SettingsService.set_many( { 'ksef.status': 'ready', 'ksef.last_sync_at': datetime.utcnow().isoformat(), }, company_id=self.company.id if self.company else None, ) except RuntimeError as exc: message = str(exc) if 'HTTP 429' in message or 'ogranicza liczbę zapytań' in message: current_app.logger.warning('Synchronizacja KSeF wstrzymana przez limit API: %s', message) else: current_app.logger.error('Sync failed: %s', message) log.status = 'error' log.message = message errors += 1 SettingsService.set_many( {'ksef.status': 'error'}, company_id=self.company.id if self.company else None, ) except Exception as exc: current_app.logger.exception('Sync failed: %s', exc) log.status = 'error' log.message = str(exc) errors += 1 SettingsService.set_many( {'ksef.status': 'error'}, company_id=self.company.id if self.company else None, ) RedisService.delete(f'dashboard.summary.company.{self.company.id if self.company else "global"}') RedisService.delete(f'health.status.company.{self.company.id if self.company else "global"}') log.errors = errors log.finished_at = datetime.utcnow() db.session.commit() return log def run_manual_sync(self): return self._run('manual') def run_scheduled_sync(self): return self._run('scheduled') @staticmethod def start_manual_sync_async(app, company_id): company = db.session.get(Company, company_id) log = SyncLog( company_id=company_id, sync_type='manual', status='queued', started_at=datetime.utcnow(), message='Zadanie zakolejkowane', total=1, ) db.session.add(log) db.session.commit() log_id = log.id def worker(): with app.app_context(): company_local = db.session.get(Company, company_id) log_local = db.session.get(SyncLog, log_id) log_local.status = 'started' log_local.message = 'Start pobierania' db.session.commit() SyncService(company_local)._run('manual', existing_log=log_local) Thread(target=worker, daemon=True).start() return log_id