Агуулгыг алгасах

Касс, POS, kiosk — дэлгэц дээрх QR

Энэ хуудсаар

Юу хийх: касс/kiosk-ийн дэлгэцэнд сагс бүрийн динамик QR гаргаж, хүн Gerege App-аар уншуулж төлөхөд баримтаа хэвлэх. Хугацаа: ~2 цаг. Хэрэгтэй: худалдагч апп ба test credential (бүртгэл), касс ↔ таны backend холболт, webhook URL.

Сагс бүрт шинэ qr intent → pay_url-ийг QR болгож дэлгэцэнд → хүн Gerege App-ын QR товчоор уншуулна → payment_intent.claimed («Утсаараа баталгаажуулна уу») → payment_intent.succeeded (баримт). Байнгын, хэвлэсэн QR/NFC (машин, ширээ, хаалга) хэрэгтэй бол — терминал ба sticker.

1. Урсгал

Касс / kiosk           Таны backend                 Gerege                   Хүн (Gerege App)
  │ сагс хаагдлаа         │                             │                           │
  │── POST /sales ───────▶│ POST /payment-intents       │                           │
  │                       │ {presentment:"qr", …}       │                           │
  │                       │────────────────────────────▶│                           │
  │◀── {qr: pay_url, 3500}│◀──── {id, pay_url} ─────────│                           │
  │ QR + «3 500₮» дэлгэцэнд                             │◀──── QR уншуулна (claim) ─│
  │                       │◀── payment_intent.claimed ──│                           │
  │◀── «Утсаараа баталгаажуулна уу»                     │◀──── Face ID → төлнө ─────│
  │                       │◀── payment_intent.succeeded │                           │
  │◀── «Төлөгдлөө» → баримт хэвлэх                      │                           │

Касс secret барихгүй: зөвхөн таны backend-тэй (өөрийн token, TLS) ярьж, pay_url-ийг харуулна.

2. Сагс бүрт intent

curl https://geregeapp.mn/api/v1/miniapp/payment-intents \
  -u "$GEREGE_CLIENT_ID:$GEREGE_CLIENT_SECRET" \
  -H 'Content-Type: application/json' -H 'Idempotency-Key: sale-0042-1188' \
  -d '{"amount": 3500, "description": "Касс №2 · баримт 1188", "reference": "sale-0042-1188",
       "expires_in": 180, "presentment": "qr", "metadata": {"branch": "sbd-1", "till": "2"}}'
  • expires_in богино (120–300 с): дэлгэцэн дээр үлдсэн QR-ийг дараагийн хүн уншуулж болохгүй.
  • Idempotency-Key = таны борлуулалтын дугаар: касс → backend хүсэлт давтагдсан ч нэг intent, нэг QR.
  • capture_method анхдагч automatic — хүн баталмагц мөнгө авна. Бараа дараа нь гардаг kiosk (хоол, түлш, locker) бол manual + capture_within — вендинг-ийн загвар.
  • description хүний хэтэвчний хуулгад харагдана (Gerege App-ын төлөх цонх харуулахгүй): дэлгүүр/касс/баримтын дугаар — хувийн мэдээлэл биш.
  • Олон эзэмшигчтэй (франчайз, маркетплейс) бол sub_merchant_id (дэд худалдагч).

3. QR зурах

  • Агуулга нь яг pay_url (https://geregeapp.mn/pay/<24 тэмдэгт>) — өөрчлөхгүй, богиносгохгүй, өөр домэйноор дамжуулахгүй.
  • Error correction M (эсвэл Q), чимээгүй бүс (quiet zone) ≥ 4 модуль, цагаан дэвсгэр дээр бараан, урвуу өнгөгүй.
  • Дэлгэцэнд ≥ 3 см талтай (30–40 см-ээс уншигдана); гэрэлтэй дэлгэцэнд гялбаагүй.
  • QR-ийн дэргэд: дүн, таны дэлгүүрийн нэр, «Gerege App-аар уншуулна уу», үлдсэн хугацаа.
  • QR-ийг баримт дээр хэвлэж «дараа төл» гэхгүй — тэр нь нэхэмжлэх-ийн урсгал.

4. Үр дүнг хүлээх — webhook эсвэл polling

Хоёр замыг хослуул: webhook нь хурдан (event commit хийгдмэгц шууд нэг удаа илгээгдэнэ), polling нь webhook хоцорсон/алдагдсан үеийн нөөц.

Event / status Касс
payment_intent.claimed (claimed_at орсон) «Утсаараа баталгаажуулна уу»
payment_intent.succeeded / succeeded «Төлөгдлөө» → баримт хэвлэх, сагс хаах
payment_intent.failed / failed «Амжилтгүй — дахин оролдоно уу» (хүн хугацаа дуустал дахин төлж болно)
payment_intent.expired / expired QR-ийг арилгаж «Дахин эхлүүлэх»
payment_intent.canceled / canceled хүн апп дотроо татгалзсан эсвэл та цуцалсан

Касс таны backend-ийг 1–2 с тутам асууна (Gerege-ийг биш — касс secret барихгүй); backend нь webhook-оор мэдсэнээ хариулж, 10 с-ээс илүү мэдээгүй бол GET /payment-intents/{id}-оор шалгана. Server API-ийн хязгаар минутад 600 хүсэлт (burst 120) — касс бүр Gerege-ийг шууд poll хийвэл хурдан дуусна.

5. Цуцлах, сагс өөрчлөх

  • Кассчин цуцалсан / сагс өөрчлөгдсөн → хуучин intent-ийг POST /payment-intents/{id}/cancel, шинэ сагсанд шинэ intent (шинэ Idempotency-Key). Хүн төлж байх үед (processing) 409 in_progress — хэдэн секунд хүлээгээд дахин уншина.
  • Хүн уншуулаад явчихсан — баталгаажаагүй claim 90 секундэд өөрөө дуусна, эсвэл POST /payment-intents/{id}/unclaim-аар шууд суллана (дараагийн хүн уншуулж болно). Нэг хүн нэг дор ≤ 3 баталгаажаагүй claim барина.
  • Буруу хүн уншуулсан (ард зогсож байсан хүн) — мөн unclaim. pay_url-ийг QR-ээс өөр хаана ч гаргахгүй.
  • Төлөгдсөнийг буцаах — POST /payment-intents/{id}/refunds (бүтэн эсвэл хэсэгчлэн, 90 хоногийн дотор).

6. Касс эсвэл сүлжээ тасарвал

Юу болсон Юу хийх
Касс → backend тасарсан касс борлуулалтаа локал журналд үлдээж, холбогдмогц sale дугаараар backend-ээс асууна
Backend → Gerege timeout ижил Idempotency-Key-ээр давтах — ижил intent, ижил pay_url
Webhook ирээгүй backend GET /payment-intents/{id} (эсвэл ?reference=) — polling нь нөөц
Хүн төлсөн ч касс «хүлээж» гацсан backend-ээс reference-ээр дахин асуу; succeeded бол баримт хэвлэ
Касс өдрийн хаалт GET /payment-intents?reference=… эсвэл тулгалт

7. Олон касс, салбар

  • metadata (≤ 20 түлхүүр) — branch, till, cashier зэрэг таны ID; webhook ба GET-д буцаж ирнэ, хүнд харагдахгүй.
  • terminal_id нь зөвхөн terminal presentment-д (qr-д өгвөл 400 validation_error). Касс бүрийн байнгын QR/NFC хэрэгтэй бол кассыг терминал болгож бүртгэнэ — терминал ба sticker.
  • Салбар/касс бүрээр задалсан тайлан — «удахгүй» (самбар). Одоогоор metadata-аар өөрөө нэгтгэнэ.

8. Код

Node (@gerege/miniapp-node) — QR зурах нь таны сонгосон сан (жишээ нь qrcode):

const pi = await gerege.paymentIntents.create(
  { presentment: "qr", amount: sale.total, description: `Касс №${sale.till} · баримт ${sale.no}`,
    reference: sale.id, expires_in: 180, metadata: { branch: sale.branch, till: String(sale.till) } },
  { idempotencyKey: sale.id },
);
till.showQR(pi.pay_url, sale.total); // зөвхөн дэлгэцэнд; лог-д бичихгүй

Go (sdk/go):

pi, err := c.PaymentIntents.Create(ctx, sale.ID, miniapp.CreatePaymentIntentParams{
    Amount: sale.Total, Description: fmt.Sprintf("Касс №%d · баримт %s", sale.Till, sale.No),
    Reference: sale.ID, ExpiresIn: 180, Presentment: miniapp.PresentmentQR,
    Metadata: map[string]string{"branch": sale.Branch},
})

Webhook хүлээн авагчийн бүтэн жишээ: examples/node/src/webhook.ts, examples/go/cmd/webhook/main.go.

9. Хэрэгжилтийн төлөв

Хэсэг Төлөв
qr intent, pay_url, claim (90 с, ≤ 3), unclaim, шууд webhook ✓
Gerege App iOS-ийн QR уншигч (баруун доод QR товч) /pay/<token>-ийг таних → claim → төлөх цонх ✓
Салбар, кассаар задалсан тайлан «удахгүй»