Nfc tag lifecycle
Рецепт: полный жизненный цикл NFC метки.
Регистрация¶
Endpoint: NfcTagEndpoint.registerTag — только consumer аккаунты, NFC должен быть включён (assertNfcEnabled).
Квота: максимум 3 активные метки на пользователя. При превышении — CloseloopException(code: 'NFC_TAG_QUOTA').
// Клиент → Auth Center (через Serverpod client)
final registration = await client.nfcTag.registerTag(label: 'Браслет');
// registration.token — открытый токен, отдаётся ОДИН РАЗ
// registration.tagId — ID метки в БД
// registration.expiresAt — +90 дней от создания
После получения token клиент записывает его на NFC-чип, затем:
// Зафиксировать UID физического чипа (идемпотентно)
await client.nfcTag.confirmTagWritten(tagId, chipUid: chipUid);
chipUid необходим для последующего сброса чипа сотрудником. Без него метка считается «сиротой».
Установка лимитов¶
Endpoint: NfcTagEndpoint.setTagLimits — только владелец, только при включённом NFC. Лимиты в сатанах (1 THB = 100 satang).
// Установить лимиты
final tag = await client.nfcTag.setTagLimits(
tagId,
perTapLimit: 50000, // 500 THB за одно касание
dailyLimit: 200000, // 2000 THB в день
);
// Снять лимиты (передать null)
final tag = await client.nfcTag.setTagLimits(tagId); // perTapLimit и dailyLimit = null
Лимиты хранятся только на сервере в таблице nfc_tag, чип не модифицируется.
Оплата через NFC¶
Endpoint: MerchantEndpoint.chargeByNfc — только merchant/agent аккаунты.
Flow:
1. Мерчант считывает токен с NFC-чипа покупателя
2. Вызывает chargeByNfc(token, amountSatang, currency)
3. Auth Center: находит метку по hashToken(token), проверяет статус, counter, лимиты
4. Auth Center: вызывает PM POST /intents с подписанным HMAC запросом
5. Возвращает NfcChargeResult с intentId и статусом
// Мерчант
final result = await client.merchant.chargeByNfc(
token: tokenFromChip,
amountSatang: 50000, // 500 THB
currency: 'THB',
);
Ротация¶
Endpoint: NfcTagEndpoint.rotateTag — перевыпуск токена для существующей метки. Используется если чип нужно перезаписать (смена устройства).
final registration = await client.nfcTag.rotateTag(tagId);
// Новый token, новый expiresAt (+90 дней), lastCounter сброшен в 0
// Лимиты (perTapLimit, dailyLimit) сохраняются
После ротации — записать новый токен на чип, вызвать confirmTagWritten.
Когда ротировать: если токен скомпрометирован, устройство сменилось, или срок действия истёк.
Отзыв¶
Endpoint: NfcTagEndpoint.revokeTag — доступен даже при выключенном NFC (аварийный kill-switch).
| Ситуация | Поведение |
|---|---|
chipUid == null (сирота) |
Hard delete: строка удаляется из реестра |
chipUid != null |
Soft revoke: status='revoked', revokedAt=now |
Отозванная метка с привязанным chipUid ждёт физического сброса сотрудником через NfcChipResetEndpoint.
Сброс чипа (сотрудник)¶
Доступен аккаунтам agent и ролям operator/superadmin.
Шаг 1: Найти чип¶
// По UID чипа (основной путь)
final creds = await client.nfcChipReset.getChipResetCredentials(
chipUid: '04:A1:B2:C3:D4:E5:F6',
);
// Fallback: по остаточному токену с чипа
final creds = await client.nfcChipReset.getChipResetCredentials(
token: tokenReadFromChip,
);
// creds.tagId, creds.status, creds.label
Каждый вызов записывается в audit_log.
Шаг 2: Физически сбросить чип¶
Сотрудник стирает страницы NFC-чипа через мобильное устройство (без PWD_AUTH — write-protection не используется).
Шаг 3: Подтвердить сброс¶
await client.nfcChipReset.confirmChipReset(chipUid: chipUid);
// Строка nfc_tag удаляется из реестра (hard delete)
// Следующий пользователь чипа сможет зарегистрировать его заново
Идемпотентно: повторный вызов после удаления — тихий успех.