Перейти к содержимому

Повторы и externalId

Главное правило: один externalId — один фискальный чек. Сколько бы раз кассовая программа ни отправила чек с тем же номером и тем же содержимым, ФМ пробьёт его не больше одного раза.

  1. Приложение получает чек и сначала записывает его в журнал на диске, а потом отправляет в ФМ.
  2. Пока чек пробивается, повтор получает 409 BRIDGE_RECEIPT_IN_PROGRESS.
  3. Когда ФМ ответил, результат сохраняется в журнале. Повтор получает тот же результат с "duplicate": true.
  4. Если ФМ отказал (код OFD1_* или OFD2_*), чек точно не пробит. Когда причина вне чека (смена не открыта, ФМ не вставлен), устраните её и отправьте тот же чек с тем же номером. Если нужно изменить сам чек — это новый чек с новым externalId.
  5. Если ФМ не ответил на шаге регистрации, неизвестно, пробит ли чек. Повтор получает 409 BRIDGE_RECEIPT_STATE_UNKNOWN, пока кассир не проверит чек и не нажмёт в приложении «Не пробит».

Журнал переживает перезапуск приложения и компьютера: чек, который пробивался в момент сбоя, после запуска считается «исход неизвестен».

Ответ Что делать
200, duplicate: false Чек пробит — сохраните result, напечатайте чек
200, duplicate: true Чек был пробит раньше — сохраните result, не пробивайте заново
Нет ответа, обрыв, таймаут Повторите тот же запрос с тем же externalId
409 BRIDGE_RECEIPT_IN_PROGRESS Подождите 2–5 секунд и повторите
409 BRIDGE_RECEIPT_STATE_UNKNOWN Покажите кассиру title и action; повторите после его проверки
409 BRIDGE_EXTERNAL_ID_CONFLICT Номер уже занят другим чеком — это ошибка в кассовой программе
422 OFD1_* / OFD2_* Покажите кассиру текст, после исправления повторите с тем же номером
402 LICENSE_* Касса отключена — покажите текст; чек не записан, после оплаты отправьте снова

Пример на JavaScript:

async function registerWithRetry(receipt, attempts = 5) {
for (let attempt = 1; ; attempt++) {
let response;
try {
response = await fetch(`${API}/v1/receipts`, {
method: 'POST',
headers: { 'X-Api-Key': KEY, 'Content-Type': 'application/json' },
body: JSON.stringify(receipt),
signal: AbortSignal.timeout(60_000),
});
} catch {
// Обрыв или таймаут: тот же чек, тот же externalId — второй раз он не пробьётся
if (attempt < attempts) continue;
throw new Error('Приложение не отвечает');
}
const body = await response.json();
if (response.ok) return body.result;
if (body.error.code === 'BRIDGE_RECEIPT_IN_PROGRESS' && attempt < attempts) {
await new Promise((resolve) => setTimeout(resolve, 3000));
continue;
}
throw Object.assign(new Error(body.error.title), body.error);
}
}
  • Уникальный для каждого чека на этой кассе: номер документа в вашей программе, UUID и т.п. До 64 символов.
  • Постоянный: при повторе — тот же номер. Не генерируйте новый номер на каждую попытку — иначе защита от двойного чека не сработает.
  • Возврат — это отдельный чек со своим externalId, а ссылка на продажу — в поле refund.

«Тем же содержимым» приложение считает тот же JSON чека. Если номер совпал, а позиции или суммы другие — это 409 BRIDGE_EXTERNAL_ID_CONFLICT: приложение не знает, какой чек вы имели в виду.

GET /v1/receipts/{externalId} возвращает чек из журнала: status (registered, failed, unknown, pending), result и текст ошибки. Это удобно, если кассовая программа перезапустилась и не знает, чем кончилась последняя попытка.