Sari la conținut

Achiziție client

Achiziția unui client traversează un funnel cu lifecycle-uri separate: oportunitate CRM (CrmOpportunity), ofertă comercială (Offer) și contract (Contract). Fiecare are propria stare; trecerea de la unul la altul declanșează automatizări (creare partener, programare facturi recurente). Pagina descrie pas cu pas cine ce face, cu stările exacte din cod.

Vezi și corespondentul vizual (swimlane) pe board-ul FigJam: ASYDATA pe Figma.

  • Firma de contabilitate — rolul de vânzări / contabil care lucrează pipeline-ul (vezi actori și roluri).
  • Clientul (prospect) — compania țintă, încă fără cont în ASYDATA.
  • ASYDATA (System) — WebApi + serviciile de aplicație care aplică automatizările.
  • MSign — semnătura electronică (prin Prasada), pentru contractul semnat de ambele părți.

Firma creează un deal în pipeline. Entitatea e CrmOpportunity (asydata-app-backend-dotnet/src/Asy.Domain/Crm/CrmOpportunity.cs). Un deal se leagă fie de un client existent (RelationshipId), fie de un prospect rece prin ProspectCompanyName — înainte să existe un partener în baza firmei.

  • Status pornește Open (enum OpportunityStatus { Open, Won, Lost }).
  • Etapele vin din pipeline_stages; Status se ține sincron cu tipul etapei (serviciul IOpportunitiesService.MoveStageAsync, în asydata-app-backend-dotnet/src/Asy.Application/Crm/IOpportunitiesService.cs).
  • Probability urmărește etapa cât timp nu e setată manual; WeightedValue = Value × Probability/100.

Pentru un lead, firma emite o ofertă. Entitatea e Offer (asydata-app-backend-dotnet/src/Asy.Domain/Offers/Offer.cs), cu lifecycle propriu, separat de contract:

Stare (OfferStatus)ÎnțelesTranziție
DraftÎn lucru, editabilăimplicit la creare
SentTrimisă clientuluiMarkSent
AcceptedAcceptată de clientMarkAccepted
RejectedRespinsă de clientMarkRejected
ExpiredA depășit ValidUntil fără răspuns—

Oferta se face de obicei pentru un lead care nu e încă partener (ClientName, text liber). Liniile se împart în recurente (OfferBillingType.Recurring, cu OfferRecurrence Monthly/Quarterly/Yearly) și one-time, iar totalurile se recalculează pe cele două fluxuri (RecomputeTotals). Oferta poate fi generată dintr-un DocumentTemplate, semnată MSign și trimisă prin Document Hub — la fel ca un contract (vezi comunicare și semnare).

Când oferta e câștigată, firma o convertește în contract: IOffersService.ConvertToContractAsync (asydata-app-backend-dotnet/src/Asy.Infrastructure/Offers/OffersService.cs). Aici se întâmplă mai multe automatizări într-o singură tranzacție:

  1. Lead → partener. Dacă oferta nu are partener legat (PartnerMasterId null), serviciul creează automat un PartnerMaster (din ClientName/Title) și un PartnerRelationship firmă↔master, cu conturi contabile implicite (DefaultAccountDb = "221.1", DefaultAccountCr = "611.1"). Așa lead-ul intră în baza de parteneri la conversie.
  2. Creează contractul. Numerotat din Codificări (CommercialContract, prefix CTR), cu liniile copiate din ofertă (recurente/one-time, cu recurența mapată).
  3. Marchează oferta Accepted (MarkAccepted) și completează ConvertedToContractId — conversie idempotentă (a doua oară e respinsă).

Contractul (asydata-app-backend-dotnet/src/Asy.Domain/Contracts/Contract.cs) pornește Draft (ContractStatus { Draft, Active, Suspended, Expired, Terminated }).

  • Semnare. Contractul are două timestamp-uri de semnătură: SignedByFirmAt (firma semnează prin MSign; setat de callback-ul MSign care identifică contractul) și SignedByPartnerAt (partenerul semnează printr-un link public cu MSign — vezi comunicare și semnare).
  • Activare. Trecerea la Active se face prin IContractsService.SetStatusAsync (asydata-app-backend-dotnet/src/Asy.Application/Contracts/Services/IContractsService.cs), care apelează Contract.Activate()/Suspend()/Resume()/Terminate().

Automatizare — facturi recurente. Dacă contractul are cel puțin o linie recurentă, serviciul creează un RecurringInvoiceSchedule (asydata-app-backend-dotnet/src/Asy.Domain/Contracts/RecurringInvoiceSchedule.cs), 1:1 cu contractul. Un worker scanează WHERE is_active AND next_due_date <= today și generează facturi draft periodic, avansând NextDueDate cu un ciclu (AdvanceToNext).

La câștig, firma marchează deal-ul prin IOpportunitiesService.WinAsync (sau MoveStageAsync pe o etapă de tip won): Status = Won, Probability = 100, WonAt setat, ValueBase înghețat. Dacă deal-ul era pe un prospect rece, serviciul promovează prospectul la partener (PartnerMaster + PartnerRelationship Active) și completează RelationshipId.

Compania client trece în LifecycleStage.Active (asydata-app-backend-dotnet/src/Asy.Domain/Companies/Company.cs, enum LifecycleStage { Lead, Prospect, Onboarding, Active, Problematic, Churned }). Companiile client se creează prin Company.CreateClient(...), care le pune inițial în Onboarding.

Dacă prospectul refuză, fluxul se închide simetric:

  • Ofertă respinsă — Offer.MarkRejected → OfferStatus.Rejected (sau Expired dacă a trecut ValidUntil).
  • Deal pierdut — IOpportunitiesService.LoseAsync cere obligatoriu LostReasonId; setează Status = Lost, Probability = 0, LostAt, și opțional LostReasonNote. Motivele vin din CrmLostReason (asydata-app-backend-dotnet/src/Asy.Domain/Crm/CrmLostReason.cs).
  • Partenerul, dacă exista, poate trece în PartnerLifecycleStage.Churned (cu LostReason pe relație).
Lead Ofertă Contract Deal / Companie
─────────────────────────────────────────────────────────────────────────────────────
Opportunity=Open → Offer Draft→Sent→Accepted → Contract Draft → Opportunity=Won
│ (ConvertToContract) → semnat MSign Company.LifecycleStage
│ + PartnerMaster → Active = Active
│ + PartnerRelationship Active (+ RecurringInvoiceSchedule
│ dacă are linii recurente)
└─ Rejected / Expired ─────→ (deal Lost + LostReason)