Skip to main content

Build a quote-to-cash integration

The sales document pipeline is:

Quote → SalesOrder → Invoice → Payment → Payment allocation

Each transition is a business command. It may check permissions, status, inventory, credit, tax method, idempotency, and document version. Do not model a transition as a direct database-style status update.

1. Create a quote​

Use the operation's current input definition from the reference:

mutation CreateQuote($input: CreateQuoteInput!) {
createQuote(input: $input) {
id
quoteNumber
status
currency
subtotal
discountAmount
taxAmount
totalAmount
validUntil
version
items {
productId
variantId
quantity
unitPrice
discountAmount
taxAmount
totalPrice
}
}
}

The input contains line and header fields because ARC supports multiple clients. The server persists authoritative calculated totals and currency data. Resolve the product/variant/UOM first and do not turn a stale catalog price into a quote without a pricing check.

Use idempotencyKey when the input exposes it. A timeout after a successful create must be recovered by replaying the same logical request or searching/re-reading the external reference before attempting another create.

2. Convert the quote​

A quote becomes a sales order through the explicit conversion mutation:

mutation ConvertQuote($quoteId: ID!, $idempotencyKey: String) {
convertQuoteToSalesOrder(
quoteId: $quoteId
warehouseId: "<warehouse-id>"
idempotencyKey: $idempotencyKey
) {
id
orderNumber
status
currency
totalAmount
paymentStatus
}
}

Conversion rules to design for:

  • the quote must be in a convertible state and not expired;
  • a quote with taxed lines converts only when its tax method is EXCLUSIVE, or INCLUSIVE on a legacy quote; a taxed quote with any other or no method is refused. New documents are EXCLUSIVE-only, so INCLUSIVE appears only on legacy quotes (see money, pricing, and tax);
  • the accepted quote price is honoured, while current-price drift may be detected and audited;
  • the created order is always DRAFT; confirm it with confirmSalesOrder (section 3);
  • use the warehouse and inventory rules returned by the API; do not reserve stock locally.

3. Confirm and fulfil​

Creating an order without a quote​

You can also create a sales order directly with createSalesOrder. Two input differences from quotes matter:

  • uomId is required on every order line (optional on quote lines);
  • unitPrice is optional on order lines — when omitted, the server resolves the line price through the canonical pricing pipeline. Quote lines require unitPrice because the quote is the priced commitment.

Order status transitions are governed by the document state machine. The common confirmation shape is:

mutation ConfirmOrder($id: ID!, $expectedVersion: Int) {
confirmSalesOrder(id: $id, expectedVersion: $expectedVersion) {
id
orderNumber
status
version
paymentStatus
}
}

Read the order immediately before a transition and pass version as expectedVersion where available. Whether a stale version is refused depends on the server's concurrency mode: when enforcement is on, you get a version-conflict error (HTTP 409 on REST; on GraphQL the message says the document changed since you last loaded it), and you should re-read and decide again. Otherwise the write proceeds. Always re-read after writing.

Shipment/fulfilment changes inventory through ARC's stock movement path. Do not create a parallel inventory adjustment to make your external system appear consistent.

4. Invoice the order​

ARC supports invoice creation and quote-to-invoice conversion. Which path is correct depends on the source document and tenant workflow:

mutation ConvertQuoteToInvoice($quoteId: ID!) {
convertQuoteToInvoice(quoteId: $quoteId) {
id
invoiceNumber
status
currency
totalAmount
outstandingAmount
dueDate
}
}

Use the invoice's returned outstandingAmount, paymentStatus, dueDate, and status for reconciliation. Do not calculate outstanding balances by subtracting locally recorded payments.

5. Record and allocate a payment​

A payment and its allocations are separate operations:

mutation CreatePayment($input: CreatePaymentInput!) {
createPayment(input: $input) {
id
paymentNumber
status
amount
currencyCode
unallocatedAmount
version
}
}
mutation AllocatePayment($input: AllocatePaymentInput!) {
allocatePayment(input: $input) {
id
paymentNumber
allocatedAmount
unallocatedAmount
allocationState
version
}
}

An allocation input identifies the invoice and amount. Pass expectedVersion when the input supports it; as with orders, a stale version is refused only when the server enforces concurrency checks, and otherwise the write proceeds. After allocation, re-read the invoice and payment if your external ledger needs an exact balance.

State and error handling​

Expect domain errors when:

  • a quote is expired or already converted;
  • the quote tax method is ambiguous;
  • a document version is stale (when the server enforces concurrency checks);
  • the customer is over credit limit;
  • stock is unavailable;
  • the caller lacks the operation's permission;
  • the requested currency has no valid price.

These are not transient network failures. Handle them as reconciliation or operator-action states.

External idempotency mapping​

Use the external system's immutable ids:

external quote id → CreateQuoteInput.idempotencyKey
external order id → CreateSalesOrderInput.idempotencyKey
external payment id → CreatePaymentInput.idempotencyKey

Keep ARC ids, document numbers, statuses, totals, and timestamps in your mapping table. The document number is for display; the ARC id is the opaque resource identifier.

Next: Purchasing and inventory.