Skip to content

Transactions (Orders)

Overview

Transactions represent all orders placed through any channel. This is where you view, manage, and process orders.

Key Purpose: View and manage all orders from all channels.

Purpose

This page lets you view, filter, and manage all orders across every sales channel, including reviewing order details, resending receipts, exporting data, and tracking payment and fulfillment status.

Key Concepts

  • Transaction: A complete order record stored in MongoDB, containing items, pricing, customer data, and an order channel identifier (Kiosk, POS, Online Ordering, Table Qr Ordering, Uber Eats, Takeaway, Shopify).
  • Order Status: A single status field tracks the combined payment and fulfillment state of an order (Unpaid, Awaiting Capture, Awaiting Invoice, Awaiting Payment, Pending, Complete, Queued, In Progress, Ready, Cancelled). There is no separate payment-status field; all status filtering uses this one enum.
  • Idempotency Key: A unique key per transaction that prevents duplicate order creation; the system checks for an existing transaction with the same key before processing a new one.
  • Stats Aggregation: The stats endpoint aggregates completed transactions for a date range and optional location, returning completed transaction count, total item quantity, total revenue, and currency.
  • Spreadsheet Export: Transactions can be exported to Excel spreadsheets in paginated chunks of 500 records, including columns for order number, date, location, customer, payment status, total, items, dining option, type, device, receipt number, and channel.

Actions

View Transaction Details

Open any transaction to see the full order breakdown including items, modifiers, pricing, customer information, payment method, and status history.

Resend Receipt

Re-send a receipt for a completed transaction via the ReceiptService; only transactions with a "Complete" status are eligible for receipt resend.

Export Transactions

Download transaction data as a spreadsheet filtered by date range, location, and other criteria; the export processes records in chunks of 500 for efficiency.

Filter and Search Orders

Filter orders by date range, status (via the All / Paid / Unpaid / Stuck POS tabs), channel, dining option, order type, device, and location, or search by order number, customer name, email, or phone.

View Transaction Statistics

Access aggregated stats showing completed transaction count, total quantity sold, and total revenue for a selected date range and location. The stats response also carries total_pending — the summed total of orders awaiting invoice or awaiting payment — which is surfaced as a fourth summary card only when Invoice Collection is enabled. total_pending is location-scoped but deliberately not date-scoped. (Verified: TransactionService::stats, upvendo-backend app/Services/BackOffice/TransactionService.php lines 206-254.)

Retry POS Sync

For orders that failed to sync to the POS (shown under the "Stuck" tab), re-dispatch the POS sync job so the order is sent again.

Delete Test Transaction

Delete a transaction that was created in test mode. Only test-mode transactions can be deleted; deleting a live transaction is blocked.

Location

  • Backoffice Route: /transactions
  • Backend Controller: app/Http/Controllers/Api/BackOffice/TransactionController.php
  • Vue Component: src/views/transactions/Transactions.vue

Order Fields

Order Number

PropertyValue
Field IDorder_number
LabelOrder #
TypeString
Auto-generatedYes

Description: Unique identifier for the order.

Format: Sequential number or custom format (e.g., #1234, ORD-2024-001)


Order Status

PropertyValue
Field IDstatus
LabelStatus
TypeSelect
OptionsUnpaid, Awaiting Capture, Awaiting Invoice, Awaiting Payment, Pending, Complete, Queued, In Progress, Ready, Cancelled

Description: Combined payment and fulfillment status of the order (single status field; there is no separate payment-status field).

Statuses:

  • Unpaid: Order placed but not yet paid
  • Awaiting Capture: Payment authorized, awaiting capture
  • Awaiting Invoice: Awaiting an invoice payment
  • Awaiting Payment: Awaiting payment
  • Pending: Order is in but no payment is made/requested (e.g. third-party orders)
  • Complete: Order paid and fulfilled
  • Queued: Order queued for processing
  • In Progress: Order is being processed/prepared
  • Ready: Order ready for pickup/delivery
  • Cancelled: Order cancelled

Order Channel

PropertyValue
Field IDorder_channel
LabelOrder Channel
TypeString

Description: Where the order originated.

Channels (from App\Enums\ChannelOptions):

  • Kiosk - Self-service kiosk
  • POS - Point of sale
  • Online Ordering - Online ordering website
  • Table Qr Ordering - QR table ordering
  • Uber Eats - Uber Eats integration
  • Takeaway - Takeaway
  • Shopify - Shopify integration

Note: the Channel filter dropdown in the Transactions list currently only offers "All", "Online Ordering", and "Kiosk", even though more channel values can appear on records.


Dining Option

PropertyValue
Field IDdining_option
LabelType
TypeSelect
OptionsDelivery, For Here, Pickup, Takeout

Description: How the customer will receive the order. (Note: "Dine In" is deprecated and replaced by "For Here".)


Order Date/Time

PropertyValue
Field IDcreated_at
LabelOrder Time
TypeDateTime

Description: When the order was placed.


Scheduled Time

PropertyValue
Field IDscheduled_time
LabelScheduled For
TypeDateTime

Description: When the order should be ready (pickup) or delivered.


Customer

PropertyValue
Field IDcustomer
LabelCustomer
TypeObject

Description: Customer information.

Fields:

  • name - Customer name
  • email - Email address
  • phone - Phone number
  • customer_id - Account ID (if logged in)

Delivery Address

PropertyValue
Field IDdelivery_address
LabelDelivery Address
TypeObject
Depends Ondining_option = delivery

Description: Address for delivery orders.


Items

PropertyValue
Field IDitems
LabelOrder Items
TypeArray

Description: Items in the order.

persons is the party size on a per-person (MPLUS bestelbeperking) line; the key is absent on the stored snapshot line for every other item and is emitted as null in the order payload. On such a line quantity holds article units (party size × how many of the dish), because that is what the money and any re-order are computed from. The order-detail line-items table shows the dish count instead: it divides quantity by persons only when persons > 1 and quantity divides evenly, otherwise it shows quantity unchanged. A "For N people" note appears under the item name whenever persons is set (including persons: 1, where no division happens). So a line displayed as 2x Gourmet — For 6 people has quantity: 12, persons: 6 in the payload. Display only — no stored value changes, and the per-unit price shown is still per article unit.

Item Structure:

json
{
  "item_id": "item-123",
  "name": "Cheeseburger",
  "quantity": 2,
  "persons": null,
  "unit_price": 8.95,
  "modifiers": [
    { "name": "Large", "price": 2.00 },
    { "name": "Extra Cheese", "price": 1.50 }
  ],
  "notes": "No pickles",
  "line_total": 24.90
}

Subtotal

PropertyValue
Field IDsubtotal
LabelSubtotal
TypeCurrency

Description: Total of all items before fees and discounts.


Delivery Fee

PropertyValue
Field IDdelivery_fee
LabelDelivery Fee
TypeCurrency

Description: Delivery fee (for delivery orders).


Discount

PropertyValue
Field IDdiscount
LabelDiscount
TypeCurrency

Description: Discount applied (from offers/promo codes).


Tip

PropertyValue
Field IDtip
LabelTip
TypeCurrency

Description: Tip amount (if tips enabled).


Tax

PropertyValue
Field IDtax
LabelTax
TypeCurrency

Description: Total tax amount.


Total

PropertyValue
Field IDtotal
LabelTotal
TypeCurrency

Description: Final order total.

Calculation: Subtotal + Delivery Fee - Discount + Tip + Tax


Payment Method

PropertyValue
Field IDpayment_method
LabelPayment
TypeString

Description: How the order was paid.

Methods (the stored value is set by the payment provider / POS, so exact casing varies by source — e.g. Card, iDEAL, Bancontact, Cash):

  • card - Credit/debit card
  • ideal - iDEAL (NL)
  • bancontact - Bancontact (BE)
  • cash - Cash payment

Note: there are no distinct apple_pay / google_pay payment_method values — the code never sets them. Wallet payments made through Stripe are recorded under the underlying card method, not as separate values.


Notes

PropertyValue
Field IDnotes
LabelOrder Notes
TypeText

Description: Special instructions from customer.


Order Actions

The back-office Transactions feature is read-and-manage only. There are no manual fulfilment-progression actions (no Confirm / Mark as Preparing / Mark as Ready / Complete buttons) — fulfilment status changes are driven by payment events and the ordering/KDS flow, not by the back office. The one manual action that can change payment state is Verify payment status. The TransactionController exposes: show, dataTable, stats, export, resendReceipt, retrySync, verifyPaymentStatus, deleteTestTransaction.

Resend Receipt

Re-send a receipt for a transaction (only when its status is "Complete").

Verify Payment Status

Re-check an order's true payment status at the payment provider and settle it if the money was actually taken.

  • Where: the order-detail 3-dots overflow menu (the same menu as Resend receipt).
  • When it is shown: only when the order status is Unpaid, Awaiting Payment, Awaiting Capture or Pending, and the payment method is not Cash.
  • Endpoint: POST /back-office/transactions/{id}/verify-payment-status (permission view-transactions).
  • Outcomes: settled, settlement_queued, already_settled or not_paid. Each raises its own toast; the first three reload the order.

This is the one back-office action that can move an order out of Unpaid.

Retry POS Sync

Re-dispatch the POS sync job for an order that failed to sync (shown under the "Stuck" tab).

Delete Test Transaction

Delete a transaction that was created in test mode. Live transactions cannot be deleted.

Transaction Lifecycle Timeline

Order detail renders a Transaction Lifecycle card whenever the order's lifecycle_logs array is non-empty. It is a read-only audit trail — nothing on it changes the order. The only interactive control is the Load More (N) button that appears when the timeline is collapsed.

  • One entry per lifecycle log, drawn as a Vuetify timeline. The dot colour comes from log.level: success, info, warning or error; anything else falls back to info.
  • Event label is the transactions.lifecycle.events.<event> translation. The template also writes || log.label, but that branch is unreachable: on a missing key the translation helper returns the key path itself, which is truthy. An event with no en entry therefore renders as the literal string transactions.lifecycle.events.<event>. Only the en file has to carry the key — a locale that is missing it falls back to English.
  • Elapsed delta from the first entry is shown next to each timestamp (+12s, +1m 4s, +1h 3m). It is blank for the first entry and for any non-positive or invalid delta.
  • Metadata chips: a tonal chip per shown metadata key. Two filters run first: (1) keys with no translation under transactions.lifecycle.metadata.<key> are dropped entirely, and (2) a given key=value pair is shown only the first time it appears in the timeline, so unchanging context (e.g. Provider: hendrickx on every Kassanet step) appears once and later entries show only what actually changed. The chip label is the translated key name (Provider, Order number, Idempotency key, …), not the raw key. Values: null and undefined are skipped; objects and arrays are JSON-encoded and truncated to 60 characters with an ellipsis. The de-duplication is computed over the currently visible entries, so pressing Load More re-evaluates it across the full timeline.
  • Long timelines collapse: with more than 8 entries only the 6 most recent render, behind a Load More (N) control.
  • On mobile the timestamp moves inline underneath the event label.
  • Backend cap: only the most recent 200 lifecycle entries are stored per transaction.

Timestamps render in the venue's timezone, taken from the order's location_timezone (the timezone on the order's location snapshot). Every time carries an explicit timezone label, so a support agent and the merchant read the same clock time off the same order — e.g. Jul 17, 20:40:36 CEST. The label's form follows the viewer's locale (an en-US viewer sees GMT+2 and a 12-hour clock for the same instant), but the underlying wall-clock time is always the venue's. If an order has no location_timezone (older orders with no location-snapshot timezone), the time falls back to the viewer's local timezone — still labelled — so the two cases are distinguishable on screen. The year is deliberately dropped from the format.

Refund Order (not currently available)

A RefundDialog exists in the shared OrderDetailView.vue, but it is not wired up: no refund button is ever rendered (the dialog's showRefundDialog flag is never set to true, so it is unreachable), and the store's refundOrder action is a simulated mock (a setTimeout that returns a fake REF_<timestamp> id, no Axios call, no backend endpoint). Refunds are not processed from the back office today; handle refunds through your payment provider (e.g. the Stripe dashboard).


Business Logic

Item counts: dishes vs article units

The Item column in the transactions list and its Excel export count dishes: Transaction::getQuantity() divides a per-person line by its party size and passes every other line through untouched. The Total Items stats card is a separate aggregation (sumTransactionQuantity) and still sums article units, so it can exceed the sum of the Item column when per-person lines are present.

Order Status

An order's lifecycle is reflected by the single status field. Typical paid orders move toward Complete; unpaid/awaiting-payment orders sit in the Unpaid / Awaiting Payment / Awaiting Capture / Awaiting Invoice / Pending states; fulfillment progresses through QueuedIn ProgressReady; and orders can end in Cancelled. These transitions are driven by payment events and the ordering/KDS flow rather than by manual back-office actions.


Filter Options

  • Date range
  • Status (via the All / Paid / Unpaid / Stuck POS tabs)
  • Channel
  • Dining option
  • Order type
  • Device
  • Location
  • Order number
  • Customer name
  • Customer email
  • Customer phone

Customer Impact

Order Confirmation

Customer receives:

  • Confirmation email
  • Order number
  • Estimated time
  • Order details

Order Updates

Customer notified when:

  • Order is placed/paid
  • Order ready
  • Order cancelled

Relations

Depends On

  • Locations: Orders belong to locations
  • Menu Items: Items in orders
  • Customers: Order attribution
  • Payments: Payment processing

Affects

  • Inventory: Stock decremented
  • Loyalty: Points earned
  • Reports: Sales data
  • KDS: Orders displayed

Business Rules

  • Only transactions with a status of "Complete" can have their receipt resent; attempting to resend for an incomplete transaction returns a 400 error with "Transaction is incomplete".
  • There are no separate payment-status and order-status fields — a single status enum captures both the payment and fulfillment state.
  • Refunds are not processed from the back office: no refund button is rendered (the RefundDialog in OrderDetailView.vue is unreachable), the store's refundOrder action is a mock, and there is no refund endpoint on the TransactionController.
  • There are no manual fulfilment-progression actions in the back office (no Confirm / Mark as Preparing / Mark as Ready / Complete). Fulfilment changes are driven by payment events and the ordering/KDS flow. The single exception on the payment side is Verify payment status, which can settle an order that was actually paid.
  • Only test-mode transactions can be deleted; deleting a live transaction is blocked with a 403 error, "Only test mode transactions can be deleted".
  • Transaction stats only count orders with status "Complete" and apply date-range filtering on the order_date field, with timezone-aware parsing via the request's timezone parameter (falling back to the app default timezone).
  • The spreadsheet export aborts with a 400 error if no transactions match the given filters, rather than producing an empty file.
  • Tip validation enforces mutual exclusivity: a transaction request cannot specify both a tip percentage and a tip amount; providing both throws an InvalidArgumentException.
  • When a customer places an order and no existing customer record matches their email or phone, a new customer and customer-vendor record are created automatically, and any applicable loyalty sign-up bonus is applied.

FAQs

  • "Why can I not resend a receipt for an order?" Receipts can only be resent for transactions with a "Complete" status; if the order is still pending or was cancelled, the resend option is blocked.

  • "How does the export handle large volumes of transactions?" The export processes records in chunks of 500, writing each batch sequentially to the spreadsheet until all matching records are retrieved, preventing memory issues.

  • "What timezone are transaction dates displayed in?" Transaction dates are stored in UTC (MongoDB UTCDateTime) but are displayed and filtered using the location's configured timezone.

  • "Can a transaction belong to multiple locations?" No, each transaction is tied to exactly one location via its location_id; filtering by location returns only that location's transactions.

  • "How are duplicate orders prevented?" Each transaction carries an idempotency key; before creating a new transaction, the system checks for an existing record with the same key and returns the existing order if found.

  • "Which sales channels can I filter transactions by, and what are they called?" Transactions carry an order channel; the recognised values are Online Ordering, Kiosk, POS, Table Qr Ordering, Uber Eats, Takeaway, and Shopify. Note the Channel filter dropdown currently only offers "All", "Online Ordering", and "Kiosk".

  • "What do the status tabs at the top of the Transactions list mean?" Four tabs: All, Paid, Unpaid, and Stuck POS. The Paid tab shows orders with status Complete, Unpaid shows orders with status Unpaid, and Stuck POS shows orders whose POS sync failed.

  • "What does the 'Stuck POS' tab show and how do I fix a stuck order?" The Stuck POS tab lists transactions that failed to sync to your POS. Each stuck row has a retry icon; clicking it (Retry Sync) clears the failed flag and re-dispatches the POS sync job so the order is sent again.

  • "Why can't I resend a receipt for some orders?" Resend Receipt only works for transactions whose status is Complete. For any other status you get "Transaction is incomplete", so pending, unpaid, or cancelled orders can't have a receipt resent.

  • "What columns are included when I export transactions?" The Excel export includes Order No, Date, Location, Customer, Payment Status, Total, Items, Dining Option, Type, Device, Receipt No, and Channel.

  • "My export downloaded nothing / gave an error — why?" If no transactions match your current filters, the export stops with a "No transactions found" error instead of an empty file. Adjust your date range, location, or filters so at least one order matches, then export again.

  • "What do the summary cards at the top mean?" Normally three cards: Completed Transactions (count of fully paid orders), Total Items (quantity sold), and Total Collected (amount collected), all scoped to your current filters (date range, location, etc.). A fourth card, Total Pending, appears when Invoice Collection is enabled for your account; with it the row switches from a 3-up to a 4-up layout. Total Pending sums the totals of orders awaiting invoice or awaiting payment and, unlike the other three, is not limited by your date range — an invoice raised months ago is still outstanding today. It does respect the location filter. (Verified: card definitions in upvendo-backoffice src/utils/constants.ts lines 153-188 -- { id: 4, title: 'Total Pending', key: 'total_pending' }; gating and layout in src/views/transactions/Transactions.vue lines 107-119 and 571-575; feature flag getter isInvoiceCollectionEnabled in src/store/modules/app.ts line 323, defaulting to false at line 246; the figure itself in upvendo-backend app/Services/BackOffice/TransactionService.php lines 234-253.)

  • "Can I filter transactions to a single location?" Yes. If your account has more than one location, a Location filter appears; each transaction belongs to exactly one location. The export and stats respect the same filter.

  • "Which transaction date and timezone are used for filtering?" Date filtering uses the order date, evaluated in your location's configured timezone. Dates are stored in UTC but parsed against the location timezone, so "today" reflects your local trading day.

  • "Can I delete a transaction?" Only test-mode transactions can be deleted; deleting a real (live) transaction is blocked with "Only test mode transactions can be deleted".

Troubleshooting

Problem: Order stuck in an unpaid/pending status

Causes:

  1. Payment not completed
  2. Payment failed
  3. System error

Solutions:

  1. Check the payment in your payment provider (e.g. Stripe)
  2. Contact the customer to retry payment
  3. Note: the back office has no "manually confirm" action — status is updated by the payment flow, not manually

Problem: Order stuck (failed POS sync)

Causes:

  1. POS offline or unreachable
  2. Transient sync error
  3. POS integration misconfiguration

Solutions:

  1. Open the "Stuck" tab, which lists orders whose POS sync failed
  2. Click the retry icon (Retry Sync) on the row to re-dispatch the POS sync job
  3. If it keeps failing, check the POS connection and integration settings

Problem: Order not appearing on KDS

Causes:

  1. KDS not connected
  2. Channel filter on KDS
  3. Order status not yet eligible for KDS

Solutions:

  1. Check KDS connection
  2. Review KDS channel settings
  3. Confirm the order has reached a status that routes it to the KDS

Problem: Customer didn't receive an order email

Causes:

  1. Wrong email address
  2. Email in spam
  3. Email service issue

Solutions:

  1. Verify the email address on the order
  2. Ask the customer to check spam
  3. For receipts on completed orders, use Resend Receipt from the order detail (there is no separate "resend confirmation" action)

Problem: Need to refund an order

Note: Refunds are not processed from the back office today — the refund UI is disabled and there is no refund endpoint.

Solutions:

  1. Process the refund directly in your payment provider (e.g. the Stripe dashboard)
  2. For cash payments, handle the refund at the point of sale
  3. Contact your payment provider if the refund fails

Examples

Online Order (Delivery)

json
{
  "order_number": "1234",
  "status": "Complete",
  "order_channel": "Online Ordering",
  "dining_option": "Delivery",
  "created_at": "2024-01-15T12:30:00Z",
  "scheduled_time": "2024-01-15T13:15:00Z",
  "customer": {
    "name": "Jan de Vries",
    "email": "jan@example.com",
    "phone": "+31612345678"
  },
  "delivery_address": {
    "address_line_1": "Kalverstraat 123",
    "city": "Amsterdam",
    "postal_code": "1012 AB"
  },
  "items": [
    {
      "name": "Margherita Pizza",
      "quantity": 1,
      "unit_price": 12.50,
      "modifiers": [
        { "name": "Large", "price": 3.00 }
      ],
      "line_total": 15.50
    },
    {
      "name": "Garlic Bread",
      "quantity": 1,
      "unit_price": 4.50,
      "line_total": 4.50
    }
  ],
  "subtotal": 20.00,
  "delivery_fee": 3.50,
  "discount": 0,
  "tip": 2.00,
  "tax": 1.89,
  "total": 27.39,
  "payment_method": "ideal"
}

Kiosk Order (For Here)

json
{
  "order_number": "42",
  "status": "In Progress",
  "order_channel": "Kiosk",
  "dining_option": "For Here",
  "created_at": "2024-01-15T12:45:00Z",
  "items": [
    {
      "name": "Cheeseburger",
      "quantity": 2,
      "unit_price": 8.95,
      "modifiers": [
        { "name": "Extra Cheese", "price": 1.50 }
      ],
      "line_total": 20.90
    },
    {
      "name": "French Fries",
      "quantity": 2,
      "unit_price": 3.50,
      "line_total": 7.00
    },
    {
      "name": "Cola",
      "quantity": 2,
      "unit_price": 2.50,
      "line_total": 5.00
    }
  ],
  "subtotal": 32.90,
  "tip": 5.00,
  "tax": 2.81,
  "total": 40.71,
  "payment_method": "card"
}

Order Summary View

Order #1234 - Jan de Vries
Status: Complete
Channel: Online Ordering
Type: Delivery

Items:
  1x Margherita Pizza (Large)     €15.50
  1x Garlic Bread                  €4.50
                          ─────────────
  Subtotal                        €20.00
  Delivery Fee                     €3.50
  Tip                              €2.00
  Tax                              €1.89
                          ─────────────
  Total                           €27.39

Payment: iDEAL ✓ Paid

Delivery: Kalverstraat 123, Amsterdam
Scheduled: 13:15