Fırsat POS Partner API

7. Adım 4 — Kasada kupon kullanımı

Bu, her gün çalışan kısımdır. Tam olarak bu sırayla yapın.

7.1 Kasiyer kodu girer

Müşteri kuponu Fırsat uygulamasında iki şekilde gösterir:

  • 6 haneli kod (ör. 482913). Kasiyer elle yazar.
  • QR kod. Kasiyer okutur. İçinde ya bir link (https:// ile başlar) ya da uzun bir token vardır.

Yazılan veya okutulanı aynen code alanında gönderin. Kısaltmayın, linkin bir parçasını almayın, büyük/küçük harfini değiştirmeyin. Üç biçimi de biz çözeriz; boşlukları da yok sayarız: 482 913 ile 482913 aynı koddur.

Ayrıca bu kasanın hangi şubeye ait olduğunu (5.3'te sakladığınız branchId) bilmeniz ve masa ya da sipariş için açık bir fiş olması gerekir.

7.2 Önizleme

curl -X POST https://partner-api.firsat.dev/api/partner/pos/v1/redemptions/preview \
  -H 'Authorization: Bearer fpt_Xk3...' \
  -H 'X-Firsat-Pos: adisyo/k7qm2x' \
  -H 'X-Firsat-Connection: 0199a3c2-...' \
  -H 'Content-Type: application/json' \
  -d '{ "branchId": "0199a1b1-...", "code": "482913" }'

Olumlu cevap:

{
  "previewId": "0199c7d4-…",
  "state": "valid",
  "coupon": { "id": "0199c001-…", "dealId": "0199d001-…", "remaining": 1 },
  "lines": [
    { "menuItemId": "0199b001-…", "externalItemId": "ADS-1042", "name": "Latte", "quantity": 2, "unitPrice": 95 },
    { "menuItemId": "0199b002-…", "externalItemId": null, "name": "Günün Tatlısı", "quantity": 1, "unitPrice": 140 }
  ],
  "saleUnit": 250,
  "menuUnit": 330,
  "expiresAt": "2026-10-08T12:40:00.000Z"
}

Cevaplar bu rehberde anlatılmayan alanlar taşıyabilir; onları yok sayın.

Alanların anlamı:

AlanAnlamı
previewIdOnayda kullanacağınız id. null ise: devam etmeyin.
statevalid = devam edin. Başka her değer kasiyere gösterilecek bir sebeptir (aşağıdaki tablo).
coupon.remainingKuponun bu kullanımdan önce kaç hakkı olduğu. 1 ise bu son kullanımdır.
linesFişe eklenecek ürünler.
saleUnitFırsat'ın bu kullanım için işletmeye ödediği tutar. Kasiyer bu tutarı "Fırsat" ödeme tipiyle kapatır; kupon satırları fişinizde daha az tutuyorsa o kadarını (7.3).
menuUnitÜrünlerin tam menü fiyatı (unitPrice × quantity toplamı). Sadece gösterim için.
expiresAtBu saatten önce onaylayın. İlk okutmadan itibaren en fazla 10 dakika.

Her state ve ne yapılmalı:

statepreviewIdAnlamıKasiyere gösterilecek (önerilen metin)
validdoluKupon kullanılabilir.Ürünleri gösterin ve 7.3'e geçin.
usednullDaha önce kullanılmış."Bu kupon daha önce kullanılmış."
expirednullKuponun geçerliliği bitmiş."Kuponun süresi dolmuş."
not_yet_redeemablenullHenüz kullanılamaz (daha sonra başlıyor)."Kupon henüz kullanılamaz."
out_of_hoursnullBu saatte kullanılamaz."Kupon bu saatte kullanılamaz."
branch_invalidnullBu şubede geçerli değil."Kupon bu şubede geçerli değil."
not_foundnullBu işletmede bu koda ait kupon yok."Kod bulunamadı. Kodu kontrol edip tekrar deneyin."
open_elsewherenullKupon şu anda bu işletmenin başka bir şubesinde işlemde."Bu kupon şu anda başka bir şubede işlemde. Birkaç dakika sonra tekrar okutun."

previewId null ise durun. Fişe hiçbir şey yazmayın.

Önemli davranışlar:

  • Önizleme hiçbir şey harcamaz. Kasiyer vazgeçerse önizlemeyi unutun; kendi kendine sona erer.
  • Aynı kuponu aynı şubede iki kez okutmak (örneğin iki farklı kasada) iki seferde de aynı previewId'yi döner. Sadece bir onay başarılı olur; diğeri 409 POS_REDEMPTION_PREVIEW_USED alır.
  • Hatalı kodlar sayılır. Çözülemeyen çok fazla kod (bilinmeyen 6 haneli kodlar, bozuk QR kodlar) önizlemeyi bir süreliğine engeller (bölüm 10).

Önizleme hataları (state yerine HTTP hatası):

CevapAnlamıNe gösterilmeli
400 VALIDATION ve details.code: "QR_TOKEN_EXPIRED"Eski bir QR kod (ör. ekran görüntüsü). Uygulamadaki QR kendini yeniler."Kodun süresi dolmuş. Müşteriden uygulamada kodu yenilemesini isteyin."
400 VALIDATION (diğer)Kod 6 haneli sayı, Fırsat linki veya Fırsat token'ı değil (ör. rastgele bir barkod okutuldu)."Geçersiz kod. Müşterinin Fırsat uygulamasındaki QR'ı okutun veya 6 haneli kodu girin."
404 NOT_FOUNDbranchId bu bağlantıya ait değil (veya artık değil).GET /branches ile listeyi yenileyin. "Bu şube Fırsat bağlantısında yok. İşletme sahibi şubeyi Fırsat Patron'dan eklemeli." gösterin.
429 REDEEM_LOCKEDBu şubede çok fazla hatalı kod; birkaç dakikalığına kilitli."Çok fazla hatalı deneme. Biraz sonra tekrar deneyin."
429 RATE_LIMITEDHesabınız çok fazla hatalı kod veya çok fazla istek gönderdi (bölüm 10). Retry-After saniye bekleyin."Çok fazla deneme. Biraz sonra tekrar deneyin."

7.3 Fişe yazma

Sadece previewId doluysa.

Hedef: işiniz bittiğinde fişteki kupon satırları saleUnit eder ve kasiyer bu tutarı "Fırsat" ödeme tipiyle kapatır. Kupon satırları fişinizde zaten saleUnit'ten az tutuyorsa indirim eklemeyin ve tuttukları kadarını kapatın: "Fırsat" ile asla kupon satırlarının fişte bıraktığından fazlasını kapatmayın.

  1. lines içindeki her satırı ekleyin: externalItemId doluysa sizin eşlenen ürününüzü, null ise adı name, fiyatı unitPrice olan özel bir ürünü (POS'unuzda özel ürün yoksa 6.4'e bakın). Adet olarak quantity'yi kullanın.

  2. Bu satırlara bir indirim ekleyin, kalan tutar saleUnit olsun (satırlar zaten daha az tutuyorsa indirim yok):

    satirToplami = eklenen satırların, fişinizin gerçekte uyguladığı fiyatlarla toplamı

    indirim = max(0, satirToplami − saleUnit)

  3. Kasiyer Fırsat tutarını "Fırsat" ödeme tipiyle kapatır (bu ödeme tipini işletme sizin POS'unuzda tanımlar):

    firsatOdemesi = min(satirToplami, saleUnit)

    Müşterinin ayrıca sipariş ettiği her şey normal ödenir (nakit, kart…).

Sayılarla örnek 1. Önizleme 7.2'deki: 2 × Latte (95 TL), 1 × Günün Tatlısı (140 TL), saleUnit 250. Bu restoranın POS'unda Latte 95 değil 100 TL. Müşteri ayrıca bir su (20 TL) söylüyor.

Fiş satırıTutar
Latte × 2 (sizin ADS-1042 ürününüz, sizin fiyatınızla 100)200,00
Günün Tatlısı × 1 (özel ürün, 140)140,00
Üstteki iki satıra Fırsat indirimi: (200 + 140) − 250−90,00
Su × 1 (ayrıca sipariş)20,00
Fiş toplamı270,00
"Fırsat" ödeme tipiyle ödenen250,00
Müşterinin ödediği (nakit/kart)20,00

Kupon satırları (200 + 140 − 90) tam olarak 250 = saleUnit bırakır. Onayda (7.4) Latte fiyatınızın 100 olduğunu bildirebilirsiniz; 10 TL'lik fark işletmenin raporları için kaydedilir (priceDrift). Fırsat'ın ödediği tutarı değiştirmez.

Sayılarla örnek 2: satırlar saleUnit'ten ucuz. Aynı önizleme, ama bu POS Latte'yi 50 TL'ye satıyor. Kupon satırları 2 × 50 + 140 = 240 tutar; saleUnit 250'den az.

Fiş satırıTutar
Latte × 2 (sizin ADS-1042 ürününüz, sizin fiyatınızla 50)100,00
Günün Tatlısı × 1 (özel ürün, 140)140,00
Fırsat indirimiyok
Fiş toplamı240,00
"Fırsat" ödeme tipiyle ödenen: min(240, 250)240,00

Fırsat işletmeye yine saleUnit (250) tutarını kendi hesaplaşmasıyla öder; 10 TL'lik fark işletmenindir.

7.4 Onay

Fiş yazıldıktan sonra onaylayın:

curl -X POST https://partner-api.firsat.dev/api/partner/pos/v1/redemptions \
  -H 'Authorization: Bearer fpt_Xk3...' \
  -H 'X-Firsat-Pos: adisyo/k7qm2x' \
  -H 'X-Firsat-Connection: 0199a3c2-...' \
  -H 'Idempotency-Key: 6f1c2b8e-4c1d-4a8b-9e3f-2a7d5c9b1e04' \
  -H 'Content-Type: application/json' \
  -d '{
        "previewId": "0199c7d4-...",
        "externalOrderRef": "ADS-CHECK-88412",
        "lines": [
          { "menuItemId": "0199b001-...", "unitAmount": 100, "quantity": 2 },
          { "menuItemId": "0199b002-...", "unitAmount": 140, "quantity": 1 }
        ]
      }'
AlanZorunlu mu?Anlamı
previewIdevetÖnizlemeden gelen.
externalOrderRefevetSizin fiş / sipariş numaranız (en fazla 128 karakter). Fırsat bunu işletmeye gösterir, fişi bulabilsinler diye.
lineshayırKupon satırları, fişinizin gerçekte uyguladığı fiyatlarla (en fazla 100). Sadece fiyat farkı raporu için.

Idempotency-Key, adım adım

Bu header, bir kuponu asla iki kez harcamadan güvenle tekrar denemenizi sağlar. previewId başına tek Idempotency-Key. O onayın her tekrar denemesi aynı key'i ve aynı gövdeyi gönderir.

  1. Bir previewId için ilk onayı göndermeden önce yeni rastgele bir key üretin (UUID v4 idealdir) ve fişle birlikte, previewId'nin yanında saklayın (veritabanınızda veya yerel depolamada).
  2. Onayı bu key ile gönderin.
  3. Bu onayı tekrar denemeniz gerekirse (yalnızca 7.6'daki geçici cevaplarda), aynı key'i ve birebir aynı gövdeyi gönderin.
  4. Yeni bir previewId (sonraki kupon ya da 410'dan sonra yapılan yeni önizleme) yeni bir key alır. Bir key'i asla tekrar kullanmayın, günler sonra bile. Kaydı tuttuğumuz sürece her key'i hatırlarız; eski bir key eski cevabı tekrar döner.

Kurallar: 1–128 karakter, sadece yazdırılabilir ASCII (boşluk ve Türkçe karakter yok). UUID her zaman uyar.

Makbuz

{
  "redemptionId": "0199e9f0-…",
  "previewId": "0199c7d4-…",
  "couponId": "0199c001-…",
  "dealId": "0199d001-…",
  "externalOrderRef": "ADS-CHECK-88412",
  "state": "used",
  "remaining": 0,
  "saleUnit": 250,
  "priceDrift": 10,
  "redeemedAt": "2026-10-08T12:31:04.000Z"
}
AlanAnlamı
redemptionIdBu kullanımın Fırsat'taki id'si. Fişle birlikte saklayın. Destek ekibi buna ihtiyaç duyar.
stateKuponun son kullanımıysa used, hakkı kaldıysa active.
remainingBu kullanımdan sonra kalan hak.
saleUnitFırsat'ın bu kullanım için işletmeye ödediği tutar. Fırsat ödemesi min(satirToplami, saleUnit) olur (7.3).
priceDriftBildirdiğiniz satırlar üzerinden (sizin fiyatınız − Fırsat menü fiyatı) × adet toplamı; lines göndermediyseniz null.

Başarılı olursa kasiyere şunu gösterin: "Fırsat kuponu eklendi. <tutar> TL'yi Fırsat ödeme tipiyle kapatın." Buradaki <tutar>, 7.3'teki min(satirToplami, saleUnit) değeridir (ör. "Fırsat kuponu eklendi. 250 TL'yi Fırsat ödeme tipiyle kapatın."). Mümkünse redemptionId'yi fişe yazdırın.

7.5 Onay reddedilirse

Makbuz olmayan ve 7.6'daki geçici cevaplardan biri de olmayan her cevap kesindir. 7.3'te eklediğiniz satırları ve indirimi fişten kaldırın, sebebi gösterin ve correlationId'yi loglayın:

CevapAnlamıNe gösterilmeli
409 POS_REDEMPTION_FAILEDKullanımı reddettik. details.reason sebebi söyler (aşağıdaki tablo).Sebebin metni.
409 POS_REDEMPTION_PREVIEW_USEDBu önizleme başka bir key ile zaten onaylandı (örneğin başka bir kasada) veya zaten reddedildi."Bu kupon başka bir işlemde kullanıldı."
410 POS_REDEMPTION_PREVIEW_EXPIREDexpiresAt'ten sonra onayladınız. Hiçbir şey harcanmadı."Onay süresi doldu. Kodu tekrar okutun." Sonra önizlemeden baştan başlayın.
404 NOT_FOUNDpreviewId bu bağlantıya ait değil. Sizin tarafınızda bir hata."İşlem tamamlanamadı. Kodu tekrar okutun." Doğru bağlantıyı ve önizlemeyi gönderdiğinizi kontrol edin.
400 (herhangi bir kod)İstek hatalı (eksik veya bozuk Idempotency-Key, eksik alan, eksik header). Sizin tarafınızda bir hata."İşlem tamamlanamadı. Kodu tekrar okutun."
403 POS_CONNECTION_REVOKEDİşletme bağlantıyı iptal etmiş (5.5).5.5'teki iptal metni.
403 (diğer kodlar)Bir header hatası, ör. POS_SOLUTION_HEADER_MISMATCH."İşlem tamamlanamadı. Kodu tekrar okutun."
409 POS_IDEMPOTENCY_KEY_CONFLICTBir key'i farklı bir gövdeyle tekrar kullandınız. Sizin tarafınızda bir hata (bölüm 11)."İşlem tamamlanamadı. Kodu tekrar okutun."
Bu rehberde listelenmeyen her durum kodu veya kodRet olarak kabul edin."İşlem tamamlanamadı. Kodu tekrar okutun."

POS_REDEMPTION_FAILED'da görebileceğiniz details.reason değerleri:

reasonAnlamıÖnerilen metin
usedBu arada kullanılmış."Bu kupon daha önce kullanılmış."
expiredBu arada süresi dolmuş."Kuponun süresi dolmuş."
out_of_hoursKuponun saatleri dışında."Kupon bu saatte kullanılamaz."
branch_invalidBu şubede geçerli değil."Kupon bu şubede geçerli değil."
not_yet_redeemableHenüz kullanılamaz."Kupon henüz kullanılamaz."
branch_link_revokedİşletme önizlemeden sonra bu şubeyi bağlantıdan çıkarmış."Bu şubenin Fırsat bağlantısı kaldırılmış. İşletme sahibi şubeyi Fırsat Patron'dan yeniden eklemeli." GET /branches ile listeyi yenileyin.
confirm_rejectedDiğer tüm retler."İşlem tamamlanamadı. Kodu tekrar okutun."

İleride başka değerler eklenebilir. Tanımadığınız her sebep için genel metni gösterin.

Reddedilen bir onay aynı key ile tekrar gönderilebilir; aynı reddi tekrar döner.

7.6 Timeout ve tekrar deneme

Yalnızca aşağıda listelenen geçici cevaplarda tekrar deneyin. Diğer her cevapta durun ve fişi geri alın. Bu karar tablosu onaydan gelebilecek her cevabı kapsar:

Gelen cevapKupon harcandı mı?Ne yapılmalı
200 + makbuzEvetBitti. redemptionId'yi saklayın.
409 POS_REDEMPTION_IN_PROGRESSHenüz belli değilGeçici. 1–2 saniye bekleyip aynı key ve gövdeyle tekrar deneyin.
Timeout, bağlantı hatası, 502, 503, 504, diğer 5xxBilinmiyorGeçici. Aynı key ve gövdeyle tekrar deneyin. Fişi geri almayın.
401 POS_PARTNER_TOKEN_INVALIDHayır (istek işlenmedi)Geçici. Bir kez yeni token alıp aynı key ve gövdeyle tekrar deneyin (4.3).
429 RATE_LIMITEDHayırGeçici. Retry-After saniye bekleyip aynı key ve gövdeyle tekrar deneyin.
409 POS_REDEMPTION_FAILED / PREVIEW_USED, 410 PREVIEW_EXPIREDHayırDurun. Fiş satırlarını geri alın (7.5).
400 (herhangi bir kod)HayırDurun. Fiş satırlarını geri alın; bir entegrasyon hatası (7.5).
403 POS_CONNECTION_REVOKED, diğer 403HayırDurun. Fiş satırlarını geri alın (7.5).
404 NOT_FOUNDHayırDurun. Fiş satırlarını geri alın (7.5).
409 POS_IDEMPOTENCY_KEY_CONFLICTBu istek değilDurun. Fiş satırlarını geri alın. Bir key'i farklı bir gövdeyle tekrar kullandınız; bir entegrasyon hatası (bölüm 11).
Bu tabloda olmayan her durum kodu veya kodHayır sayınDurun. Fiş satırlarını geri alın, genel metni gösterin (7.5), correlationId'yi loglayın.

Sonucu bilinmeyen bir gönderimden sonra yalnızca kuponla ilgili bir cevapta geri alın. Bu onayın daha önceki bir gönderimi timeout'a düştüyse veya 5xx aldıysa kupon zaten harcanmış olabilir. Aynı key ile sonradan gelen 409 POS_REDEMPTION_FAILED, 409 POS_REDEMPTION_PREVIEW_USED veya 410 POS_REDEMPTION_PREVIEW_EXPIRED yine de kuponla ilgili gerçek bir cevaptır: geri alın. Diğer her "durun" cevabı (400, iki kez 401, 403, 404, tanımadığınız bir kod) kupon hakkında hiçbir şey söylemez: satırları fişte bırakın, "İşlem doğrulanamadı. Fişi olduğu gibi bırakın; işletme Fırsat desteğine başvursun." gösterin ve correlationId ile dev@firsat.com adresine yazın.

Tekrar deneme takvimi: 1 sn, 2 sn, 4 sn, 8 sn sonra, ardından her 10 sn'de bir deneyin. Yaklaşık 2 dakika sonra hâlâ belirsizse fişi olduğu gibi bırakın ve şunu gösterin: "Fırsat yanıt vermiyor, işlem doğrulanamadı. Lütfen birkaç dakika sonra 'Tekrar dene'ye basın." Saklanan aynı key ile tekrar deneyen bir "Tekrar dene" butonu bırakın. Birkaç dakika içinde aynı key her zaman kesin bir cevap alır: makbuz veya ret. Kupon asla iki kez harcanmaz.

7.7 Kasa akışının tamamı (pseudo-code)

function kuponKullan(fis, yazilanKod):
    branchId     = buKasa.firsatBranchId
    connectionId = isletme.firsatConnectionId

    onizleme = POST /redemptions/preview { branchId, code: yazilanKod }
    if onizleme bir HTTP hatasıysa: hata metnini göster (7.2); return
    if onizleme.previewId null ise: state metnini göster (7.2); return

    if posOzelUrunEkleyemiyor and onizleme.lines içinde externalItemId == null olan satır varsa:
        "Bu kupon bu kasada eşleşmemiş bir ürün içeriyor. İşletme sahibi ürünü eşlemeli." göster; return   # 6.4

    eklenenler = []
    for satir in onizleme.lines:
        if satir.externalItemId != null:
            eklenenler += fis.urunEkle(satir.externalItemId, satir.quantity)
        else:
            eklenenler += fis.ozelUrunEkle(satir.name, satir.unitPrice, satir.quantity)
    satirToplami  = toplam(eklenenler.tutar)                     # fişinizin uyguladığı fiyatlarla
    indirim       = max(0, satirToplami - onizleme.saleUnit)
    firsatOdemesi = min(satirToplami, onizleme.saleUnit)         # kasiyerin "Fırsat" ile kapattığı tutar
    if indirim > 0:
        fis.indirimEkle(uzerine: eklenenler, tutar: indirim, etiket: "Fırsat")

    key = yeniUuid()                                             # previewId başına tek key
    fis.kaydet(firsatIdempotencyKey: key, firsatPreviewId: onizleme.previewId)   # GÖNDERMEDEN ÖNCE

    govde = { previewId: onizleme.previewId, externalOrderRef: fis.numara,
              lines: eklenenler {menuItemId, unitAmount, quantity} olarak }

    loop, beklemeli tekrar deneme (7.6):
        cevap = POST /redemptions  (Idempotency-Key: key, govde)   # her seferinde aynı key, aynı gövde
        if cevap 200 ise:
            fis.kaydet(firsatRedemptionId: cevap.redemptionId)
            "Fırsat kuponu eklendi. {firsatOdemesi} TL'yi Fırsat ödeme tipiyle kapatın." göster; return
        if cevap 401 ise: token'ı yenile; continue               # bir kez; art arda ikinci 401 aşağıya düşer (4.3)
        if cevap timeout / ağ hatası / 5xx / 409 POS_REDEMPTION_IN_PROGRESS / 429 ise:
            bekle; continue                                      # burada asla geri alma
        # diğer her şey: 400, 403, 404, 409 FAILED / PREVIEW_USED / KEY_CONFLICT, 410
        # ve tanımadığınız her kod
        fis.kaldir(Fırsat satırları ve indirim)
        hata metnini göster (7.5); cevap.correlationId'yi logla; return

Bu sayfada