Proje: Okul Platform · Hub: Okul Platform — Incidents

tesekkurler/{id} multiple-form 24h dedupe eksikti

Semptom

/tesekkurler/{offer_id} sayfasında bottom sheet üzerinden önerilen okullara form gönderildikten sonra başarı popup’ı kapatıldığında, kullanıcı aynı butona tekrar tıklayıp aynı okullara birden fazla offer yaratabiliyordu. Aynı okula 24 saat içinde duplicate SchoolOffer kayıtları oluşuyordu.

Root Cause

İki katmanda da koruma yoktu:

  1. Backend: app/Http/Controllers/Ajax/FormController.php::submitMultipleForm her school_id için körü körüne SchoolOfferService::create() çağırıyordu. Service’in checkOfferCount kontrolü sadece 24 saatte toplam 10 form sınırına bakıyor, aynı okula tekrar göndermeyi engellemiyor.
  2. Frontend: sendForm() (desktop, frontend/offer/success.blade.php) ve sendPlusLeadForm() (mobile, mobile/app.js:6913) AJAX başarı sonrası okul card’larını DOM’dan kaldırmıyor, butonu disable etmiyor, çift tıklama koruması yok.

İlginç: SchoolSuggestService ilk render’da userOfferSchoolIds() ile son 1 ayda gönderilen okulları zaten hariç tutuyor. Yani sayfa yenilense problem çıkmaz; sorun yalnızca aynı sayfa açıkken tekrar submit’e izin verilmesinden geliyordu.

Çözüm

BE (Ajax/FormController::submitMultipleForm): Çağrı başında SchoolOffer::where(user_id, school_id, created_at >= now-24h) ile mevcut offer’ları çek, gelen school_ids’i bunlardan filtrele. Tamamı duplicate ise status: false + bilgi mesajı dön; aksi halde sadece kalan okullar için create(). Yanıta submittedSchoolIds ve skippedSchoolIds eklendi.

FE:

  • Çift tıklama guard’ı (_isSendingForm / _isSendingPlusLeadForm).
  • Başarı sonrası removeSubmittedSchoolCards(submittedIds) ile gönderilen card’lar DOM’dan silinir; schools array sıfırlanır.
  • Hiç okul kalmadıysa send butonları “Tüm okullara gönderildi” yazısıyla kilitlenir.
  • Tamamı duplicate döndüğünde de aynı kilit + bilgilendirme popup’ı.

Önemli Notlar

  • submitMultipleOffers (profiling flow) zaten dedupe yapıyordu — bkz [[2026-04-27-school-offer-business-rules]]. Bu bug onun kuzeni submitMultipleForm endpoint’inin aynı kuralı atladığını gösterdi.
  • Dedupe user_id’ye göre yapıldı, e-mail’e değil — çünkü tesekkurler/{id} her zaman kayıtlı bir SchoolOffer.user_id üzerinden çalışır (offer ya auth kullanıcının ya da form sırasında createUser ile oluşturulmuş kullanıcı).