Sari la conținut

Comunicare și semnare

Comunicarea cu clientul (și semnarea documentelor) trece prin patru sub-fluxuri decuplate: legarea unui chat Telegram prin token, partajarea unui document printr-un link public cu semnare MSign fără cont ASYDATA, butoanele cardului e-Factură și notificările in-app / web push. Pagina descrie pas cu pas actorii și stările.

Corespondentul vizual (swimlane) e pe board-ul FigJam: ASYDATA pe Figma.

  • Firma de contabilitate — inițiază bind-ul, partajează documente, operează cardurile.
  • ASYDATA (System) — WebApi + worker-ul Telegram + endpoint-urile publice.
  • Clientul — primește link-uri, vizualizează și semnează (fără cont ASYDATA).
  • Telegram — botul prin care vin cardurile și aprobările.
  • MSign — semnătura electronică (prin Prasada).

Pentru a primi carduri într-un chat, firma leagă chat-ul printr-un token de o singură folosire.

  • Firma generează un token (TTL 30 minute — BindTokenTtl = TimeSpan.FromMinutes(30) în asydata-app-backend-dotnet/src/Asy.Infrastructure/Telegram/TelegramService.cs).
  • Utilizatorul deschide t.me/<bot>?start={token}; worker-ul de polling consumă comanda și apelează RedeemBindTokenAsync, care creează/actualizează un TelegramChat (asydata-app-backend-dotnet/src/Asy.Domain/Telegram/TelegramChat.cs).
  • Un chat = o firmă: TelegramChat.FirmId e obligatoriu și imuabil; un chat poate fi re-legat ulterior la altă firmă (rândul se înlocuiește), dar la un moment dat servește o singură firmă.

Orice document partajabil (ShareableDocumentType { Contract, Invoice, HrContract, Act, Offer }) poate fi trimis printr-un link public. Entitatea e DocumentShare (asydata-app-backend-dotnet/src/Asy.Domain/Sharing/DocumentShare.cs).

  • Firma creează share-ul (IDocumentShareService, asydata-app-backend-dotnet/src/Asy.Application/Sharing/IDocumentShareService.cs): un Token lung URL-safe (~43 caractere base64url) și un email destinatar. Link-ul public are forma /share/{token}. Share-ul e activ cât RevokedAt e null și nu a expirat (IsActive).
  • View public. Clientul deschide link-ul fără cont. Endpoint-ul public marchează FirstViewedAt, bumpează LastViewedAt și ViewCount și scrie un DocumentEvent ShareViewed.
  • Semnare publică MSign — fără cont ASYDATA. Clientul semnează prin MSign; serviciul inițiază semnarea cu InitiatedByUserId null (partenerul nu are cont). La callback-ul MSign reușit, se setează DocumentShare.SignedAt, plus SignerName / SignerIdnp / SignerOrgIdno din certificat, și se scrie un DocumentEvent SignedByPartner. Pentru contracte, se completează Contract.SignedByPartnerAt.
  • Respingere. Clientul poate respinge documentul (RejectedAt + RejectionReason, DocumentEvent ShareRejected) — stare terminală, distinctă de revocarea inițiată de firmă (RevokedAt).

Stările unui DocumentShare (câmpuri-timestamp, nu un enum):

CâmpÎnțeles
CreatedAtlink creat
FirstViewedAt / LastViewedAt / ViewCountaudit de vizualizare
SignedAtsemnat de client prin MSign
RejectedAt / RejectionReasonrespins de client (terminal)
RevokedAtrevocat de firmă (terminal)
ExpiresAtexpirare opțională

DocumentEvent (asydata-app-backend-dotnet/src/Asy.Domain/Sharing/DocumentEvent.cs) e log-ul append-only al acțiunilor pe document, cu DocumentEventKind { Created, Updated, LinesReplaced, PdfGenerated, FileUploaded, StatusChanged, ShareSent, ShareViewed, ShareRevoked, SignedByFirm, SignedByPartner, Terminated, ShareRejected }. Acțiunile publice anonime populează ActorEmail, Ip, UserAgent (nu ActorUserId).

Cardul facturii (renderat de EfacturaCardRenderer) are butoane inline cu callback efactura:{invoiceId}:{action}:

  • Aprobă / Respinge (approve / decline) — aprobare internă, nu atinge SFS (vezi operare zilnică). În stare aprobată/respinsă, butonul devine un toggle (unset).
  • Vezi PDF (pdf) — trimite PDF-ul ca răspuns la mesaj.
  • Vezi atașamentul (attach) — dacă factura are atașament.
  • Semnează (MSign) — buton URL (nu callback), spre endpoint-ul care inițiază semnarea și redirecționează la MSign (zero mesaje noi). Ascuns la ciclu scurt, când cumpărătorul nu are ce semna.
  • Plătește (queue) — pune plata în coadă.

Când un utilizator declanșează o acțiune pentru care nu are drept, se creează un PermissionRequest (asydata-app-backend-dotnet/src/Asy.Domain/Telegram/PermissionRequest.cs), cu state machine PermissionRequestStatus { Pending, Approved, Denied, Expired }. Un card de aprobare apare în chat; un admin apasă Aprobă/Refuză (Approve / Deny), iar evenimentele (PermissionApprovedEvent / PermissionDeniedEvent) trec prin outbox → Kafka. Cererile expiră după ExpiresAt.

În paralel cu Telegram, evenimentele alimentează notificări:

  • In-app. Notification (asydata-app-backend-dotnet/src/Asy.Domain/Notifications/Notification.cs), cu un Type în formă "{domeniu}.{acțiune}" (ex. "efactura.incoming"), Title/Body, Link (rută SPA, ex. /facturi/{id}) și ReadAt.
  • Web push. PushSubscription (asydata-app-backend-dotnet/src/Asy.Domain/Notifications/PushSubscription.cs) ține Endpoint, cheile P256dh/Auth (ECDH P-256) și FailureCount (curățat la 404/410 de la serviciul de push). Un consumer web push trimite notificarea în browser pe baza acelorași evenimente.

Vezi notificări pentru detalii.

Niciun flux operațional nu apelează direct canalele de comunicare. Un eveniment de domeniu trece prin outbox → Kafka firm.events → consumeri independenți (Telegram, web push). Vezi flux event-driven.