Table of Contents

Namespace UsluzionicaServer.Services

Classes

AdminService
AuthService
BlockService

Blokiranje korisnika.

SIMETRIJA JE CELA POENTA. Red u bazi je usmeren (ko koga je blokirao), ali svaka provera gleda oba smera. Ako A blokira B:

• A ne vidi B-ove oglase, profil, recenzije ni poruke • B ne vidi A-ove — iako B ništa nije uradio • nijedan ne može da piše onom drugom

Druga stavka je ta koja se lako preskoči, a bez nje blokada ne znači ništa: osoba od koje se korisnik sklanja i dalje bi mu gledala oglase i pisala.

ŠTA SE NE DIRA: već obavljene rezervacije, isplaćeni tokeni i istorija transakcija. Blokada je socijalna mera, ne brisanje prošlosti — ako se zabrani pristup istoriji, prvi spor oko novca ostaje bez traga.

BookingService

Booking modul — upravlja celim životnim ciklusom booking zahteva: Pending → Confirmed → Completed (token nagrada) Pending → Rejected Pending → Cancelled (klijent otkazuje)

3-dnevno pravilo: provider može označiti uslugu kao izvršenu tek 3 dana nakon potvrde (AcceptedAt + ExecuteAfterDays < UtcNow). Ovo sprečava prevremeno farmovanje tokena.

BoostService

Boost modul — provider troši tokene da podigne vidljivost svog listinga.

BoostScore formula: tokensToSpend / durationDays Primer: 3 tokena × 3 dana → BoostScore += 1.0 7 tokena × 7 dana → BoostScore += 1.0 6 tokena × 3 dana → BoostScore += 2.0

BoostScore je ADITIVAN — više istovremenih boostova se sabiraju. BoostExpiryService svakih sat vremena oduzima BoostScore isteklih boostova.

CategoryService
ConversationService
EmailService

Šalje transakcione emailove (verifikacija, reset lozinke) putem SMTP-a (MailKit). Konfiguracija se čita iz appsettings.json → "Email" sekcija.

FavoriteService

Omiljeni oglasi i uslugodavaci.

Toggle pristup: isti endpoint dodaje ako ne postoji, briše ako već postoji. UNIQUE constraint (UserId, ListingId) i (UserId, ProviderProfileId) garantuje da nema duplikata čak i pri paralelnim zahtevima.

ImageModerationGate

Spaja automatsku proveru slike sa redom za moderaciju.

Postoji kao zaseban servis, a ne kao poziv u svakom od tri upload puta, zato što srednji ishod (Review) nije samo „propusti" — mora i da napravi prijavu. Da je ta logika prepisana na tri mesta, jedno bi pre ili kasnije propustilo da je napravi i slika bi prošla bez ikakvog traga.

ZAŠTO NIJE U ImageUploads: Ta klasa je statička, čista, bez DI i pokrivena unit testovima — proverava bajtove i ne dodiruje ni bazu ni mrežu. Mrežni poziv u njoj pokvario bi sve četiri osobine.

ListingService
NotificationService

Centralni servis za in-app notifikacije.

SendAsync: snima notifikaciju u bazu i odmah je push-uje korisniku preko NotificationHub-a ("user-{userId}" SignalR grupa).

Svi ostali servisi koji triggeruju eventi (BookingService, ReviewService itd.) koriste ovaj servis umesto direktnog db.Notifications.Add().

ProviderService
ReferralService

Referral nagrada se isplaćuje u DVE rate:

  1. rata — pozvani potvrdi email → pozivalac dobija SignupRewardTokens (2)
  2. rata — pozvani aktivira provajdera → pozivalac dobija ActivationRewardTokens (3)

Zašto prva rata ide na potvrdu emaila a ne na samu registraciju: registracija je besplatna i neograničena, pa bi isplata na registraciju značila da svako može da otvara naloge sa sopstvenim kodom i uzima tokene bez ikakvog rada. Potvrda emaila zahteva stvarnu, jedinstvenu i funkcionalnu adresu po nalogu. Uz to, nalog bez potvrđenog emaila ne može ni da se prijavi (AuthService. LoginAsync ga odbija), pa u ovom sistemu ionako još nije pravi nalog.

Obe metode su IDEMPOTENTNE i bezbedne pod istovremenim pozivima. To nije teorijska briga: verifikacioni link se lako aktivira dvaput (mail klijent ga prefetch-uje, pa korisnik klikne), a ResetPasswordAsync je drugi put kroz koji email može biti potvrđen.

ReportService

Prijave oglasa i korisnika, i njihovo rešavanje.

RED ZA ADMINA JE PO METI, NE PO PRIJAVI. Deset prijava istog oglasa je jedan posao i jedna odluka, ne deset. Zato je red grupisan upit, a rešavanje zatvara sve prijave za tu metu odjednom.

BROJ PRIJAVA SE NE DENORMALIZUJE. Ne postoji Listing.ReportCount. Broj se računa agregatom nad Reports pri svakom otvaranju reda. Denormalizovan brojač bi bio drugi izvor istine o istom podatku, a dva izvora se raziđu — obično posle prve izmene koja zaobiđe change tracker. Volumen ovde ni izbliza ne traži tu cenu.

SADRŽAJ SE NE SKRIVA AUTOMATSKI. Nema praga posle kog oglas nestaje sam. To bi bio poklon konkurenciji: par lažnih prijava obori tuđi oglas bez ijedne provere. Prva prijava stavlja oglas u red; sklanja ga čovek.

ReviewService

Review modul — pisanje recenzija i kalkulacija proseka providera.

Pravila:

  • Jedan autor = jedna recenzija po listingu (UNIQUE u bazi).
  • BookingRequestId je opciono; ako je dat mora biti Completed i vlasništvo autora.
  • Nakon svake nove recenzije recalculate ProviderProfile.AverageRating i TotalReviews.
TokenService

Generiše JWT access token i kriptografski sigurni refresh token.

TokenWalletService

Token modul: wallet (balans + ledger), discount token ponude, referral statistika.

UserModerationService

Deaktivacija i vraćanje naloga, sa SVIM posledicama.

Postoji kao zaseban servis zato što deaktivacija ima tri dela koja moraju da idu zajedno, a zovu je dva pozivaoca (admin ručno i rešavanje prijave). Da je logika prepisana na oba mesta, jedno bi pre ili kasnije zaboravilo jedan deo — i to tiho.

ŠTA JE RANIJE FALILO (zatečeno stanje pre ovog servisa):

  1. IsActive se proveravao SAMO pri prijavi i osvežavanju tokena (AuthService). Oglasi deaktiviranog korisnika ostajali su u pretrazi — dakle baniš nalog, a sadržaj zbog kog si ga banovao i dalje stoji.

  2. Refresh tokeni se nisu poništavali. Access token traje 60 minuta, pa je banovan korisnik nastavljao da radi do sat vremena.

  3. AdminService.DeactivateUserAsync je bio TOGGLE (SetProperty(u => !u.IsActive)). Admin koji dvaput klikne na prijavu vratio bi nalog, a da to nigde ne vidi. Zato je ovde eksplicitno „deaktiviraj" ili „vrati", nikad „obrni".

UserService

Interfaces

IEmailService

Slanje transakcionih emailova. Postoji kao interfejs iz dva razloga:

  1. To je jedina spoljna I/O zavisnost u toku registracije i reseta lozinke. Bez interfejsa bi svaki test pokušavao pravu SMTP konekciju — sporo, nepouzdano, i nemoguće u CI-ju bez pristupa mreži.

  2. Kod za reset lozinke se u bazi čuva HEŠIRAN (vidi PasswordResetCode). Test drugačije ne može doći do čistog koda — jedini način je da presretne poslatu poruku. Zato test implementacija hvata pozive u listu.