Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ Prove the refund use case with working platform integrations.
| Integration | Description | Status | Maintainer |
|-------------|-------------|--------|------------|
| [Shopify Refund Guardrail](examples/ecommerce/shopify/) | Complete Shopify app with APort verification | ✅ Active | Community |
| [WooCommerce Plugin](examples/ecommerce/woocommerce/) | WordPress plugin for order/refund verification | 🚧 In Progress | Community |
| [WooCommerce Plugin](examples/ecommerce/woocommerce/) | WordPress plugin for order/refund verification | ✅ Active | Community |
| [Stripe Connect Verification](examples/ecommerce/stripe/) | Webhook handler for Stripe Connect payouts | 📋 Planned | Community |

### 🔧 **Developer Experience Tools**
Expand Down
72 changes: 72 additions & 0 deletions examples/ecommerce/woocommerce/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# APort WooCommerce Guardrail

WordPress/WooCommerce plugin example that verifies commerce actions with APort before recording order and refund decisions.

## Features

- Settings page under **Settings > APort WooCommerce** for API key, base URL, and policy ids.
- Checkout-order verification using `commerce.order.v1` by default.
- Refund verification using `payments.refund.v1` by default.
- Agent id discovery from `X-Agent-ID`, `X-APort-Agent-ID`, or the order meta key `_aport_agent_id`.
- Order notes for allow, deny, and transport-error outcomes.
- Last APort decision, policy, and agent id saved as order metadata.
- Authenticated REST endpoint for preflight refund checks: `POST /wp-json/aport-woocommerce/v1/verify-refund`.
- PHPUnit coverage for agent-id extraction and verification context builders.

## Install

1. Copy `examples/ecommerce/woocommerce` into `wp-content/plugins/aport-woocommerce`.
2. Activate **APort WooCommerce Guardrail** in WordPress.
3. Open **Settings > APort WooCommerce**.
4. Add your APort API key and confirm policy ids:
- Orders: `commerce.order.v1`
- Refunds: `payments.refund.v1`

## Agent identity

The plugin checks for an agent id in this order:

1. `X-Agent-ID` request header.
2. `X-APort-Agent-ID` request header.
3. WooCommerce order meta `_aport_agent_id`.

This keeps the example usable with API-driven checkouts, custom storefronts, and admin-created test orders.

## Refund preflight

For custom refund flows, call the REST endpoint before creating the refund:

```http
POST /wp-json/aport-woocommerce/v1/verify-refund
Content-Type: application/json

{
"order_id": 42,
"amount": 12.50,
"agent_id": "agent_123"
}
```

Only users with `manage_woocommerce` can call the endpoint. The regular WooCommerce `woocommerce_order_refunded` hook also records a post-refund verification note so manual admin refunds are auditable.

## Development

```bash
composer install
composer test
composer lint
```

Without Composer dependencies, syntax can still be checked directly:

```bash
find . -name '*.php' -not -path './vendor/*' -print0 | xargs -0 -n1 php -l
```

## Validation checklist

- Configure a test API key in plugin settings.
- Place a test order with `X-Agent-ID` or `_aport_agent_id`.
- Confirm the order receives an APort order-verification note.
- Call `verify-refund` with a test amount and confirm the JSON decision.
- Create a WooCommerce refund and confirm a refund-verification order note is recorded.
36 changes: 36 additions & 0 deletions examples/ecommerce/woocommerce/aport-woocommerce.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php
/**
* Plugin Name: APort WooCommerce Guardrail
* Description: Verifies WooCommerce order and refund actions with APort policy checks.
* Version: 0.1.0
* Author: APort Community
* License: MIT
* Requires at least: 6.0
* Requires PHP: 7.4
* WC requires at least: 8.0
*
* @package APortWooCommerce
*/

if (!defined('ABSPATH')) {
exit;
}

define('APORT_WOOCOMMERCE_VERSION', '0.1.0');
define('APORT_WOOCOMMERCE_PATH', plugin_dir_path(__FILE__));

require_once APORT_WOOCOMMERCE_PATH . 'includes/class-aport-woocommerce-client.php';
require_once APORT_WOOCOMMERCE_PATH . 'includes/class-aport-woocommerce.php';

add_action('plugins_loaded', static function () {
if (!class_exists('WooCommerce')) {
add_action('admin_notices', static function () {
echo '<div class="notice notice-warning"><p>';
echo esc_html__('APort WooCommerce Guardrail requires WooCommerce to be active.', 'aport-woocommerce');
echo '</p></div>';
});
return;
}

APort_WooCommerce::instance()->init();
});
21 changes: 21 additions & 0 deletions examples/ecommerce/woocommerce/composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "aport/woocommerce-guardrail-example",
"description": "WooCommerce plugin example for APort order and refund verification.",
"type": "wordpress-plugin",
"license": "MIT",
"require": {
"php": ">=7.4"
},
"require-dev": {
"phpunit/phpunit": "^9.6"
},
"autoload": {
"classmap": [
"includes/"
]
},
"scripts": {
"test": "phpunit",
"lint": "find . -name '*.php' -not -path './vendor/*' -print0 | xargs -0 -n1 php -l"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php
/**
* APort API client wrapper.
*
* @package APortWooCommerce
*/

if (!defined('ABSPATH')) {
exit;
}

class APort_WooCommerce_Client
{
private $api_key;
private $base_url;

public function __construct($api_key, $base_url = 'https://api.aport.io')
{
$this->api_key = trim((string) $api_key);
$this->base_url = rtrim(trim((string) $base_url), '/');
}

public function verify($policy_id, $agent_id, array $context)
{
if ($this->api_key === '') {
return new WP_Error('aport_missing_api_key', 'APort API key is not configured.');
}

if (trim((string) $agent_id) === '') {
return new WP_Error('aport_missing_agent_id', 'Agent id is required for APort verification.');
}

$response = wp_remote_post(
$this->base_url . '/v1/verify',
array(
'timeout' => 10,
'headers' => array(
'Authorization' => 'Bearer ' . $this->api_key,
'Content-Type' => 'application/json',
'Accept' => 'application/json',
),
'body' => wp_json_encode(
array(
'policy_id' => $policy_id,
'agent_id' => $agent_id,
'context' => $context,
)
),
)
);

if (is_wp_error($response)) {
return $response;
}

$status = (int) wp_remote_retrieve_response_code($response);
$body = json_decode((string) wp_remote_retrieve_body($response), true);
if (!is_array($body)) {
$body = array();
}

if ($status < 200 || $status >= 300) {
return new WP_Error(
'aport_http_error',
sprintf('APort verification failed with HTTP %d.', $status),
array('status' => $status, 'body' => $body)
);
}

$allowed = isset($body['allowed']) ? (bool) $body['allowed'] : (isset($body['decision']) && $body['decision'] === 'allow');

return array(
'allowed' => $allowed,
'decision' => isset($body['decision']) ? (string) $body['decision'] : ($allowed ? 'allow' : 'deny'),
'reason' => isset($body['reason']) ? (string) $body['reason'] : '',
'raw' => $body,
);
}
}
Loading