# Geçmiş veri — implementasyon planı

> **Ajan işçiler için:** Bu plan görev görev uygulanır. Adımlar `- [ ]` kutucuklarıyla izlenir. Her görev kendi test döngüsünü taşır ve bağımsız olarak commit edilir.

**Kaynak spec:** `docs/specs/gecmis-veri-spec.md` · **Ticket:** EGT-108

**Hedef:** İzmir Akademi'nin devralınan 2.381 satırlık eğitim geçmişini sisteme sokmak: backoffice'te belge türü eşleme ve firma bağlama, müşteri panelinde doğrulama, doğrulanınca kişi + sertifika yazımı.

**Yaklaşım:** Excel satırları **tenant'sız kendi tablolarında** durur ve hiç tüketilmez; kişi ancak müşteri onayında `customer_users`'a doğar. Kişi açma ve sertifika yazma **mevcut yollardan** yapılır (`AddPortalPerson`, `CertificateRecord`) — ikinci bir sessiz giriş yolu açılmaz.

**Stack:** Laravel 12 · Filament 5 (backoffice) · Inertia + Vue 3 (müşteri paneli) · PostgreSQL 18 · Pest

---

## Global kısıtlar

Her görevin gereksinimleri bunları **kapsar**; ayrıca tekrar edilmez.

- **Dil:** kod/tablo/kolon/route/izin/enum/yorum **İngilizce**; kullanıcıya görünen her metin **Türkçe**, `lang/tr/` üzerinden.
- **Kod yerleşimi** (`docs/arch/kod-yerlesimi.md`): iş kuralı `app/Domain/`, orkestrasyon `app/Actions/`, okuma `app/Queries/`, yardımcı `app/Support/<Konu>/`. Filament sayfası sorgu yazmaz.
- **Test grubu:** backoffice tarafı `->group('backoffice')`, müşteri tarafı `->group('musteri')`, tenant/izin davranışı `->group('omurga')`.
- **Testte çıplak `now()` yok** — `FrozenClock` (AD-26).
- **Migration'sız DDL yok** (kırmızı çizgi 6). ⛔ **Veri hiçbir zaman migration'a gömülmez** (spec §5.5).
- **Hassas kayıt hard delete edilmez** (AD-11); satır silinmez, durumu değişir.
- **Kadans:** kalem üzerinde `--filter`; commit öncesi `pint --dirty` → `composer analyse` → ilgili grup. Tam koşu yalnız ticket kapanışında.
- **Commit:** `#EGT-108 In Progress work <süre>` ilk satırda, ikinci satırda conventional commit.

**Doğrulanmış mevcut arayüzler** (hepsi okundu):

| Ne | Nerede | İmza / şekil |
|---|---|---|
| Kişi açma | `app/Actions/Customer/AddPortalPerson.php` | `__invoke(CustomerUser $actor, NewPortalPerson $input): CustomerUser` |
| Kişi verisi | `app/Data/Customer/NewPortalPerson.php` | `name, email, phone, nationalId, hireDate, unitIds[], locationIds[], staffKind, takesTrainings` |
| Personel türü | `app/Models/CustomerUser.php` | `KIND_OFFICE='office'`, `KIND_FIELD='field'`, `KINDS` |
| Sertifika | `app/Models/CertificateRecord.php` | `SOURCE_TRAINING='training'`, `SOURCE_DECLARED='declared'` · kolonlar: `tenant_id, training_enrolment_id, certificate_no (NOT NULL), scope_name, covered_scopes, issued_on (NOT NULL), valid_until, training_definition_id, source (NOT NULL), declared_by_customer_user_id, customer_user_id` |
| Yenileme periyodu | `training_definitions.renewal_months` | tamsayı ay |
| Erişim logu | `app/Support/Audit/AccessLogger.php` | `recordQuery(string $kind, array $context): AuditAccess` |
| Maskeleme | `app/Support/Masking/Masker.php` | `nationalId()`, `phone()`, `email()`, `searchTerm()` |
| Türkçe katlama | `app/Support/Text/TurkishFold.php` | arama için |
| Backoffice sayfa deseni | `app/Filament/Backoffice/Pages/AccessLog.php` | `canAccess()`, `mount()` içinde `abort_unless`, `getNavigationGroup()` |
| Backoffice izin listesi | `config/authorization.php` → `backoffice` bloğu | |
| İzin etiketi zorunluluğu | `tests/Arch/PermissionLabelTest.php` (EGT-139) | her kaynak ve izin **tr + en** ad taşımak zorunda |
| Portal personel ekranları | `routes/panel.php` → `/personel*`, `app/Http/Controllers/Customer/PortalPeopleController.php` | |
| Uyum sorgusu | `app/Queries/Office/StaffCompliance.php` | sertifikaları `customer_user_id` üzerinden okur (EGT-136) — legacy sertifikalar kendiliğinden girer |

---

## Dosya haritası

**Yeni:**

| Dosya | Sorumluluk |
|---|---|
| `database/migrations/…_the_academy_we_took_over.php` | üç tablo |
| `app/Models/LegacyTrainingRecord.php` | satır |
| `app/Models/LegacyDocumentType.php` | 36 ham değerin eşlemesi |
| `app/Models/LegacyRecordRejection.php` | ret kaydı |
| `app/Domain/Legacy/LegacyState.php` | `unmatched · draft_linked · confirmed · rejected` |
| `app/Domain/Legacy/RecordFingerprint.php` | TCKN hash + ham tür + tarih |
| `app/Queries/Backoffice/LegacyRecords.php` | ana listenin okuması (arama, süzme, toplu TCKN) |
| `app/Actions/Backoffice/MapLegacyDocumentType.php` | tür → eğitim tanımı + ilk/yenileme |
| `app/Actions/Backoffice/LinkLegacyRecords.php` | satırları firmaya taslak bağla |
| `app/Actions/Backoffice/UnlinkLegacyRecords.php` | bağı kaldır |
| `app/Queries/Customer/MyLegacyPeople.php` | müşteri ekranının okuması, **kişi bazında gruplu** |
| `app/Actions/Customer/ConfirmLegacyPeople.php` | kişi + sertifika + davet |
| `app/Actions/Customer/RejectLegacyRecord.php` | ret |
| `app/Filament/Backoffice/Pages/LegacyDocumentTypes.php` | eşleme ekranı |
| `app/Filament/Backoffice/Pages/LegacyRecords.php` | ana liste |
| `resources/js/pages/Portal/LegacyPeople.vue` | müşteri ekranı |
| `lang/tr/legacy.php`, `lang/en/legacy.php` | metinler |

**Dokunulan:**

| Dosya | Ne |
|---|---|
| `app/Models/CertificateRecord.php` | `SOURCE_LEGACY` sabiti + `isLegacy()` |
| `config/authorization.php` | `legacy_data.view`, `legacy_data.update` |
| `lang/tr/permission.php`, `lang/en/permission.php` | kaynak ve izin adları (arch testi zorunlu kılıyor) |
| `routes/panel.php` | müşteri ekranının üç adresi |
| `resources/js/pages/Portal/People*.vue`, ilgili sorgular | **rozet** (§Görev 7) |

---

## Görev 1 — Şema ve modeller ✅

**Neden ilk:** her şey bu üç tabloya dayanıyor ve hiçbiri tenant'lı değil; yanlış kurulursa geri dönüşü pahalı.

- [x] Migration: `legacy_training_records` — spec §5.1'deki alanlar. `national_id` şifreli, `national_id_hash` **indeksli**; `fingerprint` **unique**; `state` varsayılan `unmatched`; `customer_id`, `customer_user_id`, `certificate_record_id` nullable FK (`nullOnDelete` değil — **restrict**, çünkü satır izini kaybetmemeli).
- [x] Migration: `legacy_document_types` — `raw_name` unique, `training_definition_id` ve `course_kind` nullable.
- [x] Migration: `legacy_record_rejections` — satır + firma + zaman + kim.
- [x] ⛔ **Satır güvenliği YOK ve bu bilinçli**: tablolar tenant'sız. Migration bunu yorumda gerekçesiyle yazar. ⚠ Arch istisnası GEREKMİYOR — tenant taraması `BelongsToTenant` trait'inden türüyor, trait kullanmayan model listeye hiç girmiyor (`tests/Arch/AuthorizationTest.php:150` okundu).
- [x] Modeller: `LegacyTrainingRecord`, `LegacyDocumentType`, `LegacyRecordRejection` — `Auditable` (AD-06).
- [x] `LegacyState` ve `RecordFingerprint` `app/Domain/Legacy/` altında, framework'süz.
- [x] `CertificateRecord::SOURCE_LEGACY = 'legacy'` + `isLegacy()`.
- [x] Test (`omurga`): satır yazılır; aynı parmak izi ikinci kez yazılamaz; `national_id` veritabanında **düz metin değildir**; hash ile aranabilir.

## Görev 2 — Yetki ✅

- [x] `config/authorization.php` → backoffice: `legacy_data.view`, `legacy_data.update`.
- [x] `lang/tr|en/permission.php`: kaynak adı («Geçmiş veri» / “Legacy data”) + iki iznin ad ve açıklaması. ⚠ `PermissionLabelTest` (EGT-139) eksik adı CI'da kırar.
- [x] `PermissionMatrix`'e satır: `legacy_data` → view/update.
- [x] ⛔ Müşteri tarafında **yeni izin yok**: mevcut personel yönetimi yetkisi (spec §9.1).
- [ ] Test (`omurga`): izinsiz backoffice kullanıcısı iki sayfayı da göremez; izinli görür. ⚠ Sayfalar Görev 3–4'te doğuyor; test onlarla birlikte yazılır.

## Görev 3 — Belge türü eşleme ekranı ✅

- [x] `app/Filament/Backoffice/Resources/LegacyDocumentTypes/` (Page değil **Resource** — projedeki desen bu, ve politika zorunluluğunu da o taşıyor) — 36 satır: ham ad · **kaç satırı etkilediği** · eğitim tanımı seçimi · ilk/yenileme seçimi.
- [x] `MapLegacyDocumentType` aksiyonu; audit yazar.
- [x] ⛔ Hiçbir alan önceden doldurulmaz; «Y-» öneki yalnız ham adın içinde görünür (spec §4.2).
- [x] Test (`backoffice`): eşleme yazılır ve loglanır; eşlemesi olmayan tür sertifikaya dönüşemez (Görev 6'nın reddi).

## Görev 4 — Ana liste ekranı ✅

- [x] `LegacyRecords` sorgusu: sayfalama **veritabanında**; arama TCKN (hash) / ad / firma adı (`TurkishFold`); süzgeçler durum, firması bilinmeyenler, belge türü, tarih aralığı.
- [x] ⛔ **Toplu TCKN araması**: çok satırlı bir kutuya yapıştırılan listeyi tek seferde arar (spec §6.2).
- [x] Ekran: ad · TCKN **maskeli** · telefon **maskeli** · belge türü · tarih · Şirket (ham) · **Yer (ham)** · durum · bağlı firma.
- [x] ⚠ Aynı TCKN başka firmada da görünüyorsa satırda **bilgi** olarak yazar — öneri değil.
- [x] `LinkLegacyRecords` / `UnlinkLegacyRecords`: yalnız **mevcut** müşteriler arasından seçim; bu ekran firma açmaz. Bağ, müşteri onaylamadan önce kaldırılabilir.
- [ ] ⛔ Her arama `AccessLogger::recordQuery('legacy_records_query', …)` yazar (AD-21).
- [x] Test (`backoffice`): arama loglanır; TCKN maskeli döner; bağlama satırı `draft_linked` yapar; bağ kaldırma `unmatched`'e döndürür; **hiçbir satır silinmez**.

## Görev 5 — Müşteri ekranı ✅

- [x] `MyLegacyPeople` sorgusu: **kişi bazında gruplu** (bir kişi, altında belgeleri).
- [x] `resources/js/pages/Portal/LegacyPeople.vue`: kişi başına saha/ofis · «eğitim alacak» **seçili** · e-posta (**zorunlu**) · lokasyon · eksik tarih · geçersiz kimlik düzeltmesi · gerçek sertifika numarası (opsiyonel) · **Reddet**.
- [x] ⛔ **Evrak yükleme bu ekranda YOK** *(Sets, 14.08.2026)*. Kişi henüz doğmamışken evrak yüklenemez; evrak, kişi doğup davet gittikten sonra **mevcut** yoldan gelir (`/personel/{person}/evrak` → `UploadParticipantDocument`). Bu ekran yalnız kişiyi doğurmak için gerekenleri sorar.
- [x] ⚠ TCKN ve telefon **maskeli**; telefon doğrulama ipucu olarak durur.
- [x] `RejectLegacyRecord`: satırı `unmatched`'e döndürür, `legacy_record_rejections`'a yazar (spec §7.1).
- [x] Test (`musteri`): başka firmanın satırı görünmez; e-postasız kaydedilemez; tarihi eksik kişi kaydedilemez; geçersiz kimlik düzeltilmeden kaydedilemez; ret satırı havuza döndürür ve kaydı bırakır.

## Görev 6 — Kaydetme: kişi + sertifika + davet ✅

- [x] `ConfirmLegacyPeople`:
  - Aynı TCKN firmada kayıtlıysa **mevcut kişiye belge eklenir**; kişinin adı/telefonu/e-postası **değişmez** (spec §8.1).
  - Kişi yoksa `AddPortalPerson` ile açılır — ikinci bir yazma yolu yok.
  - Sertifika: `source = legacy` · `issued_on` = **bitiş günü** (`trained_on_2` varsa o, yoksa `trained_on`) · `valid_until` = `issued_on` + eğitim tanımının `renewal_months`'u · `certificate_no` = üretilen iç numara · `scope_name` = eşlenen tanımın adı (donar).
  - Satır `confirmed`, `customer_user_id` ve `certificate_record_id` yazılır.
  - Davet gider (ofis → firma paneli, saha → katılımcı ekranları).
- [x] ⛔ Tamamı **tek transaction**; audit aynı transaction içinde (kırmızı çizgi 13).
- [x] Test (`musteri`): mevcut kişiye belge eklenir, ikinci kişi doğmaz · geçerlilik doğru hesaplanır · eşlemesiz tür reddedilir · iki günlük eğitimde `issued_on` **ikinci gün** olur · davet gönderilir.
- [x] Test (`planlama`): doğrulanan kişi **uyum sorgusunda görünür** ve sertifikası dolmuşsa «süresi doldu» der (EGT-136'nın yolundan geçer).

## Görev 7 — «Bakanlıktan doğrulayın» rozeti ✅

- [x] `source = legacy` olan her sertifika, göründüğü **her yerde** rozet taşır: kişi kartı, uyum sorgusu satırı, müşterinin personel listesi.
- [x] Metin `lang/tr/legacy.php` içinde; koda gömülmez.
- [x] Test (`musteri` + `planlama`): rozet üç ekranın üçünde de görünür.

## Görev 8 — Verinin yüklenmesi *(kod değil, denetimli işlem)*

⛔ **Betik yazılmayacak** (spec §5.5). Sıra:

- [ ] Dosya okunur, profil raporlanır: satır · benzersiz TCKN · geçersiz kimlik · firmalı/firmasız · tarihsiz · distinct belge türü.
- [ ] **Sets rapora bakar.**
- [ ] Satırlar yazılır; yazımdan sonra sayılar **yeniden okunarak** doğrulanır (yazılan = raporlanan).
- [ ] Distinct belge türleri `legacy_document_types`'a eşlemesiz yazılır.
- [ ] ⚠ 10 yıllık dosya geldiğinde: önce **fark raporu** (kaç aynı, kaç yeni, kaç çelişkili), sonra yazma.

**Sıra kararı:** yükleme **en sona** bırakılır — ekranlar fabrikayla üretilmiş veriyle yazılıp test edilir. Sets isterse şema kurulduktan sonra dev'e erken yükleme yapılabilir; production'a yükleme her hâlükârda ekranlar hazır olduktan sonradır.

---

## Kapsam dışı

- **İş zekâsı ekranı** (spec §10) — ikinci aşama. Bu plan yalnız `state` alanını doğru tutar.
- **Firma açma** — geçmiş veri firma açmaz (spec §3.1).
- **Yükleme ekranı / içe aktarma komutu** (spec §5.5).
- **`Tarih2`'nin başka bir amaçla kullanımı** — yalnız bitiş günü olarak okunur.
- **Sertifika kaydına dosya iliştirme** — bugün yok ve bu iş için gerekmiyor. Personel evrakı zaten kişiye yükleniyor; sertifikanın kendisine dosya bağlamak istenirse ayrı bir ticket, ve yalnız geçmiş veri için değil bütün sertifikalar için.
