Add_filter | Form, Server, Direct & Pi | Sage 50 Basket Format
Out of the box, our Opayo plugin is built to talk to Opayo. Some merchants required a Sage 50-friendly basket format, but didn’t have a developer to work on a custom function using our basket filter code. We don’t have a Sage 50 account to test against, so we worked with a merchant to produce this drop-in function that formats the basket the way Sage 50 expects.
Overview
This filter replaces Opayo’s standard basket string with a Sage 50-compatible format.
Data flow: WooCommerce Order → This Function → Opayo → Sage 50
Format produced:
- Line count prefix: <number_of_lines>:
- Product lines: [SKU]Name:<qty>:<unit_ex_vat>:<unit_vat>:<unit_inc_vat>:<line_inc_vat>
- Delivery line: Delivery:000:000:000:000:<delivery_total_inc_vat>
- Discount line: Discount:000:000:000:000:-<discount_total_inc_vat>
- Amounts: dot decimal (.) with exactly 2 decimals
- Tax/VAT: embedded per product line via <unit_vat>
How to Use
- You can drop the snippet into your child theme’s functions.php file, a site-specific plugin, or a safe “Code Snippets” plugin, whichever suits your setup best.
- Make a test order and verify the basket line string in your gateway request (enable our plugin’s debug logs to see it).
- Confirm with your Sage 50 import that lines are arriving as expected.
The Filter & Callback
- Filter: ag_opayo_modify_basket_full_string
- Signature: function ( string $basket, WC_Order $order ): string|null
- When it runs: Right before we hand the basket to Opayo.
- Return: The full basket string (or null to fall back to default behaviour).
Drop-in Code (copy/paste)
/**
* Opayo Basket Payload Modifier for Sage 50 Integration
*
* This function modifies WooCommerce order data into Opayo's required basket format,
* which is then passed to Sage 50 accounting software for proper transaction recording.
*
* Data Flow: WooCommerce Order → This Function → Opayo → Sage 50
*
* Current Format:
* - Line count prefix: <number_of_lines>:
* - Product lines: [SKU]Name:<qty>:<unit_ex_vat>:<unit_vat>:<unit_inc_vat>:<line_inc_vat>
* - Delivery: Delivery:000:000:000:000:<delivery_total_with_vat>
* - Discount: Discount:000:000:000:000:-<discount_total_with_vat>
* - All amounts use "." decimal separator with exactly 2 decimals
* - Tax/VAT is embedded per product line via <unit_vat>
*
*/
add_filter('ag_opayo_modify_basket_full_string', 'modify_basket_payload', 10, 2);
function modify_basket_payload($basket, $order)
{
$basket = ''; // Reset basket
$item_loop = 0; // Initialize item loop count
$basket_lines = array(); // Array to store basket lines
// Process each product/item in the order
foreach ($order->get_items() as $item) {
if ($item['qty']) {
$item_loop++; // Increment item loop for each item
$product = $item->get_product();
// Get item name and add SKU if available (matching original working format)
$item_name = esc_attr($item->get_name());
if ($product && $product->get_sku()) {
$item_name = '[' . preg_replace("/[^a-zA-Z0-9-.]+/", "", $product->get_sku()) . ']' . $item_name;
}
// Add product meta data (matching original working format)
$meta_display = '';
foreach ($item->get_formatted_meta_data() as $meta_key => $meta) {
$meta_display .= ' ( ' . $meta->display_key . ':' . $meta->value . ' )';
}
if ($meta_display) {
$item_name .= $meta_display;
}
// Sanitize item name (matching original working format)
$item_name = str_replace(':', ' ', $item_name);
// Build the basket line using Opayo format: [SKU]Name:<qty>:<unit_ex_vat>:<unit_vat>:<unit_inc_vat>:<line_inc_vat>
$unit_ex_vat = $order->get_item_total($item, false);
$unit_vat = $order->get_item_tax($item) / $item['qty']; // Unit VAT amount
$unit_inc_vat = $unit_ex_vat + $unit_vat;
$line_inc_vat = $order->get_line_total($item, true);
$basket_lines[] = $item_name // [SKU]Name
. ':' . $item['qty'] // Quantity
. ':' . number_format($unit_ex_vat, 2, '.', '') // Unit ex VAT
. ':' . number_format($unit_vat, 2, '.', '') // Unit VAT
. ':' . number_format($unit_inc_vat, 2, '.', '') // Unit inc VAT
. ':' . number_format($line_inc_vat, 2, '.', ''); // Line inc VAT
}
}
// Process any additional fees (e.g., service charges, handling fees)
if (sizeof($order->get_fees()) > 0) {
foreach ($order->get_fees() as $order_item) {
$item_loop++;
// Format fees as product lines with quantity 1 and no VAT
$basket_lines[] = str_replace(':', ' ', $order_item['name'])
. ':1:' . number_format($order_item['line_total'], 2, '.', '') // Unit price
. ':0.00:' . number_format($order_item['line_total'], 2, '.', '') // Unit VAT (0 for fees)
. ':' . number_format($order_item['line_total'], 2, '.', ''); // Line total
}
}
// Add shipping details - Format: Delivery:000:000:000:000:<delivery_total_with_vat>
$total_shipping = $order->get_shipping_total();
$shipping_tax = $order->get_shipping_tax();
if ($total_shipping > 0) {
$delivery_total = $total_shipping + $shipping_tax;
$basket_lines[] = 'Delivery:000:000:000:000:' . number_format($delivery_total, 2, '.', '');
$item_loop++;
}
// Add discount - Format: Discount:000:000:000:000:-<discount_total_with_vat>
if ($order->get_total_discount() > 0) {
$basket_lines[] = 'Discount:000:000:000:000:-' . number_format($order->get_total_discount(), 2, '.', '');
$item_loop++;
}
// Build final basket string with line count prefix
$basket = $item_loop . ':' . implode(':', $basket_lines);
// Clean up any unwanted characters
$basket = str_replace("\n", "", $basket);
$basket = str_replace("\r", "", $basket);
// Check basket length limit (Opayo has a 7500 character limit)
if (mb_strlen($basket) > 7500) {
AG_SS_Helpers::ag_log('[Basket Modifier] Basket too long, returning NULL', 'debug', 'yes');
$basket = NULL;
}
// Return the formatted basket payload for Opayo → Sage 50 integration
return $basket;
}Example Output
3:[ABC123]Blue T-Shirt ( Size:L ) :2:12.50:2.50:15.00:30.00:Delivery:000:000:000:000:4.79:Discount:000:000:000:000:-5.00
- 3 lines total (1 product + Delivery + Discount)
- Product line is [SKU]Name:qty:unit_ex:unit_vat:unit_inc:line_inc
- Delivery is inc VAT total
- Discount is a negative inc VAT total
Compatibility Notes
- Works with standard/simple/variable products.
- Fees are included as individual lines (qty 1).
- Removes standalone “Tax” and “Order Total” lines to suit Sage 50’s importer.
- Respects Opayo’s basket length limit (~7,500 chars).
Troubleshooting
- Wrong amounts showing in Sage 50: Check that VAT settings in WooCommerce match Sage 50 (rates, inclusive/exclusive display).
- Basket too long: Very large orders with many variations/options may exceed Opayo’s limit. Consider shortening product names or trimming verbose meta.
- Colons in names break the format: The snippet strips : from names. Avoid pasting colons into product/fee titles.
FAQ
Can I change the wording of “Delivery” or “Discount”?
Yes, edit the two label strings in the snippet, but keep the :000:000:000:000: structure intact.
We need meta in a different format.
Adjust the section that builds $meta_display. Keep colons out of labels/values.
Does this affect non-Sage 50 sites?
It only changes the basket string at the Opayo hand-off. If you remove the snippet, behaviour reverts to default.
Was this helpful?