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.
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.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.
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`);
}$balance = $client->getCustomerPrepaymentBalance('01234567');
echo $balance->balance; // 5000.0
foreach ($balance->openPrepayments as $p) {
printf("Receipt #%d: %s remaining\n", $p->prepaymentId, $p->remaining);
}Identifiers are not auto-merged
3. Browse / reconcile
For reconciliation, ledger views in your own UI, or finding a specific anonymous receipt, list prepayments registered through the calling VCR.
const open = await client.listPrepayments({ state: "open" });
const allForCustomer = await client.listPrepayments({
customerRef: "01234567",
});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).
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.
SRC does not validate the balance
prepayment amount larger than the customer's open balance — the server will accept the sale and write a phantom apply row flagged needsReview in the dashboard. Use getCustomerPrepaymentBalance client-side to keep the cashier honest.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.
await client.registerPrepaymentRefund({
cashier: { id: 1 },
prepaymentId: 9001,
reason: "customer_request",
});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").
| State | Meaning | Transitions to |
|---|---|---|
open | remaining > 0 | → consumed after a sale uses all of it; → refunded after a full refund. |
consumed | remaining == 0, no refund row | Terminal. |
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.