Статик терминал ба 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. Сагс татах урсгал¶
- Хүн машин/кассын өмнө сагсаа бүрдүүлнэ (машины дэлгэц, кассчин).
- Таны backend терминалд intent уяна:
{"presentment": "terminal", "terminal_id": "4b8e…", …}— машинд вендинг-ийн manual capture, касс/ширээнд ихэвчлэнautomatic. - Хүн утсаа sticker-т хүргэнэ (NFC) эсвэл QR-ийг уншуулна → Gerege App
POST /payments/claim {"terminal": "<код>"}→ тэр мөчийн нээлттэй intent → терминалынlabel/location, худалдагчийн нэр, дүн → Face ID. - Таны 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 — үнэгүй):
- «Write tags» → «New dataset» → Link / URL.
- URL: терминалын
url-ийг хуулж тавина (https://geregeapp.mn/pay/t/7k2m9x4q3p). Prefix-ийг апп өөрөөhttps://(0x04) болгоно. - Ганц URI record. Өөр record (текст, Android AAR, vCard) нэмэхгүй — Android AAR хэрэггүй: Android URL-ийг нээнэ.
- Бичих → утсаа sticker-т хүргэнэ → амжилттай.
- Уншиж шалгана (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-р үе шат) |