diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..027c14a --- /dev/null +++ b/.eslintignore @@ -0,0 +1,2 @@ +src/Resources/app/storefront/dist/storefront/js/* +src/Resources/public/administration/js/* \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..c1e313b --- /dev/null +++ b/CHANGELOG.md @@ -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 diff --git a/CHANGELOG_de-DE.md b/CHANGELOG_de-DE.md new file mode 100644 index 0000000..c42f624 --- /dev/null +++ b/CHANGELOG_de-DE.md @@ -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 diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..cb82feb --- /dev/null +++ b/LICENSE.txt @@ -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. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..8272272 --- /dev/null +++ b/README.md @@ -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. diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..0a0cf7f --- /dev/null +++ b/composer.json @@ -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" +} \ No newline at end of file diff --git a/docs/en/assets/base.css b/docs/en/assets/base.css new file mode 100644 index 0000000..bb25826 --- /dev/null +++ b/docs/en/assets/base.css @@ -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; + } +} diff --git a/docs/en/assets/base.js b/docs/en/assets/base.js new file mode 100644 index 0000000..df5c13f --- /dev/null +++ b/docs/en/assets/base.js @@ -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); diff --git a/docs/en/assets/highlight.js b/docs/en/assets/highlight.js new file mode 100644 index 0000000..9a30d5f --- /dev/null +++ b/docs/en/assets/highlight.js @@ -0,0 +1,6 @@ +/* + Highlight.js 10.0.3 (a4b1bd2d) + License: BSD-3-Clause + Copyright (c) 2006-2020, Ivan Sagalaev +*/ +var hljs=function(){"use strict";function e(n){Object.freeze(n);var t="function"==typeof n;return Object.getOwnPropertyNames(n).forEach((function(r){!n.hasOwnProperty(r)||null===n[r]||"object"!=typeof n[r]&&"function"!=typeof n[r]||t&&("caller"===r||"callee"===r||"arguments"===r)||Object.isFrozen(n[r])||e(n[r])})),n}function n(e){return e.replace(/&/g,"&").replace(//g,">")}function t(e){var n,t={},r=Array.prototype.slice.call(arguments,1);for(n in e)t[n]=e[n];return r.forEach((function(e){for(n in e)t[n]=e[n]})),t}function r(e){return e.nodeName.toLowerCase()}var a=Object.freeze({__proto__:null,escapeHTML:n,inherit:t,nodeStream:function(e){var n=[];return function e(t,a){for(var i=t.firstChild;i;i=i.nextSibling)3===i.nodeType?a+=i.nodeValue.length:1===i.nodeType&&(n.push({event:"start",offset:a,node:i}),a=e(i,a),r(i).match(/br|hr|img|input/)||n.push({event:"stop",offset:a,node:i}));return a}(e,0),n},mergeStreams:function(e,t,a){var i=0,s="",o=[];function l(){return e.length&&t.length?e[0].offset!==t[0].offset?e[0].offset"}function u(e){s+=""}function d(e){("start"===e.event?c:u)(e.node)}for(;e.length||t.length;){var g=l();if(s+=n(a.substring(i,g[0].offset)),i=g[0].offset,g===e){o.reverse().forEach(u);do{d(g.splice(0,1)[0]),g=l()}while(g===e&&g.length&&g[0].offset===i);o.reverse().forEach(c)}else"start"===g[0].event?o.push(g[0].node):o.pop(),d(g.splice(0,1)[0])}return s+n(a.substr(i))}});const i="",s=e=>!!e.kind;class o{constructor(e,n){this.buffer="",this.classPrefix=n.classPrefix,e.walk(this)}addText(e){this.buffer+=n(e)}openNode(e){if(!s(e))return;let n=e.kind;e.sublanguage||(n=`${this.classPrefix}${n}`),this.span(n)}closeNode(e){s(e)&&(this.buffer+=i)}span(e){this.buffer+=``}value(){return this.buffer}}class l{constructor(){this.rootNode={children:[]},this.stack=[this.rootNode]}get top(){return this.stack[this.stack.length-1]}get root(){return this.rootNode}add(e){this.top.children.push(e)}openNode(e){let n={kind:e,children:[]};this.add(n),this.stack.push(n)}closeNode(){if(this.stack.length>1)return this.stack.pop()}closeAllNodes(){for(;this.closeNode(););}toJSON(){return JSON.stringify(this.rootNode,null,4)}walk(e){return this.constructor._walk(e,this.rootNode)}static _walk(e,n){return"string"==typeof n?e.addText(n):n.children&&(e.openNode(n),n.children.forEach(n=>this._walk(e,n)),e.closeNode(n)),e}static _collapse(e){e.children&&(e.children.every(e=>"string"==typeof e)?(e.text=e.children.join(""),delete e.children):e.children.forEach(e=>{"string"!=typeof e&&l._collapse(e)}))}}class c extends l{constructor(e){super(),this.options=e}addKeyword(e,n){""!==e&&(this.openNode(n),this.addText(e),this.closeNode())}addText(e){""!==e&&this.add(e)}addSublanguage(e,n){let t=e.root;t.kind=n,t.sublanguage=!0,this.add(t)}toHTML(){return new o(this,this.options).value()}finalize(){}}function u(e){return e&&e.source||e}const d="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",g={begin:"\\\\[\\s\\S]",relevance:0},h={className:"string",begin:"'",end:"'",illegal:"\\n",contains:[g]},f={className:"string",begin:'"',end:'"',illegal:"\\n",contains:[g]},p={begin:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/},m=function(e,n,r){var a=t({className:"comment",begin:e,end:n,contains:[]},r||{});return a.contains.push(p),a.contains.push({className:"doctag",begin:"(?:TODO|FIXME|NOTE|BUG|XXX):",relevance:0}),a},b=m("//","$"),v=m("/\\*","\\*/"),x=m("#","$");var _=Object.freeze({__proto__:null,IDENT_RE:"[a-zA-Z]\\w*",UNDERSCORE_IDENT_RE:"[a-zA-Z_]\\w*",NUMBER_RE:"\\b\\d+(\\.\\d+)?",C_NUMBER_RE:d,BINARY_NUMBER_RE:"\\b(0b[01]+)",RE_STARTERS_RE:"!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~",BACKSLASH_ESCAPE:g,APOS_STRING_MODE:h,QUOTE_STRING_MODE:f,PHRASAL_WORDS_MODE:p,COMMENT:m,C_LINE_COMMENT_MODE:b,C_BLOCK_COMMENT_MODE:v,HASH_COMMENT_MODE:x,NUMBER_MODE:{className:"number",begin:"\\b\\d+(\\.\\d+)?",relevance:0},C_NUMBER_MODE:{className:"number",begin:d,relevance:0},BINARY_NUMBER_MODE:{className:"number",begin:"\\b(0b[01]+)",relevance:0},CSS_NUMBER_MODE:{className:"number",begin:"\\b\\d+(\\.\\d+)?(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?",relevance:0},REGEXP_MODE:{begin:/(?=\/[^\/\n]*\/)/,contains:[{className:"regexp",begin:/\//,end:/\/[gimuy]*/,illegal:/\n/,contains:[g,{begin:/\[/,end:/\]/,relevance:0,contains:[g]}]}]},TITLE_MODE:{className:"title",begin:"[a-zA-Z]\\w*",relevance:0},UNDERSCORE_TITLE_MODE:{className:"title",begin:"[a-zA-Z_]\\w*",relevance:0},METHOD_GUARD:{begin:"\\.\\s*[a-zA-Z_]\\w*",relevance:0}}),E="of and for in not or if then".split(" ");function R(e,n){return n?+n:(t=e,E.includes(t.toLowerCase())?0:1);var t}const N=n,w=t,{nodeStream:y,mergeStreams:O}=a;return function(n){var r=[],a={},i={},s=[],o=!0,l=/((^(<[^>]+>|\t|)+|(?:\n)))/gm,d="Could not find the language '{}', did you forget to load/include a language module?",g={noHighlightRe:/^(no-?highlight)$/i,languageDetectRe:/\blang(?:uage)?-([\w-]+)\b/i,classPrefix:"hljs-",tabReplace:null,useBR:!1,languages:void 0,__emitter:c};function h(e){return g.noHighlightRe.test(e)}function f(e,n,t,r){var a={code:n,language:e};T("before:highlight",a);var i=a.result?a.result:p(a.language,a.code,t,r);return i.code=a.code,T("after:highlight",i),i}function p(e,n,r,i){var s=n;function l(e,n){var t=v.case_insensitive?n[0].toLowerCase():n[0];return e.keywords.hasOwnProperty(t)&&e.keywords[t]}function c(){null!=_.subLanguage?function(){if(""!==k){var e="string"==typeof _.subLanguage;if(!e||a[_.subLanguage]){var n=e?p(_.subLanguage,k,!0,E[_.subLanguage]):m(k,_.subLanguage.length?_.subLanguage:void 0);_.relevance>0&&(T+=n.relevance),e&&(E[_.subLanguage]=n.top),w.addSublanguage(n.emitter,n.language)}else w.addText(k)}}():function(){var e,n,t,r;if(_.keywords){for(n=0,_.lexemesRe.lastIndex=0,t=_.lexemesRe.exec(k),r="";t;){r+=k.substring(n,t.index);var a=null;(e=l(_,t))?(w.addText(r),r="",T+=e[1],a=e[0],w.addKeyword(t[0],a)):r+=t[0],n=_.lexemesRe.lastIndex,t=_.lexemesRe.exec(k)}r+=k.substr(n),w.addText(r)}else w.addText(k)}(),k=""}function h(e){e.className&&w.openNode(e.className),_=Object.create(e,{parent:{value:_}})}var f={};function b(n,t){var a,i=t&&t[0];if(k+=n,null==i)return c(),0;if("begin"==f.type&&"end"==t.type&&f.index==t.index&&""===i){if(k+=s.slice(t.index,t.index+1),!o)throw(a=Error("0 width match regex")).languageName=e,a.badRule=f.rule,a;return 1}if(f=t,"begin"===t.type)return function(e){var n=e[0],t=e.rule;return t.__onBegin&&(t.__onBegin(e)||{}).ignoreMatch?function(e){return 0===_.matcher.regexIndex?(k+=e[0],1):(B=!0,0)}(n):(t&&t.endSameAsBegin&&(t.endRe=RegExp(n.replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&"),"m")),t.skip?k+=n:(t.excludeBegin&&(k+=n),c(),t.returnBegin||t.excludeBegin||(k=n)),h(t),t.returnBegin?0:n.length)}(t);if("illegal"===t.type&&!r)throw(a=Error('Illegal lexeme "'+i+'" for mode "'+(_.className||"")+'"')).mode=_,a;if("end"===t.type){var l=function(e){var n=e[0],t=s.substr(e.index),r=function e(n,t){if(function(e,n){var t=e&&e.exec(n);return t&&0===t.index}(n.endRe,t)){for(;n.endsParent&&n.parent;)n=n.parent;return n}if(n.endsWithParent)return e(n.parent,t)}(_,t);if(r){var a=_;a.skip?k+=n:(a.returnEnd||a.excludeEnd||(k+=n),c(),a.excludeEnd&&(k=n));do{_.className&&w.closeNode(),_.skip||_.subLanguage||(T+=_.relevance),_=_.parent}while(_!==r.parent);return r.starts&&(r.endSameAsBegin&&(r.starts.endRe=r.endRe),h(r.starts)),a.returnEnd?0:n.length}}(t);if(null!=l)return l}if("illegal"===t.type&&""===i)return 1;if(A>1e5&&A>3*t.index)throw Error("potential infinite loop, way more iterations than matches");return k+=i,i.length}var v=M(e);if(!v)throw console.error(d.replace("{}",e)),Error('Unknown language: "'+e+'"');!function(e){function n(n,t){return RegExp(u(n),"m"+(e.case_insensitive?"i":"")+(t?"g":""))}class r{constructor(){this.matchIndexes={},this.regexes=[],this.matchAt=1,this.position=0}addRule(e,n){n.position=this.position++,this.matchIndexes[this.matchAt]=n,this.regexes.push([n,e]),this.matchAt+=function(e){return RegExp(e.toString()+"|").exec("").length-1}(e)+1}compile(){0===this.regexes.length&&(this.exec=()=>null);let e=this.regexes.map(e=>e[1]);this.matcherRe=n(function(e,n){for(var t=/\[(?:[^\\\]]|\\.)*\]|\(\??|\\([1-9][0-9]*)|\\./,r=0,a="",i=0;i0&&(a+="|"),a+="(";o.length>0;){var l=t.exec(o);if(null==l){a+=o;break}a+=o.substring(0,l.index),o=o.substring(l.index+l[0].length),"\\"==l[0][0]&&l[1]?a+="\\"+(+l[1]+s):(a+=l[0],"("==l[0]&&r++)}a+=")"}return a}(e),!0),this.lastIndex=0}exec(e){this.matcherRe.lastIndex=this.lastIndex;let n=this.matcherRe.exec(e);if(!n)return null;let t=n.findIndex((e,n)=>n>0&&null!=e),r=this.matchIndexes[t];return Object.assign(n,r)}}class a{constructor(){this.rules=[],this.multiRegexes=[],this.count=0,this.lastIndex=0,this.regexIndex=0}getMatcher(e){if(this.multiRegexes[e])return this.multiRegexes[e];let n=new r;return this.rules.slice(e).forEach(([e,t])=>n.addRule(e,t)),n.compile(),this.multiRegexes[e]=n,n}considerAll(){this.regexIndex=0}addRule(e,n){this.rules.push([e,n]),"begin"===n.type&&this.count++}exec(e){let n=this.getMatcher(this.regexIndex);n.lastIndex=this.lastIndex;let t=n.exec(e);return t&&(this.regexIndex+=t.position+1,this.regexIndex===this.count&&(this.regexIndex=0)),t}}function i(e){let n=e.input[e.index-1],t=e.input[e.index+e[0].length];if("."===n||"."===t)return{ignoreMatch:!0}}if(e.contains&&e.contains.includes("self"))throw Error("ERR: contains `self` is not supported at the top-level of a language. See documentation.");!function r(s,o){s.compiled||(s.compiled=!0,s.__onBegin=null,s.keywords=s.keywords||s.beginKeywords,s.keywords&&(s.keywords=function(e,n){var t={};return"string"==typeof e?r("keyword",e):Object.keys(e).forEach((function(n){r(n,e[n])})),t;function r(e,r){n&&(r=r.toLowerCase()),r.split(" ").forEach((function(n){var r=n.split("|");t[r[0]]=[e,R(r[0],r[1])]}))}}(s.keywords,e.case_insensitive)),s.lexemesRe=n(s.lexemes||/\w+/,!0),o&&(s.beginKeywords&&(s.begin="\\b("+s.beginKeywords.split(" ").join("|")+")(?=\\b|\\s)",s.__onBegin=i),s.begin||(s.begin=/\B|\b/),s.beginRe=n(s.begin),s.endSameAsBegin&&(s.end=s.begin),s.end||s.endsWithParent||(s.end=/\B|\b/),s.end&&(s.endRe=n(s.end)),s.terminator_end=u(s.end)||"",s.endsWithParent&&o.terminator_end&&(s.terminator_end+=(s.end?"|":"")+o.terminator_end)),s.illegal&&(s.illegalRe=n(s.illegal)),null==s.relevance&&(s.relevance=1),s.contains||(s.contains=[]),s.contains=[].concat(...s.contains.map((function(e){return function(e){return e.variants&&!e.cached_variants&&(e.cached_variants=e.variants.map((function(n){return t(e,{variants:null},n)}))),e.cached_variants?e.cached_variants:function e(n){return!!n&&(n.endsWithParent||e(n.starts))}(e)?t(e,{starts:e.starts?t(e.starts):null}):Object.isFrozen(e)?t(e):e}("self"===e?s:e)}))),s.contains.forEach((function(e){r(e,s)})),s.starts&&r(s.starts,o),s.matcher=function(e){let n=new a;return e.contains.forEach(e=>n.addRule(e.begin,{rule:e,type:"begin"})),e.terminator_end&&n.addRule(e.terminator_end,{type:"end"}),e.illegal&&n.addRule(e.illegal,{type:"illegal"}),n}(s))}(e)}(v);var x,_=i||v,E={},w=new g.__emitter(g);!function(){for(var e=[],n=_;n!==v;n=n.parent)n.className&&e.unshift(n.className);e.forEach(e=>w.openNode(e))}();var y,O,k="",T=0,L=0,A=0,B=!1;try{for(_.matcher.considerAll();A++,B?B=!1:(_.matcher.lastIndex=L,_.matcher.considerAll()),y=_.matcher.exec(s);)O=b(s.substring(L,y.index),y),L=y.index+O;return b(s.substr(L)),w.closeAllNodes(),w.finalize(),x=w.toHTML(),{relevance:T,value:x,language:e,illegal:!1,emitter:w,top:_}}catch(n){if(n.message&&n.message.includes("Illegal"))return{illegal:!0,illegalBy:{msg:n.message,context:s.slice(L-100,L+100),mode:n.mode},sofar:x,relevance:0,value:N(s),emitter:w};if(o)return{relevance:0,value:N(s),emitter:w,language:e,top:_,errorRaised:n};throw n}}function m(e,n){n=n||g.languages||Object.keys(a);var t=function(e){const n={relevance:0,emitter:new g.__emitter(g),value:N(e),illegal:!1,top:E};return n.emitter.addText(e),n}(e),r=t;return n.filter(M).filter(k).forEach((function(n){var a=p(n,e,!1);a.language=n,a.relevance>r.relevance&&(r=a),a.relevance>t.relevance&&(r=t,t=a)})),r.language&&(t.second_best=r),t}function b(e){return g.tabReplace||g.useBR?e.replace(l,(function(e,n){return g.useBR&&"\n"===e?"
":g.tabReplace?n.replace(/\t/g,g.tabReplace):""})):e}function v(e){var n,t,r,a,s,o=function(e){var n,t=e.className+" ";if(t+=e.parentNode?e.parentNode.className:"",n=g.languageDetectRe.exec(t)){var r=M(n[1]);return r||(console.warn(d.replace("{}",n[1])),console.warn("Falling back to no-highlight mode for this block.",e)),r?n[1]:"no-highlight"}return t.split(/\s+/).find(e=>h(e)||M(e))}(e);h(o)||(T("before:highlightBlock",{block:e,language:o}),g.useBR?(n=document.createElement("div")).innerHTML=e.innerHTML.replace(/\n/g,"").replace(//g,"\n"):n=e,s=n.textContent,r=o?f(o,s,!0):m(s),(t=y(n)).length&&((a=document.createElement("div")).innerHTML=r.value,r.value=O(t,y(a),s)),r.value=b(r.value),T("after:highlightBlock",{block:e,result:r}),e.innerHTML=r.value,e.className=function(e,n,t){var r=n?i[n]:t,a=[e.trim()];return e.match(/\bhljs\b/)||a.push("hljs"),e.includes(r)||a.push(r),a.join(" ").trim()}(e.className,o,r.language),e.result={language:r.language,re:r.relevance},r.second_best&&(e.second_best={language:r.second_best.language,re:r.second_best.relevance}))}function x(){if(!x.called){x.called=!0;var e=document.querySelectorAll("pre code");r.forEach.call(e,v)}}const E={disableAutodetect:!0,name:"Plain text"};function M(e){return e=(e||"").toLowerCase(),a[e]||a[i[e]]}function k(e){var n=M(e);return n&&!n.disableAutodetect}function T(e,n){var t=e;s.forEach((function(e){e[t]&&e[t](n)}))}Object.assign(n,{highlight:f,highlightAuto:m,fixMarkup:b,highlightBlock:v,configure:function(e){g=w(g,e)},initHighlighting:x,initHighlightingOnLoad:function(){window.addEventListener("DOMContentLoaded",x,!1)},registerLanguage:function(e,t){var r;try{r=t(n)}catch(n){if(console.error("Language definition for '{}' could not be registered.".replace("{}",e)),!o)throw n;console.error(n),r=E}r.name||(r.name=e),a[e]=r,r.rawDefinition=t.bind(null,n),r.aliases&&r.aliases.forEach((function(n){i[n]=e}))},listLanguages:function(){return Object.keys(a)},getLanguage:M,requireLanguage:function(e){var n=M(e);if(n)return n;throw Error("The '{}' language is required, but not loaded.".replace("{}",e))},autoDetection:k,inherit:w,addPlugin:function(e,n){s.push(e)}}),n.debugMode=function(){o=!1},n.safeMode=function(){o=!0},n.versionString="10.0.3";for(const n in _)"object"==typeof _[n]&&e(_[n]);return Object.assign(n,_),n}({})}();"object"==typeof exports&&"undefined"!=typeof module&&(module.exports=hljs);hljs.registerLanguage("css",function(){"use strict";return function(e){var n={begin:/(?:[A-Z\_\.\-]+|--[a-zA-Z0-9_-]+)\s*:/,returnBegin:!0,end:";",endsWithParent:!0,contains:[{className:"attribute",begin:/\S/,end:":",excludeEnd:!0,starts:{endsWithParent:!0,excludeEnd:!0,contains:[{begin:/[\w-]+\(/,returnBegin:!0,contains:[{className:"built_in",begin:/[\w-]+/},{begin:/\(/,end:/\)/,contains:[e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,e.CSS_NUMBER_MODE]}]},e.CSS_NUMBER_MODE,e.QUOTE_STRING_MODE,e.APOS_STRING_MODE,e.C_BLOCK_COMMENT_MODE,{className:"number",begin:"#[0-9A-Fa-f]+"},{className:"meta",begin:"!important"}]}}]};return{name:"CSS",case_insensitive:!0,illegal:/[=\/|'\$]/,contains:[e.C_BLOCK_COMMENT_MODE,{className:"selector-id",begin:/#[A-Za-z0-9_-]+/},{className:"selector-class",begin:/\.[A-Za-z0-9_-]+/},{className:"selector-attr",begin:/\[/,end:/\]/,illegal:"$",contains:[e.APOS_STRING_MODE,e.QUOTE_STRING_MODE]},{className:"selector-pseudo",begin:/:(:)?[a-zA-Z0-9\_\-\+\(\)"'.]+/},{begin:"@(page|font-face)",lexemes:"@[a-z-]+",keywords:"@page @font-face"},{begin:"@",end:"[{;]",illegal:/:/,returnBegin:!0,contains:[{className:"keyword",begin:/@\-?\w[\w]*(\-\w+)*/},{begin:/\s/,endsWithParent:!0,excludeEnd:!0,relevance:0,keywords:"and or not only",contains:[{begin:/[a-z-]+:/,className:"attribute"},e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,e.CSS_NUMBER_MODE]}]},{className:"selector-tag",begin:"[a-zA-Z-][a-zA-Z0-9_-]*",relevance:0},{begin:"{",end:"}",illegal:/\S/,contains:[e.C_BLOCK_COMMENT_MODE,n]}]}}}());hljs.registerLanguage("bash",function(){"use strict";return function(e){const s={};Object.assign(s,{className:"variable",variants:[{begin:/\$[\w\d#@][\w\d_]*/},{begin:/\$\{/,end:/\}/,contains:[{begin:/:-/,contains:[s]}]}]});const n={className:"subst",begin:/\$\(/,end:/\)/,contains:[e.BACKSLASH_ESCAPE]},t={className:"string",begin:/"/,end:/"/,contains:[e.BACKSLASH_ESCAPE,s,n]};n.contains.push(t);const a={begin:/\$\(\(/,end:/\)\)/,contains:[{begin:/\d+#[0-9a-f]+/,className:"number"},e.NUMBER_MODE,s]};return{name:"Bash",aliases:["sh","zsh"],lexemes:/\b-?[a-z\._]+\b/,keywords:{keyword:"if then else elif fi for while in do done case esac function",literal:"true false",built_in:"break cd continue eval exec exit export getopts hash pwd readonly return shift test times trap umask unset alias bind builtin caller command declare echo enable help let local logout mapfile printf read readarray source type typeset ulimit unalias set shopt autoload bg bindkey bye cap chdir clone comparguments compcall compctl compdescribe compfiles compgroups compquote comptags comptry compvalues dirs disable disown echotc echoti emulate fc fg float functions getcap getln history integer jobs kill limit log noglob popd print pushd pushln rehash sched setcap setopt stat suspend ttyctl unfunction unhash unlimit unsetopt vared wait whence where which zcompile zformat zftp zle zmodload zparseopts zprof zpty zregexparse zsocket zstyle ztcp",_:"-ne -eq -lt -gt -f -d -e -s -l -a"},contains:[{className:"meta",begin:/^#![^\n]+sh\s*$/,relevance:10},{className:"function",begin:/\w[\w\d_]*\s*\(\s*\)\s*\{/,returnBegin:!0,contains:[e.inherit(e.TITLE_MODE,{begin:/\w[\w\d_]*/})],relevance:0},a,e.HASH_COMMENT_MODE,t,{className:"",begin:/\\"/},{className:"string",begin:/'/,end:/'/},s]}}}());hljs.registerLanguage("xml",function(){"use strict";return function(e){var n={className:"symbol",begin:"&[a-z]+;|&#[0-9]+;|&#x[a-f0-9]+;"},a={begin:"\\s",contains:[{className:"meta-keyword",begin:"#?[a-z_][a-z1-9_-]+",illegal:"\\n"}]},s=e.inherit(a,{begin:"\\(",end:"\\)"}),t=e.inherit(e.APOS_STRING_MODE,{className:"meta-string"}),i=e.inherit(e.QUOTE_STRING_MODE,{className:"meta-string"}),c={endsWithParent:!0,illegal:/`]+/}]}]}]};return{name:"HTML, XML",aliases:["html","xhtml","rss","atom","xjb","xsd","xsl","plist","wsf","svg"],case_insensitive:!0,contains:[{className:"meta",begin:"",relevance:10,contains:[a,i,t,s,{begin:"\\[",end:"\\]",contains:[{className:"meta",begin:"",contains:[a,s,i,t]}]}]},e.COMMENT("\x3c!--","--\x3e",{relevance:10}),{begin:"<\\!\\[CDATA\\[",end:"\\]\\]>",relevance:10},n,{className:"meta",begin:/<\?xml/,end:/\?>/,relevance:10},{className:"tag",begin:")",end:">",keywords:{name:"style"},contains:[c],starts:{end:"",returnEnd:!0,subLanguage:["css","xml"]}},{className:"tag",begin:")",end:">",keywords:{name:"script"},contains:[c],starts:{end:"<\/script>",returnEnd:!0,subLanguage:["javascript","handlebars","xml"]}},{className:"tag",begin:"",contains:[{className:"name",begin:/[^\/><\s]+/,relevance:0},c]}]}}}());hljs.registerLanguage("php",function(){"use strict";return function(e){var r={begin:"\\$+[a-zA-Z_-ÿ][a-zA-Z0-9_-ÿ]*"},t={className:"meta",variants:[{begin:/<\?php/,relevance:10},{begin:/<\?[=]?/},{begin:/\?>/}]},a={className:"string",contains:[e.BACKSLASH_ESCAPE,t],variants:[{begin:'b"',end:'"'},{begin:"b'",end:"'"},e.inherit(e.APOS_STRING_MODE,{illegal:null}),e.inherit(e.QUOTE_STRING_MODE,{illegal:null})]},n={variants:[e.BINARY_NUMBER_MODE,e.C_NUMBER_MODE]},i={keyword:"__CLASS__ __DIR__ __FILE__ __FUNCTION__ __LINE__ __METHOD__ __NAMESPACE__ __TRAIT__ die echo exit include include_once print require require_once array abstract and as binary bool boolean break callable case catch class clone const continue declare default do double else elseif empty enddeclare endfor endforeach endif endswitch endwhile eval extends final finally float for foreach from global goto if implements instanceof insteadof int integer interface isset iterable list new object or private protected public real return string switch throw trait try unset use var void while xor yield",literal:"false null true",built_in:"Error|0 AppendIterator ArgumentCountError ArithmeticError ArrayIterator ArrayObject AssertionError BadFunctionCallException BadMethodCallException CachingIterator CallbackFilterIterator CompileError Countable DirectoryIterator DivisionByZeroError DomainException EmptyIterator ErrorException Exception FilesystemIterator FilterIterator GlobIterator InfiniteIterator InvalidArgumentException IteratorIterator LengthException LimitIterator LogicException MultipleIterator NoRewindIterator OutOfBoundsException OutOfRangeException OuterIterator OverflowException ParentIterator ParseError RangeException RecursiveArrayIterator RecursiveCachingIterator RecursiveCallbackFilterIterator RecursiveDirectoryIterator RecursiveFilterIterator RecursiveIterator RecursiveIteratorIterator RecursiveRegexIterator RecursiveTreeIterator RegexIterator RuntimeException SeekableIterator SplDoublyLinkedList SplFileInfo SplFileObject SplFixedArray SplHeap SplMaxHeap SplMinHeap SplObjectStorage SplObserver SplObserver SplPriorityQueue SplQueue SplStack SplSubject SplSubject SplTempFileObject TypeError UnderflowException UnexpectedValueException ArrayAccess Closure Generator Iterator IteratorAggregate Serializable Throwable Traversable WeakReference Directory __PHP_Incomplete_Class parent php_user_filter self static stdClass"};return{aliases:["php","php3","php4","php5","php6","php7"],case_insensitive:!0,keywords:i,contains:[e.HASH_COMMENT_MODE,e.COMMENT("//","$",{contains:[t]}),e.COMMENT("/\\*","\\*/",{contains:[{className:"doctag",begin:"@[A-Za-z]+"}]}),e.COMMENT("__halt_compiler.+?;",!1,{endsWithParent:!0,keywords:"__halt_compiler",lexemes:e.UNDERSCORE_IDENT_RE}),{className:"string",begin:/<<<['"]?\w+['"]?$/,end:/^\w+;?$/,contains:[e.BACKSLASH_ESCAPE,{className:"subst",variants:[{begin:/\$\w+/},{begin:/\{\$/,end:/\}/}]}]},t,{className:"keyword",begin:/\$this\b/},r,{begin:/(::|->)+[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/},{className:"function",beginKeywords:"fn function",end:/[;{]/,excludeEnd:!0,illegal:"[$%\\[]",contains:[e.UNDERSCORE_TITLE_MODE,{className:"params",begin:"\\(",end:"\\)",excludeBegin:!0,excludeEnd:!0,keywords:i,contains:["self",r,e.C_BLOCK_COMMENT_MODE,a,n]}]},{className:"class",beginKeywords:"class interface",end:"{",excludeEnd:!0,illegal:/[:\(\$"]/,contains:[{beginKeywords:"extends implements"},e.UNDERSCORE_TITLE_MODE]},{beginKeywords:"namespace",end:";",illegal:/[\.']/,contains:[e.UNDERSCORE_TITLE_MODE]},{beginKeywords:"use",end:";",contains:[e.UNDERSCORE_TITLE_MODE]},{begin:"=>"},a,n]}}}());hljs.registerLanguage("php-template",function(){"use strict";return function(n){return{name:"PHP template",subLanguage:"xml",contains:[{begin:/<\?(php|=)?/,end:/\?>/,subLanguage:"php",contains:[{begin:"/\\*",end:"\\*/",skip:!0},{begin:'b"',end:'"',skip:!0},{begin:"b'",end:"'",skip:!0},n.inherit(n.APOS_STRING_MODE,{illegal:null,className:null,contains:null,skip:!0}),n.inherit(n.QUOTE_STRING_MODE,{illegal:null,className:null,contains:null,skip:!0})]}]}}}());hljs.registerLanguage("sql",function(){"use strict";return function(e){var t=e.COMMENT("--","$");return{name:"SQL",case_insensitive:!0,illegal:/[<>{}*]/,contains:[{beginKeywords:"begin end start commit rollback savepoint lock alter create drop rename call delete do handler insert load replace select truncate update set show pragma grant merge describe use explain help declare prepare execute deallocate release unlock purge reset change stop analyze cache flush optimize repair kill install uninstall checksum restore check backup revoke comment values with",end:/;/,endsWithParent:!0,lexemes:/[\w\.]+/,keywords:{keyword:"as abort abs absolute acc acce accep accept access accessed accessible account acos action activate add addtime admin administer advanced advise aes_decrypt aes_encrypt after agent aggregate ali alia alias all allocate allow alter always analyze ancillary and anti any anydata anydataset anyschema anytype apply archive archived archivelog are as asc ascii asin assembly assertion associate asynchronous at atan atn2 attr attri attrib attribu attribut attribute attributes audit authenticated authentication authid authors auto autoallocate autodblink autoextend automatic availability avg backup badfile basicfile before begin beginning benchmark between bfile bfile_base big bigfile bin binary_double binary_float binlog bit_and bit_count bit_length bit_or bit_xor bitmap blob_base block blocksize body both bound bucket buffer_cache buffer_pool build bulk by byte byteordermark bytes cache caching call calling cancel capacity cascade cascaded case cast catalog category ceil ceiling chain change changed char_base char_length character_length characters characterset charindex charset charsetform charsetid check checksum checksum_agg child choose chr chunk class cleanup clear client clob clob_base clone close cluster_id cluster_probability cluster_set clustering coalesce coercibility col collate collation collect colu colum column column_value columns columns_updated comment commit compact compatibility compiled complete composite_limit compound compress compute concat concat_ws concurrent confirm conn connec connect connect_by_iscycle connect_by_isleaf connect_by_root connect_time connection consider consistent constant constraint constraints constructor container content contents context contributors controlfile conv convert convert_tz corr corr_k corr_s corresponding corruption cos cost count count_big counted covar_pop covar_samp cpu_per_call cpu_per_session crc32 create creation critical cross cube cume_dist curdate current current_date current_time current_timestamp current_user cursor curtime customdatum cycle data database databases datafile datafiles datalength date_add date_cache date_format date_sub dateadd datediff datefromparts datename datepart datetime2fromparts day day_to_second dayname dayofmonth dayofweek dayofyear days db_role_change dbtimezone ddl deallocate declare decode decompose decrement decrypt deduplicate def defa defau defaul default defaults deferred defi defin define degrees delayed delegate delete delete_all delimited demand dense_rank depth dequeue des_decrypt des_encrypt des_key_file desc descr descri describ describe descriptor deterministic diagnostics difference dimension direct_load directory disable disable_all disallow disassociate discardfile disconnect diskgroup distinct distinctrow distribute distributed div do document domain dotnet double downgrade drop dumpfile duplicate duration each edition editionable editions element ellipsis else elsif elt empty enable enable_all enclosed encode encoding encrypt end end-exec endian enforced engine engines enqueue enterprise entityescaping eomonth error errors escaped evalname evaluate event eventdata events except exception exceptions exchange exclude excluding execu execut execute exempt exists exit exp expire explain explode export export_set extended extent external external_1 external_2 externally extract failed failed_login_attempts failover failure far fast feature_set feature_value fetch field fields file file_name_convert filesystem_like_logging final finish first first_value fixed flash_cache flashback floor flush following follows for forall force foreign form forma format found found_rows freelist freelists freepools fresh from from_base64 from_days ftp full function general generated get get_format get_lock getdate getutcdate global global_name globally go goto grant grants greatest group group_concat group_id grouping grouping_id groups gtid_subtract guarantee guard handler hash hashkeys having hea head headi headin heading heap help hex hierarchy high high_priority hosts hour hours http id ident_current ident_incr ident_seed identified identity idle_time if ifnull ignore iif ilike ilm immediate import in include including increment index indexes indexing indextype indicator indices inet6_aton inet6_ntoa inet_aton inet_ntoa infile initial initialized initially initrans inmemory inner innodb input insert install instance instantiable instr interface interleaved intersect into invalidate invisible is is_free_lock is_ipv4 is_ipv4_compat is_not is_not_null is_used_lock isdate isnull isolation iterate java join json json_exists keep keep_duplicates key keys kill language large last last_day last_insert_id last_value lateral lax lcase lead leading least leaves left len lenght length less level levels library like like2 like4 likec limit lines link list listagg little ln load load_file lob lobs local localtime localtimestamp locate locator lock locked log log10 log2 logfile logfiles logging logical logical_reads_per_call logoff logon logs long loop low low_priority lower lpad lrtrim ltrim main make_set makedate maketime managed management manual map mapping mask master master_pos_wait match matched materialized max maxextents maximize maxinstances maxlen maxlogfiles maxloghistory maxlogmembers maxsize maxtrans md5 measures median medium member memcompress memory merge microsecond mid migration min minextents minimum mining minus minute minutes minvalue missing mod mode model modification modify module monitoring month months mount move movement multiset mutex name name_const names nan national native natural nav nchar nclob nested never new newline next nextval no no_write_to_binlog noarchivelog noaudit nobadfile nocheck nocompress nocopy nocycle nodelay nodiscardfile noentityescaping noguarantee nokeep nologfile nomapping nomaxvalue nominimize nominvalue nomonitoring none noneditionable nonschema noorder nopr nopro noprom nopromp noprompt norely noresetlogs noreverse normal norowdependencies noschemacheck noswitch not nothing notice notnull notrim novalidate now nowait nth_value nullif nulls num numb numbe nvarchar nvarchar2 object ocicoll ocidate ocidatetime ociduration ociinterval ociloblocator ocinumber ociref ocirefcursor ocirowid ocistring ocitype oct octet_length of off offline offset oid oidindex old on online only opaque open operations operator optimal optimize option optionally or oracle oracle_date oradata ord ordaudio orddicom orddoc order ordimage ordinality ordvideo organization orlany orlvary out outer outfile outline output over overflow overriding package pad parallel parallel_enable parameters parent parse partial partition partitions pascal passing password password_grace_time password_lock_time password_reuse_max password_reuse_time password_verify_function patch path patindex pctincrease pctthreshold pctused pctversion percent percent_rank percentile_cont percentile_disc performance period period_add period_diff permanent physical pi pipe pipelined pivot pluggable plugin policy position post_transaction pow power pragma prebuilt precedes preceding precision prediction prediction_cost prediction_details prediction_probability prediction_set prepare present preserve prior priority private private_sga privileges procedural procedure procedure_analyze processlist profiles project prompt protection public publishingservername purge quarter query quick quiesce quota quotename radians raise rand range rank raw read reads readsize rebuild record records recover recovery recursive recycle redo reduced ref reference referenced references referencing refresh regexp_like register regr_avgx regr_avgy regr_count regr_intercept regr_r2 regr_slope regr_sxx regr_sxy reject rekey relational relative relaylog release release_lock relies_on relocate rely rem remainder rename repair repeat replace replicate replication required reset resetlogs resize resource respect restore restricted result result_cache resumable resume retention return returning returns reuse reverse revoke right rlike role roles rollback rolling rollup round row row_count rowdependencies rowid rownum rows rtrim rules safe salt sample save savepoint sb1 sb2 sb4 scan schema schemacheck scn scope scroll sdo_georaster sdo_topo_geometry search sec_to_time second seconds section securefile security seed segment select self semi sequence sequential serializable server servererror session session_user sessions_per_user set sets settings sha sha1 sha2 share shared shared_pool short show shrink shutdown si_averagecolor si_colorhistogram si_featurelist si_positionalcolor si_stillimage si_texture siblings sid sign sin size size_t sizes skip slave sleep smalldatetimefromparts smallfile snapshot some soname sort soundex source space sparse spfile split sql sql_big_result sql_buffer_result sql_cache sql_calc_found_rows sql_small_result sql_variant_property sqlcode sqldata sqlerror sqlname sqlstate sqrt square standalone standby start starting startup statement static statistics stats_binomial_test stats_crosstab stats_ks_test stats_mode stats_mw_test stats_one_way_anova stats_t_test_ stats_t_test_indep stats_t_test_one stats_t_test_paired stats_wsr_test status std stddev stddev_pop stddev_samp stdev stop storage store stored str str_to_date straight_join strcmp strict string struct stuff style subdate subpartition subpartitions substitutable substr substring subtime subtring_index subtype success sum suspend switch switchoffset switchover sync synchronous synonym sys sys_xmlagg sysasm sysaux sysdate sysdatetimeoffset sysdba sysoper system system_user sysutcdatetime table tables tablespace tablesample tan tdo template temporary terminated tertiary_weights test than then thread through tier ties time time_format time_zone timediff timefromparts timeout timestamp timestampadd timestampdiff timezone_abbr timezone_minute timezone_region to to_base64 to_date to_days to_seconds todatetimeoffset trace tracking transaction transactional translate translation treat trigger trigger_nestlevel triggers trim truncate try_cast try_convert try_parse type ub1 ub2 ub4 ucase unarchived unbounded uncompress under undo unhex unicode uniform uninstall union unique unix_timestamp unknown unlimited unlock unnest unpivot unrecoverable unsafe unsigned until untrusted unusable unused update updated upgrade upped upper upsert url urowid usable usage use use_stored_outlines user user_data user_resources users using utc_date utc_timestamp uuid uuid_short validate validate_password_strength validation valist value values var var_samp varcharc vari varia variab variabl variable variables variance varp varraw varrawc varray verify version versions view virtual visible void wait wallet warning warnings week weekday weekofyear wellformed when whene whenev wheneve whenever where while whitespace window with within without work wrapped xdb xml xmlagg xmlattributes xmlcast xmlcolattval xmlelement xmlexists xmlforest xmlindex xmlnamespaces xmlpi xmlquery xmlroot xmlschema xmlserialize xmltable xmltype xor year year_to_month years yearweek",literal:"true false null unknown",built_in:"array bigint binary bit blob bool boolean char character date dec decimal float int int8 integer interval number numeric real record serial serial8 smallint text time timestamp tinyint varchar varchar2 varying void"},contains:[{className:"string",begin:"'",end:"'",contains:[{begin:"''"}]},{className:"string",begin:'"',end:'"',contains:[{begin:'""'}]},{className:"string",begin:"`",end:"`"},e.C_NUMBER_MODE,e.C_BLOCK_COMMENT_MODE,t,e.HASH_COMMENT_MODE]},e.C_BLOCK_COMMENT_MODE,t,e.HASH_COMMENT_MODE]}}}());hljs.registerLanguage("python",function(){"use strict";return function(e){var n={keyword:"and elif is global as in if from raise for except finally print import pass return exec else break not with class assert yield try while continue del or def lambda async await nonlocal|10",built_in:"Ellipsis NotImplemented",literal:"False None True"},a={className:"meta",begin:/^(>>>|\.\.\.) /},i={className:"subst",begin:/\{/,end:/\}/,keywords:n,illegal:/#/},s={begin:/\{\{/,relevance:0},r={className:"string",contains:[e.BACKSLASH_ESCAPE],variants:[{begin:/(u|b)?r?'''/,end:/'''/,contains:[e.BACKSLASH_ESCAPE,a],relevance:10},{begin:/(u|b)?r?"""/,end:/"""/,contains:[e.BACKSLASH_ESCAPE,a],relevance:10},{begin:/(fr|rf|f)'''/,end:/'''/,contains:[e.BACKSLASH_ESCAPE,a,s,i]},{begin:/(fr|rf|f)"""/,end:/"""/,contains:[e.BACKSLASH_ESCAPE,a,s,i]},{begin:/(u|r|ur)'/,end:/'/,relevance:10},{begin:/(u|r|ur)"/,end:/"/,relevance:10},{begin:/(b|br)'/,end:/'/},{begin:/(b|br)"/,end:/"/},{begin:/(fr|rf|f)'/,end:/'/,contains:[e.BACKSLASH_ESCAPE,s,i]},{begin:/(fr|rf|f)"/,end:/"/,contains:[e.BACKSLASH_ESCAPE,s,i]},e.APOS_STRING_MODE,e.QUOTE_STRING_MODE]},l={className:"number",relevance:0,variants:[{begin:e.BINARY_NUMBER_RE+"[lLjJ]?"},{begin:"\\b(0o[0-7]+)[lLjJ]?"},{begin:e.C_NUMBER_RE+"[lLjJ]?"}]},t={className:"params",variants:[{begin:/\(\s*\)/,skip:!0,className:null},{begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,contains:["self",a,l,r,e.HASH_COMMENT_MODE]}]};return i.contains=[r,l,a],{name:"Python",aliases:["py","gyp","ipython"],keywords:n,illegal:/(<\/|->|\?)|=>/,contains:[a,l,{beginKeywords:"if",relevance:0},r,e.HASH_COMMENT_MODE,{variants:[{className:"function",beginKeywords:"def"},{className:"class",beginKeywords:"class"}],end:/:/,illegal:/[${=;\n,]/,contains:[e.UNDERSCORE_TITLE_MODE,t,{begin:/->/,endsWithParent:!0,keywords:"None"}]},{className:"meta",begin:/^[\t ]*@/,end:/$/},{begin:/\b(print|exec)\(/}]}}}());hljs.registerLanguage("plaintext",function(){"use strict";return function(t){return{name:"Plain text",aliases:["text","txt"],disableAutodetect:!0}}}());hljs.registerLanguage("diff",function(){"use strict";return function(e){return{name:"Diff",aliases:["patch"],contains:[{className:"meta",relevance:10,variants:[{begin:/^@@ +\-\d+,\d+ +\+\d+,\d+ +@@$/},{begin:/^\*\*\* +\d+,\d+ +\*\*\*\*$/},{begin:/^\-\-\- +\d+,\d+ +\-\-\-\-$/}]},{className:"comment",variants:[{begin:/Index: /,end:/$/},{begin:/={3,}/,end:/$/},{begin:/^\-{3}/,end:/$/},{begin:/^\*{3} /,end:/$/},{begin:/^\+{3}/,end:/$/},{begin:/^\*{15}$/}]},{className:"addition",begin:"^\\+",end:"$"},{className:"deletion",begin:"^\\-",end:"$"},{className:"addition",begin:"^\\!",end:"$"}]}}}());hljs.registerLanguage("csharp",function(){"use strict";return function(e){var n={keyword:"abstract as base bool break byte case catch char checked const continue decimal default delegate do double enum event explicit extern finally fixed float for foreach goto if implicit in int interface internal is lock long object operator out override params private protected public readonly ref sbyte sealed short sizeof stackalloc static string struct switch this try typeof uint ulong unchecked unsafe ushort using virtual void volatile while add alias ascending async await by descending dynamic equals from get global group into join let nameof on orderby partial remove select set value var when where yield",literal:"null false true"},i=e.inherit(e.TITLE_MODE,{begin:"[a-zA-Z](\\.?\\w)*"}),a={className:"number",variants:[{begin:"\\b(0b[01']+)"},{begin:"(-?)\\b([\\d']+(\\.[\\d']*)?|\\.[\\d']+)(u|U|l|L|ul|UL|f|F|b|B)"},{begin:"(-?)(\\b0[xX][a-fA-F0-9']+|(\\b[\\d']+(\\.[\\d']*)?|\\.[\\d']+)([eE][-+]?[\\d']+)?)"}],relevance:0},s={className:"string",begin:'@"',end:'"',contains:[{begin:'""'}]},t=e.inherit(s,{illegal:/\n/}),l={className:"subst",begin:"{",end:"}",keywords:n},r=e.inherit(l,{illegal:/\n/}),c={className:"string",begin:/\$"/,end:'"',illegal:/\n/,contains:[{begin:"{{"},{begin:"}}"},e.BACKSLASH_ESCAPE,r]},o={className:"string",begin:/\$@"/,end:'"',contains:[{begin:"{{"},{begin:"}}"},{begin:'""'},l]},g=e.inherit(o,{illegal:/\n/,contains:[{begin:"{{"},{begin:"}}"},{begin:'""'},r]});l.contains=[o,c,s,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,a,e.C_BLOCK_COMMENT_MODE],r.contains=[g,c,t,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,a,e.inherit(e.C_BLOCK_COMMENT_MODE,{illegal:/\n/})];var d={variants:[o,c,s,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE]},E=e.IDENT_RE+"(<"+e.IDENT_RE+"(\\s*,\\s*"+e.IDENT_RE+")*>)?(\\[\\])?",_={begin:"@"+e.IDENT_RE,relevance:0};return{name:"C#",aliases:["cs","c#"],keywords:n,illegal:/::/,contains:[e.COMMENT("///","$",{returnBegin:!0,contains:[{className:"doctag",variants:[{begin:"///",relevance:0},{begin:"\x3c!--|--\x3e"},{begin:""}]}]}),e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,{className:"meta",begin:"#",end:"$",keywords:{"meta-keyword":"if else elif endif define undef warning error line region endregion pragma checksum"}},d,a,{beginKeywords:"class interface",end:/[{;=]/,illegal:/[^\s:,]/,contains:[{beginKeywords:"where class"},i,{begin:"<",end:">",keywords:"in out"},e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{beginKeywords:"namespace",end:/[{;=]/,illegal:/[^\s:]/,contains:[i,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{className:"meta",begin:"^\\s*\\[",excludeBegin:!0,end:"\\]",excludeEnd:!0,contains:[{className:"meta-string",begin:/"/,end:/"/}]},{beginKeywords:"new return throw await else",relevance:0},{className:"function",begin:"("+E+"\\s+)+"+e.IDENT_RE+"\\s*\\(",returnBegin:!0,end:/\s*[{;=]/,excludeEnd:!0,keywords:n,contains:[{begin:e.IDENT_RE+"\\s*\\(",returnBegin:!0,contains:[e.TITLE_MODE],relevance:0},{className:"params",begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:n,relevance:0,contains:[d,a,e.C_BLOCK_COMMENT_MODE]},e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},_]}}}());hljs.registerLanguage("ruby",function(){"use strict";return function(e){var n="[a-zA-Z_]\\w*[!?=]?|[-+~]\\@|<<|>>|=~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~`|]|\\[\\]=?",a={keyword:"and then defined module in return redo if BEGIN retry end for self when next until do begin unless END rescue else break undef not super class case require yield alias while ensure elsif or include attr_reader attr_writer attr_accessor",literal:"true false nil"},s={className:"doctag",begin:"@[A-Za-z]+"},i={begin:"#<",end:">"},r=[e.COMMENT("#","$",{contains:[s]}),e.COMMENT("^\\=begin","^\\=end",{contains:[s],relevance:10}),e.COMMENT("^__END__","\\n$")],c={className:"subst",begin:"#\\{",end:"}",keywords:a},t={className:"string",contains:[e.BACKSLASH_ESCAPE,c],variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/`/,end:/`/},{begin:"%[qQwWx]?\\(",end:"\\)"},{begin:"%[qQwWx]?\\[",end:"\\]"},{begin:"%[qQwWx]?{",end:"}"},{begin:"%[qQwWx]?<",end:">"},{begin:"%[qQwWx]?/",end:"/"},{begin:"%[qQwWx]?%",end:"%"},{begin:"%[qQwWx]?-",end:"-"},{begin:"%[qQwWx]?\\|",end:"\\|"},{begin:/\B\?(\\\d{1,3}|\\x[A-Fa-f0-9]{1,2}|\\u[A-Fa-f0-9]{4}|\\?\S)\b/},{begin:/<<[-~]?'?(\w+)(?:.|\n)*?\n\s*\1\b/,returnBegin:!0,contains:[{begin:/<<[-~]?'?/},{begin:/\w+/,endSameAsBegin:!0,contains:[e.BACKSLASH_ESCAPE,c]}]}]},b={className:"params",begin:"\\(",end:"\\)",endsParent:!0,keywords:a},d=[t,i,{className:"class",beginKeywords:"class module",end:"$|;",illegal:/=/,contains:[e.inherit(e.TITLE_MODE,{begin:"[A-Za-z_]\\w*(::\\w+)*(\\?|\\!)?"}),{begin:"<\\s*",contains:[{begin:"("+e.IDENT_RE+"::)?"+e.IDENT_RE}]}].concat(r)},{className:"function",beginKeywords:"def",end:"$|;",contains:[e.inherit(e.TITLE_MODE,{begin:n}),b].concat(r)},{begin:e.IDENT_RE+"::"},{className:"symbol",begin:e.UNDERSCORE_IDENT_RE+"(\\!|\\?)?:",relevance:0},{className:"symbol",begin:":(?!\\s)",contains:[t,{begin:n}],relevance:0},{className:"number",begin:"(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b",relevance:0},{begin:"(\\$\\W)|((\\$|\\@\\@?)(\\w+))"},{className:"params",begin:/\|/,end:/\|/,keywords:a},{begin:"("+e.RE_STARTERS_RE+"|unless)\\s*",keywords:"unless",contains:[i,{className:"regexp",contains:[e.BACKSLASH_ESCAPE,c],illegal:/\n/,variants:[{begin:"/",end:"/[a-z]*"},{begin:"%r{",end:"}[a-z]*"},{begin:"%r\\(",end:"\\)[a-z]*"},{begin:"%r!",end:"![a-z]*"},{begin:"%r\\[",end:"\\][a-z]*"}]}].concat(r),relevance:0}].concat(r);c.contains=d,b.contains=d;var g=[{begin:/^\s*=>/,starts:{end:"$",contains:d}},{className:"meta",begin:"^([>?]>|[\\w#]+\\(\\w+\\):\\d+:\\d+>|(\\w+-)?\\d+\\.\\d+\\.\\d(p\\d+)?[^>]+>)",starts:{end:"$",contains:d}}];return{name:"Ruby",aliases:["rb","gemspec","podspec","thor","irb"],keywords:a,illegal:/\/\*/,contains:r.concat(g).concat(d)}}}());hljs.registerLanguage("scss",function(){"use strict";return function(e){var t={className:"variable",begin:"(\\$[a-zA-Z-][a-zA-Z0-9_-]*)\\b"},i={className:"number",begin:"#[0-9A-Fa-f]+"};return e.CSS_NUMBER_MODE,e.QUOTE_STRING_MODE,e.APOS_STRING_MODE,e.C_BLOCK_COMMENT_MODE,{name:"SCSS",case_insensitive:!0,illegal:"[=/|']",contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,{className:"selector-id",begin:"\\#[A-Za-z0-9_-]+",relevance:0},{className:"selector-class",begin:"\\.[A-Za-z0-9_-]+",relevance:0},{className:"selector-attr",begin:"\\[",end:"\\]",illegal:"$"},{className:"selector-tag",begin:"\\b(a|abbr|acronym|address|area|article|aside|audio|b|base|big|blockquote|body|br|button|canvas|caption|cite|code|col|colgroup|command|datalist|dd|del|details|dfn|div|dl|dt|em|embed|fieldset|figcaption|figure|footer|form|frame|frameset|(h[1-6])|head|header|hgroup|hr|html|i|iframe|img|input|ins|kbd|keygen|label|legend|li|link|map|mark|meta|meter|nav|noframes|noscript|object|ol|optgroup|option|output|p|param|pre|progress|q|rp|rt|ruby|samp|script|section|select|small|span|strike|strong|style|sub|sup|table|tbody|td|textarea|tfoot|th|thead|time|title|tr|tt|ul|var|video)\\b",relevance:0},{className:"selector-pseudo",begin:":(visited|valid|root|right|required|read-write|read-only|out-range|optional|only-of-type|only-child|nth-of-type|nth-last-of-type|nth-last-child|nth-child|not|link|left|last-of-type|last-child|lang|invalid|indeterminate|in-range|hover|focus|first-of-type|first-line|first-letter|first-child|first|enabled|empty|disabled|default|checked|before|after|active)"},{className:"selector-pseudo",begin:"::(after|before|choices|first-letter|first-line|repeat-index|repeat-item|selection|value)"},t,{className:"attribute",begin:"\\b(src|z-index|word-wrap|word-spacing|word-break|width|widows|white-space|visibility|vertical-align|unicode-bidi|transition-timing-function|transition-property|transition-duration|transition-delay|transition|transform-style|transform-origin|transform|top|text-underline-position|text-transform|text-shadow|text-rendering|text-overflow|text-indent|text-decoration-style|text-decoration-line|text-decoration-color|text-decoration|text-align-last|text-align|tab-size|table-layout|right|resize|quotes|position|pointer-events|perspective-origin|perspective|page-break-inside|page-break-before|page-break-after|padding-top|padding-right|padding-left|padding-bottom|padding|overflow-y|overflow-x|overflow-wrap|overflow|outline-width|outline-style|outline-offset|outline-color|outline|orphans|order|opacity|object-position|object-fit|normal|none|nav-up|nav-right|nav-left|nav-index|nav-down|min-width|min-height|max-width|max-height|mask|marks|margin-top|margin-right|margin-left|margin-bottom|margin|list-style-type|list-style-position|list-style-image|list-style|line-height|letter-spacing|left|justify-content|initial|inherit|ime-mode|image-orientation|image-resolution|image-rendering|icon|hyphens|height|font-weight|font-variant-ligatures|font-variant|font-style|font-stretch|font-size-adjust|font-size|font-language-override|font-kerning|font-feature-settings|font-family|font|float|flex-wrap|flex-shrink|flex-grow|flex-flow|flex-direction|flex-basis|flex|filter|empty-cells|display|direction|cursor|counter-reset|counter-increment|content|column-width|column-span|column-rule-width|column-rule-style|column-rule-color|column-rule|column-gap|column-fill|column-count|columns|color|clip-path|clip|clear|caption-side|break-inside|break-before|break-after|box-sizing|box-shadow|box-decoration-break|bottom|border-width|border-top-width|border-top-style|border-top-right-radius|border-top-left-radius|border-top-color|border-top|border-style|border-spacing|border-right-width|border-right-style|border-right-color|border-right|border-radius|border-left-width|border-left-style|border-left-color|border-left|border-image-width|border-image-source|border-image-slice|border-image-repeat|border-image-outset|border-image|border-color|border-collapse|border-bottom-width|border-bottom-style|border-bottom-right-radius|border-bottom-left-radius|border-bottom-color|border-bottom|border|background-size|background-repeat|background-position|background-origin|background-image|background-color|background-clip|background-attachment|background-blend-mode|background|backface-visibility|auto|animation-timing-function|animation-play-state|animation-name|animation-iteration-count|animation-fill-mode|animation-duration|animation-direction|animation-delay|animation|align-self|align-items|align-content)\\b",illegal:"[^\\s]"},{begin:"\\b(whitespace|wait|w-resize|visible|vertical-text|vertical-ideographic|uppercase|upper-roman|upper-alpha|underline|transparent|top|thin|thick|text|text-top|text-bottom|tb-rl|table-header-group|table-footer-group|sw-resize|super|strict|static|square|solid|small-caps|separate|se-resize|scroll|s-resize|rtl|row-resize|ridge|right|repeat|repeat-y|repeat-x|relative|progress|pointer|overline|outside|outset|oblique|nowrap|not-allowed|normal|none|nw-resize|no-repeat|no-drop|newspaper|ne-resize|n-resize|move|middle|medium|ltr|lr-tb|lowercase|lower-roman|lower-alpha|loose|list-item|line|line-through|line-edge|lighter|left|keep-all|justify|italic|inter-word|inter-ideograph|inside|inset|inline|inline-block|inherit|inactive|ideograph-space|ideograph-parenthesis|ideograph-numeric|ideograph-alpha|horizontal|hidden|help|hand|groove|fixed|ellipsis|e-resize|double|dotted|distribute|distribute-space|distribute-letter|distribute-all-lines|disc|disabled|default|decimal|dashed|crosshair|collapse|col-resize|circle|char|center|capitalize|break-word|break-all|bottom|both|bolder|bold|block|bidi-override|below|baseline|auto|always|all-scroll|absolute|table|table-cell)\\b"},{begin:":",end:";",contains:[t,i,e.CSS_NUMBER_MODE,e.QUOTE_STRING_MODE,e.APOS_STRING_MODE,{className:"meta",begin:"!important"}]},{begin:"@(page|font-face)",lexemes:"@[a-z-]+",keywords:"@page @font-face"},{begin:"@",end:"[{;]",returnBegin:!0,keywords:"and or not only",contains:[{begin:"@[a-z-]+",className:"keyword"},t,e.QUOTE_STRING_MODE,e.APOS_STRING_MODE,i,e.CSS_NUMBER_MODE]}]}}}());hljs.registerLanguage("http",function(){"use strict";return function(e){var n="HTTP/[0-9\\.]+";return{name:"HTTP",aliases:["https"],illegal:"\\S",contains:[{begin:"^"+n,end:"$",contains:[{className:"number",begin:"\\b\\d{3}\\b"}]},{begin:"^[A-Z]+ (.*?) "+n+"$",returnBegin:!0,end:"$",contains:[{className:"string",begin:" ",end:" ",excludeBegin:!0,excludeEnd:!0},{begin:n},{className:"keyword",begin:"[A-Z]+"}]},{className:"attribute",begin:"^\\w",end:": ",excludeEnd:!0,illegal:"\\n|\\s|=",starts:{end:"$",relevance:0}},{begin:"\\n\\n",starts:{subLanguage:[],endsWithParent:!0}}]}}}());hljs.registerLanguage("java",function(){"use strict";return function(e){var a="false synchronized int abstract float private char boolean var static null if const for true while long strictfp finally protected import native final void enum else break transient catch instanceof byte super volatile case assert short package default double public try this switch continue throws protected public private module requires exports do",n={className:"meta",begin:"@[À-ʸa-zA-Z_$][À-ʸa-zA-Z_$0-9]*",contains:[{begin:/\(/,end:/\)/,contains:["self"]}]};return{name:"Java",aliases:["jsp"],keywords:a,illegal:/<\/|#/,contains:[e.COMMENT("/\\*\\*","\\*/",{relevance:0,contains:[{begin:/\w+@/,relevance:0},{className:"doctag",begin:"@[A-Za-z]+"}]}),e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,{className:"class",beginKeywords:"class interface",end:/[{;=]/,excludeEnd:!0,keywords:"class interface",illegal:/[:"\[\]]/,contains:[{beginKeywords:"extends implements"},e.UNDERSCORE_TITLE_MODE]},{beginKeywords:"new throw return else",relevance:0},{className:"function",begin:"([À-ʸa-zA-Z_$][À-ʸa-zA-Z_$0-9]*(<[À-ʸa-zA-Z_$][À-ʸa-zA-Z_$0-9]*(\\s*,\\s*[À-ʸa-zA-Z_$][À-ʸa-zA-Z_$0-9]*)*>)?\\s+)+"+e.UNDERSCORE_IDENT_RE+"\\s*\\(",returnBegin:!0,end:/[{;=]/,excludeEnd:!0,keywords:a,contains:[{begin:e.UNDERSCORE_IDENT_RE+"\\s*\\(",returnBegin:!0,relevance:0,contains:[e.UNDERSCORE_TITLE_MODE]},{className:"params",begin:/\(/,end:/\)/,keywords:a,relevance:0,contains:[n,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,e.C_NUMBER_MODE,e.C_BLOCK_COMMENT_MODE]},e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{className:"number",begin:"\\b(0[bB]([01]+[01_]+[01]+|[01]+)|0[xX]([a-fA-F0-9]+[a-fA-F0-9_]+[a-fA-F0-9]+|[a-fA-F0-9]+)|(([\\d]+[\\d_]+[\\d]+|[\\d]+)(\\.([\\d]+[\\d_]+[\\d]+|[\\d]+))?|\\.([\\d]+[\\d_]+[\\d]+|[\\d]+))([eE][-+]?\\d+)?)[lLfF]?",relevance:0},n]}}}());hljs.registerLanguage("swift",function(){"use strict";return function(e){var i={keyword:"#available #colorLiteral #column #else #elseif #endif #file #fileLiteral #function #if #imageLiteral #line #selector #sourceLocation _ __COLUMN__ __FILE__ __FUNCTION__ __LINE__ Any as as! as? associatedtype associativity break case catch class continue convenience default defer deinit didSet do dynamic dynamicType else enum extension fallthrough false fileprivate final for func get guard if import in indirect infix init inout internal is lazy left let mutating nil none nonmutating open operator optional override postfix precedence prefix private protocol Protocol public repeat required rethrows return right self Self set static struct subscript super switch throw throws true try try! try? Type typealias unowned var weak where while willSet",literal:"true false nil",built_in:"abs advance alignof alignofValue anyGenerator assert assertionFailure bridgeFromObjectiveC bridgeFromObjectiveCUnconditional bridgeToObjectiveC bridgeToObjectiveCUnconditional c compactMap contains count countElements countLeadingZeros debugPrint debugPrintln distance dropFirst dropLast dump encodeBitsAsWords enumerate equal fatalError filter find getBridgedObjectiveCType getVaList indices insertionSort isBridgedToObjectiveC isBridgedVerbatimToObjectiveC isUniquelyReferenced isUniquelyReferencedNonObjC join lazy lexicographicalCompare map max maxElement min minElement numericCast overlaps partition posix precondition preconditionFailure print println quickSort readLine reduce reflect reinterpretCast reverse roundUpToAlignment sizeof sizeofValue sort split startsWith stride strideof strideofValue swap toString transcode underestimateCount unsafeAddressOf unsafeBitCast unsafeDowncast unsafeUnwrap unsafeReflect withExtendedLifetime withObjectAtPlusZero withUnsafePointer withUnsafePointerToObject withUnsafeMutablePointer withUnsafeMutablePointers withUnsafePointer withUnsafePointers withVaList zip"},n=e.COMMENT("/\\*","\\*/",{contains:["self"]}),t={className:"subst",begin:/\\\(/,end:"\\)",keywords:i,contains:[]},a={className:"string",contains:[e.BACKSLASH_ESCAPE,t],variants:[{begin:/"""/,end:/"""/},{begin:/"/,end:/"/}]},r={className:"number",begin:"\\b([\\d_]+(\\.[\\deE_]+)?|0x[a-fA-F0-9_]+(\\.[a-fA-F0-9p_]+)?|0b[01_]+|0o[0-7_]+)\\b",relevance:0};return t.contains=[r],{name:"Swift",keywords:i,contains:[a,e.C_LINE_COMMENT_MODE,n,{className:"type",begin:"\\b[A-Z][\\wÀ-ʸ']*[!?]"},{className:"type",begin:"\\b[A-Z][\\wÀ-ʸ']*",relevance:0},r,{className:"function",beginKeywords:"func",end:"{",excludeEnd:!0,contains:[e.inherit(e.TITLE_MODE,{begin:/[A-Za-z$_][0-9A-Za-z$_]*/}),{begin://},{className:"params",begin:/\(/,end:/\)/,endsParent:!0,keywords:i,contains:["self",r,a,e.C_BLOCK_COMMENT_MODE,{begin:":"}],illegal:/["']/}],illegal:/\[|%/},{className:"class",beginKeywords:"struct protocol class extension enum",keywords:i,end:"\\{",excludeEnd:!0,contains:[e.inherit(e.TITLE_MODE,{begin:/[A-Za-z$_][\u00C0-\u02B80-9A-Za-z$_]*/})]},{className:"meta",begin:"(@discardableResult|@warn_unused_result|@exported|@lazy|@noescape|@NSCopying|@NSManaged|@objc|@objcMembers|@convention|@required|@noreturn|@IBAction|@IBDesignable|@IBInspectable|@IBOutlet|@infix|@prefix|@postfix|@autoclosure|@testable|@available|@nonobjc|@NSApplicationMain|@UIApplicationMain|@dynamicMemberLookup|@propertyWrapper)"},{beginKeywords:"import",end:/$/,contains:[e.C_LINE_COMMENT_MODE,n]}]}}}());hljs.registerLanguage("less",function(){"use strict";return function(e){var n="([\\w-]+|@{[\\w-]+})",a=[],s=[],t=function(e){return{className:"string",begin:"~?"+e+".*?"+e}},r=function(e,n,a){return{className:e,begin:n,relevance:a}},i={begin:"\\(",end:"\\)",contains:s,relevance:0};s.push(e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,t("'"),t('"'),e.CSS_NUMBER_MODE,{begin:"(url|data-uri)\\(",starts:{className:"string",end:"[\\)\\n]",excludeEnd:!0}},r("number","#[0-9A-Fa-f]+\\b"),i,r("variable","@@?[\\w-]+",10),r("variable","@{[\\w-]+}"),r("built_in","~?`[^`]*?`"),{className:"attribute",begin:"[\\w-]+\\s*:",end:":",returnBegin:!0,excludeEnd:!0},{className:"meta",begin:"!important"});var c=s.concat({begin:"{",end:"}",contains:a}),l={beginKeywords:"when",endsWithParent:!0,contains:[{beginKeywords:"and not"}].concat(s)},o={begin:n+"\\s*:",returnBegin:!0,end:"[;}]",relevance:0,contains:[{className:"attribute",begin:n,end:":",excludeEnd:!0,starts:{endsWithParent:!0,illegal:"[<=$]",relevance:0,contains:s}}]},g={className:"keyword",begin:"@(import|media|charset|font-face|(-[a-z]+-)?keyframes|supports|document|namespace|page|viewport|host)\\b",starts:{end:"[;{}]",returnEnd:!0,contains:s,relevance:0}},d={className:"variable",variants:[{begin:"@[\\w-]+\\s*:",relevance:15},{begin:"@[\\w-]+"}],starts:{end:"[;}]",returnEnd:!0,contains:c}},b={variants:[{begin:"[\\.#:&\\[>]",end:"[;{}]"},{begin:n,end:"{"}],returnBegin:!0,returnEnd:!0,illegal:"[<='$\"]",relevance:0,contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,l,r("keyword","all\\b"),r("variable","@{[\\w-]+}"),r("selector-tag",n+"%?",0),r("selector-id","#"+n),r("selector-class","\\."+n,0),r("selector-tag","&",0),{className:"selector-attr",begin:"\\[",end:"\\]"},{className:"selector-pseudo",begin:/:(:)?[a-zA-Z0-9\_\-\+\(\)"'.]+/},{begin:"\\(",end:"\\)",contains:c},{begin:"!important"}]};return a.push(e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,g,d,o,b),{name:"Less",case_insensitive:!0,illegal:"[=>'/<($\"]",contains:a}}}());hljs.registerLanguage("json",function(){"use strict";return function(n){var e={literal:"true false null"},i=[n.C_LINE_COMMENT_MODE,n.C_BLOCK_COMMENT_MODE],t=[n.QUOTE_STRING_MODE,n.C_NUMBER_MODE],a={end:",",endsWithParent:!0,excludeEnd:!0,contains:t,keywords:e},l={begin:"{",end:"}",contains:[{className:"attr",begin:/"/,end:/"/,contains:[n.BACKSLASH_ESCAPE],illegal:"\\n"},n.inherit(a,{begin:/:/})].concat(i),illegal:"\\S"},s={begin:"\\[",end:"\\]",contains:[n.inherit(a)],illegal:"\\S"};return t.push(l,s),i.forEach((function(n){t.push(n)})),{name:"JSON",contains:t,keywords:e,illegal:"\\S"}}}());hljs.registerLanguage("typescript",function(){"use strict";return function(e){var n={keyword:"in if for while finally var new function do return void else break catch instanceof with throw case default try this switch continue typeof delete let yield const class public private protected get set super static implements enum export import declare type namespace abstract as from extends async await",literal:"true false null undefined NaN Infinity",built_in:"eval isFinite isNaN parseFloat parseInt decodeURI decodeURIComponent encodeURI encodeURIComponent escape unescape Object Function Boolean Error EvalError InternalError RangeError ReferenceError StopIteration SyntaxError TypeError URIError Number Math Date String RegExp Array Float32Array Float64Array Int16Array Int32Array Int8Array Uint16Array Uint32Array Uint8Array Uint8ClampedArray ArrayBuffer DataView JSON Intl arguments require module console window document any number boolean string void Promise"},r={className:"meta",begin:"@[A-Za-z$_][0-9A-Za-z$_]*"},a={begin:"\\(",end:/\)/,keywords:n,contains:["self",e.QUOTE_STRING_MODE,e.APOS_STRING_MODE,e.NUMBER_MODE]},t={className:"params",begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:n,contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,r,a]},s={className:"number",variants:[{begin:"\\b(0[bB][01]+)n?"},{begin:"\\b(0[oO][0-7]+)n?"},{begin:e.C_NUMBER_RE+"n?"}],relevance:0},i={className:"subst",begin:"\\$\\{",end:"\\}",keywords:n,contains:[]},o={begin:"html`",end:"",starts:{end:"`",returnEnd:!1,contains:[e.BACKSLASH_ESCAPE,i],subLanguage:"xml"}},c={begin:"css`",end:"",starts:{end:"`",returnEnd:!1,contains:[e.BACKSLASH_ESCAPE,i],subLanguage:"css"}},E={className:"string",begin:"`",end:"`",contains:[e.BACKSLASH_ESCAPE,i]};return i.contains=[e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,o,c,E,s,e.REGEXP_MODE],{name:"TypeScript",aliases:["ts"],keywords:n,contains:[{className:"meta",begin:/^\s*['"]use strict['"]/},e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,o,c,E,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,s,{begin:"("+e.RE_STARTERS_RE+"|\\b(case|return|throw)\\b)\\s*",keywords:"return throw case",contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,e.REGEXP_MODE,{className:"function",begin:"(\\(.*?\\)|"+e.IDENT_RE+")\\s*=>",returnBegin:!0,end:"\\s*=>",contains:[{className:"params",variants:[{begin:e.IDENT_RE},{begin:/\(\s*\)/},{begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:n,contains:["self",e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]}]}]}],relevance:0},{className:"function",beginKeywords:"function",end:/[\{;]/,excludeEnd:!0,keywords:n,contains:["self",e.inherit(e.TITLE_MODE,{begin:"[A-Za-z$_][0-9A-Za-z$_]*"}),t],illegal:/%/,relevance:0},{beginKeywords:"constructor",end:/[\{;]/,excludeEnd:!0,contains:["self",t]},{begin:/module\./,keywords:{built_in:"module"},relevance:0},{beginKeywords:"module",end:/\{/,excludeEnd:!0},{beginKeywords:"interface",end:/\{/,excludeEnd:!0,keywords:"interface extends"},{begin:/\$[(.]/},{begin:"\\."+e.IDENT_RE,relevance:0},r,a]}}}());hljs.registerLanguage("c-like",function(){"use strict";return function(e){function t(e){return"(?:"+e+")?"}var n="(decltype\\(auto\\)|"+t("[a-zA-Z_]\\w*::")+"[a-zA-Z_]\\w*"+t("<.*?>")+")",r={className:"keyword",begin:"\\b[a-z\\d_]*_t\\b"},a={className:"string",variants:[{begin:'(u8?|U|L)?"',end:'"',illegal:"\\n",contains:[e.BACKSLASH_ESCAPE]},{begin:"(u8?|U|L)?'(\\\\(x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4,8}|[0-7]{3}|\\S)|.)",end:"'",illegal:"."},{begin:/(?:u8?|U|L)?R"([^()\\ ]{0,16})\((?:.|\n)*?\)\1"/}]},s={className:"number",variants:[{begin:"\\b(0b[01']+)"},{begin:"(-?)\\b([\\d']+(\\.[\\d']*)?|\\.[\\d']+)(u|U|l|L|ul|UL|f|F|b|B)"},{begin:"(-?)(\\b0[xX][a-fA-F0-9']+|(\\b[\\d']+(\\.[\\d']*)?|\\.[\\d']+)([eE][-+]?[\\d']+)?)"}],relevance:0},i={className:"meta",begin:/#\s*[a-z]+\b/,end:/$/,keywords:{"meta-keyword":"if else elif endif define undef warning error line pragma _Pragma ifdef ifndef include"},contains:[{begin:/\\\n/,relevance:0},e.inherit(a,{className:"meta-string"}),{className:"meta-string",begin:/<.*?>/,end:/$/,illegal:"\\n"},e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},c={className:"title",begin:t("[a-zA-Z_]\\w*::")+e.IDENT_RE,relevance:0},o=t("[a-zA-Z_]\\w*::")+e.IDENT_RE+"\\s*\\(",l={keyword:"int float while private char char8_t char16_t char32_t catch import module export virtual operator sizeof dynamic_cast|10 typedef const_cast|10 const for static_cast|10 union namespace unsigned long volatile static protected bool template mutable if public friend do goto auto void enum else break extern using asm case typeid wchar_t short reinterpret_cast|10 default double register explicit signed typename try this switch continue inline delete alignas alignof constexpr consteval constinit decltype concept co_await co_return co_yield requires noexcept static_assert thread_local restrict final override atomic_bool atomic_char atomic_schar atomic_uchar atomic_short atomic_ushort atomic_int atomic_uint atomic_long atomic_ulong atomic_llong atomic_ullong new throw return and and_eq bitand bitor compl not not_eq or or_eq xor xor_eq",built_in:"std string wstring cin cout cerr clog stdin stdout stderr stringstream istringstream ostringstream auto_ptr deque list queue stack vector map set bitset multiset multimap unordered_set unordered_map unordered_multiset unordered_multimap array shared_ptr abort terminate abs acos asin atan2 atan calloc ceil cosh cos exit exp fabs floor fmod fprintf fputs free frexp fscanf future isalnum isalpha iscntrl isdigit isgraph islower isprint ispunct isspace isupper isxdigit tolower toupper labs ldexp log10 log malloc realloc memchr memcmp memcpy memset modf pow printf putchar puts scanf sinh sin snprintf sprintf sqrt sscanf strcat strchr strcmp strcpy strcspn strlen strncat strncmp strncpy strpbrk strrchr strspn strstr tanh tan vfprintf vprintf vsprintf endl initializer_list unique_ptr _Bool complex _Complex imaginary _Imaginary",literal:"true false nullptr NULL"},d=[r,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,s,a],_={variants:[{begin:/=/,end:/;/},{begin:/\(/,end:/\)/},{beginKeywords:"new throw return else",end:/;/}],keywords:l,contains:d.concat([{begin:/\(/,end:/\)/,keywords:l,contains:d.concat(["self"]),relevance:0}]),relevance:0},u={className:"function",begin:"("+n+"[\\*&\\s]+)+"+o,returnBegin:!0,end:/[{;=]/,excludeEnd:!0,keywords:l,illegal:/[^\w\s\*&:<>]/,contains:[{begin:"decltype\\(auto\\)",keywords:l,relevance:0},{begin:o,returnBegin:!0,contains:[c],relevance:0},{className:"params",begin:/\(/,end:/\)/,keywords:l,relevance:0,contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,a,s,r,{begin:/\(/,end:/\)/,keywords:l,relevance:0,contains:["self",e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,a,s,r]}]},r,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,i]};return{aliases:["c","cc","h","c++","h++","hpp","hh","hxx","cxx"],keywords:l,disableAutodetect:!0,illegal:"",keywords:l,contains:["self",r]},{begin:e.IDENT_RE+"::",keywords:l},{className:"class",beginKeywords:"class struct",end:/[{;:]/,contains:[{begin://,contains:["self"]},e.TITLE_MODE]}]),exports:{preprocessor:i,strings:a,keywords:l}}}}());hljs.registerLanguage("cpp",function(){"use strict";return function(e){var t=e.getLanguage("c-like").rawDefinition();return t.disableAutodetect=!1,t.name="C++",t.aliases=["cc","c++","h++","hpp","hh","hxx","cxx"],t}}());hljs.registerLanguage("properties",function(){"use strict";return function(e){var n="[ \\t\\f]*",t="("+n+"[:=]"+n+"|[ \\t\\f]+)",a="([^\\\\:= \\t\\f\\n]|\\\\.)+",s={end:t,relevance:0,starts:{className:"string",end:/$/,relevance:0,contains:[{begin:"\\\\\\n"}]}};return{name:".properties",case_insensitive:!0,illegal:/\S/,contains:[e.COMMENT("^\\s*[!#]","$"),{begin:"([^\\\\\\W:= \\t\\f\\n]|\\\\.)+"+t,returnBegin:!0,contains:[{className:"attr",begin:"([^\\\\\\W:= \\t\\f\\n]|\\\\.)+",endsParent:!0,relevance:0}],starts:s},{begin:a+t,returnBegin:!0,relevance:0,contains:[{className:"meta",begin:a,endsParent:!0,relevance:0}],starts:s},{className:"attr",relevance:0,begin:a+n+"$"}]}}}());hljs.registerLanguage("twig",function(){"use strict";return function(e){var a="attribute block constant cycle date dump include max min parent random range source template_from_string",n={beginKeywords:a,keywords:{name:a},relevance:0,contains:[{className:"params",begin:"\\(",end:"\\)"}]},t={begin:/\|[A-Za-z_]+:?/,keywords:"abs batch capitalize column convert_encoding date date_modify default escape filter first format inky_to_html inline_css join json_encode keys last length lower map markdown merge nl2br number_format raw reduce replace reverse round slice sort spaceless split striptags title trim upper url_encode",contains:[n]},s="apply autoescape block deprecated do embed extends filter flush for from if import include macro sandbox set use verbatim with";return s=s+" "+s.split(" ").map((function(e){return"end"+e})).join(" "),{name:"Twig",aliases:["craftcms"],case_insensitive:!0,subLanguage:"xml",contains:[e.COMMENT(/\{#/,/#}/),{className:"template-tag",begin:/\{%/,end:/%}/,contains:[{className:"name",begin:/\w+/,keywords:s,starts:{endsWithParent:!0,contains:[t,n],relevance:0}}]},{className:"template-variable",begin:/\{\{/,end:/}}/,contains:["self",t,n]}]}}}());hljs.registerLanguage("javascript",function(){"use strict";return function(e){var n={begin:/<[A-Za-z0-9\\._:-]+/,end:/\/[A-Za-z0-9\\._:-]+>|\/>/},a="[A-Za-z$_][0-9A-Za-z$_]*",s={keyword:"in of if for while finally var new function do return void else break catch instanceof with throw case default try this switch continue typeof delete let yield const export super debugger as async await static import from as",literal:"true false null undefined NaN Infinity",built_in:"eval isFinite isNaN parseFloat parseInt decodeURI decodeURIComponent encodeURI encodeURIComponent escape unescape Object Function Boolean Error EvalError InternalError RangeError ReferenceError StopIteration SyntaxError TypeError URIError Number Math Date String RegExp Array Float32Array Float64Array Int16Array Int32Array Int8Array Uint16Array Uint32Array Uint8Array Uint8ClampedArray ArrayBuffer DataView JSON Intl arguments require module console window document Symbol Set Map WeakSet WeakMap Proxy Reflect Promise"},r={className:"number",variants:[{begin:"\\b(0[bB][01]+)n?"},{begin:"\\b(0[oO][0-7]+)n?"},{begin:e.C_NUMBER_RE+"n?"}],relevance:0},i={className:"subst",begin:"\\$\\{",end:"\\}",keywords:s,contains:[]},t={begin:"html`",end:"",starts:{end:"`",returnEnd:!1,contains:[e.BACKSLASH_ESCAPE,i],subLanguage:"xml"}},c={begin:"css`",end:"",starts:{end:"`",returnEnd:!1,contains:[e.BACKSLASH_ESCAPE,i],subLanguage:"css"}},o={className:"string",begin:"`",end:"`",contains:[e.BACKSLASH_ESCAPE,i]};i.contains=[e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,t,c,o,r,e.REGEXP_MODE];var l=i.contains.concat([e.C_BLOCK_COMMENT_MODE,e.C_LINE_COMMENT_MODE]),d={className:"params",begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,contains:l};return{name:"JavaScript",aliases:["js","jsx","mjs","cjs"],keywords:s,contains:[{className:"meta",relevance:10,begin:/^\s*['"]use (strict|asm)['"]/},{className:"meta",begin:/^#!/,end:/$/},e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,t,c,o,e.C_LINE_COMMENT_MODE,e.COMMENT("/\\*\\*","\\*/",{relevance:0,contains:[{className:"doctag",begin:"@[A-Za-z]+",contains:[{className:"type",begin:"\\{",end:"\\}",relevance:0},{className:"variable",begin:a+"(?=\\s*(-)|$)",endsParent:!0,relevance:0},{begin:/(?=[^\n])\s/,relevance:0}]}]}),e.C_BLOCK_COMMENT_MODE,r,{begin:/[{,\n]\s*/,relevance:0,contains:[{begin:a+"\\s*:",returnBegin:!0,relevance:0,contains:[{className:"attr",begin:a,relevance:0}]}]},{begin:"("+e.RE_STARTERS_RE+"|\\b(case|return|throw)\\b)\\s*",keywords:"return throw case",contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,e.REGEXP_MODE,{className:"function",begin:"(\\(.*?\\)|"+a+")\\s*=>",returnBegin:!0,end:"\\s*=>",contains:[{className:"params",variants:[{begin:a},{begin:/\(\s*\)/},{begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:s,contains:l}]}]},{begin:/,/,relevance:0},{className:"",begin:/\s/,end:/\s*/,skip:!0},{variants:[{begin:"<>",end:""},{begin:n.begin,end:n.end}],subLanguage:"xml",contains:[{begin:n.begin,end:n.end,skip:!0,contains:["self"]}]}],relevance:0},{className:"function",beginKeywords:"function",end:/\{/,excludeEnd:!0,contains:[e.inherit(e.TITLE_MODE,{begin:a}),d],illegal:/\[|%/},{begin:/\$[(.]/},e.METHOD_GUARD,{className:"class",beginKeywords:"class",end:/[{;=]/,excludeEnd:!0,illegal:/[:"\[\]]/,contains:[{beginKeywords:"extends"},e.UNDERSCORE_TITLE_MODE]},{beginKeywords:"constructor",end:/\{/,excludeEnd:!0},{begin:"(get|set)\\s+(?="+a+"\\()",end:/{/,keywords:"get set",contains:[e.inherit(e.TITLE_MODE,{begin:a}),{begin:/\(\)/},d]}],illegal:/#(?!!)/}}}()); \ No newline at end of file diff --git a/docs/en/assets/jquery.js b/docs/en/assets/jquery.js new file mode 100644 index 0000000..9fd22ca --- /dev/null +++ b/docs/en/assets/jquery.js @@ -0,0 +1,2 @@ +/*! jQuery v3.5.0 | (c) JS Foundation and other contributors | jquery.org/license */ +!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],r=Object.getPrototypeOf,s=t.slice,g=t.flat?function(e){return t.flat.call(e)}:function(e){return t.concat.apply([],e)},u=t.push,i=t.indexOf,n={},o=n.toString,v=n.hasOwnProperty,a=v.toString,l=a.call(Object),y={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType},x=function(e){return null!=e&&e===e.window},E=C.document,c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.5.0",S=function(e,t){return new S.fn.init(e,t)};function p(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ye(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=S)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return g(t.replace($,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[S]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e.namespaceURI,n=(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=S,!C.getElementsByName||!C.getElementsByName(S).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+S+"-]").length||v.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||v.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+S+"+*").length||v.push(".#.+[+~]"),e.querySelectorAll("\\\f"),v.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},D=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&y(p,e)?-1:t==C||t.ownerDocument==p&&y(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&E&&!N[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function D(e,n,r){return m(n)?S.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?S.grep(e,function(e){return e===n!==r}):"string"!=typeof n?S.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(S.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||j,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof S?t[0]:t,S.merge(this,S.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),N.test(r[1])&&S.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(S):S.makeArray(e,this)}).prototype=S.fn,j=S(E);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}S.fn.extend({has:function(e){var t=S(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=E.createDocumentFragment().appendChild(E.createElement("div")),(fe=E.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),y.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="",y.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML="",y.option=!!ce.lastChild;var ge={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?S.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;n",""]);var me=/<|&#?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function qe(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&S(e).children("tbody")[0]||e}function Le(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function He(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Oe(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var Ut,Xt=[],Vt=/(=)\?(?=&|$)|\?\?/;S.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Xt.pop()||S.expando+"_"+Ct.guid++;return this[e]=!0,e}}),S.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Vt.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Vt.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Vt,"$1"+r):!1!==e.jsonp&&(e.url+=(Et.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||S.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?S(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,Xt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),y.createHTMLDocument=((Ut=E.implementation.createHTMLDocument("").body).innerHTML="
",2===Ut.childNodes.length),S.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&S(o).remove(),S.merge([],i.childNodes)));var r,i,o},S.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(S.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},S.expr.pseudos.animated=function(t){return S.grep(S.timers,function(e){return t===e.elem}).length},S.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=S.css(e,"position"),c=S(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=S.css(e,"top"),u=S.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,S.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):("number"==typeof f.top&&(f.top+="px"),"number"==typeof f.left&&(f.left+="px"),c.css(f))}},S.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){S.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===S.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===S.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=S(e).offset()).top+=S.css(e,"borderTopWidth",!0),i.left+=S.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-S.css(r,"marginTop",!0),left:t.left-i.left-S.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===S.css(e,"position"))e=e.offsetParent;return e||re})}}),S.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;S.fn[t]=function(e){return $(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),S.each(["top","left"],function(e,n){S.cssHooks[n]=$e(y.pixelPosition,function(e,t){if(t)return t=Be(e,n),Me.test(t)?S(e).position()[n]+"px":t})}),S.each({Height:"height",Width:"width"},function(a,s){S.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){S.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return $(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?S.css(e,t,i):S.style(e,t,n,i)},s,n?e:void 0,n)}})}),S.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){S.fn[t]=function(e){return this.on(t,e)}}),S.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),S.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){S.fn[n]=function(e,t){return 0 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[t]&&(void 0===o[t+1]||s'))&&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 + + + + + + + + VRPay Shopware 6 Documentation + + + + +
+
+

VRPay Shopware 6 Documentation

+

Documentation

+ +
+
+
+
+
+
+

+ 1Prerequisites

+
+
+
+

If you don’t already have one, create a VRPay account.

+
+
+
+

+ 2Installation

+
+
+
+
    +
  1. +

    Install the plugin directly from the Shopware plugin store.

    +
    + + + + + +
    +
    Note
    +
    +You also have the possibility to install the plugin using composer. See FAQ. +
    +
    +
  2. +
  3. +

    Log in to the backend of your Shopware store.

    +
  4. +
  5. +

    Navigate to Settings → System → Plugins. Click on the menu caret and select the Install link of the plugin to install it.

    +
    +
    +plugin installation +
    +
    +
  6. +
  7. +

    Activate the VRPay Payment plugin from the Plugin Manager.

    +
  8. +
+
+
+
+

+ 3Configuration

+
+
+
+
    +
  1. +

    Navigate to Settings → Plugins → VRPayment in your Shopware backend. Enter the VRPay Space ID, User ID and Authentication Key that you can create an application user.

    +
    +
    +plugin configuration +
    +
    +
    +

    If your store is configured for multiple sales channels, you may use different spaces for each store to configure different behaviours.

    +
    +
  2. +
  3. +

    Optionally after saving your configuration you can click on Set VRPayment as default payment handler. This will set VRPaymentPayment as the default payment handler for the selected sales channel.

    +
  4. +
+
+

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 → Store → Payment.

+
+
+
+

+ 4Payment method configuration

+
+
+
+
+

+ 4.1Setup

+
+
+
+

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 → Store → Payment.

+
+
+payment method configuration +
+
+
+
+

+ 4.2Payment method rules

+
+
+
+

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.

+
+
+payment method configuration availability rule +
+
+

How to create a new Rule?

+
+
    +
  1. +

    Click on “Create new rule…” in the Availability rule option and fill out the modal form with conditions, as shown below.

    +
  2. +
+
+
+payment method configuration create availability rule +
+
+
    +
  1. +

    In this example the rule is as shown below:

    +
    +
      +
    • +

      Name: Payment method for B2B.

      +
    • +
    • +

      Priority: 1 (if you wish to prioritise when using several shipping methods, please adjust the value accordingly).

      +
    • +
    • +

      Conditions: Commercial customer | Yes +Billing address: Country | Is one of | Switzerland

      +
    • +
    +
    +
  2. +
+
+

You can now select the rule in the desired payment method in the item availability rule.

+
+

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).

+
+

In this case, add another condition using the AND link and insert the following in the second condition.

+
+
    +
  1. +

    Select the newly created rule and save the changes to the chosen payment method.

    +
  2. +
+
+

Applying this rule to a payment method will result in only those customers who meet the configured conditions.

+
+
+
+

+ 4.3Customization

+
+
+
+

If you want to change the payment method description, title, logo, etc you need to do this in the payment method configuration. Changes will be synchronized automatically.

+
+
+
+
+

+ 5State graph

+
+
+
+

The Payment Process of VRPay is completely standardized for every payment method 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 Payment Documentation.

+
+

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.

+
+
+

+ 5.1State mapping of Shopware orders

+
+
+
+

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.

+
+
+

+ 5.1.1General remarks regarding order statuses

+
+
+
+

We recommend that you only change the Order status once the Payment status has reached a final state.

+
+
+
+
+

+ 5.2State mapping of Shopware payment status

+
+
+
+

Below you find a diagram that shows the state machine of Shopware for payment status including additional information for the state transitions.

+
+
+shopware 6 stage graph order +
+
+
    +
  1. +

    If the transaction is Authorized in VRPay, the Shopware order payment status is marked as In Progress.

    +
  2. +
  3. +

    If the transaction fails before or during the authorization process, the Shopware order payment status is marked as Failed.

    +
  4. +
  5. +

    If the transaction fails after the authorization, the Shopware order payment status is marked as Cancelled.

    +
  6. +
  7. +

    If the transaction invoice in VRPay is marked as Paid or Not Applicable, the Shopware order payment status is marked as Paid.

    +
  8. +
+
+
+

+ 5.2.1General remarks regarding payment statuses

+
+
+
+

We recommend that you do not change the payment status manually. If you do so, it may be changed again by the plugin.

+
+
+
+
+

+ 5.3State mapping of Shopware delivery status

+
+
+
+

Below you find a diagram that shows the state machine of Shopware delivery status including additional information for the state transitions.

+
+
+shopware 6 stage graph delivery +
+
+
    +
  1. +

    If the transaction is Confirmed status in VRPay, the Shopware order delivery status is marked as Hold.

    +
  2. +
  3. +

    If the transaction in VRPay is marked as Fulfill, the Shopware order delivery status is marked as Open.

    +
  4. +
  5. +

    If the transaction is in Decline, Failed or Voided, the Shopware order delivery status is marked as Cancelled.

    +
  6. +
+
+
+
+
+

+ 6Transaction management

+
+
+
+

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).

+
+
+

+ 6.1Complete (capture) an order

+
+
+
+

You have the possibility for your transactions to have the payment only authorized after the order is placed. Inside the connector configuration you have the option, if the payment method supports it, to define whether the payment should be completed immediately or deferred.

+
+

In order to capture a transaction, open the order and click on the Complete button.

+
+ + + + + +
+
Note
+
+When the completion is pending in VRPay the order will stay in pending state. +
+
+
+capture transaction +
+
+

Deferred payment completion

+
+

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.

+
+

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 Authorized state in VRPay and In Progress 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 Fulfill state in VRPay and into Paid state in Shopware. You can now start the fulfillment process.

+
+
+
+

+ 6.2Void a transaction

+
+
+
+

In order to void a transaction, open the order and click on the Cancel authorization button.

+
+ + + + + +
+
Note
+
+You can only void transactions that are not yet completed. +
+
+
+void transaction +
+
+
+
+

+ 6.3Refund of a transaction

+
+
+
+

You have the possibility to refund already completed transactions. In order to do so, open the captured order. By clicking on the 3 dots (…​) 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.

+
+
+refund transaction +
+
+

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.

+
+ + + + + +
+
Note
+
+It can take some time until you see the refund in Shopware. Refunds will only be visible once they have been processed successfully. +
+
+
+
+

+ 6.4On hold orders

+
+
+
+

The delivery should not be done whilst the delivery state is Hold. This happens when the transaction in VRPay +has not reached the fulfill state.

+
+

There are essentially two reasons why this can happen:

+
+
    +
  • +

    The transaction is not completed. In this case you have to complete the transaction as written above.

    +
  • +
  • +

    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.

    +
  • +
+
+

You can find more information about manual tasks in our Manual Task Documentation.

+
+
+
+

+ 6.5Limitations of the synchronization between VRPay and Shopware

+
+
+
+

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 unit price and the quantity 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.

+
+

You can find more information about Refunds in VRPay in our Refund Documentation.

+
+
+
+

+ 6.6Tokenization

+
+
+
+

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 One-Click-Payment Mode in your payment method configuration is set to allow or force storage.

+
+ + + + + +
+
Note
+
+Tokenization is not available for guest checkouts. +
+
+
+
+
+

+ 7Error logging

+
+
+
+

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.

+
+
+
+

+ 8FAQ

+
+
+
+
+

+ 8.1How to install the plugin using composer?

+
+
+
+

You can install the plugin using composer by updating the composer.json file in the root directory of your Shopware store and wait for Composer to finish updating the dependencies.

+
+
+
composer require vrpayment/shopware-6
+
+
+

Once this done, continue with step 2 of the installation process. See Installation.

+
+
+
+

+ 8.2How can I make the payment methods appear in the checkout?

+
+
+
+

Make sure that you followed the Configuration section by stating your VRPay space ID and application user’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.

+
+

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’s total calculation matches the actual order total.

+
+
+
+
+

+ 9Sending emails with Flow Builder

+
+
+
+

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).

+
+

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.

+
+
+
+

+ 10Troubleshooting

+
+
+
+
+

+ 10.1Webhook error (API version not available)

+
+
+
+

Webhooks communication fails because of a reply saying HTTP 404 with this or a similar error in the response: +{"errors":[{"code":"0","status":"404","title":"Not Found","detail":"Requested api version v1 not available, available versions are v2, v3."}]}

+
+

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.

+
+
+
+
+

+ 11Support

+
+
+
+

If you need help, feel free to contact our support.

+
+
+
+
+ +
+
+ + + + + + + + diff --git a/src/Core/Api/Configuration/Controller/ConfigurationController.php b/src/Core/Api/Configuration/Controller/ConfigurationController.php new file mode 100644 index 0000000..221090c --- /dev/null +++ b/src/Core/Api/Configuration/Controller/ConfigurationController.php @@ -0,0 +1,238 @@ + ['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([]); + } +} diff --git a/src/Core/Api/OrderDeliveryState/Command/OrderDeliveryStateCommand.php b/src/Core/Api/OrderDeliveryState/Command/OrderDeliveryStateCommand.php new file mode 100644 index 0000000..bfa3607 --- /dev/null +++ b/src/Core/Api/OrderDeliveryState/Command/OrderDeliveryStateCommand.php @@ -0,0 +1,59 @@ +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.'); + } + +} diff --git a/src/Core/Api/OrderDeliveryState/Handler/OrderDeliveryStateHandler.php b/src/Core/Api/OrderDeliveryState/Handler/OrderDeliveryStateHandler.php new file mode 100644 index 0000000..6eae659 --- /dev/null +++ b/src/Core/Api/OrderDeliveryState/Handler/OrderDeliveryStateHandler.php @@ -0,0 +1,88 @@ +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 + ); + } +} \ No newline at end of file diff --git a/src/Core/Api/OrderDeliveryState/Service/OrderDeliveryStateService.php b/src/Core/Api/OrderDeliveryState/Service/OrderDeliveryStateService.php new file mode 100644 index 0000000..720a398 --- /dev/null +++ b/src/Core/Api/OrderDeliveryState/Service/OrderDeliveryStateService.php @@ -0,0 +1,197 @@ +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); + } +} \ No newline at end of file diff --git a/src/Core/Api/PaymentMethodConfiguration/Command/PaymentMethodConfigurationCommand.php b/src/Core/Api/PaymentMethodConfiguration/Command/PaymentMethodConfigurationCommand.php new file mode 100644 index 0000000..89a1424 --- /dev/null +++ b/src/Core/Api/PaymentMethodConfiguration/Command/PaymentMethodConfigurationCommand.php @@ -0,0 +1,62 @@ +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.'); + } + +} diff --git a/src/Core/Api/PaymentMethodConfiguration/Command/PaymentMethodDefaultCommand.php b/src/Core/Api/PaymentMethodConfiguration/Command/PaymentMethodDefaultCommand.php new file mode 100644 index 0000000..3e9e309 --- /dev/null +++ b/src/Core/Api/PaymentMethodConfiguration/Command/PaymentMethodDefaultCommand.php @@ -0,0 +1,61 @@ +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.'); + } + +} diff --git a/src/Core/Api/PaymentMethodConfiguration/Entity/PaymentMethodConfigurationEntity.php b/src/Core/Api/PaymentMethodConfiguration/Entity/PaymentMethodConfigurationEntity.php new file mode 100644 index 0000000..8c9d6b3 --- /dev/null +++ b/src/Core/Api/PaymentMethodConfiguration/Entity/PaymentMethodConfigurationEntity.php @@ -0,0 +1,165 @@ +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; + } +} \ No newline at end of file diff --git a/src/Core/Api/PaymentMethodConfiguration/Entity/PaymentMethodConfigurationEntityCollection.php b/src/Core/Api/PaymentMethodConfiguration/Entity/PaymentMethodConfigurationEntityCollection.php new file mode 100644 index 0000000..28df1ba --- /dev/null +++ b/src/Core/Api/PaymentMethodConfiguration/Entity/PaymentMethodConfigurationEntityCollection.php @@ -0,0 +1,29 @@ +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; + } + +} \ No newline at end of file diff --git a/src/Core/Api/PaymentMethodConfiguration/Service/PaymentMethodConfigurationService.php b/src/Core/Api/PaymentMethodConfiguration/Service/PaymentMethodConfigurationService.php new file mode 100644 index 0000000..623764a --- /dev/null +++ b/src/Core/Api/PaymentMethodConfiguration/Service/PaymentMethodConfigurationService.php @@ -0,0 +1,617 @@ +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; + } + } + + +} diff --git a/src/Core/Api/Refund/Controller/RefundController.php b/src/Core/Api/Refund/Controller/RefundController.php new file mode 100644 index 0000000..0133c85 --- /dev/null +++ b/src/Core/Api/Refund/Controller/RefundController.php @@ -0,0 +1,149 @@ + ['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); + } +} diff --git a/src/Core/Api/Refund/Entity/RefundEntity.php b/src/Core/Api/Refund/Entity/RefundEntity.php new file mode 100644 index 0000000..e8fe94a --- /dev/null +++ b/src/Core/Api/Refund/Entity/RefundEntity.php @@ -0,0 +1,145 @@ +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; + } +} \ No newline at end of file diff --git a/src/Core/Api/Refund/Entity/RefundEntityCollection.php b/src/Core/Api/Refund/Entity/RefundEntityCollection.php new file mode 100644 index 0000000..c3d8520 --- /dev/null +++ b/src/Core/Api/Refund/Entity/RefundEntityCollection.php @@ -0,0 +1,57 @@ +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; + } +} \ No newline at end of file diff --git a/src/Core/Api/Refund/Entity/RefundEntityDefinition.php b/src/Core/Api/Refund/Entity/RefundEntityDefinition.php new file mode 100644 index 0000000..9c80bee --- /dev/null +++ b/src/Core/Api/Refund/Entity/RefundEntityDefinition.php @@ -0,0 +1,70 @@ +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; + } + +} \ No newline at end of file diff --git a/src/Core/Api/Refund/Service/RefundService.php b/src/Core/Api/Refund/Service/RefundService.php new file mode 100644 index 0000000..defb2f8 --- /dev/null +++ b/src/Core/Api/Refund/Service/RefundService.php @@ -0,0 +1,244 @@ +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(); + } + +} diff --git a/src/Core/Api/Space/Service/SpaceService.php b/src/Core/Api/Space/Service/SpaceService.php new file mode 100644 index 0000000..6728ef4 --- /dev/null +++ b/src/Core/Api/Space/Service/SpaceService.php @@ -0,0 +1,179 @@ +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; + } + +} \ No newline at end of file diff --git a/src/Core/Api/Transaction/Controller/TransactionCompletionController.php b/src/Core/Api/Transaction/Controller/TransactionCompletionController.php new file mode 100644 index 0000000..c3bccc8 --- /dev/null +++ b/src/Core/Api/Transaction/Controller/TransactionCompletionController.php @@ -0,0 +1,92 @@ + ['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 + ); + } +} diff --git a/src/Core/Api/Transaction/Controller/TransactionController.php b/src/Core/Api/Transaction/Controller/TransactionController.php new file mode 100644 index 0000000..560d3cd --- /dev/null +++ b/src/Core/Api/Transaction/Controller/TransactionController.php @@ -0,0 +1,165 @@ + ['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; + } +} diff --git a/src/Core/Api/Transaction/Controller/TransactionVoidController.php b/src/Core/Api/Transaction/Controller/TransactionVoidController.php new file mode 100644 index 0000000..bcf4fcf --- /dev/null +++ b/src/Core/Api/Transaction/Controller/TransactionVoidController.php @@ -0,0 +1,90 @@ + ['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 + ); + } +} diff --git a/src/Core/Api/Transaction/Entity/TransactionEntity.php b/src/Core/Api/Transaction/Entity/TransactionEntity.php new file mode 100644 index 0000000..a2e661e --- /dev/null +++ b/src/Core/Api/Transaction/Entity/TransactionEntity.php @@ -0,0 +1,321 @@ +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; + } +} diff --git a/src/Core/Api/Transaction/Entity/TransactionEntityCollection.php b/src/Core/Api/Transaction/Entity/TransactionEntityCollection.php new file mode 100644 index 0000000..7714125 --- /dev/null +++ b/src/Core/Api/Transaction/Entity/TransactionEntityCollection.php @@ -0,0 +1,46 @@ +getIterator() as $element) { + if ($element->getTransactionId() === $transactionId) { + return $element; + } + } + + return null; + } + + /** + * @return string + */ + protected function getExpectedClass(): string + { + return TransactionEntity::class; + } +} \ No newline at end of file diff --git a/src/Core/Api/Transaction/Entity/TransactionEntityDefinition.php b/src/Core/Api/Transaction/Entity/TransactionEntityDefinition.php new file mode 100644 index 0000000..3dcb045 --- /dev/null +++ b/src/Core/Api/Transaction/Entity/TransactionEntityDefinition.php @@ -0,0 +1,89 @@ +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; + } + +} diff --git a/src/Core/Api/Transaction/Service/OrderMailService.php b/src/Core/Api/Transaction/Service/OrderMailService.php new file mode 100644 index 0000000..966d22e --- /dev/null +++ b/src/Core/Api/Transaction/Service/OrderMailService.php @@ -0,0 +1,261 @@ +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); + } +} diff --git a/src/Core/Api/Transaction/Service/TransactionService.php b/src/Core/Api/Transaction/Service/TransactionService.php new file mode 100644 index 0000000..84f7642 --- /dev/null +++ b/src/Core/Api/Transaction/Service/TransactionService.php @@ -0,0 +1,747 @@ +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; + } +} diff --git a/src/Core/Api/WebHooks/Command/WebHooksCommand.php b/src/Core/Api/WebHooks/Command/WebHooksCommand.php new file mode 100644 index 0000000..7292b51 --- /dev/null +++ b/src/Core/Api/WebHooks/Command/WebHooksCommand.php @@ -0,0 +1,62 @@ +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.'); + } + +} diff --git a/src/Core/Api/WebHooks/Controller/WebHookController.php b/src/Core/Api/WebHooks/Controller/WebHookController.php new file mode 100644 index 0000000..be0952e --- /dev/null +++ b/src/Core/Api/WebHooks/Controller/WebHookController.php @@ -0,0 +1,706 @@ + ['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()); + } + } +} diff --git a/src/Core/Api/WebHooks/Service/WebHooksService.php b/src/Core/Api/WebHooks/Service/WebHooksService.php new file mode 100644 index 0000000..8129e99 --- /dev/null +++ b/src/Core/Api/WebHooks/Service/WebHooksService.php @@ -0,0 +1,403 @@ + 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); + } + +} \ No newline at end of file diff --git a/src/Core/Api/WebHooks/Strategy/WebHookPaymentMethodConfigurationStrategy.php b/src/Core/Api/WebHooks/Strategy/WebHookPaymentMethodConfigurationStrategy.php new file mode 100644 index 0000000..e04327c --- /dev/null +++ b/src/Core/Api/WebHooks/Strategy/WebHookPaymentMethodConfigurationStrategy.php @@ -0,0 +1,47 @@ +paymentMethodConfigurationService + ->setSalesChannelId($this->getSalesChannelId()) + ->synchronize($this->getContext()); + + return new JsonResponse(['result' => $result]); + } +} diff --git a/src/Core/Api/WebHooks/Strategy/WebHookRefundStrategy.php b/src/Core/Api/WebHooks/Strategy/WebHookRefundStrategy.php new file mode 100644 index 0000000..0eeeb01 --- /dev/null +++ b/src/Core/Api/WebHooks/Strategy/WebHookRefundStrategy.php @@ -0,0 +1,200 @@ +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; + } +} diff --git a/src/Core/Api/WebHooks/Strategy/WebHookStrategyBase.php b/src/Core/Api/WebHooks/Strategy/WebHookStrategyBase.php new file mode 100644 index 0000000..ff96b1b --- /dev/null +++ b/src/Core/Api/WebHooks/Strategy/WebHookStrategyBase.php @@ -0,0 +1,466 @@ +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; + } + } +} diff --git a/src/Core/Api/WebHooks/Strategy/WebHookStrategyInterface.php b/src/Core/Api/WebHooks/Strategy/WebHookStrategyInterface.php new file mode 100644 index 0000000..5cc934e --- /dev/null +++ b/src/Core/Api/WebHooks/Strategy/WebHookStrategyInterface.php @@ -0,0 +1,35 @@ +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; + } + } +} diff --git a/src/Core/Api/WebHooks/Strategy/WebHookTransactionInvoiceStrategy.php b/src/Core/Api/WebHooks/Strategy/WebHookTransactionInvoiceStrategy.php new file mode 100644 index 0000000..9053367 --- /dev/null +++ b/src/Core/Api/WebHooks/Strategy/WebHookTransactionInvoiceStrategy.php @@ -0,0 +1,196 @@ +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 + ); + } + } +} diff --git a/src/Core/Api/WebHooks/Strategy/WebHookTransactionStrategy.php b/src/Core/Api/WebHooks/Strategy/WebHookTransactionStrategy.php new file mode 100644 index 0000000..a100f45 --- /dev/null +++ b/src/Core/Api/WebHooks/Strategy/WebHookTransactionStrategy.php @@ -0,0 +1,169 @@ +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); + } +} diff --git a/src/Core/Api/WebHooks/Strategy/WebhookStrategyActionsInterface.php b/src/Core/Api/WebHooks/Strategy/WebhookStrategyActionsInterface.php new file mode 100644 index 0000000..a2e25de --- /dev/null +++ b/src/Core/Api/WebHooks/Strategy/WebhookStrategyActionsInterface.php @@ -0,0 +1,61 @@ +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; + } + + +} diff --git a/src/Core/Api/WebHooks/Struct/WebHookRequest.php b/src/Core/Api/WebHooks/Struct/WebHookRequest.php new file mode 100644 index 0000000..682ce75 --- /dev/null +++ b/src/Core/Api/WebHooks/Struct/WebHookRequest.php @@ -0,0 +1,204 @@ +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; + } +} \ No newline at end of file diff --git a/src/Core/Checkout/PaymentHandler/VRPaymentPaymentHandler.php b/src/Core/Checkout/PaymentHandler/VRPaymentPaymentHandler.php new file mode 100644 index 0000000..f600b5d --- /dev/null +++ b/src/Core/Checkout/PaymentHandler/VRPaymentPaymentHandler.php @@ -0,0 +1,149 @@ +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()); + } + } +} diff --git a/src/Core/Settings/Command/CreateMerchantCommand.php b/src/Core/Settings/Command/CreateMerchantCommand.php new file mode 100644 index 0000000..3f6f100 --- /dev/null +++ b/src/Core/Settings/Command/CreateMerchantCommand.php @@ -0,0 +1,189 @@ +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'); + } +} diff --git a/src/Core/Settings/Command/SettingsCommand.php b/src/Core/Settings/Command/SettingsCommand.php new file mode 100644 index 0000000..f1a6cca --- /dev/null +++ b/src/Core/Settings/Command/SettingsCommand.php @@ -0,0 +1,135 @@ +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 + ); + } +} diff --git a/src/Core/Settings/Options/Integration.php b/src/Core/Settings/Options/Integration.php new file mode 100644 index 0000000..8b53721 --- /dev/null +++ b/src/Core/Settings/Options/Integration.php @@ -0,0 +1,42 @@ +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); + } +} \ No newline at end of file diff --git a/src/Core/Settings/Struct/Settings.php b/src/Core/Settings/Struct/Settings.php new file mode 100644 index 0000000..a9bc584 --- /dev/null +++ b/src/Core/Settings/Struct/Settings.php @@ -0,0 +1,271 @@ +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; + } +} diff --git a/src/Core/Storefront/Account/Controller/AccountOrderController.php b/src/Core/Storefront/Account/Controller/AccountOrderController.php new file mode 100644 index 0000000..0978148 --- /dev/null +++ b/src/Core/Storefront/Account/Controller/AccountOrderController.php @@ -0,0 +1,130 @@ + ['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(); + } +} diff --git a/src/Core/Storefront/Account/Subscriber/AccountOrderSubscriber.php b/src/Core/Storefront/Account/Subscriber/AccountOrderSubscriber.php new file mode 100644 index 0000000..aa73301 --- /dev/null +++ b/src/Core/Storefront/Account/Subscriber/AccountOrderSubscriber.php @@ -0,0 +1,83 @@ +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); + } +} \ No newline at end of file diff --git a/src/Core/Storefront/Checkout/Controller/CheckoutController.php b/src/Core/Storefront/Checkout/Controller/CheckoutController.php new file mode 100644 index 0000000..73fb388 --- /dev/null +++ b/src/Core/Storefront/Checkout/Controller/CheckoutController.php @@ -0,0 +1,555 @@ + ['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; + } +} diff --git a/src/Core/Storefront/Checkout/Struct/CheckoutPageData.php b/src/Core/Storefront/Checkout/Struct/CheckoutPageData.php new file mode 100644 index 0000000..d926d54 --- /dev/null +++ b/src/Core/Storefront/Checkout/Struct/CheckoutPageData.php @@ -0,0 +1,200 @@ +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; + } +} \ No newline at end of file diff --git a/src/Core/Storefront/Checkout/Subscriber/CheckoutSubscriber.php b/src/Core/Storefront/Checkout/Subscriber/CheckoutSubscriber.php new file mode 100644 index 0000000..1feb143 --- /dev/null +++ b/src/Core/Storefront/Checkout/Subscriber/CheckoutSubscriber.php @@ -0,0 +1,256 @@ +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; + } + } +} diff --git a/src/Core/Storefront/Framework/Cookie/VRPaymentCookieProvider.php b/src/Core/Storefront/Framework/Cookie/VRPaymentCookieProvider.php new file mode 100644 index 0000000..5c3b714 --- /dev/null +++ b/src/Core/Storefront/Framework/Cookie/VRPaymentCookieProvider.php @@ -0,0 +1,54 @@ +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'); + } +} \ No newline at end of file diff --git a/src/Core/Util/Analytics/Analytics.php b/src/Core/Util/Analytics/Analytics.php new file mode 100644 index 0000000..f7ee765 --- /dev/null +++ b/src/Core/Util/Analytics/Analytics.php @@ -0,0 +1,42 @@ + '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); + } + } +} + + diff --git a/src/Core/Util/Exception/InvalidPayloadException.php b/src/Core/Util/Exception/InvalidPayloadException.php new file mode 100644 index 0000000..5ffe7e3 --- /dev/null +++ b/src/Core/Util/Exception/InvalidPayloadException.php @@ -0,0 +1,9 @@ +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(); + } +} diff --git a/src/Core/Util/Payload/AbstractPayload.php b/src/Core/Util/Payload/AbstractPayload.php new file mode 100644 index 0000000..5b0057b --- /dev/null +++ b/src/Core/Util/Payload/AbstractPayload.php @@ -0,0 +1,52 @@ +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); + } + +} \ No newline at end of file diff --git a/src/Core/Util/Payload/CustomProducts/CustomProductsLineItemTypes.php b/src/Core/Util/Payload/CustomProducts/CustomProductsLineItemTypes.php new file mode 100644 index 0000000..4c791fc --- /dev/null +++ b/src/Core/Util/Payload/CustomProducts/CustomProductsLineItemTypes.php @@ -0,0 +1,16 @@ +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; + } +} \ No newline at end of file diff --git a/src/Core/Util/Payload/RefundPayload.php b/src/Core/Util/Payload/RefundPayload.php new file mode 100644 index 0000000..20a762f --- /dev/null +++ b/src/Core/Util/Payload/RefundPayload.php @@ -0,0 +1,171 @@ +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; + } +} diff --git a/src/Core/Util/Payload/TransactionPayload.php b/src/Core/Util/Payload/TransactionPayload.php new file mode 100644 index 0000000..8a697d5 --- /dev/null +++ b/src/Core/Util/Payload/TransactionPayload.php @@ -0,0 +1,809 @@ +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 + ); + } +} diff --git a/src/Core/Util/PaymentMethodUtil.php b/src/Core/Util/PaymentMethodUtil.php new file mode 100644 index 0000000..d64e6df --- /dev/null +++ b/src/Core/Util/PaymentMethodUtil.php @@ -0,0 +1,205 @@ +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); + } + +} \ No newline at end of file diff --git a/src/Core/Util/Traits/VRPaymentPaymentPluginTrait.php b/src/Core/Util/Traits/VRPaymentPaymentPluginTrait.php new file mode 100644 index 0000000..4de4eeb --- /dev/null +++ b/src/Core/Util/Traits/VRPaymentPaymentPluginTrait.php @@ -0,0 +1,115 @@ +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); + } + } +} diff --git a/src/Migration/Migration1590156974PaymentMethodConfigurationEntity.php b/src/Migration/Migration1590156974PaymentMethodConfigurationEntity.php new file mode 100644 index 0000000..2eb442e --- /dev/null +++ b/src/Migration/Migration1590156974PaymentMethodConfigurationEntity.php @@ -0,0 +1,60 @@ +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 + } +} diff --git a/src/Migration/Migration1590156974TransactionEntity.php b/src/Migration/Migration1590156974TransactionEntity.php new file mode 100644 index 0000000..16b93c5 --- /dev/null +++ b/src/Migration/Migration1590156974TransactionEntity.php @@ -0,0 +1,69 @@ +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 + } +} diff --git a/src/Migration/Migration1590646356OrderEntity.php b/src/Migration/Migration1590646356OrderEntity.php new file mode 100644 index 0000000..e381e1a --- /dev/null +++ b/src/Migration/Migration1590646356OrderEntity.php @@ -0,0 +1,47 @@ +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 + } +} diff --git a/src/Migration/Migration1590646356RefundEntity.php b/src/Migration/Migration1590646356RefundEntity.php new file mode 100644 index 0000000..272d2ec --- /dev/null +++ b/src/Migration/Migration1590646356RefundEntity.php @@ -0,0 +1,59 @@ +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 + } +} diff --git a/src/Migration/Migration1590646356TransactionEntity.php b/src/Migration/Migration1590646356TransactionEntity.php new file mode 100644 index 0000000..1e36e65 --- /dev/null +++ b/src/Migration/Migration1590646356TransactionEntity.php @@ -0,0 +1,48 @@ +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 + } +} diff --git a/src/Migration/Migration1605701047PaymentMethodConfigurationEntity.php b/src/Migration/Migration1605701047PaymentMethodConfigurationEntity.php new file mode 100644 index 0000000..11c9271 --- /dev/null +++ b/src/Migration/Migration1605701047PaymentMethodConfigurationEntity.php @@ -0,0 +1,44 @@ +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 + } +} diff --git a/src/Migration/Migration1605701048TransactionEntity.php b/src/Migration/Migration1605701048TransactionEntity.php new file mode 100644 index 0000000..5368d14 --- /dev/null +++ b/src/Migration/Migration1605701048TransactionEntity.php @@ -0,0 +1,78 @@ +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 + } +} diff --git a/src/Migration/Migration1605701049StateMachineEntity.php b/src/Migration/Migration1605701049StateMachineEntity.php new file mode 100644 index 0000000..5f830dd --- /dev/null +++ b/src/Migration/Migration1605701049StateMachineEntity.php @@ -0,0 +1,89 @@ +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 + } +} diff --git a/src/Migration/Migration1684240994TransactionEntity.php b/src/Migration/Migration1684240994TransactionEntity.php new file mode 100644 index 0000000..2fc4bf9 --- /dev/null +++ b/src/Migration/Migration1684240994TransactionEntity.php @@ -0,0 +1,48 @@ +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 + } +} diff --git a/src/Resources/app/administration/src/core/service/api/vrpayment-configuration.service.js b/src/Resources/app/administration/src/core/service/api/vrpayment-configuration.service.js new file mode 100644 index 0000000..8e0d302 --- /dev/null +++ b/src/Resources/app/administration/src/core/service/api/vrpayment-configuration.service.js @@ -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; diff --git a/src/Resources/app/administration/src/core/service/api/vrpayment-refund.service.js b/src/Resources/app/administration/src/core/service/api/vrpayment-refund.service.js new file mode 100644 index 0000000..dd71437 --- /dev/null +++ b/src/Resources/app/administration/src/core/service/api/vrpayment-refund.service.js @@ -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; diff --git a/src/Resources/app/administration/src/core/service/api/vrpayment-transaction-completion.service.js b/src/Resources/app/administration/src/core/service/api/vrpayment-transaction-completion.service.js new file mode 100644 index 0000000..233ca71 --- /dev/null +++ b/src/Resources/app/administration/src/core/service/api/vrpayment-transaction-completion.service.js @@ -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; \ No newline at end of file diff --git a/src/Resources/app/administration/src/core/service/api/vrpayment-transaction-void.service.js b/src/Resources/app/administration/src/core/service/api/vrpayment-transaction-void.service.js new file mode 100644 index 0000000..a70cdcd --- /dev/null +++ b/src/Resources/app/administration/src/core/service/api/vrpayment-transaction-void.service.js @@ -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; \ No newline at end of file diff --git a/src/Resources/app/administration/src/core/service/api/vrpayment-transaction.service.js b/src/Resources/app/administration/src/core/service/api/vrpayment-transaction.service.js new file mode 100644 index 0000000..6683b6d --- /dev/null +++ b/src/Resources/app/administration/src/core/service/api/vrpayment-transaction.service.js @@ -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; \ No newline at end of file diff --git a/src/Resources/app/administration/src/core/service/api/vrpayment-webhook-register.service.js b/src/Resources/app/administration/src/core/service/api/vrpayment-webhook-register.service.js new file mode 100644 index 0000000..c612c12 --- /dev/null +++ b/src/Resources/app/administration/src/core/service/api/vrpayment-webhook-register.service.js @@ -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; diff --git a/src/Resources/app/administration/src/init/api-service.init.js b/src/Resources/app/administration/src/init/api-service.init.js new file mode 100644 index 0000000..0f93bc3 --- /dev/null +++ b/src/Resources/app/administration/src/init/api-service.init.js @@ -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); +}); \ No newline at end of file diff --git a/src/Resources/app/administration/src/main.js b/src/Resources/app/administration/src/main.js new file mode 100644 index 0000000..082fc10 --- /dev/null +++ b/src/Resources/app/administration/src/main.js @@ -0,0 +1,3 @@ +import './module/vrpayment-order'; +import './module/vrpayment-settings'; +import './init/api-service.init'; \ No newline at end of file diff --git a/src/Resources/app/administration/src/module/vrpayment-order/component/vrpayment-order-action-completion/index.html.twig b/src/Resources/app/administration/src/module/vrpayment-order/component/vrpayment-order-action-completion/index.html.twig new file mode 100644 index 0000000..506861f --- /dev/null +++ b/src/Resources/app/administration/src/module/vrpayment-order/component/vrpayment-order-action-completion/index.html.twig @@ -0,0 +1,24 @@ +{% block vrpayment_order_action_completion %} + + + {% block vrpayment_order_action_completion_amount %} + + + {% endblock %} + + {% block vrpayment_order_action_completion_confirm_button %} + + {% endblock %} + + + +{% endblock %} diff --git a/src/Resources/app/administration/src/module/vrpayment-order/component/vrpayment-order-action-completion/index.js b/src/Resources/app/administration/src/module/vrpayment-order/component/vrpayment-order-action-completion/index.js new file mode 100644 index 0000000..553ca4c --- /dev/null +++ b/src/Resources/app/administration/src/module/vrpayment-order/component/vrpayment-order-action-completion/index.js @@ -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()}`); + }); + } + }); + } + } + } +}); diff --git a/src/Resources/app/administration/src/module/vrpayment-order/component/vrpayment-order-action-refund-by-amount/index.html.twig b/src/Resources/app/administration/src/module/vrpayment-order/component/vrpayment-order-action-refund-by-amount/index.html.twig new file mode 100644 index 0000000..3f00152 --- /dev/null +++ b/src/Resources/app/administration/src/module/vrpayment-order/component/vrpayment-order-action-refund-by-amount/index.html.twig @@ -0,0 +1,26 @@ +{% block vrpayment_order_action_refund_by_amount %} + + + {% block vrpayment_order_action_refund_amount_by_amount %} + + + {% endblock %} + + {% block vrpayment_order_action_refund_confirm_button_by_amount %} + + {% endblock %} + + + +{% endblock %} diff --git a/src/Resources/app/administration/src/module/vrpayment-order/component/vrpayment-order-action-refund-by-amount/index.js b/src/Resources/app/administration/src/module/vrpayment-order/component/vrpayment-order-action-refund-by-amount/index.js new file mode 100644 index 0000000..2a0e0ad --- /dev/null +++ b/src/Resources/app/administration/src/module/vrpayment-order/component/vrpayment-order-action-refund-by-amount/index.js @@ -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()}`); + }); + } + }); + } + } +}); diff --git a/src/Resources/app/administration/src/module/vrpayment-order/component/vrpayment-order-action-refund-partial/index.html.twig b/src/Resources/app/administration/src/module/vrpayment-order/component/vrpayment-order-action-refund-partial/index.html.twig new file mode 100644 index 0000000..c3aa0cc --- /dev/null +++ b/src/Resources/app/administration/src/module/vrpayment-order/component/vrpayment-order-action-refund-partial/index.html.twig @@ -0,0 +1,31 @@ +{% block vrpayment_order_action_refund_partial %} + + + {% block vrpayment_order_action_refund_amount_partial %} + + + +
+ {{ $tc('vrpayment-order.refundAction.maxAvailableAmountToRefund') }}: + {{ this.$parent.$parent.itemRefundableAmount }} +
+ {% endblock %} + + {% block vrpayment_order_action_refund_confirm_button_partial %} + + {% endblock %} + + +
+{% endblock %} diff --git a/src/Resources/app/administration/src/module/vrpayment-order/component/vrpayment-order-action-refund-partial/index.js b/src/Resources/app/administration/src/module/vrpayment-order/component/vrpayment-order-action-refund-partial/index.js new file mode 100644 index 0000000..b9274e8 --- /dev/null +++ b/src/Resources/app/administration/src/module/vrpayment-order/component/vrpayment-order-action-refund-partial/index.js @@ -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; + } + } + } +}); diff --git a/src/Resources/app/administration/src/module/vrpayment-order/component/vrpayment-order-action-refund-selected/index.html.twig b/src/Resources/app/administration/src/module/vrpayment-order/component/vrpayment-order-action-refund-selected/index.html.twig new file mode 100644 index 0000000..b1cf34d --- /dev/null +++ b/src/Resources/app/administration/src/module/vrpayment-order/component/vrpayment-order-action-refund-selected/index.html.twig @@ -0,0 +1,16 @@ +{% block vrpayment_order_action_refund_selected %} + + + {% block vrpayment_order_action_refund_confirm_button_selected %} + + {% endblock %} + + + +{% endblock %} diff --git a/src/Resources/app/administration/src/module/vrpayment-order/component/vrpayment-order-action-refund-selected/index.js b/src/Resources/app/administration/src/module/vrpayment-order/component/vrpayment-order-action-refund-selected/index.js new file mode 100644 index 0000000..e2862b7 --- /dev/null +++ b/src/Resources/app/administration/src/module/vrpayment-order/component/vrpayment-order-action-refund-selected/index.js @@ -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()}`); + }); + } + }); + } + } +}); diff --git a/src/Resources/app/administration/src/module/vrpayment-order/component/vrpayment-order-action-refund/index.html.twig b/src/Resources/app/administration/src/module/vrpayment-order/component/vrpayment-order-action-refund/index.html.twig new file mode 100644 index 0000000..d25ef15 --- /dev/null +++ b/src/Resources/app/administration/src/module/vrpayment-order/component/vrpayment-order-action-refund/index.html.twig @@ -0,0 +1,31 @@ +{% block vrpayment_order_action_refund %} + + + {% block vrpayment_order_action_refund_amount %} + + + + +
+ {{ $tc('vrpayment-order.refundAction.maxAvailableItemsToRefund') }}: + {{ this.$parent.$parent.itemRefundableQuantity }} +
+ {% endblock %} + + {% block vrpayment_order_action_refund_confirm_button %} + + {% endblock %} + + +
+{% endblock %} diff --git a/src/Resources/app/administration/src/module/vrpayment-order/component/vrpayment-order-action-refund/index.js b/src/Resources/app/administration/src/module/vrpayment-order/component/vrpayment-order-action-refund/index.js new file mode 100644 index 0000000..243dc97 --- /dev/null +++ b/src/Resources/app/administration/src/module/vrpayment-order/component/vrpayment-order-action-refund/index.js @@ -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()}`); + }); + } + }); + } + } +}); diff --git a/src/Resources/app/administration/src/module/vrpayment-order/component/vrpayment-order-action-void/index.html.twig b/src/Resources/app/administration/src/module/vrpayment-order/component/vrpayment-order-action-void/index.html.twig new file mode 100644 index 0000000..98c67c1 --- /dev/null +++ b/src/Resources/app/administration/src/module/vrpayment-order/component/vrpayment-order-action-void/index.html.twig @@ -0,0 +1,24 @@ +{% block vrpayment_order_action_void %} + + + {% block vrpayment_order_action_void_amount %} + + + {% endblock %} + + {% block vrpayment_order_action_void_confirm_button %} + + {% endblock %} + + + +{% endblock %} diff --git a/src/Resources/app/administration/src/module/vrpayment-order/component/vrpayment-order-action-void/index.js b/src/Resources/app/administration/src/module/vrpayment-order/component/vrpayment-order-action-void/index.js new file mode 100644 index 0000000..92bbe58 --- /dev/null +++ b/src/Resources/app/administration/src/module/vrpayment-order/component/vrpayment-order-action-void/index.js @@ -0,0 +1,138 @@ +/* global Shopware */ + +import template from './index.html.twig'; + +const {Component, Mixin, Filter, Utils} = Shopware; + +Component.register('vrpayment-order-action-void', { + template, + + inject: ['VRPaymentTransactionVoidService'], + + mixins: [ + Mixin.getByName('notification') + ], + + props: { + transactionData: { + type: Object, + required: true + } + }, + + data() { + return { + isLoading: true, + isVoid: false + }; + }, + + computed: { + dateFilter() { + return Filter.getByName('date'); + }, + lineItemColumns() { + return [ + { + property: 'uniqueId', + label: this.$tc('vrpayment-order.refund.types.uniqueId'), + rawData: false, + allowResize: true, + primary: true, + width: 'auto' + }, + { + property: 'name', + label: this.$tc('vrpayment-order.refund.types.name'), + rawData: true, + allowResize: true, + sortable: true, + width: 'auto' + }, + { + property: 'quantity', + label: this.$tc('vrpayment-order.refund.types.quantity'), + rawData: true, + allowResize: true, + width: 'auto' + }, + { + property: 'amountIncludingTax', + label: this.$tc('vrpayment-order.refund.types.amountIncludingTax'), + rawData: true, + allowResize: true, + inlineEdit: 'string', + width: 'auto' + }, + { + property: 'type', + label: this.$tc('vrpayment-order.refund.types.type'), + rawData: true, + allowResize: true, + sortable: true, + width: 'auto' + }, + { + property: 'taxAmount', + label: this.$tc('vrpayment-order.refund.types.taxAmount'), + rawData: true, + allowResize: true, + width: 'auto' + } + ]; + } + }, + + created() { + this.createdComponent(); + }, + + methods: { + createdComponent() { + this.isLoading = false; + this.currency = this.transactionData.transactions[0].currency; + this.refundableAmount = this.transactionData.transactions[0].amountIncludingTax; + this.refundAmount = this.transactionData.transactions[0].amountIncludingTax; + }, + + voidPayment() { + if (this.isVoid) { + this.isLoading = true; + this.VRPaymentTransactionVoidService.createTransactionVoid( + this.transactionData.transactions[0].metaData.salesChannelId, + this.transactionData.transactions[0].id + ).then(() => { + this.createNotificationSuccess({ + title: this.$tc('vrpayment-order.voidAction.successTitle'), + message: this.$tc('vrpayment-order.voidAction.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()}`); + }); + } + }); + } + } + } +}); diff --git a/src/Resources/app/administration/src/module/vrpayment-order/extension/sw-order/index.js b/src/Resources/app/administration/src/module/vrpayment-order/extension/sw-order/index.js new file mode 100644 index 0000000..bf3a925 --- /dev/null +++ b/src/Resources/app/administration/src/module/vrpayment-order/extension/sw-order/index.js @@ -0,0 +1,75 @@ +/* global Shopware */ + +import template from './sw-order.html.twig'; +import './sw-order.scss'; + +const {Component, Context} = Shopware; +const Criteria = Shopware.Data.Criteria; + +const vrpaymentFormattedHandlerIdentifier = 'handler_vrpaymentpayment_vrpaymentpaymenthandler'; + +Component.override('sw-order-detail', { + template, + + data() { + return { + isVRPaymentPayment: false + }; + }, + + computed: { + isEditable() { + return !this.isVRPaymentPayment || this.$route.name !== 'vrpayment.order.detail'; + }, + showTabs() { + return true; + } + }, + + watch: { + orderId: { + deep: true, + handler() { + if (!this.orderId) { + this.setIsVRPaymentPayment(null); + return; + } + + const orderRepository = this.repositoryFactory.create('order'); + const orderCriteria = new Criteria(1, 1); + orderCriteria.addAssociation('transactions'); + + orderRepository.get(this.orderId, Context.api, orderCriteria).then((order) => { + if ( + (order.amountTotal <= 0) || + (order.transactions.length <= 0) || + !order.transactions[0].paymentMethodId + ) { + this.setIsVRPaymentPayment(null); + return; + } + + const paymentMethodId = order.transactions[0].paymentMethodId; + if (paymentMethodId !== undefined && paymentMethodId !== null) { + this.setIsVRPaymentPayment(paymentMethodId); + } + }); + }, + immediate: true + } + }, + + methods: { + setIsVRPaymentPayment(paymentMethodId) { + if (!paymentMethodId) { + return; + } + const paymentMethodRepository = this.repositoryFactory.create('payment_method'); + paymentMethodRepository.get(paymentMethodId, Context.api).then( + (paymentMethod) => { + this.isVRPaymentPayment = (paymentMethod.formattedHandlerIdentifier === vrpaymentFormattedHandlerIdentifier); + } + ); + } + } +}); diff --git a/src/Resources/app/administration/src/module/vrpayment-order/extension/sw-order/sw-order.html.twig b/src/Resources/app/administration/src/module/vrpayment-order/extension/sw-order/sw-order.html.twig new file mode 100644 index 0000000..27d4108 --- /dev/null +++ b/src/Resources/app/administration/src/module/vrpayment-order/extension/sw-order/sw-order.html.twig @@ -0,0 +1,15 @@ +{% block sw_order_detail_content_tabs_general %} + {% parent %} + + + {{ $tc('vrpayment-order.header') }} + +{% endblock %} + +{% block sw_order_detail_actions_slot_smart_bar_actions %} + +{% endblock %} diff --git a/src/Resources/app/administration/src/module/vrpayment-order/extension/sw-order/sw-order.scss b/src/Resources/app/administration/src/module/vrpayment-order/extension/sw-order/sw-order.scss new file mode 100644 index 0000000..ff18ca8 --- /dev/null +++ b/src/Resources/app/administration/src/module/vrpayment-order/extension/sw-order/sw-order.scss @@ -0,0 +1,11 @@ +.sw-order-detail { + .sw-tabs { + margin-top: 40px; + } + + .sw-order-detail-base .sw-card-view__content { + overflow-x: visible; + overflow-y: visible; + } + +} diff --git a/src/Resources/app/administration/src/module/vrpayment-order/index.js b/src/Resources/app/administration/src/module/vrpayment-order/index.js new file mode 100644 index 0000000..14f4ffe --- /dev/null +++ b/src/Resources/app/administration/src/module/vrpayment-order/index.js @@ -0,0 +1,40 @@ +/* global Shopware */ + +import './extension/sw-order'; +import './page/vrpayment-order-detail'; + +import deDE from './snippet/de-DE.json'; +import enGB from './snippet/en-GB.json'; +import frFR from './snippet/fr-FR.json'; +import itIT from './snippet/it-IT.json'; + +const {Module} = Shopware; + +Module.register('vrpayment-order', { + type: 'plugin', + name: 'VRPayment', + title: 'vrpayment-order.general.title', + description: 'vrpayment-order.general.descriptionTextModule', + version: '1.0.1', + targetVersion: '1.0.1', + color: '#2b52ff', + + snippets: { + 'de-DE': deDE, + 'en-GB': enGB, + 'fr-FR': frFR, + 'it-IT': itIT + }, + + routeMiddleware(next, currentRoute) { + if (currentRoute.name === 'sw.order.detail') { + currentRoute.children.push({ + component: 'vrpayment-order-detail', + name: 'vrpayment.order.detail', + isChildren: true, + path: '/sw/order/vrpayment/detail/:id' + }); + } + next(currentRoute); + } +}); diff --git a/src/Resources/app/administration/src/module/vrpayment-order/page/vrpayment-order-detail/index.html.twig b/src/Resources/app/administration/src/module/vrpayment-order/page/vrpayment-order-detail/index.html.twig new file mode 100644 index 0000000..5d68090 --- /dev/null +++ b/src/Resources/app/administration/src/module/vrpayment-order/page/vrpayment-order-detail/index.html.twig @@ -0,0 +1,200 @@ +{% block vrpayment_order_detail %} +
+
+ + + + {% block vrpayment_order_transaction_history_card %} + + + + + {% endblock %} + {% block vrpayment_order_transaction_line_items_card %} + + + + {% endblock %} + {% block vrpayment_order_transaction_refunds_card %} + + + + + {% endblock %} + {% block vrpayment_order_actions_modal_refund_partial %} + + + {% endblock %} + {% block vrpayment_order_actions_modal_refund %} + + + {% endblock %} + {% block vrpayment_order_actions_modal_refund_by_amount %} + + + {% endblock %} + {% block vrpayment_order_actions_modal_completion%} + + + {% endblock %} + {% block vrpayment_order_actions_modal_void %} + + + {% endblock %} +
+ +
+{% endblock %} diff --git a/src/Resources/app/administration/src/module/vrpayment-order/page/vrpayment-order-detail/index.js b/src/Resources/app/administration/src/module/vrpayment-order/page/vrpayment-order-detail/index.js new file mode 100644 index 0000000..a0ac885 --- /dev/null +++ b/src/Resources/app/administration/src/module/vrpayment-order/page/vrpayment-order-detail/index.js @@ -0,0 +1,448 @@ +/* global Shopware */ + +import '../../component/vrpayment-order-action-completion'; +import '../../component/vrpayment-order-action-refund'; +import '../../component/vrpayment-order-action-refund-partial'; +import '../../component/vrpayment-order-action-refund-by-amount'; +import '../../component/vrpayment-order-action-void'; +import template from './index.html.twig'; +import './index.scss'; + +const {Component, Mixin, Filter, Context, Utils} = Shopware; +const Criteria = Shopware.Data.Criteria; + +Component.register('vrpayment-order-detail', { + template, + + inject: [ + 'VRPaymentTransactionService', + 'VRPaymentRefundService', + 'repositoryFactory' + ], + + mixins: [ + Mixin.getByName('notification') + ], + + data() { + return { + transactionData: { + transactions: [], + refunds: [] + }, + transaction: {}, + lineItems: [], + refundableQuantity: 0, + itemRefundableQuantity: 0, + isLoading: true, + orderId: '', + currency: '', + modalType: '', + refundAmount: 0.00, + refundableAmount: 0.00, + itemRefundedAmount: 0.00, + itemRefundedQuantity: 0, + itemRefundableAmount: 0.00, + currentLineItem: '', + refundLineItemQuantity: [], + refundLineItemAmount: [], + selectedItems: [] + }; + }, + + metaInfo() { + return { + title: this.$tc('vrpayment-order.header') + }; + }, + + + computed: { + dateFilter() { + return Filter.getByName('date'); + }, + + relatedResourceColumns() { + return [ + { + property: 'paymentMethodName', + label: this.$tc('vrpayment-order.transactionHistory.types.payment_method'), + rawData: true + }, + { + property: 'state', + label: this.$tc('vrpayment-order.transactionHistory.types.state'), + rawData: true + }, + { + property: 'currency', + label: this.$tc('vrpayment-order.transactionHistory.types.currency'), + rawData: true + }, + { + property: 'authorized_amount', + label: this.$tc('vrpayment-order.transactionHistory.types.authorized_amount'), + rawData: true + }, + { + property: 'id', + label: this.$tc('vrpayment-order.transactionHistory.types.transaction'), + rawData: true + }, + { + property: 'customerId', + label: this.$tc('vrpayment-order.transactionHistory.types.customer'), + rawData: true + } + ]; + }, + + lineItemColumns() { + return [ + // It must be set in order to have correctly working checkbox mechanism + { + property: 'id', + rawData: true, + visible: false, + primary: true + }, + { + property: 'uniqueId', + label: this.$tc('vrpayment-order.lineItem.types.uniqueId'), + rawData: true, + visible: false, + primary: true + }, + { + property: 'name', + label: this.$tc('vrpayment-order.lineItem.types.name'), + rawData: true + }, + { + property: 'quantity', + label: this.$tc('vrpayment-order.lineItem.types.quantity'), + rawData: true + }, + { + property: 'amountIncludingTax', + label: this.$tc('vrpayment-order.lineItem.types.amountIncludingTax'), + rawData: true + }, + { + property: 'type', + label: this.$tc('vrpayment-order.lineItem.types.type'), + rawData: true + }, + { + property: 'taxAmount', + label: this.$tc('vrpayment-order.lineItem.types.taxAmount'), + rawData: true + }, + { + property: 'refundableQuantity', + rawData: true, + visible: false, + }, + ]; + }, + + refundColumns() { + return [ + { + property: 'id', + label: this.$tc('vrpayment-order.refund.types.id'), + rawData: true, + visible: true, + primary: true + }, + { + property: 'amount', + label: this.$tc('vrpayment-order.refund.types.amount'), + rawData: true + }, + { + property: 'state', + label: this.$tc('vrpayment-order.refund.types.state'), + rawData: true + }, + { + property: 'createdOn', + label: this.$tc('vrpayment-order.refund.types.createdOn'), + rawData: true + } + ]; + } + }, + + watch: { + '$route'() { + this.resetDataAttributes(); + this.createdComponent(); + } + }, + + created() { + this.createdComponent(); + }, + + methods: { + createdComponent() { + this.orderId = this.$route.params.id; + const orderRepository = this.repositoryFactory.create('order'); + const orderCriteria = new Criteria(1, 1); + orderCriteria.addAssociation('transactions'); + orderCriteria.getAssociation('transactions').addSorting(Criteria.sort('createdAt', 'DESC')); + + orderRepository.get(this.orderId, Context.api, orderCriteria).then((order) => { + this.order = order; + this.isLoading = false; + var totalAmountTemp = 0; + var refundsAmountTemp = 0; + const vrpaymentTransactionId = order.transactions[0].customFields.vrpayment_transaction_id; + this.VRPaymentTransactionService.getTransactionData(order.salesChannelId, vrpaymentTransactionId) + .then((VRPaymentTransaction) => { + this.currency = VRPaymentTransaction.transactions[0].currency; + + VRPaymentTransaction.transactions[0].authorized_amount = Utils.format.currency( + VRPaymentTransaction.transactions[0].authorizationAmount, + this.currency + ); + + VRPaymentTransaction.refunds.forEach((refund) => { + refundsAmountTemp = parseFloat(parseFloat(refundsAmountTemp) + parseFloat(refund.amount)); + refund.amount = Utils.format.currency( + refund.amount, + this.currency + ); + + refund.reductions.forEach((reduction) => { + if (reduction.quantityReduction > 0) { + if (this.refundLineItemQuantity[reduction.lineItemUniqueId] === undefined) { + this.refundLineItemQuantity[reduction.lineItemUniqueId] = reduction.quantityReduction; + } else { + this.refundLineItemQuantity[reduction.lineItemUniqueId] += reduction.quantityReduction; + } + } + if (reduction.unitPriceReduction > 0) { + if (this.refundLineItemAmount[reduction.lineItemUniqueId] === undefined) { + this.refundLineItemAmount[reduction.lineItemUniqueId] = reduction.unitPriceReduction; + } else { + this.refundLineItemAmount[reduction.lineItemUniqueId] += reduction.unitPriceReduction; + } + } + }); + + }); + + VRPaymentTransaction.transactions[0].lineItems.forEach((lineItem) => { + if (!lineItem.id) { + lineItem.id = lineItem.uniqueId; + } + + lineItem.itemRefundedAmount = parseFloat(this.refundLineItemAmount[lineItem.uniqueId] || 0) * parseInt(lineItem.quantity); + lineItem.amountIncludingTax = parseFloat(lineItem.amountIncludingTax) || 0; + + lineItem.itemRefundedQuantity = parseInt(this.refundLineItemQuantity[lineItem.uniqueId]) || 0; + lineItem.refundableAmount = parseFloat( + (lineItem.amountIncludingTax - lineItem.itemRefundedAmount).toFixed(2) + ); + + lineItem.amountIncludingTax = Utils.format.currency( + lineItem.amountIncludingTax, + this.currency + ); + + lineItem.taxAmount = Utils.format.currency( + lineItem.taxAmount, + this.currency + ); + + totalAmountTemp = parseFloat(parseFloat(totalAmountTemp) + parseFloat(lineItem.unitPriceIncludingTax * lineItem.quantity)); + + lineItem.refundableQuantity = parseInt( + parseInt(lineItem.quantity) - parseInt(this.refundLineItemQuantity[lineItem.uniqueId] || 0) + ); + + }); + + this.lineItems = VRPaymentTransaction.transactions[0].lineItems; + this.transactionData = VRPaymentTransaction; + this.transaction = this.transactionData.transactions[0]; + this.refundAmount = Number(this.transactionData.transactions[0].amountIncludingTax); + this.refundableAmount = parseFloat(parseFloat(totalAmountTemp) - parseFloat(refundsAmountTemp)); + + }).catch((errorResponse) => { + try { + this.createNotificationError({ + title: this.$tc('vrpayment-order.paymentDetails.error.title'), + message: errorResponse.message, + autoClose: false + }); + } catch (e) { + this.createNotificationError({ + title: this.$tc('vrpayment-order.paymentDetails.error.title'), + message: errorResponse.message, + autoClose: false + }); + } finally { + this.isLoading = false; + } + }); + }); + }, + downloadPackingSlip() { + window.open( + this.VRPaymentTransactionService.getPackingSlip( + this.transaction.metaData.salesChannelId, + this.transaction.id + ), + '_blank' + ); + }, + + downloadInvoice() { + window.open( + this.VRPaymentTransactionService.getInvoiceDocument( + this.transaction.metaData.salesChannelId, + this.transaction.id + ), + '_blank' + ); + }, + + resetDataAttributes() { + this.transactionData = { + transactions: [], + refunds: [] + }; + this.lineItems = []; + this.refundLineItemQuantity = []; + this.refundLineItemAmount = []; + this.isLoading = true; + }, + + spawnModal(modalType, lineItemId, refundableQuantity, itemRefundableAmount) { + this.modalType = modalType; + this.currentLineItem = lineItemId; + this.itemRefundableQuantity = refundableQuantity; + this.itemRefundableAmount = !isNaN(itemRefundableAmount) ? Math.round(itemRefundableAmount * 100) / 100 : 0; + }, + + closeModal() { + this.modalType = ''; + }, + + lineItemRefund(lineItemId) { + this.isLoading = true; + this.VRPaymentRefundService.createRefund( + this.transactionData.transactions[0].metaData.salesChannelId, + this.transactionData.transactions[0].id, + 0, + lineItemId + ).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.response.data, + autoClose: false + }); + } finally { + this.isLoading = false; + this.$emit('modal-close'); + this.$nextTick(() => { + this.$router.replace(`${this.$route.path}?hash=${Utils.createId()}`); + }); + } + }); + }, + isSelectable(item) { + return item.refundableQuantity > 0 && item.refundableAmount > 0 && item.itemRefundedAmount == 0 && item.itemRefundedQuantity == 0; + }, + onSelectionChanged(selection) { + this.selectedItems = Object.values(selection); + }, + onPerformBulkAction() { + if (this.selectedItems.length) { + // Set isLoading to true to show the loader + this.isLoading = true; + + // Force the DOM to update before proceeding with the asynchronous operations + this.$nextTick(() => { + const refundPromises = this.selectedItems.map((item) => { + return this.lineItemRefundBulk(item.uniqueId); // Simulated refund action with delay + }); + + // Wait for all refund promises to complete + Promise.all(refundPromises) + .then(() => { + // Once all promises are resolved, hide the loader and close the modal + this.isLoading = false; + this.$emit('modal-close'); + this.$nextTick(() => { + this.$router.replace(`${this.$route.path}?hash=${Utils.createId()}`); + }); + }) + .catch((error) => { + // Handle any errors during the refund process + this.createNotificationError({ + title: 'Error', + message: 'Something went wrong with the refunds', + autoClose: false + }); + this.isLoading = false; // Ensure the loader is hidden even on error + }); + }); + } + }, + lineItemRefundBulk(lineItemId) { + return new Promise((resolve, reject) => { + this.VRPaymentRefundService.createRefund( + this.transactionData.transactions[0].metaData.salesChannelId, + this.transactionData.transactions[0].id, + 0, + lineItemId + ) + .then(() => { + this.createNotificationSuccess({ + title: this.$tc('vrpayment-order.refundAction.successTitle'), + message: this.$tc('vrpayment-order.refundAction.successMessage') + }); + resolve(); + }) + .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.response.data, + autoClose: false + }); + } finally { + reject(); + } + }); + }); + }, + } +}); diff --git a/src/Resources/app/administration/src/module/vrpayment-order/page/vrpayment-order-detail/index.scss b/src/Resources/app/administration/src/module/vrpayment-order/page/vrpayment-order-detail/index.scss new file mode 100644 index 0000000..b0ffc12 --- /dev/null +++ b/src/Resources/app/administration/src/module/vrpayment-order/page/vrpayment-order-detail/index.scss @@ -0,0 +1,7 @@ +.vrpayment-order-detail__data { + display: grid; +} + +.vrpayment-order-detail__heading { + padding-top: 15px; +} diff --git a/src/Resources/app/administration/src/module/vrpayment-order/snippet/de-DE.json b/src/Resources/app/administration/src/module/vrpayment-order/snippet/de-DE.json new file mode 100644 index 0000000..ecd1e48 --- /dev/null +++ b/src/Resources/app/administration/src/module/vrpayment-order/snippet/de-DE.json @@ -0,0 +1,112 @@ +{ + "vrpayment-order": { + "buttons": { + "label": { + "completion": "Abschluss", + "download-invoice": "Rechnung herunterladen", + "download-packing-slip": "Packzettel herunterladen", + "refund": "Eine neue Rückerstattung erstellen", + "void": "Genehmigung annullieren", + "refund-whole-line-item": "Gesamte Werbebuchung erstatten", + "refund-line-item-by-quantity": "Rückerstattung nach Menge", + "refund-line-item-selected": "Rückerstattung auswählen", + "refund-line-item-parial": "Teilweise Rückerstattung" + } + }, + "captureAction": { + "button": { + "text": "Zahlung erfassen" + }, + "currentAmount": "Betrag", + "isFinal": "Dies ist die endgültige Verbuchung", + "maxAmount": "Maximaler Betrag", + "successMessage": "Ihre Verbuchung war erfolgreich", + "successTitle": "Erfolg" + }, + "general": { + "title": "Bestellungen" + }, + "header": "VRPayment Payment", + "lineItem": { + "cardTitle": "Einzelposten", + "types": { + "amountIncludingTax": "Betrag", + "name": "Name", + "quantity": "Anzahl", + "taxAmount": "Steuern", + "type": "Typ", + "uniqueId": "Eindeutige ID" + } + }, + "modal": { + "title": { + "capture": "Erfassen", + "refund": "Neue Gutschrift", + "void": "Autorisierung aufheben" + } + }, + "paymentDetails": { + "cardTitle": "Zahlung", + "error": { + "title": "Fehler beim Abrufen von Zahlungsdetails von VRPayment" + } + }, + "refund": { + "cardTitle": "Gutschriften", + "refundAmount": { + "label": "Gutschriftsbetrag" + }, + "refundQuantity": { + "label": "Refund Menge" + }, + "types": { + "amount": "Betrag", + "createdOn": "Erstellt am", + "id": "ID", + "state": "Staat" + } + }, + "refundAction": { + "confirmButton": { + "text": "Ausführen" + }, + "refundAmount": { + "label": "Betrag", + "placeholder": "Einen Betrag eingeben" + }, + "successMessage": "Ihre Rückerstattung war erfolgreich", + "successTitle": "Erfolg", + "maxAvailableItemsToRefund": "Maximal Verfügbare Artikel zum Erstatten", + "maxAvailableAmountToRefund": "Maximal verfügbarer Erstattungsbetrag" + }, + "transactionHistory": { + "cardTitle": "Einzelheiten", + "types": { + "authorized_amount": "Autorisierter Betrag", + "currency": "Währung", + "customer": "Kunde", + "payment_method": "Zahlungsweise", + "state": "Staat", + "transaction": "Transaktion" + }, + "customerId": "Customer ID", + "customerName": "Customer Name", + "creditCardHolder": "Kreditkarteninhaber", + "paymentMethod": "Zahlungsart", + "paymentMethodBrand": "Marke der Zahlungsmethode", + "PseudoCreditCardNumber": "Pseudo-Kreditkartennummer", + "CardExpire": "Karte verfällt" + }, + "voidAction": { + "confirm": { + "button": { + "cancel": "Nein", + "confirm": "Autorisierung aufheben" + }, + "message": "Wollen Sie diese Zahlung wirklich stornieren?" + }, + "successMessage": "Die Zahlung wurde erfolgreich annulliert", + "successTitle": "Erfolg" + } + } +} diff --git a/src/Resources/app/administration/src/module/vrpayment-order/snippet/en-GB.json b/src/Resources/app/administration/src/module/vrpayment-order/snippet/en-GB.json new file mode 100644 index 0000000..8f3f943 --- /dev/null +++ b/src/Resources/app/administration/src/module/vrpayment-order/snippet/en-GB.json @@ -0,0 +1,113 @@ +{ + "vrpayment-order": { + "buttons": { + "label": { + "completion": "Complete", + "download-invoice": "Download Invoice", + "download-packing-slip": "Download Packing Slip", + "refund": "Create a new refund", + "void": "Cancel authorization", + "refund-whole-line-item": "Refund whole line item", + "refund-line-item-by-quantity": "Refund by quantity", + "refund-line-item-selected": "Rembourser sélectionnés", + "refund-line-item-selected": "Refund selected", + "refund-line-item-parial": "Partial refund" + } + }, + "captureAction": { + "button": { + "text": "Capture payment" + }, + "currentAmount": "Amount", + "isFinal": "This is final capture", + "maxAmount": "Maximum amount", + "successMessage": "Your capture was successful.", + "successTitle": "Success" + }, + "general": { + "title": "Orders" + }, + "header": "VRPayment Payment", + "lineItem": { + "cardTitle": "Line Items", + "types": { + "amountIncludingTax": "Amount", + "name": "Name", + "quantity": "Quantity", + "taxAmount": "Taxes", + "type": "Type", + "uniqueId": "Unique ID" + } + }, + "modal": { + "title": { + "capture": "Capture", + "refund": "New refund", + "void": "Cancel authorization" + } + }, + "paymentDetails": { + "cardTitle": "Payment", + "error": { + "title": "Error fetching payment details from VRPayment" + } + }, + "refund": { + "cardTitle": "Refunds", + "refundAmount": { + "label": "Refund Amount" + }, + "refundQuantity": { + "label": "Refund Quantity" + }, + "types": { + "amount": "Amount", + "createdOn": "Created On", + "id": "ID", + "state": "State" + } + }, + "refundAction": { + "confirmButton": { + "text": "Execute" + }, + "refundAmount": { + "label": "Amount", + "placeholder": "Enter a amount" + }, + "successMessage": "Your refund was successful.", + "successTitle": "Success", + "maxAvailableItemsToRefund": "Maximum available items to refund", + "maxAvailableAmountToRefund": "Maximum available amount to refund" + }, + "transactionHistory": { + "cardTitle": "Details", + "types": { + "authorized_amount": "Authorized Amount", + "currency": "Currency", + "customer": "Customer", + "payment_method": "Payment Method", + "state": "State", + "transaction": "Transaction" + }, + "customerId": "Customer ID", + "customerName": "Customer Name", + "creditCardHolder": "Credit Card Holder", + "paymentMethod": "Payment Method", + "paymentMethodBrand": "Payment Method Brand", + "PseudoCreditCardNumber": "Pseudo Credit Card Number", + "CardExpire": "Card Expire" + }, + "voidAction": { + "confirm": { + "button": { + "cancel": "No", + "confirm": "Cancel authorization" + }, + "message": "Do you really want to cancel this payment?" + }, + "successMessage": "The payment was successfully voided.", + "successTitle": "Success" + } + } +} diff --git a/src/Resources/app/administration/src/module/vrpayment-order/snippet/fr-FR.json b/src/Resources/app/administration/src/module/vrpayment-order/snippet/fr-FR.json new file mode 100644 index 0000000..97688e3 --- /dev/null +++ b/src/Resources/app/administration/src/module/vrpayment-order/snippet/fr-FR.json @@ -0,0 +1,112 @@ +{ + "vrpayment-order": { + "buttons": { + "label": { + "completion": "Terminée", + "download-invoice": "Télécharger la facture", + "download-packing-slip": "Télécharger le bordereau d'expédition", + "refund": "Créer un nouveau remboursement", + "void": "Annulez l'autorisation", + "refund-whole-line-item": "Remboursement de la ligne entière", + "refund-line-item-by-quantity": "Remboursement par quantité", + "refund-line-item-selected": "Rembourser sélectionnés", + "refund-line-item-parial": "Remboursement partiel" + } + }, + "captureAction": { + "button": { + "text": "Capture du paiement" + }, + "currentAmount": "Montant", + "isFinal": "C'est la capture finale", + "maxAmount": "Montant maximal", + "successMessage": "Votre capture a été réussie.", + "successTitle": "Succès" + }, + "general": { + "title": "Commandes" + }, + "header": "VRPayment Paiement", + "lineItem": { + "cardTitle": "Articles de ligne", + "types": { + "amountIncludingTax": "Montant", + "name": "Nom", + "quantity": "Quantité", + "taxAmount": "Taxes", + "type": "Type", + "uniqueId": "ID unique" + } + }, + "modal": { + "title": { + "capture": "Capture", + "refund": "Nouveau remboursement", + "void": "Annulez l'autorisation" + } + }, + "paymentDetails": { + "cardTitle": "Paiement", + "error": { + "title": "Erreur dans la récupération des détails du paiement à partir de VRPayment" + } + }, + "refund": { + "cardTitle": "Remboursements", + "refundAmount": { + "label": "Montant du remboursement" + }, + "refundQuantity": { + "label": "Quantité à rembourser" + }, + "types": { + "amount": "Montant", + "createdOn": "Créé le", + "id": "ID", + "state": "État" + } + }, + "refundAction": { + "confirmButton": { + "text": "Exécutez" + }, + "refundAmount": { + "label": "Montant", + "placeholder": "Entrez un montant" + }, + "successMessage": "Votre remboursement a été effectué avec succès.", + "successTitle": "Succès", + "maxAvailableItemsToRefund": "Nombre maximum d'articles disponibles pour le remboursement", + "maxAvailableAmountToRefund": "Montant maximal disponible pour le remboursement" + }, + "transactionHistory": { + "cardTitle": "Détails", + "types": { + "authorized_amount": "Montant autorisé", + "currency": "Monnaie", + "customer": "Client", + "payment_method": "Mode de paiement", + "state": "État", + "transaction": "Transaction" + }, + "customerId": "Customer ID", + "customerName": "Customer Name", + "creditCardHolder": "Titulaire de la carte de crédit", + "paymentMethod": "Mode de paiement", + "paymentMethodBrand": "Marque du mode de paiement", + "PseudoCreditCardNumber": "Pseudo numéro de carte de crédit", + "CardExpire": "La carte expire" + }, + "voidAction": { + "confirm": { + "button": { + "cancel": "Non", + "confirm": "Annulez l'autorisation" + }, + "message": "Voulez-vous vraiment annuler ce paiement?" + }, + "successMessage": "Le paiement a été annulé avec succès.", + "successTitle": "Succès" + } + } +} diff --git a/src/Resources/app/administration/src/module/vrpayment-order/snippet/it-IT.json b/src/Resources/app/administration/src/module/vrpayment-order/snippet/it-IT.json new file mode 100644 index 0000000..12d174b --- /dev/null +++ b/src/Resources/app/administration/src/module/vrpayment-order/snippet/it-IT.json @@ -0,0 +1,112 @@ +{ + "vrpayment-order": { + "buttons": { + "label": { + "completion": "Completato", + "download-invoice": "Scarica fattura", + "download-packing-slip": "Scarica distinta di imballaggio", + "refund": "Crea un nuovo rimborso", + "void": "Annulla autorizzazione", + "refund-whole-line-item": "Rimborso intera riga", + "refund-line-item-by-quantity": "Rimborso per quantità", + "refund-line-item-selected": "Rimborso selezionati", + "refund-line-item-parial": "Rimborso parziale" + } + }, + "captureAction": { + "button": { + "text": "Cattura pagamento" + }, + "currentAmount": "Importo", + "isFinal": "Questa è la cattura finale", + "maxAmount": "Importo massimo", + "successMessage": "La tua cattura ha avuto successo.", + "successTitle": "Successo" + }, + "general": { + "title": "Ordini" + }, + "header": "Pagamento VRPayment", + "lineItem": { + "cardTitle": "Articoli di linea", + "types": { + "amountIncludingTax": "Importo", + "name": "Nome", + "quantity": "Quantità", + "taxAmount": "Tasse", + "type": "Tipo", + "uniqueId": "ID unico" + } + }, + "modal": { + "title": { + "capture": "Cattura", + "refund": "Nuovo rimborso", + "void": "Annulla autorizzazione" + } + }, + "paymentDetails": { + "cardTitle": "Pagamento", + "error": { + "title": "Errore nel recupero dei dettagli del pagamento da VRPayment" + } + }, + "refund": { + "cardTitle": "Rimborsi", + "refundAmount": { + "label": "Importo del rimborso" + }, + "refundQuantity": { + "label": "Quantità di rimborso" + }, + "types": { + "amount": "Importo", + "createdOn": "Creato il", + "id": "ID", + "state": "Stato" + } + }, + "refundAction": { + "confirmButton": { + "text": "Esegui" + }, + "refundAmount": { + "label": "Importo", + "placeholder": "Inserisci un importo" + }, + "successMessage": "Il tuo rimborso è andato a buon fine.", + "successTitle": "Successo", + "maxAvailableItemsToRefund": "Numero massimo di articoli disponibili da rimborsare", + "maxAvailableAmountToRefund": "Importo massimo disponibile per il rimborso" + }, + "transactionHistory": { + "cardTitle": "Dettagli", + "types": { + "authorized_amount": "Importo autorizzato", + "currency": "Valuta", + "customer": "Cliente", + "payment_method": "Metodo di pagamento", + "state": "Stato", + "transaction": "Transazione" + }, + "customerId": "Customer ID", + "customerName": "Customer Name", + "creditCardHolder": "Proprietario della carta di credito", + "paymentMethod": "Metodo di pagamento", + "paymentMethodBrand": "Metodo di pagamento Marca", + "PseudoCreditCardNumber": "Numero di carta di credito pseudo", + "CardExpire": "La carta scade" + }, + "voidAction": { + "confirm": { + "button": { + "cancel": "No", + "confirm": "Annulla autorizzazione" + }, + "message": "Vuoi davvero annullare questo pagamento?" + }, + "successMessage": "Il pagamento è stato annullato con successo.", + "successTitle": "Successo" + } + } +} diff --git a/src/Resources/app/administration/src/module/vrpayment-settings/acl/index.js b/src/Resources/app/administration/src/module/vrpayment-settings/acl/index.js new file mode 100644 index 0000000..d512d74 --- /dev/null +++ b/src/Resources/app/administration/src/module/vrpayment-settings/acl/index.js @@ -0,0 +1,58 @@ +Shopware.Service('privileges').addPrivilegeMappingEntry({ + category: 'permissions', + parent: 'vrpayment', + key: 'vrpayment', + roles: { + viewer: { + privileges: [ + 'sales_channel:read', + 'sales_channel_payment_method:read', + 'system_config:read' + ], + dependencies: [] + }, + editor: { + privileges: [ + 'sales_channel:update', + 'sales_channel_payment_method:create', + 'sales_channel_payment_method:update', + 'system_config:update', + 'system_config:create', + 'system_config:delete' + ], + dependencies: [ + 'vrpayment.viewer' + ] + } + } +}); + +Shopware.Service('privileges').addPrivilegeMappingEntry({ + category: 'permissions', + parent: null, + key: 'sales_channel', + roles: { + viewer: { + privileges: [ + 'sales_channel_payment_method:read' + ] + }, + editor: { + privileges: [ + 'payment_method:update' + ] + }, + creator: { + privileges: [ + 'payment_method:create', + 'shipping_method:create', + 'delivery_time:create' + ] + }, + deleter: { + privileges: [ + 'payment_method:delete' + ] + } + } +}); diff --git a/src/Resources/app/administration/src/module/vrpayment-settings/component/sw-vrpayment-advanced-options/index.html.twig b/src/Resources/app/administration/src/module/vrpayment-settings/component/sw-vrpayment-advanced-options/index.html.twig new file mode 100644 index 0000000..024b3ff --- /dev/null +++ b/src/Resources/app/administration/src/module/vrpayment-settings/component/sw-vrpayment-advanced-options/index.html.twig @@ -0,0 +1,43 @@ + + +
+ + + + + + + +
+
+
+ diff --git a/src/Resources/app/administration/src/module/vrpayment-settings/component/sw-vrpayment-advanced-options/index.js b/src/Resources/app/administration/src/module/vrpayment-settings/component/sw-vrpayment-advanced-options/index.js new file mode 100644 index 0000000..c745cd8 --- /dev/null +++ b/src/Resources/app/administration/src/module/vrpayment-settings/component/sw-vrpayment-advanced-options/index.js @@ -0,0 +1,66 @@ +/* global Shopware */ + +import template from './index.html.twig'; +import constants from '../../page/vrpayment-settings/configuration-constants' + +const {Component, Mixin} = Shopware; + +Component.register('sw-vrpayment-advanced-options', { + template: template, + + name: 'VRPaymentAdvancedOptions', + + inject: [ + 'acl' + ], + + mixins: [ + Mixin.getByName('notification') + ], + + props: { + actualConfigData: { + type: Object, + required: true + }, + allConfigs: { + type: Object, + required: true + }, + selectedSalesChannelId: { + required: true + }, + isLoading: { + type: Boolean, + required: true + } + }, + + data() { + return { + ...constants + }; + }, + + methods: { + checkTextFieldInheritance(value) { + if (typeof value !== 'string') { + return true; + } + + return value.length <= 0; + }, + + checkNumberFieldInheritance(value) { + if (typeof value !== 'number') { + return true; + } + + return value.length <= 0; + }, + + checkBoolFieldInheritance(value) { + return typeof value !== 'boolean'; + } + } +}); diff --git a/src/Resources/app/administration/src/module/vrpayment-settings/component/sw-vrpayment-credentials/index.html.twig b/src/Resources/app/administration/src/module/vrpayment-settings/component/sw-vrpayment-credentials/index.html.twig new file mode 100644 index 0000000..90f9b5e --- /dev/null +++ b/src/Resources/app/administration/src/module/vrpayment-settings/component/sw-vrpayment-credentials/index.html.twig @@ -0,0 +1,92 @@ +{% block vrpayment_settings_content_card_channel_config_credentials %} + + + {% block vrpayment_settings_content_card_channel_config_credentials_card_container %} + + + {% block vrpayment_settings_content_card_channel_config_credentials_card_container_settings %} +
+ + {% block vrpayment_settings_content_card_channel_config_credentials_card_container_settings_space_id %} + + + + {% endblock %} + + {% block vrpayment_settings_content_card_channel_config_credentials_card_container_settings_user_id %} + + + + {% endblock %} + + {% block vrpayment_settings_content_card_channel_config_credentials_card_container_settings_application_key %} + + + + {% endblock %} +
+ {% endblock %} + + + + {{ $tc('vrpayment-settings.settingForm.credentials.button.label') }} + + + +
+ {% endblock %} +
+ +{% endblock %} diff --git a/src/Resources/app/administration/src/module/vrpayment-settings/component/sw-vrpayment-credentials/index.js b/src/Resources/app/administration/src/module/vrpayment-settings/component/sw-vrpayment-credentials/index.js new file mode 100644 index 0000000..4478e90 --- /dev/null +++ b/src/Resources/app/administration/src/module/vrpayment-settings/component/sw-vrpayment-credentials/index.js @@ -0,0 +1,105 @@ +/* global Shopware */ + +import template from './index.html.twig'; +import constants from '../../page/vrpayment-settings/configuration-constants' + +const {Component, Mixin} = Shopware; + +Component.register('sw-vrpayment-credentials', { + template: template, + + name: 'VRPaymentCredentials', + + inject: [ + 'acl' + ], + + mixins: [ + Mixin.getByName('notification') + ], + + props: { + actualConfigData: { + type: Object, + required: true + }, + allConfigs: { + type: Object, + required: true + }, + + selectedSalesChannelId: { + required: true + }, + spaceIdFilled: { + type: Boolean, + required: true + }, + spaceIdErrorState: { + required: true + }, + userIdFilled: { + type: Boolean, + required: true + }, + userIdErrorState: { + required: true + }, + applicationKeyFilled: { + type: Boolean, + required: true + }, + applicationKeyErrorState: { + required: true + }, + isLoading: { + type: Boolean, + required: true + }, + isTesting: { + type: Boolean, + required: false + } + }, + + data() { + return { + ...constants + }; + }, + + methods: { + + checkTextFieldInheritance(value) { + if (typeof value !== 'string') { + return true; + } + + return value.length <= 0; + }, + + checkNumberFieldInheritance(value) { + if (typeof value !== 'number') { + return true; + } + + return value.length <= 0; + }, + + checkBoolFieldInheritance(value) { + return typeof value !== 'boolean'; + }, + + // Emits the 'check-api-connection-event' with the current API connection parameters. + // Used to trigger API connection testing from this component. + emitCheckApiConnectionEvent() { + const apiConnectionParams = { + spaceId: this.actualConfigData[constants.CONFIG_SPACE_ID], + userId: this.actualConfigData[constants.CONFIG_USER_ID], + applicationKey: this.actualConfigData[constants.CONFIG_APPLICATION_KEY] + }; + + this.$emit('check-api-connection-event', apiConnectionParams); + } + } +}); diff --git a/src/Resources/app/administration/src/module/vrpayment-settings/component/sw-vrpayment-options/index.html.twig b/src/Resources/app/administration/src/module/vrpayment-settings/component/sw-vrpayment-options/index.html.twig new file mode 100644 index 0000000..0760ce1 --- /dev/null +++ b/src/Resources/app/administration/src/module/vrpayment-settings/component/sw-vrpayment-options/index.html.twig @@ -0,0 +1,97 @@ +{% block vrpayment_settings_content_card_channel_config_options %} + + + {% block vrpayment_settings_content_card_channel_config_credentials_card_container %} + + + {% block vrpayment_settings_content_card_channel_config_credentials_card_container_settings %} +
+ + {% block vrpayment_settings_content_card_channel_config_credentials_card_container_settings_space_view_id %} + + + + {% endblock %} + + {% block vrpayment_settings_content_card_channel_config_credentials_card_container_settings_integration %} + + + + {% endblock %} + + {% block vrpayment_settings_content_card_channel_config_credentials_card_container_settings_line_item_consistency_enabled %} + + + + {% endblock %} + + {% block vrpayment_settings_content_card_channel_config_credentials_card_container_settings_email_enabled %} + + + + {% endblock %} +
+ {% endblock %} +
+ {% endblock %} +
+ +{% endblock %} diff --git a/src/Resources/app/administration/src/module/vrpayment-settings/component/sw-vrpayment-options/index.js b/src/Resources/app/administration/src/module/vrpayment-settings/component/sw-vrpayment-options/index.js new file mode 100644 index 0000000..b33f47d --- /dev/null +++ b/src/Resources/app/administration/src/module/vrpayment-settings/component/sw-vrpayment-options/index.js @@ -0,0 +1,77 @@ +/* global Shopware */ + +import template from './index.html.twig'; +import constants from '../../page/vrpayment-settings/configuration-constants' + +const {Component, Mixin} = Shopware; + +Component.register('sw-vrpayment-options', { + template: template, + + name: 'VRPaymentOptions', + + mixins: [ + Mixin.getByName('notification') + ], + + props: { + actualConfigData: { + type: Object, + required: true + }, + allConfigs: { + type: Object, + required: true + }, + selectedSalesChannelId: { + required: true + }, + isLoading: { + type: Boolean, + required: true + } + }, + + data() { + return { + ...constants + }; + }, + + computed: { + integrationOptions() { + return [ + { + id: 'iframe', + name: this.$tc('vrpayment-settings.settingForm.options.integration.options.iframe') + }, + { + id: 'payment_page', + name: this.$tc('vrpayment-settings.settingForm.options.integration.options.payment_page') + } + ]; + } + }, + + methods: { + checkTextFieldInheritance(value) { + if (typeof value !== 'string') { + return true; + } + + return value.length <= 0; + }, + + checkNumberFieldInheritance(value) { + if (typeof value !== 'number') { + return true; + } + + return value.length <= 0; + }, + + checkBoolFieldInheritance(value) { + return typeof value !== 'boolean'; + } + } +}); diff --git a/src/Resources/app/administration/src/module/vrpayment-settings/component/sw-vrpayment-settings-icon/index.html.twig b/src/Resources/app/administration/src/module/vrpayment-settings/component/sw-vrpayment-settings-icon/index.html.twig new file mode 100644 index 0000000..ad1f95e --- /dev/null +++ b/src/Resources/app/administration/src/module/vrpayment-settings/component/sw-vrpayment-settings-icon/index.html.twig @@ -0,0 +1,31 @@ +{% block vrpayment_settings_icon %} + + + + + + + + + + +{% endblock %} diff --git a/src/Resources/app/administration/src/module/vrpayment-settings/component/sw-vrpayment-settings-icon/index.js b/src/Resources/app/administration/src/module/vrpayment-settings/component/sw-vrpayment-settings-icon/index.js new file mode 100644 index 0000000..100915d --- /dev/null +++ b/src/Resources/app/administration/src/module/vrpayment-settings/component/sw-vrpayment-settings-icon/index.js @@ -0,0 +1,7 @@ +import template from './index.html.twig'; + +const { Component } = Shopware; + +Component.register('sw-vrpayment-settings-icon', { + template +}); diff --git a/src/Resources/app/administration/src/module/vrpayment-settings/component/sw-vrpayment-storefront-options/index.html.twig b/src/Resources/app/administration/src/module/vrpayment-settings/component/sw-vrpayment-storefront-options/index.html.twig new file mode 100644 index 0000000..78fbfb6 --- /dev/null +++ b/src/Resources/app/administration/src/module/vrpayment-settings/component/sw-vrpayment-storefront-options/index.html.twig @@ -0,0 +1,25 @@ + + +
+ + + +
+
+
+ diff --git a/src/Resources/app/administration/src/module/vrpayment-settings/component/sw-vrpayment-storefront-options/index.js b/src/Resources/app/administration/src/module/vrpayment-settings/component/sw-vrpayment-storefront-options/index.js new file mode 100644 index 0000000..009d501 --- /dev/null +++ b/src/Resources/app/administration/src/module/vrpayment-settings/component/sw-vrpayment-storefront-options/index.js @@ -0,0 +1,62 @@ +/* global Shopware */ + +import template from './index.html.twig'; +import constants from '../../page/vrpayment-settings/configuration-constants' + +const {Component, Mixin} = Shopware; + +Component.register('sw-vrpayment-storefront-options', { + template: template, + + name: 'VRPaymentStorefrontOptions', + + mixins: [ + Mixin.getByName('notification') + ], + + props: { + actualConfigData: { + type: Object, + required: true + }, + allConfigs: { + type: Object, + required: true + }, + selectedSalesChannelId: { + required: true + }, + isLoading: { + type: Boolean, + required: true + } + }, + + data() { + return { + ...constants + }; + }, + + methods: { + checkTextFieldInheritance(value) { + if (typeof value !== 'string') { + return true; + } + + return value.length <= 0; + }, + + checkNumberFieldInheritance(value) { + if (typeof value !== 'number') { + return true; + } + + return value.length <= 0; + }, + + checkBoolFieldInheritance(value) { + return typeof value !== 'boolean'; + } + } +}); diff --git a/src/Resources/app/administration/src/module/vrpayment-settings/index.js b/src/Resources/app/administration/src/module/vrpayment-settings/index.js new file mode 100644 index 0000000..b43dfbf --- /dev/null +++ b/src/Resources/app/administration/src/module/vrpayment-settings/index.js @@ -0,0 +1,61 @@ +/* global Shopware */ + +import './acl'; +import './page/vrpayment-settings'; +import './component/sw-vrpayment-credentials'; +import './component/sw-vrpayment-options'; +import './component/sw-vrpayment-settings-icon'; +import './component/sw-vrpayment-storefront-options'; +import './component/sw-vrpayment-advanced-options'; + +import deDE from './snippet/de-DE.json'; +import enGB from './snippet/en-GB.json'; +import frFR from './snippet/fr-FR.json'; +import itIT from './snippet/it-IT.json'; + +const {Module} = Shopware; + +Module.register('vrpayment-settings', { + type: 'plugin', + name: 'VRPayment', + title: 'vrpayment-settings.general.descriptionTextModule', + description: 'vrpayment-settings.general.descriptionTextModule', + color: '#28d8ff', + icon: 'default-action-settings', + version: '1.0.1', + targetVersion: '1.0.1', + + snippets: { + 'de-DE': deDE, + 'en-GB': enGB, + 'fr-FR': frFR, + 'it-IT': itIT, + }, + + routes: { + index: { + component: 'vrpayment-settings', + path: 'index', + meta: { + parentPath: 'sw.settings.index', + privilege: 'vrpayment.viewer' + }, + props: { + default: (route) => { + return { + hash: route.params.hash, + }; + }, + }, + } + }, + + settingsItem: { + group: 'plugins', + to: 'vrpayment.settings.index', + iconComponent: 'sw-vrpayment-settings-icon', + backgroundEnabled: true, + privilege: 'vrpayment.viewer' + } + +}); diff --git a/src/Resources/app/administration/src/module/vrpayment-settings/page/vrpayment-settings/configuration-constants.js b/src/Resources/app/administration/src/module/vrpayment-settings/page/vrpayment-settings/configuration-constants.js new file mode 100644 index 0000000..15853d1 --- /dev/null +++ b/src/Resources/app/administration/src/module/vrpayment-settings/page/vrpayment-settings/configuration-constants.js @@ -0,0 +1,25 @@ +export const CONFIG_DOMAIN = 'VRPaymentPayment.config'; +export const CONFIG_APPLICATION_KEY = CONFIG_DOMAIN + '.' + 'applicationKey'; +export const CONFIG_EMAIL_ENABLED = CONFIG_DOMAIN + '.' + 'emailEnabled'; +export const CONFIG_INTEGRATION = CONFIG_DOMAIN + '.' + 'integration'; +export const CONFIG_LINE_ITEM_CONSISTENCY_ENABLED = CONFIG_DOMAIN + '.' + 'lineItemConsistencyEnabled'; +export const CONFIG_SPACE_ID = CONFIG_DOMAIN + '.' + 'spaceId'; +export const CONFIG_SPACE_VIEW_ID = CONFIG_DOMAIN + '.' + 'spaceViewId'; +export const CONFIG_STOREFRONT_INVOICE_DOWNLOAD_ENABLED = CONFIG_DOMAIN + '.' + 'storefrontInvoiceDownloadEnabled'; +export const CONFIG_USER_ID = CONFIG_DOMAIN + '.' + 'userId'; +export const CONFIG_STOREFRONT_WEBHOOKS_UPDATE_ENABLED = CONFIG_DOMAIN + '.' + 'storefrontWebhooksUpdateEnabled'; +export const CONFIG_STOREFRONT_PAYMENTS_UPDATE_ENABLED = CONFIG_DOMAIN + '.' + 'storefrontPaymentsUpdateEnabled'; + +export default { + CONFIG_DOMAIN, + CONFIG_APPLICATION_KEY, + CONFIG_EMAIL_ENABLED, + CONFIG_INTEGRATION, + CONFIG_LINE_ITEM_CONSISTENCY_ENABLED, + CONFIG_SPACE_ID, + CONFIG_SPACE_VIEW_ID, + CONFIG_STOREFRONT_INVOICE_DOWNLOAD_ENABLED, + CONFIG_USER_ID, + CONFIG_STOREFRONT_WEBHOOKS_UPDATE_ENABLED, + CONFIG_STOREFRONT_PAYMENTS_UPDATE_ENABLED +}; \ No newline at end of file diff --git a/src/Resources/app/administration/src/module/vrpayment-settings/page/vrpayment-settings/index.html.twig b/src/Resources/app/administration/src/module/vrpayment-settings/page/vrpayment-settings/index.html.twig new file mode 100644 index 0000000..0fee779 --- /dev/null +++ b/src/Resources/app/administration/src/module/vrpayment-settings/page/vrpayment-settings/index.html.twig @@ -0,0 +1,145 @@ +{% block vrpayment_settings %} + + + {% block vrpayment_settings_header %} + + {% endblock %} + + {% block vrpayment_settings_actions %} + + {% endblock %} + + {% block vrpayment_settings_content %} + + {% endblock %} + +{% endblock %} \ No newline at end of file diff --git a/src/Resources/app/administration/src/module/vrpayment-settings/page/vrpayment-settings/index.js b/src/Resources/app/administration/src/module/vrpayment-settings/page/vrpayment-settings/index.js new file mode 100644 index 0000000..a8a8e79 --- /dev/null +++ b/src/Resources/app/administration/src/module/vrpayment-settings/page/vrpayment-settings/index.js @@ -0,0 +1,329 @@ +/* global Shopware */ + +import template from './index.html.twig'; +import constants from './configuration-constants'; + +const {Component, Mixin} = Shopware; + +Component.register('vrpayment-settings', { + + template: template, + + inject: [ + 'acl', + 'VRPaymentConfigurationService' + ], + + mixins: [ + Mixin.getByName('notification'), + Mixin.getByName('sw-inline-snippet') + ], + + data() { + return { + + config: {}, + + isLoading: false, + isTesting: false, + + isSaveSuccessful: false, + + applicationKeyFilled: false, + applicationKeyErrorState: false, + + spaceIdFilled: false, + spaceIdErrorState: false, + + userIdFilled: false, + userIdErrorState: false, + + isSetDefaultPaymentSuccessful: false, + isSettingDefaultPaymentMethods: false, + + configIntegrationDefaultValue: 'iframe', + configEmailEnabledDefaultValue: true, + configLineItemConsistencyEnabledDefaultValue: true, + configStorefrontInvoiceDownloadEnabledEnabledDefaultValue: true, + configStorefrontWebhooksUpdateEnabledDefaultValue: true, + configStorefrontPaymentsUpdateEnabledDefaultValue: true, + + ...constants + }; + }, + + props: { + isLoading: { + type: Boolean, + required: true + } + }, + + metaInfo() { + return { + title: this.$createTitle() + }; + }, + + created() { + // Registers a listener for the 'check-api-connection-event'. + // Triggered when this event is emitted. + this.$on('check-api-connection-event', this.onCheckApiConnection); + }, + + beforeDestroy() { + // Removes the listener for the 'check-api-connection-event' + // before the component is destroyed to prevent memory leaks. + this.$off('check-api-connection-event', this.onCheckApiConnection); + }, + + watch: { + config: { + handler(configData) { + const defaultConfig = this.$refs.configComponent.allConfigs.null; + const salesChannelId = this.$refs.configComponent.selectedSalesChannelId; + if (salesChannelId === null) { + + this.applicationKeyFilled = !!this.config[this.CONFIG_APPLICATION_KEY]; + this.spaceIdFilled = !!this.config[this.CONFIG_SPACE_ID]; + this.userIdFilled = !!this.config[this.CONFIG_USER_ID]; + + if (!(this.CONFIG_INTEGRATION in this.config)) { + this.config[this.CONFIG_INTEGRATION] = this.configIntegrationDefaultValue; + } + + if (!(this.CONFIG_EMAIL_ENABLED in this.config)) { + this.config[this.CONFIG_EMAIL_ENABLED] = this.configEmailEnabledDefaultValue; + } + + if (!(this.CONFIG_LINE_ITEM_CONSISTENCY_ENABLED in this.config)) { + this.config[this.CONFIG_LINE_ITEM_CONSISTENCY_ENABLED] = this.configLineItemConsistencyEnabledDefaultValue; + } + + if (!(this.CONFIG_STOREFRONT_INVOICE_DOWNLOAD_ENABLED in this.config)) { + this.config[this.CONFIG_STOREFRONT_INVOICE_DOWNLOAD_ENABLED] = this.configStorefrontInvoiceDownloadEnabledEnabledDefaultValue; + } + + if (!(this.CONFIG_STOREFRONT_WEBHOOKS_UPDATE_ENABLED in this.config)) { + this.config[this.CONFIG_STOREFRONT_WEBHOOKS_UPDATE_ENABLED] = this.configStorefrontWebhooksUpdateEnabledDefaultValue; + } + + if (!(this.CONFIG_STOREFRONT_PAYMENTS_UPDATE_ENABLED in this.config)) { + this.config[this.CONFIG_STOREFRONT_PAYMENTS_UPDATE_ENABLED] = this.configStorefrontPaymentsUpdateEnabledDefaultValue; + } + + } else { + + this.applicationKeyFilled = !!this.config[this.CONFIG_APPLICATION_KEY] || !!defaultConfig[this.CONFIG_APPLICATION_KEY]; + this.spaceIdFilled = !!this.config[this.CONFIG_SPACE_ID] || !!defaultConfig[this.CONFIG_SPACE_ID]; + this.userIdFilled = !!this.config[this.CONFIG_USER_ID] || !!defaultConfig[this.CONFIG_USER_ID]; + + + if (!(this.CONFIG_INTEGRATION in this.config) || !(this.CONFIG_INTEGRATION in defaultConfig)) { + this.config[this.CONFIG_INTEGRATION] = this.configIntegrationDefaultValue; + } + + if (!(this.CONFIG_EMAIL_ENABLED in this.config) || !(this.CONFIG_EMAIL_ENABLED in defaultConfig)) { + this.config[this.CONFIG_EMAIL_ENABLED] = this.configEmailEnabledDefaultValue; + } + + if (!(this.CONFIG_LINE_ITEM_CONSISTENCY_ENABLED in this.config) || !(this.CONFIG_LINE_ITEM_CONSISTENCY_ENABLED in defaultConfig)) { + this.config[this.CONFIG_LINE_ITEM_CONSISTENCY_ENABLED] = this.configLineItemConsistencyEnabledDefaultValue; + } + + if (!(this.CONFIG_STOREFRONT_INVOICE_DOWNLOAD_ENABLED in this.config) || !(this.CONFIG_STOREFRONT_INVOICE_DOWNLOAD_ENABLED in defaultConfig)) { + this.config[this.CONFIG_STOREFRONT_INVOICE_DOWNLOAD_ENABLED] = this.configStorefrontInvoiceDownloadEnabledEnabledDefaultValue; + } + + if (!(this.CONFIG_STOREFRONT_WEBHOOKS_UPDATE_ENABLED in this.config) || !(this.CONFIG_STOREFRONT_WEBHOOKS_UPDATE_ENABLED in defaultConfig)) { + this.config[this.CONFIG_STOREFRONT_WEBHOOKS_UPDATE_ENABLED] = this.configStorefrontWebhooksUpdateEnabledDefaultValue; + } + + if (!(this.CONFIG_STOREFRONT_PAYMENTS_UPDATE_ENABLED in this.config) || !(this.CONFIG_STOREFRONT_PAYMENTS_UPDATE_ENABLED in defaultConfig)) { + this.config[this.CONFIG_STOREFRONT_PAYMENTS_UPDATE_ENABLED] = this.configStorefrontPaymentsUpdateEnabledDefaultValue; + } + } + + this.$emit('salesChannelChanged'); + this.$emit('update:value', configData); + }, + deep: true + } + }, + + methods: { + checkTextFieldInheritance(value) { + if (typeof value !== 'string') { + return true; + } + + return value.length <= 0; + }, + + checkNumberFieldInheritance(value) { + if (typeof value !== 'number') { + return true; + } + + return value.length <= 0; + }, + + checkBoolFieldInheritance(value) { + return typeof value !== 'boolean'; + }, + + getInheritValue(key) { + if (this.selectedSalesChannelId == null ) { + return this.actualConfigData[key]; + } else { + return this.allConfigs['null'][key]; + } + }, + + onSave() { + if (!(this.spaceIdFilled && this.userIdFilled && this.applicationKeyFilled)) { + this.setErrorStates(); + return; + } + this.save(); + }, + + save() { + this.isLoading = true; + + this.$refs.configComponent.save().then((res) => { + if (res) { + this.config = res; + } + this.registerWebHooks(); + this.synchronizePaymentMethodConfiguration(); + this.installOrderDeliveryStates(); + }).catch((e) => { + console.error('Error:', e); + this.isLoading = false; + }); + }, + + registerWebHooks() { + if (this.config[this.CONFIG_STOREFRONT_WEBHOOKS_UPDATE_ENABLED] === false) { + return false; + } + + this.VRPaymentConfigurationService.registerWebHooks(this.$refs.configComponent.selectedSalesChannelId) + .then(() => { + this.createNotificationSuccess({ + title: this.$tc('vrpayment-settings.settingForm.titleSuccess'), + message: this.$tc('vrpayment-settings.settingForm.messageWebHookUpdated') + }); + }).catch((e) => { + this.createNotificationError({ + title: this.$tc('vrpayment-settings.settingForm.titleError'), + message: this.$tc('vrpayment-settings.settingForm.messageWebHookError') + }); + this.isLoading = false; + console.error('Error:', e); + }); + }, + + synchronizePaymentMethodConfiguration() { + if (this.config[this.CONFIG_STOREFRONT_PAYMENTS_UPDATE_ENABLED] === false) { + return false; + } + + this.VRPaymentConfigurationService.synchronizePaymentMethodConfiguration(this.$refs.configComponent.selectedSalesChannelId) + .then(() => { + this.createNotificationSuccess({ + title: this.$tc('vrpayment-settings.settingForm.titleSuccess'), + message: this.$tc('vrpayment-settings.settingForm.messagePaymentMethodConfigurationUpdated') + }); + this.isLoading = false; + }).catch((e) => { + this.createNotificationError({ + title: this.$tc('vrpayment-settings.settingForm.titleError'), + message: this.$tc('vrpayment-settings.settingForm.messagePaymentMethodConfigurationError') + }); + this.isLoading = false; + console.error('Error:', e); + }); + }, + + installOrderDeliveryStates(){ + this.VRPaymentConfigurationService.installOrderDeliveryStates() + .then(() => { + this.createNotificationSuccess({ + title: this.$tc('vrpayment-settings.settingForm.titleSuccess'), + message: this.$tc('vrpayment-settings.settingForm.messageOrderDeliveryStateUpdated') + }); + this.isLoading = false; + }).catch(() => { + this.createNotificationError({ + title: this.$tc('vrpayment-settings.settingForm.titleError'), + message: this.$tc('vrpayment-settings.settingForm.messageOrderDeliveryStateError') + }); + this.isLoading = false; + }); + }, + + onSetPaymentMethodDefault() { + this.isSettingDefaultPaymentMethods = true; + this.VRPaymentConfigurationService.setVRPaymentAsSalesChannelPaymentDefault( + this.$refs.configComponent.selectedSalesChannelId + ).then(() => { + this.isSettingDefaultPaymentMethods = false; + this.isSetDefaultPaymentSuccessful = true; + this.createNotificationSuccess({ + title: this.$tc('vrpayment-settings.settingForm.titleSuccess'), + message: this.$tc('vrpayment-settings.salesChannelCard.messageDefaultPaymentUpdated') + }); + }); + }, + + setErrorStates() { + const messageNotBlankErrorState = { + code: 1, + detail: this.$tc('vrpayment-settings.messageNotBlank') + }; + + if (!this.spaceIdFilled) { + this.spaceIdErrorState = messageNotBlankErrorState; + } + + if (!this.userIdFilled) { + this.userIdErrorState = messageNotBlankErrorState; + } + + if (!this.applicationKeyFilled) { + this.applicationKeyErrorState = messageNotBlankErrorState; + } + }, + + // Handles the 'check-api-connection-event'. + // Uses the provided apiConnectionData to perform API connection checks. + onCheckApiConnection(apiConnectionData) { + const { spaceId, userId, applicationKey } = apiConnectionData; + this.isTesting = true; + + this.VRPaymentConfigurationService.checkApiConnection(spaceId, userId, applicationKey) + .then((res) => { + if (res.result === 200) { + this.createNotificationSuccess({ + title: this.$tc('vrpayment-settings.settingForm.credentials.alert.title'), + message: this.$tc('vrpayment-settings.settingForm.credentials.alert.successMessage') + }); + } else { + this.createNotificationError({ + title: this.$tc('vrpayment-settings.settingForm.credentials.alert.title'), + message: this.$tc('vrpayment-settings.settingForm.credentials.alert.errorMessage') + }); + } + this.isTesting = false; + }).catch(() => { + this.createNotificationError({ + title: this.$tc('vrpayment-settings.settingForm.credentials.alert.title'), + message: this.$tc('vrpayment-settings.settingForm.credentials.alert.errorMessage') + }); + this.isTesting = false; + }); + } + } +}); diff --git a/src/Resources/app/administration/src/module/vrpayment-settings/snippet/de-DE.json b/src/Resources/app/administration/src/module/vrpayment-settings/snippet/de-DE.json new file mode 100644 index 0000000..e0a79c8 --- /dev/null +++ b/src/Resources/app/administration/src/module/vrpayment-settings/snippet/de-DE.json @@ -0,0 +1,105 @@ +{ + "sw-privileges": { + "permissions": { + "parents": { + "vrpayment": "VRPayment plugin" + }, + "vrpayment": { + "label": "VRPayment berechtigungen" + } + } + }, + "vrpayment-settings": { + "general": { + "descriptionTextModule": "VRPayment-Einstellungen", + "mainMenuItemGeneral": "VRPayment" + }, + "header": "VRPayment", + "messageNotBlank": "Dieser Wert sollte nicht leer sein.", + "salesChannelCard": { + "button": { + "description": "Klicken Sie auf diese Schaltfläche, um VRPayment als Standard-Zahlungsabwickler im ausgewählten Vertriebskanal festzulegen", + "label": "VRPayment als Standard-Zahlungsabwickler festlegen" + }, + "messageDefaultPaymentError": "VRPayment als Standard-Zahlungsabwickler konnte nicht festgelegt werden..", + "messageDefaultPaymentUpdated": "VRPayment als Standard-Zahlungsabwickler wurde festgelegt." + }, + "settingForm": { + "credentials": { + "applicationKey": { + "label": "Application Key", + "tooltipText": "Der Anwendungsschlüssel wird verwendet, um dieses Plugin mit der API VRPayment zu authentifizieren." + }, + "cardTitle": "Anmeldedaten", + "spaceId": { + "label": "Space ID", + "tooltipText": "Die Space ID wird verwendet, um dieses Plugin mit der API VRPayment zu authentifizieren." + }, + "userId": { + "label": "User ID", + "tooltipText": "Die Benutzer-ID wird verwendet, um dieses Plugin mit der VRPayment-API zu authentifizieren." + }, + "button": { + "description": "Klicken Sie auf diese Schaltfläche, um die VRPayment API zu testen", + "label": "API Verbindung testen" + }, + "alert": { + "title": "API-Test", + "successMessage": "Die Verbindung wurde erfolgreich getestet.", + "errorMessage": "Die Verbindung ist fehlgeschlagen. Versuchen Sie es erneut." + } + }, + "messageSaveSuccess": "VRPayment-Einstellungen wurden gespeichert.", + "messageOrderDeliveryStateError": "VRPayment OrderDeliveryState konnte nicht gespeichert werden.", + "messageOrderDeliveryStateUpdated": "VRPayment OrderDeliveryState wurde aktualisiert.", + "messagePaymentMethodConfigurationError": "VRPayment PaymentMethodConfiguration konnte nicht gespeichert werden. Bitte überprüfen Sie Ihre Anmeldedaten.", + "messagePaymentMethodConfigurationUpdated": "VRPayment PaymentMethodConfiguration wurde registriert.", + "messageWebHookError": "VRPayment WebHook konnte nicht gespeichert werden. Bitte überprüfen Sie Ihre Zugangsdaten.", + "messageWebHookUpdated": "VRPayment WebHook wurde aktualisiert.", + "options": { + "cardTitle": "Optionen", + "emailEnabled": { + "label": "Auftragsbestätigung per E-Mail senden", + "tooltipText": "Wenn diese Einstellung aktiviert ist, erhalten Ihre Kunden eine E-Mail von Ihrem Geschäft, wenn die Zahlung ihrer Bestellung autorisiert ist." + }, + "integration": { + "label": "Integration", + "options": { + "iframe": "Iframe", + "payment_page": "Payment Page" + }, + "tooltipText": "Integration" + }, + "lineItemConsistencyEnabled": { + "label": "Konsistenz der Einzelposten", + "tooltipText": "Wenn diese Option aktiviert ist, stimmen die Summen der Einzelposten in VRPaymentPayment immer mit der Shopware-Bestellsumme überein." + }, + "spaceViewId": { + "label": "Space View ID", + "tooltipText": "Space View ID" + } + }, + "save": "Speichern", + "storefrontOptions": { + "cardTitle": "Storefront-Optionen", + "invoiceDownloadEnabled": { + "label": "Rechnung Download", + "tooltipText": "Wenn diese Einstellung aktiviert ist, können Ihre Kunden Auftragsrechnungen von VRPayment herunterladen." + } + }, + "advancedOptions": { + "cardTitle": "Erweiterte-Optionen", + "webhooksUpdateEnabled": { + "label": "Webhooks-Update", + "tooltipText": "Wenn diese Einstellung aktiviert ist, wird das Webhook-Update ausgelöst, wenn Sie die Einstellungen speichern" + }, + "paymentsUpdateEnabled": { + "label": "Payments-Update", + "tooltipText": "Wenn diese Einstellung aktiviert ist, wird die Aktualisierung der Zahlungsmethoden ausgelöst, wenn Sie die Einstellungen speichern" + } + }, + "titleError": "Fehler", + "titleSuccess": "Erfolg" + } + } +} \ No newline at end of file diff --git a/src/Resources/app/administration/src/module/vrpayment-settings/snippet/en-GB.json b/src/Resources/app/administration/src/module/vrpayment-settings/snippet/en-GB.json new file mode 100644 index 0000000..506d73f --- /dev/null +++ b/src/Resources/app/administration/src/module/vrpayment-settings/snippet/en-GB.json @@ -0,0 +1,105 @@ +{ + "sw-privileges": { + "permissions": { + "parents": { + "vrpayment": "VRPayment plugin" + }, + "vrpayment": { + "label": "VRPayment permissions" + } + } + }, + "vrpayment-settings": { + "general": { + "descriptionTextModule": "VRPayment settings", + "mainMenuItemGeneral": "VRPayment" + }, + "header": "VRPayment", + "messageNotBlank": "This value should not be blank.", + "salesChannelCard": { + "button": { + "description": "Click this button to set VRPayment as default payment handler in the selected SalesChannel", + "label": "Set VRPayment as default payment handler" + }, + "messageDefaultPaymentError": "VRPayment as default payment could not be set.", + "messageDefaultPaymentUpdated": "VRPayment as default payment has been set." + }, + "settingForm": { + "credentials": { + "applicationKey": { + "label": "Application Key", + "tooltipText": "The Application Key is used to authenticate this plugin with the VRPayment API." + }, + "cardTitle": "Credentials", + "spaceId": { + "label": "Space ID", + "tooltipText": "The space ID is used to authenticate this plugin with the VRPayment API." + }, + "userId": { + "label": "User ID", + "tooltipText": "The user ID is used to authenticate this plugin with the VRPayment API." + }, + "button": { + "description": "Click this button to test the VRPayment API", + "label": "API connection test" + }, + "alert": { + "title": "API Test", + "successMessage": "The connection was successfully tested.", + "errorMessage": "The connection was failed. Try it again." + } + }, + "messageSaveSuccess": "VRPayment settings have been saved.", + "messageOrderDeliveryStateError": "VRPayment OrderDeliveryState could not be saved.", + "messageOrderDeliveryStateUpdated": "VRPayment OrderDeliveryState has been updated.", + "messagePaymentMethodConfigurationError": "VRPayment PaymentMethodConfiguration could not be saved. Please check your credentials.", + "messagePaymentMethodConfigurationUpdated": "VRPayment PaymentMethodConfiguration has been registered.", + "messageWebHookError": "VRPayment WebHook could not be saved. Please check your credentials.", + "messageWebHookUpdated": "VRPayment WebHook has been updated.", + "options": { + "cardTitle": "Options", + "emailEnabled": { + "label": "Send order confirmation email", + "tooltipText": "If this setting is enabled your customers will receive an email from your store when their order payment is authorised" + }, + "integration": { + "label": "Integration", + "options": { + "iframe": "Iframe", + "payment_page": "Payment Page" + }, + "tooltipText": "Integration" + }, + "lineItemConsistencyEnabled": { + "label": "Line item consistency", + "tooltipText": "If this option is enabled line item totals in VRPaymentPayment will always match Shopware order total" + }, + "spaceViewId": { + "label": "Space View ID", + "tooltipText": "Space View ID" + } + }, + "save": "Save", + "storefrontOptions": { + "cardTitle": "Storefront Options", + "invoiceDownloadEnabled": { + "label": "Invoice Download", + "tooltipText": "If this setting is enabled your customers will be able to download order invoices from VRPayment" + } + }, + "advancedOptions": { + "cardTitle": "Advanced Options", + "webhooksUpdateEnabled": { + "label": "Webhooks Update", + "tooltipText": "If this setting is enabled webhook update will be triggered when you save settings" + }, + "paymentsUpdateEnabled": { + "label": "Payments Update", + "tooltipText": "If this setting is enabled payment methods update will be triggered when you save settings" + } + }, + "titleError": "Error", + "titleSuccess": "Success" + } + } +} \ No newline at end of file diff --git a/src/Resources/app/administration/src/module/vrpayment-settings/snippet/fr-FR.json b/src/Resources/app/administration/src/module/vrpayment-settings/snippet/fr-FR.json new file mode 100644 index 0000000..d8f2445 --- /dev/null +++ b/src/Resources/app/administration/src/module/vrpayment-settings/snippet/fr-FR.json @@ -0,0 +1,105 @@ +{ + "sw-privileges": { + "permissions": { + "parents": { + "vrpayment": "VRPayment brancher" + }, + "vrpayment": { + "label": "VRPayment autorisations" + } + } + }, + "vrpayment-settings": { + "general": { + "descriptionTextModule": "Paramètres de VRPayment", + "mainMenuItemGeneral": "VRPayment" + }, + "header": "VRPayment", + "messageNotBlank": "Cette valeur ne doit pas être vide.", + "salesChannelCard": { + "button": { + "description": "Cliquez sur ce bouton pour définir VRPayment comme gestionnaire de paiement par défaut dans le canal de vente sélectionné.", + "label": "Définir VRPayment comme gestionnaire de paiement par défaut" + }, + "messageDefaultPaymentError": "VRPayment comme paiement par défaut n'a pas pu être défini.", + "messageDefaultPaymentUpdated": "VRPayment comme paiement par défaut a été défini." + }, + "settingForm": { + "credentials": { + "applicationKey": { + "label": "Application Key", + "tooltipText": "La clé d'application est utilisée pour authentifier ce plugin avec l'API." + }, + "cardTitle": "Références", + "spaceId": { + "label": "Space ID", + "tooltipText": "L'ID de l'espace est utilisé pour authentifier ce plugin avec l'API VRPayment.." + }, + "userId": { + "label": "User ID", + "tooltipText": "L'ID utilisateur est utilisé pour authentifier ce plugin avec l'API VRPayment." + }, + "button": { + "description": "Cliquez sur ce bouton pour tester l'API VRPayment.", + "label": "Test de connexion à l'API" + }, + "alert": { + "title": "Test API", + "successMessage": "La connexion a été testée avec succès.", + "errorMessage": "La connexion a échoué. Réessayez." + } + }, + "messageSaveSuccess": "Les paramètres de VRPayment ont été enregistrés.", + "messageOrderDeliveryStateError": "Les paramètres de VRPayment OrderDeliveryState n'ont pas pu être enregistrés.", + "messageOrderDeliveryStateUpdated": "VRPayment OrderDeliveryState a été mis à jour.", + "messagePaymentMethodConfigurationError": "VRPayment PaymentMethodConfiguration n'a pas pu être enregistré. Veuillez vérifier vos informations d'identification.", + "messagePaymentMethodConfigurationUpdated": "VRPayment PaymentMethodConfiguration a été enregistré.", + "messageWebHookError": "VRPayment WebHook n'a pas pu être enregistré. Veuillez vérifier vos informations d'identification.", + "messageWebHookUpdated": "VRPayment WebHook a été mis à jour.", + "options": { + "cardTitle": "Options", + "emailEnabled": { + "label": "Envoyer un e-mail de confirmation de commande", + "tooltipText": "If this setting is enabled your customers will receive an email from your store when their order payment is authorised" + }, + "integration": { + "label": "Integration", + "options": { + "iframe": "Iframe", + "payment_page": "Page de paiement" + }, + "tooltipText": "Integration" + }, + "lineItemConsistencyEnabled": { + "label": "Cohérence des postes de ligne", + "tooltipText": "Si cette option est activée, les totaux des articles dans VRPaymentPayment correspondront toujours au total de la commande Shopware." + }, + "spaceViewId": { + "label": "Space View ID", + "tooltipText": "Space View ID" + } + }, + "save": "Enregistrer", + "storefrontOptions": { + "cardTitle": "Storefront Options", + "invoiceDownloadEnabled": { + "label": "Téléchargement de facture", + "tooltipText": "Si ce paramètre est activé, vos clients pourront télécharger les factures de commande depuis VRPayment" + } + }, + "advancedOptions": { + "cardTitle": "Options avancées", + "webhooksUpdateEnabled": { + "label": "Mise à jour des webhooks", + "tooltipText": "Si ce paramètre est activé, la mise à jour des webhooks sera déclenchée lorsque vous enregistrerez les paramètres." + }, + "paymentsUpdateEnabled": { + "label": "Mise à jour des paiements", + "tooltipText": "Si ce paramètre est activé, la mise à jour des méthodes de paiement sera déclenchée lorsque vous enregistrez les paramètres." + } + }, + "titleError": "Erreur", + "titleSuccess": "Succès" + } + } + } \ No newline at end of file diff --git a/src/Resources/app/administration/src/module/vrpayment-settings/snippet/it-IT.json b/src/Resources/app/administration/src/module/vrpayment-settings/snippet/it-IT.json new file mode 100644 index 0000000..ef8f8b0 --- /dev/null +++ b/src/Resources/app/administration/src/module/vrpayment-settings/snippet/it-IT.json @@ -0,0 +1,105 @@ +{ + "sw-privileges": { + "permissions": { + "parents": { + "vrpayment": "VRPayment brancher" + }, + "vrpayment": { + "label": "VRPayment autorisations" + } + } + }, + "vrpayment-settings": { + "general": { + "descriptionTextModule": "Impostazioni VRPayment", + "mainMenuItemGeneral": "VRPayment" + }, + "header": "VRPayment", + "messageNotBlank": "Questo valore non dovrebbe essere vuoto.", + "salesChannelCard": { + "button": { + "description": "Fai clic su questo pulsante per impostare VRPayment come gestore di pagamento predefinito nel SalesChannel selezionato", + "label": "Imposta VRPayment come gestore di pagamento predefinito" + }, + "messageDefaultPaymentError": "Non è stato possibile impostare VRPayment come pagamento predefinito.", + "messageDefaultPaymentUpdated": "VRPayment come pagamento predefinito è stato impostato." + }, + "settingForm": { + "credentials": { + "applicationKey": { + "label": "Chiave di applicazione", + "tooltipText": "La chiave dell'applicazione è usata per autenticare questo plugin con l'API VRPayment." + }, + "cardTitle": "Credenziali", + "spaceId": { + "label": "ID spazio", + "tooltipText": "L'ID dello spazio è usato per autenticare questo plugin con l'API VRPayment." + }, + "userId": { + "label": "ID utente", + "tooltipText": "L'ID utente è usato per autenticare questo plugin con l'API VRPayment." + }, + "button": { + "description": "Fare clic su questo pulsante per testare l'API VRPayment.", + "label": "Test di connessione API" + }, + "alert": { + "title": "Test API", + "successMessage": "La connessione è stata testata con successo.", + "errorMessage": "La connessione è fallita. Riprovare." + } + }, + "messageSaveSuccess": "Le impostazioni di VRPayment sono state salvate.", + "messageOrderDeliveryStateError": "VRPayment OrderDeliveryState non può essere salvato.", + "messageOrderDeliveryStateUpdated": "VRPayment OrderDeliveryState è stato aggiornato.", + "messagePaymentMethodConfigurationError": "VRPayment PaymentMethodConfiguration non può essere salvato. Per favore controlla le tue credenziali.", + "messagePaymentMethodConfigurationUpdated": "VRPayment PaymentMethodConfiguration è stato registrato.", + "messageWebHookError": "VRPayment WebHook non può essere salvato. Per favore controlla le tue credenziali.", + "messageWebHookUpdated": "VRPayment WebHook è stato aggiornato.", + "options": { + "cardTitle": "Opzioni", + "emailEnabled": { + "label": "Invia email di conferma dell'ordine", + "tooltipText": "Se questa impostazione è abilitata i tuoi clienti riceveranno un'email dal tuo negozio quando il pagamento del loro ordine sarà autorizzato" + }, + "integration": { + "label": "Integrazione", + "options": { + "iframe": "Iframe", + "payment_page": "Pagina di pagamento" + }, + "tooltipText": "Integrazione" + }, + "lineItemConsistencyEnabled": { + "label": "Coerenza dell'elemento linea", + "tooltipText": "Se questa opzione è abilitata i totali degli articoli in VRPaymentPayment corrisponderanno sempre al totale dell'ordine Shopware" + }, + "spaceViewId": { + "label": "ID della vista spazio", + "tooltipText": "ID della vista spaziale" + } + }, + "save": "Salva", + "storefrontOptions": { + "cardTitle": "Opzioni vetrina", + "invoiceDownloadEnabled": { + "label": "Scaricamento fattura", + "tooltipText": "Se questa impostazione è abilitata i tuoi clienti potranno scaricare le fatture degli ordini da VRPayment" + } + }, + "advancedOptions": { + "cardTitle": "Opzioni avanzate", + "webhooksUpdateEnabled": { + "label": "Aggiornamento webhooks", + "tooltipText": "Se questa impostazione è abilitata l'aggiornamento dei webhook sarà attivato quando si salvano le impostazioni" + }, + "paymentsUpdateEnabled": { + "label": "Aggiornamento pagamenti", + "tooltipText": "Se questa impostazione è abilitata l'aggiornamento dei metodi di pagamento verrà attivato quando si salvano le impostazioni" + } + }, + "titleError": "Errore", + "titleSuccess": "Successo" + } + } +} diff --git a/src/Resources/app/storefront/src/main.js b/src/Resources/app/storefront/src/main.js new file mode 100644 index 0000000..429526e --- /dev/null +++ b/src/Resources/app/storefront/src/main.js @@ -0,0 +1,16 @@ +// Import all necessary Storefront plugins and scss files +import VRPaymentCheckoutPlugin + from './vrpayment-checkout-plugin/vrpayment-checkout-plugin.plugin'; + +// Register them via the existing PluginManager +const PluginManager = window.PluginManager; +PluginManager.register( + 'VRPaymentCheckoutPlugin', + VRPaymentCheckoutPlugin, + '[data-vrpayment-checkout-plugin]' +); + +if (module.hot) { + // noinspection JSValidateTypes + module.hot.accept(); +} \ No newline at end of file diff --git a/src/Resources/app/storefront/src/scss/base.scss b/src/Resources/app/storefront/src/scss/base.scss new file mode 100644 index 0000000..2a1dd3e --- /dev/null +++ b/src/Resources/app/storefront/src/scss/base.scss @@ -0,0 +1,65 @@ +.vrpayment-payment { + .vrpayment-payment-panel { + position: relative; + } + + /* Center the loader */ + #vrpaymentLoader { + margin: 3em 0; + + div { + left: 50%; + top: 50%; + width: 7.5em; + height: 7.5em; + margin: 0 auto; + border: 0.5em solid #f3f3f3; + border-radius: 50%; + border-top: 0.5em solid #008490; + animation: spin 2s linear infinite; + -moz-animation: spin 2s linear infinite; + -webkit-animation: spin 2s linear infinite; + -o-animation: spin 2s linear infinite; + } + } + + #confirmOrderForm { + overflow: hidden; + } + + @-moz-keyframes spin { + 0% { + -webkit-transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(360deg); + } + } + + @keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } + } + + @-o-keyframes spin { + 0% { + -webkit-transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(360deg); + } + } + + @-webkit-keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } + } +} diff --git a/src/Resources/app/storefront/src/vrpayment-checkout-plugin/vrpayment-checkout-plugin.plugin.js b/src/Resources/app/storefront/src/vrpayment-checkout-plugin/vrpayment-checkout-plugin.plugin.js new file mode 100644 index 0000000..5c0c033 --- /dev/null +++ b/src/Resources/app/storefront/src/vrpayment-checkout-plugin/vrpayment-checkout-plugin.plugin.js @@ -0,0 +1,27 @@ +/* eslint-disable import/no-unresolved */ + +// noinspection NpmUsedModulesInstalled +import Plugin from 'src/plugin-system/plugin.class'; +import HttpClient from 'src/service/http-client.service'; + + +class VRPaymentCheckoutPlugin extends Plugin { + + static options = { + payment_method_tabs: 'ul.vrpayment-payment-panel li', + payment_method_iframe_prefix: 'iframe_payment_method_', + payment_method_iframe_class: '.vrpayment-payment-iframe', + payment_method_handler_name: 'vrpayment_payment_handler', + payment_method_handler_prefix: 'vrpayment_handler_', + payment_method_handler_status: 'input[name="vrpayment_payment_handler_validation_status"]', + payment_form: 'confirmOrderForm', + }; + + init() { + // @TODO Move JS to Plugin + this._client = new HttpClient(window.accessKey); + } + +} + +export default VRPaymentCheckoutPlugin; \ No newline at end of file diff --git a/src/Resources/config/packages/monolog.yaml b/src/Resources/config/packages/monolog.yaml new file mode 100644 index 0000000..bf1c251 --- /dev/null +++ b/src/Resources/config/packages/monolog.yaml @@ -0,0 +1,9 @@ +monolog: + channels: ['vrpayment_payment'] + handlers: + security: + # log all messages (since debug is the lowest level) + level: debug + type: stream + path: '%kernel.logs_dir%/vrpayment.log' + channels: [ 'vrpayment_payment' ] diff --git a/src/Resources/config/plugin.png b/src/Resources/config/plugin.png new file mode 100644 index 0000000..11fd7cb Binary files /dev/null and b/src/Resources/config/plugin.png differ diff --git a/src/Resources/config/routes.xml b/src/Resources/config/routes.xml new file mode 100644 index 0000000..dc96175 --- /dev/null +++ b/src/Resources/config/routes.xml @@ -0,0 +1,8 @@ + + + + + diff --git a/src/Resources/config/services.xml b/src/Resources/config/services.xml new file mode 100644 index 0000000..b0ce66c --- /dev/null +++ b/src/Resources/config/services.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Resources/config/services/core/api/configuration.xml b/src/Resources/config/services/core/api/configuration.xml new file mode 100644 index 0000000..32d3644 --- /dev/null +++ b/src/Resources/config/services/core/api/configuration.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Resources/config/services/core/api/order_delivery_state.xml b/src/Resources/config/services/core/api/order_delivery_state.xml new file mode 100644 index 0000000..c6b21e5 --- /dev/null +++ b/src/Resources/config/services/core/api/order_delivery_state.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Resources/config/services/core/api/payment_method_configuration.xml b/src/Resources/config/services/core/api/payment_method_configuration.xml new file mode 100644 index 0000000..01bf633 --- /dev/null +++ b/src/Resources/config/services/core/api/payment_method_configuration.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Resources/config/services/core/api/refund.xml b/src/Resources/config/services/core/api/refund.xml new file mode 100644 index 0000000..6ecf374 --- /dev/null +++ b/src/Resources/config/services/core/api/refund.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Resources/config/services/core/api/space.xml b/src/Resources/config/services/core/api/space.xml new file mode 100644 index 0000000..aab44c6 --- /dev/null +++ b/src/Resources/config/services/core/api/space.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + diff --git a/src/Resources/config/services/core/api/transaction.xml b/src/Resources/config/services/core/api/transaction.xml new file mode 100644 index 0000000..1ab8f77 --- /dev/null +++ b/src/Resources/config/services/core/api/transaction.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Resources/config/services/core/api/webhooks.xml b/src/Resources/config/services/core/api/webhooks.xml new file mode 100644 index 0000000..fa6e6ad --- /dev/null +++ b/src/Resources/config/services/core/api/webhooks.xml @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Resources/config/services/core/checkout.xml b/src/Resources/config/services/core/checkout.xml new file mode 100644 index 0000000..e980a67 --- /dev/null +++ b/src/Resources/config/services/core/checkout.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + diff --git a/src/Resources/config/services/core/settings.xml b/src/Resources/config/services/core/settings.xml new file mode 100644 index 0000000..73d15ff --- /dev/null +++ b/src/Resources/config/services/core/settings.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Resources/config/services/core/storefront/account.xml b/src/Resources/config/services/core/storefront/account.xml new file mode 100644 index 0000000..11447b3 --- /dev/null +++ b/src/Resources/config/services/core/storefront/account.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Resources/config/services/core/storefront/checkout.xml b/src/Resources/config/services/core/storefront/checkout.xml new file mode 100644 index 0000000..f022b03 --- /dev/null +++ b/src/Resources/config/services/core/storefront/checkout.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Resources/config/services/core/util.xml b/src/Resources/config/services/core/util.xml new file mode 100644 index 0000000..35ec889 --- /dev/null +++ b/src/Resources/config/services/core/util.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Resources/public/administration/css/v-r-payment-payment.css b/src/Resources/public/administration/css/v-r-payment-payment.css new file mode 100644 index 0000000..3891703 --- /dev/null +++ b/src/Resources/public/administration/css/v-r-payment-payment.css @@ -0,0 +1,2 @@ +.sw-order-detail .sw-tabs{margin-top:40px}.sw-order-detail .sw-order-detail-base .sw-card-view__content{overflow-x:visible;overflow-y:visible} +.vrpayment-order-detail__data{display:grid}.vrpayment-order-detail__heading{padding-top:15px} diff --git a/src/Resources/public/administration/js/v-r-payment-payment.js b/src/Resources/public/administration/js/v-r-payment-payment.js new file mode 100644 index 0000000..6ad2286 --- /dev/null +++ b/src/Resources/public/administration/js/v-r-payment-payment.js @@ -0,0 +1 @@ +(function(){var e={915:function(){},694:function(){},174:function(){Shopware.Service("privileges").addPrivilegeMappingEntry({category:"permissions",parent:"vrpayment",key:"vrpayment",roles:{viewer:{privileges:["sales_channel:read","sales_channel_payment_method:read","system_config:read"],dependencies:[]},editor:{privileges:["sales_channel:update","sales_channel_payment_method:create","sales_channel_payment_method:update","system_config:update","system_config:create","system_config:delete"],dependencies:["vrpayment.viewer"]}}}),Shopware.Service("privileges").addPrivilegeMappingEntry({category:"permissions",parent:null,key:"sales_channel",roles:{viewer:{privileges:["sales_channel_payment_method:read"]},editor:{privileges:["payment_method:update"]},creator:{privileges:["payment_method:create","shipping_method:create","delivery_time:create"]},deleter:{privileges:["payment_method:delete"]}}})},208:function(e,t,n){var a=n(915);a.__esModule&&(a=a.default),"string"==typeof a&&(a=[[e.id,a,""]]),a.locals&&(e.exports=a.locals),n(346).Z("78cc8960",a,!0,{})},894:function(e,t,n){var a=n(694);a.__esModule&&(a=a.default),"string"==typeof a&&(a=[[e.id,a,""]]),a.locals&&(e.exports=a.locals),n(346).Z("01449d44",a,!0,{})},346:function(e,t,n){"use strict";function a(e,t){for(var n=[],a={},i=0;in.parts.length&&(a.parts.length=n.parts.length)}else{for(var s=[],i=0;i\n {{ $tc(\'vrpayment-order.header\') }}\n\n{% endblock %}\n\n{% block sw_order_detail_actions_slot_smart_bar_actions %}\n\n{% endblock %}\n',data(){return{isVRPaymentPayment:!1}},computed:{isEditable(){return!this.isVRPaymentPayment||"vrpayment.order.detail"!==this.$route.name},showTabs(){return!0}},watch:{orderId:{deep:!0,handler(){if(!this.orderId){this.setIsVRPaymentPayment(null);return}let e=this.repositoryFactory.create("order"),n=new a(1,1);n.addAssociation("transactions"),e.get(this.orderId,t.api,n).then(e=>{if(e.amountTotal<=0||e.transactions.length<=0||!e.transactions[0].paymentMethodId){this.setIsVRPaymentPayment(null);return}let t=e.transactions[0].paymentMethodId;null!=t&&this.setIsVRPaymentPayment(t)})},immediate:!0}},methods:{setIsVRPaymentPayment(e){e&&this.repositoryFactory.create("payment_method").get(e,t.api).then(e=>{this.isVRPaymentPayment="handler_vrpaymentpayment_vrpaymentpaymenthandler"===e.formattedHandlerIdentifier})}}});let{Component:i,Mixin:r,Filter:s,Utils:o}=Shopware;i.register("vrpayment-order-action-completion",{template:'{% block vrpayment_order_action_completion %}\n\n\n {% block vrpayment_order_action_completion_amount %}\n \n \n {% endblock %}\n\n {% block vrpayment_order_action_completion_confirm_button %}\n \n {% endblock %}\n\n \n\n{% endblock %}\n',inject:["VRPaymentTransactionCompletionService"],mixins:[r.getByName("notification")],props:{transactionData:{type:Object,required:!0}},data(){return{isLoading:!0,isCompletion:!1}},computed:{dateFilter(){return s.getByName("date")}},created(){this.createdComponent()},methods:{createdComponent(){this.isLoading=!1},completion(){this.isCompletion&&(this.isLoading=!0,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=!1,this.$emit("modal-close"),this.$nextTick(()=>{this.$router.replace(`${this.$route.path}?hash=${o.createId()}`)})}).catch(e=>{try{this.createNotificationError({title:e.response.data.errors[0].title,message:e.response.data.errors[0].detail,autoClose:!1})}catch(t){this.createNotificationError({title:e.title,message:e.message,autoClose:!1})}finally{this.isLoading=!1,this.$emit("modal-close"),this.$nextTick(()=>{this.$router.replace(`${this.$route.path}?hash=${o.createId()}`)})}}))}}});let{Component:l,Mixin:c,Filter:d,Utils:m}=Shopware;l.register("vrpayment-order-action-refund",{template:'{% block vrpayment_order_action_refund %}\n\n\n {% block vrpayment_order_action_refund_amount %}\n\n \n \n\n
\n {{ $tc(\'vrpayment-order.refundAction.maxAvailableItemsToRefund\') }}:\n {{ this.$parent.$parent.itemRefundableQuantity }}\n
\n {% endblock %}\n\n {% block vrpayment_order_action_refund_confirm_button %}\n \n {% endblock %}\n\n \n
\n{% endblock %}\n',inject:["VRPaymentRefundService"],mixins:[c.getByName("notification")],props:{transactionData:{type:Object,required:!0},orderId:{type:String,required:!0}},data(){return{refundQuantity:0,isLoading:!0,currentLineItem:""}},computed:{dateFilter(){return d.getByName("date")}},created(){this.createdComponent()},methods:{createdComponent(){this.isLoading=!1,this.refundQuantity=1},refund(){this.isLoading=!0,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=!1,this.$emit("modal-close"),this.$nextTick(()=>{this.$router.replace(`${this.$route.path}?hash=${m.createId()}`)})}).catch(e=>{try{this.createNotificationError({title:e.response.data.errors[0].title,message:e.response.data.errors[0].detail,autoClose:!1})}catch(t){this.createNotificationError({title:e.title,message:e.message,autoClose:!1})}finally{this.isLoading=!1,this.$emit("modal-close"),this.$nextTick(()=>{this.$router.replace(`${this.$route.path}?hash=${m.createId()}`)})}})}}});let{Component:u,Mixin:p,Filter:h,Utils:g}=Shopware;u.register("vrpayment-order-action-refund-partial",{template:'{% block vrpayment_order_action_refund_partial %}\n\n\n {% block vrpayment_order_action_refund_amount_partial %}\n \n \n\n
\n {{ $tc(\'vrpayment-order.refundAction.maxAvailableAmountToRefund\') }}:\n {{ this.$parent.$parent.itemRefundableAmount }}\n
\n {% endblock %}\n\n {% block vrpayment_order_action_refund_confirm_button_partial %}\n \n {% endblock %}\n\n \n
\n{% endblock %}\n',inject:["VRPaymentRefundService"],mixins:[p.getByName("notification")],props:{transactionData:{type:Object,required:!0},orderId:{type:String,required:!0}},data(){return{isLoading:!0,currency:this.transactionData.transactions[0].currency,refundAmount:0}},computed:{dateFilter(){return h.getByName("date")}},created(){this.createdComponent()},methods:{createdComponent(){this.isLoading=!1,this.currency=this.transactionData.transactions[0].currency,this.refundAmount=this.$parent.$parent.itemRefundableAmount},createPartialRefund(e){this.isLoading=!0,this.VRPaymentRefundService.createPartialRefund(this.transactionData.transactions[0].metaData.salesChannelId,this.transactionData.transactions[0].id,this.refundAmount,e).then(()=>{this.createNotificationSuccess({title:this.$tc("vrpayment-order.refundAction.successTitle"),message:this.$tc("vrpayment-order.refundAction.successMessage")}),this.isLoading=!1,this.$emit("modal-close"),this.$nextTick(()=>{this.$router.replace(`${this.$route.path}?hash=${g.createId()}`)})}).catch(e=>{try{this.createNotificationError({title:e.response.data.errors[0].title,message:e.response.data.errors[0].detail,autoClose:!1})}catch(t){this.createNotificationError({title:e.title,message:e.message,autoClose:!1})}finally{this.isLoading=!1,this.$emit("modal-close"),this.$nextTick(()=>{this.$router.replace(`${this.$route.path}?hash=${g.createId()}`)})}})}},watch:{refundAmount(e){null!==e&&(this.refundAmount=Math.round(100*e)/100)}}});let{Component:y,Mixin:f,Filter:v,Utils:b}=Shopware;y.register("vrpayment-order-action-refund-by-amount",{template:'{% block vrpayment_order_action_refund_by_amount %}\n\n\n {% block vrpayment_order_action_refund_amount_by_amount %}\n \n \n {% endblock %}\n\n {% block vrpayment_order_action_refund_confirm_button_by_amount %}\n \n {% endblock %}\n\n \n\n{% endblock %}\n',inject:["VRPaymentRefundService"],mixins:[f.getByName("notification")],props:{transactionData:{type:Object,required:!0},orderId:{type:String,required:!0}},data(){return{isLoading:!0,currency:this.transactionData.transactions[0].currency,refundAmount:0,refundableAmount:0}},computed:{dateFilter(){return v.getByName("date")}},created(){this.createdComponent()},methods:{createdComponent(){this.isLoading=!1,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=!0,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=!1,this.$emit("modal-close"),this.$nextTick(()=>{this.$router.replace(`${this.$route.path}?hash=${b.createId()}`)})}).catch(e=>{try{this.createNotificationError({title:e.response.data.errors[0].title,message:e.response.data.errors[0].detail,autoClose:!1})}catch(t){this.createNotificationError({title:e.title,message:e.message,autoClose:!1})}finally{this.isLoading=!1,this.$emit("modal-close"),this.$nextTick(()=>{this.$router.replace(`${this.$route.path}?hash=${b.createId()}`)})}})}}});let{Component:_,Mixin:I,Filter:C,Utils:w}=Shopware;_.register("vrpayment-order-action-void",{template:'{% block vrpayment_order_action_void %}\n\n\n {% block vrpayment_order_action_void_amount %}\n \n \n {% endblock %}\n\n {% block vrpayment_order_action_void_confirm_button %}\n \n {% endblock %}\n\n \n\n{% endblock %}\n',inject:["VRPaymentTransactionVoidService"],mixins:[I.getByName("notification")],props:{transactionData:{type:Object,required:!0}},data(){return{isLoading:!0,isVoid:!1}},computed:{dateFilter(){return C.getByName("date")},lineItemColumns(){return[{property:"uniqueId",label:this.$tc("vrpayment-order.refund.types.uniqueId"),rawData:!1,allowResize:!0,primary:!0,width:"auto"},{property:"name",label:this.$tc("vrpayment-order.refund.types.name"),rawData:!0,allowResize:!0,sortable:!0,width:"auto"},{property:"quantity",label:this.$tc("vrpayment-order.refund.types.quantity"),rawData:!0,allowResize:!0,width:"auto"},{property:"amountIncludingTax",label:this.$tc("vrpayment-order.refund.types.amountIncludingTax"),rawData:!0,allowResize:!0,inlineEdit:"string",width:"auto"},{property:"type",label:this.$tc("vrpayment-order.refund.types.type"),rawData:!0,allowResize:!0,sortable:!0,width:"auto"},{property:"taxAmount",label:this.$tc("vrpayment-order.refund.types.taxAmount"),rawData:!0,allowResize:!0,width:"auto"}]}},created(){this.createdComponent()},methods:{createdComponent(){this.isLoading=!1,this.currency=this.transactionData.transactions[0].currency,this.refundableAmount=this.transactionData.transactions[0].amountIncludingTax,this.refundAmount=this.transactionData.transactions[0].amountIncludingTax},voidPayment(){this.isVoid&&(this.isLoading=!0,this.VRPaymentTransactionVoidService.createTransactionVoid(this.transactionData.transactions[0].metaData.salesChannelId,this.transactionData.transactions[0].id).then(()=>{this.createNotificationSuccess({title:this.$tc("vrpayment-order.voidAction.successTitle"),message:this.$tc("vrpayment-order.voidAction.successMessage")}),this.isLoading=!1,this.$emit("modal-close"),this.$nextTick(()=>{this.$router.replace(`${this.$route.path}?hash=${w.createId()}`)})}).catch(e=>{try{this.createNotificationError({title:e.response.data.errors[0].title,message:e.response.data.errors[0].detail,autoClose:!1})}catch(t){this.createNotificationError({title:e.title,message:e.message,autoClose:!1})}finally{this.isLoading=!1,this.$emit("modal-close"),this.$nextTick(()=>{this.$router.replace(`${this.$route.path}?hash=${w.createId()}`)})}}))}}}),n(894);let{Component:E,Mixin:S,Filter:T,Context:P,Utils:A}=Shopware,N=Shopware.Data.Criteria;E.register("vrpayment-order-detail",{template:'{% block vrpayment_order_detail %}\n
\n
\n \n \n \n {% block vrpayment_order_transaction_history_card %}\n \n \n\n \n {% endblock %}\n {% block vrpayment_order_transaction_line_items_card %}\n \n \n \n {% endblock %}\n {% block vrpayment_order_transaction_refunds_card %}\n \n \n\n \n {% endblock %}\n {% block vrpayment_order_actions_modal_refund_partial %}\n \n \n {% endblock %}\n {% block vrpayment_order_actions_modal_refund %}\n \n \n {% endblock %}\n {% block vrpayment_order_actions_modal_refund_by_amount %}\n \n \n {% endblock %}\n {% block vrpayment_order_actions_modal_completion%}\n \n \n {% endblock %}\n {% block vrpayment_order_actions_modal_void %}\n \n \n {% endblock %}\n
\n \n
\n{% endblock %}\n',inject:["VRPaymentTransactionService","VRPaymentRefundService","repositoryFactory"],mixins:[S.getByName("notification")],data(){return{transactionData:{transactions:[],refunds:[]},transaction:{},lineItems:[],refundableQuantity:0,itemRefundableQuantity:0,isLoading:!0,orderId:"",currency:"",modalType:"",refundAmount:0,refundableAmount:0,itemRefundedAmount:0,itemRefundedQuantity:0,itemRefundableAmount:0,currentLineItem:"",refundLineItemQuantity:[],refundLineItemAmount:[],selectedItems:[]}},metaInfo(){return{title:this.$tc("vrpayment-order.header")}},computed:{dateFilter(){return T.getByName("date")},relatedResourceColumns(){return[{property:"paymentMethodName",label:this.$tc("vrpayment-order.transactionHistory.types.payment_method"),rawData:!0},{property:"state",label:this.$tc("vrpayment-order.transactionHistory.types.state"),rawData:!0},{property:"currency",label:this.$tc("vrpayment-order.transactionHistory.types.currency"),rawData:!0},{property:"authorized_amount",label:this.$tc("vrpayment-order.transactionHistory.types.authorized_amount"),rawData:!0},{property:"id",label:this.$tc("vrpayment-order.transactionHistory.types.transaction"),rawData:!0},{property:"customerId",label:this.$tc("vrpayment-order.transactionHistory.types.customer"),rawData:!0}]},lineItemColumns(){return[{property:"id",rawData:!0,visible:!1,primary:!0},{property:"uniqueId",label:this.$tc("vrpayment-order.lineItem.types.uniqueId"),rawData:!0,visible:!1,primary:!0},{property:"name",label:this.$tc("vrpayment-order.lineItem.types.name"),rawData:!0},{property:"quantity",label:this.$tc("vrpayment-order.lineItem.types.quantity"),rawData:!0},{property:"amountIncludingTax",label:this.$tc("vrpayment-order.lineItem.types.amountIncludingTax"),rawData:!0},{property:"type",label:this.$tc("vrpayment-order.lineItem.types.type"),rawData:!0},{property:"taxAmount",label:this.$tc("vrpayment-order.lineItem.types.taxAmount"),rawData:!0},{property:"refundableQuantity",rawData:!0,visible:!1}]},refundColumns(){return[{property:"id",label:this.$tc("vrpayment-order.refund.types.id"),rawData:!0,visible:!0,primary:!0},{property:"amount",label:this.$tc("vrpayment-order.refund.types.amount"),rawData:!0},{property:"state",label:this.$tc("vrpayment-order.refund.types.state"),rawData:!0},{property:"createdOn",label:this.$tc("vrpayment-order.refund.types.createdOn"),rawData:!0}]}},watch:{$route(){this.resetDataAttributes(),this.createdComponent()}},created(){this.createdComponent()},methods:{createdComponent(){this.orderId=this.$route.params.id;let e=this.repositoryFactory.create("order"),t=new N(1,1);t.addAssociation("transactions"),t.getAssociation("transactions").addSorting(N.sort("createdAt","DESC")),e.get(this.orderId,P.api,t).then(e=>{this.order=e,this.isLoading=!1;var t=0,n=0;let a=e.transactions[0].customFields.vrpayment_transaction_id;this.VRPaymentTransactionService.getTransactionData(e.salesChannelId,a).then(e=>{this.currency=e.transactions[0].currency,e.transactions[0].authorized_amount=A.format.currency(e.transactions[0].authorizationAmount,this.currency),e.refunds.forEach(e=>{n=parseFloat(parseFloat(n)+parseFloat(e.amount)),e.amount=A.format.currency(e.amount,this.currency),e.reductions.forEach(e=>{e.quantityReduction>0&&(void 0===this.refundLineItemQuantity[e.lineItemUniqueId]?this.refundLineItemQuantity[e.lineItemUniqueId]=e.quantityReduction:this.refundLineItemQuantity[e.lineItemUniqueId]+=e.quantityReduction),e.unitPriceReduction>0&&(void 0===this.refundLineItemAmount[e.lineItemUniqueId]?this.refundLineItemAmount[e.lineItemUniqueId]=e.unitPriceReduction:this.refundLineItemAmount[e.lineItemUniqueId]+=e.unitPriceReduction)})}),e.transactions[0].lineItems.forEach(e=>{e.id||(e.id=e.uniqueId),e.itemRefundedAmount=parseFloat(this.refundLineItemAmount[e.uniqueId]||0)*parseInt(e.quantity),e.amountIncludingTax=parseFloat(e.amountIncludingTax)||0,e.itemRefundedQuantity=parseInt(this.refundLineItemQuantity[e.uniqueId])||0,e.refundableAmount=parseFloat((e.amountIncludingTax-e.itemRefundedAmount).toFixed(2)),e.amountIncludingTax=A.format.currency(e.amountIncludingTax,this.currency),e.taxAmount=A.format.currency(e.taxAmount,this.currency),t=parseFloat(parseFloat(t)+parseFloat(e.unitPriceIncludingTax*e.quantity)),e.refundableQuantity=parseInt(parseInt(e.quantity)-parseInt(this.refundLineItemQuantity[e.uniqueId]||0))}),this.lineItems=e.transactions[0].lineItems,this.transactionData=e,this.transaction=this.transactionData.transactions[0],this.refundAmount=Number(this.transactionData.transactions[0].amountIncludingTax),this.refundableAmount=parseFloat(parseFloat(t)-parseFloat(n))}).catch(e=>{try{this.createNotificationError({title:this.$tc("vrpayment-order.paymentDetails.error.title"),message:e.message,autoClose:!1})}catch(t){this.createNotificationError({title:this.$tc("vrpayment-order.paymentDetails.error.title"),message:e.message,autoClose:!1})}finally{this.isLoading=!1}})})},downloadPackingSlip(){window.open(this.VRPaymentTransactionService.getPackingSlip(this.transaction.metaData.salesChannelId,this.transaction.id),"_blank")},downloadInvoice(){window.open(this.VRPaymentTransactionService.getInvoiceDocument(this.transaction.metaData.salesChannelId,this.transaction.id),"_blank")},resetDataAttributes(){this.transactionData={transactions:[],refunds:[]},this.lineItems=[],this.refundLineItemQuantity=[],this.refundLineItemAmount=[],this.isLoading=!0},spawnModal(e,t,n,a){this.modalType=e,this.currentLineItem=t,this.itemRefundableQuantity=n,this.itemRefundableAmount=isNaN(a)?0:Math.round(100*a)/100},closeModal(){this.modalType=""},lineItemRefund(e){this.isLoading=!0,this.VRPaymentRefundService.createRefund(this.transactionData.transactions[0].metaData.salesChannelId,this.transactionData.transactions[0].id,0,e).then(()=>{this.createNotificationSuccess({title:this.$tc("vrpayment-order.refundAction.successTitle"),message:this.$tc("vrpayment-order.refundAction.successMessage")}),this.isLoading=!1,this.$emit("modal-close"),this.$nextTick(()=>{this.$router.replace(`${this.$route.path}?hash=${A.createId()}`)})}).catch(e=>{try{this.createNotificationError({title:e.response.data.errors[0].title,message:e.response.data.errors[0].detail,autoClose:!1})}catch(t){this.createNotificationError({title:e.title,message:e.response.data,autoClose:!1})}finally{this.isLoading=!1,this.$emit("modal-close"),this.$nextTick(()=>{this.$router.replace(`${this.$route.path}?hash=${A.createId()}`)})}})},isSelectable(e){return e.refundableQuantity>0&&e.refundableAmount>0&&0==e.itemRefundedAmount&&0==e.itemRefundedQuantity},onSelectionChanged(e){this.selectedItems=Object.values(e)},onPerformBulkAction(){this.selectedItems.length&&(this.isLoading=!0,this.$nextTick(()=>{Promise.all(this.selectedItems.map(e=>this.lineItemRefundBulk(e.uniqueId))).then(()=>{this.isLoading=!1,this.$emit("modal-close"),this.$nextTick(()=>{this.$router.replace(`${this.$route.path}?hash=${A.createId()}`)})}).catch(e=>{this.createNotificationError({title:"Error",message:"Something went wrong with the refunds",autoClose:!1}),this.isLoading=!1})}))},lineItemRefundBulk(e){return new Promise((t,n)=>{this.VRPaymentRefundService.createRefund(this.transactionData.transactions[0].metaData.salesChannelId,this.transactionData.transactions[0].id,0,e).then(()=>{this.createNotificationSuccess({title:this.$tc("vrpayment-order.refundAction.successTitle"),message:this.$tc("vrpayment-order.refundAction.successMessage")}),t()}).catch(e=>{try{this.createNotificationError({title:e.response.data.errors[0].title,message:e.response.data.errors[0].detail,autoClose:!1})}catch(t){this.createNotificationError({title:e.title,message:e.response.data,autoClose:!1})}finally{n()}})})}}});var D=JSON.parse('{"vrpayment-order":{"buttons":{"label":{"completion":"Abschluss","download-invoice":"Rechnung herunterladen","download-packing-slip":"Packzettel herunterladen","refund":"Eine neue R\xfcckerstattung erstellen","void":"Genehmigung annullieren","refund-whole-line-item":"Gesamte Werbebuchung erstatten","refund-line-item-by-quantity":"R\xfcckerstattung nach Menge","refund-line-item-selected":"R\xfcckerstattung ausw\xe4hlen","refund-line-item-parial":"Teilweise R\xfcckerstattung"}},"captureAction":{"button":{"text":"Zahlung erfassen"},"currentAmount":"Betrag","isFinal":"Dies ist die endg\xfcltige Verbuchung","maxAmount":"Maximaler Betrag","successMessage":"Ihre Verbuchung war erfolgreich","successTitle":"Erfolg"},"general":{"title":"Bestellungen"},"header":"VRPayment Payment","lineItem":{"cardTitle":"Einzelposten","types":{"amountIncludingTax":"Betrag","name":"Name","quantity":"Anzahl","taxAmount":"Steuern","type":"Typ","uniqueId":"Eindeutige ID"}},"modal":{"title":{"capture":"Erfassen","refund":"Neue Gutschrift","void":"Autorisierung aufheben"}},"paymentDetails":{"cardTitle":"Zahlung","error":{"title":"Fehler beim Abrufen von Zahlungsdetails von VRPayment"}},"refund":{"cardTitle":"Gutschriften","refundAmount":{"label":"Gutschriftsbetrag"},"refundQuantity":{"label":"Refund Menge"},"types":{"amount":"Betrag","createdOn":"Erstellt am","id":"ID","state":"Staat"}},"refundAction":{"confirmButton":{"text":"Ausf\xfchren"},"refundAmount":{"label":"Betrag","placeholder":"Einen Betrag eingeben"},"successMessage":"Ihre R\xfcckerstattung war erfolgreich","successTitle":"Erfolg","maxAvailableItemsToRefund":"Maximal Verf\xfcgbare Artikel zum Erstatten","maxAvailableAmountToRefund":"Maximal verf\xfcgbarer Erstattungsbetrag"},"transactionHistory":{"cardTitle":"Einzelheiten","types":{"authorized_amount":"Autorisierter Betrag","currency":"W\xe4hrung","customer":"Kunde","payment_method":"Zahlungsweise","state":"Staat","transaction":"Transaktion"},"customerId":"Customer ID","customerName":"Customer Name","creditCardHolder":"Kreditkarteninhaber","paymentMethod":"Zahlungsart","paymentMethodBrand":"Marke der Zahlungsmethode","PseudoCreditCardNumber":"Pseudo-Kreditkartennummer","CardExpire":"Karte verf\xe4llt"},"voidAction":{"confirm":{"button":{"cancel":"Nein","confirm":"Autorisierung aufheben"},"message":"Wollen Sie diese Zahlung wirklich stornieren?"},"successMessage":"Die Zahlung wurde erfolgreich annulliert","successTitle":"Erfolg"}}}'),k=JSON.parse('{"vrpayment-order":{"buttons":{"label":{"completion":"Complete","download-invoice":"Download Invoice","download-packing-slip":"Download Packing Slip","refund":"Create a new refund","void":"Cancel authorization","refund-whole-line-item":"Refund whole line item","refund-line-item-by-quantity":"Refund by quantity","refund-line-item-selected":"Refund selected","refund-line-item-parial":"Partial refund"}},"captureAction":{"button":{"text":"Capture payment"},"currentAmount":"Amount","isFinal":"This is final capture","maxAmount":"Maximum amount","successMessage":"Your capture was successful.","successTitle":"Success"},"general":{"title":"Orders"},"header":"VRPayment Payment","lineItem":{"cardTitle":"Line Items","types":{"amountIncludingTax":"Amount","name":"Name","quantity":"Quantity","taxAmount":"Taxes","type":"Type","uniqueId":"Unique ID"}},"modal":{"title":{"capture":"Capture","refund":"New refund","void":"Cancel authorization"}},"paymentDetails":{"cardTitle":"Payment","error":{"title":"Error fetching payment details from VRPayment"}},"refund":{"cardTitle":"Refunds","refundAmount":{"label":"Refund Amount"},"refundQuantity":{"label":"Refund Quantity"},"types":{"amount":"Amount","createdOn":"Created On","id":"ID","state":"State"}},"refundAction":{"confirmButton":{"text":"Execute"},"refundAmount":{"label":"Amount","placeholder":"Enter a amount"},"successMessage":"Your refund was successful.","successTitle":"Success","maxAvailableItemsToRefund":"Maximum available items to refund","maxAvailableAmountToRefund":"Maximum available amount to refund"},"transactionHistory":{"cardTitle":"Details","types":{"authorized_amount":"Authorized Amount","currency":"Currency","customer":"Customer","payment_method":"Payment Method","state":"State","transaction":"Transaction"},"customerId":"Customer ID","customerName":"Customer Name","creditCardHolder":"Credit Card Holder","paymentMethod":"Payment Method","paymentMethodBrand":"Payment Method Brand","PseudoCreditCardNumber":"Pseudo Credit Card Number","CardExpire":"Card Expire"},"voidAction":{"confirm":{"button":{"cancel":"No","confirm":"Cancel authorization"},"message":"Do you really want to cancel this payment?"},"successMessage":"The payment was successfully voided.","successTitle":"Success"}}}'),R=JSON.parse('{"vrpayment-order":{"buttons":{"label":{"completion":"Termin\xe9e","download-invoice":"T\xe9l\xe9charger la facture","download-packing-slip":"T\xe9l\xe9charger le bordereau d\'exp\xe9dition","refund":"Cr\xe9er un nouveau remboursement","void":"Annulez l\'autorisation","refund-whole-line-item":"Remboursement de la ligne enti\xe8re","refund-line-item-by-quantity":"Remboursement par quantit\xe9","refund-line-item-selected":"Rembourser s\xe9lectionn\xe9s","refund-line-item-parial":"Remboursement partiel"}},"captureAction":{"button":{"text":"Capture du paiement"},"currentAmount":"Montant","isFinal":"C\'est la capture finale","maxAmount":"Montant maximal","successMessage":"Votre capture a \xe9t\xe9 r\xe9ussie.","successTitle":"Succ\xe8s"},"general":{"title":"Commandes"},"header":"VRPayment Paiement","lineItem":{"cardTitle":"Articles de ligne","types":{"amountIncludingTax":"Montant","name":"Nom","quantity":"Quantit\xe9","taxAmount":"Taxes","type":"Type","uniqueId":"ID unique"}},"modal":{"title":{"capture":"Capture","refund":"Nouveau remboursement","void":"Annulez l\'autorisation"}},"paymentDetails":{"cardTitle":"Paiement","error":{"title":"Erreur dans la r\xe9cup\xe9ration des d\xe9tails du paiement \xe0 partir de VRPayment"}},"refund":{"cardTitle":"Remboursements","refundAmount":{"label":"Montant du remboursement"},"refundQuantity":{"label":"Quantit\xe9 \xe0 rembourser"},"types":{"amount":"Montant","createdOn":"Cr\xe9\xe9 le","id":"ID","state":"\xc9tat"}},"refundAction":{"confirmButton":{"text":"Ex\xe9cutez"},"refundAmount":{"label":"Montant","placeholder":"Entrez un montant"},"successMessage":"Votre remboursement a \xe9t\xe9 effectu\xe9 avec succ\xe8s.","successTitle":"Succ\xe8s","maxAvailableItemsToRefund":"Nombre maximum d\'articles disponibles pour le remboursement","maxAvailableAmountToRefund":"Montant maximal disponible pour le remboursement"},"transactionHistory":{"cardTitle":"D\xe9tails","types":{"authorized_amount":"Montant autoris\xe9","currency":"Monnaie","customer":"Client","payment_method":"Mode de paiement","state":"\xc9tat","transaction":"Transaction"},"customerId":"Customer ID","customerName":"Customer Name","creditCardHolder":"Titulaire de la carte de cr\xe9dit","paymentMethod":"Mode de paiement","paymentMethodBrand":"Marque du mode de paiement","PseudoCreditCardNumber":"Pseudo num\xe9ro de carte de cr\xe9dit","CardExpire":"La carte expire"},"voidAction":{"confirm":{"button":{"cancel":"Non","confirm":"Annulez l\'autorisation"},"message":"Voulez-vous vraiment annuler ce paiement?"},"successMessage":"Le paiement a \xe9t\xe9 annul\xe9 avec succ\xe8s.","successTitle":"Succ\xe8s"}}}'),O=JSON.parse('{"vrpayment-order":{"buttons":{"label":{"completion":"Completato","download-invoice":"Scarica fattura","download-packing-slip":"Scarica distinta di imballaggio","refund":"Crea un nuovo rimborso","void":"Annulla autorizzazione","refund-whole-line-item":"Rimborso intera riga","refund-line-item-by-quantity":"Rimborso per quantit\xe0","refund-line-item-selected":"Rimborso selezionati","refund-line-item-parial":"Rimborso parziale"}},"captureAction":{"button":{"text":"Cattura pagamento"},"currentAmount":"Importo","isFinal":"Questa \xe8 la cattura finale","maxAmount":"Importo massimo","successMessage":"La tua cattura ha avuto successo.","successTitle":"Successo"},"general":{"title":"Ordini"},"header":"Pagamento VRPayment","lineItem":{"cardTitle":"Articoli di linea","types":{"amountIncludingTax":"Importo","name":"Nome","quantity":"Quantit\xe0","taxAmount":"Tasse","type":"Tipo","uniqueId":"ID unico"}},"modal":{"title":{"capture":"Cattura","refund":"Nuovo rimborso","void":"Annulla autorizzazione"}},"paymentDetails":{"cardTitle":"Pagamento","error":{"title":"Errore nel recupero dei dettagli del pagamento da VRPayment"}},"refund":{"cardTitle":"Rimborsi","refundAmount":{"label":"Importo del rimborso"},"refundQuantity":{"label":"Quantit\xe0 di rimborso"},"types":{"amount":"Importo","createdOn":"Creato il","id":"ID","state":"Stato"}},"refundAction":{"confirmButton":{"text":"Esegui"},"refundAmount":{"label":"Importo","placeholder":"Inserisci un importo"},"successMessage":"Il tuo rimborso \xe8 andato a buon fine.","successTitle":"Successo","maxAvailableItemsToRefund":"Numero massimo di articoli disponibili da rimborsare","maxAvailableAmountToRefund":"Importo massimo disponibile per il rimborso"},"transactionHistory":{"cardTitle":"Dettagli","types":{"authorized_amount":"Importo autorizzato","currency":"Valuta","customer":"Cliente","payment_method":"Metodo di pagamento","state":"Stato","transaction":"Transazione"},"customerId":"Customer ID","customerName":"Customer Name","creditCardHolder":"Proprietario della carta di credito","paymentMethod":"Metodo di pagamento","paymentMethodBrand":"Metodo di pagamento Marca","PseudoCreditCardNumber":"Numero di carta di credito pseudo","CardExpire":"La carta scade"},"voidAction":{"confirm":{"button":{"cancel":"No","confirm":"Annulla autorizzazione"},"message":"Vuoi davvero annullare questo pagamento?"},"successMessage":"Il pagamento \xe8 stato annullato con successo.","successTitle":"Successo"}}}');let{Module:x}=Shopware;x.register("vrpayment-order",{type:"plugin",name:"VRPayment",title:"vrpayment-order.general.title",description:"vrpayment-order.general.descriptionTextModule",version:"1.0.1",targetVersion:"1.0.1",color:"#2b52ff",snippets:{"de-DE":D,"en-GB":k,"fr-FR":R,"it-IT":O},routeMiddleware(e,t){"sw.order.detail"===t.name&&t.children.push({component:"vrpayment-order-detail",name:"vrpayment.order.detail",isChildren:!0,path:"/sw/order/vrpayment/detail/:id"}),e(t)}}),n(174);let F="VRPaymentPayment.config";var $={CONFIG_DOMAIN:F,CONFIG_APPLICATION_KEY:F+".applicationKey",CONFIG_EMAIL_ENABLED:F+".emailEnabled",CONFIG_INTEGRATION:F+".integration",CONFIG_LINE_ITEM_CONSISTENCY_ENABLED:F+".lineItemConsistencyEnabled",CONFIG_SPACE_ID:F+".spaceId",CONFIG_SPACE_VIEW_ID:F+".spaceViewId",CONFIG_STOREFRONT_INVOICE_DOWNLOAD_ENABLED:F+".storefrontInvoiceDownloadEnabled",CONFIG_USER_ID:F+".userId",CONFIG_STOREFRONT_WEBHOOKS_UPDATE_ENABLED:F+".storefrontWebhooksUpdateEnabled",CONFIG_STOREFRONT_PAYMENTS_UPDATE_ENABLED:F+".storefrontPaymentsUpdateEnabled"};let{Component:V,Mixin:L}=Shopware;V.register("vrpayment-settings",{template:'{% block vrpayment_settings %}\n\n\n {% block vrpayment_settings_header %}\n \n {% endblock %}\n\n {% block vrpayment_settings_actions %}\n \n {% endblock %}\n\n {% block vrpayment_settings_content %}\n \n {% endblock %}\n\n{% endblock %}',inject:["acl","VRPaymentConfigurationService"],mixins:[L.getByName("notification"),L.getByName("sw-inline-snippet")],data(){return{config:{},isLoading:!1,isTesting:!1,isSaveSuccessful:!1,applicationKeyFilled:!1,applicationKeyErrorState:!1,spaceIdFilled:!1,spaceIdErrorState:!1,userIdFilled:!1,userIdErrorState:!1,isSetDefaultPaymentSuccessful:!1,isSettingDefaultPaymentMethods:!1,configIntegrationDefaultValue:"iframe",configEmailEnabledDefaultValue:!0,configLineItemConsistencyEnabledDefaultValue:!0,configStorefrontInvoiceDownloadEnabledEnabledDefaultValue:!0,configStorefrontWebhooksUpdateEnabledDefaultValue:!0,configStorefrontPaymentsUpdateEnabledDefaultValue:!0,...$}},props:{isLoading:{type:Boolean,required:!0}},metaInfo(){return{title:this.$createTitle()}},created(){this.$on("check-api-connection-event",this.onCheckApiConnection)},beforeDestroy(){this.$off("check-api-connection-event",this.onCheckApiConnection)},watch:{config:{handler(e){let t=this.$refs.configComponent.allConfigs.null;null===this.$refs.configComponent.selectedSalesChannelId?(this.applicationKeyFilled=!!this.config[this.CONFIG_APPLICATION_KEY],this.spaceIdFilled=!!this.config[this.CONFIG_SPACE_ID],this.userIdFilled=!!this.config[this.CONFIG_USER_ID],this.CONFIG_INTEGRATION in this.config||(this.config[this.CONFIG_INTEGRATION]=this.configIntegrationDefaultValue),this.CONFIG_EMAIL_ENABLED in this.config||(this.config[this.CONFIG_EMAIL_ENABLED]=this.configEmailEnabledDefaultValue),this.CONFIG_LINE_ITEM_CONSISTENCY_ENABLED in this.config||(this.config[this.CONFIG_LINE_ITEM_CONSISTENCY_ENABLED]=this.configLineItemConsistencyEnabledDefaultValue),this.CONFIG_STOREFRONT_INVOICE_DOWNLOAD_ENABLED in this.config||(this.config[this.CONFIG_STOREFRONT_INVOICE_DOWNLOAD_ENABLED]=this.configStorefrontInvoiceDownloadEnabledEnabledDefaultValue),this.CONFIG_STOREFRONT_WEBHOOKS_UPDATE_ENABLED in this.config||(this.config[this.CONFIG_STOREFRONT_WEBHOOKS_UPDATE_ENABLED]=this.configStorefrontWebhooksUpdateEnabledDefaultValue),this.CONFIG_STOREFRONT_PAYMENTS_UPDATE_ENABLED in this.config||(this.config[this.CONFIG_STOREFRONT_PAYMENTS_UPDATE_ENABLED]=this.configStorefrontPaymentsUpdateEnabledDefaultValue)):(this.applicationKeyFilled=!!this.config[this.CONFIG_APPLICATION_KEY]||!!t[this.CONFIG_APPLICATION_KEY],this.spaceIdFilled=!!this.config[this.CONFIG_SPACE_ID]||!!t[this.CONFIG_SPACE_ID],this.userIdFilled=!!this.config[this.CONFIG_USER_ID]||!!t[this.CONFIG_USER_ID],this.CONFIG_INTEGRATION in this.config&&this.CONFIG_INTEGRATION in t||(this.config[this.CONFIG_INTEGRATION]=this.configIntegrationDefaultValue),this.CONFIG_EMAIL_ENABLED in this.config&&this.CONFIG_EMAIL_ENABLED in t||(this.config[this.CONFIG_EMAIL_ENABLED]=this.configEmailEnabledDefaultValue),this.CONFIG_LINE_ITEM_CONSISTENCY_ENABLED in this.config&&this.CONFIG_LINE_ITEM_CONSISTENCY_ENABLED in t||(this.config[this.CONFIG_LINE_ITEM_CONSISTENCY_ENABLED]=this.configLineItemConsistencyEnabledDefaultValue),this.CONFIG_STOREFRONT_INVOICE_DOWNLOAD_ENABLED in this.config&&this.CONFIG_STOREFRONT_INVOICE_DOWNLOAD_ENABLED in t||(this.config[this.CONFIG_STOREFRONT_INVOICE_DOWNLOAD_ENABLED]=this.configStorefrontInvoiceDownloadEnabledEnabledDefaultValue),this.CONFIG_STOREFRONT_WEBHOOKS_UPDATE_ENABLED in this.config&&this.CONFIG_STOREFRONT_WEBHOOKS_UPDATE_ENABLED in t||(this.config[this.CONFIG_STOREFRONT_WEBHOOKS_UPDATE_ENABLED]=this.configStorefrontWebhooksUpdateEnabledDefaultValue),this.CONFIG_STOREFRONT_PAYMENTS_UPDATE_ENABLED in this.config&&this.CONFIG_STOREFRONT_PAYMENTS_UPDATE_ENABLED in t||(this.config[this.CONFIG_STOREFRONT_PAYMENTS_UPDATE_ENABLED]=this.configStorefrontPaymentsUpdateEnabledDefaultValue)),this.$emit("salesChannelChanged"),this.$emit("update:value",e)},deep:!0}},methods:{checkTextFieldInheritance(e){return"string"!=typeof e||e.length<=0},checkNumberFieldInheritance(e){return"number"!=typeof e||e.length<=0},checkBoolFieldInheritance(e){return"boolean"!=typeof e},getInheritValue(e){return null==this.selectedSalesChannelId?this.actualConfigData[e]:this.allConfigs.null[e]},onSave(){if(!(this.spaceIdFilled&&this.userIdFilled&&this.applicationKeyFilled)){this.setErrorStates();return}this.save()},save(){this.isLoading=!0,this.$refs.configComponent.save().then(e=>{e&&(this.config=e),this.registerWebHooks(),this.synchronizePaymentMethodConfiguration(),this.installOrderDeliveryStates()}).catch(e=>{console.error("Error:",e),this.isLoading=!1})},registerWebHooks(){if(!1===this.config[this.CONFIG_STOREFRONT_WEBHOOKS_UPDATE_ENABLED])return!1;this.VRPaymentConfigurationService.registerWebHooks(this.$refs.configComponent.selectedSalesChannelId).then(()=>{this.createNotificationSuccess({title:this.$tc("vrpayment-settings.settingForm.titleSuccess"),message:this.$tc("vrpayment-settings.settingForm.messageWebHookUpdated")})}).catch(e=>{this.createNotificationError({title:this.$tc("vrpayment-settings.settingForm.titleError"),message:this.$tc("vrpayment-settings.settingForm.messageWebHookError")}),this.isLoading=!1,console.error("Error:",e)})},synchronizePaymentMethodConfiguration(){if(!1===this.config[this.CONFIG_STOREFRONT_PAYMENTS_UPDATE_ENABLED])return!1;this.VRPaymentConfigurationService.synchronizePaymentMethodConfiguration(this.$refs.configComponent.selectedSalesChannelId).then(()=>{this.createNotificationSuccess({title:this.$tc("vrpayment-settings.settingForm.titleSuccess"),message:this.$tc("vrpayment-settings.settingForm.messagePaymentMethodConfigurationUpdated")}),this.isLoading=!1}).catch(e=>{this.createNotificationError({title:this.$tc("vrpayment-settings.settingForm.titleError"),message:this.$tc("vrpayment-settings.settingForm.messagePaymentMethodConfigurationError")}),this.isLoading=!1,console.error("Error:",e)})},installOrderDeliveryStates(){this.VRPaymentConfigurationService.installOrderDeliveryStates().then(()=>{this.createNotificationSuccess({title:this.$tc("vrpayment-settings.settingForm.titleSuccess"),message:this.$tc("vrpayment-settings.settingForm.messageOrderDeliveryStateUpdated")}),this.isLoading=!1}).catch(()=>{this.createNotificationError({title:this.$tc("vrpayment-settings.settingForm.titleError"),message:this.$tc("vrpayment-settings.settingForm.messageOrderDeliveryStateError")}),this.isLoading=!1})},onSetPaymentMethodDefault(){this.isSettingDefaultPaymentMethods=!0,this.VRPaymentConfigurationService.setVRPaymentAsSalesChannelPaymentDefault(this.$refs.configComponent.selectedSalesChannelId).then(()=>{this.isSettingDefaultPaymentMethods=!1,this.isSetDefaultPaymentSuccessful=!0,this.createNotificationSuccess({title:this.$tc("vrpayment-settings.settingForm.titleSuccess"),message:this.$tc("vrpayment-settings.salesChannelCard.messageDefaultPaymentUpdated")})})},setErrorStates(){let e={code:1,detail:this.$tc("vrpayment-settings.messageNotBlank")};this.spaceIdFilled||(this.spaceIdErrorState=e),this.userIdFilled||(this.userIdErrorState=e),this.applicationKeyFilled||(this.applicationKeyErrorState=e)},onCheckApiConnection(e){let{spaceId:t,userId:n,applicationKey:a}=e;this.isTesting=!0,this.VRPaymentConfigurationService.checkApiConnection(t,n,a).then(e=>{200===e.result?this.createNotificationSuccess({title:this.$tc("vrpayment-settings.settingForm.credentials.alert.title"),message:this.$tc("vrpayment-settings.settingForm.credentials.alert.successMessage")}):this.createNotificationError({title:this.$tc("vrpayment-settings.settingForm.credentials.alert.title"),message:this.$tc("vrpayment-settings.settingForm.credentials.alert.errorMessage")}),this.isTesting=!1}).catch(()=>{this.createNotificationError({title:this.$tc("vrpayment-settings.settingForm.credentials.alert.title"),message:this.$tc("vrpayment-settings.settingForm.credentials.alert.errorMessage")}),this.isTesting=!1})}}});let{Component:M,Mixin:B}=Shopware;M.register("sw-vrpayment-credentials",{template:'{% block vrpayment_settings_content_card_channel_config_credentials %}\n \n\n {% block vrpayment_settings_content_card_channel_config_credentials_card_container %}\n \n\n {% block vrpayment_settings_content_card_channel_config_credentials_card_container_settings %}\n
\n\n {% block vrpayment_settings_content_card_channel_config_credentials_card_container_settings_space_id %}\n \n \n \n {% endblock %}\n\n {% block vrpayment_settings_content_card_channel_config_credentials_card_container_settings_user_id %}\n \n \n \n {% endblock %}\n\n {% block vrpayment_settings_content_card_channel_config_credentials_card_container_settings_application_key %}\n \n \n \n {% endblock %}\n
\n {% endblock %}\n\n \n \n {{ $tc(\'vrpayment-settings.settingForm.credentials.button.label\') }}\n \n \n\n
\n {% endblock %}\n \n\n{% endblock %}\n',name:"VRPaymentCredentials",inject:["acl"],mixins:[B.getByName("notification")],props:{actualConfigData:{type:Object,required:!0},allConfigs:{type:Object,required:!0},selectedSalesChannelId:{required:!0},spaceIdFilled:{type:Boolean,required:!0},spaceIdErrorState:{required:!0},userIdFilled:{type:Boolean,required:!0},userIdErrorState:{required:!0},applicationKeyFilled:{type:Boolean,required:!0},applicationKeyErrorState:{required:!0},isLoading:{type:Boolean,required:!0},isTesting:{type:Boolean,required:!1}},data(){return{...$}},methods:{checkTextFieldInheritance(e){return"string"!=typeof e||e.length<=0},checkNumberFieldInheritance(e){return"number"!=typeof e||e.length<=0},checkBoolFieldInheritance(e){return"boolean"!=typeof e},emitCheckApiConnectionEvent(){let e={spaceId:this.actualConfigData[$.CONFIG_SPACE_ID],userId:this.actualConfigData[$.CONFIG_USER_ID],applicationKey:this.actualConfigData[$.CONFIG_APPLICATION_KEY]};this.$emit("check-api-connection-event",e)}}});let{Component:z,Mixin:G}=Shopware;z.register("sw-vrpayment-options",{template:'{% block vrpayment_settings_content_card_channel_config_options %}\n \n\n {% block vrpayment_settings_content_card_channel_config_credentials_card_container %}\n \n\n {% block vrpayment_settings_content_card_channel_config_credentials_card_container_settings %}\n
\n\n {% block vrpayment_settings_content_card_channel_config_credentials_card_container_settings_space_view_id %}\n \n \n \n {% endblock %}\n\n {% block vrpayment_settings_content_card_channel_config_credentials_card_container_settings_integration %}\n \n \n \n {% endblock %}\n\n {% block vrpayment_settings_content_card_channel_config_credentials_card_container_settings_line_item_consistency_enabled %}\n \n \n \n {% endblock %}\n\n {% block vrpayment_settings_content_card_channel_config_credentials_card_container_settings_email_enabled %}\n \n \n \n {% endblock %}\n
\n {% endblock %}\n
\n {% endblock %}\n
\n\n{% endblock %}\n',name:"VRPaymentOptions",mixins:[G.getByName("notification")],props:{actualConfigData:{type:Object,required:!0},allConfigs:{type:Object,required:!0},selectedSalesChannelId:{required:!0},isLoading:{type:Boolean,required:!0}},data(){return{...$}},computed:{integrationOptions(){return[{id:"iframe",name:this.$tc("vrpayment-settings.settingForm.options.integration.options.iframe")},{id:"payment_page",name:this.$tc("vrpayment-settings.settingForm.options.integration.options.payment_page")}]}},methods:{checkTextFieldInheritance(e){return"string"!=typeof e||e.length<=0},checkNumberFieldInheritance(e){return"number"!=typeof e||e.length<=0},checkBoolFieldInheritance(e){return"boolean"!=typeof e}}});let{Component:q}=Shopware;q.register("sw-vrpayment-settings-icon",{template:'{% block vrpayment_settings_icon %}\n \n \n\n\n \n\n\n\n\n \n{% endblock %}\n'});let{Component:U,Mixin:H}=Shopware;U.register("sw-vrpayment-storefront-options",{template:'\n \n
\n \n \n \n
\n
\n
\n\n',name:"VRPaymentStorefrontOptions",mixins:[H.getByName("notification")],props:{actualConfigData:{type:Object,required:!0},allConfigs:{type:Object,required:!0},selectedSalesChannelId:{required:!0},isLoading:{type:Boolean,required:!0}},data(){return{...$}},methods:{checkTextFieldInheritance(e){return"string"!=typeof e||e.length<=0},checkNumberFieldInheritance(e){return"number"!=typeof e||e.length<=0},checkBoolFieldInheritance(e){return"boolean"!=typeof e}}});let{Component:W,Mixin:K}=Shopware;W.register("sw-vrpayment-advanced-options",{template:'\n \n
\n \n \n \n\n \n \n \n
\n
\n
\n\n',name:"VRPaymentAdvancedOptions",inject:["acl"],mixins:[K.getByName("notification")],props:{actualConfigData:{type:Object,required:!0},allConfigs:{type:Object,required:!0},selectedSalesChannelId:{required:!0},isLoading:{type:Boolean,required:!0}},data(){return{...$}},methods:{checkTextFieldInheritance(e){return"string"!=typeof e||e.length<=0},checkNumberFieldInheritance(e){return"number"!=typeof e||e.length<=0},checkBoolFieldInheritance(e){return"boolean"!=typeof e}}});var Q=JSON.parse('{"sw-privileges":{"permissions":{"parents":{"vrpayment":"VRPayment plugin"},"vrpayment":{"label":"VRPayment berechtigungen"}}},"vrpayment-settings":{"general":{"descriptionTextModule":"VRPayment-Einstellungen","mainMenuItemGeneral":"VRPayment"},"header":"VRPayment","messageNotBlank":"Dieser Wert sollte nicht leer sein.","salesChannelCard":{"button":{"description":"Klicken Sie auf diese Schaltfl\xe4che, um VRPayment als Standard-Zahlungsabwickler im ausgew\xe4hlten Vertriebskanal festzulegen","label":"VRPayment als Standard-Zahlungsabwickler festlegen"},"messageDefaultPaymentError":"VRPayment als Standard-Zahlungsabwickler konnte nicht festgelegt werden..","messageDefaultPaymentUpdated":"VRPayment als Standard-Zahlungsabwickler wurde festgelegt."},"settingForm":{"credentials":{"applicationKey":{"label":"Application Key","tooltipText":"Der Anwendungsschl\xfcssel wird verwendet, um dieses Plugin mit der API VRPayment zu authentifizieren."},"cardTitle":"Anmeldedaten","spaceId":{"label":"Space ID","tooltipText":"Die Space ID wird verwendet, um dieses Plugin mit der API VRPayment zu authentifizieren."},"userId":{"label":"User ID","tooltipText":"Die Benutzer-ID wird verwendet, um dieses Plugin mit der VRPayment-API zu authentifizieren."},"button":{"description":"Klicken Sie auf diese Schaltfl\xe4che, um die VRPayment API zu testen","label":"API Verbindung testen"},"alert":{"title":"API-Test","successMessage":"Die Verbindung wurde erfolgreich getestet.","errorMessage":"Die Verbindung ist fehlgeschlagen. Versuchen Sie es erneut."}},"messageSaveSuccess":"VRPayment-Einstellungen wurden gespeichert.","messageOrderDeliveryStateError":"VRPayment OrderDeliveryState konnte nicht gespeichert werden.","messageOrderDeliveryStateUpdated":"VRPayment OrderDeliveryState wurde aktualisiert.","messagePaymentMethodConfigurationError":"VRPayment PaymentMethodConfiguration konnte nicht gespeichert werden. Bitte \xfcberpr\xfcfen Sie Ihre Anmeldedaten.","messagePaymentMethodConfigurationUpdated":"VRPayment PaymentMethodConfiguration wurde registriert.","messageWebHookError":"VRPayment WebHook konnte nicht gespeichert werden. Bitte \xfcberpr\xfcfen Sie Ihre Zugangsdaten.","messageWebHookUpdated":"VRPayment WebHook wurde aktualisiert.","options":{"cardTitle":"Optionen","emailEnabled":{"label":"Auftragsbest\xe4tigung per E-Mail senden","tooltipText":"Wenn diese Einstellung aktiviert ist, erhalten Ihre Kunden eine E-Mail von Ihrem Gesch\xe4ft, wenn die Zahlung ihrer Bestellung autorisiert ist."},"integration":{"label":"Integration","options":{"iframe":"Iframe","payment_page":"Payment Page"},"tooltipText":"Integration"},"lineItemConsistencyEnabled":{"label":"Konsistenz der Einzelposten","tooltipText":"Wenn diese Option aktiviert ist, stimmen die Summen der Einzelposten in VRPaymentPayment immer mit der Shopware-Bestellsumme \xfcberein."},"spaceViewId":{"label":"Space View ID","tooltipText":"Space View ID"}},"save":"Speichern","storefrontOptions":{"cardTitle":"Storefront-Optionen","invoiceDownloadEnabled":{"label":"Rechnung Download","tooltipText":"Wenn diese Einstellung aktiviert ist, k\xf6nnen Ihre Kunden Auftragsrechnungen von VRPayment herunterladen."}},"advancedOptions":{"cardTitle":"Erweiterte-Optionen","webhooksUpdateEnabled":{"label":"Webhooks-Update","tooltipText":"Wenn diese Einstellung aktiviert ist, wird das Webhook-Update ausgel\xf6st, wenn Sie die Einstellungen speichern"},"paymentsUpdateEnabled":{"label":"Payments-Update","tooltipText":"Wenn diese Einstellung aktiviert ist, wird die Aktualisierung der Zahlungsmethoden ausgel\xf6st, wenn Sie die Einstellungen speichern"}},"titleError":"Fehler","titleSuccess":"Erfolg"}}}'),j=JSON.parse('{"sw-privileges":{"permissions":{"parents":{"vrpayment":"VRPayment plugin"},"vrpayment":{"label":"VRPayment permissions"}}},"vrpayment-settings":{"general":{"descriptionTextModule":"VRPayment settings","mainMenuItemGeneral":"VRPayment"},"header":"VRPayment","messageNotBlank":"This value should not be blank.","salesChannelCard":{"button":{"description":"Click this button to set VRPayment as default payment handler in the selected SalesChannel","label":"Set VRPayment as default payment handler"},"messageDefaultPaymentError":"VRPayment as default payment could not be set.","messageDefaultPaymentUpdated":"VRPayment as default payment has been set."},"settingForm":{"credentials":{"applicationKey":{"label":"Application Key","tooltipText":"The Application Key is used to authenticate this plugin with the VRPayment API."},"cardTitle":"Credentials","spaceId":{"label":"Space ID","tooltipText":"The space ID is used to authenticate this plugin with the VRPayment API."},"userId":{"label":"User ID","tooltipText":"The user ID is used to authenticate this plugin with the VRPayment API."},"button":{"description":"Click this button to test the VRPayment API","label":"API connection test"},"alert":{"title":"API Test","successMessage":"The connection was successfully tested.","errorMessage":"The connection was failed. Try it again."}},"messageSaveSuccess":"VRPayment settings have been saved.","messageOrderDeliveryStateError":"VRPayment OrderDeliveryState could not be saved.","messageOrderDeliveryStateUpdated":"VRPayment OrderDeliveryState has been updated.","messagePaymentMethodConfigurationError":"VRPayment PaymentMethodConfiguration could not be saved. Please check your credentials.","messagePaymentMethodConfigurationUpdated":"VRPayment PaymentMethodConfiguration has been registered.","messageWebHookError":"VRPayment WebHook could not be saved. Please check your credentials.","messageWebHookUpdated":"VRPayment WebHook has been updated.","options":{"cardTitle":"Options","emailEnabled":{"label":"Send order confirmation email","tooltipText":"If this setting is enabled your customers will receive an email from your store when their order payment is authorised"},"integration":{"label":"Integration","options":{"iframe":"Iframe","payment_page":"Payment Page"},"tooltipText":"Integration"},"lineItemConsistencyEnabled":{"label":"Line item consistency","tooltipText":"If this option is enabled line item totals in VRPaymentPayment will always match Shopware order total"},"spaceViewId":{"label":"Space View ID","tooltipText":"Space View ID"}},"save":"Save","storefrontOptions":{"cardTitle":"Storefront Options","invoiceDownloadEnabled":{"label":"Invoice Download","tooltipText":"If this setting is enabled your customers will be able to download order invoices from VRPayment"}},"advancedOptions":{"cardTitle":"Advanced Options","webhooksUpdateEnabled":{"label":"Webhooks Update","tooltipText":"If this setting is enabled webhook update will be triggered when you save settings"},"paymentsUpdateEnabled":{"label":"Payments Update","tooltipText":"If this setting is enabled payment methods update will be triggered when you save settings"}},"titleError":"Error","titleSuccess":"Success"}}}'),Y=JSON.parse('{"sw-privileges":{"permissions":{"parents":{"vrpayment":"VRPayment brancher"},"vrpayment":{"label":"VRPayment autorisations"}}},"vrpayment-settings":{"general":{"descriptionTextModule":"Param\xe8tres de VRPayment","mainMenuItemGeneral":"VRPayment"},"header":"VRPayment","messageNotBlank":"Cette valeur ne doit pas \xeatre vide.","salesChannelCard":{"button":{"description":"Cliquez sur ce bouton pour d\xe9finir VRPayment comme gestionnaire de paiement par d\xe9faut dans le canal de vente s\xe9lectionn\xe9.","label":"D\xe9finir VRPayment comme gestionnaire de paiement par d\xe9faut"},"messageDefaultPaymentError":"VRPayment comme paiement par d\xe9faut n\'a pas pu \xeatre d\xe9fini.","messageDefaultPaymentUpdated":"VRPayment comme paiement par d\xe9faut a \xe9t\xe9 d\xe9fini."},"settingForm":{"credentials":{"applicationKey":{"label":"Application Key","tooltipText":"La cl\xe9 d\'application est utilis\xe9e pour authentifier ce plugin avec l\'API."},"cardTitle":"R\xe9f\xe9rences","spaceId":{"label":"Space ID","tooltipText":"L\'ID de l\'espace est utilis\xe9 pour authentifier ce plugin avec l\'API VRPayment.."},"userId":{"label":"User ID","tooltipText":"L\'ID utilisateur est utilis\xe9 pour authentifier ce plugin avec l\'API VRPayment."},"button":{"description":"Cliquez sur ce bouton pour tester l\'API VRPayment.","label":"Test de connexion \xe0 l\'API"},"alert":{"title":"Test API","successMessage":"La connexion a \xe9t\xe9 test\xe9e avec succ\xe8s.","errorMessage":"La connexion a \xe9chou\xe9. R\xe9essayez."}},"messageSaveSuccess":"Les param\xe8tres de VRPayment ont \xe9t\xe9 enregistr\xe9s.","messageOrderDeliveryStateError":"Les param\xe8tres de VRPayment OrderDeliveryState n\'ont pas pu \xeatre enregistr\xe9s.","messageOrderDeliveryStateUpdated":"VRPayment OrderDeliveryState a \xe9t\xe9 mis \xe0 jour.","messagePaymentMethodConfigurationError":"VRPayment PaymentMethodConfiguration n\'a pas pu \xeatre enregistr\xe9. Veuillez v\xe9rifier vos informations d\'identification.","messagePaymentMethodConfigurationUpdated":"VRPayment PaymentMethodConfiguration a \xe9t\xe9 enregistr\xe9.","messageWebHookError":"VRPayment WebHook n\'a pas pu \xeatre enregistr\xe9. Veuillez v\xe9rifier vos informations d\'identification.","messageWebHookUpdated":"VRPayment WebHook a \xe9t\xe9 mis \xe0 jour.","options":{"cardTitle":"Options","emailEnabled":{"label":"Envoyer un e-mail de confirmation de commande","tooltipText":"If this setting is enabled your customers will receive an email from your store when their order payment is authorised"},"integration":{"label":"Integration","options":{"iframe":"Iframe","payment_page":"Page de paiement"},"tooltipText":"Integration"},"lineItemConsistencyEnabled":{"label":"Coh\xe9rence des postes de ligne","tooltipText":"Si cette option est activ\xe9e, les totaux des articles dans VRPaymentPayment correspondront toujours au total de la commande Shopware."},"spaceViewId":{"label":"Space View ID","tooltipText":"Space View ID"}},"save":"Enregistrer","storefrontOptions":{"cardTitle":"Storefront Options","invoiceDownloadEnabled":{"label":"T\xe9l\xe9chargement de facture","tooltipText":"Si ce param\xe8tre est activ\xe9, vos clients pourront t\xe9l\xe9charger les factures de commande depuis VRPayment"}},"advancedOptions":{"cardTitle":"Options avanc\xe9es","webhooksUpdateEnabled":{"label":"Mise \xe0 jour des webhooks","tooltipText":"Si ce param\xe8tre est activ\xe9, la mise \xe0 jour des webhooks sera d\xe9clench\xe9e lorsque vous enregistrerez les param\xe8tres."},"paymentsUpdateEnabled":{"label":"Mise \xe0 jour des paiements","tooltipText":"Si ce param\xe8tre est activ\xe9, la mise \xe0 jour des m\xe9thodes de paiement sera d\xe9clench\xe9e lorsque vous enregistrez les param\xe8tres."}},"titleError":"Erreur","titleSuccess":"Succ\xe8s"}}}'),Z=JSON.parse('{"sw-privileges":{"permissions":{"parents":{"vrpayment":"VRPayment brancher"},"vrpayment":{"label":"VRPayment autorisations"}}},"vrpayment-settings":{"general":{"descriptionTextModule":"Impostazioni VRPayment","mainMenuItemGeneral":"VRPayment"},"header":"VRPayment","messageNotBlank":"Questo valore non dovrebbe essere vuoto.","salesChannelCard":{"button":{"description":"Fai clic su questo pulsante per impostare VRPayment come gestore di pagamento predefinito nel SalesChannel selezionato","label":"Imposta VRPayment come gestore di pagamento predefinito"},"messageDefaultPaymentError":"Non \xe8 stato possibile impostare VRPayment come pagamento predefinito.","messageDefaultPaymentUpdated":"VRPayment come pagamento predefinito \xe8 stato impostato."},"settingForm":{"credentials":{"applicationKey":{"label":"Chiave di applicazione","tooltipText":"La chiave dell\'applicazione \xe8 usata per autenticare questo plugin con l\'API VRPayment."},"cardTitle":"Credenziali","spaceId":{"label":"ID spazio","tooltipText":"L\'ID dello spazio \xe8 usato per autenticare questo plugin con l\'API VRPayment."},"userId":{"label":"ID utente","tooltipText":"L\'ID utente \xe8 usato per autenticare questo plugin con l\'API VRPayment."},"button":{"description":"Fare clic su questo pulsante per testare l\'API VRPayment.","label":"Test di connessione API"},"alert":{"title":"Test API","successMessage":"La connessione \xe8 stata testata con successo.","errorMessage":"La connessione \xe8 fallita. Riprovare."}},"messageSaveSuccess":"Le impostazioni di VRPayment sono state salvate.","messageOrderDeliveryStateError":"VRPayment OrderDeliveryState non pu\xf2 essere salvato.","messageOrderDeliveryStateUpdated":"VRPayment OrderDeliveryState \xe8 stato aggiornato.","messagePaymentMethodConfigurationError":"VRPayment PaymentMethodConfiguration non pu\xf2 essere salvato. Per favore controlla le tue credenziali.","messagePaymentMethodConfigurationUpdated":"VRPayment PaymentMethodConfiguration \xe8 stato registrato.","messageWebHookError":"VRPayment WebHook non pu\xf2 essere salvato. Per favore controlla le tue credenziali.","messageWebHookUpdated":"VRPayment WebHook \xe8 stato aggiornato.","options":{"cardTitle":"Opzioni","emailEnabled":{"label":"Invia email di conferma dell\'ordine","tooltipText":"Se questa impostazione \xe8 abilitata i tuoi clienti riceveranno un\'email dal tuo negozio quando il pagamento del loro ordine sar\xe0 autorizzato"},"integration":{"label":"Integrazione","options":{"iframe":"Iframe","payment_page":"Pagina di pagamento"},"tooltipText":"Integrazione"},"lineItemConsistencyEnabled":{"label":"Coerenza dell\'elemento linea","tooltipText":"Se questa opzione \xe8 abilitata i totali degli articoli in VRPaymentPayment corrisponderanno sempre al totale dell\'ordine Shopware"},"spaceViewId":{"label":"ID della vista spazio","tooltipText":"ID della vista spaziale"}},"save":"Salva","storefrontOptions":{"cardTitle":"Opzioni vetrina","invoiceDownloadEnabled":{"label":"Scaricamento fattura","tooltipText":"Se questa impostazione \xe8 abilitata i tuoi clienti potranno scaricare le fatture degli ordini da VRPayment"}},"advancedOptions":{"cardTitle":"Opzioni avanzate","webhooksUpdateEnabled":{"label":"Aggiornamento webhooks","tooltipText":"Se questa impostazione \xe8 abilitata l\'aggiornamento dei webhook sar\xe0 attivato quando si salvano le impostazioni"},"paymentsUpdateEnabled":{"label":"Aggiornamento pagamenti","tooltipText":"Se questa impostazione \xe8 abilitata l\'aggiornamento dei metodi di pagamento verr\xe0 attivato quando si salvano le impostazioni"}},"titleError":"Errore","titleSuccess":"Successo"}}}');let{Module:J}=Shopware;J.register("vrpayment-settings",{type:"plugin",name:"VRPayment",title:"vrpayment-settings.general.descriptionTextModule",description:"vrpayment-settings.general.descriptionTextModule",color:"#28d8ff",icon:"default-action-settings",version:"1.0.1",targetVersion:"1.0.1",snippets:{"de-DE":Q,"en-GB":j,"fr-FR":Y,"it-IT":Z},routes:{index:{component:"vrpayment-settings",path:"index",meta:{parentPath:"sw.settings.index",privilege:"vrpayment.viewer"},props:{default:e=>({hash:e.params.hash})}}},settingsItem:{group:"plugins",to:"vrpayment.settings.index",iconComponent:"sw-vrpayment-settings-icon",backgroundEnabled:!0,privilege:"vrpayment.viewer"}});let X=Shopware.Classes.ApiService;var ee=class extends X{constructor(e,t,n="vrpayment"){super(e,t,n)}registerWebHooks(e=null){let t=this.getBasicHeaders(),n=`${Shopware.Context.api.apiPath}/_action/${this.getApiBasePath()}/configuration/register-web-hooks`;return this.httpClient.post(n,{salesChannelId:e},{headers:t}).then(e=>X.handleResponse(e))}checkApiConnection(e=null,t=null,n=null){let a=this.getBasicHeaders(),i=`${Shopware.Context.api.apiPath}/_action/${this.getApiBasePath()}/configuration/check-api-connection`;return this.httpClient.post(i,{spaceId:e,userId:t,applicationId:n},{headers:a}).then(e=>X.handleResponse(e))}setVRPaymentAsSalesChannelPaymentDefault(e=null){let t=this.getBasicHeaders(),n=`${Shopware.Context.api.apiPath}/_action/${this.getApiBasePath()}/configuration/set-vrpayment-as-sales-channel-payment-default`;return this.httpClient.post(n,{salesChannelId:e},{headers:t}).then(e=>X.handleResponse(e))}synchronizePaymentMethodConfiguration(e=null){let t=this.getBasicHeaders(),n=`${Shopware.Context.api.apiPath}/_action/${this.getApiBasePath()}/configuration/synchronize-payment-method-configuration`;return this.httpClient.post(n,{salesChannelId:e},{headers:t}).then(e=>X.handleResponse(e))}installOrderDeliveryStates(){let e=this.getBasicHeaders(),t=`${Shopware.Context.api.apiPath}/_action/${this.getApiBasePath()}/configuration/install-order-delivery-states`;return this.httpClient.post(t,{},{headers:e}).then(e=>X.handleResponse(e))}};let et=Shopware.Classes.ApiService;var en=class extends et{constructor(e,t,n="vrpayment"){super(e,t,n)}createRefund(e,t,n,a){let i=this.getBasicHeaders(),r=`${Shopware.Context.api.apiPath}/_action/${this.getApiBasePath()}/refund/create-refund/`;return this.httpClient.post(r,{salesChannelId:e,transactionId:t,quantity:n,lineItemId:a},{headers:i}).then(e=>et.handleResponse(e))}createRefundByAmount(e,t,n){let a=this.getBasicHeaders(),i=`${Shopware.Context.api.apiPath}/_action/${this.getApiBasePath()}/refund/create-refund-by-amount/`;return this.httpClient.post(i,{salesChannelId:e,transactionId:t,refundableAmount:n},{headers:a}).then(e=>et.handleResponse(e))}createPartialRefund(e,t,n,a){let i=this.getBasicHeaders(),r=`${Shopware.Context.api.apiPath}/_action/${this.getApiBasePath()}/refund/create-partial-refund/`;return this.httpClient.post(r,{salesChannelId:e,transactionId:t,refundableAmount:n,lineItemId:a},{headers:i}).then(e=>et.handleResponse(e))}};let ea=Shopware.Classes.ApiService;var ei=class extends ea{constructor(e,t,n="vrpayment"){super(e,t,n)}getTransactionData(e,t){let n=this.getBasicHeaders(),a=`${Shopware.Context.api.apiPath}/_action/${this.getApiBasePath()}/transaction/get-transaction-data/`;return this.httpClient.post(a,{salesChannelId:e,transactionId:t},{headers:n}).then(e=>ea.handleResponse(e))}getInvoiceDocument(e,t){return`${Shopware.Context.api.apiPath}/_action/${this.getApiBasePath()}/transaction/get-invoice-document/${e}/${t}`}getPackingSlip(e,t){return`${Shopware.Context.api.apiPath}/_action/${this.getApiBasePath()}/transaction/get-packing-slip/${e}/${t}`}};let er=Shopware.Classes.ApiService;var es=class extends er{constructor(e,t,n="vrpayment"){super(e,t,n)}createTransactionCompletion(e,t){let n=this.getBasicHeaders(),a=`${Shopware.Context.api.apiPath}/_action/${this.getApiBasePath()}/transaction-completion/create-transaction-completion/`;return this.httpClient.post(a,{salesChannelId:e,transactionId:t},{headers:n}).then(e=>er.handleResponse(e))}};let eo=Shopware.Classes.ApiService;var el=class extends eo{constructor(e,t,n="vrpayment"){super(e,t,n)}createTransactionVoid(e,t){let n=this.getBasicHeaders(),a=`${Shopware.Context.api.apiPath}/_action/${this.getApiBasePath()}/transaction-void/create-transaction-void/`;return this.httpClient.post(a,{salesChannelId:e,transactionId:t},{headers:n}).then(e=>eo.handleResponse(e))}};let{Application:ec}=Shopware;ec.addServiceProvider("VRPaymentConfigurationService",e=>new ee(ec.getContainer("init").httpClient,e.loginService)),ec.addServiceProvider("VRPaymentRefundService",e=>new en(ec.getContainer("init").httpClient,e.loginService)),ec.addServiceProvider("VRPaymentTransactionService",e=>new ei(ec.getContainer("init").httpClient,e.loginService)),ec.addServiceProvider("VRPaymentTransactionCompletionService",e=>new es(ec.getContainer("init").httpClient,e.loginService)),ec.addServiceProvider("VRPaymentTransactionVoidService",e=>new el(ec.getContainer("init").httpClient,e.loginService))}()})(); \ No newline at end of file diff --git a/src/Resources/public/storefront/js/app.js b/src/Resources/public/storefront/js/app.js new file mode 100644 index 0000000..fc1c90c --- /dev/null +++ b/src/Resources/public/storefront/js/app.js @@ -0,0 +1,201 @@ +/* global window */ +// noinspection ThisExpressionReferencesGlobalObjectJS +(function (window) { + /** + * VRPaymentCheckout + * @type { + * { + * payment_method_handler_name: string, + * payment_method_iframe_class: string, + * init: init, + * validationCallBack: validationCallBack, + * payment_method_handler_status: string, + * submitPayment: (function(*): boolean), + * payment_method_iframe_prefix: string, + * payment_form_id: string, + * payment_method_handler_prefix: string, + * payment_method_tabs: string, + * getIframe: (function(): boolean + * } + * } + */ + const VRPaymentCheckout = { + /** + * Variables + */ + payment_panel_id: 'vrpayment-payment-panel', + payment_method_iframe_id: 'vrpayment-payment-iframe', + payment_method_handler_name: 'vrpayment_payment_handler', + payment_method_handler_status: 'input[name="vrpayment_payment_handler_validation_status"]', + payment_form_id: 'confirmOrderForm', + button_cancel_id: 'vrpaymentOrderCancel', + button_home_override: 'vrpaymentHomeLink', + loader_id: 'vrpaymentLoader', + checkout_url: null, + checkout_url_id: 'checkoutUrl', + cart_recreate_url: null, + cart_recreate_url_id: 'cartRecreateUrl', + handler: null, + + /** + * Initialize plugin + */ + init: function () { + VRPaymentCheckout.activateLoader(true); + this.checkout_url = document.getElementById(this.checkout_url_id).value; + this.cart_recreate_url = document.getElementById(this.cart_recreate_url_id).value; + + document.getElementById(this.button_cancel_id).addEventListener('click', this.recreateCart, false); + document.getElementById(this.button_home_override).addEventListener('click', this.recreateCart, false); + document.getElementById(this.payment_form_id).addEventListener('submit', this.submitPayment, false); + + VRPaymentCheckout.getIframe(); + }, + + activateLoader: function (activate) { + const buttons = document.querySelectorAll('button'); + if (activate) { + for (let i = 0; i < buttons.length; i++) { + buttons[i].disabled = true; + } + } else { + for (let i = 0; i < buttons.length; i++) { + buttons[i].disabled = false; + } + } + }, + + hideLoader: function () { + const loader = document.getElementById(VRPaymentCheckout.loader_id); + if (loader !== null && loader.parentNode !== null) { + loader.parentNode.removeChild(loader); + } + VRPaymentCheckout.activateLoader(false); + }, + + recreateCart: function (e) { + window.location.href = VRPaymentCheckout.cart_recreate_url; + e.preventDefault(); + }, + + /** + * Submit form + * + * @param event + * @return {boolean} + */ + submitPayment: function (event) { + VRPaymentCheckout.activateLoader(true); + VRPaymentCheckout.handler.validate(); + event.preventDefault(); + return false; + }, + + /** + * Get iframe + */ + getIframe: function () { + const paymentPanel = document.getElementById(VRPaymentCheckout.payment_panel_id); + const paymentMethodConfigurationId = paymentPanel.dataset.id; + const iframeContainer = document.getElementById(VRPaymentCheckout.payment_method_iframe_id); + + if (!VRPaymentCheckout.handler) { // iframe has not been loaded yet + // noinspection JSUnresolvedFunction + VRPaymentCheckout.handler = window.IframeCheckoutHandler(paymentMethodConfigurationId); + // noinspection JSUnresolvedFunction + VRPaymentCheckout.handler.setValidationCallback(function(validationResult){ + VRPaymentCheckout.hideErrors(); + VRPaymentCheckout.validationCallBack(validationResult); + }); + VRPaymentCheckout.handler.setInitializeCallback(VRPaymentCheckout.hideLoader()); + VRPaymentCheckout.handler.setHeightChangeCallback(function(height){ + if(height < 1){ // iframe has no fields + VRPaymentCheckout.handler.submit(); + } + }); + VRPaymentCheckout.handler.create(iframeContainer); + setTimeout(VRPaymentCheckout.hideLoader(), 10000); + + } + }, + + /** + * validation callback + * @param validationResult + */ + validationCallBack: function (validationResult) { + if (validationResult.success) { + document.querySelector(this.payment_method_handler_status).value = true; + VRPaymentCheckout.handler.submit(); + } else { + document.body.scrollTop = 0; + document.documentElement.scrollTop = 0; + + if (validationResult.errors) { + VRPaymentCheckout.showErrors(validationResult.errors); + } + document.querySelector(this.payment_method_handler_status).value = false; + VRPaymentCheckout.activateLoader(false); + } + }, + + showErrors: function(errors) { + let alert = document.createElement('div'); + alert.setAttribute('class', 'alert alert-danger'); + alert.setAttribute('role', 'alert'); + alert.setAttribute('id', 'vrpayment-errors'); + document.getElementsByClassName('flashbags')[0].appendChild(alert); + + let alertContentContainer = document.createElement('div'); + alertContentContainer.setAttribute('class', 'alert-content-container'); + alert.appendChild(alertContentContainer); + + let alertContent = document.createElement('div'); + alertContent.setAttribute('class', 'alert-content'); + alertContentContainer.appendChild(alertContent); + + if (errors.length > 1) { + let alertList = document.createElement('ul'); + alertList.setAttribute('class', 'alert-list'); + alertContent.appendChild(alertList); + for (let index = 0; index < errors.length; index++) { + let alertListItem = document.createElement('li'); + alertListItem.innerHTML = errors[index]; + alertList.appendChild(alertListItem); + } + } else { + alertContent.innerHTML = errors[0]; + } + }, + + hideErrors: function() { + let errorElement = document.getElementById('vrpayment-errors'); + if (errorElement) { + errorElement.parentNode.removeChild(errorElement); + } + } + }; + + window.VRPaymentCheckout = VRPaymentCheckout; + +}(typeof window !== "undefined" ? window : this)); + +/** + * Vanilla JS over JQuery + */ +window.addEventListener('load', function (e) { + VRPaymentCheckout.init(); + window.history.pushState({}, document.title, VRPaymentCheckout.cart_recreate_url); + window.history.pushState({}, document.title, VRPaymentCheckout.checkout_url); +}, false); + +/** + * This only works if the user has interacted with the page + * @link https://stackoverflow.com/questions/57339098/chrome-popstate-not-firing-on-back-button-if-no-user-interaction + */ +window.addEventListener('popstate', function (e) { + if (window.history.state == null) { // This means it's page load + return; + } + window.location.href = VRPaymentCheckout.cart_recreate_url; +}, false); diff --git a/src/Resources/snippet/storefront/vrpayment.de-DE.json b/src/Resources/snippet/storefront/vrpayment.de-DE.json new file mode 100644 index 0000000..07308e5 --- /dev/null +++ b/src/Resources/snippet/storefront/vrpayment.de-DE.json @@ -0,0 +1,27 @@ +{ + "vrpayment": { + "account": { + "downloadInvoice": "Rechnung herunterladen" + }, + "cookie": { + "name": "VRPayment-Zahlungen" + }, + "deliveryState": { + "hold": "Halten", + "unhold": "Aufheben" + }, + "payButton": "Zahlen", + "payHeader": "Bestellung bezahlen", + "payload": { + "adjustmentLineItem": "Anpassung Einzelposten", + "shipping": { + "lineItem": "Versand", + "name": "Versand" + }, + "taxes": "Steuern" + }, + "paymentMethod": { + "notAvailable": "Die Zahlungsmethode ist derzeit nicht verfügbar. Bitte wählen Sie eine andere Zahlungsmethode." + } + } +} \ No newline at end of file diff --git a/src/Resources/snippet/storefront/vrpayment.en-GB.json b/src/Resources/snippet/storefront/vrpayment.en-GB.json new file mode 100644 index 0000000..fbbe547 --- /dev/null +++ b/src/Resources/snippet/storefront/vrpayment.en-GB.json @@ -0,0 +1,27 @@ +{ + "vrpayment": { + "account": { + "downloadInvoice": "Download Invoice" + }, + "cookie": { + "name": "VRPayment Payment" + }, + "deliveryState": { + "hold": "Hold", + "unhold": "Unhold" + }, + "payButton": "Pay", + "payHeader": "Pay order", + "payload": { + "adjustmentLineItem": "Adjustment Line Item", + "shipping": { + "lineItem": "Shipping", + "name": "Shipping" + }, + "taxes": "Taxes" + }, + "paymentMethod": { + "notAvailable": "Payment method is not currently available. Please choose another payment method." + } + } +} \ No newline at end of file diff --git a/src/Resources/snippet/storefront/vrpayment.fr-FR.json b/src/Resources/snippet/storefront/vrpayment.fr-FR.json new file mode 100644 index 0000000..6840863 --- /dev/null +++ b/src/Resources/snippet/storefront/vrpayment.fr-FR.json @@ -0,0 +1,27 @@ +{ + "vrpayment": { + "account": { + "downloadInvoice": "Télécharger la facture" + }, + "cookie": { + "name": "VRPayment Paiement" + }, + "deliveryState": { + "hold": "Tenir", + "unhold": "Détacher" + }, + "payButton": "Payez", + "payHeader": "Commande de paiement", + "payload": { + "adjustmentLineItem": "Poste d'ajustement", + "shipping": { + "lineItem": "Expédition", + "name": "Expédition" + }, + "taxes": "Taxes" + }, + "paymentMethod": { + "notAvailable": "Le mode de paiement n'est pas disponible actuellement. Veuillez choisir un autre mode de paiement." + } + } +} \ No newline at end of file diff --git a/src/Resources/snippet/storefront/vrpayment.it-IT.json b/src/Resources/snippet/storefront/vrpayment.it-IT.json new file mode 100644 index 0000000..d214335 --- /dev/null +++ b/src/Resources/snippet/storefront/vrpayment.it-IT.json @@ -0,0 +1,27 @@ +{ + "vrpayment": { + "account": { + "downloadInvoice": "Scaricare la fattura" + }, + "cookie": { + "name": "Pagamento VRPayment" + }, + "deliveryState": { + "hold": "Tenere", + "unhold": "Aprite" + }, + "payButton": "Paga", + "payHeader": "Ordine di pagamento", + "payload": { + "adjustmentLineItem": "Voce di aggiustamento", + "shipping": { + "lineItem": "Spedizione", + "name": "Spedizione" + }, + "taxes": "Tasse" + }, + "paymentMethod": { + "notAvailable": "Il metodo di pagamento non è attualmente disponibile. Si prega di scegliere un altro metodo di pagamento." + } + } +} diff --git a/src/Resources/views/storefront/page/account/order-history/order-item.html.twig b/src/Resources/views/storefront/page/account/order-history/order-item.html.twig new file mode 100644 index 0000000..d77c25d --- /dev/null +++ b/src/Resources/views/storefront/page/account/order-history/order-item.html.twig @@ -0,0 +1,18 @@ +{% sw_extends '@Storefront/storefront/page/account/order-history/order-item.html.twig' %} +{% block page_account_order_item_context_menu_content %} + {{ parent() }} + {% block vrpayment_page_account_order_item_context_menu_content %} + {% if page.extensions.vrpaymentSettings and page.extensions.vrpaymentSettings.storefrontInvoiceDownloadEnabled %} + {% set vrpaymentFormattedHandlerIdentifier = 'handler_vrpaymentpayment_vrpaymentpaymenthandler' %} + {% set orderPaymentState = order.transactions.last.stateMachineState.technicalName %} + {% set orderPaymentMethodFormattedHandlerIdentifier = order.transactions.last.paymentMethod.formattedHandlerIdentifier %} + {% if (vrpaymentFormattedHandlerIdentifier == orderPaymentMethodFormattedHandlerIdentifier) and (orderPaymentState in ['paid', 'refunded']) %} + + {{ "vrpayment.account.downloadInvoice"|trans|sw_sanitize }} + + {% endif %} + {% endif %} + {% endblock %} +{% endblock %} \ No newline at end of file diff --git a/src/Resources/views/storefront/page/checkout/order/vrpayment.html.twig b/src/Resources/views/storefront/page/checkout/order/vrpayment.html.twig new file mode 100644 index 0000000..5e6c15b --- /dev/null +++ b/src/Resources/views/storefront/page/checkout/order/vrpayment.html.twig @@ -0,0 +1,162 @@ +{% sw_extends '@Storefront/storefront/page/checkout/_page.html.twig' %} + +{% block base_header %} + {% sw_include '@VRPaymentPayment/storefront/page/checkout/order/vrpayment_header.html.twig' %} +{% endblock %} + +{% block base_navigation %}{% endblock %} +{% block base_body_classes %}vrpayment-payment is-act-confirmpage{% endblock %} + +{% block page_checkout_main_content %} + {% block page_checkout_pay %} + {% block page_checkout_confirm_header %} +

+ {{ "vrpayment.payHeader"|trans|sw_sanitize }} +

+ {% endblock %} + + {# TODO: move this into a separate file #} + {% block page_checkout_confirm_address %} +
+ {% block page_checkout_confirm_address_shipping %} + {% if page.cart is defined %} + {% set lineItems = page.cart.lineItems %} + {% endif %} + {% if page.order is defined %} + {% set lineItems = page.order.lineItems %} + {% endif %} + {% if not page.isHideShippingAddress() %} +
+
+
+ {% block page_checkout_confirm_address_shipping_title %} +
+ {{ "checkout.shippingAddressHeader"|trans|sw_sanitize }} +
+ {% endblock %} + + {% block page_checkout_confirm_address_shipping_data %} +
+ {% sw_include '@Storefront/storefront/component/address/address.html.twig' with { + 'address': context.customer.defaultShippingAddress + } %} +
+ {% endblock %} + + {% block page_checkout_confirm_address_shipping_actions %} +
+ {% set addressEditorOptions = { + changeShipping: true, + addressId: context.customer.defaultShippingAddressId, + } %} +
+ {% endblock %} +
+
+
+ {% endif %} + {% endblock %} + + {% block page_checkout_confirm_address_billing %} +
+
+
+ {% block page_checkout_confirm_address_billing_title %} +
+ {{ "checkout.billingAddressHeader"|trans|sw_sanitize }} +
+ {% endblock %} + + {% block page_checkout_confirm_address_billing_data %} +
+ {% set shippingAddress = context.customer.activeShippingAddress %} + {% set billingAddress = context.customer.activeBillingAddress %} + {% if shippingAddress.id is defined and shippingAddress.id is same as(billingAddress.id) %} + {% block page_checkout_confirm_address_billing_data_equal %} +

+ {{ "checkout.addressEqualText"|trans|sw_sanitize }} +

+ {% endblock %} + {% else %} + {% sw_include '@Storefront/storefront/component/address/address.html.twig' with { + 'address': context.customer.defaultBillingAddress + } %} + {% endif %} +
+ {% endblock %} + + {% block page_checkout_confirm_address_billing_actions %} +
+ {% set addressEditorOptions = { + changeBilling: true, + addressId: context.customer.defaultBillingAddressId, + } %} +
+ {% endblock %} +
+
+
+ {% endblock %} +
+ {% endblock %} + + {% block page_checkout_pay_order_form %} +
+
+ {% sw_include '@VRPaymentPayment/storefront/page/checkout/order/vrpayment_payment.html.twig' %} +
+
+ {% endblock %} + + {% block page_checkout_pay_product_table %} +
+
+ {% block page_checkout_pay_table_header %} + {% sw_include '@Storefront/storefront/component/checkout/cart-header.html.twig' %} + {% endblock %} + + {% block page_checkout_pay_items %} + {% for lineItem in page.order.nestedLineItems %} + {% block page_checkout_pay_item %} + {% sw_include '@Storefront/storefront/component/line-item/line-item.html.twig' %} + {% endblock %} + {% endfor %} + {% endblock %} +
+
+ {% endblock %} + {% endblock %} +{% endblock %} + +{% block page_checkout_aside_actions %} +
+
+ + + +
+
+{% endblock %} + +{% block base_footer %} + {% sw_include '@Storefront/storefront/layout/footer/footer-minimal.html.twig' %} +{% endblock %} + +{% block base_body_script %} + {{ parent() }} + {% if page.extensions.vRPaymentData %} + {% if page.extensions.vRPaymentData.deviceJavascriptUrl %} + + {% endif %} + {% if page.extensions.vRPaymentData.javascriptUrl %} + + {% endif %} + + {% endif %} +{% endblock %} diff --git a/src/Resources/views/storefront/page/checkout/order/vrpayment_header.html.twig b/src/Resources/views/storefront/page/checkout/order/vrpayment_header.html.twig new file mode 100644 index 0000000..4d958c1 --- /dev/null +++ b/src/Resources/views/storefront/page/checkout/order/vrpayment_header.html.twig @@ -0,0 +1,53 @@ +{% sw_extends '@Storefront/storefront/layout/header/header-minimal.html.twig' %} + +{% block layout_header_minimal_logo %} + +{% endblock %} + +{% block layout_header_minimal_button %} +
+ +
+{% endblock %} \ No newline at end of file diff --git a/src/Resources/views/storefront/page/checkout/order/vrpayment_payment.html.twig b/src/Resources/views/storefront/page/checkout/order/vrpayment_payment.html.twig new file mode 100644 index 0000000..820ae9f --- /dev/null +++ b/src/Resources/views/storefront/page/checkout/order/vrpayment_payment.html.twig @@ -0,0 +1,24 @@ +{% set vRPaymentData = page.extensions.vRPaymentData %} +{% if vRPaymentData and vRPaymentData.integration %} + {% if vRPaymentData.integration in ['iframe'] %} + {% for transactionPossiblePaymentMethod in vRPaymentData.transactionPossiblePaymentMethods %} +
+
+
+ {{ transactionPossiblePaymentMethod.getName() }} +
+
+
+ +
+
+
+
+ {% endfor %} + {% endif %} +{% endif %} \ No newline at end of file diff --git a/src/VRPaymentPayment.php b/src/VRPaymentPayment.php new file mode 100644 index 0000000..e305188 --- /dev/null +++ b/src/VRPaymentPayment.php @@ -0,0 +1,136 @@ +disablePaymentMethods($uninstallContext->getContext()); + $this->removeConfiguration($uninstallContext->getContext()); + $this->deleteUserData($uninstallContext); + } + + /** + * @param \Shopware\Core\Framework\Plugin\Context\ActivateContext $activateContext + * @return void + */ + public function activate(ActivateContext $activateContext): void + { + parent::activate($activateContext); + $this->enablePaymentMethods($activateContext->getContext()); + } + + /** + * @param \Shopware\Core\Framework\Plugin\Context\DeactivateContext $deactivateContext + * @return void + */ + public function deactivate(DeactivateContext $deactivateContext): void + { + parent::deactivate($deactivateContext); + $this->disablePaymentMethods($deactivateContext->getContext()); + } + + public function build(ContainerBuilder $container): void + { + parent::build($container); + + $locator = new FileLocator('Resources/config'); + + $resolver = new LoaderResolver([ + new YamlFileLoader($container, $locator), + new GlobFileLoader($container, $locator), + new DirectoryLoader($container, $locator), + ]); + + $configLoader = new DelegatingLoader($resolver); + + $confDir = \rtrim($this->getPath(), '/') . '/Resources/config'; + + $configLoader->load($confDir . '/{packages}/*.yaml', 'glob'); + } + + public function enrichPrivileges(): array + { + return [ + 'sales_channel.viewer' => [ + self::VRPAYMENT_SALES_CHANNEL_PRIVILEGE_READ, + self::VRPAYMENT_SALES_CHANNEL_PRIVILEGE_RUN_READ, + self::VRPAYMENT_SALES_CHANNEL_PRIVILEGE_RUN_UPDATE, + self::VRPAYMENT_SALES_CHANNEL_PRIVILEGE_RUN_CREATE, + self::VRPAYMENT_SALES_CHANNEL_PRIVILEGE_RUN_LOG_READ, + 'sales_channel_payment_method:read', + ], + 'sales_channel.editor' => [ + self::VRPAYMENT_SALES_CHANNEL_PRIVILEGE_UPDATE, + self::VRPAYMENT_SALES_CHANNEL_PRIVILEGE_RUN_DELETE, + 'payment_method:update', + ], + 'sales_channel.creator' => [ + self::VRPAYMENT_SALES_CHANNEL_PRIVILEGE_CREATE, + 'payment_method:create', + 'shipping_method:create', + 'delivery_time:create', + ], + 'sales_channel.deleter' => [ + self::VRPAYMENT_SALES_CHANNEL_PRIVILEGE_DELETE, + ], + ]; + } + + /** + * {@inheritdoc} + */ + public function executeComposerCommands(): bool + { + // The plugin needs the SDK to be installed via composer. + return true; + } +}