Sari la conținut

Bot Telegram

Botul Telegram al ASYDATA e un bot multi-tenant care leagă un chat de o firmă (prin token), trimite carduri interactive de e-Factură (primite + emise) cu butoane de acțiune și rulează un flux de aprobare de permisiuni. Sursa de referință e docs/modules/telegram.md; codul trăiește în worker-ul src/Workers/Asy.Workers.Telegram/, domeniul în src/Asy.Domain/Telegram/, iar webhook-ul + REST-ul de management în src/Asy.WebApi/Endpoints/TelegramEndpoints.cs.

Principiul cheie: decuplare prin evenimente. Sync-ul de e-Factură nu cheamă direct Telegram — ridică un eveniment de domeniu care trece prin outbox → Kafka. Vezi Flux event-driven.

EfacturaService.SyncInvoicesAsync (factură nouă / status schimbat)
└─ invoice.RaiseEFacturaReceived() / RaiseEFacturaStatusChanged() / RaiseIssued()
└─ OutboxSaveChangesInterceptor → tabela outbox_events
└─ OutboxRelayService (poll 500ms) → Kafka topic `firm.events`
├─ FirmEventsConsumer (group `telegram-worker`) → card Telegram
└─ WebPushConsumer (group `webpush-worker`) → web push

FirmEventsConsumer (src/Workers/Asy.Workers.Telegram/FirmEventsConsumer.cs, ConsumerGroup = "telegram-worker") consumă firm.events și dispatch-uiește pe tipul evenimentului. Evenimentele de factură și de permisiuni sunt în src/Asy.Domain/Telegram/Events/PermissionEvents.cs: InvoiceReceivedEvent, InvoiceEFacturaStatusChangedEvent, InvoiceIssuedEvent, Permission{Requested,Approved,Denied}Event.

Un chat se leagă de o firmă printr-un token de unică folosință. Comanda /start <token> (în grup /start@botname <token>, normalizat prin strip @botname):

  • token fără prefix → bind de firmă (TelegramBindToken);
  • token cu prefix P- → bind de partener (PartnerTelegramBindToken).

1 chat = 1 firmă, last-bind-wins. Entitățile sunt în src/Asy.Domain/Telegram/TelegramChat.cs (TelegramChat, TelegramBindToken).

Cardul are două variante (renderere separate):

  • primit — src/Asy.Infrastructure/Telegram/EfacturaCardRenderer.cs;
  • emis — src/Asy.Infrastructure/Telegram/InvoiceIssuedCardRenderer.cs.

Callback data are formatul efactura:{invoiceId}:{action}, cu action ∈ approve | decline | unset | pdf | attach | queue. Dispatch-ul partajat (poller + webhook) e în src/Asy.Infrastructure/Telegram/TelegramEfacturaCallbackHandler.cs.

ButonAcțiuneEfect
Aprobă / Respingeapprove / declineaprobare internă (toggle) pe Invoice.InternalApprovalStatus; nu atinge SFS
(aprobat/respins)unsetrevine la cele 2 butoane
Vezi PDFpdfSendDocument ca reply la card
Vezi atașamentattachSendDocument (dacă există anexă)
Semnează (MSign)buton-URLredirect spre semnare (vezi mai jos)
Plăteștequeuepune plata în coadă

Aprobatorul și semnatarul pot diferi: cardul arată „Aprobată intern de X (Telegram)” + „Semnată la SFS de Y” (din Invoice.EFacturaSignedByName).

MSign nu e callback, ci buton-URL (WithUrl) spre GET /api/telegram/msign/{invoiceId}?t={hmac}. Endpoint-ul (TelegramEndpoints.cs) validează token-ul HMAC (MsignLinkToken, HMAC-SHA256 cu Telegram:WebhookSecret, comparație în timp constant), inițiază LAZY cererea MSign și face 302 spre MSign. Un tap, zero mesaje noi în chat.

La schimbarea statusului (semnat / respins / plătit), InvoiceEFacturaStatusChangedEvent ajunge la consumer, care re-editează cardul existent (EditMessageText), direcție-aware: card primit (EfacturaCardRenderer) vs card emis (InvoiceIssuedCardRenderer). Asociația (InvoiceId, ChatId) → MessageId permite editarea din orice sursă.

MediuMecanismConfig
devlong-pollTelegram:PollingEnabled = true (implicit) — TelegramPollerService
prodwebhookTelegram:PollingEnabled = false, Telegram:WebhookSecret obligatoriu

Webhook-ul e POST /api/telegram/webhook, validat cu header-ul X-Telegram-Bot-Api-Secret-Token. Callback-urile pe ambele căi folosesc același ITelegramEfacturaCallbackHandler.

Entitate / tabelRol
telegram_chats (TelegramChat)chat legat de firmă + EventsEnabledJson (toggle per tip de eveniment)
telegram_card_messages (TelegramCardMessage)(InvoiceId, ChatId) unic → MessageId, pentru editarea cardului
telegram_bind_tokens (TelegramBindToken)token de bind de firmă
telegram_chat_members (TelegramChatMember)membri + rol + permisiuni granulare (PermissionsJson)
permission_requests (PermissionRequest)starea fluxului de aprobare
partner_telegram_*bind + chat-uri de partener (prefix P-)

EventsEnabledJson e un JSON de tip { "invoiceNew": true, "syncError": false, … }; cheia lipsă = implicit activată pentru tipurile noi de eveniment.

Când un user fără drept declanșează o acțiune sensibilă (ex. efactura.sign), se ridică PermissionRequestedEvent → card de aprobare cu butoanele Aprob/Refuz (approval:{requestId}:{approve|deny}). Decizia ridică PermissionApprovedEvent / PermissionDeniedEvent, iar consumerul re-editează cardul cu rezultatul.