Release 6.1.10

This commit is contained in:
Drew Rowan
2025-01-22 15:31:27 +01:00
parent 9d83aa83a0
commit c8acf18146
163 changed files with 19465 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
src/Resources/app/storefront/dist/storefront/js/*
src/Resources/public/administration/js/*
+330
View File
@@ -0,0 +1,330 @@
# 6.1.10
- Multiple/bulk refund for line item.
- Partial line item refund.
- Fixed refund by line item option.
- Composer dependencies are managed by Shopware system
# 6.1.9
- Prevents calling a non existing method in webhook invocation.
- Prevents error if delivery data is empty
- Refactored composer.json, completing its require section.
# 6.1.8
- Implemented key signing
- Tax rate adjustment when products have different tax rates
# 6.1.7
- Fixed messaging which showed that shipping and billing address were always the same
# 6.1.6
- Bumped sdk version
# 6.1.5
- Support for Shopware 6.6.3.1 and Vue 3
# 6.1.4
- Improved plugin's settings form
- Support for Shopware 6.6.2.0
# 6.1.3
- Solvency check support for Powerpay and MF Group Invoice payment methods
- Improved handling of abandoned transactions
# 6.1.2
- Fixed redirect to confirmation page after reload
# 6.1.1
- Fixed deprecated OrderNotFoundException
# 6.1.0
- Fixed checkout issues after deactivating/activating plugin
- Fixed plugin uninstall action
- Fixed invoice payment method email function when order is shipped
# 6.0.0
- Support for Shopware 6.6
# 5.0.7
- Fix for refunds when a discount code is used
# 5.0.6
- Version bump for marketplace release
# 5.0.5
- Fixed an issue where all payment methods disappeared upon activation of the plugin.
- Fixed an issue where the shopping cart doubled in quantity if the customer placed an item in the shopping cart, went to payment via TWINT, cancelled the transaction in TWINT using "Cancel payment", and was then redirected back to the store.
# 5.0.4
- Adjust documentation and release command
# 5.0.3
- Support of PHP 8.2
- Cast to string the option of product attribute.
- If delivery is null do not try to hold it.
# 5.0.2
- Fix bug which happens when pressing back to shop or home clears cart.
# 5.0.1
- Adjust documentation
# 5.0.0
- Update composer file to only support 6.5
# 4.0.56
- Adjustment of the documentation
# 4.0.54
- Support of Shopware 6.5
- Support of latest PHP SDK 3.2.0
# 4.0.53
- Creation of a new column in the transaction table called erp_merchant_id
# 4.0.52
- Support of Shopware 6.4.20.1
# 4.0.51
- Wrong link format in error message.
# 4.0.45
- Add additional information of the Credit Card (Validity Date, Pseudo Credit Card number and PayID) for transaction using this Payment Method
- Compatibility SW v6.4.17.1
-
# 4.0.42
- Rollback to remove functionality of sending version to payment portal
# 4.0.41
- Sends to the payment portal a more specific version of shopware being used.
# 4.0.36
- Compatibility SW v6.4.13.0
# 4.0.29
- Fix to hide birthdate field if it's already provided
- Tested against SW v6.4.9.0
# 4.0.28
- Added italian translations
- Tested against SW v6.4.9.0
# 4.0.26
- Added documentation around flow builder
# 4.0.25
- Fixed transaction invoice instant payment handling.
- Tested with v6.4.7.0
# 4.0.24
- Added refunds by amount
# 4.0.23
- Fixed cart recreate function for custom products
# 4.0.22
- Added support for French
# 4.0.21
- Custom products options displayed as separate line items
# 4.0.20
- Fixed company name for shipping address
# 4.0.17
- Fixed settings to import webhooks and payment methods
# 4.0.16
- Added settings to control update of webhooks and payment methods
# 4.0.15
- Adjust VRPay/SW6 documentation - how to do refunds
# 4.0.14
- Support for Shopware 6.4.6
# 4.0.13
- Loader Chrome IOS fix
# 4.0.12
- Security fix
# 4.0.11
- Reverted auto-submit on empty iframe as it is not working properly at all cases
# 4.0.10
- Fixed "Allow payment change after checkout" option behavior
# 4.0.9
- Allow to mark payment status as paid from status reminded
# 4.0.8
- Checkout form auto submission implemented when iFrame returns no input fields
# 4.0.7
- Fix Transaction Rollback error on unsupported languages
# 4.0.6
- Fix for delivery state change error
# 4.0.5
- Fixed plugin uninstall action
# 4.0.4
- Line item based refunds
# 4.0.3
- Update SDK
# 4.0.2
- Fixed shipping line item name
# 4.0.1
- Fixed tax calculation for custom products
# 4.0.0
- Support for Shopware 6.4
# 3.1.0
- Support for Custom Products plugin
# 3.0.0
- Fix transaction versioning
- Update SDK
# 2.1.1
- Round amounts
- Redirect if the cart can not be recreated
# 2.1.0
- Fix email issues
# 2.0.0
- Fix cart recreation on promotions
- Remove availability rules
- Handle orders less than or equal to zero
# 1.4.3
- Silence missing order webhook errors
- Fix iframe breakout
# 1.4.2
- Fix payment method bug on first time install
# 1.4.1
- Fetch active payment methods only
# 1.4.0
- Fix payment method availability rule
- Fix email sending
- Cancel failed orders
# 1.3.0
- Update payment method syncing
# 1.2.0
- Add payment method availability rule
- Hardcoded system languages
# 1.1.27
- Retry orders on unavailable payment method
# 1.1.26
- Fix locales and translations
# 1.1.25
- Fix Email sending
# 1.1.24
- Fix webhook response
- Fix translation
- Prepare for Shopware 6.4
# 1.1.23
- Submit payment form when iframe has no fields
# 1.1.22
- Order invoice download setting
# 1.1.21
- Remove hardcoded Shopware API version
# 1.1.20
- Update webhook URLs on plugin update
- Add translations
- Fix email bug
# 1.1.19
- Allow customers to download order invoices
# 1.1.18
- Test against Shopware 6.3
- Fix error on invalid space id
- Remove hardcoded Shopware API version
# 1.1.17
- Use DAL on webhook locks
# 1.1.16
- Only provide translations for available languages
- Return CustomerCanceledAsyncPaymentException on cancelled transactions
- Update SDK to 2.1.1
# 1.1.15
- Send customer first name and last name from billing and shipping profiles
- Respect Shop URL
# 1.1.14
- Add cookies to the cookie manager
- Resize icon to 40px * 40px
- Fix line item attributes
# 1.1.13
- Include vendor folder in Shopware store releases
# 1.1.12
- Update doc path
# 1.1.11
- Add documentation
# 1.1.10
- Stop responding with server errors when orders are not found
# 1.1.9
- Put try catch on webhook install
# 1.1.8
- Remove unhelpful tickets info in release comments
# 1.1.7
- Implement promotions
- Code refactoring
# 1.1.6
- Disable sales channel selection on showcases
- Add product attributes to transaction payload
# 1.1.5
- Fix settings bug
# 1.1.4
- Disable changing credentials on the showcases
# 1.1.3
- Make line item consistency default
- Confirm transaction right away
- Update settings descriptions
# 1.1.2
- Prepare internal server side install for showcases and demos
# 1.1.1
- Stop default emails being sent
- Prettify payment page
# 1.1.0
- Handle empty/default Settings values
- Save refunds to db, and reload order tab on changes
# 1.0.0
- First version of the VRPayment integrations for Shopware 6
+328
View File
@@ -0,0 +1,328 @@
# 6.1.10
- Mehrfache/gesammelte Rückerstattung für Einzelposten.
- Teilweise Rückerstattung von Einzelposten.
- Fehler bei der Rückerstattung durch die Option Einzelposten behoben.
- Composer-Abhängigkeiten werden vom Shopware-System verwaltet
# 6.1.9
- Verhindert das Aufrufen einer nicht existierenden Methode bei der Webhook-Ausführung.
- Verhindert einen Fehler, wenn Lieferdaten leer sind.
- Composer.json überarbeitet und den Abschnitt "require" vervollständigt.
# 6.1.8
- Schlüsselsignatur implementiert
- Steuersatzanpassung, wenn Produkte unterschiedliche Steuersätze haben
# 6.1.7
- Meldung behoben, die anzeigte, dass Versand- und Rechnungsadresse immer identisch waren
# 6.1.6
- Versionserhöhung für das SDK
# 6.1.5
- Unterstützung von Shopware 6.6.3.1 und Vue 3
# 6.1.4
- Das Einstellungsformular des Plugins wurde verbessert
- Unterstützung von Shopware 6.6.2.0
# 6.1.3
- Unterstützung der Bonitätsprüfung für die Zahlungsmethoden Powerpay und MF Group Invoice
- Verbesserte Handhabung abgebrochener Transaktionen
# 6.1.2
- Die Weiterleitung zur Bestätigungsseite nach dem Neuladen wurde behoben
# 6.1.1
- Fixed deprecated OrderNotFoundException
# 6.1.0
- Checkout-Probleme nach dem Deaktivieren/Aktivieren des Plugins behoben
- Plugin-Deinstallationsaktion behoben
- Die E-Mail-Funktion für die Rechnungszahlungsmethode beim Versand der Bestellung wurde korrigiert
# 6.0.0
- Unterstützung von Shopware 6.6
# 5.0.7
- Fix für Rückerstattungen, wenn ein Rabattcode verwendet wird
# 5.0.6
- Versionserhöhung für die Marktveröffentlichung
# 5.0.5
Es wurde ein Problem behoben, bei dem alle Zahlungsmethoden nach der Aktivierung des Plugins verschwanden.
- Es wurde ein Problem behoben, bei dem sich die Warenkorbmenge verdoppelte, wenn der Kunde einen Artikel in den Warenkorb legte, über TWINT zur Zahlung ging, die Transaktion in TWINT mit „Zahlung stornieren“ abbrach und anschliessend zurück zum Shop weitergeleitet wurde.
# 5.0.4
- Dokumentation und Freigabebefehl anpassen
# 5.0.3
- Unterstützung des neuesten PHP 8.2
Umwandeln, um die Option des Produktattributs in einen String umzuwandeln.
- Wenn die Lieferung null ist, versuchen Sie nicht, sie zurückzuhalten.
# 5.0.2
- Behebung eines Fehlers, der auftritt, wenn Sie auf „Zurück zum Shop“ oder „Zuhause“ drücken, um den Warenkorb zu löschen.
# 5.0.1
- Anpassung der Dokumentation
# 5.0.0
- Aktualisieren Sie die Composer-Datei so, dass sie nur 6.5 unterstützt
# 4.0.56
- Anpassung der Dokumentation
# 4.0.54
- Unterstützung von Shopware 6.5
- Unterstützung des neuesten PHP SDK 3.2.0
# 4.0.53
- Erstellung einer neuen Spalte in der Transaktionstabelle mit dem Namen erp_merchant_id
# 4.0.52
- Unterstützung von Shopware 6.4.20.1
# 4.0.51
- Falsches Linkformat in der Fehlermeldung.
# 4.0.50
- Steuerinformationen wurden von der Versands- zu der Rechnungsstellung verschoben.
- Teilweise war die Synchronisierung der Portal Daten zu SW6 unvollständig.
- Lösen eines Fehlers: Der bezahlte Betrag im Portal wurde nicht an SW6 gemeldet.
- Lösen eines Fehlers: Nach Wechsel der Zahlungsmethode im Checkout, wurde der Zahlungsstatus auf "bezahlt" gesetzt.
- Lösen eines Fehlers: Beim Auflisten der Zahlungsmethoden durch den Kunden im Checkout Prozess.
# 4.0.45
- Fügen Sie zusätzliche Informationen der Kreditkarte (Gültigkeitsdatum, Pseudo-Kreditkartennummer und PayID) für Transaktionen mit dieser Zahlungsmethode hinzu
- Getestet mit SW v6.4.17.1
-
# 4.0.29
- Korrektur zum Ausblenden des Geburtsdatumsfelds, wenn es bereits vorhanden ist
- Getestet mit SW v6.4.9.0
# 4.0.28
- Italienische Übersetzungen hinzugefügt
- Getestet mit SW v6.4.9.0
# 4.0.26
- Dokumentation zum Flow Builder hinzugefügt
# 4.0.25
- Die Handhabung der sofortigen Zahlung von Transaktionsrechnungen wurde korrigiert.
- Getestet mit v6.4.7.0
# 4.0.24
- Rückerstattungen nach Betrag hinzugefügt
# 4.0.23
- Korrigierte Warenkorb-Neuerstellungsfunktion für benutzerdefinierte Produkte
# 4.0.22
- Unterstützung für Französisch hinzugefügt
# 4.0.21
- Benutzerdefinierte Produktoptionen werden als separate Einzelposten angezeigt
# 4.0.20
- Fester Firmenname für Lieferadresse
# 4.0.17
- Einstellungen zum Importieren von Webhooks und Zahlungsmethoden korrigiert
# 4.0.16
- Einstellungen zur Steuerung der Aktualisierung von Webhooks und Zahlungsmethoden hinzugefügt
# 4.0.15
- VRPay/SW6-Dokumentation anpassen wie man Rückerstattungen durchführt
# 4.0.14
- Unterstützung für Shopware 6.4.6
# 4.0.13
- Loader Chrome IOS beheben
# 4.0.12
- Implementierte Sicherheitskorrektur
# 4.0.11
- Automatisches Senden bei leerem iframe zurückgesetzt, da es nicht in allen Fällen richtig funktioniert
# 4.0.10
- Das Verhalten der Option "Zahlungsänderung nach der Kasse zulassen" behoben
# 4.0.9
- Erlaube, den Zahlungsstatus als bezahlt ab Status erinnert zu markieren
# 4.0.8
- Automatische Übermittlung des Checkout-Formulars implementiert, wenn iFrame keine Eingabefelder zurückgibt
# 4.0.7
- Behebung des Transaktions-Rollback-Fehlers in nicht unterstützten Sprachen
# 4.0.6
- Fehler beim Ändern des Lieferstatus behoben
# 4.0.5
- Deinstallation Aktion des Plugins behoben
# 4.0.4
- Erstattungen von Werbebuchungen
# 4.0.3
- Aktualisieren Sie das SDK
# 4.0.2
- Der Name der Versand-Einzelposten wurde korrigiert
# 4.0.1
- Feste Steuerberechnung für kundenspezifische Produkte
# 4.0.0
- Unterstützung für Shopware 6.4
# 3.1.0
- Unterstützung für Custom Products Plugin
# 3.0.0
- Korrigieren Sie die Transaktionsversionierung
- Aktualisieren Sie das SDK
# 2.1.1
- Runde Beträge
- Weiterleiten, wenn der Wagen nicht neu erstellt werden kann
# 2.1.0
- E-Mail-Probleme behoben
# 2.0.0
- Warenkorb-Wiederherstellung bei Werbeaktionen korrigiert
- Verfügbarkeitsregeln entfernt
- Verbessertes Behandeln von Aufträge kleiner oder gleich Null
# 1.4.3
- Fehlende Webhook-Fehler ausschließen
- Iframe-Ausbruch behoben
# 1.4.2
- Behebung des Fehlers bei der Zahlungsmethode bei der Erstinstallation
# 1.4.1
- Rufen Sie nur aktive Zahlungsmethoden ab
# 1.4.0
- Festlegen der Verfügbarkeitsregel für Zahlungsmethoden
- E-Mail-Versand korrigiert
- Fehlgeschlagene Bestellungen stornieren
# 1.3.0
- Aktualisieren Sie die Synchronisierung der Zahlungsmethode
# 1.2.0
- Verfügbarkeitsregel für Zahlungsmethoden hinzufügen
- Hardcodierte Systemsprachen
# 1.1.27
- Wiederholen Sie Bestellungen bei nicht verfügbarer Zahlungsmethode
# 1.1.26
- Korrigieren Sie Gebietsschemas und Übersetzungen
# 1.1.25
- E-Mail-Versand korrigiert
# 1.1.24
- Webhook-Antwort korrigiert
- Übersetzung korrigieren
- Bereiten Sie sich auf Shopware vor 6.4
# 1.1.23
- Senden Sie das Zahlungsformular, wenn iframe keine Felder enthält
# 1.1.22
- Einstellung zum Herunterladen der Bestellrechnung
# 1.1.21
- Entfernen Sie die fest codierte Shopware-API-Version
# 1.1.20
- Aktualisieren Sie die Webhook-URLs beim Plugin-Update
- Übersetzungen hinzufügen
- E-Mail-Fehler behoben
# 1.1.19
- Kunden können Bestellrechnungen herunterladen
# 1.1.18
- Test gegen Shopware 6.3
- Fehler bei ungültiger Speicherplatz-ID behoben
- Entfernen Sie die fest codierte Shopware-API-Version
# 1.1.17
- Verwenden Sie DAL für Webhook-Sperren
# 1.1.16
- Stellen Sie nur Übersetzungen für verfügbare Sprachen bereit
- CustomerCanceledAsyncPaymentException für stornierte Transaktionen zurückgeben
- Aktualisieren Sie das SDK auf 2.1.1
# 1.1.15
- Senden Sie den Vor- und Nachnamen des Kunden aus den Rechnungs- und Versandprofilen
- Respektieren Sie die Shop-URL
# 1.1.14
- Fügen Sie dem Cookie-Manager Cookies hinzu
- Ändern Sie die Größe des Symbols auf 40px * 40px
- Korrektur von Werbebuchungsattributen
# 1.1.13
- Fügen Sie den Lieferantenordner in Shopware Store-Versionen ein
# 1.1.12
- Dokumentpfad aktualisieren
# 1.1.11
- Dokumentation hinzufügen
# 1.1.10
- Reagieren Sie nicht mehr mit Serverfehlern, wenn keine Bestellungen gefunden werden
# 1.1.9
- Setzen Sie try catch auf die Webhook-Installation
# 1.1.8
- Entfernen Sie nicht hilfreiche Ticketinformationen in den Release-Kommentaren
# 1.1.7
- Werbeaktionen durchführen
- Code Refactoring
# 1.1.6
- Deaktivieren Sie die Auswahl der Vertriebskanäle für Vitrinen
- Fügen Sie der Transaktionsnutzlast Produktattribute hinzu
# 1.1.5
- Einstellungsfehler behoben
# 1.1.4
- Deaktivieren Sie das Ändern der Anmeldeinformationen für die Vitrinen
# 1.1.3
- Legen Sie die Konsistenz der Werbebuchung als Standard fest
- Bestätigen Sie die Transaktion sofort
- Aktualisieren Sie die Einstellungsbeschreibungen
# 1.1.2
- Bereiten Sie die interne serverseitige Installation für Vitrinen und Demos vor
# 1.1.1
- Stoppen Sie das Senden von Standard-E-Mails
- Verschönern Sie die Zahlungsseite
# 1.1.0
- Behandeln Sie leere / Standardeinstellungswerte
- Speichern Sie Rückerstattungen in db und laden Sie die Registerkarte Bestellung bei Änderungen neu
# 1.0.0
- Erste Version der VRPayment-Integrationen für Shopware 6
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2025 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.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+80
View File
@@ -0,0 +1,80 @@
VRPayment Payment for Shopware 6
=============================
The VRPayment Payment plugin wraps around the VRPayment API. This library facilitates your interaction with various services such as transactions.
Please note that this plugin is for versions 6.5 and 6.6. For the 6.4 plugin please visit [our Shopware 6.4 plugin](https://github.com/vr-payment/shopware-6-4).
## Requirements
- Shopware 6.5.x or Shopware 6.6.x. See table below.
- PHP minimum version supported by the each shop version.
## Supported versions
___________________________________________________________________________________
| Shopware 6 version | Plugin major version | Supported until |
|-------------------------------|------------------------|------------------------|
| Shopware 6.6.x | 6.x | Further notice |
| Shopware 6.5.x | 5.x | October 2024 |
-----------------------------------------------------------------------------------
## Installation
You can use **Composer** or **install manually**
### Composer
The preferred method is via [composer](https://getcomposer.org). Follow the
[installation instructions](https://getcomposer.org/doc/00-intro.md) if you do not already have
composer installed.
Once composer is installed, execute the following command from the shop root to install the plugin:
```bash
composer require vrpayment/shopware-6
php bin/console plugin:refresh
php bin/console plugin:install --activate --clearCache VRPaymentPayment
```
#### Update via composer
```bash
composer update vrpayment/shopware-6
php bin/console plugin:refresh
php bin/console plugin:install --activate --clearCache VRPaymentPayment
```
### Manual Installation
Alternatively you can download the package in its entirety. The [Releases](../../releases) page lists all stable versions.
Uncompress the zip file you download, and include the autoloader in your project:
```bash
# unzip to ShopwareInstallDir/custom/plugins/VRPaymentPayment
# For versions 6.1.10 and older, the SDK is installed automatically when installing the plugin in the shop, so you don't need to
# run the following command.
composer require vrpayment/sdk 4.6.0
php bin/console plugin:refresh
php bin/console plugin:install --activate --clearCache VRPaymentPayment
```
## Usage
The library needs to be configured with your account's space id, user id, and application key which are available in your VRPayment
account dashboard.
### Logs and debugging
To view the logs please run the command below:
```bash
cd shopware/install/dir
tail -f var/log/vrpayment_payment*.log
```
## Documentation
[Documentation](https://gateway.vr-payment.de/doc/shopware-6/6.1.10/docs/en/documentation.html)
## License
Please see the [license file](https://github.com/vr-payment/shopware-6/blob/master/LICENSE.txt) for more information.
+63
View File
@@ -0,0 +1,63 @@
{
"authors": [
{
"homepage": "https://www.vr-payment.de/",
"name": "VRPay"
}
],
"autoload": {
"psr-4": {
"VRPaymentPayment\\": "src/"
}
},
"description": "VRPayment integration for Shopware 6",
"extra": {
"copyright": "(c) by VRPay",
"description": {
"de-DE": "VRPayment integration f\u00fcr Shopware 6",
"en-GB": "VRPayment integration for Shopware 6",
"fr-FR": "Int\u00e9gration de VRPayment pour Shopware 6",
"it-IT": "Integrazione VRPayment per Shopware"
},
"label": {
"de-DE": "VRPayment Produkte f\u00fcr Shopware 6",
"en-GB": "VRPayment Products for Shopware 6",
"fr-FR": "VRPayment Produits for Shopware 6",
"it-IT": "VRPayment Prodotti per Shopware 6"
},
"manufacturerLink": {
"de-DE": "https://www.vr-payment.de/",
"en-GB": "https://www.vr-payment.de/",
"fr-FR": "https://www.vr-payment.de/",
"it-IT": "https://www.vr-payment.de/"
},
"supportLink": {
"de-DE": "https://www.vr-payment.de/hotline",
"en-GB": "https://www.vr-payment.de/hotline",
"fr-FR": "https://www.vr-payment.de/hotline",
"it-IT": "https://www.vr-payment.de/hotline"
},
"shopware-plugin-class": "VRPaymentPayment\\VRPaymentPayment"
},
"homepage": "https://www.vr-payment.de//",
"keywords": [
"VRPay",
"payment",
"php",
"shopware"
],
"license": "Apache-2.0",
"name": "vrpayment/shopware-6",
"require": {
"ext-curl": "*",
"ext-json": "*",
"ext-mbstring": "*",
"php": ">=8.2",
"shopware/core": "6.6.*",
"shopware/administration": "~6.6.0",
"shopware/storefront": "6.6.*",
"vrpayment/sdk": "4.6.0"
},
"type": "shopware-platform-plugin",
"version": "6.1.10"
}
+692
View File
@@ -0,0 +1,692 @@
* {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
*:before, *:after {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
html {
font-size: 100%;
font-family: sans-serif;
line-height: 1.15;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
-ms-overflow-style: scrollbar;
-webkit-tap-highlight-color: transparent;
}
@-ms-viewport {
width: device-width;
}
article, aside, dialog, figcaption, figure, footer, header, hgroup, main, nav, section {
display: block;
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
font-size: 1rem;
font-weight: 300;
line-height: 1.5;
color: #212529;
text-align: left;
background-color: #fff;
padding-right: 0 !important;
position: relative;
}
html,body {
width: 100%;
height: 100%;
}
[tabindex="-1"]:focus {
outline: 0 !important;
}
hr {
box-sizing: content-box;
height: 0;
overflow: visible;
}
h1, h2, h3, h4, h5, h6 {
margin-top: 0;
margin-bottom: 0.5rem;
}
h1 {
font-size: 2.5rem;
font-weight: 200;
margin-bottom: 1.875rem;
}
h2 {
font-size: 1.625rem;
font-weight: 300;
margin-bottom: 1.3rem;
}
h3 {
font-size: 1.3rem;
font-weight: 300;
margin-top: 1.3rem;
}
h4 {
font-size: 1.125rem;
font-weight: 400;
margin-top: 1.875rem;
margin-bottom: 1.3rem;
}
h5 {
font-size: 1rem;
font-weight: bold;
margin-top: 1.875rem;
margin-bottom: 1.3rem;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
abbr[title], abbr[data-original-title] {
text-decoration: underline;
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
cursor: help;
border-bottom: 0;
}
address {
margin-bottom: 1rem;
font-style: normal;
line-height: inherit;
}
ol, ul, dl {
margin-top: 0;
margin-bottom: 1rem;
}
ol ol, ul ul, ol ul, ul ol {
margin-bottom: 0;
}
dt {
font-weight: 700;
}
dd {
margin-bottom: .5rem;
margin-left: 0;
}
blockquote {
margin: 0 0 1rem;
}
dfn {
font-style: italic;
}
b, strong {
font-weight: bold;
}
small {
font-size: 80%;
}
sub, sup {
position: relative;
font-size: 75%;
line-height: 0;
vertical-align: baseline;
}
sub {
bottom: -.25em;
}
sup {
top: -.5em;
}
a {
color: #007bff;
text-decoration: none;
background-color: transparent;
-webkit-text-decoration-skip: objects;
}
a:hover {
color: #0056b3;
text-decoration: underline;
}
a:not([href]):not([tabindex]) {
color: inherit;
text-decoration: none;
}
a:not([href]):not([tabindex]):hover, a:not([href]):not([tabindex]):focus {
color: inherit;
text-decoration: none;
}
a:not([href]):not([tabindex]):focus {
outline: 0;
}
pre, code, kbd, samp {
font-family: monospace, monospace;
font-size: 90%;
padding: 2px 4px 2px 4px;
color: #c7254e;
background-color: #f9f2f4;
border-radius: 4px;
}
pre {
margin-top: 0;
margin-bottom: 1rem;
overflow: auto;
-ms-overflow-style: scrollbar;
}
figure {
margin: 0 0 1rem;
}
img {
vertical-align: middle;
border-style: none;
}
svg:not(:root) {
overflow: hidden;
}
table {
border-collapse: collapse;
background-color: transparent;
}
caption {
padding-top: 8px;
padding-bottom: 8px;
color: #a7a7a7;
text-align: left;
}
th {
text-align: left;
}
output {
display: inline-block;
}
summary {
display: list-item;
cursor: pointer;
}
template {
display: none;
}
table col[class*="col-"] {
position: static;
float: none;
display: table-column;
}
table td[class*="col-"],table th[class*="col-"] {
position: static;
float: none;
display: table-cell;
}
ol.glossary {
counter-reset: glossary-counter;
list-style: none;
padding-left: 40px;
}
ol.glossary li {
counter-increment: glossary-counter;
position: relative;
}
ol.glossary li::before {
content: counter(glossary-counter);
position: absolute;
background-color: #73EAA9;
color: #fff;
border-radius: 100px;
width: 24px;
left: -40px;
text-align: center;
font-weight: bold;
line-height: 24px;
}
.layout-wrapper {
position: relative;
width: 100%;
height: auto;
min-height: 100%;
}
.layout-title {
padding: 1.875rem 0;
border-bottom: 1px solid #f0f0f0;
}
.layout-title h1 {
font-size: 3rem;
font-weight: 200;
text-align: center;
margin: 0;
}
.layout-title h2 {
font-size: 2rem;
font-weight: 200;
text-align: center;
color: #999;
margin-bottom: 0;
}
.layout-navigation .nav {
padding: 1.875rem 0;
border-bottom: 1px solid #f0f0f0;
text-align: center;
background: #fff;
z-index: 1000;
}
.layout-navigation .nav > li {
display: inline-block;
}
.layout-navigation .nav > li > a {
border: 1px solid #007bff;
border-radius: 100px;
padding: 6px 12px;
margin: 0 8px;
}
.layout-navigation .nav > li > a:hover, .layout-navigation .nav > li > a:active, .layout-navigation .nav > li > a:focus {
border: 1px solid #0056b3;
color: #0056b3;
text-decoration: none;
}
.layout-content {
position: relative;
}
.layout-content:before, .layout-content:after {
content: " ";
display: table;
}
.layout-content:after {
clear: both;
}
.layout-content .col-right {
width: 25%;
float: right;
}
.layout-content .col-right-wrapper {
width: 100%;
position: relative;
overflow-x: hidden;
overflow-y: auto;
padding: 2.5rem 2rem 0;
}
.layout-content .col-body {
width: 75%;
float: left;
}
.layout-content .col-body:before, .layout-content .col-body:after {
content: " ";
display: table;
}
.layout-content .col-body:after {
clear: both;
}
.layout-content .col-body-wrapper {
position: relative;
width: 100%;
padding: 2.5rem 2rem 0;
}
.nav {
padding-left: 0;
margin-bottom: 0;
list-style: none;
line-height: 2;
}
.table-of-contents {
padding: 1.25rem 0;
}
.table-of-contents .nav > li > a {
display: flex;
}
.table-of-contents .nav > li > a .item-number {
display: none;
}
.table-of-contents .nav > li > a .item-title {
color: #212529;
overflow: hidden;
text-overflow: ellipsis;
flex-grow: 1;
white-space: nowrap;
}
.table-of-contents .nav > li > a .item-title:hover {
color: #0056b3;
}
.table-of-contents .nav > li.extended > a .item-title, .table-of-contents .nav > li.active > a .item-title, .table-of-contents .nav > li.extended > a .item-title:hover, .table-of-contents .nav > li.active > a .item-title:hover {
color: #007bff;
}
.table-of-contents > .nav > li > .nav {
display: none;
margin-bottom: 0.5rem;
}
.table-of-contents > .nav > li > .nav > li > a {
padding-left: 1rem;
}
.table-of-contents > .nav > li > .nav > li > a .item-title {
font-size: 0.875rem;
}
.table-of-contents > .nav > li > .nav > li > .nav > li > a {
padding-left: 2rem;
}
.table-of-contents > .nav > li > .nav > li > .nav > li > a .item-title {
font-size: 0.75rem;
}
.table-of-contents > .nav > li.active > .nav {
display: block;
}
.chapter {
margin: 0 0 6rem;
font-weight: 300;
}
.section {
margin-top: 3rem;
}
.chapter > .chapter-title h1, .chapter > .chapter-title h2, .chapter > .chapter-title h3, .chapter > .chapter-title h4, .chapter > .chapter-title h5, .chapter > .chapter-title h6, .section > .section-title h1, .section > .section-title h2, .section > .section-title h3, .section > .section-title h4, .section > .section-title h5, .section > .section-title h6 {
margin-top: 0;
}
.chapter > .chapter-title h1 {
border-bottom: 2px solid #eeeeee;
margin-bottom: 1.5rem;
padding-bottom: 0.2em;
}
.chapter-title .title-number, .section-title .title-number {
display: none;
}
.paragraph {
line-height: 1.75em;
}
.paragraph + .paragraph {
margin-top: 1em;
}
.dlist {
margin-top: 30px;
}
.dlist dl dt {
float: left;
width: 160px;
clear: left;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.dlist dl dd {
margin-left: 180px;
}
.ulist {
margin-top: 30px;
}
.imageblock {
margin: 30px auto;
}
.imageblock .content img {
max-width: 100%;
}
.imageblock .title {
padding: 10px 0 0;
}
.exampleblock, .quoteblock, .literalblock {
background: #f5f4f4;
padding: 20px;
margin: 30px 0;
}
.exampleblock .title, .quoteblock .title, .literalblock .title {
text-transform: uppercase;
font-size: 0.75em;
font-weight: 400;
color: #979797;
margin-bottom: 10px;
}
.quoteblock blockquote {
margin: 0;
padding: 0;
border: 0;
font-size: inherit;
}
.quoteblock blockquote p:last-child, .quoteblock blockquote ul:last-child, .quoteblock blockquote ol:last-child {
margin-bottom: 9px;
}
.literalblock pre {
border: 0;
padding: 0;
margin: 0;
}
.listingblock {
margin: 30px 0;
}
.listingblock pre {
border: 0;
padding: 0;
margin: 0;
}
.listingblock pre code {
display: block;
padding: 20px;
}
.admonitionblock {
line-height: 1.8em;
padding: 20px;
margin: 30px 0;
}
.admonitionblock .icon {
display: none;
}
.admonitionblock.important {
background: #fce1e1;
border-left: 5px solid #ff6060;
}
.admonitionblock.note, .admonitionblock.tip {
background: #e0f2fc;
border-left: 5px solid #88d5ff;
}
.admonitionblock.caution, .admonitionblock.warning {
background: #fdf3d8;
border-left: 5px solid #f1c654;
}
table.tableblock {
background-color: #fff;
width: 100%;
max-width: 100%;
margin-bottom: 18px;
margin: 30px 0;
}
table.tableblock > thead > tr > th, table.tableblock > tbody > tr > th, table.tableblock > tfoot > tr > th, table.tableblock > thead > tr > td, table.tableblock > tbody > tr > td, table.tableblock > tfoot > tr > td {
padding: 8px;
line-height: 1.42857143;
vertical-align: top;
border-top: 1px solid #eee;
}
table.tableblock > thead > tr > th {
vertical-align: bottom;
border-bottom: 2px solid #eee;
}
table.tableblock > caption + thead > tr:first-child > th, table.tableblock > colgroup + thead > tr:first-child > th, table.tableblock > thead:first-child > tr:first-child > th, table.tableblock > caption + thead > tr:first-child > td, table.tableblock > colgroup + thead > tr:first-child > td, table.tableblock > thead:first-child > tr:first-child > td {
border-top: 0;
}
table.tableblock > tbody + tbody {
border-top: 2px solid #eee;
}
table.tableblock .table {
background-color: #fff;
}
table.tableblock > tbody > tr:nth-of-type(odd) {
background-color: #f7f7f7;
}
table.tableblock > thead > tr > th p:last-child, table.tableblock > tbody > tr > th p:last-child, table.tableblock > tfoot > tr > th p:last-child, table.tableblock > thead > tr > td p:last-child, table.tableblock > tbody > tr > td p:last-child, table.tableblock > tfoot > tr > td p:last-child {
margin-bottom: 0;
}
.loaded .table-of-contents .nav .nav {
display: none;
}
@media (min-width: 1200px) {
.layout-wrapper .layout-title, .layout-wrapper .layout-navigation, .layout-wrapper .layout-content {
max-width: 1200px;
margin-left: auto;
margin-right: auto;
}
}
@media (max-width: 991px) {
html {
font-size: 90%;
}
.layout-content .col-right {
display: none;
}
.layout-content .col-body {
width: 100%;
}
}
@media print {
body {
color: #000;
font-family: Georgia, "Times New Roman", Times, serif;
}
a {
color: #000;
}
h1 {
font-size: 1.6rem;
}
h2 {
font-size: 1.4rem;
}
h3 {
font-size: 1.2rem;
}
h4 {
font-size: 1rem;
}
h5 {
font-size: 0.9rem;
}
.layout-title h1 {
font-size: 2rem;
}
.layout-content .col-right {
display: none;
}
.layout-content .col-body {
width: 100%;
}
.chapter {
margin-bottom: 3rem;
}
.section {
margin-top: 2rem;
}
}
+14
View File
@@ -0,0 +1,14 @@
(function($){
hljs.initHighlightingOnLoad();
$(document).ready(function(){
$('.col-right-wrapper').stick_in_parent({
parent: '.layout-content'
});
$('body').scrollspy({
target: '.table-of-contents'
});
});
})(jQuery);
File diff suppressed because one or more lines are too long
+2
View File
File diff suppressed because one or more lines are too long
+83
View File
@@ -0,0 +1,83 @@
/*
Monokai Sublime style. Derived from Monokai by noformnocontent http://nn.mit-license.org/
*/
.hljs {
display: block;
overflow-x: auto;
padding: 0.5em;
background: #23241f;
}
.hljs,
.hljs-tag,
.hljs-subst {
color: #f8f8f2;
}
.hljs-strong,
.hljs-emphasis {
color: #a8a8a2;
}
.hljs-bullet,
.hljs-quote,
.hljs-number,
.hljs-regexp,
.hljs-literal,
.hljs-link {
color: #ae81ff;
}
.hljs-code,
.hljs-title,
.hljs-section,
.hljs-selector-class {
color: #a6e22e;
}
.hljs-strong {
font-weight: bold;
}
.hljs-emphasis {
font-style: italic;
}
.hljs-keyword,
.hljs-selector-tag,
.hljs-name,
.hljs-attr {
color: #f92672;
}
.hljs-symbol,
.hljs-attribute {
color: #66d9ef;
}
.hljs-params,
.hljs-class .hljs-title {
color: #f8f8f2;
}
.hljs-string,
.hljs-type,
.hljs-built_in,
.hljs-builtin-name,
.hljs-selector-id,
.hljs-selector-attr,
.hljs-selector-pseudo,
.hljs-addition,
.hljs-variable,
.hljs-template-variable {
color: #e6db74;
}
.hljs-comment,
.hljs-deletion,
.hljs-meta {
color: #75715e;
}
+9
View File
@@ -0,0 +1,9 @@
/* ========================================================================
* Bootstrap: scrollspy.js v3.3.7
* http://getbootstrap.com/javascript/#scrollspy
* ========================================================================
* Copyright 2011-2016 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* ======================================================================== */
!function(r){"use strict";function o(t,s){this.$body=r(document.body),this.$scrollElement=r(t).is(document.body)?r(window):r(t),this.options=r.extend({},o.DEFAULTS,s),this.selector=(this.options.target||"")+" .nav li > a",this.offsets=[],this.targets=[],this.activeTarget=null,this.scrollHeight=0,this.$scrollElement.on("scroll.bs.scrollspy",r.proxy(this.process,this)),this.refresh(),this.process()}function s(i){return this.each(function(){var t=r(this),s=t.data("bs.scrollspy"),e="object"==typeof i&&i;s||t.data("bs.scrollspy",s=new o(this,e)),"string"==typeof i&&s[i]()})}o.VERSION="3.3.7",o.DEFAULTS={offset:10},o.prototype.getScrollHeight=function(){return this.$scrollElement[0].scrollHeight||Math.max(this.$body[0].scrollHeight,document.documentElement.scrollHeight)},o.prototype.refresh=function(){var t=this,i="offset",o=0;this.offsets=[],this.targets=[],this.scrollHeight=this.getScrollHeight(),r.isWindow(this.$scrollElement[0])||(i="position",o=this.$scrollElement.scrollTop()),this.$body.find(this.selector).map(function(){var t=r(this),s=t.data("target")||t.attr("href"),e=/^#./.test(s)&&r(s);return e&&e.length&&e.is(":visible")&&[[e[i]().top+o,s]]||null}).sort(function(t,s){return t[0]-s[0]}).each(function(){t.offsets.push(this[0]),t.targets.push(this[1])})},o.prototype.process=function(){var t,s=this.$scrollElement.scrollTop()+this.options.offset,e=this.getScrollHeight(),i=this.options.offset+e-this.$scrollElement.height(),o=this.offsets,r=this.targets,l=this.activeTarget;if(this.scrollHeight!=e&&this.refresh(),i<=s)return l!=(t=r[r.length-1])&&this.activate(t);if(l&&s<o[0])return this.activeTarget=null,this.clear();for(t=o.length;t--;)l!=r[t]&&s>=o[t]&&(void 0===o[t+1]||s<o[t+1])&&this.activate(r[t])},o.prototype.activate=function(t){this.activeTarget=t,this.clear();var s=this.selector+'[data-target="'+t+'"],'+this.selector+'[href="'+t+'"]',e=r(s).parents("li").addClass("active");e.parent(".dropdown-menu").length&&(e=e.closest("li.dropdown").addClass("active")),e.trigger("activate.bs.scrollspy")},o.prototype.clear=function(){r(this.selector).parentsUntil(this.options.target,".active").removeClass("active")};var t=r.fn.scrollspy;r.fn.scrollspy=s,r.fn.scrollspy.Constructor=o,r.fn.scrollspy.noConflict=function(){return r.fn.scrollspy=t,this},r(window).on("load.bs.scrollspy.data-api",function(){r('[data-spy="scroll"]').each(function(){var t=r(this);s.call(t,t.data())})})}(jQuery);
+5
View File
@@ -0,0 +1,5 @@
/**
@license Sticky-kit v1.1.2 | WTFPL | Leaf Corcoran 2015 | http://leafo.net
*/
(function(){var M,Q;M=this.jQuery||window.jQuery,Q=M(window),M.fn.stick_in_parent=function(t){var x,o,C,i,e,P,s,V,F,z,r,I,A,j;for(null==t&&(t={}),j=t.sticky_class,P=t.inner_scrolling,A=t.recalc_every,I=t.parent,F=t.offset_top,z=t.offset_bottom,V=t.spacer,C=t.bottoming,null==F&&(F=0),null==z&&(z=0),null==I&&(I=void 0),null==P&&(P=!0),null==j&&(j="is_stuck"),x=M(document),null==C&&(C=!0),r=function(t){var o,i;return window.getComputedStyle?(t[0],o=window.getComputedStyle(t[0]),i=parseFloat(o.getPropertyValue("width"))+parseFloat(o.getPropertyValue("margin-left"))+parseFloat(o.getPropertyValue("margin-right")),"border-box"!==o.getPropertyValue("box-sizing")&&(i+=parseFloat(o.getPropertyValue("border-left-width"))+parseFloat(o.getPropertyValue("border-right-width"))+parseFloat(o.getPropertyValue("padding-left"))+parseFloat(o.getPropertyValue("padding-right"))),i):t.outerWidth(!0)},i=function(n,l,a,c,p,d,u,f){var h,t,g,m,k,y,b,v,o,_,w,e;if(!n.data("sticky_kit")){if(n.data("sticky_kit",!0),k=x.height(),b=n.parent(),null!=I&&(b=b.closest(I)),!b.length)throw"failed to find stick parent";return h=g=!1,(w=null!=V?V&&n.closest(V):M('<div class="sticky-kit-manual-spacer" />'))&&w.css("position",n.css("position")),(v=function(){var t,o,i;if(!f)return k=x.height(),t=parseInt(b.css("border-top-width"),10),o=parseInt(b.css("padding-top"),10),l=parseInt(b.css("padding-bottom"),10),a=b.offset().top+t+o,c=b.height(),g&&(h=g=!1,null==V&&(n.insertAfter(w),w.detach()),n.css({position:"",top:"",width:"",bottom:""}).removeClass(j),i=!0),p=n.offset().top-(parseInt(n.css("margin-top"),10)||0)-F,d=n.outerHeight(!0),u=n.css("float"),w&&w.css({width:r(n),height:d,display:n.css("display"),"vertical-align":n.css("vertical-align"),float:u}),i?e():void 0})(),m=void 0,y=F,_=A,e=function(){var t,o,i,e,s,r;if(d!==c&&!f)return i=!1,null!=_&&(_-=1)<=0&&(_=A,v(),i=!0),i||x.height()===k||(v(),i=!0),e=Q.scrollTop(),null!=m&&(o=e-m),m=e,g?(C&&(s=c+a<e+d+y+z,h&&!s&&(h=!1,n.css({position:"fixed",bottom:"",top:y}).removeClass("is_bottomed").trigger("sticky_kit:unbottom"))),e<p&&(g=!1,y=F,null==V&&("left"!==u&&"right"!==u||n.insertAfter(w),w.detach()),t={position:"",width:"",top:""},n.css(t).removeClass(j).trigger("sticky_kit:unstick")),P&&(r=Q.height())<d+F&&(h||(y-=o,y=Math.max(r-d,y),y=Math.min(F,y),g&&n.css({top:y+"px"})))):p<e&&(g=!0,(t={position:"fixed",top:y}).width="border-box"===n.css("box-sizing")?n.outerWidth()+"px":n.width()+"px",n.css(t).addClass(j),null==V&&(n.after(w),"left"!==u&&"right"!==u||w.append(n)),n.trigger("sticky_kit:stick")),g&&C&&(null==s&&(s=c+a<e+d+y+z),!h&&s)?(h=!0,"static"===b.css("position")&&b.css({position:"relative"}),n.css({position:"absolute",bottom:l+z,top:"auto"}).addClass("is_bottomed").trigger("sticky_kit:bottom")):void 0},o=function(){return v(),e()},t=function(){if(f=!0,Q.off("touchmove",e),Q.off("scroll",e),Q.off("resize",o),M(document.body).off("sticky_kit:recalc",o),n.off("sticky_kit:detach",t),n.removeData("sticky_kit"),n.css({position:"",bottom:"",top:"",width:""}),b.position("position",""),g)return null==V&&("left"!==u&&"right"!==u||n.insertAfter(w),w.remove()),n.removeClass(j)},Q.on("touchmove",e),Q.on("scroll",e),Q.on("resize",o),M(document.body).on("sticky_kit:recalc",o),n.on("sticky_kit:detach",t),setTimeout(e,0)}},e=0,s=this.length;e<s;e++)o=this[e],i(M(o));return this}}).call(this);
+704
View File
@@ -0,0 +1,704 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<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="VRPay, Shopware, Shopware Plugin, Payment, Payment Integration, Documentation"><meta name="description" value="The documentation for the Shopware 6 plugin that enables processing payments with VRPay.">
<link rel="canonical" href="https://plugin-documentation.wallee.com/wallee-payment/shopware-6/master/VRPaymentPayment/docs/en/documentation.html" />
<title>VRPay Shopware 6 Documentation</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>VRPay Shopware 6 Documentation</h1>
<h2>Documentation</h2> </div>
<div class="layout-navigation">
<ul class="nav">
<li>
<a href="https://gateway.vr-payment.de/user/login">
Sign Up
</a>
</li>
<li>
<a href="https://github.com/vr-payment/shopware-6/releases/tag/6.1.10/">
Source
</a>
</li>
</ul> </div>
<div class="layout-content">
<div class="col-body">
<div class="col-body-wrapper">
<div class="body-container">
<div class="chapter" id="_prerequisites">
<div class="chapter-title">
<h1>
<span class="title-number">1</span>Prerequisites </h1>
</div>
<div class="chapter-body">
<div class="paragraph">
<p>If you don&#8217;t already have one, create a <a href="https://gateway.vr-payment.de/user/login/">VRPay</a> account.</p>
</div> </div>
</div> <div class="chapter" id="_installation">
<div class="chapter-title">
<h1>
<span class="title-number">2</span>Installation </h1>
</div>
<div class="chapter-body">
<div class="olist arabic">
<ol class="arabic">
<li>
<p>Install the plugin directly from the Shopware plugin store.</p>
<div class="admonitionblock note">
<table>
<tr>
<td class="icon">
<div class="title">Note</div>
</td>
<td class="content">
You also have the possibility to install the plugin using composer. See <a href="#_faq">FAQ</a>.
</td>
</tr>
</table>
</div>
</li>
<li>
<p>Log in to the backend of your Shopware store.</p>
</li>
<li>
<p>Navigate to Settings &#8594; System &#8594; Plugins. Click on the menu caret and select the <code>Install</code> link of the plugin to install it.</p>
<div class="imageblock">
<div class="content">
<img src="resource/plugin-installation.png" alt="plugin installation"/>
</div>
</div>
</li>
<li>
<p>Activate the VRPay Payment plugin from the Plugin Manager.</p>
</li>
</ol>
</div> </div>
</div> <div class="chapter" id="_configuration">
<div class="chapter-title">
<h1>
<span class="title-number">3</span>Configuration </h1>
</div>
<div class="chapter-body">
<div class="olist arabic">
<ol class="arabic">
<li>
<p>Navigate to Settings &#8594; Plugins &#8594; VRPayment in your Shopware backend. Enter the VRPay Space ID, User ID and Authentication Key that you can create an <a href="https://gateway.vr-payment.de/en-us/doc/permission-concept#_create_application_users" target="_blank">application user</a>.</p>
<div class="imageblock">
<div class="content">
<img src="resource/plugin-configuration.png" alt="plugin configuration"/>
</div>
</div>
<div class="paragraph">
<p>If your store is configured for multiple sales channels, you may use different spaces for each store to configure different behaviours.</p>
</div>
</li>
<li>
<p>Optionally after saving your configuration you can click on <code>Set VRPayment as default payment handler</code>. This will set VRPaymentPayment as the default payment handler for the selected sales channel.</p>
</li>
</ol>
</div><div class="paragraph">
<p>The main configuration is finished now. You should see the payment methods in your checkout. To view the payment method configuration in the backend of Shopware go to Settings &#8594; Store &#8594; Payment.</p>
</div> </div>
</div> <div class="chapter" id="_payment_method_configuration">
<div class="chapter-title">
<h1>
<span class="title-number">4</span>Payment method configuration </h1>
</div>
<div class="chapter-body">
<div class="section" id="_setup">
<div class="section-title">
<h2>
<span class="title-number">4.1</span>Setup </h2>
</div>
<div class="section-body">
<div class="paragraph">
<p>The VRPay payment method configurations are synchronized automatically into the Shopware store. There are just a few payment method settings in the Shopware store in Settings &#8594; Store &#8594; Payment.</p>
</div><div class="imageblock">
<div class="content">
<img src="resource/payment-method-configuration.png" alt="payment method configuration"/>
</div>
</div> </div>
</div> <div class="section" id="_payment_method_rules">
<div class="section-title">
<h2>
<span class="title-number">4.2</span>Payment method rules </h2>
</div>
<div class="section-body">
<div class="paragraph">
<p>If you would like to restrict a payment method to certain conditions (B2B, cart amount, etc), you can create or choose a rule with the Availability Rule option in the shop backend.</p>
</div><div class="imageblock">
<div class="content">
<img src="resource/payment-method-configuration-availability-rule.png" alt="payment method configuration availability rule"/>
</div>
</div><div class="paragraph">
<p><strong>How to create a new Rule?</strong></p>
</div><div class="olist arabic">
<ol class="arabic">
<li>
<p>Click on “Create new rule…” in the Availability rule option and fill out the modal form with conditions, as shown below.</p>
</li>
</ol>
</div><div class="imageblock">
<div class="content">
<img src="resource/payment-method-configuration-create-availability-rule.png" alt="payment method configuration create availability rule"/>
</div>
</div><div class="olist arabic">
<ol class="arabic">
<li>
<p>In this example the rule is as shown below:</p>
<div class="ulist">
<ul>
<li>
<p>Name: Payment method for B2B.</p>
</li>
<li>
<p>Priority: 1 (if you wish to prioritise when using several shipping methods, please adjust the value accordingly).</p>
</li>
<li>
<p>Conditions: Commercial customer | Yes
Billing address: Country | Is one of | Switzerland</p>
</li>
</ul>
</div>
</li>
</ol>
</div><div class="paragraph">
<p>You can now select the rule in the desired payment method in the item availability rule.</p>
</div><div class="paragraph">
<p>It is also conceivable to add a further condition to the rule above, e.g. to make the payment method additionally possible only from a certain purchase value (e.g. 250.00).</p>
</div><div class="paragraph">
<p>In this case, add another condition using the AND link and insert the following in the second condition.</p>
</div><div class="olist arabic">
<ol class="arabic">
<li>
<p>Select the newly created rule and save the changes to the chosen payment method.</p>
</li>
</ol>
</div><div class="paragraph">
<p>Applying this rule to a payment method will result in only those customers who meet the configured conditions.</p>
</div> </div>
</div> <div class="section" id="_customization">
<div class="section-title">
<h2>
<span class="title-number">4.3</span>Customization </h2>
</div>
<div class="section-body">
<div class="paragraph">
<p>If you want to change the payment method description, title, logo, etc you need to do this in the <a href="https://gateway.vr-payment.de/space/select?target=/payment/method-configuration/list" target="_blank">payment method configuration</a>. Changes will be synchronized automatically.</p>
</div> </div>
</div> </div>
</div> <div class="chapter" id="_state_graph">
<div class="chapter-title">
<h1>
<span class="title-number">5</span>State graph </h1>
</div>
<div class="chapter-body">
<div class="paragraph">
<p>The Payment Process of VRPay is <strong>completely standardized for every payment method</strong> you can process. This gives you the ability to simply add
a payment method or processor without changes inside of your Shopware configuration. An overview about the states and the payment processes of VRPay
can be found in the <a href="https://gateway.vr-payment.de/en-us/doc/payment/transaction-process" target="_blank">Payment Documentation</a>.</p>
</div><div class="paragraph">
<p>In the following section we provide you an overview about how the VRPay states are mapped into the Shopware State graph for orders and payment states.</p>
</div> <div class="section" id="_state_mapping_of_shopware_orders">
<div class="section-title">
<h2>
<span class="title-number">5.1</span>State mapping of Shopware orders </h2>
</div>
<div class="section-body">
<div class="paragraph">
<p>When the order gets abandoned also Order status goes to "cancel" after approx. 40 minutes. We also change the Payment status, and the Delivery status.</p>
</div> <div class="section" id="_general_remarks_regarding_order_statuses">
<div class="section-title">
<h3>
<span class="title-number">5.1.1</span>General remarks regarding order statuses </h3>
</div>
<div class="section-body">
<div class="paragraph">
<p>We recommend that you only change the Order status once the Payment status has reached a final state.</p>
</div> </div>
</div> </div>
</div> <div class="section" id="_state_mapping_of_shopware_payment_status">
<div class="section-title">
<h2>
<span class="title-number">5.2</span>State mapping of Shopware payment status </h2>
</div>
<div class="section-body">
<div class="paragraph">
<p>Below you find a diagram that shows the state machine of Shopware for payment status including additional information for the state transitions.</p>
</div><div class="imageblock">
<div class="content">
<img src="resource/shopware_6_stage_graph_order.svg" alt="shopware 6 stage graph order"/>
</div>
</div><div class="olist glossary">
<ol class="glossary">
<li>
<p>If the transaction is <code>Authorized</code> in VRPay, the Shopware order payment status is marked as <code>In Progress</code>.</p>
</li>
<li>
<p>If the transaction fails before or during the authorization process, the Shopware order payment status is marked as <code>Failed</code>.</p>
</li>
<li>
<p>If the transaction fails after the authorization, the Shopware order payment status is marked as <code>Cancelled</code>.</p>
</li>
<li>
<p>If the transaction invoice in VRPay is marked as <code>Paid</code> or <code>Not Applicable</code>, the Shopware order payment status is marked as <code>Paid</code>.</p>
</li>
</ol>
</div> <div class="section" id="_general_remarks_regarding_payment_statuses">
<div class="section-title">
<h3>
<span class="title-number">5.2.1</span>General remarks regarding payment statuses </h3>
</div>
<div class="section-body">
<div class="paragraph">
<p>We recommend that you do not change the payment status manually. If you do so, it may be changed again by the plugin.</p>
</div> </div>
</div> </div>
</div> <div class="section" id="_state_mapping_of_shopware_delivery_status">
<div class="section-title">
<h2>
<span class="title-number">5.3</span>State mapping of Shopware delivery status </h2>
</div>
<div class="section-body">
<div class="paragraph">
<p>Below you find a diagram that shows the state machine of Shopware delivery status including additional information for the state transitions.</p>
</div><div class="imageblock">
<div class="content">
<img src="resource/shopware_6_stage_graph_delivery.svg" alt="shopware 6 stage graph delivery"/>
</div>
</div><div class="olist glossary">
<ol class="glossary">
<li>
<p>If the transaction is <code>Confirmed</code> status in VRPay, the Shopware order delivery status is marked as <code>Hold</code>.</p>
</li>
<li>
<p>If the transaction in VRPay is marked as <code>Fulfill</code>, the Shopware order delivery status is marked as <code>Open</code>.</p>
</li>
<li>
<p>If the transaction is in <code>Decline</code>, <code>Failed</code> or <code>Voided</code>, the Shopware order delivery status is marked as <code>Cancelled</code>.</p>
</li>
</ol>
</div> </div>
</div> </div>
</div> <div class="chapter" id="_transaction_management">
<div class="chapter-title">
<h1>
<span class="title-number">6</span>Transaction management </h1>
</div>
<div class="chapter-body">
<div class="paragraph">
<p>You can capture, cancel and refund transactions directly from within the Shopware backend. Please note
if you refund, void or capture transactions inside VRPay the events will be synchronized into
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">6.1</span>Complete (capture) an order </h2>
</div>
<div class="section-body">
<div class="paragraph">
<p>You have the possibility for your transactions to have the payment only authorized after the order is placed. Inside the <a href="https://gateway.vr-payment.de/space/select?target=/payment/connector-configuration/list" target="_blank">connector configuration</a> you have the option, if the payment method supports it, to define whether the payment should be completed immediately or deferred.</p>
</div><div class="paragraph">
<p>In order to capture a transaction, open the order and click on the <code>Complete</code> button.</p>
</div><div class="admonitionblock note">
<table>
<tr>
<td class="icon">
<div class="title">Note</div>
</td>
<td class="content">
When the completion is pending in VRPay the order will stay in pending state.
</td>
</tr>
</table>
</div><div class="imageblock">
<div class="content">
<img src="resource/capture-transaction.png" alt="capture transaction"/>
</div>
</div><div class="paragraph">
<p><strong>Deferred payment completion</strong></p>
</div><div class="paragraph">
<p>Retailers often have the case that they want to authorize transactions only and start the fulfillment process once all items are shippable. This is also possible with VRPay.</p>
</div><div class="paragraph">
<p>However, certain processes should be followed. If you have configured payment completion to be deferred you should capture the transaction before you initiate the shipment
as it can always happen that a completion fails. If you want to be sure that you do not ship items for which you have not been paid you should postpone the shipment until
the fulfill state is reached. Initially the transaction will be in the <code>Authorized</code> state in VRPay and <code>In Progress</code> in Shopware. If you want to start the fulfillment process make sure you initiate the completion process as described above. Once the completion was successful the order will switch into the <code>Fulfill</code> state in VRPay and into <code>Paid</code> state in Shopware. You can now start the fulfillment process.</p>
</div> </div>
</div> <div class="section" id="_void_a_transaction">
<div class="section-title">
<h2>
<span class="title-number">6.2</span>Void a transaction </h2>
</div>
<div class="section-body">
<div class="paragraph">
<p>In order to void a transaction, open the order and click on the <code>Cancel authorization</code> button.</p>
</div><div class="admonitionblock note">
<table>
<tr>
<td class="icon">
<div class="title">Note</div>
</td>
<td class="content">
You can only void transactions that are not yet completed.
</td>
</tr>
</table>
</div><div class="imageblock">
<div class="content">
<img src="resource/void-transaction.png" alt="void transaction"/>
</div>
</div> </div>
</div> <div class="section" id="_refund_of_a_transaction">
<div class="section-title">
<h2>
<span class="title-number">6.3</span>Refund of a transaction </h2>
</div>
<div class="section-body">
<div class="paragraph">
<p>You have the possibility to refund already completed transactions. In order to do so, open the captured order. By clicking on the 3 dots (&#8230;&#8203;) on a line-item, you can refund the line-item partially (if it has a higher quantity than 1), or you can refund the whole line-item. In case the payment method does not support refunds, you will not see the possibility to issue online refunds.</p>
</div><div class="imageblock">
<div class="content">
<img src="resource/refund-transaction.png" alt="refund transaction"/>
</div>
</div><div class="paragraph">
<p>You can carry out as many individual refunds as you wish until you have reached the total amount of the original order.
The status of the order then automatically switches to complete.</p>
</div><div class="admonitionblock note">
<table>
<tr>
<td class="icon">
<div class="title">Note</div>
</td>
<td class="content">
It can take some time until you see the refund in Shopware. Refunds will only be visible once they have been processed successfully.
</td>
</tr>
</table>
</div> </div>
</div> <div class="section" id="_on_hold_orders">
<div class="section-title">
<h2>
<span class="title-number">6.4</span>On hold orders </h2>
</div>
<div class="section-body">
<div class="paragraph">
<p>The delivery should not be done whilst the delivery state is <code>Hold</code>. This happens when the transaction in VRPay
has not reached the fulfill state.</p>
</div><div class="paragraph">
<p>There are essentially two reasons why this can happen:</p>
</div><div class="ulist">
<ul>
<li>
<p>The transaction is not completed. In this case you have to complete the transaction as written above.</p>
</li>
<li>
<p>We are not able to tell if you should fulfill the order. The delivery decision is done automatically. If this does not happen
within the defined time frame, VRPay will generate a manual task which you should observe and follow the instructions.</p>
</li>
</ul>
</div><div class="paragraph">
<p>You can find more information about manual tasks in our <a href="https://gateway.vr-payment.de/en-us/doc/manual-tasks" target="_blank">Manual Task Documentation</a>.</p>
</div> </div>
</div> <div class="section" id="_limitations_of_the_synchronization_between_whitelabelname_and_shopware">
<div class="section-title">
<h2>
<span class="title-number">6.5</span>Limitations of the synchronization between VRPay and Shopware </h2>
</div>
<div class="section-body">
<div class="paragraph">
<p>Please note that captures, voids and refunds done in VRPay are synchronized. However, there are some
limitations. Inside VRPay you are able to change the <strong>unit price</strong> and the <strong>quantity</strong> at once. This can not
be done in the Shopware backend. We therefore recommend that you
perform the refunds always inside the Shopware backend and not inside VRPay. If a refund
cannot be synchronized it will be sent to the processor but it could be that you do not see it inside
your Shopware backend.</p>
</div><div class="paragraph">
<p>You can find more information about Refunds in VRPay in our <a href="https://gateway.vr-payment.de/en-us/doc/payment/refund" target="_blank">Refund Documentation</a>.</p>
</div> </div>
</div> <div class="section" id="_tokenization">
<div class="section-title">
<h2>
<span class="title-number">6.6</span>Tokenization </h2>
</div>
<div class="section-body">
<div class="paragraph">
<p>In case the payment method supports tokenization you can store the payment details of your customer for future purchases.
In order to use this feature make sure that the <strong>One-Click-Payment Mode</strong> in your <a href="https://gateway.vr-payment.de/space/select?target=/payment/method-configuration/list">payment method configuration</a> is set to <code>allow</code> or <code>force</code> storage.</p>
</div><div class="admonitionblock note">
<table>
<tr>
<td class="icon">
<div class="title">Note</div>
</td>
<td class="content">
Tokenization is not available for guest checkouts.
</td>
</tr>
</table>
</div> </div>
</div> </div>
</div> <div class="chapter" id="_error_logging">
<div class="chapter-title">
<h1>
<span class="title-number">7</span>Error logging </h1>
</div>
<div class="chapter-body">
<div class="paragraph">
<p>The extension uses the Shopware logging functions which are automatically active in your Shopware store.
The extension will log various unexpected errors or information which can help identify the cause of the error. You can find the logs on the server of your store in the var/log/ folder.</p>
</div> </div>
</div> <div class="chapter" id="_faq">
<div class="chapter-title">
<h1>
<span class="title-number">8</span>FAQ </h1>
</div>
<div class="chapter-body">
<div class="section" id="_how_to_install_the_plugin_using_composer">
<div class="section-title">
<h2>
<span class="title-number">8.1</span>How to install the plugin using composer? </h2>
</div>
<div class="section-body">
<div class="paragraph">
<p>You can install the plugin using composer by updating the <code>composer.json</code> file in the root directory of your Shopware store and wait for Composer to finish updating the dependencies.</p>
</div><div class="listingblock">
<div class="content">
<pre>composer require vrpayment/shopware-6</pre>
</div>
</div><div class="paragraph">
<p>Once this done, continue with step 2 of the installation process. See <a href="#_installation">Installation</a>.</p>
</div> </div>
</div> <div class="section" id="_how_can_i_make_the_payment_methods_appear_in_the_checkout">
<div class="section-title">
<h2>
<span class="title-number">8.2</span>How can I make the payment methods appear in the checkout? </h2>
</div>
<div class="section-body">
<div class="paragraph">
<p>Make sure that you followed the <a href="#_configuration">Configuration</a> section by stating your VRPay space ID and application user&#8217;s access information in the Shopware backend. By saving the configuration form the synchronization of the payment methods and the set up of the webhooks are initiated.</p>
</div><div class="paragraph">
<p>If this does not solve the problem, it could be that you use a special fee or coupon module that we do not support. Try to disable this plugin and see if it helps.
The payment methods are only displayed if the plugin&#8217;s total calculation matches the actual order total.</p>
</div> </div>
</div> </div>
</div> <div class="chapter" id="_sending_emails_with_flow_builder">
<div class="chapter-title">
<h1>
<span class="title-number">9</span>Sending emails with Flow Builder </h1>
</div>
<div class="chapter-body">
<div class="paragraph">
<p>In order for your emails to send when setting up custom flows using Flow Builder you must ensure that "Send order confirmation email" is disabled (located in the plugin settings).</p>
</div><div class="paragraph">
<p>If you you have already made seperate channel adjustments (using the channel dropdown in the plugin settings) you will have to ensure that the emails are disabled per channel accordingly.</p>
</div> </div>
</div> <div class="chapter" id="_troubleshooting">
<div class="chapter-title">
<h1>
<span class="title-number">10</span>Troubleshooting </h1>
</div>
<div class="chapter-body">
<div class="section" id="_webhook_error_api_version_not_available">
<div class="section-title">
<h2>
<span class="title-number">10.1</span>Webhook error (API version not available) </h2>
</div>
<div class="section-body">
<div class="paragraph">
<p>Webhooks communication fails because of a reply saying HTTP 404 with this or a similar error in the response:
<code>{"errors":[{"code":"0","status":"404","title":"Not Found","detail":"Requested api version v1 not available, available versions are v2, v3."}]}</code></p>
</div><div class="paragraph">
<p>Solution:
The Webhooks have to be updated. Shopware is using an API Version for all "URLs", which has to be updated in case shopware itself was updated.</p>
</div> </div>
</div> </div>
</div> <div class="chapter" id="_support">
<div class="chapter-title">
<h1>
<span class="title-number">11</span>Support </h1>
</div>
<div class="chapter-body">
<div class="paragraph">
<p>If you need help, feel free to contact our <a href="https://www.vr-payment.de/hotline">support</a>.</p>
</div> </div>
</div> </div>
</div>
</div>
<div class="col-right">
<div class="col-right-wrapper">
<div class="table-of-contents">
<ul class="nav">
<li class="nav-level-1">
<a href="#_prerequisites">
<span class="item-number">1</span>
<span class="item-title">Prerequisites</span>
</a>
</li> <li class="nav-level-1">
<a href="#_installation">
<span class="item-number">2</span>
<span class="item-title">Installation</span>
</a>
</li> <li class="nav-level-1">
<a href="#_configuration">
<span class="item-number">3</span>
<span class="item-title">Configuration</span>
</a>
</li> <li class="nav-level-1">
<a href="#_payment_method_configuration">
<span class="item-number">4</span>
<span class="item-title">Payment method configuration</span>
</a>
<ul class="nav">
<li class="nav-level-2">
<a href="#_setup">
<span class="item-number">4.1</span>
<span class="item-title">Setup</span>
</a>
</li> <li class="nav-level-2">
<a href="#_payment_method_rules">
<span class="item-number">4.2</span>
<span class="item-title">Payment method rules</span>
</a>
</li> <li class="nav-level-2">
<a href="#_customization">
<span class="item-number">4.3</span>
<span class="item-title">Customization</span>
</a>
</li> </ul>
</li> <li class="nav-level-1">
<a href="#_state_graph">
<span class="item-number">5</span>
<span class="item-title">State graph</span>
</a>
<ul class="nav">
<li class="nav-level-2">
<a href="#_state_mapping_of_shopware_orders">
<span class="item-number">5.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">5.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">5.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">5.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">5.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">6</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">6.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">6.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">6.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">6.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">6.5</span>
<span class="item-title">Limitations of the synchronization between VRPay and Shopware</span>
</a>
</li> <li class="nav-level-2">
<a href="#_tokenization">
<span class="item-number">6.6</span>
<span class="item-title">Tokenization</span>
</a>
</li> </ul>
</li> <li class="nav-level-1">
<a href="#_error_logging">
<span class="item-number">7</span>
<span class="item-title">Error logging</span>
</a>
</li> <li class="nav-level-1">
<a href="#_faq">
<span class="item-number">8</span>
<span class="item-title">FAQ</span>
</a>
<ul class="nav">
<li class="nav-level-2">
<a href="#_how_to_install_the_plugin_using_composer">
<span class="item-number">8.1</span>
<span class="item-title">How to install the plugin using composer?</span>
</a>
</li> <li class="nav-level-2">
<a href="#_how_can_i_make_the_payment_methods_appear_in_the_checkout">
<span class="item-number">8.2</span>
<span class="item-title">How can I make the payment methods appear in the checkout?</span>
</a>
</li> </ul>
</li> <li class="nav-level-1">
<a href="#_sending_emails_with_flow_builder">
<span class="item-number">9</span>
<span class="item-title">Sending emails with Flow Builder</span>
</a>
</li> <li class="nav-level-1">
<a href="#_troubleshooting">
<span class="item-number">10</span>
<span class="item-title">Troubleshooting</span>
</a>
<ul class="nav">
<li class="nav-level-2">
<a href="#_webhook_error_api_version_not_available">
<span class="item-number">10.1</span>
<span class="item-title">Webhook error (API version not available)</span>
</a>
</li> </ul>
</li> <li class="nav-level-1">
<a href="#_support">
<span class="item-number">11</span>
<span class="item-title">Support</span>
</a>
</li> </ul>
</div> </div>
</div>
</div>
</div>
<script type="text/javascript" src="assets/jquery.js"></script>
<script type="text/javascript" src="assets/scrollspy.js"></script>
<script type="text/javascript" src="assets/sticky-kit.js"></script>
<script type="text/javascript" src="assets/highlight.js"></script>
<script type="text/javascript" src="assets/base.js"></script>
</body>
</html>
@@ -0,0 +1,238 @@
<?php declare(strict_types=1);
namespace VRPaymentPayment\Core\Api\Configuration\Controller;
use Psr\Log\LoggerInterface;
use Shopware\Core\{
Framework\Context,
Framework\Log\Package};
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\{
HttpFoundation\JsonResponse,
HttpFoundation\Request,
HttpFoundation\Response,
Routing\Attribute\Route};
use VRPaymentPayment\Core\{
Api\OrderDeliveryState\Service\OrderDeliveryStateService,
Api\PaymentMethodConfiguration\Service\PaymentMethodConfigurationService,
Api\WebHooks\Service\WebHooksService,
Api\Space\Service\SpaceService,
Settings\Service\SettingsService,
Util\PaymentMethodUtil};
/**
* Class ConfigurationController
*
* This class handles web calls that are made via the VRPaymentPayment settings page.
*
* @package VRPaymentPayment\Core\Api\Config\Controller
*/
#[Package('system-settings')]
#[Route(defaults: ['_routeScope' => ['api']])]
class ConfigurationController extends AbstractController {
/**
* @var \VRPaymentPayment\Core\Api\WebHooks\Service\WebHooksService
*/
protected $webHooksService;
/**
* @var \VRPaymentPayment\Core\Api\Space\Service\SpaceService
*/
protected $spaceService;
/**
* @var \Psr\Log\LoggerInterface
*/
protected $logger;
/**
* @var \VRPaymentPayment\Core\Settings\Service\SettingsService
*/
protected $settingsService;
/**
* @var \VRPaymentPayment\Core\Util\PaymentMethodUtil
*/
private $paymentMethodUtil;
/**
* @var \VRPaymentPayment\Core\Api\PaymentMethodConfiguration\Service\PaymentMethodConfigurationService
*/
private $paymentMethodConfigurationService;
/**
* @param PaymentMethodUtil $paymentMethodUtil
* @param PaymentMethodConfigurationService $paymentMethodConfigurationService
* @param WebHooksService $webHooksService
* @param SpaceService $spaceService
* @param SettingsService $settingsService
*/
public function __construct(
PaymentMethodUtil $paymentMethodUtil,
PaymentMethodConfigurationService $paymentMethodConfigurationService,
WebHooksService $webHooksService,
SpaceService $spaceService,
SettingsService $settingsService
)
{
$this->webHooksService = $webHooksService;
$this->spaceService = $spaceService;
$this->paymentMethodUtil = $paymentMethodUtil;
$this->paymentMethodConfigurationService = $paymentMethodConfigurationService;
$this->settingsService = $settingsService;
}
/**
* @param \Psr\Log\LoggerInterface $logger
* @internal
* @required
*
*/
public function setLogger(LoggerInterface $logger): void
{
$this->logger = $logger;
}
/**
* Set VRPaymentPayment as the default payment for a give sales channel
*
* @param \Symfony\Component\HttpFoundation\Request $request
* @param \Shopware\Core\Framework\Context $context
* @return \Symfony\Component\HttpFoundation\JsonResponse
*
*/
#[Route("/api/_action/vrpayment/configuration/set-vrpayment-as-sales-channel-payment-default",
name: "api.action.vrpayment.configuration.set-vrpayment-as-sales-channel-payment-default",
methods: ['POST'])]
public function setVRPaymentAsSalesChannelPaymentDefault(Request $request, Context $context): JsonResponse
{
$salesChannelId = $request->request->get('salesChannelId');
$salesChannelId = ($salesChannelId == 'null') ? null : $salesChannelId;
$this->paymentMethodUtil->setVRPaymentAsDefaultPaymentMethod($context, $salesChannelId);
return new JsonResponse([]);
}
/**
* Register web hooks
*
* @param \Symfony\Component\HttpFoundation\Request $request
* @return \Symfony\Component\HttpFoundation\JsonResponse
* @throws \VRPayment\Sdk\ApiException
* @throws \VRPayment\Sdk\Http\ConnectionException
* @throws \VRPayment\Sdk\VersioningException
*
*/
#[Route("/api/_action/vrpayment/configuration/register-web-hooks",
name: "api.action.vrpayment.configuration.register-web-hooks",
methods: ['POST'])]
public function registerWebHooks(Request $request): JsonResponse
{
$settings = $this->settingsService->getSettings();
if ($settings->isWebhooksUpdateEnabled() === false) {
$this->logger->info('Webhooks update disabled by settings');
return new JsonResponse([]);
}
$salesChannelId = $request->request->get('salesChannelId');
$salesChannelId = ($salesChannelId == 'null') ? null : $salesChannelId;
$result = $this->webHooksService->setSalesChannelId($salesChannelId)->install();
return new JsonResponse(['result' => $result]);
}
/**
* Test API connection
* If the API data is incorrect, an entry must appear in the event log file in the Shopware folder /var/log/
* @see https://developer.shopware.com/docs/resources/guidelines/testing/store/quality-guidelines-plugins/#every-app-accessing-external-api-services
*
* @param \Symfony\Component\HttpFoundation\Request $request
* @return \Symfony\Component\HttpFoundation\JsonResponse
* @throws \VRPayment\Sdk\ApiException
* @throws \VRPayment\Sdk\Http\ConnectionException
* @throws \VRPayment\Sdk\VersioningException
*
*/
#[Route("/api/_action/vrpayment/configuration/check-api-connection",
name: "api.action.vrpayment.configuration.check-api-connection",
methods: ['POST'])]
public function checkApiConnection(Request $request): JsonResponse
{
$spaceId = (int)$request->request->getInt('spaceId');
$userId = (int)$request->request->getInt('userId');
$applicationId = $request->request->get('applicationId');
$result = $this->spaceService
->setSpaceId($spaceId)
->setUserId($userId)
->setApplicationId($applicationId)
->checkSpace();
if (null === $result) {
$this->logger->error('API test connection was failed. Wrong credentials');
return new JsonResponse([['result' => 400]]);
}
$this->logger->info('API test connection was successfully tested.');
return new JsonResponse(['result' => 200]);
}
/**
* Synchronize payment method configurations
*
* @param \Symfony\Component\HttpFoundation\Request $request
* @param \Shopware\Core\Framework\Context $context
* @return \Symfony\Component\HttpFoundation\JsonResponse
*
*/
#[Route("/api/_action/vrpayment/configuration/synchronize-payment-method-configuration",
name: "api.action.vrpayment.configuration.synchronize-payment-method-configuration",
methods: ['POST'])]
public function synchronizePaymentMethodConfiguration(Request $request, Context $context): JsonResponse
{
$settings = $this->settingsService->getSettings();
if ($settings->isPaymentsUpdateEnabled() === false) {
$this->logger->info('Payment methods update disabled by settings');
return new JsonResponse([]);
}
$salesChannelId = $request->request->get('salesChannelId');
$salesChannelId = ($salesChannelId == 'null') ? null : $salesChannelId;
$status = Response::HTTP_OK;
try {
$result = $this->paymentMethodConfigurationService->setSalesChannelId($salesChannelId)->synchronize($context);
} catch (\Exception $exception) {
$status = Response::HTTP_NOT_ACCEPTABLE;
$result = [
'errorTitle' => $exception->getMessage(),
'errorMessage' => $exception->getTraceAsString()
];
$this->logger->emergency($exception->getTraceAsString());
}
return new JsonResponse(['result' => $result], $status);
}
/**
* Install OrderDeliveryStates
*
* @param \Shopware\Core\Framework\Context $context
* @return \Symfony\Component\HttpFoundation\JsonResponse
*
*/
#[Route("/api/_action/vrpayment/configuration/install-order-delivery-states",
name: "api.action.vrpayment.configuration.install-order-delivery-states",
methods: ['POST'])]
public function installOrderDeliveryStates(Context $context): JsonResponse
{
/**
* @var \VRPaymentPayment\Core\Api\OrderDeliveryState\Service\OrderDeliveryStateService $orderDeliveryStateService
*/
$orderDeliveryStateService = $this->container->get(OrderDeliveryStateService::class);
$orderDeliveryStateService->install($context);
return new JsonResponse([]);
}
}
@@ -0,0 +1,59 @@
<?php declare(strict_types=1);
namespace VRPaymentPayment\Core\Api\OrderDeliveryState\Command;
use Shopware\Core\Framework\Context;
use Symfony\Component\{
Console\Command\Command,
Console\Attribute\AsCommand,
Console\Input\InputInterface,
Console\Output\OutputInterface};
use VRPaymentPayment\Core\Api\OrderDeliveryState\Service\OrderDeliveryStateService;
/**
* Class OrderDeliveryStateCommand
*
* @package VRPaymentPayment\Core\Api\OrderDeliveryState\Command
*/
#[AsCommand(name: 'vrpayment:order-delivery-states:install')]
class OrderDeliveryStateCommand extends Command {
/**
* @var \VRPaymentPayment\Core\Api\OrderDeliveryState\Service\OrderDeliveryStateService
*/
protected $orderDeliveryStateService;
/**
* OrderDeliveryStateCommand constructor.
*
* @param \VRPaymentPayment\Core\Api\OrderDeliveryState\Service\OrderDeliveryStateService $orderDeliveryStateService
*/
public function __construct(OrderDeliveryStateService $orderDeliveryStateService)
{
parent::__construct();
$this->orderDeliveryStateService = $orderDeliveryStateService;
}
/**
* @param \Symfony\Component\Console\Input\InputInterface $input
* @param \Symfony\Component\Console\Output\OutputInterface $output
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$output->writeln('Install VRPaymentPayment extra delivery states...');
$this->orderDeliveryStateService->install(Context::createDefaultContext());
return 0;
}
/**
* Configures the current command.
*/
protected function configure()
{
$this->setDescription('Installs VRPaymentPayment extra delivery states.')
->setHelp('This command installs VRPaymentPayment extra delivery states.');
}
}
@@ -0,0 +1,88 @@
<?php declare(strict_types=1);
namespace VRPaymentPayment\Core\Api\OrderDeliveryState\Handler;
use Shopware\Core\{
Checkout\Order\Aggregate\OrderDelivery\OrderDeliveryDefinition,
Framework\Context,
System\StateMachine\Aggregation\StateMachineTransition\StateMachineTransitionActions,
System\StateMachine\StateMachineRegistry,
System\StateMachine\Transition};
/**
* Class OrderDeliveryStateHandler
*
* @package VRPaymentPayment\Core\Api\OrderDeliveryState\Handler
*/
class OrderDeliveryStateHandler {
public const STATE_HOLD = 'hold';
public const ACTION_HOLD = 'hold';
public const ACTION_UNHOLD = 'unhold';
/**
* @var \Shopware\Core\System\StateMachine\StateMachineRegistry
*/
private $stateMachineRegistry;
/**
* OrderDeliveryStateHandler constructor.
*
* @param \Shopware\Core\System\StateMachine\StateMachineRegistry $stateMachineRegistry
*/
public function __construct(StateMachineRegistry $stateMachineRegistry)
{
$this->stateMachineRegistry = $stateMachineRegistry;
}
/**
* @param string $entityId
* @param \Shopware\Core\Framework\Context $context
*/
public function hold(string $entityId, Context $context): void
{
$this->stateMachineRegistry->transition(
new Transition(
OrderDeliveryDefinition::ENTITY_NAME,
$entityId,
self::ACTION_HOLD,
'stateId'
),
$context
);
}
/**
* @param string $entityId
* @param \Shopware\Core\Framework\Context $context
*/
public function unhold(string $entityId, Context $context): void
{
$this->stateMachineRegistry->transition(
new Transition(
OrderDeliveryDefinition::ENTITY_NAME,
$entityId,
self::ACTION_UNHOLD,
'stateId'
),
$context
);
}
/**
* @param string $entityId
* @param \Shopware\Core\Framework\Context $context
*/
public function cancel(string $entityId, Context $context): void
{
$this->stateMachineRegistry->transition(
new Transition(
OrderDeliveryDefinition::ENTITY_NAME,
$entityId,
StateMachineTransitionActions::ACTION_CANCEL,
'stateId'
),
$context
);
}
}
@@ -0,0 +1,197 @@
<?php declare(strict_types=1);
namespace VRPaymentPayment\Core\Api\OrderDeliveryState\Service;
use Psr\Container\ContainerInterface;
use Shopware\Core\{
Checkout\Order\Aggregate\OrderDelivery\OrderDeliveryStates,
Framework\Context,
Framework\DataAbstractionLayer\Search\Criteria,
Framework\DataAbstractionLayer\Search\Filter\EqualsFilter,
Framework\Uuid\Uuid};
use VRPaymentPayment\Core\{
Api\OrderDeliveryState\Handler\OrderDeliveryStateHandler,
Util\LocaleCodeProvider};
/**
* Class OrderDeliveryStateService
*
* @package VRPaymentPayment\Core\Api\OrderDeliveryState\Service
*/
class OrderDeliveryStateService {
/**
* @var \Psr\Container\ContainerInterface
*/
protected $container;
/**
* @var \VRPaymentPayment\Core\Util\LocaleCodeProvider
*/
protected $localeCodeProvider;
/**
* @var \Shopware\Core\System\StateMachine\StateMachineDefinition
*/
protected $stateMachineRepository;
/**
* @var \Shopware\Core\System\StateMachine\Aggregation\StateMachineTransition\StateMachineTransitionDefinition
*/
protected $stateMachineTransitionRepository;
/**
* @var \Shopware\Core\System\StateMachine\Aggregation\StateMachineState\StateMachineStateDefinition
*/
protected $stateMachineStateRepository;
/**
* OrderDeliveryStateHandler constructor.
*
* @param \Psr\Container\ContainerInterface $container
*/
public function __construct(ContainerInterface $container)
{
$this->container = $container;
$this->localeCodeProvider = $this->container->get(LocaleCodeProvider::class);
$this->stateMachineRepository = $this->container->get('state_machine.repository');
$this->stateMachineStateRepository = $this->container->get('state_machine_state.repository');
$this->stateMachineTransitionRepository = $this->container->get('state_machine_transition.repository');
}
/**
* @param \Shopware\Core\Framework\Context $context
*/
public function install(Context $context): void
{
$stateMachineId = $this->getStateMachineEntity($context);
$holdStateId = $this->getHoldStateId($stateMachineId, $context);
$openStateId = $this->getOpenStateId($stateMachineId, $context);
$this->upsertHoldTransition($stateMachineId, $openStateId, $holdStateId, $context);
$this->upsertUnholdTransition($stateMachineId, $holdStateId, $openStateId, $context);
}
/**
* @param \Shopware\Core\Framework\Context $context
*
* @return \Shopware\Core\System\StateMachine\StateMachineEntity
*/
protected function getStateMachineEntity(Context $context): string
{
$stateMachineCriteria = (new Criteria())
->addFilter(new EqualsFilter('technicalName', OrderDeliveryStates::STATE_MACHINE));
return $this->stateMachineRepository->search($stateMachineCriteria, $context)->first()->getId();
}
/**
* @param string $stateMachineId
* @param \Shopware\Core\Framework\Context $context
*
* @return string
*/
protected function getHoldStateId(string $stateMachineId, Context $context): string
{
$holdStateMachineStateCriteria = (new Criteria())
->addFilter(
new EqualsFilter('technicalName', OrderDeliveryStateHandler::STATE_HOLD),
new EqualsFilter('stateMachineId', $stateMachineId)
);
$holdStateMachineStateEntity = $this->stateMachineStateRepository->search($holdStateMachineStateCriteria, $context)->first();
$holdStateId = is_null($holdStateMachineStateEntity) ? Uuid::randomHex() : $holdStateMachineStateEntity->getId();
if (is_null($holdStateMachineStateEntity)) {
$translations = $this->localeCodeProvider->getAvailableTranslations('vrpayment.deliveryState.hold', 'Hold', $context);
$data = [
'id' => $holdStateId,
'technicalName' => OrderDeliveryStateHandler::STATE_HOLD,
'stateMachineId' => $stateMachineId,
'translations' => $translations,
];
$this->stateMachineStateRepository->upsert([$data], $context);
}
return $holdStateId;
}
/**
* @param string $stateMachineId
* @param \Shopware\Core\Framework\Context $context
*
* @return string
*/
protected function getOpenStateId(string $stateMachineId, Context $context): string
{
$stateMachineStateCriteria = (new Criteria())
->addFilter(
new EqualsFilter('technicalName', OrderDeliveryStates::STATE_OPEN),
new EqualsFilter('stateMachineId', $stateMachineId)
);
return $this->stateMachineStateRepository->search($stateMachineStateCriteria, $context)->first()->getId();
}
/**
* @param string $stateMachineId
* @param string $openStateId
* @param string $holdStateId
* @param \Shopware\Core\Framework\Context $context
*/
protected function upsertHoldTransition(string $stateMachineId, string $openStateId, string $holdStateId, Context $context): void
{
$translations = $this->localeCodeProvider->getAvailableTranslations('vrpayment.deliveryState.hold','Hold', $context);
$this->upsertTransition(OrderDeliveryStateHandler::ACTION_HOLD, $stateMachineId, $openStateId, $holdStateId, $translations, $context);
}
/**
* @param string $actionName
* @param string $stateMachineId
* @param string $fromStateId
* @param string $toStateId
* @param array $translations
* @param \Shopware\Core\Framework\Context $context
*/
protected function upsertTransition(string $actionName, string $stateMachineId, string $fromStateId, string $toStateId, array $translations, Context $context): void
{
$criteria = (new Criteria())
->addFilter(
new EqualsFilter('actionName', $actionName),
new EqualsFilter('stateMachineId', $stateMachineId),
new EqualsFilter('fromStateId', $fromStateId),
new EqualsFilter('toStateId', $toStateId)
);
$stateMachineTransitionEntity = $this->stateMachineTransitionRepository->search($criteria, $context)->first();
$transitionId = is_null($stateMachineTransitionEntity) ? Uuid::randomHex() : $stateMachineTransitionEntity->getId();
if (is_null($stateMachineTransitionEntity)) {
$data = [
'id' => $transitionId,
'actionName' => $actionName,
'stateMachineId' => $stateMachineId,
'fromStateId' => $fromStateId,
'toStateId' => $toStateId,
'translations' => $translations,
];
$this->stateMachineTransitionRepository->upsert([$data], $context);
}
}
/**
* @param string $stateMachineId
* @param string $openStateId
* @param string $holdStateId
* @param \Shopware\Core\Framework\Context $context
*/
protected function upsertUnholdTransition(string $stateMachineId, string $holdStateId, string $openStateId, Context $context): void
{
$translations = $this->localeCodeProvider->getAvailableTranslations('vrpayment.deliveryState.unhold','Unhold',$context);
$this->upsertTransition(OrderDeliveryStateHandler::ACTION_UNHOLD, $stateMachineId, $holdStateId, $openStateId, $translations, $context);
}
}
@@ -0,0 +1,62 @@
<?php declare(strict_types=1);
namespace VRPaymentPayment\Core\Api\PaymentMethodConfiguration\Command;
use Shopware\Core\Framework\Context;
use Symfony\Component\{
Console\Command\Command,
Console\Attribute\AsCommand,
Console\Input\InputInterface,
Console\Output\OutputInterface};
use VRPaymentPayment\Core\Api\PaymentMethodConfiguration\Service\PaymentMethodConfigurationService;
/**
* Class PaymentMethodConfigurationCommand
*
* @package VRPaymentPayment\Core\Api\PaymentMethodConfiguration\Command
*/
#[AsCommand(name: 'vrpayment:payment-method:configuration')]
class PaymentMethodConfigurationCommand extends Command {
/**
* @var \VRPaymentPayment\Core\Api\PaymentMethodConfiguration\Service\PaymentMethodConfigurationService
*/
protected $paymentMethodConfigurationService;
/**
* PaymentMethodConfigurationCommand constructor.
*
* @param \VRPaymentPayment\Core\Api\PaymentMethodConfiguration\Service\PaymentMethodConfigurationService $paymentMethodConfigurationService
*/
public function __construct(PaymentMethodConfigurationService $paymentMethodConfigurationService)
{
parent::__construct();
$this->paymentMethodConfigurationService = $paymentMethodConfigurationService;
}
/**
* @param \Symfony\Component\Console\Input\InputInterface $input
* @param \Symfony\Component\Console\Output\OutputInterface $output
* @return int
* @throws \VRPayment\Sdk\ApiException
* @throws \VRPayment\Sdk\Http\ConnectionException
* @throws \VRPayment\Sdk\VersioningException
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$output->writeln('Fetch VRPaymentPayment space available payment methods...');
$this->paymentMethodConfigurationService->synchronize(Context::createDefaultContext());
return 0;
}
/**
* Configures the current command.
*/
protected function configure()
{
$this->setDescription('Fetches VRPaymentPayment space available payment methods.')
->setHelp('This command fetches VRPaymentPayment space available payment methods.');
}
}
@@ -0,0 +1,61 @@
<?php declare(strict_types=1);
namespace VRPaymentPayment\Core\Api\PaymentMethodConfiguration\Command;
use Shopware\Core\Framework\Context;
use Symfony\Component\{
Console\Command\Command,
Console\Attribute\AsCommand,
Console\Input\InputInterface,
Console\Output\OutputInterface};
use VRPaymentPayment\Core\Util\PaymentMethodUtil;
/**
* Class PaymentMethodDefaultCommand
*
* @package VRPaymentPayment\Core\Api\PaymentMethodConfiguration\Command
*/
#[AsCommand(name: 'vrpayment:payment-method:default')]
class PaymentMethodDefaultCommand extends Command {
/**
* @var \VRPaymentPayment\Core\Util\PaymentMethodUtil
*/
protected $paymentMethodUtil;
/**
* PaymentMethodDefaultCommand constructor.
*
* @param \VRPaymentPayment\Core\Util\PaymentMethodUtil $paymentMethodUtil
*/
public function __construct(PaymentMethodUtil $paymentMethodUtil)
{
parent::__construct();
$this->paymentMethodUtil = $paymentMethodUtil;
}
/**
* @param \Symfony\Component\Console\Input\InputInterface $input
* @param \Symfony\Component\Console\Output\OutputInterface $output
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$output->writeln('Set VRPaymentPayment as default payment method...');
$context = Context::createDefaultContext();
$this->paymentMethodUtil->setVRPaymentAsDefaultPaymentMethod($context);
$this->paymentMethodUtil->disableSystemPaymentMethods($context);
return 0;
}
/**
* Configures the current command.
*/
protected function configure()
{
$this->setDescription('Sets VRPaymentPayment as default payment method.')
->setHelp('This command updates VRPaymentPayment as default payment method for all SalesChannels.');
}
}
@@ -0,0 +1,165 @@
<?php declare(strict_types=1);
namespace VRPaymentPayment\Core\Api\PaymentMethodConfiguration\Entity;
use Shopware\Core\{
Checkout\Payment\PaymentMethodEntity,
Framework\DataAbstractionLayer\Entity,
Framework\DataAbstractionLayer\EntityIdTrait};
/**
* Class PaymentMethodConfigurationEntity
*
* @package VRPaymentPayment\Core\Api\PaymentMethodConfiguration\Entity
*/
class PaymentMethodConfigurationEntity extends Entity {
use EntityIdTrait;
/**
* @var array
*/
protected $data;
/**
* @var \Shopware\Core\Checkout\Payment\PaymentMethodEntity|null
*/
protected $paymentMethod;
/**
* @var int
*/
protected $paymentMethodConfigurationId;
/**
* @var string
*/
protected $paymentMethodId;
/**
* @var string
*/
protected $sortOrder;
/**
* @var int
*/
protected $spaceId;
/**
* @var string
*/
protected $state;
/**
* @return array
*/
public function getData(): array
{
return $this->data;
}
/**
* @param array $data
*/
public function setData(array $data): void
{
$this->data = $data;
}
/**
* @return \Shopware\Core\Checkout\Payment\PaymentMethodEntity|null
*/
public function getPaymentMethod(): ?PaymentMethodEntity
{
return $this->paymentMethod;
}
/**
* @param \Shopware\Core\Checkout\Payment\PaymentMethodEntity $paymentMethod
*/
public function setPaymentMethod(PaymentMethodEntity $paymentMethod): void
{
$this->paymentMethod = $paymentMethod;
}
/**
* @return int
*/
public function getPaymentMethodConfigurationId(): int
{
return $this->paymentMethodConfigurationId;
}
/**
* @param int $paymentMethodConfigurationId
*/
public function setPaymentMethodConfigurationId(int $paymentMethodConfigurationId): void
{
$this->paymentMethodConfigurationId = $paymentMethodConfigurationId;
}
/**
* @return string
*/
public function getPaymentMethodId(): string
{
return $this->paymentMethodId;
}
/**
* @param string $paymentMethodId
*/
public function setPaymentMethodId(string $paymentMethodId): void
{
$this->paymentMethodId = $paymentMethodId;
}
/**
* @return string
*/
public function getSortOrder(): string
{
return $this->sortOrder;
}
/**
* @param string $sortOrder
*/
public function setSortOrder(string $sortOrder): void
{
$this->sortOrder = $sortOrder;
}
/**
* @return int
*/
public function getSpaceId(): int
{
return $this->spaceId;
}
/**
* @param int $spaceId
*/
public function setSpaceId(int $spaceId): void
{
$this->spaceId = $spaceId;
}
/**
* @return string
*/
public function getState(): string
{
return $this->state;
}
/**
* @param string $state
*/
public function setState(string $state): void
{
$this->state = $state;
}
}
@@ -0,0 +1,29 @@
<?php declare(strict_types=1);
namespace VRPaymentPayment\Core\Api\PaymentMethodConfiguration\Entity;
use Shopware\Core\Framework\DataAbstractionLayer\EntityCollection;
/**
* Class PaymentMethodConfigurationEntityCollection
*
* @package VRPaymentPayment\Core\Api\PaymentMethodConfiguration\Entity
*
* @method void add(PaymentMethodConfigurationEntity $entity)
* @method void set(string $key, PaymentMethodConfigurationEntity $entity)
* @method PaymentMethodConfigurationEntity[] getIterator()
* @method PaymentMethodConfigurationEntity[] getElements()
* @method PaymentMethodConfigurationEntity|null get(string $key)
* @method PaymentMethodConfigurationEntity|null first()
* @method PaymentMethodConfigurationEntity|null last()
*/
class PaymentMethodConfigurationEntityCollection extends EntityCollection {
/**
* @return string
*/
protected function getExpectedClass(): string
{
return PaymentMethodConfigurationEntity::class;
}
}
@@ -0,0 +1,72 @@
<?php declare(strict_types=1);
namespace VRPaymentPayment\Core\Api\PaymentMethodConfiguration\Entity;
use Shopware\Core\{
Checkout\Payment\PaymentMethodDefinition,
Framework\DataAbstractionLayer\EntityDefinition,
Framework\DataAbstractionLayer\Field\CreatedAtField,
Framework\DataAbstractionLayer\Field\FkField,
Framework\DataAbstractionLayer\Field\Flag\PrimaryKey,
Framework\DataAbstractionLayer\Field\Flag\Required,
Framework\DataAbstractionLayer\Field\IdField,
Framework\DataAbstractionLayer\Field\IntField,
Framework\DataAbstractionLayer\Field\JsonField,
Framework\DataAbstractionLayer\Field\OneToOneAssociationField,
Framework\DataAbstractionLayer\Field\StringField,
Framework\DataAbstractionLayer\Field\UpdatedAtField,
Framework\DataAbstractionLayer\FieldCollection};
/**
* Class PaymentMethodConfigurationEntityDefinition
*
* @package VRPaymentPayment\Core\Api\PaymentMethodConfiguration\Entity
*/
class PaymentMethodConfigurationEntityDefinition extends EntityDefinition {
public const ENTITY_NAME = 'vrpayment_payment_method_configuration';
/**
* @return string
*/
public function getEntityName(): string
{
return self::ENTITY_NAME;
}
/**
* @return \Shopware\Core\Framework\DataAbstractionLayer\FieldCollection
*/
protected function defineFields(): FieldCollection
{
return new FieldCollection([
(new IdField('id', 'id'))->addFlags(new PrimaryKey(), new Required()),
(new JsonField('data', 'data'))->addFlags(new Required()),
(new IntField('payment_method_configuration_id', 'paymentMethodConfigurationId'))->addFlags(new Required()),
(new FkField('payment_method_id', 'paymentMethodId', PaymentMethodDefinition::class))->addFlags(new Required()),
(new IntField('sort_order', 'sortOrder'))->addFlags(new Required()),
(new IntField('space_id', 'spaceId'))->addFlags(new Required()),
(new StringField('state', 'state'))->addFlags(new Required()),
new OneToOneAssociationField('paymentMethod', 'payment_method_id', 'id', PaymentMethodDefinition::class, true),
new CreatedAtField(),
new UpdatedAtField(),
]);
}
/**
* @return string
*/
public function getCollectionClass(): string
{
return PaymentMethodConfigurationEntityCollection::class;
}
/**
* @return string
*/
public function getEntityClass(): string
{
return PaymentMethodConfigurationEntity::class;
}
}
@@ -0,0 +1,617 @@
<?php declare(strict_types=1);
namespace VRPaymentPayment\Core\Api\PaymentMethodConfiguration\Service;
use Doctrine\DBAL\Connection;
use Psr\Log\LoggerInterface;
use Shopware\Core\{
Content\ImportExport\DataAbstractionLayer\Serializer\Entity\MediaSerializer,
Content\ImportExport\DataAbstractionLayer\Serializer\SerializerRegistry,
Content\ImportExport\Struct\Config,
Content\Media\MediaDefinition,
Framework\Context,
Framework\DataAbstractionLayer\Search\Criteria,
Framework\DataAbstractionLayer\Search\Filter\EqualsFilter,
Framework\Plugin\Util\PluginIdProvider,
Framework\Uuid\Uuid};
use Symfony\Component\DependencyInjection\ContainerInterface;
use VRPayment\Sdk\{
ApiClient,
Model\CreationEntityState,
Model\CriteriaOperator,
Model\EntityQuery,
Model\EntityQueryFilter,
Model\EntityQueryFilterType,
Model\PaymentMethodConfiguration,
Model\RestLanguage};
use VRPaymentPayment\Core\{
Api\PaymentMethodConfiguration\Entity\PaymentMethodConfigurationEntity,
Api\PaymentMethodConfiguration\Entity\PaymentMethodConfigurationEntityDefinition,
Checkout\PaymentHandler\VRPaymentPaymentHandler,
Settings\Service\SettingsService,
Util\LocaleCodeProvider};
use VRPaymentPayment\VRPaymentPayment;
/**
* Class PaymentMethodConfigurationService
*
* @package VRPaymentPayment\Core\Api\PaymentMethodConfiguration\Service
*/
class PaymentMethodConfigurationService {
/**
* @var \VRPaymentPayment\Core\Settings\Service\SettingsService
*/
protected $settingsService;
/**
* @var \VRPayment\Sdk\ApiClient
*/
protected $apiClient;
/**
* Space Id
*
* @var int
*/
protected $spaceId;
/**
* @var \Symfony\Component\DependencyInjection\ContainerInterface
*/
protected $container;
/**
* @var \Shopware\Core\Content\ImportExport\DataAbstractionLayer\Serializer\SerializerRegistry
*/
protected $serializerRegistry;
/**
* @var \Psr\Log\LoggerInterface
*/
protected $logger;
/**
* @var ?string $salesChannelId
*/
private $salesChannelId;
/**
* @var \Shopware\Core\Content\ImportExport\DataAbstractionLayer\Serializer\Entity\MediaSerializer
*/
private $mediaSerializer;
/**
* @var
*/
private $languages;
/**
* @var \VRPaymentPayment\Core\Util\LocaleCodeProvider
*/
private $localeCodeProvider;
/**
* @var \Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface
*/
private $ruleRepository;
/**
* @var \Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface
*/
private $paymentMethodRepository;
/**
* PaymentMethodConfigurationService constructor.
*
* @param \VRPaymentPayment\Core\Settings\Service\SettingsService $settingsService
* @param \Symfony\Component\DependencyInjection\ContainerInterface $container
* @param \Shopware\Core\Content\ImportExport\DataAbstractionLayer\Serializer\Entity\MediaSerializer $mediaSerializer
* @param \Shopware\Core\Content\ImportExport\DataAbstractionLayer\Serializer\SerializerRegistry $serializerRegistry
*/
public function __construct(
SettingsService $settingsService,
ContainerInterface $container,
MediaSerializer $mediaSerializer,
SerializerRegistry $serializerRegistry
)
{
$this->container = $container;
$this->ruleRepository = $this->container->get('rule.repository');
$this->settingsService = $settingsService;
$this->mediaSerializer = $mediaSerializer;
$this->serializerRegistry = $serializerRegistry;
$this->localeCodeProvider = $this->container->get(LocaleCodeProvider::class);
$this->paymentMethodRepository = $this->container->get('payment_method.repository');
}
/**
* @param \Psr\Log\LoggerInterface $logger
*
* @internal
* @required
*
*/
public function setLogger(LoggerInterface $logger): void
{
$this->logger = $logger;
}
/**
* @return \VRPayment\Sdk\ApiClient
*/
public function getApiClient(): ApiClient
{
return $this->apiClient;
}
/**
* @param \VRPayment\Sdk\ApiClient $apiClient
*
* @return \VRPaymentPayment\Core\Api\PaymentMethodConfiguration\Service\PaymentMethodConfigurationService
*/
public function setApiClient(ApiClient $apiClient): PaymentMethodConfigurationService
{
$this->apiClient = $apiClient;
return $this;
}
/**
* @param \Shopware\Core\Framework\Context $context
*
* @return array
* @throws \VRPayment\Sdk\ApiException
* @throws \VRPayment\Sdk\Http\ConnectionException
* @throws \VRPayment\Sdk\VersioningException
*/
public function synchronize(Context $context): array
{
// Configuration
$settings = $this->settingsService->getSettings($this->getSalesChannelId());
$this->setSpaceId($settings->getSpaceId())
->setApiClient($settings->getApiClient());
$this->disablePaymentMethodConfigurations($context);
$this->enablePaymentMethodConfigurations($context);
$this->disableOrphanedPaymentMethods();
return [];
}
/**
* Get sales channel id
*
* @return string|null
*/
public function getSalesChannelId(): ?string
{
return $this->salesChannelId;
}
/**
* Set sales channel id
*
* @param string|null $salesChannelId
*
* @return \VRPaymentPayment\Core\Api\PaymentMethodConfiguration\Service\PaymentMethodConfigurationService
*/
public function setSalesChannelId(?string $salesChannelId = null): PaymentMethodConfigurationService
{
$this->salesChannelId = $salesChannelId;
return $this;
}
/**
* @param \Shopware\Core\Framework\Context $context
*/
private function disablePaymentMethodConfigurations(Context $context): void
{
$data = [];
$paymentMethodData = [];
$salesChannelPaymentMethodData = [];
$criteria = (new Criteria())->addFilter(new EqualsFilter('spaceId', $this->getSpaceId()));
/**
* @var $vRPaymentPMConfigurationRepository
*/
$vRPaymentPMConfigurationRepository = $this->container->get(PaymentMethodConfigurationEntityDefinition::ENTITY_NAME . '.repository');
/** @var EntityRepositoryInterface $salesChannelPaymentRepository */
$salesChannelPaymentRepository = $this->container->get('sales_channel_payment_method.repository');
$paymentMethodConfigurationEntities = $vRPaymentPMConfigurationRepository
->search($criteria, $context)
->getEntities();
if (!empty($paymentMethodConfigurationEntities)) {
/**
* @var $paymentMethodConfigurationEntity \VRPaymentPayment\Core\Api\PaymentMethodConfiguration\Entity\PaymentMethodConfigurationEntity
*/
foreach ($paymentMethodConfigurationEntities as $paymentMethodConfigurationEntity) {
$data[] = [
'id' => $paymentMethodConfigurationEntity->getId(),
'state' => CreationEntityState::INACTIVE,
];
$paymentMethodData[] = [
'id' => $paymentMethodConfigurationEntity->getId(),
'active' => false,
];
$salesChannelPaymentMethodData[] = [
'paymentMethodId' => $paymentMethodConfigurationEntity->getId(),
];
}
try {
$vRPaymentPMConfigurationRepository->update($data, $context);
$this->paymentMethodRepository->update($paymentMethodData, $context);
$salesChannelPaymentRepository->delete($salesChannelPaymentMethodData, $context);
} catch (\Exception $exception) {
$this->logger->critical($exception->getMessage());
}
}
}
/**
* Full proof method to disable any orphaned payment methods
*
*/
protected function disableOrphanedPaymentMethods(): void
{
try {
$query = "UPDATE payment_method
SET active=0
WHERE handler_identifier=:handler_identifier AND id NOT IN (
SELECT payment_method_id FROM vrpayment_payment_method_configuration
)";
$params = [
'handler_identifier' => VRPaymentPaymentHandler::class,
];
$connection = $this->container->get(Connection::class);
$connection->executeQuery($query, $params);
} catch (\Exception $exception) {
$this->logger->critical($exception->getMessage());
}
}
/**
* @param string $paymentMethodId
* @param bool $active
* @param \Shopware\Core\Framework\Context $context
*/
protected function setPaymentMethodIsActive(string $paymentMethodId, bool $active, Context $context): void
{
$paymentMethod = [
'id' => $paymentMethodId,
'active' => $active,
];
$this->paymentMethodRepository->update([$paymentMethod], $context);
}
/**
* Enable payment methods from VRPayment API
*
* @param \Shopware\Core\Framework\Context $context
*
* @throws \VRPayment\Sdk\ApiException
* @throws \VRPayment\Sdk\Http\ConnectionException
* @throws \VRPayment\Sdk\VersioningException
*/
private function enablePaymentMethodConfigurations(Context $context): void
{
$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
);
$id = is_null($paymentMethodConfigurationEntity) ? Uuid::randomHex() : $paymentMethodConfigurationEntity->getId();
$data = [
'id' => $id,
'paymentMethodConfigurationId' => $paymentMethodConfiguration->getId(),
'paymentMethodId' => $id,
'data' => json_decode(strval($paymentMethodConfiguration), true),
'sortOrder' => $paymentMethodConfiguration->getSortOrder(),
'spaceId' => $paymentMethodConfiguration->getSpaceId(),
'state' => CreationEntityState::ACTIVE,
];
$this->upsertPaymentMethod($id, $paymentMethodConfiguration, $context);
$this->container->get(PaymentMethodConfigurationEntityDefinition::ENTITY_NAME . '.repository')
->upsert([$data], $context);
}
}
/**
* Fetch active merchant payment methods from VRPayment API
*
* @return \VRPayment\Sdk\Model\PaymentMethodConfiguration[]
* @throws \VRPayment\Sdk\ApiException
* @throws \VRPayment\Sdk\Http\ConnectionException
* @throws \VRPayment\Sdk\VersioningException
*/
private function getPaymentMethodConfigurations(): array
{
$entityQueryFilter = (new EntityQueryFilter())
->setOperator(CriteriaOperator::EQUALS)
->setFieldName('state')
->setType(EntityQueryFilterType::LEAF)
->setValue(CreationEntityState::ACTIVE);
$entityQuery = (new EntityQuery())->setFilter($entityQueryFilter);
$paymentMethodConfigurations = $this->apiClient->getPaymentMethodConfigurationService()->search(
$this->getSpaceId(),
$entityQuery
);
usort($paymentMethodConfigurations, function (PaymentMethodConfiguration $item1, PaymentMethodConfiguration $item2) {
return $item1->getSortOrder() <=> $item2->getSortOrder();
});
return $paymentMethodConfigurations;
}
/**
* @return int
*/
public function getSpaceId(): int
{
return $this->spaceId;
}
/**
* @param int $spaceId
*
* @return \VRPaymentPayment\Core\Api\PaymentMethodConfiguration\Service\PaymentMethodConfigurationService
*/
public function setSpaceId(int $spaceId): PaymentMethodConfigurationService
{
$this->spaceId = $spaceId;
return $this;
}
/**
* @param int $spaceId
* @param int $paymentMethodConfigurationId
* @param \Shopware\Core\Framework\Context $context
*
* @return \VRPaymentPayment\Core\Api\PaymentMethodConfiguration\Entity\PaymentMethodConfigurationEntity|null
*/
protected function getPaymentMethodConfigurationEntity(
int $spaceId,
int $paymentMethodConfigurationId,
Context $context
): ?PaymentMethodConfigurationEntity
{
$criteria = (new Criteria())->addFilter(
new EqualsFilter('spaceId', $spaceId),
new EqualsFilter('paymentMethodConfigurationId', $paymentMethodConfigurationId)
);
return $this->container->get(PaymentMethodConfigurationEntityDefinition::ENTITY_NAME . '.repository')
->search($criteria, $context)
->getEntities()
->first();
}
/**
* @param int $spaceId
* @param Context $context
* @return array
*/
public function getAllPaymentMethodConfigurations(int $spaceId, Context $context): array
{
$criteria = (new Criteria())->addFilter(new EqualsFilter('spaceId', $spaceId));
$configurations = $this->container->get(PaymentMethodConfigurationEntityDefinition::ENTITY_NAME . '.repository')
->search($criteria, $context)
->getEntities();
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),
];
$data['mediaId'] = $this->upsertMedia($id, $paymentMethodConfiguration, $context);
$data = array_filter($data);
$this->paymentMethodRepository->upsert([$data], $context);
}
/**
* @param \VRPayment\Sdk\Model\PaymentMethodConfiguration $paymentMethodConfiguration
* @param \Shopware\Core\Framework\Context $context
*
* @return array
* @throws \VRPayment\Sdk\ApiException
* @throws \VRPayment\Sdk\Http\ConnectionException
* @throws \VRPayment\Sdk\VersioningException
*/
protected function getPaymentMethodConfigurationTranslation(PaymentMethodConfiguration $paymentMethodConfiguration, Context $context): array
{
$translations = [];
$locales = $this->localeCodeProvider->getAvailableLocales($context);
foreach ($locales as $locale) {
$translations[$locale] = [
'name' => $this->translate($paymentMethodConfiguration->getResolvedTitle(), $locale) ?? $paymentMethodConfiguration->getName(),
'description' => $this->translate($paymentMethodConfiguration->getResolvedDescription(), $locale) ?? '',
];
}
return $translations;
}
/**
* @param array $translatedString
* @param string $locale
*
* @return string|null
* @throws \VRPayment\Sdk\ApiException
* @throws \VRPayment\Sdk\Http\ConnectionException
* @throws \VRPayment\Sdk\VersioningException
*/
protected function translate(array $translatedString, string $locale): ?string
{
$translation = null;
if (isset($translatedString[$locale])) {
$translation = $translatedString[$locale];
}
if (is_null($translation)) {
$primaryLanguage = $this->findPrimaryLanguage($locale);
if (!is_null($primaryLanguage) && isset($translatedString[$primaryLanguage->getIetfCode()])) {
$translation = $translatedString[$primaryLanguage->getIetfCode()];
}
if (is_null($translation) && isset($translatedString['en-US'])) {
$translation = $translatedString['en-US'];
}
}
return $translation;
}
/**
* Returns the primary language in the given group.
*
* @param $code
*
* @return \VRPayment\Sdk\Model\RestLanguage|null
* @throws \VRPayment\Sdk\ApiException
* @throws \VRPayment\Sdk\Http\ConnectionException
* @throws \VRPayment\Sdk\VersioningException
*/
protected function findPrimaryLanguage(string $code): ?RestLanguage
{
$code = substr($code, 0, 2);
foreach ($this->getLanguages() as $language) {
if (($language->getIso2Code() == $code) && $language->getPrimaryOfGroup()) {
return $language;
}
}
return null;
}
/**
*
* @return array
* @throws \VRPayment\Sdk\ApiException
* @throws \VRPayment\Sdk\Http\ConnectionException
* @throws \VRPayment\Sdk\VersioningException
*/
protected function getLanguages(): array
{
if (is_null($this->languages)) {
$this->languages = $this->apiClient->getLanguageService()->all();
}
return $this->languages;
}
/**
* Upload Payment Method icons
*
* @param string $id
* @param \VRPayment\Sdk\Model\PaymentMethodConfiguration $paymentMethodConfiguration
* @param \Shopware\Core\Framework\Context $context
*
* @return string|null
*/
protected function upsertMedia(string $id, PaymentMethodConfiguration $paymentMethodConfiguration, Context $context): ?string
{
try {
$mediaDefaultFolderRepository = $this->container->get('media_default_folder.repository');
$mediaDefaultFolderRepository->upsert([
[
'id' => $id,
'associationFields' => [],
'entity' => 'payment_method_' . $paymentMethodConfiguration->getId(),
],
], $context);
$mediaFolderRepository = $this->container->get('media_folder.repository');
$mediaFolderRepository->upsert([
[
'id' => $id,
'defaultFolderId' => $id,
'name' => $paymentMethodConfiguration->getName(),
'useParentConfiguration' => false,
'configuration' => [],
],
], $context);
/**
* @var \Shopware\Core\Content\Media\MediaDefinition
*/
$mediaDefinition = $this->container->get(MediaDefinition::class);
$this->mediaSerializer->setRegistry($this->serializerRegistry);
$data = [
'id' => $id,
'title' => $paymentMethodConfiguration->getName(),
'url' => $paymentMethodConfiguration->getResolvedImageUrl(),
'mediaFolderId' => $id,
];
$data = $this->mediaSerializer->deserialize(new Config([], [], []), $mediaDefinition, $data);
$this->container->get('media.repository')->upsert([$data], $context);
return $id;
} catch (\Exception $e) {
$this->logger->critical($e->getMessage(), [$e->getTraceAsString()]);
return null;
}
}
}
@@ -0,0 +1,149 @@
<?php declare(strict_types=1);
namespace VRPaymentPayment\Core\Api\Refund\Controller;
use Psr\Log\LoggerInterface;
use Shopware\Core\Framework\Context;
use Shopware\Core\Framework\Log\Package;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\{
HttpFoundation\Request,
HttpFoundation\Response,
Routing\Attribute\Route,
};
use VRPaymentPayment\Core\{
Api\Refund\Service\RefundService,
Settings\Service\SettingsService
};
/**
* Class RefundController
*
* @package VRPaymentPayment\Core\Api\Refund\Controller
*
*/
#[Package('sales-channel')]
#[Route(defaults: ['_routeScope' => ['api']])]
class RefundController extends AbstractController
{
/**
* @var \VRPaymentPayment\Core\Api\Refund\Service\RefundService
*/
protected $refundService;
/**
* @var \VRPaymentPayment\Core\Settings\Service\SettingsService
*/
protected $settingsService;
/**
* @var \Psr\Log\LoggerInterface
*/
protected $logger;
/**
* RefundController constructor.
*
* @param \VRPaymentPayment\Core\Api\Refund\Service\RefundService $refundService
* @param \VRPaymentPayment\Core\Settings\Service\SettingsService $settingsService
*/
public function __construct(RefundService $refundService, SettingsService $settingsService)
{
$this->settingsService = $settingsService;
$this->refundService = $refundService;
}
/**
* @param \Psr\Log\LoggerInterface $logger
* @internal
* @required
*
*/
public function setLogger(LoggerInterface $logger): void
{
$this->logger = $logger;
}
/**
* @param \Symfony\Component\HttpFoundation\Request $request
* @param \Shopware\Core\Framework\Context $context
* @return \Symfony\Component\HttpFoundation\Response
* @throws \VRPayment\Sdk\ApiException
* @throws \VRPayment\Sdk\Http\ConnectionException
* @throws \VRPayment\Sdk\VersioningException
*/
#[Route("/api/_action/vrpayment/refund/create-refund/",
name: "api.action.vrpayment.refund.create-refund",
methods: ['POST'])]
public function createRefund(Request $request, Context $context): Response
{
$salesChannelId = $request->request->get('salesChannelId');
$transactionId = $request->request->get('transactionId');
$quantity = (int)$request->request->get('quantity');
$lineItemId = $request->request->get('lineItemId');
$settings = $this->settingsService->getSettings($salesChannelId);
$apiClient = $settings->getApiClient();
$transaction = $apiClient->getTransactionService()->read($settings->getSpaceId(), $transactionId);
$refund = $this->refundService->create($transaction, $context, $lineItemId, $quantity);
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);
}
return new Response(null, Response::HTTP_NO_CONTENT);
}
/**
* @param \Symfony\Component\HttpFoundation\Request $request
* @param \Shopware\Core\Framework\Context $context
* @return \Symfony\Component\HttpFoundation\Response
* @throws \VRPayment\Sdk\ApiException
* @throws \VRPayment\Sdk\Http\ConnectionException
* @throws \VRPayment\Sdk\VersioningException
*/
#[Route("/api/_action/vrpayment/refund/create-refund-by-amount/",
name: "api.action.vrpayment.refund.create.refund.by.amount",
methods: ['POST'])]
public function createRefundByAmount(Request $request, Context $context): Response
{
$salesChannelId = $request->request->get('salesChannelId');
$transactionId = $request->request->get('transactionId');
$refundableAmount = $request->request->get('refundableAmount');
$settings = $this->settingsService->getSettings($salesChannelId);
$apiClient = $settings->getApiClient();
$transaction = $apiClient->getTransactionService()->read($settings->getSpaceId(), $transactionId);
$this->refundService->createRefundByAmount($transaction, $refundableAmount, $context);
return new Response(null, Response::HTTP_NO_CONTENT);
}
/**
* @param \Symfony\Component\HttpFoundation\Request $request
* @param \Shopware\Core\Framework\Context $context
* @return \Symfony\Component\HttpFoundation\Response
* @throws \VRPayment\Sdk\ApiException
* @throws \VRPayment\Sdk\Http\ConnectionException
* @throws \VRPayment\Sdk\VersioningException
*/
#[Route("/api/_action/vrpayment/refund/create-partial-refund/",
name: "api.action.vrpayment.refund.create.partial.refund",
methods: ['POST'])]
public function createPartialRefund(Request $request, Context $context): Response
{
$salesChannelId = $request->request->get('salesChannelId');
$transactionId = $request->request->get('transactionId');
$refundableAmount = $request->request->get('refundableAmount');
$lineItemId = $request->request->get('lineItemId');
$settings = $this->settingsService->getSettings($salesChannelId);
$apiClient = $settings->getApiClient();
$transaction = $apiClient->getTransactionService()->read($settings->getSpaceId(), $transactionId);
$this->refundService->createPartialRefund($transaction, $context, $lineItemId, $refundableAmount);
return new Response(null, Response::HTTP_NO_CONTENT);
}
}
+145
View File
@@ -0,0 +1,145 @@
<?php declare(strict_types=1);
namespace VRPaymentPayment\Core\Api\Refund\Entity;
use Shopware\Core\{
Framework\DataAbstractionLayer\Entity,
Framework\DataAbstractionLayer\EntityIdTrait};
use VRPaymentPayment\Core\Api\Transaction\Entity\TransactionEntityDefinition;
/**
* Class RefundEntity
*
* @package VRPaymentPayment\Core\Api\Refund\Entity
*/
class RefundEntity extends Entity {
use EntityIdTrait;
/**
* @var array
*/
protected $data;
/**
* @var int
*/
protected $refundId;
/**
* @var int
*/
protected $spaceId;
/**
* @var string
*/
protected $state;
/**
* @var ?\VRPaymentPayment\Core\Api\Transaction\Entity\TransactionEntityDefinition
*/
protected $transaction;
/**
* @var int
*/
protected $transactionId;
/**
* @return array
*/
public function getData(): array
{
return $this->data;
}
/**
* @param array $data
*/
public function setData(array $data): void
{
$this->data = $data;
}
/**
* @return int
*/
public function getRefundId(): int
{
return $this->refundId;
}
/**
* @param int $refundId
*/
public function setRefundId(int $refundId): void
{
$this->refundId = $refundId;
}
/**
* @return int
*/
public function getSpaceId(): int
{
return $this->spaceId;
}
/**
* @param int $spaceId
*/
public function setSpaceId(int $spaceId): void
{
$this->spaceId = $spaceId;
}
/**
* @return string
*/
public function getState(): string
{
return $this->state;
}
/**
* @param string $state
*/
public function setState(string $state): void
{
$this->state = $state;
}
/**
*
* @return \VRPaymentPayment\Core\Api\Transaction\Entity\TransactionEntityDefinition|null
*/
public function getTransaction(): ?TransactionEntityDefinition
{
return $this->transaction;
}
/**
* @param \VRPaymentPayment\Core\Api\Transaction\Entity\TransactionEntityDefinition $transaction
*/
public function setTransaction(TransactionEntityDefinition $transaction): void
{
$this->transaction = $transaction;
}
/**
* @return int
*/
public function getTransactionId(): int
{
return $this->transactionId;
}
/**
* @param int $transactionId
*/
public function setTransactionId(int $transactionId): void
{
$this->transactionId = $transactionId;
}
}
@@ -0,0 +1,57 @@
<?php declare(strict_types=1);
namespace VRPaymentPayment\Core\Api\Refund\Entity;
use Shopware\Core\Framework\DataAbstractionLayer\EntityCollection;
/**
* Class RefundEntityCollection
*
* @package VRPaymentPayment\Core\Api\Refund\Entity
*
* @method void add(RefundEntity $entity)
* @method void set(string $key, RefundEntity $entity)
* @method RefundEntity[] getIterator()
* @method RefundEntity[] getElements()
* @method RefundEntity|null get(string $key)
* @method RefundEntity|null first()
* @method RefundEntity|null last()
*/
class RefundEntityCollection extends EntityCollection {
/**
* @param int $transactionId
* @return \VRPaymentPayment\Core\Api\Refund\Entity\RefundEntityCollection
*/
public function filterByTransactionId(int $transactionId): RefundEntityCollection
{
return $this->filter(function (RefundEntity $refund) use ($transactionId) {
return $refund->getTransactionId() === $transactionId;
});
}
/**
* Get by refund id
*
* @param int $refundId
* @return \VRPaymentPayment\Core\Api\Refund\Entity\RefundEntity|null
*/
public function getByRefundId(int $refundId): ?RefundEntity
{
foreach ($this->getIterator() as $element) {
if ($element->getRefundId() === $refundId) {
return $element;
}
}
return null;
}
/**
* @return string
*/
protected function getExpectedClass(): string
{
return RefundEntity::class;
}
}
@@ -0,0 +1,70 @@
<?php declare(strict_types=1);
namespace VRPaymentPayment\Core\Api\Refund\Entity;
use Shopware\Core\{
Framework\DataAbstractionLayer\EntityDefinition,
Framework\DataAbstractionLayer\Field\CreatedAtField,
Framework\DataAbstractionLayer\Field\Flag\PrimaryKey,
Framework\DataAbstractionLayer\Field\Flag\Required,
Framework\DataAbstractionLayer\Field\IdField,
Framework\DataAbstractionLayer\Field\IntField,
Framework\DataAbstractionLayer\Field\JsonField,
Framework\DataAbstractionLayer\Field\ManyToOneAssociationField,
Framework\DataAbstractionLayer\Field\StringField,
Framework\DataAbstractionLayer\Field\UpdatedAtField,
Framework\DataAbstractionLayer\FieldCollection};
use VRPaymentPayment\Core\Api\Transaction\Entity\TransactionEntityDefinition;
/**
* Class RefundEntityDefinition
*
* @package VRPaymentPayment\Core\Api\Refund\Entity
*/
class RefundEntityDefinition extends EntityDefinition {
public const ENTITY_NAME = 'vrpayment_refund';
/**
* @return string
*/
public function getEntityName(): string
{
return self::ENTITY_NAME;
}
/**
* @return \Shopware\Core\Framework\DataAbstractionLayer\FieldCollection
*/
protected function defineFields(): FieldCollection
{
return new FieldCollection([
(new IdField('id', 'id'))->addFlags(new PrimaryKey(), new Required()),
(new JsonField('data', 'data'))->addFlags(new Required()),
(new IntField('refund_id', 'refundId'))->addFlags(new Required()),
(new IntField('space_id', 'spaceId'))->addFlags(new Required()),
(new StringField('state', 'state'))->addFlags(new Required()),
(new IntField('transaction_id', 'transactionId'))->addFlags(new Required()),
new ManyToOneAssociationField('transaction', 'transaction_id', TransactionEntityDefinition::class, 'transaction_id'),
new CreatedAtField(),
new UpdatedAtField(),
]);
}
/**
* @return string
*/
public function getCollectionClass(): string
{
return RefundEntityCollection::class;
}
/**
* @return string
*/
public function getEntityClass(): string
{
return RefundEntity::class;
}
}
@@ -0,0 +1,244 @@
<?php declare(strict_types=1);
namespace VRPaymentPayment\Core\Api\Refund\Service;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
use Shopware\Core\{
Framework\Context,
Framework\DataAbstractionLayer\Search\Criteria,
Framework\DataAbstractionLayer\Search\Filter\EqualsFilter,
Framework\Uuid\Uuid
};
use VRPayment\Sdk\{
Model\Refund,
Model\Transaction
};
use VRPaymentPayment\Core\{
Api\Refund\Entity\RefundEntity,
Api\Transaction\Entity\TransactionEntity,
Api\Transaction\Entity\TransactionEntityDefinition,
Settings\Service\SettingsService,
Util\Payload\RefundPayload
};
/**
* Class RefundService
*
* @package VRPaymentPayment\Core\Api\Refund\Service
*/
class RefundService
{
/**
* @var \Psr\Container\ContainerInterface
*/
protected $container;
/**
* @var \Psr\Log\LoggerInterface
*/
private $logger;
/**
* @var \VRPaymentPayment\Core\Settings\Service\SettingsService
*/
private $settingsService;
/**
* RefundService constructor.
*
* @param \Psr\Container\ContainerInterface $container
* @param \VRPaymentPayment\Core\Settings\Service\SettingsService $settingsService
*/
public function __construct(ContainerInterface $container, SettingsService $settingsService)
{
$this->container = $container;
$this->settingsService = $settingsService;
}
/**
* @param \Psr\Log\LoggerInterface $logger
*
* @internal
* @required
*
*/
public function setLogger(LoggerInterface $logger): void
{
$this->logger = $logger;
}
/**
* The pay function will be called after the customer completed the order.
* Allows to process the order and store additional information.
*
* A redirect to the url will be performed
*
* @param \VRPayment\Sdk\Model\Transaction $transaction
* @param string|null $lineItemId
* @param int $quantity
* @param \Shopware\Core\Framework\Context $context
*
* @return \VRPayment\Sdk\Model\Refund|null
* @throws \Exception
*/
public function create(Transaction $transaction, Context $context, ?string $lineItemId, int $quantity): ?Refund
{
try {
$transactionEntity = $this->getTransactionEntityByTransactionId($transaction->getId(), $context);
$settings = $this->settingsService->getSettings($transactionEntity->getSalesChannel()->getId());
$apiClient = $settings->getApiClient();
$refundPayloadClass = new RefundPayload();
$refundPayloadClass->setLogger($this->logger);
$refundPayload = $refundPayloadClass->get($transaction, $lineItemId, $quantity);
if (!is_null($refundPayload)) {
$refund = $apiClient->getRefundService()->refund($settings->getSpaceId(), $refundPayload);
$this->upsert($refund, $context);
return $refund;
}
} catch (\Exception $exception) {
$this->logger->critical($exception->getMessage());
}
return null;
}
/**
* The pay function will be called after the customer completed the order.
* Allows to process the order and store additional information.
*
* A redirect to the url will be performed
*
* @param \VRPayment\Sdk\Model\Transaction $transaction
* @param float $refundableAmount
* @param \Shopware\Core\Framework\Context $context
*
* @return \VRPayment\Sdk\Model\Refund|null
* @throws \Exception
*/
public function createRefundByAmount(Transaction $transaction, float $refundableAmount, Context $context): ?Refund
{
try {
$transactionEntity = $this->getTransactionEntityByTransactionId($transaction->getId(), $context);
$settings = $this->settingsService->getSettings($transactionEntity->getSalesChannel()->getId());
$apiClient = $settings->getApiClient();
$refundPayloadClass = new RefundPayload();
$refundPayloadClass->setLogger($this->logger);
$refundPayload = $refundPayloadClass->getByAmount($transaction, $refundableAmount);
if (!is_null($refundPayload)) {
$refund = $apiClient->getRefundService()->refund($settings->getSpaceId(), $refundPayload);
$this->upsert($refund, $context);
return $refund;
}
} catch (\Exception $exception) {
$this->logger->critical($exception->getMessage());
}
return null;
}
/**
* The pay function will be called after the customer completed the order.
* Allows to process the order and store additional information.
*
* A redirect to the url will be performed
*
* @param \VRPayment\Sdk\Model\Transaction $transaction
* @param \Shopware\Core\Framework\Context $context
* @param string $lineItemId
* @param float $amount
*
* @return \VRPayment\Sdk\Model\Refund|null
* @throws \Exception
*/
public function createPartialRefund(Transaction $transaction, Context $context, string $lineItemId, float $amount): ?Refund
{
try {
$transactionEntity = $this->getTransactionEntityByTransactionId($transaction->getId(), $context);
$settings = $this->settingsService->getSettings($transactionEntity->getSalesChannel()->getId());
$apiClient = $settings->getApiClient();
$refundPayloadClass = new RefundPayload();
$refundPayloadClass->setLogger($this->logger);
$refundPayload = $refundPayloadClass->getForPartial($transaction, $lineItemId, $amount);
if (!is_null($refundPayload)) {
$refund = $apiClient->getRefundService()->refund($settings->getSpaceId(), $refundPayload);
$this->upsert($refund, $context);
return $refund;
}
} catch (\Exception $exception) {
$this->logger->critical($exception->getMessage());
}
return null;
}
/**
* Get transaction entity by VRPayment transaction id
*
* @param int $transactionId
* @param \Shopware\Core\Framework\Context $context
*
* @return \VRPaymentPayment\Core\Api\Transaction\Entity\TransactionEntity
*/
public function getTransactionEntityByTransactionId(int $transactionId, Context $context): TransactionEntity
{
return $this->container->get(TransactionEntityDefinition::ENTITY_NAME . '.repository')
->search(
(new Criteria())->addFilter(new EqualsFilter('transactionId', $transactionId)),
$context
)
->first();
}
/**
* Persist VRPayment transaction
*
* @param \Shopware\Core\Framework\Context $context
* @param \VRPayment\Sdk\Model\Refund $refund
*/
public function upsert(Refund $refund, Context $context): void
{
$refundEntity = $this->getByRefundId($refund->getId(), $context);
$id = is_null($refundEntity) ? Uuid::randomHex() : $refundEntity->getId();
try {
$data = [
'id' => $id,
'data' => json_decode(strval($refund), true),
'refundId' => $refund->getId(),
'spaceId' => $refund->getLinkedSpaceId(),
'state' => $refund->getState(),
'transactionId' => $refund->getTransaction()->getId(),
];
$data = array_filter($data);
$this->container->get('vrpayment_refund.repository')->upsert([$data], $context);
} catch (\Exception $exception) {
$this->logger->critical(__CLASS__ . ' : ' . __FUNCTION__ . ' : ' . $exception->getMessage());
}
}
/**
* Get refund entity by VRPayment refund id
*
* @param int $refundId
* @param \Shopware\Core\Framework\Context $context
*
* @return \VRPaymentPayment\Core\Api\Refund\Entity\RefundEntity|null
*/
public function getByRefundId(int $refundId, Context $context): ?RefundEntity
{
return $this->container->get('vrpayment_refund.repository')
->search(
(new Criteria())->addFilter(new EqualsFilter('refundId', $refundId)),
$context
)
->first();
}
}
+179
View File
@@ -0,0 +1,179 @@
<?php declare(strict_types=1);
namespace VRPaymentPayment\Core\Api\Space\Service;
use Psr\Log\LoggerInterface;
use Shopware\Core\PlatformRequest;
use VRPayment\Sdk\{
ApiClient,
Model\Space
};
/**
* Class SpaceService
*
* @package VRPaymentPayment\Core\Api\Space\Service
*/
class SpaceService {
/**
* @var \VRPayment\Sdk\ApiClient
*/
protected $apiClient;
/**
* Space Id
*
* @var int
*/
protected $spaceId;
/**
* Application Id
*
* @var string
*/
protected $applicationId;
/**
* User Id
*
* @var int
*/
protected $userId;
/**
* @var \Psr\Log\LoggerInterface
*/
protected $logger;
/**
* @param \Psr\Log\LoggerInterface $logger
*
* @internal
* @required
*
*/
public function setLogger(LoggerInterface $logger): void
{
$this->logger = $logger;
}
/**
* @return \VRPayment\Sdk\ApiClient
*/
public function getApiClient(): ApiClient
{
return $this->apiClient;
}
/**
* @param \VRPayment\Sdk\ApiClient $apiClient
*
* @return \VRPaymentPayment\Core\Api\Space\Service\SpaceService
*/
public function setApiClient(ApiClient $apiClient): SpaceService
{
$this->apiClient = $apiClient;
return $this;
}
/**
* @return int
*/
public function getSpaceId(): int
{
return $this->spaceId;
}
/**
* @param int $spaceId
*
* @return \VRPaymentPayment\Core\Api\Space\Service\SpaceService
*/
public function setSpaceId(int $spaceId): SpaceService
{
$this->spaceId = $spaceId;
return $this;
}
/**
* Get user id
* @return int
*/
public function getUserId(): int
{
return $this->userId;
}
/**
* @param int $userId
*
* @return \VRPaymentPayment\Core\Api\Space\Service\SpaceService
*/
public function setUserId(int $userId): SpaceService
{
$this->userId = $userId;
return $this;
}
/**
* Get user key credential
* @return string
*/
public function getApplicationId(): string
{
return $this->applicationId;
}
/**
* @param string $applicationId
*
* @return \VRPaymentPayment\Core\Api\Space\Service\SpaceService
*/
public function setApplicationId(string $applicationId): SpaceService
{
$this->applicationId = $applicationId;
return $this;
}
/**
* Check Space
* Reads the entity with the given space id and user credentials and returns it.
* If the user credentials are not valid, an exception is thrown.
* The purpose of this method is simply to validate that a user has access
* with their credentials to a space on the portal.
* @see On the portal /doc/api/web-service#space-service--read
*
* @return Space|null
* @throws \VRPayment\Sdk\ApiException
* @throws \VRPayment\Sdk\Http\ConnectionException
* @throws \VRPayment\Sdk\VersioningException
*/
public function checkSpace(): ?Space
{
// Configuration
$this->setApiClient(new ApiClient($this->getUserId(), $this->getApplicationId()));
return $this->read();
}
/**
* Read Space
*
* @return Space|null
*/
protected function read(): ?Space
{
$returnValue = null;
try {
$returnValue = $this->apiClient->getSpaceService()->read($this->getSpaceId());
} catch (\Exception $exception) {
$this->logger->critical($exception->getTraceAsString());
}
return $returnValue;
}
}
@@ -0,0 +1,92 @@
<?php declare(strict_types=1);
namespace VRPaymentPayment\Core\Api\Transaction\Controller;
use Psr\Log\LoggerInterface;
use Shopware\Core\Framework\Log\Package;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\{
HttpFoundation\JsonResponse,
HttpFoundation\Request,
HttpFoundation\Response,
Routing\Attribute\Route};
use VRPayment\Sdk\{
Model\TransactionState};
use VRPaymentPayment\Core\Settings\Service\SettingsService;
/**
* Class TransactionCompletionController
*
* @package VRPaymentPayment\Core\Api\Transaction\Controller
*
*/
#[Package('sales-channel')]
#[Route(defaults: ['_routeScope' => ['api']])]
class TransactionCompletionController extends AbstractController {
/**
* @var \VRPaymentPayment\Core\Settings\Service\SettingsService
*/
protected $settingsService;
/**
* @var \Psr\Log\LoggerInterface
*/
protected $logger;
/**
* TransactionCompletionController constructor.
*
* @param \VRPaymentPayment\Core\Settings\Service\SettingsService $settingsService
*/
public function __construct(SettingsService $settingsService)
{
$this->settingsService = $settingsService;
}
/**
* @param \Psr\Log\LoggerInterface $logger
* @internal
* @required
*
*/
public function setLogger(LoggerInterface $logger): void
{
$this->logger = $logger;
}
/**
* @param \Symfony\Component\HttpFoundation\Request $request
* @return \Symfony\Component\HttpFoundation\JsonResponse
* @throws \VRPayment\Sdk\ApiException
* @throws \VRPayment\Sdk\Http\ConnectionException
* @throws \VRPayment\Sdk\VersioningException
*
*/
#[Route("/api/_action/vrpayment/transaction-completion/create-transaction-completion/",
name: "api.action.vrpayment.transaction-completion.create-transaction-completion",
methods: ['POST'])]
public function createTransactionCompletion(Request $request): JsonResponse
{
$salesChannelId = $request->request->get('salesChannelId');
$transactionId = $request->request->get('transactionId');
$settings = $this->settingsService->getSettings($salesChannelId);
$apiClient = $settings->getApiClient();
$transaction = $apiClient->getTransactionService()->read($settings->getSpaceId(), $transactionId);
if ($transaction->getState() == TransactionState::AUTHORIZED) {
$transactionCompletion = $apiClient->getTransactionCompletionService()->completeOnline($settings->getSpaceId(), $transaction->getId());
return new JsonResponse(strval($transactionCompletion), Response::HTTP_OK, [], true);
}
return new JsonResponse(
[
'message' => strtr('Transaction is in state {state}, it can not be completed at this time', ['{state}' => $transaction->getState()]),
],
Response::HTTP_NOT_ACCEPTABLE
);
}
}
@@ -0,0 +1,165 @@
<?php declare(strict_types=1);
namespace VRPaymentPayment\Core\Api\Transaction\Controller;
use Psr\Log\LoggerInterface;
use Shopware\Core\{
Framework\Context,
Framework\Log\Package};
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\{
HttpFoundation\HeaderUtils,
HttpFoundation\JsonResponse,
HttpFoundation\Request,
HttpFoundation\Response,
Routing\Attribute\Route};
use VRPaymentPayment\{
Core\Api\Transaction\Service\TransactionService,
Core\Settings\Service\SettingsService};
/**
* Class TransactionController
*
* @package VRPaymentPayment\Core\Api\Transaction\Controller
*
*/
#[Package('sales-channel')]
#[Route(defaults: ['_routeScope' => ['api']])]
class TransactionController extends AbstractController {
/**
* @var \VRPaymentPayment\Core\Settings\Service\SettingsService
*/
protected $settingsService;
/**
* @var \Psr\Log\LoggerInterface
*/
protected $logger;
/**
* @var \VRPaymentPayment\Core\Api\Transaction\Service\TransactionService
*/
protected $transactionService;
/**
* TransactionController constructor.
*
* @param \VRPaymentPayment\Core\Settings\Service\SettingsService $settingsService
* @param \VRPaymentPayment\Core\Api\Transaction\Service\TransactionService $transactionService
*/
public function __construct(SettingsService $settingsService, TransactionService $transactionService)
{
$this->settingsService = $settingsService;
$this->transactionService = $transactionService;
}
/**
* @param \Psr\Log\LoggerInterface $logger
*
* @internal
* @required
*
*/
public function setLogger(LoggerInterface $logger): void
{
$this->logger = $logger;
}
/**
* @param \Symfony\Component\HttpFoundation\Request $request
* @param \Shopware\Core\Framework\Context $context
*
* @return \Symfony\Component\HttpFoundation\JsonResponse
*/
#[Route("/api/_action/vrpayment/transaction/get-transaction-data/",
name: "api.action.vrpayment.transaction.get-transaction-data",
methods: ['POST'])]
public function getTransactionData(Request $request, Context $context): JsonResponse
{
$transactionId = $request->request->get('transactionId');
$transaction = $this->transactionService->getByTransactionId(intval($transactionId), $context);
$refundCollection = $this->transactionService->getRefundEntityCollectionByTransactionId(intval($transactionId), $context);
$refunds = [];
foreach ($refundCollection as $refundEntity) {
$refunds[] = $refundEntity ? $refundEntity->getData() : [];
}
return new JsonResponse([
'refunds' => $refunds,
'transactions' => [$transaction ? $transaction->getData() : []],
]);
}
/**
* @param string $salesChannelId
* @param int $transactionId
*
* @return \Symfony\Component\HttpFoundation\Response
* @throws \VRPayment\Sdk\ApiException
* @throws \VRPayment\Sdk\Http\ConnectionException
* @throws \VRPayment\Sdk\VersioningException
*
*/
#[Route("/api/_action/vrpayment/transaction/get-invoice-document/{salesChannelId}/{transactionId}",
name: "api.action.vrpayment.transaction.get-invoice-document",
methods: ['GET'],
defaults: ["csrf_protected" => false, "auth_required" => false])]
public function getInvoiceDocument(string $salesChannelId, int $transactionId): Response
{
$settings = $this->settingsService->getSettings($salesChannelId);
$apiClient = $settings->getApiClient();
$invoiceDocument = $apiClient->getTransactionService()->getInvoiceDocument($settings->getSpaceId(), $transactionId);
$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;
}
/**
* @param string $salesChannelId
* @param int $transactionId
*
* @return \Symfony\Component\HttpFoundation\Response
* @throws \VRPayment\Sdk\ApiException
* @throws \VRPayment\Sdk\Http\ConnectionException
* @throws \VRPayment\Sdk\VersioningException
*
*/
#[Route("/api/_action/vrpayment/transaction/get-packing-slip/{salesChannelId}/{transactionId}",
name: "api.action.vrpayment.transaction.get-packing-slip",
methods: ['GET'],
defaults: ["csrf_protected" => false, "auth_required" => false])]
public function getPackingSlip(string $salesChannelId, int $transactionId): Response
{
$settings = $this->settingsService->getSettings($salesChannelId);
$apiClient = $settings->getApiClient();
$invoiceDocument = $apiClient->getTransactionService()->getPackingSlip($settings->getSpaceId(), $transactionId);
$forceDownload = true;
$filename = preg_replace('/[\x00-\x1F\x7F-\xFF]/', '_', $invoiceDocument->getTitle()) . '.pdf';
$disposition = HeaderUtils::makeDisposition(
$forceDownload ? HeaderUtils::DISPOSITION_ATTACHMENT : HeaderUtils::DISPOSITION_INLINE,
$filename,
$filename
// only printable ascii
);
$response = new Response(base64_decode($invoiceDocument->getData()));
$response->headers->set('Content-Type', $invoiceDocument->getMimeType());
$response->headers->set('Content-Disposition', $disposition);
return $response;
}
}
@@ -0,0 +1,90 @@
<?php declare(strict_types=1);
namespace VRPaymentPayment\Core\Api\Transaction\Controller;
use Psr\Log\LoggerInterface;
use Shopware\Core\Framework\Log\Package;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\{
HttpFoundation\JsonResponse,
HttpFoundation\Request,
HttpFoundation\Response,
Routing\Attribute\Route};
use VRPayment\Sdk\{
Model\TransactionState};
use VRPaymentPayment\Core\Settings\Service\SettingsService;
/**
* Class TransactionVoidController
*
* @package VRPaymentPayment\Core\Api\Transaction\Controller
*
*/
#[Package('sales-channel')]
#[Route(defaults: ['_routeScope' => ['api']])]
class TransactionVoidController extends AbstractController {
/**
* @var \VRPaymentPayment\Core\Settings\Service\SettingsService
*/
protected $settingsService;
/**
* @var \Psr\Log\LoggerInterface
*/
protected $logger;
/**
* TransactionVoidController constructor.
*
* @param \VRPaymentPayment\Core\Settings\Service\SettingsService $settingsService
*/
public function __construct(SettingsService $settingsService)
{
$this->settingsService = $settingsService;
}
/**
* @param \Psr\Log\LoggerInterface $logger
* @internal
* @required
*
*/
public function setLogger(LoggerInterface $logger): void
{
$this->logger = $logger;
}
/**
* @param \Symfony\Component\HttpFoundation\Request $request
* @return \Symfony\Component\HttpFoundation\JsonResponse
* @throws \VRPayment\Sdk\ApiException
* @throws \VRPayment\Sdk\Http\ConnectionException
* @throws \VRPayment\Sdk\VersioningException
*
*/
#[Route("/api/_action/vrpayment/transaction-void/create-transaction-void/",
name: "api.action.vrpayment.transaction-void.create-transaction-void",
methods: ['POST'])]
public function createTransactionVoid(Request $request): JsonResponse
{
$salesChannelId = $request->request->get('salesChannelId');
$transactionId = $request->request->get('transactionId');
$settings = $this->settingsService->getSettings($salesChannelId);
$apiClient = $settings->getApiClient();
$transaction = $apiClient->getTransactionService()->read($settings->getSpaceId(), $transactionId);
if ($transaction->getState() == TransactionState::AUTHORIZED) {
$transactionVoid = $apiClient->getTransactionVoidService()->voidOnline($settings->getSpaceId(), $transaction->getId());
return new JsonResponse(strval($transactionVoid), Response::HTTP_OK, [], true);
}
return new JsonResponse(
[
'message' => strtr('Transaction is in state {state}, it can not be completed at this time.', ['{state}' => $transaction->getState()]),
],
Response::HTTP_NOT_ACCEPTABLE
);
}
}
@@ -0,0 +1,321 @@
<?php declare(strict_types=1);
namespace VRPaymentPayment\Core\Api\Transaction\Entity;
use Shopware\Core\{
Checkout\Order\Aggregate\OrderTransaction\OrderTransactionEntity,
Checkout\Order\OrderEntity,
Checkout\Payment\PaymentMethodEntity,
Framework\DataAbstractionLayer\Entity,
Framework\DataAbstractionLayer\EntityIdTrait,
System\SalesChannel\SalesChannelEntity};
use VRPaymentPayment\Core\Api\Refund\Entity\RefundEntityCollection;
/**
* Class TransactionEntity
*
* @package VRPaymentPayment\Core\Api\Transaction\Entity
*/
class TransactionEntity extends Entity {
use EntityIdTrait;
/**
* @var bool
*/
protected $confirmationEmailSent;
/**
* @var string
*/
protected $erpMerchantId;
/**
* @var array
*/
protected $data;
/**
* @var \Shopware\Core\Checkout\Payment\PaymentMethodEntity
*/
protected $paymentMethod;
/**
* @var string
*/
protected $paymentMethodId;
/**
* @var \Shopware\Core\Checkout\Order\OrderEntity
*/
protected $order;
/**
* @var string
*/
protected $orderId;
/**
* @var \Shopware\Core\Checkout\Order\Aggregate\OrderTransaction\OrderTransactionEntity
*/
protected $orderTransaction;
/**
* @var string
*/
protected $orderTransactionId;
/**
* @var \VRPaymentPayment\Core\Api\Refund\Entity\RefundEntityCollection
*/
protected $refunds;
/**
* @var int
*/
protected $spaceId;
/**
* @var string
*/
protected $state;
/**
* @var \Shopware\Core\System\SalesChannel\SalesChannelEntity
*/
protected $salesChannel;
/**
* @var string
*/
protected $salesChannelId;
/**
* @var int
*/
protected $transactionId;
/**
* @return bool
*/
public function isConfirmationEmailSent(): bool
{
return $this->confirmationEmailSent;
}
/**
* @param bool $confirmationEmailSent
*/
public function setConfirmationEmailSent(bool $confirmationEmailSent): void
{
$this->confirmationEmailSent = $confirmationEmailSent;
}
/**
* @return array
*/
public function getData(): array
{
return $this->data;
}
/**
* @param array $data
*/
public function setData(array $data): void
{
$this->data = $data;
}
/**
* @return string
*/
public function getPaymentMethodId(): string
{
return $this->paymentMethodId;
}
/**
* @param string $paymentMethodId
*/
public function setPaymentMethodId(string $paymentMethodId): void
{
$this->paymentMethodId = $paymentMethodId;
}
/**
* @return string
*/
public function getOrderId(): string
{
return $this->orderId;
}
/**
* @param string $orderId
*/
public function setOrderId(string $orderId): void
{
$this->orderId = $orderId;
}
/**
* @return string
*/
public function getOrderTransactionId(): string
{
return $this->orderTransactionId;
}
/**
* @param string $orderTransactionId
*/
public function setOrderTransactionId(string $orderTransactionId): void
{
$this->orderTransactionId = $orderTransactionId;
}
/**
* @return int
*/
public function getSpaceId(): int
{
return $this->spaceId;
}
/**
* @param int $spaceId
*/
public function setSpaceId(int $spaceId): void
{
$this->spaceId = $spaceId;
}
/**
* @return string
*/
public function getState(): string
{
return $this->state;
}
/**
* @param string $state
*/
public function setState(string $state): void
{
$this->state = $state;
}
/**
* @return string
*/
public function getSalesChannelId(): string
{
return $this->salesChannelId;
}
/**
* @param string $salesChannelId
*/
public function setSalesChannelId(string $salesChannelId): void
{
$this->salesChannelId = $salesChannelId;
}
/**
* @return int
*/
public function getTransactionId(): int
{
return $this->transactionId;
}
/**
* @param int $transactionId
*/
public function setTransactionId(int $transactionId): void
{
$this->transactionId = $transactionId;
}
/**
* @return \Shopware\Core\Checkout\Payment\PaymentMethodEntity
*/
public function getPaymentMethod(): PaymentMethodEntity
{
return $this->paymentMethod;
}
/**
* @param \Shopware\Core\Checkout\Payment\PaymentMethodEntity $paymentMethod
*/
public function setPaymentMethod(PaymentMethodEntity $paymentMethod): void
{
$this->paymentMethod = $paymentMethod;
}
/**
* @return \Shopware\Core\Checkout\Order\OrderEntity
*/
public function getOrder(): OrderEntity
{
return $this->order;
}
/**
* @param \Shopware\Core\Checkout\Order\OrderEntity $order
*/
public function setOrder(OrderEntity $order): void
{
$this->order = $order;
}
/**
* @return \Shopware\Core\Checkout\Order\Aggregate\OrderTransaction\OrderTransactionEntity
*/
public function getOrderTransaction(): OrderTransactionEntity
{
return $this->orderTransaction;
}
/**
* @param \Shopware\Core\Checkout\Order\Aggregate\OrderTransaction\OrderTransactionEntity $orderTransaction
*/
public function setOrderTransaction(OrderTransactionEntity $orderTransaction): void
{
$this->orderTransaction = $orderTransaction;
}
/**
* @return \VRPaymentPayment\Core\Api\Refund\Entity\RefundEntityCollection|null
*/
public function getRefunds(): ?RefundEntityCollection
{
return $this->refunds;
}
/**
* @param \VRPaymentPayment\Core\Api\Refund\Entity\RefundEntityCollection $refunds
*/
public function setRefunds(RefundEntityCollection $refunds): void
{
$this->refunds = $refunds;
}
/**
* @return \Shopware\Core\System\SalesChannel\SalesChannelEntity
*/
public function getSalesChannel(): SalesChannelEntity
{
return $this->salesChannel;
}
/**
* @param \Shopware\Core\System\SalesChannel\SalesChannelEntity $salesChannel
*/
public function setSalesChannel(SalesChannelEntity $salesChannel): void
{
$this->salesChannel = $salesChannel;
}
}
@@ -0,0 +1,46 @@
<?php declare(strict_types=1);
namespace VRPaymentPayment\Core\Api\Transaction\Entity;
use Shopware\Core\Framework\DataAbstractionLayer\EntityCollection;
/**
* Class TransactionEntityCollection
*
* @package VRPaymentPayment\Core\Api\Transaction\Entity
*
* @method void add(TransactionEntity $entity)
* @method void set(string $key, TransactionEntity $entity)
* @method TransactionEntity[] getIterator()
* @method TransactionEntity[] getElements()
* @method TransactionEntity|null get(string $key)
* @method TransactionEntity|null first()
* @method TransactionEntity|null last()
*/
class TransactionEntityCollection extends EntityCollection {
/**
* Get by transaction id
*
* @param int $transactionId
* @return \VRPaymentPayment\Core\Api\Transaction\Entity\TransactionEntity|null
*/
public function getByTransactionId(int $transactionId): ?TransactionEntity
{
foreach ($this->getIterator() as $element) {
if ($element->getTransactionId() === $transactionId) {
return $element;
}
}
return null;
}
/**
* @return string
*/
protected function getExpectedClass(): string
{
return TransactionEntity::class;
}
}
@@ -0,0 +1,89 @@
<?php declare(strict_types=1);
namespace VRPaymentPayment\Core\Api\Transaction\Entity;
use Shopware\Core\{Checkout\Order\Aggregate\OrderTransaction\OrderTransactionDefinition,
Checkout\Order\OrderDefinition,
Checkout\Payment\PaymentMethodDefinition,
Framework\DataAbstractionLayer\EntityDefinition,
Framework\DataAbstractionLayer\Field\BoolField,
Framework\DataAbstractionLayer\Field\CreatedAtField,
Framework\DataAbstractionLayer\Field\FkField,
Framework\DataAbstractionLayer\Field\Flag\ApiAware,
Framework\DataAbstractionLayer\Field\Flag\CascadeDelete,
Framework\DataAbstractionLayer\Field\Flag\PrimaryKey,
Framework\DataAbstractionLayer\Field\Flag\Required,
Framework\DataAbstractionLayer\Field\IdField,
Framework\DataAbstractionLayer\Field\IntField,
Framework\DataAbstractionLayer\Field\JsonField,
Framework\DataAbstractionLayer\Field\OneToManyAssociationField,
Framework\DataAbstractionLayer\Field\OneToOneAssociationField,
Framework\DataAbstractionLayer\Field\ReferenceVersionField,
Framework\DataAbstractionLayer\Field\StringField,
Framework\DataAbstractionLayer\Field\UpdatedAtField,
Framework\DataAbstractionLayer\FieldCollection,
System\SalesChannel\SalesChannelDefinition};
use VRPaymentPayment\Core\Api\Refund\Entity\RefundEntityDefinition;
/**
* Class TransactionEntityDefinition
*
* @package VRPaymentPayment\Core\Api\Transaction\Entity
*/
class TransactionEntityDefinition extends EntityDefinition {
public const ENTITY_NAME = 'vrpayment_transaction';
/**
* @return string
*/
public function getEntityName(): string
{
return self::ENTITY_NAME;
}
/**
* @return \Shopware\Core\Framework\DataAbstractionLayer\FieldCollection
*/
protected function defineFields(): FieldCollection
{
return new FieldCollection([
(new IdField('id', 'id'))->addFlags(new PrimaryKey(), new Required()),
new BoolField('confirmation_email_sent', 'confirmationEmailSent'),
new StringField('erp_merchant_id', 'erpMerchantId'),
(new JsonField('data', 'data'))->addFlags(new Required()),
(new FkField('payment_method_id', 'paymentMethodId', PaymentMethodDefinition::class))->addFlags(new Required()),
(new FkField('order_id', 'orderId', OrderDefinition::class))->addFlags(new Required()),
(new FkField('order_transaction_id', 'orderTransactionId', OrderTransactionDefinition::class))->addFlags(new Required()),
(new IntField('space_id', 'spaceId'))->addFlags(new Required()),
(new StringField('state', 'state'))->addFlags(new Required()),
(new FkField('sales_channel_id', 'salesChannelId', SalesChannelDefinition::class))->addFlags(new Required()),
(new IntField('transaction_id', 'transactionId'))->addFlags(new Required()),
new OneToOneAssociationField('paymentMethod', 'payment_method_id', 'id', PaymentMethodDefinition::class, true),
new OneToOneAssociationField('order', 'order_id', 'id', OrderDefinition::class, true),
new OneToOneAssociationField('orderTransaction', 'order_transaction_id', 'id', OrderTransactionDefinition::class, true),
(new OneToManyAssociationField('refunds', RefundEntityDefinition::class, 'transaction_id', 'transaction_id'))->addFlags(new CascadeDelete()),
new OneToOneAssociationField('salesChannel', 'sales_channel_id', 'id', SalesChannelDefinition::class, true),
(new ReferenceVersionField(OrderDefinition::class))->addFlags(new ApiAware(), new Required()),
new CreatedAtField(),
new UpdatedAtField(),
]);
}
/**
* @return string
*/
public function getCollectionClass(): string
{
return TransactionEntityCollection::class;
}
/**
* @return string
*/
public function getEntityClass(): string
{
return TransactionEntity::class;
}
}
@@ -0,0 +1,261 @@
<?php declare(strict_types=1);
namespace VRPaymentPayment\Core\Api\Transaction\Service;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
use Shopware\Core\{
Checkout\Cart\Event\CheckoutOrderPlacedEvent,
Checkout\Cart\CartException,
Checkout\Order\OrderEntity,
Content\MailTemplate\MailTemplateEntity,
Content\Mail\Service\AbstractMailService,
Framework\Context,
Framework\DataAbstractionLayer\Search\Criteria,
Framework\DataAbstractionLayer\Search\Filter\EqualsFilter,
Framework\DataAbstractionLayer\Search\Filter\NotFilter,
Framework\DataAbstractionLayer\Search\Filter\OrFilter,
Framework\Event\EventAction\EventActionCollection};
use VRPaymentPayment\Core\{
Api\Transaction\Entity\TransactionEntity,
Api\Transaction\Entity\TransactionEntityDefinition};
/**
* Class OrderMailService
*
* @package VRPaymentPayment\Core\Api\Transaction\Service
*/
class OrderMailService {
public const EMAIL_ORIGIN_IS_VRPAYMENT = "isVRPaymentPayment";
/**
* @var \Psr\Container\ContainerInterface
*/
protected $container;
/**
* @var \Psr\Log\LoggerInterface
*/
protected $logger;
/**
* @var \Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface
*/
protected $mailTemplateRepository;
/**
* @var \Shopware\Core\Content\Mail\Service\AbstractMailService
*/
protected $mailService;
/**
* MailService constructor.
*
* @param \Psr\Container\ContainerInterface $container
* @param \Shopware\Core\Content\Mail\Service\AbstractMailService $mailService
*/
public function __construct(ContainerInterface $container, AbstractMailService $mailService)
{
$this->container = $container;
$this->mailService = $mailService;
}
/**
* @param \Psr\Log\LoggerInterface $logger
*
* @internal
* @required
*
*/
public function setLogger(LoggerInterface $logger): void
{
$this->logger = $logger;
}
/**
* @param string $orderId
* @param \Shopware\Core\Framework\Context $context
*/
public function send(string $orderId, Context $context): void
{
try {
$transactionEntity = $this->getTransactionEntityByOrderId($orderId, $context);
if ($transactionEntity->isConfirmationEmailSent()) {
return;
}
$order = $this->getOrder($orderId, $context);
if (is_null($order->getOrderCustomer())) {
return;
}
$languageIdChain[] = $order->getLanguageId();
$contextLanguageIdChain = $context->getLanguageIdChain();
foreach ($contextLanguageIdChain as $languageId) {
$contextLanguageIdChain[] = $languageId;
}
array_unique($languageIdChain);
$context->assign(['languageIdChain' => $languageIdChain,]);
$templateData = [
'order' => $order,
self::EMAIL_ORIGIN_IS_VRPAYMENT => true,
];
$data = $this->getData($order, $context);
foreach ($data as $datum){
$this->mailService->send($datum, $context, $templateData);
}
$this->markTransactionEntityConfirmationEmailAsSent($orderId, $context);
} catch (\Exception $exception) {
$this->logger->critical($exception->getMessage());
}
}
/**
* Get transaction entity by orderId
*
* @param string $orderId
* @param \Shopware\Core\Framework\Context $context
*
* @return \VRPaymentPayment\Core\Api\Transaction\Entity\TransactionEntity
*/
protected function getTransactionEntityByOrderId(string $orderId, Context $context): TransactionEntity
{
return $this->container->get(TransactionEntityDefinition::ENTITY_NAME . '.repository')
->search(new Criteria([$orderId]), $context)
->get($orderId);
}
/**
* @param string $orderId
* @param \Shopware\Core\Framework\Context $context
*
* @return \Shopware\Core\Checkout\Order\OrderEntity
*/
protected function getOrder(string $orderId, Context $context): OrderEntity
{
$orderCriteria = (new Criteria([$orderId]))->addAssociations([
'addresses',
'addresses.country',
'currency',
'deliveries',
'deliveries.shippingCosts',
'deliveries.shippingMethod',
'deliveries.shippingOrderAddress',
'deliveries.shippingOrderAddress.country',
'documents',
'language',
'lineItems',
'orderCustomer',
'orderCustomer.customer',
'orderCustomer.salutation',
'salesChannel',
'stateMachineState',
'tags',
'transactions',
'transactions.paymentMethod',
]);
/** @var OrderEntity|null $order */
$order = $this->container->get('order.repository')->search($orderCriteria, $context)->first();
if (is_null($order)) {
throw CartException::orderNotFound($orderId);
}
return $order;
}
/**
* @param \Shopware\Core\Checkout\Order\OrderEntity $order
* @param \Shopware\Core\Framework\Context $context
*
* @return array
*/
protected function getData(OrderEntity $order, Context $context): array
{
$data = [];
/**
* @var
*/
/** @var \Shopware\Core\Framework\Event\EventAction\EventActionCollection $eventActionEntities */
$eventActionEntities = $this->getBusinessEvents($order, $context);
$customerRecipient = [
$order->getOrderCustomer()->getEmail() => $order->getOrderCustomer()->getFirstName() . ' ' . $order->getOrderCustomer()->getLastName(),
];
foreach ($eventActionEntities as $eventActionEntity) {
$eventConfig = $eventActionEntity->getConfig();
$mailTemplateId = $eventConfig['mail_template_id'];
$recipients = !empty($eventConfig['recipients']) ? $eventConfig['recipients'] : $customerRecipient;
$mailTemplate = $this->getMailTemplateById($context, $mailTemplateId);
$data[] = [
'recipients' => $recipients,
'senderName' => $mailTemplate->getTranslation('senderName'),
'salesChannelId' => $order->getSalesChannelId(),
'templateId' => $mailTemplateId,
'customFields' => $mailTemplate->getCustomFields(),
'contentHtml' => $mailTemplate->getTranslation('contentHtml'),
'contentPlain' => $mailTemplate->getTranslation('contentPlain'),
'subject' => $mailTemplate->getTranslation('subject'),
];
}
return $data;
}
protected function getBusinessEvents(OrderEntity $order, Context $context): EventActionCollection
{
$criteria = (new Criteria())
->addAssociations([
'rules',
'salesChannels',
])
->addFilter(new EqualsFilter('eventName', CheckoutOrderPlacedEvent::EVENT_NAME))
->addFilter(new EqualsFilter('active', true))
->addFilter(new NotFilter(NotFilter::CONNECTION_AND, [new EqualsFilter('config.mail_template_id', null)]))
->addFilter(new OrFilter([
new EqualsFilter('salesChannels.id', $order->getSalesChannelId()),
new EqualsFilter('salesChannels.id', null),
]));
/** @var EventActionCollection $events */
$events = $this->container->get('event_action.repository')
->search($criteria, $context)
->getEntities();
return $events;
}
/**
* @param \Shopware\Core\Framework\Context $context
* @param string $id
*
* @return \Shopware\Core\Content\MailTemplate\MailTemplateEntity
*/
protected function getMailTemplateById(Context $context, string $id): MailTemplateEntity
{
$criteria = (new Criteria([$id]))->addAssociations(['media', 'media.media', 'salesChannels', 'mailTemplateType']);
$mailTemplateEntity = $this->container->get('mail_template.repository')->search($criteria, $context)->first();
return $mailTemplateEntity;
}
/**
* @param string $orderId
* @param \Shopware\Core\Framework\Context $context
*/
protected function markTransactionEntityConfirmationEmailAsSent(string $orderId, Context $context)
{
$this->container->get(TransactionEntityDefinition::ENTITY_NAME . '.repository')->upsert([['id' => $orderId, 'confirmationEmailSent' => true]], $context);
}
}
@@ -0,0 +1,747 @@
<?php declare(strict_types=1);
namespace VRPaymentPayment\Core\Api\Transaction\Service;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
use Shopware\Core\{
Checkout\Cart\CartException,
Checkout\Cart\LineItem\LineItem,
Checkout\Order\OrderEntity,
Checkout\Payment\Cart\AsyncPaymentTransactionStruct,
Framework\Context,
Framework\DataAbstractionLayer\Search\Criteria,
Framework\DataAbstractionLayer\Search\Filter\EqualsFilter,
System\SalesChannel\SalesChannelContext
};
use Shopware\Storefront\Page\Checkout\Confirm\CheckoutConfirmPageLoadedEvent;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use VRPayment\Sdk\{
Model\AddressCreate,
Model\ChargeAttempt,
Model\CreationEntityState,
Model\CriteriaOperator,
Model\EntityQuery,
Model\EntityQueryFilter,
Model\EntityQueryFilterType,
Model\Gender,
Model\LineItemAttributeCreate,
Model\LineItemCreate,
Model\LineItemType,
Model\Transaction,
Model\TransactionCreate,
Model\TransactionPending,
Model\TransactionState,
};
use VRPaymentPayment\Core\{
Api\OrderDeliveryState\Handler\OrderDeliveryStateHandler,
Api\Refund\Entity\RefundEntityCollection,
Api\Refund\Entity\RefundEntityDefinition,
Api\Transaction\Entity\TransactionEntity,
Api\Transaction\Entity\TransactionEntityDefinition,
Settings\Options\Integration,
Settings\Service\SettingsService,
Util\LocaleCodeProvider,
Util\Payload\CustomProducts\CustomProductsLineItemTypes,
Util\Payload\TransactionPayload
};
/**
* Class TransactionService
*
* @package VRPaymentPayment\Core\Api\Transaction\Service
*/
class TransactionService
{
/**
* @var \Psr\Container\ContainerInterface
*/
protected $container;
/**
* @var \VRPaymentPayment\Core\Util\LocaleCodeProvider
*/
private $localeCodeProvider;
/**
* @var \Psr\Log\LoggerInterface
*/
private $logger;
/**
* @var \VRPaymentPayment\Core\Settings\Service\SettingsService
*/
private $settingsService;
const CARD_HOLDER_KEY = '1456765000789';
const PSEUDO_CODE_KEY = '1485172176673';
const CARD_VALIDITY_KEY = '1456765711187';
const PAY_ID_KEY = '1484042941549';
const ADDITIONAL_TRANSACTION_DETAILS_ORDER_ID_KEY = '1464680013786';
/**
* TransactionService constructor.
*
* @param \Psr\Container\ContainerInterface $container
* @param \VRPaymentPayment\Core\Util\LocaleCodeProvider $localeCodeProvider
* @param \VRPaymentPayment\Core\Settings\Service\SettingsService $settingsService
*/
public function __construct(
ContainerInterface $container,
LocaleCodeProvider $localeCodeProvider,
SettingsService $settingsService
)
{
$this->container = $container;
$this->localeCodeProvider = $localeCodeProvider;
$this->settingsService = $settingsService;
}
/**
* @param \Psr\Log\LoggerInterface $logger
*
* @internal
* @required
*
*/
public function setLogger(LoggerInterface $logger): void
{
$this->logger = $logger;
}
/**
* The pay function will be called after the customer completed the order.
* Allows to process the order and store additional information.
*
* A redirect to the url will be performed
*
* @param \Shopware\Core\Checkout\Payment\Cart\AsyncPaymentTransactionStruct $transaction
* @param \Shopware\Core\System\SalesChannel\SalesChannelContext $salesChannelContext
*
* @return string
* @throws \VRPayment\Sdk\ApiException
* @throws \VRPayment\Sdk\Http\ConnectionException
* @throws \VRPayment\Sdk\VersioningException
*/
public function create(
AsyncPaymentTransactionStruct $transaction,
SalesChannelContext $salesChannelContext
): string
{
$salesChannelId = $salesChannelContext->getSalesChannel()->getId();
$settings = $this->settingsService->getSettings($salesChannelId);
$apiClient = $settings->getApiClient();
$failedStates = [
TransactionState::DECLINE,
TransactionState::FAILED,
TransactionState::VOIDED,
];
$pendingTransaction = $this->read($_SESSION['transactionId'], $salesChannelId);
if (in_array($pendingTransaction->getState(), $failedStates)) {
unset($_SESSION['transactionId']);
$pendingTransactionId = $this->createPendingTransaction($salesChannelContext);
$pendingTransaction = $this->read($pendingTransactionId, $salesChannelId);
}
$transactionPayloadClass = (new TransactionPayload(
$this->container,
$this->localeCodeProvider,
$salesChannelContext,
$settings,
$transaction
));
$transactionPayloadClass->setLogger($this->logger);
$transactionPayload = $transactionPayloadClass->get($pendingTransaction->getVersion());
$createdTransaction = $apiClient->getTransactionService()
->confirm($settings->getSpaceId(), $transactionPayload);
$this->addVRPaymentTransactionId(
$transaction,
$salesChannelContext->getContext(),
$createdTransaction->getId(),
$settings->getSpaceId()
);
$redirectUrl = $this->container->get('router')->generate(
'frontend.vrpayment.checkout.pay',
['orderId' => $transaction->getOrder()->getId(),],
UrlGeneratorInterface::ABSOLUTE_URL
);
if ($settings->getIntegration() == Integration::PAYMENT_PAGE) {
$redirectUrl = $apiClient->getTransactionPaymentPageService()
->paymentPageUrl($settings->getSpaceId(), $createdTransaction->getId());
}
$this->upsert(
$createdTransaction,
$salesChannelContext->getContext(),
$transaction->getOrderTransaction()->getPaymentMethodId(),
$transaction->getOrder()->getSalesChannelId()
);
$_SESSION['transactionId'] = null;
$_SESSION['arrayOfPossibleMethods'] = null;
$_SESSION['addressCheck'] = null;
$_SESSION['currencyCheck'] = null;
$this->holdDelivery($transaction->getOrder()->getId(), $salesChannelContext->getContext());
return $redirectUrl;
}
/**
* @param \Shopware\Core\Checkout\Payment\Cart\AsyncPaymentTransactionStruct $transaction
* @param \Shopware\Core\Framework\Context $context
* @param int $vrpaymentTransactionId
* @param int $spaceId
*/
protected function addVRPaymentTransactionId(
AsyncPaymentTransactionStruct $transaction,
Context $context,
int $vrpaymentTransactionId,
int $spaceId
): void
{
$data = [
'id' => $transaction->getOrderTransaction()->getId(),
'customFields' => [
TransactionPayload::ORDER_TRANSACTION_CUSTOM_FIELDS_VRPAYMENT_TRANSACTION_ID => $vrpaymentTransactionId,
TransactionPayload::ORDER_TRANSACTION_CUSTOM_FIELDS_VRPAYMENT_SPACE_ID => $spaceId,
],
];
$this->container->get('order_transaction.repository')->update([$data], $context);
}
/**
* Persist VRPayment transaction
*
* @param \VRPayment\Sdk\Model\Transaction $transaction
* @param \Shopware\Core\Framework\Context $context
* @param string|null $paymentMethodId
* @param string|null $salesChannelId
*/
public function upsert(
Transaction $transaction,
Context $context,
string $paymentMethodId = null,
string $salesChannelId = null
): void
{
try {
$transactionId = $transaction->getId();
$transactionMetaData = $transaction->getMetaData();
if (!$salesChannelId) {
$salesChannelId = $transactionMetaData['salesChannelId'] ?? '';
}
$orderId = $transactionMetaData[TransactionPayload::VRPAYMENT_METADATA_ORDER_ID];
$orderTransactionId = $transactionMetaData[TransactionPayload::VRPAYMENT_METADATA_ORDER_TRANSACTION_ID];
$dataParamValue = json_decode(strval($transaction), true);
$brandName = '';
if (isset($dataParamValue['paymentConnectorConfiguration'])) {
$brandName = $dataParamValue['paymentConnectorConfiguration']
? $dataParamValue['paymentConnectorConfiguration']['name']
: '';
}
$dataParamValue['brandName'] = $brandName;
$paymentMethodName = '';
if (isset($dataParamValue['paymentConnectorConfiguration'])) {
$paymentMethodName = $dataParamValue['paymentConnectorConfiguration']
? $dataParamValue['paymentConnectorConfiguration']['paymentMethodConfiguration']['name']
: '';
}
$dataParamValue['paymentMethodName'] = $paymentMethodName;
$chargeAttempt = $this->getChargeAttempt($salesChannelId, $transactionId);
$erpMerchantId = null;
if ($chargeAttempt) {
$creditCardHolder = $this->getChargeAttemptAdditionalData($chargeAttempt, self::CARD_HOLDER_KEY);
$dataParamValue['creditCardHolder'] = $creditCardHolder ? $creditCardHolder[0] : '';
$pseudoCardNumber = $this->getChargeAttemptAdditionalData($chargeAttempt, self::PSEUDO_CODE_KEY);
$dataParamValue['pseudoCardNumber'] = $pseudoCardNumber ? $pseudoCardNumber[0] : '';
$payId = $this->getChargeAttemptAdditionalData($chargeAttempt, self::PAY_ID_KEY);
$dataParamValue['payId'] = $payId ? $payId[0] : '';
$dataParamValue['customerName'] = isset($transactionMetaData[TransactionPayload::VRPAYMENT_METADATA_CUSTOMER_NAME])
? $transactionMetaData[TransactionPayload::VRPAYMENT_METADATA_CUSTOMER_NAME]
: '';
$creditCardValidity = $this->getChargeAttemptAdditionalData($chargeAttempt, self::CARD_VALIDITY_KEY);
if (isset($creditCardValidity['cardExpireMonth']) && isset($creditCardValidity['cardExpireYear'])) {
$creditCardExpireMonth = $creditCardValidity['cardExpireMonth'] ?? null;
if (!empty($creditCardExpireMonth)) {
$dataParamValue['cardExpireMonth'] = sprintf("%02d", $creditCardExpireMonth);
}
$creditCardExpireYear = $creditCardValidity['cardExpireYear'] ?? null;
if (!empty($creditCardExpireYear)) {
$dataParamValue['cardExpireYear'] = $creditCardExpireYear;
}
}
$erpMerchantId = $this->getChargeAttemptAdditionalData($chargeAttempt, self::ADDITIONAL_TRANSACTION_DETAILS_ORDER_ID_KEY);
$erpMerchantId = $erpMerchantId ? $erpMerchantId[0] : null;
}
$data = [
'id' => $orderId,
'erpMerchantId' => $erpMerchantId,
'data' => $dataParamValue,
'paymentMethodId' => $paymentMethodId,
'orderId' => $orderId,
'orderTransactionId' => $orderTransactionId,
'spaceId' => $transaction->getLinkedSpaceId(),
'state' => $transaction->getState(),
'salesChannelId' => $salesChannelId,
'transactionId' => $transaction->getId(),
];
$data = array_filter($data);
$this->container->get(TransactionEntityDefinition::ENTITY_NAME . '.repository')->upsert([$data], $context);
} catch (\Exception $exception) {
$this->logger->critical(__CLASS__ . ' : ' . __FUNCTION__ . ' : ' . $exception->getMessage());
}
}
/**
* Hold delivery
*
* @param string $orderId
* @param \Shopware\Core\Framework\Context $context
*/
private function holdDelivery(string $orderId, Context $context)
{
try {
/**
* @var OrderDeliveryStateHandler $orderDeliveryStateHandler
*/
$orderEntity = $this->getOrderEntity($orderId, $context);
$orderDeliveryStateHandler = $this->container->get(OrderDeliveryStateHandler::class);
if (null !== $orderEntity->getDeliveries()->last()) {
$orderDeliveryStateHandler->hold($orderEntity->getDeliveries()->last()->getId(), $context);
}
} catch (\Exception $exception) {
$this->logger->critical($exception->getTraceAsString());
}
}
/**
* Get order
*
* @param String $orderId
* @param \Shopware\Core\Framework\Context $context
*
* @return \Shopware\Core\Checkout\Order\OrderEntity
*/
private function getOrderEntity(string $orderId, Context $context): OrderEntity
{
try {
$criteria = (new Criteria([$orderId]))->addAssociations(['deliveries']);
$order = $this->container->get('order.repository')->search(
$criteria,
$context
)->first();
if (is_null($order)) {
throw CartException::orderNotFound($orderId);
}
return $order;
} catch (\Exception $e) {
throw CartException::orderNotFound($orderId);
}
}
/**
* Get transaction entity by orderId
*
* @param string $orderId
* @param \Shopware\Core\Framework\Context $context
*
* @return \VRPaymentPayment\Core\Api\Transaction\Entity\TransactionEntity
*/
public function getByOrderId(string $orderId, Context $context): TransactionEntity
{
return $this->container->get(TransactionEntityDefinition::ENTITY_NAME . '.repository')
->search(new Criteria([$orderId]), $context)
->get($orderId);
}
/**
* Read transaction from VRPayment API
*
* @param int $transactionId
* @param string $salesChannelId
*
* @return \VRPayment\Sdk\Model\Transaction
* @throws \VRPayment\Sdk\ApiException
* @throws \VRPayment\Sdk\Http\ConnectionException
* @throws \VRPayment\Sdk\VersioningException
*/
public function read(int $transactionId, string $salesChannelId): Transaction
{
$settings = $this->settingsService->getSettings($salesChannelId);
return $settings->getApiClient()->getTransactionService()->read($settings->getSpaceId(), $transactionId);
}
/**
* Get transaction entity by VRPayment transaction id
*
* @param int $transactionId
* @param \Shopware\Core\Framework\Context $context
*
* @return \VRPaymentPayment\Core\Api\Transaction\Entity\TransactionEntity|null
*/
public function getByTransactionId(int $transactionId, Context $context): ?TransactionEntity
{
return $this->container->get(TransactionEntityDefinition::ENTITY_NAME . '.repository')
->search(
(new Criteria())->addFilter(new EqualsFilter('transactionId', $transactionId))
->addAssociations(['refunds']), $context
)
->first();
}
/**
* Get transaction entity by VRPayment order transaction id
*
* @param string $transactionId
* @param \Shopware\Core\Framework\Context $context
*
* @return \VRPaymentPayment\Core\Api\Transaction\Entity\TransactionEntity|null
*/
public function getByOrderTransactionId(string $orderTransactionId, Context $context): ?TransactionEntity
{
return $this->container->get(TransactionEntityDefinition::ENTITY_NAME . '.repository')
->search(
(new Criteria())->addFilter(new EqualsFilter('orderTransactionId', $orderTransactionId))
->addAssociations(['refunds']), $context
)
->first();
}
/**
* Get transaction entity by VRPayment transaction id
*
* @param int $transactionId
* @param \Shopware\Core\Framework\Context $context
*
* @return \VRPaymentPayment\Core\Api\Refund\Entity\RefundEntityCollection
*/
public function getRefundEntityCollectionByTransactionId(int $transactionId, Context $context): ?RefundEntityCollection
{
return $this->container->get(RefundEntityDefinition::ENTITY_NAME . '.repository')
->search(
(new Criteria())->addFilter(new EqualsFilter('transactionId', $transactionId)), $context
)
->getEntities();
}
/**
* @param string $orderId
* @param float $invoicePaidAmount
* @param Context $context
* @return void
*/
public function updateOrderTotalPriceByInvoiceTotal(string $orderId, float $invoicePaidAmount, Context $context): void
{
$price = $this->getOrderEntity($orderId, $context)->getPrice();
if ($price->getTotalPrice() === $invoicePaidAmount) {
return;
}
$data = [
'id' => $orderId,
'price' => [
'netPrice' => $price->getNetPrice(),
'rawTotal' => $price->getRawTotal(),
'taxRules' => $price->getTaxRules(),
'taxStatus' => $price->getTaxStatus(),
'totalPrice' => $invoicePaidAmount,
'positionPrice' => $price->getPositionPrice(),
'calculatedTaxes' => $price->getCalculatedTaxes()
],
];
$this->container->get('order.repository')->update([$data], $context);
}
/**
* @param SalesChannelContext $salesChannelContext
* @param CheckoutConfirmPageLoadedEvent|null $event
* @return int
*/
public function createPendingTransaction(SalesChannelContext $salesChannelContext, ?CheckoutConfirmPageLoadedEvent $event = null): int
{
$expiredTransaction = true;
$transactionId = $_SESSION['transactionId'] ?? null;
$settings = $this->settingsService->getValidSettings($salesChannelContext->getSalesChannel()->getId());
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;
}
}
if (!$transactionId || $expiredTransaction) {
$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();
}
}
$billingAddress->setFamilyName($familyName);
$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 = "";
if ($customer->getGuest() === false) {
$customerId = $customer->getCustomerNumber();
}
$protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https://' : 'http://';
$homeUrl = $protocol . $_SERVER['HTTP_HOST'];
$currency = $salesChannelContext->getCurrency()->getIsoCode();
$transactionPayload = (new TransactionCreate())
->setBillingAddress($billingAddress)
->setLineItems($lineItems)
->setCurrency($currency)
->setSpaceViewId($settings->getSpaceViewId())
->setAutoConfirmationEnabled(false)
->setChargeRetryEnabled(false)
->setCustomerEmailAddress($customer->getEmail())
->setCustomerId($customerId)
->setSuccessUrl($homeUrl . '?success')
->setFailedUrl($homeUrl . '?fail');
$transactionService = $settings->getApiClient()->getTransactionService();
$transaction = $transactionService->create($settings->getSpaceId(), $transactionPayload);
$transactionId = $transaction->getId();
$_SESSION['transactionId'] = $transactionId;
}
return $transactionId;
}
/**
* @param SalesChannelContext $salesChannelContext
* @param int $transactionId
* @return void
*/
public function updateTempTransaction(SalesChannelContext $salesChannelContext, int $transactionId): void
{
$pendingTransaction = new TransactionPending();
$pendingTransaction->setId($transactionId);
$settings = $this->settingsService->getValidSettings($salesChannelContext->getSalesChannel()->getId());
$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();
$pendingTransaction->setCurrency($currency);
$pendingTransaction->setBillingAddress($billingAddress);
$settings->getApiClient()->getTransactionService()
->update($settings->getSpaceId(), $pendingTransaction);
}
/**
* @param ChargeAttempt|null $chargeAttempt
* @param string $descriptorKey
* @return array
*/
private function getChargeAttemptAdditionalData(?ChargeAttempt $chargeAttempt, string $descriptorKey): array
{
if (!$chargeAttempt) {
return [];
}
$labels = $chargeAttempt->getLabels() ?? [];
if (empty($labels)) {
return [];
}
foreach ($labels as $label) {
$descriptor = $label->getDescriptor();
if ((string)$descriptor->getId() !== $descriptorKey) {
continue;
}
switch ($descriptorKey) {
case self::CARD_HOLDER_KEY:
return [$label->getContentAsString()];
case self::PSEUDO_CODE_KEY:
return [$label->getContentAsString()];
case self::PAY_ID_KEY:
return [$label->getContentAsString()];
case self::ADDITIONAL_TRANSACTION_DETAILS_ORDER_ID_KEY:
return [$label->getContentAsString()];
case self::CARD_VALIDITY_KEY:
$validityYear = '';
$validityMonth = '';
foreach ($label->getContent() as $cardValidityItem) {
if (strlen((string)$cardValidityItem) === 1 || strlen((string)$cardValidityItem) === 2) {
$validityMonth = $cardValidityItem;
} elseif (strlen((string)$cardValidityItem) === 4) {
$validityYear = $cardValidityItem;
}
}
if (empty($validityYear) || empty($validityMonth)) {
return [];
}
return [
'cardExpireMonth' => $validityMonth,
'cardExpireYear' => $validityYear,
];
}
}
return [];
}
/**
* @param string $salesChannelId
* @param int $transactionId
* @return ChargeAttempt|null
*/
private function getChargeAttempt(string $salesChannelId, int $transactionId): ?ChargeAttempt
{
/** @noinspection PhpParamsInspection */
$entityQueryFilter = (new EntityQueryFilter())
->setType(EntityQueryFilterType::LEAF)
->setOperator(CriteriaOperator::EQUALS)
->setFieldName('charge.transaction')
->setValue($transactionId);
$query = (new EntityQuery())->setFilter($entityQueryFilter);
$settings = $this->settingsService->getSettings($salesChannelId);
$chargeAttempts = $settings->getApiClient()->getChargeAttemptService()->search($settings->getSpaceId(), $query);
return $chargeAttempts ? $chargeAttempts[0] : null;
}
/**
* @param LineItem $productData
* @return LineItemCreate
*/
private function createTempLineItem(LineItem $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());
$lineItem->setType(LineItemType::PRODUCT);
return $lineItem;
}
}
@@ -0,0 +1,62 @@
<?php declare(strict_types=1);
namespace VRPaymentPayment\Core\Api\WebHooks\Command;
use Symfony\Component\{
Console\Command\Command,
Console\Attribute\AsCommand,
Console\Input\InputInterface,
Console\Output\OutputInterface};
use VRPaymentPayment\Core\Api\WebHooks\Service\WebHooksService;
/**
* Class WebHooksCommand
*
* @package VRPaymentPayment\Core\Api\WebHooks\Command
*/
#[AsCommand(name: 'vrpayment:webhooks:install')]
class WebHooksCommand extends Command {
/**
* @var \VRPaymentPayment\Core\Api\WebHooks\Service\WebHooksService
*/
protected $webHooksService;
/**
* WebHooksCommand constructor.
*
* @param \VRPaymentPayment\Core\Api\WebHooks\Service\WebHooksService $webHooksService
*/
public function __construct(WebHooksService $webHooksService)
{
parent::__construct();
$this->webHooksService = $webHooksService;
}
/**
* @param \Symfony\Component\Console\Input\InputInterface $input
* @param \Symfony\Component\Console\Output\OutputInterface $output
*
* @return int
* @throws \VRPayment\Sdk\ApiException
* @throws \VRPayment\Sdk\Http\ConnectionException
* @throws \VRPayment\Sdk\VersioningException
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$output->writeln('Install VRPaymentPayment webhooks...');
$this->webHooksService->install();
return 0;
}
/**
* Configures the current command.
*/
protected function configure()
{
$this->setDescription('Install VRPaymentPayment webhooks.')
->setHelp('This command installs VRPaymentPayment webhooks.');
}
}
@@ -0,0 +1,706 @@
<?php declare(strict_types=1);
namespace VRPaymentPayment\Core\Api\WebHooks\Controller;
use Doctrine\DBAL\{
Connection,
TransactionIsolationLevel};
use Psr\Log\LoggerInterface;
use Shopware\Core\{
Checkout\Cart\CartException,
Checkout\Order\Aggregate\OrderDelivery\OrderDeliveryEntity,
Checkout\Order\Aggregate\OrderTransaction\OrderTransactionEntity,
Checkout\Order\Aggregate\OrderTransaction\OrderTransactionStateHandler,
Checkout\Order\Aggregate\OrderTransaction\OrderTransactionStates,
Checkout\Order\OrderEntity,
Checkout\Order\SalesChannel\OrderService,
Framework\Context,
Framework\DataAbstractionLayer\Search\Criteria,
Framework\DataAbstractionLayer\Search\Sorting\FieldSorting,
Framework\Routing\Annotation\RouteScope,
System\StateMachine\Aggregation\StateMachineTransition\StateMachineTransitionActions,
System\StateMachine\Exception\IllegalTransitionException};
use Shopware\Core\Checkout\Order\OrderStates;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Shopware\Core\Framework\Log\Package;
use Symfony\Component\{
HttpFoundation\JsonResponse,
HttpFoundation\ParameterBag,
HttpFoundation\Request,
HttpFoundation\Response,
Routing\Attribute\Route};
use VRPayment\Sdk\{
Model\RefundState,
Model\Transaction,
Model\TransactionInvoiceState,
Model\TransactionState,
Model\TransactionInvoice,};
use VRPaymentPayment\Core\{Api\OrderDeliveryState\Handler\OrderDeliveryStateHandler,
Api\PaymentMethodConfiguration\Service\PaymentMethodConfigurationService,
Api\Refund\Service\RefundService,
Api\Transaction\Service\OrderMailService,
Api\Transaction\Service\TransactionService,
Api\WebHooks\Strategy\WebHookPaymentMethodConfigurationStrategy,
Api\WebHooks\Strategy\WebHookRefundStrategy,
Api\WebHooks\Strategy\WebHookStrategyManager,
Api\WebHooks\Strategy\WebHookTransactionInvoiceStrategy,
Api\WebHooks\Strategy\WebHookTransactionStrategy,
Api\WebHooks\Struct\WebHookRequest,
Settings\Service\SettingsService,
Util\Payload\TransactionPayload};
/**
* Class WebHookController
*
* @package VRPaymentPayment\Core\Api\WebHooks\Controller
*
*/
#[Package('sales-channel')]
#[Route(defaults: ['_routeScope' => ['api']])]
class WebHookController extends AbstractController {
/**
* @var \Doctrine\DBAL\Connection
*/
protected $connection;
/**
* @var \Psr\Log\LoggerInterface
*/
protected $logger;
/**
* @var \VRPaymentPayment\Core\Api\Transaction\Service\OrderMailService
*/
protected $orderMailService;
/**
* @var \Shopware\Core\Checkout\Order\Aggregate\OrderTransaction\OrderTransactionStateHandler
*/
protected $orderTransactionStateHandler;
/**
* @var \VRPaymentPayment\Core\Api\PaymentMethodConfiguration\Service\PaymentMethodConfigurationService
*/
protected $paymentMethodConfigurationService;
/**
* @var \VRPaymentPayment\Core\Settings\Struct\Settings
*/
protected $settings;
/**
* @var \VRPaymentPayment\Core\Settings\Service\SettingsService
*/
protected $settingsService;
/**
* @var \VRPaymentPayment\Core\Api\Refund\Service\RefundService
*/
protected $refundService;
/**
* @var \VRPaymentPayment\Core\Api\Transaction\Service\TransactionService
*/
protected $transactionService;
/**
* Transaction Final States
*
* @var array
*/
protected $transactionFinalStates = [
OrderTransactionStates::STATE_CANCELLED,
OrderTransactionStates::STATE_PAID,
OrderTransactionStates::STATE_REFUNDED,
];
/**
* Transaction Failed States
*
* @var array
*/
protected $transactionFailedStates = [
TransactionState::DECLINE,
TransactionState::FAILED,
TransactionState::VOIDED,
];
protected $vrpaymentTransactionSuccessStates = [
TransactionState::AUTHORIZED,
TransactionState::COMPLETED,
TransactionState::FULFILL,
];
/**
* @var \Shopware\Core\Checkout\Order\OrderEntity
*/
private $orderEntity;
/**
* @var \Shopware\Core\Checkout\Order\SalesChannel\OrderService
*/
private $orderService;
/**
* @var \VRPaymentPayment\Core\Api\WebHooks\Strategy\WebHookStrategyManager
*/
private $webHookStrategyManager;
const LINE_ITEM_TYPE_FEE = 'FEE';
/**
* WebHookController constructor.
*
* @param \Doctrine\DBAL\Connection $connection
* @param \Shopware\Core\Checkout\Order\Aggregate\OrderTransaction\OrderTransactionStateHandler $orderTransactionStateHandler
* @param \Shopware\Core\Checkout\Order\SalesChannel\OrderService $orderService
* @param \VRPaymentPayment\Core\Api\PaymentMethodConfiguration\Service\PaymentMethodConfigurationService $paymentMethodConfigurationService
* @param \VRPaymentPayment\Core\Api\Refund\Service\RefundService $refundService
* @param \VRPaymentPayment\Core\Api\Transaction\Service\OrderMailService $orderMailService
* @param \VRPaymentPayment\Core\Api\Transaction\Service\TransactionService $transactionService
* @param \VRPaymentPayment\Core\Settings\Service\SettingsService $settingsService
* @param \VRPaymentPayment\Core\Api\WebHooks\Strategy\WebHookStrategyManager $settingsService
*/
public function __construct(
Connection $connection,
OrderTransactionStateHandler $orderTransactionStateHandler,
OrderService $orderService,
PaymentMethodConfigurationService $paymentMethodConfigurationService,
RefundService $refundService,
OrderMailService $orderMailService,
TransactionService $transactionService,
SettingsService $settingsService,
WebHookStrategyManager $webHookStrategyManager
)
{
$this->connection = $connection;
$this->orderTransactionStateHandler = $orderTransactionStateHandler;
$this->paymentMethodConfigurationService = $paymentMethodConfigurationService;
$this->refundService = $refundService;
$this->orderMailService = $orderMailService;
$this->transactionService = $transactionService;
$this->settingsService = $settingsService;
$this->orderService = $orderService;
$this->webHookStrategyManager = $webHookStrategyManager;
}
/**
* @param \Psr\Log\LoggerInterface $logger
*
* @internal
* @required
*
*/
public function setLogger(LoggerInterface $logger): void
{
$this->logger = $logger;
}
/**
* This is the method VRPayment calls
*
* @param \Symfony\Component\HttpFoundation\Request $request
* @param \Shopware\Core\Framework\Context $context
* @param string $salesChannelId
*
* @return \Symfony\Component\HttpFoundation\JsonResponse|\Symfony\Component\HttpFoundation\Response
*/
#[Route(
path: "/api/_action/vrpayment/webHook/callback/{salesChannelId}",
name: "api.action.vrpayment.webhook.update",
options: ["seo" => false],
defaults: [
"csrf_protected" => false,
"XmlHttpRequest" => true,
"auth_required" => false,
],
methods: ["POST"],
)]
public function callback(Request $request, Context $context, string $salesChannelId): Response
{
$status = Response::HTTP_UNPROCESSABLE_ENTITY;
$callBackData = new WebHookRequest();
try {
// 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);
$apiClient = $this->settings->getApiClient();
$callBackData->assign($requestJson);
// Handling of payloads without a signature (legacy method).
// Deprecated since 3.0.12
if (empty($signature)) {
switch ($callBackData->getListenerEntityTechnicalName()) {
case WebHookRequest::PAYMENT_METHOD_CONFIGURATION:
return $this->updatePaymentMethodConfiguration($context, $salesChannelId);
case WebHookRequest::REFUND:
return $this->updateRefund($callBackData, $context);
case WebHookRequest::TRANSACTION:
return $this->updateTransaction($callBackData, $context);
case WebHookRequest::TRANSACTION_INVOICE:
return $this->updateTransactionInvoice($callBackData, $context);
default:
$this->logger->warning(__CLASS__ . ' : ' . __FUNCTION__ . ' : Listener not implemented : ', $callBackData->jsonSerialize());
}
}
// Handling of payloads with a valid signature.
// This payload signed has the transaction state
if (!empty($signature) && $apiClient->getWebhookEncryptionService()->isContentValid($signature, $request->getContent())) {
return $this->webHookStrategyManager->process($callBackData, $context, $salesChannelId);
}
$status = Response::HTTP_OK;
} catch (\Exception $exception) {
$this->logger->critical(__CLASS__ . ' : ' . __FUNCTION__ . ' : ' . $exception->getMessage(), $callBackData->jsonSerialize());
}
return new JsonResponse(['data' => $callBackData], $status);
}
/**
* Handle VRPayment Payment Method Configuration callback
*
* @param \Shopware\Core\Framework\Context $context
* @param string $salesChannelId
*
* @return \Symfony\Component\HttpFoundation\Response
* @throws \VRPayment\Sdk\ApiException
* @throws \VRPayment\Sdk\Http\ConnectionException
* @throws \VRPayment\Sdk\VersioningException
* @deprecated 6.1.8 No longer used by internal code and not recommended.
* @see WebHookPaymentMethodConfigurationStrategy
*/
private function updatePaymentMethodConfiguration(Context $context, string $salesChannelId = null): Response
{
$result = $this->paymentMethodConfigurationService->setSalesChannelId($salesChannelId)->synchronize($context);
return new JsonResponse(['result' => $result]);
}
/**
* Handle VRPayment Refund callback
*
* @param \VRPaymentPayment\Core\Api\WebHooks\Struct\WebHookRequest $callBackData
* @param \Shopware\Core\Framework\Context $context
*
* @return \Symfony\Component\HttpFoundation\Response
* @deprecated 6.1.8 No longer used by internal code and not recommended.
* @see WebHookRefundStrategy
*/
public function updateRefund(WebHookRequest $callBackData, Context $context): Response
{
$status = Response::HTTP_UNPROCESSABLE_ENTITY;
try {
/**
* @var \VRPayment\Sdk\Model\Transaction $transaction
*/
$refund = $this->settings->getApiClient()->getRefundService()
->read($callBackData->getSpaceId(), $callBackData->getEntityId());
$orderId = $refund->getTransaction()->getMetaData()[TransactionPayload::VRPAYMENT_METADATA_ORDER_ID];
if(!empty($orderId)) {
$this->executeLocked($orderId, $context, function () use ($orderId, $refund, $context) {
$this->refundService->upsert($refund, $context);
$orderTransactionId = $refund->getTransaction()->getMetaData()[TransactionPayload::VRPAYMENT_METADATA_ORDER_TRANSACTION_ID];
$orderTransaction = $this->getOrderTransaction($orderId, $context);
if (
in_array(
$orderTransaction->getStateMachineState()?->getTechnicalName(),
[
OrderTransactionStates::STATE_PAID,
OrderTransactionStates::STATE_PARTIALLY_PAID,
]
) &&
($refund->getState() == RefundState::SUCCESSFUL)
) {
if ($refund->getAmount() == $orderTransaction->getAmount()->getTotalPrice()) {
$this->orderTransactionStateHandler->refund($orderTransactionId, $context);
} else {
if ($refund->getAmount() < $orderTransaction->getAmount()->getTotalPrice()) {
$this->orderTransactionStateHandler->refundPartially($orderTransactionId, $context);
}
}
} elseif ($orderTransaction->getStateMachineState()?->getTechnicalName()
=== OrderTransactionStates::STATE_PARTIALLY_REFUNDED &&
($refund->getState() == RefundState::SUCCESSFUL)
) {
$transactionByOrderTransactionId = $this->transactionService->getByOrderTransactionId($orderTransactionId, $context);
$totalRefundedAmount = $this->getTotalRefundedAmount($transactionByOrderTransactionId->getTransactionId(), $context);
if (floatval($orderTransaction->getAmount()->getTotalPrice()) - $totalRefundedAmount <= 0) {
$this->orderTransactionStateHandler->refund($orderTransactionId, $context);
}
}
});
}
$status = Response::HTTP_OK;
} catch (CartException $exception) {
$status = Response::HTTP_OK;
$this->logger->info(__CLASS__ . ' : ' . __FUNCTION__ . ' : ' . $exception->getMessage(), $callBackData->jsonSerialize());
} catch (IllegalTransitionException $exception) {
$status = Response::HTTP_OK;
$this->logger->info(__CLASS__ . ' : ' . __FUNCTION__ . ' : ' . $exception->getMessage(), $callBackData->jsonSerialize());
} catch (\Exception $exception) {
$this->logger->critical(__CLASS__ . ' : ' . __FUNCTION__ . ' : ' . $exception->getMessage(), $callBackData->jsonSerialize());
}
return new JsonResponse(['data' => $callBackData->jsonSerialize()], $status);
}
/**
* @param int $transactionId
* @param Context $context
* @return float
*/
private function getTotalRefundedAmount(int $transactionId, Context $context): float
{
$amount = 0;
$refunds = $this->transactionService->getRefundEntityCollectionByTransactionId($transactionId, $context);
foreach ($refunds as $refund) {
$amount += floatval($refund->getData()['amount'] ?? 0);
}
return (float) (string) $amount;
}
/**
* @param string $orderId
* @param Context $context
* @param callable $operation
*
* @return mixed
* @throws \Exception
*/
private function executeLocked(string $orderId, Context $context, callable $operation)
{
//$this->connection->setTransactionIsolation(TransactionIsolationLevel::READ_COMMITTED);
//$this->connection->beginTransaction();
try {
$data = [
'id' => $orderId,
'vrpayment_lock' => date('Y-m-d H:i:s'),
];
$order = $this->container->get('order.repository')->search(new Criteria([$orderId]), $context)->first();
if(empty($order)){
throw CartException::orderNotFound($orderId);
}
$this->container->get('order.repository')->upsert([$data], $context);
$result = $operation();
//$this->connection->commit();
return $result;
} catch (\Exception $exception) {
//$this->connection->rollBack();
throw $exception;
}
}
/**
* @param String $orderId
* @param \Shopware\Core\Framework\Context $context
*
* @return OrderTransactionEntity
* @deprecated 6.1.8 No longer used by internal code and not recommended.
* @see WebHookTransactionStrategy
*/
private function getOrderTransaction(String $orderId, Context $context): OrderTransactionEntity
{
return $this->getOrderEntity($orderId, $context)->getTransactions()->last();
}
/**
* Get order
*
* @param String $orderId
* @param \Shopware\Core\Framework\Context $context
*
* @return \Shopware\Core\Checkout\Order\OrderEntity
* @deprecated 6.1.8 No longer used by internal code and not recommended.
* @see WebHookTransactionStrategy
*/
private function getOrderEntity(string $orderId, Context $context): OrderEntity
{
if (is_null($this->orderEntity)) {
$criteria = (new Criteria([$orderId]))
->addAssociations(['deliveries', 'transactions']);
$criteria->getAssociation('transactions')
->addSorting(new FieldSorting('createdAt', FieldSorting::ASCENDING));
try {
$this->orderEntity = $this->container->get('order.repository')->search(
$criteria,
$context
)->first();
if (is_null($this->orderEntity)) {
throw CartException::orderNotFound($orderId);
}
} catch (\Exception $e) {
throw CartException::orderNotFound($orderId);
}
}
return $this->orderEntity;
}
/**
* Handle VRPayment Transaction callback
*
* @param \VRPaymentPayment\Core\Api\WebHooks\Struct\WebHookRequest $callBackData
* @param \Shopware\Core\Framework\Context $context
*
* @return \Symfony\Component\HttpFoundation\Response
* @deprecated 6.1.8 No longer used by internal code and not recommended.
* @see WebHookTransactionStrategy
*/
private function updateTransaction(WebHookRequest $callBackData, Context $context): Response
{
$status = Response::HTTP_UNPROCESSABLE_ENTITY;
try {
/**
* @var \VRPayment\Sdk\Model\Transaction $transaction
* @var \Shopware\Core\Checkout\Order\OrderEntity $order
*/
$transaction = $this->settings->getApiClient()
->getTransactionService()
->read($callBackData->getSpaceId(), $callBackData->getEntityId());
$orderId = $transaction->getMetaData()[TransactionPayload::VRPAYMENT_METADATA_ORDER_ID];
if(!empty($orderId) && !$transaction->getParent()) {
$this->executeLocked($orderId, $context, function () use ($orderId, $transaction, $context, $callBackData) {
$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: {$orderTransaction->getStateMachineState()?->getTechnicalName()}");
if (!in_array(
$orderTransaction->getStateMachineState()?->getTechnicalName(),
$this->transactionFinalStates
)) {
switch ($transaction->getState()) {
case TransactionState::FAILED:
$this->orderTransactionStateHandler->fail($orderTransactionId, $context);
$this->unholdAndCancelDelivery($orderId, $context);
break;
case TransactionState::DECLINE:
case TransactionState::VOIDED:
$this->orderTransactionStateHandler->cancel($orderTransactionId, $context);
$this->unholdAndCancelDelivery($orderId, $context);
break;
case TransactionState::FULFILL:
$this->unholdDelivery($orderId, $context);
break;
case TransactionState::AUTHORIZED:
$this->orderTransactionStateHandler->process($orderTransactionId, $context);
$this->sendEmail($transaction, $context);
break;
default:
break;
}
}
});
}
$status = Response::HTTP_OK;
} catch (CartException $exception) {
$status = Response::HTTP_OK;
$this->logger->info(__CLASS__ . ' : ' . __FUNCTION__ . ' : ' . $exception->getMessage(), $callBackData->jsonSerialize());
} catch (IllegalTransitionException $exception) {
$status = Response::HTTP_OK;
$this->logger->info(__CLASS__ . ' : ' . __FUNCTION__ . ' : ' . $exception->getMessage(), $callBackData->jsonSerialize());
} catch (\Exception $exception) {
$this->logger->critical(__CLASS__ . ' : ' . __FUNCTION__ . ' : ' . $exception->getMessage(), $callBackData->jsonSerialize());
}
return new JsonResponse(['data' => $callBackData->jsonSerialize()], $status);
}
/**
* @param \VRPayment\Sdk\Model\Transaction $transaction
* @param \Shopware\Core\Framework\Context $context
* @deprecated 6.1.8 No longer used by internal code and not recommended.
*/
protected function sendEmail(Transaction $transaction, Context $context): void
{
$orderId = $transaction->getMetaData()[TransactionPayload::VRPAYMENT_METADATA_ORDER_ID];
if ($this->settings->isEmailEnabled() && in_array($transaction->getState(), $this->vrpaymentTransactionSuccessStates)) {
$this->orderMailService->send($orderId, $context);
}
}
/**
* Handle VRPayment TransactionInvoice callback
*
* @param \VRPaymentPayment\Core\Api\WebHooks\Struct\WebHookRequest $callBackData
* @param \Shopware\Core\Framework\Context $context
*
* @return \Symfony\Component\HttpFoundation\Response
* @deprecated 6.1.8 No longer used by internal code and not recommended.
* @see WebHookTransactionInvoiceStrategy
*/
public function updateTransactionInvoice(WebHookRequest $callBackData, Context $context): Response
{
$status = Response::HTTP_UNPROCESSABLE_ENTITY;
try {
/**
* @var \VRPayment\Sdk\Model\Transaction $transaction
* @var TransactionInvoice $transactionInvoice
*/
$transactionInvoice = $this->settings->getApiClient()->getTransactionInvoiceService()
->read($callBackData->getSpaceId(), $callBackData->getEntityId());
$orderId = $transactionInvoice->getCompletion()
->getLineItemVersion()
->getTransaction()
->getMetaData()[TransactionPayload::VRPAYMENT_METADATA_ORDER_ID];
if(!empty($orderId)) {
$this->executeLocked($orderId, $context, function () use ($orderId, $transactionInvoice, $context) {
$orderTransactionId = $transactionInvoice->getCompletion()
->getLineItemVersion()
->getTransaction()
->getMetaData()[TransactionPayload::VRPAYMENT_METADATA_ORDER_TRANSACTION_ID];
$orderTransaction = $this->getOrderTransaction($orderId, $context);
$this->updatePriceIfAdditionalItemsExist($transactionInvoice, $orderTransaction, $context);
if (!in_array(
$orderTransaction->getStateMachineState()?->getTechnicalName(),
$this->transactionFinalStates
)) {
switch ($transactionInvoice->getState()) {
case TransactionInvoiceState::DERECOGNIZED:
$this->orderTransactionStateHandler->cancel($orderTransactionId, $context);
break;
case TransactionInvoiceState::NOT_APPLICABLE:
case TransactionInvoiceState::PAID:
$this->orderTransactionStateHandler->paid($orderTransactionId, $context);
$this->unholdDelivery($orderTransactionId, $context);
break;
default:
break;
}
}
});
}
$status = Response::HTTP_OK;
} catch (CartException $exception) {
$status = Response::HTTP_OK;
$this->logger->info(__CLASS__ . ' : ' . __FUNCTION__ . ' : ' . $exception->getMessage(), $callBackData->jsonSerialize());
} catch (IllegalTransitionException $exception) {
$status = Response::HTTP_OK;
$this->logger->info(__CLASS__ . ' : ' . __FUNCTION__ . ' : ' . $exception->getMessage(), $callBackData->jsonSerialize());
} catch (\Exception $exception) {
$this->logger->critical(__CLASS__ . ' : ' . __FUNCTION__ . ' : ' . $exception->getMessage(), $callBackData->jsonSerialize());
}
return new JsonResponse(['data' => $callBackData->jsonSerialize()], $status);
}
/**
* Updates order table field price only if there are additional items added from portal side
*
* @param TransactionInvoice $transactionInvoice
* @param OrderTransactionEntity $orderTransaction
* @param Context $context
* @return void
*/
private function updatePriceIfAdditionalItemsExist(TransactionInvoice $transactionInvoice, OrderTransactionEntity $orderTransaction, Context $context): void
{
$completionLineItems = $transactionInvoice->getCompletion()->getLineItems();
$lineItems = $transactionInvoice->getLineItems();
if (count($completionLineItems) !== count($lineItems)) {
$this->transactionService->updateOrderTotalPriceByInvoiceTotal(
$orderTransaction->getOrderId(),
$transactionInvoice->getOutstandingAmount(),
$context
);
}
}
/**
* Hold delivery
*
* @param string $orderId
* @param \Shopware\Core\Framework\Context $context
*/
private function unholdDelivery(string $orderId, Context $context): void
{
try {
/**
* @var OrderDeliveryStateHandler $orderDeliveryStateHandler
*/
$order = $this->getOrderEntity($orderId, $context);
/**
* @var OrderDeliveryEntity $orderDelivery
*/
$orderDelivery = $order->getDeliveries()?->last();
if (null === $orderDelivery) {
return;
}
if ($orderDelivery->getStateMachineState()?->getTechnicalName() !== OrderDeliveryStateHandler::STATE_HOLD){
return;
}
$orderDeliveryStateHandler = $this->container->get(OrderDeliveryStateHandler::class);
$orderDeliveryStateHandler->unhold($orderDelivery->getId(), $context);
} catch (\Exception $exception) {
$this->logger->info($exception->getMessage(), $exception->getTrace());
}
}
/**
* Unhold and cancel delivery
*
* @param string $orderId
* @param \Shopware\Core\Framework\Context $context
*/
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 {
/**
* @var OrderDeliveryStateHandler $orderDeliveryStateHandler
*/
$orderDeliveryStateHandler = $this->container->get(OrderDeliveryStateHandler::class);
/**
* @var OrderDeliveryEntity $orderDelivery
*/
$orderDelivery = $order->getDeliveries()?->last();
if (null === $orderDelivery) {
return;
}
if ($orderDelivery->getStateMachineState()?->getTechnicalName() !== OrderDeliveryStateHandler::STATE_HOLD){
return;
}
$orderDeliveryId = $orderDelivery->getId();
$orderDeliveryStateHandler->unhold($orderDeliveryId, $context);
$orderDeliveryStateHandler->cancel($orderDeliveryId, $context);
} catch (\Exception $exception) {
$this->logger->info($exception->getMessage(), $exception->getTrace());
}
}
}
@@ -0,0 +1,403 @@
<?php declare(strict_types=1);
namespace VRPaymentPayment\Core\Api\WebHooks\Service;
use Psr\Log\LoggerInterface;
use Shopware\Core\PlatformRequest;
use Symfony\Component\{
Routing\Generator\UrlGeneratorInterface,
Routing\RouterInterface,};
use VRPayment\Sdk\{
ApiClient,
Model\CreationEntityState,
Model\CriteriaOperator,
Model\EntityQuery,
Model\EntityQueryFilter,
Model\EntityQueryFilterType,
Model\RefundState,
Model\TransactionInvoiceState,
Model\TransactionState,
Model\WebhookListener,
Model\WebhookListenerCreate,
Model\WebhookUrl,
Model\WebhookUrlCreate,};
use VRPaymentPayment\Core\{
Api\WebHooks\Struct\Entity,
Settings\Service\SettingsService};
/**
* Class WebHooksService
*
* @package VRPaymentPayment\Core\Api\WebHooks\Service
*/
class WebHooksService {
public const TRANSACTION = 1472041829003;
public const TRANSACTION_INVOICE = 1472041816898;
public const REFUND = 1472041839405;
public const PAYMENT_METHOD_CONFIGURATION = 1472041857405;
/**
* @var \VRPaymentPayment\Core\Settings\Service\SettingsService
*/
protected $settingsService;
/**
* @var \Symfony\Component\Routing\RouterInterface
*/
protected $router;
/**
* @var \VRPayment\Sdk\ApiClient
*/
protected $apiClient;
/**
* Space Id
*
* @var int
*/
protected $spaceId;
/**
* WebHook configs
*/
protected $webHookEntitiesConfig = [];
/**
* WebHook configs
*/
protected $webHookEntityArrayConfig = [
/**
* Transaction WebHook Entity Id
*
* @link https://www.vr-payment.de//doc/api/webhook-entity/view/1472041829003
*/
[
'id' => WebHooksService::TRANSACTION,
'name' => 'Shopware6::WebHook::Transaction',
'states' => [
TransactionState::AUTHORIZED,
TransactionState::COMPLETED,
TransactionState::CONFIRMED,
TransactionState::DECLINE,
TransactionState::FAILED,
TransactionState::FULFILL,
TransactionState::PROCESSING,
TransactionState::VOIDED,
],
'notifyEveryChange' => false,
],
/**
* Transaction Invoice WebHook Entity Id
*
* @link https://www.vr-payment.de//doc/api/webhook-entity/view/1472041816898
*/
[
'id' => WebHooksService::TRANSACTION_INVOICE,
'name' => 'Shopware6::WebHook::Transaction Invoice',
'states' => [
TransactionInvoiceState::NOT_APPLICABLE,
TransactionInvoiceState::PAID,
TransactionInvoiceState::DERECOGNIZED,
],
'notifyEveryChange' => false,
],
/**
* Refund WebHook Entity Id
*
* @link https://www.vr-payment.de//doc/api/webhook-entity/view/1472041839405
*/
[
'id' => WebHooksService::REFUND,
'name' => 'Shopware6::WebHook::Refund',
'states' => [
RefundState::FAILED,
RefundState::SUCCESSFUL,
],
'notifyEveryChange' => false,
],
/**
* Payment Method Configuration Id
*
* @link https://www.vr-payment.de//doc/api/webhook-entity/view/1472041857405
*/
[
'id' => WebHooksService::PAYMENT_METHOD_CONFIGURATION,
'name' => 'Shopware6::WebHook::Payment Method Configuration',
'states' => [
CreationEntityState::ACTIVE,
CreationEntityState::DELETED,
CreationEntityState::DELETING,
CreationEntityState::INACTIVE
],
'notifyEveryChange' => true,
],
];
/**
* @var \Psr\Log\LoggerInterface
*/
protected $logger;
/**
* @var ?string $salesChannelId
*/
private $salesChannelId;
/**
* WebHooksService constructor.
*
* @param \VRPaymentPayment\Core\Settings\Service\SettingsService $settingsService
* @param \Symfony\Component\Routing\RouterInterface $router
*/
public function __construct(SettingsService $settingsService, RouterInterface $router)
{
$this->router = $router;
$this->settingsService = $settingsService;
$this->setWebHookEntitiesConfig();
}
/**
* Set webhook configs
*/
protected function setWebHookEntitiesConfig(): void
{
foreach ($this->webHookEntityArrayConfig as $item) {
$this->webHookEntitiesConfig[] = (new Entity())
->setId((int) $item['id'])
->setName($item['name'])
->setStates($item['states'])
->setNotifyEveryChange($item['notifyEveryChange']);
}
}
/**
* @param \Psr\Log\LoggerInterface $logger
*
* @internal
* @required
*
*/
public function setLogger(LoggerInterface $logger): void
{
$this->logger = $logger;
}
/**
* @return \VRPayment\Sdk\ApiClient
*/
public function getApiClient(): ApiClient
{
return $this->apiClient;
}
/**
* @param \VRPayment\Sdk\ApiClient $apiClient
*
* @return \VRPaymentPayment\Core\Api\WebHooks\Service\WebHooksService
*/
public function setApiClient(ApiClient $apiClient): WebHooksService
{
$this->apiClient = $apiClient;
return $this;
}
/**
* @return int
*/
public function getSpaceId(): int
{
return $this->spaceId;
}
/**
* @param int $spaceId
*
* @return \VRPaymentPayment\Core\Api\WebHooks\Service\WebHooksService
*/
public function setSpaceId(int $spaceId): WebHooksService
{
$this->spaceId = $spaceId;
return $this;
}
/**
* Install WebHooks
*
* @return array
* @throws \VRPayment\Sdk\ApiException
* @throws \VRPayment\Sdk\Http\ConnectionException
* @throws \VRPayment\Sdk\VersioningException
*/
public function install(): array
{
// Configuration
$settings = $this->settingsService->getSettings($this->getSalesChannelId());
$this->setSpaceId($settings->getSpaceId())->setApiClient($settings->getApiClient());
return $this->installListeners();
}
/**
* Get sales channel id
*
* @return string|null
*/
public function getSalesChannelId(): ?string
{
return $this->salesChannelId;
}
/**
* Set sales channel id
*
* @param string|null $salesChannelId
*
* @return \VRPaymentPayment\Core\Api\WebHooks\Service\WebHooksService
*/
public function setSalesChannelId(?string $salesChannelId = null): WebHooksService
{
$this->salesChannelId = $salesChannelId;
return $this;
}
/**
* Install Listeners
*
* @return array
*/
protected function installListeners(): array
{
$this->logger->info('Installing webhooks.');
$returnValue = [];
try {
$webHookUrlId = $this->getOrCreateWebHookUrl()->getId();
$installedWebHooks = $this->getInstalledWebHookListeners($webHookUrlId);
$webHookEntityIds = array_map(function (WebhookListener $webHook) {
return $webHook->getEntity();
}, $installedWebHooks);
/**
* @var \VRPaymentPayment\Core\Api\WebHooks\Struct\Entity $data
*/
foreach ($this->webHookEntitiesConfig as $data) {
if (in_array($data->getId(), $webHookEntityIds)) {
continue;
}
$entity = (new WebhookListenerCreate())
->setName($data->getName())
->setEntity($data->getId())
->setNotifyEveryChange($data->isNotifyEveryChange())
->setState(CreationEntityState::CREATE)
->setEntityStates($data->getStates())
->setEnablePayloadSignatureAndState( true )
->setUrl($webHookUrlId);
$returnValue[] = $this->apiClient->getWebhookListenerService()->create($this->spaceId, $entity);
}
} catch (\Exception $exception) {
$this->logger->critical($exception->getTraceAsString());
return $exception->getTrace();
}
return $returnValue;
}
/**
* Create WebHook URL
*
* @return WebhookUrl
* @throws \VRPayment\Sdk\ApiException
* @throws \VRPayment\Sdk\Http\ConnectionException
* @throws \VRPayment\Sdk\VersioningException
*/
protected function getOrCreateWebHookUrl(): WebhookUrl
{
$url = $this->getWebHookCallBackUrl();
/** @noinspection PhpParamsInspection */
$entityQueryFilter = (new EntityQueryFilter())
->setType(EntityQueryFilterType::_AND)
->setChildren([
$this->getEntityFilter('state', CreationEntityState::ACTIVE),
$this->getEntityFilter('url', $url),
]);
$query = (new EntityQuery())->setFilter($entityQueryFilter)->setNumberOfEntities(1);
$webHookUrls = $this->apiClient->getWebhookUrlService()->search($this->spaceId, $query);
if (!empty($webHookUrls[0])) {
return $webHookUrls[0];
}
/** @noinspection PhpParamsInspection */
$entity = (new WebhookUrlCreate())
->setName('Shopware6::WebHookURL')
->setUrl($url)
->setState(CreationEntityState::ACTIVE);
return $this->apiClient->getWebhookUrlService()->create($this->spaceId, $entity);
}
/**
* Creates and returns a new entity filter.
*
* @param string $fieldName
* @param $value
* @param string $operator
*
* @return \VRPayment\Sdk\Model\EntityQueryFilter
*/
protected function getEntityFilter(string $fieldName, $value, string $operator = CriteriaOperator::EQUALS): EntityQueryFilter
{
/** @noinspection PhpParamsInspection */
return (new EntityQueryFilter())
->setType(EntityQueryFilterType::LEAF)
->setOperator($operator)
->setFieldName($fieldName)
->setValue($value);
}
/**
* Get web hook callback url
*
* @return string
*/
protected function getWebHookCallBackUrl(): string
{
return $this->router->generate(
'api.action.vrpayment.webhook.update',
['salesChannelId' => $this->getSalesChannelId() ?? 'null',],
UrlGeneratorInterface::ABSOLUTE_URL
);
}
/**
* @param int $webHookUrlId
*
* @return array
* @throws \VRPayment\Sdk\ApiException
* @throws \VRPayment\Sdk\Http\ConnectionException
* @throws \VRPayment\Sdk\VersioningException
*/
protected function getInstalledWebHookListeners(int $webHookUrlId): array
{
/** @noinspection PhpParamsInspection */
$entityQueryFilter = (new EntityQueryFilter())
->setType(EntityQueryFilterType::_AND)
->setChildren([
$this->getEntityFilter('state', CreationEntityState::ACTIVE),
$this->getEntityFilter('url.id', $webHookUrlId),
]);
$query = (new EntityQuery())->setFilter($entityQueryFilter);
return $this->apiClient->getWebhookListenerService()->search($this->spaceId, $query);
}
}
@@ -0,0 +1,47 @@
<?php
namespace VRPaymentPayment\Core\Api\WebHooks\Strategy;
use Exception;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use VRPaymentPayment\Core\Api\WebHooks\Service\WebHooksService;
use VRPaymentPayment\Core\Api\WebHooks\Struct\WebHookRequest;
/**
* Handles the strategy for processing webhook requests related to manual tasks.
*
* This class extends the base webhook strategy class and is tailored specifically for handling
* webhooks that deal with manual task updates. These tasks could involve manual interventions required
* for certain operations within the system, which are triggered by external webhook events.
*
* @package VRPaymentPayment\Core\Api\WebHooks\Strategy
*/
class WebHookPaymentMethodConfigurationStrategy extends WebHookStrategyBase {
/**
* @inheritDoc
*/
public function match(string $webhookEntityId): bool {
return WebHooksService::PAYMENT_METHOD_CONFIGURATION == $webhookEntityId;
}
/**
* Processes the incoming webhook request that pertains to manual tasks.
*
* This method activates the manual task service to handle updates based on the data provided
* in the webhook request. It could involve marking tasks as completed, updating their status, or
* initiating sub-processes required as part of the task resolution.
*
* @param WebHookRequest $request The webhook request object containing all necessary data.
* @return Response The method does not return a value but updates the state of manual tasks based on the webhook data.
* @throws Exception Throws an exception if there is a failure in processing the manual task updates.
*/
public function process(WebHookRequest $request): Response {
$result = $this->paymentMethodConfigurationService
->setSalesChannelId($this->getSalesChannelId())
->synchronize($this->getContext());
return new JsonResponse(['result' => $result]);
}
}
@@ -0,0 +1,200 @@
<?php
namespace VRPaymentPayment\Core\Api\WebHooks\Strategy;
use Exception;
use Symfony\Component\HttpFoundation\{
JsonResponse,
Response,};
use Shopware\Core\{
Checkout\Cart\CartException,
Checkout\Order\Aggregate\OrderTransaction\OrderTransactionStates,
Framework\Context,
System\StateMachine\Exception\IllegalTransitionException};
use VRPayment\Sdk\{
Model\Refund,
Model\RefundState,
Model\Transaction,
Model\TransactionInvoiceState,
Model\TransactionState,
Model\TransactionInvoice,};
use VRPaymentPayment\Core\{
Api\WebHooks\Service\WebHooksService,
Api\WebHooks\Struct\WebHookRequest,
Util\Payload\TransactionPayload};
/**
* Handles the strategy for processing webhook requests related to manual tasks.
*
* This class extends the base webhook strategy class and is tailored specifically for handling
* webhooks that deal with manual task updates. These tasks could involve manual interventions required
* for certain operations within the system, which are triggered by external webhook events.
*
* @package VRPaymentPayment\Core\Api\WebHooks\Strategy
*/
class WebHookRefundStrategy extends WebHookStrategyBase implements WebhookStrategyActionsInterface {
/**
* @inheritDoc
*/
public function match(string $webhookEntityId): bool
{
return WebHooksService::REFUND == $webhookEntityId;
}
/**
* Loads the relevant entity from the API based on the webhook request.
*
* This method utilizes the TransactionService to fetch entity details (e.g., transaction data)
* based on the space and entity ID provided in the webhook request.
*
* @param WebHookRequest $request request.
* @return object|\VRPayment\Sdk\Model\Refund
* @throws \VRPayment\Sdk\ApiException ApiException.
* @throws \VRPayment\Sdk\Http\ConnectionException ConnectionException.
* @throws \VRPayment\Sdk\VersioningException VersioningException.
*/
public function getTransaction(WebHookRequest $request)
{
return $this->settings->getApiClient()
->getRefundService()
->read($request->getSpaceId(), $request->getEntityId());
}
/**
* @inheritDoc
*/
public function getOrderIdByTransaction($transaction): string
{
/** @var \VRPayment\Sdk\Model\Refund $transaction */
return $transaction->getTransaction()
->getMetaData()[TransactionPayload::VRPAYMENT_METADATA_ORDER_ID];
}
/**
* Check if the request state is applicable.
*
* This method checks if the state of the transaction from a webhook request
* is one of the predefined applicable states.
*
* @param WebHookRequest $request The webhook request containing the transaction state.
* @return bool Returns true if the state is applicable, false otherwise.
*/
public function isRequestStateApplicable(WebHookRequest $request): bool
{
$applicableStates = [
RefundState::SUCCESSFUL,
];
return in_array($request->getState(), $applicableStates);
}
/**
* Processes the incoming webhook request that pertains to manual tasks.
*
* This method activates the manual task service to handle updates based on the data provided
* in the webhook request. It could involve marking tasks as completed, updating their status, or
* initiating sub-processes required as part of the task resolution.
*
* @param WebHookRequest $request The webhook request object containing all necessary data.
* @return Response The method does not return a value but updates the state of manual tasks based on the webhook data.
* @throws Exception Throws an exception if there is a failure in processing the manual task updates.
*/
public function process(WebHookRequest $request): Response
{
return $this->updateRefund($request, $this->getContext());
}
/**
* Processes the refund callback for a VRPayment transaction, updating the associated order transaction state based on the refund status.
* This method handles different refund scenarios, including full and partial refunds, and adjusts the order transaction state accordingly.
* It ensures transactional integrity by locking the order record during updates to prevent concurrent modifications.
* Logs the outcome of the operation and any exceptions encountered.
*
* @param WebHookRequest $request The webhook request data encapsulating the refund details.
* @param Context $context Shopware execution context, providing scope for operations like database access.
*
* @return Response Returns a JSON response indicating the outcome of the refund processing.
*/
public function updateRefund(WebHookRequest $request, Context $context): Response
{
$status = Response::HTTP_UNPROCESSABLE_ENTITY;
try {
$refund = $this->getTransaction($request);
$orderId = $this->getOrderIdByTransaction($refund);
if(!empty($orderId)) {
$this->executeLocked($orderId, $context, function () use ($orderId, $refund, $context, $request) {
$this->refundService->upsert($refund, $context);
$orderTransactionId = $refund->getTransaction()->getMetaData()[TransactionPayload::VRPAYMENT_METADATA_ORDER_TRANSACTION_ID];
$orderTransaction = $this->getOrderTransaction($orderId, $context);
if (
in_array(
$orderTransaction->getStateMachineState()?->getTechnicalName(),
[
OrderTransactionStates::STATE_PAID,
OrderTransactionStates::STATE_PARTIALLY_PAID,
]
) &&
($request->getState() == RefundState::SUCCESSFUL)
) {
if ($refund->getAmount() == $orderTransaction->getAmount()->getTotalPrice()) {
$this->orderTransactionStateHandler->refund($orderTransactionId, $context);
} else {
if ($refund->getAmount() < $orderTransaction->getAmount()->getTotalPrice()) {
$this->orderTransactionStateHandler->refundPartially($orderTransactionId, $context);
}
}
} elseif ($orderTransaction->getStateMachineState()?->getTechnicalName() ===
OrderTransactionStates::STATE_PARTIALLY_REFUNDED &&
($request->getState() == RefundState::SUCCESSFUL)
) {
$transactionByOrderTransactionId = $this->transactionService->getByOrderTransactionId($orderTransactionId, $context);
$totalRefundedAmount = $this->getTotalRefundedAmount($transactionByOrderTransactionId->getTransactionId(), $context);
if (floatval($orderTransaction->getAmount()->getTotalPrice()) - $totalRefundedAmount <= 0) {
$this->orderTransactionStateHandler->refund($orderTransactionId, $context);
}
}
});
}
$status = Response::HTTP_OK;
} catch (CartException $exception) {
$status = Response::HTTP_OK;
$this->logRequest($exception, $request, 'info');
} catch (IllegalTransitionException $exception) {
$status = Response::HTTP_OK;
$this->logRequest($exception, $request, 'info');
} catch (\Exception $exception) {
$this->logRequest($exception, $request, 'critical');
}
return new JsonResponse(['data' => $request->jsonSerialize()], $status);
}
/**
* Calculates the total amount refunded for a specific transaction by summing up all refunds associated with it.
* This method queries the database for all refund records related to the transaction and aggregates their amounts.
* It ensures accurate financial calculations that are crucial for adjusting transaction states and reporting.
*
* @param int $transactionId The unique identifier of the transaction for which to calculate the total refunded amount.
* @param Context $context Shopware execution context, providing scope for operations like database access.
*
* @return float The total amount refunded for the specified transaction, converted to a float to ensure precision in calculations.
*/
private function getTotalRefundedAmount(int $transactionId, Context $context): float
{
$amount = 0;
$refunds = $this->transactionService->getRefundEntityCollectionByTransactionId($transactionId, $context);
foreach ($refunds as $refund) {
$amount += floatval($refund->getData()['amount'] ?? 0);
}
return (float) (string) $amount;
}
}
@@ -0,0 +1,466 @@
<?php
namespace VRPaymentPayment\Core\Api\WebHooks\Strategy;
use Doctrine\DBAL\Connection;
use Psr\{
Container\ContainerInterface,
Log\LoggerInterface,};
use Shopware\Core\{
Checkout\Cart\CartException,
Checkout\Order\Aggregate\OrderDelivery\OrderDeliveryEntity,
Checkout\Order\Aggregate\OrderTransaction\OrderTransactionEntity,
Checkout\Order\Aggregate\OrderTransaction\OrderTransactionStateHandler,
Checkout\Order\Aggregate\OrderTransaction\OrderTransactionStates,
Checkout\Order\OrderEntity,
Checkout\Order\SalesChannel\OrderService,
Framework\Context,
Framework\DataAbstractionLayer\Search\Criteria,
Framework\DataAbstractionLayer\Search\Sorting\FieldSorting,
System\StateMachine\Aggregation\StateMachineTransition\StateMachineTransitionActions,};
use Symfony\Component\{
HttpFoundation\ParameterBag};
use VRPayment\Sdk\{
Model\RefundState,
Model\Transaction,
Model\TransactionInvoiceState,
Model\TransactionState,
Model\TransactionInvoice,
ApiException,
Http\ConnectionException,
VersioningException,};
use VRPaymentPayment\Core\{
Api\OrderDeliveryState\Handler\OrderDeliveryStateHandler,
Api\PaymentMethodConfiguration\Service\PaymentMethodConfigurationService,
Api\Refund\Service\RefundService,
Api\Transaction\Service\OrderMailService,
Api\Transaction\Service\TransactionService,
Api\WebHooks\Struct\WebHookRequest,
Settings\Service\SettingsService,
Settings\Struct\Settings,};
/**
* Abstract class WebHookStrategyBase
*
* Serves as a base class for all webhook strategy implementations. It provides common methods needed to process webhook requests,
* such as loading entity data from the API, retrieving order details, and more.
* Note: Not all standard methods are applicable in all derived strategy classes. In some strategies, certain operations
* may intentionally be left unimplemented to reflect that they are not relevant to the specific type of webhook being handled.
*
* @package VRPaymentPayment\Core\Api\WebHooks\Strategy
*/
abstract class WebHookStrategyBase implements WebHookStrategyInterface {
/**
* @var LoggerInterface
*/
protected $logger;
/**
* @var Context
*/
private $context;
/**
* @var string
*/
private $salesChannelId;
/**
* @var OrderService
*/
protected $orderService;
/**
* @var Settings
*/
protected $settings;
/**
* @var ContainerInterface
*/
protected $container;
/**
* @var Connection
*/
protected $connection;
/**
* @var OrderMailService
*/
protected $orderMailService;
/**
* @var OrderTransactionStateHandler
*/
protected $orderTransactionStateHandler;
/**
* @var PaymentMethodConfigurationService
*/
protected $paymentMethodConfigurationService;
/**
* @var SettingsService
*/
protected $settingsService;
/**
* @var RefundService
*/
protected $refundService;
/**
* @var TransactionService
*/
protected $transactionService;
/**
* @var OrderEntity
*/
protected $orderEntity;
/**
* Transaction Final States
*
* @var array
*/
public $transactionFinalStates = [
OrderTransactionStates::STATE_CANCELLED,
OrderTransactionStates::STATE_PAID,
OrderTransactionStates::STATE_REFUNDED,
];
public $vrpaymentTransactionSuccessStates = [
TransactionState::AUTHORIZED,
TransactionState::COMPLETED,
TransactionState::FULFILL,
];
/**
* WebHookStrategyBase constructor.
*
* @param Connection $connection
* @param OrderTransactionStateHandler $orderTransactionStateHandler
* @param OrderService $orderService
* @param PaymentMethodConfigurationService $paymentMethodConfigurationService
* @param RefundService $refundService
* @param OrderMailService $orderMailService
* @param TransactionService $transactionService
* @param SettingsService $settingsService
* @param ContainerInterface $container
* @param LoggerInterface $vrpaymentPaymentLogger
* @see "$vrpaymentPaymentLogger", please read the documentation "How to Autowire Logger Channels"
*/
public function __construct(
Connection $connection,
OrderTransactionStateHandler $orderTransactionStateHandler,
OrderService $orderService,
PaymentMethodConfigurationService $paymentMethodConfigurationService,
RefundService $refundService,
OrderMailService $orderMailService,
TransactionService $transactionService,
SettingsService $settingsService,
ContainerInterface $container,
LoggerInterface $vrpaymentPaymentLogger
) {
$this->connection = $connection;
$this->orderTransactionStateHandler = $orderTransactionStateHandler;
$this->paymentMethodConfigurationService = $paymentMethodConfigurationService;
$this->refundService = $refundService;
$this->orderMailService = $orderMailService;
$this->transactionService = $transactionService;
$this->settingsService = $settingsService;
$this->orderService = $orderService;
$this->container = $container;
$this->logger = $vrpaymentPaymentLogger;
}
/**
* Sets the context for the current operation.
*
* This method assigns a new context to be used in subsequent operations within this instance.
* Passing a null value clears the current context.
*
* @param Context|null $context The new context to set, or null to clear the existing context.
* @return self Returns this instance to allow for method chaining.
*/
public function setContext(?Context $context): self
{
$this->context = $context;
return $this;
}
/**
* Get the current context.
*
* This method returns the context that has been set for this instance, which may be used in various operations.
* If no context has been set, it returns null.
*
* @return Context|null The current context if set; otherwise, null.
*/
public function getContext(): ?Context
{
return $this->context;
}
/**
* Sets the sales channel ID for this instance.
*
* This method updates the sales channel ID. This ID is used in various operations that are specific to a sales channel.
*
* @param string|null $salesChannelId The sales channel ID to be set.
* @return $this Provides a fluent interface by returning itself.
*/
public function setSalesChannelId(?string $salesChannelId): self
{
$this->salesChannelId = $salesChannelId;
return $this;
}
/**
* Retrieves the current sales channel ID.
*
* This method returns the sales channel ID that has been set for this instance. If no ID has been set, it returns null.
*
* @return string|null The current sales channel ID if set; otherwise, null.
*/
public function getSalesChannelId(): ?string
{
return $this->salesChannelId;
}
/**
* Updates the settings based on the current sales channel.
*
* This method fetches and applies the settings specific to the sales channel ID currently set for this instance.
* @return $this Provides a fluent interface by returning itself.
*/
public function setCurrentSettingsBySalesChannel(): self
{
$this->settings = $this->getSettingsBySalesChannel($this->getSalesChannelId());
return $this;
}
/**
* Get settings for a specific sales channel.
*
* This method accesses settings from the settings service using the provided sales channel ID. It returns configuration settings
* that are specific to the given sales channel.
*
* @param string|null $salesChannelId The ID of the sales channel for which settings are being requested. If null, it may default to system-wide settings or no settings.
* @return Settings The settings object containing configuration details for the specified sales channel.
*/
protected function getSettingsBySalesChannel(?string $salesChannelId): Settings
{
return $this->settingsService->getSettings($salesChannelId);
}
/**
* Get an order entity based on the order ID.
*
* This method fetches an order entity from the database using the provided order ID and context. If the order entity has not
* been fetched before, it performs a database query to retrieve it and caches it for future use.
*
* @param string $orderId The unique identifier of the order.
* @param Context $context The context of the current operation, including scope and permissions.
* @return OrderEntity The order entity associated with the provided ID.
* @throws CartException If the order cannot be found.
*/
protected function getOrderEntity(string $orderId, Context $context): OrderEntity
{
if (is_null($this->orderEntity)) {
$criteria = (new Criteria([$orderId]))
->addAssociations(['deliveries', 'transactions']);
$criteria->getAssociation('transactions')
->addSorting(new FieldSorting('createdAt', FieldSorting::ASCENDING));
try {
$this->orderEntity = $this->container
->get('order.repository')
->search($criteria, $context)
->first();
if (is_null($this->orderEntity)) {
throw CartException::orderNotFound($orderId);
}
} catch (\Exception $e) {
throw CartException::orderNotFound($orderId);
}
}
return $this->orderEntity;
}
/**
* Get the last transaction associated with an order.
*
* This method accesses the last transaction of an order based on the provided order ID and context.
*
* @param string $orderId The unique identifier of the order.
* @param Context $context The context of the current operation, including scope and permissions.
* @return OrderTransactionEntity The last transaction entity of the specified order.
*/
protected function getOrderTransaction(String $orderId, Context $context): OrderTransactionEntity
{
return $this->getOrderEntity($orderId, $context)->getTransactions()->last();
}
/**
* Unholds the delivery of an order.
*
* This method changes the state of an order's last delivery from 'held' to 'released', allowing further processing like shipping.
*
* @param string $orderId The unique identifier of the order.
* @param Context $context The context of the current operation, including scope and permissions.
*/
protected function unholdDelivery(string $orderId, Context $context): void
{
try {
$order = $this->getOrderEntity($orderId, $context);
/** @var OrderDeliveryEntity $orderDelivery */
$orderDelivery = $order->getDeliveries()?->last();
if (null === $orderDelivery) {
return;
}
if ($orderDelivery->getStateMachineState()?->getTechnicalName() !== OrderDeliveryStateHandler::STATE_HOLD){
return;
}
/** @var OrderDeliveryStateHandler $orderDeliveryStateHandler */
$orderDeliveryStateHandler = $this->container->get(OrderDeliveryStateHandler::class);
$orderDeliveryStateHandler->unhold($orderDelivery->getId(), $context);
} catch (\Exception $exception) {
$this->logger->info($exception->getMessage(), $exception->getTrace());
}
}
/**
* Releases any holds and cancels the delivery of an order. If the order's delivery is not on hold, this method does nothing.
* Any exceptions encountered during the process are logged for debugging.
*
* @param string $orderId The ID of the order to process.
* @param Context $context Shopware execution context for the current operation.
*/
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 {
$orderDeliveryStateHandler = $this->container->get(OrderDeliveryStateHandler::class);
/** @var OrderDeliveryEntity $orderDelivery */
$orderDelivery = $order->getDeliveries()?->last();
if (null === $orderDelivery) {
return;
}
if ($orderDelivery->getStateMachineState()?->getTechnicalName() !== OrderDeliveryStateHandler::STATE_HOLD){
return;
}
$orderDeliveryId = $orderDelivery->getId();
$orderDeliveryStateHandler->unhold($orderDeliveryId, $context);
$orderDeliveryStateHandler->cancel($orderDeliveryId, $context);
} catch (\Exception $exception) {
$this->logger->info($exception->getMessage(), $exception->getTrace());
}
}
/**
* Executes a locked operation on an order.
*
* This method ensures that the operation on the order is executed in a locked context, preventing other processes from interfering.
* It locks the order, performs the operation, and then commits or rolls back the transaction based on the success of the operation.
*
* @param string $orderId The unique identifier of the order.
* @param Context $context The context of the current operation, including scope and permissions.
* @param callable $operation The operation to execute on the order.
* @return mixed The result of the operation.
* @throws Exception If the operation fails.
*/
protected function executeLocked(string $orderId, Context $context, callable $operation)
{
try {
$data = [
'id' => $orderId,
'vrpayment_lock' => date('Y-m-d H:i:s'),
];
$order = $this->container->get('order.repository')->search(new Criteria([$orderId]), $context)->first();
if(empty($order)){
throw CartException::orderNotFound($orderId);
}
$this->container->get('order.repository')->upsert([$data], $context);
$result = $operation();
return $result;
} catch (\Exception $exception) {
throw $exception;
}
}
/**
* Sends an email based on the transaction state.
*
* This method checks if the transaction state matches any of the successful states and sends an email if enabled in the settings.
*
* @param Transaction $transaction The transaction object containing the state and metadata.
* @param Context $context The context of the current operation, including scope and permissions.
* @param string $orderId The unique identifier of the order associated with the transaction.
*/
protected function sendEmail(Transaction $transaction, Context $context, string $orderId): void
{
$salesChannelId = $this->getSalesChannelId();
$this->settings = $this->getSettingsBySalesChannel($salesChannelId);
if ($this->settings->isEmailEnabled()
&& in_array($transaction->getState(), $this->vrpaymentTransactionSuccessStates)) {
$this->orderMailService->send($orderId, $context);
}
}
/**
* Logs a message with dynamic retrieval of the class and method names from where it is called, and allows specifying the log level.
*
* This method captures the class and method that called it using a backtrace, which automates the process
* of logging without needing to manually specify the source of the log entry. It enhances error tracking
* and informational logging by providing precise source identification for a variety of log levels.
*
* @param \Throwable $exception The exception to log, providing the error details.
* @param WebHookRequest $request The HTTP request context, used for additional logging data.
* @param string $logLevel The level of the log entry ('info', 'critical', etc.), controlling how the log is processed.
*/
protected function logRequest(\Throwable $exception, WebHookRequest $request, string $logLevel = 'info'): void
{
$class = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]['class'];
$function = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]['function'];
$message = $class . ' : ' . $function . ' : ' . $exception->getMessage();
switch ($logLevel) {
case 'critical':
$this->logger->critical($message, $request->jsonSerialize());
break;
case 'debug':
$this->logger->debug($message, $request->jsonSerialize());
break;
case 'info':
default:
$this->logger->info($message, $request->jsonSerialize());
break;
}
}
}
@@ -0,0 +1,35 @@
<?php
namespace VRPaymentPayment\Core\Api\WebHooks\Strategy;
use Symfony\Component\HttpFoundation\Response;
use VRPaymentPayment\Core\Api\WebHooks\Struct\WebHookRequest;
/**
* Class Entity
* Defines a strategy interface for processing webhook requests.
*
* @package VRPaymentPayment\Core\Api\WebHooks\Strategy
*/
interface WebHookStrategyInterface {
/**
* Checks if the provided webhook entity ID matches the expected ID.
*
* This method is intended to verify whether the entity ID from a webhook request matches
* a specific ID configured within the WebHooksService. This can be used to validate that the
* webhook is relevant and should be processed further.
*
* @param string $webhookEntityId The entity ID from the webhook request.
* @return bool Returns true if the ID matches the system's criteria, false otherwise.
*/
public function match(string $webhookEntityId): bool;
/**
* Process the webhook request.
*
* @param WebHookRequest $request The webhook request object.
* @return Response
*/
public function process(WebHookRequest $request): Response;
}
@@ -0,0 +1,120 @@
<?php
namespace VRPaymentPayment\Core\Api\WebHooks\Strategy;
use Exception;
use Psr\Log\LoggerInterface;
use Shopware\Core\Framework\Context;
use Symfony\Component\{
HttpFoundation\JsonResponse,
HttpFoundation\Response,};
use VRPaymentPayment\Core\Api\WebHooks\Struct\WebHookRequest;
/**
* Handles the management and processing of different webhook strategies.
*
* This manager class holds references to different webhook strategies and delegates
* the processing of incoming webhook requests to the appropriate strategy based on
* the type of the webhook. Each strategy corresponds to a specific type of webhook
* and contains the logic needed to handle that specific webhook type.
*
* @package VRPaymentPayment\Core\Api\WebHooks\Strategy
*/
class WebHookStrategyManager {
/**
* @var iterable Holds instances of webhook strategies.
*/
protected $strategies;
/**
* @var LoggerInterface
*/
protected $logger;
/**
* Constructor for the webhook manager.
*
* Initializes instances of each specific webhook strategy and stores them in an associative array.
* Each key in this array corresponds to a webhook type, and each value is an instance of a strategy
* class that handles that specific type of webhook.
* @param iterable $strategies
* @param LoggerInterface $vrpaymentPaymentLogger
* @see "$vrpaymentPaymentLogger", please read the documentation "How to Autowire Logger Channels"
*/
public function __construct(
iterable $strategies,
LoggerInterface $vrpaymentPaymentLogger
) {
$this->strategies = $strategies;
$this->logger = $vrpaymentPaymentLogger;
}
/**
* Resolves the appropriate strategy for handling the given webhook request based on webhook type.
*
* This method fetches the webhook entity using the listener entity ID from the request, checks if a corresponding
* strategy exists, and returns the strategy if found.
*
* @param WebHookRequest $request The incoming webhook request.
* @param Context $context The shopware context.
* @param string $salesChannelId The sales channel ID.
* @return WebhookStrategyInterface The strategy to handle the request.
* @throws Exception If no strategy can be resolved.
*/
private function resolveStrategy(WebHookRequest $request, Context $context, ?string $salesChannelId): ?WebHookStrategyInterface
{
// Check if the strategy exists for the retrieved transaction ID.
foreach ($this->strategies as $strategy) {
/** @var WebhookStrategyInterface $strategy */
if ($strategy->match($request->getListenerEntityId())) {
$strategy
->setContext($context)
->setSalesChannelId($salesChannelId)
->setCurrentSettingsBySalesChannel();
return $strategy;
}
}
return null;
}
/**
* Processes the incoming webhook by delegating to the appropriate strategy.
*
* This method determines the type of the incoming webhook request and uses it
* to look up the corresponding strategy. If a strategy is found, it delegates the
* request processing to that strategy. If no strategy is found for the type, it
* throws an exception.
*
* @param WebHookRequest $request The incoming webhook request object.
* @param Context $context
* @param string $salesChannelId
* @return Response
* @throws Exception If no strategy is available for the webhook type provided in the request.
*/
public function process(WebHookRequest $request, Context $context, ?string $salesChannelId): Response
{
try {
$strategy = $this->resolveStrategy($request, $context, $salesChannelId);
//If there is no strategy available
if (empty($strategy)) {
$this->logger->warning("No strategy available for the transaction ID: {transactionId}", [
'transactionId' => $request->getListenerEntityId(),
]);
return new JsonResponse(['data' => $request->jsonSerialize()], Response::HTTP_OK);
}
//This reduces the number of unnecessary api calls.
if (method_exists($strategy, "isRequestStateApplicable") && !$strategy->isRequestStateApplicable($request)) {
return new JsonResponse(['data' => $request->jsonSerialize()], Response::HTTP_OK);
}
//If the request state applies for current strategy, then it will be processed.
return $strategy->process($request);
} catch ( Exception $e) {
throw $e;
}
}
}
@@ -0,0 +1,196 @@
<?php
namespace VRPaymentPayment\Core\Api\WebHooks\Strategy;
use Shopware\Core\{
Checkout\Cart\CartException,
Checkout\Order\Aggregate\OrderTransaction\OrderTransactionEntity,
Framework\Context,
System\StateMachine\Exception\IllegalTransitionException};
use Exception;
use Symfony\Component\{
HttpFoundation\JsonResponse,
HttpFoundation\Response,};
use VRPayment\Sdk\{
Model\RefundState,
Model\Transaction,
Model\TransactionInvoiceState,
Model\TransactionState,
Model\TransactionInvoice,};
use VRPaymentPayment\Core\{
Api\WebHooks\Service\WebHooksService,
Api\WebHooks\Struct\WebHookRequest,
Util\Payload\TransactionPayload};
/**
* Handles the strategy for processing webhook requests related to manual tasks.
*
* This class extends the base webhook strategy class and is tailored specifically for handling
* webhooks that deal with manual task updates. These tasks could involve manual interventions required
* for certain operations within the system, which are triggered by external webhook events.
*
* @package VRPaymentPayment\Core\Api\WebHooks\Strategy
*/
class WebHookTransactionInvoiceStrategy extends WebHookStrategyBase implements WebhookStrategyActionsInterface {
/**
* @inheritDoc
*/
public function match(string $webhookEntityId): bool {
return WebHooksService::TRANSACTION_INVOICE == $webhookEntityId;
}
/**
* Loads the relevant entity from the API based on the webhook request.
*
* This method utilizes the TransactionService to fetch entity details (e.g., transaction data)
* based on the space and entity ID provided in the webhook request.
*
* @param WebHookRequest $request request.
* @return object|\VRPayment\Sdk\Model\TransactionInvoice
* @throws \VRPayment\Sdk\ApiException ApiException.
* @throws \VRPayment\Sdk\Http\ConnectionException ConnectionException.
* @throws \VRPayment\Sdk\VersioningException VersioningException.
*/
public function getTransaction(WebHookRequest $request)
{
return $this->settings->getApiClient()
->getTransactionInvoiceService()
->read($request->getSpaceId(), $request->getEntityId());
}
/**
* @inheritDoc
*/
public function getOrderIdByTransaction($transaction): string
{
/** @var \VRPayment\Sdk\Model\TransactionInvoice $transaction */
return $transaction->getCompletion()
->getLineItemVersion()
->getTransaction()
->getMetaData()[TransactionPayload::VRPAYMENT_METADATA_ORDER_ID];
}
/**
* Check if the request state is applicable.
*
* This method checks if the state of the transaction from a webhook request
* is one of the predefined applicable states.
*
* @param WebHookRequest $request The webhook request containing the transaction state.
* @return bool Returns true if the state is applicable, false otherwise.
*/
public function isRequestStateApplicable(WebHookRequest $request): bool
{
$applicableStates = [
TransactionInvoiceState::DERECOGNIZED,
TransactionInvoiceState::NOT_APPLICABLE,
TransactionInvoiceState::PAID,
];
return in_array($request->getState(), $applicableStates);
}
/**
* Processes the incoming webhook request that pertains to manual tasks.
*
* This method activates the manual task service to handle updates based on the data provided
* in the webhook request. It could involve marking tasks as completed, updating their status, or
* initiating sub-processes required as part of the task resolution.
*
* @param WebHookRequest $request The webhook request object containing all necessary data.
* @return Response The method does not return a value but updates the state of manual tasks based on the webhook data.
* @throws Exception Throws an exception if there is a failure in processing the manual task updates.
*/
public function process(WebHookRequest $request): Response
{
return $this->updateTransactionInvoice($request, $this->getContext());
}
/**
* Processes the VRPayment TransactionInvoice webhook request by updating transaction and order states based on the invoice state.
* This method handles the entire lifecycle of the invoice processing within the system, from fetching transaction data,
* locking operations for safety, updating transaction statuses based on invoice changes, and handling order delivery states.
*
* @param WebHookRequest $request The data received from the webhook.
* @param Context $context The context within which this operation is performed, encapsulating scope-specific information like permissions and current store details.
*
* @return Response Returns a JSON response indicating the status of the operation, whether it was successful or resulted in an error.
*/
public function updateTransactionInvoice(WebHookRequest $request, Context $context): Response
{
$status = Response::HTTP_UNPROCESSABLE_ENTITY;
try {
$transactionInvoice = $this->getTransaction($request);
$orderId = $this->getOrderIdByTransaction($transactionInvoice);
if(!empty($orderId)) {
$this->executeLocked($orderId, $context, function () use ($orderId, $transactionInvoice, $context, $request) {
$orderTransactionId = $transactionInvoice->getCompletion()
->getLineItemVersion()
->getTransaction()
->getMetaData()[TransactionPayload::VRPAYMENT_METADATA_ORDER_TRANSACTION_ID];
$orderTransaction = $this->getOrderTransaction($orderId, $context);
$this->updatePriceIfAdditionalItemsExist($transactionInvoice, $orderTransaction, $context);
if (!in_array(
$orderTransaction->getStateMachineState()?->getTechnicalName(),
$this->transactionFinalStates
)) {
switch ($request->getState()) {
case TransactionInvoiceState::DERECOGNIZED:
$this->orderTransactionStateHandler->cancel($orderTransactionId, $context);
break;
case TransactionInvoiceState::NOT_APPLICABLE:
case TransactionInvoiceState::PAID:
$this->orderTransactionStateHandler->paid($orderTransactionId, $context);
$this->unholdDelivery($orderTransactionId, $context);
break;
default:
break;
}
}
});
}
$status = Response::HTTP_OK;
} catch (CartException $exception) {
$status = Response::HTTP_OK;
$this->logRequest($exception, $request, 'info');
} catch (IllegalTransitionException $exception) {
$status = Response::HTTP_OK;
$this->logRequest($exception, $request, 'info');
} catch (\Exception $exception) {
$this->logRequest($exception, $request, 'critical');
}
return new JsonResponse(['data' => $request->jsonSerialize()], $status);
}
/**
* Updates the order's total price if there are additional items added to the transaction invoice compared to the completion invoice.
* This method checks for discrepancies between the line items listed in the transaction invoice and its completion part,
* adjusting the order's total price to reflect any additional items added on the portal side.
*
* @param TransactionInvoice $transactionInvoice The transaction invoice object containing detailed line items and completion details.
* @param OrderTransactionEntity $orderTransaction The order transaction entity linked to the invoice, used for updating order details.
* @param Context $context The operational context providing settings and environment for the operation.
*/
private function updatePriceIfAdditionalItemsExist(
TransactionInvoice $transactionInvoice,
OrderTransactionEntity $orderTransaction,
Context $context
): void {
$completionLineItems = $transactionInvoice->getCompletion()->getLineItems();
$lineItems = $transactionInvoice->getLineItems();
if (count($completionLineItems) !== count($lineItems)) {
$this->transactionService->updateOrderTotalPriceByInvoiceTotal(
$orderTransaction->getOrderId(),
$transactionInvoice->getOutstandingAmount(),
$context
);
}
}
}
@@ -0,0 +1,169 @@
<?php
namespace VRPaymentPayment\Core\Api\WebHooks\Strategy;
use Symfony\Component\HttpFoundation\{
JsonResponse,
Response,};
use Shopware\Core\{
Checkout\Cart\CartException,
Framework\Context,
System\StateMachine\Exception\IllegalTransitionException};
use VRPayment\Sdk\{
Model\RefundState,
Model\Transaction,
Model\TransactionInvoiceState,
Model\TransactionState,
Model\TransactionInvoice,};
use VRPaymentPayment\Core\{
Api\WebHooks\Service\WebHooksService,
Api\WebHooks\Struct\WebHookRequest,
Util\Payload\TransactionPayload};
/**
* Class WebHookTransactionStrategy
*
* This class provides the implementation for processing transaction webhooks.
* It includes methods for handling specific actions that need to be taken when
* transaction-related webhook notifications are received, such as updating order
* statuses, recording transaction logs, or triggering further business logic.
*
* @package VRPaymentPayment\Core\Api\WebHooks\Strategy
*/
class WebHookTransactionStrategy extends WebHookStrategyBase implements WebhookStrategyActionsInterface {
/**
* @inheritDoc
*/
public function match(string $webhookEntityId): bool {
return WebHooksService::TRANSACTION == $webhookEntityId;
}
/**
* Loads the relevant entity from the API based on the webhook request.
*
* This method utilizes the TransactionService to fetch entity details (e.g., transaction data)
* based on the space and entity ID provided in the webhook request.
*
* @param WebHookRequest $request request.
* @return object|\VRPayment\Sdk\Model\Transaction
* @throws \VRPayment\Sdk\ApiException ApiException.
* @throws \VRPayment\Sdk\Http\ConnectionException ConnectionException.
* @throws \VRPayment\Sdk\VersioningException VersioningException.
*/
public function getTransaction(WebHookRequest $request) {
return $this->settings->getApiClient()
->getTransactionService()
->read($request->getSpaceId(), $request->getEntityId());
}
/**
* @inheritDoc
*/
public function getOrderIdByTransaction($transaction): string
{
/** @var \VRPayment\Sdk\Model\Transaction $transaction */
return $transaction->getMetaData()[TransactionPayload::VRPAYMENT_METADATA_ORDER_ID];
}
/**
* Check if the request state is applicable.
*
* This method checks if the state of the transaction from a webhook request
* is one of the predefined applicable states.
*
* @param WebHookRequest $request The webhook request containing the transaction state.
* @return bool Returns true if the state is applicable, false otherwise.
*/
public function isRequestStateApplicable(WebHookRequest $request): bool
{
$applicableStates = [
TransactionState::FAILED,
TransactionState::DECLINE,
TransactionState::VOIDED,
TransactionState::FULFILL,
TransactionState::AUTHORIZED,
];
return in_array($request->getState(), $applicableStates);
}
/**
* Process the webhook request.
*
* @param WebHookRequest $request The webhook request object.
* @return Response.
*/
public function process(WebHookRequest $request): Response
{
return $this->updateTransaction($request, $this->getContext());
}
/**
* Handles the processing of webhook callbacks related to VRPayment transactions.
* This method updates or handles transaction states based on the webhook data received.
*
* @param WebHookRequest $request The data received from the webhook, encapsulating the transaction details.
* @param Context $context The operational context providing settings and environment for transaction processing.
* @return Response Returns a JSON response indicating the result of the transaction update operation.
*/
private function updateTransaction(WebHookRequest $request, Context $context): Response
{
$status = Response::HTTP_UNPROCESSABLE_ENTITY;
try {
/** @var \Shopware\Core\Checkout\Order\OrderEntity $order */
$transaction = $this->getTransaction($request);
$orderId = $transaction->getMetaData()[TransactionPayload::VRPAYMENT_METADATA_ORDER_ID];
if(!empty($orderId) && !$transaction->getParent()) {
$this->executeLocked($orderId, $context, function () use ($orderId, $transaction, $context, $request) {
$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}", [
'orderId' => $orderId,
'state' => $orderTransaction->getStateMachineState()?->getTechnicalName(),
]);
if (!in_array(
$orderTransaction->getStateMachineState()?->getTechnicalName(),
$this->transactionFinalStates
)) {
switch ($request->getState()) {
case TransactionState::FAILED:
$this->orderTransactionStateHandler->fail($orderTransactionId, $context);
$this->unholdAndCancelDelivery($orderId, $context);
break;
case TransactionState::DECLINE:
case TransactionState::VOIDED:
$this->orderTransactionStateHandler->cancel($orderTransactionId, $context);
$this->unholdAndCancelDelivery($orderId, $context);
break;
case TransactionState::FULFILL:
$this->unholdDelivery($orderId, $context);
break;
case TransactionState::AUTHORIZED:
$this->orderTransactionStateHandler->process($orderTransactionId, $context);
$this->sendEmail($transaction, $context, $orderId);
break;
default:
break;
}
}
});
}
$status = Response::HTTP_OK;
} catch (CartException $exception) {
$status = Response::HTTP_OK;
$this->logRequest($exception, $request, 'info');
} catch (IllegalTransitionException $exception) {
$status = Response::HTTP_OK;
$this->logRequest($exception, $request, 'info');
} catch (\Exception $exception) {
$this->logRequest($exception, $request, 'critical');
}
return new JsonResponse(['data' => $request->jsonSerialize()], $status);
}
}
@@ -0,0 +1,61 @@
<?php
namespace VRPaymentPayment\Core\Api\WebHooks\Strategy;
use VRPaymentPayment\Core\Api\WebHooks\Struct\WebHookRequest;
use VRPayment\Sdk\{
Model\Refund,
Model\RefundState,
Model\Transaction,
Model\TransactionInvoiceState,
Model\TransactionState,
Model\TransactionInvoice,
ApiException,
Http\ConnectionException,
VersioningException,};
/**
* Class Entity
* Defines a strategy interface for processing webhook requests.
*
* @package VRPaymentPayment\Core\Api\WebHooks\Strategy
*/
interface WebhookStrategyActionsInterface {
/**
* Check if the request state is applicable.
*
* This method checks if the state of the transaction from a webhook request
* is one of the predefined applicable states.
*
* @param WebHookRequest $request The webhook request containing the transaction state.
* @return bool Returns true if the state is applicable, false otherwise.
*/
public function isRequestStateApplicable(WebHookRequest $request): bool;
/**
* Loads the relevant entity from the API based on the webhook request.
*
* This method utilizes the TransactionService to fetch entity details
* based on the space and entity ID provided in the webhook request.
*
* @param WebHookRequest $request request.
* @return object|Transaction
* @throws ApiException ApiException.
* @throws ConnectionException ConnectionException.
* @throws VersioningException VersioningException.
*/
public function getTransaction(WebHookRequest $request);
/**
* Get the order ID associated with a transaction.
*
* This method abstracts the retrieval of an order ID based on a provided metadata transaction object.
* Implementing classes must define the specific logic to extract the order ID from the transaction.
* The transaction object can be of type Transaction, TransactionInvoiceState, or Refund.
*
* @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): string;
}
+107
View File
@@ -0,0 +1,107 @@
<?php
namespace VRPaymentPayment\Core\Api\WebHooks\Struct;
use Shopware\Core\Framework\Struct\Struct;
/**
* Class Entity
*
* @package VRPaymentPayment\Core\Api\WebHooks\Struct
*/
class Entity extends Struct {
/**
* @var int
*/
protected $id;
/**
* @var string
*/
protected $name;
/**
* @var array
*/
protected $states;
/**
* @var bool
*/
protected $notifyEveryChange = false;
/**
* @return int
*/
public function getId(): int
{
return $this->id;
}
/**
* @param int $id
* @return \VRPaymentPayment\Core\Api\WebHooks\Struct\Entity
*/
public function setId(int $id): Entity
{
$this->id = $id;
return $this;
}
/**
* @return string
*/
public function getName(): string
{
return $this->name;
}
/**
* @param string $name
* @return \VRPaymentPayment\Core\Api\WebHooks\Struct\Entity
*/
public function setName(string $name): Entity
{
$this->name = $name;
return $this;
}
/**
* @return array
*/
public function getStates(): array
{
return $this->states;
}
/**
* @param array $states
* @return \VRPaymentPayment\Core\Api\WebHooks\Struct\Entity
*/
public function setStates(array $states): Entity
{
$this->states = $states;
return $this;
}
/**
* @return bool
*/
public function isNotifyEveryChange(): bool
{
return $this->notifyEveryChange;
}
/**
* @param bool $notifyEveryChange
* @return \VRPaymentPayment\Core\Api\WebHooks\Struct\Entity
*/
public function setNotifyEveryChange(bool $notifyEveryChange): Entity
{
$this->notifyEveryChange = $notifyEveryChange;
return $this;
}
}
@@ -0,0 +1,204 @@
<?php
namespace VRPaymentPayment\Core\Api\WebHooks\Struct;
use Shopware\Core\Framework\Struct\Struct;
/**
* Class WebHookRequest
*
* @package VRPaymentPayment\Core\Api\WebHooks\Struct
*/
class WebHookRequest extends Struct {
public const PAYMENT_METHOD_CONFIGURATION = 'PaymentMethodConfiguration';
public const REFUND = 'Refund';
public const TRANSACTION = 'Transaction';
public const TRANSACTION_INVOICE = 'TransactionInvoice';
/**
* @var int
*/
protected $eventId;
/**
* @var int
*/
protected $entityId;
/**
* @var int
*/
protected $listenerEntityId;
/**
* @var string
*/
protected $listenerEntityTechnicalName;
/**
* @var int
*/
protected $spaceId;
/**
* @var int
*/
protected $webhookListenerId;
/**
* @var string
*/
protected $timestamp;
/**
* Entity state.
*
* @var mixed
*/
protected $state;
/**
* @return int
*/
public function getEventId(): int
{
return $this->eventId;
}
/**
* @param int $eventId
* @return WebHookRequest
*/
public function setEventId(int $eventId): WebHookRequest
{
$this->eventId = $eventId;
return $this;
}
/**
* @return int
*/
public function getEntityId(): int
{
return $this->entityId;
}
/**
* @param int $entityId
* @return WebHookRequest
*/
public function setEntityId(int $entityId): WebHookRequest
{
$this->entityId = $entityId;
return $this;
}
/**
* @return int
*/
public function getListenerEntityId(): int
{
return $this->listenerEntityId;
}
/**
* @param int $listenerEntityId
* @return WebHookRequest
*/
public function setListenerEntityId(int $listenerEntityId): WebHookRequest
{
$this->listenerEntityId = $listenerEntityId;
return $this;
}
/**
* @return string
*/
public function getListenerEntityTechnicalName(): string
{
return $this->listenerEntityTechnicalName;
}
/**
* @param string $listenerEntityTechnicalName
* @return WebHookRequest
*/
public function setListenerEntityTechnicalName(string $listenerEntityTechnicalName): WebHookRequest
{
$this->listenerEntityTechnicalName = $listenerEntityTechnicalName;
return $this;
}
/**
* @return int
*/
public function getSpaceId(): int
{
return $this->spaceId;
}
/**
* @param int $spaceId
* @return WebHookRequest
*/
public function setSpaceId(int $spaceId): WebHookRequest
{
$this->spaceId = $spaceId;
return $this;
}
/**
* @return int
*/
public function getWebhookListenerId(): int
{
return $this->webhookListenerId;
}
/**
* @param int $webhookListenerId
* @return WebHookRequest
*/
public function setWebhookListenerId(int $webhookListenerId): WebHookRequest
{
$this->webhookListenerId = $webhookListenerId;
return $this;
}
/**
* @return string
*/
public function getTimestamp(): string
{
return $this->timestamp;
}
/**
* @param string $timestamp
* @return WebHookRequest
*/
public function setTimestamp(string $timestamp): WebHookRequest
{
$this->timestamp = $timestamp;
return $this;
}
/**
* @return string
*/
public function getState(): string
{
return $this->state;
}
/**
* @param string $state
* @return WebHookRequest
*/
public function setState(string $state): WebHookRequest
{
$this->state = $state;
return $this;
}
}
@@ -0,0 +1,149 @@
<?php declare(strict_types=1);
namespace VRPaymentPayment\Core\Checkout\PaymentHandler;
use Psr\Log\LoggerInterface;
use Shopware\Core\{
Checkout\Order\Aggregate\OrderTransaction\OrderTransactionStateHandler,
Checkout\Payment\Cart\AsyncPaymentTransactionStruct,
Checkout\Payment\Cart\PaymentHandler\AsynchronousPaymentHandlerInterface,
Checkout\Payment\Exception\AsyncPaymentFinalizeException,
Checkout\Payment\Exception\AsyncPaymentProcessException,
Checkout\Payment\Exception\CustomerCanceledAsyncPaymentException,
Framework\Validation\DataBag\RequestDataBag,
System\SalesChannel\SalesChannelContext
};
use Symfony\Component\{
HttpFoundation\RedirectResponse,
HttpFoundation\Request
};
use VRPayment\Sdk\Model\TransactionState;
use VRPaymentPayment\Core\Api\Transaction\Service\TransactionService;
/**
* Class VRPaymentPaymentHandler
*
* @package VRPaymentPayment\Core\Checkout\PaymentHandler
*/
class VRPaymentPaymentHandler implements AsynchronousPaymentHandlerInterface
{
/**
* @var \VRPaymentPayment\Core\Api\Transaction\Service\TransactionService
*/
protected $transactionService;
/**
* @var \Psr\Log\LoggerInterface
*/
private $logger;
/**
* @var \Shopware\Core\Checkout\Order\Aggregate\OrderTransaction\OrderTransactionStateHandler
*/
private $orderTransactionStateHandler;
/**
* VRPaymentPaymentHandler constructor.
*
* @param \VRPaymentPayment\Core\Api\Transaction\Service\TransactionService $transactionService
* @param \Shopware\Core\Checkout\Order\Aggregate\OrderTransaction\OrderTransactionStateHandler $orderTransactionStateHandler
*/
public function __construct(TransactionService $transactionService, OrderTransactionStateHandler $orderTransactionStateHandler)
{
$this->transactionService = $transactionService;
$this->orderTransactionStateHandler = $orderTransactionStateHandler;
}
/**
* @param \Psr\Log\LoggerInterface $logger
* @internal
* @required
*
*/
public function setLogger(LoggerInterface $logger): void
{
$this->logger = $logger;
}
/**
* The pay function will be called after the customer completed the order.
* Allows to process the order and store additional information.
*
* A redirect to the url will be performed
*
* @param \Shopware\Core\Checkout\Payment\Cart\AsyncPaymentTransactionStruct $transaction
* @param \Shopware\Core\Framework\Validation\DataBag\RequestDataBag $dataBag
* @param \Shopware\Core\System\SalesChannel\SalesChannelContext $salesChannelContext
* @return \Symfony\Component\HttpFoundation\RedirectResponse
*/
public function pay(
AsyncPaymentTransactionStruct $transaction,
RequestDataBag $dataBag,
SalesChannelContext $salesChannelContext
): RedirectResponse
{
try {
$redirectUrl = $transaction->getReturnUrl();
if ($transaction->getOrder()->getAmountTotal() > 0) {
$transactionId = $_SESSION['transactionId'] ?? null;
if ($transactionId === null) {
$this->transactionService->createPendingTransaction($salesChannelContext);
}
$redirectUrl = $this->transactionService->create($transaction, $salesChannelContext);
}
return new RedirectResponse($redirectUrl);
} catch (\Exception $e) {
unset($_SESSION['transactionId']);
$errorMessage = 'An error occurred during the communication with external payment gateway : ' . $e->getMessage();
$this->logger->critical($errorMessage);
throw new \Exception($transaction->getOrderTransaction()->getId() . ': ' . $errorMessage);
}
}
/**
* The finalize function will be called when the user is redirected back to shop from the payment gateway.
*
* Throw a @param \Shopware\Core\Checkout\Payment\Cart\AsyncPaymentTransactionStruct $transaction
* @param \Symfony\Component\HttpFoundation\Request $request
* @param \Shopware\Core\System\SalesChannel\SalesChannelContext $salesChannelContext
* @throws \VRPayment\Sdk\ApiException
* @throws \VRPayment\Sdk\Http\ConnectionException
* @throws \VRPayment\Sdk\VersioningException
* @see AsyncPaymentFinalizeException exception if an error ocurres while calling an external payment API
* Throw a @see CustomerCanceledAsyncPaymentException exception if the customer canceled the payment process on
* payment provider page
*
*/
public function finalize(
AsyncPaymentTransactionStruct $transaction,
Request $request,
SalesChannelContext $salesChannelContext
): void
{
if ($transaction->getOrder()->getAmountTotal() > 0) {
$transactionEntity = $this->transactionService->getByOrderId(
$transaction->getOrder()->getId(),
$salesChannelContext->getContext()
);
$vRPaymentTransaction = $this->transactionService->read(
$transactionEntity->getTransactionId(),
$salesChannelContext->getSalesChannel()->getId()
);
if (in_array($vRPaymentTransaction->getState(), [TransactionState::FAILED])) {
$errorMessage = strtr('Customer canceled payment for :orderId on SalesChannel :salesChannelName', [
':orderId' => $transaction->getOrder()->getId(),
':salesChannelName' => $salesChannelContext->getSalesChannel()->getName(),
]);
unset($_SESSION['transactionId']);
$this->logger->info($errorMessage);
throw new \Exception($transaction->getOrder()->getId());
}
} else {
$this->orderTransactionStateHandler->paid($transaction->getOrderTransaction()->getId(), $salesChannelContext->getContext());
}
}
}
@@ -0,0 +1,189 @@
<?php declare(strict_types=1);
namespace VRPaymentPayment\Core\Settings\Command;
use Symfony\Component\{
Console\Command\Command,
Console\Attribute\AsCommand,
Console\Input\InputInterface,
Console\Input\InputArgument,
Console\Input\InputOption,
Console\Output\OutputInterface,
PasswordHasher\Hasher\UserPasswordHasherInterface};
use Shopware\Core\Framework\{
DataAbstractionLayer\EntityRepository,
DataAbstractionLayer\EntityRepositoryInterface,
DataAbstractionLayer\Search\Criteria,
DataAbstractionLayer\Search\Filter\EqualsFilter,
Uuid\Uuid,
Context};
use VRPaymentPayment\Core\{
Settings\Options\Integration,
Settings\Service\SettingsService};
/**
* Class CreateMerchantCommand
* @internal
* @package VRPaymentPayment\Core\Settings\Command
*/
#[AsCommand(name: 'vrpayment:settings:create-merchant')]
class CreateMerchantCommand extends Command {
/**
* @var EntityRepositoryInterface Repository for user entities
*/
private $userRepository;
/**
* @var EntityRepositoryInterface Repository for user role entities
*/
private $userRoleRepository;
/**
* @var EntityRepositoryInterface Repository for locale entities
*/
private $localeRepository;
/**
* CreateMerchantUserCommand constructor.
*
* @param EntityRepositoryInterface $userRepository
* @param EntityRepositoryInterface $userRoleRepository
* @param EntityRepositoryInterface $localeRepository
*/
public function __construct(
EntityRepository $userRepository,
EntityRepository $userRoleRepository,
EntityRepository $localeRepository,
) {
parent::__construct();
$this->userRepository = $userRepository;
$this->userRoleRepository = $userRoleRepository;
$this->localeRepository = $localeRepository;
}
/**
* Executes the command to create a new merchant user with a specific role.
*
* @param InputInterface $input
* @param OutputInterface $output
* @return int Command::SUCCESS on success, Command::FAILURE on failure
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$output->writeln('Creating VRPaymentPayment merchant with custom role...');
$firstName = $input->getOption('firstName');
$lastName = $input->getOption('lastName');
$email = $input->getOption('email') ?? 'merchant@merchant.com';
$password = $input->getOption('password') ?? 'merchant123';
$context = Context::createDefaultContext();
// Check if user already exists
$criteria = new Criteria();
$criteria->addFilter(new EqualsFilter('email', $email));
$existingUser = $this->userRepository->search($criteria, $context)->first();
if ($existingUser) {
$output->writeln('User already exists.');
return Command::SUCCESS;
}
// Create role if it doesn't exist
$roleId = $this->getOrCreateRoleId('VRPayment viewer', $context);
// Create user if it doesn't exist
$this->userRepository->create([
[
'id' => Uuid::randomHex(),
'username' => $email,
'email' => $email,
'firstName' => $firstName,
'lastName' => $lastName,
'password' => $password,
'admin' => false,
'localeId' => $this->getLocaleId($context),
'aclRoles' => [
[
'id' => $roleId
]
],
]
], $context);
$output->writeln('Merchant user created successfully.');
return Command::SUCCESS;
}
/**
* Fetches the default locale ID.
*
* @param Context $context
* @return string Locale ID
* @throws \RuntimeException If the default locale is not found
*/
private function getLocaleId(Context $context): string
{
// Fetch the default locale id
$criteria = new Criteria();
$criteria->addFilter(new EqualsFilter('code', 'en-GB'));
$localeId = $this->localeRepository->searchIds($criteria, $context)->firstId();
if (!$localeId) {
throw new \RuntimeException('Default locale not found');
}
return $localeId;
}
/**
* Fetches the role ID for a given role name or creates the role if it does not exist.
*
* @param string $roleName
* @param Context $context
* @return string Role ID
* @throws \RuntimeException If the role cannot be created or found
*/
private function getOrCreateRoleId(string $roleName, Context $context): string
{
$criteria = new Criteria();
$criteria->addFilter(new EqualsFilter('name', $roleName));
$roleId = $this->userRoleRepository->searchIds($criteria, $context)->firstId();
if (!$roleId) {
$roleId = Uuid::randomHex();
$this->userRoleRepository->create([
[
'id' => $roleId,
'name' => $roleName,
'privileges' => [
'vrpayment.viewer',
'vrpayment_sales_channel:read',
'vrpayment_sales_channel_run:read',
'vrpayment_sales_channel_run_log:read',
'language:read',
'locale:read',
'system_config:read'
]
]
], $context);
}
return $roleId;
}
/**
* Configures the current command.
*/
protected function configure(): void
{
$this
->setDescription('Creates a new merchant user with specific roles.')
->addOption('firstName', null, InputOption::VALUE_OPTIONAL, 'First name of the merchant user', 'Merchant')
->addOption('lastName', null, InputOption::VALUE_OPTIONAL, 'Last name of the merchant user', 'Merchant')
->addOption('email', null, InputOption::VALUE_OPTIONAL, 'Email of the merchant user')
->addOption('password', null, InputOption::VALUE_OPTIONAL, 'Password of the merchant user');
}
}
@@ -0,0 +1,135 @@
<?php declare(strict_types=1);
namespace VRPaymentPayment\Core\Settings\Command;
use Symfony\Component\{
Console\Command\Command,
Console\Attribute\AsCommand,
Console\Input\InputInterface,
Console\Input\InputOption,
Console\Output\OutputInterface};
use VRPaymentPayment\Core\{
Settings\Options\Integration,
Settings\Service\SettingsService};
/**
* Class SettingsCommand
* @internal
* @package VRPaymentPayment\Core\Settings\Command
*/
#[AsCommand(name: 'vrpayment:settings:install')]
class SettingsCommand extends Command {
/**
* @var \VRPaymentPayment\Core\Settings\Service\SettingsService
*/
protected $settingsService;
/**
* SettingsCommand constructor.
* @param \VRPaymentPayment\Core\Settings\Service\SettingsService $settingsService
*/
public function __construct(SettingsService $settingsService)
{
parent::__construct();
$this->settingsService = $settingsService;
}
/**
* @param \Symfony\Component\Console\Input\InputInterface $input
* @param \Symfony\Component\Console\Output\OutputInterface $output
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$output->writeln('Set VRPaymentPayment settings...');
$this->settingsService->updateSettings([
SettingsService::CONFIG_APPLICATION_KEY => $input->getOption(SettingsService::CONFIG_APPLICATION_KEY),
SettingsService::CONFIG_EMAIL_ENABLED => $input->getOption(SettingsService::CONFIG_EMAIL_ENABLED),
SettingsService::CONFIG_INTEGRATION => $input->getOption(SettingsService::CONFIG_INTEGRATION),
SettingsService::CONFIG_LINE_ITEM_CONSISTENCY_ENABLED => $input->getOption(SettingsService::CONFIG_LINE_ITEM_CONSISTENCY_ENABLED),
SettingsService::CONFIG_SPACE_ID => $input->getOption(SettingsService::CONFIG_SPACE_ID),
SettingsService::CONFIG_SPACE_VIEW_ID => $input->getOption(SettingsService::CONFIG_SPACE_VIEW_ID),
SettingsService::CONFIG_STOREFRONT_INVOICE_DOWNLOAD_ENABLED => $input->getOption(SettingsService::CONFIG_STOREFRONT_INVOICE_DOWNLOAD_ENABLED),
SettingsService::CONFIG_USER_ID => $input->getOption(SettingsService::CONFIG_USER_ID),
SettingsService::CONFIG_STOREFRONT_WEBHOOKS_UPDATE_ENABLED => $input->getOption(SettingsService::CONFIG_STOREFRONT_WEBHOOKS_UPDATE_ENABLED),
SettingsService::CONFIG_STOREFRONT_PAYMENTS_UPDATE_ENABLED => $input->getOption(SettingsService::CONFIG_STOREFRONT_PAYMENTS_UPDATE_ENABLED),
]);
return Command::SUCCESS;
}
/**
* Configures the current command.
*/
protected function configure()
{
$this->setDescription('Sets VRPaymentPayment settings.')
->setHelp('This command updates VRPaymentPayment settings for all SalesChannels.')
->addOption(
SettingsService::CONFIG_APPLICATION_KEY,
SettingsService::CONFIG_APPLICATION_KEY,
InputOption::VALUE_REQUIRED,
SettingsService::CONFIG_APPLICATION_KEY
)
->addOption(
SettingsService::CONFIG_SPACE_ID,
SettingsService::CONFIG_SPACE_ID,
InputOption::VALUE_REQUIRED,
SettingsService::CONFIG_SPACE_ID
)
->addOption(
SettingsService::CONFIG_USER_ID,
SettingsService::CONFIG_USER_ID,
InputOption::VALUE_REQUIRED,
SettingsService::CONFIG_USER_ID
)
->addOption(
SettingsService::CONFIG_EMAIL_ENABLED,
SettingsService::CONFIG_EMAIL_ENABLED,
InputOption::VALUE_OPTIONAL,
SettingsService::CONFIG_EMAIL_ENABLED,
true
)
->addOption(
SettingsService::CONFIG_INTEGRATION,
SettingsService::CONFIG_INTEGRATION,
InputOption::VALUE_OPTIONAL,
SettingsService::CONFIG_INTEGRATION,
Integration::IFRAME
)
->addOption(
SettingsService::CONFIG_LINE_ITEM_CONSISTENCY_ENABLED,
SettingsService::CONFIG_LINE_ITEM_CONSISTENCY_ENABLED,
InputOption::VALUE_OPTIONAL,
SettingsService::CONFIG_LINE_ITEM_CONSISTENCY_ENABLED,
true
)
->addOption(
SettingsService::CONFIG_STOREFRONT_INVOICE_DOWNLOAD_ENABLED,
SettingsService::CONFIG_STOREFRONT_INVOICE_DOWNLOAD_ENABLED,
InputOption::VALUE_OPTIONAL,
SettingsService::CONFIG_STOREFRONT_INVOICE_DOWNLOAD_ENABLED,
true
)
->addOption(
SettingsService::CONFIG_SPACE_VIEW_ID,
SettingsService::CONFIG_SPACE_VIEW_ID,
InputOption::VALUE_OPTIONAL,
SettingsService::CONFIG_SPACE_VIEW_ID,
''
)
->addOption(
SettingsService::CONFIG_STOREFRONT_WEBHOOKS_UPDATE_ENABLED,
SettingsService::CONFIG_STOREFRONT_WEBHOOKS_UPDATE_ENABLED,
InputOption::VALUE_OPTIONAL,
SettingsService::CONFIG_STOREFRONT_WEBHOOKS_UPDATE_ENABLED,
true
) ->addOption(
SettingsService::CONFIG_STOREFRONT_PAYMENTS_UPDATE_ENABLED,
SettingsService::CONFIG_STOREFRONT_PAYMENTS_UPDATE_ENABLED,
InputOption::VALUE_OPTIONAL,
SettingsService::CONFIG_STOREFRONT_PAYMENTS_UPDATE_ENABLED,
true
);
}
}
+42
View File
@@ -0,0 +1,42 @@
<?php declare(strict_types=1);
namespace VRPaymentPayment\Core\Settings\Options;
/**
* Class Integration
*
* @package VRPaymentPayment\Core\Settings\Options
*/
class Integration {
/**
* Possible values of this enum
*/
public const CHARGE_FLOW = 'charge_flow';
public const DIRECT_CARD_PROCESSING = 'direct_card_processing';
public const IFRAME = 'iframe';
public const LIGHTBOX = 'lightbox';
public const MOBILE_WEB = 'mobile_web_view';
public const PAYMENT_LINK = 'payment_link';
public const PAYMENT_PAGE = 'payment_page';
public const TERMINAL = 'terminal';
/**
* Gets allowable values of the enum
* @return string[]
*/
public static function getAllowableEnumValues(): array
{
return [
self::CHARGE_FLOW,
self::DIRECT_CARD_PROCESSING,
self::IFRAME,
self::LIGHTBOX,
self::MOBILE_WEB,
self::PAYMENT_LINK,
self::PAYMENT_PAGE,
self::TERMINAL,
];
}
}
@@ -0,0 +1,143 @@
<?php declare(strict_types=1);
namespace VRPaymentPayment\Core\Settings\Service;
use Psr\Log\LoggerInterface;
use Shopware\Core\System\SystemConfig\SystemConfigService;
use VRPaymentPayment\Core\Settings\Struct\Settings;
/**
* Class SettingsService
*
* @package VRPaymentPayment\Core\Settings\Service
*/
class SettingsService {
/**
* Prefix to VRPayment configs
*/
public const SYSTEM_CONFIG_DOMAIN = 'VRPaymentPayment.config.';
public const CONFIG_APPLICATION_KEY = 'applicationKey';
public const CONFIG_EMAIL_ENABLED = 'emailEnabled';
public const CONFIG_INTEGRATION = 'integration';
public const CONFIG_LINE_ITEM_CONSISTENCY_ENABLED = 'lineItemConsistencyEnabled';
public const CONFIG_SPACE_ID = 'spaceId';
public const CONFIG_SPACE_VIEW_ID = 'spaceViewId';
public const CONFIG_STOREFRONT_INVOICE_DOWNLOAD_ENABLED = 'storefrontInvoiceDownloadEnabled';
public const CONFIG_USER_ID = 'userId';
public const CONFIG_STOREFRONT_WEBHOOKS_UPDATE_ENABLED = 'storefrontWebhooksUpdateEnabled';
public const CONFIG_STOREFRONT_PAYMENTS_UPDATE_ENABLED = 'storefrontPaymentsUpdateEnabled';
/**
* @var \Shopware\Core\System\SystemConfig\SystemConfigService
*/
private $systemConfigService;
/**
* @var \Psr\Log\LoggerInterface
*/
private $logger;
/**
* SettingsService constructor.
*
* @param \Shopware\Core\System\SystemConfig\SystemConfigService $systemConfigService
*/
public function __construct(SystemConfigService $systemConfigService)
{
$this->systemConfigService = $systemConfigService;
}
/**
* @param \Psr\Log\LoggerInterface $logger
* @internal
* @required
*
*/
public function setLogger(LoggerInterface $logger): void
{
$this->logger = $logger;
}
/**
* Update setting
*
* @param array $settings
* @param string|null $salesChannelId
*/
public function updateSettings(array $settings, ?string $salesChannelId = null): void
{
foreach ($settings as $key => $value) {
$this->systemConfigService->set(
self::SYSTEM_CONFIG_DOMAIN . $key,
$value,
$salesChannelId
);
}
}
/**
* Get valid settings
*
* @param string|null $salesChannelId
* @return \VRPaymentPayment\Core\Settings\Struct\Settings|null
*/
public function getValidSettings(?string $salesChannelId = null): ?Settings
{
$settings = $this->getSettings($salesChannelId);
if (empty($settings->getSpaceId())) {
$this->logger->critical('Empty spaceId setting');
return null;
}
if (empty($settings->getUserId())) {
$this->logger->critical('Empty userId setting');
return null;
}
if (empty($settings->getIntegration())) {
$this->logger->critical('Empty integration setting');
return null;
}
if (empty($settings->getApplicationKey())) {
$this->logger->critical('Empty applicationKey setting');
return null;
}
return $settings;
}
/**
* Get settings
*
* @param string|null $salesChannelId
* @return \VRPaymentPayment\Core\Settings\Struct\Settings
*/
public function getSettings(?string $salesChannelId = null): Settings
{
$values = $this->systemConfigService->getDomain(
self::SYSTEM_CONFIG_DOMAIN,
$salesChannelId,
true
);
$propertyValuePairs = [];
/** @var string $key */
foreach ($values as $key => $value) {
$property = (string) \mb_substr($key, \mb_strlen(self::SYSTEM_CONFIG_DOMAIN));
if ($property === '') {
continue;
}
if (!is_numeric($value) && empty($value)) {
$this->logger->warning(strtr('Empty value :value for settings :property.', [':property' => $property, ':value' => $value]));
}
$propertyValuePairs[$property] = $value;
}
return (new Settings())->assign($propertyValuePairs);
}
}
+271
View File
@@ -0,0 +1,271 @@
<?php declare(strict_types=1);
namespace VRPaymentPayment\Core\Settings\Struct;
use Shopware\Core\Framework\Struct\Struct;
use VRPayment\Sdk\ApiClient;
use VRPaymentPayment\Core\Util\Analytics\Analytics;
/**
* Class Settings
*
* @package VRPaymentPayment\Core\Settings\Struct
*/
class Settings extends Struct {
/**
* @var \VRPayment\Sdk\ApiClient
*/
protected $apiClient;
/**
* Application Key
*
* @var string
*/
protected $applicationKey;
/**
* Enable emails
*
* @var bool
*/
protected $emailEnabled = true;
/**
* Preferred integration
*
* @var string
*/
protected $integration;
/**
* Enforce line item consistency
*
* @var bool
*/
protected $lineItemConsistencyEnabled;
/**
* Enable storefront invoice download
*
* @var bool
*/
protected $storefrontInvoiceDownloadEnabled = true;
/**
* Space Id
*
* @var int
*/
protected $spaceId;
/**
* Space View Id
*
* @var ?int
*/
protected $spaceViewId;
/**
* Enable webhooks update
*
* @var bool
*/
protected $webhooksUpdate = true;
/**
* Enable payments update
*
* @var bool
*/
protected $paymentsUpdate = true;
/**
* User id
*
* @var int
*/
protected $userId;
/**
* @return bool
*/
public function isEmailEnabled(): bool
{
return boolval($this->emailEnabled);
}
/**
* @param bool $emailEnabled
*/
public function setEmailEnabled(bool $emailEnabled): void
{
$this->emailEnabled = $emailEnabled;
}
/**
* @return string
*/
public function getIntegration(): string
{
return strval($this->integration);
}
/**
* @param string $integration
*/
public function setIntegration(string $integration): void
{
$this->integration = $integration;
}
/**
* @return bool
*/
public function isLineItemConsistencyEnabled(): bool
{
return boolval($this->lineItemConsistencyEnabled);
}
/**
* @param bool $lineItemConsistencyEnabled
*/
public function setLineItemConsistencyEnabled(bool $lineItemConsistencyEnabled): void
{
$this->lineItemConsistencyEnabled = $lineItemConsistencyEnabled;
}
/**
* @return bool
*/
public function isStorefrontInvoiceDownloadEnabled(): bool
{
return boolval($this->storefrontInvoiceDownloadEnabled);
}
/**
* @param bool $storefrontInvoiceDownloadEnabled
*/
public function setStorefrontInvoiceDownloadEnabled(bool $storefrontInvoiceDownloadEnabled): void
{
$this->storefrontInvoiceDownloadEnabled = $storefrontInvoiceDownloadEnabled;
}
/**
* @return int
*/
public function getSpaceId(): int
{
return intval($this->spaceId);
}
/**
* @param int $spaceId
*/
public function setSpaceId(int $spaceId): void
{
$this->spaceId = $spaceId;
}
/**
* @return int|null
*/
public function getSpaceViewId(): ?int
{
if (!empty($this->spaceViewId) && is_numeric($this->spaceViewId)) {
return intval($this->spaceViewId);
}
return null;
}
/**
* @param int $spaceViewId
*/
public function setSpaceViewId(int $spaceViewId): void
{
$this->spaceViewId = $spaceViewId;
}
/**
* @return bool
*/
public function isWebhooksUpdateEnabled(): bool
{
return boolval($this->webhooksUpdate);
}
/**
* @param bool $webhooksUpdate
*/
public function setWebhooksEnabled(bool $webhooksUpdate): void
{
$this->webhooksUpdate = $webhooksUpdate;
}
/**
* @return bool
*/
public function isPaymentsUpdateEnabled(): bool
{
return boolval($this->paymentsUpdate);
}
/**
* @param bool $paymentsUpdate
*/
public function setPaymentsEnabled(bool $paymentsUpdate): void
{
$this->paymentsUpdate = $paymentsUpdate;
}
/**
* Get SDK ApiClient
*
* @return \VRPayment\Sdk\ApiClient
*/
public function getApiClient(): ApiClient
{
if (is_null($this->apiClient)) {
$this->apiClient = new ApiClient($this->getUserId(), $this->getApplicationKey());
$apiClientBasePath = getenv('VRPAYMENT_API_BASE_PATH') ? getenv('VRPAYMENT_API_BASE_PATH') : $this->apiClient->getBasePath();
$this->apiClient->setBasePath($apiClientBasePath);
Analytics::addHeaders($this->apiClient);
}
return $this->apiClient;
}
/**
* @return int
*/
public function getUserId(): int
{
return intval($this->userId);
}
/**
* @param int $userId
*/
public function setUserId(int $userId): void
{
$this->userId = $userId;
}
/**
* @return string
*/
public function getApplicationKey(): string
{
return strval($this->applicationKey);
}
/**
* @param string $applicationKey
*/
public function setApplicationKey(string $applicationKey): void
{
$this->applicationKey = $applicationKey;
}
}
@@ -0,0 +1,130 @@
<?php declare(strict_types=1);
namespace VRPaymentPayment\Core\Storefront\Account\Controller;
use Psr\Log\LoggerInterface;
use Shopware\Core\{
Checkout\Cart\Exception\CustomerNotLoggedInException,
Checkout\Customer\CustomerEntity,
PlatformRequest,
System\SalesChannel\SalesChannelContext
};
use Shopware\Storefront\Controller\StorefrontController;
use Shopware\Core\Framework\Log\Package;
use Symfony\Component\{
HttpFoundation\HeaderUtils,
HttpFoundation\RequestStack,
HttpFoundation\Response,
Routing\Attribute\Route,
Security\Core\Exception\AccessDeniedException
};
use VRPaymentPayment\Core\{
Api\Transaction\Service\TransactionService,
Settings\Service\SettingsService
};
#[Package('storefront')]
#[Route(defaults: ['_routeScope' => ['storefront']])]
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;
}
/**
* @param \Psr\Log\LoggerInterface $logger
* @internal
* @required
*
*/
public function setLogger(LoggerInterface $logger): void
{
$this->logger = $logger;
}
/**
* 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();
}
}
@@ -0,0 +1,83 @@
<?php declare(strict_types=1);
namespace VRPaymentPayment\Core\Storefront\Account\Subscriber;
use Psr\Log\LoggerInterface;
use Shopware\Core\Framework\Struct\ArrayStruct;
use Shopware\Storefront\Page\Account\Order\AccountOrderPageLoadedEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use VRPaymentPayment\Core\Settings\Service\SettingsService;
/**
* Class AccountOrderSubscriber
*
* @package VRPaymentPayment\Core\Storefront\Account\Subscriber
*/
class AccountOrderSubscriber implements EventSubscriberInterface {
/**
* @var \Psr\Log\LoggerInterface
*/
protected $logger;
/**
* @var \VRPaymentPayment\Core\Settings\Service\SettingsService
*/
private $settingsService;
/**
* CheckoutSubscriber constructor.
*
* @param \VRPaymentPayment\Core\Settings\Service\SettingsService $settingsService
*/
public function __construct(SettingsService $settingsService)
{
$this->settingsService = $settingsService;
}
/**
* @param \Psr\Log\LoggerInterface $logger
* @internal
* @required
*
*/
public function setLogger(LoggerInterface $logger): void
{
$this->logger = $logger;
}
/**
* @return array
*/
public static function getSubscribedEvents(): array
{
return [
AccountOrderPageLoadedEvent::class => ['onAccountOrderPageLoaded', 1],
];
}
/**
* Pass settings to template
*
* @param \Shopware\Storefront\Page\Account\Order\AccountOrderPageLoadedEvent $event
*/
public function onAccountOrderPageLoaded(AccountOrderPageLoadedEvent $event): void
{
$vrpaymentSettings = new ArrayStruct();
$vrpaymentSettings->set(SettingsService::CONFIG_STOREFRONT_INVOICE_DOWNLOAD_ENABLED, false);
try {
$settings = $this->settingsService->getValidSettings($event->getSalesChannelContext()->getSalesChannel()->getId());
if (is_null($settings)) {
$this->logger->notice('Disabling invoice downloads');
} else {
$vrpaymentSettings->set(SettingsService::CONFIG_STOREFRONT_INVOICE_DOWNLOAD_ENABLED, $settings->isStorefrontInvoiceDownloadEnabled());
}
} catch (\Exception $e) {
$this->logger->error($e->getMessage());
}
$event->getPage()->addExtension('vrpaymentSettings', $vrpaymentSettings);
}
}
@@ -0,0 +1,555 @@
<?php declare(strict_types=1);
namespace VRPaymentPayment\Core\Storefront\Checkout\Controller;
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,
Framework\Log\Package,
Framework\Routing\Exception\MissingRequestParameterException,
Framework\Uuid\Uuid,
Framework\Uuid\Exception\InvalidUuidException,
Framework\Validation\DataBag\RequestDataBag,
System\SalesChannel\SalesChannelContext
};
use Shopware\Storefront\{
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
};
use VRPaymentPayment\Core\{
Api\Transaction\Service\TransactionService,
Settings\Options\Integration,
Settings\Service\SettingsService,
Storefront\Checkout\Struct\CheckoutPageData,
Util\Payload\CustomProducts\CustomProductsLineItemTypes
};
/**
* Class CheckoutController
*
* @package VRPaymentPayment\Core\Storefront\Checkout\Controller
*
*/
#[Package('checkout')]
#[Route(defaults: ['_routeScope' => ['storefront']])]
class CheckoutController extends StorefrontController {
/**
* @var \Shopware\Storefront\Page\GenericPageLoader
*/
protected $genericLoader;
/**
* @var \Shopware\Core\Checkout\Cart\SalesChannel\CartService
*/
protected $cartService;
/**
* @var \VRPaymentPayment\Core\Settings\Service\SettingsService
*/
protected $settingsService;
/**
* @var \VRPaymentPayment\Core\Settings\Struct\Settings
*/
protected $settings;
/**
* @var \VRPaymentPayment\Core\Api\Transaction\Service\TransactionService
*/
protected $transactionService;
/**
* @var \Psr\Log\LoggerInterface
*/
private $logger;
/**
* @var \Shopware\Core\Checkout\Cart\LineItemFactoryRegistry
*/
private $lineItemFactoryRegistry;
/**
* @var \Shopware\Core\Checkout\Order\SalesChannel\AbstractOrderRoute
*/
private $orderRoute;
/**
* 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
*
*/
#[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');
if (empty($orderId)) {
throw new MissingRequestParameterException('orderId');
}
// Configuration
$this->settings = $this->settingsService->getSettings($salesChannelContext->getSalesChannel()->getId());
$transaction = $this->getTransaction($orderId, $salesChannelContext->getContext());
$recreateCartUrl = $this->generateUrl(
'frontend.vrpayment.checkout.recreate-cart',
['orderId' => $orderId,],
UrlGeneratorInterface::ABSOLUTE_URL
);
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);
}
}
$possiblePaymentMethods = $this->settings->getApiClient()
->getTransactionService()
->fetchPaymentMethods(
$this->settings->getSpaceId(),
$transaction->getId(),
$this->settings->getIntegration()
);
if (empty($possiblePaymentMethods)) {
$this->addFlash('danger', $this->trans('vrpayment.paymentMethod.notAvailable'));
return $this->redirect($recreateCartUrl, Response::HTTP_MOVED_PERMANENTLY);
}
$javascriptUrl = $this->getTransactionJavaScriptUrl($transaction->getId());
// 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);
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
*
*/
#[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');
if (empty($orderId)) {
throw new MissingRequestParameterException('orderId');
}
try {
$this->cartService->deleteCart($salesChannelContext);
$cart = $this->cartService->createNew($salesChannelContext->getToken());
// 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');
}
$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;
}
}
@@ -0,0 +1,200 @@
<?php declare(strict_types=1);
namespace VRPaymentPayment\Core\Storefront\Checkout\Struct;
use Shopware\Core\Framework\Struct\Struct;
/**
* Class CheckoutPageData
*
* @package VRPaymentPayment\Storefront\Checkout\Struct
*/
class CheckoutPageData extends Struct {
/**
* @var string
*/
protected $cartRecreateUrl;
/**
* @var string
*/
protected $checkoutUrl;
/**
* @var string
*/
protected $deviceJavascriptUrl;
/**
* @var string
*/
protected $integration;
/**
* @var string
*/
protected $javascriptUrl;
/**
* @var string
*/
protected $paymentMethodId;
/**
* @var array
*/
protected $possiblePaymentMethodsArray = [];
/**
* @var array
*/
protected $transactionPossiblePaymentMethods = [];
/**
* @return string
*/
public function getCartRecreateUrl(): string
{
return $this->cartRecreateUrl;
}
/**
* @param string $cartRecreateUrl
* @return CheckoutPageData
*/
public function setCartRecreateUrl(string $cartRecreateUrl): CheckoutPageData
{
$this->cartRecreateUrl = $cartRecreateUrl;
return $this;
}
/**
* @return string
*/
public function getCheckoutUrl(): string
{
return $this->checkoutUrl;
}
/**
* @param string $checkoutUrl
* @return CheckoutPageData
*/
public function setCheckoutUrl(string $checkoutUrl): CheckoutPageData
{
$this->checkoutUrl = $checkoutUrl;
return $this;
}
/**
* @return string
*/
public function getDeviceJavascriptUrl(): string
{
return $this->deviceJavascriptUrl;
}
/**
* @param int $spaceId
* @param string $sessionId
* @return \VRPaymentPayment\Core\Storefront\Checkout\Struct\CheckoutPageData
*/
public function setDeviceJavascriptUrl(int $spaceId, string $sessionId): CheckoutPageData
{
$this->deviceJavascriptUrl = strtr('https://gateway.vr-payment.de/s/{spaceId}/payment/device.js?sessionIdentifier={sessionId}', [
'{spaceId}' => $spaceId,
'{sessionId}' => $sessionId,
]);
return $this;
}
/**
* @return string
*/
public function getJavascriptUrl(): string
{
return $this->javascriptUrl;
}
/**
* JavaScript URL
*
* @param string $javascriptUrl
* @return \VRPaymentPayment\Core\Storefront\Checkout\Struct\CheckoutPageData
*/
public function setJavascriptUrl(string $javascriptUrl): CheckoutPageData
{
$this->javascriptUrl = $javascriptUrl;
return $this;
}
/**
* @return array
*/
public function getPossiblePaymentMethodsArray(): array
{
return $this->possiblePaymentMethodsArray;
}
/**
* @param array $possiblePaymentMethodsArray
* @return \VRPaymentPayment\Core\Storefront\Checkout\Struct\CheckoutPageData
*/
public function setPossiblePaymentMethodsArray(array $possiblePaymentMethodsArray): CheckoutPageData
{
$this->possiblePaymentMethodsArray = $possiblePaymentMethodsArray;
return $this;
}
/**
* @return array
*/
public function getTransactionPossiblePaymentMethods(): array
{
return $this->transactionPossiblePaymentMethods;
}
/**
* @param array $transactionPossiblePaymentMethods
* @return \VRPaymentPayment\Core\Storefront\Checkout\Struct\CheckoutPageData
*/
public function setTransactionPossiblePaymentMethods(array $transactionPossiblePaymentMethods): CheckoutPageData
{
$this->transactionPossiblePaymentMethods = $transactionPossiblePaymentMethods;
return $this;
}
/**
* @return string
*/
public function getIntegration(): string
{
return $this->integration;
}
/**
* @param string $integration
* @return \VRPaymentPayment\Core\Storefront\Checkout\Struct\CheckoutPageData
*/
public function setIntegration(string $integration): CheckoutPageData
{
$this->integration = $integration;
return $this;
}
/**
* @return string
*/
public function getPaymentMethodId(): string
{
return $this->paymentMethodId;
}
/**
* Payment method id from Shopware database
*
* @param string $paymentMethodId
* @return \VRPaymentPayment\Core\Storefront\Checkout\Struct\CheckoutPageData
*/
public function setPaymentMethodId(string $paymentMethodId): CheckoutPageData
{
$this->paymentMethodId = $paymentMethodId;
return $this;
}
}
@@ -0,0 +1,256 @@
<?php declare(strict_types=1);
namespace VRPaymentPayment\Core\Storefront\Checkout\Subscriber;
use Psr\Log\LoggerInterface;
use Shopware\Core\{Checkout\Order\Aggregate\OrderTransaction\OrderTransactionCollection,
Checkout\Order\Aggregate\OrderTransaction\OrderTransactionStates,
Checkout\Order\OrderEntity,
Content\MailTemplate\Service\Event\MailBeforeValidateEvent};
use Shopware\Core\System\SalesChannel\SalesChannelContext;
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};
/**
* Class CheckoutSubscriber
*
* @package VRPaymentPayment\Storefront\Checkout\Subscriber
*/
class CheckoutSubscriber implements EventSubscriberInterface
{
/**
* @var \Psr\Log\LoggerInterface
*/
protected $logger;
/**
* @var \VRPaymentPayment\Core\Api\PaymentMethodConfiguration\Service\PaymentMethodConfigurationService
*/
private $paymentMethodConfigurationService;
/**
* @var \VRPaymentPayment\Core\Api\Transaction\Service\TransactionService
*/
private $transactionService;
/**
* @var \VRPaymentPayment\Core\Settings\Service\SettingsService
*/
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;
$this->settingsService = $settingsService;
$this->paymentMethodUtil = $paymentMethodUtil;
}
/**
* @param \Psr\Log\LoggerInterface $logger
*
* @internal
* @required
*
*/
public function setLogger(LoggerInterface $logger): void
{
$this->logger = $logger;
}
/**
* @return array
*/
public static function getSubscribedEvents(): array
{
return [
CheckoutConfirmPageLoadedEvent::class => ['onConfirmPageLoaded', 1],
MailBeforeValidateEvent::class => ['onMailBeforeValidate', 1],
];
}
/**
* Stop order emails being sent out
*
* @param \Shopware\Core\Content\MailTemplate\Service\Event\MailBeforeValidateEvent $event
*/
public function onMailBeforeValidate(MailBeforeValidateEvent $event): void
{
$templateData = $event->getTemplateData();
/**
* @var $order \Shopware\Core\Checkout\Order\OrderEntity
*/
$order = !empty($templateData['order']) && $templateData['order'] instanceof OrderEntity ? $templateData['order'] : null;
if (!empty($order) && $order->getAmountTotal() > 0) {
$isVRPaymentEmailSettingEnabled = $this->settingsService->getSettings($order->getSalesChannelId())->isEmailEnabled();
if (!$isVRPaymentEmailSettingEnabled) { //setting is disabled
return;
}
$orderTransactions = $order->getTransactions();
if (!($orderTransactions instanceof OrderTransactionCollection)) {
return;
}
$orderTransactionLast = $orderTransactions->last();
if (empty($orderTransactionLast) || empty($orderTransactionLast->getPaymentMethod())) { // no payment method available
return;
}
$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, 'iframe');
$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;
}
$paymentMethodConfigurationId = $localPaymentMethods[$paymentMethodCollectionItem->getId()];
if (!\in_array($paymentMethodConfigurationId, $_SESSION['arrayOfPossibleMethods'])) {
$paymentMethodCollection->remove($paymentMethodCollectionItem->getId());
}
}
}
/**
* @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);
}
$_SESSION['arrayOfPossibleMethods'] = null;
$_SESSION['addressCheck'] = $addressHash;
$_SESSION['currencyCheck'] = $currency;
}
}
}
@@ -0,0 +1,54 @@
<?php declare(strict_types=1);
namespace VRPaymentPayment\Core\Storefront\Framework\Cookie;
use Shopware\Storefront\Framework\Cookie\CookieProviderInterface;
/**
* Class VRPaymentCookieProvider
*
* @package VRPaymentPayment\Core\Storefront\Framework\Cookie
*/
class VRPaymentCookieProvider implements CookieProviderInterface {
/**
* @var CookieProviderInterface
*/
private $original;
public function __construct(CookieProviderInterface $cookieProvider)
{
$this->original = $cookieProvider;
}
public function getCookieGroups(): array
{
$cookies = $this->original->getCookieGroups();
foreach ($cookies as &$cookie) {
if (!\is_array($cookie)) {
continue;
}
if (!$this->isRequiredCookieGroup($cookie)) {
continue;
}
if (!\array_key_exists('entries', $cookie)) {
continue;
}
$cookie['entries'][] = [
'snippet_name' => 'vrpayment.cookie.name',
'cookie' => 'vrpayment-cookie-key',
];
}
return $cookies;
}
private function isRequiredCookieGroup(array $cookie): bool
{
return (\array_key_exists('isRequired', $cookie) && $cookie['isRequired'] === true)
&& (\array_key_exists('snippet_name', $cookie) && $cookie['snippet_name'] === 'cookie.groupRequired');
}
}
+42
View File
@@ -0,0 +1,42 @@
<?php declare(strict_types=1);
namespace VRPaymentPayment\Core\Util\Analytics;
use VRPayment\Sdk\ApiClient;
/**
* Class Analytics
*
* @package VRPaymentPayment\Core\Util\Analytics
*/
class Analytics {
public const SHOP_SYSTEM = 'x-meta-shop-system';
public const SHOP_SYSTEM_VERSION = 'x-meta-shop-system-version';
public const SHOP_SYSTEM_AND_VERSION = 'x-meta-shop-system-and-version';
/**
* @return array
*/
public static function getDefaultData()
{
return [
self::SHOP_SYSTEM => 'shopware',
self::SHOP_SYSTEM_VERSION => '6',
self::SHOP_SYSTEM_AND_VERSION => 'shopware-6',
];
}
/**
* @param \VRPayment\Sdk\ApiClient $apiClient
*/
public static function addHeaders(ApiClient &$apiClient)
{
$data = self::getDefaultData();
foreach ($data as $key => $value) {
$apiClient->addDefaultHeader($key, $value);
}
}
}
@@ -0,0 +1,9 @@
<?php declare(strict_types=1);
namespace VRPaymentPayment\Core\Util\Exception;
class InvalidPayloadException extends \InvalidArgumentException{
}
+189
View File
@@ -0,0 +1,189 @@
<?php declare(strict_types=1);
namespace VRPaymentPayment\Core\Util;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
use Shopware\Core\{
Defaults,
Framework\Adapter\Translation\Translator,
Framework\Context,
Framework\DataAbstractionLayer\EntityRepositoryInterface,
Framework\DataAbstractionLayer\Search\Criteria,
System\Language\LanguageCollection,
System\Language\LanguageEntity};
/**
* Class LocaleCodeProvider
*
* @package VRPaymentPayment\Core\Util
*/
class LocaleCodeProvider {
public const LOCALE_GREAT_BRITAIN_ENGLISH = 'en-GB';
public const LOCALE_GERMANY_GERMAN = 'de-DE';
public const LOCALE_FRANCE_FRENCH = 'fr-FR';
public const LOCALE_ITALY_ITALIAN = 'it-IT';
/**
* @var \Psr\Log\LoggerInterface
*/
protected $logger;
/**
* @var ContainerInterface
*/
protected $container;
/**
* @var \Shopware\Core\Framework\Adapter\Translation\Translator
*/
protected $translator;
/**
* @var EntityRepositoryInterface
*/
private $languageRepository;
/**
* LocaleCodeProvider constructor.
*
* @param \Psr\Container\ContainerInterface $container
* @param \Shopware\Core\Framework\Adapter\Translation\Translator $translator
*/
public function __construct(ContainerInterface $container, Translator $translator)
{
$this->container = $container;
$this->translator = $translator;
$this->languageRepository = $this->container->get('language.repository');
}
/**
* @param \Psr\Log\LoggerInterface $logger
*
* @internal
* @required
*
*/
public function setLogger(LoggerInterface $logger): void
{
$this->logger = $logger;
}
/**
* @param \Shopware\Core\Framework\Context $context
*
* @return string
*/
public function getLocaleCodeFromContext(Context $context): string
{
$defaultLocale = self::LOCALE_GREAT_BRITAIN_ENGLISH;
$languageId = $context->getLanguageId();
/** @var \Shopware\Core\System\Language\LanguageCollection $languageCollection */
$languageCollection = $this->languageRepository->search(
(new Criteria([$languageId]))->addAssociation('locale'),
$context
)->getEntities();
$language = $languageCollection->get($languageId);
if (is_null($language)) {
return $defaultLocale;
}
return $language->getLocale() ? $language->getLocale()->getCode() : $defaultLocale;
}
/**
* @param \Shopware\Core\Framework\Context $context
*
* @return string
*/
public function getDefaultLocaleCode(Context $context): string
{
$defaultLocale = self::LOCALE_GREAT_BRITAIN_ENGLISH;
$languageId = Defaults::LANGUAGE_SYSTEM;
/** @var \Shopware\Core\System\Language\LanguageCollection $languageCollection */
$languageCollection = $this->languageRepository->search(
(new Criteria([$languageId]))->addAssociation('locale'),
$context
)->getEntities();
$language = $languageCollection->get($languageId);
if (is_null($language)) {
return $defaultLocale;
}
return $language->getLocale() ? $language->getLocale()->getCode() : $defaultLocale;
}
/**
* Get available translations
*
* @param string $snippet
* @param string $fallback
* @param \Shopware\Core\Framework\Context $context
*
* @return array
*/
public function getAvailableTranslations(string $snippet, string $fallback, Context $context): array
{
$locales = $this->getAvailableLocales($context);
$translations = [];
foreach ($locales as $locale) {
$translation = $this->translator->trans($snippet, [], null, $locale);
$pattern = '/^vrpayment\./';
// there is a bug/lack of documentation on Shopware translations, sometimes the translation does not work
if (preg_match($pattern, $translation)) { // string not translated
$translation = $this->translator->trans($snippet, [], 'storefront', $locale);
}
if (preg_match($pattern, $translation)) { // string not translated
$translation = $fallback;
}
$translations[$locale]['name'] = $translation;
}
return $translations;
}
/**
* Get all locales available
*
* @param \Shopware\Core\Framework\Context $context
*
* @return array
*/
public function getAvailableLocales(Context $context): array
{
$availableLanguages = $this->getAvailableLanguages($context);
$locales = array_map(function (LanguageEntity $language) {
return $language->getLocale()->getCode();
},
$availableLanguages->jsonSerialize()
);
$locales[] = $this->getDefaultLocaleCode($context);
$locales[] = self::LOCALE_GERMANY_GERMAN;
$locales[] = self::LOCALE_GREAT_BRITAIN_ENGLISH;
$locales[] = self::LOCALE_FRANCE_FRENCH;
$locales[] = self::LOCALE_ITALY_ITALIAN;
$locales = array_unique($locales);
return $locales;
}
/**
* Get available languages
*
* @param \Shopware\Core\Framework\Context $context
*
* @return \Shopware\Core\System\Language\LanguageCollection
*/
public function getAvailableLanguages(Context $context): LanguageCollection
{
return $this->languageRepository->search((new Criteria())->addAssociations([
'locale',
]), $context)->getEntities();
}
}
+52
View File
@@ -0,0 +1,52 @@
<?php declare(strict_types=1);
namespace VRPaymentPayment\Core\Util\Payload;
use Psr\Log\LoggerInterface;
/**
* Class AbstractPayload
*
* @package VRPaymentPayment\Core\Util\Payload
*/
abstract class AbstractPayload {
/**
* @var \Psr\Log\LoggerInterface
*/
protected $logger;
/**
* @param \Psr\Log\LoggerInterface $logger
* @internal
* @required
*
*/
public function setLogger(LoggerInterface $logger): void
{
$this->logger = $logger;
}
/**
* Fix string length string to specific length.
*
* @param string $string
* @param int $maxLength
* @return string
*/
protected function fixLength(string $string, int $maxLength): string
{
return mb_substr($string, 0, $maxLength, 'UTF-8');
}
/**
* @param $amount
* @param int $precision
*
* @return float
*/
public static function round(float $amount, int $precision = 2): float {
return \round($amount, $precision);
}
}
@@ -0,0 +1,16 @@
<?php declare(strict_types=1);
namespace VRPaymentPayment\Core\Util\Payload\CustomProducts;
class CustomProductsLineItemTypes {
public const LINE_ITEM_TYPE_PRODUCT = 'product';
public const LINE_ITEM_TYPE_CUSTOMIZED_PRODUCTS = 'customized-products';
public const LINE_ITEM_TYPE_CUSTOMIZED_PRODUCTS_OPTION = 'customized-products-option';
public const PRODUCT_OPTION_TYPE_DATETIME = 'datetime';
public const PRODUCT_OPTION_TYPE_TIMESTAMP = 'timestamp';
public const PRODUCT_OPTION_TYPE_IMAGE_UPLOAD = 'imageupload';
public const PRODUCT_OPTION_TYPE_IMAGE_SELECT = 'imageselect';
public const PRODUCT_OPTION_TYPE_FILE_UPLOAD = 'fileupload';
}
@@ -0,0 +1,131 @@
<?php declare(strict_types=1);
namespace VRPaymentPayment\Core\Util\Payload\CustomProducts;
use Shopware\Core\{
Checkout\Cart\Tax\Struct\CalculatedTaxCollection,
Checkout\Order\Aggregate\OrderLineItem\OrderLineItemEntity
};
use VRPayment\Sdk\{
Model\LineItemAttributeCreate,
Model\TaxCreate
};
use VRPaymentPayment\Core\Util\Exception\InvalidPayloadException;
trait CustomProductsLineItems {
/**
* Get Custom Product attributes
*
* @param \Shopware\Core\Checkout\Order\Aggregate\OrderLineItem\OrderLineItemEntity $shopLineItem
*
* @return array
*/
public function getCustomProductLineItemAttribute(OrderLineItemEntity $shopLineItem)
{
$customProductsOptions = $this->transaction->getOrder()
->getLineItems()
->filterByType(CustomProductsLineItemTypes::LINE_ITEM_TYPE_CUSTOMIZED_PRODUCTS_OPTION)
->filterByProperty('parentId', $shopLineItem->getId());
$productAttributes = [];
foreach ($customProductsOptions as $option) {
$label = $option->getLabel();
$value = $this->extractValueFromPayload($option);
if ($value === null) {
continue;
}
$lineItemAttributeCreate = (new LineItemAttributeCreate())
->setLabel($this->fixLength($label, 512))
->setValue($this->fixLength($value, 512));
if ($lineItemAttributeCreate->valid()) {
$key = $this->fixLength('option_' . md5($label), 40);
$productAttributes[$key] = $lineItemAttributeCreate;
} else {
$this->logger->critical('LineItemAttributeCreate payload invalid:', $lineItemAttributeCreate->listInvalidProperties());
throw new InvalidPayloadException('LineItemAttributeCreate payload invalid:' . json_encode($lineItemAttributeCreate->listInvalidProperties()));
}
}
return $productAttributes;
}
/**
* @param \Shopware\Core\Checkout\Cart\Tax\Struct\CalculatedTaxCollection $calculatedTaxes
* @param string $title
* @param $amount
*
* @return array
*/
public function getCustomProductTaxes(CalculatedTaxCollection $calculatedTaxes, string $title, $amount)
{
$taxes = [];
$sumOfTaxes = $this->getSumOfTaxes($calculatedTaxes);
foreach ($calculatedTaxes as $calculatedTax) {
$taxRate = ($calculatedTax->getTax() * 100) / ($amount - $sumOfTaxes);
$taxRate = (float) number_format($taxRate, 8, '.', '');
$tax = (new TaxCreate())
->setRate($taxRate)
->setTitle($this->fixLength($title . ' : ' . $calculatedTax->getTaxRate(), 40));
if (!$tax->valid()) {
$this->logger->critical('Tax payload invalid:', $tax->listInvalidProperties());
throw new InvalidPayloadException('Tax payload invalid:' . json_encode($tax->listInvalidProperties()));
}
$taxes [] = $tax;
}
return $taxes;
}
/**
* Extract Custom Product Attribute value
*
* @param \Shopware\Core\Checkout\Order\Aggregate\OrderLineItem\OrderLineItemEntity $option
*
* @return string|null
*/
protected function extractValueFromPayload(OrderLineItemEntity $option): ?string
{
$payload = $option->getPayload() ?? [];
$type = $payload['type'] ?? null;
$value = $payload['value'] ?? 'on';
if (!$type) {
return null;
}
if ($type === CustomProductsLineItemTypes::PRODUCT_OPTION_TYPE_DATETIME) {
return $value ? \date('d.m.Y', \strtotime($value)) : null;
}
if ($type === CustomProductsLineItemTypes::PRODUCT_OPTION_TYPE_TIMESTAMP) {
return $value ? \date('H:i', \strtotime($value)) : null;
}
if ($type === CustomProductsLineItemTypes::PRODUCT_OPTION_TYPE_IMAGE_UPLOAD || $type === CustomProductsLineItemTypes::PRODUCT_OPTION_TYPE_FILE_UPLOAD) {
return \implode(', ', \array_column($option->getPayload()['media'] ?? [], 'filename'));
}
return $value;
}
/**
* @param CalculatedTaxCollection $calculatedTaxes
* @return float
*/
private function getSumOfTaxes(CalculatedTaxCollection $calculatedTaxes): float
{
$sumOfTaxes = 0;
foreach ($calculatedTaxes as $calculatedTax) {
$sumOfTaxes += $calculatedTax->getTax();
}
return $sumOfTaxes;
}
}
+171
View File
@@ -0,0 +1,171 @@
<?php declare(strict_types=1);
namespace VRPaymentPayment\Core\Util\Payload;
use VRPayment\Sdk\{
Model\LineItem,
Model\RefundCreate,
Model\RefundType,
Model\Transaction,
Model\TransactionState
};
use VRPaymentPayment\Core\Util\Exception\InvalidPayloadException;
/**
* Class RefundPayload
*
* @package VRPaymentPayment\Core\Util\Payload
*/
class RefundPayload extends AbstractPayload
{
/**
* @param \VRPayment\Sdk\Model\Transaction $transaction
* @param string $lineItemId
* @param int $quantity
* @return \VRPayment\Sdk\Model\RefundCreate|null
* @throws \Exception
*/
public function get(Transaction $transaction, string $lineItemId, int $quantity): ?RefundCreate
{
$lineItem = $this->findLineItemByUniqueId($transaction['line_items'], $lineItemId);
if ($lineItem === null) {
$errorMessage = sprintf('Line item doesn\'t exist: %s', $lineItemId);
$this->logger->critical($errorMessage);
throw new InvalidPayloadException($errorMessage);
}
$price = 0;
// If refund the whole line item
if ($quantity === 0) {
$quantity = $lineItem['quantity'];
$price = $lineItem['unit_price_including_tax'];
}
$amount = floatval($quantity * $lineItem['unit_price_including_tax']);
if (
($transaction->getState() == TransactionState::FULFILL) &&
($amount <= floatval($transaction->getAuthorizationAmount()))
) {
$reduction = new \VRPayment\Sdk\Model\LineItemReductionCreate();
$reduction->setLineItemUniqueId($lineItem['unique_id']);
$reduction->setQuantityReduction($quantity);
$reduction->setUnitPriceReduction($price);
$refund = (new RefundCreate())
->setReductions([$reduction])
->setTransaction($transaction->getId())
->setMerchantReference($this->fixLength($transaction->getMerchantReference(), 100))
->setExternalId($this->fixLength(uniqid('refund_', true), 100))
/** @noinspection PhpParamsInspection */
->setType(RefundType::MERCHANT_INITIATED_ONLINE);
if (!$refund->valid()) {
$this->logger->critical('Refund payload invalid:', $refund->listInvalidProperties());
throw new InvalidPayloadException('Refund payload invalid:' . json_encode($refund->listInvalidProperties()));
}
return $refund;
}
return null;
}
/**
* @param \VRPayment\Sdk\Model\Transaction $transaction
* @param float $amount
* @return \VRPayment\Sdk\Model\RefundCreate|null
* @throws \Exception
*/
public function getByAmount(Transaction $transaction, float $amount): ?RefundCreate
{
if (
($transaction->getState() == TransactionState::FULFILL) &&
($amount <= floatval($transaction->getAuthorizationAmount()))
) {
$refund = (new RefundCreate())
->setAmount(self::round($amount))
->setTransaction($transaction->getId())
->setMerchantReference($this->fixLength($transaction->getMerchantReference(), 100))
->setExternalId($this->fixLength(uniqid('refund_', true), 100))
->setType(RefundType::MERCHANT_INITIATED_ONLINE);
if (!$refund->valid()) {
$this->logger->critical('Refund payload invalid:', $refund->listInvalidProperties());
throw new InvalidPayloadException('Refund payload invalid:' . json_encode($refund->listInvalidProperties()));
}
return $refund;
}
return null;
}
/**
* @param \VRPayment\Sdk\Model\Transaction $transaction
* @param string $lineItemId
* @param int $quantity
* @return \VRPayment\Sdk\Model\RefundCreate|null
* @throws \Exception
*/
public function getForPartial(Transaction $transaction, string $lineItemId, float $amount): ?RefundCreate
{
$lineItem = $this->findLineItemByUniqueId($transaction['line_items'], $lineItemId);
if ($lineItem === null) {
$errorMessage = sprintf('Line item doesn\'t exist: %s', $lineItemId);
$this->logger->critical($errorMessage);
throw new InvalidPayloadException($errorMessage);
}
$unitPrice = floatval($lineItem['unit_price_including_tax']);
$quantityAvailable = intval($lineItem['quantity']);
$totalItemAmount = $unitPrice * $quantityAvailable;
if (
($transaction->getState() == TransactionState::FULFILL) &&
($amount <= $totalItemAmount)
) {
$reduction = new \VRPayment\Sdk\Model\LineItemReductionCreate();
$reduction->setLineItemUniqueId($lineItemId);
$reduction->setQuantityReduction(0);
$reduction->setUnitPriceReduction($amount / $quantityAvailable);
$refund = (new RefundCreate())
->setReductions([$reduction])
->setTransaction($transaction->getId())
->setMerchantReference($this->fixLength($transaction->getMerchantReference(), 100))
->setExternalId($this->fixLength(uniqid('refund_', true), 100))
/** @noinspection PhpParamsInspection */
->setType(RefundType::MERCHANT_INITIATED_ONLINE);
if (!$refund->valid()) {
$this->logger->critical('Refund payload invalid:', $refund->listInvalidProperties());
throw new InvalidPayloadException('Refund payload invalid:' . json_encode($refund->listInvalidProperties()));
}
return $refund;
}
return null;
}
/**
* @param array $lineItems
* @param string $uniqueId
* @return LineItem|null
*/
private function findLineItemByUniqueId(array $lineItems, string $uniqueId): ?LineItem
{
$lineItems = \array_values(
\array_filter($lineItems, function ($item) use ($uniqueId) {
return $item['unique_id'] === $uniqueId;
})
);
return $lineItems[0] ?? null;
}
}
@@ -0,0 +1,809 @@
<?php declare(strict_types=1);
namespace VRPaymentPayment\Core\Util\Payload;
use Psr\Container\ContainerInterface;
use Shopware\Core\{Checkout\Cart\Tax\Struct\CalculatedTaxCollection,
Checkout\Customer\Aggregate\CustomerAddress\CustomerAddressEntity,
Checkout\Customer\CustomerEntity,
Checkout\Order\Aggregate\OrderLineItem\OrderLineItemEntity,
Checkout\Payment\Cart\AsyncPaymentTransactionStruct,
Framework\DataAbstractionLayer\Search\Criteria,
System\SalesChannel\SalesChannelContext
};
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use VRPayment\Sdk\{Model\AddressCreate,
Model\ChargeAttempt,
Model\CreationEntityState,
Model\CriteriaOperator,
Model\EntityQuery,
Model\EntityQueryFilter,
Model\EntityQueryFilterType,
Model\LineItemAttributeCreate,
Model\LineItemCreate,
Model\LineItemType,
Model\TaxCreate,
Model\TransactionCreate,
Model\TransactionPending
};
use VRPaymentPayment\Core\{Api\PaymentMethodConfiguration\Entity\PaymentMethodConfigurationEntity,
Settings\Struct\Settings,
Util\Exception\InvalidPayloadException,
Util\LocaleCodeProvider,
Util\Payload\CustomProducts\CustomProductsLineItems,
Util\Payload\CustomProducts\CustomProductsLineItemTypes
};
/**
* Class TransactionPayload
*
* @package VRPaymentPayment\Core\Util\Payload
*/
class TransactionPayload extends AbstractPayload
{
use CustomProductsLineItems;
public const ORDER_TRANSACTION_CUSTOM_FIELDS_VRPAYMENT_SPACE_ID = 'vrpayment_space_id';
public const ORDER_TRANSACTION_CUSTOM_FIELDS_VRPAYMENT_TRANSACTION_ID = 'vrpayment_transaction_id';
public const VRPAYMENT_METADATA_SALES_CHANNEL_ID = 'salesChannelId';
public const VRPAYMENT_METADATA_ORDER_ID = 'orderId';
public const VRPAYMENT_METADATA_ORDER_TRANSACTION_ID = 'orderTransactionId';
public const VRPAYMENT_METADATA_CUSTOMER_NAME = 'customerName';
/**
* @var \Shopware\Core\System\SalesChannel\SalesChannelContext
*/
protected $salesChannelContext;
/**
* @var \Shopware\Core\Checkout\Payment\Cart\AsyncPaymentTransactionStruct
*/
protected $transaction;
/**
* @var \VRPaymentPayment\Core\Settings\Struct\Settings
*/
protected $settings;
/**
* @var \Psr\Container\ContainerInterface
*/
protected $container;
/**
* @var \VRPaymentPayment\Core\Util\LocaleCodeProvider
*/
private $localeCodeProvider;
/**
* @var TranslatorInterface
*/
protected $translator;
/**
* TransactionPayload constructor.
*
* @param \Psr\Container\ContainerInterface $container
* @param \VRPaymentPayment\Core\Util\LocaleCodeProvider $localeCodeProvider
* @param \Shopware\Core\System\SalesChannel\SalesChannelContext $salesChannelContext
* @param \VRPaymentPayment\Core\Settings\Struct\Settings $settings
* @param \Shopware\Core\Checkout\Payment\Cart\AsyncPaymentTransactionStruct $transaction
*/
public function __construct(
ContainerInterface $container,
LocaleCodeProvider $localeCodeProvider,
SalesChannelContext $salesChannelContext,
Settings $settings,
AsyncPaymentTransactionStruct $transaction
)
{
$this->localeCodeProvider = $localeCodeProvider;
$this->salesChannelContext = $salesChannelContext;
$this->settings = $settings;
$this->transaction = $transaction;
$this->container = $container;
$this->translator = $this->container->get('translator');
}
/**
* Get Transaction Payload
*
* @return \VRPayment\Sdk\Model\TransactionPending
* @throws \Exception
*/
public function get(int $version): TransactionPending
{
$customer = $this->salesChannelContext->getCustomer();
$lineItems = $this->getLineItems();
$billingAddress = $this->getAddressPayload($customer, $customer->getActiveBillingAddress());
$shippingAddress = $this->getAddressPayload($customer, $customer->getActiveShippingAddress(), false);
$customerId = null;
$customerName = null;
if ($customer->getGuest() === false) {
$customerId = $customer->getCustomerNumber();
$customerName = '';
if ($customer->getGuest() === false) {
$customerId = $customer->getCustomerNumber();
$customerName = $customer->getSalutation()->getDisplayName() . ' ' . $customer->getFirstName() . ' ' . $customer->getLastName();
}
}
$transactionData = [
'currency' => $this->salesChannelContext->getCurrency()->getIsoCode(),
'customer_email_address' => $billingAddress->getEmailAddress(),
'customer_id' => $customerId,
'language' => $this->localeCodeProvider->getLocaleCodeFromContext($this->salesChannelContext->getContext()) ?? null,
'merchant_reference' => $this->fixLength($this->transaction->getOrder()->getOrderNumber(), 100),
'meta_data' => [
self::VRPAYMENT_METADATA_ORDER_ID => $this->transaction->getOrder()->getId(),
self::VRPAYMENT_METADATA_ORDER_TRANSACTION_ID => $this->transaction->getOrderTransaction()->getId(),
self::VRPAYMENT_METADATA_SALES_CHANNEL_ID => $this->salesChannelContext->getSalesChannel()->getId(),
self::VRPAYMENT_METADATA_CUSTOMER_NAME => $customerName,
],
'shipping_method' => $this->salesChannelContext->getShippingMethod()->getName() ? $this->fixLength($this->salesChannelContext->getShippingMethod()->getName(), 200) : null,
'space_view_id' => $this->settings->getSpaceViewId() ?? null,
];
// we have to manually check for these additional fields as they might not be active
if (!empty($additionalAddress1 = $customer->getDefaultBillingAddress()->getAdditionalAddressLine1())) {
$transactionData['meta_data']['additionalAddress1'] = $additionalAddress1;
}
if (!empty($additionalAddress2 = $customer->getDefaultBillingAddress()->getAdditionalAddressLine2())) {
$transactionData['meta_data']['additionalAddress2'] = $additionalAddress2;
}
if (!empty($this->transaction->getOrder()->getCustomerComment())) {
$transactionData['meta_data']['customer_comment'] = $this->transaction->getOrder()->getCustomerComment();
}
$vatIds = $customer->getVatIds();
if (!empty($vatIds)) {
$taxNumber = $vatIds[0];
$transactionData['meta_data']['taxNumber'] = $taxNumber;
}
if (!empty($companyDepartment = $customer->getDefaultBillingAddress()->getDepartment())) {
$transactionData['meta_data']['billingCompanyDepartment'] = $companyDepartment;
}
if (!empty($companyDepartment = $customer->getDefaultShippingAddress()->getDepartment())) {
$transactionData['meta_data']['shippingCompanyDepartment'] = $companyDepartment;
}
$transactionPayload = (new TransactionPending())
->setId($_SESSION['transactionId'])
->setVersion($version)
->setBillingAddress($billingAddress)
->setCurrency($transactionData['currency'])
->setCustomerEmailAddress($transactionData['customer_email_address'])
->setCustomerId($transactionData['customer_id'])
->setLanguage($transactionData['language'])
->setLineItems($lineItems)
->setMerchantReference($transactionData['merchant_reference'])
->setMetaData($transactionData['meta_data'])
->setShippingAddress($shippingAddress)
->setShippingMethod($transactionData['shipping_method']);
$paymentConfiguration = $this->getPaymentConfiguration($this->salesChannelContext->getPaymentMethod()->getId());
$transactionPayload->setAllowedPaymentMethodConfigurations([$paymentConfiguration->getPaymentMethodConfigurationId()]);
$successUrl = $this->transaction->getReturnUrl() . '&status=paid';
$failedUrl = $this->getFailUrl($this->transaction->getOrder()->getId()) . '&status=fail';
$transactionPayload->setSuccessUrl($successUrl)
->setFailedUrl($failedUrl);
if (!$transactionPayload->valid()) {
$this->logger->critical('Transaction payload invalid:', $transactionPayload->listInvalidProperties());
throw new InvalidPayloadException('Transaction payload invalid:' . json_encode($transactionPayload->listInvalidProperties()));
}
return $transactionPayload;
}
/**
* Get transaction line items
*
* @return \VRPayment\Sdk\Model\LineItemCreate[]
* @throws \Exception
*/
protected function getLineItems(): array
{
$lineItems = [];
$items = $this->transaction->getOrder()->getLineItems();
foreach ($items as $shopLineItem) {
if ($this->shouldSkipLineItem($shopLineItem)) {
continue;
}
if ($this->isCustomProductOption($shopLineItem)) {
$shopLineItem = $this->updateCustomProductOptionLabel($shopLineItem);
}
$lineItem = $this->createLineItem($shopLineItem);
$this->validateLineItem($lineItem);
$lineItems[] = $lineItem;
}
$this->processDiscounts($items, $lineItems);
$this->sortLineItemsByName($lineItems);
$this->addOptionalLineItems($lineItems);
return $lineItems;
}
/**
* Determine if a line item should be skipped.
*/
protected function shouldSkipLineItem($shopLineItem): bool
{
return in_array($shopLineItem->getType(), [
CustomProductsLineItemTypes::LINE_ITEM_TYPE_CUSTOMIZED_PRODUCTS,
'promotion'
]);
}
/**
* Check if the line item is a custom product option.
*/
protected function isCustomProductOption($shopLineItem): bool
{
return $shopLineItem->getType() === CustomProductsLineItemTypes::LINE_ITEM_TYPE_CUSTOMIZED_PRODUCTS_OPTION;
}
/**
* Update the label of a custom product option.
*/
protected function updateCustomProductOptionLabel($shopLineItem)
{
$customProductOptionParentLabel = $this->getCustomProductOptionLabel($shopLineItem->getParentId());
$shopLineItem->setLabel($customProductOptionParentLabel . ': ' . $shopLineItem->getLabel());
return $shopLineItem;
}
/**
* Validate the created line item.
*/
protected function validateLineItem($lineItem): void
{
if (!$lineItem->valid()) {
$this->logger->critical('LineItem payload invalid:', $lineItem->listInvalidProperties());
throw new InvalidPayloadException('LineItem payload invalid: ' . json_encode($lineItem->listInvalidProperties()));
}
}
/**
* Process discounts from the order items and add them to the line items array.
*/
protected function processDiscounts($items, array &$lineItems): void
{
$itemsArray = is_array($items) ? $items : iterator_to_array($items);
$discounts = array_filter($itemsArray, function ($orderItem) {
return $orderItem->getType() === 'promotion';
});
if ($discounts) {
$this->addDiscountLineItem(current($discounts), $lineItems);
}
}
/**
* Add discount line item.
*/
protected function addDiscountLineItem($discount, array &$lineItems): void
{
$calculatedPrice = $discount->getPrice();
$calculatedTaxesCollection = $calculatedPrice->getCalculatedTaxes();
foreach ($calculatedTaxesCollection as $calculatedTax) {
$rate = $calculatedTax->getTaxRate();
$lineItem = new LineItemCreate();
$amount = $this->calculateDiscountAmount($calculatedTax);
$lineItem->setAmountIncludingTax($amount)
->setName(sprintf('DISCOUNT: %s (%s%% tax)', $discount->getLabel(), $rate))
->setQuantity(1)
->setShippingRequired(false)
->setSku('sku-discount-' . $rate, 200)
->setType(LineItemType::DISCOUNT)
->setUniqueId('coupon-sku-discount-' . $rate . '-' . $rate);
$taxRate = new TaxCreate(['title' => 'Discount Tax: ' . $rate, 'rate' => $rate]);
$lineItem->setTaxes([$taxRate]);
$lineItems[] = $lineItem;
}
}
/**
* Calculate discount amount including tax if necessary.
*/
protected function calculateDiscountAmount($calculatedTax): float
{
$amount = self::round($calculatedTax->getPrice());
if ($this->transaction->getOrder()->getTaxStatus() === 'net') {
$amount = self::round($amount + $calculatedTax->getTax());
}
return $amount;
}
/**
* Sort line items by name.
*/
protected function sortLineItemsByName(array &$lineItems): void
{
usort($lineItems, function ($lineItem1, $lineItem2) {
return strcmp($lineItem1->getName(), $lineItem2->getName());
});
}
/**
* Add optional shipping and adjustment line items.
*/
protected function addOptionalLineItems(array &$lineItems): void
{
if (count($this->transaction->getOrder()->getShippingCosts()->getCalculatedTaxes()) === 1) {
if ($shippingLineItem = $this->getShippingLineItem()) {
$lineItems[] = $shippingLineItem;
}
} else {
if ($multipleShippingLineItems = $this->getMultipleShippingLineItems()) {
$lineItems = array_merge($lineItems, $multipleShippingLineItems);
}
}
if ($adjustmentLineItem = $this->getAdjustmentLineItem($lineItems)) {
$lineItems[] = $adjustmentLineItem;
}
}
/**
* @param string $lineItemParentId
* @return string
*/
protected function getCustomProductOptionLabel(string $lineItemParentId): string
{
$label = '';
foreach ($this->transaction->getOrder()->getLineItems() as $shopLineItem) {
if ($shopLineItem->getParentId() === $lineItemParentId && $shopLineItem->getType() === CustomProductsLineItemTypes::LINE_ITEM_TYPE_PRODUCT) {
$label = $shopLineItem->getLabel();
break;
}
}
return $label;
}
/**
*
* @return \VRPayment\Sdk\Model\LineItemCreate|null
* @throws \Exception
* @var \Shopware\Core\Checkout\Order\Aggregate\OrderLineItem\OrderLineItemEntity $shopLineItem
*/
protected function createLineItem(OrderLineItemEntity $shopLineItem): ?LineItemCreate
{
$uniqueId = $shopLineItem->getId();
$sku = $shopLineItem->getProductId() ? $shopLineItem->getProductId() : $uniqueId;
$payLoad = $shopLineItem->getPayload();
if (!empty($payLoad) && !empty($payLoad['productNumber'])) {
$sku = $payLoad['productNumber'];
}
$sku = $this->fixLength($sku, 200);
$amount = $shopLineItem->getTotalPrice() ? self::round($shopLineItem->getTotalPrice()) : 0;
//include Tax Excluded for Net Tax display customer group
if ($this->transaction->getOrder()->getTaxStatus() === 'net') {
$amount = self::round($amount + $shopLineItem->getPrice()->getCalculatedTaxes()->getAmount());
}
$lineItem = (new LineItemCreate())
->setName($this->fixLength($shopLineItem->getLabel(), 150))
->setUniqueId($uniqueId)
->setSku($sku)
->setQuantity($shopLineItem->getQuantity() ?? 1)
->setAmountIncludingTax($amount);
if (!empty($shopLineItem->getType()) && $shopLineItem->getType() == CustomProductsLineItemTypes::LINE_ITEM_TYPE_CUSTOMIZED_PRODUCTS) {
$productAttributes = $this->getCustomProductLineItemAttribute($shopLineItem);
$taxes = $this->getCustomProductTaxes(
$shopLineItem->getPrice()->getCalculatedTaxes(),
$this->translator->trans('vrpayment.payload.taxes'),
$amount
);
} else {
$productAttributes = $this->getProductAttributes($shopLineItem);
$taxes = $this->getTaxes(
$shopLineItem->getPrice()->getCalculatedTaxes(),
$this->translator->trans('vrpayment.payload.taxes')
);
}
if (!empty($productAttributes)) {
$lineItem->setAttributes($productAttributes);
}
if (!empty($taxes)) {
$lineItem->setTaxes($taxes);
}
if ($shopLineItem->getTotalPrice() >= 0) {
$lineItem->setType(LineItemType::PRODUCT);
} else {
$lineItem->setType(LineItemType::DISCOUNT);
}
return $lineItem;
}
/**
* @param \Shopware\Core\Checkout\Cart\Tax\Struct\CalculatedTaxCollection $calculatedTaxes
* @param string $title
*
* @return array
*/
protected function getTaxes(CalculatedTaxCollection $calculatedTaxes, string $title): array
{
$taxes = [];
foreach ($calculatedTaxes as $calculatedTax) {
$tax = (new TaxCreate())
->setRate($calculatedTax->getTaxRate())
->setTitle($this->fixLength($title . ' : ' . $calculatedTax->getTaxRate(), 40));
if (!$tax->valid()) {
$this->logger->critical('Tax payload invalid:', $tax->listInvalidProperties());
throw new InvalidPayloadException('Tax payload invalid:' . json_encode($tax->listInvalidProperties()));
}
$taxes [] = $tax;
}
return $taxes;
}
/**
* @param \Shopware\Core\Checkout\Order\Aggregate\OrderLineItem\OrderLineItemEntity $shopLineItem
*
* @return array|null
*/
protected function getProductAttributes(OrderLineItemEntity $shopLineItem): ?array
{
$productAttributes = [];
$lineItemPayload = $shopLineItem->getPayload();
if (is_array($lineItemPayload) && !empty($lineItemPayload['options'])) {
foreach ($lineItemPayload['options'] as $option) {
$label = $option['group'];
$lineItemAttributeCreate = (new LineItemAttributeCreate())
->setLabel($this->fixLength($label, 512))
->setValue($this->fixLength((string)$option['option'], 512));
if ($lineItemAttributeCreate->valid()) {
$key = $this->fixLength('option_' . md5($label), 40);
$productAttributes[$key] = $lineItemAttributeCreate;
} else {
$this->logger->critical('LineItemAttributeCreate payload invalid:', $lineItemAttributeCreate->listInvalidProperties());
throw new InvalidPayloadException('LineItemAttributeCreate payload invalid:' . json_encode($lineItemAttributeCreate->listInvalidProperties()));
}
}
}
return empty($productAttributes) ? null : $productAttributes;
}
/**
* @return \VRPayment\Sdk\Model\LineItemCreate|null
*/
protected function getShippingLineItem(): ?LineItemCreate
{
try {
$amount = $this->transaction->getOrder()->getShippingTotal();
$amount = self::round($amount);
if ($amount > 0) {
$shippingName = $this->salesChannelContext->getShippingMethod()->getName() ?? $this->translator->trans('vrpayment.payload.shipping.name');
$taxes = $this->getTaxes(
$this->transaction->getOrder()->getShippingCosts()->getCalculatedTaxes(),
$shippingName
);
if ($this->transaction->getOrder()->getTaxStatus() === 'net') {
$amount = self::round($amount + $this->transaction->getOrder()->getShippingCosts()->getCalculatedTaxes()->getAmount());
}
$lineItem = (new LineItemCreate())
->setAmountIncludingTax($amount)
->setName($this->fixLength($shippingName . ' ' . $this->translator->trans('vrpayment.payload.shipping.lineItem'), 150))
->setQuantity($this->transaction->getOrder()->getShippingCosts()->getQuantity() ?? 1)
->setTaxes($taxes)
->setSku($this->fixLength($shippingName . '-Shipping', 200))
/** @noinspection PhpParamsInspection */
->setType(LineItemType::SHIPPING)
->setUniqueId($this->fixLength($shippingName . '-Shipping', 200));
if (!$lineItem->valid()) {
$this->logger->critical('Shipping LineItem payload invalid:', $lineItem->listInvalidProperties());
throw new InvalidPayloadException('Shipping LineItem payload invalid:' . json_encode($lineItem->listInvalidProperties()));
}
return $lineItem;
}
} catch (\Exception $exception) {
$this->logger->critical(__CLASS__ . ' : ' . __FUNCTION__ . ' : ' . $exception->getMessage());
}
return null;
}
/**
* @return array
*/
protected function getMultipleShippingLineItems(): array
{
try {
if ($this->transaction->getOrder()->getShippingTotal() > 0) {
$lineItems = [];
$shippingName = $this->salesChannelContext->getShippingMethod()->getName() ?? $this->translator->trans('vrpayment.payload.shipping.name');
$isFirst = true;
foreach ($this->transaction->getOrder()->getShippingCosts()->getCalculatedTaxes() as $taxItem) {
$amount = self::round($taxItem->getPrice());
if ($this->transaction->getOrder()->getTaxStatus() === 'net') {
$amount = self::round($amount + $taxItem->getTax());
}
$taxRate = $taxItem->getTaxRate();
$tax = (new TaxCreate())
->setRate($taxRate)
->setTitle('Tax rate: '.$taxRate);
$name = $taxRate . '%-' . $shippingName;
$lineItem = (new LineItemCreate())
->setAmountIncludingTax($amount)
->setName($this->fixLength($name . ' ' . $this->translator->trans('vrpayment.payload.shipping.lineItem'), 150))
->setQuantity($this->transaction->getOrder()->getShippingCosts()->getQuantity() ?? 1)
->setTaxes([$tax])
->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 (!$lineItem->valid()) {
$this->logger->critical('Shipping LineItem payload invalid:', $lineItem->listInvalidProperties());
throw new InvalidPayloadException('Shipping LineItem payload invalid:' . json_encode($lineItem->listInvalidProperties()));
}
$lineItems[] = $lineItem;
$isFirst = false;
}
return $lineItems;
}
} catch (\Exception $exception) {
$this->logger->critical(__CLASS__ . ' : ' . __FUNCTION__ . ' : ' . $exception->getMessage());
}
return [];
}
/**
* Get Adjustment Line Item
*
* @param \VRPayment\Sdk\Model\LineItemCreate[] $lineItems
*
* @return \VRPayment\Sdk\Model\LineItemCreate|null
* @throws \Exception
*/
protected function getAdjustmentLineItem(array &$lineItems): ?LineItemCreate
{
$lineItem = null;
$lineItemPriceTotal = array_sum(array_map(static function (LineItemCreate $lineItem) {
return $lineItem->getAmountIncludingTax();
}, $lineItems));
$adjustmentPrice = $this->transaction->getOrder()->getAmountTotal() - $lineItemPriceTotal;
$adjustmentPrice = self::round($adjustmentPrice);
if (abs($adjustmentPrice) != 0) {
if ($this->settings->isLineItemConsistencyEnabled()) {
$error = strtr('LineItems total :lineItemTotal does not add up to order total :orderTotal', [
':lineItemTotal' => $lineItemPriceTotal,
':orderTotal' => $this->transaction->getOrder()->getAmountTotal(),
]);
$this->logger->critical($error);
throw new \Exception($error);
} else {
$lineItem = (new LineItemCreate())
->setName($this->translator->trans('vrpayment.payload.adjustmentLineItem'))
->setUniqueId('Adjustment-Line-Item')
->setSku('Adjustment-Line-Item')
->setQuantity(1);
/** @noinspection PhpParamsInspection */
$lineItem->setAmountIncludingTax($adjustmentPrice)
->setType(($adjustmentPrice > 0) ? LineItemType::FEE : LineItemType::DISCOUNT);
if (!$lineItem->valid()) {
$this->logger->critical('Adjustment LineItem payload invalid:', $lineItem->listInvalidProperties());
throw new InvalidPayloadException('Adjustment LineItem payload invalid:' . json_encode($lineItem->listInvalidProperties()));
}
}
}
return $lineItem;
}
/**
* Get address payload
*
* @param \Shopware\Core\Checkout\Customer\CustomerEntity $customer
* @param \Shopware\Core\Checkout\Customer\Aggregate\CustomerAddress\CustomerAddressEntity $customerAddressEntity
*
* @return \VRPayment\Sdk\Model\AddressCreate
* @throws \Exception
*/
protected function getAddressPayload(CustomerEntity $customer, CustomerAddressEntity $customerAddressEntity, bool $returnSalesTaxNumber = true): AddressCreate
{
// Family name
$family_name = null;
if (!empty($customerAddressEntity->getLastName())) {
$family_name = $customerAddressEntity->getLastName();
} else {
if (!empty($customer->getLastName())) {
$family_name = $customer->getLastName();
}
}
$family_name = !empty($family_name) ? $this->fixLength($family_name, 100) : null;
// Given name
$given_name = null;
if (!empty($customerAddressEntity->getFirstName())) {
$given_name = $customerAddressEntity->getFirstName();
} else {
if (!empty($customer->getFirstName())) {
$given_name = $customer->getFirstName();
}
}
$given_name = !empty($given_name) ? $this->fixLength($given_name, 100) : null;
// Organization name
$organization_name = null;
if (!empty($customerAddressEntity->getCompany())) {
$organization_name = $customerAddressEntity->getCompany();
}
$organization_name = !empty($organization_name) ? $this->fixLength($organization_name, 100) : null;
$salesTaxNumber = null;
if ($returnSalesTaxNumber) {
// salesTaxNumber
$vatIds = $customer->getVatIds();
if (!empty($vatIds)) {
$salesTaxNumber = $vatIds[0];
}
}
// Salutation
$salutation = null;
if (!(
empty($customerAddressEntity->getSalutation()) ||
empty($customerAddressEntity->getSalutation()->getDisplayName())
)) {
$salutation = $customerAddressEntity->getSalutation()->getDisplayName();
} else {
if (!empty($customer->getSalutation())) {
$salutation = $customer->getSalutation()->getDisplayName();
}
}
$salutation = !empty($salutation) ? $this->fixLength($salutation, 20) : null;
$birthday = null;
if (!empty($customer->getBirthday())) {
$birthday = new \DateTime();
$birthday->setTimestamp($customer->getBirthday()->getTimestamp());
$birthday = $birthday->format('Y-m-d');
}
$postalState = $customerAddressEntity?->getCountryState()?->getName() ?? '';
if (empty($postalState)) {
$postalState = $customerAddressEntity?->getCountryState()?->getShortCode() ?? '';
}
$addressData = [
'city' => $customerAddressEntity->getCity() ? $this->fixLength($customerAddressEntity->getCity(), 100) : null,
'country' => $customerAddressEntity->getCountry() ? $customerAddressEntity->getCountry()->getIso() : null,
'email_address' => $customer->getEmail() ? $this->fixLength($customer->getEmail(), 254) : null,
'family_name' => $family_name,
'given_name' => $given_name,
'organization_name' => $organization_name,
'phone_number' => $customerAddressEntity->getPhoneNumber() ? $this->fixLength($customerAddressEntity->getPhoneNumber(), 100) : null,
'postcode' => $customerAddressEntity->getZipcode() ? $this->fixLength($customerAddressEntity->getZipcode(), 40) : null,
'postal_state' => $postalState,
'salutation' => $salutation,
'street' => $customerAddressEntity->getStreet() ? $this->fixLength($customerAddressEntity->getStreet(), 300) : null,
'birthday' => $birthday
];
if ($returnSalesTaxNumber) {
$addressData['sales_tax_number'] = $salesTaxNumber;
}
$addressPayload = (new AddressCreate())
->setCity($addressData['city'])
->setCountry($addressData['country'])
->setEmailAddress($addressData['email_address'])
->setFamilyName($addressData['family_name'])
->setGivenName($addressData['given_name'])
->setOrganizationName($addressData['organization_name'])
->setPhoneNumber($addressData['phone_number'])
->setPostCode($addressData['postcode'])
->setPostalState($addressData['postal_state'])
->setSalutation($addressData['salutation'])
->setStreet($addressData['street']);
if ($returnSalesTaxNumber) {
$addressPayload->setSalesTaxNumber($addressData['sales_tax_number']);
}
if (!empty($addressData['birthday'])) {
$addressPayload->setDateOfBirth($addressData['birthday']);
}
if (!$addressPayload->valid()) {
$this->logger->critical('Address payload invalid:', $addressPayload->listInvalidProperties());
throw new InvalidPayloadException('Address payload invalid:' . json_encode($addressPayload->listInvalidProperties()));
}
return $addressPayload;
}
/**
* @param string $id
*
* @return \VRPaymentPayment\Core\Api\PaymentMethodConfiguration\Entity\PaymentMethodConfigurationEntity
*/
protected function getPaymentConfiguration(string $id): PaymentMethodConfigurationEntity
{
$criteria = (new Criteria([$id]));
return $this->container->get('vrpayment_payment_method_configuration.repository')
->search($criteria, $this->salesChannelContext->getContext())
->getEntities()->first();
}
/**
* Get failure URL
*
* @param string $orderId
*
* @return string
*/
protected function getFailUrl(string $orderId): string
{
return $this->container->get('router')->generate(
'frontend.vrpayment.checkout.recreate-cart',
['orderId' => $orderId,],
UrlGeneratorInterface::ABSOLUTE_URL
);
}
}
+205
View File
@@ -0,0 +1,205 @@
<?php declare(strict_types=1);
namespace VRPaymentPayment\Core\Util;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
use Shopware\Core\{
Framework\Context,
Framework\DataAbstractionLayer\Search\Criteria,
Framework\DataAbstractionLayer\Search\Filter\EqualsFilter,
Framework\DataAbstractionLayer\Search\Filter\NotFilter,
Framework\DataAbstractionLayer\Search\Sorting\FieldSorting,
System\SalesChannel\SalesChannelCollection,};
use VRPaymentPayment\Core\Checkout\PaymentHandler\VRPaymentPaymentHandler;
/**
* Class PaymentMethodUtil
*
* @package VRPaymentPayment\Core\Util
*/
class PaymentMethodUtil {
/**
* @var \Psr\Log\LoggerInterface
*/
protected $logger;
/**
* @var \Psr\Container\ContainerInterface
*/
protected $container;
/**
* @var \Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface
*/
private $paymentRepository;
/**
* @var \Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface
*/
private $salesChannelRepository;
/**
* @var \Shopware\Core\System\SalesChannel\Aggregate\SalesChannelPaymentMethod\SalesChannelPaymentMethodDefinition
*/
private $salesChannelPaymentMethodRepository;
/**
* PaymentMethodUtil constructor.
*
* @param \Psr\Container\ContainerInterface $container
*/
public function __construct(ContainerInterface $container)
{
$this->container = $container;
$this->paymentRepository = $this->container->get('payment_method.repository');
$this->salesChannelRepository = $this->container->get('sales_channel.repository');
$this->salesChannelPaymentMethodRepository = $this->container->get('sales_channel_payment_method.repository');
}
/**
* @param \Psr\Log\LoggerInterface $logger
* @internal
* @required
*
*/
public function setLogger(LoggerInterface $logger): void
{
$this->logger = $logger;
}
/**
* @param \Shopware\Core\Framework\Context $context
* @param string|null $salesChannelId
*/
public function setVRPaymentAsDefaultPaymentMethod(Context $context, ?string $salesChannelId = null): void
{
$paymentMethodIds = $this->getVRPaymentPaymentMethodIds($context);
if (empty($paymentMethodIds)) {
return;
}
$salesChannelsToChange = $this->getSalesChannelsToChange($context, $salesChannelId);
$updateData = [];
foreach ($salesChannelsToChange as $salesChannel) {
foreach ($paymentMethodIds as $paymentMethodId) {
$salesChannelUpdateData = [
'id' => $salesChannel->getId(),
'paymentMethodId' => $paymentMethodId,
];
$paymentMethodCollection = $salesChannel->getPaymentMethods();
if (is_null($paymentMethodCollection) || is_null($paymentMethodCollection->get($paymentMethodId))) {
$salesChannelUpdateData['paymentMethods'][] = [
'id' => $paymentMethodId,
];
}
$updateData[] = $salesChannelUpdateData;
}
}
$this->salesChannelRepository->update($updateData, $context);
}
/**
* @param \Shopware\Core\Framework\Context $context
* @return array
*/
public function getVRPaymentPaymentMethodIds(Context $context): array
{
$criteria = (new Criteria())
->addFilter(new EqualsFilter('handlerIdentifier', VRPaymentPaymentHandler::class))
->addSorting(new FieldSorting('position'));
return $this->paymentRepository->searchIds($criteria, $context)->getIds();
}
/**
* @param \Shopware\Core\Framework\Context $context
* @param string|null $salesChannelId
* @return \Shopware\Core\System\SalesChannel\SalesChannelCollection
*/
private function getSalesChannelsToChange(Context $context, ?string $salesChannelId = null): SalesChannelCollection
{
$criteria = is_null($salesChannelId) ? new Criteria() : new Criteria([$salesChannelId]);
$criteria->addAssociation('paymentMethods');
return $this->salesChannelRepository->search($criteria, $context)->getEntities();
}
/**
* Disable System Payment Methods
*
* @param \Shopware\Core\Framework\Context $context
*
*/
public function disableSystemPaymentMethods(Context $context): void
{
$paymentMethodIds = $this->getSystemPaymentMethodIds($context);
$this->setPaymentMethodIsActive($paymentMethodIds, false, $context);
$this->disableSalesChannelPaymentMethods($paymentMethodIds, $context);
}
/**
* @param \Shopware\Core\Framework\Context $context
* @return string[]
*/
protected function getSystemPaymentMethodIds(Context $context): array
{
$criteria = (new Criteria())
->addFilter(new NotFilter(
NotFilter::CONNECTION_AND,
[
new EqualsFilter('handlerIdentifier', VRPaymentPaymentHandler::class),
]
));
return $this->paymentRepository->searchIds($criteria, $context)->getIds();
}
/**
* @param array $paymentMethodIds
* @param bool $active
* @param \Shopware\Core\Framework\Context $context
*
*/
protected function setPaymentMethodIsActive(array $paymentMethodIds, bool $active, Context $context): void
{
$data = [];
foreach ($paymentMethodIds as $paymentMethodId) {
$data[] = [
'id' => $paymentMethodId,
'active' => $active,
];
}
$this->paymentRepository->update($data, $context);
}
/**
* @param array $paymentMethodIds
* @param \Shopware\Core\Framework\Context $context
*
*/
protected function disableSalesChannelPaymentMethods(array $paymentMethodIds, Context $context)
{
$data = [];
$salesChannels = $this->getSalesChannelsToChange($context);
foreach ($salesChannels as $salesChannel) {
foreach ($paymentMethodIds as $paymentMethodId) {
$data[] = [
'paymentMethodId' => $paymentMethodId,
'salesChannelId' => $salesChannel->getId(),
];
}
}
$this->salesChannelPaymentMethodRepository->delete($data, $context);
}
}
@@ -0,0 +1,115 @@
<?php declare(strict_types=1);
namespace VRPaymentPayment\Core\Util\Traits;
use Doctrine\DBAL\Connection;
use Shopware\Core\{
Framework\Context,
Framework\DataAbstractionLayer\EntityRepositoryInterface,
Framework\DataAbstractionLayer\Search\Criteria,
Framework\DataAbstractionLayer\Search\Filter\ContainsFilter,
Framework\DataAbstractionLayer\Search\Filter\EqualsFilter,
Framework\Plugin\Context\UninstallContext};
use VRPaymentPayment\Core\{
Checkout\PaymentHandler\VRPaymentPaymentHandler,
Settings\Service\SettingsService};
/**
* Trait VRPaymentPaymentPluginTrait
*
* We use a trait keep the plugin class clean and free of auxiliary functions.
*
* @package VRPaymentPayment\Core\Util\Traits
*/
trait VRPaymentPaymentPluginTrait {
/**
* @param \Shopware\Core\Framework\Context $context
*/
protected function enablePaymentMethods(Context $context)
{
$paymentMethodIds = $this->getPaymentMethodIds($context);
foreach ($paymentMethodIds as $paymentMethodId) {
$this->setPaymentMethodIsActive($paymentMethodId, true, $context);
}
}
/**
* @param \Shopware\Core\Framework\Context $context
* @return string[]
*/
protected function getPaymentMethodIds(Context $context): array
{
/** @var EntityRepositoryInterface $paymentRepository */
$paymentRepository = $this->container->get('payment_method.repository');
$criteria = (new Criteria())
->addFilter(new EqualsFilter('handlerIdentifier', VRPaymentPaymentHandler::class));
return $paymentRepository->searchIds($criteria, $context)->getIds();
}
/**
* @param string $paymentMethodId
* @param bool $active
* @param \Shopware\Core\Framework\Context $context
* @return void
*/
protected function setPaymentMethodIsActive(string $paymentMethodId, bool $active, Context $context): void
{
$paymentMethod = [
'id' => $paymentMethodId,
'active' => $active,
];
/** @var EntityRepositoryInterface $paymentRepository */
$paymentRepository = $this->container->get('payment_method.repository');
$paymentRepository->update([$paymentMethod], $context);
}
/**
* @param \Shopware\Core\Framework\Context $context
* @return void
*/
protected function disablePaymentMethods(Context $context): void
{
$paymentMethodIds = $this->getPaymentMethodIds($context);
foreach ($paymentMethodIds as $paymentMethodId) {
$this->setPaymentMethodIsActive($paymentMethodId, false, $context);
}
}
/**
* @param \Shopware\Core\Framework\Context $context
* @return void
*/
private function removeConfiguration(Context $context): void
{
$criteria = (new Criteria())
->addFilter(new ContainsFilter('configurationKey', SettingsService::SYSTEM_CONFIG_DOMAIN));
$systemConfigRepository = $this->container->get('system_config.repository');
$idSearchResult = $systemConfigRepository->searchIds($criteria, $context);
foreach ($idSearchResult->getIds() as $id) {
$systemConfigRepository->delete([['id' => $id]], $context);
}
}
/**
* Delete user data when plugin is uninstalled
*
* @internal
* @param \Shopware\Core\Framework\Plugin\Context\UninstallContext $uninstallContext
* @return void
*/
protected function deleteUserData(UninstallContext $uninstallContext): void
{
$connection = $this->container->get(Connection::class);
// Check if the column exists before attempting to drop it
$columns = $connection->fetchAllAssociative("SHOW COLUMNS FROM `order` LIKE 'vrpayment_lock'");
if (!empty($columns)) {
$query = 'ALTER TABLE `order` DROP COLUMN `vrpayment_lock`;';
$connection->executeStatement($query);
}
}
}
@@ -0,0 +1,60 @@
<?php declare(strict_types=1);
namespace VRPaymentPayment\Migration;
use Doctrine\DBAL\Connection;
use Shopware\Core\Framework\Migration\MigrationStep;
/**
* Class Migration1590156974PaymentMethodConfigurationEntity
*
* @package VRPaymentPayment\Migration
*/
class Migration1590156974PaymentMethodConfigurationEntity extends MigrationStep {
/**
* get creation timestamp
*
* @return int
*/
public function getCreationTimestamp(): int
{
return 1590156974;
}
/**
* update non-destructive changes
*
* @param \Doctrine\DBAL\Connection $connection
* @throws \Doctrine\DBAL\DBALException
*/
public function update(Connection $connection): void
{
$connection->executeStatement('
CREATE TABLE IF NOT EXISTS `vrpayment_payment_method_configuration` (
`id` BINARY(16) NOT NULL,
`data` JSON NOT NULL,
`payment_method_configuration_id` INT UNSIGNED NOT NULL,
`payment_method_id` BINARY(16) NOT NULL,
`sort_order` TINYINT UNSIGNED NOT NULL,
`space_id` INT UNSIGNED NOT NULL,
`state` VARCHAR(255) NOT NULL,
`created_at` DATETIME(3) NOT NULL,
`updated_at` DATETIME(3) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `payment_method_configuration_id_space_id_UNIQUE` (`payment_method_configuration_id`,`space_id`),
KEY `fk.vrp_payment_method_configuration.payment_method_id` (`payment_method_id`),
CONSTRAINT `fk.vrp_payment_method_configuration.payment_method_id` FOREIGN KEY (`payment_method_id`) REFERENCES `payment_method` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
');
}
/**
* update destructive changes
*
* @param \Doctrine\DBAL\Connection $connection
*/
public function updateDestructive(Connection $connection): void
{
// implement update destructive
}
}
@@ -0,0 +1,69 @@
<?php declare(strict_types=1);
namespace VRPaymentPayment\Migration;
use Doctrine\DBAL\Connection;
use Shopware\Core\Framework\Migration\MigrationStep;
/**
* Class Migration1590156974TransactionEntity
*
* @package VRPaymentPayment\Migration
*/
class Migration1590156974TransactionEntity extends MigrationStep {
/**
* get creation timestamp
*
* @return int
*/
public function getCreationTimestamp(): int
{
return 1590156974;
}
/**
* update non-destructive changes
*
* @param \Doctrine\DBAL\Connection $connection
* @throws \Doctrine\DBAL\DBALException
*/
public function update(Connection $connection): void
{
$connection->executeStatement('
CREATE TABLE IF NOT EXISTS `vrpayment_transaction` (
`id` BINARY(16) NOT NULL,
`data` JSON NOT NULL,
`payment_method_id` BINARY(16) NOT NULL,
`order_id` BINARY(16) NOT NULL,
`order_transaction_id` BINARY(16) NOT NULL,
`space_id` INT UNSIGNED NOT NULL,
`state` VARCHAR(255) NOT NULL,
`sales_channel_id` BINARY(16) NOT NULL,
`transaction_id` INT UNSIGNED NOT NULL,
`created_at` DATETIME(3) NOT NULL,
`updated_at` DATETIME(3) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `transaction_id_UNIQUE` (`transaction_id`),
KEY `fk.vrp_transaction.order_id` (`order_id`),
KEY `fk.vrp_transaction.order_transaction_id` (`order_transaction_id`),
KEY `fk.vrp_transaction.payment_method_id` (`payment_method_id`),
KEY `fk.vrp_transaction.sales_channel_id` (`sales_channel_id`),
CONSTRAINT `fk.vrp_transaction.order_id` FOREIGN KEY (`order_id`) REFERENCES `order` (`id`) ON DELETE CASCADE,
CONSTRAINT `fk.vrp_transaction.order_transaction_id` FOREIGN KEY (`order_transaction_id`) REFERENCES `order_transaction` (`id`) ON DELETE CASCADE,
CONSTRAINT `fk.vrp_transaction.payment_method_id` FOREIGN KEY (`payment_method_id`) REFERENCES `payment_method` (`id`) ON DELETE CASCADE,
CONSTRAINT `fk.vrp_transaction.sales_channel_id` FOREIGN KEY (`sales_channel_id`) REFERENCES `sales_channel` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
');
}
/**
* update destructive changes
*
* @param \Doctrine\DBAL\Connection $connection
*/
public function updateDestructive(Connection $connection): void
{
// implement update destructive
}
}
@@ -0,0 +1,47 @@
<?php declare(strict_types=1);
namespace VRPaymentPayment\Migration;
use Doctrine\DBAL\Connection;
use Shopware\Core\Framework\Migration\MigrationStep;
/**
* Class Migration1590646356OrderEntity
*
* @package VRPaymentPayment\Migration
*/
class Migration1590646356OrderEntity extends MigrationStep {
/**
* get creation timestamp
*
* @return int
*/
public function getCreationTimestamp(): int
{
return 1590646356;
}
/**
* update non-destructive changes
*
* @param \Doctrine\DBAL\Connection $connection
*/
public function update(Connection $connection): void
{
try {
$connection->executeStatement('ALTER TABLE `order` ADD COLUMN `vrpayment_lock` DATETIME DEFAULT NULL;');
}catch (\Exception $exception){
echo $exception->getMessage();
}
}
/**
* update destructive changes
*
* @param \Doctrine\DBAL\Connection $connection
*/
public function updateDestructive(Connection $connection): void
{
// implement update destructive
}
}
@@ -0,0 +1,59 @@
<?php declare(strict_types=1);
namespace VRPaymentPayment\Migration;
use Doctrine\DBAL\Connection;
use Shopware\Core\Framework\Migration\MigrationStep;
/**
* Class Migration1590646356RefundEntity
*
* @package VRPaymentPayment\Migration
*/
class Migration1590646356RefundEntity extends MigrationStep {
/**
* get creation timestamp
*
* @return int
*/
public function getCreationTimestamp(): int
{
return 1590646356;
}
/**
* update non-destructive changes
*
* @param \Doctrine\DBAL\Connection $connection
* @throws \Doctrine\DBAL\DBALException
*/
public function update(Connection $connection): void
{
$connection->executeStatement('
CREATE TABLE IF NOT EXISTS `vrpayment_refund` (
`id` BINARY(16) NOT NULL,
`data` JSON NOT NULL,
`refund_id` INT UNSIGNED NOT NULL,
`space_id` INT UNSIGNED NOT NULL,
`state` VARCHAR(255) NOT NULL,
`transaction_id` INT UNSIGNED NOT NULL,
`created_at` DATETIME(3) NOT NULL,
`updated_at` DATETIME(3) DEFAULT NULL,
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
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
');
}
/**
* update destructive changes
*
* @param \Doctrine\DBAL\Connection $connection
*/
public function updateDestructive(Connection $connection): void
{
// implement update destructive
}
}
@@ -0,0 +1,48 @@
<?php declare(strict_types=1);
namespace VRPaymentPayment\Migration;
use Doctrine\DBAL\Connection;
use Shopware\Core\Framework\Migration\MigrationStep;
/**
* Class Migration1590646356TransactionEntity
*
* @package VRPaymentPayment\Migration
*/
class Migration1590646356TransactionEntity extends MigrationStep {
/**
* get creation timestamp
*
* @return int
*/
public function getCreationTimestamp(): int
{
return 1590646356;
}
/**
* update non-destructive changes
*
* @param \Doctrine\DBAL\Connection $connection
*/
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`;');
}catch (\Exception $exception){
// column probably exists
}
}
/**
* update destructive changes
*
* @param \Doctrine\DBAL\Connection $connection
*/
public function updateDestructive(Connection $connection): void
{
// implement update destructive
}
}
@@ -0,0 +1,44 @@
<?php declare(strict_types=1);
namespace VRPaymentPayment\Migration;
use Doctrine\DBAL\Connection;
use Shopware\Core\Framework\Migration\MigrationStep;
/**
* Class Migration1605701047PaymentMethodConfigurationEntity
*
* @package VRPaymentPayment\Migration
*/
class Migration1605701047PaymentMethodConfigurationEntity extends MigrationStep {
/**
* get creation timestamp
*
* @return int
*/
public function getCreationTimestamp(): int
{
return 1605701047;
}
/**
* update non-destructive changes
*
* @param \Doctrine\DBAL\Connection $connection
* @throws \Doctrine\DBAL\DBALException
*/
public function update(Connection $connection): void
{
$connection->executeStatement('ALTER TABLE `vrpayment_payment_method_configuration` CHANGE `payment_method_configuration_id` `payment_method_configuration_id` bigint unsigned NOT NULL AFTER `data`;');
}
/**
* update destructive changes
*
* @param \Doctrine\DBAL\Connection $connection
*/
public function updateDestructive(Connection $connection): void
{
// implement update destructive
}
}
@@ -0,0 +1,78 @@
<?php declare(strict_types=1);
namespace VRPaymentPayment\Migration;
use Doctrine\DBAL\Connection;
use Shopware\Core\Framework\Migration\MigrationStep;
/**
* Class Migration1605701048TransactionEntity
*
* @package VRPaymentPayment\Migration
*/
class Migration1605701048TransactionEntity extends MigrationStep
{
/**
* get creation timestamp
*
* @return int
*/
public function getCreationTimestamp(): int
{
return 1605701048;
}
/**
* update non-destructive changes
*
* @param \Doctrine\DBAL\Connection $connection
*/
public function update(Connection $connection): void
{
try {
$connection->executeStatement('
ALTER TABLE `vrpayment_transaction`
ADD `order_version_id` binary(16) NOT NULL AFTER `transaction_id`;
');
$connection->executeStatement('
UPDATE `vrpayment_transaction` 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`
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`,
DROP FOREIGN KEY `fk.vrp_transaction.sales_channel_id`;
');
$connection->executeStatement('
ALTER TABLE `vrpayment_transaction`
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`)
REFERENCES `payment_method` (`id`) ON UPDATE CASCADE ON DELETE RESTRICT,
ADD CONSTRAINT `fk.vrp_transaction_sales_channel_id` FOREIGN KEY (`sales_channel_id`)
REFERENCES `sales_channel` (`id`) ON UPDATE CASCADE ON DELETE RESTRICT;
');
} catch (\Exception $exception) {
// column probably exists
}
}
/**
* update destructive changes
*
* @param \Doctrine\DBAL\Connection $connection
*/
public function updateDestructive(Connection $connection): void
{
// implement update destructive
}
}
@@ -0,0 +1,89 @@
<?php declare(strict_types=1);
namespace VRPaymentPayment\Migration;
use Doctrine\DBAL\Connection;
use Shopware\Core\Framework\Migration\MigrationStep;
use Shopware\Core\Framework\Uuid\Uuid;
use Shopware\Core\System\StateMachine\Aggregation\StateMachineState\StateMachineStateDefinition;
use Shopware\Core\System\StateMachine\Aggregation\StateMachineTransition\StateMachineTransitionDefinition;
use Shopware\Core\System\StateMachine\StateMachineDefinition;
/**
* Class Migration1605701049StateMachineEntity
*
* @package VRPaymentPayment\Migration
*/
class Migration1605701049StateMachineEntity extends MigrationStep
{
/**
* get creation timestamp
*
* @return int
*/
public function getCreationTimestamp(): int
{
return 1605701049;
}
/**
* update non-destructive changes
*
* @param \Doctrine\DBAL\Connection $connection
*/
public function update(Connection $connection): void
{
try {
// Enable mark transaction as paid when it's on status reminded
$table = StateMachineDefinition::ENTITY_NAME;
$stateMachineId = $connection->fetchOne(
"SELECT id FROM `$table` WHERE `technical_name` = :technical_name",
[
'technical_name' => 'order_transaction.state',
]
);
$table = StateMachineStateDefinition::ENTITY_NAME;
$remindedStateId = $connection->fetchOne(
"SELECT id FROM `$table` WHERE `technical_name` = :technical_name AND `state_machine_id` = :state_machine_id",
[
'technical_name' => 'reminded',
'state_machine_id' => $stateMachineId,
]
);
$paidStateId = $connection->fetchOne(
"SELECT id FROM `$table` WHERE `technical_name` = :technical_name AND `state_machine_id` = :state_machine_id",
[
'technical_name' => 'paid',
'state_machine_id' => $stateMachineId,
]
);
$id = Uuid::randomBytes();
$connection->insert(StateMachineTransitionDefinition::ENTITY_NAME,
[
'id' => $id,
'action_name' => 'paid',
'state_machine_id' => $stateMachineId,
'from_state_id' => $remindedStateId,
'to_state_id' => $paidStateId,
'created_at' => date('Y-m-d H:i:s')
]
);
} catch (\Exception $exception) {
// column probably exists
}
}
/**
* update destructive changes
*
* @param \Doctrine\DBAL\Connection $connection
*/
public function updateDestructive(Connection $connection): void
{
// implement update destructive
}
}
@@ -0,0 +1,48 @@
<?php declare(strict_types=1);
namespace VRPaymentPayment\Migration;
use Doctrine\DBAL\Connection;
use Shopware\Core\Framework\Migration\MigrationStep;
/**
* Class Migration1684240994TransactionEntity
*
* @package VRPaymentPayment\Migration
*/
class Migration1684240994TransactionEntity extends MigrationStep {
/**
* get creation timestamp
*
* @return int
*/
public function getCreationTimestamp(): int
{
return 1684240994;
}
/**
* update non-destructive changes
*
* @param \Doctrine\DBAL\Connection $connection
*/
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`;');
}catch (\Exception $exception){
// column probably exists
}
}
/**
* update destructive changes
*
* @param \Doctrine\DBAL\Connection $connection
*/
public function updateDestructive(Connection $connection): void
{
// implement update destructive
}
}
@@ -0,0 +1,141 @@
/* global Shopware */
const ApiService = Shopware.Classes.ApiService;
/**
* @class VRPaymentPayment\Core\Api\Config\Controller\ConfigurationController
*/
class VRPaymentConfigurationService extends ApiService {
/**
* VRPaymentConfigurationService constructor
*
* @param httpClient
* @param loginService
* @param apiEndpoint
*/
constructor(httpClient, loginService, apiEndpoint = 'vrpayment') {
super(httpClient, loginService, apiEndpoint);
}
/**
* Register web hooks
*
* @param {String|null} salesChannelId
* @return {*}
*/
registerWebHooks(salesChannelId = null) {
const headers = this.getBasicHeaders();
const apiRoute = `${Shopware.Context.api.apiPath}/_action/${this.getApiBasePath()}/configuration/register-web-hooks`;
return this.httpClient.post(
apiRoute,
{
salesChannelId: salesChannelId
},
{
headers: headers
}
).then((response) => {
return ApiService.handleResponse(response);
});
}
/**
* Test API connection
*
* @param {int|null} spaceId
* @param {int|null} userId
* @param {String|null} applicationId
* @return {*}
*/
checkApiConnection(spaceId = null, userId = null, applicationId = null) {
const headers = this.getBasicHeaders();
const apiRoute = `${Shopware.Context.api.apiPath}/_action/${this.getApiBasePath()}/configuration/check-api-connection`;
return this.httpClient.post(
apiRoute,
{
spaceId: spaceId,
userId: userId,
applicationId: applicationId
},
{
headers: headers
}
).then((response) => {
return ApiService.handleResponse(response);
});
}
/**
* Set's the default payment method to VRPayment for the given salesChannel id.
*
* @param {String|null} salesChannelId
*
* @returns {Promise}
*/
setVRPaymentAsSalesChannelPaymentDefault(salesChannelId = null) {
const headers = this.getBasicHeaders();
const apiRoute = `${Shopware.Context.api.apiPath}/_action/${this.getApiBasePath()}/configuration/set-vrpayment-as-sales-channel-payment-default`;
return this.httpClient.post(
apiRoute,
{
salesChannelId: salesChannelId
},
{
headers: headers
}
).then((response) => {
return ApiService.handleResponse(response);
});
}
/**
*
* @param salesChannelId
* @return {Promise}
*/
synchronizePaymentMethodConfiguration(salesChannelId = null) {
const headers = this.getBasicHeaders();
const apiRoute = `${Shopware.Context.api.apiPath}/_action/${this.getApiBasePath()}/configuration/synchronize-payment-method-configuration`;
return this.httpClient.post(
apiRoute,
{
salesChannelId: salesChannelId
},
{
headers: headers
}
).then((response) => {
return ApiService.handleResponse(response);
});
}
/**
*
* @return {*}
*/
installOrderDeliveryStates() {
const headers = this.getBasicHeaders();
const apiRoute = `${Shopware.Context.api.apiPath}/_action/${this.getApiBasePath()}/configuration/install-order-delivery-states`;
return this.httpClient.post(
apiRoute,
{
},
{
headers: headers
}
).then((response) => {
return ApiService.handleResponse(response);
});
}
}
export default VRPaymentConfigurationService;
@@ -0,0 +1,110 @@
/* global Shopware */
const ApiService = Shopware.Classes.ApiService;
/**
* @class VRPaymentPayment\Core\Api\Transaction\Controller\RefundController
*/
class VRPaymentRefundService extends ApiService {
/**
* VRPaymentRefundService constructor
*
* @param httpClient
* @param loginService
* @param apiEndpoint
*/
constructor(httpClient, loginService, apiEndpoint = 'vrpayment') {
super(httpClient, loginService, apiEndpoint);
}
/**
* Refund a transaction
*
* @param {String} salesChannelId
* @param {int} transactionId
* @param {int} quantity
* @param {int} lineItemId
* @return {*}
*/
createRefund(salesChannelId, transactionId, quantity, lineItemId) {
const headers = this.getBasicHeaders();
const apiRoute = `${Shopware.Context.api.apiPath}/_action/${this.getApiBasePath()}/refund/create-refund/`;
return this.httpClient.post(
apiRoute,
{
salesChannelId: salesChannelId,
transactionId: transactionId,
quantity: quantity,
lineItemId: lineItemId
},
{
headers: headers
}
).then((response) => {
return ApiService.handleResponse(response);
});
}
/**
* Refund a transaction
*
* @param {String} salesChannelId
* @param {int} transactionId
* @param {float} refundableAmount
* @return {*}
*/
createRefundByAmount(salesChannelId, transactionId, refundableAmount) {
const headers = this.getBasicHeaders();
const apiRoute = `${Shopware.Context.api.apiPath}/_action/${this.getApiBasePath()}/refund/create-refund-by-amount/`;
return this.httpClient.post(
apiRoute,
{
salesChannelId: salesChannelId,
transactionId: transactionId,
refundableAmount: refundableAmount
},
{
headers: headers
}
).then((response) => {
return ApiService.handleResponse(response);
});
}
/**
* Refund a transaction
*
* @param {String} salesChannelId
* @param {int} transactionId
* @param {float} refundableAmount
* @param {String} lineItemId
* @return {*}
*/
createPartialRefund(salesChannelId, transactionId, refundableAmount, lineItemId) {
const headers = this.getBasicHeaders();
const apiRoute = `${Shopware.Context.api.apiPath}/_action/${this.getApiBasePath()}/refund/create-partial-refund/`;
return this.httpClient.post(
apiRoute,
{
salesChannelId: salesChannelId,
transactionId: transactionId,
refundableAmount: refundableAmount,
lineItemId: lineItemId
},
{
headers: headers
}
).then((response) => {
return ApiService.handleResponse(response);
});
}
}
export default VRPaymentRefundService;
@@ -0,0 +1,48 @@
/* global Shopware */
const ApiService = Shopware.Classes.ApiService;
/**
* @class VRPaymentPayment\Core\Api\Transaction\Controller\TransactionCompletionController
*/
class VRPaymentTransactionCompletionService extends ApiService {
/**
* VRPaymentTransactionCompletionService constructor
*
* @param httpClient
* @param loginService
* @param apiEndpoint
*/
constructor(httpClient, loginService, apiEndpoint = 'vrpayment') {
super(httpClient, loginService, apiEndpoint);
}
/**
* Complete a transaction
*
* @param {String} salesChannelId
* @param {int} transactionId
* @return {*}
*/
createTransactionCompletion(salesChannelId, transactionId) {
const headers = this.getBasicHeaders();
const apiRoute = `${Shopware.Context.api.apiPath}/_action/${this.getApiBasePath()}/transaction-completion/create-transaction-completion/`;
return this.httpClient.post(
apiRoute,
{
salesChannelId: salesChannelId,
transactionId: transactionId
},
{
headers: headers
}
).then((response) => {
return ApiService.handleResponse(response);
});
}
}
export default VRPaymentTransactionCompletionService;
@@ -0,0 +1,48 @@
/* global Shopware */
const ApiService = Shopware.Classes.ApiService;
/**
* @class VRPaymentPayment\Core\Api\Transaction\Controller\TransactionVoidController
*/
class VRPaymentTransactionVoidService extends ApiService {
/**
* VRPaymentTransactionVoidService constructor
*
* @param httpClient
* @param loginService
* @param apiEndpoint
*/
constructor(httpClient, loginService, apiEndpoint = 'vrpayment') {
super(httpClient, loginService, apiEndpoint);
}
/**
* Void a transaction
*
* @param {String} salesChannelId
* @param {int} transactionId
* @return {*}
*/
createTransactionVoid(salesChannelId, transactionId) {
const headers = this.getBasicHeaders();
const apiRoute = `${Shopware.Context.api.apiPath}/_action/${this.getApiBasePath()}/transaction-void/create-transaction-void/`;
return this.httpClient.post(
apiRoute,
{
salesChannelId: salesChannelId,
transactionId: transactionId
},
{
headers: headers
}
).then((response) => {
return ApiService.handleResponse(response);
});
}
}
export default VRPaymentTransactionVoidService;
@@ -0,0 +1,71 @@
/* global Shopware */
const ApiService = Shopware.Classes.ApiService;
/**
* @class VRPaymentPayment\Core\Api\Transaction\Controller\TransactionController
*/
class VRPaymentTransactionService extends ApiService {
/**
* VRPaymentTransactionService constructor
*
* @param httpClient
* @param loginService
* @param apiEndpoint
*/
constructor(httpClient, loginService, apiEndpoint = 'vrpayment') {
super(httpClient, loginService, apiEndpoint);
}
/**
* Get transaction data
*
* @param {String} salesChannelId
* @param {int} transactionId
* @return {*}
*/
getTransactionData(salesChannelId, transactionId) {
const headers = this.getBasicHeaders();
const apiRoute = `${Shopware.Context.api.apiPath}/_action/${this.getApiBasePath()}/transaction/get-transaction-data/`;
return this.httpClient.post(
apiRoute,
{
salesChannelId: salesChannelId,
transactionId: transactionId
},
{
headers: headers
}
).then((response) => {
return ApiService.handleResponse(response);
});
}
/**
* Download Invoice Document
*
* @param context
* @param salesChannelId
* @param transactionId
* @return {string}
*/
getInvoiceDocument(salesChannelId, transactionId) {
return `${Shopware.Context.api.apiPath}/_action/${this.getApiBasePath()}/transaction/get-invoice-document/${salesChannelId}/${transactionId}`;
}
/**
* Download Packing slip
*
* @param salesChannelId
* @param transactionId
* @return {string}
*/
getPackingSlip(salesChannelId, transactionId) {
return `${Shopware.Context.api.apiPath}/_action/${this.getApiBasePath()}/transaction/get-packing-slip/${salesChannelId}/${transactionId}`;
}
}
export default VRPaymentTransactionService;
@@ -0,0 +1,44 @@
/* global Shopware */
const ApiService = Shopware.Classes.ApiService;
/**
* @class VRPaymentPayment\Core\Api\WebHooks\Controller\WebHookController
*/
class VRPaymentWebHookRegisterService extends ApiService {
/**
* VRPaymentWebHookRegisterService
*
* @param httpClient
* @param loginService
* @param apiEndpoint
*/
constructor(httpClient, loginService, apiEndpoint = 'vrpayment') {
super(httpClient, loginService, apiEndpoint);
}
/**
* Register a webhook
*
* @param {String|null} salesChannelId
* @return {*}
*/
registerWebHook(salesChannelId) {
const headers = this.getBasicHeaders();
const apiRoute = `${Shopware.Context.api.apiPath}/_action/${this.getApiBasePath()}/webHook/register/${salesChannelId}`;
return this.httpClient.post(
apiRoute,
{},
{
headers: headers
}
).then((response) => {
return ApiService.handleResponse(response);
});
}
}
export default VRPaymentWebHookRegisterService;
@@ -0,0 +1,42 @@
/* global Shopware */
import VRPaymentConfigurationService from '../core/service/api/vrpayment-configuration.service';
import VRPaymentRefundService from '../core/service/api/vrpayment-refund.service';
import VRPaymentTransactionService from '../core/service/api/vrpayment-transaction.service';
import VRPaymentTransactionCompletionService
from '../core/service/api/vrpayment-transaction-completion.service';
import VRPaymentTransactionVoidService
from '../core/service/api/vrpayment-transaction-void.service';
const {Application} = Shopware;
// noinspection JSUnresolvedFunction
Application.addServiceProvider('VRPaymentConfigurationService', (container) => {
const initContainer = Application.getContainer('init');
return new VRPaymentConfigurationService(initContainer.httpClient, container.loginService);
});
// noinspection JSUnresolvedFunction
Application.addServiceProvider('VRPaymentRefundService', (container) => {
const initContainer = Application.getContainer('init');
return new VRPaymentRefundService(initContainer.httpClient, container.loginService);
});
// noinspection JSUnresolvedFunction
Application.addServiceProvider('VRPaymentTransactionService', (container) => {
const initContainer = Application.getContainer('init');
return new VRPaymentTransactionService(initContainer.httpClient, container.loginService);
});
// noinspection JSUnresolvedFunction
Application.addServiceProvider('VRPaymentTransactionCompletionService', (container) => {
const initContainer = Application.getContainer('init');
return new VRPaymentTransactionCompletionService(initContainer.httpClient, container.loginService);
});
// noinspection JSUnresolvedFunction
Application.addServiceProvider('VRPaymentTransactionVoidService', (container) => {
const initContainer = Application.getContainer('init');
return new VRPaymentTransactionVoidService(initContainer.httpClient, container.loginService);
});
@@ -0,0 +1,3 @@
import './module/vrpayment-order';
import './module/vrpayment-settings';
import './init/api-service.init';
@@ -0,0 +1,24 @@
{% block vrpayment_order_action_completion %}
<sw-modal variant="small"
:title="$tc(`vrpayment-order.modal.title.capture`)"
@modal-close="$emit('modal-close')">
{% block vrpayment_order_action_completion_amount %}
<sw-checkbox-field
:label="$tc('vrpayment-order.captureAction.button.text')"
v-model:value="isCompletion">
</sw-checkbox-field>
{% endblock %}
{% block vrpayment_order_action_completion_confirm_button %}
<template #modal-footer>
<sw-button variant="primary"
@click="completion">
{{ $tc('vrpayment-order.refundAction.confirmButton.text') }}
</sw-button>
</template>
{% endblock %}
<sw-loader v-if="isLoading"></sw-loader>
</sw-modal>
{% endblock %}
@@ -0,0 +1,86 @@
/* global Shopware */
import template from './index.html.twig';
const {Component, Mixin, Filter, Utils} = Shopware;
Component.register('vrpayment-order-action-completion', {
template: template,
inject: ['VRPaymentTransactionCompletionService'],
mixins: [
Mixin.getByName('notification')
],
props: {
transactionData: {
type: Object,
required: true
}
},
data() {
return {
isLoading: true,
isCompletion: false
};
},
computed: {
dateFilter() {
return Filter.getByName('date');
}
},
created() {
this.createdComponent();
},
methods: {
createdComponent() {
this.isLoading = false;
},
completion() {
if (this.isCompletion) {
this.isLoading = true;
this.VRPaymentTransactionCompletionService.createTransactionCompletion(
this.transactionData.transactions[0].metaData.salesChannelId,
this.transactionData.transactions[0].id
).then(() => {
this.createNotificationSuccess({
title: this.$tc('vrpayment-order.captureAction.successTitle'),
message: this.$tc('vrpayment-order.captureAction.successMessage')
});
this.isLoading = false;
this.$emit('modal-close');
this.$nextTick(() => {
this.$router.replace(`${this.$route.path}?hash=${Utils.createId()}`);
});
}).catch((errorResponse) => {
try {
this.createNotificationError({
title: errorResponse.response.data.errors[0].title,
message: errorResponse.response.data.errors[0].detail,
autoClose: false
});
} catch (e) {
this.createNotificationError({
title: errorResponse.title,
message: errorResponse.message,
autoClose: false
});
} finally {
this.isLoading = false;
this.$emit('modal-close');
this.$nextTick(() => {
this.$router.replace(`${this.$route.path}?hash=${Utils.createId()}`);
});
}
});
}
}
}
});
@@ -0,0 +1,26 @@
{% block vrpayment_order_action_refund_by_amount %}
<sw-modal variant="small"
:title="$tc(`vrpayment-order.modal.title.refund`)"
@modal-close="$emit('modal-close')">
{% block vrpayment_order_action_refund_amount_by_amount %}
<sw-number-field
:max="refundableAmount"
:min="0"
v-model:value="refundAmount"
:label="$tc('vrpayment-order.refund.refundAmount.label')"
:suffix="currency">
</sw-number-field>
{% endblock %}
{% block vrpayment_order_action_refund_confirm_button_by_amount %}
<template #modal-footer>
<sw-button variant="primary" @click="refundByAmount()">
{{ $tc('vrpayment-order.refundAction.confirmButton.text') }}
</sw-button>
</template>
{% endblock %}
<sw-loader v-if="isLoading"></sw-loader>
</sw-modal>
{% endblock %}
@@ -0,0 +1,94 @@
/* global Shopware */
import template from './index.html.twig';
const {Component, Mixin, Filter, Utils} = Shopware;
Component.register('vrpayment-order-action-refund-by-amount', {
template,
inject: ['VRPaymentRefundService'],
mixins: [
Mixin.getByName('notification')
],
props: {
transactionData: {
type: Object,
required: true
},
orderId: {
type: String,
required: true
}
},
data() {
return {
isLoading: true,
currency: this.transactionData.transactions[0].currency,
refundAmount: 0,
refundableAmount: 0,
};
},
computed: {
dateFilter() {
return Filter.getByName('date');
}
},
created() {
this.createdComponent();
},
methods: {
createdComponent() {
this.isLoading = false;
this.currency = this.transactionData.transactions[0].currency;
this.refundAmount = Number(this.transactionData.transactions[0].amountIncludingTax);
this.refundableAmount = Number(this.transactionData.transactions[0].amountIncludingTax);
},
refundByAmount() {
this.isLoading = true;
this.VRPaymentRefundService.createRefundByAmount(
this.transactionData.transactions[0].metaData.salesChannelId,
this.transactionData.transactions[0].id,
this.refundAmount
).then(() => {
this.createNotificationSuccess({
title: this.$tc('vrpayment-order.refundAction.successTitle'),
message: this.$tc('vrpayment-order.refundAction.successMessage')
});
this.isLoading = false;
this.$emit('modal-close');
this.$nextTick(() => {
this.$router.replace(`${this.$route.path}?hash=${Utils.createId()}`);
});
}).catch((errorResponse) => {
try {
this.createNotificationError({
title: errorResponse.response.data.errors[0].title,
message: errorResponse.response.data.errors[0].detail,
autoClose: false
});
} catch (e) {
this.createNotificationError({
title: errorResponse.title,
message: errorResponse.message,
autoClose: false
});
} finally {
this.isLoading = false;
this.$emit('modal-close');
this.$nextTick(() => {
this.$router.replace(`${this.$route.path}?hash=${Utils.createId()}`);
});
}
});
}
}
});
@@ -0,0 +1,31 @@
{% block vrpayment_order_action_refund_partial %}
<sw-modal variant="small"
:title="$tc(`vrpayment-order.modal.title.refund`)"
@modal-close="$emit('modal-close')">
{% block vrpayment_order_action_refund_amount_partial %}
<sw-number-field
:max="this.$parent.$parent.itemRefundableAmount"
:min="0.00"
v-model:value="refundAmount"
:label="$tc('vrpayment-order.refund.refundAmount.label')"
:suffix="currency">
</sw-number-field>
<div>
{{ $tc('vrpayment-order.refundAction.maxAvailableAmountToRefund') }}:
<b>{{ this.$parent.$parent.itemRefundableAmount }}</b>
</div>
{% endblock %}
{% block vrpayment_order_action_refund_confirm_button_partial %}
<template #modal-footer>
<sw-button variant="primary" @click="createPartialRefund(this.$parent.$parent.currentLineItem)">
{{ $tc('vrpayment-order.refundAction.confirmButton.text') }}
</sw-button>
</template>
{% endblock %}
<sw-loader v-if="isLoading"></sw-loader>
</sw-modal>
{% endblock %}
@@ -0,0 +1,101 @@
/* global Shopware */
import template from './index.html.twig';
const {Component, Mixin, Filter, Utils} = Shopware;
Component.register('vrpayment-order-action-refund-partial', {
template,
inject: ['VRPaymentRefundService'],
mixins: [
Mixin.getByName('notification')
],
props: {
transactionData: {
type: Object,
required: true
},
orderId: {
type: String,
required: true
}
},
data() {
return {
isLoading: true,
currency: this.transactionData.transactions[0].currency,
refundAmount: 0.00,
};
},
computed: {
dateFilter() {
return Filter.getByName('date');
}
},
created() {
this.createdComponent();
},
methods: {
createdComponent() {
this.isLoading = false;
this.currency = this.transactionData.transactions[0].currency;
this.refundAmount = this.$parent.$parent.itemRefundableAmount;
},
createPartialRefund(itemUniqueId) {
this.isLoading = true;
this.VRPaymentRefundService.createPartialRefund(
this.transactionData.transactions[0].metaData.salesChannelId,
this.transactionData.transactions[0].id,
this.refundAmount,
itemUniqueId
).then(() => {
this.createNotificationSuccess({
title: this.$tc('vrpayment-order.refundAction.successTitle'),
message: this.$tc('vrpayment-order.refundAction.successMessage')
});
this.isLoading = false;
this.$emit('modal-close');
this.$nextTick(() => {
this.$router.replace(`${this.$route.path}?hash=${Utils.createId()}`);
});
}).catch((errorResponse) => {
try {
this.createNotificationError({
title: errorResponse.response.data.errors[0].title,
message: errorResponse.response.data.errors[0].detail,
autoClose: false
});
} catch (e) {
this.createNotificationError({
title: errorResponse.title,
message: errorResponse.message,
autoClose: false
});
} finally {
this.isLoading = false;
this.$emit('modal-close');
this.$nextTick(() => {
this.$router.replace(`${this.$route.path}?hash=${Utils.createId()}`);
});
}
});
}
},
watch: {
refundAmount(newValue) {
if (newValue !== null) {
this.refundAmount = Math.round(newValue * 100) / 100;
}
}
}
});
@@ -0,0 +1,16 @@
{% block vrpayment_order_action_refund_selected %}
<sw-modal variant="small"
:title="$tc(`vrpayment-order.modal.title.refund`)"
@modal-close="$emit('modal-close')">
{% block vrpayment_order_action_refund_confirm_button_selected %}
<template #modal-footer>
<sw-button variant="primary" @click="refundSelected()">
{{ $tc('vrpayment-order.refundAction.confirmButton.text') }}
</sw-button>
</template>
{% endblock %}
<sw-loader v-if="isLoading"></sw-loader>
</sw-modal>
{% endblock %}
@@ -0,0 +1,94 @@
/* global Shopware */
import template from './index.html.twig';
const {Component, Mixin, Filter, Utils} = Shopware;
Component.register('vrpayment-order-action-refund-selected', {
template,
inject: ['VRPaymentRefundService'],
mixins: [
Mixin.getByName('notification')
],
props: {
transactionData: {
type: Object,
required: true
},
orderId: {
type: String,
required: true
}
},
data() {
return {
isLoading: true,
currency: this.transactionData.transactions[0].currency,
refundAmount: 0,
refundableAmount: 0,
};
},
computed: {
dateFilter() {
return Filter.getByName('date');
}
},
created() {
this.createdComponent();
},
methods: {
createdComponent() {
this.isLoading = false;
this.currency = this.transactionData.transactions[0].currency;
this.refundAmount = Number(this.transactionData.transactions[0].amountIncludingTax);
this.refundableAmount = Number(this.transactionData.transactions[0].amountIncludingTax);
},
refundSelected() {
this.isLoading = true;
this.VRPaymentRefundService.createRefundByAmount(
this.transactionData.transactions[0].metaData.salesChannelId,
this.transactionData.transactions[0].id,
this.refundAmount
).then(() => {
this.createNotificationSuccess({
title: this.$tc('vrpayment-order.refundAction.successTitle'),
message: this.$tc('vrpayment-order.refundAction.successMessage')
});
this.isLoading = false;
this.$emit('modal-close');
this.$nextTick(() => {
this.$router.replace(`${this.$route.path}?hash=${Utils.createId()}`);
});
}).catch((errorResponse) => {
try {
this.createNotificationError({
title: errorResponse.response.data.errors[0].title,
message: errorResponse.response.data.errors[0].detail,
autoClose: false
});
} catch (e) {
this.createNotificationError({
title: errorResponse.title,
message: errorResponse.message,
autoClose: false
});
} finally {
this.isLoading = false;
this.$emit('modal-close');
this.$nextTick(() => {
this.$router.replace(`${this.$route.path}?hash=${Utils.createId()}`);
});
}
});
}
}
});
@@ -0,0 +1,31 @@
{% block vrpayment_order_action_refund %}
<sw-modal variant="small"
:title="$tc(`vrpayment-order.modal.title.refund`)"
@modal-close="$emit('modal-close')">
{% block vrpayment_order_action_refund_amount %}
<sw-number-field
:max="this.$parent.$parent.itemRefundableQuantity"
:min="0"
v-model:value="refundQuantity"
:label="$tc('vrpayment-order.refund.refundQuantity.label')">
</sw-number-field>
<div>
{{ $tc('vrpayment-order.refundAction.maxAvailableItemsToRefund') }}:
<b>{{ this.$parent.$parent.itemRefundableQuantity }}</b>
</div>
{% endblock %}
{% block vrpayment_order_action_refund_confirm_button %}
<template #modal-footer>
<sw-button variant="primary" @click="refund()">
{{ $tc('vrpayment-order.refundAction.confirmButton.text') }}
</sw-button>
</template>
{% endblock %}
<sw-loader v-if="isLoading"></sw-loader>
</sw-modal>
{% endblock %}
@@ -0,0 +1,92 @@
/* global Shopware */
import template from './index.html.twig';
const {Component, Mixin, Filter, Utils} = Shopware;
Component.register('vrpayment-order-action-refund', {
template,
inject: ['VRPaymentRefundService'],
mixins: [
Mixin.getByName('notification')
],
props: {
transactionData: {
type: Object,
required: true
},
orderId: {
type: String,
required: true
}
},
data() {
return {
refundQuantity: 0,
isLoading: true,
currentLineItem: '',
};
},
computed: {
dateFilter() {
return Filter.getByName('date');
}
},
created() {
this.createdComponent();
},
methods: {
createdComponent() {
this.isLoading = false;
this.refundQuantity = 1;
},
refund() {
this.isLoading = true;
this.VRPaymentRefundService.createRefund(
this.transactionData.transactions[0].metaData.salesChannelId,
this.transactionData.transactions[0].id,
this.refundQuantity,
this.$parent.$parent.currentLineItem
).then(() => {
this.createNotificationSuccess({
title: this.$tc('vrpayment-order.refundAction.successTitle'),
message: this.$tc('vrpayment-order.refundAction.successMessage')
});
this.isLoading = false;
this.$emit('modal-close');
this.$nextTick(() => {
this.$router.replace(`${this.$route.path}?hash=${Utils.createId()}`);
});
}).catch((errorResponse) => {
try {
this.createNotificationError({
title: errorResponse.response.data.errors[0].title,
message: errorResponse.response.data.errors[0].detail,
autoClose: false
});
} catch (e) {
this.createNotificationError({
title: errorResponse.title,
message: errorResponse.message,
autoClose: false
});
} finally {
this.isLoading = false;
this.$emit('modal-close');
this.$nextTick(() => {
this.$router.replace(`${this.$route.path}?hash=${Utils.createId()}`);
});
}
});
}
}
});

Some files were not shown because too many files have changed in this diff Show More