Store Projects

Project: Sales-Reporting Assistant

⏱ 15 min

This project queries orders with wc_get_orders(), aggregates the numbers in plain PHP, and only then hands a small, pre-summarized dataset to wp_ai_client_prompt() to turn into a narrative. That ordering matters: the model narrates a report your code already computed correctly, it never does the arithmetic itself.

What you'll learn in this lesson
Aggregating orders into a report structure
Totals, order counts, and top products, computed in PHP before any AI call.
Why you pre-aggregate instead of feeding raw orders to the model
Token cost, accuracy, and staying within reasonable prompt sizes.
Redacting customer PII before it reaches an AI provider
Names and emails have no place in a sales-trend prompt.
Returning both the raw numbers and the narrative
So the summary is always checkable against the underlying data.
Prerequisites

WordPress 7.0+ with an AI provider configured. WooCommerce with completed order history covering more than a few days, a single day of test data won’t produce a meaningful trend to summarize. This lesson doesn’t depend on WooCommerce 10.9’s abilities, it uses wc_get_orders() directly.

Step 1: Aggregating orders in PHP, not in the prompt

// File: my-plugin.php
function my_plugin_build_sales_report( $days ) {
	$orders = wc_get_orders( array(
		'status'       => array( 'completed', 'processing' ),
		'date_created' => '>' . ( time() - $days * DAY_IN_SECONDS ),
		'limit'        => -1,
	) );

	$total_revenue = 0;
	$order_count   = count( $orders );
	$product_counts = array();

	foreach ( $orders as $order ) {
		$total_revenue += (float) $order->get_total();
		foreach ( $order->get_items() as $item ) {
			$name = $item->get_name();
			$product_counts[ $name ] = ( $product_counts[ $name ] ?? 0 ) + $item->get_quantity();
		}
	}

	arsort( $product_counts );

	return array(
		'days'           => $days,
		'order_count'    => $order_count,
		'total_revenue'  => round( $total_revenue, 2 ),
		'top_products'   => array_slice( $product_counts, 0, 5, true ),
	);
}

Every number in this report is computed with plain PHP arithmetic over real order data, nothing here depends on the AI model getting math right.

Step 2: Registering the ability, redacting before the AI call

// File: my-plugin.php
add_action( 'wp_abilities_api_init', function () {
	wp_register_ability(
		'my-plugin/summarize-sales-trends',
		array(
			'label'         => __( 'Summarize sales trends', 'my-plugin' ),
			'description'   => __( 'Aggregates recent order data into totals and top products, then generates a plain-language summary. No customer names or emails are included.', 'my-plugin' ),
			'category'      => 'store-inventory',
			'input_schema'  => array(
				'type'       => 'object',
				'properties' => array(
					'days' => array( 'type' => 'integer', 'default' => 7 ),
				),
			),
			'output_schema' => array(
				'type'       => 'object',
				'properties' => array(
					'report'  => array( 'type' => 'object' ),
					'summary' => array( 'type' => 'string' ),
				),
			),
			'permission_callback' => function () {
				return current_user_can( 'view_woocommerce_reports' );
			},
			'execute_callback' => function ( $input ) {
				$report = my_plugin_build_sales_report( $input['days'] ?? 7 );

				$prompt = sprintf(
					"Summarize this WooCommerce sales data in three plain-language sentences, " .
					"no customer names or emails are present, do not invent any: %s",
					wp_json_encode( $report )
				);

				$summary = wp_ai_client_prompt( $prompt )->generate_text();

				return array( 'report' => $report, 'summary' => trim( $summary ) );
			},
			'meta' => array(
				'annotations' => array( 'readonly' => true ),
				'mcp'         => array( 'public' => false ),
			),
		)
	);
} );
Aggregate before you prompt, never send raw order objects

wc_get_orders() results carry billing names, emails, and addresses. This ability never passes an order object itself to wp_ai_client_prompt(), only the aggregated $report array, which by construction contains product names and numbers, nothing customer-identifiable. Treat “did I aggregate first” as a mandatory check any time an ability sends order-derived data to a third-party AI provider.

Note also that view_woocommerce_reports rather than manage_woocommerce is used here, it’s the more precisely scoped capability for a read-only reporting feature, letting you grant this to roles that shouldn’t necessarily be able to edit orders or products.

Step 3: Returning both the numbers and the narrative

The output schema intentionally includes report alongside summary. Anyone reading the AI-generated sentence can check it against the actual numbers in the same response, rather than trusting the narrative blindly.

Test it

wp-env run cli wp eval '
$ability = wp_get_ability( "my-plugin/summarize-sales-trends" );
$result  = $ability->execute( array( "days" => 30 ) );
echo $result["summary"] . "\n";
print_r( $result["report"] );
'

Cross-check the printed summary against the printed report, they should agree exactly.

Feeding too many orders into one prompt

On a busy store, thirty days of raw order data can be thousands of line items, well past what’s reasonable to hand a model even after light aggregation. Cap the date range sensibly, and if you need longer-range trends, aggregate into weekly or monthly buckets in PHP first rather than widening the raw dataset the prompt sees.

Recap

The sales-reporting assistant computes real totals and top products with wc_get_orders() and plain PHP arithmetic, redacts customer-identifiable fields before anything reaches an AI provider, and uses wp_ai_client_prompt() purely to phrase an already-correct aggregate as a readable summary, returned alongside the raw numbers so the narrative stays checkable.

Resources & further reading

← Project: Order-Management Automation Project: Natural-Language Store Manager →