Compare commits

...

12 Commits

Author SHA1 Message Date
andrewrowanwallee 05c83b1ac6 Release 7.3.4 2026-05-28 17:34:20 +02:00
andrewrowanwallee 93f24a2cac Release 7.3.3 2026-04-29 11:43:34 +02:00
andrewrowanwallee dbd0b6808c Release 7.3.2 2026-02-24 11:48:14 +01:00
andrewrowanwallee 3f3bf866dd Release 7.3.1 2026-02-09 14:17:02 +01:00
andrewrowanwallee 4fbae35058 Release 7.3.0 2026-01-27 10:24:15 +01:00
andrewrowanwallee 922f66e784 Release 7.2.0 2026-01-14 15:43:19 +01:00
andrewrowanwallee 089555e77f Release 7.1.6 2025-11-24 11:56:40 +01:00
andrewrowanwallee 55bdb5c640 Release 7.1.5 2025-11-11 13:52:29 +01:00
andrewrowanwallee d1a78fedcf Release 7.1.4 2025-10-30 16:49:17 +01:00
andrewrowanwallee d7ea44d506 Release 7.1.3 2025-10-27 12:47:17 +01:00
andrewrowanwallee fa480808b9 Release 7.1.2 2025-10-06 12:23:20 +02:00
andrewrowanwallee 017bb0e416 Release 7.1.1 2025-09-22 12:00:10 +02:00
81 changed files with 5034 additions and 1791 deletions
+59
View File
@@ -1,3 +1,62 @@
# 7.3.4
- Shopware 6.7.10.0 compatible
- Fix for cart being lost when changing payment methods
- Fixed for incorrectly reference function (getState())
- Fix cache not being cleaned after payment errors
- Fix missing shipping costs
- Minor fix for Customer Id null inconsistency
# 7.3.3
- Shopware 6.7.9.0 compatible
- Fix for only showing 25 sales channels in selector
- Fixed bug with discount occasionally blocking payment methods
# 7.3.2
- Fix for partial refunds using standard refund option
- Fixed issue with undefined array key
- Fixed issue with recurring payments
- Fixed concrete class reference; used interface instead
# 7.3.1
- Shopware 6.7.7.0 compatibility
- Fix for missing payment icons
- Fixed issue with 'Entire Set' coupons returning error due to totals mismatch
- Fixed issue with orphaned order_transactions causing an admin UI failure
- Fixed error thrown when customers who aren't logged in attempt to download an invoice
# 7.3.0
- Headless storefront support
# 7.2.0
- Renamed database table to avoid a naming conflict with legacy plugins
- Fixed issue with refunds failing for payments using Invoice
- Fix to respect sort order of payment methods
# 7.1.6
- Improved rounding handling to force 2 decimal places
- Removed references to unused classes
- Fixed some type errors
- Further improved 0 amount transaction; state transitions
# 7.1.5
- Improved analytics
- Improved error handling for refunding amount 0 and too many items
# 7.1.4
- Updated SDK to 4.8.1
# 7.1.3
- Fixed issue with radio button/switch settings not saving
- Fixed issue with error screen sporadically happening after failed payments
# 7.1.2
- Support the ability to have different spaces for differet sales channels
- Fixed issue where Twint would not appear sometimes
# 7.1.1
- Updated documentation
- Fixed issue with addresses not being correctly synced
# 7.1.0
## Feature
- Support subscription payment methods
+57
View File
@@ -1,3 +1,60 @@
# 7.3.4
- Kompatibel mit Shopware 6.7.10.0
- Problem behoben, bei dem der Warenkorb beim Ändern der Zahlungsmethode verloren ging
- Fehlerhafte Referenzierung der Funktion getState() behoben
- Problem behoben, bei dem der Cache nach Zahlungsfehlern nicht geleert wurde
- Fehlende Versandkosten behoben
- Kleinere Korrektur eines Fehlers mit der Kunden-ID (null)
# 7.3.3
- Kompatibel mit Shopware 6.7.9.0
- Problem behoben, dass im Selektor nur 25 Vertriebskanäle angezeigt wurden
- Fehler behoben, der gelegentlich dazu führte, dass Rabatte Zahlungsmethoden blockierten
# 7.3.2
- Problem mit Teilrückerstattungen über die Standardrückerstattungsoption behoben
- Problem mit undefiniertem Array-Schlüssel behoben
- Problem mit einziehenden Zahlungen behoben
- Konkrete Klasse durch Verwendung einer Schnittstelle korrigiert
# 7.3.1
- Kompatibilität mit Shopware 6.7.7.0
- Fehlende Zahlungssymbole behoben
- Problem mit „Gesamtes Set“-Gutscheinen aufgrund von Summenabweichungen behoben
- Problem mit verwaisten Bestelltransaktionen behoben, die zu einem Fehler in der Admin-Oberfläche führten
- Ein Fehler wurde behoben, der auftrat, wenn nicht angemeldete Kunden versuchten, eine Rechnung herunterzuladen.
# 7.3.0
- Headless Storefront unterstützung
# 7.2.0
- Datenbanktabelle umbenannt, um Namenskonflikte mit älteren Plugins zu vermeiden.
- Problem mit fehlgeschlagenen Rückerstattungen bei Zahlungen mit Rechnungen behoben.
# 7.1.6
- Verbesserte Rundungsbehandlung für zwei Dezimalstellen
- Entfernte Verweise auf ungenutzte Klassen
- Behebung einiger Typfehler
- Weitere Verbesserung von Transaktionen mit dem Betrag 0 und Zustandsübergängen
# 7.1.5
- Verbesserte Analysefunktionen
- Verbesserte Fehlerbehandlung bei Rückerstattungen von 0 Beträgen und zu vielen Artikeln
# 7.1.4
- Aktualisiertes SDK - 4.8.1
# 7.1.3
Problem behoben, bei dem die Einstellungen von Optionsfeldern/Schaltern nicht gespeichert wurden.
Problem behoben, bei dem nach fehlgeschlagenen Zahlungen sporadisch ein Fehlerbildschirm angezeigt wurde.
# 7.1.2
- Unterstützung der Möglichkeit, unterschiedliche Bereiche für verschiedene Vertriebskanäle zu nutzen.
- Problem behoben, bei dem Twint manchmal nicht angezeigt wurde.
# 7.1.1
- Dokumentation aktualisiert
- Problem behoben, bei dem Adressen nicht korrekt synchronisiert wurden.
# 7.1.0
Unterstützung für Abonnement-Zahlungsmethoden
Support für Shopware 6.7.2.0
+1 -1
View File
@@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2025 VR Payment GmbH
Copyright 2026 VR Payment GmbH
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
+35 -10
View File
@@ -13,10 +13,10 @@ Please note that this plugin is for versions 6.5, 6.6 or 6.7. For the 6.4 plugin
## Documentation
- For English documentation click [here](@WalleeDocPath(/docs/en/documentation.html))
- Für die deutsche Dokumentation klicken Sie [hier](@WalleeDocPath(/docs/de/documentation.html))
- Pour la documentation Française, cliquez [ici](@WalleeDocPath(/docs/fr/documentation.html))
- Per la documentazione in tedesco, clicca [qui](@WalleeDocPath(/docs/it/documentation.html))
- For English documentation click [here](https://docs.plugin-documentation.vr-payment.de/vr-payment/shopware-6/7.3.4/docs/en/documentation.html)
- Für die deutsche Dokumentation klicken Sie [hier](https://docs.plugin-documentation.vr-payment.de/vr-payment/shopware-6/7.3.4/docs/de/documentation.html)
- Pour la documentation Française, cliquez [ici](https://docs.plugin-documentation.vr-payment.de/vr-payment/shopware-6/7.3.4/docs/fr/documentation.html)
- Per la documentazione in tedesco, clicca [qui](https://docs.plugin-documentation.vr-payment.de/vr-payment/shopware-6/7.3.4/docs/it/documentation.html)
## Installation
@@ -28,26 +28,51 @@ Please note that this plugin is for versions 6.5, 6.6 or 6.7. For the 6.4 plugin
Copy
composer require vrpayment/shopware-6
php bin/console plugin:refresh
php bin/console plugin:install --activate --clearCache VRPaymentPayment
php bin/console plugin:install --activate --clearCache VRPayment
```
### Manual Installation
1. Download the latest [Release](../../releases)
2. Extract the ZIP to custom/plugins/VRPaymentPayment.
2. Extract the ZIP to custom/plugins/VRPayment.
```bash
Copy
bin/console plugin:refresh
bin/console plugin:install --activate --clearCache VRPaymentPayment
bin/console plugin:install --activate --clearCache VRPayment
```
## Update
### Via Administration
1. Go to Shopware Admin > Extensions > My extensions.
2. Find VRPaymentPayment.
3. Click Update.
### Via CLI
1. Deploy the new plugin files (replace the folder in custom/plugins/VRPaymentPayment or upload/install a new ZIP).
2. Run:
```bash
bin/console plugin:refresh
bin/console plugin:update --clearCache VRPaymentPayment
bin/console cache:clear
```
## Configuration
### API Credentials
1. Navigate to Shopware Admin > Settings > VRPayment Payment.
1. Navigate to Shopware Admin > Settings > VRPayment.
2. Enter your Space ID, User ID, and API Key (obtained from the [VR Payment Portal](https://gateway.vr-payment.de/)).
### VRPayment does not appear in Settings
1. You should run the following commmand
```bash
Copy
bin/build-administration.sh
```
### Payment Methods
Configure supported methods (e.g., credit cards, Apple Pay) via the [VR Payment Portal](https://gateway.vr-payment.de/).
@@ -69,7 +94,7 @@ ________________________________________________________________________________
| Shopware 6 version | Plugin major version | Supported until |
|-------------------------------|------------------------|------------------------|
| Shopware 6.7.x | 7.x | Further notice |
| Shopware 6.6.x | 6.x | December 2025 |
| Shopware 6.6.x | 6.x | March 2026 |
| Shopware 6.5.x | 5.x | October 2024 |
-----------------------------------------------------------------------------------
@@ -78,7 +103,7 @@ ________________________________________________________________________________
```bash
Copy
tail -f var/log/vrpayment_payment*.log
tail -f var/log/vrpayment*.log
```
### Common Issues:
+1 -1
View File
@@ -59,5 +59,5 @@
"vrpayment/sdk": "^4.0.0"
},
"type": "shopware-platform-plugin",
"version": "7.1.0"
"version": "7.3.4"
}
+138 -54
View File
@@ -5,7 +5,7 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<meta name="keywords" value="VR Payment, Shopware, Shopware Plugin, Payment, Payment Integration, Documentation"><meta name="description" value="The documentation for the Shopware 6 plugin that enables processing payments with VR Payment.">
<link rel="canonical" href="@WalleeCanonicalPath(https://plugin-documentation.wallee.com/wallee-payment, VRPaymentPayment/docs/de/documentation.html)" />
<link rel="canonical" href="https://plugin-documentation.wallee.com/wallee-payment/shopware-6/master/VRPaymentPayment/docs/de/documentation.html" />
<title>VR Payment Zahlungs-Plugin für Shopware 6</title>
<link href="assets/monokai-sublime.css" rel="stylesheet" />
<link href="assets/base.css" rel="stylesheet" />
@@ -23,7 +23,7 @@
</a>
</li>
<li>
<a href="@WalleeReleasePath()">
<a href="https://github.com/vr-payment/shopware-6/releases/tag/7.3.4/">
Source
</a>
</li>
@@ -152,7 +152,7 @@
<div class="content">
<pre class="highlight"><code class="language-bash" data-lang="bash">composer require vrpayment/shopware-6
php bin/console plugin:refresh
php bin/console plugin:install --activate --clearCache VRPaymentPayment</code></pre>
php bin/console plugin:install --activate --clearCache VRPayment</code></pre>
</div>
</div>
</li>
@@ -177,7 +177,69 @@ php bin/console plugin:install --activate --clearCache VRPaymentPayment</code></
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-bash" data-lang="bash">php bin/console plugin:refresh
php bin/console plugin:install --activate --clearCache VRPaymentPayment</code></pre>
php bin/console plugin:install --activate --clearCache VRPayment</code></pre>
</div>
</div>
</li>
</ol>
</div> </div>
</div> <div class="section" id="_vrpayment_erscheint_nicht_in_den_einstellungen">
<div class="section-title">
<h2>
<span class="title-number">4.4</span>VRPayment erscheint nicht in den Einstellungen </h2>
</div>
<div class="section-body">
<div class="paragraph">
<p>Folgender Befehl muss ausgeführt werden</p>
</div><div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-bash" data-lang="bash">bin/build-administration.sh</code></pre>
</div>
</div> </div>
</div> </div>
</div> <div class="chapter" id="_update">
<div class="chapter-title">
<h1>
<span class="title-number">5</span>Update </h1>
</div>
<div class="chapter-body">
<div class="section" id="_via_administration">
<div class="section-title">
<h2>
<span class="title-number">5.1</span>Via Administration </h2>
</div>
<div class="section-body">
<div class="olist arabic">
<ol class="arabic">
<li>
<p>Gehe zu Shopware Admin &gt; Erweiterungen &gt; Meine Erweiterungen.</p>
</li>
<li>
<p>Suche nach VRPaymentPayment.</p>
</li>
<li>
<p>Klicke auf Aktualisieren.</p>
</li>
</ol>
</div> </div>
</div> <div class="section" id="_via_cli">
<div class="section-title">
<h2>
<span class="title-number">5.2</span>Via CLI </h2>
</div>
<div class="section-body">
<div class="olist arabic">
<ol class="arabic">
<li>
<p>Stelle die neuen Plugin-Dateien bereit (ersetze den Ordner <code>custom/plugins/VRPaymentPayment</code> oder lade ein neues ZIP hoch/installiere es).</p>
</li>
<li>
<p>Führe aus:</p>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-bash" data-lang="bash">bin/console plugin:refresh
bin/console plugin:update --clearCache VRPaymentPayment
bin/console cache:clear</code></pre>
</div>
</div>
</li>
@@ -187,7 +249,7 @@ php bin/console plugin:install --activate --clearCache VRPaymentPayment</code></
</div> <div class="chapter" id="portal-startup-guide">
<div class="chapter-title">
<h1>
<span class="title-number">5</span>Portal-Startanleitung </h1>
<span class="title-number">6</span>Portal-Startanleitung </h1>
</div>
<div class="chapter-body">
<div class="paragraph">
@@ -206,7 +268,7 @@ Wählen Sie das passende Abo aus es sollte E-Commerce-Transaktionen unterst
</div> <div class="section" id="_erstellen_sie_den_api_schlüssel">
<div class="section-title">
<h2>
<span class="title-number">5.1</span>Erstellen Sie den API-Schlüssel: </h2>
<span class="title-number">6.1</span>Erstellen Sie den API-Schlüssel: </h2>
</div>
<div class="section-body">
<div class="olist arabic">
@@ -292,7 +354,7 @@ Bitte beachten Sie, dass das Laden der Rollen einige Sekunden dauern kann.
</div> <div class="section" id="_zahlungsmethoden_einrichten">
<div class="section-title">
<h2>
<span class="title-number">5.2</span>Zahlungsmethoden einrichten </h2>
<span class="title-number">6.2</span>Zahlungsmethoden einrichten </h2>
</div>
<div class="section-body">
<div class="paragraph">
@@ -354,7 +416,7 @@ Bitte beachten Sie, dass die Konnektoren doppelt erscheinen, da einer für Zahlu
</div> <div class="chapter" id="_shop_startanleitung">
<div class="chapter-title">
<h1>
<span class="title-number">6</span>Shop-Startanleitung </h1>
<span class="title-number">7</span>Shop-Startanleitung </h1>
</div>
<div class="chapter-body">
<div class="olist arabic">
@@ -364,7 +426,7 @@ Bitte beachten Sie, dass die Konnektoren doppelt erscheinen, da einer für Zahlu
<div class="olist loweralpha">
<ol class="loweralpha" type="a">
<li>
<p>Navigieren Sie zu <strong>Shopware Admin → Einstellungen → Erweiterungen → VR Payment Payment</strong> und klicken Sie auf Speichern.</p>
<p>Navigieren Sie zu <strong>Shopware Admin → Einstellungen → Erweiterungen → VR Payment</strong> und klicken Sie auf Speichern.</p>
</li>
<li>
<p>Geben Sie Ihre <code>Space ID</code>, <code>User ID</code>, und <code>API Key</code> ein.</p>
@@ -493,7 +555,7 @@ Bitte beachten Sie, dass diese Option leer bleiben sollte, wenn Sie die Space Vi
</div> <div class="chapter" id="_transaktionszustandsdiagramm">
<div class="chapter-title">
<h1>
<span class="title-number">7</span>Transaktionszustandsdiagramm </h1>
<span class="title-number">8</span>Transaktionszustandsdiagramm </h1>
</div>
<div class="chapter-body">
<div class="paragraph">
@@ -503,7 +565,7 @@ Bitte beachten Sie, dass diese Option leer bleiben sollte, wenn Sie die Space Vi
</div> <div class="section" id="_zustandsabbildung_von_shopware_bestellungen">
<div class="section-title">
<h2>
<span class="title-number">7.1</span>Zustandsabbildung von Shopware-Bestellungen </h2>
<span class="title-number">8.1</span>Zustandsabbildung von Shopware-Bestellungen </h2>
</div>
<div class="section-body">
<div class="paragraph">
@@ -511,7 +573,7 @@ Bitte beachten Sie, dass diese Option leer bleiben sollte, wenn Sie die Space Vi
</div> <div class="section" id="_allgemeine_anmerkungen_zu_bestellstatus">
<div class="section-title">
<h3>
<span class="title-number">7.1.1</span>Allgemeine Anmerkungen zu Bestellstatus </h3>
<span class="title-number">8.1.1</span>Allgemeine Anmerkungen zu Bestellstatus </h3>
</div>
<div class="section-body">
<div class="paragraph">
@@ -521,7 +583,7 @@ Bitte beachten Sie, dass diese Option leer bleiben sollte, wenn Sie die Space Vi
</div> <div class="section" id="_zustandsabbildung_des_shopware_zahlungsstatus">
<div class="section-title">
<h2>
<span class="title-number">7.2</span>Zustandsabbildung des Shopware-Zahlungsstatus </h2>
<span class="title-number">8.2</span>Zustandsabbildung des Shopware-Zahlungsstatus </h2>
</div>
<div class="section-body">
<div class="paragraph">
@@ -548,7 +610,7 @@ Bitte beachten Sie, dass diese Option leer bleiben sollte, wenn Sie die Space Vi
</div> <div class="section" id="_allgemeine_anmerkungen_zu_zahlungsstatus">
<div class="section-title">
<h3>
<span class="title-number">7.2.1</span>Allgemeine Anmerkungen zu Zahlungsstatus </h3>
<span class="title-number">8.2.1</span>Allgemeine Anmerkungen zu Zahlungsstatus </h3>
</div>
<div class="section-body">
<div class="paragraph">
@@ -558,7 +620,7 @@ Bitte beachten Sie, dass diese Option leer bleiben sollte, wenn Sie die Space Vi
</div> <div class="section" id="_zustandsabbildung_des_shopware_lieferstatus">
<div class="section-title">
<h2>
<span class="title-number">7.3</span>Zustandsabbildung des Shopware-Lieferstatus </h2>
<span class="title-number">8.3</span>Zustandsabbildung des Shopware-Lieferstatus </h2>
</div>
<div class="section-body">
<div class="paragraph">
@@ -584,7 +646,7 @@ Bitte beachten Sie, dass diese Option leer bleiben sollte, wenn Sie die Space Vi
</div> <div class="chapter" id="_transaktionsverwaltung">
<div class="chapter-title">
<h1>
<span class="title-number">8</span>Transaktionsverwaltung </h1>
<span class="title-number">9</span>Transaktionsverwaltung </h1>
</div>
<div class="chapter-body">
<div class="paragraph">
@@ -592,7 +654,7 @@ Bitte beachten Sie, dass diese Option leer bleiben sollte, wenn Sie die Space Vi
</div> <div class="section" id="_bestellung_abschließen_erfassen">
<div class="section-title">
<h2>
<span class="title-number">8.1</span>Bestellung abschließen (erfassen) </h2>
<span class="title-number">9.1</span>Bestellung abschließen (erfassen) </h2>
</div>
<div class="section-body">
<div class="paragraph">
@@ -620,7 +682,7 @@ Wenn der Abschluss in VR Payment ausstehend ist, bleibt die Bestellung im Status
</div> <div class="section" id="_transaktion_stornieren">
<div class="section-title">
<h2>
<span class="title-number">8.2</span>Transaktion stornieren </h2>
<span class="title-number">9.2</span>Transaktion stornieren </h2>
</div>
<div class="section-body">
<div class="paragraph">
@@ -644,7 +706,7 @@ Sie können nur Transaktionen stornieren, die noch nicht abgeschlossen sind.
</div> <div class="section" id="_rückerstattung_einer_transaktion">
<div class="section-title">
<h2>
<span class="title-number">8.3</span>Rückerstattung einer Transaktion </h2>
<span class="title-number">9.3</span>Rückerstattung einer Transaktion </h2>
</div>
<div class="section-body">
<div class="paragraph">
@@ -670,7 +732,7 @@ Es kann einige Zeit dauern, bis Sie die Rückerstattung in Shopware sehen. Rück
</div> <div class="section" id="_bestellungen_auf_on_hold">
<div class="section-title">
<h2>
<span class="title-number">8.4</span>Bestellungen auf On Hold </h2>
<span class="title-number">9.4</span>Bestellungen auf On Hold </h2>
</div>
<div class="section-body">
<div class="paragraph">
@@ -692,7 +754,7 @@ Es kann einige Zeit dauern, bis Sie die Rückerstattung in Shopware sehen. Rück
</div> <div class="section" id="_einschränkungen_der_synchronisierung_zwischen_whitelabelname_und_shopware">
<div class="section-title">
<h2>
<span class="title-number">8.5</span>Einschränkungen der Synchronisierung zwischen VR Payment und Shopware </h2>
<span class="title-number">9.5</span>Einschränkungen der Synchronisierung zwischen VR Payment und Shopware </h2>
</div>
<div class="section-body">
<div class="paragraph">
@@ -703,7 +765,7 @@ Es kann einige Zeit dauern, bis Sie die Rückerstattung in Shopware sehen. Rück
</div> <div class="section" id="_tokenisierung">
<div class="section-title">
<h2>
<span class="title-number">8.6</span>Tokenisierung </h2>
<span class="title-number">9.6</span>Tokenisierung </h2>
</div>
<div class="section-body">
<div class="paragraph">
@@ -723,7 +785,7 @@ Die Tokenisierung ist für Gast-Checkouts nicht verfügbar
</div> <div class="section" id="_wiederkehrende_zahlungen">
<div class="section-title">
<h2>
<span class="title-number">8.7</span>Wiederkehrende Zahlungen </h2>
<span class="title-number">9.7</span>Wiederkehrende Zahlungen </h2>
</div>
<div class="section-body">
<div class="paragraph">
@@ -733,7 +795,7 @@ Tokenisierung unterstützt, kann er für Abonnements verwendet werden. Die wiede
</div> <div class="section" id="_hauptfunktionen">
<div class="section-title">
<h2>
<span class="title-number">8.8</span>Hauptfunktionen </h2>
<span class="title-number">9.8</span>Hauptfunktionen </h2>
</div>
<div class="section-body">
<div class="ulist">
@@ -758,7 +820,7 @@ Tokenisierung unterstützt, kann er für Abonnements verwendet werden. Die wiede
</div> <div class="section" id="_fehlerbehebung">
<div class="section-title">
<h2>
<span class="title-number">8.9</span>Fehlerbehebung </h2>
<span class="title-number">9.9</span>Fehlerbehebung </h2>
</div>
<div class="section-body">
<div class="ulist">
@@ -768,7 +830,7 @@ Tokenisierung unterstützt, kann er für Abonnements verwendet werden. Die wiede
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-bash" data-lang="bash">COPY
tail -f var/log/whitelabelname_payment*.log</code></pre>
tail -f var/log/whitelabelname*.log</code></pre>
</div>
</div>
</li>
@@ -790,7 +852,7 @@ tail -f var/log/whitelabelname_payment*.log</code></pre>
</div> <div class="section" id="_faqs">
<div class="section-title">
<h2>
<span class="title-number">8.10</span>FAQs </h2>
<span class="title-number">9.10</span>FAQs </h2>
</div>
<div class="section-body">
<div class="paragraph">
@@ -819,7 +881,7 @@ A: Ja, das Plugin unterstützt Wallets wie Apple Pay.</p>
</div> <div class="chapter" id="_änderungsprotokoll">
<div class="chapter-title">
<h1>
<span class="title-number">9</span>Änderungsprotokoll </h1>
<span class="title-number">10</span>Änderungsprotokoll </h1>
</div>
<div class="chapter-body">
<div class="paragraph">
@@ -828,7 +890,7 @@ A: Ja, das Plugin unterstützt Wallets wie Apple Pay.</p>
</div> <div class="chapter" id="_mitwirken">
<div class="chapter-title">
<h1>
<span class="title-number">10</span>Mitwirken </h1>
<span class="title-number">11</span>Mitwirken </h1>
</div>
<div class="chapter-body">
<div class="paragraph">
@@ -839,7 +901,7 @@ A: Ja, das Plugin unterstützt Wallets wie Apple Pay.</p>
</div> <div class="chapter" id="_support">
<div class="chapter-title">
<h1>
<span class="title-number">11</span>Support </h1>
<span class="title-number">12</span>Support </h1>
</div>
<div class="chapter-body">
<div class="paragraph">
@@ -888,135 +950,157 @@ A: Ja, das Plugin unterstützt Wallets wie Apple Pay.</p>
<span class="item-number">4.3</span>
<span class="item-title">Via Composer (Recommended)</span>
</a>
</li> <li class="nav-level-2">
<a href="#_vrpayment_erscheint_nicht_in_den_einstellungen">
<span class="item-number">4.4</span>
<span class="item-title">VRPayment erscheint nicht in den Einstellungen</span>
</a>
</li> </ul>
</li> <li class="nav-level-1">
<a href="#_update">
<span class="item-number">5</span>
<span class="item-title">Update</span>
</a>
<ul class="nav">
<li class="nav-level-2">
<a href="#_via_administration">
<span class="item-number">5.1</span>
<span class="item-title">Via Administration</span>
</a>
</li> <li class="nav-level-2">
<a href="#_via_cli">
<span class="item-number">5.2</span>
<span class="item-title">Via CLI</span>
</a>
</li> </ul>
</li> <li class="nav-level-1">
<a href="#portal-startup-guide">
<span class="item-number">5</span>
<span class="item-number">6</span>
<span class="item-title">Portal-Startanleitung</span>
</a>
<ul class="nav">
<li class="nav-level-2">
<a href="#_erstellen_sie_den_api_schlüssel">
<span class="item-number">5.1</span>
<span class="item-number">6.1</span>
<span class="item-title">Erstellen Sie den API-Schlüssel:</span>
</a>
</li> <li class="nav-level-2">
<a href="#_zahlungsmethoden_einrichten">
<span class="item-number">5.2</span>
<span class="item-number">6.2</span>
<span class="item-title">Zahlungsmethoden einrichten</span>
</a>
</li> </ul>
</li> <li class="nav-level-1">
<a href="#_shop_startanleitung">
<span class="item-number">6</span>
<span class="item-number">7</span>
<span class="item-title">Shop-Startanleitung</span>
</a>
</li> <li class="nav-level-1">
<a href="#_transaktionszustandsdiagramm">
<span class="item-number">7</span>
<span class="item-number">8</span>
<span class="item-title">Transaktionszustandsdiagramm</span>
</a>
<ul class="nav">
<li class="nav-level-2">
<a href="#_zustandsabbildung_von_shopware_bestellungen">
<span class="item-number">7.1</span>
<span class="item-number">8.1</span>
<span class="item-title">Zustandsabbildung von Shopware-Bestellungen</span>
</a>
<ul class="nav">
<li class="nav-level-3">
<a href="#_allgemeine_anmerkungen_zu_bestellstatus">
<span class="item-number">7.1.1</span>
<span class="item-number">8.1.1</span>
<span class="item-title">Allgemeine Anmerkungen zu Bestellstatus</span>
</a>
</li> </ul>
</li> <li class="nav-level-2">
<a href="#_zustandsabbildung_des_shopware_zahlungsstatus">
<span class="item-number">7.2</span>
<span class="item-number">8.2</span>
<span class="item-title">Zustandsabbildung des Shopware-Zahlungsstatus</span>
</a>
<ul class="nav">
<li class="nav-level-3">
<a href="#_allgemeine_anmerkungen_zu_zahlungsstatus">
<span class="item-number">7.2.1</span>
<span class="item-number">8.2.1</span>
<span class="item-title">Allgemeine Anmerkungen zu Zahlungsstatus</span>
</a>
</li> </ul>
</li> <li class="nav-level-2">
<a href="#_zustandsabbildung_des_shopware_lieferstatus">
<span class="item-number">7.3</span>
<span class="item-number">8.3</span>
<span class="item-title">Zustandsabbildung des Shopware-Lieferstatus</span>
</a>
</li> </ul>
</li> <li class="nav-level-1">
<a href="#_transaktionsverwaltung">
<span class="item-number">8</span>
<span class="item-number">9</span>
<span class="item-title">Transaktionsverwaltung</span>
</a>
<ul class="nav">
<li class="nav-level-2">
<a href="#_bestellung_abschließen_erfassen">
<span class="item-number">8.1</span>
<span class="item-number">9.1</span>
<span class="item-title">Bestellung abschließen (erfassen)</span>
</a>
</li> <li class="nav-level-2">
<a href="#_transaktion_stornieren">
<span class="item-number">8.2</span>
<span class="item-number">9.2</span>
<span class="item-title">Transaktion stornieren</span>
</a>
</li> <li class="nav-level-2">
<a href="#_rückerstattung_einer_transaktion">
<span class="item-number">8.3</span>
<span class="item-number">9.3</span>
<span class="item-title">Rückerstattung einer Transaktion</span>
</a>
</li> <li class="nav-level-2">
<a href="#_bestellungen_auf_on_hold">
<span class="item-number">8.4</span>
<span class="item-number">9.4</span>
<span class="item-title">Bestellungen auf On Hold</span>
</a>
</li> <li class="nav-level-2">
<a href="#_einschränkungen_der_synchronisierung_zwischen_whitelabelname_und_shopware">
<span class="item-number">8.5</span>
<span class="item-number">9.5</span>
<span class="item-title">Einschränkungen der Synchronisierung zwischen VR Payment und Shopware</span>
</a>
</li> <li class="nav-level-2">
<a href="#_tokenisierung">
<span class="item-number">8.6</span>
<span class="item-number">9.6</span>
<span class="item-title">Tokenisierung</span>
</a>
</li> <li class="nav-level-2">
<a href="#_wiederkehrende_zahlungen">
<span class="item-number">8.7</span>
<span class="item-number">9.7</span>
<span class="item-title">Wiederkehrende Zahlungen</span>
</a>
</li> <li class="nav-level-2">
<a href="#_hauptfunktionen">
<span class="item-number">8.8</span>
<span class="item-number">9.8</span>
<span class="item-title">Hauptfunktionen</span>
</a>
</li> <li class="nav-level-2">
<a href="#_fehlerbehebung">
<span class="item-number">8.9</span>
<span class="item-number">9.9</span>
<span class="item-title">Fehlerbehebung</span>
</a>
</li> <li class="nav-level-2">
<a href="#_faqs">
<span class="item-number">8.10</span>
<span class="item-number">9.10</span>
<span class="item-title">FAQs</span>
</a>
</li> </ul>
</li> <li class="nav-level-1">
<a href="#_änderungsprotokoll">
<span class="item-number">9</span>
<span class="item-number">10</span>
<span class="item-title">Änderungsprotokoll</span>
</a>
</li> <li class="nav-level-1">
<a href="#_mitwirken">
<span class="item-number">10</span>
<span class="item-number">11</span>
<span class="item-title">Mitwirken</span>
</a>
</li> <li class="nav-level-1">
<a href="#_support">
<span class="item-number">11</span>
<span class="item-number">12</span>
<span class="item-title">Support</span>
</a>
</li> </ul>
+140 -56
View File
@@ -5,7 +5,7 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<meta name="keywords" value="VR Payment, Shopware, Shopware Plugin, Payment, Payment Integration, Documentation"><meta name="description" value="The documentation for the Shopware 6 plugin that enables processing payments with VR Payment.">
<link rel="canonical" href="@WalleeCanonicalPath(https://plugin-documentation.wallee.com/wallee-payment, VRPaymentPayment/docs/en/documentation.html)" />
<link rel="canonical" href="https://plugin-documentation.wallee.com/wallee-payment/shopware-6/master/VRPaymentPayment/docs/en/documentation.html" />
<title>VR Payment Shopware 6 Documentation</title>
<link href="assets/monokai-sublime.css" rel="stylesheet" />
<link href="assets/base.css" rel="stylesheet" />
@@ -23,7 +23,7 @@
</a>
</li>
<li>
<a href="@WalleeReleasePath()">
<a href="https://github.com/vr-payment/shopware-6/releases/tag/7.3.4/">
Source
</a>
</li>
@@ -39,7 +39,7 @@
</div>
<div class="chapter-body">
<div class="paragraph">
<p>The VR Payment Payment Plugin integrates modern payment processing into Shopware 6, offering features like iFrame-based payments, refunds, captures, and PCI compliance. It supports seamless integration with the VR Payment Portal for managing transactions and payment methods.</p>
<p>The VR Payment Plugin integrates modern payment processing into Shopware 6, offering features like iFrame-based payments, refunds, captures, and PCI compliance. It supports seamless integration with the VR Payment Portal for managing transactions and payment methods.</p>
</div><div class="paragraph">
<p>Important: Please note that only Major (e.g. 6.x.0.0) and Minor (e.g. 6.0.x.0) update will be tested for compatibility within a 2 weeks after release.</p>
</div> </div>
@@ -127,7 +127,7 @@
</div>
</li>
<li>
<p>Activate the VR Payment Payment plugin from the Plugin Manager.</p>
<p>Activate the VR Payment plugin from the Plugin Manager.</p>
</li>
</ol>
</div> </div>
@@ -148,7 +148,7 @@
<div class="content">
<pre class="highlight"><code class="language-bash" data-lang="bash">composer require vrpayment/shopware-6
php bin/console plugin:refresh
php bin/console plugin:install --activate --clearCache VRPaymentPayment</code></pre>
php bin/console plugin:install --activate --clearCache VRPayment</code></pre>
</div>
</div>
</li>
@@ -173,7 +173,69 @@ php bin/console plugin:install --activate --clearCache VRPaymentPayment</code></
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-bash" data-lang="bash">php bin/console plugin:refresh
php bin/console plugin:install --activate --clearCache VRPaymentPayment</code></pre>
php bin/console plugin:install --activate --clearCache VRPayment</code></pre>
</div>
</div>
</li>
</ol>
</div> </div>
</div> <div class="section" id="_vrpayment_does_not_appear_in_settings">
<div class="section-title">
<h2>
<span class="title-number">4.4</span>VRPayment does not appear in Settings </h2>
</div>
<div class="section-body">
<div class="paragraph">
<p>You should run the following commmand</p>
</div><div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-bash" data-lang="bash">bin/build-administration.sh</code></pre>
</div>
</div> </div>
</div> </div>
</div> <div class="chapter" id="_update">
<div class="chapter-title">
<h1>
<span class="title-number">5</span>Update </h1>
</div>
<div class="chapter-body">
<div class="section" id="_via_administration">
<div class="section-title">
<h2>
<span class="title-number">5.1</span>Via Administration </h2>
</div>
<div class="section-body">
<div class="olist arabic">
<ol class="arabic">
<li>
<p>Go to Shopware Admin &gt; Extensions &gt; My extensions.</p>
</li>
<li>
<p>Find VRPaymentPayment.</p>
</li>
<li>
<p>Click Update.</p>
</li>
</ol>
</div> </div>
</div> <div class="section" id="_via_cli">
<div class="section-title">
<h2>
<span class="title-number">5.2</span>Via CLI </h2>
</div>
<div class="section-body">
<div class="olist arabic">
<ol class="arabic">
<li>
<p>Deploy the new plugin files (replace the folder in <code>custom/plugins/VRPaymentPayment</code> or upload/install a new ZIP).</p>
</li>
<li>
<p>Run:</p>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-bash" data-lang="bash">bin/console plugin:refresh
bin/console plugin:update --clearCache VRPaymentPayment
bin/console cache:clear</code></pre>
</div>
</div>
</li>
@@ -183,7 +245,7 @@ php bin/console plugin:install --activate --clearCache VRPaymentPayment</code></
</div> <div class="chapter" id="portal-startup-guide">
<div class="chapter-title">
<h1>
<span class="title-number">5</span>Portal Startup Guide </h1>
<span class="title-number">6</span>Portal Startup Guide </h1>
</div>
<div class="chapter-body">
<div class="paragraph">
@@ -202,7 +264,7 @@ Please select the proper subscription plan - it should support ecommerce transac
</div> <div class="section" id="_create_the_api_key">
<div class="section-title">
<h2>
<span class="title-number">5.1</span>Create the API key: </h2>
<span class="title-number">6.1</span>Create the API key: </h2>
</div>
<div class="section-body">
<div class="olist arabic">
@@ -288,7 +350,7 @@ Please note that Roles might be loading for few seconds
</div> <div class="section" id="_setup_payment_methods">
<div class="section-title">
<h2>
<span class="title-number">5.2</span>Setup Payment Methods </h2>
<span class="title-number">6.2</span>Setup Payment Methods </h2>
</div>
<div class="section-body">
<div class="paragraph">
@@ -350,7 +412,7 @@ Please note that the connectors seems duplicated but it because one is for Physi
</div> <div class="chapter" id="_shop_startup_guide">
<div class="chapter-title">
<h1>
<span class="title-number">6</span>Shop Startup Guide </h1>
<span class="title-number">7</span>Shop Startup Guide </h1>
</div>
<div class="chapter-body">
<div class="olist arabic">
@@ -360,7 +422,7 @@ Please note that the connectors seems duplicated but it because one is for Physi
<div class="olist loweralpha">
<ol class="loweralpha" type="a">
<li>
<p>Navigate to <strong>Shopware Admin</strong><strong>Settings</strong><strong>Extensions</strong><strong>VR Payment Payment</strong> and click on <strong>Save</strong>.</p>
<p>Navigate to <strong>Shopware Admin</strong><strong>Settings</strong><strong>Extensions</strong><strong>VR Payment</strong> and click on <strong>Save</strong>.</p>
</li>
<li>
<p>Enter your <code>Space ID</code>, <code>User ID</code>, and <code>API Key</code></p>
@@ -489,7 +551,7 @@ Please note that if you do not use the Space View Id; this option should stay em
</div> <div class="chapter" id="_transaction_state_graph">
<div class="chapter-title">
<h1>
<span class="title-number">7</span>Transaction State graph </h1>
<span class="title-number">8</span>Transaction State graph </h1>
</div>
<div class="chapter-body">
<div class="paragraph">
@@ -501,7 +563,7 @@ can be found in the <a href="https://gateway.vr-payment.de/en-us/doc/payment/tra
</div> <div class="section" id="_state_mapping_of_shopware_orders">
<div class="section-title">
<h2>
<span class="title-number">7.1</span>State mapping of Shopware orders </h2>
<span class="title-number">8.1</span>State mapping of Shopware orders </h2>
</div>
<div class="section-body">
<div class="paragraph">
@@ -509,7 +571,7 @@ can be found in the <a href="https://gateway.vr-payment.de/en-us/doc/payment/tra
</div> <div class="section" id="_general_remarks_regarding_order_statuses">
<div class="section-title">
<h3>
<span class="title-number">7.1.1</span>General remarks regarding order statuses </h3>
<span class="title-number">8.1.1</span>General remarks regarding order statuses </h3>
</div>
<div class="section-body">
<div class="paragraph">
@@ -519,7 +581,7 @@ can be found in the <a href="https://gateway.vr-payment.de/en-us/doc/payment/tra
</div> <div class="section" id="_state_mapping_of_shopware_payment_status">
<div class="section-title">
<h2>
<span class="title-number">7.2</span>State mapping of Shopware payment status </h2>
<span class="title-number">8.2</span>State mapping of Shopware payment status </h2>
</div>
<div class="section-body">
<div class="paragraph">
@@ -546,7 +608,7 @@ can be found in the <a href="https://gateway.vr-payment.de/en-us/doc/payment/tra
</div> <div class="section" id="_general_remarks_regarding_payment_statuses">
<div class="section-title">
<h3>
<span class="title-number">7.2.1</span>General remarks regarding payment statuses </h3>
<span class="title-number">8.2.1</span>General remarks regarding payment statuses </h3>
</div>
<div class="section-body">
<div class="paragraph">
@@ -556,7 +618,7 @@ can be found in the <a href="https://gateway.vr-payment.de/en-us/doc/payment/tra
</div> <div class="section" id="_state_mapping_of_shopware_delivery_status">
<div class="section-title">
<h2>
<span class="title-number">7.3</span>State mapping of Shopware delivery status </h2>
<span class="title-number">8.3</span>State mapping of Shopware delivery status </h2>
</div>
<div class="section-body">
<div class="paragraph">
@@ -582,7 +644,7 @@ can be found in the <a href="https://gateway.vr-payment.de/en-us/doc/payment/tra
</div> <div class="chapter" id="_transaction_management">
<div class="chapter-title">
<h1>
<span class="title-number">8</span>Transaction management </h1>
<span class="title-number">9</span>Transaction management </h1>
</div>
<div class="chapter-body">
<div class="paragraph">
@@ -592,7 +654,7 @@ Shopware. However, there are some limitations (see below).</p>
</div> <div class="section" id="_complete_capture_an_order">
<div class="section-title">
<h2>
<span class="title-number">8.1</span>Complete (capture) an order </h2>
<span class="title-number">9.1</span>Complete (capture) an order </h2>
</div>
<div class="section-body">
<div class="paragraph">
@@ -626,7 +688,7 @@ the fulfill state is reached. Initially the transaction will be in the <code>Aut
</div> <div class="section" id="_void_a_transaction">
<div class="section-title">
<h2>
<span class="title-number">8.2</span>Void a transaction </h2>
<span class="title-number">9.2</span>Void a transaction </h2>
</div>
<div class="section-body">
<div class="paragraph">
@@ -650,7 +712,7 @@ You can only void transactions that are not yet completed.
</div> <div class="section" id="_refund_of_a_transaction">
<div class="section-title">
<h2>
<span class="title-number">8.3</span>Refund of a transaction </h2>
<span class="title-number">9.3</span>Refund of a transaction </h2>
</div>
<div class="section-body">
<div class="paragraph">
@@ -677,7 +739,7 @@ It can take some time until you see the refund in Shopware. Refunds will only be
</div> <div class="section" id="_on_hold_orders">
<div class="section-title">
<h2>
<span class="title-number">8.4</span>On hold orders </h2>
<span class="title-number">9.4</span>On hold orders </h2>
</div>
<div class="section-body">
<div class="paragraph">
@@ -701,7 +763,7 @@ within the defined time frame, VR Payment will generate a manual task which you
</div> <div class="section" id="_limitations_of_the_synchronization_between_whitelabelname_and_shopware">
<div class="section-title">
<h2>
<span class="title-number">8.5</span>Limitations of the synchronization between VR Payment and Shopware </h2>
<span class="title-number">9.5</span>Limitations of the synchronization between VR Payment and Shopware </h2>
</div>
<div class="section-body">
<div class="paragraph">
@@ -717,7 +779,7 @@ your Shopware backend.</p>
</div> <div class="section" id="_tokenization">
<div class="section-title">
<h2>
<span class="title-number">8.6</span>Tokenization </h2>
<span class="title-number">9.6</span>Tokenization </h2>
</div>
<div class="section-body">
<div class="paragraph">
@@ -738,7 +800,7 @@ Tokenization is not available for guest checkouts.
</div> <div class="section" id="_recurring_payments">
<div class="section-title">
<h2>
<span class="title-number">8.7</span>Recurring payments </h2>
<span class="title-number">9.7</span>Recurring payments </h2>
</div>
<div class="section-body">
<div class="paragraph">
@@ -748,7 +810,7 @@ tokenization, it can be used for subscriptions. The recurring payment is fully m
</div> <div class="section" id="_key_features">
<div class="section-title">
<h2>
<span class="title-number">8.8</span>Key Features </h2>
<span class="title-number">9.8</span>Key Features </h2>
</div>
<div class="section-body">
<div class="ulist">
@@ -773,7 +835,7 @@ tokenization, it can be used for subscriptions. The recurring payment is fully m
</div> <div class="section" id="_troubleshooting">
<div class="section-title">
<h2>
<span class="title-number">8.9</span>Troubleshooting </h2>
<span class="title-number">9.9</span>Troubleshooting </h2>
</div>
<div class="section-body">
<div class="ulist">
@@ -782,7 +844,7 @@ tokenization, it can be used for subscriptions. The recurring payment is fully m
<p>Logs: Check payment logs with:</p>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-bash" data-lang="bash">tail -f var/log/whitelabelname_payment*.log</code></pre>
<pre class="highlight"><code class="language-bash" data-lang="bash">tail -f var/log/whitelabelname*.log</code></pre>
</div>
</div>
</li>
@@ -804,7 +866,7 @@ tokenization, it can be used for subscriptions. The recurring payment is fully m
</div> <div class="section" id="_faqs">
<div class="section-title">
<h2>
<span class="title-number">8.10</span>FAQs </h2>
<span class="title-number">9.10</span>FAQs </h2>
</div>
<div class="section-body">
<div class="paragraph">
@@ -836,7 +898,7 @@ tokenization, it can be used for subscriptions. The recurring payment is fully m
</div> <div class="chapter" id="_changelog">
<div class="chapter-title">
<h1>
<span class="title-number">9</span>Changelog </h1>
<span class="title-number">10</span>Changelog </h1>
</div>
<div class="chapter-body">
<div class="paragraph">
@@ -845,7 +907,7 @@ tokenization, it can be used for subscriptions. The recurring payment is fully m
</div> <div class="chapter" id="_contributing">
<div class="chapter-title">
<h1>
<span class="title-number">10</span>Contributing </h1>
<span class="title-number">11</span>Contributing </h1>
</div>
<div class="chapter-body">
<div class="paragraph">
@@ -856,7 +918,7 @@ tokenization, it can be used for subscriptions. The recurring payment is fully m
</div> <div class="chapter" id="_support">
<div class="chapter-title">
<h1>
<span class="title-number">11</span>Support </h1>
<span class="title-number">12</span>Support </h1>
</div>
<div class="chapter-body">
<div class="paragraph">
@@ -905,135 +967,157 @@ tokenization, it can be used for subscriptions. The recurring payment is fully m
<span class="item-number">4.3</span>
<span class="item-title">Manual Installation</span>
</a>
</li> <li class="nav-level-2">
<a href="#_vrpayment_does_not_appear_in_settings">
<span class="item-number">4.4</span>
<span class="item-title">VRPayment does not appear in Settings</span>
</a>
</li> </ul>
</li> <li class="nav-level-1">
<a href="#_update">
<span class="item-number">5</span>
<span class="item-title">Update</span>
</a>
<ul class="nav">
<li class="nav-level-2">
<a href="#_via_administration">
<span class="item-number">5.1</span>
<span class="item-title">Via Administration</span>
</a>
</li> <li class="nav-level-2">
<a href="#_via_cli">
<span class="item-number">5.2</span>
<span class="item-title">Via CLI</span>
</a>
</li> </ul>
</li> <li class="nav-level-1">
<a href="#portal-startup-guide">
<span class="item-number">5</span>
<span class="item-number">6</span>
<span class="item-title">Portal Startup Guide</span>
</a>
<ul class="nav">
<li class="nav-level-2">
<a href="#_create_the_api_key">
<span class="item-number">5.1</span>
<span class="item-number">6.1</span>
<span class="item-title">Create the API key:</span>
</a>
</li> <li class="nav-level-2">
<a href="#_setup_payment_methods">
<span class="item-number">5.2</span>
<span class="item-number">6.2</span>
<span class="item-title">Setup Payment Methods</span>
</a>
</li> </ul>
</li> <li class="nav-level-1">
<a href="#_shop_startup_guide">
<span class="item-number">6</span>
<span class="item-number">7</span>
<span class="item-title">Shop Startup Guide</span>
</a>
</li> <li class="nav-level-1">
<a href="#_transaction_state_graph">
<span class="item-number">7</span>
<span class="item-number">8</span>
<span class="item-title">Transaction State graph</span>
</a>
<ul class="nav">
<li class="nav-level-2">
<a href="#_state_mapping_of_shopware_orders">
<span class="item-number">7.1</span>
<span class="item-number">8.1</span>
<span class="item-title">State mapping of Shopware orders</span>
</a>
<ul class="nav">
<li class="nav-level-3">
<a href="#_general_remarks_regarding_order_statuses">
<span class="item-number">7.1.1</span>
<span class="item-number">8.1.1</span>
<span class="item-title">General remarks regarding order statuses</span>
</a>
</li> </ul>
</li> <li class="nav-level-2">
<a href="#_state_mapping_of_shopware_payment_status">
<span class="item-number">7.2</span>
<span class="item-number">8.2</span>
<span class="item-title">State mapping of Shopware payment status</span>
</a>
<ul class="nav">
<li class="nav-level-3">
<a href="#_general_remarks_regarding_payment_statuses">
<span class="item-number">7.2.1</span>
<span class="item-number">8.2.1</span>
<span class="item-title">General remarks regarding payment statuses</span>
</a>
</li> </ul>
</li> <li class="nav-level-2">
<a href="#_state_mapping_of_shopware_delivery_status">
<span class="item-number">7.3</span>
<span class="item-number">8.3</span>
<span class="item-title">State mapping of Shopware delivery status</span>
</a>
</li> </ul>
</li> <li class="nav-level-1">
<a href="#_transaction_management">
<span class="item-number">8</span>
<span class="item-number">9</span>
<span class="item-title">Transaction management</span>
</a>
<ul class="nav">
<li class="nav-level-2">
<a href="#_complete_capture_an_order">
<span class="item-number">8.1</span>
<span class="item-number">9.1</span>
<span class="item-title">Complete (capture) an order</span>
</a>
</li> <li class="nav-level-2">
<a href="#_void_a_transaction">
<span class="item-number">8.2</span>
<span class="item-number">9.2</span>
<span class="item-title">Void a transaction</span>
</a>
</li> <li class="nav-level-2">
<a href="#_refund_of_a_transaction">
<span class="item-number">8.3</span>
<span class="item-number">9.3</span>
<span class="item-title">Refund of a transaction</span>
</a>
</li> <li class="nav-level-2">
<a href="#_on_hold_orders">
<span class="item-number">8.4</span>
<span class="item-number">9.4</span>
<span class="item-title">On hold orders</span>
</a>
</li> <li class="nav-level-2">
<a href="#_limitations_of_the_synchronization_between_whitelabelname_and_shopware">
<span class="item-number">8.5</span>
<span class="item-number">9.5</span>
<span class="item-title">Limitations of the synchronization between VR Payment and Shopware</span>
</a>
</li> <li class="nav-level-2">
<a href="#_tokenization">
<span class="item-number">8.6</span>
<span class="item-number">9.6</span>
<span class="item-title">Tokenization</span>
</a>
</li> <li class="nav-level-2">
<a href="#_recurring_payments">
<span class="item-number">8.7</span>
<span class="item-number">9.7</span>
<span class="item-title">Recurring payments</span>
</a>
</li> <li class="nav-level-2">
<a href="#_key_features">
<span class="item-number">8.8</span>
<span class="item-number">9.8</span>
<span class="item-title">Key Features</span>
</a>
</li> <li class="nav-level-2">
<a href="#_troubleshooting">
<span class="item-number">8.9</span>
<span class="item-number">9.9</span>
<span class="item-title">Troubleshooting</span>
</a>
</li> <li class="nav-level-2">
<a href="#_faqs">
<span class="item-number">8.10</span>
<span class="item-number">9.10</span>
<span class="item-title">FAQs</span>
</a>
</li> </ul>
</li> <li class="nav-level-1">
<a href="#_changelog">
<span class="item-number">9</span>
<span class="item-number">10</span>
<span class="item-title">Changelog</span>
</a>
</li> <li class="nav-level-1">
<a href="#_contributing">
<span class="item-number">10</span>
<span class="item-number">11</span>
<span class="item-title">Contributing</span>
</a>
</li> <li class="nav-level-1">
<a href="#_support">
<span class="item-number">11</span>
<span class="item-number">12</span>
<span class="item-title">Support</span>
</a>
</li> </ul>
+166 -82
View File
@@ -5,15 +5,15 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<meta name="keywords" value="VR Payment, Shopware, Shopware Plugin, Payment, Payment Integration, Documentation"><meta name="description" value="The documentation for the Shopware 6 plugin that enables processing payments with VR Payment.">
<link rel="canonical" href="@WalleeCanonicalPath(https://plugin-documentation.wallee.com/wallee-payment, VRPaymentPayment/docs/fr/documentation.html)" />
<title>Wallee Payment Plugin pour Shopware 6</title>
<link rel="canonical" href="https://plugin-documentation.wallee.com/wallee-payment/shopware-6/master/VRPaymentPayment/docs/fr/documentation.html" />
<title>VR Payment Plugin pour Shopware 6</title>
<link href="assets/monokai-sublime.css" rel="stylesheet" />
<link href="assets/base.css" rel="stylesheet" />
</head>
<body class="documentation">
<div class="layout-wrapper">
<div class="layout-title">
<h1>Wallee Payment Plugin pour Shopware 6</h1>
<h1>VR Payment Plugin pour Shopware 6</h1>
<h2>Documentation</h2> </div>
<div class="layout-navigation">
<ul class="nav">
@@ -23,7 +23,7 @@
</a>
</li>
<li>
<a href="@WalleeReleasePath()">
<a href="https://github.com/vr-payment/shopware-6/releases/tag/7.3.4/">
Source
</a>
</li>
@@ -39,7 +39,7 @@
</div>
<div class="chapter-body">
<div class="paragraph">
<p>Le plugin de paiement Wallee intègre un traitement moderne des paiements dans Shopware 6, offrant des fonctionnalités telles que les paiements basés sur iFrame, les remboursements, les captures et la conformité PCI. Il permet une intégration transparente avec le portail Wallee pour la gestion des transactions et des méthodes de paiement.</p>
<p>Le plugin de paiement VR Payment intègre un traitement moderne des paiements dans Shopware 6, offrant des fonctionnalités telles que les paiements basés sur iFrame, les remboursements, les captures et la conformité PCI. Il permet une intégration transparente avec le portail VR Payment pour la gestion des transactions et des méthodes de paiement.</p>
</div><div class="paragraph">
<p>Important : Veuillez noter que seules les mises à jour majeures (par exemple 6.x.0.0) et mineures (par exemple 6.0.x.0) seront testées pour la compatibilité dans les 2 semaines suivant la publication.</p>
</div> </div>
@@ -54,7 +54,7 @@
</div><div class="paragraph">
<p><strong>PHP</strong>: Version minimale requise pour votre installation Shopware (e.g., 7.4+).</p>
</div><div class="paragraph">
<p><strong>Compte Wallee</strong>: Obtenir Space ID, User ID, et clé API du <strong>Portail Wallee</strong> (voir le <a href="#portal-startup-guide">Guide de démarrage du Portail</a>).</p>
<p><strong>Compte VR Payment</strong>: Obtenir Space ID, User ID, et clé API du <strong>Portail VR Payment</strong> (voir le <a href="#portal-startup-guide">Guide de démarrage du Portail</a>).</p>
</div> </div>
</div> <div class="chapter" id="compatibility">
<div class="chapter-title">
@@ -125,7 +125,7 @@
</div>
</li>
<li>
<p>Activez le plugin wallee Payment à partir du gestionnaire de plugins</p>
<p>Activez le plugin VR Payment à partir du gestionnaire de plugins</p>
</li>
</ol>
</div> </div>
@@ -146,7 +146,7 @@
<div class="content">
<pre class="highlight"><code class="language-bash" data-lang="bash">composer require vrpayment/shopware-6
php bin/console plugin:refresh
php bin/console plugin:install --activate --clearCache VRPaymentPayment</code></pre>
php bin/console plugin:install --activate --clearCache VRPayment</code></pre>
</div>
</div>
</li>
@@ -167,14 +167,76 @@ php bin/console plugin:install --activate --clearCache VRPaymentPayment</code></
</div><div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-bash" data-lang="bash">php bin/console plugin:refresh
php bin/console plugin:install --activate --clearCache VRPaymentPayment</code></pre>
php bin/console plugin:install --activate --clearCache VRPayment</code></pre>
</div>
</div> </div>
</div> <div class="section" id="_si_vrpayment_n_apparaît_pas_dans_les_paramètres">
<div class="section-title">
<h2>
<span class="title-number">4.4</span>Si VRPayment n&#8217;apparaît pas dans les paramètres </h2>
</div>
<div class="section-body">
<div class="paragraph">
<p>Vous devez exécuter la commande suivante</p>
</div><div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-bash" data-lang="bash">bin/build-administration.sh</code></pre>
</div>
</div> </div>
</div> </div>
</div> <div class="chapter" id="_mise_à_jour">
<div class="chapter-title">
<h1>
<span class="title-number">5</span>Mise à jour </h1>
</div>
<div class="chapter-body">
<div class="section" id="_via_l_administration">
<div class="section-title">
<h2>
<span class="title-number">5.1</span>Via ladministration </h2>
</div>
<div class="section-body">
<div class="olist arabic">
<ol class="arabic">
<li>
<p>Allez dans Shopware Admin &gt; Extensions &gt; Mes extensions.</p>
</li>
<li>
<p>Recherchez VRPaymentPayment.</p>
</li>
<li>
<p>Cliquez sur Mettre à jour.</p>
</li>
</ol>
</div> </div>
</div> <div class="section" id="_via_la_cli">
<div class="section-title">
<h2>
<span class="title-number">5.2</span>Via la CLI </h2>
</div>
<div class="section-body">
<div class="olist arabic">
<ol class="arabic">
<li>
<p>Déployez les nouveaux fichiers du plugin (remplacez le dossier <code>custom/plugins/VRPaymentPayment</code> ou téléversez/installez un nouveau ZIP).</p>
</li>
<li>
<p>Exécutez :</p>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-bash" data-lang="bash">bin/console plugin:refresh
bin/console plugin:update --clearCache VRPaymentPayment
bin/console cache:clear</code></pre>
</div>
</div>
</li>
</ol>
</div> </div>
</div> </div>
</div> <div class="chapter" id="portal-startup-guide">
<div class="chapter-title">
<h1>
<span class="title-number">5</span>Guide de démarrage pour le Portail </h1>
<span class="title-number">6</span>Guide de démarrage pour le Portail </h1>
</div>
<div class="chapter-body">
<div class="paragraph">
@@ -193,7 +255,7 @@ Veuillez sélectionner le plan d&#8217;abonnement approprié - il doit prendre e
</div> <div class="section" id="_créez_la_clé_api">
<div class="section-title">
<h2>
<span class="title-number">5.1</span>Créez la clé API: </h2>
<span class="title-number">6.1</span>Créez la clé API: </h2>
</div>
<div class="section-body">
<div class="olist arabic">
@@ -279,7 +341,7 @@ Veuillez noter que le chargement des rôles peut durer quelques secondes.
</div> <div class="section" id="_configurer_les_modes_de_paiement">
<div class="section-title">
<h2>
<span class="title-number">5.2</span>Configurer les modes de paiement </h2>
<span class="title-number">6.2</span>Configurer les modes de paiement </h2>
</div>
<div class="section-body">
<div class="paragraph">
@@ -341,7 +403,7 @@ Veuillez noter que les connecteurs semblent faire double emploi, mais c&#8217;es
</div> <div class="chapter" id="_guide_de_démarrage_pour_shopware">
<div class="chapter-title">
<h1>
<span class="title-number">6</span>Guide de démarrage pour Shopware </h1>
<span class="title-number">7</span>Guide de démarrage pour Shopware </h1>
</div>
<div class="chapter-body">
<div class="olist arabic">
@@ -351,7 +413,7 @@ Veuillez noter que les connecteurs semblent faire double emploi, mais c&#8217;es
<div class="olist loweralpha">
<ol class="loweralpha" type="a">
<li>
<p>Naviguez vers <strong>Shopware Admin → Paramètres → Extensions → Wallee Payment</strong> et cliquez sur Sauvegarder.</p>
<p>Naviguez vers <strong>Shopware Admin → Paramètres → Extensions → VR Payment</strong> et cliquez sur Sauvegarder.</p>
</li>
<li>
<p>Entrez votre <code>Space ID</code>, <code>User ID</code>, et <code>clé API</code></p>
@@ -374,7 +436,7 @@ Veuillez noter que les connecteurs semblent faire double emploi, mais c&#8217;es
<li>
<p><strong>Méthodes de Paiment</strong></p>
<div class="paragraph">
<p>Les méthodes de paiement disponibles à la caisse sont gérées par le Portail Wallee. Si vous souhaitez désactiver une méthode de paiement, vous devez la désactiver à partir du portail.</p>
<p>Les méthodes de paiement disponibles à la caisse sont gérées par le Portail VR Payment. Si vous souhaitez désactiver une méthode de paiement, vous devez la désactiver à partir du portail.</p>
</div>
<div class="admonitionblock note">
<table>
@@ -480,17 +542,17 @@ Veuillez noter que si vous n&#8217;utilisez pas Space View Id, cette option doit
</div> <div class="chapter" id="_différents_etats_pour_une_transaction">
<div class="chapter-title">
<h1>
<span class="title-number">7</span>Différents Etats pour une Transaction </h1>
<span class="title-number">8</span>Différents Etats pour une Transaction </h1>
</div>
<div class="chapter-body">
<div class="paragraph">
<p>Le processus de paiement de wallee est complètement standardisé pour chaque méthode de paiement que vous pouvez traiter. Cela vous permet d&#8217;ajouter simplement une méthode de paiement ou un processeur sans modifier la configuration de votre Shopware. Une vue d&#8217;ensemble des états et des processus de paiement de wallee peut être trouvée dans la <a href="https://gateway.vr-payment.de/en-us/doc/payment/transaction-process" target="_blank">documentation sur les paiments.</a>.</p>
<p>Le processus de paiement de VR Payment est complètement standardisé pour chaque méthode de paiement que vous pouvez traiter. Cela vous permet d&#8217;ajouter simplement une méthode de paiement ou un processeur sans modifier la configuration de votre Shopware. Une vue d&#8217;ensemble des états et des processus de paiement de VR Payment peut être trouvée dans la <a href="https://gateway.vr-payment.de/en-us/doc/payment/transaction-process" target="_blank">documentation sur les paiments.</a>.</p>
</div><div class="paragraph">
<p>Dans la section suivante, nous vous donnons un aperçu de la façon dont les états de wallee sont mappés dans le graphique des états de Shopware pour les commandes et les états de paiement.</p>
<p>Dans la section suivante, nous vous donnons un aperçu de la façon dont les états de VR Payment sont mappés dans le graphique des états de Shopware pour les commandes et les états de paiement.</p>
</div> <div class="section" id="_cartographie_des_différents_états_d_une_commande_de_shopware">
<div class="section-title">
<h2>
<span class="title-number">7.1</span>Cartographie des différents états dune commande de Shopware </h2>
<span class="title-number">8.1</span>Cartographie des différents états dune commande de Shopware </h2>
</div>
<div class="section-body">
<div class="paragraph">
@@ -498,7 +560,7 @@ Veuillez noter que si vous n&#8217;utilisez pas Space View Id, cette option doit
</div> <div class="section" id="_remarque_générales_concernant_les_status_des_commandes">
<div class="section-title">
<h3>
<span class="title-number">7.1.1</span>Remarque générales concernant les status des commandes </h3>
<span class="title-number">8.1.1</span>Remarque générales concernant les status des commandes </h3>
</div>
<div class="section-body">
<div class="paragraph">
@@ -508,11 +570,11 @@ Veuillez noter que si vous n&#8217;utilisez pas Space View Id, cette option doit
</div> <div class="section" id="_cartographie_des_différents_états_du_paiement_de_shopware">
<div class="section-title">
<h2>
<span class="title-number">7.2</span>Cartographie des différents états du paiement de Shopware </h2>
<span class="title-number">8.2</span>Cartographie des différents états du paiement de Shopware </h2>
</div>
<div class="section-body">
<div class="paragraph">
<p>Vous trouverez ci-dessous un diagramme qui montre lassociation des différents états de Shopware pour l&#8217;état de paiement pour wallee, ainsi que des informations supplémentaires sur les transitions entre les états.</p>
<p>Vous trouverez ci-dessous un diagramme qui montre lassociation des différents états de Shopware pour l&#8217;état de paiement pour VR Payment, ainsi que des informations supplémentaires sur les transitions entre les états.</p>
</div><div class="imageblock">
<div class="content">
<img src="resource/shopware_6_stage_graph_order.svg" alt="shopware 6 stage graph order"/>
@@ -520,7 +582,7 @@ Veuillez noter que si vous n&#8217;utilisez pas Space View Id, cette option doit
</div><div class="olist glossary">
<ol class="glossary">
<li>
<p>Si la transaction est <code>Autorisée</code> dans wallee, le statut du paiement de la commande dans le Shopware est marqué comme étant <code>En Cours</code>.</p>
<p>Si la transaction est <code>Autorisée</code> dans VR Payment, le statut du paiement de la commande dans le Shopware est marqué comme étant <code>En Cours</code>.</p>
</li>
<li>
<p>Si la transaction échoue avant ou pendant le processus d&#8217;autorisation, le statut du paiement de la commande du Shopware est marqué comme <code>Échouée</code>.</p>
@@ -529,13 +591,13 @@ Veuillez noter que si vous n&#8217;utilisez pas Space View Id, cette option doit
<p>Si la transaction échoue après l&#8217;autorisation, le statut du paiement de la commande du Shopware est marqué comme <code>Annulée</code>.</p>
</li>
<li>
<p>Si la facture de la transaction dans wallee est marquée comme <code>Payée</code> ou <code>Non Applicable</code>, le statut du paiement de la commande dans le Shopware est marqué comme <code>Payée</code>.</p>
<p>Si la facture de la transaction dans VR Payment est marquée comme <code>Payée</code> ou <code>Non Applicable</code>, le statut du paiement de la commande dans le Shopware est marqué comme <code>Payée</code>.</p>
</li>
</ol>
</div> <div class="section" id="_remarques_générales_concernant_les_différents_status_pour_les_paiements">
<div class="section-title">
<h3>
<span class="title-number">7.2.1</span>Remarques générales concernant les différents status pour les paiements </h3>
<span class="title-number">8.2.1</span>Remarques générales concernant les différents status pour les paiements </h3>
</div>
<div class="section-body">
<div class="paragraph">
@@ -545,7 +607,7 @@ Veuillez noter que si vous n&#8217;utilisez pas Space View Id, cette option doit
</div> <div class="section" id="_carthographie_des_différents_états_de_livraison_chez_shopware">
<div class="section-title">
<h2>
<span class="title-number">7.3</span>Carthographie des différents états de livraison chez Shopware </h2>
<span class="title-number">8.3</span>Carthographie des différents états de livraison chez Shopware </h2>
</div>
<div class="section-body">
<div class="paragraph">
@@ -557,10 +619,10 @@ Veuillez noter que si vous n&#8217;utilisez pas Space View Id, cette option doit
</div><div class="olist glossary">
<ol class="glossary">
<li>
<p>Si la transaction est <code>confirmée</code> dans wallee, le statut de livraison de la commande dans le Shopware est indiqué comme étant <code>En Attente</code>.</p>
<p>Si la transaction est <code>confirmée</code> dans VR Payment, le statut de livraison de la commande dans le Shopware est indiqué comme étant <code>En Attente</code>.</p>
</li>
<li>
<p>Si la transaction dans wallee est marquée comme <code>Délivrée</code>, le statut de livraison de la commande Shopware est marqué comme <code>Ouvert</code>.</p>
<p>Si la transaction dans VR Payment est marquée comme <code>Délivrée</code>, le statut de livraison de la commande Shopware est marqué comme <code>Ouvert</code>.</p>
</li>
<li>
<p>Si la transaction est en statut <code>Déclinée</code>, <code>Échouée</code> ou <code>Annulée</code>, le statut de livraison de la commande du Shopware est marqué comme <code>Annulée</code>.</p>
@@ -571,15 +633,15 @@ Veuillez noter que si vous n&#8217;utilisez pas Space View Id, cette option doit
</div> <div class="chapter" id="_gestion_des_transactions">
<div class="chapter-title">
<h1>
<span class="title-number">8</span>Gestion des Transactions </h1>
<span class="title-number">9</span>Gestion des Transactions </h1>
</div>
<div class="chapter-body">
<div class="paragraph">
<p>Vous pouvez capturer, annuler et rembourser des transactions directement depuis le backend de Shopware. Veuillez noter que si vous remboursez, annulez ou capturez des transactions dans wallee, les événements seront synchronisés dans Shopware. Cependant, il y a quelques limitations (voir ci-dessous).</p>
<p>Vous pouvez capturer, annuler et rembourser des transactions directement depuis le backend de Shopware. Veuillez noter que si vous remboursez, annulez ou capturez des transactions dans VR Payment, les événements seront synchronisés dans Shopware. Cependant, il y a quelques limitations (voir ci-dessous).</p>
</div> <div class="section" id="_complete_capture_an_order">
<div class="section-title">
<h2>
<span class="title-number">8.1</span>Complete (capture) an order </h2>
<span class="title-number">9.1</span>Complete (capture) an order </h2>
</div>
<div class="section-body">
<div class="paragraph">
@@ -593,7 +655,7 @@ Veuillez noter que si vous n&#8217;utilisez pas Space View Id, cette option doit
<div class="title">Note</div>
</td>
<td class="content">
Lorsque le paiement est en attente dans wallee, la commande reste en attente.
Lorsque le paiement est en attente dans VR Payment, la commande reste en attente.
</td>
</tr>
</table>
@@ -604,14 +666,14 @@ Lorsque le paiement est en attente dans wallee, la commande reste en attente.
</div><div class="paragraph">
<p><strong>Finalisation du paiement de manière différée</strong></p>
</div><div class="paragraph">
<p>Les détaillants souhaitent souvent autoriser les transactions et lancer le processus d&#8217;exécution une fois que tous les articles peuvent être expédiés. Cela est également possible avec wallee.</p>
<p>Les détaillants souhaitent souvent autoriser les transactions et lancer le processus d&#8217;exécution une fois que tous les articles peuvent être expédiés. Cela est également possible avec VR Payment.</p>
</div><div class="paragraph">
<p>Cependant, certains processus doivent être suivis. Si vous avez configuré la finalisation du paiement pour qu&#8217;il soit différé, vous devez capturer la transaction avant d&#8217;initier l&#8217;expédition, car il peut toujours arriver quune finalisation échoue. Si vous voulez être sûr de ne pas expédier d&#8217;articles pour lesquels vous n&#8217;avez pas été payé, vous devez reporter l&#8217;expédition jusqu&#8217;à ce que l&#8217;état <code>Confirmé</code> soit atteint. Au départ, la transaction sera dans l&#8217;état <code>Autorisé</code> dans wallee et <code>En cours</code> dans Shopware. Si vous souhaitez lancer le processus d&#8217;exécution, assurez-vous de lancer le processus d&#8217;achèvement comme décrit ci-dessus. Une fois le processus terminé avec succès, la commande passera à l&#8217;état <code>Confirmée</code> dans wallee et à l&#8217;état Payée dans Shopware. Vous pouvez maintenant lancer le processus de livraison .</p>
<p>Cependant, certains processus doivent être suivis. Si vous avez configuré la finalisation du paiement pour qu&#8217;il soit différé, vous devez capturer la transaction avant d&#8217;initier l&#8217;expédition, car il peut toujours arriver quune finalisation échoue. Si vous voulez être sûr de ne pas expédier d&#8217;articles pour lesquels vous n&#8217;avez pas été payé, vous devez reporter l&#8217;expédition jusqu&#8217;à ce que l&#8217;état <code>Confirmé</code> soit atteint. Au départ, la transaction sera dans l&#8217;état <code>Autorisé</code> dans VR Payment et <code>En cours</code> dans Shopware. Si vous souhaitez lancer le processus d&#8217;exécution, assurez-vous de lancer le processus d&#8217;achèvement comme décrit ci-dessus. Une fois le processus terminé avec succès, la commande passera à l&#8217;état <code>Confirmée</code> dans VR Payment et à l&#8217;état Payée dans Shopware. Vous pouvez maintenant lancer le processus de livraison .</p>
</div> </div>
</div> <div class="section" id="_annuler_une_transaction">
<div class="section-title">
<h2>
<span class="title-number">8.2</span>Annuler une transaction </h2>
<span class="title-number">9.2</span>Annuler une transaction </h2>
</div>
<div class="section-body">
<div class="paragraph">
@@ -635,7 +697,7 @@ Vous ne pouvez annuler que les transactions qui ne sont pas encore complétée..
</div> <div class="section" id="_remboursement_d_une_transaction">
<div class="section-title">
<h2>
<span class="title-number">8.3</span>Remboursement d&#8217;une transaction </h2>
<span class="title-number">9.3</span>Remboursement d&#8217;une transaction </h2>
</div>
<div class="section-body">
<div class="paragraph">
@@ -661,11 +723,11 @@ Il peut s&#8217;écouler un certain temps avant que vous ne voyiez le remboursem
</div> <div class="section" id="_commandes_en_attente">
<div class="section-title">
<h2>
<span class="title-number">8.4</span>Commandes en attente </h2>
<span class="title-number">9.4</span>Commandes en attente </h2>
</div>
<div class="section-body">
<div class="paragraph">
<p>La livraison ne doit pas être effectuée tant que l&#8217;état de la livraison est en attente. Cela se produit lorsque la transaction dans wallee n&#8217;a pas atteint l&#8217;état Confirmé.</p>
<p>La livraison ne doit pas être effectuée tant que l&#8217;état de la livraison est en attente. Cela se produit lorsque la transaction dans VR Payment n&#8217;a pas atteint l&#8217;état Confirmé.</p>
</div><div class="paragraph">
<p>Il y a essentiellement deux raisons pour lesquelles cela peut se produire :</p>
</div><div class="ulist">
@@ -674,27 +736,27 @@ Il peut s&#8217;écouler un certain temps avant que vous ne voyiez le remboursem
<p>La transaction n&#8217;est pas terminée. Dans ce cas, vous devez compléter la transaction comme indiqué ci-dessus..</p>
</li>
<li>
<p>Nous ne sommes pas en mesure de déterminer si vous devez honorer la commande. La décision de livraison est prise automatiquement. Si cela ne se produit pas dans le délai défini, wallee génère une tâche manuelle que vous devez observer et suivre les instructions.</p>
<p>Nous ne sommes pas en mesure de déterminer si vous devez honorer la commande. La décision de livraison est prise automatiquement. Si cela ne se produit pas dans le délai défini, VR Payment génère une tâche manuelle que vous devez observer et suivre les instructions.</p>
</li>
</ul>
</div><div class="paragraph">
<p>Vous trouverez plus d&#8217;informations sur les tâches manuelles dans notre <a href="https://gateway.vr-payment.de/en-us/doc/manual-tasks" target="_blank">Documentation sur les Tâches Manuelles.</a>.</p>
</div> </div>
</div> <div class="section" id="_limites_de_la_synchronisation_entre_wallee_et_shopware">
</div> <div class="section" id="_limites_de_la_synchronisation_entre_whitelabelname_et_shopware">
<div class="section-title">
<h2>
<span class="title-number">8.5</span>Limites de la synchronisation entre wallee et Shopware </h2>
<span class="title-number">9.5</span>Limites de la synchronisation entre VR Payment et Shopware </h2>
</div>
<div class="section-body">
<div class="paragraph">
<p>Veuillez noter que les captures, annulations et remboursements effectués dans wallee sont synchronisés. Cependant, il y a quelques limitations. Dans wallee, vous pouvez modifier le <strong>prix unitaire</strong> et <strong>la quantité</strong> en une seule fois. Cela n&#8217;est pas possible dans le backend du Shopware. Nous vous recommandons donc d&#8217;effectuer les remboursements toujours dans le backend de Shopware et non dans wallee. Si un remboursement ne peut pas être synchronisé, il sera envoyé au processeur, mais il se peut que vous ne le voyiez pas dans votre backend Shopware.</p>
<p>Veuillez noter que les captures, annulations et remboursements effectués dans VR Payment sont synchronisés. Cependant, il y a quelques limitations. Dans VR Payment, vous pouvez modifier le <strong>prix unitaire</strong> et <strong>la quantité</strong> en une seule fois. Cela n&#8217;est pas possible dans le backend du Shopware. Nous vous recommandons donc d&#8217;effectuer les remboursements toujours dans le backend de Shopware et non dans VR Payment. Si un remboursement ne peut pas être synchronisé, il sera envoyé au processeur, mais il se peut que vous ne le voyiez pas dans votre backend Shopware.</p>
</div><div class="paragraph">
<p>Vous pouvez trouver plus d&#8217;informations sur les remboursements dans wallee dans notre <a href="https://gateway.vr-payment.de/en-us/doc/payment/refund" target="_blank">Documentation sur les Remboursements.</a>.</p>
<p>Vous pouvez trouver plus d&#8217;informations sur les remboursements dans VR Payment dans notre <a href="https://gateway.vr-payment.de/en-us/doc/payment/refund" target="_blank">Documentation sur les Remboursements.</a>.</p>
</div> </div>
</div> <div class="section" id="_tokenisation">
<div class="section-title">
<h2>
<span class="title-number">8.6</span>Tokenisation </h2>
<span class="title-number">9.6</span>Tokenisation </h2>
</div>
<div class="section-body">
<div class="paragraph">
@@ -714,7 +776,7 @@ La tokenisation n&#8217;est pas disponible pour les paiements par les invités.
</div> <div class="section" id="_paiements_récurrents">
<div class="section-title">
<h2>
<span class="title-number">8.7</span>Paiements récurrents </h2>
<span class="title-number">9.7</span>Paiements récurrents </h2>
</div>
<div class="section-body">
<div class="paragraph">
@@ -723,7 +785,7 @@ La tokenisation n&#8217;est pas disponible pour les paiements par les invités.
</div> <div class="section" id="_caractéristiques_pricinpales">
<div class="section-title">
<h2>
<span class="title-number">8.8</span>Caractéristiques Pricinpales </h2>
<span class="title-number">9.8</span>Caractéristiques Pricinpales </h2>
</div>
<div class="section-body">
<div class="ulist">
@@ -732,13 +794,13 @@ La tokenisation n&#8217;est pas disponible pour les paiements par les invités.
<p><strong>Intégration iFrame</strong>: Intégrez des formulaires de paiement directement dans votre checkout.</p>
</li>
<li>
<p><strong>Remboursements &amp; Captures</strong>: Déclenchez des remboursements complets/partiels et des captures à partir de Shopware ou du portail Wallee.</p>
<p><strong>Remboursements &amp; Captures</strong>: Déclenchez des remboursements complets/partiels et des captures à partir de Shopware ou du portail VR Payment.</p>
</li>
<li>
<p><strong>Support Multi-Magasins</strong>: Gérez les configurations sur plusieurs magasins.</p>
</li>
<li>
<p><strong>Mises à jour automatiques</strong>: Les méthodes de paiement se synchronisent dynamiquement via l&#8217;API Wallee.</p>
<p><strong>Mises à jour automatiques</strong>: Les méthodes de paiement se synchronisent dynamiquement via l&#8217;API VR Payment.</p>
</li>
<li>
<p><strong>Paiements récurrents</strong>: Effectuez des paiements récurrents avec les abonnements (Shopware Commercial) et VRPayment.</p>
@@ -748,7 +810,7 @@ La tokenisation n&#8217;est pas disponible pour les paiements par les invités.
</div> <div class="section" id="_troubleshooting">
<div class="section-title">
<h2>
<span class="title-number">8.9</span>Troubleshooting </h2>
<span class="title-number">9.9</span>Troubleshooting </h2>
</div>
<div class="section-body">
<div class="ulist">
@@ -757,7 +819,7 @@ La tokenisation n&#8217;est pas disponible pour les paiements par les invités.
<p>Logs: Vérifiez les logs des payments avec:</p>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-bash" data-lang="bash">tail -f var/log/whitelabelname_payment*.log</code></pre>
<pre class="highlight"><code class="language-bash" data-lang="bash">tail -f var/log/whitelabelname*.log</code></pre>
</div>
</div>
</li>
@@ -769,7 +831,7 @@ La tokenisation n&#8217;est pas disponible pour les paiements par les invités.
<p>Assurez-vous que la commande <code>composer update whitelabelname/shopware-6</code> est exécutée après les mises à jour.</p>
</li>
<li>
<p>Vérifier que les identifiants de l&#8217;API correspondent à votre compte Wallee.</p>
<p>Vérifier que les identifiants de l&#8217;API correspondent à votre compte VR Payment.</p>
</li>
</ul>
</div>
@@ -779,7 +841,7 @@ La tokenisation n&#8217;est pas disponible pour les paiements par les invités.
</div> <div class="section" id="_faqs">
<div class="section-title">
<h2>
<span class="title-number">8.10</span>FAQs </h2>
<span class="title-number">9.10</span>FAQs </h2>
</div>
<div class="section-body">
<div class="paragraph">
@@ -795,7 +857,7 @@ A: Vous devez vérifier que les webhooks ont été correctement créés. Pour ce
</div>
</div><div class="paragraph">
<p><strong>Q: Ce plugin prend-il en charge les paiements en un clic ?</strong>
A: Oui, via la tokenisation dans le portail Wallee.</p>
A: Oui, via la tokenisation dans le portail VR Payment.</p>
</div><div class="paragraph">
<p><strong>Q: Comment gérer la conformité PCI ?</strong>
A: Le plugin utilise l&#8217;intégration iFrame, réduisant les exigences PCI à SAQ-A.</p>
@@ -807,7 +869,7 @@ A: Oui, le plugin prend en charge les portefeuilles comme Apple Pay.</p>
</div> <div class="chapter" id="_changelog">
<div class="chapter-title">
<h1>
<span class="title-number">9</span>Changelog </h1>
<span class="title-number">10</span>Changelog </h1>
</div>
<div class="chapter-body">
<div class="paragraph">
@@ -816,7 +878,7 @@ A: Oui, le plugin prend en charge les portefeuilles comme Apple Pay.</p>
</div> <div class="chapter" id="_contribuer">
<div class="chapter-title">
<h1>
<span class="title-number">10</span>Contribuer </h1>
<span class="title-number">11</span>Contribuer </h1>
</div>
<div class="chapter-body">
<div class="paragraph">
@@ -827,7 +889,7 @@ A: Oui, le plugin prend en charge les portefeuilles comme Apple Pay.</p>
</div> <div class="chapter" id="_support">
<div class="chapter-title">
<h1>
<span class="title-number">11</span>Support </h1>
<span class="title-number">12</span>Support </h1>
</div>
<div class="chapter-body">
<div class="paragraph">
@@ -876,135 +938,157 @@ A: Oui, le plugin prend en charge les portefeuilles comme Apple Pay.</p>
<span class="item-number">4.3</span>
<span class="item-title">Via Composer (Recommended)</span>
</a>
</li> <li class="nav-level-2">
<a href="#_si_vrpayment_n_apparaît_pas_dans_les_paramètres">
<span class="item-number">4.4</span>
<span class="item-title">Si VRPayment n&amp;#8217;apparaît pas dans les paramètres</span>
</a>
</li> </ul>
</li> <li class="nav-level-1">
<a href="#_mise_à_jour">
<span class="item-number">5</span>
<span class="item-title">Mise à jour</span>
</a>
<ul class="nav">
<li class="nav-level-2">
<a href="#_via_l_administration">
<span class="item-number">5.1</span>
<span class="item-title">Via ladministration</span>
</a>
</li> <li class="nav-level-2">
<a href="#_via_la_cli">
<span class="item-number">5.2</span>
<span class="item-title">Via la CLI</span>
</a>
</li> </ul>
</li> <li class="nav-level-1">
<a href="#portal-startup-guide">
<span class="item-number">5</span>
<span class="item-number">6</span>
<span class="item-title">Guide de démarrage pour le Portail</span>
</a>
<ul class="nav">
<li class="nav-level-2">
<a href="#_créez_la_clé_api">
<span class="item-number">5.1</span>
<span class="item-number">6.1</span>
<span class="item-title">Créez la clé API:</span>
</a>
</li> <li class="nav-level-2">
<a href="#_configurer_les_modes_de_paiement">
<span class="item-number">5.2</span>
<span class="item-number">6.2</span>
<span class="item-title">Configurer les modes de paiement</span>
</a>
</li> </ul>
</li> <li class="nav-level-1">
<a href="#_guide_de_démarrage_pour_shopware">
<span class="item-number">6</span>
<span class="item-number">7</span>
<span class="item-title">Guide de démarrage pour Shopware</span>
</a>
</li> <li class="nav-level-1">
<a href="#_différents_etats_pour_une_transaction">
<span class="item-number">7</span>
<span class="item-number">8</span>
<span class="item-title">Différents Etats pour une Transaction</span>
</a>
<ul class="nav">
<li class="nav-level-2">
<a href="#_cartographie_des_différents_états_d_une_commande_de_shopware">
<span class="item-number">7.1</span>
<span class="item-number">8.1</span>
<span class="item-title">Cartographie des différents états dune commande de Shopware</span>
</a>
<ul class="nav">
<li class="nav-level-3">
<a href="#_remarque_générales_concernant_les_status_des_commandes">
<span class="item-number">7.1.1</span>
<span class="item-number">8.1.1</span>
<span class="item-title">Remarque générales concernant les status des commandes</span>
</a>
</li> </ul>
</li> <li class="nav-level-2">
<a href="#_cartographie_des_différents_états_du_paiement_de_shopware">
<span class="item-number">7.2</span>
<span class="item-number">8.2</span>
<span class="item-title">Cartographie des différents états du paiement de Shopware</span>
</a>
<ul class="nav">
<li class="nav-level-3">
<a href="#_remarques_générales_concernant_les_différents_status_pour_les_paiements">
<span class="item-number">7.2.1</span>
<span class="item-number">8.2.1</span>
<span class="item-title">Remarques générales concernant les différents status pour les paiements</span>
</a>
</li> </ul>
</li> <li class="nav-level-2">
<a href="#_carthographie_des_différents_états_de_livraison_chez_shopware">
<span class="item-number">7.3</span>
<span class="item-number">8.3</span>
<span class="item-title">Carthographie des différents états de livraison chez Shopware</span>
</a>
</li> </ul>
</li> <li class="nav-level-1">
<a href="#_gestion_des_transactions">
<span class="item-number">8</span>
<span class="item-number">9</span>
<span class="item-title">Gestion des Transactions</span>
</a>
<ul class="nav">
<li class="nav-level-2">
<a href="#_complete_capture_an_order">
<span class="item-number">8.1</span>
<span class="item-number">9.1</span>
<span class="item-title">Complete (capture) an order</span>
</a>
</li> <li class="nav-level-2">
<a href="#_annuler_une_transaction">
<span class="item-number">8.2</span>
<span class="item-number">9.2</span>
<span class="item-title">Annuler une transaction</span>
</a>
</li> <li class="nav-level-2">
<a href="#_remboursement_d_une_transaction">
<span class="item-number">8.3</span>
<span class="item-number">9.3</span>
<span class="item-title">Remboursement d&amp;#8217;une transaction</span>
</a>
</li> <li class="nav-level-2">
<a href="#_commandes_en_attente">
<span class="item-number">8.4</span>
<span class="item-number">9.4</span>
<span class="item-title">Commandes en attente</span>
</a>
</li> <li class="nav-level-2">
<a href="#_limites_de_la_synchronisation_entre_wallee_et_shopware">
<span class="item-number">8.5</span>
<span class="item-title">Limites de la synchronisation entre wallee et Shopware</span>
<a href="#_limites_de_la_synchronisation_entre_whitelabelname_et_shopware">
<span class="item-number">9.5</span>
<span class="item-title">Limites de la synchronisation entre VR Payment et Shopware</span>
</a>
</li> <li class="nav-level-2">
<a href="#_tokenisation">
<span class="item-number">8.6</span>
<span class="item-number">9.6</span>
<span class="item-title">Tokenisation</span>
</a>
</li> <li class="nav-level-2">
<a href="#_paiements_récurrents">
<span class="item-number">8.7</span>
<span class="item-number">9.7</span>
<span class="item-title">Paiements récurrents</span>
</a>
</li> <li class="nav-level-2">
<a href="#_caractéristiques_pricinpales">
<span class="item-number">8.8</span>
<span class="item-number">9.8</span>
<span class="item-title">Caractéristiques Pricinpales</span>
</a>
</li> <li class="nav-level-2">
<a href="#_troubleshooting">
<span class="item-number">8.9</span>
<span class="item-number">9.9</span>
<span class="item-title">Troubleshooting</span>
</a>
</li> <li class="nav-level-2">
<a href="#_faqs">
<span class="item-number">8.10</span>
<span class="item-number">9.10</span>
<span class="item-title">FAQs</span>
</a>
</li> </ul>
</li> <li class="nav-level-1">
<a href="#_changelog">
<span class="item-number">9</span>
<span class="item-number">10</span>
<span class="item-title">Changelog</span>
</a>
</li> <li class="nav-level-1">
<a href="#_contribuer">
<span class="item-number">10</span>
<span class="item-number">11</span>
<span class="item-title">Contribuer</span>
</a>
</li> <li class="nav-level-1">
<a href="#_support">
<span class="item-number">11</span>
<span class="item-number">12</span>
<span class="item-title">Support</span>
</a>
</li> </ul>
+152 -68
View File
@@ -5,7 +5,7 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<meta name="keywords" value="VR Payment, Shopware, Shopware Plugin, Payment, Payment Integration, Documentation"><meta name="description" value="The documentation for the Shopware 6 plugin that enables processing payments with VR Payment.">
<link rel="canonical" href="@WalleeCanonicalPath(https://plugin-documentation.wallee.com/wallee-payment, VRPaymentPayment/docs/it/documentation.html)" />
<link rel="canonical" href="https://plugin-documentation.wallee.com/wallee-payment/shopware-6/master/VRPaymentPayment/docs/it/documentation.html" />
<title>VR Payment Shopware 6 Documentation</title>
<link href="assets/monokai-sublime.css" rel="stylesheet" />
<link href="assets/base.css" rel="stylesheet" />
@@ -23,7 +23,7 @@
</a>
</li>
<li>
<a href="@WalleeReleasePath()">
<a href="https://github.com/vr-payment/shopware-6/releases/tag/7.3.4/">
Source
</a>
</li>
@@ -39,7 +39,7 @@
</div>
<div class="chapter-body">
<div class="paragraph">
<p>Il Wallee Payment Plugin integra l&#8217;elaborazione moderna dei pagamenti in Shopware 6, offrendo funzionalità come pagamenti basati su iFrame, rimborsi, acquisizioni e conformità PCI. Supporta l&#8217;integrazione perfetta con il [Portale VR Payment](<a href="https://gateway.vr-payment.de/" class="bare">https://gateway.vr-payment.de/</a>) per la gestione delle transazioni e dei metodi di pagamento.</p>
<p>Il VR Payment Plugin integra l&#8217;elaborazione moderna dei pagamenti in Shopware 6, offrendo funzionalità come pagamenti basati su iFrame, rimborsi, acquisizioni e conformità PCI. Supporta l&#8217;integrazione perfetta con il <a href="https://gateway.vr-payment.de/" target="_blank">Portale VR Payment</a> per la gestione delle transazioni e dei metodi di pagamento.</p>
</div><div class="paragraph">
<p>Importante: Si prega di notare che solo gli aggiornamenti Major (ad es. 6.x.0.0) e Minor (ad es. 6.0.x.0) saranno testati per la compatibilità entro 2 settimane dal rilascio.</p>
</div> </div>
@@ -54,7 +54,7 @@
</div><div class="paragraph">
<p>PHP: Versione minima richiesta dalla vostra installazione di Shopware (ad es. 7.4+).</p>
</div><div class="paragraph">
<p>Account Wallee: Ottenere Space ID, User ID e API Key dal <a href="#portal-startup-guide">Pannello di Controllo Wallee.</a>.</p>
<p>Account VR Payment: Ottenere Space ID, User ID e API Key dal <a href="#portal-startup-guide">Pannello di Controllo VR Payment.</a>.</p>
</div> </div>
</div> <div class="chapter" id="compatibility">
<div class="chapter-title">
@@ -125,7 +125,7 @@
</div>
</li>
<li>
<p>Attivate il plugin Wallee Payment dal Gestore Plugin.</p>
<p>Attivate il plugin VR Payment dal Gestore Plugin.</p>
</li>
</ol>
</div> </div>
@@ -146,7 +146,7 @@
<div class="content">
<pre class="highlight"><code class="language-bash" data-lang="bash">composer require vrpayment/shopware-6
php bin/console plugin:refresh
php bin/console plugin:install --activate --clearCache VRPaymentPayment</code></pre>
php bin/console plugin:install --activate --clearCache VRPayment</code></pre>
</div>
</div>
</li>
@@ -171,7 +171,69 @@ php bin/console plugin:install --activate --clearCache VRPaymentPayment</code></
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-bash" data-lang="bash">php bin/console plugin:refresh
php bin/console plugin:install --activate --clearCache VRPaymentPayment</code></pre>
php bin/console plugin:install --activate --clearCache VRPayment</code></pre>
</div>
</div>
</li>
</ol>
</div> </div>
</div> <div class="section" id="_vrpayment_non_appare_nelle_impostazioni">
<div class="section-title">
<h2>
<span class="title-number">4.4</span>VRPayment non appare nelle impostazioni </h2>
</div>
<div class="section-body">
<div class="paragraph">
<p>Dovrebbe eseguire il seguente comando</p>
</div><div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-bash" data-lang="bash">bin/build-administration.sh</code></pre>
</div>
</div> </div>
</div> </div>
</div> <div class="chapter" id="_aggiornamento">
<div class="chapter-title">
<h1>
<span class="title-number">5</span>Aggiornamento </h1>
</div>
<div class="chapter-body">
<div class="section" id="_tramite_amministrazione">
<div class="section-title">
<h2>
<span class="title-number">5.1</span>Tramite Amministrazione </h2>
</div>
<div class="section-body">
<div class="olist arabic">
<ol class="arabic">
<li>
<p>Vai su Shopware Admin &gt; Estensioni &gt; Le mie estensioni.</p>
</li>
<li>
<p>Trova VRPaymentPayment.</p>
</li>
<li>
<p>Clicca su Aggiorna.</p>
</li>
</ol>
</div> </div>
</div> <div class="section" id="_tramite_cli">
<div class="section-title">
<h2>
<span class="title-number">5.2</span>Tramite CLI </h2>
</div>
<div class="section-body">
<div class="olist arabic">
<ol class="arabic">
<li>
<p>Distribuisci i nuovi file del plugin (sostituisci la cartella <code>custom/plugins/VRPaymentPayment</code> oppure carica/installa un nuovo ZIP).</p>
</li>
<li>
<p>Esegui:</p>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-bash" data-lang="bash">bin/console plugin:refresh
bin/console plugin:update --clearCache VRPaymentPayment
bin/console cache:clear</code></pre>
</div>
</div>
</li>
@@ -181,7 +243,7 @@ php bin/console plugin:install --activate --clearCache VRPaymentPayment</code></
</div> <div class="chapter" id="portal-startup-guide">
<div class="chapter-title">
<h1>
<span class="title-number">5</span>Guida Rapida al Portale </h1>
<span class="title-number">6</span>Guida Rapida al Portale </h1>
</div>
<div class="chapter-body">
<div class="paragraph">
@@ -200,7 +262,7 @@ Selezionate il piano di abbonamento appropriato: dovrebbe supportare le transazi
</div> <div class="section" id="_create_la_chiave_api">
<div class="section-title">
<h2>
<span class="title-number">5.1</span>Create la chiave API: </h2>
<span class="title-number">6.1</span>Create la chiave API: </h2>
</div>
<div class="section-body">
<div class="olist arabic">
@@ -286,7 +348,7 @@ Si prega di notare che il caricamento dei ruoli potrebbe richiedere alcuni secon
</div> <div class="section" id="_configurate_i_metodi_di_pagamento">
<div class="section-title">
<h2>
<span class="title-number">5.2</span>Configurate i Metodi di Pagamento </h2>
<span class="title-number">6.2</span>Configurate i Metodi di Pagamento </h2>
</div>
<div class="section-body">
<div class="paragraph">
@@ -348,7 +410,7 @@ Si prega di notare che i connettori sembrano duplicati, ma è perché uno è per
</div> <div class="chapter" id="_guida_rapida_al_shop">
<div class="chapter-title">
<h1>
<span class="title-number">6</span>Guida Rapida al Shop </h1>
<span class="title-number">7</span>Guida Rapida al Shop </h1>
</div>
<div class="chapter-body">
<div class="olist arabic">
@@ -358,7 +420,7 @@ Si prega di notare che i connettori sembrano duplicati, ma è perché uno è per
<div class="olist loweralpha">
<ol class="loweralpha" type="a">
<li>
<p>Navigate su <strong>Shopware Admin → Impostazioni → Estensioni → VR Payment Payment</strong> e cliccate su Salva.</p>
<p>Navigate su <strong>Shopware Admin → Impostazioni → Estensioni → VR Payment</strong> e cliccate su Salva.</p>
</li>
<li>
<p>Inserite il vostro Space ID, User ID, e API Key</p>
@@ -381,7 +443,7 @@ Si prega di notare che i connettori sembrano duplicati, ma è perché uno è per
<li>
<p><strong>Metodi di Pagamento</strong></p>
<div class="paragraph">
<p>I metodi di pagamento disponibili al checkout sono gestiti dal [Portale VR Payment](<a href="https://gateway.vr-payment.de/" class="bare">https://gateway.vr-payment.de/</a>). Se desiderate disabilitare un metodo di pagamento, dovrete disabilitarlo dal portale.</p>
<p>I metodi di pagamento disponibili al checkout sono gestiti dal <a href="https://gateway.vr-payment.de/" target="_blank">Portale VR Payment</a>. Se desiderate disabilitare un metodo di pagamento, dovrete disabilitarlo dal portale.</p>
</div>
<div class="admonitionblock note">
<table>
@@ -487,17 +549,17 @@ Si prega di notare che se non utilizzate lo Space View Id, questa opzione dovreb
</div> <div class="chapter" id="_grafico_dello_stato_della_transazione">
<div class="chapter-title">
<h1>
<span class="title-number">7</span>Grafico dello Stato della Transazione </h1>
<span class="title-number">8</span>Grafico dello Stato della Transazione </h1>
</div>
<div class="chapter-body">
<div class="paragraph">
<p>Il processo di pagamento di Wallee è completamente standardizzato per ogni metodo di pagamento che potete elaborare. Questo vi dà la possibilità di aggiungere semplicemente un metodo di pagamento o un processore senza modifiche all&#8217;interno della vostra configurazione di Shopware. Una panoramica degli stati e dei processi di pagamento di Wallee è disponibile nella <a href="https://gateway.vr-payment.de/en-us/doc/payment/transaction-process" target="_blank">Documentazione sui Pagamenti</a>.</p>
<p>Il processo di pagamento di VR Payment è completamente standardizzato per ogni metodo di pagamento che potete elaborare. Questo vi dà la possibilità di aggiungere semplicemente un metodo di pagamento o un processore senza modifiche all&#8217;interno della vostra configurazione di Shopware. Una panoramica degli stati e dei processi di pagamento di VR Payment è disponibile nella <a href="https://gateway.vr-payment.de/en-us/doc/payment/transaction-process" target="_blank">Documentazione sui Pagamenti</a>.</p>
</div><div class="paragraph">
<p>Nella sezione seguente, forniamo una panoramica di come gli stati di Wallee sono mappati nel grafico degli stati di Shopware per gli ordini e gli stati di pagamento.</p>
<p>Nella sezione seguente, forniamo una panoramica di come gli stati di VR Payment sono mappati nel grafico degli stati di Shopware per gli ordini e gli stati di pagamento.</p>
</div> <div class="section" id="_mappatura_degli_stati_degli_ordini_di_shopware">
<div class="section-title">
<h2>
<span class="title-number">7.1</span>Mappatura degli Stati degli Ordini di Shopware </h2>
<span class="title-number">8.1</span>Mappatura degli Stati degli Ordini di Shopware </h2>
</div>
<div class="section-body">
<div class="paragraph">
@@ -505,7 +567,7 @@ Si prega di notare che se non utilizzate lo Space View Id, questa opzione dovreb
</div> <div class="section" id="_osservazioni_generali_riguardo_agli_stati_degli_ordini">
<div class="section-title">
<h3>
<span class="title-number">7.1.1</span>Osservazioni Generali Riguardo agli Stati degli Ordini </h3>
<span class="title-number">8.1.1</span>Osservazioni Generali Riguardo agli Stati degli Ordini </h3>
</div>
<div class="section-body">
<div class="paragraph">
@@ -515,7 +577,7 @@ Si prega di notare che se non utilizzate lo Space View Id, questa opzione dovreb
</div> <div class="section" id="_mappatura_dello_stato_di_pagamento_di_shopware">
<div class="section-title">
<h2>
<span class="title-number">7.2</span>Mappatura dello Stato di Pagamento di Shopware </h2>
<span class="title-number">8.2</span>Mappatura dello Stato di Pagamento di Shopware </h2>
</div>
<div class="section-body">
<div class="paragraph">
@@ -542,7 +604,7 @@ Si prega di notare che se non utilizzate lo Space View Id, questa opzione dovreb
</div> <div class="section" id="_osservazioni_generali_riguardo_agli_stati_di_pagamento">
<div class="section-title">
<h3>
<span class="title-number">7.2.1</span>Osservazioni Generali Riguardo agli Stati di Pagamento </h3>
<span class="title-number">8.2.1</span>Osservazioni Generali Riguardo agli Stati di Pagamento </h3>
</div>
<div class="section-body">
<div class="paragraph">
@@ -552,7 +614,7 @@ Si prega di notare che se non utilizzate lo Space View Id, questa opzione dovreb
</div> <div class="section" id="_mappatura_dello_stato_di_spedizione_di_shopware">
<div class="section-title">
<h2>
<span class="title-number">7.3</span>Mappatura dello Stato di Spedizione di Shopware </h2>
<span class="title-number">8.3</span>Mappatura dello Stato di Spedizione di Shopware </h2>
</div>
<div class="section-body">
<div class="paragraph">
@@ -578,7 +640,7 @@ Si prega di notare che se non utilizzate lo Space View Id, questa opzione dovreb
</div> <div class="chapter" id="_gestione_delle_transazioni">
<div class="chapter-title">
<h1>
<span class="title-number">8</span>Gestione delle Transazioni </h1>
<span class="title-number">9</span>Gestione delle Transazioni </h1>
</div>
<div class="chapter-body">
<div class="paragraph">
@@ -586,7 +648,7 @@ Si prega di notare che se non utilizzate lo Space View Id, questa opzione dovreb
</div> <div class="section" id="_completare_acquisire_un_ordine">
<div class="section-title">
<h2>
<span class="title-number">8.1</span>Completare (Acquisire) un Ordine </h2>
<span class="title-number">9.1</span>Completare (Acquisire) un Ordine </h2>
</div>
<div class="section-body">
<div class="paragraph">
@@ -600,7 +662,7 @@ Si prega di notare che se non utilizzate lo Space View Id, questa opzione dovreb
<div class="title">Note</div>
</td>
<td class="content">
Quando il completamento è in sospeso in Wallee, l&#8217;ordine rimarrà nello stato "in sospeso".
Quando il completamento è in sospeso in VR Payment, l&#8217;ordine rimarrà nello stato "in sospeso".
</td>
</tr>
</table>
@@ -618,7 +680,7 @@ Quando il completamento è in sospeso in Wallee, l&#8217;ordine rimarrà nello s
</div> <div class="section" id="_annullare_una_transazione">
<div class="section-title">
<h2>
<span class="title-number">8.2</span>Annullare una transazione </h2>
<span class="title-number">9.2</span>Annullare una transazione </h2>
</div>
<div class="section-body">
<div class="paragraph">
@@ -642,7 +704,7 @@ Puoi annullare solo le transazioni che non sono ancora state completate
</div> <div class="section" id="_rimborso_di_una_transazione">
<div class="section-title">
<h2>
<span class="title-number">8.3</span>Rimborso di una Transazione </h2>
<span class="title-number">9.3</span>Rimborso di una Transazione </h2>
</div>
<div class="section-body">
<div class="paragraph">
@@ -668,11 +730,11 @@ Potrebbe volerci un po' di tempo prima che vediate il rimborso in Shopware. I ri
</div> <div class="section" id="_ordini_in_attesa">
<div class="section-title">
<h2>
<span class="title-number">8.4</span>Ordini in Attesa </h2>
<span class="title-number">9.4</span>Ordini in Attesa </h2>
</div>
<div class="section-body">
<div class="paragraph">
<p>La spedizione non dovrebbe essere effettuata mentre lo stato di spedizione è Hold. Questo accade quando la transazione in Wallee non ha raggiunto lo stato di evasione.</p>
<p>La spedizione non dovrebbe essere effettuata mentre lo stato di spedizione è Hold. Questo accade quando la transazione in VR Payment non ha raggiunto lo stato di evasione.</p>
</div><div class="paragraph">
<p>Ci sono essenzialmente due motivi per cui questo può accadere:</p>
</div><div class="ulist">
@@ -681,27 +743,27 @@ Potrebbe volerci un po' di tempo prima che vediate il rimborso in Shopware. I ri
<p>La transazione non è completata. In questo caso, dovete completare la transazione come scritto sopra.</p>
</li>
<li>
<p>Non siamo in grado di dire se dovete evadere l&#8217;ordine. La decisione di spedizione viene presa automaticamente. Se ciò non accade entro il periodo di tempo definito, Wallee genererà un&#8217;attività manuale che dovreste osservare e seguire le istruzioni.</p>
<p>Non siamo in grado di dire se dovete evadere l&#8217;ordine. La decisione di spedizione viene presa automaticamente. Se ciò non accade entro il periodo di tempo definito, VR Payment genererà un&#8217;attività manuale che dovreste osservare e seguire le istruzioni.</p>
</li>
</ul>
</div><div class="paragraph">
<p>Potete trovare maggiori informazioni sulle attività manuali nella nostra <a href="https://gateway.vr-payment.de/en-us/doc/manual-tasks" target="_blank">Documentazione sulle Attività Manuali.</a>.</p>
</div> </div>
</div> <div class="section" id="_limitazioni_della_sincronizzazione_tra_wallee_e_shopware">
</div> <div class="section" id="_limitazioni_della_sincronizzazione_tra_whitelabelname_e_shopware">
<div class="section-title">
<h2>
<span class="title-number">8.5</span>Limitazioni della Sincronizzazione tra Wallee e Shopware </h2>
<span class="title-number">9.5</span>Limitazioni della Sincronizzazione tra VR Payment e Shopware </h2>
</div>
<div class="section-body">
<div class="paragraph">
<p>Si prega di notare che le acquisizioni, le annullazioni e i rimborsi effettuati in Wallee vengono sincronizzati. Tuttavia, ci sono alcune limitazioni. All&#8217;interno di Wallee, potete modificare contemporaneamente <strong>il prezzo unitario</strong> e <strong>la quantità</strong>. Questo non può essere fatto nel backend di Shopware. Raccomandiamo pertanto di effettuare sempre i rimborsi all&#8217;interno del backend di Shopware e non all&#8217;interno di Wallee. Se un rimborso non può essere sincronizzato, verrà inviato al processore, ma potrebbe essere che non lo vediate all&#8217;interno del vostro backend di Shopware.</p>
<p>Si prega di notare che le acquisizioni, le annullazioni e i rimborsi effettuati in VR Payment vengono sincronizzati. Tuttavia, ci sono alcune limitazioni. All&#8217;interno di VR Payment, potete modificare contemporaneamente <strong>il prezzo unitario</strong> e <strong>la quantità</strong>. Questo non può essere fatto nel backend di Shopware. Raccomandiamo pertanto di effettuare sempre i rimborsi all&#8217;interno del backend di Shopware e non all&#8217;interno di VR Payment. Se un rimborso non può essere sincronizzato, verrà inviato al processore, ma potrebbe essere che non lo vediate all&#8217;interno del vostro backend di Shopware.</p>
</div><div class="paragraph">
<p>Potete trovare maggiori informazioni sui Rimborsi in Wallee nella nostra <a href="https://gateway.vr-payment.de/en-us/doc/payment/refund" target="_blank">Documentazione sui Rimborsi.</a>.</p>
<p>Potete trovare maggiori informazioni sui Rimborsi in VR Payment nella nostra <a href="https://gateway.vr-payment.de/en-us/doc/payment/refund" target="_blank">Documentazione sui Rimborsi.</a>.</p>
</div> </div>
</div> <div class="section" id="_tokenization">
<div class="section-title">
<h2>
<span class="title-number">8.6</span>Tokenization </h2>
<span class="title-number">9.6</span>Tokenization </h2>
</div>
<div class="section-body">
<div class="paragraph">
@@ -721,7 +783,7 @@ La tokenizzazione non è disponibile per i checkout degli ospiti.
</div> <div class="section" id="_pagamenti_ricorrenti">
<div class="section-title">
<h2>
<span class="title-number">8.7</span>Pagamenti ricorrenti </h2>
<span class="title-number">9.7</span>Pagamenti ricorrenti </h2>
</div>
<div class="section-body">
<div class="paragraph">
@@ -730,7 +792,7 @@ La tokenizzazione non è disponibile per i checkout degli ospiti.
</div> <div class="section" id="_key_features">
<div class="section-title">
<h2>
<span class="title-number">8.8</span>Key Features </h2>
<span class="title-number">9.8</span>Key Features </h2>
</div>
<div class="section-body">
<div class="ulist">
@@ -755,7 +817,7 @@ La tokenizzazione non è disponibile per i checkout degli ospiti.
</div> <div class="section" id="_risoluzione_dei_problemi">
<div class="section-title">
<h2>
<span class="title-number">8.9</span>Risoluzione dei Problemi </h2>
<span class="title-number">9.9</span>Risoluzione dei Problemi </h2>
</div>
<div class="section-body">
<div class="ulist">
@@ -764,7 +826,7 @@ La tokenizzazione non è disponibile per i checkout degli ospiti.
<p>Logs: Controlla i log dei pagamenti con:</p>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-bash" data-lang="bash">tail -f var/log/whitelabelname_payment*.log</code></pre>
<pre class="highlight"><code class="language-bash" data-lang="bash">tail -f var/log/whitelabelname*.log</code></pre>
</div>
</div>
</li>
@@ -786,7 +848,7 @@ La tokenizzazione non è disponibile per i checkout degli ospiti.
</div> <div class="section" id="_faqs">
<div class="section-title">
<h2>
<span class="title-number">8.10</span>FAQs </h2>
<span class="title-number">9.10</span>FAQs </h2>
</div>
<div class="section-body">
<div class="paragraph">
@@ -818,7 +880,7 @@ La tokenizzazione non è disponibile per i checkout degli ospiti.
</div> <div class="chapter" id="_changelog">
<div class="chapter-title">
<h1>
<span class="title-number">9</span>Changelog </h1>
<span class="title-number">10</span>Changelog </h1>
</div>
<div class="chapter-body">
<div class="paragraph">
@@ -827,7 +889,7 @@ La tokenizzazione non è disponibile per i checkout degli ospiti.
</div> <div class="chapter" id="_contribuzione">
<div class="chapter-title">
<h1>
<span class="title-number">10</span>Contribuzione </h1>
<span class="title-number">11</span>Contribuzione </h1>
</div>
<div class="chapter-body">
<div class="paragraph">
@@ -838,7 +900,7 @@ La tokenizzazione non è disponibile per i checkout degli ospiti.
</div> <div class="chapter" id="_support">
<div class="chapter-title">
<h1>
<span class="title-number">11</span>Support </h1>
<span class="title-number">12</span>Support </h1>
</div>
<div class="chapter-body">
<div class="paragraph">
@@ -887,135 +949,157 @@ La tokenizzazione non è disponibile per i checkout degli ospiti.
<span class="item-number">4.3</span>
<span class="item-title">Installazione manuale</span>
</a>
</li> <li class="nav-level-2">
<a href="#_vrpayment_non_appare_nelle_impostazioni">
<span class="item-number">4.4</span>
<span class="item-title">VRPayment non appare nelle impostazioni</span>
</a>
</li> </ul>
</li> <li class="nav-level-1">
<a href="#_aggiornamento">
<span class="item-number">5</span>
<span class="item-title">Aggiornamento</span>
</a>
<ul class="nav">
<li class="nav-level-2">
<a href="#_tramite_amministrazione">
<span class="item-number">5.1</span>
<span class="item-title">Tramite Amministrazione</span>
</a>
</li> <li class="nav-level-2">
<a href="#_tramite_cli">
<span class="item-number">5.2</span>
<span class="item-title">Tramite CLI</span>
</a>
</li> </ul>
</li> <li class="nav-level-1">
<a href="#portal-startup-guide">
<span class="item-number">5</span>
<span class="item-number">6</span>
<span class="item-title">Guida Rapida al Portale</span>
</a>
<ul class="nav">
<li class="nav-level-2">
<a href="#_create_la_chiave_api">
<span class="item-number">5.1</span>
<span class="item-number">6.1</span>
<span class="item-title">Create la chiave API:</span>
</a>
</li> <li class="nav-level-2">
<a href="#_configurate_i_metodi_di_pagamento">
<span class="item-number">5.2</span>
<span class="item-number">6.2</span>
<span class="item-title">Configurate i Metodi di Pagamento</span>
</a>
</li> </ul>
</li> <li class="nav-level-1">
<a href="#_guida_rapida_al_shop">
<span class="item-number">6</span>
<span class="item-number">7</span>
<span class="item-title">Guida Rapida al Shop</span>
</a>
</li> <li class="nav-level-1">
<a href="#_grafico_dello_stato_della_transazione">
<span class="item-number">7</span>
<span class="item-number">8</span>
<span class="item-title">Grafico dello Stato della Transazione</span>
</a>
<ul class="nav">
<li class="nav-level-2">
<a href="#_mappatura_degli_stati_degli_ordini_di_shopware">
<span class="item-number">7.1</span>
<span class="item-number">8.1</span>
<span class="item-title">Mappatura degli Stati degli Ordini di Shopware</span>
</a>
<ul class="nav">
<li class="nav-level-3">
<a href="#_osservazioni_generali_riguardo_agli_stati_degli_ordini">
<span class="item-number">7.1.1</span>
<span class="item-number">8.1.1</span>
<span class="item-title">Osservazioni Generali Riguardo agli Stati degli Ordini</span>
</a>
</li> </ul>
</li> <li class="nav-level-2">
<a href="#_mappatura_dello_stato_di_pagamento_di_shopware">
<span class="item-number">7.2</span>
<span class="item-number">8.2</span>
<span class="item-title">Mappatura dello Stato di Pagamento di Shopware</span>
</a>
<ul class="nav">
<li class="nav-level-3">
<a href="#_osservazioni_generali_riguardo_agli_stati_di_pagamento">
<span class="item-number">7.2.1</span>
<span class="item-number">8.2.1</span>
<span class="item-title">Osservazioni Generali Riguardo agli Stati di Pagamento</span>
</a>
</li> </ul>
</li> <li class="nav-level-2">
<a href="#_mappatura_dello_stato_di_spedizione_di_shopware">
<span class="item-number">7.3</span>
<span class="item-number">8.3</span>
<span class="item-title">Mappatura dello Stato di Spedizione di Shopware</span>
</a>
</li> </ul>
</li> <li class="nav-level-1">
<a href="#_gestione_delle_transazioni">
<span class="item-number">8</span>
<span class="item-number">9</span>
<span class="item-title">Gestione delle Transazioni</span>
</a>
<ul class="nav">
<li class="nav-level-2">
<a href="#_completare_acquisire_un_ordine">
<span class="item-number">8.1</span>
<span class="item-number">9.1</span>
<span class="item-title">Completare (Acquisire) un Ordine</span>
</a>
</li> <li class="nav-level-2">
<a href="#_annullare_una_transazione">
<span class="item-number">8.2</span>
<span class="item-number">9.2</span>
<span class="item-title">Annullare una transazione</span>
</a>
</li> <li class="nav-level-2">
<a href="#_rimborso_di_una_transazione">
<span class="item-number">8.3</span>
<span class="item-number">9.3</span>
<span class="item-title">Rimborso di una Transazione</span>
</a>
</li> <li class="nav-level-2">
<a href="#_ordini_in_attesa">
<span class="item-number">8.4</span>
<span class="item-number">9.4</span>
<span class="item-title">Ordini in Attesa</span>
</a>
</li> <li class="nav-level-2">
<a href="#_limitazioni_della_sincronizzazione_tra_wallee_e_shopware">
<span class="item-number">8.5</span>
<span class="item-title">Limitazioni della Sincronizzazione tra Wallee e Shopware</span>
<a href="#_limitazioni_della_sincronizzazione_tra_whitelabelname_e_shopware">
<span class="item-number">9.5</span>
<span class="item-title">Limitazioni della Sincronizzazione tra VR Payment e Shopware</span>
</a>
</li> <li class="nav-level-2">
<a href="#_tokenization">
<span class="item-number">8.6</span>
<span class="item-number">9.6</span>
<span class="item-title">Tokenization</span>
</a>
</li> <li class="nav-level-2">
<a href="#_pagamenti_ricorrenti">
<span class="item-number">8.7</span>
<span class="item-number">9.7</span>
<span class="item-title">Pagamenti ricorrenti</span>
</a>
</li> <li class="nav-level-2">
<a href="#_key_features">
<span class="item-number">8.8</span>
<span class="item-number">9.8</span>
<span class="item-title">Key Features</span>
</a>
</li> <li class="nav-level-2">
<a href="#_risoluzione_dei_problemi">
<span class="item-number">8.9</span>
<span class="item-number">9.9</span>
<span class="item-title">Risoluzione dei Problemi</span>
</a>
</li> <li class="nav-level-2">
<a href="#_faqs">
<span class="item-number">8.10</span>
<span class="item-number">9.10</span>
<span class="item-title">FAQs</span>
</a>
</li> </ul>
</li> <li class="nav-level-1">
<a href="#_changelog">
<span class="item-number">9</span>
<span class="item-number">10</span>
<span class="item-title">Changelog</span>
</a>
</li> <li class="nav-level-1">
<a href="#_contribuzione">
<span class="item-number">10</span>
<span class="item-number">11</span>
<span class="item-title">Contribuzione</span>
</a>
</li> <li class="nav-level-1">
<a href="#_support">
<span class="item-number">11</span>
<span class="item-number">12</span>
<span class="item-title">Support</span>
</a>
</li> </ul>
@@ -251,9 +251,9 @@ class PaymentMethodConfigurationService {
{
$data = [];
$paymentMethodData = [];
$salesChannelPaymentMethodData = [];
$criteria = (new Criteria())->addFilter(new EqualsFilter('state', 'ACTIVE'));
$criteria = (new Criteria())->addFilter(new EqualsFilter('state', 'ACTIVE'))
->addFilter(new EqualsFilter('spaceId', $this->getSpaceId()));
/**
* @var $vRPaymentPMConfigurationRepository
@@ -344,40 +344,67 @@ class PaymentMethodConfigurationService {
$paymentMethodConfigurations = $this->getPaymentMethodConfigurations();
$this->logger->debug('Updating payment methods', $paymentMethodConfigurations);
/**
* @var $paymentMethodConfiguration \VRPayment\Sdk\Model\PaymentMethodConfiguration
*/
foreach ($paymentMethodConfigurations as $paymentMethodConfiguration) {
$paymentMethodConfigurationEntity = $this->getPaymentMethodConfigurationEntity(
$paymentMethodConfiguration->getSpaceId(),
$paymentMethodConfiguration->getId(),
$context
$entity = $this->getPaymentMethodConfigurationEntity(
$paymentMethodConfiguration->getSpaceId(),
$paymentMethodConfiguration->getId(),
$context
);
$id = is_null($paymentMethodConfigurationEntity) ? Uuid::randomHex() : $paymentMethodConfigurationEntity->getId();
$configId = $entity ? $entity->getId() : Uuid::randomHex();
$technicalName = $paymentMethodConfiguration->getName();
$paymentMethodId = $this->getOrCreatePaymentMethodId(
$technicalName,
VRPaymentPaymentHandler::class,
$context
);
$data = [
'id' => $id,
'paymentMethodConfigurationId' => $paymentMethodConfiguration->getId(),
'paymentMethodId' => $id,
'data' => json_decode(strval($paymentMethodConfiguration), true),
'sortOrder' => $paymentMethodConfiguration->getSortOrder(),
'spaceId' => $paymentMethodConfiguration->getSpaceId(),
'state' => CreationEntityState::ACTIVE,
'id' => $configId,
'paymentMethodConfigurationId' => $paymentMethodConfiguration->getId(),
'paymentMethodId' => $paymentMethodId,
'data' => json_decode(strval($paymentMethodConfiguration), true),
'sortOrder' => $paymentMethodConfiguration->getSortOrder(),
'spaceId' => $paymentMethodConfiguration->getSpaceId(),
'state' => CreationEntityState::ACTIVE,
];
$this->upsertPaymentMethod($id, $paymentMethodConfiguration, $context);
try {
$this->container->get(PaymentMethodConfigurationEntityDefinition::ENTITY_NAME . '.repository')->upsert([$data], $context);
} catch (\Exception $e) {
$this->logger->error($e->getMessage(), [$e->getTraceAsString()]);
}
try {
$this->upsertPaymentMethod($paymentMethodId, $paymentMethodConfiguration, $context);
$this->container
->get(PaymentMethodConfigurationEntityDefinition::ENTITY_NAME . '.repository')
->upsert([$data], $context);
} catch (\Exception $e) {
$this->logger->error($e->getMessage(), [$e->getTraceAsString()]);
}
}
}
private function getOrCreatePaymentMethodId(string $technicalName, string $handlerIdentifier, Context $context): string
{
$criteria = new Criteria();
$criteria->addFilter(new EqualsFilter('technicalName', $technicalName));
$criteria->setLimit(1);
$existing = $this->paymentMethodRepository->search($criteria, $context)->first();
if ($existing !== null) {
return $existing->getId();
}
$paymentMethodId = Uuid::randomHex();
$this->paymentMethodRepository->upsert([[
'id' => $paymentMethodId,
'handlerIdentifier' => $handlerIdentifier,
'technicalName' => $technicalName,
'name' => $technicalName,
'active' => false,
]], $context);
return $paymentMethodId;
}
/**
* Fetch active merchant payment methods from VRPayment API
*
@@ -467,53 +494,6 @@ class PaymentMethodConfigurationService {
return $configurations->getElements();
}
/**
* Update or insert Payment Method
*
* @param string $id
* @param \VRPayment\Sdk\Model\PaymentMethodConfiguration $paymentMethodConfiguration
* @param \Shopware\Core\Framework\Context $context
*
* @throws \VRPayment\Sdk\ApiException
* @throws \VRPayment\Sdk\Http\ConnectionException
* @throws \VRPayment\Sdk\VersioningException
*/
protected function upsertPaymentMethod(
string $id,
PaymentMethodConfiguration $paymentMethodConfiguration,
Context $context
): void
{
/** @var PluginIdProvider $pluginIdProvider */
$pluginIdProvider = $this->container->get(PluginIdProvider::class);
$pluginId = $pluginIdProvider->getPluginIdByBaseClass(
VRPaymentPayment::class,
$context
);
$data = [
'id' => $id,
'handlerIdentifier' => VRPaymentPaymentHandler::class,
'pluginId' => $pluginId,
'position' => $paymentMethodConfiguration->getSortOrder() - 100,
'afterOrderEnabled' => true,
'active' => true,
'translations' => $this->getPaymentMethodConfigurationTranslation($paymentMethodConfiguration, $context),
'technicalName' => $paymentMethodConfiguration->getName(),
];
$data['mediaId'] = $this->upsertMedia($id, $paymentMethodConfiguration, $context);
$data = array_filter($data);
try {
$this->paymentMethodRepository->upsert([$data], $context);
} catch (\Exception $e) {
$this->logger->error($e->getMessage(), [$e->getTraceAsString()]);
}
}
/**
* @param \VRPayment\Sdk\Model\PaymentMethodConfiguration $paymentMethodConfiguration
* @param \Shopware\Core\Framework\Context $context
@@ -605,54 +585,102 @@ class PaymentMethodConfigurationService {
}
/**
* Upload Payment Method icons
*
* @param string $id
* @param \VRPayment\Sdk\Model\PaymentMethodConfiguration $paymentMethodConfiguration
* @param \Shopware\Core\Framework\Context $context
*
* @return string|null
* Update or insert Payment Method
*/
protected function upsertPaymentMethod(
string $id,
PaymentMethodConfiguration $paymentMethodConfiguration,
Context $context
): void {
/** @var PluginIdProvider $pluginIdProvider */
$pluginIdProvider = $this->container->get(PluginIdProvider::class);
$pluginId = $pluginIdProvider->getPluginIdByBaseClass(
VRPaymentPayment::class,
$context
);
$data = [
'id' => $id,
'handlerIdentifier' => VRPaymentPaymentHandler::class,
'pluginId' => $pluginId,
'position' => $paymentMethodConfiguration->getSortOrder() - 100,
'afterOrderEnabled' => true,
'active' => true,
'translations' => $this->getPaymentMethodConfigurationTranslation($paymentMethodConfiguration, $context),
'technicalName' => $paymentMethodConfiguration->getName(),
];
$mediaId = $this->upsertMedia($id, $paymentMethodConfiguration, $context);
if ($mediaId) {
$data['mediaId'] = $mediaId;
}
try {
$this->paymentMethodRepository->upsert([$data], $context);
} catch (\Exception $e) {
$this->logger->error($e->getMessage(), [$e->getTraceAsString()]);
}
}
/**
* Upload or update Payment Method icons
*/
protected function upsertMedia(string $id, PaymentMethodConfiguration $paymentMethodConfiguration, Context $context): ?string
{
try {
$existingRecord = $this->getMediaDefaultFolderForPaymentMethod($paymentMethodConfiguration, $context);
$folderKey = 'payment_method_' . $paymentMethodConfiguration->getId();
if ($existingRecord->count() > 0) {
$id = $existingRecord->first()->getId();
// Check existing default folder
$criteria = new Criteria();
$criteria->addFilter(new EqualsFilter('entity', $folderKey));
$existingFolder = $this->mediaDefaultFolderRepository->search($criteria, $context);
$folderId = $id;
if ($existingFolder->count() > 0) {
$folderId = $existingFolder->first()->getId();
}
// Ensure default folder
$this->mediaDefaultFolderRepository->upsert([
[
'id' => $id,
'associationFields' => [],
'entity' => 'payment_method_' . $paymentMethodConfiguration->getId(),
],
[
'id' => $folderId,
'associationFields' => [],
'entity' => $folderKey,
],
], $context);
// Ensure media folder
$this->mediaFolderRepository->upsert([
[
'id' => $id,
'defaultFolderId' => $id,
'name' => $paymentMethodConfiguration->getName(),
'useParentConfiguration' => false,
'configuration' => [],
],
[
'id' => $folderId,
'defaultFolderId' => $folderId,
'name' => $paymentMethodConfiguration->getName(),
'useParentConfiguration' => false,
'configuration' => [],
],
], $context);
/**
* @var \Shopware\Core\Content\Media\MediaDefinition
*/
// Media insert/update
// detect if collision, return existing id
$existingId = $this->checkMediaAlreadyExists($paymentMethodConfiguration->getResolvedImageUrl(), $context);
if ($existingId) {
return $existingId;
}
$mediaDefinition = $this->container->get(MediaDefinition::class);
$this->mediaSerializer->setRegistry($this->serializerRegistry);
$data = [
'id' => $id,
'title' => $paymentMethodConfiguration->getName(),
'url' => $paymentMethodConfiguration->getResolvedImageUrl(),
'mediaFolderId' => $id,
'id' => $id,
'title' => $paymentMethodConfiguration->getName(),
'url' => $paymentMethodConfiguration->getResolvedImageUrl(),
'mediaFolderId' => $folderId,
];
$data = $this->mediaSerializer->deserialize(new Config([], [], []), $mediaDefinition, $data);
$this->mediaRepository->upsert([$data], $context);
return $id;
} catch (\Exception $e) {
$this->logger->critical($e->getMessage(), [$e->getTraceAsString()]);
@@ -660,6 +688,22 @@ class PaymentMethodConfigurationService {
}
}
private function checkMediaAlreadyExists($paymentMethodUrl, $context) {
// detect if collision, return existing id
if (preg_match('#/([^/]+)\.[^/.]+$#', $paymentMethodUrl, $matches)) {
$filename = $matches[1];
}
$criteria = new Criteria();
$criteria->addFilter(new EqualsFilter('fileName', $filename));
$existing = $this->mediaRepository->search($criteria, $context)->first();
if ($existing) {
return $existing->getId();
}
return false;
}
/**
* Retrieves media default folder for a given payment method configuration.
*
@@ -13,7 +13,9 @@ use Symfony\Component\{
};
use VRPaymentPayment\Core\{
Api\Refund\Service\RefundService,
Settings\Service\SettingsService
Api\Transaction\Service\TransactionService,
Settings\Service\SettingsService,
Util\Exception\RefundNotSupportedException
};
/**
@@ -40,17 +42,24 @@ class RefundController extends AbstractController
* @var \Psr\Log\LoggerInterface
*/
protected $logger;
/**
* @var \VRPaymentPayment\Core\Api\Transaction\Service\TransactionService
*/
protected $transactionService;
/**
* RefundController constructor.
*
* @param \VRPaymentPayment\Core\Api\Refund\Service\RefundService $refundService
* @param \VRPaymentPayment\Core\Settings\Service\SettingsService $settingsService
* @param \VRPaymentPayment\Core\Api\Transaction\Service\TransactionService $transactionService
*/
public function __construct(RefundService $refundService, SettingsService $settingsService)
public function __construct(RefundService $refundService, SettingsService $settingsService, TransactionService $transactionService)
{
$this->settingsService = $settingsService;
$this->refundService = $refundService;
$this->transactionService = $transactionService;
}
/**
@@ -81,12 +90,29 @@ class RefundController extends AbstractController
$transactionId = $request->request->get('transactionId');
$quantity = (int)$request->request->get('quantity');
$lineItemId = $request->request->get('lineItemId');
if ($quantity === null || $quantity <= 0) {
return new Response('refundQuantityZero', Response::HTTP_BAD_REQUEST);
}
$settings = $this->settingsService->getSettings($salesChannelId);
$apiClient = $settings->getApiClient();
$transaction = $apiClient->getTransactionService()->read($settings->getSpaceId(), $transactionId);
$refund = $this->refundService->create($transaction, $context, $lineItemId, $quantity);
$maxQuantity = $this->refundService->getMaxRefundableQuantity($transaction, $context, $lineItemId);
if ($quantity > $maxQuantity) {
return new Response('refundExceedsQuantity', Response::HTTP_BAD_REQUEST);
}
try {
$refund = $this->refundService->create($transaction, $context, $lineItemId, $quantity);
} catch (RefundNotSupportedException $exception) {
$this->logger->info('Payment method does not support online refunds for transaction: ' . $transactionId);
return new Response('methodDoesNotSupportRefund', Response::HTTP_BAD_REQUEST);
}
if ($refund === null) {
return new Response('Refund was not created. Please check the refund amound or if the item was not refunded before', Response::HTTP_BAD_REQUEST);
}
@@ -110,17 +136,35 @@ class RefundController extends AbstractController
$salesChannelId = $request->request->get('salesChannelId');
$transactionId = $request->request->get('transactionId');
$refundableAmount = $request->request->get('refundableAmount');
if ($refundableAmount === null || $refundableAmount <= 0.0) {
return new Response('refundAmountZero', Response::HTTP_BAD_REQUEST);
}
$settings = $this->settingsService->getSettings($salesChannelId);
$apiClient = $settings->getApiClient();
$transaction = $apiClient->getTransactionService()->read($settings->getSpaceId(), $transactionId);
$refund = $this->refundService->createRefundByAmount($transaction, $refundableAmount, $context);
$completed = (float) $transaction->getCompletedAmount();
$refunded = (float) $transaction->getRefundedAmount();
$maxRefund = round($completed - $refunded, 2);
if ($refund === null) {
return new Response('refundExceedsAmount', Response::HTTP_BAD_REQUEST);
if ($refundableAmount > $maxRefund) {
return new Response('refundExceedsAmount', Response::HTTP_BAD_REQUEST);
}
try {
$refund = $this->refundService->createRefundByAmount($transaction, $refundableAmount, $context);
} catch (RefundNotSupportedException $exception) {
$this->logger->info('Payment method does not support online refunds for transaction: ' . $transactionId);
return new Response('methodDoesNotSupportRefund', Response::HTTP_BAD_REQUEST);
}
if ($refund === null) {
return new Response('refundExceedsAmount', Response::HTTP_BAD_REQUEST);
}
return new Response(null, Response::HTTP_NO_CONTENT);
}
@@ -146,7 +190,13 @@ class RefundController extends AbstractController
$apiClient = $settings->getApiClient();
$transaction = $apiClient->getTransactionService()->read($settings->getSpaceId(), $transactionId);
$this->refundService->createPartialRefund($transaction, $context, $lineItemId, $refundableAmount);
try {
$refund = $this->refundService->createPartialRefund($transaction, $context, $lineItemId, $refundableAmount);
} catch (RefundNotSupportedException $exception) {
$this->logger->info('Payment method does not support online refunds for transaction: ' . $transactionId);
return new Response('methodDoesNotSupportRefund', Response::HTTP_BAD_REQUEST);
}
return new Response(null, Response::HTTP_NO_CONTENT);
}
+89 -2
View File
@@ -12,14 +12,20 @@ use Shopware\Core\{
};
use VRPayment\Sdk\{
Model\Refund,
Model\Transaction
Model\Transaction,
Model\CriteriaOperator,
Model\EntityQueryFilter,
Model\EntityQueryFilterType,
Model\EntityQuery,
ApiException
};
use VRPaymentPayment\Core\{
Api\Refund\Entity\RefundEntity,
Api\Transaction\Entity\TransactionEntity,
Api\Transaction\Entity\TransactionEntityDefinition,
Settings\Service\SettingsService,
Util\Payload\RefundPayload
Util\Payload\RefundPayload,
Util\Exception\RefundNotSupportedException
};
/**
@@ -99,6 +105,12 @@ class RefundService
$this->upsert($refund, $context);
return $refund;
}
} catch (ApiException $exception) {
$message = $exception->getMessage();
$this->logger->critical($message);
if ($exception->getCode() === 442 && str_contains($message, 'does not support online refunds')) {
throw new RefundNotSupportedException($message, 0, $exception);
}
} catch (\Exception $exception) {
$this->logger->critical($exception->getMessage());
}
@@ -134,6 +146,12 @@ class RefundService
$this->upsert($refund, $context);
return $refund;
}
} catch (ApiException $exception) {
$message = $exception->getMessage();
$this->logger->critical($message);
if ($exception->getCode() === 442 && str_contains($message, 'does not support online refunds')) {
throw new RefundNotSupportedException($message, 0, $exception);
}
} catch (\Exception $exception) {
$this->logger->critical($exception->getMessage());
}
@@ -170,6 +188,12 @@ class RefundService
$this->upsert($refund, $context);
return $refund;
}
} catch (ApiException $exception) {
$message = $exception->getMessage();
$this->logger->critical($message);
if ($exception->getCode() === 442 && str_contains($message, 'does not support online refunds')) {
throw new RefundNotSupportedException($message, 0, $exception);
}
} catch (\Exception $exception) {
$this->logger->critical($exception->getMessage());
}
@@ -241,4 +265,67 @@ class RefundService
->first();
}
/**
* Get total refunded quantity for transaction's line item by lineItemId.
*
* @param \VRPayment\Sdk\Model\Transaction $transaction
* @param \Shopware\Core\Framework\Context $context
* @param string $lineItemId
*
* @return int
*/
public function getRefundedQuantity(Transaction $transaction, Context $context, string $lineItemId): int {
$transactionEntity = $this->getTransactionEntityByTransactionId($transaction->getId(), $context);
$settings = $this->settingsService->getSettings($transactionEntity->getSalesChannel()->getId());
$apiClient = $settings->getApiClient();
$entityQueryFilter = (new EntityQueryFilter())
->setType(EntityQueryFilterType::LEAF)
->setOperator(CriteriaOperator::EQUALS)
->setFieldName('transaction.id')
->setValue($transaction->getId());
$query = (new EntityQuery())->setFilter($entityQueryFilter);
$refunds = $apiClient->getRefundService()->search($settings->getSpaceId(), $query);
$refundedQuantity = 0;
foreach ($refunds as $refund) {
foreach ($refund->getReductions() as $reduction) {
if ($reduction->getLineItemUniqueId() === $lineItemId) {
$refundedQuantity += (int) $reduction->getQuantityReduction();
}
}
}
return $refundedQuantity;
}
/**
* Get maximum quantity of available items to refund for line item.
*
* @param \VRPayment\Sdk\Model\Transaction $transaction
* @param \Shopware\Core\Framework\Context $context
* @param string $lineItemId
*
* @return int
*/
public function getMaxRefundableQuantity(Transaction $transaction, Context $context, string $lineItemId): int {
$originalQuantity = 0;
foreach ($transaction->getLineItems() as $lineItem) {
if ($lineItem->getUniqueId() === $lineItemId) {
$originalQuantity = (int) $lineItem->getQuantity();
break;
}
}
$refundedQuantity = $this->getRefundedQuantity($transaction, $context, $lineItemId);
$maxQuantity = $originalQuantity - $refundedQuantity;
return $maxQuantity;
}
}
@@ -32,7 +32,7 @@ use VRPaymentPayment\Core\Api\Refund\Entity\RefundEntityDefinition;
*/
class TransactionEntityDefinition extends EntityDefinition {
public const ENTITY_NAME = 'vrpayment_transaction';
public const ENTITY_NAME = 'vrpayment_transaction_data';
/**
* @return string
@@ -1,7 +1,10 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
namespace VRPaymentPayment\Core\Api\Transaction\Service;
use Psr\Cache\CacheItemPoolInterface;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
use Shopware\Core\{
@@ -15,6 +18,7 @@ use Shopware\Core\{
System\SalesChannel\SalesChannelContext
};
use Shopware\Storefront\Page\Checkout\Confirm\CheckoutConfirmPageLoadedEvent;
use Shopware\Storefront\Page\Account\Order\AccountEditOrderPageLoadedEvent;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use VRPayment\Sdk\Model\{
AddressCreate,
@@ -28,6 +32,7 @@ use VRPayment\Sdk\Model\{
LineItemAttributeCreate,
LineItemCreate,
LineItemType,
TaxCreate,
TokenizationMode,
Transaction,
TransactionCreate,
@@ -44,8 +49,12 @@ use VRPaymentPayment\Core\{
Settings\Service\SettingsService,
Util\LocaleCodeProvider,
Util\Payload\CustomProducts\CustomProductsLineItemTypes,
Util\Payload\TransactionPayload
Util\Payload\TransactionPayload,
Util\Analytics\Analytics
};
use Shopware\Core\Checkout\Order\Aggregate\OrderLineItem\OrderLineItemEntity;
use Shopware\Core\Framework\Struct\ArrayEntity;
use Shopware\Commercial\Subscription\Framework\Struct\SubscriptionContextStruct;
/**
* Class TransactionService
@@ -74,6 +83,12 @@ class TransactionService
*/
private $settingsService;
/**
* Cache for storing pending transaction IDs across headless requests.
* @var CacheItemPoolInterface
*/
private CacheItemPoolInterface $cache;
const CARD_HOLDER_KEY = '1456765000789';
const PSEUDO_CODE_KEY = '1485172176673';
const CARD_VALIDITY_KEY = '1456765711187';
@@ -86,16 +101,18 @@ class TransactionService
* @param \Psr\Container\ContainerInterface $container
* @param \VRPaymentPayment\Core\Util\LocaleCodeProvider $localeCodeProvider
* @param \VRPaymentPayment\Core\Settings\Service\SettingsService $settingsService
* @param CacheItemPoolInterface $cache Cache for headless transaction persistence
*/
public function __construct(
ContainerInterface $container,
LocaleCodeProvider $localeCodeProvider,
SettingsService $settingsService
)
{
SettingsService $settingsService,
CacheItemPoolInterface $cache
) {
$this->container = $container;
$this->localeCodeProvider = $localeCodeProvider;
$this->settingsService = $settingsService;
$this->cache = $cache;
}
/**
@@ -127,8 +144,7 @@ class TransactionService
public function create(
PaymentTransactionStruct $transaction,
SalesChannelContext $salesChannelContext
): string
{
): string {
$criteria = new Criteria([$transaction->getOrderTransactionId()]);
$criteria->addAssociation('order');
$orderTransaction = $this->container->get('order_transaction.repository')->search($criteria, $salesChannelContext->getContext())->first();
@@ -137,13 +153,28 @@ class TransactionService
$settings = $this->settingsService->getSettings($salesChannelId);
$apiClient = $settings->getApiClient();
$transactionId = $_SESSION['transactionId'] ?? null;
// Get transaction ID from cache (headless) or session (storefront).
$transactionId = $this->getTransactionIdFromContext($salesChannelContext);
$pendingTransaction = null;
// Try to read the pending transaction if we have an ID stored.
if ($transactionId !== null) {
$pendingTransaction = $this->read($_SESSION['transactionId'], $salesChannelId);
try {
$pendingTransaction = $this->read($transactionId, $salesChannelId);
// Verify it's still in PENDING state - otherwise we can't reuse it.
if ($pendingTransaction != null && $pendingTransaction->getState() !== TransactionState::PENDING) {
$pendingTransaction = null;
}
} catch (\Exception $e) {
// Transaction may have been deleted, expired, or is invalid - we'll create a new one.
$this->logger?->debug('Could not read pending transaction, will create new one: ' . $e->getMessage());
$pendingTransaction = null;
}
}
if ($transactionId === null || $pendingTransaction === null || $pendingTransaction->getState() !== TransactionState::PENDING) {
unset($_SESSION['transactionId']);
// Create a new transaction if we don't have a valid pending one.
if ($pendingTransaction === null) {
$this->clearTransactionIdFromContext($salesChannelContext);
$pendingTransactionId = $this->createPendingTransaction($salesChannelContext);
$pendingTransaction = $this->read($pendingTransactionId, $salesChannelId);
}
@@ -156,6 +187,7 @@ class TransactionService
$transaction
));
$transactionPayloadClass->setLogger($this->logger);
$transactionPayloadClass->setTransactionId($pendingTransaction->getId());
$transactionPayload = $transactionPayloadClass->get($pendingTransaction->getVersion());
$createdTransaction = $apiClient->getTransactionService()
@@ -165,7 +197,8 @@ class TransactionService
$transaction,
$salesChannelContext->getContext(),
$createdTransaction->getId(),
$settings->getSpaceId()
$settings->getSpaceId(),
$salesChannelContext->getToken()
);
$redirectUrl = $this->container->get('router')->generate(
@@ -174,6 +207,16 @@ class TransactionService
UrlGeneratorInterface::ABSOLUTE_URL
);
// If the request comes from the Store API (headless), we should not redirect to a Storefront Twig page.
// Instead, we return the returnUrl so the headless client can handle the next steps (e.g. rendering the iframe).
$request = $this->container->get('request_stack')->getCurrentRequest();
if ($request) {
$routeScope = $request->attributes->get('_route_scope', []);
if (in_array('store-api', $routeScope, true)) {
$redirectUrl = $transaction->getReturnUrl();
}
}
if ($settings->getIntegration() == Integration::PAYMENT_PAGE) {
$redirectUrl = $apiClient->getTransactionPaymentPageService()
->paymentPageUrl($settings->getSpaceId(), $createdTransaction->getId());
@@ -185,10 +228,20 @@ class TransactionService
$orderTransaction->getPaymentMethodId(),
$orderTransaction->getOrder()->getSalesChannelId()
);
$_SESSION['transactionId'] = null;
$_SESSION['arrayOfPossibleMethods'] = null;
$_SESSION['addressCheck'] = null;
$_SESSION['currencyCheck'] = null;
$salesChannelContext->getContext()->addExtension(
'checkoutState',
new ArrayEntity([
'transactionId' => null,
'addressHash' => null,
'currency' => null,
])
);
$salesChannelContext->getContext()->addExtension(
'possibleMethods',
new ArrayEntity(['ids' => []])
);
$this->holdDelivery($orderTransaction->getOrder()->getId(), $salesChannelContext->getContext());
@@ -201,15 +254,21 @@ class TransactionService
*
* @return void
*/
public function createRecurringTransaction(TransactionCreate $sdkTransactionCreate, string $spaceId = ""): Transaction {
public function createRecurringTransaction(TransactionCreate $sdkTransactionCreate, string $spaceId = ""): Transaction
{
$settings = $this->settingsService->getSettings();
if (empty($spaceId)) {
$spaceId = $settings->getSpaceId();
}
$sdkTransaction = $settings->getApiClient()->getTransactionService()->create($spaceId, $sdkTransactionCreate);
$apiClient = $settings->getApiClient();
Analytics::addHeaders($apiClient, [
Analytics::SUBSCRIPTION_TRANSACTION => true
]);
$sdkTransaction = $apiClient->getTransactionService()->create($spaceId, $sdkTransactionCreate);
if ($sdkTransaction->valid()) {
return $settings->getApiClient()->getTransactionService()->processWithoutUserInteraction($spaceId, $sdkTransaction->getId());
return $apiClient->getTransactionService()->processWithoutUserInteraction($spaceId, $sdkTransaction->getId());
}
throw new \Exception("The transacion is not valid and could not be created.");
@@ -225,15 +284,21 @@ class TransactionService
PaymentTransactionStruct $transaction,
Context $context,
int $vrpaymentTransactionId,
int $spaceId
): void
{
int $spaceId,
?string $token = null
): void {
$customFields = [
TransactionPayload::ORDER_TRANSACTION_CUSTOM_FIELDS_VRPAYMENT_TRANSACTION_ID => $vrpaymentTransactionId,
TransactionPayload::ORDER_TRANSACTION_CUSTOM_FIELDS_VRPAYMENT_SPACE_ID => $spaceId,
];
if ($token) {
$customFields[TransactionPayload::ORDER_TRANSACTION_CUSTOM_FIELDS_VRPAYMENT_TOKEN] = $token;
}
$data = [
'id' => $transaction->getOrderTransactionId(),
'customFields' => [
TransactionPayload::ORDER_TRANSACTION_CUSTOM_FIELDS_VRPAYMENT_TRANSACTION_ID => $vrpaymentTransactionId,
TransactionPayload::ORDER_TRANSACTION_CUSTOM_FIELDS_VRPAYMENT_SPACE_ID => $spaceId,
],
'customFields' => $customFields,
];
$this->container->get('order_transaction.repository')->update([$data], $context);
}
@@ -249,10 +314,9 @@ class TransactionService
public function upsert(
Transaction $transaction,
Context $context,
string $paymentMethodId = null,
string $salesChannelId = null
): void
{
?string $paymentMethodId = null,
?string $salesChannelId = null
): void {
try {
$transactionId = $transaction->getId();
@@ -265,6 +329,13 @@ class TransactionService
$orderId = $transactionMetaData[TransactionPayload::VRPAYMENT_METADATA_ORDER_ID];
$orderTransactionId = $transactionMetaData[TransactionPayload::VRPAYMENT_METADATA_ORDER_TRANSACTION_ID];
if (!$paymentMethodId) {
$criteria = new Criteria([$orderTransactionId]);
$criteria->addAssociation('order');
$orderTransaction = $this->container->get('order_transaction.repository')->search($criteria, $context)->first();
$paymentMethodId = $orderTransaction->getPaymentMethodId();
}
$dataParamValue = json_decode(strval($transaction), true);
$brandName = '';
if (isset($dataParamValue['paymentConnectorConfiguration'])) {
@@ -331,7 +402,6 @@ class TransactionService
$data = array_filter($data);
$this->container->get(TransactionEntityDefinition::ENTITY_NAME . '.repository')->upsert([$data], $context);
} catch (\Exception $exception) {
$this->logger->critical(__CLASS__ . ' : ' . __FUNCTION__ . ' : ' . $exception->getMessage());
}
@@ -367,7 +437,7 @@ class TransactionService
*
* @return \Shopware\Core\Checkout\Order\OrderEntity
*/
protected function getOrderEntity(string $orderId, Context $context): OrderEntity
public function getOrderEntity(string $orderId, Context $context): OrderEntity
{
try {
$criteria = (new Criteria([$orderId]))->addAssociations(['deliveries']);
@@ -382,7 +452,6 @@ class TransactionService
} catch (\Exception $e) {
throw CartException::orderNotFound($orderId);
}
}
/**
@@ -430,7 +499,8 @@ class TransactionService
return $this->container->get(TransactionEntityDefinition::ENTITY_NAME . '.repository')
->search(
(new Criteria())->addFilter(new EqualsFilter('transactionId', $transactionId))
->addAssociations(['refunds']), $context
->addAssociations(['refunds']),
$context
)
->first();
}
@@ -448,7 +518,8 @@ class TransactionService
return $this->container->get(TransactionEntityDefinition::ENTITY_NAME . '.repository')
->search(
(new Criteria())->addFilter(new EqualsFilter('orderTransactionId', $orderTransactionId))
->addAssociations(['refunds']), $context
->addAssociations(['refunds']),
$context
)
->first();
}
@@ -465,7 +536,8 @@ class TransactionService
{
return $this->container->get(RefundEntityDefinition::ENTITY_NAME . '.repository')
->search(
(new Criteria())->addFilter(new EqualsFilter('transactionId', $transactionId)), $context
(new Criteria())->addFilter(new EqualsFilter('transactionId', $transactionId)),
$context
)
->getEntities();
}
@@ -502,25 +574,29 @@ class TransactionService
/**
* @param SalesChannelContext $salesChannelContext
* @param CheckoutConfirmPageLoadedEvent|null $event
*
* @return int
*/
public function createPendingTransaction(SalesChannelContext $salesChannelContext, ?CheckoutConfirmPageLoadedEvent $event = null): int
public function createPendingTransaction(SalesChannelContext $salesChannelContext, $event = null): int
{
$expiredTransaction = true;
$transactionId = $_SESSION['transactionId'] ?? null;
// Get transaction ID from cache (headless) or session (storefront).
$transactionId = $this->getTransactionIdFromContext($salesChannelContext);
$settings = $this->settingsService->getValidSettings($salesChannelContext->getSalesChannel()->getId());
if (!$settings) {
throw new \Exception('Space settings not configured');
}
if ($transactionId) {
$transactionService = $settings->getApiClient()->getTransactionService();
$pendingTransaction = $transactionService->read($settings->getSpaceId(), $transactionId);
$failedStates = [
TransactionState::DECLINE,
TransactionState::FAILED,
TransactionState::VOIDED,
];
if (!in_array($pendingTransaction->getState(), $failedStates)) {
$expiredTransaction = false;
try {
$transactionService = $settings->getApiClient()->getTransactionService();
$pendingTransaction = $transactionService->read($settings->getSpaceId(), $transactionId);
if ($pendingTransaction->getState() === TransactionState::PENDING) {
$expiredTransaction = false;
}
} catch (\Exception $e) {
// Transaction may have been deleted, expired, or is invalid - treat as expired.
$expiredTransaction = true;
}
}
@@ -528,79 +604,20 @@ class TransactionService
$settings = $this->settingsService->getValidSettings($salesChannelContext->getSalesChannel()->getId());
$customer = $salesChannelContext->getCustomer();
$customerBillingAddress = $customer->getActiveBillingAddress();
$billingAddress = new AddressCreate();
$customerAddressEntity = $customer->getActiveBillingAddress();
$familyName = "";
if (!empty($customerAddressEntity->getLastName())) {
$familyName = $customerAddressEntity->getLastName();
} else {
if (!empty($customer->getLastName())) {
$familyName = $customer->getLastName();
}
if ($customer === null) {
throw new \Exception('Customer is required to create a transaction');
}
$billingAddress->setFamilyName($familyName);
$lineItems = $this->extractLineItems(
$event,
$salesChannelContext,
);
$givenName = "";
if (!empty($customerAddressEntity->getFirstName())) {
$givenName = $customerAddressEntity->getFirstName();
} else {
if (!empty($customer->getFirstName())) {
$givenName = $customer->getFirstName();
}
}
$billingAddress->setGivenName($givenName);
$billingAddress->setOrganizationName($customerBillingAddress->getCompany());
$billingAddress->setPhoneNumber($customerAddressEntity->getPhoneNumber());
$billingAddress->setCountry($customerBillingAddress->getCountry()->getIso());
$postalState = $customerBillingAddress?->getCountryState()?->getName() ?? '';
if (empty($postalState)) {
$postalState = $customerBillingAddress?->getCountryState()?->getShortCode() ?? '';
}
$billingAddress->setPostalState($postalState);
$billingAddress->setPostCode($customerBillingAddress->getZipcode());
$billingAddress->setStreet($customerBillingAddress->getStreet());
$billingAddress->setEmailAddress($customer->getEmail());
if (!empty($customer->getBirthday())) {
$birthday = new \DateTime();
$birthday->setTimestamp($customer->getBirthday()->getTimestamp());
$birthday = $birthday->format('Y-m-d');
$billingAddress->setDateOfBirth($birthday);
}
$salutation = "";
if (!(
empty($customerAddressEntity->getSalutation()) ||
empty($customerAddressEntity->getSalutation()->getDisplayName())
)) {
$salutation = $customerAddressEntity->getSalutation()->getDisplayName();
} else {
if (!empty($customer->getSalutation())) {
$salutation = $customer->getSalutation()->getDisplayName();
}
}
$billingAddress->setGender(strtolower($customerAddressEntity->getSalutation()->getSalutationKey()) === 'mr' ? Gender::MALE : Gender::FEMALE);
$billingAddress->setSalutation($salutation);
$lineItems = [];
if ($event) {
$cartLineItems = $event->getPage()->getCart()->getLineItems()->getElements();
foreach ($cartLineItems as $cartLineItem) {
if ($cartLineItem->getType() === CustomProductsLineItemTypes::LINE_ITEM_TYPE_CUSTOMIZED_PRODUCTS) {
continue;
}
$lineItems[] = $this->createTempLineItem($cartLineItem);
}
}
$customerId = "";
/*
* For guest checkouts, the customer ID is set to null rather than an empty string.
* This ensures consistency with TransactionPayload which also uses null for guests,
* preventing the Portal from treating the difference as an update/change in customer details.
*/
$customerId = null;
if ($customer->getGuest() === false) {
$customerId = $customer->getCustomerNumber();
}
@@ -608,23 +625,40 @@ class TransactionService
$protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https://' : 'http://';
$homeUrl = $protocol . $_SERVER['HTTP_HOST'];
$currency = $salesChannelContext->getCurrency()->getIsoCode();
$billingAddress = $this->buildAddress($salesChannelContext, $customer->getActiveBillingAddress());
$shippingAddress = $this->buildAddress($salesChannelContext, $customer->getActiveShippingAddress());
if (!$settings) {
throw new \Exception('Space settings not configured');
}
$language = $this->localeCodeProvider->getLocaleCodeFromContext($salesChannelContext->getContext());
$transactionPayload = (new TransactionCreate())
->setBillingAddress($billingAddress)
->setShippingAddress($shippingAddress)
->setLineItems($lineItems)
->setCurrency($currency)
->setLanguage($language)
->setSpaceViewId($settings->getSpaceViewId())
->setAutoConfirmationEnabled(false)
->setChargeRetryEnabled(false)
->setCustomerEmailAddress($customer->getEmail())
->setCustomerId($customerId)
->setSuccessUrl($homeUrl . '?success')
->setFailedUrl($homeUrl . '?fail')
->setTokenizationMode(TokenizationMode::FORCE_CREATION);
->setFailedUrl($homeUrl . '?fail');
if ($this->isSubscription($salesChannelContext)) {
$transactionPayload->setTokenizationMode(TokenizationMode::FORCE_CREATION);
}
$transactionService = $settings->getApiClient()->getTransactionService();
$transaction = $transactionService->create($settings->getSpaceId(), $transactionPayload);
$transactionId = $transaction->getId();
$_SESSION['transactionId'] = $transactionId;
// Store in cache and session for transaction reuse.
$this->storeTransactionIdInContext($salesChannelContext, $transactionId);
}
return $transactionId;
@@ -635,7 +669,7 @@ class TransactionService
* @param int $transactionId
* @return void
*/
public function updateTempTransaction(SalesChannelContext $salesChannelContext, int $transactionId): void
public function updateTempTransaction(SalesChannelContext $salesChannelContext, int $transactionId, array $lineItems = []): void
{
$pendingTransaction = new TransactionPending();
$pendingTransaction->setId($transactionId);
@@ -644,30 +678,97 @@ class TransactionService
$transaction = $settings->getApiClient()->getTransactionService()->read($settings->getSpaceId(), $transactionId);
$pendingTransaction->setVersion($transaction->getVersion());
$customerBillingAddress = $salesChannelContext->getCustomer()->getActiveBillingAddress();
$billingAddress = new AddressCreate();
$billingAddress->setStreet($customerBillingAddress->getStreet());
$billingAddress->setCity($customerBillingAddress->getCity());
$billingAddress->setCountry($customerBillingAddress->getCountry()->getIso());
$billingAddress->setPostCode($customerBillingAddress->getZipcode());
$postalState = $customerBillingAddress?->getCountryState()?->getName() ?? '';
if (empty($postalState)) {
$postalState = $customerBillingAddress?->getCountryState()?->getShortCode() ?? '';
}
$billingAddress->setPostalState($postalState);
$billingAddress->setOrganizationName($customerBillingAddress->getCompany());
$currency = $salesChannelContext->getCurrency()->getIsoCode();
$language = $this->localeCodeProvider->getLocaleCodeFromContext($salesChannelContext->getContext());
$pendingTransaction->setCurrency($currency);
$pendingTransaction->setLanguage($language);
$billingAddress = $this->buildAddress($salesChannelContext, $salesChannelContext->getCustomer()->getActiveBillingAddress());
$shippingAddress = $this->buildAddress($salesChannelContext, $salesChannelContext->getCustomer()->getActiveShippingAddress());
$pendingTransaction->setBillingAddress($billingAddress);
$pendingTransaction->setShippingAddress($shippingAddress);
if (!empty($lineItems)) {
$pendingTransaction->setLineItems($lineItems);
}
$settings->getApiClient()->getTransactionService()
->update($settings->getSpaceId(), $pendingTransaction);
}
/**
* Extracts line items from the given source (Event or Cart) and appends shipping costs.
*
* @param mixed $source
* @param SalesChannelContext|null $salesChannelContext
* @return array
*/
public function extractLineItems(
$source,
?SalesChannelContext $salesChannelContext = null,
): array {
$lineItems = [];
if ($source) {
if ($source instanceof CheckoutConfirmPageLoadedEvent) {
$cartLineItems = $source->getPage()->getCart()->getLineItems()->getElements();
foreach ($cartLineItems as $cartLineItem) {
if ($cartLineItem->getType() === CustomProductsLineItemTypes::LINE_ITEM_TYPE_CUSTOMIZED_PRODUCTS) {
continue;
}
$lineItems[] = $this->createTempLineItem($cartLineItem);
}
} elseif ($source instanceof AccountEditOrderPageLoadedEvent) {
$order = $source->getPage()->getOrder();
foreach ($order->getLineItems() as $orderLineItem) {
$lineItems[] = $this->createTempLineItem($orderLineItem);
}
} elseif ($source instanceof \Shopware\Core\Checkout\Cart\Cart) {
$cartLineItems = $source->getLineItems()->getElements();
foreach ($cartLineItems as $cartLineItem) {
if ($cartLineItem->getType() === CustomProductsLineItemTypes::LINE_ITEM_TYPE_CUSTOMIZED_PRODUCTS) {
continue;
}
$lineItems[] = $this->createTempLineItem($cartLineItem);
}
}
// Extract and append shipping costs as a line item if applicable.
$shippingCosts = null;
$taxStatus = 'gross';
if ($source instanceof CheckoutConfirmPageLoadedEvent) {
$cart = $source->getPage()->getCart();
$shippingCosts = $cart->getDeliveries()->getShippingCosts();
if ($salesChannelContext !== null) {
$taxStatus = $salesChannelContext->getTaxState();
}
} elseif ($source instanceof AccountEditOrderPageLoadedEvent) {
$order = $source->getPage()->getOrder();
$shippingCosts = $order->getShippingCosts();
$taxStatus = $order->getTaxStatus();
} elseif ($source instanceof \Shopware\Core\Checkout\Cart\Cart) {
$shippingCosts = $source->getDeliveries()->getShippingCosts();
if ($salesChannelContext !== null) {
$taxStatus = $salesChannelContext->getTaxState();
}
}
if ($shippingCosts !== null) {
$shippingLineItem = $this->extractShippingLineItem(
$shippingCosts,
$taxStatus,
$salesChannelContext,
);
if ($shippingLineItem !== null) {
$lineItems[] = $shippingLineItem;
}
}
}
return $lineItems;
}
/**
* @param ChargeAttempt|null $chargeAttempt
* @param string $descriptorKey
@@ -752,20 +853,296 @@ class TransactionService
return $chargeAttempts ? $chargeAttempts[0] : null;
}
/**
* @param LineItem $productData
* @return LineItemCreate
*/
private function createTempLineItem(LineItem $productData): LineItemCreate
private function createTempLineItem($productData): LineItemCreate
{
$lineItem = new LineItemCreate();
$lineItem->setName($productData->getLabel());
$lineItem->setUniqueId($productData->getId());
$lineItem->setSku($productData->getId());
$lineItem->setQuantity($productData->getQuantity());
$lineItem->setAmountIncludingTax($productData->getPrice()->getUnitPrice());
$price = $productData->getPrice();
$unit = $price->getUnitPrice();
// Expects discounts as separate items, avoid negative prices
if ($unit < 0) {
return $this->mapDiscountLineItem($productData);
}
if ($productData instanceof LineItem) {
$lineItem->setName($productData->getLabel());
$lineItem->setUniqueId($productData->getId());
$lineItem->setSku($productData->getReferencedId() ?? $productData->getId());
$lineItem->setQuantity($productData->getQuantity());
$lineItem->setAmountIncludingTax($this->round($unit));
} elseif ($productData instanceof OrderLineItemEntity) {
$lineItem->setName($productData->getLabel());
$lineItem->setUniqueId($productData->getId());
$lineItem->setSku($productData->getProductId() ?? $productData->getIdentifier() ?? $productData->getId());
$lineItem->setQuantity($productData->getQuantity());
$lineItem->setAmountIncludingTax($this->round($unit));
} else {
throw new \InvalidArgumentException('Unsupported line item type: ' . get_class($productData));
}
$lineItem->setType(LineItemType::PRODUCT);
return $lineItem;
}
/**
* Build a VRPayment address from Shopware customer address.
*
* @param \Shopware\Core\System\SalesChannel\SalesChannelContext $salesChannelContext
* @param \Shopware\Core\Checkout\Customer\Aggregate\CustomerAddress\CustomerAddressEntity $addressEntity
* @return \VRPayment\Sdk\Model\AddressCreate
*/
private function buildAddress(
SalesChannelContext $salesChannelContext,
\Shopware\Core\Checkout\Customer\Aggregate\CustomerAddress\CustomerAddressEntity $addressEntity
): AddressCreate {
$customer = $salesChannelContext->getCustomer();
$address = new AddressCreate();
$address->setFamilyName($addressEntity->getLastName() ?: $customer->getLastName() ?: '');
$address->setGivenName($addressEntity->getFirstName() ?: $customer->getFirstName() ?: '');
$address->setOrganizationName($addressEntity->getCompany());
$address->setPhoneNumber($addressEntity->getPhoneNumber());
$address->setCountry($addressEntity->getCountry()->getIso());
$address->setCity($addressEntity->getCity() ?: '');
$postalState = $addressEntity?->getCountryState()?->getName()
?: $addressEntity?->getCountryState()?->getShortCode()
?: '';
$address->setPostalState($postalState);
$address->setPostCode($addressEntity->getZipcode());
$address->setStreet($addressEntity->getStreet());
$address->setEmailAddress($customer->getEmail());
if (!empty($customer->getBirthday())) {
$birthday = (new \DateTimeImmutable())
->setTimestamp($customer->getBirthday()->getTimestamp())
->format('Y-m-d');
$address->setDateOfBirth($birthday);
}
$salutationEntity = $addressEntity->getSalutation() ?: $customer->getSalutation();
$address->setSalutation($salutationEntity?->getDisplayName() ?? '');
$address->setGender(
strtolower($salutationEntity?->getSalutationKey() ?? '') === 'mr'
? Gender::MALE
: Gender::FEMALE
);
return $address;
}
/**
* Checks if it's subscription context.
*
* @param \Shopware\Core\System\SalesChannel\SalesChannelContext $salesChannelContext
* @return bool
*/
private function isSubscription(SalesChannelContext $salesChannelContext): bool
{
$extensionName = 'subscription';
if (class_exists(\Shopware\Commercial\Subscription\Framework\Struct\SubscriptionContextStruct::class)) {
$extensionName = SubscriptionContextStruct::SUBSCRIPTION_EXTENSION;
}
if ($salesChannelContext->hasExtension($extensionName)) {
return true;
}
return false;
}
/**
* @param $amount
* @param int $precision
*
* @return float
*/
private function round($value, $precision = 2): float
{
return \round($value, $precision);
}
/**
* Generates a cache key for the pending transaction ID.
* Uses customer ID for authenticated users, which works for both headless and storefront.
*
* @param SalesChannelContext $salesChannelContext
* @return string|null
*/
private function getPendingTransactionCacheKey(SalesChannelContext $salesChannelContext): ?string
{
$customer = $salesChannelContext->getCustomer();
if ($customer) {
return 'vrpn_pending_transaction_id_customer_' . $customer->getId();
}
return null;
}
/**
* Retrieves the stored pending transaction ID from cache or session.
* Uses customer ID as cache key for headless (stateless) support.
* Falls back to session for Storefront (stateful) compatibility.
*
* @param SalesChannelContext $salesChannelContext
* @return int|null The transaction ID if found, otherwise null.
*/
private function getTransactionIdFromContext(SalesChannelContext $salesChannelContext): ?int
{
// Try cache first (for headless/API where session might not persist or be shared).
$cacheKey = $this->getPendingTransactionCacheKey($salesChannelContext);
if ($cacheKey) {
$item = $this->cache->getItem($cacheKey);
if ($item->isHit()) {
return (int) $item->get();
}
}
// Fallback to PHP session for traditional Storefront compatibility.
if (isset($_SESSION['transactionId'])) {
return (int) $_SESSION['transactionId'];
}
return null;
}
/**
* Clears the pending transaction ID from cache and session.
*
* @param SalesChannelContext $salesChannelContext
*/
public function clearTransactionIdFromContext(SalesChannelContext $salesChannelContext): void
{
// Clear from cache key.
$cacheKey = $this->getPendingTransactionCacheKey($salesChannelContext);
if ($cacheKey) {
$this->cache->deleteItem($cacheKey);
}
// Clear from session.
if (isset($_SESSION['transactionId'])) {
unset($_SESSION['transactionId']);
}
}
/**
* Stores the pending transaction ID in cache and session.
* This persists in the database (via cache) and works across all request types (Storefront & headless).
*
* @param SalesChannelContext $salesChannelContext
* @param int $transactionId
*/
private function storeTransactionIdInContext(SalesChannelContext $salesChannelContext, int $transactionId): void
{
// Store in cache for headless.
$cacheKey = $this->getPendingTransactionCacheKey($salesChannelContext);
if ($cacheKey) {
$item = $this->cache->getItem($cacheKey);
$item->set($transactionId);
// Expire after 2 hours to avoid stale data (matching typical cart lifetime).
$item->expiresAfter(7200);
$this->cache->save($item);
}
// Store in session for Storefront.
$_SESSION['transactionId'] = $transactionId;
}
/**
* Creates a discount line item for negative-priced cart entries.
*
* @param $productData
* @return LineItemCreate
*
*/
private function mapDiscountLineItem($productData): LineItemCreate
{
$price = $productData->getPrice();
$lineItem = new LineItemCreate();
$amount = abs($price->getTotalPrice());
$lineItem->setName($productData->getLabel() ?: 'Discount');
$lineItem->setUniqueId('discount-' . $productData->getId());
$lineItem->setSku('discount');
$lineItem->setQuantity(1);
$lineItem->setAmountIncludingTax($this->round($amount));
$lineItem->setType(LineItemType::DISCOUNT);
return $lineItem;
}
/**
* Extracts shipping line item from cart/order shipping costs.
*
* @param mixed $shippingCosts
* @param string $taxStatus
* @param SalesChannelContext|null $salesChannelContext
* @return LineItemCreate|null
*/
private function extractShippingLineItem(
$shippingCosts,
string $taxStatus,
?SalesChannelContext $salesChannelContext = null,
): ?LineItemCreate {
// When shipping costs are extracted from a Cart, they are returned as a PriceCollection
// containing multiple CalculatedPrice items. We must sum them to get a single aggregated price.
if ($shippingCosts instanceof \Shopware\Core\Checkout\Cart\Price\Struct\PriceCollection) {
$shippingCosts = $shippingCosts->sum();
}
if (!$shippingCosts instanceof \Shopware\Core\Checkout\Cart\Price\Struct\CalculatedPrice) {
return null;
}
if ($shippingCosts->getTotalPrice() <= 0) {
return null;
}
$amount = $shippingCosts->getTotalPrice();
if ($taxStatus === 'net') {
$amount += $shippingCosts->getCalculatedTaxes()->getAmount();
}
$roundedAmount = $this->round($amount);
$shippingMethodName = $salesChannelContext?->getShippingMethod()?->getName();
$translator = $this->container->has('translator') ? $this->container->get('translator') : null;
$fallbackName = $translator ? $translator->trans('vrpayment.payload.shipping.name') : 'Shipping';
$shippingName = $shippingMethodName ?? $fallbackName;
$shippingLineItem = new LineItemCreate();
$shippingLineItem->setAmountIncludingTax($roundedAmount)
->setName($this->fixLength($shippingName . ' ' . ($translator ? $translator->trans('vrpayment.payload.shipping.lineItem') : 'Shipping'), 150))
->setQuantity($shippingCosts->getQuantity() ?? 1)
->setSku($this->fixLength($shippingName . '-Shipping', 200))
->setType(LineItemType::SHIPPING)
->setUniqueId($this->fixLength($shippingName . '-Shipping', 200));
if ($taxStatus !== 'tax-free') {
$taxes = [];
foreach ($shippingCosts->getCalculatedTaxes() as $calculatedTax) {
$tax = (new TaxCreate())
->setRate($calculatedTax->getTaxRate())
->setTitle($this->fixLength($shippingName . ' : ' . $calculatedTax->getTaxRate(), 40));
$taxes[] = $tax;
}
$shippingLineItem->setTaxes($taxes);
}
return $shippingLineItem;
}
/**
* Fix string length to specific length.
*
* @param string $string
* @param int $maxLength
* @return string
*/
private function fixLength(string $string, int $maxLength): string
{
return \mb_substr($string, 0, $maxLength, 'UTF-8');
}
}
@@ -224,8 +224,18 @@ class WebHookController extends AbstractController {
// Configuration
$salesChannelId = $salesChannelId == 'null' ? null : $salesChannelId;
$this->settings = $this->settingsService->getSettings($salesChannelId);
$signature = $request->server->get('HTTP_X_SIGNATURE');
$requestJson = json_decode($request->getContent(), true);
if ($requestJson['eventId'] == null && $requestJson['entityId'] == null && $requestJson['listenerEntityId'] == null && $requestJson['listenerEntityId'] == null && $requestJson['listenerEntityTechnicalName'] == null && $requestJson['spaceId'] == null) {
throw new \InvalidArgumentException('Empty webhook');
}
if (!$this->settings->getSpaceId() || !$this->settings->getUserId() || !$this->settings->getApplicationKey()) {
throw new \InvalidArgumentException('Not correct webhook configuration for salesChannelId: ' . $salesChannelId . ' Debug: ' . var_dump($requestJson));
}
$apiClient = $this->settings->getApiClient();
$callBackData->assign($requestJson);
@@ -476,7 +486,7 @@ class WebHookController extends AbstractController {
$transaction = $this->settings->getApiClient()
->getTransactionService()
->read($callBackData->getSpaceId(), $callBackData->getEntityId());
$orderId = $transaction->getMetaData()[TransactionPayload::VRPAYMENT_METADATA_ORDER_ID];
$orderId = $transaction->getMetaData()[TransactionPayload::VRPAYMENT_METADATA_ORDER_ID] ?? null;
if(!empty($orderId) && !$transaction->getParent()) {
$this->executeLocked($orderId, $context, function () use ($orderId, $transaction, $context, $callBackData) {
$this->transactionService->upsert($transaction, $context);
@@ -690,16 +700,6 @@ class WebHookController extends AbstractController {
private function unholdAndCancelDelivery(string $orderId, Context $context): void
{
$order = $this->getOrderEntity($orderId, $context);
try {
$this->orderService->orderStateTransition(
$order->getId(),
StateMachineTransitionActions::ACTION_CANCEL,
new ParameterBag(),
$context
);
} catch (\Exception $exception) {
$this->logger->info($exception->getMessage(), $exception->getTrace());
}
try {
/**
@@ -64,7 +64,7 @@ class WebHookRefundStrategy extends WebHookStrategyBase implements WebhookStrate
/**
* @inheritDoc
*/
public function getOrderIdByTransaction($transaction): string
public function getOrderIdByTransaction($transaction): string|null
{
/** @var \VRPayment\Sdk\Model\Refund $transaction */
return $transaction->getTransaction()
@@ -378,16 +378,6 @@ abstract class WebHookStrategyBase implements WebHookStrategyInterface {
protected function unholdAndCancelDelivery(string $orderId, Context $context): void
{
$order = $this->getOrderEntity($orderId, $context);
try {
$this->orderService->orderStateTransition(
$order->getId(),
StateMachineTransitionActions::ACTION_CANCEL,
new ParameterBag(),
$context
);
} catch (\Exception $exception) {
$this->logger->info($exception->getMessage(), $exception->getTrace());
}
try {
@@ -63,10 +63,10 @@ class WebHookTransactionInvoiceStrategy extends WebHookStrategyBase implements W
/**
* @inheritDoc
*/
public function getOrderIdByTransaction($transaction): string
public function getOrderIdByTransaction($transactionInvoice): string|null
{
/** @var \VRPayment\Sdk\Model\TransactionInvoice $transaction */
return $transaction->getCompletion()
return $transactionInvoice->getCompletion()
->getLineItemVersion()
->getTransaction()
->getMetaData()[TransactionPayload::VRPAYMENT_METADATA_ORDER_ID];
@@ -61,7 +61,7 @@ class WebHookTransactionStrategy extends WebHookStrategyBase implements WebhookS
/**
* @inheritDoc
*/
public function getOrderIdByTransaction(Transaction $transaction): string
public function getOrderIdByTransaction(Transaction $transaction): string|null
{
/** @var \VRPayment\Sdk\Model\Transaction $transaction */
return $transaction->getMetaData()[TransactionPayload::VRPAYMENT_METADATA_ORDER_ID];
@@ -116,10 +116,12 @@ class WebHookTransactionStrategy extends WebHookStrategyBase implements WebhookS
/** @var \Shopware\Core\Checkout\Order\OrderEntity $order */
$transaction = $this->getTransaction($request);
$token = $transaction->getToken();
$orderId = $transaction->getMetaData()[TransactionPayload::VRPAYMENT_METADATA_ORDER_ID];
$orderId = $transaction->getMetaData()[TransactionPayload::VRPAYMENT_METADATA_ORDER_ID] ?? null;
if (!empty($orderId) && !$transaction->getParent()) {
$this->executeLocked($orderId, $context, function () use ($orderId, $transaction, $context, $request, $token) {
$this->transactionService->upsert($transaction, $context);
if ($this->allowUpsert($transaction, $orderId, $context)) {
$this->transactionService->upsert($transaction, $context);
}
$orderTransactionId = $transaction->getMetaData()[TransactionPayload::VRPAYMENT_METADATA_ORDER_TRANSACTION_ID];
$orderTransaction = $this->getOrderTransaction($orderId, $context);
$this->logger->info("OrderId: {orderId} Current state: {state}", [
@@ -178,4 +180,26 @@ class WebHookTransactionStrategy extends WebHookStrategyBase implements WebhookS
return new JsonResponse(['data' => $request->jsonSerialize()], $status);
}
/**
* Checks the incoming transaction ID against the current transaction ID of the order.
* If they dont match, the saved transaction in the database remains unchanged.
*
* @param Transaction $transaction The transaction data retrieved from the portal.
* @param string $orderId The order ID of the current transaction.
* @param Context $context The operational context providing settings and environment for transaction processing.
* @return bool Returns a value that determines whether to upsert the transaction into the database.
*/
private function allowUpsert(Transaction $transaction, string $orderId, Context $context): bool
{
try {
$transactionEntity = $this->transactionService->getByOrderId($orderId, $context);
if ($transactionEntity->getTransactionId() !== $transaction->getId()) {
return false;
}
} catch (\Throwable $e) {
}
return true;
}
}
@@ -57,5 +57,5 @@ interface WebhookStrategyActionsInterface {
* @param Transaction|TransactionInvoiceState|Refund|mixed $transaction The transaction object from which the order ID should be extracted.
* @return string The order ID as a string.
*/
public function getOrderIdByTransaction(Transaction $transaction): string;
public function getOrderIdByTransaction(Transaction $transaction): string|null;
}
@@ -1,4 +1,6 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
namespace VRPaymentPayment\Core\Checkout\PaymentHandler;
@@ -23,7 +25,7 @@ use Shopware\Core\{
Framework\Struct\Struct,
Framework\Validation\DataBag\RequestDataBag,
System\StateMachine\Aggregation\StateMachineState\StateMachineStateEntity,
System\SalesChannel\Context\SalesChannelContextService,
System\SalesChannel\Context\SalesChannelContextServiceInterface,
System\SalesChannel\Context\SalesChannelContextServiceParameters
};
use Shopware\Core\Framework\Util\Random;
@@ -69,7 +71,7 @@ class VRPaymentPaymentHandler extends AbstractPaymentHandler
*/
private $orderTransactionStateHandler;
protected SalesChannelContextService $salesChannelContextService;
protected SalesChannelContextServiceInterface $salesChannelContextService;
protected EntityRepository $orderTransactionRepository;
@@ -82,7 +84,7 @@ class VRPaymentPaymentHandler extends AbstractPaymentHandler
CustomCartPersister $cartPersister,
PluginTransactionService $pluginTransactionService,
OrderTransactionStateHandler $orderTransactionStateHandler,
SalesChannelContextService $salesChannelContextService,
SalesChannelContextServiceInterface $salesChannelContextService,
EntityRepository $orderTransactionRepository,
?EntityRepository $subscriptionRepository,
) {
@@ -119,13 +121,14 @@ class VRPaymentPaymentHandler extends AbstractPaymentHandler
PaymentTransactionStruct $transaction,
Context $context,
?Struct $validateStruct
): RedirectResponse
{
): RedirectResponse {
try {
$orderTransactionId = $transaction->getOrderTransactionId();
$orderTransaction = $this->orderTransactionRepository->search(
(new Criteria([$orderTransactionId]))
->addAssociation('order'), $context
->addAssociation('order')
->addAssociation('stateMachineState'),
$context
)->getEntities()->first();
$contextSource = $context->getSource();
@@ -133,7 +136,9 @@ class VRPaymentPaymentHandler extends AbstractPaymentHandler
$salesChannelContextId = $contextSource->getSalesChannelId();
}
$parameters = new SalesChannelContextServiceParameters($salesChannelContextId, $request->getSession()->get("sw-context-token", Random::getAlphanumericString(32)), originalContext: $context);
$contextToken = $this->getContextToken($request);
$parameters = new SalesChannelContextServiceParameters($salesChannelContextId, $contextToken, languageId: $context->getLanguageId(), originalContext: $context);
$salesChannelContext = $this->salesChannelContextService->get($parameters);
$redirectUrl = $transaction->getReturnUrl();
@@ -146,10 +151,21 @@ class VRPaymentPaymentHandler extends AbstractPaymentHandler
}
return new RedirectResponse($redirectUrl);
} catch (\Throwable $e) {
$request->getSession()->remove('transactionId');
// Clear the transaction ID from the cache or session context depending on whether the SalesChannelContext was initialized
// to prevent subsequent checkout attempts from reusing a failed/invalid transaction ID.
if (isset($salesChannelContext)) {
$this->pluginTransactionService->clearTransactionIdFromContext($salesChannelContext);
} else {
$request->getSession()->remove('transactionId');
}
$errorMessage = 'An error occurred during the communication with external payment gateway : ' . $e->getMessage();
$this->logger->critical($errorMessage);
throw PaymentException::customerCanceled($transaction->getOrderTransaction()->getId(), $errorMessage);
// If the transaction has already been marked as cancelled (e.g. via webhook or concurrent request),
// throw an interrupted exception to signal that payment processing cannot be finalized normally.
if ($orderTransaction->getStateMachineState()?->getTechnicalName() === OrderTransactionStates::STATE_CANCELLED) {
throw PaymentException::asyncFinalizeInterrupted($orderTransaction->getOrder()->getId(), $errorMessage);
}
throw PaymentException::customerCanceled($transaction->getOrderTransactionId(), $errorMessage);
}
}
@@ -168,12 +184,13 @@ class VRPaymentPaymentHandler extends AbstractPaymentHandler
Request $request,
PaymentTransactionStruct $transaction,
Context $context
): void
{
): void {
$orderTransactionId = $transaction->getOrderTransactionId();
$orderTransaction = $this->orderTransactionRepository->search(
(new Criteria([$orderTransactionId]))
->addAssociation('order'), $context
->addAssociation('order')
->addAssociation('stateMachineState'),
$context
)->getEntities()->first();
if ($orderTransaction->getOrder()->getAmountTotal() > 0) {
@@ -187,28 +204,65 @@ class VRPaymentPaymentHandler extends AbstractPaymentHandler
$transactionEntity->getSalesChannelId()
);
if (in_array($vRPaymentTransaction->getState(), [TransactionState::FAILED])) {
if (in_array($vRPaymentTransaction->getState(), [TransactionState::FAILED, TransactionState::DECLINE, TransactionState::VOIDED,])) {
$errorMessage = strtr('Customer canceled payment for :orderId on SalesChannel :salesChannelName', [
':orderId' => $orderTransaction->getOrder()->getId(),
':salesChannelName' => $transactionEntity->getSalesChannelId(),
]);
$request->getSession()->remove('transactionId');
// Retrieve the sales channel context parameters to clear the transaction ID from cache or session context,
// ensuring that a failed transaction is not reused when the customer retries payment.
$token = $this->getContextToken($request);
if ($token) {
$parameters = new SalesChannelContextServiceParameters(
$transactionEntity->getSalesChannelId(),
$token,
originalContext: $context,
);
$salesChannelContext = $this->salesChannelContextService->get($parameters);
$this->pluginTransactionService->clearTransactionIdFromContext($salesChannelContext);
} else {
$request->getSession()->remove('transactionId');
}
$this->logger->info($errorMessage);
throw PaymentException::customerCanceled($transaction->getOrderTransaction()->getId(), $errorMessage);
throw PaymentException::customerCanceled($orderTransactionId, $errorMessage);
}
} else {
$this->orderTransactionStateHandler->paid($orderTransaction->getId(), $context);
}
$token = $request->getSession()->get('sw-context-token');
$token = $this->getContextToken($request);
if ($token) {
$salesChannelId = $transactionEntity->getSalesChannelId();
$orderEntity = $this->pluginTransactionService->getOrderEntity(
$orderTransaction->getOrder()->getId(),
$context
);
$salesChannelId = $orderEntity->getSalesChannelId();
$parameters = new SalesChannelContextServiceParameters($salesChannelId, $token, originalContext: $context);
$salesChannelContext = $this->salesChannelContextService->get($parameters);
$salesChannelContext->getContext()->addState('do-cart-delete');
$this->logger->info('Clearing cart with token: ' . $token);
$this->cartPersister->delete($salesChannelContext->getToken(), $salesChannelContext);
} else {
// Fallback: Try to get token from transaction custom fields (for Headless redirects)
$customFields = $orderTransaction->getCustomFields() ?? [];
$storedToken = $customFields[TransactionPayload::ORDER_TRANSACTION_CUSTOM_FIELDS_VRPAYMENT_TOKEN] ?? null;
if ($storedToken) {
$this->logger->info('Clearing cart with stored token: ' . $storedToken);
$orderEntity = $this->pluginTransactionService->getOrderEntity(
$orderTransaction->getOrder()->getId(),
$context
);
$salesChannelId = $orderEntity->getSalesChannelId();
$parameters = new SalesChannelContextServiceParameters($salesChannelId, $storedToken, originalContext: $context);
$salesChannelContext = $this->salesChannelContextService->get($parameters);
$salesChannelContext->getContext()->addState('do-cart-delete');
$this->cartPersister->delete($salesChannelContext->getToken(), $salesChannelContext);
}
}
}
@@ -219,7 +273,7 @@ class VRPaymentPaymentHandler extends AbstractPaymentHandler
PaymentHandlerType $type,
string $paymentMethodId,
Context $context
): bool {
): bool {
// Both PaymentHandlerType::RECURRING and PaymentHandlerType::REFUND are supported
//TODO: check that the payment method really supports recurring.
// In order to do that, we need to get this information in when synching the payment methods.
@@ -271,7 +325,7 @@ class VRPaymentPaymentHandler extends AbstractPaymentHandler
throw PaymentException::recurringInterrupted($newTransactionId, 'No orders found associated with the subscription.');
}
$orders->sort(fn (OrderEntity $a, OrderEntity $b) => $a->getCreatedAt() <=> $b->getCreatedAt());
$orders->sort(fn(OrderEntity $a, OrderEntity $b) => $a->getCreatedAt() <=> $b->getCreatedAt());
/** @var OrderEntity|null $originalOrder */
$originalOrder = $orders->first();
@@ -283,7 +337,7 @@ class VRPaymentPaymentHandler extends AbstractPaymentHandler
/** @var OrderTransactionEntity|null $originalTransaction */
$originalTransaction = $originalTransactions->filter(
fn (OrderTransactionEntity $t) => $t->getStateMachineState()?->getTechnicalName() === OrderTransactionStates::STATE_PAID
fn(OrderTransactionEntity $t) => $t->getStateMachineState()?->getTechnicalName() === OrderTransactionStates::STATE_PAID
)->first();
if ($originalTransaction === null) {
@@ -292,7 +346,8 @@ class VRPaymentPaymentHandler extends AbstractPaymentHandler
$newOrderTransaction = $this->orderTransactionRepository->search(
(new Criteria([$newTransactionId]))
->addAssociation('order'), $context
->addAssociation('order'),
$context
)->getEntities()->first();
$orderNumber = $newOrderTransaction->getOrder()->getOrderNumber();
@@ -365,8 +420,7 @@ class VRPaymentPaymentHandler extends AbstractPaymentHandler
// Update the new order transaction with the new transaction details
$this->orderTransactionRepository->update([$data], $context);
$this->pluginTransactionService->upsert($newSdkTransaction, $context);
}
catch (\Throwable $e) {
} catch (\Throwable $e) {
$errorMessage = 'An error occurred during the communication with external payment gateway : ' . $e->getMessage();
$this->logger->critical($errorMessage);
throw PaymentException::recurringInterrupted($transaction->getOrderTransactionId(), $errorMessage);
@@ -379,7 +433,8 @@ class VRPaymentPaymentHandler extends AbstractPaymentHandler
* @param \VRPayment\Sdk\Model\Address $address The address model from the SDK.
* @return \VRPayment\Sdk\Model\AddressCreate The newly created AddressCreate instance.
*/
private function addressCreateFromSdk(\VRPayment\Sdk\Model\Address $address): \VRPayment\Sdk\Model\AddressCreate {
private function addressCreateFromSdk(\VRPayment\Sdk\Model\Address $address): \VRPayment\Sdk\Model\AddressCreate
{
$addressCreate = new \VRPayment\Sdk\Model\AddressCreate;
$addressCreate->setCity($address->getCity());
@@ -419,7 +474,9 @@ class VRPaymentPaymentHandler extends AbstractPaymentHandler
{
$lineItemCreate = new \VRPayment\Sdk\Model\LineItemCreate();
$lineItemCreate->setAmountIncludingTax($lineItem->getAmountIncludingTax());
$roundedPrice = $this->round($lineItem->getAmountIncludingTax());
$lineItemCreate->setAmountIncludingTax($roundedPrice);
$attributes = $lineItem->getAttributes();
$attributesCreate = [];
@@ -456,4 +513,30 @@ class VRPaymentPaymentHandler extends AbstractPaymentHandler
return $lineItemCreate;
}
/**
* @param $amount
* @param int $precision
*
* @return float
*/
private function round($value, $precision = 2): float
{
return \round($value, $precision);
}
private function getContextToken(Request $request): ?string
{
$headerContextToken = $request->headers->get('sw-context-token');
if ($headerContextToken) {
return $headerContextToken;
}
$sessionContextToken = $request->getSession()->get("sw-context-token");
if ($sessionContextToken) {
return $sessionContextToken;
}
return null;
}
}
@@ -0,0 +1,121 @@
<?php
declare(strict_types=1);
namespace VRPaymentPayment\Core\Checkout\PaymentMethod\SalesChannel;
use Shopware\Core\Checkout\Payment\PaymentMethodDefinition;
use Shopware\Core\Checkout\Payment\SalesChannel\AbstractPaymentMethodRoute;
use Shopware\Core\Checkout\Payment\SalesChannel\PaymentMethodRouteResponse;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Framework\DataAbstractionLayer\Search\EntitySearchResult;
use Shopware\Core\Framework\Log\Package;
use Shopware\Core\System\SalesChannel\SalesChannelContext;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Attribute\Route;
use VRPaymentPayment\Core\Checkout\Service\PaymentMethodFilterService;
#[Package('checkout')]
/**
* This decorator intercepts the Store API payment method route to apply WhitelabelMachineName-specific filtering.
* It ensures that only payment methods valid for the current transaction (given currency, amount, etc.) are shown.
*/
class PaymentMethodRouteDecorator extends AbstractPaymentMethodRoute
{
/**
* @var AbstractPaymentMethodRoute
* The original route being decorated.
*/
private AbstractPaymentMethodRoute $decorated;
/**
* @var PaymentMethodFilterService
* Service used to filter the payment methods based on WhitelabelMachineName API availability.
*/
private PaymentMethodFilterService $paymentMethodFilterService;
/**
* @param AbstractPaymentMethodRoute $decorated
* @param PaymentMethodFilterService $paymentMethodFilterService
*/
public function __construct(
AbstractPaymentMethodRoute $decorated,
PaymentMethodFilterService $paymentMethodFilterService
) {
$this->decorated = $decorated;
$this->paymentMethodFilterService = $paymentMethodFilterService;
}
/**
* Returns the decorated service.
*
* @return AbstractPaymentMethodRoute
*/
public function getDecorated(): AbstractPaymentMethodRoute
{
return $this->decorated;
}
/**
* Loads the payment methods and applies the WhitelabelMachineName filter to the result.
*
* @param Request $request
* @param SalesChannelContext $context
* @param Criteria $criteria
* @return PaymentMethodRouteResponse
*/
#[Route(
path: '/store-api/payment-method',
name: 'store-api.payment.method',
methods: ['GET', 'POST'],
defaults: ['_entity' => 'payment_method']
)]
public function load(Request $request, SalesChannelContext $context, Criteria $criteria): PaymentMethodRouteResponse
{
// Fetch the initial list of payment methods from the decorated service.
$response = $this->decorated->load($request, $context, $criteria);
$currentRoute = $request->attributes->get('_route');
if ($currentRoute === 'frontend.checkout.finish.page' || !$this->allowFilterPaymentMethods($request)) {
return $response;
}
$paymentMethods = $response->getPaymentMethods();
// Apply WhitelabelMachineName-specific filtering logic via the dedicated service.
$filteredCollection = $this->paymentMethodFilterService->filterPaymentMethods(
$paymentMethods,
$context
);
// Return the filtered results as a new response.
return new PaymentMethodRouteResponse(
new EntitySearchResult(
'payment_method',
(int)$filteredCollection->count(),
$filteredCollection,
null,
$criteria,
$context->getContext()
)
);
}
/**
* We prevent filtering methods unless onlyAvailable is true.
* This is because the filterPaymentMethods() function creates unnecessary pending transactions in the
* portal when logged-in users navigate between pages.
* The onlyAvailable flag applies rule-based filtering of payment methods and is usually true on checkout pages,
* so we apply filterPaymentMethods() only when relevant.
*
* @param Request $request
* @return bool
*/
private function allowFilterPaymentMethods(Request $request): bool
{
if ($request->query->getBoolean('onlyAvailable') || $request->request->getBoolean('onlyAvailable')) {
return true;
}
return false;
}
}
@@ -0,0 +1,298 @@
<?php
declare(strict_types=1);
namespace VRPaymentPayment\Core\Checkout\Service;
use Shopware\Core\Checkout\Cart\Cart;
use Shopware\Core\Checkout\Cart\LineItemFactoryRegistry;
use Shopware\Core\Checkout\Cart\SalesChannel\CartService;
use Shopware\Core\Checkout\Order\Aggregate\OrderLineItem\OrderLineItemCollection;
use Shopware\Core\Checkout\Order\Aggregate\OrderLineItem\OrderLineItemEntity;
use Shopware\Core\Checkout\Order\OrderEntity;
use Shopware\Core\Framework\Context;
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Framework\Validation\DataBag\RequestDataBag;
use Shopware\Core\System\SalesChannel\SalesChannelContext;
use Symfony\Component\HttpFoundation\Request;
use VRPaymentPayment\Core\Util\Payload\CustomProducts\CustomProductsLineItemTypes;
/**
* This service handles the reconstruction of a Shopware cart from an existing order.
* It ensures that line items, including those from complex plugins like 'Customized Products',
* are correctly re-added to a fresh cart.
*/
class CartRecoveryService
{
/**
* @var CartService
* Shopware service for cart-related operations.
*/
private CartService $cartService;
/**
* @var LineItemFactoryRegistry
* Registry to create Shopware LineItems from raw data.
*/
private LineItemFactoryRegistry $lineItemFactoryRegistry;
/**
* @var EntityRepository
* Repository for accessing order data.
*/
private EntityRepository $orderRepository;
/**
* @var object|null
* Optional route service for adding customized products to the cart.
*/
private ?object $addCustomizedProductsRoute;
/**
* @param CartService $cartService
* @param LineItemFactoryRegistry $lineItemFactoryRegistry
* @param EntityRepository $orderRepository
* @param object|null $addCustomizedProductsRoute
*/
public function __construct(
CartService $cartService,
LineItemFactoryRegistry $lineItemFactoryRegistry,
EntityRepository $orderRepository,
?object $addCustomizedProductsRoute = null
) {
$this->cartService = $cartService;
$this->lineItemFactoryRegistry = $lineItemFactoryRegistry;
$this->orderRepository = $orderRepository;
$this->addCustomizedProductsRoute = $addCustomizedProductsRoute;
}
/**
* Recreates a cart based on the items in an existing order.
*
* @param OrderEntity $order The source order.
* @param SalesChannelContext $salesChannelContext The current sales channel context.
* @return Cart The newly created and populated cart.
*/
public function recreateCartFromOrder(OrderEntity $order, SalesChannelContext $salesChannelContext): Cart
{
// Start with a clean slate by deleting any existing cart for the current session.
$this->cartService->deleteCart($salesChannelContext);
$cart = $this->cartService->createNew($salesChannelContext->getToken());
$orderItems = $order->getLineItems();
if ($orderItems === null) {
return $cart;
}
// Special handling for Customized Products if the plugin logic is available.
if ($this->hasCustomProducts($orderItems) && $this->addCustomizedProductsRoute) {
$cart = $this->addCustomProducts($orderItems, $salesChannelContext, $cart);
}
/** @var OrderLineItemEntity $orderLineItemEntity */
foreach ($orderItems as $orderLineItemEntity) {
$type = (string)$orderLineItemEntity->getType();
// Skip child items and complex types that should have been handled by specialized logic.
if ($type !== CustomProductsLineItemTypes::LINE_ITEM_TYPE_PRODUCT || $orderLineItemEntity->getParentId() !== null) {
continue;
}
// Create a standard product line item.
$lineItem = $this->lineItemFactoryRegistry->create([
'id' => $orderLineItemEntity->getId(),
'quantity' => (int)$orderLineItemEntity->getQuantity(),
'referencedId' => (string)$orderLineItemEntity->getReferencedId(),
'type' => $type,
], $salesChannelContext);
// Preserve payload data to ensure product options and other metadata are carried over.
$lineItemPayload = $orderLineItemEntity->getPayload();
if (!empty($lineItemPayload)) {
$lineItem->setPayload($lineItemPayload);
}
// Add the item to the cart.
$cart = $this->cartService->add($cart, $lineItem, $salesChannelContext);
}
return $cart;
}
/**
* Checks if the order contains any line items belonging to the Customized Products plugin.
*
* @param OrderLineItemCollection $orderItems The items in the order.
* @return bool True if customized products are present.
*/
private function hasCustomProducts(OrderLineItemCollection $orderItems): bool
{
/** @var OrderLineItemEntity $orderItem */
foreach ($orderItems as $orderItem) {
if ($orderItem->getType() === CustomProductsLineItemTypes::LINE_ITEM_TYPE_CUSTOMIZED_PRODUCTS) {
return true;
}
}
return false;
}
/**
* Specialized logic to re-add customized products to the cart via the plugin's own route.
*
* @param OrderLineItemCollection $orderItems All order items.
* @param SalesChannelContext $salesChannelContext Context.
* @param Cart $cart Current cart.
* @return Cart Updated cart.
*/
private function addCustomProducts(OrderLineItemCollection $orderItems, SalesChannelContext $salesChannelContext, Cart $cart): Cart
{
if (!$this->addCustomizedProductsRoute) {
return $cart;
}
/** @var OrderLineItemEntity $orderItem */
foreach ($orderItems as $orderItem) {
if ($orderItem->getType() !== CustomProductsLineItemTypes::LINE_ITEM_TYPE_CUSTOMIZED_PRODUCTS) {
continue;
}
// Find the main product associated with this customized product container.
$product = $this->getCustomProduct($orderItems, (string)$orderItem->getId());
if (!$product) continue;
// Gather the chosen options and their values.
$productOptions = $this->getCustomProductOptions($orderItems, (string)$orderItem->getId());
$optionValues = $this->getOptionValues($productOptions);
// Prepare the data bag for the specialized add-to-cart route.
$params = new RequestDataBag([
'customized-products-template' => new RequestDataBag([
'id' => (string)$orderItem->getReferencedId(),
'options' => new RequestDataBag($optionValues),
]),
]);
$request = new Request([], [
'lineItems' => [
(string)$product->getReferencedId() => [
'quantity' => (int)$orderItem->getQuantity(),
'id' => (string)$product->getReferencedId(),
'type' => CustomProductsLineItemTypes::LINE_ITEM_TYPE_PRODUCT,
'referencedId' => (string)$product->getReferencedId(),
'stackable' => (bool)$orderItem->getStackable(),
'removable' => (bool)$orderItem->getRemovable(),
]
]
]);
// Call the Customized Products plugin's internal logic to add the item with its complex configuration.
$this->addCustomizedProductsRoute->add($params, $request, $salesChannelContext, $cart);
// Re-fetch the cart to reflect changes made by the plugin route.
$cart = $this->cartService->getCart($salesChannelContext->getToken(), $salesChannelContext);
}
return $cart;
}
/**
* Finds the main product item within a customized product structure.
*
* @param OrderLineItemCollection $orderItems All order items.
* @param string $parentId The ID of the customized product container.
* @return OrderLineItemEntity|null The product line item entity.
*/
private function getCustomProduct(OrderLineItemCollection $orderItems, string $parentId): ?OrderLineItemEntity
{
/** @var OrderLineItemEntity $orderItem */
foreach ($orderItems as $orderItem) {
if ($orderItem->getType() === CustomProductsLineItemTypes::LINE_ITEM_TYPE_PRODUCT && $orderItem->getParentId() === $parentId) {
return $orderItem;
}
}
return null;
}
/**
* Gathers all option items for a customized product.
*
* @param OrderLineItemCollection $orderItems All items.
* @param string $parentId The ID of the customized product container.
* @return OrderLineItemEntity[] List of option entities.
*/
private function getCustomProductOptions(OrderLineItemCollection $orderItems, string $parentId): array
{
$options = [];
/** @var OrderLineItemEntity $orderItem */
foreach ($orderItems as $orderItem) {
if ($orderItem->getType() === CustomProductsLineItemTypes::LINE_ITEM_TYPE_CUSTOMIZED_PRODUCTS_OPTION && $orderItem->getParentId() === $parentId) {
$options[] = $orderItem;
}
}
return $options;
}
/**
* Converts a list of options into a data structure suitable for the Customized Products logic.
*
* @param OrderLineItemEntity[] $productOptions List of option entities.
* @return array Formatted option values.
*/
private function getOptionValues(array $productOptions): array
{
$optionValues = [];
foreach ($productOptions as $productOption) {
$payload = (array)$productOption->getPayload();
$optionType = (string)($payload['type'] ?? '');
switch ($optionType) {
case CustomProductsLineItemTypes::PRODUCT_OPTION_TYPE_IMAGE_UPLOAD:
case CustomProductsLineItemTypes::PRODUCT_OPTION_TYPE_FILE_UPLOAD:
$media = (array)($payload['media'] ?? []);
foreach ($media as $mediaItem) {
$optionValues[(string)$productOption->getReferencedId()] = new RequestDataBag([
'media' => new RequestDataBag([
(string)$mediaItem['filename'] => new RequestDataBag([
'id' => (string)$mediaItem['mediaId'],
'filename' => (string)$mediaItem['filename'],
]),
]),
]);
}
break;
default:
$optionValues[(string)$productOption->getReferencedId()] = new RequestDataBag([
'value' => (string)($payload['value'] ?? ''),
]);
}
}
return $optionValues;
}
/**
* Fetches a full order entity with necessary associations for recovery or display.
*
* @param string $orderId The ID of the order.
* @param Context $context The current system context.
* @return OrderEntity The order entity.
* @throws \Exception If the order is not found.
*/
public function getOrderEntity(string $orderId, Context $context): OrderEntity
{
$criteria = (new Criteria([$orderId]))
->addAssociation('lineItems.cover')
->addAssociation('transactions.paymentMethod')
->addAssociation('deliveries.shippingMethod');
/** @var OrderEntity|null $order */
$order = $this->orderRepository->search($criteria, $context)->get($orderId);
if (!$order) {
throw new \Exception('Order not found');
}
return $order;
}
}
@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
namespace VRPaymentPayment\Core\Checkout\Service;
use Shopware\Core\System\SalesChannel\SalesChannelContext;
use VRPaymentPayment\Core\Api\Transaction\Service\TransactionService;
use VRPaymentPayment\Core\Settings\Service\SettingsService;
/**
* This service provides methods for retrieving invoice documents associated with WhitelabelMachineName transactions.
* It abstracts the API calls needed to fetch invoice data from the WhitelabelMachineName platform.
*/
class InvoiceService
{
/**
* @var SettingsService
* The service used to access sales channel specific settings.
*/
protected SettingsService $settingsService;
/**
* @var TransactionService
* The service used to handle transaction-specific operations.
*/
protected TransactionService $transactionService;
/**
* @param SettingsService $settingsService
* @param TransactionService $transactionService
*/
public function __construct(
SettingsService $settingsService,
TransactionService $transactionService
) {
$this->settingsService = $settingsService;
$this->transactionService = $transactionService;
}
/**
* Fetches the invoice document metadata for a given order.
*
* @param string $orderId The Shopware order ID.
* @param SalesChannelContext $salesChannelContext The current context.
*
* @return object The invoice document metadata (instance of \VRPayment\Sdk\Model\RenderedDocument).
*/
public function getInvoiceDocument(string $orderId, SalesChannelContext $salesChannelContext): object
{
// Retrieve valid settings for the current sales channel.
$settings = $this->settingsService->getSettings($salesChannelContext->getSalesChannel()->getId());
// Fetch the local transaction entity associated with the order.
$transactionEntity = $this->transactionService->getByOrderId($orderId, $salesChannelContext->getContext());
// Perform the API call to WhitelabelMachineName to get the invoice document metadata.
return $settings->getApiClient()->getTransactionService()->getInvoiceDocument(
(int)$settings->getSpaceId(),
(int)$transactionEntity->getTransactionId()
);
}
}
@@ -0,0 +1,210 @@
<?php
declare(strict_types=1);
namespace VRPaymentPayment\Core\Checkout\Service;
use Shopware\Core\Framework\Uuid\Uuid;
use Shopware\Core\System\SalesChannel\SalesChannelContext;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Routing\RouterInterface;
use VRPaymentPayment\Core\Api\Transaction\Service\TransactionService;
use VRPaymentPayment\Core\Settings\Options\Integration;
use VRPaymentPayment\Core\Settings\Service\SettingsService;
use VRPaymentPayment\Core\Checkout\Struct\PaymentConfigStruct;
use VRPaymentPayment\Core\Util\LocaleCodeProvider;
use VRPayment\Sdk\Model\TransactionState;
/**
* This service provides the consolidated payment configuration needed for the frontend.
* it handles the generation of JS URLs and provides integration parameters for both Storefront and headless clients.
*/
class PaymentIntegrationService
{
/**
* @var TransactionService
* Service to handle transaction-specific API calls.
*/
private TransactionService $transactionService;
/**
* @var SettingsService
* Service to retrieve sales channel specific settings.
*/
private SettingsService $settingsService;
/**
* @var TransactionManagementService
* Service to manage transaction state.
*/
private TransactionManagementService $transactionManagementService;
/**
* @var RouterInterface
* Shopware router for generating callback and redirect URLs.
*/
private RouterInterface $router;
/**
* @var LocaleCodeProvider
*/
private LocaleCodeProvider $localeCodeProvider;
/**
* @param TransactionService $transactionService
* @param SettingsService $settingsService
* @param TransactionManagementService $transactionManagementService
* @param RouterInterface $router
* @param LocaleCodeProvider $localeCodeProvider
*/
public function __construct(
TransactionService $transactionService,
SettingsService $settingsService,
TransactionManagementService $transactionManagementService,
RouterInterface $router,
LocaleCodeProvider $localeCodeProvider
) {
$this->transactionService = $transactionService;
$this->settingsService = $settingsService;
$this->transactionManagementService = $transactionManagementService;
$this->router = $router;
$this->localeCodeProvider = $localeCodeProvider;
}
/**
* Generates the payment configuration for a given transaction ID.
* This is used on the checkout confirm page before the order is created.
*
* @param int $transactionId The WhitelabelMachineName transaction ID.
* @param SalesChannelContext $salesChannelContext The context.
* @return PaymentConfigStruct The consolidated integration data.
*/
public function getConfigForTransaction(
int $transactionId,
SalesChannelContext $salesChannelContext
): PaymentConfigStruct {
$settings = $this->settingsService->getSettings($salesChannelContext->getSalesChannel()->getId());
// Fetch the transaction details from WhitelabelMachineName API.
$vrpaymentTransaction = $settings->getApiClient()->getTransactionService()->read(
$settings->getSpaceId(),
$transactionId
);
$localeCode = $this->localeCodeProvider->getLocaleCodeFromContext($salesChannelContext->getContext());
$paymentPageLocale = $this->localeCodeProvider->mapToPaymentPageLocale($localeCode);
$javascriptUrl = $this->getTransactionJavaScriptUrl($settings, $transactionId, $paymentPageLocale);
$possiblePaymentMethods = $settings->getApiClient()
->getTransactionService()
->fetchPaymentMethods(
$settings->getSpaceId(),
$transactionId,
$settings->getIntegration()
);
$cartRecreateUrl = $this->router->generate(
'frontend.checkout.cart.page',
[],
UrlGeneratorInterface::ABSOLUTE_URL
);
$checkoutUrl = $this->router->generate(
'frontend.checkout.confirm.page',
[],
UrlGeneratorInterface::ABSOLUTE_URL
);
return (new PaymentConfigStruct())
->setIntegration((string)$settings->getIntegration())
->setJavascriptUrl($javascriptUrl)
->setDeviceJavascriptUrl($this->getDeviceJavascriptUrl((int)$settings->getSpaceId()))
->setTransactionPossiblePaymentMethods($possiblePaymentMethods)
->setTransactionId($transactionId)
->setSpaceId((int)$settings->getSpaceId())
->setCartRecreateUrl($cartRecreateUrl)
->setCheckoutUrl($checkoutUrl);
}
/**
* Generates the payment configuration for a given order.
*
* @param string $orderId The Shopware order ID.
* @param SalesChannelContext $salesChannelContext The context.
* @param string|null $cartRecreateUrl Optional override for the cart recreation URL.
* @param string|null $checkoutUrl Optional override for the checkout confirmation URL.
* @return PaymentConfigStruct The consolidated integration data.
*/
public function getPaymentConfig(
string $orderId,
SalesChannelContext $salesChannelContext,
?string $cartRecreateUrl = null,
?string $checkoutUrl = null
): PaymentConfigStruct {
// Retrieve settings and the transaction entity for the order.
$settings = $this->settingsService->getSettings($salesChannelContext->getSalesChannel()->getId());
$transactionEntity = $this->transactionService->getByOrderId($orderId, $salesChannelContext->getContext());
// Default to storefront URLs if no overrides are provided.
if ($cartRecreateUrl === null) {
$cartRecreateUrl = $this->router->generate(
'frontend.vrpayment.checkout.recreate-cart',
['orderId' => $orderId],
UrlGeneratorInterface::ABSOLUTE_URL
);
}
if ($checkoutUrl === null) {
$checkoutUrl = $this->router->generate(
'frontend.checkout.confirm.page',
[],
UrlGeneratorInterface::ABSOLUTE_URL
);
}
return $this->getConfigForTransaction((int)$transactionEntity->getTransactionId(), $salesChannelContext)
->setCartRecreateUrl($cartRecreateUrl)
->setCheckoutUrl($checkoutUrl);
}
/**
* Determines the JavaScript URL for the WhitelabelMachineName integration.
*
* @param mixed $settings The plugin settings.
* @param int $transactionId The transaction ID.
* @param string $paymentPageLocale The payment page locale.
* @return string The absolute URL to the JavaScript component.
*/
private function getTransactionJavaScriptUrl($settings, int $transactionId, string $paymentPageLocale = ''): string
{
$javascriptUrl = '';
switch ($settings->getIntegration()) {
case Integration::IFRAME:
$javascriptUrl = $settings->getApiClient()->getTransactionIframeService()
->javascriptUrl($settings->getSpaceId(), $transactionId);
break;
case Integration::LIGHTBOX:
$javascriptUrl = $settings->getApiClient()->getTransactionLightboxService()
->javascriptUrl($settings->getSpaceId(), $transactionId);
break;
}
if ($javascriptUrl && $paymentPageLocale) {
$separator = str_contains($javascriptUrl, '?') ? '&' : '?';
$javascriptUrl .= $separator . 'language=' . $paymentPageLocale;
}
return $javascriptUrl;
}
/**
* Generates the device tracking JavaScript URL.
*
* @param int $spaceId The WhitelabelMachineName space ID.
* @return string The tracking URL.
*/
private function getDeviceJavascriptUrl(int $spaceId): string
{
return 'https://gateway.vr-payment.de/s/' . $spaceId . '/payment/device.js?session=' . Uuid::randomHex();
}
}
@@ -0,0 +1,260 @@
<?php
declare(strict_types=1);
namespace VRPaymentPayment\Core\Checkout\Service;
use Shopware\Core\Checkout\Payment\PaymentMethodCollection;
use Shopware\Core\System\SalesChannel\SalesChannelContext;
use Shopware\Core\Checkout\Cart\SalesChannel\CartService;
use VRPaymentPayment\Core\Api\PaymentMethodConfiguration\Service\PaymentMethodConfigurationService;
use VRPaymentPayment\Core\Api\Transaction\Service\TransactionService;
use VRPaymentPayment\Core\Checkout\PaymentHandler\VRPaymentPaymentHandler;
use VRPaymentPayment\Core\Settings\Service\SettingsService;
use VRPaymentPayment\Core\Util\PaymentMethodUtil;
use Shopware\Core\Framework\Struct\ArrayEntity;
use Psr\Log\LoggerInterface;
/**
* This service centralizes the logic for filtering WhitelabelMachineName payment methods.
* It ensures that only valid and available payment methods are displayed to the customer,
* based on the current transaction state and configured settings.
*/
class PaymentMethodFilterService
{
/**
* @var PaymentMethodConfigurationService
* Service to handle WhitelabelMachineName payment method configurations.
*/
private PaymentMethodConfigurationService $paymentMethodConfigurationService;
/**
* @var TransactionService
* Service to manage WhitelabelMachineName transactions via API.
*/
private TransactionService $transactionService;
/**
* @var SettingsService
* Service to retrieve plugin settings.
*/
private SettingsService $settingsService;
/**
* @var PaymentMethodUtil
* Utility for payment method operations.
*/
private PaymentMethodUtil $paymentMethodUtil;
/**
* @var LoggerInterface
*/
private LoggerInterface $logger;
/**
* @var TransactionManagementService
* Service to manage transaction state consistency.
*/
private TransactionManagementService $transactionManagementService;
/**
* @var CartService
*/
private CartService $cartService;
/**
* @param SettingsService $settingsService
* @param TransactionService $transactionService
* @param PaymentMethodConfigurationService $paymentMethodConfigurationService
* @param PaymentMethodUtil $paymentMethodUtil
* @param TransactionManagementService $transactionManagementService
* @param CartService $cartService
*/
public function __construct(
SettingsService $settingsService,
TransactionService $transactionService,
PaymentMethodConfigurationService $paymentMethodConfigurationService,
PaymentMethodUtil $paymentMethodUtil,
TransactionManagementService $transactionManagementService,
CartService $cartService
) {
$this->settingsService = $settingsService;
$this->transactionService = $transactionService;
$this->paymentMethodConfigurationService = $paymentMethodConfigurationService;
$this->paymentMethodUtil = $paymentMethodUtil;
$this->transactionManagementService = $transactionManagementService;
$this->cartService = $cartService;
}
/**
* @param LoggerInterface $logger
*/
public function setLogger(LoggerInterface $logger): void
{
$this->logger = $logger;
}
/**
* Filters the given collection of payment methods based on WhitelabelMachineName's availability logic.
*
* @param PaymentMethodCollection $paymentMethodCollection The initial collection of payment methods.
* @param SalesChannelContext $salesChannelContext The current sales channel context.
* @param mixed $event Optional event that triggered the filtering.
* @return PaymentMethodCollection The filtered collection of payment methods.
*/
public function filterPaymentMethods(
PaymentMethodCollection $paymentMethodCollection,
SalesChannelContext $salesChannelContext,
$event = null
): PaymentMethodCollection {
// Fetch valid settings for the current sales channel.
$settings = $this->settingsService->getValidSettings($salesChannelContext->getSalesChannel()->getId());
// If settings are missing, remove all WhitelabelMachineName payment methods to prevent incorrect behavior.
if (is_null($settings)) {
return $this->removeVRPaymentPaymentMethods($paymentMethodCollection, $salesChannelContext);
}
// If there is no customer, we cannot create a transaction or perform API-based filtering.
// This typically happens on non-checkout pages like the frontpage footer.
if ($salesChannelContext->getCustomer() === null) {
return $paymentMethodCollection;
}
$source = $event;
if ($source === null) {
// In headless (Store API) flow, event is null. We explicitly fetch the cart to get line items.
$source = $this->cartService->getCart($salesChannelContext->getToken(), $salesChannelContext);
}
// Ensure a pending transaction exists in WhitelabelMachineName for correct filtering,
// using the transaction management service for state consistency.
$createdTransactionId = $this->transactionManagementService->getOrCreatePendingTransaction($salesChannelContext, $source);
// Update the temporary transaction if customer data has changed.
$this->transactionManagementService->updateTempTransactionIfNeeded($salesChannelContext, $createdTransactionId, $source);
// Fetch available payment method IDs from WhitelabelMachineName API for this transaction.
$allowedIds = $this->fetchAvailablePaymentMethodIds($settings, $createdTransactionId, $salesChannelContext);
// Return a new collection containing only allowed methods.
return $this->buildFilteredCollection($paymentMethodCollection, $allowedIds, $settings->getSpaceId(), $salesChannelContext);
}
/**
* Removes all WhitelabelMachineName-related payment methods from the collection.
*
* @param PaymentMethodCollection $paymentMethodCollection The collection to clean.
* @param SalesChannelContext $salesChannelContext The context.
* @return PaymentMethodCollection The cleaned collection.
*/
private function removeVRPaymentPaymentMethods(
PaymentMethodCollection $paymentMethodCollection,
SalesChannelContext $salesChannelContext
): PaymentMethodCollection {
$paymentMethodIds = $this->paymentMethodUtil->getVRPaymentPaymentMethodIds($salesChannelContext->getContext());
foreach ($paymentMethodIds as $paymentMethodId) {
$paymentMethodCollection->remove($paymentMethodId);
}
return $paymentMethodCollection;
}
/**
* Fetches the list of allowed payment method IDs from the WhitelabelMachineName API.
*
* @param mixed $settings The plugin settings.
* @param int $createdTransactionId The WhitelabelMachineName transaction ID.
* @param SalesChannelContext $salesChannelContext The context.
* @return string[] Array of allowed payment method configuration IDs.
*/
private function fetchAvailablePaymentMethodIds(
$settings,
int $createdTransactionId,
SalesChannelContext $salesChannelContext
): array {
$transactionService = $settings->getApiClient()->getTransactionService();
$possiblePaymentMethods = $transactionService->fetchPaymentMethods(
$settings->getSpaceId(),
$createdTransactionId,
$settings->getIntegration()
);
$arrayOfPossibleMethods = [];
foreach ($possiblePaymentMethods as $possiblePaymentMethod) {
$arrayOfPossibleMethods[] = (string) $possiblePaymentMethod->getId();
}
// Store the allowed IDs in context extension for later use.
$salesChannelContext->getContext()->addExtension(
'possibleMethods',
new ArrayEntity(['ids' => $arrayOfPossibleMethods])
);
return $arrayOfPossibleMethods;
}
/**
* Builds a filtered PaymentMethodCollection based on allowed IDs.
*
* Filters the original collection (which already has Shopware's availability rules applied)
* to only include WhitelabelMachineName methods that are also allowed by the API.
* Non-WhitelabelMachineName methods are kept as-is.
*
* @param PaymentMethodCollection $paymentMethodCollection Original collection (already rule-filtered by Shopware).
* @param string[] $allowedIds List of allowed configuration IDs from the WhitelabelMachineName API.
* @param int $spaceId WhitelabelMachineName space ID.
* @param SalesChannelContext $salesChannelContext The context.
* @return PaymentMethodCollection The final collection.
*/
private function buildFilteredCollection(
PaymentMethodCollection $paymentMethodCollection,
array $allowedIds,
int $spaceId,
SalesChannelContext $salesChannelContext
): PaymentMethodCollection {
// Fetch all WhitelabelMachineName payment method configurations for the space.
$paymentMethodConfigurations = $this->paymentMethodConfigurationService
->getAllPaymentMethodConfigurations($spaceId, $salesChannelContext->getContext());
// Build a map of Shopware payment method ID => configuration for methods allowed by the API.
$allowedWLConfigByPmId = [];
foreach ($paymentMethodConfigurations as $paymentMethodConfiguration) {
if ($paymentMethodConfiguration->getPaymentMethod() === null) {
continue;
}
$pmId = $paymentMethodConfiguration->getPaymentMethod()->getId();
$pmConfigId = (string) $paymentMethodConfiguration->getPaymentMethodConfigurationId();
if (
$paymentMethodConfiguration->getSpaceId() === $spaceId
&& \in_array($pmConfigId, $allowedIds, true)
) {
$allowedWLConfigByPmId[$pmId] = $paymentMethodConfiguration;
}
}
// Filter the original collection to preserve Shopware's availability rule filtering.
// Non-WLM methods pass through unchanged; WLM methods are kept only if allowed by the API.
$collection = new PaymentMethodCollection();
foreach ($paymentMethodCollection as $method) {
$isVRPaymentPM = VRPaymentPaymentHandler::class === $method->getHandlerIdentifier();
if (!$isVRPaymentPM) {
$collection->add($method);
continue;
}
if (isset($allowedWLConfigByPmId[$method->getId()])) {
$method->addExtension('vrpayment_config', $allowedWLConfigByPmId[$method->getId()]);
$collection->add($method);
}
}
$collection->sort(function ($a, $b) {
return ($a->getPosition() ?? 0) <=> ($b->getPosition() ?? 0);
});
return $collection;
}
}
@@ -0,0 +1,210 @@
<?php
declare(strict_types=1);
namespace VRPaymentPayment\Core\Checkout\Service;
use Psr\Cache\CacheItemPoolInterface;
use Shopware\Core\Framework\Struct\ArrayEntity;
use Shopware\Core\System\SalesChannel\SalesChannelContext;
use VRPaymentPayment\Core\Api\Transaction\Service\TransactionService;
use VRPaymentPayment\Core\Settings\Service\SettingsService;
use VRPayment\Sdk\Model\TransactionState;
/**
* This service manages the lifecycle of WhitelabelMachineName transactions and their state within the Shopware context.
* It provides methods to retrieve, create, and update transactions while ensuring state consistency.
*/
class TransactionManagementService
{
/**
* @var TransactionService
* The service used to interact with the WhitelabelMachineName API for transaction operations.
*/
private TransactionService $transactionService;
/**
* @var SettingsService
* The service used to retrieve configuration settings for the current sales channel.
*/
private SettingsService $settingsService;
/**
* @var CacheItemPoolInterface
* Cache for headless transaction persistence
*/
private CacheItemPoolInterface $cache;
/**
* @param TransactionService $transactionService
* @param SettingsService $settingsService
* @param CacheItemPoolInterface $cache
*/
public function __construct(
TransactionService $transactionService,
SettingsService $settingsService,
CacheItemPoolInterface $cache
) {
$this->transactionService = $transactionService;
$this->settingsService = $settingsService;
$this->cache = $cache;
}
/**
* Retrieves an existing pending transaction ID from the context or creates a new one if necessary.
*
* @param SalesChannelContext $salesChannelContext The current sales channel context.
* @param mixed $event Optional event context.
* @return int The WhitelabelMachineName transaction ID.
* @throws \Exception If settings are not configured.
*/
public function getOrCreatePendingTransaction(SalesChannelContext $salesChannelContext, $event = null): int
{
// Try to get the transaction ID from the current context state.
$transactionId = $this->getTransactionIdFromContext($salesChannelContext);
$settings = $this->settingsService->getValidSettings($salesChannelContext->getSalesChannel()->getId());
if (!$settings) {
throw new \Exception('Space settings not configured');
}
$expiredTransaction = true;
if ($transactionId) {
try {
// Verify if the transaction still exists and is in a PENDING state.
$pendingTransaction = $this->transactionService->read($transactionId, (string)$salesChannelContext->getSalesChannel()->getId());
if ($pendingTransaction->getState() === TransactionState::PENDING) {
$expiredTransaction = false;
}
} catch (\Exception $e) {
// If the transaction cannot be read, we treat it as expired or invalid.
}
}
// Create a new transaction if none exists or the existing one is no longer valid.
if (!$transactionId || $expiredTransaction) {
$transactionId = (int)$this->transactionService->createPendingTransaction($salesChannelContext, $event);
$this->storeTransactionIdInContext($salesChannelContext, $transactionId);
}
return $transactionId;
}
/**
* Updates the WhitelabelMachineName transaction if the customer context (address or currency) or line items have changed.
*
* @param SalesChannelContext $salesChannelContext The current context.
* @param int $transactionId The WhitelabelMachineName transaction ID to update.
* @param mixed $event The event that triggered this update (optional).
*/
public function updateTempTransactionIfNeeded(SalesChannelContext $salesChannelContext, int $transactionId, $event = null): void
{
$ctx = $salesChannelContext->getContext();
/** @var ArrayEntity|null $ext */
$ext = $ctx->getExtension('checkoutState');
$oldAddressHash = $ext instanceof ArrayEntity ? (string)$ext->get('addressHash') : null;
$oldCurrency = $ext instanceof ArrayEntity ? (string)$ext->get('currency') : null;
$oldLineItemHash = $ext instanceof ArrayEntity ? (string)$ext->get('lineItemHash') : null;
$customer = $salesChannelContext->getCustomer();
$addressHash = $customer ? md5(json_encode((array) $customer)) : null;
$currency = (string)$salesChannelContext->getCurrency()->getIsoCode();
$lineItems = $this->transactionService->extractLineItems(
$event,
$salesChannelContext,
);
$lineItemHash = !empty($lineItems) ? md5(json_encode($lineItems)) : $oldLineItemHash;
$needsUpdate = ($oldAddressHash !== $addressHash)
|| ($oldCurrency !== $currency)
|| ($oldLineItemHash !== $lineItemHash);
if ($needsUpdate) {
// Update the transaction in WhitelabelMachineName to reflect current cart and customer data.
if ($transactionId) {
$this->transactionService->updateTempTransaction($salesChannelContext, $transactionId, $lineItems);
}
// Clear payment method cache as options might have changed due to address/currency change.
$ctx->addExtension('possibleMethods', new ArrayEntity(['ids' => []]));
// Persist the new state hash in the context.
$ctx->addExtension(
'checkoutState',
new ArrayEntity([
'transactionId' => $transactionId,
'addressHash' => $addressHash,
'currency' => $currency,
'lineItemHash' => $lineItemHash,
])
);
}
}
/**
* Retrieves the stored WhitelabelMachineName transaction ID from the context, cache, or session.
*
* @param SalesChannelContext $salesChannelContext The context.
* @return int|null The transaction ID if found, otherwise null.
*/
public function getTransactionIdFromContext(SalesChannelContext $salesChannelContext): ?int
{
/** @var ArrayEntity|null $ext */
$ext = $salesChannelContext->getContext()->getExtension('checkoutState');
if ($ext instanceof ArrayEntity && $ext->get('transactionId')) {
return (int) $ext->get('transactionId');
}
// Try to get from cache (headless support).
$customer = $salesChannelContext->getCustomer();
if ($customer) {
$cacheKey = 'vrpn_pending_transaction_id_customer_' . $customer->getId();
$item = $this->cache->getItem($cacheKey);
if ($item->isHit()) {
return (int) $item->get();
}
}
// Fallback to PHP session for traditional Storefront compatibility.
if (isset($_SESSION['transactionId'])) {
return (int) $_SESSION['transactionId'];
}
return null;
}
/**
* Persists the transaction ID in the context state, cache, and session.
*
* @param SalesChannelContext $salesChannelContext The context.
* @param int $transactionId The transaction ID to store.
*/
private function storeTransactionIdInContext(SalesChannelContext $salesChannelContext, int $transactionId): void
{
$ctx = $salesChannelContext->getContext();
/** @var ArrayEntity|null $ext */
$ext = $ctx->getExtension('checkoutState');
$data = $ext instanceof ArrayEntity ? $ext->all() : [];
$data['transactionId'] = $transactionId;
// Store in context extension for stateless (headless) support within the request.
$ctx->addExtension('checkoutState', new ArrayEntity($data));
// Store in cache for persistent headless support.
$customer = $salesChannelContext->getCustomer();
if ($customer) {
$cacheKey = 'vrpn_pending_transaction_id_customer_' . $customer->getId();
$item = $this->cache->getItem($cacheKey);
$item->set($transactionId);
$item->expiresAfter(7200);
$this->cache->save($item);
}
// Sync with PHP session for stateful Storefront support.
$_SESSION['transactionId'] = $transactionId;
}
}
@@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
namespace VRPaymentPayment\Core\Checkout\StoreApi\Route;
use Shopware\Core\Framework\Log\Package;
use Shopware\Core\System\SalesChannel\SalesChannelContext;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Attribute\Route;
use VRPaymentPayment\Core\Api\Transaction\Service\TransactionService;
use VRPaymentPayment\Core\Checkout\Service\CartRecoveryService;
#[Package('checkout')]
#[Route(defaults: ['_routeScope' => ['store-api']])]
/**
* This Store API route allows headless clients to recreate a cart from an existing order.
* This is particularly useful for 'Try Again' scenarios if a payment fails.
*/
class CartRecoveryRoute
{
/**
* @var CartRecoveryService
* Service to handle the logic of reconstructing a cart from order items.
*/
private CartRecoveryService $cartRecoveryService;
/**
* @var TransactionService
* Service to clear and manage transactions.
*/
private TransactionService $transactionService;
/**
* @param CartRecoveryService $cartRecoveryService
* @param TransactionService $transactionService
*/
public function __construct(
CartRecoveryService $cartRecoveryService,
TransactionService $transactionService,
) {
$this->cartRecoveryService = $cartRecoveryService;
$this->transactionService = $transactionService;
}
/**
* Recreates a cart based on the provided order ID.
*
* @param string $orderId The ID of the order to recover the cart from.
* @param Request $request The incoming request.
* @param SalesChannelContext $context The current sales channel context.
* @return JsonResponse A JSON response containing either the new cart data or an error message.
*/
#[Route(
path: '/store-api/vrpayment/checkout/recreate-cart/{orderId}',
name: 'store-api.vrpayment.checkout.recreate-cart',
methods: ['POST']
)]
public function recreate(string $orderId, Request $request, SalesChannelContext $context): JsonResponse
{
try {
// Fetch the order entity.
$order = $this->cartRecoveryService->getOrderEntity($orderId, $context->getContext());
// Security check: ensure the order belongs to the current sales channel.
if ($order->getSalesChannelId() !== $context->getSalesChannelId()) {
return new JsonResponse(['error' => 'Sales channel mismatch'], 403);
}
// Clear the transaction ID from cache and session to prevent the subsequent
// checkout attempt from reusing a stale/failed transaction.
$this->transactionService->clearTransactionIdFromContext($context);
// Perform the cart reconstruction.
$cart = $this->cartRecoveryService->recreateCartFromOrder($order, $context);
// Return the reconstructed cart data.
return new JsonResponse($cart);
} catch (\Exception $e) {
// Handle any exceptions during the process.
return new JsonResponse(['error' => $e->getMessage()], 400);
}
}
}
@@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
namespace VRPaymentPayment\Core\Checkout\StoreApi\Route;
use Shopware\Core\Framework\Log\Package;
use Shopware\Core\System\SalesChannel\SalesChannelContext;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Attribute\Route;
use VRPaymentPayment\Core\Checkout\Service\InvoiceService;
#[Package('checkout')]
#[Route(defaults: ['_routeScope' => ['store-api']])]
/**
* This Store API route provides access to invoice documents for headless clients.
*/
class InvoiceRoute
{
/**
* @var InvoiceService
* Service to handle the retrieval of invoice documents from WhitelabelMachineName.
*/
private InvoiceService $invoiceService;
/**
* @param InvoiceService $invoiceService
*/
public function __construct(InvoiceService $invoiceService)
{
$this->invoiceService = $invoiceService;
}
/**
* Fetches the invoice document metadata and content for a given order.
*
* @param string $orderId The ID of the order.
* @param Request $request The incoming request.
* @param SalesChannelContext $context The current sales channel context.
* @return JsonResponse A JSON response containing the invoice title, MIME type, and base64-encoded data.
*/
#[Route(
path: '/store-api/vrpayment/account/order/invoice/{orderId}',
name: 'store-api.vrpayment.account.order.invoice',
methods: ['GET']
)]
public function load(string $orderId, Request $request, SalesChannelContext $context): JsonResponse
{
try {
// Retrieve the invoice document via the dedicated service.
$invoice = $this->invoiceService->getInvoiceDocument($orderId, $context);
// Structure the response for headless consumption.
return new JsonResponse([
'title' => (string)$invoice->getTitle(),
'mimeType' => (string)$invoice->getMimeType(),
'data' => (string)$invoice->getData(), // Base64 encoded
]);
} catch (\Exception $e) {
// Handle errors (e.g., order not found or API failure).
return new JsonResponse(['error' => $e->getMessage()], 400);
}
}
}
@@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
namespace VRPaymentPayment\Core\Checkout\StoreApi\Route;
use Shopware\Core\Framework\Log\Package;
use Shopware\Core\System\SalesChannel\SalesChannelContext;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Attribute\Route;
use VRPaymentPayment\Core\Checkout\Service\PaymentIntegrationService;
/**
* This route provides transaction-specific configuration for a given order.
* It is primarily used by headless clients to initialize the WhitelabelMachineName payment iframe or lightbox.
*/
#[Package('checkout')]
#[Route(defaults: ['_routeScope' => ['store-api']])]
class WhitelabelMachineNameTransactionInfoRoute
{
/**
* @var PaymentIntegrationService
* The service responsible for generating the unified payment configuration.
*/
private PaymentIntegrationService $paymentIntegrationService;
/**
* @param PaymentIntegrationService $paymentIntegrationService
*/
public function __construct(PaymentIntegrationService $paymentIntegrationService)
{
$this->paymentIntegrationService = $paymentIntegrationService;
}
/**
* Loads the payment configuration for a specific order.
*
* @param string $orderId The Shopware order ID.
* @param Request $request The incoming request object.
* @param SalesChannelContext $context The current sales channel context.
* @return JsonResponse JSON response containing the PaymentConfigStruct data.
*/
#[Route(
path: '/store-api/vrpayment/transaction/info/{orderId}',
name: 'store-api.vrpayment.transaction.info',
methods: ['GET']
)]
public function load(string $orderId, Request $request, SalesChannelContext $context): JsonResponse
{
try {
// Retrieve the payment configuration using the common service.
// This ensures logic parity between Storefront and Store API.
$config = $this->paymentIntegrationService->getPaymentConfig($orderId, $context);
// Return the configuration as a JSON response for the headless client.
return new JsonResponse($config);
} catch (\Exception $e) {
// In case of error, return a 400 response with the error message.
return new JsonResponse(['error' => $e->getMessage()], 400);
}
}
}
@@ -0,0 +1,205 @@
<?php
declare(strict_types=1);
namespace VRPaymentPayment\Core\Checkout\Struct;
use Shopware\Core\Framework\Struct\Struct;
/**
* This struct encapsulates all the configuration data required to initialize a WhitelabelMachineName payment integration.
*/
class PaymentConfigStruct extends Struct
{
/**
* @var string
* The type of integration (e.g., 'iframe' or 'lightbox').
*/
protected string $integration;
/**
* @var string
* The absolute URL to the WhitelabelMachineName JavaScript component for transaction handling.
*/
protected string $javascriptUrl;
/**
* @var string
* The absolute URL to the WhitelabelMachineName JavaScript component for device tracking.
*/
protected string $deviceJavascriptUrl;
/**
* @var array
* A list of possible payment methods for the current transaction.
*/
protected array $transactionPossiblePaymentMethods;
/**
* @var int
* The unique ID of the WhitelabelMachineName transaction.
*/
protected int $transactionId;
/**
* @var int
* The unique ID of the WhitelabelMachineName space.
*/
protected int $spaceId;
/**
* @var string|null
* The URL to redirect to if the cart needs to be recreated (e.g., after an error).
*/
protected ?string $cartRecreateUrl = null;
/**
* @var string|null
* The URL to the checkout confirmation page.
*/
protected ?string $checkoutUrl = null;
/**
* @return string
*/
public function getIntegration(): string
{
return $this->integration;
}
/**
* @param string $integration
* @return self
*/
public function setIntegration(string $integration): self
{
$this->integration = $integration;
return $this;
}
/**
* @return string
*/
public function getJavascriptUrl(): string
{
return $this->javascriptUrl;
}
/**
* @param string $javascriptUrl
* @return self
*/
public function setJavascriptUrl(string $javascriptUrl): self
{
$this->javascriptUrl = $javascriptUrl;
return $this;
}
/**
* @return string
*/
public function getDeviceJavascriptUrl(): string
{
return $this->deviceJavascriptUrl;
}
/**
* @param string $deviceJavascriptUrl
* @return self
*/
public function setDeviceJavascriptUrl(string $deviceJavascriptUrl): self
{
$this->deviceJavascriptUrl = $deviceJavascriptUrl;
return $this;
}
/**
* @return array
*/
public function getTransactionPossiblePaymentMethods(): array
{
return $this->transactionPossiblePaymentMethods;
}
/**
* @param array $transactionPossiblePaymentMethods
* @return self
*/
public function setTransactionPossiblePaymentMethods(array $transactionPossiblePaymentMethods): self
{
$this->transactionPossiblePaymentMethods = $transactionPossiblePaymentMethods;
return $this;
}
/**
* @return int
*/
public function getTransactionId(): int
{
return $this->transactionId;
}
/**
* @param int $transactionId
* @return self
*/
public function setTransactionId(int $transactionId): self
{
$this->transactionId = $transactionId;
return $this;
}
/**
* @return int
*/
public function getSpaceId(): int
{
return $this->spaceId;
}
/**
* @param int $spaceId
* @return self
*/
public function setSpaceId(int $spaceId): self
{
$this->spaceId = $spaceId;
return $this;
}
/**
* @return string|null
*/
public function getCartRecreateUrl(): ?string
{
return $this->cartRecreateUrl;
}
/**
* @param string|null $cartRecreateUrl
* @return self
*/
public function setCartRecreateUrl(?string $cartRecreateUrl): self
{
$this->cartRecreateUrl = $cartRecreateUrl;
return $this;
}
/**
* @return string|null
*/
public function getCheckoutUrl(): ?string
{
return $this->checkoutUrl;
}
/**
* @param string|null $checkoutUrl
* @return self
*/
public function setCheckoutUrl(?string $checkoutUrl): self
{
$this->checkoutUrl = $checkoutUrl;
return $this;
}
}
@@ -1,10 +1,12 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
namespace VRPaymentPayment\Core\Storefront\Account\Controller;
use Psr\Log\LoggerInterface;
use Shopware\Core\{
Checkout\Cart\Exception\CustomerNotLoggedInException,
Checkout\Cart\CartException,
Checkout\Customer\CustomerEntity,
PlatformRequest,
System\SalesChannel\SalesChannelContext
@@ -20,111 +22,134 @@ use Symfony\Component\{
};
use VRPaymentPayment\Core\{
Api\Transaction\Service\TransactionService,
Settings\Service\SettingsService
Checkout\Service\InvoiceService
};
#[Package('storefront')]
#[Route(defaults: ['_routeScope' => ['storefront']])]
/**
* This controller handles account-related actions for orders, specifically
* allowing customers to download their invoice documents.
*/
class AccountOrderController extends StorefrontController
{
/**
* @var \VRPaymentPayment\Core\Settings\Service\SettingsService
*/
protected $settingsService;
/**
* @var \Psr\Log\LoggerInterface
*/
protected $logger;
/**
* @var \VRPaymentPayment\Core\Api\Transaction\Service\TransactionService
*/
protected $transactionService;
/**
* @var RequestStack
*/
protected $requestStack;
/**
* AccountOrderController constructor.
* @param \VRPaymentPayment\Core\Settings\Service\SettingsService $settingsService
* @param \VRPaymentPayment\Core\Api\Transaction\Service\TransactionService $transactionService
* @param RequestStack $requestStack
*/
public function __construct(SettingsService $settingsService, TransactionService $transactionService, RequestStack $requestStack)
{
$this->settingsService = $settingsService;
$this->transactionService = $transactionService;
$this->requestStack = $requestStack;
/**
* @var LoggerInterface
*/
protected LoggerInterface $logger;
/**
* @var TransactionService
* Local transaction service for order data retrieval.
*/
protected TransactionService $transactionService;
/**
* @var RequestStack
* Symfony service to access the current request context.
*/
protected RequestStack $requestStack;
/**
* @var InvoiceService
* Service to fetch invoice documents from WhitelabelMachineName.
*/
private InvoiceService $invoiceService;
/**
* @param TransactionService $transactionService
* @param RequestStack $requestStack
* @param InvoiceService $invoiceService
*/
public function __construct(
TransactionService $transactionService,
RequestStack $requestStack,
InvoiceService $invoiceService
) {
$this->transactionService = $transactionService;
$this->requestStack = $requestStack;
$this->invoiceService = $invoiceService;
}
/**
* @param LoggerInterface $logger
*/
public function setLogger(LoggerInterface $logger): void
{
$this->logger = $logger;
}
/**
* Downloads an invoice document for a specific order.
*
* @param string $orderId The ID of the order.
* @param SalesChannelContext $salesChannelContext The context.
* @return Response The PDF document as a download response.
*/
#[Route(
path: "/vrpayment/account/order/download/invoice/document/{orderId}",
name: "frontend.vrpayment.account.order.download.invoice.document",
methods: ['GET']
)]
public function downloadInvoiceDocument(string $orderId, SalesChannelContext $salesChannelContext): Response
{
try {
// Ensure the user is logged in.
$customer = $this->getLoggedInCustomer();
// Fetch the transaction entity to verify ownership.
$transactionEntity = $this->transactionService->getByOrderId($orderId, $salesChannelContext->getContext());
// Security check: ensure the document belongs to the logged-in customer.
if (strcasecmp((string)$customer->getCustomerNumber(), (string)$transactionEntity->getData()['customerId']) != 0) {
throw new AccessDeniedException();
}
// Retrieve the invoice document metadata and content.
/** @var object $invoiceDocument */
$invoiceDocument = $this->invoiceService->getInvoiceDocument($orderId, $salesChannelContext);
// Sanitize the filename for the download.
$filename = preg_replace('/[\x00-\x1F\x7F-\xFF]/', '_', (string)$invoiceDocument->getTitle()) . '.pdf';
$disposition = HeaderUtils::makeDisposition(
HeaderUtils::DISPOSITION_ATTACHMENT,
$filename,
$filename
);
// Create the response with the PDF content (base64 decoded).
$response = new Response(base64_decode((string)$invoiceDocument->getData()));
$response->headers->set('Content-Type', (string)$invoiceDocument->getMimeType());
$response->headers->set('Content-Disposition', $disposition);
return $response;
} catch (\Exception $e) {
$this->logger->error($e->getMessage());
return $this->redirectToRoute('frontend.home.page');
}
/**
* @param \Psr\Log\LoggerInterface $logger
* @internal
* @required
*
*/
public function setLogger(LoggerInterface $logger): void
{
$this->logger = $logger;
}
/**
* Helper to retrieve the currently logged-in customer.
*
* @return CustomerEntity
* @throws CartException
*/
protected function getLoggedInCustomer(): CustomerEntity
{
$request = $this->requestStack->getCurrentRequest();
if (!$request) {
throw CartException::customerNotLoggedIn();
}
/**
* Download invoice document
*
* @param string $orderId
* @param \Shopware\Core\System\SalesChannel\SalesChannelContext $salesChannelContext
* @return \Symfony\Component\HttpFoundation\Response
*
* @throws \VRPayment\Sdk\ApiException
* @throws \VRPayment\Sdk\Http\ConnectionException
* @throws \VRPayment\Sdk\VersioningException
*/
#[Route("/vrpayment/account/order/download/invoice/document/{orderId}",
name: "frontend.vrpayment.account.order.download.invoice.document",
methods: ['GET'])]
public function downloadInvoiceDocument(string $orderId, SalesChannelContext $salesChannelContext): Response
{
$customer = $this->getLoggedInCustomer();
$settings = $this->settingsService->getSettings($salesChannelContext->getSalesChannel()->getId());
$transactionEntity = $this->transactionService->getByOrderId($orderId, $salesChannelContext->getContext());
if (strcasecmp($customer->getCustomerNumber(), $transactionEntity->getData()['customerId']) != 0) {
throw new AccessDeniedException();
}
$invoiceDocument = $settings->getApiClient()->getTransactionService()->getInvoiceDocument($settings->getSpaceId(), $transactionEntity->getTransactionId());
$forceDownload = true;
$filename = preg_replace('/[\x00-\x1F\x7F-\xFF]/', '_', $invoiceDocument->getTitle()) . '.pdf';
$disposition = HeaderUtils::makeDisposition(
$forceDownload ? HeaderUtils::DISPOSITION_ATTACHMENT : HeaderUtils::DISPOSITION_INLINE,
$filename,
$filename
);
$response = new Response(base64_decode($invoiceDocument->getData()));
$response->headers->set('Content-Type', $invoiceDocument->getMimeType());
$response->headers->set('Content-Disposition', $disposition);
return $response;
}
/**
* @return CustomerEntity
*/
protected function getLoggedInCustomer(): CustomerEntity
{
$request = $this->requestStack->getCurrentRequest();
if (!$request) {
throw new CustomerNotLoggedInException();
}
$context = $request->attributes->get(PlatformRequest::ATTRIBUTE_SALES_CHANNEL_CONTEXT_OBJECT);
if ($context && $context->getCustomer() && $context->getCustomer()->getGuest() === false) {
return $context->getCustomer();
}
throw new CustomerNotLoggedInException();
/** @var SalesChannelContext|null $context */
$context = $request->attributes->get(PlatformRequest::ATTRIBUTE_SALES_CHANNEL_CONTEXT_OBJECT);
if ($context && $context->getCustomer() && $context->getCustomer()->getGuest() === false) {
return $context->getCustomer();
}
throw CartException::customerNotLoggedIn();
}
}
@@ -1,568 +1,241 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
namespace VRPaymentPayment\Core\Storefront\Checkout\Controller;
use VRPayment\Sdk\Model\TransactionState as SdkTransactionState;
use VRPaymentPayment\Core\Api\Transaction\Service\TransactionService;
use Psr\Log\LoggerInterface;
use Shopware\Core\{
Checkout\Cart\Cart,
Checkout\Cart\CartException,
Checkout\Cart\LineItemFactoryRegistry,
Checkout\Cart\SalesChannel\CartService,
Checkout\Order\Aggregate\OrderLineItem\OrderLineItemCollection,
Checkout\Order\Aggregate\OrderLineItem\OrderLineItemEntity,
Checkout\Order\OrderEntity,
Checkout\Order\SalesChannel\AbstractOrderRoute,
Framework\Context,
Framework\DataAbstractionLayer\Search\Criteria,
Framework\DataAbstractionLayer\Search\Filter\EqualsFilter,
Framework\DataAbstractionLayer\Search\Sorting\FieldSorting,
Checkout\Cart\CartException,
Checkout\Order\SalesChannel\AbstractOrderRoute,
Framework\Log\Package,
Framework\Routing\Exception\MissingRequestParameterException,
Framework\Uuid\Uuid,
Framework\Uuid\Exception\InvalidUuidException,
Framework\Validation\DataBag\RequestDataBag,
System\SalesChannel\SalesChannelContext
Framework\Routing\RoutingException,
Framework\DataAbstractionLayer\Search\Criteria,
System\SalesChannel\SalesChannelContext
};
use Shopware\Storefront\{
Controller\StorefrontController,
Page\Checkout\Finish\CheckoutFinishPage,
Page\GenericPageLoaderInterface
Controller\StorefrontController,
Page\Checkout\Finish\CheckoutFinishPage,
Page\GenericPageLoaderInterface
};
use Symfony\Component\{
HttpFoundation\Request,
HttpFoundation\Response,
Routing\Attribute\Route,
Routing\Generator\UrlGeneratorInterface
};
use VRPayment\Sdk\{
Model\Transaction,
Model\TransactionState
HttpFoundation\Request,
HttpFoundation\Response,
Routing\Attribute\Route
};
use VRPaymentPayment\Core\{
Api\Transaction\Service\TransactionService,
Settings\Options\Integration,
Settings\Service\SettingsService,
Storefront\Checkout\Struct\CheckoutPageData,
Util\Payload\CustomProducts\CustomProductsLineItemTypes
Checkout\Service\CartRecoveryService,
Checkout\Service\PaymentIntegrationService,
Settings\Service\SettingsService
};
/**
* Class CheckoutController
*
* @package VRPaymentPayment\Core\Storefront\Checkout\Controller
*
*/
#[Package('checkout')]
#[Route(defaults: ['_routeScope' => ['storefront']])]
class CheckoutController extends StorefrontController {
/**
* This controller handles Storefront-specific actions for the WhitelabelMachineName integration,
* such as rendering the payment page and recreating a cart from a failed order.
*/
class CheckoutController extends StorefrontController
{
/**
* @var GenericPageLoaderInterface
* Loader for basic Shopware page data.
*/
protected GenericPageLoaderInterface $genericLoader;
/**
* @var \Shopware\Storefront\Page\GenericPageLoader
*/
protected $genericLoader;
/**
* @var SettingsService
* Plugin settings service.
*/
protected SettingsService $settingsService;
/**
* @var \Shopware\Core\Checkout\Cart\SalesChannel\CartService
*/
protected $cartService;
/**
* @var LoggerInterface|null
* Logger for recording errors and important information.
*/
private ?LoggerInterface $logger = null;
/**
* @var \VRPaymentPayment\Core\Settings\Service\SettingsService
*/
protected $settingsService;
/**
* @var AbstractOrderRoute
* Shopware service for order retrieval.
*/
private AbstractOrderRoute $orderRoute;
/**
* @var \VRPaymentPayment\Core\Settings\Struct\Settings
*/
protected $settings;
/**
* @var CartRecoveryService
* Service to help customers recover their cart from a past order.
*/
private CartRecoveryService $cartRecoveryService;
/**
* @var \VRPaymentPayment\Core\Api\Transaction\Service\TransactionService
*/
protected $transactionService;
/**
* @var PaymentIntegrationService
* Service to provide the integration parameters (JS URL, transaction ID, etc.).
*/
private PaymentIntegrationService $paymentIntegrationService;
/**
* @var \Psr\Log\LoggerInterface
*/
private $logger;
/**
* @var TransactionService
* Service to check transaction details.
*/
private TransactionService $transactionService;
/**
* @var \Shopware\Core\Checkout\Cart\LineItemFactoryRegistry
*/
private $lineItemFactoryRegistry;
/**
* @param SettingsService $settingsService
* @param GenericPageLoaderInterface $genericLoader
* @param AbstractOrderRoute $orderRoute
* @param CartRecoveryService $cartRecoveryService
* @param PaymentIntegrationService $paymentIntegrationService
*/
public function __construct(
SettingsService $settingsService,
GenericPageLoaderInterface $genericLoader,
AbstractOrderRoute $orderRoute,
CartRecoveryService $cartRecoveryService,
PaymentIntegrationService $paymentIntegrationService,
TransactionService $transactionService
) {
$this->genericLoader = $genericLoader;
$this->settingsService = $settingsService;
$this->orderRoute = $orderRoute;
$this->cartRecoveryService = $cartRecoveryService;
$this->paymentIntegrationService = $paymentIntegrationService;
$this->transactionService = $transactionService;
}
/**
* @var \Shopware\Core\Checkout\Order\SalesChannel\AbstractOrderRoute
*/
private $orderRoute;
/**
* @param LoggerInterface $logger
*/
public function setLogger(LoggerInterface $logger): void
{
$this->logger = $logger;
}
/**
* PaymentController constructor.
*
* @param \Shopware\Core\Checkout\Cart\LineItemFactoryRegistry $lineItemFactoryRegistry
* @param \Shopware\Core\Checkout\Cart\SalesChannel\CartService $cartService
* @param \VRPaymentPayment\Core\Settings\Service\SettingsService $settingsService
* @param \VRPaymentPayment\Core\Api\Transaction\Service\TransactionService $transactionService
* @param \Shopware\Storefront\Page\GenericPageLoaderInterface $genericLoader
* @param \Shopware\Core\Checkout\Order\SalesChannel\AbstractOrderRoute $orderRoute
*/
public function __construct(
LineItemFactoryRegistry $lineItemFactoryRegistry,
CartService $cartService,
SettingsService $settingsService,
TransactionService $transactionService,
GenericPageLoaderInterface $genericLoader,
AbstractOrderRoute $orderRoute
)
{
$this->cartService = $cartService;
$this->genericLoader = $genericLoader;
$this->settingsService = $settingsService;
$this->transactionService = $transactionService;
$this->lineItemFactoryRegistry = $lineItemFactoryRegistry;
$this->orderRoute = $orderRoute;
}
/**
* @param \Psr\Log\LoggerInterface $logger
*
* @internal
* @required
*
*/
public function setLogger(LoggerInterface $logger): void
{
$this->logger = $logger;
}
/**
* @param \Shopware\Core\System\SalesChannel\SalesChannelContext $salesChannelContext
* @param \Symfony\Component\HttpFoundation\Request $request
*
* @return \Symfony\Component\HttpFoundation\Response
* @throws \VRPayment\Sdk\ApiException
* @throws \VRPayment\Sdk\Http\ConnectionException
* @throws \VRPayment\Sdk\VersioningException
*
*/
/**
* Renders the WhitelabelMachineName payment page (usually contains the iframe or lightbox script).
*
* @param SalesChannelContext $salesChannelContext The current context.
* @param Request $request The incoming request.
* @return Response The rendered payment page.
*/
#[Route(
path: "/vrpayment/checkout/pay",
name: "frontend.vrpayment.checkout.pay",
options: ["seo" => false],
methods: ["GET"],
)]
public function pay(SalesChannelContext $salesChannelContext, Request $request): Response
{
$orderId = $request->query->get('orderId');
public function pay(SalesChannelContext $salesChannelContext, Request $request): Response
{
$orderId = (string)$request->query->get('orderId');
if (empty($orderId)) {
throw new MissingRequestParameterException('orderId');
}
if (empty($orderId)) {
throw RoutingException::missingRequestParameter('orderId');
}
// Configuration
$this->settings = $this->settingsService->getSettings($salesChannelContext->getSalesChannel()->getId());
try {
// Load the order with necessary associations for the product table and addresses.
$criteria = new Criteria([$orderId]);
$criteria->addAssociation('lineItems.product')
->addAssociation('deliveries.shippingOrderAddress.country')
->addAssociation('orderCustomer.customer')
->addAssociation('transactions.paymentMethod');
$transaction = $this->getTransaction($orderId, $salesChannelContext->getContext());
$recreateCartUrl = $this->generateUrl(
'frontend.vrpayment.checkout.recreate-cart',
['orderId' => $orderId,],
UrlGeneratorInterface::ABSOLUTE_URL
);
$order = $this->orderRoute->load(new Request(), $salesChannelContext, $criteria)->getOrders()->first();
if (in_array(
$transaction->getState(),
[
TransactionState::AUTHORIZED,
TransactionState::COMPLETED,
TransactionState::FULFILL,
]
)) {
return $this->redirect($transaction->getSuccessUrl(), Response::HTTP_MOVED_PERMANENTLY);
} else {
if (in_array(
$transaction->getState(),
[
TransactionState::DECLINE,
TransactionState::FAILED,
TransactionState::VOIDED,
]
)) {
return $this->redirect($transaction->getFailedUrl(), Response::HTTP_MOVED_PERMANENTLY);
}
}
if (!$order) {
throw RoutingException::missingRequestParameter('orderId');
}
$possiblePaymentMethods = $this->settings->getApiClient()
->getTransactionService()
->fetchPaymentMethods(
$this->settings->getSpaceId(),
$transaction->getId(),
$this->settings->getIntegration()
);
// Fetch the configuration required for the frontend integration.
$paymentConfig = $this->paymentIntegrationService->getPaymentConfig($orderId, $salesChannelContext);
if (empty($possiblePaymentMethods)) {
$this->addFlash('danger', $this->trans('vrpayment.paymentMethod.notAvailable'));
return $this->redirect($recreateCartUrl, Response::HTTP_MOVED_PERMANENTLY);
}
// Load a generic Shopware page to have layout headers/footers.
$page = $this->genericLoader->load($request, $salesChannelContext);
$page->addExtension('vRPaymentData', $paymentConfig);
$javascriptUrl = $this->getTransactionJavaScriptUrl($transaction->getId());
// Assign the order to the page so the templates can access page.order.
$page->assign(['order' => $order]);
// Set Checkout Page Data
$checkoutPageData = (new CheckoutPageData())
->setIntegration($this->settings->getIntegration())
->setJavascriptUrl($javascriptUrl)
->setDeviceJavascriptUrl($this->settings->getSpaceId(), Uuid::randomHex())
->setTransactionPossiblePaymentMethods($possiblePaymentMethods)
->setCheckoutUrl($this->generateUrl(
'frontend.vrpayment.checkout.pay',
['orderId' => $orderId,],
UrlGeneratorInterface::ABSOLUTE_URL
))
->setCartRecreateUrl($recreateCartUrl);
$page = $this->load($request, $salesChannelContext);
$page->addExtension('vRPaymentData', $checkoutPageData);
// Render the specialized Twig template for WhitelabelMachineName.
return $this->renderStorefront(
'@VRPaymentPayment/storefront/page/checkout/order/vrpayment.html.twig',
['page' => $page]
);
} catch (\Exception $e) {
if ($this->logger) {
$this->logger->error($e->getMessage());
}
// Clear the transaction ID from the cache/session context on error to ensure
// subsequent payment attempts will create/retrieve a clean transaction.
$this->transactionService->clearTransactionIdFromContext($salesChannelContext);
$this->addFlash('danger', $this->trans('vrpayment.paymentMethod.notAvailable'));
return $this->redirectToRoute('frontend.home.page');
}
}
return $this->renderStorefront(
'@VRPaymentPayment/storefront/page/checkout/order/vrpayment.html.twig',
['page' => $page]
);
}
/**
* Get transaction Javascript URL
*
* @param int $transactionId
*
* @return string
* @throws \VRPayment\Sdk\ApiException
* @throws \VRPayment\Sdk\Http\ConnectionException
* @throws \VRPayment\Sdk\VersioningException
*/
private function getTransactionJavaScriptUrl(int $transactionId): string
{
$javascriptUrl = '';
switch ($this->settings->getIntegration()) {
case Integration::IFRAME:
$javascriptUrl = $this->settings->getApiClient()->getTransactionIframeService()
->javascriptUrl($this->settings->getSpaceId(), $transactionId);
break;
case Integration::LIGHTBOX:
$javascriptUrl = $this->settings->getApiClient()->getTransactionLightboxService()
->javascriptUrl($this->settings->getSpaceId(), $transactionId);
break;
default:
$this->logger->critical(strtr('invalid integration : :integration', [':integration' => $this->settings->getIntegration()]));
}
return $javascriptUrl;
}
/**
* @param $orderId
* @param \Shopware\Core\Framework\Context $context
*
* @return \VRPayment\Sdk\Model\Transaction
* @throws \VRPayment\Sdk\ApiException
* @throws \VRPayment\Sdk\Http\ConnectionException
* @throws \VRPayment\Sdk\VersioningException
*/
private function getTransaction($orderId, Context $context): Transaction
{
$transactionEntity = $this->transactionService->getByOrderId($orderId, $context);
return $this->settings->getApiClient()->getTransactionService()->read($this->settings->getSpaceId(), $transactionEntity->getTransactionId());
}
/**
* @param \Symfony\Component\HttpFoundation\Request $request
* @param \Shopware\Core\System\SalesChannel\SalesChannelContext $salesChannelContext
*
* @return \Shopware\Storefront\Page\Checkout\Finish\CheckoutFinishPage
*/
protected function load(Request $request, SalesChannelContext $salesChannelContext): CheckoutFinishPage
{
$page = CheckoutFinishPage::createFrom($this->genericLoader->load($request, $salesChannelContext));
$page->setOrder($this->getOrder($request, $salesChannelContext));
return $page;
}
/**
* @param \Symfony\Component\HttpFoundation\Request $request
* @param \Shopware\Core\System\SalesChannel\SalesChannelContext $salesChannelContext
*
* @return \Shopware\Core\Checkout\Order\OrderEntity
*/
private function getOrder(Request $request, SalesChannelContext $salesChannelContext): OrderEntity
{
$orderId = $request->get('orderId');
if (!$orderId) {
throw new MissingRequestParameterException('orderId', '/orderId');
}
$criteria = (new Criteria([$orderId]))
->addAssociation('lineItems.cover')
->addAssociation('transactions.paymentMethod')
->addAssociation('deliveries.shippingMethod');
$customer = $salesChannelContext->getCustomer();
if ($customer !== null) {
$criteria = $criteria->addFilter(new EqualsFilter('order.orderCustomer.customerId', $customer->getId()));
}
$criteria->getAssociation('transactions')->addSorting(new FieldSorting('createdAt'));
try {
$searchResult = $this->orderRoute
->load(new Request(), $salesChannelContext, $criteria)
->getOrders();
} catch (InvalidUuidException $e) {
throw CartException::orderNotFound($orderId);
}
/** @var OrderEntity|null $order */
$order = $searchResult->get($orderId);
if (!$order) {
throw CartException::orderNotFound($orderId);
}
return $order;
}
/**
* Recreate Cart
*
* @param \Symfony\Component\HttpFoundation\Request $request
* @param \Shopware\Core\System\SalesChannel\SalesChannelContext $salesChannelContext
*
* @return \Symfony\Component\HttpFoundation\Response
*
*/
/**
* Redirects the user to a route that recreates their cart from an existing order.
* This is useful for allowing users to try payment again with different details.
*
* @param Request $request The incoming request.
* @param SalesChannelContext $salesChannelContext The context.
* @return Response Redirect to the checkout confirmation page.
*/
#[Route(
path: "/vrpayment/checkout/recreate-cart",
name: "frontend.vrpayment.checkout.recreate-cart",
options: ["seo" => false],
methods: ["GET"],
)]
public function recreateCart(Request $request, SalesChannelContext $salesChannelContext)
{
$orderId = $request->query->get('orderId');
public function recreateCart(Request $request, SalesChannelContext $salesChannelContext): Response
{
$orderId = (string)$request->query->get('orderId');
if (empty($orderId)) {
throw new MissingRequestParameterException('orderId');
}
if (empty($orderId)) {
throw RoutingException::missingRequestParameter('orderId');
}
// Adoption for Headless Storefronts
$orderRepo = $this->container->get('order.repository');
$criteria = new Criteria([$orderId]);
try {
// Find the order that should be recovered.
$order = $this->cartRecoveryService->getOrderEntity($orderId, $salesChannelContext->getContext());
$orderEntity = $orderRepo->search($criteria, $salesChannelContext->getContext())->first();
// Security: Order must belong to the active sales channel.
if ($order->getSalesChannelId() !== $salesChannelContext->getSalesChannelId()) {
return $this->redirectToRoute('frontend.home.page');
}
if($orderEntity->getSalesChannelId() !== $salesChannelContext->getSalesChannelId()) {
$this->settings = $this->settingsService->getSettings($orderEntity->getSalesChannelId());
$trans = $this->getTransaction($orderId, $salesChannelContext->getContext());
return $this->redirect($trans->getSuccessUrl());
}
// End Adoption for Headless Storefronts
// Perform the recovery process.
$transactionEntity = $this->transactionService->getByOrderId($order->getId(), $salesChannelContext->getContext());
if ($transactionEntity) {
$transaction = $this->transactionService->read($transactionEntity->getTransactionId(), $salesChannelContext->getSalesChannelId());
if (in_array($transaction->getState(), [
SdkTransactionState::AUTHORIZED,
SdkTransactionState::CONFIRMED,
SdkTransactionState::FULFILL
])) {
return $this->redirectToRoute('frontend.checkout.finish.page', ['orderId' => $orderId]);
}
try {
$this->cartService->deleteCart($salesChannelContext);
$cart = $this->cartService->createNew($salesChannelContext->getToken());
if ($transaction->getUserFailureMessage()) {
$this->addFlash('danger', $transaction->getUserFailureMessage());
}
}
// Clear the transaction ID from cache and session to prevent the subsequent
// checkout attempt from reusing a stale/failed transaction.
$this->transactionService->clearTransactionIdFromContext($salesChannelContext);
// Configuration
$this->settings = $this->settingsService->getSettings($salesChannelContext->getSalesChannel()->getId());
$orderEntity = $this->getOrder($request, $salesChannelContext);
$lastTransaction = $orderEntity->getTransactions()->last();
if ($lastTransaction && !$lastTransaction->getPaymentMethod()->getAfterOrderEnabled()) {
return $this->redirectToRoute('frontend.home.page');
}
$this->cartRecoveryService->recreateCartFromOrder($order, $salesChannelContext);
} catch (\Exception $exception) {
$this->addFlash('danger', $this->trans('error.addToCartError'));
if ($this->logger) {
$this->logger->critical($exception->getMessage());
}
return $this->redirectToRoute('frontend.home.page');
}
$transaction = $this->getTransaction($orderId, $salesChannelContext->getContext());
if (!empty($transaction->getUserFailureMessage())) {
$this->addFlash('danger', $transaction->getUserFailureMessage());
}
$orderItems = $orderEntity->getLineItems();
$hasCustomProducts = $this->hasCustomProducts($orderItems);
if ($hasCustomProducts === true) {
$cart = $this->addCustomProducts($orderItems, $request, $salesChannelContext);
}
foreach ($orderItems as $orderLineItemEntity) {
$type = $orderLineItemEntity->getType();
if ($type !== CustomProductsLineItemTypes::LINE_ITEM_TYPE_PRODUCT || $orderLineItemEntity->getParentid() !== null) {
continue;
}
$lineItem = $this->lineItemFactoryRegistry->create([
'id' => $orderLineItemEntity->getId(),
'quantity' => $orderLineItemEntity->getQuantity(),
'referencedId' => $orderLineItemEntity->getReferencedId(),
'type' => $type,
], $salesChannelContext);
$lineItemPayload = $orderLineItemEntity->getPayload();
if (!empty($lineItemPayload)) {
$lineItem->setPayload($lineItemPayload);
}
$cart = $this->cartService->add($cart, $lineItem, $salesChannelContext);
}
} catch (\Exception $exception) {
$this->addFlash('danger', $this->trans('error.addToCartError'));
$this->logger->critical($exception->getMessage());
return $this->redirectToRoute('frontend.home.page');
}
return $this->redirectToRoute('frontend.checkout.confirm.page');
}
/**
* @param OrderLineItemCollection $orderItems
*
* @return bool
*/
private function hasCustomProducts(OrderLineItemCollection $orderItems): bool
{
foreach ($orderItems as $orderItem) {
if ($orderItem->getType() === CustomProductsLineItemTypes::LINE_ITEM_TYPE_CUSTOMIZED_PRODUCTS) {
return true;
}
}
return false;
}
/**
* @param OrderLineItemCollection $orderItems
* @param string $parentId
*
* @return OrderLineItemEntity|null
*/
private function getCustomProduct(OrderLineItemCollection $orderItems, string $parentId): ?OrderLineItemEntity
{
foreach ($orderItems as $orderItem) {
if ($orderItem->getType() === CustomProductsLineItemTypes::LINE_ITEM_TYPE_PRODUCT && $orderItem->getParentId() === $parentId) {
return $orderItem;
}
}
return null;
}
/**
* @param OrderLineItemCollection $orderItems
* @param string $parentId
*
* @return array
*/
private function getCustomProductOptions(OrderLineItemCollection $orderItems, string $parentId): array
{
$options = [];
foreach ($orderItems as $orderItem) {
if ($orderItem->getType() === CustomProductsLineItemTypes::LINE_ITEM_TYPE_CUSTOMIZED_PRODUCTS_OPTION && $orderItem->getParentId() === $parentId) {
$options[] = $orderItem;
}
}
return $options;
}
/**
* @param $orderItems
* @param $request
* @param $salesChannelContext
*
* @return Cart
*/
private function addCustomProducts(OrderLineItemCollection $orderItems, Request $request, SalesChannelContext $salesChannelContext): Cart
{
$cart = $this->cartService->getCart($salesChannelContext->getToken(), $salesChannelContext);
if (!\class_exists('Swag\\CustomizedProducts\\Core\\Checkout\\Cart\\Route\\AddCustomizedProductsToCartRoute')) {
return $cart;
}
$customProductsService = $this->get('Swag\CustomizedProducts\Core\Checkout\Cart\Route\AddCustomizedProductsToCartRoute');
foreach ($orderItems as $orderItem) {
if ($orderItem->getType() !== CustomProductsLineItemTypes::LINE_ITEM_TYPE_CUSTOMIZED_PRODUCTS) {
continue;
}
$product = $this->getCustomProduct($orderItems, $orderItem->getId());
$productOptions = $this->getCustomProductOptions($orderItems, $orderItem->getId());
$optionValues = $this->getOptionValues($productOptions);
$params = new RequestDataBag([
'customized-products-template' => new RequestDataBag([
'id' => $orderItem->getReferencedId(),
'options' => new RequestDataBag($optionValues),
]),
]);
$request->request->add(
[
'lineItems' =>
[
$product->getProductId() =>
[
'quantity' => $orderItem->getQuantity(),
'id' => $product->getProductId(),
'type' => CustomProductsLineItemTypes::LINE_ITEM_TYPE_PRODUCT,
'referencedId' => $product->getReferencedId(),
'stackable' => $orderItem->getStackable(),
'removable' => $orderItem->getRemovable(),
]
]
]
);
$customProductsService->add($params, $request, $salesChannelContext, $cart);
$cart = $this->cartService->getCart($salesChannelContext->getToken(), $salesChannelContext);
}
return $cart;
}
/**
* @param array $productOptions
*
* @return array
*/
private function getOptionValues(array $productOptions): array
{
$optionValues = [];
foreach ($productOptions as $productOption) {
$optionType = $productOption->getPayload()['type'] ?: '';
switch ($optionType) {
case CustomProductsLineItemTypes::PRODUCT_OPTION_TYPE_IMAGE_UPLOAD:
case CustomProductsLineItemTypes::PRODUCT_OPTION_TYPE_FILE_UPLOAD:
$media = $productOption->getPayload()['media'] ?: [];
foreach ($media as $mediaItem) {
$optionValues[$productOption->getReferencedId()] = new RequestDataBag([
'media' => new RequestDataBag([
$mediaItem['filename'] => new RequestDataBag([
'id' => $mediaItem['mediaId'],
'filename' => $mediaItem['filename'],
]),
]),
]);
}
break;
default:
$optionValues[$productOption->getReferencedId()] = new RequestDataBag([
'value' => $productOption->getPayload()['value'] ?: '',
]);
}
}
return $optionValues;
}
// Send the user back to the checkout confirm page with their items restored.
return $this->redirectToRoute('frontend.checkout.confirm.page');
}
}
@@ -1,92 +1,70 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
namespace VRPaymentPayment\Core\Storefront\Checkout\Subscriber;
use Psr\Log\LoggerInterface;
use Shopware\Core\{Checkout\Order\Aggregate\OrderTransaction\OrderTransactionCollection,
use Shopware\Core\{
Checkout\Order\Aggregate\OrderTransaction\OrderTransactionCollection,
Checkout\Order\Aggregate\OrderTransaction\OrderTransactionStates,
Checkout\Order\OrderEntity,
Content\MailTemplate\Service\Event\MailBeforeValidateEvent};
Content\MailTemplate\Service\Event\MailBeforeValidateEvent
};
use Shopware\Core\Checkout\Payment\PaymentMethodCollection;
use Shopware\Core\System\SalesChannel\SalesChannelContext;
use Shopware\Storefront\Page\Account\Order\AccountEditOrderPageLoadedEvent;
use Shopware\Storefront\Page\Account\PaymentMethod\AccountPaymentMethodPageLoadedEvent;
use Shopware\Storefront\Page\Checkout\Confirm\CheckoutConfirmPageLoadedEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use VRPaymentPayment\Core\{Api\Transaction\Service\OrderMailService,
Api\Transaction\Service\TransactionService,
Checkout\PaymentHandler\VRPaymentPaymentHandler,
Settings\Service\SettingsService,
Settings\Struct\Settings,
Util\PaymentMethodUtil};
use VRPaymentPayment\Core\Api\PaymentMethodConfiguration\Service\PaymentMethodConfigurationService;
use VRPaymentPayment\Sdk\{Model\AddressCreate,
Model\ChargeAttempt,
Model\CreationEntityState,
Model\CriteriaOperator,
Model\EntityQuery,
Model\EntityQueryFilter,
Model\EntityQueryFilterType,
Model\LineItemAttributeCreate,
Model\LineItemCreate,
Model\LineItemType,
Model\TaxCreate,
Model\Transaction,
Model\TransactionCreate,
Model\TransactionPending};
use VRPaymentPayment\Core\Checkout\PaymentHandler\VRPaymentPaymentHandler;
use VRPaymentPayment\Core\Settings\Service\SettingsService;
use VRPaymentPayment\Core\Checkout\Service\PaymentMethodFilterService;
use VRPaymentPayment\Core\Checkout\Service\PaymentIntegrationService;
/**
* Class CheckoutSubscriber
*
* @package VRPaymentPayment\Storefront\Checkout\Subscriber
* This subscriber listens to page load events in the Storefront to filter out
* WhitelabelMachineName payment methods that are not applicable for the current cart or customer.
*/
class CheckoutSubscriber implements EventSubscriberInterface
{
/**
* @var LoggerInterface
*/
protected LoggerInterface $logger;
/**
* @var \Psr\Log\LoggerInterface
* @var SettingsService
*/
protected $logger;
private SettingsService $settingsService;
/**
* @var \VRPaymentPayment\Core\Api\PaymentMethodConfiguration\Service\PaymentMethodConfigurationService
* @var PaymentMethodFilterService
*/
private $paymentMethodConfigurationService;
private PaymentMethodFilterService $paymentMethodFilterService;
/**
* @var \VRPaymentPayment\Core\Api\Transaction\Service\TransactionService
* @var PaymentIntegrationService
*/
private $transactionService;
private PaymentIntegrationService $paymentIntegrationService;
/**
* @var \VRPaymentPayment\Core\Settings\Service\SettingsService
* @param SettingsService $settingsService
* @param PaymentMethodFilterService $paymentMethodFilterService
* @param PaymentIntegrationService $paymentIntegrationService
*/
private $settingsService;
/**
* @var \VRPaymentPayment\Core\Util\PaymentMethodUtil
*/
private $paymentMethodUtil;
/**
* CheckoutSubscriber constructor.
*
* @param \VRPaymentPayment\Core\Api\PaymentMethodConfiguration\Service\PaymentMethodConfigurationService $paymentMethodConfigurationService
* @param \VRPaymentPayment\Core\Api\Transaction\Service\TransactionService $transactionService
* @param \VRPaymentPayment\Core\Settings\Service\SettingsService $settingsService
* @param \VRPaymentPayment\Core\Util\PaymentMethodUtil $paymentMethodUtil
*/
public function __construct(PaymentMethodConfigurationService $paymentMethodConfigurationService, TransactionService $transactionService, SettingsService $settingsService, PaymentMethodUtil $paymentMethodUtil)
{
$this->paymentMethodConfigurationService = $paymentMethodConfigurationService;
$this->transactionService = $transactionService;
public function __construct(
SettingsService $settingsService,
PaymentMethodFilterService $paymentMethodFilterService,
PaymentIntegrationService $paymentIntegrationService
) {
$this->settingsService = $settingsService;
$this->paymentMethodUtil = $paymentMethodUtil;
$this->paymentMethodFilterService = $paymentMethodFilterService;
$this->paymentIntegrationService = $paymentIntegrationService;
}
/**
* @param \Psr\Log\LoggerInterface $logger
*
* @internal
* @required
*
* @param LoggerInterface $logger
*/
public function setLogger(LoggerInterface $logger): void
{
@@ -94,36 +72,75 @@ class CheckoutSubscriber implements EventSubscriberInterface
}
/**
* Register events to listen to.
*
* @return array
*/
public static function getSubscribedEvents(): array
{
return [
CheckoutConfirmPageLoadedEvent::class => ['onConfirmPageLoaded', 1],
"subscription." . CheckoutConfirmPageLoadedEvent::class => ['onConfirmPageLoaded', 1],
CheckoutConfirmPageLoadedEvent::class => 'onPageLoaded',
AccountEditOrderPageLoadedEvent::class => 'onPageLoaded',
AccountPaymentMethodPageLoadedEvent::class => 'onPageLoaded',
"subscription." . CheckoutConfirmPageLoadedEvent::class => ['onPageLoaded', 1],
MailBeforeValidateEvent::class => ['onMailBeforeValidate', 1],
];
}
/**
* Stop order emails being sent out
* Handles filtering of payment methods when a relevant page is loaded.
*
* @param \Shopware\Core\Content\MailTemplate\Service\Event\MailBeforeValidateEvent $event
* @param mixed $event The page loaded event.
*/
public function onPageLoaded($event): void
{
try {
$salesChannelContext = $event->getSalesChannelContext();
// Access the payment methods available for the current page.
$paymentMethodCollection = $event->getPage()->getPaymentMethods();
// Delegate filtering to the centralized service.
$filteredCollection = $this->paymentMethodFilterService->filterPaymentMethods(
$paymentMethodCollection,
$salesChannelContext,
$event
);
// Update the page with the filtered list.
$event->getPage()->setPaymentMethods($filteredCollection);
// If we are on a checkout or account page and have a pending transaction, provide integration data.
$transactionId = $salesChannelContext->getContext()->getExtension('vrpayment_transaction_id');
if ($transactionId) {
$paymentConfig = $this->paymentIntegrationService->getConfigForTransaction(
(int) $transactionId->getVars()['value'],
$salesChannelContext
);
$event->getPage()->addExtension('vRPaymentData', $paymentConfig);
}
} catch (\Exception $e) {
$this->logger->error($e->getMessage());
}
}
/**
* Handles logic before a mail is validated/sent.
*
* @param MailBeforeValidateEvent $event
*/
public function onMailBeforeValidate(MailBeforeValidateEvent $event): void
{
$templateData = $event->getTemplateData();
/**
* @var $order \Shopware\Core\Checkout\Order\OrderEntity
*/
/** @var OrderEntity|null $order */
$order = !empty($templateData['order']) && $templateData['order'] instanceof OrderEntity ? $templateData['order'] : null;
if (!empty($order) && $order->getAmountTotal() > 0) {
// Check if WhitelabelMachineName emails are enabled for this sales channel.
$isVRPaymentEmailSettingEnabled = (bool)$this->settingsService->getSettings($order->getSalesChannelId())->isEmailEnabled();
$isVRPaymentEmailSettingEnabled = $this->settingsService->getSettings($order->getSalesChannelId())->isEmailEnabled();
if (!$isVRPaymentEmailSettingEnabled) { //setting is disabled
if (!$isVRPaymentEmailSettingEnabled) {
return;
}
@@ -131,131 +148,30 @@ class CheckoutSubscriber implements EventSubscriberInterface
if (!($orderTransactions instanceof OrderTransactionCollection)) {
return;
}
$orderTransactionLast = $orderTransactions->last();
if (empty($orderTransactionLast) || empty($orderTransactionLast->getPaymentMethod())) { // no payment method available
if (empty($orderTransactionLast) || empty($orderTransactionLast->getPaymentMethod())) {
return;
}
// Check if the payment method used belongs to this plugin.
$isVRPaymentPM = VRPaymentPaymentHandler::class == $orderTransactionLast->getPaymentMethod()->getHandlerIdentifier();
if (!$isVRPaymentPM) { // not our payment method
return;
}
$isOrderTransactionStateOpen = in_array(
$orderTransactionLast->getStateMachineState()->getTechnicalName(), [
OrderTransactionStates::STATE_OPEN,
OrderTransactionStates::STATE_IN_PROGRESS,
]);
if (!$isOrderTransactionStateOpen) { // order payment status is open or in progress
return;
}
}
}
/**
* @param \Shopware\Storefront\Page\Checkout\Confirm\CheckoutConfirmPageLoadedEvent $event
*/
public function onConfirmPageLoaded(CheckoutConfirmPageLoadedEvent $event): void
{
try {
$salesChannelContext = $event->getSalesChannelContext();
$settings = $this->settingsService->getValidSettings($salesChannelContext->getSalesChannel()->getId());
if (is_null($settings)) {
$this->logger->notice('Removing payment methods because settings are invalid');
$this->removeVRPaymentPaymentMethodFromConfirmPage($event);
}
$createdTransactionId = $this->transactionService->createPendingTransaction($salesChannelContext, $event);
$this->updateTempTransactionIfNeeded($salesChannelContext, $createdTransactionId);
$this->getAvailablePaymentMethods($settings, $createdTransactionId);
$this->setPossiblePaymentMethods($settings->getSpaceId(), $event);
} catch (\Exception $e) {
$this->logger->error($e->getMessage());
$this->removeVRPaymentPaymentMethodFromConfirmPage($event);
}
}
/**
* @param \Shopware\Storefront\Page\Checkout\Confirm\CheckoutConfirmPageLoadedEvent $event
*/
private function removeVRPaymentPaymentMethodFromConfirmPage(CheckoutConfirmPageLoadedEvent $event): void
{
$paymentMethodCollection = $event->getPage()->getPaymentMethods();
$paymentMethodIds = $this->paymentMethodUtil->getVRPaymentPaymentMethodIds($event->getContext());
foreach ($paymentMethodIds as $paymentMethodId) {
$paymentMethodCollection->remove($paymentMethodId);
}
}
/**
* @param Settings $settings
* @param int $createdTransactionId
* @return void
*/
private function getAvailablePaymentMethods(Settings $settings, int $createdTransactionId): void
{
$transactionService = $settings->getApiClient()->getTransactionService();
$possiblePaymentMethods = $transactionService->fetchPaymentMethods(
$settings->getSpaceId(),
$createdTransactionId,
$settings->getIntegration()
);
$arrayOfPossibleMethods = [];
foreach ($possiblePaymentMethods as $possiblePaymentMethod) {
$arrayOfPossibleMethods[] = $possiblePaymentMethod->getid();
}
$_SESSION['arrayOfPossibleMethods'] = $arrayOfPossibleMethods;
}
/**
* @param int $spaceId
* @param CheckoutConfirmPageLoadedEvent $event
* @return void
*/
private function setPossiblePaymentMethods(int $spaceId, CheckoutConfirmPageLoadedEvent $event): void
{
$localPaymentMethods = [];
$paymentMethodConfigurations = $this->paymentMethodConfigurationService->getAllPaymentMethodConfigurations($spaceId, $event->getSalesChannelContext()->getContext());
foreach ($paymentMethodConfigurations as $paymentMethodConfiguration) {
$localPaymentMethods[$paymentMethodConfiguration->getId()] = $paymentMethodConfiguration->getPaymentMethodConfigurationId();
}
$paymentMethodCollection = $event->getPage()->getPaymentMethods();
foreach ($paymentMethodCollection as $paymentMethodCollectionItem) {
$isVRPaymentPM = VRPaymentPaymentHandler::class == $paymentMethodCollectionItem->getHandlerIdentifier();
if (!$isVRPaymentPM) {
continue;
return;
}
$paymentMethodConfigurationId = $localPaymentMethods[$paymentMethodCollectionItem->getId()];
if (!\in_array($paymentMethodConfigurationId, $_SESSION['arrayOfPossibleMethods'])) {
$paymentMethodCollection->remove($paymentMethodCollectionItem->getId());
}
}
}
// Verify if the transaction is in a state where an email should be handled.
$isOrderTransactionStateOpen = in_array(
$orderTransactionLast->getStateMachineState()->getTechnicalName(),
[
OrderTransactionStates::STATE_OPEN,
OrderTransactionStates::STATE_IN_PROGRESS,
]
);
/**
* @param SalesChannelContext $salesChannelContext
* @param int $createdTransactionId
* @return void
*/
private function updateTempTransactionIfNeeded(SalesChannelContext $salesChannelContext, int $createdTransactionId): void
{
$addressCheck = $_SESSION['addressCheck'] ?? null;
$currencyCheck = $_SESSION['currencyCheck'] ?? null;
$customer = $salesChannelContext->getCustomer();
$addressHash = md5(json_encode((array)$customer));
$currency = $salesChannelContext->getCurrency()->getIsoCode();
if (($addressCheck && $currencyCheck) && $addressCheck !== $addressHash || $currencyCheck !== $currency) {
if ($createdTransactionId) {
$this->transactionService->updateTempTransaction($salesChannelContext, $createdTransactionId);
if (!$isOrderTransactionStateOpen) {
return;
}
$_SESSION['arrayOfPossibleMethods'] = null;
$_SESSION['addressCheck'] = $addressHash;
$_SESSION['currencyCheck'] = $currency;
}
}
}
+4 -2
View File
@@ -15,6 +15,7 @@ class Analytics {
public const SHOP_SYSTEM_VERSION = 'x-meta-shop-system-version';
public const SHOP_SYSTEM_AND_VERSION = 'x-meta-shop-system-and-version';
public const PLUGIN_SYSTEM_VERSION = 'x-meta-plugin-version';
public const SUBSCRIPTION_TRANSACTION = 'x-meta-subscription-transaction';
/**
* @return array
@@ -25,16 +26,17 @@ class Analytics {
self::SHOP_SYSTEM => 'shopware',
self::SHOP_SYSTEM_VERSION => '6',
self::SHOP_SYSTEM_AND_VERSION => 'shopware-6',
self::PLUGIN_SYSTEM_VERSION => '7.1.0',
self::PLUGIN_SYSTEM_VERSION => '7.3.4',
];
}
/**
* @param \VRPayment\Sdk\ApiClient $apiClient
*/
public static function addHeaders(ApiClient &$apiClient)
public static function addHeaders(ApiClient &$apiClient, array $additionalHeaders = [])
{
$data = self::getDefaultData();
$data = array_merge($data, $additionalHeaders);
foreach ($data as $key => $value) {
$apiClient->addDefaultHeader($key, $value);
}
@@ -0,0 +1,8 @@
<?php declare(strict_types=1);
namespace VRPaymentPayment\Core\Util\Exception;
class RefundNotSupportedException extends \LogicException{
}
+20
View File
@@ -91,6 +91,26 @@ class LocaleCodeProvider {
return $language->getLocale() ? $language->getLocale()->getCode() : $defaultLocale;
}
/**
* Maps a locale code to a VRPayment-supported payment page locale by matching the language prefix.
* E.g. de-CH -> de-DE, fr-CH -> fr-FR, en-US -> en-GB, it-CH -> it-IT.
*
* @param string $localeCode
* @return string
*/
public function mapToPaymentPageLocale(string $localeCode): string
{
$supportedLocales = [
'de' => self::LOCALE_GERMANY_GERMAN,
'fr' => self::LOCALE_FRANCE_FRENCH,
'it' => self::LOCALE_ITALY_ITALIAN,
'en' => self::LOCALE_GREAT_BRITAIN_ENGLISH,
];
$languagePrefix = substr($localeCode, 0, 2);
return $supportedLocales[$languagePrefix] ?? self::LOCALE_GREAT_BRITAIN_ENGLISH;
}
/**
* @param \Shopware\Core\Framework\Context $context
+111 -48
View File
@@ -1,10 +1,13 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
namespace VRPaymentPayment\Core\Util\Payload;
use Psr\Container\ContainerInterface;
use Shopware\Core\{Checkout\Cart\Tax\Struct\CalculatedTaxCollection,
use Shopware\Core\{
Checkout\Cart\Tax\Struct\CalculatedTaxCollection,
Checkout\Customer\Aggregate\CustomerAddress\CustomerAddressEntity,
Checkout\Customer\CustomerEntity,
Checkout\Order\Aggregate\OrderLineItem\OrderLineItemEntity,
@@ -15,10 +18,12 @@ use Shopware\Core\{Checkout\Cart\Tax\Struct\CalculatedTaxCollection,
};
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
use Shopware\Core\Framework\Api\Context\SalesChannelApiSource;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use VRPayment\Sdk\{Model\AddressCreate,
use VRPayment\Sdk\{
Model\AddressCreate,
Model\ChargeAttempt,
Model\CreationEntityState,
Model\CriteriaOperator,
@@ -32,7 +37,8 @@ use VRPayment\Sdk\{Model\AddressCreate,
Model\TransactionCreate,
Model\TransactionPending
};
use VRPaymentPayment\Core\{Api\PaymentMethodConfiguration\Entity\PaymentMethodConfigurationEntity,
use VRPaymentPayment\Core\{
Api\PaymentMethodConfiguration\Entity\PaymentMethodConfigurationEntity,
Settings\Struct\Settings,
Util\Exception\InvalidPayloadException,
Util\LocaleCodeProvider,
@@ -98,6 +104,16 @@ class TransactionPayload extends AbstractPayload
protected OrderEntity $order;
/**
* @var int
*/
protected $transactionId;
public function setTransactionId(int $transactionId): void
{
$this->transactionId = $transactionId;
}
/**
* TransactionPayload constructor.
*
@@ -113,8 +129,7 @@ class TransactionPayload extends AbstractPayload
SalesChannelContext $salesChannelContext,
Settings $settings,
PaymentTransactionStruct $transaction
)
{
) {
$this->localeCodeProvider = $localeCodeProvider;
$this->salesChannelContext = $salesChannelContext;
$this->settings = $settings;
@@ -135,7 +150,7 @@ class TransactionPayload extends AbstractPayload
->addAssociation('orderCustomer')
->addAssociation('transactions')
->addAssociation('currency')
;
;
$this->order = $this->container->get('order.repository')->search($criteria, $this->salesChannelContext->getContext())->getEntities()->first();
}
@@ -182,7 +197,7 @@ class TransactionPayload extends AbstractPayload
'merchant_reference' => $this->fixLength($this->order->getOrderNumber(), 100),
'meta_data' => [
self::VRPAYMENT_METADATA_ORDER_ID => $this->order->getId(),
self::VRPAYMENT_METADATA_ORDER_TRANSACTION_ID => $this->order->getTransactions()->first()->getId(),
self::VRPAYMENT_METADATA_ORDER_TRANSACTION_ID => $this->transaction->getOrderTransactionId(),
self::VRPAYMENT_METADATA_SALES_CHANNEL_ID => $this->salesChannelContext->getSalesChannel()->getId(),
self::VRPAYMENT_METADATA_CUSTOMER_NAME => $customerName,
],
@@ -218,7 +233,7 @@ class TransactionPayload extends AbstractPayload
}
$transactionPayload = (new TransactionPending())
->setId($_SESSION['transactionId'])
->setId($this->transactionId)
->setVersion($version)
->setBillingAddress($billingAddress)
->setCurrency($transactionData['currency'])
@@ -231,12 +246,21 @@ class TransactionPayload extends AbstractPayload
->setShippingAddress($shippingAddress)
->setShippingMethod($transactionData['shipping_method']);
$paymentConfiguration = $this->getPaymentConfiguration($this->salesChannelContext->getPaymentMethod()->getId());
$paymentConfiguration = $this->getPaymentConfiguration(
$this->salesChannelContext->getPaymentMethod()->getId(),
$this->settings->getSpaceId()
);
$transactionPayload->setAllowedPaymentMethodConfigurations([$paymentConfiguration->getPaymentMethodConfigurationId()]);
if ($paymentConfiguration) {
$transactionPayload->setAllowedPaymentMethodConfigurations([
$paymentConfiguration->getPaymentMethodConfigurationId()
]);
}
$successUrl = $this->transaction->getReturnUrl() . '&status=paid';
$failedUrl = $this->getFailUrl($this->order->getId()) . '&status=fail';
// For headless clients, use the returnUrl for failure as well (they handle the status parameter).
// For Storefront, use the recreate-cart route.
$failedUrl = $this->getFailUrl($this->order->getId(), $this->transaction->getReturnUrl()) . '&status=fail';
$transactionPayload->setSuccessUrl($successUrl)
->setFailedUrl($failedUrl);
@@ -288,8 +312,8 @@ class TransactionPayload extends AbstractPayload
protected function shouldSkipLineItem($shopLineItem): bool
{
return in_array($shopLineItem->getType(), [
CustomProductsLineItemTypes::LINE_ITEM_TYPE_CUSTOMIZED_PRODUCTS,
'promotion'
CustomProductsLineItemTypes::LINE_ITEM_TYPE_CUSTOMIZED_PRODUCTS,
'promotion'
]);
}
@@ -354,7 +378,7 @@ class TransactionPayload extends AbstractPayload
$rate = $calculatedTax->getTaxRate();
$amount = $this->calculateDiscountAmount($calculatedTax);
$lineItems[] = $this->createDiscountLineItem($discountName, $amount, $rate);
$lineItems[] = $this->createDiscountLineItem($discountName, $amount, $rate, $discount->getId());
}
} else {
$taxRules = $calculatedPrice->getTaxRules();
@@ -363,12 +387,12 @@ class TransactionPayload extends AbstractPayload
foreach ($taxRules as $taxRule) {
$rate = $taxRule->getTaxRate();
$amount = $calculatedPrice->getTotalPrice();
$lineItems[] = $this->createDiscountLineItem($discountName, $amount, $rate);
$lineItems[] = $this->createDiscountLineItem($discountName, $amount, $rate, $discount->getId());
}
} else {
$rate = $this->getDefaultTaxRate();
$amount = $calculatedPrice->getTotalPrice();
$lineItems[] = $this->createDiscountLineItem($discountName, $amount, $rate);
$lineItems[] = $this->createDiscountLineItem($discountName, $amount, $rate, $discount->getId());
}
}
}
@@ -379,7 +403,7 @@ class TransactionPayload extends AbstractPayload
* @param float $rate
* @return LineItemCreate
*/
private function createDiscountLineItem(string $discountName, float $amount, float $rate): LineItemCreate
private function createDiscountLineItem(string $discountName, float $amount, float $rate, string $discountId): LineItemCreate
{
$lineItem = new LineItemCreate();
@@ -390,13 +414,15 @@ class TransactionPayload extends AbstractPayload
$discountTitle = sprintf('DISCOUNT: %s', $discountName);
}
$lineItem->setAmountIncludingTax($amount)
$roundedAmount = self::round($amount);
$lineItem->setAmountIncludingTax($roundedAmount)
->setName($discountTitle)
->setQuantity(1)
->setShippingRequired(false)
->setSku($discountSkuName, 200)
->setType(LineItemType::DISCOUNT)
->setUniqueId('coupon-' . $discountSkuName);
->setUniqueId('coupon-' . $discountSkuName . '-' . $discountId);
$taxRate = new TaxCreate([
'title' => 'Discount Tax: ' . $rate,
@@ -513,12 +539,14 @@ class TransactionPayload extends AbstractPayload
$amount = self::round($amount + $shopLineItem->getPrice()->getCalculatedTaxes()->getAmount());
}
$roundedAmount = self::round($amount);
$lineItem = (new LineItemCreate())
->setName($this->fixLength($shopLineItem->getLabel(), 150))
->setUniqueId($uniqueId)
->setSku($sku)
->setQuantity($shopLineItem->getQuantity() ?? 1)
->setAmountIncludingTax($amount);
->setAmountIncludingTax($roundedAmount);
if (!empty($shopLineItem->getType()) && $shopLineItem->getType() == CustomProductsLineItemTypes::LINE_ITEM_TYPE_CUSTOMIZED_PRODUCTS) {
@@ -529,7 +557,6 @@ class TransactionPayload extends AbstractPayload
$this->translator->trans('vrpayment.payload.taxes'),
$amount
);
} else {
$productAttributes = $this->getProductAttributes($shopLineItem);
@@ -579,7 +606,7 @@ class TransactionPayload extends AbstractPayload
throw new InvalidPayloadException('Tax payload invalid:' . json_encode($tax->listInvalidProperties()));
}
$taxes [] = $tax;
$taxes[] = $tax;
}
return $taxes;
@@ -637,9 +664,10 @@ class TransactionPayload extends AbstractPayload
$amount = self::round($amount + $this->order->getShippingCosts()->getCalculatedTaxes()->getAmount());
}
$roundedAmount = self::round($amount);
$lineItem = (new LineItemCreate())
->setAmountIncludingTax($amount)
->setAmountIncludingTax($roundedAmount)
->setName($this->fixLength($shippingName . ' ' . $this->translator->trans('vrpayment.payload.shipping.lineItem'), 150))
->setQuantity($this->order->getShippingCosts()->getQuantity() ?? 1)
->setSku($this->fixLength($shippingName . '-Shipping', 200))
@@ -658,7 +686,6 @@ class TransactionPayload extends AbstractPayload
return $lineItem;
}
} catch (\Exception $exception) {
$this->logger->critical(__CLASS__ . ' : ' . __FUNCTION__ . ' : ' . $exception->getMessage());
}
@@ -684,17 +711,19 @@ class TransactionPayload extends AbstractPayload
}
$taxRate = $taxItem->getTaxRate();
$tax = (new TaxCreate())
->setRate($taxRate)
->setTitle('Tax rate: '.$taxRate);
->setRate($taxRate)
->setTitle('Tax rate: ' . $taxRate);
$roundedAmount = self::round($amount);
$name = $taxRate . '%-' . $shippingName;
$lineItem = (new LineItemCreate())
->setAmountIncludingTax($amount)
->setName($this->fixLength($name . ' ' . $this->translator->trans('vrpayment.payload.shipping.lineItem'), 150))
->setQuantity($this->order->getShippingCosts()->getQuantity() ?? 1)
->setSku($this->fixLength($name . '-Shipping', 200))
->setType($isFirst ? LineItemType::SHIPPING : LineItemType::FEE) // First item as SHIPPING, rest as FEE
->setUniqueId($this->fixLength($name . '-Shipping', 200));
->setAmountIncludingTax($roundedAmount)
->setName($this->fixLength($name . ' ' . $this->translator->trans('vrpayment.payload.shipping.lineItem'), 150))
->setQuantity($this->order->getShippingCosts()->getQuantity() ?? 1)
->setSku($this->fixLength($name . '-Shipping', 200))
->setType($isFirst ? LineItemType::SHIPPING : LineItemType::FEE) // First item as SHIPPING, rest as FEE
->setUniqueId($this->fixLength($name . '-Shipping', 200));
if ($this->order->getTaxStatus() !== 'tax-free') {
$lineItem->setTaxes([$tax]);
@@ -710,7 +739,6 @@ class TransactionPayload extends AbstractPayload
}
return $lineItems;
}
} catch (\Exception $exception) {
$this->logger->critical(__CLASS__ . ' : ' . __FUNCTION__ . ' : ' . $exception->getMessage());
}
@@ -753,7 +781,6 @@ class TransactionPayload extends AbstractPayload
]);
$this->logger->critical($error);
throw new \Exception($error);
} else {
$lineItem = (new LineItemCreate())
->setName($this->translator->trans('vrpayment.payload.adjustmentLineItem'))
@@ -761,7 +788,8 @@ class TransactionPayload extends AbstractPayload
->setSku('Adjustment-Line-Item')
->setQuantity(1);
/** @noinspection PhpParamsInspection */
$lineItem->setAmountIncludingTax($adjustmentPrice)
$roundedAdjustmentPrice = self::round($adjustmentPrice);
$lineItem->setAmountIncludingTax($roundedAdjustmentPrice)
->setType(($adjustmentPrice > 0) ? LineItemType::FEE : LineItemType::DISCOUNT);
if (!$lineItem->valid()) {
@@ -834,7 +862,6 @@ class TransactionPayload extends AbstractPayload
} else {
if (!empty($customer->getSalutation())) {
$salutation = $customer->getSalutation()->getDisplayName();
}
}
$salutation = !empty($salutation) ? $this->fixLength($salutation, 20) : null;
@@ -900,28 +927,64 @@ class TransactionPayload extends AbstractPayload
}
/**
* @param string $id
*
* @return \VRPaymentPayment\Core\Api\PaymentMethodConfiguration\Entity\PaymentMethodConfigurationEntity
* @param string $paymentMethodId
* @param int $spaceId
* @return PaymentMethodConfigurationEntity|null
*/
protected function getPaymentConfiguration(string $id): PaymentMethodConfigurationEntity
protected function getPaymentConfiguration(string $paymentMethodId, int $spaceId): ?PaymentMethodConfigurationEntity
{
$criteria = (new Criteria([$id]));
$criteria = new Criteria();
$criteria->addFilter(new EqualsFilter('paymentMethodId', $paymentMethodId));
$criteria->addFilter(new EqualsFilter('spaceId', $spaceId));
return $this->container->get('vrpayment_payment_method_configuration.repository')
->search($criteria, $this->salesChannelContext->getContext())
->getEntities()->first();
->first();
}
/**
* Get failure URL
* Generates the failure URL for payment transactions.
* For headless clients (Store API), returns the client's returnUrl.
* For Storefront, returns the recreate-cart route.
*
* @param string $orderId
*
* @return string
* @param string $orderId The order ID for the Storefront route.
* @param string|null $returnUrl The client's return URL (used for headless).
* @return string The failure URL.
*/
protected function getFailUrl(string $orderId): string
protected function getFailUrl(string $orderId, ?string $returnUrl = null): string
{
// For headless clients (Store API) or custom integrations, we use the returnUrl so the client can handle the failure.
// We detect "Standard Storefront" usage by checking if the returnUrl matches the standard Shopware 'finalize-transaction' route.
// If it DOES match, we usually redirect to 'recreate-cart' for better error handling. (Storefront / Edit Order)
// If it DOES NOT match (e.g. custom URL, headless URL), we return it as is.
$isStandardShopwareUrl = strpos($returnUrl ?? '', 'payment/finalize-transaction') !== false;
$request = $this->container->get('request_stack')->getCurrentRequest();
$isJsonRequest = false;
if ($request) {
// Check for JSON preference
$format = $request->getPreferredFormat();
$contentTypes = $request->getAcceptableContentTypes();
$isJsonRequest = $format === 'json' || in_array('application/json', $contentTypes);
}
if ($returnUrl) {
// Case 1: Custom URL (Headless detection part 1)
// If the URL is custom (not standard Shopware), we always respect it.
if (!$isStandardShopwareUrl) {
return $returnUrl;
}
// Case 2: Standard URL + JSON Request (Headless detection part 2)
// If URL is standard BUT request prefers JSON, it's likely a headless app (Store API) being proxied.
if ($isStandardShopwareUrl && $isJsonRequest) {
return $returnUrl;
}
}
// Default: Storefront (SystemSource, SalesChannelSource, or AdminSalesChannelApiSource via Edit Order/HTML)
// We generate the recreate-cart route to restore the cart and show the error.
return $this->container->get('router')->generate(
'frontend.vrpayment.checkout.recreate-cart',
['orderId' => $orderId,],
@@ -31,7 +31,7 @@ class Migration1590156974TransactionEntity extends MigrationStep {
public function update(Connection $connection): void
{
$connection->executeStatement('
CREATE TABLE IF NOT EXISTS `vrpayment_transaction` (
CREATE TABLE IF NOT EXISTS `vrpayment_transaction_tmp` (
`id` BINARY(16) NOT NULL,
`data` JSON NOT NULL,
`payment_method_id` BINARY(16) NOT NULL,
@@ -42,7 +42,7 @@ class Migration1590646356RefundEntity extends MigrationStep {
PRIMARY KEY (`id`),
UNIQUE KEY `refund_id_UNIQUE` (`refund_id`),
KEY `fk.vrp_refund.transaction_id` (`transaction_id`),
CONSTRAINT `fk.vrp_refund.transaction_id` FOREIGN KEY (`transaction_id`) REFERENCES `vrpayment_transaction` (`transaction_id`) ON DELETE CASCADE
CONSTRAINT `fk.vrp_refund.transaction_id` FOREIGN KEY (`transaction_id`) REFERENCES `vrpayment_transaction_tmp` (`transaction_id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
');
}
@@ -30,7 +30,7 @@ class Migration1590646356TransactionEntity extends MigrationStep {
public function update(Connection $connection): void
{
try {
$connection->executeStatement('ALTER TABLE `vrpayment_transaction` ADD COLUMN `confirmation_email_sent` TINYINT(1) NOT NULL DEFAULT 0 AFTER `id`;');
$connection->executeStatement('ALTER TABLE `vrpayment_transaction_tmp` ADD COLUMN `confirmation_email_sent` TINYINT(1) NOT NULL DEFAULT 0 AFTER `id`;');
}catch (\Exception $exception){
// column probably exists
}
@@ -33,19 +33,19 @@ class Migration1605701048TransactionEntity extends MigrationStep
try {
$connection->executeStatement('
ALTER TABLE `vrpayment_transaction`
ALTER TABLE `vrpayment_transaction_tmp`
ADD `order_version_id` binary(16) NOT NULL AFTER `transaction_id`;
');
$connection->executeStatement('
UPDATE `vrpayment_transaction` t1
UPDATE `vrpayment_transaction_tmp` t1
INNER JOIN `order` t2
ON t1.order_id = t2.id
SET t1.order_version_id = t2.version_id;
');
$connection->executeStatement('
ALTER TABLE `vrpayment_transaction`
ALTER TABLE `vrpayment_transaction_tmp`
DROP FOREIGN KEY `fk.vrp_transaction.order_id`,
DROP FOREIGN KEY `fk.vrp_transaction.order_transaction_id`,
DROP FOREIGN KEY `fk.vrp_transaction.payment_method_id`,
@@ -53,7 +53,7 @@ class Migration1605701048TransactionEntity extends MigrationStep
');
$connection->executeStatement('
ALTER TABLE `vrpayment_transaction`
ALTER TABLE `vrpayment_transaction_tmp`
ADD CONSTRAINT `fk.vrp_transaction_order_id` FOREIGN KEY (`order_id`, `order_version_id`)
REFERENCES `order` (`id`, `version_id`) ON DELETE CASCADE ON UPDATE CASCADE,
ADD CONSTRAINT `fk.vrp_transaction_payment_method_id` FOREIGN KEY (`payment_method_id`)
@@ -30,7 +30,7 @@ class Migration1684240994TransactionEntity extends MigrationStep {
public function update(Connection $connection): void
{
try {
$connection->executeStatement('ALTER TABLE `vrpayment_transaction` ADD COLUMN `erp_merchant_id` VARCHAR(255) DEFAULT NULL AFTER `confirmation_email_sent`;');
$connection->executeStatement('ALTER TABLE `vrpayment_transaction_tmp` ADD COLUMN `erp_merchant_id` VARCHAR(255) DEFAULT NULL AFTER `confirmation_email_sent`;');
}catch (\Exception $exception){
// column probably exists
}
@@ -0,0 +1,323 @@
<?php declare(strict_types=1);
namespace VRPaymentPayment\Migration;
use Doctrine\DBAL\Connection;
use Shopware\Core\Framework\Migration\MigrationStep;
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
/**
* Class Migration1766067106TransactionEntity
*
* @package VRPaymentPayment\Migration
*/
class Migration1766067106TransactionEntity extends MigrationStep
{
/**
* get creation timestamp
*
* @return int
*/
public function getCreationTimestamp(): int
{
return 1766067106;
}
/**
* update non-destructive changes
*
* @param \Doctrine\DBAL\Connection $connection
*/
public function update(Connection $connection): void
{
$oldTableName = 'vrpayment_transaction';
$tempTableName = 'vrpayment_transaction_tmp';
$realTableName = 'vrpayment_transaction_data';
$logger = new Logger('vrpayment_migration');
$logger->pushHandler(new StreamHandler(dirname(__DIR__, 5) . '/var/log/vrpayment-migration.log'));
$logger->info(
'Migration start', [
'old_table_exists' => $this->tableExists($connection, $oldTableName),
'temp_table_exists' => $this->tableExists($connection, $tempTableName),
'real_table_exists' => $this->tableExists($connection, $realTableName),
]
);
if ($this->tableExists($connection, $tempTableName)) {
// If _temp table exists, it means that this is a fresh installation.
$logger->info('Fresh installation detected.');
$connection->executeStatement(
sprintf('RENAME TABLE `%s` TO `%s`', $tempTableName, $realTableName)
);
$logger->info('Fresh installation finished.');
} else {
// If _temp does not exist, it means that this could be a version upgrade.
$logger->info('Possible plugin upgrade detected.');
if ($this->tableExists($connection, $oldTableName) && !$this->isOldPluginTable($connection, $oldTableName)) {
$logger->info('Old vrpayment_transaction table detected.');
// If vrpayment_transaction already exists and does not belong to old plugin,
// it means that this is indeed a version update.
$this->syncTransactionTable($connection, $oldTableName);
$logger->info('Old vrpayment_transaction table sync finished.');
$this->syncRefundTable($connection, $oldTableName);
$logger->info('Old vrpayment_refund table sync finished.');
$connection->executeStatement(
sprintf('RENAME TABLE `%s` TO `%s`', $oldTableName, $realTableName)
);
$logger->info('Old vrpayment_transaction table renaming completed.');
}
$logger->info('Possible plugin upgrade finished.');
// If vrpayment_transaction exists and it does belong to old plugin,
// it means we must run it in parallel.
}
$logger->info('Migration finished.');
return;
}
/**
* Check if table exists.
*
* @param \Doctrine\DBAL\Connection $connection
* @param string $table
*
* @return bool
*/
public function tableExists(Connection $connection, string $table): bool {
$result = $connection->fetchOne('SHOW TABLES LIKE :table', ['table' => $table]);
return $result !== false && $result !== null;
}
/**
* Check if table belongs to old plugin.
*
* @param \Doctrine\DBAL\Connection $connection
* @param string $table
*
* @return bool
*/
public function isOldPluginTable(Connection $connection, string $table): bool {
$oldTableExclusiveColumns = [
'finalized_at' => 'datetime',
'refunded_at' => 'datetime',
'initial_transaction_mode' => 'varchar',
'manual_capture' => 'tinyint',
'partial_refunded_at' => 'datetime',
'refunded_amount' => 'double',
'amount_to_refund' => 'double',
];
$resultColumns = $connection->fetchAllAssociative(
'SELECT LOWER(COLUMN_NAME) AS column_name, LOWER(DATA_TYPE) AS data_type
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = :table',
['table' => $table]
);
$dbColumns = [];
foreach($resultColumns as $column) {
$dbColumns[$column['column_name']] = $column['data_type'];
}
$oldPluginTable = true;
foreach($oldTableExclusiveColumns as $columnName => $columnType) {
if(!isset($dbColumns[$columnName])) {
$oldPluginTable = false;
break;
}
if ($dbColumns[$columnName] !== $columnType) {
$oldPluginTable = false;
break;
}
}
return $oldPluginTable;
}
/**
* Synchronizes the transaction table with the current/latest version.
*
* @param \Doctrine\DBAL\Connection $connection
* @param string $table
*/
private function syncTransactionTable(Connection $connection, string $table): void {
$this->addColumnIfMissing($connection, $table, 'confirmation_email_sent', "TINYINT(1) NOT NULL DEFAULT 0 AFTER `id`");
$this->addColumnIfMissing($connection, $table, 'erp_merchant_id', "VARCHAR(255) DEFAULT NULL AFTER `confirmation_email_sent`");
$this->addColumnIfMissing($connection, $table, 'data', "LONGTEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL CHECK (json_valid(`data`)) AFTER `erp_merchant_id`");
$this->addColumnIfMissing($connection, $table, 'payment_method_id', "BINARY(16) NOT NULL");
$this->addColumnIfMissing($connection, $table, 'order_id', "BINARY(16) NOT NULL");
$this->addColumnIfMissing($connection, $table, 'order_transaction_id', "BINARY(16) NOT NULL");
$this->addColumnIfMissing($connection, $table, 'space_id', "INT(10) UNSIGNED NOT NULL");
$this->addColumnIfMissing($connection, $table, 'state', "VARCHAR(255) NOT NULL");
$this->addColumnIfMissing($connection, $table, 'sales_channel_id', "BINARY(16) NOT NULL");
$this->addColumnIfMissing($connection, $table, 'transaction_id', "INT(10) UNSIGNED NOT NULL");
$this->addColumnIfMissing($connection, $table, 'order_version_id', "BINARY(16) NOT NULL AFTER `transaction_id`");
$this->addColumnIfMissing($connection, $table, 'created_at', "DATETIME(3) NOT NULL");
$this->addColumnIfMissing($connection, $table, 'updated_at', "DATETIME(3) DEFAULT NULL");
$this->ensureIndexBySql($connection, $table, 'fk.vrp_transaction.order_id', "KEY `fk.vrp_transaction.order_id` (`order_id`)");
$this->ensureIndexBySql($connection, $table, 'fk.vrp_transaction.order_transaction_id', "KEY `fk.vrp_transaction.order_transaction_id` (`order_transaction_id`)");
$this->ensureIndexBySql($connection, $table, 'fk.vrp_transaction.payment_method_id', "KEY `fk.vrp_transaction.payment_method_id` (`payment_method_id`)");
$this->ensureIndexBySql($connection, $table, 'fk.vrp_transaction.sales_channel_id', "KEY `fk.vrp_transaction.sales_channel_id` (`sales_channel_id`)");
$this->ensureIndexBySql($connection, $table, 'fk.vrp_transaction', "KEY `fk.vrp_transaction` (`order_id`,`order_version_id`)");
$this->ensureForeignKey(
$connection,
$table,
'fk.vrp_transaction_order_id',
['order_id', 'order_version_id'],
'order',
['id', 'version_id'],
'CASCADE',
'CASCADE'
);
$this->ensureForeignKey(
$connection,
$table,
'fk.vrp_transaction_payment_method_id',
['payment_method_id'],
'payment_method',
['id'],
'RESTRICT',
'CASCADE'
);
$this->ensureForeignKey(
$connection,
$table,
'fk.vrp_transaction_sales_channel_id',
['sales_channel_id'],
'sales_channel',
['id'],
'RESTRICT',
'CASCADE'
);
}
/**
* Synchronizes the parts of the refund table related to transactions with the current/latest version.
*
* @param \Doctrine\DBAL\Connection $connection
* @param string $table
*/
private function syncRefundTable(Connection $connection, string $table): void {
$refundTable = 'vrpayment_refund';
$this->ensureIndexBySql($connection, $refundTable, 'fk.vrp_refund.transaction_id', "KEY `fk.vrp_refund.transaction_id` (`transaction_id`)");
$this->ensureForeignKey(
$connection,
$refundTable,
'fk.vrp_refund.transaction_id',
['transaction_id'],
$table,
['transaction_id'],
'CASCADE',
null
);
}
/**
* Adds column to the table if it's missing.
*
* @param \Doctrine\DBAL\Connection $connection
* @param string $table
* @param string $column
* @param string $sqlFragment
*/
private function addColumnIfMissing(Connection $connection, string $table, string $column, string $sqlFragment): void {
if ($this->columnExists($connection, $table, $column)) {
return;
}
$connection->executeStatement(
sprintf("ALTER TABLE `%s` ADD COLUMN `%s` %s", $table, $column, $sqlFragment)
);
}
/**
* Adds index to the table if it's missing.
*
* @param \Doctrine\DBAL\Connection $connection
* @param string $table
* @param string $indexName
* @param string $sqlFragment
*/
private function ensureIndexBySql(Connection $connection, string $table, string $indexName, string $sqlFragment): void {
if ($this->indexExists($connection, $table, $indexName)) {
return;
}
$connection->executeStatement(
sprintf("ALTER TABLE `%s` ADD %s", $table, $sqlFragment)
);
}
/**
* Adds foreign key constraint to the table if it's missing.
*
* @param \Doctrine\DBAL\Connection $connection
* @param string $table
* @param string $constraintName
* @param string $columns
* @param string $refTable
* @param string $refColumns
* @param string|null $onDelete
* @param string|null $onUpdate
*/
private function ensureForeignKey(
Connection $connection,
string $table,
string $constraintName,
array $columns,
string $refTable,
array $refColumns,
?string $onDelete,
?string $onUpdate
): void {
if ($this->foreignKeyExists($connection, $table, $constraintName)) {
return;
}
$columnsList = '`' . implode('`,`', $columns) . '`';
$refColumnsList = '`' . implode('`,`', $refColumns) . '`';
$connection->executeStatement(
sprintf(
"ALTER TABLE `%s`
ADD CONSTRAINT `%s` FOREIGN KEY (%s)
REFERENCES `%s` (%s)%s%s",
$table,
$constraintName,
$columnsList,
$refTable,
$refColumnsList,
$onDelete ? " ON DELETE {$onDelete}" : "",
$onUpdate ? " ON UPDATE {$onUpdate}" : ""
)
);
}
/**
* Check if foreign key constraint exists.
*
* @param \Doctrine\DBAL\Connection $connection
* @param string $table
* @param string $constraintName
*
* @return bool
*/
private function foreignKeyExists(Connection $connection, string $table, $constraintName): bool {
$result = $connection->fetchOne(
"SELECT 1 FROM information_schema.referential_constraints
WHERE constraint_schema = DATABASE()
AND table_name = ?
AND constraint_name = ?
LIMIT 1",
[$table,$constraintName]
);
return $result !== false && $result !== null;
}
/**
* update destructive changes
*
* @param \Doctrine\DBAL\Connection $connection
*/
public function updateDestructive(Connection $connection): void
{
// implement update destructive
}
}
@@ -70,14 +70,20 @@ Component.register('vrpayment-order-action-refund-by-amount', {
});
}).catch((errorResponse) => {
try {
var errorTitle;
var errorTitle = errorResponse?.response?.data?.errors?.[0]?.title ?? this.$tc('vrpayment-order.refundAction.refundCreateError.errorTitle')
var errorMessage;
if (errorResponse.response.data == 'refundExceedsAmount') {
errorTitle = this.$tc('vrpayment-order.refundAction.refundExceedsTotalError.title');
errorMessage = this.$tc('vrpayment-order.refundAction.refundExceedsTotalError.messageRefundAmountExceedsAvailableBalance');
} else {
errorTitle = errorResponse.response.data.errors[0].title;
errorMessage = errorResponse.response.data.errors[0].detail;
switch(errorResponse.response.data) {
case 'refundAmountZero':
errorMessage = this.$tc('vrpayment-order.refundAction.refundCreateError.messageRefundAmountIsZero');
break;
case 'refundExceedsAmount':
errorMessage = this.$tc('vrpayment-order.refundAction.refundCreateError.messageRefundAmountExceedsAvailableBalance');
break;
case 'methodDoesNotSupportRefund':
errorMessage = this.$tc('vrpayment-order.refundAction.refundCreateError.messagePaymentMethodDoesNotSupportRefund');
break;
default:
errorMessage = errorResponse.response.data.errors[0].detail;
}
this.createNotificationError({
title: errorTitle,
@@ -71,9 +71,18 @@ Component.register('vrpayment-order-action-refund-partial', {
});
}).catch((errorResponse) => {
try {
var errorTitle = errorResponse?.response?.data?.errors?.[0]?.title ?? this.$tc('vrpayment-order.refundAction.refundCreateError.errorTitle')
var errorMessage;
switch(errorResponse.response.data) {
case 'methodDoesNotSupportRefund':
errorMessage = this.$tc('vrpayment-order.refundAction.refundCreateError.messagePaymentMethodDoesNotSupportRefund');
break;
default:
errorMessage = errorResponse.response.data.errors[0].detail;
}
this.createNotificationError({
title: errorResponse.response.data.errors[0].title,
message: errorResponse.response.data.errors[0].detail,
title: errorTitle,
message: errorMessage,
autoClose: false
});
} catch (e) {
@@ -70,9 +70,18 @@ Component.register('vrpayment-order-action-refund-selected', {
});
}).catch((errorResponse) => {
try {
var errorTitle = errorResponse?.response?.data?.errors?.[0]?.title ?? this.$tc('vrpayment-order.refundAction.refundCreateError.errorTitle')
var errorMessage;
switch(errorResponse.response.data) {
case 'methodDoesNotSupportRefund':
errorMessage = this.$tc('vrpayment-order.refundAction.refundCreateError.messagePaymentMethodDoesNotSupportRefund');
break;
default:
errorMessage = errorResponse.response.data.errors[0].detail;
}
this.createNotificationError({
title: errorResponse.response.data.errors[0].title,
message: errorResponse.response.data.errors[0].detail,
title: errorTitle,
message: errorMessage,
autoClose: false
});
} catch (e) {
@@ -9,6 +9,7 @@
:max="this.$parent.$parent.itemRefundableQuantity"
:min="0"
v-model="refundQuantity"
number-type="int"
:label="$tc('vrpayment-order.refund.refundQuantity.label')">
</mt-number-field>
@@ -68,9 +68,24 @@ Component.register('vrpayment-order-action-refund', {
});
}).catch((errorResponse) => {
try {
var errorTitle = errorResponse?.response?.data?.errors?.[0]?.title ?? this.$tc('vrpayment-order.refundAction.refundCreateError.errorTitle')
var errorMessage;
switch(errorResponse.response.data) {
case 'refundQuantityZero':
errorMessage = this.$tc('vrpayment-order.refundAction.refundCreateError.messageRefundQuantityIsZero');
break;
case 'refundExceedsQuantity':
errorMessage = this.$tc('vrpayment-order.refundAction.refundCreateError.messageRefundQuantityExceedsAvailableBalance');
break;
case 'methodDoesNotSupportRefund':
errorMessage = this.$tc('vrpayment-order.refundAction.refundCreateError.messagePaymentMethodDoesNotSupportRefund');
break;
default:
errorMessage = errorResponse.response.data.errors[0].detail;
}
this.createNotificationError({
title: errorResponse.response.data.errors[0].title,
message: errorResponse.response.data.errors[0].detail,
title: errorTitle,
message: errorMessage,
autoClose: false
});
} catch (e) {
@@ -98,7 +98,7 @@
<template #actions="{ item }">
<sw-context-menu-item
:disabled="transaction.state != 'FULFILL' || item.refundableQuantity != item.quantity || item.refundableAmount == 0 || item.itemRefundedAmount > 0 || item.itemRefundedQuantity > 0"
@click="lineItemRefund(item.uniqueId)">
@click="lineItemRefund(item.uniqueId, item.quantity)">
{{ $tc('vrpayment-order.buttons.label.refund-whole-line-item') }}
</sw-context-menu-item>
@@ -332,12 +332,12 @@ Component.register('vrpayment-order-detail', {
this.modalType = '';
},
lineItemRefund(lineItemId) {
lineItemRefund(lineItemId, itemQuantity) {
this.isLoading = true;
this.VRPaymentRefundService.createRefund(
this.transactionData.transactions[0].metaData.salesChannelId,
this.transactionData.transactions[0].id,
0,
itemQuantity,
lineItemId
).then(() => {
this.createNotificationSuccess({
@@ -351,9 +351,18 @@ Component.register('vrpayment-order-detail', {
});
}).catch((errorResponse) => {
try {
var errorTitle = errorResponse?.response?.data?.errors?.[0]?.title ?? this.$tc('vrpayment-order.refundAction.refundCreateError.errorTitle')
var errorMessage;
switch(errorResponse.response.data) {
case 'methodDoesNotSupportRefund':
errorMessage = this.$tc('vrpayment-order.refundAction.refundCreateError.messagePaymentMethodDoesNotSupportRefund');
break;
default:
errorMessage = errorResponse.response.data.errors[0].detail;
}
this.createNotificationError({
title: errorResponse.response.data.errors[0].title,
message: errorResponse.response.data.errors[0].detail,
title: errorTitle,
message: errorMessage,
autoClose: false
});
} catch (e) {
@@ -385,7 +394,7 @@ Component.register('vrpayment-order-detail', {
// Force the DOM to update before proceeding with the asynchronous operations
this.$nextTick(() => {
const refundPromises = this.selectedItems.map((item) => {
return this.lineItemRefundBulk(item.uniqueId); // Simulated refund action with delay
return this.lineItemRefundBulk(item.uniqueId, item.quantity); // Simulated refund action with delay
});
// Wait for all refund promises to complete
@@ -399,6 +408,10 @@ Component.register('vrpayment-order-detail', {
});
})
.catch((error) => {
if (error?.response?.data === 'methodDoesNotSupportRefund') {
this.isLoading = false;
return;
}
// Handle any errors during the refund process
this.createNotificationError({
title: 'Error',
@@ -410,12 +423,12 @@ Component.register('vrpayment-order-detail', {
});
}
},
lineItemRefundBulk(lineItemId) {
lineItemRefundBulk(lineItemId, itemQuantity) {
return new Promise((resolve, reject) => {
this.VRPaymentRefundService.createRefund(
this.transactionData.transactions[0].metaData.salesChannelId,
this.transactionData.transactions[0].id,
0,
itemQuantity,
lineItemId
)
.then(() => {
@@ -427,11 +440,20 @@ Component.register('vrpayment-order-detail', {
})
.catch((errorResponse) => {
try {
this.createNotificationError({
title: errorResponse.response.data.errors[0].title,
message: errorResponse.response.data.errors[0].detail,
autoClose: false
});
var errorTitle = errorResponse?.response?.data?.errors?.[0]?.title ?? this.$tc('vrpayment-order.refundAction.refundCreateError.errorTitle')
var errorMessage;
switch(errorResponse.response.data) {
case 'methodDoesNotSupportRefund':
errorMessage = this.$tc('vrpayment-order.refundAction.refundCreateError.messagePaymentMethodDoesNotSupportRefund');
break;
default:
errorMessage = errorResponse.response.data.errors[0].detail;
}
this.createNotificationError({
title: errorTitle,
message: errorMessage,
autoClose: false
});
} catch (e) {
this.createNotificationError({
title: errorResponse.title,
@@ -439,7 +461,7 @@ Component.register('vrpayment-order-detail', {
autoClose: false
});
} finally {
reject();
reject(errorResponse);
}
});
});
@@ -78,9 +78,13 @@
"successTitle": "Erfolg",
"maxAvailableItemsToRefund": "Maximal Verfügbare Artikel zum Erstatten",
"maxAvailableAmountToRefund": "Maximal verfügbarer Erstattungsbetrag",
"refundExceedsTotalError": {
"title": "Fehler beim Erstellen der Rückerstattung.",
"messageRefundAmountExceedsAvailableBalance": "Der Rückerstattungsbetrag übersteigt das verfügbare Guthaben."
"refundCreateError": {
"errorTitle": "Fehler beim Erstellen der Rückerstattung.",
"messageRefundAmountExceedsAvailableBalance": "Der Rückerstattungsbetrag übersteigt das verfügbare Guthaben.",
"messageRefundAmountIsZero": "Der Rückerstattungsbetrag muss größer als 0 sein.",
"messageRefundQuantityExceedsAvailableBalance": "Rückerstattung nach Menge überschreitet die maximal verfügbare Anzahl an Artikeln zur Rückerstattung.",
"messageRefundQuantityIsZero": "Rückerstattung nach Menge muss größer als 0 sein.",
"messagePaymentMethodDoesNotSupportRefund": "Die Zahlungsmethode unterstützt keine Online-Rückerstattungen."
}
},
"transactionHistory": {
@@ -9,7 +9,6 @@
"void": "Cancel authorization",
"refund-whole-line-item": "Refund whole line item",
"refund-line-item-by-quantity": "Refund by quantity",
"refund-line-item-selected": "Rembourser sélectionnés",
"refund-line-item-selected": "Refund selected",
"refund-line-item-parial": "Partial refund"
}
@@ -79,9 +78,13 @@
"successTitle": "Success",
"maxAvailableItemsToRefund": "Maximum available items to refund",
"maxAvailableAmountToRefund": "Maximum available amount to refund",
"refundExceedsTotalError": {
"title": "Error while creating the refund.",
"messageRefundAmountExceedsAvailableBalance": "Refund amount exceeds available balance."
"refundCreateError": {
"errorTitle": "Error while creating the refund.",
"messageRefundAmountExceedsAvailableBalance": "Refund amount exceeds available balance.",
"messageRefundAmountIsZero": "Refund amount must be greater than 0.",
"messageRefundQuantityExceedsAvailableBalance": "Refund by quantity exceeds maximum available items to refund.",
"messageRefundQuantityIsZero": "Refund by quantity must be greater than 0.",
"messagePaymentMethodDoesNotSupportRefund": "Payment method does not support online refunds."
}
},
"transactionHistory": {
@@ -78,9 +78,13 @@
"successTitle": "Succès",
"maxAvailableItemsToRefund": "Nombre maximum d'articles disponibles pour le remboursement",
"maxAvailableAmountToRefund": "Montant maximal disponible pour le remboursement",
"refundExceedsTotalError": {
"title": "Erreur lors de la création du remboursement.",
"messageRefundAmountExceedsAvailableBalance": "Le montant du remboursement dépasse le solde disponible."
"refundCreateError": {
"errorTitle": "Erreur lors de la création du remboursement.",
"messageRefundAmountExceedsAvailableBalance": "Le montant du remboursement dépasse le solde disponible.",
"messageRefundAmountIsZero": "Le montant du remboursement doit être supérieur à 0.",
"messageRefundQuantityExceedsAvailableBalance": "Le remboursement par quantité dépasse le nombre maximal darticles remboursables.",
"messageRefundQuantityIsZero": "Le remboursement par quantité doit être supérieur à 0.",
"messagePaymentMethodDoesNotSupportRefund": "Le mode de paiement ne prend pas en charge les remboursements en ligne."
}
},
"transactionHistory": {
@@ -78,9 +78,13 @@
"successTitle": "Successo",
"maxAvailableItemsToRefund": "Numero massimo di articoli disponibili da rimborsare",
"maxAvailableAmountToRefund": "Importo massimo disponibile per il rimborso",
"refundExceedsTotalError": {
"title": "Errore durante la creazione del rimborso.",
"messageRefundAmountExceedsAvailableBalance": "LL'importo del rimborso supera il saldo disponibile."
"refundCreateError": {
"errorTitle": "Errore durante la creazione del rimborso.",
"messageRefundAmountExceedsAvailableBalance": "LL'importo del rimborso supera il saldo disponibile.",
"messageRefundAmountIsZero": "L'importo del rimborso deve essere superiore a 0.",
"messageRefundQuantityExceedsAvailableBalance": "Il rimborso per quantità supera il numero massimo di articoli rimborsabili.",
"messageRefundQuantityIsZero": "Il rimborso per quantità deve essere maggiore di 0.",
"messagePaymentMethodDoesNotSupportRefund": "Il metodo di pagamento non supporta i rimborsi online."
}
},
"transactionHistory": {
@@ -7,16 +7,16 @@
:inheritedValue="selectedSalesChannelId == null ? null : allConfigs['null'][CONFIG_STOREFRONT_WEBHOOKS_UPDATE_ENABLED]"
:customInheritationCheckFunction="checkBoolFieldInheritance">
<template #content="props">
<mt-switch
<sw-switch-field
:name="CONFIG_STOREFRONT_WEBHOOKS_UPDATE_ENABLED"
bordered
:mapInheritance="props"
:label="$tc('vrpayment-settings.settingForm.advancedOptions.webhooksUpdateEnabled.label')"
:helpText="$tc('vrpayment-settings.settingForm.advancedOptions.webhooksUpdateEnabled.tooltipText')"
:disabled="props.isInherited"
:checked="props.currentValue"
@update:checked="props.updateCurrentValue">
</mt-switch>
:value="props.currentValue"
@update:value="props.updateCurrentValue">
</sw-switch-field>
</template>
</sw-inherit-wrapper>
@@ -25,16 +25,16 @@
:inheritedValue="selectedSalesChannelId == null ? null : allConfigs['null'][CONFIG_STOREFRONT_PAYMENTS_UPDATE_ENABLED]"
:customInheritationCheckFunction="checkBoolFieldInheritance">
<template #content="props">
<mt-switch
<sw-switch-field
:name="CONFIG_STOREFRONT_PAYMENTS_UPDATE_ENABLED"
bordered
:mapInheritance="props"
:label="$tc('vrpayment-settings.settingForm.advancedOptions.paymentsUpdateEnabled.label')"
:helpText="$tc('vrpayment-settings.settingForm.advancedOptions.paymentsUpdateEnabled.tooltipText')"
:disabled="props.isInherited"
:checked="props.currentValue"
@update:checked="props.updateCurrentValue">
</mt-switch>
:value="props.currentValue"
@update:value="props.updateCurrentValue">
</sw-switch-field>
</template>
</sw-inherit-wrapper>
</div>
@@ -14,8 +14,8 @@
{% block vrpayment_settings_content_card_channel_config_credentials_card_container_settings_space_id %}
<sw-inherit-wrapper
v-model:value="actualConfigData[CONFIG_SPACE_ID]"
:inheritedValue="selectedSalesChannelId === null ? null : allConfigs['null'][CONFIG_SPACE_ID]"
:customInheritationCheckFunction="checkNumberFieldInheritance">
:inheritedValue="getInheritedValue(CONFIG_SPACE_ID)"
@update:value="onSwitchInput">
<template #content="props">
<mt-number-field
:name="CONFIG_SPACE_ID"
@@ -23,7 +23,7 @@
:mapInheritance="props"
:label="$tc('vrpayment-settings.settingForm.credentials.spaceId.label')"
:helpText="$tc('vrpayment-settings.settingForm.credentials.spaceId.tooltipText')"
:disabled="props.isInherited || !acl.can('vrpayment.editor')"
:disabled="!acl.can('vrpayment.editor')"
:model-value="props.currentValue"
:error="spaceIdErrorState"
@update:model-value="props.updateCurrentValue">
@@ -35,7 +35,7 @@
{% block vrpayment_settings_content_card_channel_config_credentials_card_container_settings_user_id %}
<sw-inherit-wrapper
v-model:value="actualConfigData[CONFIG_USER_ID]"
:inheritedValue="selectedSalesChannelId === null ? null : allConfigs['null'][CONFIG_USER_ID]"
:inheritedValue="getInheritedValue(CONFIG_USER_ID)"
:customInheritationCheckFunction="checkNumberFieldInheritance">
<template #content="props">
<mt-number-field
@@ -44,7 +44,7 @@
:mapInheritance="props"
:label="$tc('vrpayment-settings.settingForm.credentials.userId.label')"
:helpText="$tc('vrpayment-settings.settingForm.credentials.userId.tooltipText')"
:disabled="props.isInherited || !acl.can('vrpayment.editor')"
:disabled="!acl.can('vrpayment.editor')"
:model-value="props.currentValue"
:error="userIdErrorState"
@update:model-value="props.updateCurrentValue">
@@ -56,7 +56,7 @@
{% block vrpayment_settings_content_card_channel_config_credentials_card_container_settings_application_key %}
<sw-inherit-wrapper
v-model:value="actualConfigData[CONFIG_APPLICATION_KEY]"
:inheritedValue="selectedSalesChannelId === null ? null : allConfigs['null'][CONFIG_APPLICATION_KEY]"
:inheritedValue="getInheritedValue(CONFIG_APPLICATION_KEY)"
:customInheritationCheckFunction="checkTextFieldInheritance">
<template #content="props">
<mt-password-field
@@ -66,7 +66,7 @@
:mapInheritance="props"
:label="$tc('vrpayment-settings.settingForm.credentials.applicationKey.label')"
:helpText="$tc('vrpayment-settings.settingForm.credentials.applicationKey.tooltipText')"
:disabled="props.isInherited || !acl.can('vrpayment.editor')"
:disabled="!acl.can('vrpayment.editor')"
:model-value="props.currentValue"
:error="applicationKeyErrorState"
@update:model-value="props.updateCurrentValue">
@@ -6,7 +6,7 @@ import constants from '../../page/vrpayment-settings/configuration-constants'
const {Component, Mixin} = Shopware;
Component.register('sw-vrpayment-credentials', {
template: template,
template,
name: 'VRPaymentCredentials',
@@ -29,7 +29,9 @@ Component.register('sw-vrpayment-credentials', {
},
selectedSalesChannelId: {
required: true
type: [String, null],
required: false,
default: null
},
spaceIdFilled: {
type: Boolean,
@@ -68,38 +70,42 @@ Component.register('sw-vrpayment-credentials', {
};
},
computed: {
currentConfig() {
if (this.selectedSalesChannelId && this.allConfigs[this.selectedSalesChannelId]) {
return this.allConfigs[this.selectedSalesChannelId];
}
return this.allConfigs['null'] || {};
}
},
methods: {
checkTextFieldInheritance(value) {
return !value || value.length <= 0;
},
checkTextFieldInheritance(value) {
if (typeof value !== 'string') {
return true;
}
checkNumberFieldInheritance(value) {
return value == null || value === '';
},
return value.length <= 0;
},
checkNumberFieldInheritance(value) {
if (typeof value !== 'number') {
return true;
}
return value.length <= 0;
},
checkBoolFieldInheritance(value) {
return typeof value !== 'boolean';
},
checkBoolFieldInheritance(value) {
return typeof value !== 'boolean';
},
// Emits the 'check-api-connection-event' with the current API connection parameters.
// Used to trigger API connection testing from this component.
emitCheckApiConnectionEvent() {
const apiConnectionParams = {
spaceId: this.actualConfigData[constants.CONFIG_SPACE_ID],
userId: this.actualConfigData[constants.CONFIG_USER_ID],
applicationKey: this.actualConfigData[constants.CONFIG_APPLICATION_KEY]
spaceId: this.currentConfig[constants.CONFIG_SPACE_ID],
userId: this.currentConfig[constants.CONFIG_USER_ID],
applicationKey: this.currentConfig[constants.CONFIG_APPLICATION_KEY]
};
this.$emit('check-api-connection-event', apiConnectionParams);
},
getInheritedValue(key) {
return this.allConfigs['null']?.[key] ?? null;
}
}
});
@@ -55,16 +55,16 @@
:inheritedValue="selectedSalesChannelId == null ? null : allConfigs['null'][CONFIG_LINE_ITEM_CONSISTENCY_ENABLED]"
:customInheritationCheckFunction="checkBoolFieldInheritance">
<template #content="props">
<mt-switch
<sw-switch-field
:name="CONFIG_LINE_ITEM_CONSISTENCY_ENABLED"
bordered
:mapInheritance="props"
:label="$tc('vrpayment-settings.settingForm.options.lineItemConsistencyEnabled.label')"
:helpText="$tc('vrpayment-settings.settingForm.options.lineItemConsistencyEnabled.tooltipText')"
:disabled="props.isInherited"
:checked="props.currentValue"
@update:checked="props.updateCurrentValue">
</mt-switch>
:value="props.currentValue"
@update:value="props.updateCurrentValue">
</sw-switch-field>
</template>
</sw-inherit-wrapper>
{% endblock %}
@@ -75,16 +75,16 @@
:inheritedValue="selectedSalesChannelId == null ? null : allConfigs['null'][CONFIG_EMAIL_ENABLED]"
:customInheritationCheckFunction="checkBoolFieldInheritance">
<template #content="props">
<mt-switch
<sw-switch-field
:name="CONFIG_EMAIL_ENABLED"
bordered
:mapInheritance="props"
:label="$tc('vrpayment-settings.settingForm.options.emailEnabled.label')"
:helpText="$tc('vrpayment-settings.settingForm.options.emailEnabled.tooltipText')"
:disabled="props.isInherited"
:checked="props.currentValue"
@update:checked="props.updateCurrentValue">
</mt-switch>
:value="props.currentValue"
@update:value="props.updateCurrentValue">
</sw-switch-field>
</template>
</sw-inherit-wrapper>
{% endblock %}
@@ -7,16 +7,16 @@
:inheritedValue="selectedSalesChannelId == null ? null : allConfigs['null'][CONFIG_STOREFRONT_INVOICE_DOWNLOAD_ENABLED]"
:customInheritationCheckFunction="checkBoolFieldInheritance">
<template #content="props">
<mt-switch
<sw-switch-field
:name="CONFIG_STOREFRONT_INVOICE_DOWNLOAD_ENABLED"
bordered
:mapInheritance="props"
:label="$tc('vrpayment-settings.settingForm.storefrontOptions.invoiceDownloadEnabled.label')"
:helpText="$tc('vrpayment-settings.settingForm.storefrontOptions.invoiceDownloadEnabled.tooltipText')"
:disabled="props.isInherited"
:checked="props.currentValue"
@update:checked="props.updateCurrentValue">
</mt-switch>
:value="props.currentValue"
@update:value="props.updateCurrentValue">
</sw-switch-field>
</template>
</sw-inherit-wrapper>
</div>
@@ -10,6 +10,9 @@ export const CONFIG_USER_ID = CONFIG_DOMAIN + '.' + 'userId';
export const CONFIG_STOREFRONT_WEBHOOKS_UPDATE_ENABLED = CONFIG_DOMAIN + '.' + 'storefrontWebhooksUpdateEnabled';
export const CONFIG_STOREFRONT_PAYMENTS_UPDATE_ENABLED = CONFIG_DOMAIN + '.' + 'storefrontPaymentsUpdateEnabled';
// References vendor/shopware/core/Defaults.php::SALES_CHANNEL_TYPE_STOREFRONT
export const STOREFRONT_SALES_CHANNEL_TYPE_ID = '8a243080f92e4c719546314b577cf82b';
export default {
CONFIG_DOMAIN,
CONFIG_APPLICATION_KEY,
@@ -21,5 +24,6 @@ export default {
CONFIG_STOREFRONT_INVOICE_DOWNLOAD_ENABLED,
CONFIG_USER_ID,
CONFIG_STOREFRONT_WEBHOOKS_UPDATE_ENABLED,
CONFIG_STOREFRONT_PAYMENTS_UPDATE_ENABLED
};
CONFIG_STOREFRONT_PAYMENTS_UPDATE_ENABLED,
STOREFRONT_SALES_CHANNEL_TYPE_ID
};
@@ -1,146 +1,142 @@
{% block vrpayment_settings %}
<sw-page class="vrpayment-settings">
<sw-page class="vrpayment-settings">
{% block vrpayment_settings_header %}
<template #smart-bar-header>
<h2>
{{ $tc('sw-settings.index.title') }}
<mt-icon name="small-arrow-medium-right" size="16px"></mt-icon>
{{ $tc('vrpayment-settings.header') }}
</h2>
</template>
{% endblock %}
{% block vrpayment_settings_header %}
<template #smart-bar-header>
<h2>
{{ $tc('sw-settings.index.title') }}
<mt-icon name="small-arrow-medium-right" size="16px"></mt-icon>
{{ $tc('vrpayment-settings.header') }}
</h2>
</template>
{% endblock %}
{% block vrpayment_settings_actions %}
<template #smart-bar-actions>
{% block vrpayment_settings_actions_save %}
<mt-button
v-model:value="isSaveSuccessful"
class="sw-settings-login-registration__save-action"
variant="primary"
:isLoading="isLoading"
:disabled="isLoading"
@click="onSave">
{{ $tc('vrpayment-settings.settingForm.save') }}
</mt-button>
{% endblock %}
</template>
{% endblock %}
{% block vrpayment_settings_actions %}
<template #smart-bar-actions>
{% block vrpayment_settings_actions_save %}
<mt-button
v-model:value="isSaveSuccessful"
class="sw-settings-login-registration__save-action"
variant="primary"
:isLoading="isLoading"
:disabled="isLoading"
@click="onSave">
{{ $tc('vrpayment-settings.settingForm.save') }}
</mt-button>
{% endblock %}
</template>
{% endblock %}
{% block vrpayment_settings_content %}
<template #content>
{% block vrpayment_settings_content %}
<template #content>
{% block vrpayment_settings_content_card %}
<mt-card-view>
{% block vrpayment_settings_content_card %}
<mt-card-view>
{% block vrpayment_settings_content_card_channel_config %}
<sw-sales-channel-config v-model:value="config"
ref="configComponent"
:domain="CONFIG_DOMAIN">
{% block vrpayment_settings_content_card_channel_config %}
<sw-sales-channel-config v-model:value="config"
v-model:selectedSalesChannelId="selectedSalesChannelId"
ref="configComponent"
:domain="CONFIG_DOMAIN">
{% block vrpayment_settings_content_card_channel_config_sales_channel %}
<template #select="{ onInput, selectedSalesChannelId, salesChannel }">
{% block vrpayment_settings_content_card_channel_config_sales_channel %}
<template #select="{ onInput, selectedSalesChannelId, salesChannel }">
{% block vrpayment_settings_content_card_channel_config_sales_channel_card %}
<mt-card title="Sales Channel Switch">
{% block vrpayment_settings_content_card_channel_config_sales_channel_card %}
<mt-card title="Sales Channel Switch">
{% block vrpayment_settings_content_card_channel_config_sales_channel_card_title %}
<sw-entity-single-select
v-model:value="selectedSalesChannelId"
labelProperty="translated.name"
valueProperty="id"
:mapInheritance="props"
:isLoading="isLoading"
entity="sales_channel"
@update:value="onInput">
</sw-entity-single-select>
{% endblock %}
{% block vrpayment_settings_content_card_channel_config_sales_channel_card_footer %}
<template #footer>
{% block vrpayment_settings_content_card_channel_config_sales_channel_card_title %}
<sw-sales-channel-switch
ref="channelSwitch"
@change-sales-channel-id="onSalesChannelSwitchChange($event, onInput)">
</sw-sales-channel-switch>
{% endblock %}
{% block vrpayment_settings_content_card_channel_config_sales_channel_card_footer %}
<template #footer>
{% block vrpayment_settings_content_card_channel_config_sales_channel_card_footer_container %}
<sw-container columns="2fr 1fr" gap="0px 30px">
{% block vrpayment_settings_content_card_channel_config_sales_channel_card_footer_container %}
<sw-container columns="2fr 1fr" gap="0px 30px">
{% block vrpayment_settings_content_card_channel_config_sales_channel_card_footer_container_text %}
<p>{{ $tc('vrpayment-settings.salesChannelCard.button.description') }}</p>
{% endblock %}
{% block vrpayment_settings_content_card_channel_config_sales_channel_card_footer_container_text %}
<p>{{ $tc('vrpayment-settings.salesChannelCard.button.description') }}</p>
{% endblock %}
{% block vrpayment_settings_content_card_channel_config_sales_channel_card_footer_container_button %}
<sw-button
variant="primary"
v-model:value="isSetDefaultPaymentSuccessful"
:isLoading="isSettingDefaultPaymentMethods"
@click="onSetPaymentMethodDefault">
{{ $tc('vrpayment-settings.salesChannelCard.button.label') }}
</sw-button>
{% endblock %}
</sw-container>
{% endblock %}
</template>
{% endblock %}
</mt-card>
{% endblock %}
</template>
{% endblock %}
{% block vrpayment_settings_content_card_channel_config_sales_channel_card_footer_container_button %}
<sw-button
variant="primary"
v-model:value="isSetDefaultPaymentSuccessful"
:isLoading="isSettingDefaultPaymentMethods"
@click="onSetPaymentMethodDefault">
{{ $tc('vrpayment-settings.salesChannelCard.button.label') }}
</sw-button>
{% endblock %}
</sw-container>
{% endblock %}
</template>
{% endblock %}
</mt-card>
{% endblock %}
</template>
{% endblock %}
{% block vrpayment_settings_content_card_channel_config_cards %}
<template #content="{ actualConfigData, allConfigs, selectedSalesChannelId }">
<div v-if="actualConfigData">
{% block vrpayment_settings_content_card_channel_config_cards %}
<template #content="{ actualConfigData, allConfigs, selectedSalesChannelId }">
<div v-if="actualConfigData">
<sw-vrpayment-credentials
:actualConfigData="actualConfigData"
:allConfigs="allConfigs"
:selectedSalesChannelId="selectedSalesChannelId"
:spaceIdErrorState="spaceIdErrorState"
:userIdErrorState="userIdErrorState"
:applicationKeyErrorState="applicationKeyErrorState"
:spaceIdFilled="spaceIdFilled"
:userIdFilled="userIdFilled"
:applicationKeyFilled="applicationKeyFilled"
:isLoading="isLoading"
:isTesting="isTesting"
@check-api-connection-event="onCheckApiConnection"
></sw-vrpayment-credentials>
<sw-vrpayment-credentials
:actualConfigData="actualConfigData"
:allConfigs="allConfigs"
:selectedSalesChannelId="selectedSalesChannelId"
:spaceIdErrorState="spaceIdErrorState"
:userIdErrorState="userIdErrorState"
:applicationKeyErrorState="applicationKeyErrorState"
:spaceIdFilled="spaceIdFilled"
:userIdFilled="userIdFilled"
:applicationKeyFilled="applicationKeyFilled"
:isLoading="isLoading"
:isTesting="isTesting"
@check-api-connection-event="onCheckApiConnection"
></sw-vrpayment-credentials>
<sw-vrpayment-options
:actualConfigData="actualConfigData"
:allConfigs="allConfigs"
:isLoading="isLoading"
:selectedSalesChannelId="selectedSalesChannelId"
>
</sw-vrpayment-options>
<sw-vrpayment-options
:actualConfigData="actualConfigData"
:allConfigs="allConfigs"
:isLoading="isLoading"
:selectedSalesChannelId="selectedSalesChannelId"
>
</sw-vrpayment-options>
<sw-vrpayment-storefront-options
:actualConfigData="actualConfigData"
:allConfigs="allConfigs"
:isLoading="isLoading"
:selectedSalesChannelId="selectedSalesChannelId"
>
</sw-vrpayment-storefront-options>
<sw-vrpayment-storefront-options
:actualConfigData="actualConfigData"
:allConfigs="allConfigs"
:isLoading="isLoading"
:selectedSalesChannelId="selectedSalesChannelId"
>
</sw-vrpayment-storefront-options>
<sw-vrpayment-advanced-options
:actualConfigData="actualConfigData"
:allConfigs="allConfigs"
:isLoading="isLoading"
:selectedSalesChannelId="selectedSalesChannelId"
>
</sw-vrpayment-advanced-options>
<sw-vrpayment-advanced-options
:actualConfigData="actualConfigData"
:allConfigs="allConfigs"
:isLoading="isLoading"
:selectedSalesChannelId="selectedSalesChannelId"
>
</sw-vrpayment-advanced-options>
</div>
</template>
{% endblock %}
</div>
</template>
{% endblock %}
</sw-sales-channel-config>
{% endblock %}
</sw-sales-channel-config>
{% endblock %}
{% block vrpayment_settings_content_card_loading %}
<mt-loader v-if="isLoading"></mt-loader>
{% endblock %}
</mt-card-view>
{% endblock %}
{% block vrpayment_settings_content_card_loading %}
<mt-loader v-if="isLoading"></mt-loader>
{% endblock %}
</mt-card-view>
{% endblock %}
</template>
{% endblock %}
</sw-page>
</template>
{% endblock %}
</sw-page>
{% endblock %}
@@ -3,7 +3,7 @@
import template from './index.html.twig';
import constants from './configuration-constants';
const {Component, Mixin} = Shopware;
const { Component, Mixin } = Shopware;
Component.register('vrpayment-settings', {
@@ -11,7 +11,8 @@ Component.register('vrpayment-settings', {
inject: [
'acl',
'VRPaymentConfigurationService'
'VRPaymentConfigurationService',
'repositoryFactory'
],
mixins: [
@@ -40,6 +41,7 @@ Component.register('vrpayment-settings', {
isSetDefaultPaymentSuccessful: false,
isSettingDefaultPaymentMethods: false,
selectedSalesChannelId: null,
configIntegrationDefaultValue: 'payment_page',
configEmailEnabledDefaultValue: true,
@@ -68,8 +70,8 @@ Component.register('vrpayment-settings', {
watch: {
config: {
handler(configData) {
const defaultConfig = this.$refs.configComponent.allConfigs.null;
const salesChannelId = this.$refs.configComponent.selectedSalesChannelId;
const defaultConfig = (this.$refs.configComponent.allConfigs || {}).null || {};
const salesChannelId = this.selectedSalesChannelId;
if (salesChannelId === null) {
this.applicationKeyFilled = !!this.config[this.CONFIG_APPLICATION_KEY];
@@ -136,6 +138,16 @@ Component.register('vrpayment-settings', {
this.$emit('update:value', configData);
},
deep: true
},
selectedSalesChannelId: {
handler(newValue) {
this.$nextTick(() => {
if (this.$refs.channelSwitch) {
this.$refs.channelSwitch.salesChannelId = newValue || '';
}
});
}
}
},
@@ -161,21 +173,93 @@ Component.register('vrpayment-settings', {
},
getInheritValue(key) {
if (this.selectedSalesChannelId == null ) {
if (this.selectedSalesChannelId == null) {
return this.actualConfigData[key];
} else {
return this.allConfigs['null'][key];
}
},
onSave() {
async onSave() {
if (!(this.spaceIdFilled && this.userIdFilled && this.applicationKeyFilled)) {
this.setErrorStates();
return;
}
this.isLoading = true;
const validationError = await this.validateHeadlessIntegration();
if (validationError === 'HEADLESS') {
this.createNotificationError({
title: this.$tc('vrpayment-settings.settingForm.titleError'),
message: this.$tc('vrpayment-settings.settingForm.messageHeadlessIntegrationError')
});
this.isLoading = false;
return;
} else if (validationError === 'GLOBAL') {
this.createNotificationError({
title: this.$tc('vrpayment-settings.settingForm.titleError'),
message: this.$tc('vrpayment-settings.settingForm.messageGlobalIframeError')
});
this.isLoading = false;
return;
}
this.save();
},
async validateHeadlessIntegration() {
const salesChannelId = this.selectedSalesChannelId;
const currentIntegration = this.config[this.CONFIG_INTEGRATION];
// If integration is 'payment_page', it is always valid.
if (currentIntegration === 'payment_page') {
return null;
}
const salesChannelRepo = this.repositoryFactory.create('sales_channel');
try {
if (salesChannelId) {
// Specific Sales Channel Check
const salesChannel = await salesChannelRepo.get(salesChannelId, Shopware.Context.api);
const currentTypeId = salesChannel.typeId.replace(/-/g, '');
// REST-2: Inverted Logic
// We only allow 'iframe' integration if the Sales Channel is of type 'Storefront'.
// Any other type (Headless, Product Export, Custom, etc.) does not support Iframe injection.
const isStorefront = currentTypeId === constants.STOREFRONT_SALES_CHANNEL_TYPE_ID;
if (!isStorefront) {
return 'HEADLESS';
}
} else {
// Global Scope ("All Sales Channels") Check
// We must check if there is ANY Sales Channel that is NOT Storefront.
// If so, we cannot allow "Iframe" globally, as it would break those channels.
const criteria = new Shopware.Data.Criteria();
criteria.addFilter(
Shopware.Data.Criteria.not(
'AND',
[Shopware.Data.Criteria.equals('typeId', constants.STOREFRONT_SALES_CHANNEL_TYPE_ID)]
)
);
criteria.setLimit(1); // We only need to know if at least one exists
const result = await salesChannelRepo.search(criteria, Shopware.Context.api);
if (result.total > 0) {
return 'GLOBAL';
}
}
return null;
} catch (e) {
console.error(e);
return null;
}
},
save() {
this.isLoading = true;
@@ -197,7 +281,7 @@ Component.register('vrpayment-settings', {
return false;
}
this.VRPaymentConfigurationService.registerWebHooks(this.$refs.configComponent.selectedSalesChannelId)
this.VRPaymentConfigurationService.registerWebHooks(this.selectedSalesChannelId)
.then(() => {
this.createNotificationSuccess({
title: this.$tc('vrpayment-settings.settingForm.titleSuccess'),
@@ -210,7 +294,7 @@ Component.register('vrpayment-settings', {
});
this.isLoading = false;
console.error('Error:', e);
});
});
},
synchronizePaymentMethodConfiguration() {
@@ -218,7 +302,7 @@ Component.register('vrpayment-settings', {
return false;
}
this.VRPaymentConfigurationService.synchronizePaymentMethodConfiguration(this.$refs.configComponent.selectedSalesChannelId)
this.VRPaymentConfigurationService.synchronizePaymentMethodConfiguration(this.selectedSalesChannelId)
.then(() => {
this.createNotificationSuccess({
title: this.$tc('vrpayment-settings.settingForm.titleSuccess'),
@@ -232,10 +316,10 @@ Component.register('vrpayment-settings', {
});
this.isLoading = false;
console.error('Error:', e);
});
});
},
installOrderDeliveryStates(){
installOrderDeliveryStates() {
this.VRPaymentConfigurationService.installOrderDeliveryStates()
.then(() => {
this.createNotificationSuccess({
@@ -249,13 +333,13 @@ Component.register('vrpayment-settings', {
message: this.$tc('vrpayment-settings.settingForm.messageOrderDeliveryStateError')
});
this.isLoading = false;
});
});
},
onSetPaymentMethodDefault() {
this.isSettingDefaultPaymentMethods = true;
this.VRPaymentConfigurationService.setVRPaymentAsSalesChannelPaymentDefault(
this.$refs.configComponent.selectedSalesChannelId
this.selectedSalesChannelId
).then(() => {
this.isSettingDefaultPaymentMethods = false;
this.isSetDefaultPaymentSuccessful = true;
@@ -311,7 +395,14 @@ Component.register('vrpayment-settings', {
message: this.$tc('vrpayment-settings.settingForm.credentials.alert.errorMessage')
});
this.isTesting = false;
});
});
},
onSalesChannelSwitchChange(id, onInput) {
this.selectedSalesChannelId = id;
if (typeof onInput === 'function') {
onInput(id);
}
}
}
});
@@ -1,105 +1,107 @@
{
"sw-privileges": {
"permissions": {
"parents": {
"vrpayment": "VRPayment plugin"
},
"vrpayment": {
"label": "VRPayment permissions"
}
}
"permissions": {
"parents": {
"vrpayment": "VRPayment plugin"
},
"vrpayment": {
"label": "VRPayment permissions"
}
}
},
"vrpayment-settings": {
"general": {
"descriptionTextModule": "VRPayment settings",
"mainMenuItemGeneral": "VRPayment"
},
"header": "VRPayment",
"messageNotBlank": "This value should not be blank.",
"salesChannelCard": {
"button": {
"description": "Click this button to set VRPayment as default payment handler in the selected SalesChannel",
"label": "Set VRPayment as default payment handler"
},
"messageDefaultPaymentError": "VRPayment as default payment could not be set.",
"messageDefaultPaymentUpdated": "VRPayment as default payment has been set."
},
"settingForm": {
"credentials": {
"applicationKey": {
"label": "Application Key",
"tooltipText": "The Application Key is used to authenticate this plugin with the VRPayment API."
},
"cardTitle": "Credentials",
"spaceId": {
"label": "Space ID",
"tooltipText": "The space ID is used to authenticate this plugin with the VRPayment API."
},
"userId": {
"label": "User ID",
"tooltipText": "The user ID is used to authenticate this plugin with the VRPayment API."
},
"button": {
"description": "Click this button to test the VRPayment API",
"label": "API connection test"
},
"alert": {
"title": "API Test",
"successMessage": "The connection was successfully tested.",
"errorMessage": "The connection was failed. Try it again."
}
},
"messageSaveSuccess": "VRPayment settings have been saved.",
"messageOrderDeliveryStateError": "VRPayment OrderDeliveryState could not be saved.",
"messageOrderDeliveryStateUpdated": "VRPayment OrderDeliveryState has been updated.",
"messagePaymentMethodConfigurationError": "VRPayment PaymentMethodConfiguration could not be saved. Please check your credentials.",
"messagePaymentMethodConfigurationUpdated": "VRPayment PaymentMethodConfiguration has been registered.",
"messageWebHookError": "VRPayment WebHook could not be saved. Please check your credentials.",
"messageWebHookUpdated": "VRPayment WebHook has been updated.",
"options": {
"cardTitle": "Options",
"emailEnabled": {
"label": "Send order confirmation email",
"tooltipText": "If this setting is enabled your customers will receive an email from your store when their order payment is authorised"
},
"integration": {
"label": "Integration",
"options": {
"iframe": "Iframe",
"payment_page": "Payment Page"
},
"tooltipText": "Integration"
},
"lineItemConsistencyEnabled": {
"label": "Line item consistency",
"tooltipText": "If this option is enabled line item totals in VRPaymentPayment will always match Shopware order total"
},
"spaceViewId": {
"label": "Space View ID",
"tooltipText": "Space View ID"
}
},
"save": "Save",
"storefrontOptions": {
"cardTitle": "Storefront Options",
"invoiceDownloadEnabled": {
"label": "Invoice Download",
"tooltipText": "If this setting is enabled your customers will be able to download order invoices from VRPayment"
}
},
"advancedOptions": {
"cardTitle": "Advanced Options",
"webhooksUpdateEnabled": {
"label": "Webhooks Update",
"tooltipText": "If this setting is enabled webhook update will be triggered when you save settings"
},
"paymentsUpdateEnabled": {
"label": "Payments Update",
"tooltipText": "If this setting is enabled payment methods update will be triggered when you save settings"
}
},
"titleError": "Error",
"titleSuccess": "Success"
}
"general": {
"descriptionTextModule": "VRPayment settings",
"mainMenuItemGeneral": "VRPayment"
},
"header": "VRPayment",
"messageNotBlank": "This value should not be blank.",
"salesChannelCard": {
"button": {
"description": "Click this button to set VRPayment as default payment handler in the selected SalesChannel",
"label": "Set VRPayment as default payment handler"
},
"messageDefaultPaymentError": "VRPayment as default payment could not be set.",
"messageDefaultPaymentUpdated": "VRPayment as default payment has been set."
},
"settingForm": {
"credentials": {
"applicationKey": {
"label": "Application Key",
"tooltipText": "The Application Key is used to authenticate this plugin with the VRPayment API."
},
"cardTitle": "Credentials",
"spaceId": {
"label": "Space ID",
"tooltipText": "The space ID is used to authenticate this plugin with the VRPayment API."
},
"userId": {
"label": "User ID",
"tooltipText": "The user ID is used to authenticate this plugin with the VRPayment API."
},
"button": {
"description": "Click this button to test the VRPayment API",
"label": "API connection test"
},
"alert": {
"title": "API Test",
"successMessage": "The connection was successfully tested.",
"errorMessage": "The connection was failed. Try it again."
}
},
"messageSaveSuccess": "VRPayment settings have been saved.",
"messageOrderDeliveryStateError": "VRPayment OrderDeliveryState could not be saved.",
"messageOrderDeliveryStateUpdated": "VRPayment OrderDeliveryState has been updated.",
"messagePaymentMethodConfigurationError": "VRPayment PaymentMethodConfiguration could not be saved. Please check your credentials.",
"messagePaymentMethodConfigurationUpdated": "VRPayment PaymentMethodConfiguration has been registered.",
"messageWebHookError": "VRPayment WebHook could not be saved. Please check your credentials.",
"messageWebHookUpdated": "VRPayment WebHook has been updated.",
"messageHeadlessIntegrationError": "Iframe integration is only supported for Storefront Sales Channels.",
"messageGlobalIframeError": "Iframe integration cannot be set globally because you have non-Storefront Sales Channels.",
"options": {
"cardTitle": "Options",
"emailEnabled": {
"label": "Send order confirmation email",
"tooltipText": "If this setting is enabled your customers will receive an email from your store when their order payment is authorised"
},
"integration": {
"label": "Integration",
"options": {
"iframe": "Iframe",
"payment_page": "Payment Page"
},
"tooltipText": "Integration"
},
"lineItemConsistencyEnabled": {
"label": "Line item consistency",
"tooltipText": "If this option is enabled line item totals in VRPaymentPayment will always match Shopware order total"
},
"spaceViewId": {
"label": "Space View ID",
"tooltipText": "Space View ID"
}
},
"save": "Save",
"storefrontOptions": {
"cardTitle": "Storefront Options",
"invoiceDownloadEnabled": {
"label": "Invoice Download",
"tooltipText": "If this setting is enabled your customers will be able to download order invoices from VRPayment"
}
},
"advancedOptions": {
"cardTitle": "Advanced Options",
"webhooksUpdateEnabled": {
"label": "Webhooks Update",
"tooltipText": "If this setting is enabled webhook update will be triggered when you save settings"
},
"paymentsUpdateEnabled": {
"label": "Payments Update",
"tooltipText": "If this setting is enabled payment methods update will be triggered when you save settings"
}
},
"titleError": "Error",
"titleSuccess": "Success"
}
}
}
File diff suppressed because one or more lines are too long
@@ -3,25 +3,170 @@
// noinspection NpmUsedModulesInstalled
import Plugin from 'src/plugin-system/plugin.class';
import HttpClient from 'src/service/http-client.service';
import DomAccess from 'src/helper/dom-access.helper';
/**
* VRPaymentCheckoutPlugin
*
* This plugin handles the initialization and lifecycle of the WhitelabelMachineName payment iframe
* within the Shopware 6 checkout confirm page.
*/
class VRPaymentCheckoutPlugin extends Plugin {
static options = {
payment_method_tabs: 'ul.vrpayment-payment-panel li',
payment_method_iframe_prefix: 'iframe_payment_method_',
payment_method_iframe_class: '.vrpayment-payment-iframe',
payment_method_handler_name: 'vrpayment_payment_handler',
payment_method_handler_prefix: 'vrpayment_handler_',
payment_panel_id: 'vrpayment-payment-panel',
payment_method_iframe_id: 'vrpayment-payment-iframe',
payment_method_handler_status: 'input[name="vrpayment_payment_handler_validation_status"]',
payment_form: 'confirmOrderForm',
payment_form_id: 'confirmOrderForm',
loader_id: 'vrpaymentLoader',
checkout_url_id: 'checkoutUrl',
cart_recreate_url_id: 'cartRecreateUrl',
};
/**
* Initializes the plugin, variables, and events.
*/
init() {
// @TODO Move JS to Plugin
this._client = new HttpClient(window.accessKey);
try {
this._initVariables();
this._registerEvents();
this._getIframe();
} catch (e) {
// Silently fail if elements are not found; this allows the plugin to be loaded on pages where it might be conditionally absent.
}
}
/**
* Finds and stores references to relevant DOM elements.
* @private
*/
_initVariables() {
this.checkoutUrl = DomAccess.getElement(document, `#${this.options.checkout_url_id}`).value;
this.cartRecreateUrl = DomAccess.getElement(document, `#${this.options.cart_recreate_url_id}`).value;
this.paymentForm = DomAccess.getElement(document, `#${this.options.payment_form_id}`);
this.paymentPanel = this.el;
this.iframeContainer = DomAccess.getElement(this.paymentPanel, `#${this.options.payment_method_iframe_id}`);
this.handler = null;
}
/**
* Registers event listeners for the payment form and browser history.
* @private
*/
_registerEvents() {
this.paymentForm.addEventListener('submit', this._submitPayment.bind(this), false);
// Handle back button/popstate to ensure cart consistency
window.addEventListener('popstate', this._onPopstate.bind(this), false);
window.history.pushState({}, document.title, this.cartRecreateUrl);
window.history.pushState({}, document.title, this.checkoutUrl);
}
/**
* Handles browser back button activity.
* @private
*/
_onPopstate() {
if (window.history.state == null) {
return;
}
window.location.href = this.cartRecreateUrl;
}
/**
* Intercepts the form submission to validate the iframe content first.
* @param {Event} event
* @private
*/
_submitPayment(event) {
this._activateLoader(true);
if (this.handler) {
this.handler.validate();
event.preventDefault();
return false;
}
}
/**
* Initializes the WhitelabelMachineName Iframe handler and creates the iframe.
* @private
*/
_getIframe() {
const paymentMethodConfigurationId = this.paymentPanel.dataset.id;
if (!this.handler) {
// IframeCheckoutHandler is expected to be global from the SDK script
if (typeof window.IframeCheckoutHandler === 'function') {
this.handler = window.IframeCheckoutHandler(paymentMethodConfigurationId);
this.handler.setValidationCallback(this._validationCallBack.bind(this));
this.handler.setInitializeCallback(this._hideLoader.bind(this));
this.handler.setHeightChangeCallback((height) => {
if (height < 1) {
this.handler.submit();
}
});
this.handler.create(this.iframeContainer);
setTimeout(this._hideLoader.bind(this), 10000);
}
}
}
/**
* Callback for iframe validation results.
* @param {Object} validationResult
* @private
*/
_validationCallBack(validationResult) {
const statusInputs = document.querySelectorAll(this.options.payment_method_handler_status);
if (validationResult.success) {
statusInputs.forEach(input => {
input.value = 'true';
});
this.handler.submit();
} else {
statusInputs.forEach(input => {
input.value = 'false';
});
this._activateLoader(false);
if (validationResult.errors) {
this._showErrors(validationResult.errors);
}
}
}
/**
* Enables or disables buttons on the page to indicate loading.
* @param {boolean} activate
* @private
*/
_activateLoader(activate) {
const buttons = document.querySelectorAll('button');
buttons.forEach(button => {
button.disabled = activate;
});
}
/**
* Hides the loader overlay once the iframe is ready.
* @private
*/
_hideLoader() {
const loader = document.getElementById(this.options.loader_id);
if (loader && loader.parentNode) {
loader.parentNode.removeChild(loader);
}
this._activateLoader(false);
}
/**
* Displays validation errors to the user.
* @param {Array} errors
* @private
*/
_showErrors(errors) {
// Fallback to alert if no native Shopware mechanism is easily accessible here
alert(errors.join('\n'));
}
}
export default VRPaymentCheckoutPlugin;
export default VRPaymentCheckoutPlugin;
+3 -1
View File
@@ -13,9 +13,11 @@
<import resource="./services/core/storefront/checkout.xml"/>
<import resource="./services/core/checkout.xml"/>
<import resource="./services/core/checkout_services.xml"/>
<import resource="./services/core/settings.xml"/>
<import resource="./services/core/store_api.xml"/>
<import resource="./services/core/util.xml"/>
</imports>
<services>
</services>
</container>
</container>
@@ -10,6 +10,7 @@
<service id="VRPaymentPayment\Core\Api\Refund\Controller\RefundController" public="true">
<argument type="service" id="VRPaymentPayment\Core\Api\Refund\Service\RefundService"/>
<argument type="service" id="VRPaymentPayment\Core\Settings\Service\SettingsService"/>
<argument type="service" id="VRPaymentPayment\Core\Api\Transaction\Service\TransactionService"/>
<call method="setLogger">
<argument type="service" id="monolog.logger.vrpayment_payment"/>
</call>
@@ -58,6 +58,8 @@
<argument type="service" id="service_container"/>
<argument type="service" id="VRPaymentPayment\Core\Util\LocaleCodeProvider"/>
<argument type="service" id="VRPaymentPayment\Core\Settings\Service\SettingsService"/>
<!-- Cache for headless transaction persistence -->
<argument type="service" id="cache.system"/>
<call method="setLogger">
<argument type="service" id="monolog.logger.vrpayment_payment"/>
</call>
@@ -0,0 +1,47 @@
<?xml version="1.0" ?>
<container xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://symfony.com/schema/dic/services"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<service id="VRPaymentPayment\Core\Checkout\Service\TransactionManagementService">
<argument type="service" id="VRPaymentPayment\Core\Api\Transaction\Service\TransactionService"/>
<argument type="service" id="VRPaymentPayment\Core\Settings\Service\SettingsService"/>
<!-- Cache for headless transaction persistence -->
<argument type="service" id="cache.system"/>
</service>
<service id="VRPaymentPayment\Core\Checkout\Service\PaymentMethodFilterService">
<argument type="service" id="VRPaymentPayment\Core\Settings\Service\SettingsService"/>
<argument type="service" id="VRPaymentPayment\Core\Api\Transaction\Service\TransactionService"/>
<argument type="service" id="VRPaymentPayment\Core\Api\PaymentMethodConfiguration\Service\PaymentMethodConfigurationService"/>
<argument type="service" id="VRPaymentPayment\Core\Util\PaymentMethodUtil"/>
<argument type="service" id="VRPaymentPayment\Core\Checkout\Service\TransactionManagementService"/>
<argument type="service" id="Shopware\Core\Checkout\Cart\SalesChannel\CartService"/>
<call method="setLogger">
<argument type="service" id="monolog.logger.vrpayment_payment"/>
</call>
</service>
<service id="VRPaymentPayment\Core\Checkout\Service\PaymentIntegrationService">
<argument type="service" id="VRPaymentPayment\Core\Api\Transaction\Service\TransactionService"/>
<argument type="service" id="VRPaymentPayment\Core\Settings\Service\SettingsService"/>
<argument type="service" id="VRPaymentPayment\Core\Checkout\Service\TransactionManagementService"/>
<argument type="service" id="router"/>
<argument type="service" id="VRPaymentPayment\Core\Util\LocaleCodeProvider"/>
</service>
<service id="VRPaymentPayment\Core\Checkout\Service\CartRecoveryService">
<argument type="service" id="Shopware\Core\Checkout\Cart\SalesChannel\CartService"/>
<argument type="service" id="Shopware\Core\Checkout\Cart\LineItemFactoryRegistry"/>
<argument type="service" id="order.repository"/>
<argument type="service" id="Swag\CustomizedProducts\Core\Checkout\Cart\Route\AddCustomizedProductsToCartRoute" on-invalid="null"/>
</service>
<service id="VRPaymentPayment\Core\Checkout\Service\InvoiceService">
<argument type="service" id="VRPaymentPayment\Core\Settings\Service\SettingsService"/>
<argument type="service" id="VRPaymentPayment\Core\Api\Transaction\Service\TransactionService"/>
</service>
</services>
</container>
@@ -0,0 +1,28 @@
<?xml version="1.0" ?>
<container xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://symfony.com/schema/dic/services"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<service id="VRPaymentPayment\Core\Checkout\PaymentMethod\SalesChannel\PaymentMethodRouteDecorator"
decorates="Shopware\Core\Checkout\Payment\SalesChannel\PaymentMethodRoute"
decoration-inner-name="VRPaymentPayment\Core\Checkout\PaymentMethod\SalesChannel\PaymentMethodRouteDecorator.inner">
<argument type="service" id="VRPaymentPayment\Core\Checkout\PaymentMethod\SalesChannel\PaymentMethodRouteDecorator.inner"/>
<argument type="service" id="VRPaymentPayment\Core\Checkout\Service\PaymentMethodFilterService"/>
</service>
<service id="VRPaymentPayment\Core\Checkout\StoreApi\Route\WhitelabelMachineNameTransactionInfoRoute">
<argument type="service" id="VRPaymentPayment\Core\Checkout\Service\PaymentIntegrationService"/>
</service>
<service id="VRPaymentPayment\Core\Checkout\StoreApi\Route\CartRecoveryRoute">
<argument type="service" id="VRPaymentPayment\Core\Checkout\Service\CartRecoveryService"/>
<argument type="service" id="VRPaymentPayment\Core\Api\Transaction\Service\TransactionService"/>
</service>
<service id="VRPaymentPayment\Core\Checkout\StoreApi\Route\InvoiceRoute">
<argument type="service" id="VRPaymentPayment\Core\Checkout\Service\InvoiceService"/>
</service>
</services>
</container>
@@ -7,9 +7,9 @@
<services>
<!-- Controllers -->
<service id="VRPaymentPayment\Core\Storefront\Account\Controller\AccountOrderController" public="true">
<argument type="service" id="VRPaymentPayment\Core\Settings\Service\SettingsService"/>
<argument type="service" id="VRPaymentPayment\Core\Api\Transaction\Service\TransactionService"/>
<argument type="service" id="Symfony\Component\HttpFoundation\RequestStack"/>
<argument type="service" id="VRPaymentPayment\Core\Checkout\Service\InvoiceService"/>
<call method="setLogger">
<argument type="service" id="monolog.logger.vrpayment_payment"/>
</call>
@@ -7,30 +7,25 @@
<services>
<!-- Controllers -->
<service id="VRPaymentPayment\Core\Storefront\Checkout\Controller\CheckoutController" public="true">
<argument type="service" id="Shopware\Core\Checkout\Cart\LineItemFactoryRegistry"/>
<argument type="service" id="Shopware\Core\Checkout\Cart\SalesChannel\CartService"/>
<argument type="service" id="VRPaymentPayment\Core\Settings\Service\SettingsService"/>
<argument type="service" id="VRPaymentPayment\Core\Api\Transaction\Service\TransactionService"/>
<argument type="service" id="Shopware\Storefront\Page\GenericPageLoader"/>
<argument type="service" id="Shopware\Core\Checkout\Order\SalesChannel\OrderRoute"/>
<argument type="service" id="VRPaymentPayment\Core\Checkout\Service\CartRecoveryService"/>
<argument type="service" id="VRPaymentPayment\Core\Checkout\Service\PaymentIntegrationService"/>
<argument type="service" id="VRPaymentPayment\Core\Api\Transaction\Service\TransactionService"/>
<call method="setLogger">
<argument type="service" id="monolog.logger.vrpayment_payment"/>
</call>
<call method="setContainer">
<argument type="service" id="service_container"/>
</call>
<!-- Removed in 6.7 -->
<!-- <call method="setTwig">
<argument type="service" id="twig"/>
</call> -->
</service>
<!-- Subscribers -->
<service id="VRPaymentPayment\Core\Storefront\Checkout\Subscriber\CheckoutSubscriber">
<argument id="VRPaymentPayment\Core\Api\PaymentMethodConfiguration\Service\PaymentMethodConfigurationService" type="service"/>
<argument id="VRPaymentPayment\Core\Api\Transaction\Service\TransactionService" type="service"/>
<argument id="VRPaymentPayment\Core\Settings\Service\SettingsService" type="service"/>
<argument id="VRPaymentPayment\Core\Util\PaymentMethodUtil" type="service"/>
<argument id="VRPaymentPayment\Core\Checkout\Service\PaymentMethodFilterService" type="service"/>
<argument id="VRPaymentPayment\Core\Checkout\Service\PaymentIntegrationService" type="service"/>
<call method="setLogger">
<argument type="service" id="monolog.logger.vrpayment_payment"/>
</call>
@@ -0,0 +1,25 @@
{
"base": "/bundles/vrpaymentpayment/administration/",
"entryPoints": {
"v-r-payment-payment": {
"css": [
"/bundles/vrpaymentpayment/administration/assets/v-r-payment-payment-D4AH6HY2.css"
],
"dynamic": [],
"js": [
"/bundles/vrpaymentpayment/administration/assets/v-r-payment-payment-CPfpiGQp.js"
],
"legacy": false,
"preload": []
}
},
"legacy": false,
"metadatas": {},
"version": [
"7.1.0",
7,
1,
0
],
"viteServer": null
}
@@ -0,0 +1,11 @@
{
"main.js": {
"file": "assets/v-r-payment-payment-CPfpiGQp.js",
"name": "v-r-payment-payment",
"src": "main.js",
"isEntry": true,
"css": [
"assets/v-r-payment-payment-D4AH6HY2.css"
]
}
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
.sw-order-detail .sw-tabs{margin-top:40px}.sw-order-detail .sw-order-detail-base .mt-card-view__content{overflow-x:visible;overflow-y:visible}.vrpayment-order-detail__data{display:grid}.vrpayment-order-detail__heading{padding-top:15px}
@@ -0,0 +1,20 @@
{% sw_extends '@Storefront/storefront/component/payment/payment-method.html.twig' %}
{% block component_payment_method_description %}
{{ parent() }}
{# Check if the payment method handler belongs to WhitelabelMachineName #}
{% if payment.handlerIdentifier == 'VRPaymentPayment\\Core\\Checkout\\PaymentHandler\\VRPaymentPaymentHandler' %}
{# Retrieve the runtime-attached configuration from the extension #}
{% set vrpConfig = payment.extensions.vrpayment_config %}
{% if vrpConfig %}
{% set paymentMethodConfigurationId = vrpConfig.paymentMethodConfigurationId %}
{# Include the iframe container template #}
{% sw_include '@VRPaymentPayment/storefront/component/payment/vrpayment_iframe.html.twig' with {
'paymentMethodConfigurationId': paymentMethodConfigurationId
} %}
{% endif %}
{% endif %}
{% endblock %}
@@ -0,0 +1,10 @@
<div id="vrpayment-payment-panel"
class="vrpayment-payment-panel"
data-vrpayment-checkout-plugin="true"
data-id="{{ paymentMethodConfigurationId }}">
<div id="vrpaymentLoader"><div></div></div>
<input value="false" type="hidden" name="vrpayment_payment_handler_validation_status"
form="confirmOrderForm">
<div id="vrpayment-payment-iframe"
class="vrpayment-payment-iframe"></div>
</div>
@@ -0,0 +1,26 @@
{% sw_extends '@Storefront/storefront/page/checkout/confirm/index.html.twig' %}
{% block base_body_script %}
{{ parent() }}
{# Provide WhitelabelMachineName integration data if available in page extensions #}
{% if page.extensions.vRPaymentData %}
{% set vrpData = page.extensions.vRPaymentData %}
{# Hidden inputs required by the JS components #}
<input type="hidden" id="cartRecreateUrl" value="{{ vrpData.cartRecreateUrl }}" />
<input type="hidden" id="checkoutUrl" value="{{ vrpData.checkoutUrl }}" />
{# Load WhitelabelMachineName SDK scripts #}
{% if vrpData.deviceJavascriptUrl %}
<script src="{{ vrpData.deviceJavascriptUrl }}" async="async"></script>
{% endif %}
{% if vrpData.javascriptUrl %}
<script src="{{ vrpData.javascriptUrl }}"></script>
{% endif %}
{# We don't need app.js if we use the modern plugin, but we might keep it for now if other pages depend on it.
However, for the confirm page, the modern plugin registered in main.js will take over
whenever it finds [data-vrpayment-checkout-plugin]. #}
{% endif %}
{% endblock %}