Перейти к содержимому
Документация для разработчиков
Руководствоv1

Предоплата через 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 обязателен: компания с ИНН или физлицо.

TypeScript
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.
PHP
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. Проверьте баланс

Когда клиент вернётся, проверьте его открытый баланс по ИНН, на который он платил. Баланс считается по всей компании: если под одним ИНН несколько касс, кассиру не нужно знать, какая из них приняла аванс.

TypeScript
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`);
}
PHP
$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.

TypeScript
const open = await client.listPrepayments({ state: "open" });
const forCustomer = await client.listPrepayments({ customerRef: "01234567" });
PHP
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. Сумма оплат должна равняться сумме продажи: чек не может оставить часть цены неоплаченной.

TypeScript
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",
    },
  ],
});
PHP
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. Тогда продажа спишет именно эту предоплату и не тронет остальные, так что их по-прежнему можно вернуть. Это работает и для покупателей-физлиц.

TypeScript
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",
    },
  ],
});
PHP
$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, пока продажа не завершится.

5. Возврат

Возврат отдаёт всю предоплату, а с returnRemainder: true — всё, что от неё осталось после продаж. В запросе передаются номер предоплаты и причина, но не сумма.

TypeScript
await client.registerPrepaymentRefund({
  cashier: { id: 1 },
  prepaymentId: prepayment.prepaymentId,
  reason: "customer_request",
});
PHP
use BlobSolutions\VcrAm\Input\RegisterPrepaymentRefundInput;
use BlobSolutions\VcrAm\RefundReason;

$client->registerPrepaymentRefund(new RegisterPrepaymentRefundInput(
    cashier: CashierId::byInternalId(1),
    prepaymentId: $prepayment->prepaymentId,
    reason: RefundReason::CustomerRequest,
));
TypeScript
await client.registerPrepaymentRefund({
  cashier: { id: 1 },
  prepaymentId: prepayment.prepaymentId,
  returnRemainder: true,
  reason: "customer_request",
});
PHP
$client->registerPrepaymentRefund(new RegisterPrepaymentRefundInput(
    cashier: CashierId::byInternalId(1),
    prepaymentId: $prepayment->prepaymentId,
    returnRemainder: true,
    reason: RefundReason::CustomerRequest,
));

Состояния предоплаты

Каждая предоплата в списке и в детальном ответе содержит вычисленное поле state.

СостояниеЗначениеПереходит в
openremaining больше нуляconsumed, когда продажи израсходуют всю сумму; refunded после возврата
consumedremaining равен нулю, возврата не былоКонечное
refundedЗарегистрирован возвратКонечное

Авансы, полученные до VCR.AM

Авансы, полученные до начала работы с VCR.AM, можно добавить в журнал со страницы компании в личном кабинете («Импортировать существующую предоплату»). Импорт не обращается в налоговую. Импортированные суммы видны только в личном кабинете: проверка баланса через API и автоматический зачёт в продажах их не видят. Эндпоинта для импорта в API нет.