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

Статик терминал ба NFC sticker

Энэ хуудсаар

Юу хийх: машин, касс, ширээ, хаалга зэрэг байнгын төлөх цэгт нэг удаа терминал бүртгэж, QR + NFC sticker хэвлэж, бичиж, түгжиж, туршаад байршуулах. Хугацаа: терминал ~10 мин; sticker захиалга 1–2 долоо хоног; бичих/түгжих нэг sticker-т ~1 мин. Хэрэгтэй: худалдагч апп ба live түлхүүр (эсвэл консол), NFC-тэй утас, NXP TagWriter (iOS/Android).

Терминал нь нэг байнгын код. Sticker (QR ба NFC) тэр кодыг агуулсан URL-ийг л барина — дүн хэзээ ч үгүй. Дүн нь тухайн мөчид таны backend терминалд уясан нээлттэй intent-ээс ирнэ («сагс татах» урсгал).

1. Терминал гэж юу вэ

          нэг удаа                                   сагс бүрт
┌───────────────────────────┐        ┌───────────────────────────────────────────────┐
│ POST /terminals           │        │ таны backend: POST /payment-intents            │
│  → code: 7k2m9x4q3p       │        │   {presentment:"terminal", terminal_id, …}     │
│  → url:  https://geregeapp│        │                     │                          │
│          .mn/pay/t/7k2m…  │        │                     ▼                          │
│ sticker: QR + NFC (url)   │──────▶ │ терминал 7k2m9x4q3p: нээлттэй intent 3 500₮    │
└───────────────────────────┘ хүн    │                     ▲                          │
                              хүргэнэ│ Gerege App: claim {"terminal":"7k2m9x4q3p"}     │
                                     └───────────────────────────────────────────────┘
  • Код: 10 тэмдэгт, 23456789abcdefghjkmnpqrstuvwxyz (0/o, 1/l/i-гүй — нүдээр уншина), серверээс олгогдож хэзээ ч өөрчлөгдөхгүй. Нууц биш (хэвлэгдэнэ).
  • Терминалд claim хийгдээгүй нээлттэй intent орчин бүрт нэг (test сагс live машиныг хаахгүй). Хоёр дахийг үүсгэвэл 409 terminal_busy (payment_intent: нээлттэйн id).
  • Хүн уншуулахад: эхлээд тэр хүний аль хэдийн claim хийсэн нээлттэй intent, дараа нь claim хийгдээгүй, дараа нь claim нь хугацаа өнгөрсөн. Юу ч байхгүй бол 404 nothing_pending («Энэ терминалд одоогоор төлөх зүйл алга»).
  • Апп-ын эзэн ба тест хэрэглэгчид терминалын test сагсыг эхэлж, бусад хүн зөвхөн live-ийг авна.

2. Терминал бүртгэх

Бичих (үүсгэх, засах) нь зөвхөн live түлхүүр эсвэл консол — терминал ба дэд худалдагч хоёр орчинд нэг бүртгэл тул алдагдсан sandbox secret бодит машиныг өөр тийш чиглүүлж болохгүй (test → 403 live_only; test түлхүүр уншина). Консолд: /api/v1/miniapp/dev/apps/{id}/terminals (ижил талбарууд).

curl https://geregeapp.mn/api/v1/miniapp/terminals -u "$GEREGE_LIVE_ID:$GEREGE_LIVE_SECRET" \
  -H 'Content-Type: application/json' \
  -d '{"label": "Машин №7", "location": "СБД, 1-р хороо, Их сургуулийн гудамж 3, 1 давхар",
       "reference": "VM-0007", "sub_merchant_id": "9a7c5d1e-…"}'
{
  "id": "4b8e2f0c-6a1d-4f3b-9c2e-7d5a1b0e3f9a", "object": "terminal",
  "code": "7k2m9x4q3p", "url": "https://geregeapp.mn/pay/t/7k2m9x4q3p",
  "label": "Машин №7", "location": "СБД, 1-р хороо, Их сургуулийн гудамж 3, 1 давхар",
  "reference": "VM-0007", "sub_merchant": "9a7c5d1e-…", "status": "active",
  "created_at": "2026-10-27T09:00:00Z", "updated_at": "2026-10-27T09:00:00Z"
}
Талбар Дүрэм
label 1–60, хүнд Face ID-аас өмнө харагдана — «Машин №7» гэх мэт яг таних нэр
location ≤ 120, мөн харагдана — sticker-ийг өөр газар наавал хүн зөрүүг анзаарна
reference таны ID (машины serial), ^[A-Za-z0-9_.:-]{1,64}$, апп дотор давтагдахгүй: дахин POST → тэр терминал 200 (Idempotency-Key хэрэггүй); өөр терминалд байвал 409 reference_taken
sub_merchant_id олон эзэмшигчтэй бол (дэд худалдагч); intent-д автоматаар орно

GET /terminals ({"data": […]}), GET /terminals/{id}, PATCH /terminals/{id} (дээрх талбарууд + status: "active" | "disabled"). Олноор нь (≤ 500, CSV) бүртгэх batch — «удахгүй».

3. Сагс татах урсгал

  1. Хүн машин/кассын өмнө сагсаа бүрдүүлнэ (машины дэлгэц, кассчин).
  2. Таны backend терминалд intent уяна: {"presentment": "terminal", "terminal_id": "4b8e…", …} — машинд вендинг-ийн manual capture, касс/ширээнд ихэвчлэн automatic.
  3. Хүн утсаа sticker-т хүргэнэ (NFC) эсвэл QR-ийг уншуулна → Gerege App POST /payments/claim {"terminal": "<код>"} → тэр мөчийн нээлттэй intent → терминалын label/location, худалдагчийн нэр, дүн → Face ID.
  4. Таны backend webhook (payment_intent.claimed, .succeeded / .requires_capture)-оор үргэлжлүүлнэ.

Gerege App-гүй компьютер/утас тэр URL-ийг нээвэл нийтийн хуудас GET /api/v1/miniapp/pay/t/{code}-оор терминал ба claim хийгдээгүй live intent-ийг (pending, эсвэл null) харуулна — тайлбар, reference, хэн төлж буй нь хэзээ ч үгүй («удахгүй» — цөмийн /pay/t хуудас; API бэлэн).

4. Аль URL-ийг бичих вэ

URL Хэзээ Төлөв
Терминалын url — https://geregeapp.mn/pay/t/<code> QR ба NFC-д бичих цорын ганц хаяг API ✓; Gerege App iOS ✓ (QR, апп доторх NFC); Universal Link ба арын NFC уншилт — цөмийн AASA /pay/* production-д гармагц
https://appclip.geregeapp.mn/pay?t=<code> бичихгүй — appclip. host байхгүй; Gerege App энэ хэлбэрийг таньдаг л App Clip «удахгүй»
  • URL-ийг API/консолын хариунаас хуулна — гараар бичихгүй (нэг тэмдэгтийн алдаа = өөр терминал эсвэл юу ч биш).
  • Түгжсэн sticker-ийг хэзээ ч дахин бичихгүй тул нэг л хэлбэр: терминалын url, консолын хэвлэх хуудас, Gerege App-ын sticker бичигч бүгд https://geregeapp.mn/pay/t/<code>-ийг бичнэ; App Clip гарсан ч энэ URL ажиллана.
  • App Clip гарсан ч тамхины (насны хязгаартай) машинд идэвхжихгүй (вендинг §11).

NDEF-ийн хэмжээ: https:// нь URI prefix 0x04 болж шахагдана — geregeapp.mn/pay/t/7k2m9x4q3p 29 байт + толгой ≈ 36 байт. NTAG213-ийн 144 байт хангалттай.

5. NFC sticker худалдаж авах

Чип Хэрэглэгчийн санах ой Хэзээ
NTAG213 144 байт анхдагч сонголт — URL-д хангалттай, хамгийн хямд
NTAG215 504 байт нийлүүлэгчид 213 байхгүй бол
NTAG216 888 байт хэрэггүй (илүү том өгөгдөлд)
NTAG 424 DNA — хуулбарыг танидаг SUN (доор) — «удахгүй» дэмжинэ

Захиалахдаа нийлүүлэгчид ингэж хэлнэ:

  • «NXP NTAG213, ISO/IEC 14443-A, NFC Forum Type 2, NDEF formatted, unlocked» — түгжигдээгүй ирэх нь чухал (өөрөө бичиж түгжинэ).
  • Металл гадаргуу (вендинг машины их бие, металл касс) → anti-metal (on-metal) sticker: антенны ард ферритийн давхаргатай (~1 мм зузаан). Энгийн sticker металл дээр уншигдахгүй.
  • Антенн ≥ 25 мм (iPhone-ийн антенн утасны дээд хэсэгт — жижиг tag-ийг олоход хэцүү); sticker-ийн нийт хэмжээ 40–50 мм.
  • Гадаа/чийгтэй бол PET/epoxy бүрхүүлтэй; хөлдөлт, наранд тэсвэртэй цавуу.
  • Нийлүүлэгч таны CSV-ээс URL бичиж түгжиж өгч болно — тэр үед ч sticker бүрийг хүлээж аваад уншиж шалгана (§9).

NTAG 424 DNA (SUN) — «удахгүй» (4-р үе шат): уншуулалт бүрт шифрлэсэн UID + тоолуур + CMAC-ийг URL-д нэмдэг (?picc=…&cmac=…), сервер tag бүрийн AES түлхүүрээр шалгана → хуулбар sticker, хэвлэсэн кодоор алсаас claim хийх хоёулаа танигдана. Одоо худалдаж авах шаардлагагүй; машины хамгаалалтыг чангатгах үед шилжинэ.

6. Хэвлэх давхарга

Нэг sticker = NFC inlay + хэвлэсэн нүүр:

  • Материал: винил (эсвэл PET) + UV ламинат (гэрэл, үрэлтэд өнгө алдахгүй); гадаа бол цаг агаарт тэсвэртэй; tamper-evident («VOID» үлдээдэг) материал — хуулж өөр газар наавал мэдэгдэнэ.
  • Хэмжээ: 40–50 мм (дөрвөлжин эсвэл дугуй).
  • Нүүр: Gerege App-ын лого · NFC тэмдэг (утас хүргэх газрыг заана) · QR (утас NFC-гүй, эсвэл NFC унтраастай хүнд — ижил url) · хүний уншихуйц код (7k2m9x4q3p) · «Gerege App-аар төлөх».
  • QR: ≥ 25 мм, error correction M, чимээгүй бүс ≥ 4 модуль, цагаан дээр бараан — ламинатын гялбаа уншилтыг муутгахгүй эсэхийг шалга.
  • NFC тэмдэг ба QR нэг sticker дээр: хүн утсаа хүргэх эсвэл уншуулахыг өөрөө сонгоно.

Терминал бүрийн QR-тэй хэвлэх хуудас (PDF: A4 24 sticker / 50×50 мм / tent) — «удахгүй» (самбар ба GET /terminals/…/print-sheet). Тэр болтол QR-ийг url-аас өөрийн хэвлэлийн загвараар гаргана.

7. Бичих (encode)

NXP TagWriter (iOS, Android — үнэгүй):

  1. «Write tags» → «New dataset» → Link / URL.
  2. URL: терминалын url-ийг хуулж тавина (https://geregeapp.mn/pay/t/7k2m9x4q3p). Prefix-ийг апп өөрөө https:// (0x04) болгоно.
  3. Ганц URI record. Өөр record (текст, Android AAR, vCard) нэмэхгүй — Android AAR хэрэггүй: Android URL-ийг нээнэ.
  4. Бичих → утсаа sticker-т хүргэнэ → амжилттай.
  5. Уншиж шалгана (TagWriter «Read», эсвэл iPhone-ийн арын уншилт): яг тэр URL, нэг record.

NFC Tools (iOS/Android) ч мөн адил: «Write» → «Add a record» → «URL / URI».

Gerege App-ын sticker бичигч (iOS): байгууллагын админ Тохиргоо → Хэтэвч → «Худалдагч ба байгууллага» → NFC sticker — терминалаа сонгоод sticker-т хүргэхэд url-ийг бичиж, буцааж уншиж шалгана; түгжих нь тусдаа, баталгаажуулалттай алхам. Тэр хуудас session-ийг тухайн байгууллага руу түр шилжүүлж, гарахад буцаана.

8. Түгжих (lock)

Бичиж шалгасны дараа түгжинэ — түгжээгүй tag-ийг хэн ч утсаараа 2 секундэд дарж бичнэ. Гэхдээ цөмийн AASA (/pay/*) production-д гарч, sticker-т хүргэхэд Gerege App нээгдэхийг баталтал бүү түгж — тэр болтол түгжээгүй sticker-ээр турш.

Арга Юу болох Хэзээ
Permanent read-only (NTAG21x lock bits + CC-ийн write access) буцаах боломжгүй, хэзээ ч дахин бичигдэхгүй зөвлөж буй — sticker нэг терминалд насан туршдаа
Нууц үгээр хамгаалах (PWD_AUTH: 32 бит PWD, AUTH0, PROT) бичих нь нууц үгтэй; дараа нь URL солих боломжтой sticker-ийг дараа өөр терминалд ашиглах бол
  • TagWriter-ийн бичих тохиргооны read-only / lock сонголт, эсвэл NFC Tools → «Other» → «Lock tag». Түгжихээс өмнө URL-ийг дахин уншиж шалга — алдаатай URL-ийг түгжвэл тэр sticker хаягдал.
  • PWD_AUTH-ийн нууц үг агаараар ил дамждаг, 32 бит — ойроос сонсож болно. Хамгаалалт нь «санамсаргүй хүн дарж бичихгүй» төдий; permanent lock илүү найдвартай.
  • Түгжсэн ч хуулбар хийж болно (URL нийтийнх) — тэр нь таны терминалыг л нэрлэнэ; хуулбарыг танихад NTAG 424 DNA (§5).

9. Турших

Түгжээгүй test sticker-ээр эхэл: бичих → унших → дахин бичих гэх мэт алдааг хаягдалгүй засна. Дараа нь бодит (anti-metal) sticker-ийг машин дээр нь (металл, хаалганы ард) туршина — ширээн дээр уншигдсан tag металл дээр уншигдахгүй байж болно.

Шалгах Хэрхэн
URL зөв, ганц record TagWriter «Read» / NFC Tools «Read»
iPhone-ийн арын уншилт дэлгэц асаалттай iPhone-ийг sticker-т хүргэ → мэдэгдэл → Gerege App (цөмийн AASA /pay/* production-д гармагц); Gerege App доторх «NFC уншуулах» одоо ажиллана
Android URL нээгдэнэ (Gerege App Android түр зогссон — вэб хуудас)
QR Gerege App-ын QR уншигч, утасны камер
Түгжигдсэн дахин бичих оролдлого татгалзагдана
Сагстай test түлхүүрээр терминалд test intent уяад тест хэрэглэгчийн утсаар

Бодит sticker-гүй — Android-аар NFC tag emulate (HCE). Android утас NFC Forum Type 4 Tag-ийг дуурайж NDEF URI өгнө; iPhone-ийн Core NFC Type 4 NDEF-ийг уншина. Ингэснээр sticker захиалахаас өмнө урсгалаа туршина, URL-ийг кодоор сольж олон терминалыг шалгана. Уншигч дараах APDU-уудыг илгээнэ:

Уншигч илгээнэ Утга Таны service хариулна
00 A4 04 00 07 D2760000850101 00 NDEF Tag Application сонгох 90 00
00 A4 00 0C 02 E103 Capability Container файл сонгох 90 00
00 B0 00 00 0F CC унших 000F 20 003B 0034 04 06 E104 00FF 00 FF + 90 00
00 A4 00 0C 02 E104 NDEF файл сонгох 90 00
00 B0 <offset> <length> NLEN (2 байт) ба NDEF message унших байтууд + 90 00
// AndroidManifest.xml: <service android:name=".StickerHce" android:exported="true"
//   android:permission="android.permission.BIND_NFC_SERVICE"> + intent-filter
//   android.nfc.cardemulation.action.HOST_APDU_SERVICE + meta-data res/xml/apdu_service.xml.
class StickerHce : HostApduService() {
    private val ndef = NdefMessage(NdefRecord.createUri("https://geregeapp.mn/pay/t/7k2m9x4q3p")).toByteArray()
    private val ndefFile = byteArrayOf((ndef.size shr 8).toByte(), ndef.size.toByte()) + ndef // NLEN + message
    private val ccFile = hex("000F20003B00340406E10400FF00FF")
    private var selected: ByteArray? = null

    override fun processCommandApdu(apdu: ByteArray, extras: Bundle?): ByteArray {
        val h = apdu.joinToString("") { "%02X".format(it) }
        return when {
            h.startsWith("00A4040007D2760000850101") -> OK
            h.startsWith("00A4000C02E103") -> { selected = ccFile; OK }
            h.startsWith("00A4000C02E104") -> { selected = ndefFile; OK }
            h.startsWith("00B0") && apdu.size >= 5 -> {
                val file = selected ?: return NOT_FOUND
                val offset = ((apdu[2].toInt() and 0xFF) shl 8) or (apdu[3].toInt() and 0xFF)
                val length = (apdu[4].toInt() and 0xFF).let { if (it == 0) 256 else it }
                if (offset > file.size) NOT_FOUND
                else file.copyOfRange(offset, minOf(offset + length, file.size)) + OK
            }
            else -> NOT_FOUND
        }
    }

    override fun onDeactivated(reason: Int) { selected = null }

    private companion object {
        val OK = hex("9000")
        val NOT_FOUND = hex("6A82")
        fun hex(s: String) = ByteArray(s.length / 2) { s.substring(it * 2, it * 2 + 2).toInt(16).toByte() }
    }
}
<!-- res/xml/apdu_service.xml -->
<host-apdu-service xmlns:android="http://schemas.android.com/apk/res/android"
    android:description="@string/app_name" android:requireDeviceUnlock="false">
    <aid-group android:category="other" android:description="@string/app_name">
        <aid-filter android:name="D2760000850101" />
    </aid-group>
</host-apdu-service>

iPhone дээр эхлээд NDEF уншигч апп-аар (TagWriter, NFC Tools — foreground) шалга; арын уншилт HCE-д төхөөрөмжөөс хамаарч өөр байж болно. Энэ нь хөгжүүлэлтийн хэрэгсэл — бодит цэгт хэзээ ч sticker-ийн оронд биш.

10. Байршуулах ба солих эрсдэл

Байршуулах: хүн утсаа хүргэх өндөрт (1–1.4 м), дэлгэц/товчны ойролцоо, нэг цэгт нэг sticker; металл дээр anti-metal; эргэн тойронд өөр NFC tag/карт уншигч байхгүй.

Эрсдэл Хамгаалалт
Таны sticker-ийн дээр хуурамч sticker (өөр худалдагчийн терминал руу) Gerege App төлөхөөс өмнө бүртгэлийн худалдагчийн нэр, терминалын label/location-ийг харуулна — тэдгээрийг үнэн зөв, тодорхой бич; tamper-evident материал; ажилтан тогтмол шалгана; хүнд «нэр, байршлыг шалгаарай» гэсэн тэмдэглэл
Хуурамч QR → фишинг вэб хүн зөвхөн Gerege App дотор төлнө; Gerege вэб хуудсаар PIN, нууц үг хэзээ ч асуухгүй; URL https://geregeapp.mn/…
Түгжээгүй NFC-г дарж бичих түгжих (§8)
Хэвлэсэн кодоор алсаас claim (код нийтийнх) баталгаажаагүй claim 90 с-д дуусна; нэг хүн ≤ 3; POST …/unclaim; машины дэлгэцэнд «Утсаараа баталгаажуул» харагдвал хүн байгаа эсэхийг машин мэднэ; NTAG 424 DNA SUN «удахгүй»
Tag-ийг хуулах хуулбар нь таны терминалыг л нэрлэнэ; таних — SUN «удахгүй»
Sticker-ийг өөр машинд шилжүүлсэн location зөрнө; машин зөөхөд терминалыг PATCH, sticker-ийг солих

Терминалын статистик (уншуулалт ↔ төлбөр — хуурамч sticker-ийг илрүүлэх) — «удахгүй» (самбар).

11. Терминалыг идэвхгүй болгох, шилжүүлэх

  • Идэвхгүй — PATCH /terminals/{id} {"status": "disabled"}: claim → 404 unknown_terminal, нийтийн хуудас 404, нээлттэй сагсыг token-оор ч төлөхгүй (409 terminal_disabled), шинэ intent → 409 terminal_disabled. Sticker хэвээр — "active" болгоход дахин ажиллана.
  • Машин зарагдсан — PATCH /terminals/{id} {"sub_merchant_id": "<шинэ эзэмшигч>"} (тэр дэд худалдагчийн payout батлагдсан байх). Код, sticker өөрчлөгдөхгүй; өмнөх эзэмшигчийн аль хэдийн capture хийсэн мөнгө түүнд үлдэнэ.
  • Машин зөөсөн — location-ийг шинэчил.

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

Хэсэг Төлөв
Терминал (бүртгэх, засах, идэвхгүй), terminal intent, claim, нийтийн GET /pay/t/{code} ✓
Gerege App iOS: апп доторх NFC уншигч, QR, /pay/t/<code> Universal Link → claim → төлөх цонх ✓ (Universal Link ба арын NFC уншилт — цөмийн AASA /pay/* production-д гармагц)
App Clip «удахгүй»
Хэвлэх хуудас (PDF), batch бүртгэл, терминалын статистик «удахгүй»
Gerege App-ын NFC sticker бичигч (байгууллагын админ) ✓ (iOS)
NTAG 424 DNA (SUN) «удахгүй» (4-р үе шат)