knddb_track_and_trace 1.0.1
knddb_track_and_trace: ^1.0.1 copied to clipboard
Dart/Flutter SDK для Track and Trace API системы KNMDB / KNDDB (Кыргызская национальная база лекарственных средств): OAuth2-аутентификация, автопродление токена, смена пользователя «на ходу», все мето [...]
knddb_track_and_trace #
Dart/Flutter SDK для Track and Trace API системы KNMDB / KNDDB (Кыргызская национальная база лекарственных средств).
Работает в мобильных приложениях (Flutter, Android, iOS), desktop-приложениях (Windows, macOS, Linux), веб-приложениях и консольных утилитах на Dart.
Возможности #
| Возможность | Описание |
|---|---|
| Все методы API | 23 метода Track and Trace + вход и выход OAuth 2.0 |
| Автопродление токена | По refresh_token, с автоматическим повторным входом |
| Смена пользователя «на ходу» | Ключи сессий и вкладываемые области |
| Сохранение входа | KnddbSessionStorage — вход переживает перезапуск приложения |
| Типизированные модели | Запросы и ответы с русской документацией каждого поля |
| Минимум зависимостей | Только http и meta |
| Терпимый к формату разбор | Перечисления принимаются как числа, имена и названия |
Установка #
flutter pub add knddb_track_and_trace
Или добавьте в pubspec.yaml:
dependencies:
knddb_track_and_trace: ^1.0.0
Требуется Dart 3.6 или новее.
Контуры: тестовый и боевой #
| Контур | Базовый адрес | Когда использовать |
|---|---|---|
KnddbEnvironment.test |
https://testndbapi.med.kg/ |
Разработка и отладка. По умолчанию. |
KnddbEnvironment.production |
https://ndbapi.med.kg/ |
Промышленная эксплуатация |
// Тестовый контур — значение по умолчанию.
final test = KnddbClientOptions();
// Боевой контур.
final production = KnddbClientOptions(environment: KnddbEnvironment.production);
print(production.effectiveBaseUrl); // https://ndbapi.med.kg/
print(production.isProduction); // true
Важно. На боевом контуре все операции реальны и необратимы: созданные декларации влияют на фактический оборот лекарственных средств. По умолчанию SDK использует тестовый контур — так случайная ошибка в конфигурации не затронет промышленные данные.
Адрес можно задать вручную для локального стенда или прокси:
final options = KnddbClientOptions(baseUrl: 'http://localhost:5001/');
Быстрый старт #
import 'package:knddb_track_and_trace/knddb_track_and_trace.dart';
Future<void> main() async {
final client = KnddbApiClient(
options: KnddbClientOptions(environment: KnddbEnvironment.test),
);
// Вход
await client.signIn('ваш_логин', 'ваш_пароль');
// Список организаций
final stakeholders = await client.getAllStakeholders();
for (final organization in stakeholders.stakeholders ?? const []) {
print('${organization.code}: ${organization.name} (${organization.type.label})');
}
client.close();
}
Проверка лекарства без авторизации #
final client = KnddbApiClient();
final medicine = await client.productInquiryByQrCode('010460000000001721SN...');
print('${medicine.productName}, годен до ${medicine.expirationDate}');
print(medicine.verificationMessage); // «Упаковка легальна и доступна к продаже»
switch (medicine.verificationVerdict) {
case 'recalled':
// Упаковка отозвана — показываем красный экран
break;
case 'expired':
// Срок годности истёк
break;
case 'ok':
// Всё в порядке
break;
}
client.close();
Права доступа #
Роли API настраиваются администратором департамента лекарственных средств через административную панель KNMDB. Интеграции достаточно логина и пароля: запрашивать или проверять права не нужно.
Если метод вернул 403, обратитесь к администратору департамента
лекарственных средств:
try {
await client.importDeclaration(request);
} on KnddbApiException catch (error) {
if (error.isForbidden) {
// Показываем пользователю: вопрос к администратору, а не сбой приложения
}
}
Смена пользователя «на ходу» #
final client = KnddbApiClient(
options: KnddbClientOptions(environment: KnddbEnvironment.production),
);
// Два пользователя в одном клиенте
await client.signIn('pharmacist', 'secret', sessionKey: 'user:42');
await client.signIn('warehouse', 'secret', sessionKey: 'user:77');
// Переключиться без обращения к сети
client.useSession('user:42');
final stock1 = await client.getStockInheldList();
// Временная область — от имени другого пользователя
client.beginUserScope('user:77');
try {
final stock2 = await client.getStockInheldList();
} finally {
client.endUserScope();
}
// Проверки
print(client.currentSessionKey); // user:42
print(client.currentUserName); // pharmacist
print(client.isAuthenticated); // true
// Выход одного пользователя — остальные сессии сохраняются
await client.signOutSession('user:42');
Каждая сессия хранит собственные токены, поэтому переключение пользователя не требует повторного запроса токена и не влияет на других пользователей.
Сохранение входа между запусками #
Реализуйте KnddbSessionStorage поверх защищённого хранилища платформы —
готовый адаптер для Flutter есть в
flutter/knddb_track_and_trace_flutter.
final client = KnddbApiClient(
options: KnddbClientOptions(environment: KnddbEnvironment.production),
storage: FlutterSecureSessionStorage(),
);
// При старте приложения
if (await client.restoreSession()) {
// Пользователь уже вошёл — показываем основной экран
} else {
// Нужен вход
await client.signIn(login, password);
}
Пароль на диск не сохраняется: в хранилище попадают только токены и логин.
Обработка ошибок #
Главное, что нужно знать об API KNMDB. Ошибки бизнес-логики приходят с HTTP-статусом 200 в конверте
{ resultCode, resultMessage, actionResult }. Отличить успех от ошибки по статусу невозможно — признак ошибки это ненулевойresultCode.SDK проверяет
resultCodeсам и выбрасываетKnddbApiException, поэтому достаточно ловить исключение: код, проверяющий только статус, получил бы «успех» и пустой объект данных.
try {
await client.salesDeclaration(request);
} on KnddbApiException catch (error) when (error.isBusinessError) {
// HTTP-статус здесь 200, а причина — в resultCode
switch (error.resultCode) {
case KnddbResultCodes.productWithQrCodeNotFound: // 6022
print('Упаковка с таким QR-кодом не найдена в системе');
case KnddbResultCodes.productWithQrCodeNotSuitableForSale: // 6023
print('Упаковка уже продана или не подходит для продажи');
default:
print(error.resultDescription); // русское описание кода
}
}
Операторские ошибки #
| Код | Что случилось | Что показать пользователю |
|---|---|---|
6022 |
Упаковка не найдена | «Упаковка не найдена. Проверьте код и повторите сканирование» |
6023 |
Упаковка не подходит для продажи | «Упаковка уже продана или не подходит для продажи» |
6004 |
QR-код уже в системе | «Эта упаковка уже введена в оборот» |
6033 |
Упаковка уже объявлена | «Упаковка уже объявлена ранее» |
6034 |
Декларация другой организации | «Документ принадлежит другой организации» |
6045 |
Упаковку нельзя деактивировать | «Упаковка продана — деактивация невозможна» |
Общая обработка #
try {
await client.importDeclaration(request);
} on KnddbAuthenticationException catch (error) {
// 401 или ошибка входа — нужен повторный вход
await client.signIn(login, password);
} on KnddbApiException catch (error) {
if (error.isProductNotFound) {
// HTTP 404 или код 6022/6039 — удобно для проверки лекарства
print('Упаковка не найдена');
} else if (error.isProductNotSuitableForSale) {
print('Упаковка уже продана');
} else if (error.isBusinessError) {
print('${error.resultCode}: ${error.resultDescription}');
} else if (error.isForbidden) {
// Права настраиваются администратором департамента
print('Нужны права: обратитесь к администратору департамента');
} else {
print('HTTP ${error.statusCode}: ${error.message}');
print('traceId: ${error.traceId}');
for (final entry in (error.errors ?? const {}).entries) {
print(' ${entry.key}: ${entry.value}');
}
}
}
Свойства исключения #
| Свойство | Описание |
|---|---|
resultCode |
Код результата из конверта. null, если конверта в ответе не было |
resultMessage |
Сообщение сервера из конверта (при ошибке — на английском) |
isBusinessError |
Конверт с ненулевым resultCode — ошибка бизнес-логики с HTTP 200 |
isProductNotFound |
HTTP 404 или код 6022/6039 |
isProductNotSuitableForSale |
Код 6023 |
resultDescription |
Русское описание кода; если код неизвестен — resultMessage |
statusCode |
HTTP-статус; для ошибок бизнес-логики — 200 |
isNotFound, isForbidden, isBadRequest, isConflict |
HTTP 404 / 403 / 400 / 409 |
isTransientFailure |
5xx или 408 — имеет смысл повторить |
traceId, errors, problemDetails |
Диагностика для поддержки |
| Тип исключения | Когда возникает |
|---|---|
KnddbApiException |
Неуспешный HTTP-статус или ненулевой resultCode в конверте |
KnddbAuthenticationException |
401, неверный логин или пароль, отсутствие учётных данных, ошибка на /connect/token |
KnddbConfigurationException |
Настройки некорректны, сессия не найдена |
Если что-то не работает #
| Симптом | Причина | Что делать |
|---|---|---|
Вход падает с resultCode 2 «An error occurred while saving the entity changes» |
Не отправлен заголовок user-agent: сервер записывает его в базу при входе |
SDK отправляет его всегда. Если задаёте свой — проверьте, что он не пустой |
Метод с датой падает с resultCode 2 |
Дата передана с временем (2026-03-15T12:00:00Z) |
SDK отбрасывает время автоматически; сервер принимает только yyyy-MM-dd |
| Код считает ошибку успехом и получает пустой объект | Проверяется только HTTP-статус, а ошибка пришла с HTTP 200 | Проверяйте resultCode или ловите KnddbApiException |
Полный список кодов результата: API-REFERENCE.md, Коды результата.
Методы API — краткий обзор #
| Метод | HTTP |
|---|---|
getAllStakeholders() |
GET .../GetAllStakeholders |
getMedicineList(request?) |
POST .../GetMedicineList |
productInquiryByQrCode(qrCode) |
POST .../ProductInquiryQRCode — без авторизации |
productInquiryByGtinSn(gtin, serialNumber) |
POST .../ProductInquiryGtinSn — без авторизации |
importDeclaration(request) |
POST .../ImportDeclaration |
productionDeclaration(request) |
POST .../ProductionDeclaration |
transferDeclaration(request) |
POST .../TransferDeclaration |
transferAccept(declarationId) |
POST .../TransferAccept |
transferCancel(declarationId) |
POST .../TransferCancel |
transferReturn(request) |
POST .../TransferReturn |
transferReturnCancel(declarationId) |
POST .../TransferReturnCancel |
getTransferDeclaration(declarationId) |
GET .../GetTransferDeclaration |
getTransferListByFilter(request?) |
POST .../GetTransferListByFilter |
stockDeclaration(request) |
POST .../StockDeclaration |
stockDeclarationCancel(declarationId) |
POST .../StockDeclarationCancel |
getStockInheldList() |
POST .../GetStockInheldList |
getStockInheldListByGtin(gtin) |
POST .../GetStockInheldListByGtin |
getPartialSaleInfo(qrCode) |
POST .../GetPartialSaleInfo |
salesDeclaration(request) |
POST .../SalesDeclaration |
salesDeclarationCancel(declarationId) |
POST .../SalesDeclarationCancel |
salesDeclarationBoxCancel(qrCode) |
POST .../SalesDeclarationBoxCancel |
deactivateDeclaration(request) |
POST .../DeactivateDeclaration |
Устаревшее (deprecated) #
| Метод | Причина | Что делать вместо него |
|---|---|---|
getSupportedQrTypes() |
Метод GET /api/TrackAndTrace/GetSupportedQRTypes исключён из API департамента лекарственных средств |
Значение qrTypeId согласовать с департаментом и задать константой в конфигурации |
Метод и типы GetSupportedQRTypesResponse, QRTypeInfo помечены @Deprecated
и будут удалены из пакета, когда окончательно исчезнут из API.
Перечисления #
Все перечисления содержат числовое значение сервера, человекочитаемое название
и метод fromWire, принимающий число, имя члена или название:
StakeholderType.fromWire(5); // StakeholderType.pharmacy
StakeholderType.fromWire('Pharmacy'); // StakeholderType.pharmacy
ProductState.fromWire('Transfer Initiated'); // ProductState.transferInitiated
ConsumptionType.fromWire(40); // ConsumptionType.disposalForDeadline
| Перечисление | Значения |
|---|---|
StakeholderType |
producer (1) … warehouse (7) |
ProductState |
production (1) … deactivationCancelled (18) |
ProductStatus |
inStock (1), stockOut (2), inTransfer (3) |
TransferState |
initiated (1), accepted (2), cancelled (3) |
TransferType |
initiate (1), accept (2) |
ConsumptionType |
systemIn (0) … sample (100) |
TrackAndTraceStatus |
notTracked (1), labelMandatory (2), labelAndTraceMandatory (3) |
Для причин списания есть готовое русское название:
print(ConsumptionType.disposalForDeadline.reasonRu); // «Истёк срок годности»
Полное описание значений — в docs/ENUMS.md.
Документация #
| Документ | Для чего |
|---|---|
| README.md (этот файл) | Установка, контуры, быстрый старт, смена пользователя |
| FLUTTER.md | Для приложений Flutter: экраны входа, хранение токенов, сборка |
| SCENARIOS.md | Начните отсюда для интеграции: пошаговые сценарии «от импортёра до аптеки» с кодом |
| API-REFERENCE.md | Все методы, каждое поле запросов и ответов |
| ENUMS.md | Все перечисления: числа, названия, пояснения |
Документация описывает серверный API, поэтому примеры в ней приведены на C#
(эталонный SDK). Имена методов и полей моделей в Dart совпадают с C# —
отличается только регистр: GetAllStakeholdersAsync → getAllStakeholders.
Работаете с Flutter? #
Этот пакет — чистый Dart: он не знает о защищённых хранилищах платформы
и не содержит элементов интерфейса. Для приложения Flutter добавьте пакет
knddb_track_and_trace_flutter:
он даёт сохранение входа между запусками (Keychain, Android Keystore, DPAPI)
и готовый KnddbAuthController для экрана входа. Полное руководство —
FLUTTER.md.
Лицензия #
MIT — см. LICENSE.