Skip to content
Docs for developers
Guide
v1

Prepayment workflow

Two-receipt model for advances on VCR.AM: deposit, balance lookup, apply to a sale, and full refund. Code samples in TypeScript and PHP.

The two-receipt model

Armenia's tax code treats an advance payment and the sale it funds as two independent fiscal receipts. Govt Decision 1976-Ն Annex 7 (prepayment receipt) and Annex 3 (sale receipt) specify separate schemas; nothing on the wire ties the two together.

VCR.AM mirrors that legal model. You register a deposit with POST /prepayments, and later — when the customer comes back to spend it — you register a regular sale with POST /sales and declare how much of its total comes from a prior advance via amount.prepayment. SRC does not validate that the declared prepayment amount actually came from any specific receipt; the merchant's books are responsible for tracking the balance.

VCR.AM keeps that book for you in the PrepaymentLedger — an append-only ledger scoped to the BusinessEntity (TIN). Every deposit, apply, and refund is a signed row; the customer's balance is SUM(amount) WHERE entityId = ? AND customerRef = ?. The SDK exposes derived remaining and state fields on every prepayment so you don't need to reimplement the math.

1. Record a deposit

Capture the advance the moment the customer pays. The cashier does not need to know how (or when) the money will be spent yet.

TypeScript
import { VCRClient } from "@blob-solutions/vcr-am-sdk";

const client = new VCRClient(process.env.VCR_API_KEY!);

const receipt = await client.registerPrepayment({
  cashier: { id: 1 },
  // Cash only — split across cash + nonCash if needed.
  amount: { cash: "5000" },
  // Optional: TIN, phone, or email. If omitted, the prepayment is
  // anonymous — discoverable only by receipt number.
  buyer: { type: "business", tin: "01234567" },
});

// Hand the customer the printed receipt: receipt.urlId + receipt.crn.
PHP
use BlobSolutions\VcrAm\Input\Buyer;
use BlobSolutions\VcrAm\Input\PrepaymentAmount;
use BlobSolutions\VcrAm\Input\RegisterPrepaymentInput;
use BlobSolutions\VcrAm\Input\CashierRef;
use BlobSolutions\VcrAm\VcrClient;

$client = new VcrClient(apiKey: $apiKey);

$receipt = $client->registerPrepayment(new RegisterPrepaymentInput(
    cashier: CashierRef::byId(1),
    amount: PrepaymentAmount::cash('5000'),
    buyer: Buyer::business(tin: '01234567'),
));

2. Look up balance at sale time

When the customer returns, look up their open balance against the same identifier they gave at deposit time. Result is scoped to the BusinessEntity, so a merchant running multiple VCRs under one TIN sees a single wallet — the cashier doesn't need to know which till took the deposit.

TypeScript
const balance = await client.getCustomerPrepaymentBalance({
  customerRef: "01234567", // TIN, normalized E.164 phone, or lowercased email
});

console.log(balance.balance); // 5000
for (const p of balance.openPrepayments) {
  console.log(`Receipt #${p.prepaymentId}: ${p.remaining} remaining`);
}
PHP
$balance = $client->getCustomerPrepaymentBalance('01234567');

echo $balance->balance; // 5000.0
foreach ($balance->openPrepayments as $p) {
    printf("Receipt #%d: %s remaining\n", $p->prepaymentId, $p->remaining);
}

3. Browse / reconcile

For reconciliation, ledger views in your own UI, or finding a specific anonymous receipt, list prepayments registered through the calling VCR.

TypeScript
const open = await client.listPrepayments({ state: "open" });
const allForCustomer = await client.listPrepayments({
  customerRef: "01234567",
});
PHP
use BlobSolutions\VcrAm\PrepaymentState;

$open = $client->listPrepayments(state: PrepaymentState::Open);
$allForCustomer = $client->listPrepayments(customerRef: '01234567');

The endpoint caps at 500 rows. For larger sets, filter by customerRef; cross-customer pagination is not currently part of the v1 surface.

4. Apply to a sale

Spending the advance is a regular sale with a prepayment entry in amount. Combine with cash, nonCash, and compensation as needed — the server enforces sum(amounts) == sum(items).

TypeScript
const sale = await client.registerSale({
  cashier: { id: 1 },
  buyer: { type: "business", tin: "01234567" },
  amount: { prepayment: "5000", cash: "2000" },
  items: [
    { offerId: 42, quantity: "1", price: "7000", department: 1 },
  ],
});

VCR.AM's ledger consumes the customer's open prepayments FIFO (oldest first) by the same customerRef you pass on the sale's buyer. SRC receives only the aggregate prePaymentAmount number; the FIFO attribution lives entirely in VCR.AM and is visible in the dashboard ledger view.

5. Refund (full only)

Per Govt Decision 1976-Ն Annex 3 §29, prepayment refunds are full-only. There is no partial-refund variant. If the customer changes their mind after partial consumption, you must register a sale refund for the consumed portion first, then refund the (now-untouched) prepayment.

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

$client->registerPrepaymentRefund(new RegisterPrepaymentRefundInput(
    cashier: CashierRef::byId(1),
    prepaymentId: 9001,
    reason: RefundReason::CustomerRequest,
));

State machine

Every prepayment carries a derived state on the detail and list endpoints. Use it to gate UI affordances ("Refund this prepayment" is only safe when state === "open").

StateMeaningTransitions to
open
remaining > 0→ consumed after a sale uses all of it; → refunded after a full refund.
consumed
remaining == 0, no refund rowTerminal.
refunded
A PrepaymentRefund row exists.Terminal. Always full per Annex 3 §29.

Off-system / historical balances

Prepayments captured before VCR onboarding — cash advances on paper, migrations from another POS — can be added to the ledger from the dashboard's entity overview ("Import an existing prepayment"). The import does not talk to SRC; it only writes a manual_import ledger row so the balance shows up at sale time. There is no API endpoint for this on v1 — it requires an authenticated owner / accountant session, not just an API key.