> ## Documentation Index
> Fetch the complete documentation index at: https://docs.diyanet.pro/llms.txt
> Use this file to discover all available pages before exploring further.

# Aggregate'ler

> Aggregate kavramı, User aggregate'inin derinlemesine incelemesi ve tüm aggregate'lerin özeti.

Bir **aggregate**, birlikte tutarlı kalması gereken entity ve value object'lerin oluşturduğu sınırdır. Sınırın tepesinde tek bir **aggregate root** durur; dışarıdan tüm erişim ve değişiklik bu root üzerinden geçer. Bu sayede iş kuralları (invariant) tek bir yerde garanti altına alınır.

<Note>
  **Temel kural:** Çocuk entity'ler dışarıya doğrudan açılmaz. `User.Sessions` koleksiyonuna yeni oturum eklemek için `user.StartSession(...)` çağrılır — `user.Sessions.Add(...)` değil. Repository yalnızca root için vardır.
</Note>

## User aggregate'i — derinlemesine

`User`, back-office (personel) kullanıcısını temsil eden en zengin aggregate'tir. Tüm desenleri tek örnekte gösterir.

```csharp theme={null}
public class User : Entity, IAggregateRoot
{
    public long ReferenceNumber { get; protected set; }
    public FullName? FullName { get; private set; }
    public Phone?    Phone    { get; private set; }
    public Email?    Email    { get; private set; }

    public bool IsPhoneNumberVerified { get; private set; }
    public bool IsEmailVerified       { get; private set; }

    public TotpConfigurationInfo? Totp { get; private set; }
    public virtual int StatusId { get; private set; }
    public virtual UserStatus Status => UserStatus.FromValue<UserStatus>(StatusId);

    public int TokenVersion { get; private set; } = 1; // RBAC token invalidation

    public virtual ICollection<UserSession> Sessions { get; private set; } = new HashSet<UserSession>();
    public virtual ICollection<UserOtpChallenge> OtpChallenges { get; private set; } = new HashSet<UserOtpChallenge>();
    public virtual ICollection<UserIdentityProvider> IdentityProviders { get; private set; } = new HashSet<UserIdentityProvider>();
    public virtual ICollection<UserRole> UserRoles { get; private set; } = new HashSet<UserRole>();
    // ...
}
```

### Value object alanları

`FullName`, `Phone`, `Email` birer value object'tir (nullable — kullanıcı telefon veya e-posta ile kaydolabilir). `Totp` ise `TotpConfigurationInfo` VO'sudur. Setter'lar `private`'tır; değişim yalnızca davranış metotları üzerinden olur.

### Çocuk entity'ler

| Çocuk entity           | Rolü                                                                 |
| ---------------------- | -------------------------------------------------------------------- |
| `UserSession`          | Refresh token tabanlı oturum (cihaz + IP bilgisi ile).               |
| `UserOtpChallenge`     | SMS/E-posta OTP doğrulama denemesi (deneme/yeniden gönderim sayacı). |
| `UserIdentityProvider` | Dış IdP bağlantısı (Google/Meta/Apple/Keycloak).                     |
| `UserRole`             | Rol atama kaydı (opsiyonel `TenantId` ile).                          |

### internal ctor — factory zorunluluğu

Constructor `internal`'dır. Bu, `User`'ın **yalnızca aynı assembly içindeki factory** (`UserFactory`) veya domain service (`UserRegistrationService`) tarafından oluşturulabilmesini garanti eder. Böylece uniqueness gibi kurallar atlanamaz.

```csharp theme={null}
protected User() { }
internal User(FullName? fullName, Phone? phone = null, Email? email = null) : this()
{
    this.Id = GuidFactory.New();

    if (phone == null && email == null)
        throw new DomainException("Kullanıcı kaydı için en az bir telefon ya da e-posta tanımlanmalıdır");

    this.FullName = fullName;
    this.Phone    = phone;
    this.Email    = email;
    this.StatusId = UserStatus.Draft.Id;

    AddDomainEvent(new UserCreatedDomainEvent(Id, fullName, Phone, Email));
}
```

<Tip>
  `protected User()` parametresiz ctor EF Core'un materializasyonu için gereklidir; `internal` ctor ise iş kurallarını barındırır. Oluşturma anında `UserCreatedDomainEvent` kuyruğa eklenir — kayıt persist edildikten **sonra** dispatch edilir.
</Tip>

### UserStatus — durum makinesi

```csharp theme={null}
public class UserStatus : Enumeration
{
    public static UserStatus Draft     = new(0, nameof(Draft));     // kanal henüz doğrulanmadı
    public static UserStatus Active    = new(1, nameof(Active));    // en az bir kanal doğrulandı, login serbest
    public static UserStatus Suspended = new(2, nameof(Suspended)); // kullanıcı kendi dondurdu
    public static UserStatus Banned    = new(3, nameof(Banned));    // admin tamamen engelledi
    public static UserStatus Pending   = new(4, nameof(Pending));   // dış IdP geldi, admin onayı bekliyor
}
```

Durum, davranışı kapılayan hesaplanan property'lerle okunur:

```csharp theme={null}
public bool CanLogin      => Status == UserStatus.Active;
public bool CanRequestOtp => Status == UserStatus.Draft || Status == UserStatus.Active;
public bool IsBlocked     => Status == UserStatus.Banned || Status == UserStatus.Suspended;
```

### Davranış metotları

Aggregate'in tüm yetenekleri açık niyetli (intention-revealing) metotlardır. Birkaç örnek:

<AccordionGroup>
  <Accordion title="Oturum: StartSession / RefreshSession">
    ```csharp theme={null}
    public void StartSession(RefreshTokenInfo refreshToken, bool isTrustedDevice, DeviceInfo device, ClientIpInfo clientIp)
    {
        if (this.IsBlocked)
            throw new DomainException("Kullanıcı durumu oturum açmaya uygun değil.");

        var session = new UserSession(GuidFactory.New(), this, refreshToken, isTrustedDevice, device, clientIp);
        this.Sessions.Add(session);
    }
    ```

    `RefreshSession`, mevcut token'ı eşleştirip yeni token'a döndürür (rotation).
  </Accordion>

  <Accordion title="OTP: AddOtpChallenge / ResendOtp / VerifyOtp / VerifyChallenge">
    ```csharp theme={null}
    public void AddOtpChallenge(OtpCode code, OtpType type)
    {
        if (CanRequestOtp == false)
            throw new DomainException("Kullanıcı durumu giriş yapmaya uygun değil.");

        var validityMinutes = type == OtpType.Sms ? 1 : 2;
        var challenge = new UserOtpChallenge(this, GuidFactory.New(), code, type, DateTime.UtcNow.AddMinutes(validityMinutes));
        OtpChallenges.Add(challenge);

        AddDomainEvent(new UserOtpGeneratedDomainEvent(this, code, type, this.Phone, this.Email));
    }
    ```

    `VerifyOtp`/`VerifyChallenge` doğru kodda ilgili kanalı doğrular ve `UserPhoneNumberVerifiedDomainEvent` / `UserEmailVerifiedDomainEvent` yayar.
  </Accordion>

  <Accordion title="TOTP: ConfigureTotp / EnableTotp / DisableTotp / VerifyTotp">
    ```csharp theme={null}
    public bool IsTotpEnabled => Totp is { IsEnabled: true };

    public void ConfigureTotp(TotpSecret secret) => this.Totp = TotpConfigurationInfo.Configure(secret);
    public void EnableTotp()  { if (Totp == null) throw new DomainException("TOTP konfigüre edilmemiş"); Totp = Totp.Enable(); }
    public void VerifyTotp(long timeStep)
    {
        if (Totp is null) throw new DomainException("TOTP tanımlı değil");
        if (!Totp.CanAccept(timeStep)) throw new DomainException("Kod tekrar kullanılamaz");
        Totp = Totp.MarkUsed(timeStep); // replay koruması
    }
    ```

    `TotpConfigurationInfo` immutable bir VO'dur; her geçiş yeni bir örnek döndürür.
  </Accordion>

  <Accordion title="RBAC: AssignRole / RevokeRole / InvalidateTokens">
    ```csharp theme={null}
    public void AssignRole(int roleId, Guid? tenantId, Guid assignedBy)
    {
        if (!Enumeration.InRange<Role>(roleId))
            throw new DomainException($"Geçersiz rol: {roleId}");

        if (UserRoles.Any(r => r.RoleId == roleId && r.TenantId == tenantId && !r.IsDeleted()))
            return; // idempotent

        UserRoles.Add(new UserRole(GuidFactory.New(), Id, roleId, tenantId, assignedBy));
        AddDomainEvent(new UserRoleAssignedDomainEvent(Id, roleId, tenantId, assignedBy));
    }
    ```

    `InvalidateTokens()`, `TokenVersion`'ı artırarak mevcut tüm JWT'leri geçersiz kılar.
  </Accordion>

  <Accordion title="Dış IdP: AddOrUpdateExternalProvider / ActivateOnExternalLogin">
    ```csharp theme={null}
    public void AddOrUpdateExternalProvider(AuthProviderType providerType, string providerUserId, string? displayName)
    {
        var existing = IdentityProviders.FirstOrDefault(x =>
            x.ProviderType == providerType && x.ProviderUserId == providerUserId);

        if (existing is not null) { existing.UpdateDisplayNameIfProvided(displayName); return; } // idempotent

        IdentityProviders.Add(new UserIdentityProvider(GuidFactory.New(), Id, providerType, providerUserId, displayName));
    }
    ```

    `ActivateOnExternalLogin()`, başarılı dış login'de `Draft`/`Pending` kullanıcıyı `Active`'e geçirir; `Banned`/`Suspended`'e dokunmaz, `Active` ise idempotent çıkar.
  </Accordion>
</AccordionGroup>

<Warning>
  Davranış metotlarında tekrar eden iki desen dikkat çeker: **idempotency** (aynı işlem tekrar çağrılırsa sessizce geçer) ve **guard + DomainException** (kural ihlalinde anlamlı hata). Yeni metot yazarken her ikisini de göz önünde bulundurun.
</Warning>

## Citizen aggregate — User'a paralel

`Citizen`, vatandaş portalının kullanıcısıdır ve `User` ile neredeyse birebir aynı yapıya sahiptir (`FullName/Phone/Email`, OTP/TOTP/oturum, dış IdP, `CitizenStatus`). Ama **ayrı bir aggregate'tir** — farklı tablolar, farklı Keycloak realm'i ve farklı iş kuralları taşır. Ek olarak `CitizenProfileImageInfo` (profil fotoğrafı) içerir.

```csharp theme={null}
public class Citizen : Entity, IAggregateRoot
{
    public FullName? FullName { get; private set; }
    public CitizenProfileImageInfo? ProfileImage { get; private set; }
    public virtual CitizenStatus Status => CitizenStatus.FromValue<CitizenStatus>(StatusId);
    public virtual ICollection<CitizenSession> Sessions { get; private set; }
    // ... User ile paralel ...
}
```

<Info>
  İki aggregate'i kasıtlı olarak ayrı tutmak, personel ve vatandaş kimlik akışlarının bağımsız evrilmesini sağlar. Ortak ihtiyaçlar value object düzeyinde (`FullName`, `Phone`...) paylaşılır.
</Info>

## Diğer aggregate'ler — özet

<AccordionGroup>
  <Accordion title="Organization (+ Branch, StaffMember, StaffPermission)">
    Kurum/tenant. `Branch` (şube) ve `StaffMember` (personel) çocuk entity'leri; `StaffPermission` izin kayıtları. `AddBranch(...)` gibi root metotları ile yönetilir.
  </Accordion>

  <Accordion title="RolePermission">
    RBAC rol–izin eşlemesi. Hangi rolün hangi `PermissionCode`'a sahip olduğunu tutar.
  </Accordion>

  <Accordion title="BagisBasvuru / BagisBasvuruPlan">
    Bağış başvurusu bir **state machine**'dir: `OnBasvuru → Incelemede → ...` Her geçiş `BagisBasvuruStatusHistory` kaydı ve domain event üretir (`IncelemeyeAl`, `Onayla`, `Reddet`, `IptalEt`). `BagisBasvuruPlan` başvuru şablonudur.
  </Accordion>

  <Accordion title="EtkinlikBasvuru / EtkinlikBasvuruPlan">
    Etkinlik başvurusu; bağış ile paralel state machine. Plan kontenjanı dolunca `EtkinlikBasvuruPlanKontenjanDolduDomainEvent` yayılır, iptalde kontenjan geri verilir.
  </Accordion>

  <Accordion title="SupportTicket">
    Destek talebi. Çocuklar: `SupportTicketComment`, `SupportTicketAttachment`, `SupportTicketStatusHistory`. Atama ve durum değişiminde event yayar.
  </Accordion>

  <Accordion title="AdminNotification, LegalDocument, Center, SiteSettings, District">
    `AdminNotification`: SSE ile yayınlanan admin bildirimi. `LegalDocument`: yasal metin + `LegalDocumentVersion`. `Center`: merkez lokasyonu. `SiteSettings`: tekil site ayarı. `District`: `int` kimlikli sabit ilçe referans verisi.
  </Accordion>
</AccordionGroup>

### Tüm aggregate tablosu

| Aggregate             | Çocuk entity'ler / VO'lar                                                                            | Enumeration'lar                                                  |
| --------------------- | ---------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- |
| `User`                | `UserSession`, `UserOtpChallenge`, `UserIdentityProvider`, `UserRole`                                | `UserStatus`                                                     |
| `Citizen`             | `CitizenSession`, `CitizenOtpChallenge`, `CitizenIdentityProvider`, `CitizenProfileImageInfo`        | `CitizenStatus`                                                  |
| `Organization`        | `Branch`, `StaffMember`, `StaffPermission`                                                           | `OrganizationStatus`, `BranchStatus`, `StaffStatus`, `StaffRole` |
| `RolePermission`      | `Permission`, `PermissionCodes`                                                                      | `Role`                                                           |
| `Announcement`        | `AnnouncementImageInfo`                                                                              | —                                                                |
| `BagisBasvuru`        | `BagisBasvuruStatusHistory`, `BagisOdemeInfo`                                                        | `BagisBasvuruStatus`, `OdemeStatus`                              |
| `BagisBasvuruPlan`    | `BagisPlanImageInfo`                                                                                 | `BagisBasvuruPlanType`                                           |
| `EtkinlikBasvuru`     | `EtkinlikBasvuruStatusHistory`                                                                       | `EtkinlikBasvuruStatus`                                          |
| `EtkinlikBasvuruPlan` | `EtkinlikPlanImageInfo`                                                                              | `EtkinlikBasvuruPlanType`                                        |
| `SupportTicket`       | `SupportTicketComment`, `SupportTicketAttachment`, `SupportTicketStatusHistory`, `DiagnosticContext` | `SupportTicketStatus/Priority/Type/Source/ReporterType`          |
| `AdminNotification`   | `AdminNotificationRead`                                                                              | `NotificationCategory`, `NotificationSeverity`                   |
| `LegalDocument`       | `LegalDocumentVersion`                                                                               | `LegalDocumentType`                                              |
| `Center`              | —                                                                                                    | —                                                                |
| `SiteSettings`        | —                                                                                                    | —                                                                |
| `District`            | —                                                                                                    | `City`                                                           |

## Sonraki adımlar

<CardGroup cols={2}>
  <Card title="Value Object'ler" icon="gem" href="/domain/value-objects">
    Aggregate alanlarında kullanılan VO kataloğu.
  </Card>

  <Card title="Domain Event'ler" icon="bolt" href="/domain/domain-events">
    `AddDomainEvent` ile yayılan olayların dispatch akışı.
  </Card>

  <Card title="Factory ve Servisler" icon="industry" href="/domain/factories-services">
    `internal` ctor'lu aggregate'ler nasıl oluşturulur.
  </Card>

  <Card title="SharedKernel" icon="layer-group" href="/domain/shared-kernel">
    `Entity`, `Enumeration` ve guard altyapısı.
  </Card>
</CardGroup>
