Architecture Guide
Fakturownia Pro for Magento 2 is built entirely on Magento's native extension architecture. It uses no core rewrites, no direct database queries against core tables, and no patching of Magento classes. This design means the module is upgrade-safe, composable with other extensions, and behaves predictably under Magento's deployment toolchain.
This guide explains the key architectural patterns the module uses and why they matter for stores that need long-term maintainability and compatibility with Adobe Commerce Cloud.
Why "Native Architecture" Matters
The Magento ecosystem has a long history of modules that take shortcuts: overriding core classes with rewrites, querying sales_order directly with raw SQL, or hooking into undocumented internal methods. These modules work until Magento releases an upgrade that touches the same code โ then they break.
Fakturownia Pro avoids every one of these patterns:
| Pattern | What we do | What we avoid |
|---|---|---|
| Class customization | Plugin (interceptor) on public interfaces | <rewrite> class overrides |
| Data persistence | Declarative schema, own tables | Raw ALTER TABLE in install scripts |
| Order data access | WooCommerce CRUD / Magento Repository | Direct $wpdb / $connection->query() |
| Feature registration | events.xml, crontab.xml, di.xml | __construct overrides |
| Service boundaries | Interface + implementation + DI | Static methods, singletons |
The practical result: when you run composer update magento/magento2-base, the module continues working. When Adobe patches the OrderRepositoryInterface, the module gets the fix for free.
Dependency Injection
The module is built entirely on Magento's DI container. Every class receives its dependencies through constructor injection โ there are no ObjectManager::getInstance() calls in the module code outside of integration test bootstrapping.
Constructor Injection Example
The core service class illustrates the pattern:
namespace Plugkit\FakturowniaProMagento2\Model\Service;
class DocumentGenerationService implements DocumentGenerationServiceInterface
{
public function __construct(
private readonly HttpClientInterface $httpClient,
private readonly InvoiceRepositoryInterface $invoiceRepository,
private readonly QueueRepositoryInterface $queueRepository,
private readonly ConfigInterface $config,
private readonly LoggerInterface $logger,
private readonly OrderRepositoryInterface $orderRepository,
) {}
}Every dependency is an interface, not a concrete class. This means you can swap any implementation via di.xml without modifying module code:
<!-- Override the HTTP client with a custom implementation -->
<preference for="Plugkit\FakturowniaProMagento2\Api\HttpClientInterface"
type="YourVendor\YourModule\Model\CustomHttpClient" />Virtual Types
For configuration-specific variants (e.g., a rate-limited HTTP client vs. a standard one), the module uses virtual types to avoid code duplication:
<!-- Virtual type: HTTP client with extended timeout for PDF downloads -->
<virtualType name="Plugkit\FakturowniaProMagento2\Model\PdfHttpClient"
type="Plugkit\FakturowniaProMagento2\Model\HttpClient">
<arguments>
<argument name="timeout" xsi:type="number">60</argument>
</arguments>
</virtualType>Proxy Classes for Performance
Services with expensive constructors (e.g., the document generation service that loads configuration at construction time) are injected as proxy classes in contexts where the full service is not always needed:
<!-- Use proxy in observer โ only resolves the full service when the method is called -->
<type name="Plugkit\FakturowniaProMagento2\Observer\OrderStateChangeObserver">
<arguments>
<argument name="generationService" xsi:type="object">
Plugkit\FakturowniaProMagento2\Model\Service\DocumentGenerationService\Proxy
</argument>
</arguments>
</type>Declarative Schema
All database schema is declared in etc/db_schema.xml. There are no InstallSchema.php or UpgradeSchema.php scripts โ declarative schema handles both initial installation and incremental upgrades automatically.
Tables
plugkit_fakturownia_queue
The invoice generation job queue. Every invoice generation request that is not processed synchronously enters this table.
| Column | Type | Nullable | Description |
|---|---|---|---|
| queue_id | INT UNSIGNED | No | Primary key (auto-increment) |
| order_id | INT UNSIGNED | No | Magento order ID |
| increment_id | VARCHAR(50) | No | Order increment ID (e.g., 000000001) |
| document_type | VARCHAR(20) | No | invoice, receipt, correction, proforma |
| status | VARCHAR(20) | No | pending, processing, failed, failed_permanent, completed |
| retry_count | TINYINT UNSIGNED | No | Number of retries attempted (0โ255) |
| error_message | TEXT | Yes | Last error message from failed attempt |
| scheduled_at | DATETIME | No | When to process (used for exponential backoff) |
| created_at | DATETIME | No | Queue entry creation timestamp |
| processed_at | DATETIME | Yes | When processing completed or last attempted |
Indexes: order_id, status, (status, scheduled_at) โ the last index covers the queue processor query that selects pending jobs due for processing.
plugkit_fakturownia_invoices
Audit log of all invoice operations. Records are immutable โ every generation attempt writes a new row.
| Column | Type | Nullable | Description |
|---|---|---|---|
| log_id | INT UNSIGNED | No | Primary key |
| order_id | INT UNSIGNED | No | Magento order ID |
| fakturownia_invoice_id | VARCHAR(50) | Yes | Fakturownia internal document ID |
| invoice_number | VARCHAR(100) | Yes | Human-readable invoice number |
| document_type | VARCHAR(20) | No | Document type |
| operation | VARCHAR(20) | No | create, update, delete |
| status | VARCHAR(20) | No | success, failure |
| request_payload | JSON | Yes | Full API request sent (for debugging) |
| response_body | JSON | Yes | Full API response received |
| duration_ms | INT UNSIGNED | Yes | API call duration in milliseconds |
| created_at | DATETIME | No | Log entry timestamp |
request_payload and response_body are only populated when debug logging is enabled. In production mode, they are NULL to avoid storing PII in the database.
Service Contracts
The module defines PHP interfaces for all public-facing functionality. External code (including your custom modules) should program against these interfaces, not the concrete implementations.
Primary Interfaces
DocumentGenerationServiceInterface
The main entry point for generating invoice documents.
namespace Plugkit\FakturowniaProMagento2\Api;
interface DocumentGenerationServiceInterface
{
/**
* Generate an invoice document for the given order.
*
* @param int $orderId Magento order entity ID
* @param string $documentType 'invoice'|'receipt'|'correction'|'proforma'
* @param bool $force Regenerate even if invoice already exists
* @throws \Plugkit\FakturowniaProMagento2\Api\Exception\ApiException
* @throws \Plugkit\FakturowniaProMagento2\Api\Exception\OrderNotFoundException
*/
public function generate(int $orderId, string $documentType = 'invoice', bool $force = false): GenerationResultInterface;
/**
* Queue an invoice generation job for background processing.
*/
public function enqueue(int $orderId, string $documentType = 'invoice'): QueueJobInterface;
}InvoiceRepositoryInterface
Repository for reading invoice log records.
interface InvoiceRepositoryInterface
{
public function getByOrderId(int $orderId): InvoiceLogInterface;
public function getList(SearchCriteriaInterface $searchCriteria): InvoiceLogSearchResultsInterface;
}QueueRepositoryInterface
Repository for reading and managing queue jobs.
interface QueueRepositoryInterface
{
public function getById(int $queueId): QueueJobInterface;
public function getByOrderId(int $orderId): QueueJobInterface;
public function save(QueueJobInterface $queueJob): QueueJobInterface;
public function delete(QueueJobInterface $queueJob): bool;
public function getList(SearchCriteriaInterface $searchCriteria): QueueJobSearchResultsInterface;
}Calling the Service from Custom Code
// In your custom module โ inject via constructor
public function __construct(
private readonly \Plugkit\FakturowniaProMagento2\Api\DocumentGenerationServiceInterface $generationService,
) {}
// Synchronous generation
try {
$result = $this->generationService->generate($orderId, 'invoice');
$invoiceNumber = $result->getInvoiceNumber();
} catch (\Plugkit\FakturowniaProMagento2\Api\Exception\ApiException $e) {
// Handle Fakturownia API error
}
// Async via queue (recommended for order placement flow)
$job = $this->generationService->enqueue($orderId, 'invoice');Plugin (Interceptor) Pattern
The module uses Magento plugins to hook into core order processing without overriding classes. Plugins are the safe, upgrade-compatible alternative to class rewrites.
OrderStateChangePlugin
Intercepts Magento\Sales\Model\Order::setState() to check whether an invoice should be generated when the order transitions to a new state.
class OrderStateChangePlugin
{
public function afterSetState(
\Magento\Sales\Model\Order $subject,
\Magento\Sales\Model\Order $result,
string $state
): \Magento\Sales\Model\Order {
$this->invoiceRuleProcessor->processStateChange($subject, $state);
return $result;
}
}An after plugin on setState fires after the state is changed and the order object is in its new state โ the correct moment to read the new state and trigger invoice generation logic.
OrderSavePlugin
Intercepts Magento\Sales\Api\OrderRepositoryInterface::save() after order persistence to handle cases where state changes occur without setState being called directly (e.g., mass actions in the admin grid).
class OrderSavePlugin
{
public function afterSave(
\Magento\Sales\Api\OrderRepositoryInterface $subject,
\Magento\Sales\Api\Data\OrderInterface $result
): \Magento\Sales\Api\Data\OrderInterface {
if ($this->stateHasChanged($result)) {
$this->invoiceRuleProcessor->processOrder($result);
}
return $result;
}
}Plugin Registration
Both plugins are registered in etc/di.xml:
<type name="Magento\Sales\Model\Order">
<plugin name="plugkit_fakturownia_order_state_change"
type="Plugkit\FakturowniaProMagento2\Plugin\OrderStateChangePlugin"
sortOrder="100"
disabled="false" />
</type>
<type name="Magento\Sales\Api\OrderRepositoryInterface">
<plugin name="plugkit_fakturownia_order_save"
type="Plugkit\FakturowniaProMagento2\Plugin\OrderSavePlugin"
sortOrder="100"
disabled="false" />
</type>The sortOrder="100" ensures other plugins with lower sort orders run first โ the module processes invoice rules after all core state changes are settled.
To disable a specific plugin without uninstalling the module (useful during debugging):
<!-- In your custom module's di.xml -->
<type name="Magento\Sales\Model\Order">
<plugin name="plugkit_fakturownia_order_state_change" disabled="true" />
</type>Extension Attributes
The module attaches invoice data to Magento's order entities without modifying the core schema, using the standard extension attributes mechanism.
Registered Attributes (etc/extension_attributes.xml)
<extension_attributes for="Magento\Sales\Api\Data\OrderInterface">
<attribute code="fakturownia_invoice_id" type="string" />
<attribute code="fakturownia_invoice_number" type="string" />
<attribute code="fakturownia_invoice_status" type="string" />
<attribute code="fakturownia_nip" type="string" />
<attribute code="fakturownia_document_type" type="string" />
</extension_attributes>
<extension_attributes for="Magento\Sales\Api\Data\OrderAddressInterface">
<attribute code="fakturownia_nip" type="string" />
</extension_attributes>Reading Extension Attributes
$order = $this->orderRepository->get($orderId);
$extensionAttributes = $order->getExtensionAttributes();
$invoiceNumber = $extensionAttributes->getFakturowniaInvoiceNumber();
$invoiceStatus = $extensionAttributes->getFakturowniaInvoiceStatus();
$nip = $extensionAttributes->getFakturowniaNip();These Attributes in Magento's REST API
Extension attributes are automatically included in the order object returned by GET /rest/V1/orders/{id}:
{
"entity_id": 1234,
"increment_id": "000001234",
"extension_attributes": {
"fakturownia_invoice_id": "98765432",
"fakturownia_invoice_number": "FS/2025/042",
"fakturownia_invoice_status": "generated",
"fakturownia_nip": "1234567890"
}
}They are also searchable via the searchCriteria filter syntax in the REST API โ see Configuration for query examples.
Cron Jobs
The module registers a dedicated fakturownia_pro cron group in etc/crontab.xml. Using a dedicated group means the module's cron jobs run in isolation โ a slow Fakturownia API call does not block Magento's core cron group from processing indexer jobs or catalog price rules.
<group id="fakturownia_pro">
<job name="fakturownia_process_queue" instance="Plugkit\FakturowniaProMagento2\Cron\ProcessQueue"
method="execute">
<schedule>* * * * *</schedule>
</job>
<job name="fakturownia_retry_failed" instance="Plugkit\FakturowniaProMagento2\Cron\RetryFailed"
method="execute">
<schedule>*/5 * * * *</schedule>
</job>
<job name="fakturownia_cleanup_logs" instance="Plugkit\FakturowniaProMagento2\Cron\CleanupLogs"
method="execute">
<schedule>0 2 * * *</schedule>
</job>
</group>| Job | Schedule | What it does |
|---|---|---|
| fakturownia_process_queue | Every minute | Picks up to 50 pending jobs and processes them synchronously |
| fakturownia_retry_failed | Every 5 minutes | Re-queues failed jobs that have not exhausted their retry limit |
| fakturownia_cleanup_logs | Daily 02:00 | Deletes plugkit_fakturownia_invoices rows older than the configured retention period |
Running the Cron Group
# Run once manually (useful for testing)
bin/magento cron:run --group=fakturownia_pro
# Check that jobs are scheduled
bin/magento cron:status | grep fakturowniaAdobe Commerce (Cloud) Compatibility
The module has been tested and is compatible with Adobe Commerce 2.4.5 through 2.4.7 and Adobe Commerce Cloud.
No Adobe Commerce-Exclusive APIs
The module uses only Magento Open Source APIs. It does not use:
- B2B module extensions (Company, NegotiableQuote, etc.)
- Adobe Commerce exclusive payment or catalog APIs
- Commerce-specific database tables
This means the module installs identically on Open Source and Commerce editions.
Adobe Commerce Cloud Deployment
On Adobe Commerce Cloud, the standard deployment hooks run setup:upgrade and setup:di:compile automatically. No extra build hooks are needed.
Ensure the fakturownia_pro cron group is not excluded in .magento.env.yaml:
stage:
deploy:
CRON_CONSUMERS_RUNNER:
cron_run: trueFor environments where custom cron schedules are managed via .magento.app.yaml:
crons:
fakturownia_queue:
spec: "* * * * *"
cmd: "php bin/magento cron:run --group=fakturownia_pro"Next Steps
- Retry Queue โ queue states, retry strategy, and monitoring
- Configuration โ all settings including store scope and cron adjustment
- Invoice Rules โ per-state automation using the observer/plugin system