Implement the reservation Component
Create aCODE_TS Component named Reserve order balance. The code below uses the same public declarations delivered to Monaco.
type Order = {
orderId: string;
accountId: string;
amountMinor: string;
currency: string;
status: string;
};
type Balance = {
balanceId: string;
accountId: string;
currency: string;
availableMinor: string;
reservedMinor: string;
};
const { orderId, requestKey } = api.input()?.body as {
orderId: string;
requestKey: string;
};
const operationId = api.getOperationId();
const principal = api.currentUser();
const gateKey = `order-reservation:${requestKey}`;
const settleOrderTemplateId = String(
api.input()?.templateInputs?.settleOrderTemplateId,
);
if (!util.isUUID(settleOrderTemplateId)) {
api.throw(500, {
message: 'Settlement Job template is not configured',
operationId,
});
}
const acquired = await api.acquireIdempotencyKey(gateKey, 86_400, {
operationId,
orderId,
state: 'PROCESSING',
});
if (!acquired) {
const existing = await api.getDatabaseData('transactions', {
filter: { field: 'requestKey', op: 'eq', value: requestKey },
take: 1,
});
if (existing.results === 1) {
return {
operationId,
originalOperationId: existing.data[0].operationId,
duplicate: true,
transactionId: existing.data[0].transactionId,
status: existing.data[0].status,
};
}
api.throw(409, {
message:
'A request with this idempotency key is already processing',
operationId,
});
}
let reservation;
try {
reservation = await api.transactionDatabase(
async (tx) => {
const existingTransactions = await tx.getDatabaseData(
'transactions',
{
filter: { field: 'requestKey', op: 'eq', value: requestKey },
take: 1,
},
{ lock: 'update' },
);
if (existingTransactions.results === 1) {
return {
duplicate: true,
transactionId: existingTransactions.data[0].transactionId,
originalOperationId: existingTransactions.data[0].operationId,
};
}
const orders = await tx.getDatabaseData<Order>(
'orders',
{
filter: { field: 'orderId', op: 'eq', value: orderId },
take: 1,
},
{ lock: 'update' },
);
if (orders.results !== 1) {
api.throw(404, { message: 'Order not found', operationId });
}
const order = orders.data[0];
if (order.status !== 'PENDING') {
api.throw(409, {
message: 'Order is not eligible for reservation',
operationId,
currentStatus: order.status,
});
}
const balances = await tx.getDatabaseData<Balance>(
'balances',
{
filter: {
and: [
{ field: 'accountId', op: 'eq', value: order.accountId },
{ field: 'currency', op: 'eq', value: order.currency },
],
},
take: 1,
},
{ lock: 'update' },
);
if (balances.results !== 1) {
api.throw(409, {
message: 'Balance is unavailable',
operationId,
});
}
const balance = balances.data[0];
const amountMinor = BigInt(order.amountMinor);
const availableMinor = BigInt(balance.availableMinor);
const reservedMinor = BigInt(balance.reservedMinor);
if (amountMinor <= 0n || availableMinor < amountMinor) {
api.throw(409, {
message: 'Insufficient available balance',
operationId,
});
}
const transactionId = util.randomUUID();
await tx.updateDatabaseData(
'balances',
{ balanceId: balance.balanceId },
{
availableMinor: (availableMinor - amountMinor).toString(),
reservedMinor: (reservedMinor + amountMinor).toString(),
lastOperationId: operationId,
},
);
await tx.updateDatabaseData(
'orders',
{ orderId: order.orderId, status: 'PENDING' },
{
status: 'RESERVED',
reservationTransactionId: transactionId,
lastOperationId: operationId,
},
);
await tx.insertDatabaseData('transactions', [
{
transactionId,
requestKey,
orderId: order.orderId,
accountId: order.accountId,
type: 'RESERVATION',
amountMinor: amountMinor.toString(),
currency: order.currency,
status: 'PENDING_SETTLEMENT',
operationId,
requestedBy: principal.userId,
},
]);
return {
duplicate: false,
transactionId,
originalOperationId: undefined,
accountId: order.accountId,
amountMinor: amountMinor.toString(),
currency: order.currency,
};
},
{
isolationLevel: 'SERIALIZABLE',
timeoutMs: 15_000,
},
);
} catch (error) {
await api.releaseIdempotencyKey(gateKey);
throw error;
}
const jobId = reservation.duplicate
? undefined
: await api.triggerJob(settleOrderTemplateId, {
orderId,
transactionId: reservation.transactionId,
parentOperationId: operationId,
});
api.log(
{
message: 'Order balance reserved',
args: {
operationId,
orderId,
transactionId: reservation.transactionId,
jobId,
requestedBy: principal.userId,
},
},
'INFO',
);
return {
operationId,
transactionId: reservation.transactionId,
jobId,
duplicate: reservation.duplicate,
originalOperationId: reservation.originalOperationId,
status: reservation.duplicate ? 'ALREADY_RESERVED' : 'RESERVED',
};
api.transactionDatabase() commits when its callback resolves and rolls back when it throws. Await each tx.* call sequentially; parallel transaction operations are rejected. The default timeout is 15 seconds, and explicit timeoutMs values are limited to 1–30 seconds.transactions.requestKey and existing ledger row are the durable business truth after the cache entry expires.
Configure the Endpoint
Open Endpoints, create the definition inactive, and configure it before activation.| UI field | Value for this guide | Reason |
|---|---|---|
| Name | Reserve order balance | Searchable operator name. |
| Method | POST | The command changes state. |
| Path | /orders/reserve | Stable public route. |
| Component | Reserve order balance | Selects the reviewed logic. |
| Component Version | Exact reviewed version | Prevents an unrelated activation changing the contract. |
| Runtime input | settleOrderTemplateId | Keeps deployment configuration outside caller-controlled input. |
| Request guard | reserveOrderRequest | Rejects malformed input before execution. |
| Validation status | 422 | Separates invalid input from business conflicts. |
| Success status | 200 | The balance reservation is complete when returned. |
| Hide validation details | On for public clients | Avoids exposing internal schema details. |
| Hide request | On | Reduces retained sensitive payload data. |
| Hide response | Off or policy-driven | Keeps the returned IDs available to authorized support. |
Request
curl --request POST \
--url "$REVO_ENDPOINT_URL/orders/reserve" \
--header "Authorization: Bearer $REVO_SERVICE_ACCOUNT_KEY" \
--header "Content-Type: application/json" \
--data '{
"orderId": "0195af19-9b41-7a7c-8c5c-35ab875e165c",
"requestKey": "checkout-0195af19-9b41-7a7c-8c5c-35ab875e165c-v1"
}'
Response
{
"operationId": "0195af23-78d0-7c6c-b444-82238598e122",
"transactionId": "0195af23-7a2e-7567-a9a2-8ab735cf3788",
"jobId": "0195af23-7b7a-7d17-ae86-c4b16d8e47ec",
"status": "RESERVED"
}
x-revo-oid response header in the calling system. It is the fastest starting point for a cross-service trace when a client did not preserve the JSON response.
Continue with first production workflow.
Previous: step 1. Next: step 3.
