Guide 08 · Developer docs

Commerce

A Core dna store is a set of records and an API that moves a purchase from cart to order: products described in every language and priced in every currency the store sells in, a cart, a checkout that becomes an order, invoices, payments, tax and shipping. The site builds the storefront on top: the cart and checkout pages, their scripts, and the customer emails.

L2–L3
Unit 05 · From payment to fulfilment

Invoices, transactions and refunds

How invoices, their items and their transactions relate to an order, which one is the commercial document and which the money, and how a refund is calculated, including the adjustment that is meant to go negative.

Who this is for. Developers connecting orders to accounting or payment reconciliation, and anyone who reads refund figures. After this page you can say which record holds an amount, read an invoice's status, and explain every number on a refund.

Order, invoice, transaction

An order holds what was bought and where it is going. The money is recorded below it: on invoices, and, for each movement of money, on transactions that belong to an invoice.

How an order's money is recorded
Order ── lines, addresses, fulfilments
  │
  ├── Invoice (one or more) ── the commercial document: what is owed
  │     items: item, shipping, handling, insurance, surcharge,
  │            discount, tax, memo
  │     status: draft → open → paid or void; paid → refunded
  │     │
  │     └── Transaction (one or more) ── the money moving at the provider
  │           type: authorization, capture, void, refund
  │           status: pending, success, error
  │
  └── Refund (none or more) ── money returned against one invoice
        refund items: the invoice items it returns
        status: pending, success, failed, cancelled
        paid back by a refund transaction on that invoice

The invoice is the commercial document: what is owed, item by item, in one currency. The transaction is the money: an authorisation, a capture, a void or a refund, recorded as it happened at the payment provider. An order can have more than one invoice; each has its own items, transactions and status.

An invoice is linked to its order by a pair of fields that name the record's type and id (sourceEntity and sourceRecordId), not by a fixed relation. That is how the invoice the purchase call creates for a checkout carries on, unchanged, as the order's invoice: the conversion re-points the pair (What the conversion does, in order).

Invoice items and the invoice total

Item typeIn the total
item, shipping, handling, insurance, surchargeAdded
discountA summary line; the discount itself is taken from the discount on each item
taxA summary line; the tax itself is taken from the tax on each item
memoInformation only

The invoice total is (item + shipping + handling + insurance + surcharge) − discount + tax. The discount, tax and memo items are there for whoever reads the invoice, so their amounts are not counted a second time. An invoice saves only when its stored total equals that sum to its currency's decimal places.

Invoice status

An invoice moves forward: draft → open → paid or void, and paid → refunded.

  • A new invoice starts as draft or open.
  • An open invoice goes on to paid or void, never back to draft.
  • Only a paid invoice can become refunded.
  • void and refunded are final.
  • Only a draft invoice can be deleted. Deleting any other is refused with "Issued invoices cannot be deleted": an issued invoice is part of the record of the sale.

Transactions

A transaction's type is authorization, capture, void or refund, and its status pending, success or error. A transaction is final from the moment it is saved; a change is refused with "Transactions are immutable." A correction is a new transaction, so the invoice keeps the full history of what happened at the provider.

A transaction is in its invoice's currency: every capture and refund on an invoice reports in the currency the purchase was made in, whatever the site's or the shopper's currency is today. How that currency reaches the provider is on The currency a payment is taken in.

Refunds

Staff issue a refund in the admin, from the order or from an invoice. To work out a refund's figures without issuing it, integrations call the refund calculation, POST /cdna-api/orders/refund/calculate (Orders API reference).

A refund returns money against one invoice. Its refund items say which invoice items it returns: a full refund returns them all, a partial refund some of them. The items that can be refunded are of type item, shipping, handling, insurance and surcharge; the discount and tax come along with them, from the discount and tax on each item. A refund's total is:

item + shipping + handling + insurance − discount + tax + surcharge + adjustment

  • The total must be above zero.
  • The total can never exceed what is left on the invoice: its total less everything already refunded.
  • A refund's status is pending, success, failed or cancelled. It can be changed only while pending; after that it is final ("Refunds are immutable").
  • Refund items are final from the moment they are created, even while their refund is still pending. To return different items, cancel the pending refund and issue a new one.

An order can still be refunded when isRefundable() is true: it has at least one invoice, and less has been refunded than its total. That is the condition for showing a refund control on a staff screen, or a "request a refund" link to a member.

The adjustment is meant to go negative

The adjustment is a correction to the refund total, and its sign is deliberate. A negative adjustment reduces the refund, for a restocking fee or a penalty; a positive one increases it. The platform's own documentation of the refund's adjustment total records the decision: "Negative values are intentional: a negative adjustment reduces the refund total (e.g., to account for restocking fees or partial penalties), while a positive adjustment increases it." For example:

100.00 item − 20.00 discount + 10.00 tax + (−5.00) adjustment = 85.00

The refund calculator also writes a negative adjustment itself. When the items chosen would come to more than is left on the invoice, it stores the difference in the adjustment and caps the refund at what remains; the note in the calculator reads "Store the difference in adjustmentTotal. This could result in a negative adjustmentTotal." Say an invoice of 110.00 has had 30.00 refunded already, and the items chosen for a second refund come to 90.00: the refund is 80.00, with an adjustment of −10.00. So an adjustment that nobody typed can appear on a refund, and the cap is the reason.

Keep the sign

Do not "fix" a negative adjustment. In an integration, accept the adjustment with its sign, never validate it as a positive number or take its absolute value, and show it as it is, so the refund lines add up to the refund total.

An order's refunds, and the total refunded so far, read like this wherever a template has the order, such as the member's order screen:

Template code
{foreach $order->getRefunds() as $refund}
  <p>Refund ({$refund->getStatus()}): {$refund->getTotalAsMoney()->format()}
    {if $refund->getAdjustmentTotal() != 0}, including an adjustment of {$refund->getAdjustmentTotalAsMoney()->format()}{/if}</p>
{/foreach}
<p>Refunded so far: {$order->getRefundedTotalAsMoney()->format()}</p>
{if $order->isRefundable()}
  <p><a href="/contact?order={$order->getNumber()|escape:'url'}">Request a refund</a></p>
{/if}
{% for refund in order.getRefunds() %}
  <p>Refund ({{ refund.getStatus() }}): {{ refund.getTotalAsMoney().format() }}
    {% if refund.getAdjustmentTotal() != 0 %}, including an adjustment of {{ refund.getAdjustmentTotalAsMoney().format() }}{% endif %}</p>
{% endfor %}
<p>Refunded so far: {{ order.getRefundedTotalAsMoney().format() }}</p>
{% if order.isRefundable() %}
  <p><a href="/contact?order={{ order.getNumber()|url_encode }}">Request a refund</a></p>
{% endif %}

Every amount is in the invoice's currency, and the money getters format it in that currency. A notice to the customer when a refund succeeds is a job on the refund: see Customer emails.

Go deeper

Was this page helpful?

Your answer goes to the docs team.