> ## 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.

# Vatandaş Kaydı

> SignUpUserCommand → OTP SMS → VerifyOtp ile aktivasyon. Domain service, factory, domain event ve SMS gönderiminin uçtan uca akışı.

Vatandaş kaydı, telefon numarasıyla **OTP tabanlı** bir self-registration akışıdır. Kayıt isteği bir `User` aggregate'ı `Draft` durumunda oluşturur, bir OTP üretir ve SMS ile gönderir; kullanıcı kodu doğrulayınca telefon "verified" işaretlenir. Bu akış tamamen lokal (`User` aggregate) çalışır — Keycloak'a gerek yoktur.

<Info>
  İlgili dosyalar:
  `src/DiyanetCleanArchitecture.API/Controllers/AuthController.cs` ·
  `Application/Features/Authentication/Website/Commands/SignUpUser/*` ·
  `Domain/Services/UserRegistrationService.cs` ·
  `Domain/AggregatesModel/UserAggregate/User.cs` ·
  `Application/DomainEventHandlers/UserOtpGenerated/SendOtpSmsDomainEventHandler.cs`
</Info>

## Sequence — kayıt (`sign-up`)

```mermaid theme={null}
sequenceDiagram
    autonumber
    participant U as Vatandaş
    participant W as Website SPA
    participant C as AuthController
    participant H as SignUpUserCommandHandler
    participant RS as UserRegistrationService
    participant AG as User (aggregate)
    participant UoW as IUnitOfWork
    participant MED as MediatR (dispatch)
    participant SMS as SendOtpSmsDomainEventHandler → NetGSM

    U->>W: Ad soyad + telefon
    W->>C: POST /api/auth/sign-up
    C->>H: _mediator.Send(SignUpUserCommand)
    H->>RS: RegisterNewUserAsync(fullName, phone)
    RS->>RS: UserByPhoneNumberSpecification ile uniqueness kontrol
    RS-->>H: new User(...)  (Status = Draft, UserCreatedDomainEvent)
    H->>AG: AddOtpChallenge(OtpCode.Generate(), OtpType.Sms)
    Note over AG: UserOtpGeneratedDomainEvent biriker
    H->>UoW: SaveEntitiesAsync(ct)
    UoW->>MED: SaveChanges → domain event'leri dispatch
    MED->>SMS: Handle(UserOtpGeneratedDomainEvent)
    SMS->>SMS: phone != null && type == Sms ? gönder
    SMS-->>U: SMS (4 haneli kod)
    H->>H: _authService.CreateOtpChallengeToken(user, Sms, challenge.Id)
    H-->>C: ResponseWrapper<OtpChallengeDto> { ChallengeToken }
    C-->>W: 201 Created + { challengeToken }
```

## Adım adım

<Steps>
  <Step title="Controller — ince giriş noktası">
    `AuthController.SignUp` yalnızca komutu MediatR'a iletir. Başarıda `201 Created` döner.

    ```csharp theme={null}
    [AllowAnonymous]
    [HttpPost("sign-up")]
    public async Task<IActionResult> SignUp([FromBody] SignUpUserCommand command)
    {
        var result = await _mediator.Send(command);
        return result.IsSuccess ? Created("", result.Response) : BadRequest(result.Response);
    }
    ```

    Command şekli (`SignUpUserCommand`): `Phone` (zorunlu), `FullName?`, `Consents?`. Validator `Phone` için `MustBeValidMobilePhone()`, `FullName` için boş olamaz + max 100 karakter kuralını uygular.
  </Step>

  <Step title="Domain service — uniqueness + factory">
    Handler önce `UserRegistrationService.RegisterNewUserAsync` çağırır. Bu servis telefon/e-posta **global benzersizliğini** spec ile kontrol eder ve geçerse `new User(...)` döner:

    ```csharp theme={null}
    var existingUserByPhone = await _userRepository
        .FirstOrDefaultAsync(new UserByPhoneNumberSpecification(phone), cancellationToken);

    if (existingUserByPhone is not null)
    {
        if (existingUserByPhone.IsPhoneNumberVerified == false)
            throw new DomainException("... zaten mevcut fakat üyelik onayı yapılmamış. Lütfen giriş yapınız");
        throw new DomainException("Bu telefon numarasına sahip bir kullanıcı zaten mevcut");
    }
    ```

    <Note>
      "Doğrulanmamış ama var olan" telefon ayrı mesajla ele alınır: kullanıcı kayıt yerine **giriş** akışına yönlendirilir. `DomainException` global exception handler tarafından `400`'e çevrilir.
    </Note>
  </Step>

  <Step title="Aggregate — Draft kullanıcı + OTP challenge">
    `User`'ın `internal` ctor'u durumu `Draft` yapar ve `UserCreatedDomainEvent` biriktirir. Handler ardından OTP üretip aggregate'e ekler:

    ```csharp theme={null}
    var otpCode = OtpCode.Generate();              // 4 hane, 1000-9999
    var otpType = phone != null ? OtpType.Sms : OtpType.Email;
    user.AddOtpChallenge(otpCode, otpType);
    ```

    `User.AddOtpChallenge` içeride bir `UserOtpChallenge` (SMS için 1 dk geçerli) oluşturur ve `UserOtpGeneratedDomainEvent` biriktirir:

    ```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));
    }
    ```

    `CanRequestOtp`, `Draft` veya `Active` durumlar için `true`'dur — yeni kayıt `Draft` olduğundan geçer.
  </Step>

  <Step title="Kaydet — event dispatch + SMS">
    ```csharp theme={null}
    await _repository.AddAsync(user);
    bool result = await _unitOfWork.SaveEntitiesAsync(cancellationToken);
    if (!result)
        throw new ApplicationException("Kayıt işlemi sırasında bir sorun oluştu.");
    ```

    `SaveEntitiesAsync`, SaveChanges'ten sonra biriken domain event'leri dispatch eder. `UserOtpGeneratedDomainEvent`'i `SendOtpSmsDomainEventHandler` dinler:

    ```csharp theme={null}
    public sealed class SendOtpSmsDomainEventHandler
        : INotificationHandler<UserOtpGeneratedDomainEvent>,
          INotificationHandler<UserOtpResentDomainEvent>
    {
        private readonly IOtpSmsService _otpSmsService;

        private async Task SendIfSmsAsync(Phone? phone, OtpCode code, OtpType type)
        {
            if (phone is null || type != OtpType.Sms) return;   // guard
            await _otpSmsService.SendAsync(code, phone);        // NetGSM REST v2
        }
    }
    ```

    `IOtpSmsService` implementasyonu (`OtpSmsService`) NetGSM OTP endpoint'ine `Phone.ToNetGsmFormat()` ile gönderir. Bkz. [SMS Servisi](/services/sms).
  </Step>

  <Step title="Challenge token üret + dön">
    Handler aktif challenge'ı bulur ve sonraki adımda kullanılacak **kısa ömürlü challenge token**'ı üretir:

    ```csharp theme={null}
    var challenge = user?.GetActiveOtpChallenge(otpType);
    if (challenge is null) throw new DomainException("Aktif OTP doğrulama kodu bulunamadı");

    var challengeToken = _authService.CreateOtpChallengeToken(user, otpType, challenge.Id);

    return ResponseWrapper<OtpChallengeDto>.Success(new OtpChallengeDto
    {
        ChallengeToken = challengeToken
    });
    ```

    `ChallengeToken`, `OTPChallenge` JWT şemasıyla imzalanır ve `userId` + `challengeId`'yi taşır. SPA bunu saklar; SMS kodu girilince `verify-otp` çağrısında `Authorization` header'ında gönderir.
  </Step>

  <Step title="Aktivasyon — VerifyOtp">
    Kullanıcı SMS kodunu girer. SPA, challenge token'ı bearer olarak taşıyan `verify-otp` çağrısı yapar:

    ```csharp theme={null}
    [Authorize(AuthenticationSchemes = AuthenticationSchemeTypes.OTPChallengeAuthenticationScheme)]
    [HttpPost("verify-otp")]
    public async Task<IActionResult> VerifyOtp([FromBody] VerifyOtpCommand command, CancellationToken ct)
    {
        var result = await _mediator.Send(command, ct);
        if (result.IsSuccess == false) return BadRequest(result);
        Response.AppendRefreshToken(result.Response.RefreshToken);   // HttpOnly cookie
        return Ok(new VerifyOtpResponse(result.Response.AccessToken, result.Response.User));
    }
    ```

    `VerifyOtpCommandHandler`, `userId`/`challengeId`'yi `IOtpChallengeContext` üzerinden (challenge token'dan) okur, kodu doğrular, telefonu "verified" işaretler ve oturum başlatır:

    ```csharp theme={null}
    if (!user.VerifyChallenge(challengeId, request.Code))
        throw new ApplicationException("Geçersiz veya süresi dolmuş kod");
    // ...
    user.StartSession(refreshTokenInfo, request.IsTrustedDevice, deviceInfo, clientInfo);
    ```

    `User.VerifyChallenge` başarılıysa `IsPhoneNumberVerified = true` yapar ve `UserPhoneNumberVerifiedDomainEvent` yayar. Doğrulama detayları için [OTP / TOTP Akışı](/playbooks/otp-flow).
  </Step>
</Steps>

## Hata senaryoları

| Durum                               | Nerede                       | Sonuç                                                                                        |
| ----------------------------------- | ---------------------------- | -------------------------------------------------------------------------------------------- |
| Telefon zaten kayıtlı (doğrulanmış) | `UserRegistrationService`    | `DomainException` → `400`                                                                    |
| Telefon var ama doğrulanmamış       | `UserRegistrationService`    | `DomainException` ("giriş yapınız") → `400`                                                  |
| Geçersiz telefon formatı            | `SignUpUserCommandValidator` | `ValidationException` → `422` + errors                                                       |
| SMS gönderilemedi (NetGSM)          | `OtpSmsService`              | `SmsServiceException` → `503` (kayıt yine de oluşmuştur; `ResendOtp` ile tekrar denenebilir) |
| Yanlış / süresi dolmuş OTP kodu     | `VerifyOtpCommandHandler`    | `ApplicationException` → `400`                                                               |

<Warning>
  OTP kodu DB'de `UserOtpChallenge.Code` (bir `OtpCode` value object) olarak tutulur ve SMS sadece bir kez gönderilir. Kullanıcıya kod ulaşmazsa **yeni kayıt değil**, `resend-otp` çağrılmalıdır (cooldown 60 sn, maksimum 3 yeniden gönderim — bkz. OTP playbook'u).
</Warning>

## İlgili

<CardGroup cols={2}>
  <Card title="OTP / TOTP Akışı" icon="key" href="/playbooks/otp-flow">
    VerifyOtp, ResendOtp ve TOTP detayları.
  </Card>

  <Card title="SMS Servisi" icon="comment-sms" href="/services/sms">
    NetGSM entegrasyonu, test numaraları, encoding.
  </Card>

  <Card title="User Aggregate" icon="user" href="/domain/aggregates">
    Durumlar, çocuk entity'ler, metodlar.
  </Card>

  <Card title="Domain Event'ler" icon="diagram-project" href="/events/domain-events">
    AddDomainEvent → dispatch mekaniği.
  </Card>
</CardGroup>
