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

Payments — payment intent

Mini app-д мөнгө авах цорын ганц арга. Гадны төлбөрийн хэрэгсэл (карт форм, QR, банк шилжүүлэг) mini app дотор ХОРИГЛОНО. Мөнгө нь Gerege Wallet-ийн ledger (core banking)-д хөдөлнө; хүн өөрийн хэтэвчний дансаар төлнө.

1. Урсгал

  1. Хүн mini app-д «Төлөх» дарна → frontend → өөрийн backend.
  2. Backend → POST /api/v1/miniapp/payment-intents (Bearer хүний access token, scope payments, Idempotency-Key) → intent (requires_confirmation). Дүнг backend өөрийн каталогоос тооцно.
  3. Frontend → bridge requestPayment({intentId}) → host өөрийн UI-д баталгаажуулна (§3).
  4. Backend төлбөрийг webhook payment_intent.succeeded ба/эсвэл GET /payment-intents/{id}-ээр шалгасны дараа л бараа/үйлчилгээ олгоно. Bridge-ийн succeeded-ээр олгох ХОРИГЛОНО.

2. API (https://geregeapp.mn/api/v1/miniapp)

Auth. Mini app-ын intent үүсгэх — зөвхөн хүний Bearer access token (scope payments): intent үргэлж хэн нэгнийх, token нь хэнийх болохыг хэлнэ. (Mini app биш гадны систем — вэб дэлгүүр, kiosk, вендинг — client credential-аар presentment: "qr" | "redirect" | "terminal" intent үүсгэнэ: pay-with-gerege.md.) Унших, жагсаах, capture, цуцлах, буцаах — Bearer эсвэл апп-ын client credential (Authorization: Basic base64(client_id:client_secret)) — webhook handler, reconciler, нийлүүлэгчийн алдааны дараах буцаалт зэрэг хүнгүй сервер талын үйлдэлд. Аль ч тохиолдолд зөвхөн өөрийн апп-ын, credential-ийн орчны (test/live) intent-д хүрнэ. Rate limit: минутад 600, burst 120.

Үүсгэх — POST /payment-intents

Idempotency-Key толгой ЗААВАЛ: 1–128 хэвлэгдэх ASCII, зайгүй. Нэг апп × орчинд key үүрд өвөрмөц: ижил key + ижил body (ба ижил хүн) → тэр intent 200; ижил key + өөр body/хүн → 409 idempotency_conflict.

{ "amount": 45000, "currency": "MNT", "description": "Japan 5GB 15 хоног", "reference": "order_8812",
  "expires_in": 600, "capture_method": "automatic", "metadata": { "package": "JC071" } }
Талбар Дүрэм
amount MNT бүхэл тоо, 100 ≤ amount ≤ 50 000 000, мөн апп-ын max_amount-аас ихгүй (доор)
currency MNT (анхдагч)
description 1–80 тэмдэгт, хүнд харагдана
reference ≤ 64, мөр шилжилтгүй; таны захиалгын id — апп дотор өвөрмөц ЗӨВЛӨЖ БАЙНА
expires_in 60–1800 секунд (анхдагч 600)
capture_method automatic (анхдагч) эсвэл manual (§4)
metadata ≤ 20 түлхүүр; түлхүүр ≤ 40, утга ≤ 500 тэмдэгт, string
presentment app (анхдагч — хүний token-д цорын ганц зөвшөөрөгдөх утга). qr/redirect/terminal — pay-with-gerege.md
min_age 13–21, апп-ынхаас багагүй — баталгаажуулах үед хүний насыг шалгана (403 age_restricted)
capture_within зөвхөн manual: 60–604 800 секунд (анхдагч 7 хоног) — барьцааны хугацаа (§4)

max_amount — нэг төлбөрийн дээд дүнг оператор апп тус бүрт тогтооно (PUT /api/v1/miniapp/review/apps/{id}/limits {"max_amount": 500000}, 100–50 000 000, null = арилгах); хөгжүүлэгч өөрөө өөрчлөхгүй, консолын апп-ын мэдээлэлд max_amount гэж харагдана. Хэтэрвэл 400 validation_error (details[0].path = "amount"). Тогтоогоогүй бол дээд хязгаар нь 50 000 000.

Мэдэхгүй талбар → 400. Live intent-ийг дараах үед үүсгэх үедээ татгалзана: core banking тохируулагдаагүй (503 payments_unavailable), оператор payout данс тохируулаагүй (409 payouts_not_configured).

Хариу 201 (шинэ) / 200 (idempotent давталт):

{ "id": "pi_4fQ2mX8kLp0aZr7TbN3cVw9E", "object": "payment_intent", "status": "requires_confirmation",
  "amount": 45000, "currency": "MNT", "description": "Japan 5GB 15 хоног", "reference": "order_8812",
  "user_ref": "u_9f2c4e7a1b3d5f60718293a4b5c6d7e8", "livemode": true, "capture_method": "automatic",
  "refunded_amount": 0, "fee": null, "net": null, "failure_code": null, "expires_at": "2026-10-22T09:10:00Z",
  "capture_before": null, "confirmed_at": null, "created_at": "2026-10-22T09:00:00Z", "metadata": { "package": "JC071" },
  "presentment": "app", "terminal": null, "sub_merchant": null, "return_url": null, "claimed_at": null,
  "min_age": null, "capture_within": null }

1.7.0-аас нэмэгдсэн талбарууд (бүх intent-д): presentment, terminal (терминалын id), sub_merchant (дэд худалдагчийн id), return_url, claimed_at, min_age, capture_within (manual-д тоо, бусад null) — app intent-д presentment: "app", бусад нь null (min_age, capture_within-ийг өгсөн бол тэр утга). Худалдагчийн claimless intent-д user_ref claim хүртэл null (pay-with-gerege.md).

fee, net (OMAS) — оператор апп-ын тооцоог асаасан үед capture хийгдсэн live төлбөрт шимтгэл ба amount − fee; хуучин зам (тооцоо унтраалттай), test горим, capture-ээс өмнө null (§7). OMAS-аас нэмэгдсэн: object, capture_method, capture_before.

Бусад

Method Зам Тайлбар
GET /payment-intents/{id} Төлөв. Хугацаа нь дууссан бол тэр дор нь expired болж хариулна
GET /payment-intents?reference=&status=&limit=&after= Шинэ нь эхэнд. limit 1–100 (анхдагч 20); дараагийн хуудас after=<өмнөх хуудасны сүүлийн id>. Хариу {data: [...], has_more}
POST /payment-intents/{id}/capture Manual capture (§4). Аль хэдийн capture хийгдсэнийг байгаагаар нь 200
POST /payment-intents/{id}/cancel Төлөгдөөгүйг (requires_confirmation, failed) цуцлах, эсвэл manual-ын барьцааг суллах (requires_capture). Аль хэдийн canceled → 200
POST /payment-intents/{id}/refunds {amount?, reason?} + Idempotency-Key (§5)

Төлөв

requires_confirmation ──хүн баталгаажуулна──▶ processing ──▶ succeeded ──▶ partially_refunded ──▶ refunded
        │   ▲                                     │
        │   └──────── failed ◀── татгалзсан ──────┤ (manual) ──▶ requires_capture ──capture──▶ succeeded
        ├──cancel──▶ canceled                                        └──cancel / capture_before──▶ canceled / expired
        └──expires_at──▶ expired
  • processing — ledger-ийн хариу хүлээгдэж байна. Алдаа ч биш, амжилт ч биш: дахин бүү төлүүл, дараа нь дахин унш (платформ өөрөө ижил idempotency key-ээр тулгаж дуусгана).
  • failed — дахин төлөх боломжтой (хүн өөр данс сонгож болно); failure_code шалтгааныг хэлнэ.
  • requires_capture — manual intent-ийн мөнгө хүний дансанд барьцаалагдсан.

3. Баталгаажуулалт (host)

  • Host intent-ийг өөрийн session-ээр татаж mini app-ын нэр/icon (registry-ээс), дүн, тайлбар-ыг өөрийн UI-д харуулна; хүн хэтэвчний дансаа сонгоно. Баталгаажуулалт: Face ID / нууц код.
  • Үлдэгдэл хүрэлцэхгүй (insufficient_funds) бол host өөр данс эсвэл цэнэглэлт санал болгоно.
  • livemode: false intent нь «ТЕСТ» шошготой.
  • Хүний дансны KYC хязгаар (нэг удаагийн / өдрийн / сарын, хүний KYC түвшнээр) барьцаа (hold)-нд хэрэглэгдэнэ: хэтэрвэл баталгаажуулалт 403 limit_exceeded, intent failed (failure_code: limit_exceeded, payment_intent.failed) — төлөгдөх боломжтой хэвээр (өөр данс, эсвэл дараа). Барьцаа хязгаарт нэг удаа тоологдоно; цуцлагдсан/хугацаа дууссан барьцаа хязгаарыг чөлөөлнө. Апп тус бүрийн өдрийн хязгаар — «удахгүй».

4. Manual capture

capture_method: "manual" — бараа/үйлчилгээг олгож чадах нь тодорхой болсон үед л мөнгө авах (eSIM идэвхжих, суудал баталгаажих). Хүн баталгаажуулахад мөнгө барьцаалагдана → requires_capture + payment_intent.requires_capture webhook. Дараа нь backend:

  • POST …/capture → succeeded (202 бол processing, дахин унш);
  • эсвэл POST …/cancel → барьцаа суллагдана (canceled; буцаалт биш).
  • capture_before (баталгаажуулснаас 7 хоног, эсвэл capture_within секунд) өнгөрвөл барьцаа автоматаар суллагдана → expired (failure_code: "authorization_expired"). Хугацаа өнгөрсний дараа capture → 409 expired.
  • Capture үргэлж бүтэн дүнгээр. Дүнтэй хэсэгчилсэн capture ({amount}) — «удахгүй»: бүтэн capture хийгээд үлдсэнийг буцаана (§5).

5. Буцаалт — POST /payment-intents/{id}/refunds

{"amount": 10000, "reason": "supplier_failed"} + Idempotency-Key (intent тус бүрт өвөрмөц).

  • amount орхивол үлдсэнийг бүгдийг; нийлбэр ≤ төлсөн дүн. reason ≤ 255.
  • Зөвхөн succeeded / partially_refunded intent-ийг, төлбөрөөс хойш 90 хоногийн дотор.
  • Хариу: 201 (шинэ), 200 (давталт), 202 (pending — ledger-ийн хариу хүлээгдэж байна; refund.succeeded / refund.failed webhook ирнэ).
{ "id": "re_2kLm9Pq4Rs6Tu8Vw0Xy1Za3B", "object": "refund", "intent_id": "pi_…", "amount": 10000,
  "status": "succeeded", "reason": "supplier_failed", "failure_code": null, "livemode": true,
  "created_at": "2026-10-22T09:00:00Z" }

Алдаа: 409 not_refundable, 409 refund_window_expired, 400 validation_error (дүн хэтэрсэн), 409 idempotency_conflict.

6. Test горим

  • Credential хоёр хос: test (mapp_test_…) ба live (mapp_…). Test credential-ээр үүссэн intent livemode:false — мөнгө хөдлөхгүй, ledger-т хүрэхгүй.
  • Test intent-ийн үр дүнг amount-ын сүүлийн хоёр орон шийднэ (хүн host-д баталгаажуулсны дараа):
amount төгсгөл Үр дүн
…01 (жишээ 10001) failed, failure_code: "test_declined", payment_intent.failed
…02 (жишээ 10002) canceled, payment_intent.canceled
бусад, automatic succeeded, payment_intent.succeeded
бусад, manual requires_capture, дараа нь capture → succeeded
  • Webhook test горимд ч илгээгдэнэ (livemode:false).
  • Test credential зөвхөн бүртгэсэн гишүүн ба тест хэрэглэгчдийн нээлтэд ажиллана (auth.md §2).
  • Апп-ыг огт нээлгүйгээр: консолоос test intent үүсгэж, host-ийн баталгаажуулалтыг тоглуулж болно (/dev/apps/{id}/test-intents, …/{pi}/confirm — console.md §3); дээрх дүнгийн дүрэм, webhook нь ижил.

7. Тооцоо (settlement)

Payout дансыг developer өөрөө өөрчилж чадахгүй — оператор тохируулна (PUT /api/v1/miniapp/review/apps/{id}/payout); developer масктай дугаарыг л харна. Мөнгө хоёр замын аль нэгээр явна:

  • Тооцоогүй (анхдагч): capture хийгдмэгц бүтэн дүн payout данс руу шууд орно; буцаалт тэр данснаас гарна; fee/net нь null.
  • Тооцоотой (оператор апп-ыг асаасан): capture апп-ын клиринг данс руу (core banking-ийн GL) орж, хуваарийн дагуу тооцоогоор шимтгэлийг хасаад payout данс руу шилжинэ. Асаахаас өмнө capture хийгдсэн төлбөр хуучин замаараа үлдэнэ.

Шимтгэл ба хуваарь — апп бүрт оператор тогтооно (төлбөрийн дүрэм, хувилбартай; хүчин төгөлдөр болсон дүрмийг хэзээ ч засахгүй, шинэ хувилбар үүсгэнэ). Консолд GET /api/v1/miniapp/dev/apps/{id}/payment-rule-ээр харна.

  • Төлбөр бүрийн шимтгэл capture-ийн мөчид хүчинтэй дүрмээр: round_half_up(amount × fee_bps / 10000) + fee_fixed, дараа нь fee_min-ээс багагүй, fee_max-аас ихгүй, төлбөрөөс ихгүй. Хагас нь дээш тойрно: 1.5% × 10 034 = 150.51 → 151, 1.5% × 100 = 1.5 → 2.
  • Буцаалт шимтгэлийг буцаахгүй, дүрэм refund_fee: true гэвэл буцаалтын хувиар буцаана (хэсэгчилсэн буцаалтуудын нийлбэр бүтэн буцаалтад шимтгэлтэй яг тэнцүү).
  • Хуваарь: daily / weekdays / weekly:<mon…sun> / manual, Улаанбаатарын HH:MM-д, T+N: Улаанбаатарын d өдрийн төлбөр d+N өдрийн ажиллалтаас хойш. Дүрэмгүй бол шимтгэл 0, ажлын өдөр 10:00, T+1.
  • Holdback (нөөц) байхгүй.

Settle хийгдсэн төлбөрийн буцаалт клирингээс (тухайн апп-ын settle хийгдээгүй мөнгөнөөс) гарна. Клиринг хүрэлцэхгүй бол буцаалт failed, failure_code: "settlement_shortfall" (refund.failed webhook) — дараа дахин оролдоно уу эсвэл оператортой холбогдоно уу. Тооцооны дүнгээс хэтэрсэн буцаалт ба авах мөнгөгүй шимтгэл дараагийн тооцоонд шилжинэ (clearing_carried_*, fee_carried_*).

API (OMAS 1.0.2) — Bearer access_token эсвэл client Basic, зөвхөн өөрийн апп, зөвхөн live (test credential хоосон жагсаалт / 404):

Method Зам Хариу
GET /settlements?status=pending\|paid\|failed&limit=1–100&cursor= {items: Settlement[], next_cursor, has_more}, шинэ нь эхэнд
GET /settlements/{id} Settlement + intents[] + refund_items[] + truncated (> 1000 мөр)
{ "id": "st_7QwErTy2UiOp4AsDf6GhJk8L", "object": "settlement", "status": "paid", "cutoff": "2026-10-21T16:00:00Z",
  "gross": 100000, "refunds": 5000, "reserve_refunds": 0, "fee": 2000, "net": 93000, "holdback": 0, "released": 0,
  "recovered": 0, "paid": 93000, "intent_count": 3, "destination_masked": "******0042", "holdback_release_at": null,
  "livemode": true, "created_at": "2026-10-22T08:58:00Z", "paid_at": "2026-10-22T08:59:00Z", "refund_count": 1,
  "fee_assessed": 2000, "fee_returned": 0, "fee_carried_in": 0, "fee_carried_out": 0,
  "clearing_carried_in": 0, "clearing_carried_out": 0, "failure_code": null }

Мөн "sub_merchant": "<id>" | null — дэд худалдагчтай худалдагч апп-д тооцоо (апп, дэд худалдагч) тус бүрт тусдаа (pay-with-gerege.md §8); null = апп-ын өөрийн урсгал.

net = gross − refunds − fee, paid = net − recovered (holdback байхгүй). fee — энэ тооцоонд үнэхээр авсан шимтгэл (буцаасан нь их бол сөрөг); recovered — өмнөх тооцооноос хэтэрсэн буцаалтыг одоо суутгасан (сөрөг бол дараагийнх руу шилжсэн). Тооцоо төлөгдмөгц settlement.paid webhook (webhooks.md); paid 0 байж болно.