ohne Preference, ohne Rewrite-Konflikte
Wer die Rechnungslogik oder die Gutschrift-Verarbeitung in Magento 2 per Preference komplett neu schreibt, riskiert Konflikte mit jedem weiteren Modul und jedem Core-Update. Plugins auf InvoiceRepositoryInterface, CreditmemoManagementInterface und ShipmentRepositoryInterface erlauben gezielte Erweiterungen: eigene Nummerierung, Freigabe-Workflows für Gutschriften, zusätzliche Totals-Zeilen und automatische Rechnungserstellung bei Versand, ohne die Kernlogik zu ersetzen.
Inhaltsverzeichnis
- 1. Warum Plugins statt Preferences fuer die Rechnungslogik
- 2. Rechnungsnummerierung und Validierung per Plugin auf InvoiceRepositoryInterface
- 3. Business-Regeln fuer Gutschriften: Around-Plugin auf CreditmemoManagementInterface
- 4. Observer fuer Side-Effects: ERP-Synchronisation und Audit-Log
- 5. Audit-Log-Entity per db_schema.xml
- 6. Custom Totals Collector fuer Gebuehren- und Rabattzeilen
- 7. Automatische Rechnungserstellung bei Versand
- 8. Deployment, Sortierung und Testbarkeit von Plugins
- 9. Preference vs. Plugin im direkten Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Warum Plugins statt Preferences fuer die Rechnungslogik
Sobald ein Magento-Projekt eigene Anforderungen an die Rechnungslogik oder die Gutschrift-Verarbeitung stellt, greifen viele Teams reflexartig zur Preference und schreiben die komplette Klasse Magento\Sales\Model\Order\Invoice oder den kompletten CreditmemoManagement-Service neu. Das Problem: Eine Preference ersetzt die gesamte Klasse und macht das Projekt inkompatibel mit jedem weiteren Modul, das dieselbe Klasse ebenfalls rewritet. Bei einem Magento-Update oder einer Drittanbieter-Extension bricht die Rechnungslogik dann ohne Vorwarnung, weil zwei Preferences sich gegenseitig überschreiben. Ein Plugin, also ein Interceptor, hängt sich stattdessen gezielt vor, um oder nach eine öffentliche Methode eines Service Contracts, ohne die Originalimplementierung zu verlieren.
Für die Rechnungslogik und die Gutschrift-Verarbeitung bedeutet das konkret: Statt InvoiceManagement komplett neu zu schreiben, wird ein around-Plugin auf InvoiceManagementInterface::setCapture oder InvoiceRepositoryInterface::save registriert. Statt CreditmemoManagement zu ersetzen, wird ein around-Plugin auf CreditmemoManagementInterface::refund gehängt. So bleibt die Kernlogik von Magento unangetastet, mehrere Module können parallel auf denselben Service Contract wirken, solange die sortOrder sauber gepflegt ist, und ein Core-Update betrifft nur die eigene Erweiterung, nicht die gesamte Rechnungslogik des Shops.
2. Rechnungsnummerierung und Validierung per Plugin auf InvoiceRepositoryInterface
Eine häufige Projektanforderung: Rechnungen sollen zusätzlich zur Standard-Increment-ID eine eigene, projektspezifische Rechnungsnummer erhalten, zum Beispiel für die Übergabe an ein Buchhaltungssystem. Der saubere Weg dafür ist ein around-Plugin auf Magento\Sales\Api\InvoiceRepositoryInterface::save. Bevor der eigentliche $proceed-Aufruf die Rechnung persistiert, prüft das Plugin die Pflichtfelder, wirft bei fehlender Increment-ID eine LocalizedException und setzt bei Bedarf ein eigenes Attribut custom_invoice_number über einen dedizierten Nummerngenerator. Dieser Generator ist selbst wieder ein Service Contract, sodass die Nummernlogik austauschbar bleibt, statt hart im Plugin zu stehen.
Wichtig ist die Reihenfolge: Ein around-Plugin auf InvoiceRepositoryInterface::save greift erst, wenn die Rechnung bereits vollständig aufgebaut ist, aber noch nicht in der Datenbank liegt. Für Validierungen, die vor der eigentlichen Erfassung greifen müssen, etwa die Prüfung, ob eine Zahlungsart Rechnungslogik mit Teilzahlungen überhaupt zulässt, ist ein Plugin auf InvoiceManagementInterface::setCapture der richtige Ansatz, weil dort noch entschieden wird, ob die Rechnung online oder offline erfasst wird. Beide Plugins zusammen decken die typischen Validierungsfälle rund um die Rechnungserstellung ab, ohne dass eine Preference nötig wird.
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
<!-- Invoice: custom numbering and validation on save -->
<type name="Magento\Sales\Api\InvoiceRepositoryInterface">
<plugin name="mironsoft_invoice_numbering_validation" type="Mironsoft\SalesLogic\Plugin\InvoiceNumberingPlugin" sortOrder="10"/>
</type>
<!-- Invoice: capture mode validation before online/offline decision -->
<type name="Magento\Sales\Api\InvoiceManagementInterface">
<plugin name="mironsoft_invoice_capture_validation" type="Mironsoft\SalesLogic\Plugin\CaptureValidationPlugin" sortOrder="20"/>
</type>
<!-- Creditmemo: business rule enforcement on refund -->
<type name="Magento\Sales\Api\CreditmemoManagementInterface">
<plugin name="mironsoft_creditmemo_refund_guard" type="Mironsoft\SalesLogic\Plugin\RefundApprovalPlugin" sortOrder="10"/>
</type>
<!-- Shipment: automatic invoice creation -->
<type name="Magento\Sales\Api\ShipmentRepositoryInterface">
<plugin name="mironsoft_autoinvoice_on_shipment" type="Mironsoft\SalesLogic\Plugin\AutoInvoiceOnShipmentPlugin" sortOrder="10"/>
</type>
</config>
3. Business-Regeln fuer Gutschriften: Around-Plugin auf CreditmemoManagementInterface
Die Gutschrift-Verarbeitung in Magento läuft über Magento\Sales\Api\CreditmemoManagementInterface::refund. Genau an dieser Stelle lassen sich Business-Regeln durchsetzen, die über die reine Magento-Standardlogik hinausgehen: Ein Schwellenwert, ab dem eine Gutschrift eine Manager-Freigabe benötigt, eine automatische Gutschrift von Treuepunkten bei Rückerstattung, oder eine Sperre für bestimmte Zahlungsarten, bei denen Rückerstattungen manuell im Backend des Zahlungsanbieters erfolgen müssen. Ein around-Plugin ist hier die richtige Wahl, weil es den kompletten Aufruf abfangen und bei Regelverstoß mit einer LocalizedException abbrechen kann, bevor Magento überhaupt mit dem Zahlungsanbieter kommuniziert.
Der Approval-Checker im folgenden Beispiel ist bewusst als eigenes Service Contract-Interface modelliert, nicht als Methode direkt im Plugin. Das erlaubt es, die Freigabelogik unabhängig zu testen und später zu erweitern, etwa um eine Rolle-basierte Prüfung oder eine Anbindung an ein externes Approval-Workflow-Tool. Für die Auto-Anwendung von Treuepunkten bei einer Gutschrift gilt dieselbe Struktur: Nach erfolgreichem $proceed()-Aufruf greift ein weiteres kleines Plugin oder ein Observer, der die Punkte auf Basis der Gutschriftsumme gutschreibt.
<?php
declare(strict_types=1);
namespace Mironsoft\SalesLogic\Plugin;
use Magento\Framework\Exception\LocalizedException;
use Magento\Sales\Api\CreditmemoManagementInterface;
use Magento\Sales\Api\Data\CreditmemoInterface;
use Mironsoft\SalesLogic\Model\ApprovalCheckerInterface;
use Psr\Log\LoggerInterface;
/**
* Enforces manager approval for high-value credit memo refunds.
*/
class RefundApprovalPlugin
{
private const APPROVAL_THRESHOLD = 500.00;
/**
* @param ApprovalCheckerInterface $approvalChecker Service that checks pending approval flags
* @param LoggerInterface $logger Logger for blocked refund attempts
*/
public function __construct(
private readonly ApprovalCheckerInterface $approvalChecker,
private readonly LoggerInterface $logger
) {
}
/**
* Blocks the refund above the configured threshold unless a manager approval exists.
*
* @param CreditmemoManagementInterface $subject Intercepted service contract
* @param callable $proceed Original refund logic
* @param CreditmemoInterface $creditmemo Credit memo to be refunded
* @param bool $offlineRequested Whether an offline refund was requested
* @return bool
* @throws LocalizedException
*/
public function aroundRefund(
CreditmemoManagementInterface $subject,
callable $proceed,
CreditmemoInterface $creditmemo,
bool $offlineRequested = false
): bool {
$grandTotal = (float) $creditmemo->getGrandTotal();
if ($grandTotal > self::APPROVAL_THRESHOLD && !$this->approvalChecker->isApproved($creditmemo)) {
$this->logger->warning(sprintf(
'Refund blocked: credit memo for order %s exceeds threshold without approval',
(string) $creditmemo->getOrderId()
));
throw new LocalizedException(
__('Refunds above %1 require manager approval before processing.', self::APPROVAL_THRESHOLD)
);
}
return $proceed($creditmemo, $offlineRequested);
}
}
4. Observer fuer Side-Effects: ERP-Synchronisation und Audit-Log
Nicht jede Anforderung an die Rechnungslogik ist eine Regel, die den Ablauf verändern oder blockieren soll. Wenn nach erfolgreicher Rechnungs- oder Gutschrift-Erstellung lediglich ein Nebeneffekt ausgelöst werden soll, etwa die Benachrichtigung eines ERP-Systems oder das Schreiben eines Audit-Log-Eintrags, ist ein Observer die passendere Wahl als ein Plugin. Die Events sales_order_invoice_save_after und sales_order_creditmemo_save_after werden von Magento nach jedem erfolgreichen Speichervorgang ausgelöst und liefern das jeweilige Entity-Objekt über getEvent()->getData().
Der entscheidende Unterschied zum Plugin: Ein Observer kann den Ablauf nicht mehr verhindern, da die Rechnung oder Gutschrift zu diesem Zeitpunkt bereits gespeichert ist. Das ist gewollt, denn Side-Effects wie eine ERP-Synchronisation dürfen niemals einen fehlgeschlagenen Netzwerkaufruf zum Grund machen, warum eine Rechnung nicht gespeichert wird. Im folgenden Beispiel kapselt der Observer sowohl die ERP-Benachrichtigung als auch das Schreiben des Audit-Log-Eintrags in einem try/catch-Block, damit ein Fehler im ERP-Aufruf niemals die Kernlogik der Rechnungslogik beeinträchtigt.
<?php
declare(strict_types=1);
namespace Mironsoft\SalesLogic\Observer;
use Magento\Framework\Event\Observer;
use Magento\Framework\Event\ObserverInterface;
use Magento\Sales\Model\Order\Invoice;
use Mironsoft\SalesLogic\Api\AuditLogRepositoryInterface;
use Mironsoft\SalesLogic\Api\Data\AuditLogInterfaceFactory;
use Mironsoft\SalesLogic\Model\ErpNotifierInterface;
use Psr\Log\LoggerInterface;
/**
* Notifies the ERP system and writes an audit log entry after invoice creation.
*/
class InvoiceSaveAfterObserver implements ObserverInterface
{
/**
* @param ErpNotifierInterface $erpNotifier Service that pushes invoice data to the ERP
* @param AuditLogRepositoryInterface $auditLogRepository Repository for the audit log entity
* @param AuditLogInterfaceFactory $auditLogFactory Factory for the audit log data model
* @param LoggerInterface $logger Logger for side-effect failures
*/
public function __construct(
private readonly ErpNotifierInterface $erpNotifier,
private readonly AuditLogRepositoryInterface $auditLogRepository,
private readonly AuditLogInterfaceFactory $auditLogFactory,
private readonly LoggerInterface $logger
) {
}
/**
* Executes the ERP notification and audit log write, isolated from the save transaction.
*
* @param Observer $observer Event observer carrying the invoice entity
* @return void
*/
public function execute(Observer $observer): void
{
/** @var Invoice $invoice */
$invoice = $observer->getEvent()->getData('invoice');
try {
$this->erpNotifier->notifyInvoiceCreated($invoice);
$auditLog = $this->auditLogFactory->create();
$auditLog->setOrderId((int) $invoice->getOrderId());
$auditLog->setEntityType('invoice');
$auditLog->setEntityIncrementId((string) $invoice->getIncrementId());
$auditLog->setEventType('created');
$auditLog->setPayload((string) $invoice->getGrandTotal());
$this->auditLogRepository->save($auditLog);
} catch (\Throwable $exception) {
$this->logger->error('ERP notification failed: ' . $exception->getMessage());
}
}
}
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd">
<event name="sales_order_invoice_save_after">
<observer name="mironsoft_invoice_erp_audit" instance="Mironsoft\SalesLogic\Observer\InvoiceSaveAfterObserver"/>
</event>
<event name="sales_order_creditmemo_save_after">
<observer name="mironsoft_creditmemo_erp_audit" instance="Mironsoft\SalesLogic\Observer\CreditmemoSaveAfterObserver"/>
</event>
</config>
5. Audit-Log-Entity per db_schema.xml
Für Nachvollziehbarkeit in der Rechnungslogik und der Gutschrift-Verarbeitung braucht es eine eigene Audit-Log-Tabelle, in der jede relevante Aktion mit Zeitstempel, Entity-Typ und Payload festgehalten wird. Statt eines InstallSchema-Scripts kommt hier konsequent das deklarative Schema zum Einsatz: eine db_schema.xml im Modul definiert Tabelle, Spalten, den Primärschlüssel, einen Fremdschlüssel auf sales_order und einen Index auf die Order-ID. Deklaratives Schema erzeugt automatisch die passenden Alter-Statements bei Änderungen und verzichtet komplett auf manuell verwaltete Setup-Versionsnummern.
Der Zugriff auf diese Tabelle erfolgt ausschließlich über ein eigenes Repository, das dem Service-Contract-Muster folgt: ein AuditLogRepositoryInterface mit save(), getById() und getList(), dazu ein AuditLogInterface als Datenmodell mit Gettern und Settern für jede Spalte. So bleibt der Observer aus Abschnitt 4 vollständig von der konkreten Persistenzimplementierung entkoppelt und lässt sich in Unit-Tests problemlos mit einem Mock des Repositories testen, ohne eine echte Datenbank zu benötigen.
<?xml version="1.0"?>
<schema xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd">
<table name="mironsoft_saleslogic_audit_log" resource="default" engine="innodb" comment="Sales Logic Audit Log">
<column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="true" comment="Entity ID"/>
<column xsi:type="int" name="order_id" padding="10" unsigned="true" nullable="false" comment="Order ID"/>
<column xsi:type="varchar" name="entity_type" nullable="false" length="32" comment="Entity Type (invoice/creditmemo)"/>
<column xsi:type="varchar" name="entity_increment_id" nullable="false" length="64" comment="Increment ID"/>
<column xsi:type="varchar" name="event_type" nullable="false" length="32" comment="Event Type"/>
<column xsi:type="text" name="payload" nullable="true" comment="Payload"/>
<column xsi:type="timestamp" name="created_at" on_update="false" nullable="false" default="CURRENT_TIMESTAMP" comment="Created At"/>
<constraint xsi:type="primary" referenceId="PRIMARY">
<column name="entity_id"/>
</constraint>
<constraint xsi:type="foreign" referenceId="MIRONSOFT_SALESLOGIC_AUDIT_LOG_ORDER_ID_SALES_ORDER_ENTITY_ID"
table="mironsoft_saleslogic_audit_log" column="order_id"
referenceTable="sales_order" referenceColumn="entity_id" onDelete="CASCADE"/>
<index referenceId="MIRONSOFT_SALESLOGIC_AUDIT_LOG_ORDER_ID" indexType="btree">
<column name="order_id"/>
</index>
</table>
</schema>
6. Custom Totals Collector fuer Gebuehren- und Rabattzeilen
Ein verbreiteter, aber falscher Ansatz für zusätzliche Gebühren oder Rabatte auf einer Rechnung ist die direkte Manipulation von getGrandTotal() und setGrandTotal() innerhalb eines Plugins oder Observers. Das funktioniert kurzfristig, bricht aber sobald ein weiteres Modul ebenfalls Totals verändert, weil die Reihenfolge der Eingriffe undefiniert ist und sich Summen gegenseitig überschreiben können. Der korrekte Mechanismus in der Rechnungslogik von Magento ist ein eigener Totals Collector, der von Magento\Sales\Model\Order\Invoice\Total\AbstractTotal erbt und über die sales.xml mit einer festen sort_order in die bestehende Kette der Totals-Berechnung eingehängt wird.
Die Methode collect() erhält das vollständige Invoice-Objekt, berechnet die zusätzliche Zeile über einen eigenen Service (hier HandlingFeeCalculator) und addiert den Betrag korrekt sowohl auf grand_total als auch auf base_grand_total. Weil die Collector-Kette von Magento selbst verwaltet wird, spielt es keine Rolle, in welcher Reihenfolge andere Module eigene Totals registrieren, solange die sort_order sinnvoll gewählt ist. Für Gutschrift-Totals gilt dieselbe Struktur mit Magento\Sales\Model\Order\Creditmemo\Total\AbstractTotal und einer Registrierung im entsprechenden total_creditmemo-Knoten der sales.xml.
<?php
declare(strict_types=1);
namespace Mironsoft\SalesLogic\Model\Order\Invoice\Total;
use Magento\Sales\Model\Order\Invoice;
use Magento\Sales\Model\Order\Invoice\Total\AbstractTotal;
use Mironsoft\SalesLogic\Model\HandlingFeeCalculatorInterface;
/**
* Adds a custom handling fee line to the invoice totals.
*/
class HandlingFee extends AbstractTotal
{
/**
* @param HandlingFeeCalculatorInterface $calculator Calculates the fee for the given order
* @param array $data Additional constructor data forwarded to the parent
*/
public function __construct(
private readonly HandlingFeeCalculatorInterface $calculator,
array $data = []
) {
parent::__construct($data);
}
/**
* Collects and applies the handling fee to the invoice grand total.
*
* @param Invoice $invoice Invoice being totalled
* @return $this
*/
public function collect(Invoice $invoice): self
{
$fee = $this->calculator->calculateFor($invoice->getOrder());
if ($fee <= 0.0) {
return $this;
}
$invoice->setHandlingFee($fee);
$invoice->setBaseHandlingFee($fee);
$invoice->setGrandTotal((float) $invoice->getGrandTotal() + $fee);
$invoice->setBaseGrandTotal((float) $invoice->getBaseGrandTotal() + $fee);
return $this;
}
}
7. Automatische Rechnungserstellung bei Versand
Viele B2B-Projekte benötigen eine Rechnungslogik, bei der eine Rechnung nicht manuell im Backend erstellt wird, sondern automatisch entsteht, sobald eine Sendung erfasst wird. Dafür bietet sich ein after-Plugin auf Magento\Sales\Api\ShipmentRepositoryInterface::save an. Nach erfolgreichem Speichern der Sendung prüft das Plugin, ob die zugehörige Order eine automatische Rechnung erlaubt (etwa auf Basis der Zahlungsart oder eines Kundengruppen-Flags) und ruft anschließend InvoiceManagementInterface::prepareInvoice() sowie InvoiceRepositoryInterface::save() auf, um die Rechnung zu erzeugen.
Wichtig dabei: Das Plugin sollte niemals den Speichervorgang der Sendung selbst blockieren, wenn die Rechnungserstellung fehlschlägt. Ein Fehler in der automatischen Rechnungslogik darf nicht dazu führen, dass eine bereits versendete Sendung nicht gespeichert werden kann. Deshalb wird die Rechnungserstellung in einem eigenen try/catch-Block gekapselt, ein Fehler wird geloggt und gegebenenfalls über den Audit-Log-Mechanismus aus Abschnitt 5 protokolliert, aber die eigentliche $proceed()-Rückgabe der Sendung bleibt unangetastet.
8. Deployment, Sortierung und Testbarkeit von Plugins
Sobald mehrere Module gleichzeitig auf InvoiceRepositoryInterface, CreditmemoManagementInterface oder ShipmentRepositoryInterface pluginen, entscheidet die sortOrder in der di.xml über die Reihenfolge der Ausführung. Bei around-Plugins ist diese Reihenfolge besonders kritisch, weil ein früheres Plugin den $proceed-Aufruf eines späteren Plugins komplett verhindern kann, wenn es eine Exception wirft. Es empfiehlt sich, Validierungs-Plugins mit niedriger sortOrder vor reinen Erweiterungs-Plugins zu registrieren, damit ungültige Zustände so früh wie möglich abgefangen werden, bevor teurere Operationen laufen. Der disabled-Parameter erlaubt es, ein Plugin in bestimmten Umgebungen gezielt zu deaktivieren, ohne die Registrierung zu entfernen.
Nach jeder Änderung an di.xml muss der DI-Cache neu kompiliert werden, im Produktivmodus über bin/magento setup:di:compile, im Entwicklungsmodus reicht in der Regel bin/magento cache:flush. Für Tests gilt: around-Plugins lassen sich in Unit-Tests isoliert prüfen, indem $proceed als einfaches Callable gemockt wird, das einen definierten Rückgabewert liefert. Damit lässt sich die Business-Logik im Plugin unabhängig von der eigentlichen Magento-Implementierung testen. Für den vollständigen Ablauf, inklusive der Interaktion mit dem Service Contract selbst, sind Integrationstests mit einer echten Repository-Instanz sinnvoller.
9. Preference vs. Plugin im direkten Vergleich
Die Entscheidung zwischen Preference und Plugin für die Rechnungslogik und Gutschrift-Verarbeitung sollte kein Bauchgefühl sein, sondern eine bewusste Architekturentscheidung. Die folgende Tabelle stellt die relevanten Kriterien gegenüber.
| Kriterium | Preference (Rewrite) | Plugin (Interceptor) |
|---|---|---|
| Kompatibilität mit anderen Modulen | Nur ein Modul kann dieselbe Klasse rewriten | Beliebig viele Plugins pro Interface möglich |
| Update-Sicherheit | Bricht bei geänderter Kernklasse still | Nur betroffene Methode ist gekoppelt |
| Umfang des Eingriffs | Komplette Klasse inklusive ungenutzter Methoden | Gezielt eine oder wenige Methoden |
| Testbarkeit | Ganze Originalklasse muss neu getestet werden | Plugin isoliert mit gemocktem $proceed testbar |
| Projekt-Konvention | Widerspricht dem Plugin-First-Ansatz | Entspricht Service Contracts und Interceptor-Pattern |
In der Praxis bleibt für die Rechnungslogik und die Gutschrift-Verarbeitung kaum ein Fall übrig, der zwingend eine Preference erfordert. Selbst tiefgreifende Änderungen an der Nummerierung, der Freigabelogik oder den Totals lassen sich über around-Plugins, Observer und eigene Totals Collectors abdecken. Preferences bleiben nur dort gerechtfertigt, wo Magento keinen Service Contract und keinen Plugin-fähigen Public-Method-Zugriff anbietet, was bei den zentralen Sales-Interfaces praktisch nicht vorkommt.
10. Zusammenfassung
Die Rechnungslogik und die Gutschrift-Verarbeitung in Magento 2 lassen sich fast vollständig über Plugins statt Preferences erweitern. Ein around-Plugin auf InvoiceRepositoryInterface::save und InvoiceManagementInterface::setCapture deckt eigene Nummerierung und Validierung ab. Ein around-Plugin auf CreditmemoManagementInterface::refund setzt Freigabe-Schwellenwerte und Business-Regeln durch, ohne die Kernlogik zu ersetzen. Observer auf sales_order_invoice_save_after und sales_order_creditmemo_save_after übernehmen Side-Effects wie ERP-Synchronisation und Audit-Log, ohne den eigentlichen Speichervorgang zu gefährden.
Für zusätzliche Gebühren- oder Rabattzeilen ist ein eigener Totals Collector auf Basis von AbstractTotal der einzig robuste Weg, da direkte Manipulation von Grand-Total-Werten in Plugins mit anderen Modulen kollidiert. Ein Plugin auf ShipmentRepositoryInterface::save ermöglicht automatische Rechnungserstellung bei Versand, ohne den Versandprozess selbst zu blockieren. Eine eigene Audit-Log-Entity per db_schema.xml, angebunden über ein Repository nach Service-Contract-Muster, macht jede Änderung an der Rechnungslogik und Gutschrift-Verarbeitung nachvollziehbar und testbar.
Rechnungslogik und Gutschrift per Plugin: Das Wichtigste auf einen Blick
Rechnungslogik per Plugin
Around-Plugin auf InvoiceRepositoryInterface::save und InvoiceManagementInterface::setCapture statt Preference fuer Nummerierung und Validierung.
Gutschrift-Freigabe
Around-Plugin auf CreditmemoManagementInterface::refund erzwingt Schwellenwerte und Business-Regeln vor der Rueckerstattung.
Side-Effects per Observer
sales_order_invoice_save_after und sales_order_creditmemo_save_after fuer ERP-Sync und Audit-Log, entkoppelt vom Speichervorgang.
Totals & Schema
Eigene Gebuehrenzeilen ueber AbstractTotal-Collector, Audit-Log ueber db_schema.xml und Service Contracts.
11. FAQ: Rechnungslogik und Gutschrift per Plugin
1Warum Plugin statt Preference fuer die Rechnungslogik?
2Wie funktioniert das around-Plugin auf CreditmemoManagementInterface::refund?
3Wo werden diese Plugins registriert?
4Plugin oder Observer bei Rechnungen?
5Warum keine direkte Total-Manipulation im Plugin?
6Wie speichere ich ein Audit-Log?
7Mehrere Plugins auf demselben Interface moeglich?
8Wie teste ich ein around-Plugin?
9Automatische Rechnung bei Versand?
10Was passiert bei zwei Plugins auf derselben Methode?
Mironsoft
Magento 2 Backend-Entwicklung, Sales Module und Service Contracts
Rechnungslogik und Gutschrift-Workflows, die zum Projekt passen?
Wir entwerfen und implementieren Plugins, Totals Collectors und Audit-Log-Entities fuer eure Rechnungs- und Gutschrift-Prozesse, sauber ueber Service Contracts, ohne Preferences und ohne Rewrite-Konflikte bei Updates.
Plugin-Architektur
Around-Plugins fuer InvoiceRepositoryInterface und CreditmemoManagementInterface
Totals Collector
Eigene Gebuehren- und Rabattzeilen sauber ueber sales.xml registriert
Audit-Log & ERP
Declarative Schema fuer Audit-Entities und Observer fuer ERP-Synchronisation