File size: 11,726 Bytes
65083ae
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
"""database.py — Veritabanı katmanı.

Bu modül SQLite ile ilgili HER ŞEYİ içerir; başka hiçbir dosya doğrudan SQL
yazmaz. Böylece veritabanı değişirse (örneğin PostgreSQL'e geçilirse) yalnızca
bu dosya değişir.

Tablolar:
    seferler        — uçuş bilgileri ve boş koltuk sayısı
    rezervasyonlar  — yapılan rezervasyonlar (seferler tablosuna bağlı)
"""

import os
import random
import sqlite3
import string
from datetime import datetime, timedelta

VERITABANI = os.environ.get("DB_YOLU", "ucus.db")


# ---------------------------------------------------------------------------
# Bağlantı
# ---------------------------------------------------------------------------
def baglan():
    """Veritabanı bağlantısı açar.

    row_factory = sqlite3.Row sayesinde sonuçlara sütun adıyla erişilebilir:
        satir["kalkis"]   yerine   satir[1]
    Bu, kodu okunabilir kılar ve sütun sırası değişse bile bozulmaz.

    foreign_keys = ON: SQLite'ta yabancı anahtar denetimi varsayılan olarak
    KAPALIDIR; her bağlantıda açılması gerekir. Bu olmadan, var olmayan bir
    sefere rezervasyon eklenebilirdi.
    """
    conn = sqlite3.connect(VERITABANI)
    conn.row_factory = sqlite3.Row
    conn.execute("PRAGMA foreign_keys = ON")
    return conn


# ---------------------------------------------------------------------------
# Şema
# ---------------------------------------------------------------------------
def tablolari_olustur():
    """Tabloları oluşturur (yoksa)."""
    with baglan() as conn:
        conn.executescript("""
            CREATE TABLE IF NOT EXISTS seferler (
                id           INTEGER PRIMARY KEY AUTOINCREMENT,
                ucus_kodu    TEXT    NOT NULL UNIQUE,
                firma        TEXT    NOT NULL,
                kalkis       TEXT    NOT NULL,
                varis        TEXT    NOT NULL,
                tarih        TEXT    NOT NULL,
                kalkis_saati TEXT    NOT NULL,
                varis_saati  TEXT    NOT NULL,
                fiyat        REAL    NOT NULL,
                bos_koltuk   INTEGER NOT NULL CHECK (bos_koltuk >= 0)
            );

            CREATE TABLE IF NOT EXISTS rezervasyonlar (
                pnr              TEXT    PRIMARY KEY,
                sefer_id         INTEGER NOT NULL,
                yolcu_adi        TEXT    NOT NULL,
                koltuk_sayisi    INTEGER NOT NULL CHECK (koltuk_sayisi > 0),
                toplam_fiyat     REAL    NOT NULL,
                durum            TEXT    NOT NULL DEFAULT 'onaylandi',
                olusturma_tarihi TEXT    NOT NULL,
                FOREIGN KEY (sefer_id) REFERENCES seferler(id)
            );

            CREATE INDEX IF NOT EXISTS idx_sefer_arama
                ON seferler(kalkis, varis, tarih);
        """)


# CHECK (bos_koltuk >= 0):
#   Veritabanı düzeyinde koruma. Bir hata sonucu koltuk sayısı eksiye
#   düşmeye çalışırsa SQLite işlemi reddeder. Koddaki kontrolün yedeğidir.
#
# UNIQUE (ucus_kodu):
#   Aynı uçuş kodu iki kez eklenemez.
#
# INDEX:
#   Arama sorgusu (kalkis + varis + tarih) sık çalışacağı için hızlandırılır.


# ---------------------------------------------------------------------------
# Örnek veri
# ---------------------------------------------------------------------------
SEHIRLER = [
    ("İstanbul", "IST"), ("Ankara", "ESB"), ("İzmir", "ADB"),
    ("Antalya", "AYT"), ("Trabzon", "TZX"), ("Adana", "ADA"),
    ("Gaziantep", "GZT"), ("Dalaman", "DLM"),
]

FIRMALAR = ["Anadolu Hava", "Marmara Havayolları", "Ege Air", "Toros Havacılık"]


def ornek_veri_ekle(sefer_sayisi=28):
    """Rastgele ama gerçekçi seferler üretir.

    Sabit tohum (seed) kullanılır: her çalıştırmada aynı veri oluşur.
    Böylece test sonuçları tekrarlanabilir olur.
    """
    with baglan() as conn:
        mevcut = conn.execute("SELECT COUNT(*) FROM seferler").fetchone()[0]
        if mevcut > 0:
            return mevcut  # veri zaten var, tekrar ekleme

        rnd = random.Random(42)
        bugun = datetime.now().date()
        eklenen = 0
        kullanilan_kodlar = set()

        while eklenen < sefer_sayisi:
            kalkis, kalkis_kod = rnd.choice(SEHIRLER)
            varis, _ = rnd.choice(SEHIRLER)
            if kalkis == varis:
                continue                       # aynı şehre uçuş olmaz

            # Uçuş kodu: TK1234 gibi
            kod = f"{kalkis_kod[:2]}{rnd.randint(1000, 9999)}"
            if kod in kullanilan_kodlar:
                continue
            kullanilan_kodlar.add(kod)

            tarih = bugun + timedelta(days=rnd.randint(1, 14))
            kalkis_saat = rnd.choice(
                ["06:30", "08:15", "10:45", "13:20", "15:50", "18:30", "21:10"])

            # Varış saati: 1-2 saat sonra
            sa, dk = map(int, kalkis_saat.split(":"))
            sure = rnd.choice([60, 75, 90, 105, 120])
            varis_dt = datetime(2000, 1, 1, sa, dk) + timedelta(minutes=sure)

            conn.execute("""
                INSERT INTO seferler
                    (ucus_kodu, firma, kalkis, varis, tarih,
                     kalkis_saati, varis_saati, fiyat, bos_koltuk)
                VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
            """, (
                kod,
                rnd.choice(FIRMALAR),
                kalkis, varis,
                tarih.isoformat(),
                kalkis_saat,
                varis_dt.strftime("%H:%M"),
                round(rnd.uniform(850, 4200), 2),
                rnd.randint(0, 60),            # bazı seferler dolu olabilir (0)
            ))
            eklenen += 1

        return eklenen


# ---------------------------------------------------------------------------
# Okuma işlemleri
# ---------------------------------------------------------------------------
def sefer_ara(kalkis=None, varis=None, tarih=None, limit=10):
    """Seferleri arar. Parametreler isteğe bağlıdır; verilenler filtre olur."""
    sorgu = "SELECT * FROM seferler WHERE bos_koltuk > 0"
    parametreler = []

    # Sorguyu parça parça kur: yalnızca verilen filtreler eklenir.
    # Dikkat: değerler doğrudan metne eklenmez, ? ile parametre olarak geçer.
    # Bu, SQL injection saldırılarını önler.
    if kalkis:
        sorgu += " AND LOWER(kalkis) LIKE LOWER(?)"
        parametreler.append(f"%{kalkis}%")
    if varis:
        sorgu += " AND LOWER(varis) LIKE LOWER(?)"
        parametreler.append(f"%{varis}%")
    if tarih:
        sorgu += " AND tarih = ?"
        parametreler.append(tarih)

    sorgu += " ORDER BY tarih, kalkis_saati LIMIT ?"
    parametreler.append(limit)

    with baglan() as conn:
        satirlar = conn.execute(sorgu, parametreler).fetchall()
    return [dict(s) for s in satirlar]


def sefer_getir(sefer_id):
    """Tek bir seferi id ile getirir. Yoksa None döner."""
    with baglan() as conn:
        satir = conn.execute(
            "SELECT * FROM seferler WHERE id = ?", (sefer_id,)).fetchone()
    return dict(satir) if satir else None


def rezervasyon_getir(pnr):
    """PNR ile rezervasyonu, bağlı olduğu sefer bilgisiyle birlikte getirir.

    JOIN: iki tabloyu birleştirir. rezervasyonlar.sefer_id ile
    seferler.id eşleştirilerek uçuş detayları da tek sorguda alınır.
    """
    with baglan() as conn:
        satir = conn.execute("""
            SELECT r.*, s.ucus_kodu, s.firma, s.kalkis, s.varis,
                   s.tarih, s.kalkis_saati, s.varis_saati
            FROM rezervasyonlar r
            JOIN seferler s ON r.sefer_id = s.id
            WHERE r.pnr = ?
        """, (pnr.upper(),)).fetchone()
    return dict(satir) if satir else None


# ---------------------------------------------------------------------------
# Yazma işlemi
# ---------------------------------------------------------------------------
def _pnr_uret():
    """6 karakterlik rezervasyon kodu üretir (örn. 'K7X2M9')."""
    karakterler = string.ascii_uppercase + string.digits
    return "".join(random.choices(karakterler, k=6))


def rezervasyon_olustur(sefer_id, yolcu_adi, koltuk_sayisi):
    """Rezervasyon oluşturur ve seferin boş koltuk sayısını düşürür.

    Bu iki işlem TEK BİR TRANSACTION içinde yapılır: ya ikisi de gerçekleşir
    ya da hiçbiri. Aksi hâlde rezervasyon kaydedilip koltuk düşmezse aynı
    koltuk iki kez satılabilir.

    Dönüş: (basarili: bool, sonuc: dict)
    """
    conn = baglan()
    try:
        # BEGIN IMMEDIATE: yazma kilidini hemen alır. Aynı anda iki kişi
        # son koltuğu almaya çalışırsa biri beklemek zorunda kalır.
        conn.execute("BEGIN IMMEDIATE")

        sefer = conn.execute(
            "SELECT * FROM seferler WHERE id = ?", (sefer_id,)).fetchone()

        # --- Doğrulama 1: sefer var mı? ---
        # Model uydurma bir sefer_id gönderirse burada durur.
        if sefer is None:
            conn.rollback()
            return False, {"hata": f"{sefer_id} numaralı sefer bulunamadı."}

        # --- Doğrulama 2: yeterli koltuk var mı? ---
        if sefer["bos_koltuk"] < koltuk_sayisi:
            conn.rollback()
            return False, {
                "hata": f"Yetersiz koltuk. Talep: {koltuk_sayisi}, "
                        f"mevcut: {sefer['bos_koltuk']}."
            }

        # --- Benzersiz PNR üret ---
        for _ in range(10):
            pnr = _pnr_uret()
            varsa = conn.execute(
                "SELECT 1 FROM rezervasyonlar WHERE pnr = ?", (pnr,)).fetchone()
            if not varsa:
                break
        else:
            conn.rollback()
            return False, {"hata": "Rezervasyon kodu üretilemedi, tekrar deneyin."}

        toplam = round(sefer["fiyat"] * koltuk_sayisi, 2)

        # --- İşlem 1: rezervasyonu kaydet ---
        conn.execute("""
            INSERT INTO rezervasyonlar
                (pnr, sefer_id, yolcu_adi, koltuk_sayisi,
                 toplam_fiyat, durum, olusturma_tarihi)
            VALUES (?, ?, ?, ?, ?, 'onaylandi', ?)
        """, (pnr, sefer_id, yolcu_adi, koltuk_sayisi, toplam,
              datetime.now().isoformat(timespec="seconds")))

        # --- İşlem 2: koltuk sayısını düşür ---
        conn.execute(
            "UPDATE seferler SET bos_koltuk = bos_koltuk - ? WHERE id = ?",
            (koltuk_sayisi, sefer_id))

        conn.commit()          # ikisi birden kalıcı olur

        return True, {
            "pnr": pnr,
            "ucus_kodu": sefer["ucus_kodu"],
            "firma": sefer["firma"],
            "kalkis": sefer["kalkis"],
            "varis": sefer["varis"],
            "tarih": sefer["tarih"],
            "kalkis_saati": sefer["kalkis_saati"],
            "yolcu_adi": yolcu_adi,
            "koltuk_sayisi": koltuk_sayisi,
            "toplam_fiyat": toplam,
            "kalan_koltuk": sefer["bos_koltuk"] - koltuk_sayisi,
        }

    except sqlite3.Error as e:
        conn.rollback()        # hata olursa hiçbir değişiklik kalmaz
        return False, {"hata": f"Veritabanı hatası: {e}"}
    finally:
        conn.close()


# ---------------------------------------------------------------------------
def hazirla():
    """Uygulama başlarken çağrılır: tabloları kurar, veri yoksa ekler."""
    tablolari_olustur()
    return ornek_veri_ekle()


if __name__ == "__main__":
    sayi = hazirla()
    print(f"Veritabanı hazır: {VERITABANI}")
    with baglan() as c:
        toplam = c.execute("SELECT COUNT(*) FROM seferler").fetchone()[0]
        rez = c.execute("SELECT COUNT(*) FROM rezervasyonlar").fetchone()[0]
    print(f"  Sefer sayısı       : {toplam}")
    print(f"  Rezervasyon sayısı : {rez}")