Предоплата через API
Как принять аванс через API VCR.AM: чек предоплаты, проверка баланса, зачёт аванса в продаже и возврат. Примеры на TypeScript и PHP.
Два чека
Постановление правительства 1976-Ն предусматривает два вида чеков электронного кассового аппарата: чек предоплаты на деньги, полученные до передачи товара или оказания услуги, и обычный чек продажи (Приложение 5, пункты 12–14). Продажа, в которой используется аванс, отражает его видом оплаты «использование предоплаты» (Приложение 5, пункт 18). В чеке предоплаты нет позиций, итогов и НДС (Приложение 7, пункт 2).
Поэтому вызовов два: POST /prepayments, когда клиент платит аванс, и позже POST /sales с суммой аванса в amount.prepayment. Налоговая получает только эту сумму: в запросе нет ничего, что связывало бы продажу с конкретным чеком предоплаты.
VCR.AM ведёт эту связь за вас в журнале по каждой компании: каждый аванс, зачёт и возврат — отдельная строка, привязанная к ИНН покупателя (ՀՎՀՀ). Каждая предоплата возвращается с вычисленными полями remaining и state.
1. Зарегистрируйте аванс
Регистрируйте предоплату сразу, как только клиент заплатил. buyer обязателен: компания с ИНН или физлицо.
import { VCRClient } from "@blob-solutions/vcr-am-sdk";
const apiKey = process.env.VCR_API_KEY;
if (apiKey === undefined) throw new Error("VCR_API_KEY is not set");
const client = new VCRClient(apiKey);
const prepayment = await client.registerPrepayment({
cashier: { id: 1 },
amount: { nonCash: "5000" },
// A company buyer: the ledger tracks its balance by TIN.
// For a private person: buyer: { type: "individual" }
buyer: { type: "business_entity", tin: "01234567" },
});
// Keep prepayment.prepaymentId: a refund needs it.use BlobSolutions\VcrAm\Input\Buyer;
use BlobSolutions\VcrAm\Input\CashierId;
use BlobSolutions\VcrAm\Input\PrepaymentAmount;
use BlobSolutions\VcrAm\Input\RegisterPrepaymentInput;
use BlobSolutions\VcrAm\VcrClient;
$client = new VcrClient(apiKey: $apiKey);
$prepayment = $client->registerPrepayment(new RegisterPrepaymentInput(
cashier: CashierId::byInternalId(1),
amount: new PrepaymentAmount(nonCash: '5000'),
// For a private person: Buyer::individual()
buyer: Buyer::businessEntity(tin: '01234567'),
));
// Keep $prepayment->prepaymentId: a refund needs it.2. Проверьте баланс
Когда клиент вернётся, проверьте его открытый баланс по ИНН, на который он платил. Баланс считается по всей компании: если под одним ИНН несколько касс, кассиру не нужно знать, какая из них приняла аванс.
const balance = await client.getCustomerPrepaymentBalance({
customerRef: "01234567", // the buyer's TIN
});
console.log(balance.balance); // 5000
for (const open of balance.openPrepayments) {
console.log(`Prepayment #${open.prepaymentId}: ${open.remaining} left`);
}$balance = $client->getCustomerPrepaymentBalance('01234567');
echo $balance->balance; // 5000.0
foreach ($balance->openPrepayments as $open) {
printf("Prepayment #%d: %s left\n", $open->prepaymentId, $open->remaining);
}3. Список и сверка
Для сверки или собственного журнала получите список предоплат, зарегистрированных на этой кассе. Фильтруйте по customerRef (ИНН покупателя) или по state.
const open = await client.listPrepayments({ state: "open" });
const forCustomer = await client.listPrepayments({ customerRef: "01234567" });use BlobSolutions\VcrAm\PrepaymentState;
$open = $client->listPrepayments(state: PrepaymentState::Open);
$forCustomer = $client->listPrepayments(customerRef: '01234567');Эндпоинт возвращает не больше 500 последних предоплат, и state применяется внутри этих 500. На загруженной кассе фильтруйте по customerRef, чтобы добраться до более старых.
4. Зачтите аванс в продаже
Зачёт аванса — это обычная продажа с суммой prepayment в amount, при необходимости вместе с cash, nonCash или compensation. Сумма оплат должна равняться сумме продажи: чек не может оставить часть цены неоплаченной.
const sale = await client.registerSale({
cashier: { id: 1 },
buyer: { type: "business_entity", tin: "01234567" },
// 5,000 from the advance, the remaining 10,000 paid now by card.
amount: { prepayment: "5000", nonCash: "10000" },
items: [
{
offer: { externalId: "legal-service" },
quantity: "1",
price: "15000",
unit: "pc",
},
],
});use BlobSolutions\VcrAm\Input\Offer;
use BlobSolutions\VcrAm\Input\RegisterSaleInput;
use BlobSolutions\VcrAm\Input\SaleAmount;
use BlobSolutions\VcrAm\Input\SaleItem;
use BlobSolutions\VcrAm\Unit;
$sale = $client->registerSale(RegisterSaleInput::withAmount(
cashier: CashierId::byInternalId(1),
items: [
new SaleItem(
offer: Offer::existing('legal-service'),
quantity: '1',
price: '15000',
unit: Unit::Piece,
),
],
// 5,000 from the advance, the remaining 10,000 paid now by card.
amount: new SaleAmount(prepayment: '5000', nonCash: '10000'),
buyer: Buyer::businessEntity(tin: '01234567'),
));Если покупатель — компания, журнал списывает сумму с открытых предоплат этого ИНН, начиная с самой старой.
Если предоплата относится к конкретному заказу, укажите её в prepaymentId. Тогда продажа спишет именно эту предоплату и не тронет остальные, так что их по-прежнему можно вернуть. Это работает и для покупателей-физлиц.
const sale = await client.registerSale({
cashier: { id: 1 },
buyer: { type: "individual" },
amount: { prepayment: "15000" },
// The prepayment taken for this order.
prepaymentId: prepayment.prepaymentId,
items: [
{
offer: { externalId: "legal-service" },
quantity: "1",
price: "15000",
unit: "pc",
},
],
});$sale = $client->registerSale(RegisterSaleInput::withAmount(
cashier: CashierId::byInternalId(1),
items: [
new SaleItem(
offer: Offer::existing('legal-service'),
quantity: '1',
price: '15000',
unit: Unit::Piece,
),
],
amount: new SaleAmount(prepayment: '15000'),
buyer: Buyer::individual(),
// The prepayment taken for this order.
prepaymentId: $prepayment->prepaymentId,
));Если предоплата не покрывает amount.prepayment, продажа отклоняется с кодом 400 ещё до отправки в налоговую: предоплата возвращена, доступно слишком мало или она принадлежит другому ИНН. Ничего не сохраняется, поэтому отправьте исправленную продажу с новым Idempotency-Key. На неизвестный номер придёт 404. Пока продажа ждёт ответа налоговой, её сумма зарезервирована: другая продажа её не возьмёт, а возврат этой предоплаты отвечает 409, пока продажа не завершится.
prepaymentId не указан, журнал не может понять, чей аванс тратится, и списывает его с самой старой открытой предоплаты компании, кто бы её ни внёс. Чек при этом верный, а остатки remaining в VCR.AM — нет, и ту старую предоплату больше нельзя вернуть. Указывайте предоплату явно.prepaymentId ни налоговая, ни VCR.AM не отклоняют сумму prepayment больше открытого баланса клиента. Продажа пройдёт, а её строка в журнале будет помечена needsReview в личном кабинете. Перед продажей вызывайте getCustomerPrepaymentBalance.5. Возврат
Возврат отдаёт всю предоплату, а с returnRemainder: true — всё, что от неё осталось после продаж. В запросе передаются номер предоплаты и причина, но не сумма.
await client.registerPrepaymentRefund({
cashier: { id: 1 },
prepaymentId: prepayment.prepaymentId,
reason: "customer_request",
});use BlobSolutions\VcrAm\Input\RegisterPrepaymentRefundInput;
use BlobSolutions\VcrAm\RefundReason;
$client->registerPrepaymentRefund(new RegisterPrepaymentRefundInput(
cashier: CashierId::byInternalId(1),
prepaymentId: $prepayment->prepaymentId,
reason: RefundReason::CustomerRequest,
));409 и ничего не сохраняется. Передайте returnRemainder: true, чтобы вернуть остаток; если продажи использовали всё, ответ будет 400. Возврат у предоплаты один, поэтому остаток возвращается целиком и теми же способами оплаты. После этого чек предоплаты показывает остаток и пометку, которую требует Приложение 5, пункт 15. Возврат продажи использованную предоплату не восстанавливает.await client.registerPrepaymentRefund({
cashier: { id: 1 },
prepaymentId: prepayment.prepaymentId,
returnRemainder: true,
reason: "customer_request",
});$client->registerPrepaymentRefund(new RegisterPrepaymentRefundInput(
cashier: CashierId::byInternalId(1),
prepaymentId: $prepayment->prepaymentId,
returnRemainder: true,
reason: RefundReason::CustomerRequest,
));Состояния предоплаты
Каждая предоплата в списке и в детальном ответе содержит вычисленное поле state.
| Состояние | Значение | Переходит в |
|---|---|---|
| open | remaining больше нуля | consumed, когда продажи израсходуют всю сумму; refunded после возврата |
| consumed | remaining равен нулю, возврата не было | Конечное |
| refunded | Зарегистрирован возврат | Конечное |
Авансы, полученные до VCR.AM
Авансы, полученные до начала работы с VCR.AM, можно добавить в журнал со страницы компании в личном кабинете («Импортировать существующую предоплату»). Импорт не обращается в налоговую. Импортированные суммы видны только в личном кабинете: проверка баланса через API и автоматический зачёт в продажах их не видят. Эндпоинта для импорта в API нет.