Skip to content

Architecture

This document describes the technical architecture of the Innosend Magento 2 modules.

System Architecture

High-Level Overview

flowchart TB
    subgraph Magento["Magento 2 Store"]
        direction LR

        subgraph Frontend["Frontend (Checkout)"]
            direction TB
            Shipping["Shipping Method"]
            Modal["Pickup Points"]
            JS["JavaScript Components"]
        end
        subgraph IntegrationAPI["IntegrationAPI"]
            direction LR
            Integration["Integration Core Module"]
            Integration --> JS
            JS --> Modal
        end
        subgraph Backend["PHP Backend"]
            direction LR
            Pickup["Pickup Points Module"]
            Order["Order Connector Module"]


        end
        Pickup --> Integration
        Order --> Integration
        Integration --> Modal

        APIClient["API Client"]
        Integration <---> APIClient

        Frontend ---> Backend
    end

    API["Innosend API<br/>api.innosend.eu"]
    APIClient --->|HTTPS| API

Module Dependencies

flowchart TB
    Meta["innosend/magento2 (metapackage)"]
    Integration["innosend/magento2-integration"]
    Pickup["innosend/magento2-pickup-points"]
    Order["innosend/magento2-order-connector"]

    Meta --> Integration
    Meta --> Pickup
    Meta --> Order
    Pickup --> Integration
    Pickup --> Order
    Order --> Integration
   

Integration Module Architecture

API Client

The API client provides a unified interface for all Innosend API communication.

interface ClientInterface
{
    public function get(string $endpoint, array $params = []): array;
    public function post(string $endpoint, array $data = []): array;
    public function put(string $endpoint, array $data = []): array;
    public function delete(string $endpoint): array;
    public function isEnabled(): bool;
}

Client Implementation

flowchart TB
    subgraph APIClient["API Client"]
        direction TB
        Config["Config Provider"] --> HTTP["HTTP Client"] --> Error["Error Handler"]
        Config --> Parser["Response Parser"]
        HTTP --> Parser
        Error --> Parser
    end

Configuration Management

class Config
{
    public function isEnabled(): bool;
    public function getMode(): string;           // 'test' or 'live'
    public function getEndpointUrl(): string;    // Based on mode
    public function getApiKey(): string;
    public function getApiSecret(): string;
    public function getTimeout(): int;
    public function isValid(): bool;
}

Pickup Points Module Architecture

Data Flow

flowchart TB
    Address["Customer enters address"]
    JS["JavaScript triggers AJAX request"]
    Controller["GetPickupPoints Controller"]
    APIClient["API Client Pickup Points"]
    API["Innosend API"]
    Response["JSON Response to Frontend"]
    Modal["Render Pickup Points Modal"]

    Address --> JS
    JS --> Controller
    Controller --> APIClient
    APIClient <-->|request/response| API
    APIClient --> Response
    Response --> Modal

Database Schema

-- Quote pickup point (temporary during checkout)
CREATE TABLE innosend_quote_pickup_point (
    entity_id INT AUTO_INCREMENT PRIMARY KEY,
    quote_id INT NOT NULL,
    pickup_point_id VARCHAR(255),
    carrier_code VARCHAR(50),
    name VARCHAR(255),
    street VARCHAR(255),
    house_number VARCHAR(50),
    postal_code VARCHAR(20),
    city VARCHAR(100),
    country_code VARCHAR(2),
    latitude DECIMAL(10, 8),
    longitude DECIMAL(11, 8),
    opening_hours TEXT,
    distance DECIMAL(10, 2),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    FOREIGN KEY (quote_id) REFERENCES quote(entity_id) ON DELETE CASCADE
);

-- Order pickup point (permanent after order)
CREATE TABLE innosend_order_pickup_point (
    entity_id INT AUTO_INCREMENT PRIMARY KEY,
    order_id INT NOT NULL,
    -- Same fields as quote pickup point
    FOREIGN KEY (order_id) REFERENCES sales_order(entity_id) ON DELETE CASCADE
);

Frontend Components

flowchart TB
    subgraph Frontend["view/frontend/"]
        subgraph Layout["layout/"]
            LayoutXML["checkout_index_index.xml"]
        end
        subgraph Templates["templates/pickup-points/"]
            Checkout["checkout.phtml"]
            Modal["modal.phtml"]
            Selected["selected.phtml"]
            Wrapper["wrapper.phtml"]
        end
        subgraph Web["web/"]
            subgraph CSS["css/"]
                Styles["pickup-points.css"]
            end
            subgraph JS["js/"]
                MainJS["pickup-points.js"]
                MapJS["pickup-points-map.js"]
            end
            subgraph Template["template/pickup-points/"]
                ModalHTML["modal.html"]
                ItemHTML["pickup-point-item.html"]
                SelectedHTML["selected.html"]
            end
        end
    end

JavaScript Component Architecture

// Knockout.js component structure
define(["uiComponent", "ko", "mage/storage", "Magento_Checkout/js/model/quote"], function (
  Component,
  ko,
  storage,
  quote
) {
  "use strict";

  return Component.extend({
    defaults: {
      pickupPoints: ko.observableArray([]),
      selectedPickupPoint: ko.observable(null),
      isLoading: ko.observable(false),
    },

    initialize: function () {
      this._super();
      this.loadPickupPoints();
    },

    loadPickupPoints: function () {
      // AJAX call to fetch pickup points
    },

    selectPickupPoint: function (pickupPoint) {
      // Save selection and update checkout
    },
  });
});

Order Connector Module Architecture

Order Sync Flow

flowchart TB
    Order["Order Placement"]
    Event["sales_order_place_after Event"]
    Copy["Copy Pickup Point to Order"]
    Sync["Sync Order to Innosend"]
    Log["Log Event"]
    Mapper["OrderMapper (Transform data)"]
    APIClient["API Client<br/>POST /orders"]
    Success{"Success?"}
    LogSuccess["Log Success"]
    Queue["Queue for Retry"]
    Cron["Cron Job Retries"]

    Order --> Event
    Event --> Copy
    Event --> Sync
    Event --> Log
    Sync --> Mapper
    Mapper --> APIClient
    APIClient --> Success
    Success -->|Yes| LogSuccess
    Success -->|No| Queue
    Queue --> Cron
    Cron -.->|retry| Mapper

Order Mapper

class OrderMapper
{
    /**
     * Map Magento order to Innosend format
     */
    public function map(OrderInterface $order): array
    {
        return [
            'external_id' => $order->getIncrementId(),
            'order_number' => $order->getIncrementId(),
            'created_at' => $order->getCreatedAt(),
            'currency' => $order->getOrderCurrencyCode(),
            'total' => $order->getGrandTotal(),
            'customer' => $this->mapCustomer($order),
            'billing_address' => $this->mapAddress($order->getBillingAddress()),
            'shipping_address' => $this->mapAddress($order->getShippingAddress()),
            'items' => $this->mapItems($order),
            'pickup_point' => $this->mapPickupPoint($order)
        ];
    }
}

Cron Configuration

<!-- etc/crontab.xml -->
<config>
    <group id="innosend">
        <job name="innosend_sync_orders" instance="Innosend\OrderConnector\Cron\SyncOrders" method="execute">
            <schedule>*/5 * * * *</schedule>
        </job>
        <job name="innosend_status_sync" instance="Innosend\OrderConnector\Cron\StatusSync" method="execute">
            <schedule>*/15 * * * *</schedule>
        </job>
    </group>
</config>

Extension Attributes

Quote Extension

<!-- etc/extension_attributes.xml -->
<config>
    <extension_attributes for="Magento\Quote\Api\Data\CartInterface">
        <attribute code="pickup_point" type="Innosend\PickupPoints\Api\Data\QuotePickupPointInterface"/>
    </extension_attributes>
</config>

Order Extension

<config>
    <extension_attributes for="Magento\Sales\Api\Data\OrderInterface">
        <attribute code="pickup_point" type="Innosend\PickupPoints\Api\Data\OrderPickupPointInterface"/>
    </extension_attributes>
</config>

Security Architecture

Credential Storage

flowchart TB
    subgraph Admin["Admin Configuration"]
        Input["API Key / Secret Input"]
        Encrypted["Config Backend Encrypted"]
        Storage["Encrypted Storage<br/>(core_config_data)"]

        Input --> Encrypted
        Encrypted --> Storage
    end

API Communication

  • All API calls use HTTPS
  • Basic authentication with encrypted credentials
  • CSP whitelist for external map resources
  • Rate limiting awareness

Performance Considerations

Caching Strategy

Data Cache Type TTL
Configuration Config cache Until cleared
Pickup points No cache (real-time) -
Order sync status Database Permanent

Optimization Tips

  1. Frontend

  2. JavaScript is loaded only on checkout

  3. Map tiles are lazy-loaded
  4. Pickup points paginated in modal

  5. Backend

  6. API calls are async where possible
  7. Failed syncs use exponential backoff
  8. Batch processing for status updates

Error Handling

Logging Levels

// Critical - System failure
$this->logger->critical('API connection lost', ['error' => $e->getMessage()]);

// Error - Operation failed
$this->logger->error('Order sync failed', ['order_id' => $orderId]);

// Warning - Degraded operation
$this->logger->warning('Retrying failed sync', ['attempt' => $attempt]);

// Info - Normal operations
$this->logger->info('Order synced successfully', ['order_id' => $orderId]);

// Debug - Detailed information
$this->logger->debug('API response', ['response' => $response]);

Error Recovery

flowchart TB
    Start["Order Sync Failed"]
    RetryEnabled{"Retry Enabled?"}
    MaxAttempts{"Under Max Attempts?"}
    LogSkip["Log & Skip"]
    MarkFailed["Mark as Failed<br/>Log for Review"]
    Queue["Queue for Retry<br/>(Next Cron Run)"]

    Start --> RetryEnabled
    RetryEnabled -->|No| LogSkip
    RetryEnabled -->|Yes| MaxAttempts
    MaxAttempts -->|No| MarkFailed
    MaxAttempts -->|Yes| Queue