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¶
-
Frontend
-
JavaScript is loaded only on checkout
- Map tiles are lazy-loaded
-
Pickup points paginated in modal
-
Backend
- API calls are async where possible
- Failed syncs use exponential backoff
- 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