WordPress Payment Implementation

1. Overview

This document describes the technical implementation of the event-payment-plugin-woocommerce plugin, which handles payment processing and order synchronisation with the admin-service.

2. Plugin Architecture

2.1. Trait-Based Composition

The plugin uses a trait-based architecture where EPAWooHandler is the main class that composes multiple traits:

class EPAWooHandler {
    use EPAAdmin;                    // Settings page
    use EPAConnTest;                 // API connectivity validation
    use EPACustomEndpoint;           // REST API registration
    use EPAOnPaymentProcessed;       // Post-purchase sync
    use EPAOnStatusUpdate;           // Order status changes
    use EPAWooCustomTemplates;       // Template overrides
    use MBox_TribeEvent;             // Event selector metabox
    use EPAOnOrderCancel;            // Cancellation handling
    use EPAFallbackGuzzleConn;       // HTTP fallback
}

2.2. Hook Registration

The plugin registers hooks for the complete WooCommerce order lifecycle:

Hook Priority Handler

woocommerce_before_pay_action

99

EpaUpdateOrderBillingBeforePayment()

woocommerce_thankyou

10

EpaOnPaymentProcessed()

woocommerce_order_status_pending

10

EPAOnOrderStatusUpdate()

woocommerce_order_status_processing

10

EPAOnOrderStatusUpdate()

woocommerce_order_status_completed

10

EPAOnOrderStatusUpdate()

woocommerce_order_status_on-hold

10

EPAOnOrderStatusUpdate()

woocommerce_order_status_cancelled

10

EPAOnOrderStatusUpdate()

woocommerce_order_status_refunded

10

EPAOnOrderStatusUpdate()

woocommerce_order_status_failed

10

EPAOnOrderStatusUpdate()

3. REST Endpoint Implementation

3.1. Endpoint Registration

add_action('rest_api_init', function () {
    register_rest_route('payment-api/v1', '/order/event/create/', [
        'methods'  => 'POST',
        'callback' => [$this, 'handleOrderCreation'],
        'permission_callback' => '__return_true'
    ]);
});

3.2. Payload Validation

The endpoint performs multi-layer validation:

  1. API key validation - Checks payload contains valid API key

  2. Payload type detection - Determines if standard event or membership

  3. Required field validation - Verifies all required fields present

  4. Product validation - Confirms products exist in WooCommerce

private function validatePayload($payload) {
    if (empty($payload['apiKey']) ||
        $payload['apiKey'] !== get_option('epa_callback_api_key')) {
        return new WP_Error('unauthorized', 'Invalid API key', ['status' => 401]);
    }
    // Additional validation...
}

3.3. Order Creation Flow

function handleStandardEventData($payload) {
    // 1. Create WooCommerce order
    $order = wc_create_order(['status' => 'draft']);

    // 2. Store admin order reference
    $order->update_meta_data('epa_admin_portal_order_id', $payload['orderId']);
    $order->update_meta_data('event_type', 'standard');

    // 3. Add line items for each participant
    foreach ($payload['participants'] as $participant) {
        $product = wc_get_product($participant['productId']);
        $itemId = $order->add_product($product, 1);

        // Add participant metadata
        wc_add_order_item_meta($itemId, 'Entrant ID', $participant['id']);
        wc_add_order_item_meta($itemId, 'Entrant Name', $participant['name']);
        // ...
    }

    // 4. Calculate totals
    $order->set_total($payload['total'] - $payload['discount']);
    $order->save();

    return [
        'redirectURL' => $order->get_checkout_payment_url(),
        'orderNumber' => $order->get_id()
    ];
}

4. Status Synchronisation

4.1. API Client Integration

The plugin uses a generated PHP API client from the OpenAPI specification:

class AdminServiceClient {
    private static $instance = null;
    private $orderApi;
    private $membershipApi;

    public static function getInstance() {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    public function getOrderResourceApi() {
        if ($this->orderApi === null) {
            $config = Configuration::getDefaultConfiguration()
                ->setApiKey('X-API-KEY', get_option('epa_admin_api_key'))
                ->setHost(get_option('epa_admin_api_url'));

            $this->orderApi = new OrderResourceExApi(
                new GuzzleHttp\Client(['timeout' => 15]),
                $config
            );
        }
        return $this->orderApi;
    }
}

4.2. DTO Mapping

function createParticipantOrderDTO($order, $status) {
    $dto = new ParticipantOrderDTO();
    $dto->setId($order->get_meta('epa_admin_portal_order_id'));
    $dto->setStatus($this->mapOrderStatus($order->get_status()));
    $dto->setPaymentReference($order->get_id());

    // Extract participant IDs from line items
    $participantIds = [];
    foreach ($order->get_items() as $item) {
        $entrantId = wc_get_order_item_meta($item->get_id(), 'Entrant ID');
        if ($entrantId) {
            $participantIds[] = $entrantId;
        }
    }
    $dto->setParticipantIds($participantIds);

    return $dto;
}

5. Order Metadata Structure

5.1. Order Level

Meta Key Description

epa_admin_portal_order_id

Links to admin-service order

epa_order_synced

Flag to prevent duplicate syncs

epa_order_sync_response

Last API response (debugging)

event_type

standard or membership

event_id

Event ID from admin-service

event_name

Event name

event_date

Event date

5.2. Line Item Level (Events)

Meta Key Description

Entrant ID

Participant ID

Entrant Name

Full name

Entrant DOB

Date of birth

Category

Age category

Race Type(s)

Comma-separated race type names (plain text, no IDs)

Blank values are never written: the plugin skips any entrant field that arrives empty, so the corresponding meta row simply does not exist on that line item.

5.2.1. Day-licence line items

A participant who opted into a CSA day licence produces a second line item on the same order (same Entrant ID, different product — the event’s configured day-licence product, resolved from Event.productDayLicense falling back to Series.productDayLicense). The admin-service payload deliberately omits category and raceTypes for that entry, so the day-licence line renders with the participant’s name and date of birth only. This is what visually distinguishes a day licence from an event entry on the order, invoice, and e-mail — each day licence is a named, quantity-1 line item.

5.3. Line Item Level (Memberships)

Meta Key Description

_membership_id

Member ID(s)

Type

Membership type name

Period

Membership period

Name

Member name

6. Error Handling

6.1. Logging Strategy

if (defined('EPA_DEBUG') && EPA_DEBUG) {
    $logger = wc_get_logger();
    $logger->info('Order sync initiated', [
        'source' => 'event-payment-api',
        'order_id' => $order->get_id(),
        'admin_order_id' => $adminOrderId
    ]);
}

Logs are written to:

  • WooCommerce logs: wp-content/uploads/wc-logs/

  • Plugin debug log: event-payment-plugin/debug.log

6.2. Order Notes

All API interactions are recorded as order notes for admin visibility:

$order->add_order_note(sprintf(
    'Admin-service sync: %s (Status: %s)',
    $response->success ? 'Success' : 'Failed',
    $order->get_status()
));

7. User Management

7.1. Auto-Creation

function createOrLinkUser($email, $order) {
    $user = get_user_by('email', $email);

    if (!$user) {
        $userId = wp_create_user(
            sanitize_email($email),
            wp_generate_password(),
            $email
        );
        $user = get_user_by('ID', $userId);
    }

    $order->set_customer_id($user->ID);
    $order->save();

    return $user;
}

7.2. Auto-Login

After successful payment, if the order email matches an existing account:

if (is_user_logged_in() === false) {
    $user = get_user_by('email', $order->get_billing_email());
    if ($user) {
        wp_set_current_user($user->ID);
        wp_set_auth_cookie($user->ID);
    }
}