Skip to content

Architecture

Module overview

src/
├── Dixa.php                    # Plugin bootstrap — registers REST routes, boots modules
├── DixaDataGenerator.php       # Builds the customer payload for the REST response
├── API/
│   ├── API.php                 # Façade — instantiates and exposes resource clients
│   ├── APIClient.php           # Guzzle client preconfigured for https://dev.dixa.io/v1/
│   ├── Requests/Models/        # Request models (EndUser, AnonymizationModel, …)
│   └── Resources/
│       ├── Clients/            # EndUsers, Anonymization, CustomerServiceStatus
│       └── Schemas/            # Response schemas (EndUser, ErrorSchema, …)
├── CLI/
│   ├── CLI.php                 # Registers WP-CLI command groups
│   └── Commands/               # Anonymization, Users
├── Data/
│   ├── DixaExporter.php        # create_or_patch helper; tracks export state via user meta
│   ├── DixaCustomer.php        # Customer data mapper
│   ├── DixaOrder.php           # Order data mapper
│   └── …
├── Frontend/
│   └── CustomerServiceStatusShortCode.php  # [dixa-status] shortcode
└── Modules/
    ├── Jobs/CustomerServiceStatus.php      # Action Scheduler job — polls Dixa every 30 min
    ├── Watchers/UserWatcher.php            # WooCommerce hooks → async sync via Action Scheduler
    └── OrderStatus/                        # Order status REST endpoint for Dixa MIM Tools
        ├── REST/RouteController.php        # Registers POST /dixa/v1/order-status
        ├── Auth/BasicAuthGuard.php         # Basic Auth against .env credentials + rate limiting
        ├── RateLimiter.php                 # Transient-backed attempt counter
        ├── OrderLookupService.php          # order_number + email/phone verification
        ├── EstimatedDelivery.php           # Default per-carrier business-day estimate + `dixa_order_status_estimated_delivery` filter
        └── Data/OrderStatusData.php        # Response DTO, `dixa_order_status_data` filter

Data flow — customer sync

sequenceDiagram
    participant WC as WooCommerce
    participant Watcher as UserWatcher
    participant AS as Action Scheduler
    participant Exporter as DixaExporter
    participant Dixa as Dixa API

    WC->>Watcher: woocommerce_created_customer
    Watcher->>AS: as_enqueue_async_action(dixa/scheduler/create-customer)
    AS->>Exporter: export_customer($customer_id)
    Exporter->>Dixa: POST /endusers (create)
    alt EmailExists / PhoneNumberExists
        Dixa-->>Exporter: error
        Exporter->>Dixa: GET /endusers?email= (lookup)
        Exporter->>Dixa: PATCH /endusers/{id}
    end
    Exporter->>WC: update_user_meta(exported_to_dixa, 1)

Customer service status flow

The CustomerServiceStatus job runs every 30 minutes via Action Scheduler. It calls the Dixa business-hours API and writes the result to the dixa_customer_service_status option, which the [dixa-status] shortcode reads synchronously.

REST API endpoints

  • Dixa.php registers a read-only customer lookup endpoint under wc/v3/dixa/v1/list, gated by WooCommerce's own REST auth/capabilities.
  • Modules/OrderStatus/REST/RouteController.php registers POST /dixa/v1/order-status for Dixa MIM Tools, deliberately outside the wc/v3 namespace so it isn't intercepted by WooCommerce's REST auth — it authenticates and rate-limits itself instead (see flow below). It also hooks rest_authentication_errors at PHP_INT_MAX, clearing any conflicting global Basic Auth rejection (e.g. from Application Passwords or a security plugin also reading PHP_AUTH_USER) for its own path only, so its own BasicAuthGuard always gets the final say regardless of what else is installed on the site.

See api.md for both endpoints' request/response reference.

Order status lookup flow

sequenceDiagram
    participant Mim as Dixa MIM Tools
    participant Route as OrderStatus\REST\RouteController
    participant Auth as BasicAuthGuard
    participant Limiter as RateLimiter
    participant Lookup as OrderLookupService
    participant WC as WooCommerce

    Mim->>Route: POST /dixa/v1/order-status
    Route->>Auth: authorize(request)
    Auth->>Limiter: is_locked_out(identity)?
    alt locked out
        Auth-->>Mim: 429 rate_limited
    else credentials invalid
        Auth->>Limiter: register_failure(identity)
        Auth-->>Mim: 401 unauthorized
    else authorized
        Route->>Lookup: find(order_number, email, phone)
        Lookup->>WC: wc_get_order(order_number)
        alt order missing or email/phone mismatch
            Lookup-->>Route: EntityNotFoundException
            Route->>Limiter: register_failure(identity)
            Route-->>Mim: 404 order_not_found
        else match
            Route-->>Mim: 200 OrderStatusData (incl. estimated_delivery filter result)
        end
    end

Every failure path — unknown order, wrong email, wrong phone — returns the same generic 404 order_not_found, on purpose: a caller must never be able to tell "no such order" apart from "right order, wrong contact details".