Compare commits

...

9 Commits

Author SHA1 Message Date
andrewrowanwallee e732454683 Release 6.2.1 2026-02-12 13:13:32 +01:00
andrewrowanwallee 57246e23ce Release 6.2.0 2026-01-15 09:20:03 +01:00
andrewrowanwallee 68592a9409 Release 6.1.17 2025-11-03 13:29:47 +01:00
andrewrowanwallee 1393f4ff7c Release 6.1.16 2025-10-01 10:15:25 +02:00
andrewrowanwallee d714cf2f84 release 6.1.15 2025-09-17 12:11:32 +02:00
andrewrowanwallee 2f9a30ebd3 Release 6.1.14 2025-06-11 11:16:50 +02:00
andrewrowanwallee 2f4d38b4b2 Release 6.1.13 2025-05-27 11:43:52 +02:00
andrewrowanwallee 2cba2a8f3e Release 6.1.12 2025-03-26 11:59:32 +01:00
andrewrowanwallee 3f291ef7ea Release 6.1.12 2025-03-05 12:03:51 +01:00
74 changed files with 8215 additions and 945 deletions
+34 -1
View File
@@ -1,3 +1,36 @@
# 6.2.1
- Fixed issue with multiple discount codes
# 6.2.0
- Renamed database table to avoid a naming conflict with legacy plugins
- Fixed issue with refunds failing for payments using Invoice
# 6.1.17
- Sales channels now support different spaces
- Upgraded SDK to include latest fallback CA Bundle
- Fixed error screen when returning from portal on failed payment
# 6.1.16
- Fixed issue with pending orders remaining open
# 6.1.15
- Fixed issue with shipping costs not being processed correctly
# 6.1.14
- Disable Recreate Cart for Headless Storefront Order
- Added the correct Exception Type to the finalize method
# 6.1.13
- Updated English documentation
- Added French, German and Italian documentation
# 6.1.12
- Compatibility with 6.6.10.x
- Prevent duplicate transactions being created when the first times out
- Fix for error when changing space credentials
- Payment status now shows refunded/partially refunded
- Order delivery status now shows 'open' instead of 'hold'
# 6.1.11 # 6.1.11
- Implement payment page integration. - Implement payment page integration.
- Fixed bug with duplicate payment methods appearing - Fixed bug with duplicate payment methods appearing
@@ -141,7 +174,7 @@
- Added settings to control update of webhooks and payment methods - Added settings to control update of webhooks and payment methods
# 4.0.15 # 4.0.15
- Adjust VRPay/SW6 documentation - how to do refunds - Adjust VR Payment/SW6 documentation - how to do refunds
# 4.0.14 # 4.0.14
- Support for Shopware 6.4.6 - Support for Shopware 6.4.6
+34 -1
View File
@@ -1,3 +1,36 @@
# 6.2.1
- Problem mit mehreren Rabattcodes behoben
# 6.2.0
- Datenbanktabelle umbenannt, um Namenskonflikte mit älteren Plugins zu vermeiden.
- Problem mit fehlgeschlagenen Rückerstattungen bei Zahlungen mit Rechnungen behoben.
# 6.1.17
- Vertriebskanäle unterstützen jetzt verschiedene Bereiche
- SDK aktualisiert und enthält nun das neueste CA-Fallback-Bundle
- Fehlerbildschirm beim Zurückkehren vom Portal nach fehlgeschlagener Zahlung behoben
# 6.1.16
- Problem behoben, bei dem die Versandkosten nicht korrekt verarbeitet wurden
# 6.1.15
- Problem behoben, bei dem ausstehende Bestellungen offen blieben
# 6.1.14
Warenkorb neu erstellen für Headless Storefront Order deaktivieren
Der korrekte Ausnahmetyp wurde zur Finalisierungsmethode hinzugefügt
# 6.1.13
Englische Dokumentation aktualisiert
Französische, deutsche und italienische Dokumentation hinzugefügt
# 6.1.12
- Kompatibilität mit 6.6.10.x
- Verhindern Sie, dass beim ersten Timeout doppelte Transaktionen erstellt werden
- Fehler beim Ändern der Space-Anmeldeinformationen behoben
- Der Zahlungsstatus zeigt jetzt „erstattet/teilweise erstattet“ an
- Der Lieferstatus der Bestellung wird jetzt „Offen“ statt „Halten“ angezeigt.
# 6.1.11 # 6.1.11
- Integration der Zahlungsseite implementieren. - Integration der Zahlungsseite implementieren.
- Fehler mit doppelten angezeigten Zahlungsmethoden behoben - Fehler mit doppelten angezeigten Zahlungsmethoden behoben
@@ -139,7 +172,7 @@
- Einstellungen zur Steuerung der Aktualisierung von Webhooks und Zahlungsmethoden hinzugefügt - Einstellungen zur Steuerung der Aktualisierung von Webhooks und Zahlungsmethoden hinzugefügt
# 4.0.15 # 4.0.15
- VRPay/SW6-Dokumentation anpassen wie man Rückerstattungen durchführt - VR Payment/SW6-Dokumentation anpassen wie man Rückerstattungen durchführt
# 4.0.14 # 4.0.14
- Unterstützung für Shopware 6.4.6 - Unterstützung für Shopware 6.4.6
+1 -1
View File
@@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier same "printed page" as the copyright notice for easier
identification within third-party archives. identification within third-party archives.
Copyright 2025 VR Payment GmbH Copyright 2026 VR Payment GmbH
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
+95 -54
View File
@@ -1,17 +1,83 @@
VRPayment Payment for Shopware 6 VR Payment Integration for Shopware 6
============================= =============================
The VRPayment Payment plugin wraps around the VRPayment API. This library facilitates your interaction with various services such as transactions. ## **Overview**
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). The VR Payment Payment Plugin integrates modern payment processing into Shopware 6, offering features like iFrame-based payments, refunds, captures, and PCI compliance. It supports seamless integration with the [VR Payment Portal](https://gateway.vr-payment.de/) for managing transactions and payment methods.
## Requirements ## Requirements
- Shopware 6.5.x or Shopware 6.6.x. See table below. - **Shopware Version:** 6.5.x or 6.6.x (see [compatibility table](#compatibility)).
- PHP minimum version supported by the each shop version. - **PHP:** Minimum version as required by your Shopware installation (e.g., 7.4+).
- **VR Payment Account:** Obtain `Space ID`, `User ID`, and `API Key` from the [VR Payment Dashboard](https://gateway.vr-payment.de/).
## Supported versions ## Documentation
- For English documentation click [here](https://docs.plugin-documentation.vr-payment.de/vr-payment/shopware-6/6.2.1/docs/en/documentation.html)
- Für die deutsche Dokumentation klicken Sie [hier](https://docs.plugin-documentation.vr-payment.de/vr-payment/shopware-6/6.2.1/docs/de/documentation.html)
- Pour la documentation Française, cliquez [ici](https://docs.plugin-documentation.vr-payment.de/vr-payment/shopware-6/6.2.1/docs/fr/documentation.html)
- Per la documentazione in tedesco, clicca [qui](https://docs.plugin-documentation.vr-payment.de/vr-payment/shopware-6/6.2.1/docs/it/documentation.html)
## Installation
### **Via Composer (Recommended)**
1. Navigate to your Shopware root directory.
2. Run:
```bash
Copy
composer require vrpayment/shopware-6
php bin/console plugin:refresh
php bin/console plugin:install --activate --clearCache VRPaymentPayment
```
### Manual Installation
1. Download the latest [Release](../../releases)
2. Extract the ZIP to custom/plugins/VRPaymentPayment.
```bash
Copy
bin/console plugin:refresh
bin/console plugin:install --activate --clearCache VRPaymentPayment
```
## Update
### Via Administration
1. Go to Shopware Admin > Extensions > My extensions.
2. Find VRPaymentPayment.
3. Click Update.
### Via CLI
1. Deploy the new plugin files (replace the folder in custom/plugins/VRPaymentPayment or upload/install a new ZIP).
2. Run:
```bash
bin/console plugin:refresh
bin/console plugin:update --clearCache VRPaymentPayment
bin/console cache:clear
```
## Configuration
### API Credentials
1. Navigate to Shopware Admin > Settings > VRPayment Payment.
2. Enter your Space ID, User ID, and API Key (obtained from the [VR Payment Portal](https://gateway.vr-payment.de/)).
### Payment Methods
Configure supported methods (e.g., credit cards, Apple Pay) via the [VR Payment Portal](https://gateway.vr-payment.de/).
### Key Features
**iFrame Integration**: Embed payment forms directly into your checkout.
**Refunds & Captures**: Trigger full/partial refunds and captures from Shopware or the [VR Payment Portal](https://gateway.vr-payment.de/).
**Multi-Store Support**: Manage configurations across multiple stores.
**Automatic Updates**: Payment methods sync dynamically via the VRPayment API.
## Compatibiliity
___________________________________________________________________________________ ___________________________________________________________________________________
| Shopware 6 version | Plugin major version | Supported until | | Shopware 6 version | Plugin major version | Supported until |
@@ -20,60 +86,35 @@ ________________________________________________________________________________
| Shopware 6.5.x | 5.x | October 2024 | | Shopware 6.5.x | 5.x | October 2024 |
----------------------------------------------------------------------------------- -----------------------------------------------------------------------------------
## Installation ### Troubleshooting
**Logs**: Check payment logs with:
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 ```bash
composer require vrpayment/shopware-6 Copy
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 tail -f var/log/vrpayment_payment*.log
``` ```
### Common Issues:
## Documentation Ensure composer update vrpayment/shopware-6 is run after updates.
[Documentation](https://gateway.vr-payment.de/doc/shopware-6/6.1.11/docs/en/documentation.html) Verify API credentials match your VRPayment account.
## FAQs
**Q: Does this plugin support one-click payments?**
A: Yes, via tokenization in the VRPayment Portal.
**Q: How do I handle PCI compliance?**
A: The plugin uses iFrame integration, reducing PCI requirements to SAQ-A.
### Changelog
For version-specific updates, see the [GitHub Releases](https://github.com/vr-payment/shopware-6/releases).
### Contributing
Report issues via GitHub Issues.
Follow the Shopware Plugin Base Guide for development.
This template combines technical clarity with user-friendly guidance. For advanced customization (e.g., overriding templates or payment handlers), refer to the Shopware Documentation.
## License ## License
+61 -61
View File
@@ -1,63 +1,63 @@
{ {
"authors": [ "authors": [
{ {
"homepage": "https://www.vr-payment.de/", "homepage": "https://www.vr-payment.de/",
"name": "VRPay" "name": "VR Payment"
} }
], ],
"autoload": { "autoload": {
"psr-4": { "psr-4": {
"VRPaymentPayment\\": "src/" "VRPaymentPayment\\": "src/"
} }
}, },
"description": "VRPayment integration for Shopware 6", "description": "VRPayment integration for Shopware 6",
"extra": { "extra": {
"copyright": "(c) by VRPay", "copyright": "(c) by VR Payment",
"description": { "description": {
"de-DE": "VRPayment integration f\u00fcr Shopware 6", "de-DE": "VRPayment integration für Shopware 6",
"en-GB": "VRPayment integration for Shopware 6", "en-GB": "VRPayment integration for Shopware 6",
"fr-FR": "Int\u00e9gration de VRPayment pour Shopware 6", "fr-FR": "Intégration de VRPayment pour Shopware 6",
"it-IT": "Integrazione VRPayment per Shopware" "it-IT": "Integrazione VRPayment per Shopware"
}, },
"label": { "label": {
"de-DE": "VRPayment Produkte f\u00fcr Shopware 6", "de-DE": "VRPayment Produkte für Shopware 6",
"en-GB": "VRPayment Products for Shopware 6", "en-GB": "VRPayment Products for Shopware 6",
"fr-FR": "VRPayment Produits for Shopware 6", "fr-FR": "VRPayment Produits for Shopware 6",
"it-IT": "VRPayment Prodotti per Shopware 6" "it-IT": "VRPayment Prodotti per Shopware 6"
}, },
"manufacturerLink": { "manufacturerLink": {
"de-DE": "https://www.vr-payment.de/", "de-DE": "https://www.vr-payment.de/",
"en-GB": "https://www.vr-payment.de/", "en-GB": "https://www.vr-payment.de/",
"fr-FR": "https://www.vr-payment.de/", "fr-FR": "https://www.vr-payment.de/",
"it-IT": "https://www.vr-payment.de/" "it-IT": "https://www.vr-payment.de/"
}, },
"supportLink": { "supportLink": {
"de-DE": "https://www.vr-payment.de/hotline", "de-DE": "https://www.vr-payment.de/hotline",
"en-GB": "https://www.vr-payment.de/hotline", "en-GB": "https://www.vr-payment.de/hotline",
"fr-FR": "https://www.vr-payment.de/hotline", "fr-FR": "https://www.vr-payment.de/hotline",
"it-IT": "https://www.vr-payment.de/hotline" "it-IT": "https://www.vr-payment.de/hotline"
}, },
"shopware-plugin-class": "VRPaymentPayment\\VRPaymentPayment" "shopware-plugin-class": "VRPaymentPayment\\VRPaymentPayment"
}, },
"homepage": "https://www.vr-payment.de//", "homepage": "https://www.vr-payment.de//",
"keywords": [ "keywords": [
"VRPay", "VR Payment",
"payment", "payment",
"php", "php",
"shopware" "shopware"
], ],
"license": "Apache-2.0", "license": "Apache-2.0",
"name": "vrpayment/shopware-6", "name": "vrpayment/shopware-6",
"require": { "require": {
"ext-curl": "*", "ext-curl": "*",
"ext-json": "*", "ext-json": "*",
"ext-mbstring": "*", "ext-mbstring": "*",
"php": ">=8.2", "php": ">=8.2",
"shopware/core": "6.6.*", "shopware/core": "~6.6.0",
"shopware/administration": "~6.6.0", "shopware/administration": "~6.6.0",
"shopware/storefront": "6.6.*", "shopware/storefront":"~6.6.0",
"vrpayment/sdk": "4.6.0" "vrpayment/sdk": "^4.0.0"
}, },
"type": "shopware-platform-plugin", "type": "shopware-platform-plugin",
"version": "6.1.11" "version": "6.2.1"
} }
+692
View File
@@ -0,0 +1,692 @@
* {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
*:before, *:after {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
html {
font-size: 100%;
font-family: sans-serif;
line-height: 1.15;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
-ms-overflow-style: scrollbar;
-webkit-tap-highlight-color: transparent;
}
@-ms-viewport {
width: device-width;
}
article, aside, dialog, figcaption, figure, footer, header, hgroup, main, nav, section {
display: block;
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
font-size: 1rem;
font-weight: 300;
line-height: 1.5;
color: #212529;
text-align: left;
background-color: #fff;
padding-right: 0 !important;
position: relative;
}
html,body {
width: 100%;
height: 100%;
}
[tabindex="-1"]:focus {
outline: 0 !important;
}
hr {
box-sizing: content-box;
height: 0;
overflow: visible;
}
h1, h2, h3, h4, h5, h6 {
margin-top: 0;
margin-bottom: 0.5rem;
}
h1 {
font-size: 2.5rem;
font-weight: 200;
margin-bottom: 1.875rem;
}
h2 {
font-size: 1.625rem;
font-weight: 300;
margin-bottom: 1.3rem;
}
h3 {
font-size: 1.3rem;
font-weight: 300;
margin-top: 1.3rem;
}
h4 {
font-size: 1.125rem;
font-weight: 400;
margin-top: 1.875rem;
margin-bottom: 1.3rem;
}
h5 {
font-size: 1rem;
font-weight: bold;
margin-top: 1.875rem;
margin-bottom: 1.3rem;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
abbr[title], abbr[data-original-title] {
text-decoration: underline;
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
cursor: help;
border-bottom: 0;
}
address {
margin-bottom: 1rem;
font-style: normal;
line-height: inherit;
}
ol, ul, dl {
margin-top: 0;
margin-bottom: 1rem;
}
ol ol, ul ul, ol ul, ul ol {
margin-bottom: 0;
}
dt {
font-weight: 700;
}
dd {
margin-bottom: .5rem;
margin-left: 0;
}
blockquote {
margin: 0 0 1rem;
}
dfn {
font-style: italic;
}
b, strong {
font-weight: bold;
}
small {
font-size: 80%;
}
sub, sup {
position: relative;
font-size: 75%;
line-height: 0;
vertical-align: baseline;
}
sub {
bottom: -.25em;
}
sup {
top: -.5em;
}
a {
color: #007bff;
text-decoration: none;
background-color: transparent;
-webkit-text-decoration-skip: objects;
}
a:hover {
color: #0056b3;
text-decoration: underline;
}
a:not([href]):not([tabindex]) {
color: inherit;
text-decoration: none;
}
a:not([href]):not([tabindex]):hover, a:not([href]):not([tabindex]):focus {
color: inherit;
text-decoration: none;
}
a:not([href]):not([tabindex]):focus {
outline: 0;
}
pre, code, kbd, samp {
font-family: monospace, monospace;
font-size: 90%;
padding: 2px 4px 2px 4px;
color: #c7254e;
background-color: #f9f2f4;
border-radius: 4px;
}
pre {
margin-top: 0;
margin-bottom: 1rem;
overflow: auto;
-ms-overflow-style: scrollbar;
}
figure {
margin: 0 0 1rem;
}
img {
vertical-align: middle;
border-style: none;
}
svg:not(:root) {
overflow: hidden;
}
table {
border-collapse: collapse;
background-color: transparent;
}
caption {
padding-top: 8px;
padding-bottom: 8px;
color: #a7a7a7;
text-align: left;
}
th {
text-align: left;
}
output {
display: inline-block;
}
summary {
display: list-item;
cursor: pointer;
}
template {
display: none;
}
table col[class*="col-"] {
position: static;
float: none;
display: table-column;
}
table td[class*="col-"],table th[class*="col-"] {
position: static;
float: none;
display: table-cell;
}
ol.glossary {
counter-reset: glossary-counter;
list-style: none;
padding-left: 40px;
}
ol.glossary li {
counter-increment: glossary-counter;
position: relative;
}
ol.glossary li::before {
content: counter(glossary-counter);
position: absolute;
background-color: #73EAA9;
color: #fff;
border-radius: 100px;
width: 24px;
left: -40px;
text-align: center;
font-weight: bold;
line-height: 24px;
}
.layout-wrapper {
position: relative;
width: 100%;
height: auto;
min-height: 100%;
}
.layout-title {
padding: 1.875rem 0;
border-bottom: 1px solid #f0f0f0;
}
.layout-title h1 {
font-size: 3rem;
font-weight: 200;
text-align: center;
margin: 0;
}
.layout-title h2 {
font-size: 2rem;
font-weight: 200;
text-align: center;
color: #999;
margin-bottom: 0;
}
.layout-navigation .nav {
padding: 1.875rem 0;
border-bottom: 1px solid #f0f0f0;
text-align: center;
background: #fff;
z-index: 1000;
}
.layout-navigation .nav > li {
display: inline-block;
}
.layout-navigation .nav > li > a {
border: 1px solid #007bff;
border-radius: 100px;
padding: 6px 12px;
margin: 0 8px;
}
.layout-navigation .nav > li > a:hover, .layout-navigation .nav > li > a:active, .layout-navigation .nav > li > a:focus {
border: 1px solid #0056b3;
color: #0056b3;
text-decoration: none;
}
.layout-content {
position: relative;
}
.layout-content:before, .layout-content:after {
content: " ";
display: table;
}
.layout-content:after {
clear: both;
}
.layout-content .col-right {
width: 25%;
float: right;
}
.layout-content .col-right-wrapper {
width: 100%;
position: relative;
overflow-x: hidden;
overflow-y: auto;
padding: 2.5rem 2rem 0;
}
.layout-content .col-body {
width: 75%;
float: left;
}
.layout-content .col-body:before, .layout-content .col-body:after {
content: " ";
display: table;
}
.layout-content .col-body:after {
clear: both;
}
.layout-content .col-body-wrapper {
position: relative;
width: 100%;
padding: 2.5rem 2rem 0;
}
.nav {
padding-left: 0;
margin-bottom: 0;
list-style: none;
line-height: 2;
}
.table-of-contents {
padding: 1.25rem 0;
}
.table-of-contents .nav > li > a {
display: flex;
}
.table-of-contents .nav > li > a .item-number {
display: none;
}
.table-of-contents .nav > li > a .item-title {
color: #212529;
overflow: hidden;
text-overflow: ellipsis;
flex-grow: 1;
white-space: nowrap;
}
.table-of-contents .nav > li > a .item-title:hover {
color: #0056b3;
}
.table-of-contents .nav > li.extended > a .item-title, .table-of-contents .nav > li.active > a .item-title, .table-of-contents .nav > li.extended > a .item-title:hover, .table-of-contents .nav > li.active > a .item-title:hover {
color: #007bff;
}
.table-of-contents > .nav > li > .nav {
display: none;
margin-bottom: 0.5rem;
}
.table-of-contents > .nav > li > .nav > li > a {
padding-left: 1rem;
}
.table-of-contents > .nav > li > .nav > li > a .item-title {
font-size: 0.875rem;
}
.table-of-contents > .nav > li > .nav > li > .nav > li > a {
padding-left: 2rem;
}
.table-of-contents > .nav > li > .nav > li > .nav > li > a .item-title {
font-size: 0.75rem;
}
.table-of-contents > .nav > li.active > .nav {
display: block;
}
.chapter {
margin: 0 0 6rem;
font-weight: 300;
}
.section {
margin-top: 3rem;
}
.chapter > .chapter-title h1, .chapter > .chapter-title h2, .chapter > .chapter-title h3, .chapter > .chapter-title h4, .chapter > .chapter-title h5, .chapter > .chapter-title h6, .section > .section-title h1, .section > .section-title h2, .section > .section-title h3, .section > .section-title h4, .section > .section-title h5, .section > .section-title h6 {
margin-top: 0;
}
.chapter > .chapter-title h1 {
border-bottom: 2px solid #eeeeee;
margin-bottom: 1.5rem;
padding-bottom: 0.2em;
}
.chapter-title .title-number, .section-title .title-number {
display: none;
}
.paragraph {
line-height: 1.75em;
}
.paragraph + .paragraph {
margin-top: 1em;
}
.dlist {
margin-top: 30px;
}
.dlist dl dt {
float: left;
width: 160px;
clear: left;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.dlist dl dd {
margin-left: 180px;
}
.ulist {
margin-top: 30px;
}
.imageblock {
margin: 30px auto;
}
.imageblock .content img {
max-width: 100%;
}
.imageblock .title {
padding: 10px 0 0;
}
.exampleblock, .quoteblock, .literalblock {
background: #f5f4f4;
padding: 20px;
margin: 30px 0;
}
.exampleblock .title, .quoteblock .title, .literalblock .title {
text-transform: uppercase;
font-size: 0.75em;
font-weight: 400;
color: #979797;
margin-bottom: 10px;
}
.quoteblock blockquote {
margin: 0;
padding: 0;
border: 0;
font-size: inherit;
}
.quoteblock blockquote p:last-child, .quoteblock blockquote ul:last-child, .quoteblock blockquote ol:last-child {
margin-bottom: 9px;
}
.literalblock pre {
border: 0;
padding: 0;
margin: 0;
}
.listingblock {
margin: 30px 0;
}
.listingblock pre {
border: 0;
padding: 0;
margin: 0;
}
.listingblock pre code {
display: block;
padding: 20px;
}
.admonitionblock {
line-height: 1.8em;
padding: 20px;
margin: 30px 0;
}
.admonitionblock .icon {
display: none;
}
.admonitionblock.important {
background: #fce1e1;
border-left: 5px solid #ff6060;
}
.admonitionblock.note, .admonitionblock.tip {
background: #e0f2fc;
border-left: 5px solid #88d5ff;
}
.admonitionblock.caution, .admonitionblock.warning {
background: #fdf3d8;
border-left: 5px solid #f1c654;
}
table.tableblock {
background-color: #fff;
width: 100%;
max-width: 100%;
margin-bottom: 18px;
margin: 30px 0;
}
table.tableblock > thead > tr > th, table.tableblock > tbody > tr > th, table.tableblock > tfoot > tr > th, table.tableblock > thead > tr > td, table.tableblock > tbody > tr > td, table.tableblock > tfoot > tr > td {
padding: 8px;
line-height: 1.42857143;
vertical-align: top;
border-top: 1px solid #eee;
}
table.tableblock > thead > tr > th {
vertical-align: bottom;
border-bottom: 2px solid #eee;
}
table.tableblock > caption + thead > tr:first-child > th, table.tableblock > colgroup + thead > tr:first-child > th, table.tableblock > thead:first-child > tr:first-child > th, table.tableblock > caption + thead > tr:first-child > td, table.tableblock > colgroup + thead > tr:first-child > td, table.tableblock > thead:first-child > tr:first-child > td {
border-top: 0;
}
table.tableblock > tbody + tbody {
border-top: 2px solid #eee;
}
table.tableblock .table {
background-color: #fff;
}
table.tableblock > tbody > tr:nth-of-type(odd) {
background-color: #f7f7f7;
}
table.tableblock > thead > tr > th p:last-child, table.tableblock > tbody > tr > th p:last-child, table.tableblock > tfoot > tr > th p:last-child, table.tableblock > thead > tr > td p:last-child, table.tableblock > tbody > tr > td p:last-child, table.tableblock > tfoot > tr > td p:last-child {
margin-bottom: 0;
}
.loaded .table-of-contents .nav .nav {
display: none;
}
@media (min-width: 1200px) {
.layout-wrapper .layout-title, .layout-wrapper .layout-navigation, .layout-wrapper .layout-content {
max-width: 1200px;
margin-left: auto;
margin-right: auto;
}
}
@media (max-width: 991px) {
html {
font-size: 90%;
}
.layout-content .col-right {
display: none;
}
.layout-content .col-body {
width: 100%;
}
}
@media print {
body {
color: #000;
font-family: Georgia, "Times New Roman", Times, serif;
}
a {
color: #000;
}
h1 {
font-size: 1.6rem;
}
h2 {
font-size: 1.4rem;
}
h3 {
font-size: 1.2rem;
}
h4 {
font-size: 1rem;
}
h5 {
font-size: 0.9rem;
}
.layout-title h1 {
font-size: 2rem;
}
.layout-content .col-right {
display: none;
}
.layout-content .col-body {
width: 100%;
}
.chapter {
margin-bottom: 3rem;
}
.section {
margin-top: 2rem;
}
}
+14
View File
@@ -0,0 +1,14 @@
(function($){
hljs.initHighlightingOnLoad();
$(document).ready(function(){
$('.col-right-wrapper').stick_in_parent({
parent: '.layout-content'
});
$('body').scrollspy({
target: '.table-of-contents'
});
});
})(jQuery);
File diff suppressed because one or more lines are too long
+2
View File
File diff suppressed because one or more lines are too long
+83
View File
@@ -0,0 +1,83 @@
/*
Monokai Sublime style. Derived from Monokai by noformnocontent http://nn.mit-license.org/
*/
.hljs {
display: block;
overflow-x: auto;
padding: 0.5em;
background: #23241f;
}
.hljs,
.hljs-tag,
.hljs-subst {
color: #f8f8f2;
}
.hljs-strong,
.hljs-emphasis {
color: #a8a8a2;
}
.hljs-bullet,
.hljs-quote,
.hljs-number,
.hljs-regexp,
.hljs-literal,
.hljs-link {
color: #ae81ff;
}
.hljs-code,
.hljs-title,
.hljs-section,
.hljs-selector-class {
color: #a6e22e;
}
.hljs-strong {
font-weight: bold;
}
.hljs-emphasis {
font-style: italic;
}
.hljs-keyword,
.hljs-selector-tag,
.hljs-name,
.hljs-attr {
color: #f92672;
}
.hljs-symbol,
.hljs-attribute {
color: #66d9ef;
}
.hljs-params,
.hljs-class .hljs-title {
color: #f8f8f2;
}
.hljs-string,
.hljs-type,
.hljs-built_in,
.hljs-builtin-name,
.hljs-selector-id,
.hljs-selector-attr,
.hljs-selector-pseudo,
.hljs-addition,
.hljs-variable,
.hljs-template-variable {
color: #e6db74;
}
.hljs-comment,
.hljs-deletion,
.hljs-meta {
color: #75715e;
}
+9
View File
@@ -0,0 +1,9 @@
/* ========================================================================
* Bootstrap: scrollspy.js v3.3.7
* http://getbootstrap.com/javascript/#scrollspy
* ========================================================================
* Copyright 2011-2016 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* ======================================================================== */
!function(r){"use strict";function o(t,s){this.$body=r(document.body),this.$scrollElement=r(t).is(document.body)?r(window):r(t),this.options=r.extend({},o.DEFAULTS,s),this.selector=(this.options.target||"")+" .nav li > a",this.offsets=[],this.targets=[],this.activeTarget=null,this.scrollHeight=0,this.$scrollElement.on("scroll.bs.scrollspy",r.proxy(this.process,this)),this.refresh(),this.process()}function s(i){return this.each(function(){var t=r(this),s=t.data("bs.scrollspy"),e="object"==typeof i&&i;s||t.data("bs.scrollspy",s=new o(this,e)),"string"==typeof i&&s[i]()})}o.VERSION="3.3.7",o.DEFAULTS={offset:10},o.prototype.getScrollHeight=function(){return this.$scrollElement[0].scrollHeight||Math.max(this.$body[0].scrollHeight,document.documentElement.scrollHeight)},o.prototype.refresh=function(){var t=this,i="offset",o=0;this.offsets=[],this.targets=[],this.scrollHeight=this.getScrollHeight(),r.isWindow(this.$scrollElement[0])||(i="position",o=this.$scrollElement.scrollTop()),this.$body.find(this.selector).map(function(){var t=r(this),s=t.data("target")||t.attr("href"),e=/^#./.test(s)&&r(s);return e&&e.length&&e.is(":visible")&&[[e[i]().top+o,s]]||null}).sort(function(t,s){return t[0]-s[0]}).each(function(){t.offsets.push(this[0]),t.targets.push(this[1])})},o.prototype.process=function(){var t,s=this.$scrollElement.scrollTop()+this.options.offset,e=this.getScrollHeight(),i=this.options.offset+e-this.$scrollElement.height(),o=this.offsets,r=this.targets,l=this.activeTarget;if(this.scrollHeight!=e&&this.refresh(),i<=s)return l!=(t=r[r.length-1])&&this.activate(t);if(l&&s<o[0])return this.activeTarget=null,this.clear();for(t=o.length;t--;)l!=r[t]&&s>=o[t]&&(void 0===o[t+1]||s<o[t+1])&&this.activate(r[t])},o.prototype.activate=function(t){this.activeTarget=t,this.clear();var s=this.selector+'[data-target="'+t+'"],'+this.selector+'[href="'+t+'"]',e=r(s).parents("li").addClass("active");e.parent(".dropdown-menu").length&&(e=e.closest("li.dropdown").addClass("active")),e.trigger("activate.bs.scrollspy")},o.prototype.clear=function(){r(this.selector).parentsUntil(this.options.target,".active").removeClass("active")};var t=r.fn.scrollspy;r.fn.scrollspy=s,r.fn.scrollspy.Constructor=o,r.fn.scrollspy.noConflict=function(){return r.fn.scrollspy=t,this},r(window).on("load.bs.scrollspy.data-api",function(){r('[data-spy="scroll"]').each(function(){var t=r(this);s.call(t,t.data())})})}(jQuery);
+5
View File
@@ -0,0 +1,5 @@
/**
@license Sticky-kit v1.1.2 | WTFPL | Leaf Corcoran 2015 | http://leafo.net
*/
(function(){var M,Q;M=this.jQuery||window.jQuery,Q=M(window),M.fn.stick_in_parent=function(t){var x,o,C,i,e,P,s,V,F,z,r,I,A,j;for(null==t&&(t={}),j=t.sticky_class,P=t.inner_scrolling,A=t.recalc_every,I=t.parent,F=t.offset_top,z=t.offset_bottom,V=t.spacer,C=t.bottoming,null==F&&(F=0),null==z&&(z=0),null==I&&(I=void 0),null==P&&(P=!0),null==j&&(j="is_stuck"),x=M(document),null==C&&(C=!0),r=function(t){var o,i;return window.getComputedStyle?(t[0],o=window.getComputedStyle(t[0]),i=parseFloat(o.getPropertyValue("width"))+parseFloat(o.getPropertyValue("margin-left"))+parseFloat(o.getPropertyValue("margin-right")),"border-box"!==o.getPropertyValue("box-sizing")&&(i+=parseFloat(o.getPropertyValue("border-left-width"))+parseFloat(o.getPropertyValue("border-right-width"))+parseFloat(o.getPropertyValue("padding-left"))+parseFloat(o.getPropertyValue("padding-right"))),i):t.outerWidth(!0)},i=function(n,l,a,c,p,d,u,f){var h,t,g,m,k,y,b,v,o,_,w,e;if(!n.data("sticky_kit")){if(n.data("sticky_kit",!0),k=x.height(),b=n.parent(),null!=I&&(b=b.closest(I)),!b.length)throw"failed to find stick parent";return h=g=!1,(w=null!=V?V&&n.closest(V):M('<div class="sticky-kit-manual-spacer" />'))&&w.css("position",n.css("position")),(v=function(){var t,o,i;if(!f)return k=x.height(),t=parseInt(b.css("border-top-width"),10),o=parseInt(b.css("padding-top"),10),l=parseInt(b.css("padding-bottom"),10),a=b.offset().top+t+o,c=b.height(),g&&(h=g=!1,null==V&&(n.insertAfter(w),w.detach()),n.css({position:"",top:"",width:"",bottom:""}).removeClass(j),i=!0),p=n.offset().top-(parseInt(n.css("margin-top"),10)||0)-F,d=n.outerHeight(!0),u=n.css("float"),w&&w.css({width:r(n),height:d,display:n.css("display"),"vertical-align":n.css("vertical-align"),float:u}),i?e():void 0})(),m=void 0,y=F,_=A,e=function(){var t,o,i,e,s,r;if(d!==c&&!f)return i=!1,null!=_&&(_-=1)<=0&&(_=A,v(),i=!0),i||x.height()===k||(v(),i=!0),e=Q.scrollTop(),null!=m&&(o=e-m),m=e,g?(C&&(s=c+a<e+d+y+z,h&&!s&&(h=!1,n.css({position:"fixed",bottom:"",top:y}).removeClass("is_bottomed").trigger("sticky_kit:unbottom"))),e<p&&(g=!1,y=F,null==V&&("left"!==u&&"right"!==u||n.insertAfter(w),w.detach()),t={position:"",width:"",top:""},n.css(t).removeClass(j).trigger("sticky_kit:unstick")),P&&(r=Q.height())<d+F&&(h||(y-=o,y=Math.max(r-d,y),y=Math.min(F,y),g&&n.css({top:y+"px"})))):p<e&&(g=!0,(t={position:"fixed",top:y}).width="border-box"===n.css("box-sizing")?n.outerWidth()+"px":n.width()+"px",n.css(t).addClass(j),null==V&&(n.after(w),"left"!==u&&"right"!==u||w.append(n)),n.trigger("sticky_kit:stick")),g&&C&&(null==s&&(s=c+a<e+d+y+z),!h&&s)?(h=!0,"static"===b.css("position")&&b.css({position:"relative"}),n.css({position:"absolute",bottom:l+z,top:"auto"}).addClass("is_bottomed").trigger("sticky_kit:bottom")):void 0},o=function(){return v(),e()},t=function(){if(f=!0,Q.off("touchmove",e),Q.off("scroll",e),Q.off("resize",o),M(document.body).off("sticky_kit:recalc",o),n.off("sticky_kit:detach",t),n.removeData("sticky_kit"),n.css({position:"",bottom:"",top:"",width:""}),b.position("position",""),g)return null==V&&("left"!==u&&"right"!==u||n.insertAfter(w),w.remove()),n.removeClass(j)},Q.on("touchmove",e),Q.on("scroll",e),Q.on("resize",o),M(document.body).on("sticky_kit:recalc",o),n.on("sticky_kit:detach",t),setTimeout(e,0)}},e=0,s=this.length;e<s;e++)o=this[e],i(M(o));return this}}).call(this);
File diff suppressed because it is too large Load Diff
+662 -275
View File
File diff suppressed because it is too large Load Diff
+692
View File
@@ -0,0 +1,692 @@
* {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
*:before, *:after {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
html {
font-size: 100%;
font-family: sans-serif;
line-height: 1.15;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
-ms-overflow-style: scrollbar;
-webkit-tap-highlight-color: transparent;
}
@-ms-viewport {
width: device-width;
}
article, aside, dialog, figcaption, figure, footer, header, hgroup, main, nav, section {
display: block;
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
font-size: 1rem;
font-weight: 300;
line-height: 1.5;
color: #212529;
text-align: left;
background-color: #fff;
padding-right: 0 !important;
position: relative;
}
html,body {
width: 100%;
height: 100%;
}
[tabindex="-1"]:focus {
outline: 0 !important;
}
hr {
box-sizing: content-box;
height: 0;
overflow: visible;
}
h1, h2, h3, h4, h5, h6 {
margin-top: 0;
margin-bottom: 0.5rem;
}
h1 {
font-size: 2.5rem;
font-weight: 200;
margin-bottom: 1.875rem;
}
h2 {
font-size: 1.625rem;
font-weight: 300;
margin-bottom: 1.3rem;
}
h3 {
font-size: 1.3rem;
font-weight: 300;
margin-top: 1.3rem;
}
h4 {
font-size: 1.125rem;
font-weight: 400;
margin-top: 1.875rem;
margin-bottom: 1.3rem;
}
h5 {
font-size: 1rem;
font-weight: bold;
margin-top: 1.875rem;
margin-bottom: 1.3rem;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
abbr[title], abbr[data-original-title] {
text-decoration: underline;
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
cursor: help;
border-bottom: 0;
}
address {
margin-bottom: 1rem;
font-style: normal;
line-height: inherit;
}
ol, ul, dl {
margin-top: 0;
margin-bottom: 1rem;
}
ol ol, ul ul, ol ul, ul ol {
margin-bottom: 0;
}
dt {
font-weight: 700;
}
dd {
margin-bottom: .5rem;
margin-left: 0;
}
blockquote {
margin: 0 0 1rem;
}
dfn {
font-style: italic;
}
b, strong {
font-weight: bold;
}
small {
font-size: 80%;
}
sub, sup {
position: relative;
font-size: 75%;
line-height: 0;
vertical-align: baseline;
}
sub {
bottom: -.25em;
}
sup {
top: -.5em;
}
a {
color: #007bff;
text-decoration: none;
background-color: transparent;
-webkit-text-decoration-skip: objects;
}
a:hover {
color: #0056b3;
text-decoration: underline;
}
a:not([href]):not([tabindex]) {
color: inherit;
text-decoration: none;
}
a:not([href]):not([tabindex]):hover, a:not([href]):not([tabindex]):focus {
color: inherit;
text-decoration: none;
}
a:not([href]):not([tabindex]):focus {
outline: 0;
}
pre, code, kbd, samp {
font-family: monospace, monospace;
font-size: 90%;
padding: 2px 4px 2px 4px;
color: #c7254e;
background-color: #f9f2f4;
border-radius: 4px;
}
pre {
margin-top: 0;
margin-bottom: 1rem;
overflow: auto;
-ms-overflow-style: scrollbar;
}
figure {
margin: 0 0 1rem;
}
img {
vertical-align: middle;
border-style: none;
}
svg:not(:root) {
overflow: hidden;
}
table {
border-collapse: collapse;
background-color: transparent;
}
caption {
padding-top: 8px;
padding-bottom: 8px;
color: #a7a7a7;
text-align: left;
}
th {
text-align: left;
}
output {
display: inline-block;
}
summary {
display: list-item;
cursor: pointer;
}
template {
display: none;
}
table col[class*="col-"] {
position: static;
float: none;
display: table-column;
}
table td[class*="col-"],table th[class*="col-"] {
position: static;
float: none;
display: table-cell;
}
ol.glossary {
counter-reset: glossary-counter;
list-style: none;
padding-left: 40px;
}
ol.glossary li {
counter-increment: glossary-counter;
position: relative;
}
ol.glossary li::before {
content: counter(glossary-counter);
position: absolute;
background-color: #73EAA9;
color: #fff;
border-radius: 100px;
width: 24px;
left: -40px;
text-align: center;
font-weight: bold;
line-height: 24px;
}
.layout-wrapper {
position: relative;
width: 100%;
height: auto;
min-height: 100%;
}
.layout-title {
padding: 1.875rem 0;
border-bottom: 1px solid #f0f0f0;
}
.layout-title h1 {
font-size: 3rem;
font-weight: 200;
text-align: center;
margin: 0;
}
.layout-title h2 {
font-size: 2rem;
font-weight: 200;
text-align: center;
color: #999;
margin-bottom: 0;
}
.layout-navigation .nav {
padding: 1.875rem 0;
border-bottom: 1px solid #f0f0f0;
text-align: center;
background: #fff;
z-index: 1000;
}
.layout-navigation .nav > li {
display: inline-block;
}
.layout-navigation .nav > li > a {
border: 1px solid #007bff;
border-radius: 100px;
padding: 6px 12px;
margin: 0 8px;
}
.layout-navigation .nav > li > a:hover, .layout-navigation .nav > li > a:active, .layout-navigation .nav > li > a:focus {
border: 1px solid #0056b3;
color: #0056b3;
text-decoration: none;
}
.layout-content {
position: relative;
}
.layout-content:before, .layout-content:after {
content: " ";
display: table;
}
.layout-content:after {
clear: both;
}
.layout-content .col-right {
width: 25%;
float: right;
}
.layout-content .col-right-wrapper {
width: 100%;
position: relative;
overflow-x: hidden;
overflow-y: auto;
padding: 2.5rem 2rem 0;
}
.layout-content .col-body {
width: 75%;
float: left;
}
.layout-content .col-body:before, .layout-content .col-body:after {
content: " ";
display: table;
}
.layout-content .col-body:after {
clear: both;
}
.layout-content .col-body-wrapper {
position: relative;
width: 100%;
padding: 2.5rem 2rem 0;
}
.nav {
padding-left: 0;
margin-bottom: 0;
list-style: none;
line-height: 2;
}
.table-of-contents {
padding: 1.25rem 0;
}
.table-of-contents .nav > li > a {
display: flex;
}
.table-of-contents .nav > li > a .item-number {
display: none;
}
.table-of-contents .nav > li > a .item-title {
color: #212529;
overflow: hidden;
text-overflow: ellipsis;
flex-grow: 1;
white-space: nowrap;
}
.table-of-contents .nav > li > a .item-title:hover {
color: #0056b3;
}
.table-of-contents .nav > li.extended > a .item-title, .table-of-contents .nav > li.active > a .item-title, .table-of-contents .nav > li.extended > a .item-title:hover, .table-of-contents .nav > li.active > a .item-title:hover {
color: #007bff;
}
.table-of-contents > .nav > li > .nav {
display: none;
margin-bottom: 0.5rem;
}
.table-of-contents > .nav > li > .nav > li > a {
padding-left: 1rem;
}
.table-of-contents > .nav > li > .nav > li > a .item-title {
font-size: 0.875rem;
}
.table-of-contents > .nav > li > .nav > li > .nav > li > a {
padding-left: 2rem;
}
.table-of-contents > .nav > li > .nav > li > .nav > li > a .item-title {
font-size: 0.75rem;
}
.table-of-contents > .nav > li.active > .nav {
display: block;
}
.chapter {
margin: 0 0 6rem;
font-weight: 300;
}
.section {
margin-top: 3rem;
}
.chapter > .chapter-title h1, .chapter > .chapter-title h2, .chapter > .chapter-title h3, .chapter > .chapter-title h4, .chapter > .chapter-title h5, .chapter > .chapter-title h6, .section > .section-title h1, .section > .section-title h2, .section > .section-title h3, .section > .section-title h4, .section > .section-title h5, .section > .section-title h6 {
margin-top: 0;
}
.chapter > .chapter-title h1 {
border-bottom: 2px solid #eeeeee;
margin-bottom: 1.5rem;
padding-bottom: 0.2em;
}
.chapter-title .title-number, .section-title .title-number {
display: none;
}
.paragraph {
line-height: 1.75em;
}
.paragraph + .paragraph {
margin-top: 1em;
}
.dlist {
margin-top: 30px;
}
.dlist dl dt {
float: left;
width: 160px;
clear: left;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.dlist dl dd {
margin-left: 180px;
}
.ulist {
margin-top: 30px;
}
.imageblock {
margin: 30px auto;
}
.imageblock .content img {
max-width: 100%;
}
.imageblock .title {
padding: 10px 0 0;
}
.exampleblock, .quoteblock, .literalblock {
background: #f5f4f4;
padding: 20px;
margin: 30px 0;
}
.exampleblock .title, .quoteblock .title, .literalblock .title {
text-transform: uppercase;
font-size: 0.75em;
font-weight: 400;
color: #979797;
margin-bottom: 10px;
}
.quoteblock blockquote {
margin: 0;
padding: 0;
border: 0;
font-size: inherit;
}
.quoteblock blockquote p:last-child, .quoteblock blockquote ul:last-child, .quoteblock blockquote ol:last-child {
margin-bottom: 9px;
}
.literalblock pre {
border: 0;
padding: 0;
margin: 0;
}
.listingblock {
margin: 30px 0;
}
.listingblock pre {
border: 0;
padding: 0;
margin: 0;
}
.listingblock pre code {
display: block;
padding: 20px;
}
.admonitionblock {
line-height: 1.8em;
padding: 20px;
margin: 30px 0;
}
.admonitionblock .icon {
display: none;
}
.admonitionblock.important {
background: #fce1e1;
border-left: 5px solid #ff6060;
}
.admonitionblock.note, .admonitionblock.tip {
background: #e0f2fc;
border-left: 5px solid #88d5ff;
}
.admonitionblock.caution, .admonitionblock.warning {
background: #fdf3d8;
border-left: 5px solid #f1c654;
}
table.tableblock {
background-color: #fff;
width: 100%;
max-width: 100%;
margin-bottom: 18px;
margin: 30px 0;
}
table.tableblock > thead > tr > th, table.tableblock > tbody > tr > th, table.tableblock > tfoot > tr > th, table.tableblock > thead > tr > td, table.tableblock > tbody > tr > td, table.tableblock > tfoot > tr > td {
padding: 8px;
line-height: 1.42857143;
vertical-align: top;
border-top: 1px solid #eee;
}
table.tableblock > thead > tr > th {
vertical-align: bottom;
border-bottom: 2px solid #eee;
}
table.tableblock > caption + thead > tr:first-child > th, table.tableblock > colgroup + thead > tr:first-child > th, table.tableblock > thead:first-child > tr:first-child > th, table.tableblock > caption + thead > tr:first-child > td, table.tableblock > colgroup + thead > tr:first-child > td, table.tableblock > thead:first-child > tr:first-child > td {
border-top: 0;
}
table.tableblock > tbody + tbody {
border-top: 2px solid #eee;
}
table.tableblock .table {
background-color: #fff;
}
table.tableblock > tbody > tr:nth-of-type(odd) {
background-color: #f7f7f7;
}
table.tableblock > thead > tr > th p:last-child, table.tableblock > tbody > tr > th p:last-child, table.tableblock > tfoot > tr > th p:last-child, table.tableblock > thead > tr > td p:last-child, table.tableblock > tbody > tr > td p:last-child, table.tableblock > tfoot > tr > td p:last-child {
margin-bottom: 0;
}
.loaded .table-of-contents .nav .nav {
display: none;
}
@media (min-width: 1200px) {
.layout-wrapper .layout-title, .layout-wrapper .layout-navigation, .layout-wrapper .layout-content {
max-width: 1200px;
margin-left: auto;
margin-right: auto;
}
}
@media (max-width: 991px) {
html {
font-size: 90%;
}
.layout-content .col-right {
display: none;
}
.layout-content .col-body {
width: 100%;
}
}
@media print {
body {
color: #000;
font-family: Georgia, "Times New Roman", Times, serif;
}
a {
color: #000;
}
h1 {
font-size: 1.6rem;
}
h2 {
font-size: 1.4rem;
}
h3 {
font-size: 1.2rem;
}
h4 {
font-size: 1rem;
}
h5 {
font-size: 0.9rem;
}
.layout-title h1 {
font-size: 2rem;
}
.layout-content .col-right {
display: none;
}
.layout-content .col-body {
width: 100%;
}
.chapter {
margin-bottom: 3rem;
}
.section {
margin-top: 2rem;
}
}
+14
View File
@@ -0,0 +1,14 @@
(function($){
hljs.initHighlightingOnLoad();
$(document).ready(function(){
$('.col-right-wrapper').stick_in_parent({
parent: '.layout-content'
});
$('body').scrollspy({
target: '.table-of-contents'
});
});
})(jQuery);
File diff suppressed because one or more lines are too long
+2
View File
File diff suppressed because one or more lines are too long
+83
View File
@@ -0,0 +1,83 @@
/*
Monokai Sublime style. Derived from Monokai by noformnocontent http://nn.mit-license.org/
*/
.hljs {
display: block;
overflow-x: auto;
padding: 0.5em;
background: #23241f;
}
.hljs,
.hljs-tag,
.hljs-subst {
color: #f8f8f2;
}
.hljs-strong,
.hljs-emphasis {
color: #a8a8a2;
}
.hljs-bullet,
.hljs-quote,
.hljs-number,
.hljs-regexp,
.hljs-literal,
.hljs-link {
color: #ae81ff;
}
.hljs-code,
.hljs-title,
.hljs-section,
.hljs-selector-class {
color: #a6e22e;
}
.hljs-strong {
font-weight: bold;
}
.hljs-emphasis {
font-style: italic;
}
.hljs-keyword,
.hljs-selector-tag,
.hljs-name,
.hljs-attr {
color: #f92672;
}
.hljs-symbol,
.hljs-attribute {
color: #66d9ef;
}
.hljs-params,
.hljs-class .hljs-title {
color: #f8f8f2;
}
.hljs-string,
.hljs-type,
.hljs-built_in,
.hljs-builtin-name,
.hljs-selector-id,
.hljs-selector-attr,
.hljs-selector-pseudo,
.hljs-addition,
.hljs-variable,
.hljs-template-variable {
color: #e6db74;
}
.hljs-comment,
.hljs-deletion,
.hljs-meta {
color: #75715e;
}
+9
View File
@@ -0,0 +1,9 @@
/* ========================================================================
* Bootstrap: scrollspy.js v3.3.7
* http://getbootstrap.com/javascript/#scrollspy
* ========================================================================
* Copyright 2011-2016 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* ======================================================================== */
!function(r){"use strict";function o(t,s){this.$body=r(document.body),this.$scrollElement=r(t).is(document.body)?r(window):r(t),this.options=r.extend({},o.DEFAULTS,s),this.selector=(this.options.target||"")+" .nav li > a",this.offsets=[],this.targets=[],this.activeTarget=null,this.scrollHeight=0,this.$scrollElement.on("scroll.bs.scrollspy",r.proxy(this.process,this)),this.refresh(),this.process()}function s(i){return this.each(function(){var t=r(this),s=t.data("bs.scrollspy"),e="object"==typeof i&&i;s||t.data("bs.scrollspy",s=new o(this,e)),"string"==typeof i&&s[i]()})}o.VERSION="3.3.7",o.DEFAULTS={offset:10},o.prototype.getScrollHeight=function(){return this.$scrollElement[0].scrollHeight||Math.max(this.$body[0].scrollHeight,document.documentElement.scrollHeight)},o.prototype.refresh=function(){var t=this,i="offset",o=0;this.offsets=[],this.targets=[],this.scrollHeight=this.getScrollHeight(),r.isWindow(this.$scrollElement[0])||(i="position",o=this.$scrollElement.scrollTop()),this.$body.find(this.selector).map(function(){var t=r(this),s=t.data("target")||t.attr("href"),e=/^#./.test(s)&&r(s);return e&&e.length&&e.is(":visible")&&[[e[i]().top+o,s]]||null}).sort(function(t,s){return t[0]-s[0]}).each(function(){t.offsets.push(this[0]),t.targets.push(this[1])})},o.prototype.process=function(){var t,s=this.$scrollElement.scrollTop()+this.options.offset,e=this.getScrollHeight(),i=this.options.offset+e-this.$scrollElement.height(),o=this.offsets,r=this.targets,l=this.activeTarget;if(this.scrollHeight!=e&&this.refresh(),i<=s)return l!=(t=r[r.length-1])&&this.activate(t);if(l&&s<o[0])return this.activeTarget=null,this.clear();for(t=o.length;t--;)l!=r[t]&&s>=o[t]&&(void 0===o[t+1]||s<o[t+1])&&this.activate(r[t])},o.prototype.activate=function(t){this.activeTarget=t,this.clear();var s=this.selector+'[data-target="'+t+'"],'+this.selector+'[href="'+t+'"]',e=r(s).parents("li").addClass("active");e.parent(".dropdown-menu").length&&(e=e.closest("li.dropdown").addClass("active")),e.trigger("activate.bs.scrollspy")},o.prototype.clear=function(){r(this.selector).parentsUntil(this.options.target,".active").removeClass("active")};var t=r.fn.scrollspy;r.fn.scrollspy=s,r.fn.scrollspy.Constructor=o,r.fn.scrollspy.noConflict=function(){return r.fn.scrollspy=t,this},r(window).on("load.bs.scrollspy.data-api",function(){r('[data-spy="scroll"]').each(function(){var t=r(this);s.call(t,t.data())})})}(jQuery);
+5
View File
@@ -0,0 +1,5 @@
/**
@license Sticky-kit v1.1.2 | WTFPL | Leaf Corcoran 2015 | http://leafo.net
*/
(function(){var M,Q;M=this.jQuery||window.jQuery,Q=M(window),M.fn.stick_in_parent=function(t){var x,o,C,i,e,P,s,V,F,z,r,I,A,j;for(null==t&&(t={}),j=t.sticky_class,P=t.inner_scrolling,A=t.recalc_every,I=t.parent,F=t.offset_top,z=t.offset_bottom,V=t.spacer,C=t.bottoming,null==F&&(F=0),null==z&&(z=0),null==I&&(I=void 0),null==P&&(P=!0),null==j&&(j="is_stuck"),x=M(document),null==C&&(C=!0),r=function(t){var o,i;return window.getComputedStyle?(t[0],o=window.getComputedStyle(t[0]),i=parseFloat(o.getPropertyValue("width"))+parseFloat(o.getPropertyValue("margin-left"))+parseFloat(o.getPropertyValue("margin-right")),"border-box"!==o.getPropertyValue("box-sizing")&&(i+=parseFloat(o.getPropertyValue("border-left-width"))+parseFloat(o.getPropertyValue("border-right-width"))+parseFloat(o.getPropertyValue("padding-left"))+parseFloat(o.getPropertyValue("padding-right"))),i):t.outerWidth(!0)},i=function(n,l,a,c,p,d,u,f){var h,t,g,m,k,y,b,v,o,_,w,e;if(!n.data("sticky_kit")){if(n.data("sticky_kit",!0),k=x.height(),b=n.parent(),null!=I&&(b=b.closest(I)),!b.length)throw"failed to find stick parent";return h=g=!1,(w=null!=V?V&&n.closest(V):M('<div class="sticky-kit-manual-spacer" />'))&&w.css("position",n.css("position")),(v=function(){var t,o,i;if(!f)return k=x.height(),t=parseInt(b.css("border-top-width"),10),o=parseInt(b.css("padding-top"),10),l=parseInt(b.css("padding-bottom"),10),a=b.offset().top+t+o,c=b.height(),g&&(h=g=!1,null==V&&(n.insertAfter(w),w.detach()),n.css({position:"",top:"",width:"",bottom:""}).removeClass(j),i=!0),p=n.offset().top-(parseInt(n.css("margin-top"),10)||0)-F,d=n.outerHeight(!0),u=n.css("float"),w&&w.css({width:r(n),height:d,display:n.css("display"),"vertical-align":n.css("vertical-align"),float:u}),i?e():void 0})(),m=void 0,y=F,_=A,e=function(){var t,o,i,e,s,r;if(d!==c&&!f)return i=!1,null!=_&&(_-=1)<=0&&(_=A,v(),i=!0),i||x.height()===k||(v(),i=!0),e=Q.scrollTop(),null!=m&&(o=e-m),m=e,g?(C&&(s=c+a<e+d+y+z,h&&!s&&(h=!1,n.css({position:"fixed",bottom:"",top:y}).removeClass("is_bottomed").trigger("sticky_kit:unbottom"))),e<p&&(g=!1,y=F,null==V&&("left"!==u&&"right"!==u||n.insertAfter(w),w.detach()),t={position:"",width:"",top:""},n.css(t).removeClass(j).trigger("sticky_kit:unstick")),P&&(r=Q.height())<d+F&&(h||(y-=o,y=Math.max(r-d,y),y=Math.min(F,y),g&&n.css({top:y+"px"})))):p<e&&(g=!0,(t={position:"fixed",top:y}).width="border-box"===n.css("box-sizing")?n.outerWidth()+"px":n.width()+"px",n.css(t).addClass(j),null==V&&(n.after(w),"left"!==u&&"right"!==u||w.append(n)),n.trigger("sticky_kit:stick")),g&&C&&(null==s&&(s=c+a<e+d+y+z),!h&&s)?(h=!0,"static"===b.css("position")&&b.css({position:"relative"}),n.css({position:"absolute",bottom:l+z,top:"auto"}).addClass("is_bottomed").trigger("sticky_kit:bottom")):void 0},o=function(){return v(),e()},t=function(){if(f=!0,Q.off("touchmove",e),Q.off("scroll",e),Q.off("resize",o),M(document.body).off("sticky_kit:recalc",o),n.off("sticky_kit:detach",t),n.removeData("sticky_kit"),n.css({position:"",bottom:"",top:"",width:""}),b.position("position",""),g)return null==V&&("left"!==u&&"right"!==u||n.insertAfter(w),w.remove()),n.removeClass(j)},Q.on("touchmove",e),Q.on("scroll",e),Q.on("resize",o),M(document.body).on("sticky_kit:recalc",o),n.on("sticky_kit:detach",t),setTimeout(e,0)}},e=0,s=this.length;e<s;e++)o=this[e],i(M(o));return this}}).call(this);
File diff suppressed because it is too large Load Diff
+692
View File
@@ -0,0 +1,692 @@
* {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
*:before, *:after {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
html {
font-size: 100%;
font-family: sans-serif;
line-height: 1.15;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
-ms-overflow-style: scrollbar;
-webkit-tap-highlight-color: transparent;
}
@-ms-viewport {
width: device-width;
}
article, aside, dialog, figcaption, figure, footer, header, hgroup, main, nav, section {
display: block;
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
font-size: 1rem;
font-weight: 300;
line-height: 1.5;
color: #212529;
text-align: left;
background-color: #fff;
padding-right: 0 !important;
position: relative;
}
html,body {
width: 100%;
height: 100%;
}
[tabindex="-1"]:focus {
outline: 0 !important;
}
hr {
box-sizing: content-box;
height: 0;
overflow: visible;
}
h1, h2, h3, h4, h5, h6 {
margin-top: 0;
margin-bottom: 0.5rem;
}
h1 {
font-size: 2.5rem;
font-weight: 200;
margin-bottom: 1.875rem;
}
h2 {
font-size: 1.625rem;
font-weight: 300;
margin-bottom: 1.3rem;
}
h3 {
font-size: 1.3rem;
font-weight: 300;
margin-top: 1.3rem;
}
h4 {
font-size: 1.125rem;
font-weight: 400;
margin-top: 1.875rem;
margin-bottom: 1.3rem;
}
h5 {
font-size: 1rem;
font-weight: bold;
margin-top: 1.875rem;
margin-bottom: 1.3rem;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
abbr[title], abbr[data-original-title] {
text-decoration: underline;
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
cursor: help;
border-bottom: 0;
}
address {
margin-bottom: 1rem;
font-style: normal;
line-height: inherit;
}
ol, ul, dl {
margin-top: 0;
margin-bottom: 1rem;
}
ol ol, ul ul, ol ul, ul ol {
margin-bottom: 0;
}
dt {
font-weight: 700;
}
dd {
margin-bottom: .5rem;
margin-left: 0;
}
blockquote {
margin: 0 0 1rem;
}
dfn {
font-style: italic;
}
b, strong {
font-weight: bold;
}
small {
font-size: 80%;
}
sub, sup {
position: relative;
font-size: 75%;
line-height: 0;
vertical-align: baseline;
}
sub {
bottom: -.25em;
}
sup {
top: -.5em;
}
a {
color: #007bff;
text-decoration: none;
background-color: transparent;
-webkit-text-decoration-skip: objects;
}
a:hover {
color: #0056b3;
text-decoration: underline;
}
a:not([href]):not([tabindex]) {
color: inherit;
text-decoration: none;
}
a:not([href]):not([tabindex]):hover, a:not([href]):not([tabindex]):focus {
color: inherit;
text-decoration: none;
}
a:not([href]):not([tabindex]):focus {
outline: 0;
}
pre, code, kbd, samp {
font-family: monospace, monospace;
font-size: 90%;
padding: 2px 4px 2px 4px;
color: #c7254e;
background-color: #f9f2f4;
border-radius: 4px;
}
pre {
margin-top: 0;
margin-bottom: 1rem;
overflow: auto;
-ms-overflow-style: scrollbar;
}
figure {
margin: 0 0 1rem;
}
img {
vertical-align: middle;
border-style: none;
}
svg:not(:root) {
overflow: hidden;
}
table {
border-collapse: collapse;
background-color: transparent;
}
caption {
padding-top: 8px;
padding-bottom: 8px;
color: #a7a7a7;
text-align: left;
}
th {
text-align: left;
}
output {
display: inline-block;
}
summary {
display: list-item;
cursor: pointer;
}
template {
display: none;
}
table col[class*="col-"] {
position: static;
float: none;
display: table-column;
}
table td[class*="col-"],table th[class*="col-"] {
position: static;
float: none;
display: table-cell;
}
ol.glossary {
counter-reset: glossary-counter;
list-style: none;
padding-left: 40px;
}
ol.glossary li {
counter-increment: glossary-counter;
position: relative;
}
ol.glossary li::before {
content: counter(glossary-counter);
position: absolute;
background-color: #73EAA9;
color: #fff;
border-radius: 100px;
width: 24px;
left: -40px;
text-align: center;
font-weight: bold;
line-height: 24px;
}
.layout-wrapper {
position: relative;
width: 100%;
height: auto;
min-height: 100%;
}
.layout-title {
padding: 1.875rem 0;
border-bottom: 1px solid #f0f0f0;
}
.layout-title h1 {
font-size: 3rem;
font-weight: 200;
text-align: center;
margin: 0;
}
.layout-title h2 {
font-size: 2rem;
font-weight: 200;
text-align: center;
color: #999;
margin-bottom: 0;
}
.layout-navigation .nav {
padding: 1.875rem 0;
border-bottom: 1px solid #f0f0f0;
text-align: center;
background: #fff;
z-index: 1000;
}
.layout-navigation .nav > li {
display: inline-block;
}
.layout-navigation .nav > li > a {
border: 1px solid #007bff;
border-radius: 100px;
padding: 6px 12px;
margin: 0 8px;
}
.layout-navigation .nav > li > a:hover, .layout-navigation .nav > li > a:active, .layout-navigation .nav > li > a:focus {
border: 1px solid #0056b3;
color: #0056b3;
text-decoration: none;
}
.layout-content {
position: relative;
}
.layout-content:before, .layout-content:after {
content: " ";
display: table;
}
.layout-content:after {
clear: both;
}
.layout-content .col-right {
width: 25%;
float: right;
}
.layout-content .col-right-wrapper {
width: 100%;
position: relative;
overflow-x: hidden;
overflow-y: auto;
padding: 2.5rem 2rem 0;
}
.layout-content .col-body {
width: 75%;
float: left;
}
.layout-content .col-body:before, .layout-content .col-body:after {
content: " ";
display: table;
}
.layout-content .col-body:after {
clear: both;
}
.layout-content .col-body-wrapper {
position: relative;
width: 100%;
padding: 2.5rem 2rem 0;
}
.nav {
padding-left: 0;
margin-bottom: 0;
list-style: none;
line-height: 2;
}
.table-of-contents {
padding: 1.25rem 0;
}
.table-of-contents .nav > li > a {
display: flex;
}
.table-of-contents .nav > li > a .item-number {
display: none;
}
.table-of-contents .nav > li > a .item-title {
color: #212529;
overflow: hidden;
text-overflow: ellipsis;
flex-grow: 1;
white-space: nowrap;
}
.table-of-contents .nav > li > a .item-title:hover {
color: #0056b3;
}
.table-of-contents .nav > li.extended > a .item-title, .table-of-contents .nav > li.active > a .item-title, .table-of-contents .nav > li.extended > a .item-title:hover, .table-of-contents .nav > li.active > a .item-title:hover {
color: #007bff;
}
.table-of-contents > .nav > li > .nav {
display: none;
margin-bottom: 0.5rem;
}
.table-of-contents > .nav > li > .nav > li > a {
padding-left: 1rem;
}
.table-of-contents > .nav > li > .nav > li > a .item-title {
font-size: 0.875rem;
}
.table-of-contents > .nav > li > .nav > li > .nav > li > a {
padding-left: 2rem;
}
.table-of-contents > .nav > li > .nav > li > .nav > li > a .item-title {
font-size: 0.75rem;
}
.table-of-contents > .nav > li.active > .nav {
display: block;
}
.chapter {
margin: 0 0 6rem;
font-weight: 300;
}
.section {
margin-top: 3rem;
}
.chapter > .chapter-title h1, .chapter > .chapter-title h2, .chapter > .chapter-title h3, .chapter > .chapter-title h4, .chapter > .chapter-title h5, .chapter > .chapter-title h6, .section > .section-title h1, .section > .section-title h2, .section > .section-title h3, .section > .section-title h4, .section > .section-title h5, .section > .section-title h6 {
margin-top: 0;
}
.chapter > .chapter-title h1 {
border-bottom: 2px solid #eeeeee;
margin-bottom: 1.5rem;
padding-bottom: 0.2em;
}
.chapter-title .title-number, .section-title .title-number {
display: none;
}
.paragraph {
line-height: 1.75em;
}
.paragraph + .paragraph {
margin-top: 1em;
}
.dlist {
margin-top: 30px;
}
.dlist dl dt {
float: left;
width: 160px;
clear: left;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.dlist dl dd {
margin-left: 180px;
}
.ulist {
margin-top: 30px;
}
.imageblock {
margin: 30px auto;
}
.imageblock .content img {
max-width: 100%;
}
.imageblock .title {
padding: 10px 0 0;
}
.exampleblock, .quoteblock, .literalblock {
background: #f5f4f4;
padding: 20px;
margin: 30px 0;
}
.exampleblock .title, .quoteblock .title, .literalblock .title {
text-transform: uppercase;
font-size: 0.75em;
font-weight: 400;
color: #979797;
margin-bottom: 10px;
}
.quoteblock blockquote {
margin: 0;
padding: 0;
border: 0;
font-size: inherit;
}
.quoteblock blockquote p:last-child, .quoteblock blockquote ul:last-child, .quoteblock blockquote ol:last-child {
margin-bottom: 9px;
}
.literalblock pre {
border: 0;
padding: 0;
margin: 0;
}
.listingblock {
margin: 30px 0;
}
.listingblock pre {
border: 0;
padding: 0;
margin: 0;
}
.listingblock pre code {
display: block;
padding: 20px;
}
.admonitionblock {
line-height: 1.8em;
padding: 20px;
margin: 30px 0;
}
.admonitionblock .icon {
display: none;
}
.admonitionblock.important {
background: #fce1e1;
border-left: 5px solid #ff6060;
}
.admonitionblock.note, .admonitionblock.tip {
background: #e0f2fc;
border-left: 5px solid #88d5ff;
}
.admonitionblock.caution, .admonitionblock.warning {
background: #fdf3d8;
border-left: 5px solid #f1c654;
}
table.tableblock {
background-color: #fff;
width: 100%;
max-width: 100%;
margin-bottom: 18px;
margin: 30px 0;
}
table.tableblock > thead > tr > th, table.tableblock > tbody > tr > th, table.tableblock > tfoot > tr > th, table.tableblock > thead > tr > td, table.tableblock > tbody > tr > td, table.tableblock > tfoot > tr > td {
padding: 8px;
line-height: 1.42857143;
vertical-align: top;
border-top: 1px solid #eee;
}
table.tableblock > thead > tr > th {
vertical-align: bottom;
border-bottom: 2px solid #eee;
}
table.tableblock > caption + thead > tr:first-child > th, table.tableblock > colgroup + thead > tr:first-child > th, table.tableblock > thead:first-child > tr:first-child > th, table.tableblock > caption + thead > tr:first-child > td, table.tableblock > colgroup + thead > tr:first-child > td, table.tableblock > thead:first-child > tr:first-child > td {
border-top: 0;
}
table.tableblock > tbody + tbody {
border-top: 2px solid #eee;
}
table.tableblock .table {
background-color: #fff;
}
table.tableblock > tbody > tr:nth-of-type(odd) {
background-color: #f7f7f7;
}
table.tableblock > thead > tr > th p:last-child, table.tableblock > tbody > tr > th p:last-child, table.tableblock > tfoot > tr > th p:last-child, table.tableblock > thead > tr > td p:last-child, table.tableblock > tbody > tr > td p:last-child, table.tableblock > tfoot > tr > td p:last-child {
margin-bottom: 0;
}
.loaded .table-of-contents .nav .nav {
display: none;
}
@media (min-width: 1200px) {
.layout-wrapper .layout-title, .layout-wrapper .layout-navigation, .layout-wrapper .layout-content {
max-width: 1200px;
margin-left: auto;
margin-right: auto;
}
}
@media (max-width: 991px) {
html {
font-size: 90%;
}
.layout-content .col-right {
display: none;
}
.layout-content .col-body {
width: 100%;
}
}
@media print {
body {
color: #000;
font-family: Georgia, "Times New Roman", Times, serif;
}
a {
color: #000;
}
h1 {
font-size: 1.6rem;
}
h2 {
font-size: 1.4rem;
}
h3 {
font-size: 1.2rem;
}
h4 {
font-size: 1rem;
}
h5 {
font-size: 0.9rem;
}
.layout-title h1 {
font-size: 2rem;
}
.layout-content .col-right {
display: none;
}
.layout-content .col-body {
width: 100%;
}
.chapter {
margin-bottom: 3rem;
}
.section {
margin-top: 2rem;
}
}
+14
View File
@@ -0,0 +1,14 @@
(function($){
hljs.initHighlightingOnLoad();
$(document).ready(function(){
$('.col-right-wrapper').stick_in_parent({
parent: '.layout-content'
});
$('body').scrollspy({
target: '.table-of-contents'
});
});
})(jQuery);
File diff suppressed because one or more lines are too long
+2
View File
File diff suppressed because one or more lines are too long
+83
View File
@@ -0,0 +1,83 @@
/*
Monokai Sublime style. Derived from Monokai by noformnocontent http://nn.mit-license.org/
*/
.hljs {
display: block;
overflow-x: auto;
padding: 0.5em;
background: #23241f;
}
.hljs,
.hljs-tag,
.hljs-subst {
color: #f8f8f2;
}
.hljs-strong,
.hljs-emphasis {
color: #a8a8a2;
}
.hljs-bullet,
.hljs-quote,
.hljs-number,
.hljs-regexp,
.hljs-literal,
.hljs-link {
color: #ae81ff;
}
.hljs-code,
.hljs-title,
.hljs-section,
.hljs-selector-class {
color: #a6e22e;
}
.hljs-strong {
font-weight: bold;
}
.hljs-emphasis {
font-style: italic;
}
.hljs-keyword,
.hljs-selector-tag,
.hljs-name,
.hljs-attr {
color: #f92672;
}
.hljs-symbol,
.hljs-attribute {
color: #66d9ef;
}
.hljs-params,
.hljs-class .hljs-title {
color: #f8f8f2;
}
.hljs-string,
.hljs-type,
.hljs-built_in,
.hljs-builtin-name,
.hljs-selector-id,
.hljs-selector-attr,
.hljs-selector-pseudo,
.hljs-addition,
.hljs-variable,
.hljs-template-variable {
color: #e6db74;
}
.hljs-comment,
.hljs-deletion,
.hljs-meta {
color: #75715e;
}
+9
View File
@@ -0,0 +1,9 @@
/* ========================================================================
* Bootstrap: scrollspy.js v3.3.7
* http://getbootstrap.com/javascript/#scrollspy
* ========================================================================
* Copyright 2011-2016 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* ======================================================================== */
!function(r){"use strict";function o(t,s){this.$body=r(document.body),this.$scrollElement=r(t).is(document.body)?r(window):r(t),this.options=r.extend({},o.DEFAULTS,s),this.selector=(this.options.target||"")+" .nav li > a",this.offsets=[],this.targets=[],this.activeTarget=null,this.scrollHeight=0,this.$scrollElement.on("scroll.bs.scrollspy",r.proxy(this.process,this)),this.refresh(),this.process()}function s(i){return this.each(function(){var t=r(this),s=t.data("bs.scrollspy"),e="object"==typeof i&&i;s||t.data("bs.scrollspy",s=new o(this,e)),"string"==typeof i&&s[i]()})}o.VERSION="3.3.7",o.DEFAULTS={offset:10},o.prototype.getScrollHeight=function(){return this.$scrollElement[0].scrollHeight||Math.max(this.$body[0].scrollHeight,document.documentElement.scrollHeight)},o.prototype.refresh=function(){var t=this,i="offset",o=0;this.offsets=[],this.targets=[],this.scrollHeight=this.getScrollHeight(),r.isWindow(this.$scrollElement[0])||(i="position",o=this.$scrollElement.scrollTop()),this.$body.find(this.selector).map(function(){var t=r(this),s=t.data("target")||t.attr("href"),e=/^#./.test(s)&&r(s);return e&&e.length&&e.is(":visible")&&[[e[i]().top+o,s]]||null}).sort(function(t,s){return t[0]-s[0]}).each(function(){t.offsets.push(this[0]),t.targets.push(this[1])})},o.prototype.process=function(){var t,s=this.$scrollElement.scrollTop()+this.options.offset,e=this.getScrollHeight(),i=this.options.offset+e-this.$scrollElement.height(),o=this.offsets,r=this.targets,l=this.activeTarget;if(this.scrollHeight!=e&&this.refresh(),i<=s)return l!=(t=r[r.length-1])&&this.activate(t);if(l&&s<o[0])return this.activeTarget=null,this.clear();for(t=o.length;t--;)l!=r[t]&&s>=o[t]&&(void 0===o[t+1]||s<o[t+1])&&this.activate(r[t])},o.prototype.activate=function(t){this.activeTarget=t,this.clear();var s=this.selector+'[data-target="'+t+'"],'+this.selector+'[href="'+t+'"]',e=r(s).parents("li").addClass("active");e.parent(".dropdown-menu").length&&(e=e.closest("li.dropdown").addClass("active")),e.trigger("activate.bs.scrollspy")},o.prototype.clear=function(){r(this.selector).parentsUntil(this.options.target,".active").removeClass("active")};var t=r.fn.scrollspy;r.fn.scrollspy=s,r.fn.scrollspy.Constructor=o,r.fn.scrollspy.noConflict=function(){return r.fn.scrollspy=t,this},r(window).on("load.bs.scrollspy.data-api",function(){r('[data-spy="scroll"]').each(function(){var t=r(this);s.call(t,t.data())})})}(jQuery);
+5
View File
@@ -0,0 +1,5 @@
/**
@license Sticky-kit v1.1.2 | WTFPL | Leaf Corcoran 2015 | http://leafo.net
*/
(function(){var M,Q;M=this.jQuery||window.jQuery,Q=M(window),M.fn.stick_in_parent=function(t){var x,o,C,i,e,P,s,V,F,z,r,I,A,j;for(null==t&&(t={}),j=t.sticky_class,P=t.inner_scrolling,A=t.recalc_every,I=t.parent,F=t.offset_top,z=t.offset_bottom,V=t.spacer,C=t.bottoming,null==F&&(F=0),null==z&&(z=0),null==I&&(I=void 0),null==P&&(P=!0),null==j&&(j="is_stuck"),x=M(document),null==C&&(C=!0),r=function(t){var o,i;return window.getComputedStyle?(t[0],o=window.getComputedStyle(t[0]),i=parseFloat(o.getPropertyValue("width"))+parseFloat(o.getPropertyValue("margin-left"))+parseFloat(o.getPropertyValue("margin-right")),"border-box"!==o.getPropertyValue("box-sizing")&&(i+=parseFloat(o.getPropertyValue("border-left-width"))+parseFloat(o.getPropertyValue("border-right-width"))+parseFloat(o.getPropertyValue("padding-left"))+parseFloat(o.getPropertyValue("padding-right"))),i):t.outerWidth(!0)},i=function(n,l,a,c,p,d,u,f){var h,t,g,m,k,y,b,v,o,_,w,e;if(!n.data("sticky_kit")){if(n.data("sticky_kit",!0),k=x.height(),b=n.parent(),null!=I&&(b=b.closest(I)),!b.length)throw"failed to find stick parent";return h=g=!1,(w=null!=V?V&&n.closest(V):M('<div class="sticky-kit-manual-spacer" />'))&&w.css("position",n.css("position")),(v=function(){var t,o,i;if(!f)return k=x.height(),t=parseInt(b.css("border-top-width"),10),o=parseInt(b.css("padding-top"),10),l=parseInt(b.css("padding-bottom"),10),a=b.offset().top+t+o,c=b.height(),g&&(h=g=!1,null==V&&(n.insertAfter(w),w.detach()),n.css({position:"",top:"",width:"",bottom:""}).removeClass(j),i=!0),p=n.offset().top-(parseInt(n.css("margin-top"),10)||0)-F,d=n.outerHeight(!0),u=n.css("float"),w&&w.css({width:r(n),height:d,display:n.css("display"),"vertical-align":n.css("vertical-align"),float:u}),i?e():void 0})(),m=void 0,y=F,_=A,e=function(){var t,o,i,e,s,r;if(d!==c&&!f)return i=!1,null!=_&&(_-=1)<=0&&(_=A,v(),i=!0),i||x.height()===k||(v(),i=!0),e=Q.scrollTop(),null!=m&&(o=e-m),m=e,g?(C&&(s=c+a<e+d+y+z,h&&!s&&(h=!1,n.css({position:"fixed",bottom:"",top:y}).removeClass("is_bottomed").trigger("sticky_kit:unbottom"))),e<p&&(g=!1,y=F,null==V&&("left"!==u&&"right"!==u||n.insertAfter(w),w.detach()),t={position:"",width:"",top:""},n.css(t).removeClass(j).trigger("sticky_kit:unstick")),P&&(r=Q.height())<d+F&&(h||(y-=o,y=Math.max(r-d,y),y=Math.min(F,y),g&&n.css({top:y+"px"})))):p<e&&(g=!0,(t={position:"fixed",top:y}).width="border-box"===n.css("box-sizing")?n.outerWidth()+"px":n.width()+"px",n.css(t).addClass(j),null==V&&(n.after(w),"left"!==u&&"right"!==u||w.append(n)),n.trigger("sticky_kit:stick")),g&&C&&(null==s&&(s=c+a<e+d+y+z),!h&&s)?(h=!0,"static"===b.css("position")&&b.css({position:"relative"}),n.css({position:"absolute",bottom:l+z,top:"auto"}).addClass("is_bottomed").trigger("sticky_kit:bottom")):void 0},o=function(){return v(),e()},t=function(){if(f=!0,Q.off("touchmove",e),Q.off("scroll",e),Q.off("resize",o),M(document.body).off("sticky_kit:recalc",o),n.off("sticky_kit:detach",t),n.removeData("sticky_kit"),n.css({position:"",bottom:"",top:"",width:""}),b.position("position",""),g)return null==V&&("left"!==u&&"right"!==u||n.insertAfter(w),w.remove()),n.removeClass(j)},Q.on("touchmove",e),Q.on("scroll",e),Q.on("resize",o),M(document.body).on("sticky_kit:recalc",o),n.on("sticky_kit:detach",t),setTimeout(e,0)}},e=0,s=this.length;e<s;e++)o=this[e],i(M(o));return this}}).call(this);
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
@@ -213,8 +213,7 @@ class PaymentMethodConfigurationService {
{ {
// Configuration // Configuration
$settings = $this->settingsService->getSettings($this->getSalesChannelId()); $settings = $this->settingsService->getSettings($this->getSalesChannelId());
$this->setSpaceId($settings->getSpaceId()) $this->setSpaceId($settings->getSpaceId())->setApiClient($settings->getApiClient());
->setApiClient($settings->getApiClient());
$this->disablePaymentMethodConfigurations($context); $this->disablePaymentMethodConfigurations($context);
$this->enablePaymentMethodConfigurations($context); $this->enablePaymentMethodConfigurations($context);
@@ -252,11 +251,16 @@ class PaymentMethodConfigurationService {
{ {
$data = []; $data = [];
$paymentMethodData = []; $paymentMethodData = [];
$salesChannelPaymentMethodData = [];
$criteria = (new Criteria())->addFilter(new EqualsFilter('spaceId', $this->getSpaceId())); $criteria = (new Criteria())->addFilter(new EqualsFilter('state', 'ACTIVE'))
->addFilter(new EqualsFilter('spaceId', $this->getSpaceId()));
$paymentMethodConfigurationEntities = $this->vRPaymentPaymentMethodConfigurationRepository /**
* @var $vRPaymentPMConfigurationRepository
*/
$vRPaymentPMConfigurationRepository = $this->container->get(PaymentMethodConfigurationEntityDefinition::ENTITY_NAME . '.repository');
$paymentMethodConfigurationEntities = $vRPaymentPMConfigurationRepository
->search($criteria, $context) ->search($criteria, $context)
->getEntities(); ->getEntities();
@@ -272,19 +276,14 @@ class PaymentMethodConfigurationService {
]; ];
$paymentMethodData[] = [ $paymentMethodData[] = [
'id' => $paymentMethodConfigurationEntity->getId(), 'id' => $paymentMethodConfigurationEntity->getPaymentMethodId(),
'active' => false, 'active' => false,
]; ];
$salesChannelPaymentMethodData[] = [
'paymentMethodId' => $paymentMethodConfigurationEntity->getId(),
];
} }
try { try {
$this->vRPaymentPaymentMethodConfigurationRepository->update($data, $context); $this->vRPaymentPaymentMethodConfigurationRepository->update($data, $context);
$this->paymentMethodRepository->update($paymentMethodData, $context); $this->paymentMethodRepository->update($paymentMethodData, $context);
$this->salesChannelPaymentRepository->delete($salesChannelPaymentMethodData, $context);
} catch (\Exception $exception) { } catch (\Exception $exception) {
$this->logger->critical($exception->getMessage()); $this->logger->critical($exception->getMessage());
} }
@@ -350,29 +349,68 @@ class PaymentMethodConfigurationService {
*/ */
foreach ($paymentMethodConfigurations as $paymentMethodConfiguration) { foreach ($paymentMethodConfigurations as $paymentMethodConfiguration) {
$paymentMethodConfigurationEntity = $this->getPaymentMethodConfigurationEntity( $entity = $this->getPaymentMethodConfigurationEntity(
$paymentMethodConfiguration->getSpaceId(), $paymentMethodConfiguration->getSpaceId(),
$paymentMethodConfiguration->getId(), $paymentMethodConfiguration->getId(),
$context $context
); );
$id = is_null($paymentMethodConfigurationEntity) ? Uuid::randomHex() : $paymentMethodConfigurationEntity->getId();
$configId = $entity ? $entity->getId() : Uuid::randomHex();
$technicalName = $paymentMethodConfiguration->getName();
$paymentMethodId = $this->getOrCreatePaymentMethodId(
$technicalName,
VRPaymentPaymentHandler::class,
$context
);
$data = [ $data = [
'id' => $id, 'id' => $configId,
'paymentMethodConfigurationId' => $paymentMethodConfiguration->getId(), 'paymentMethodConfigurationId' => $paymentMethodConfiguration->getId(),
'paymentMethodId' => $id, 'paymentMethodId' => $paymentMethodId,
'data' => json_decode(strval($paymentMethodConfiguration), true), 'data' => json_decode(strval($paymentMethodConfiguration), true),
'sortOrder' => $paymentMethodConfiguration->getSortOrder(), 'sortOrder' => $paymentMethodConfiguration->getSortOrder(),
'spaceId' => $paymentMethodConfiguration->getSpaceId(), 'spaceId' => $paymentMethodConfiguration->getSpaceId(),
'state' => CreationEntityState::ACTIVE, 'state' => CreationEntityState::ACTIVE,
]; ];
$this->upsertPaymentMethod($id, $paymentMethodConfiguration, $context);
$this->vRPaymentPaymentMethodConfigurationRepository->upsert([$data], $context); try {
$this->upsertPaymentMethod($paymentMethodId, $paymentMethodConfiguration, $context);
$this->container
->get(PaymentMethodConfigurationEntityDefinition::ENTITY_NAME . '.repository')
->upsert([$data], $context);
} catch (\Exception $e) {
$this->logger->error($e->getMessage(), [$e->getTraceAsString()]);
}
} }
} }
private function getOrCreatePaymentMethodId(string $technicalName, string $handlerIdentifier, Context $context): string
{
$criteria = new Criteria();
$criteria->addFilter(new EqualsFilter('technicalName', $technicalName));
$criteria->setLimit(1);
$existing = $this->paymentMethodRepository->search($criteria, $context)->first();
if ($existing !== null) {
return $existing->getId();
}
$paymentMethodId = Uuid::randomHex();
$this->paymentMethodRepository->upsert([[
'id' => $paymentMethodId,
'handlerIdentifier' => $handlerIdentifier,
'technicalName' => $technicalName,
'name' => $technicalName,
'active' => false,
]], $context);
return $paymentMethodId;
}
/** /**
* Fetch active merchant payment methods from VRPayment API * Fetch active merchant payment methods from VRPayment API
* *
@@ -474,33 +512,38 @@ class PaymentMethodConfigurationService {
* @throws \VRPayment\Sdk\VersioningException * @throws \VRPayment\Sdk\VersioningException
*/ */
protected function upsertPaymentMethod( protected function upsertPaymentMethod(
string $id, string $id,
PaymentMethodConfiguration $paymentMethodConfiguration, PaymentMethodConfiguration $paymentMethodConfiguration,
Context $context Context $context
): void ): void {
{
/** @var PluginIdProvider $pluginIdProvider */ /** @var PluginIdProvider $pluginIdProvider */
$pluginIdProvider = $this->container->get(PluginIdProvider::class); $pluginIdProvider = $this->container->get(PluginIdProvider::class);
$pluginId = $pluginIdProvider->getPluginIdByBaseClass( $pluginId = $pluginIdProvider->getPluginIdByBaseClass(
VRPaymentPayment::class, VRPaymentPayment::class,
$context $context
); );
$data = [ $data = [
'id' => $id, 'id' => $id,
'handlerIdentifier' => VRPaymentPaymentHandler::class, 'handlerIdentifier' => VRPaymentPaymentHandler::class,
'pluginId' => $pluginId, 'pluginId' => $pluginId,
'position' => $paymentMethodConfiguration->getSortOrder() - 100, 'position' => $paymentMethodConfiguration->getSortOrder() - 100,
'afterOrderEnabled' => true, 'afterOrderEnabled' => true,
'active' => true, 'active' => true,
'translations' => $this->getPaymentMethodConfigurationTranslation($paymentMethodConfiguration, $context), 'translations' => $this->getPaymentMethodConfigurationTranslation($paymentMethodConfiguration, $context),
'technicalName' => $paymentMethodConfiguration->getName(),
]; ];
$data['mediaId'] = $this->upsertMedia($id, $paymentMethodConfiguration, $context); $mediaId = $this->upsertMedia($id, $paymentMethodConfiguration, $context);
if ($mediaId) {
$data['mediaId'] = $mediaId;
}
$data = array_filter($data); try {
$this->paymentMethodRepository->upsert([$data], $context);
$this->paymentMethodRepository->upsert([$data], $context); } catch (\Exception $e) {
$this->logger->error($e->getMessage(), [$e->getTraceAsString()]);
}
} }
/** /**
@@ -602,46 +645,58 @@ class PaymentMethodConfigurationService {
* *
* @return string|null * @return string|null
*/ */
/**
* Upload or update Payment Method icons
*/
protected function upsertMedia(string $id, PaymentMethodConfiguration $paymentMethodConfiguration, Context $context): ?string protected function upsertMedia(string $id, PaymentMethodConfiguration $paymentMethodConfiguration, Context $context): ?string
{ {
try { try {
$existingRecord = $this->getMediaDefaultFolderForPaymentMethod($paymentMethodConfiguration, $context); $folderKey = 'payment_method_' . $paymentMethodConfiguration->getId();
if ($existingRecord->count() > 0) { // Check existing default folder
$id = $existingRecord->first()->getId(); $criteria = new Criteria();
$criteria->addFilter(new EqualsFilter('entity', $folderKey));
$existingFolder = $this->mediaDefaultFolderRepository->search($criteria, $context);
$folderId = $id;
if ($existingFolder->count() > 0) {
$folderId = $existingFolder->first()->getId();
} }
// Ensure default folder
$this->mediaDefaultFolderRepository->upsert([ $this->mediaDefaultFolderRepository->upsert([
[ [
'id' => $id, 'id' => $folderId,
'associationFields' => [], 'associationFields' => [],
'entity' => 'payment_method_' . $paymentMethodConfiguration->getId(), 'entity' => $folderKey,
], ],
], $context); ], $context);
// Ensure media folder
$this->mediaFolderRepository->upsert([ $this->mediaFolderRepository->upsert([
[ [
'id' => $id, 'id' => $folderId,
'defaultFolderId' => $id, 'defaultFolderId' => $folderId,
'name' => $paymentMethodConfiguration->getName(), 'name' => $paymentMethodConfiguration->getName(),
'useParentConfiguration' => false, 'useParentConfiguration' => false,
'configuration' => [], 'configuration' => [],
], ],
], $context); ], $context);
/** // Media insert/update
* @var \Shopware\Core\Content\Media\MediaDefinition
*/
$mediaDefinition = $this->container->get(MediaDefinition::class); $mediaDefinition = $this->container->get(MediaDefinition::class);
$this->mediaSerializer->setRegistry($this->serializerRegistry); $this->mediaSerializer->setRegistry($this->serializerRegistry);
$data = [ $data = [
'id' => $id, 'id' => $id,
'title' => $paymentMethodConfiguration->getName(), 'title' => $paymentMethodConfiguration->getName(),
'url' => $paymentMethodConfiguration->getResolvedImageUrl(), 'url' => $paymentMethodConfiguration->getResolvedImageUrl(),
'mediaFolderId' => $id, 'mediaFolderId' => $folderId,
]; ];
$data = $this->mediaSerializer->deserialize(new Config([], [], []), $mediaDefinition, $data); $data = $this->mediaSerializer->deserialize(new Config([], [], []), $mediaDefinition, $data);
$this->mediaRepository->upsert([$data], $context); $this->mediaRepository->upsert([$data], $context);
return $id; return $id;
} catch (\Exception $e) { } catch (\Exception $e) {
$this->logger->critical($e->getMessage(), [$e->getTraceAsString()]); $this->logger->critical($e->getMessage(), [$e->getTraceAsString()]);
@@ -13,7 +13,9 @@ use Symfony\Component\{
}; };
use VRPaymentPayment\Core\{ use VRPaymentPayment\Core\{
Api\Refund\Service\RefundService, Api\Refund\Service\RefundService,
Settings\Service\SettingsService Api\Transaction\Service\TransactionService,
Settings\Service\SettingsService,
Util\Exception\RefundNotSupportedException
}; };
/** /**
@@ -41,16 +43,23 @@ class RefundController extends AbstractController
*/ */
protected $logger; protected $logger;
/**
* @var \VRPaymentPayment\Core\Api\Transaction\Service\TransactionService
*/
protected $transactionService;
/** /**
* RefundController constructor. * RefundController constructor.
* *
* @param \VRPaymentPayment\Core\Api\Refund\Service\RefundService $refundService * @param \VRPaymentPayment\Core\Api\Refund\Service\RefundService $refundService
* @param \VRPaymentPayment\Core\Settings\Service\SettingsService $settingsService * @param \VRPaymentPayment\Core\Settings\Service\SettingsService $settingsService
* @param \VRPaymentPayment\Core\Api\Transaction\Service\TransactionService $transactionService
*/ */
public function __construct(RefundService $refundService, SettingsService $settingsService) public function __construct(RefundService $refundService, SettingsService $settingsService, TransactionService $transactionService)
{ {
$this->settingsService = $settingsService; $this->settingsService = $settingsService;
$this->refundService = $refundService; $this->refundService = $refundService;
$this->transactionService = $transactionService;
} }
/** /**
@@ -82,11 +91,28 @@ class RefundController extends AbstractController
$quantity = (int)$request->request->get('quantity'); $quantity = (int)$request->request->get('quantity');
$lineItemId = $request->request->get('lineItemId'); $lineItemId = $request->request->get('lineItemId');
if ($quantity === null || $quantity <= 0) {
return new Response('refundQuantityZero', Response::HTTP_BAD_REQUEST);
}
$settings = $this->settingsService->getSettings($salesChannelId); $settings = $this->settingsService->getSettings($salesChannelId);
$apiClient = $settings->getApiClient(); $apiClient = $settings->getApiClient();
$transaction = $apiClient->getTransactionService()->read($settings->getSpaceId(), $transactionId); $transaction = $apiClient->getTransactionService()->read($settings->getSpaceId(), $transactionId);
$refund = $this->refundService->create($transaction, $context, $lineItemId, $quantity);
$maxQuantity = $this->refundService->getMaxRefundableQuantity($transaction, $context, $lineItemId);
if ($quantity > $maxQuantity) {
return new Response('refundExceedsQuantity', Response::HTTP_BAD_REQUEST);
}
try {
$refund = $this->refundService->create($transaction, $context, $lineItemId, $quantity);
} catch (RefundNotSupportedException $exception) {
$this->logger->info('Payment method does not support online refunds for transaction: ' . $transactionId);
return new Response('methodDoesNotSupportRefund', Response::HTTP_BAD_REQUEST);
}
if ($refund === null) { 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('Refund was not created. Please check the refund amound or if the item was not refunded before', Response::HTTP_BAD_REQUEST);
} }
@@ -111,11 +137,33 @@ class RefundController extends AbstractController
$transactionId = $request->request->get('transactionId'); $transactionId = $request->request->get('transactionId');
$refundableAmount = $request->request->get('refundableAmount'); $refundableAmount = $request->request->get('refundableAmount');
if ($refundableAmount === null || $refundableAmount <= 0.0) {
return new Response('refundAmountZero', Response::HTTP_BAD_REQUEST);
}
$settings = $this->settingsService->getSettings($salesChannelId); $settings = $this->settingsService->getSettings($salesChannelId);
$apiClient = $settings->getApiClient(); $apiClient = $settings->getApiClient();
$transaction = $apiClient->getTransactionService()->read($settings->getSpaceId(), $transactionId); $transaction = $apiClient->getTransactionService()->read($settings->getSpaceId(), $transactionId);
$this->refundService->createRefundByAmount($transaction, $refundableAmount, $context);
$completed = (float) $transaction->getCompletedAmount();
$refunded = (float) $transaction->getRefundedAmount();
$maxRefund = round($completed - $refunded, 2);
if ($refundableAmount > $maxRefund) {
return new Response('refundExceedsAmount', Response::HTTP_BAD_REQUEST);
}
try {
$refund = $this->refundService->createRefundByAmount($transaction, $refundableAmount, $context);
} catch (RefundNotSupportedException $exception) {
$this->logger->info('Payment method does not support online refunds for transaction: ' . $transactionId);
return new Response('methodDoesNotSupportRefund', Response::HTTP_BAD_REQUEST);
}
if ($refund === null) {
return new Response(null, Response::HTTP_BAD_REQUEST);
}
return new Response(null, Response::HTTP_NO_CONTENT); return new Response(null, Response::HTTP_NO_CONTENT);
} }
@@ -142,7 +190,13 @@ class RefundController extends AbstractController
$apiClient = $settings->getApiClient(); $apiClient = $settings->getApiClient();
$transaction = $apiClient->getTransactionService()->read($settings->getSpaceId(), $transactionId); $transaction = $apiClient->getTransactionService()->read($settings->getSpaceId(), $transactionId);
$this->refundService->createPartialRefund($transaction, $context, $lineItemId, $refundableAmount);
try {
$refund = $this->refundService->createPartialRefund($transaction, $context, $lineItemId, $refundableAmount);
} catch (RefundNotSupportedException $exception) {
$this->logger->info('Payment method does not support online refunds for transaction: ' . $transactionId);
return new Response('methodDoesNotSupportRefund', Response::HTTP_BAD_REQUEST);
}
return new Response(null, Response::HTTP_NO_CONTENT); return new Response(null, Response::HTTP_NO_CONTENT);
} }
+89 -2
View File
@@ -12,14 +12,20 @@ use Shopware\Core\{
}; };
use VRPayment\Sdk\{ use VRPayment\Sdk\{
Model\Refund, Model\Refund,
Model\Transaction Model\Transaction,
Model\CriteriaOperator,
Model\EntityQueryFilter,
Model\EntityQueryFilterType,
Model\EntityQuery,
ApiException
}; };
use VRPaymentPayment\Core\{ use VRPaymentPayment\Core\{
Api\Refund\Entity\RefundEntity, Api\Refund\Entity\RefundEntity,
Api\Transaction\Entity\TransactionEntity, Api\Transaction\Entity\TransactionEntity,
Api\Transaction\Entity\TransactionEntityDefinition, Api\Transaction\Entity\TransactionEntityDefinition,
Settings\Service\SettingsService, Settings\Service\SettingsService,
Util\Payload\RefundPayload Util\Payload\RefundPayload,
Util\Exception\RefundNotSupportedException
}; };
/** /**
@@ -99,6 +105,12 @@ class RefundService
$this->upsert($refund, $context); $this->upsert($refund, $context);
return $refund; return $refund;
} }
} catch (ApiException $exception) {
$message = $exception->getMessage();
$this->logger->critical($message);
if ($exception->getCode() === 442 && str_contains($message, 'does not support online refunds')) {
throw new RefundNotSupportedException($message, 0, $exception);
}
} catch (\Exception $exception) { } catch (\Exception $exception) {
$this->logger->critical($exception->getMessage()); $this->logger->critical($exception->getMessage());
} }
@@ -134,6 +146,12 @@ class RefundService
$this->upsert($refund, $context); $this->upsert($refund, $context);
return $refund; return $refund;
} }
} catch (ApiException $exception) {
$message = $exception->getMessage();
$this->logger->critical($message);
if ($exception->getCode() === 442 && str_contains($message, 'does not support online refunds')) {
throw new RefundNotSupportedException($message, 0, $exception);
}
} catch (\Exception $exception) { } catch (\Exception $exception) {
$this->logger->critical($exception->getMessage()); $this->logger->critical($exception->getMessage());
} }
@@ -170,6 +188,12 @@ class RefundService
$this->upsert($refund, $context); $this->upsert($refund, $context);
return $refund; return $refund;
} }
} catch (ApiException $exception) {
$message = $exception->getMessage();
$this->logger->critical($message);
if ($exception->getCode() === 442 && str_contains($message, 'does not support online refunds')) {
throw new RefundNotSupportedException($message, 0, $exception);
}
} catch (\Exception $exception) { } catch (\Exception $exception) {
$this->logger->critical($exception->getMessage()); $this->logger->critical($exception->getMessage());
} }
@@ -241,4 +265,67 @@ class RefundService
->first(); ->first();
} }
/**
* Get total refunded quantity for transaction's line item by lineItemId.
*
* @param \VRPayment\Sdk\Model\Transaction $transaction
* @param \Shopware\Core\Framework\Context $context
* @param string $lineItemId
*
* @return int
*/
public function getRefundedQuantity(Transaction $transaction, Context $context, string $lineItemId): int {
$transactionEntity = $this->getTransactionEntityByTransactionId($transaction->getId(), $context);
$settings = $this->settingsService->getSettings($transactionEntity->getSalesChannel()->getId());
$apiClient = $settings->getApiClient();
$entityQueryFilter = (new EntityQueryFilter())
->setType(EntityQueryFilterType::LEAF)
->setOperator(CriteriaOperator::EQUALS)
->setFieldName('transaction.id')
->setValue($transaction->getId());
$query = (new EntityQuery())->setFilter($entityQueryFilter);
$refunds = $apiClient->getRefundService()->search($settings->getSpaceId(), $query);
$refundedQuantity = 0;
foreach ($refunds as $refund) {
foreach ($refund->getReductions() as $reduction) {
if ($reduction->getLineItemUniqueId() === $lineItemId) {
$refundedQuantity += (int) $reduction->getQuantityReduction();
}
}
}
return $refundedQuantity;
}
/**
* Get maximum quantity of available items to refund for line item.
*
* @param \VRPayment\Sdk\Model\Transaction $transaction
* @param \Shopware\Core\Framework\Context $context
* @param string $lineItemId
*
* @return int
*/
public function getMaxRefundableQuantity(Transaction $transaction, Context $context, string $lineItemId): int {
$originalQuantity = 0;
foreach ($transaction->getLineItems() as $lineItem) {
if ($lineItem->getUniqueId() === $lineItemId) {
$originalQuantity = (int) $lineItem->getQuantity();
break;
}
}
$refundedQuantity = $this->getRefundedQuantity($transaction, $context, $lineItemId);
$maxQuantity = $originalQuantity - $refundedQuantity;
return $maxQuantity;
}
} }
@@ -32,7 +32,7 @@ use VRPaymentPayment\Core\Api\Refund\Entity\RefundEntityDefinition;
*/ */
class TransactionEntityDefinition extends EntityDefinition { class TransactionEntityDefinition extends EntityDefinition {
public const ENTITY_NAME = 'vrpayment_transaction'; public const ENTITY_NAME = 'vrpayment_transaction_data';
/** /**
* @return string * @return string
@@ -15,6 +15,7 @@ use Shopware\Core\{
System\SalesChannel\SalesChannelContext System\SalesChannel\SalesChannelContext
}; };
use Shopware\Storefront\Page\Checkout\Confirm\CheckoutConfirmPageLoadedEvent; use Shopware\Storefront\Page\Checkout\Confirm\CheckoutConfirmPageLoadedEvent;
use Shopware\Storefront\Page\Account\Order\AccountEditOrderPageLoadedEvent;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use VRPayment\Sdk\{ use VRPayment\Sdk\{
Model\AddressCreate, Model\AddressCreate,
@@ -46,6 +47,9 @@ use VRPaymentPayment\Core\{
Util\Payload\TransactionPayload Util\Payload\TransactionPayload
}; };
use Shopware\Core\Checkout\Order\Aggregate\OrderLineItem\OrderLineItemEntity;
use Shopware\Core\Framework\Struct\ArrayEntity;
/** /**
* Class TransactionService * Class TransactionService
* *
@@ -132,13 +136,12 @@ class TransactionService
$settings = $this->settingsService->getSettings($salesChannelId); $settings = $this->settingsService->getSettings($salesChannelId);
$apiClient = $settings->getApiClient(); $apiClient = $settings->getApiClient();
$failedStates = [ $transactionId = $_SESSION['transactionId'] ?? null;
TransactionState::DECLINE, if ($transactionId !== null) {
TransactionState::FAILED, $pendingTransaction = $this->read($_SESSION['transactionId'], $salesChannelId);
TransactionState::VOIDED, }
];
$pendingTransaction = $this->read($_SESSION['transactionId'], $salesChannelId); if ($transactionId === null || $pendingTransaction === null || $pendingTransaction->getState() !== TransactionState::PENDING) {
if (in_array($pendingTransaction->getState(), $failedStates)) {
unset($_SESSION['transactionId']); unset($_SESSION['transactionId']);
$pendingTransactionId = $this->createPendingTransaction($salesChannelContext); $pendingTransactionId = $this->createPendingTransaction($salesChannelContext);
$pendingTransaction = $this->read($pendingTransactionId, $salesChannelId); $pendingTransaction = $this->read($pendingTransactionId, $salesChannelId);
@@ -181,10 +184,19 @@ class TransactionService
$transaction->getOrderTransaction()->getPaymentMethodId(), $transaction->getOrderTransaction()->getPaymentMethodId(),
$transaction->getOrder()->getSalesChannelId() $transaction->getOrder()->getSalesChannelId()
); );
$_SESSION['transactionId'] = null; $salesChannelContext->getContext()->addExtension(
$_SESSION['arrayOfPossibleMethods'] = null; 'checkoutState',
$_SESSION['addressCheck'] = null; new ArrayEntity([
$_SESSION['currencyCheck'] = null; 'transactionId' => null,
'addressHash' => null,
'currency' => null,
])
);
$salesChannelContext->getContext()->addExtension(
'possibleMethods',
new ArrayEntity(['ids' => []])
);
$this->holdDelivery($transaction->getOrder()->getId(), $salesChannelContext->getContext()); $this->holdDelivery($transaction->getOrder()->getId(), $salesChannelContext->getContext());
@@ -479,14 +491,18 @@ class TransactionService
/** /**
* @param SalesChannelContext $salesChannelContext * @param SalesChannelContext $salesChannelContext
* @param CheckoutConfirmPageLoadedEvent|null $event * @param $event
* @return int * @return int
*/ */
public function createPendingTransaction(SalesChannelContext $salesChannelContext, ?CheckoutConfirmPageLoadedEvent $event = null): int
{ public function createPendingTransaction(SalesChannelContext $salesChannelContext, $event = null): int
{
$expiredTransaction = true; $expiredTransaction = true;
$transactionId = $_SESSION['transactionId'] ?? null; $transactionId = $_SESSION['transactionId'] ?? null;
$settings = $this->settingsService->getValidSettings($salesChannelContext->getSalesChannel()->getId()); $settings = $this->settingsService->getValidSettings($salesChannelContext->getSalesChannel()->getId());
if (!$settings) {
throw new \Exception('Space settings not configured');
}
if ($transactionId) { if ($transactionId) {
$transactionService = $settings->getApiClient()->getTransactionService(); $transactionService = $settings->getApiClient()->getTransactionService();
@@ -495,6 +511,7 @@ class TransactionService
TransactionState::DECLINE, TransactionState::DECLINE,
TransactionState::FAILED, TransactionState::FAILED,
TransactionState::VOIDED, TransactionState::VOIDED,
null
]; ];
if (!in_array($pendingTransaction->getState(), $failedStates)) { if (!in_array($pendingTransaction->getState(), $failedStates)) {
$expiredTransaction = false; $expiredTransaction = false;
@@ -568,13 +585,20 @@ class TransactionService
$lineItems = []; $lineItems = [];
if ($event) { if ($event) {
$cartLineItems = $event->getPage()->getCart()->getLineItems()->getElements(); if ($event instanceof CheckoutConfirmPageLoadedEvent) {
foreach ($cartLineItems as $cartLineItem) { $cartLineItems = $event->getPage()->getCart()->getLineItems()->getElements();
if ($cartLineItem->getType() === CustomProductsLineItemTypes::LINE_ITEM_TYPE_CUSTOMIZED_PRODUCTS) { foreach ($cartLineItems as $cartLineItem) {
continue; if ($cartLineItem->getType() === CustomProductsLineItemTypes::LINE_ITEM_TYPE_CUSTOMIZED_PRODUCTS) {
} continue;
$lineItems[] = $this->createTempLineItem($cartLineItem); }
} $lineItems[] = $this->createTempLineItem($cartLineItem);
}
} elseif ($event instanceof AccountEditOrderPageLoadedEvent) {
$order = $event->getPage()->getOrder();
foreach ($order->getLineItems() as $orderLineItem) {
$lineItems[] = $this->createTempLineItem($orderLineItem);
}
}
} }
$customerId = ""; $customerId = "";
@@ -732,16 +756,28 @@ class TransactionService
* @param LineItem $productData * @param LineItem $productData
* @return LineItemCreate * @return LineItemCreate
*/ */
private function createTempLineItem(LineItem $productData): LineItemCreate private function createTempLineItem($productData): LineItemCreate
{ {
$lineItem = new 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; if ($productData instanceof LineItem) {
} $lineItem->setName($productData->getLabel());
$lineItem->setUniqueId($productData->getId());
$lineItem->setSku($productData->getReferencedId() ?? $productData->getId());
$lineItem->setQuantity($productData->getQuantity());
$lineItem->setAmountIncludingTax($productData->getPrice()->getUnitPrice());
} elseif ($productData instanceof OrderLineItemEntity) {
$lineItem->setName($productData->getLabel());
$lineItem->setUniqueId($productData->getId());
$lineItem->setSku($productData->getProductId() ?? $productData->getIdentifier() ?? $productData->getId());
$lineItem->setQuantity($productData->getQuantity());
$lineItem->setAmountIncludingTax($productData->getUnitPrice());
} else {
throw new \InvalidArgumentException('Unsupported line item type: ' . get_class($productData));
}
$lineItem->setType(LineItemType::PRODUCT);
return $lineItem;
}
} }
@@ -226,6 +226,14 @@ class WebHookController extends AbstractController {
$this->settings = $this->settingsService->getSettings($salesChannelId); $this->settings = $this->settingsService->getSettings($salesChannelId);
$signature = $request->server->get('HTTP_X_SIGNATURE'); $signature = $request->server->get('HTTP_X_SIGNATURE');
$requestJson = json_decode($request->getContent(), true); $requestJson = json_decode($request->getContent(), true);
if ($requestJson['eventId'] == null && $requestJson['entityId'] == null && $requestJson['listenerEntityId'] == null && $requestJson['listenerEntityId'] == null && $requestJson['listenerEntityTechnicalName'] == null && $requestJson['spaceId'] == null) {
throw new \InvalidArgumentException('Empty webhook');
}
if (!$this->settings->getSpaceId() || !$this->settings->getUserId() || !$this->settings->getApplicationKey()) {
throw new \InvalidArgumentException('Not correct webhook configuration for salesChannelId: ' . $salesChannelId . ' Debug: ' . var_dump($requestJson));
}
$apiClient = $this->settings->getApiClient(); $apiClient = $this->settings->getApiClient();
$callBackData->assign($requestJson); $callBackData->assign($requestJson);
@@ -638,25 +646,46 @@ class WebHookController extends AbstractController {
private function unholdDelivery(string $orderId, Context $context): void private function unholdDelivery(string $orderId, Context $context): void
{ {
try { try {
/** $criteria = new Criteria([$orderId]);
* @var OrderDeliveryStateHandler $orderDeliveryStateHandler $criteria->addAssociation('deliveries.stateMachineState');
*/ $order = $this->container->get('order.repository')
$order = $this->getOrderEntity($orderId, $context); ->search($criteria, $context)
/** ->first();
* @var OrderDeliveryEntity $orderDelivery
*/ if (!$order) {
$this->logger->info('Order not found: ' . $orderId);
return;
}
/** @var OrderDeliveryEntity|null $orderDelivery */
$orderDelivery = $order->getDeliveries()?->last(); $orderDelivery = $order->getDeliveries()?->last();
if (null === $orderDelivery) { if (null === $orderDelivery) {
$this->logger->info('No deliveries found for order: ' . $orderId);
return; return;
} }
if ($orderDelivery->getStateMachineState()?->getTechnicalName() !== OrderDeliveryStateHandler::STATE_HOLD){
$orderDeliveryState = $orderDelivery->getStateMachineState();
if (!$orderDeliveryState) {
$this->logger->info('Order delivery state is null for order: ' . $orderId);
return; return;
} }
$technicalName = $orderDeliveryState->getTechnicalName();
$this->logger->info('Order delivery state: ' . $technicalName);
if ($technicalName !== OrderDeliveryStateHandler::STATE_HOLD) {
$this->logger->info('Order delivery is not on hold, skipping unhold process.');
return;
}
/** @var OrderDeliveryStateHandler $orderDeliveryStateHandler */
$orderDeliveryStateHandler = $this->container->get(OrderDeliveryStateHandler::class); $orderDeliveryStateHandler = $this->container->get(OrderDeliveryStateHandler::class);
$orderDeliveryStateHandler->unhold($orderDelivery->getId(), $context); $orderDeliveryStateHandler->unhold($orderDelivery->getId(), $context);
$this->logger->info('Successfully unheld order delivery for order: ' . $orderId);
} catch (\Exception $exception) { } catch (\Exception $exception) {
$this->logger->info($exception->getMessage(), $exception->getTrace()); $this->logger->error('Error unholding order delivery: ' . $exception->getMessage(), $exception->getTrace());
} }
} }
@@ -125,41 +125,21 @@ class WebHookRefundStrategy extends WebHookStrategyBase implements WebhookStrate
$orderId = $this->getOrderIdByTransaction($refund); $orderId = $this->getOrderIdByTransaction($refund);
if(!empty($orderId)) { if(!empty($orderId)) {
$this->executeLocked($orderId, $context, function () use ($orderId, $refund, $context, $request) { $this->executeLocked($orderId, $context, function () use ($orderId, $refund, $context, $request) {
if ($request->getListenerEntityTechnicalName() == WebHookRequest::REFUND && $request->getState() == RefundState::SUCCESSFUL) {
$this->refundService->upsert($refund, $context);
$orderTransactionId = $refund->getTransaction()->getMetaData()[TransactionPayload::VRPAYMENT_METADATA_ORDER_TRANSACTION_ID];
$orderTransaction = $this->getOrderTransaction($orderId, $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,
]
) &&
($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); $transactionByOrderTransactionId = $this->transactionService->getByOrderTransactionId($orderTransactionId, $context);
$totalRefundedAmount = $this->getTotalRefundedAmount($transactionByOrderTransactionId->getTransactionId(), $context); $totalRefundedAmount = $this->getTotalRefundedAmount($transactionByOrderTransactionId->getTransactionId(), $context);
if (floatval($orderTransaction->getAmount()->getTotalPrice()) - $totalRefundedAmount <= 0) { $leftToRefund = floatval($orderTransaction->getAmount()->getTotalPrice()) - $totalRefundedAmount;
if ($leftToRefund > 0) {
$this->orderTransactionStateHandler->refundPartially($orderTransactionId, $context);
} elseif ($leftToRefund === floatval(0)) { // This trick is used, because it's float type and 0 is int
$this->orderTransactionStateHandler->refund($orderTransactionId, $context); $this->orderTransactionStateHandler->refund($orderTransactionId, $context);
} }
} }
}); });
} }
@@ -306,6 +306,14 @@ abstract class WebHookStrategyBase implements WebHookStrategyInterface {
return $this->getOrderEntity($orderId, $context)->getTransactions()->last(); 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.
*/
/** /**
* Unholds the delivery of an order. * Unholds the delivery of an order.
* *
@@ -317,22 +325,46 @@ abstract class WebHookStrategyBase implements WebHookStrategyInterface {
protected function unholdDelivery(string $orderId, Context $context): void protected function unholdDelivery(string $orderId, Context $context): void
{ {
try { try {
$order = $this->getOrderEntity($orderId, $context); $criteria = new Criteria([$orderId]);
/** @var OrderDeliveryEntity $orderDelivery */ $criteria->addAssociation('deliveries.stateMachineState');
$order = $this->container->get('order.repository')
->search($criteria, $context)
->first();
if (!$order) {
$this->logger->info('Order not found: ' . $orderId);
return;
}
/** @var OrderDeliveryEntity|null $orderDelivery */
$orderDelivery = $order->getDeliveries()?->last(); $orderDelivery = $order->getDeliveries()?->last();
if (null === $orderDelivery) { if (null === $orderDelivery) {
$this->logger->info('No deliveries found for order: ' . $orderId);
return; return;
} }
if ($orderDelivery->getStateMachineState()?->getTechnicalName() !== OrderDeliveryStateHandler::STATE_HOLD){ $orderDeliveryState = $orderDelivery->getStateMachineState();
if (!$orderDeliveryState) {
$this->logger->info('Order delivery state is null for order: ' . $orderId);
return; return;
} }
$technicalName = $orderDeliveryState->getTechnicalName();
$this->logger->info('Order delivery state: ' . $technicalName);
if ($technicalName !== OrderDeliveryStateHandler::STATE_HOLD) {
$this->logger->info('Order delivery is not on hold, skipping unhold process.');
return;
}
/** @var OrderDeliveryStateHandler $orderDeliveryStateHandler */ /** @var OrderDeliveryStateHandler $orderDeliveryStateHandler */
$orderDeliveryStateHandler = $this->container->get(OrderDeliveryStateHandler::class); $orderDeliveryStateHandler = $this->container->get(OrderDeliveryStateHandler::class);
$orderDeliveryStateHandler->unhold($orderDelivery->getId(), $context); $orderDeliveryStateHandler->unhold($orderDelivery->getId(), $context);
$this->logger->info('Successfully unheld order delivery for order: ' . $orderId);
} catch (\Exception $exception) { } catch (\Exception $exception) {
$this->logger->info($exception->getMessage(), $exception->getTrace()); $this->logger->error('Error unholding order delivery: ' . $exception->getMessage(), $exception->getTrace());
} }
} }
@@ -9,6 +9,7 @@ use Shopware\Core\{
Checkout\Payment\Cart\PaymentHandler\AsynchronousPaymentHandlerInterface, Checkout\Payment\Cart\PaymentHandler\AsynchronousPaymentHandlerInterface,
Checkout\Payment\Exception\AsyncPaymentFinalizeException, Checkout\Payment\Exception\AsyncPaymentFinalizeException,
Checkout\Payment\Exception\AsyncPaymentProcessException, Checkout\Payment\Exception\AsyncPaymentProcessException,
Checkout\Payment\PaymentException,
Checkout\Payment\Exception\CustomerCanceledAsyncPaymentException, Checkout\Payment\Exception\CustomerCanceledAsyncPaymentException,
Framework\Validation\DataBag\RequestDataBag, Framework\Validation\DataBag\RequestDataBag,
System\SalesChannel\SalesChannelContext System\SalesChannel\SalesChannelContext
@@ -140,7 +141,7 @@ class VRPaymentPaymentHandler implements AsynchronousPaymentHandlerInterface
]); ]);
unset($_SESSION['transactionId']); unset($_SESSION['transactionId']);
$this->logger->info($errorMessage); $this->logger->info($errorMessage);
throw new \Exception($transaction->getOrder()->getId()); throw PaymentException::customerCanceled($transaction->getOrderTransaction()->getId(), $errorMessage);
} }
} else { } else {
$this->orderTransactionStateHandler->paid($transaction->getOrderTransaction()->getId(), $salesChannelContext->getContext()); $this->orderTransactionStateHandler->paid($transaction->getOrderTransaction()->getId(), $salesChannelContext->getContext());
+32 -1
View File
@@ -29,6 +29,31 @@ class SettingsService {
public const CONFIG_STOREFRONT_WEBHOOKS_UPDATE_ENABLED = 'storefrontWebhooksUpdateEnabled'; public const CONFIG_STOREFRONT_WEBHOOKS_UPDATE_ENABLED = 'storefrontWebhooksUpdateEnabled';
public const CONFIG_STOREFRONT_PAYMENTS_UPDATE_ENABLED = 'storefrontPaymentsUpdateEnabled'; public const CONFIG_STOREFRONT_PAYMENTS_UPDATE_ENABLED = 'storefrontPaymentsUpdateEnabled';
/**
* List of config properties whose values allowed to be empty without triggering a warning in logger.
*
* This list is derived from testing of all config properties. The plugin fails only when either spaceId, userId, applicationKey and/or integration is empty.
* On top of that, spaceId, userId, applicationKey are marked as "required" input fields in admin interface.
*
* It is worth considering updating this list whenever a new config is introduced in settings.
* If new config is optional, left empty by design and not required for transactions to work, this list should be updated to avoid false-positive warnings.
*
* @var array
*/
private const ALLOWED_EMPTY_CONFIGS = [
// Options
self::CONFIG_SPACE_VIEW_ID,
self::CONFIG_LINE_ITEM_CONSISTENCY_ENABLED,
self::CONFIG_EMAIL_ENABLED,
// Storefront Options
self::CONFIG_STOREFRONT_INVOICE_DOWNLOAD_ENABLED,
// Advanced Options
self::CONFIG_STOREFRONT_WEBHOOKS_UPDATE_ENABLED,
self::CONFIG_STOREFRONT_PAYMENTS_UPDATE_ENABLED
];
/** /**
* @var \Shopware\Core\System\SystemConfig\SystemConfigService * @var \Shopware\Core\System\SystemConfig\SystemConfigService
*/ */
@@ -132,7 +157,13 @@ class SettingsService {
if ($property === '') { if ($property === '') {
continue; continue;
} }
if (!is_numeric($value) && empty($value)) { // Space view id is only numeric setting which can be 0. If it is, rest of the loop is skipped.
if ($property === self::CONFIG_SPACE_VIEW_ID && $value === 0) {
$propertyValuePairs[$property] = $value;
continue;
}
// Check if $value is empty and is not in the list of configs which are allowed to be empty
if (empty($value) && !in_array($property, self::ALLOWED_EMPTY_CONFIGS, true)) {
$this->logger->warning(strtr('Empty value :value for settings :property.', [':property' => $property, ':value' => $value])); $this->logger->warning(strtr('Empty value :value for settings :property.', [':property' => $property, ':value' => $value]));
} }
$propertyValuePairs[$property] = $value; $propertyValuePairs[$property] = $value;
@@ -2,15 +2,21 @@
namespace VRPaymentPayment\Core\Storefront\Checkout\Controller; namespace VRPaymentPayment\Core\Storefront\Checkout\Controller;
use Psr\Log\LoggerInterface; use Psr\{
Log\LoggerInterface,
Cache\CacheItemPoolInterface
};
use Shopware\Core\{ use Shopware\Core\{
Checkout\Payment\PaymentException,
Checkout\Cart\Cart, Checkout\Cart\Cart,
Checkout\Cart\CartException, Checkout\Cart\CartException,
Checkout\Cart\LineItemFactoryRegistry, Checkout\Cart\LineItemFactoryRegistry,
Checkout\Cart\SalesChannel\CartService, Checkout\Cart\SalesChannel\CartService,
Checkout\Order\Aggregate\OrderLineItem\OrderLineItemCollection, Checkout\Order\Aggregate\OrderLineItem\OrderLineItemCollection,
Checkout\Order\Aggregate\OrderLineItem\OrderLineItemEntity, Checkout\Order\Aggregate\OrderLineItem\OrderLineItemEntity,
Checkout\Order\Aggregate\OrderTransaction\OrderTransactionStateHandler,
Checkout\Order\OrderEntity, Checkout\Order\OrderEntity,
Checkout\Order\OrderDefinition,
Checkout\Order\SalesChannel\AbstractOrderRoute, Checkout\Order\SalesChannel\AbstractOrderRoute,
Framework\Context, Framework\Context,
Framework\DataAbstractionLayer\Search\Criteria, Framework\DataAbstractionLayer\Search\Criteria,
@@ -21,7 +27,9 @@ use Shopware\Core\{
Framework\Uuid\Uuid, Framework\Uuid\Uuid,
Framework\Uuid\Exception\InvalidUuidException, Framework\Uuid\Exception\InvalidUuidException,
Framework\Validation\DataBag\RequestDataBag, Framework\Validation\DataBag\RequestDataBag,
System\SalesChannel\SalesChannelContext System\SalesChannel\SalesChannelContext,
System\StateMachine\StateMachineRegistry,
System\StateMachine\Transition,
}; };
use Shopware\Storefront\{ use Shopware\Storefront\{
Controller\StorefrontController, Controller\StorefrontController,
@@ -31,9 +39,13 @@ use Shopware\Storefront\{
use Symfony\Component\{ use Symfony\Component\{
HttpFoundation\Request, HttpFoundation\Request,
HttpFoundation\Response, HttpFoundation\Response,
HttpFoundation\RedirectResponse,
Routing\Attribute\Route, Routing\Attribute\Route,
Routing\Generator\UrlGeneratorInterface Routing\Generator\UrlGeneratorInterface,
Cache\Adapter\FilesystemAdapter,
DependencyInjection\ParameterBag\ParameterBagInterface
}; };
use Symfony\Contracts\Cache\ItemInterface;
use VRPayment\Sdk\{ use VRPayment\Sdk\{
Model\Transaction, Model\Transaction,
Model\TransactionState Model\TransactionState
@@ -43,10 +55,10 @@ use VRPaymentPayment\Core\{
Settings\Options\Integration, Settings\Options\Integration,
Settings\Service\SettingsService, Settings\Service\SettingsService,
Storefront\Checkout\Struct\CheckoutPageData, Storefront\Checkout\Struct\CheckoutPageData,
Util\Payload\CustomProducts\CustomProductsLineItemTypes Util\Payload\CustomProducts\CustomProductsLineItemTypes,
Util\Payload\TransactionPayload
}; };
/** /**
* Class CheckoutController * Class CheckoutController
* *
@@ -57,6 +69,18 @@ use VRPaymentPayment\Core\{
#[Route(defaults: ['_routeScope' => ['storefront']])] #[Route(defaults: ['_routeScope' => ['storefront']])]
class CheckoutController extends StorefrontController { class CheckoutController extends StorefrontController {
public const ORDER_STATE_CANCEL = 'cancel';
/**
* @var \Shopware\Core\System\StateMachine\StateMachineRegistry
*/
private $stateMachineRegistry;
/**
* @var \Shopware\Core\Checkout\Order\Aggregate\OrderTransaction\OrderTransactionStateHandler
*/
protected $orderTransactionStateHandler;
/** /**
* @var \Shopware\Storefront\Page\GenericPageLoader * @var \Shopware\Storefront\Page\GenericPageLoader
*/ */
@@ -97,6 +121,11 @@ class CheckoutController extends StorefrontController {
*/ */
private $orderRoute; private $orderRoute;
/**
* @var \Psr\Cache\CacheItemPoolInterface
*/
private CacheItemPoolInterface $cache;
/** /**
* PaymentController constructor. * PaymentController constructor.
* *
@@ -106,6 +135,9 @@ class CheckoutController extends StorefrontController {
* @param \VRPaymentPayment\Core\Api\Transaction\Service\TransactionService $transactionService * @param \VRPaymentPayment\Core\Api\Transaction\Service\TransactionService $transactionService
* @param \Shopware\Storefront\Page\GenericPageLoaderInterface $genericLoader * @param \Shopware\Storefront\Page\GenericPageLoaderInterface $genericLoader
* @param \Shopware\Core\Checkout\Order\SalesChannel\AbstractOrderRoute $orderRoute * @param \Shopware\Core\Checkout\Order\SalesChannel\AbstractOrderRoute $orderRoute
* @param \Shopware\Core\Checkout\Order\Aggregate\OrderTransaction\OrderTransactionStateHandler $orderTransactionStateHandler
* @param \Shopware\Core\System\StateMachine\StateMachineRegistry $stateMachineRegistry
* @param Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface $params
*/ */
public function __construct( public function __construct(
LineItemFactoryRegistry $lineItemFactoryRegistry, LineItemFactoryRegistry $lineItemFactoryRegistry,
@@ -113,7 +145,10 @@ class CheckoutController extends StorefrontController {
SettingsService $settingsService, SettingsService $settingsService,
TransactionService $transactionService, TransactionService $transactionService,
GenericPageLoaderInterface $genericLoader, GenericPageLoaderInterface $genericLoader,
AbstractOrderRoute $orderRoute AbstractOrderRoute $orderRoute,
OrderTransactionStateHandler $orderTransactionStateHandler,
StateMachineRegistry $stateMachineRegistry,
ParameterBagInterface $params
) )
{ {
$this->cartService = $cartService; $this->cartService = $cartService;
@@ -122,6 +157,9 @@ class CheckoutController extends StorefrontController {
$this->transactionService = $transactionService; $this->transactionService = $transactionService;
$this->lineItemFactoryRegistry = $lineItemFactoryRegistry; $this->lineItemFactoryRegistry = $lineItemFactoryRegistry;
$this->orderRoute = $orderRoute; $this->orderRoute = $orderRoute;
$this->orderTransactionStateHandler = $orderTransactionStateHandler;
$this->stateMachineRegistry = $stateMachineRegistry;
$this->cache = new FilesystemAdapter('vrpayment', 0, rtrim($params->get('kernel.cache_dir'), '/') . '/vrpayment-cache');
} }
/** /**
@@ -354,6 +392,45 @@ class CheckoutController extends StorefrontController {
throw new MissingRequestParameterException('orderId'); throw new MissingRequestParameterException('orderId');
} }
// Adoption for Headless Storefronts
$orderRepo = $this->container->get('order.repository');
$criteria = new Criteria([$orderId]);
$orderEntity = $orderRepo->search($criteria, $salesChannelContext->getContext())->first();
if($orderEntity->getSalesChannelId() !== $salesChannelContext->getSalesChannelId()) {
$this->settings = $this->settingsService->getSettings($orderEntity->getSalesChannelId());
$trans = $this->getTransaction($orderId, $salesChannelContext->getContext());
// Adoption in case of duplicate requests
// Get order specific value from cache
$cacheKey = 'vrpayment_recreate_order_' . $orderId;
$isFound = $this->cache->get($cacheKey, function (ItemInterface $item) {
$item->expiresAfter(10);
return false;
});
// If value is found in cache - send user directly to successful checkout confirmation page for unpaid transactions
if ($isFound === true && in_array($trans->getState(), [TransactionState::FAILED])) {
$unpaidUrl = $this->getUnpaidUrlFromToken($trans->getSuccessUrl())
?? $this->buildUnpaidUrl($orderEntity->getSalesChannelId(), $salesChannelContext, $orderId);
if ($unpaidUrl) {
return new RedirectResponse(
$unpaidUrl . (parse_url($unpaidUrl, \PHP_URL_QUERY) ? '&' : '?') . 'error-code=' . PaymentException::PAYMENT_CUSTOMER_CANCELED_EXTERNAL
);
}
}
// Cache order specific value for some time on first request
$this->cache->delete($cacheKey);
$this->cache->get($cacheKey, function (ItemInterface $item) {
$item->expiresAfter(10);
return true;
});
return $this->redirect($trans->getSuccessUrl());
}
// End Adoption for Headless Storefronts
try { try {
$this->cartService->deleteCart($salesChannelContext); $this->cartService->deleteCart($salesChannelContext);
$cart = $this->cartService->createNew($salesChannelContext->getToken()); $cart = $this->cartService->createNew($salesChannelContext->getToken());
@@ -367,6 +444,7 @@ class CheckoutController extends StorefrontController {
} }
$transaction = $this->getTransaction($orderId, $salesChannelContext->getContext()); $transaction = $this->getTransaction($orderId, $salesChannelContext->getContext());
$orderTransactionId = $transaction->getMetaData()[TransactionPayload::VRPAYMENT_METADATA_ORDER_TRANSACTION_ID];
if (!empty($transaction->getUserFailureMessage())) { if (!empty($transaction->getUserFailureMessage())) {
$this->addFlash('danger', $transaction->getUserFailureMessage()); $this->addFlash('danger', $transaction->getUserFailureMessage());
} }
@@ -401,6 +479,18 @@ class CheckoutController extends StorefrontController {
} }
// Close the old, existing order to prevent confusion for the customer
$this->orderTransactionStateHandler->cancel($orderTransactionId, $salesChannelContext->getContext());
$this->stateMachineRegistry->transition(
new Transition(
OrderDefinition::ENTITY_NAME,
$orderId,
self::ORDER_STATE_CANCEL,
'stateId'
),
$salesChannelContext->getContext()
);
} catch (\Exception $exception) { } catch (\Exception $exception) {
$this->addFlash('danger', $this->trans('error.addToCartError')); $this->addFlash('danger', $this->trans('error.addToCartError'));
$this->logger->critical($exception->getMessage()); $this->logger->critical($exception->getMessage());
@@ -410,6 +500,74 @@ class CheckoutController extends StorefrontController {
return $this->redirectToRoute('frontend.checkout.confirm.page'); return $this->redirectToRoute('frontend.checkout.confirm.page');
} }
/**
* Tries to return successful checkout confirmation url for unpaid transactions.
*
* It achieves that by getting payment token from successUrl, parsing and decoding
* it, and finally reading the claims.
*
* @param string $successUrl
*
* @return string|null
*/
private function getUnpaidUrlFromToken(string $successUrl): ?string {
$query = [];
parse_str((string) parse_url($successUrl, PHP_URL_QUERY), $query);
$jwt = $query['_sw_payment_token'] ?? null;
if (!$jwt) {
return null;
}
$data = explode('.', $jwt, 3);
if (count($data) !== 3) {
return null;
}
[, $c, ] = $data;
try {
$urlSafeData = strtr($c, '-_', '+/');
$paddedData = str_pad($urlSafeData, \strlen($urlSafeData) % 4, '=');
$decoded = base64_decode($paddedData, true);
if (!$decoded) {
return null;
}
$claims = json_decode(json: $decoded, associative: true, flags: JSON_THROW_ON_ERROR);
$unpaidUrl = $claims['eul'] ?? null;
return $unpaidUrl;
} catch (\Throwable $e) {
$this->logger->warning("CheckoutController::getUnpaidUrlFromToken - JWT parse failed: {errorMessage}", [
'errorMessage' => $e->getMessage(),
]);
return null;
}
}
/**
* Tries to return successful checkout confirmation url for unpaid transactions.
*
* It achieves that by fetching headless storefront's base url,
* and building custom url.
*
* @param string $salesChannelId
* @param SalesChannelContext $salesChannelContext
* @param string $orderId
*
* @return string|null
*/
private function buildUnpaidUrl(string $salesChannelId, SalesChannelContext $salesChannelContext, string $orderId): ?string {
$salesChannelDomainRepo = $this->container->get('sales_channel_domain.repository');
$criteria = new Criteria();
$criteria->addFilter(new EqualsFilter('salesChannelId', $salesChannelId))->setLimit(10);
$domain = $salesChannelDomainRepo->search($criteria, $salesChannelContext->getContext())->first();
if(!$domain) {
return null;
}
$baseUrl = rtrim($domain->getUrl(), '/');
return sprintf('%s/checkout/success/%s/unpaid', $baseUrl, $orderId);
}
/** /**
* @param OrderLineItemCollection $orderItems * @param OrderLineItemCollection $orderItems
* *
@@ -4,33 +4,42 @@ namespace VRPaymentPayment\Core\Storefront\Checkout\Subscriber;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Shopware\Core\{Checkout\Order\Aggregate\OrderTransaction\OrderTransactionCollection, use Shopware\Core\{Checkout\Order\Aggregate\OrderTransaction\OrderTransactionCollection,
Checkout\Order\Aggregate\OrderTransaction\OrderTransactionStates, Checkout\Order\Aggregate\OrderTransaction\OrderTransactionStates,
Checkout\Order\OrderEntity, Checkout\Order\OrderEntity,
Content\MailTemplate\Service\Event\MailBeforeValidateEvent}; Content\MailTemplate\Service\Event\MailBeforeValidateEvent};
use Shopware\Core\Checkout\Payment\PaymentMethodCollection;
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\MultiFilter;
use Shopware\Core\System\SalesChannel\SalesChannelContext; use Shopware\Core\System\SalesChannel\SalesChannelContext;
use Shopware\Storefront\Page\Account\Order\AccountEditOrderPageLoadedEvent;
use Shopware\Storefront\Page\Account\PaymentMethod\AccountPaymentMethodPageLoadedEvent;
use Shopware\Storefront\Page\Checkout\Confirm\CheckoutConfirmPageLoadedEvent; use Shopware\Storefront\Page\Checkout\Confirm\CheckoutConfirmPageLoadedEvent;
use Shopware\Storefront\Page\Checkout\Finish\CheckoutFinishPageLoadedEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use VRPaymentPayment\Core\{Api\Transaction\Service\OrderMailService, use VRPaymentPayment\Core\{Api\Transaction\Service\TransactionService,
Api\Transaction\Service\TransactionService, Checkout\PaymentHandler\VRPaymentPaymentHandler,
Checkout\PaymentHandler\VRPaymentPaymentHandler, Settings\Service\SettingsService,
Settings\Service\SettingsService, Settings\Struct\Settings,
Settings\Struct\Settings, Util\PaymentMethodUtil};
Util\PaymentMethodUtil};
use VRPaymentPayment\Core\Api\PaymentMethodConfiguration\Service\PaymentMethodConfigurationService; use VRPaymentPayment\Core\Api\PaymentMethodConfiguration\Service\PaymentMethodConfigurationService;
use VRPaymentPayment\Sdk\{Model\AddressCreate, use VRPaymentPayment\Sdk\{Model\AddressCreate,
Model\ChargeAttempt, Model\ChargeAttempt,
Model\CreationEntityState, Model\CreationEntityState,
Model\CriteriaOperator, Model\CriteriaOperator,
Model\EntityQuery, Model\EntityQuery,
Model\EntityQueryFilter, Model\EntityQueryFilter,
Model\EntityQueryFilterType, Model\EntityQueryFilterType,
Model\LineItemAttributeCreate, Model\LineItemAttributeCreate,
Model\LineItemCreate, Model\LineItemCreate,
Model\LineItemType, Model\LineItemType,
Model\TaxCreate, Model\TaxCreate,
Model\Transaction, Model\Transaction,
Model\TransactionCreate, Model\TransactionCreate,
Model\TransactionPending}; Model\TransactionPending};
use Shopware\Core\Framework\Struct\ArrayEntity;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Sorting\FieldSorting;
/** /**
* Class CheckoutSubscriber * Class CheckoutSubscriber
@@ -65,6 +74,9 @@ class CheckoutSubscriber implements EventSubscriberInterface
*/ */
private $paymentMethodUtil; private $paymentMethodUtil;
/** @var EntityRepository */
private EntityRepository $paymentMethodRepository;
/** /**
* CheckoutSubscriber constructor. * CheckoutSubscriber constructor.
* *
@@ -73,12 +85,13 @@ class CheckoutSubscriber implements EventSubscriberInterface
* @param \VRPaymentPayment\Core\Settings\Service\SettingsService $settingsService * @param \VRPaymentPayment\Core\Settings\Service\SettingsService $settingsService
* @param \VRPaymentPayment\Core\Util\PaymentMethodUtil $paymentMethodUtil * @param \VRPaymentPayment\Core\Util\PaymentMethodUtil $paymentMethodUtil
*/ */
public function __construct(PaymentMethodConfigurationService $paymentMethodConfigurationService, TransactionService $transactionService, SettingsService $settingsService, PaymentMethodUtil $paymentMethodUtil) public function __construct(PaymentMethodConfigurationService $paymentMethodConfigurationService, TransactionService $transactionService, SettingsService $settingsService, PaymentMethodUtil $paymentMethodUtil, EntityRepository $paymentMethodRepository)
{ {
$this->paymentMethodConfigurationService = $paymentMethodConfigurationService; $this->paymentMethodConfigurationService = $paymentMethodConfigurationService;
$this->transactionService = $transactionService; $this->transactionService = $transactionService;
$this->settingsService = $settingsService; $this->settingsService = $settingsService;
$this->paymentMethodUtil = $paymentMethodUtil; $this->paymentMethodUtil = $paymentMethodUtil;
$this->paymentMethodRepository = $paymentMethodRepository;
} }
/** /**
@@ -99,8 +112,10 @@ class CheckoutSubscriber implements EventSubscriberInterface
public static function getSubscribedEvents(): array public static function getSubscribedEvents(): array
{ {
return [ return [
CheckoutConfirmPageLoadedEvent::class => ['onConfirmPageLoaded', 1], CheckoutConfirmPageLoadedEvent::class => 'onCheckoutConfirmLoaded',
MailBeforeValidateEvent::class => ['onMailBeforeValidate', 1], AccountEditOrderPageLoadedEvent::class => 'onAccountOrderEditLoaded',
AccountPaymentMethodPageLoadedEvent::class => 'onAccountPaymentMethodLoaded',
MailBeforeValidateEvent::class => ['onMailBeforeValidate', 1],
]; ];
} }
@@ -152,109 +167,233 @@ class CheckoutSubscriber implements EventSubscriberInterface
} }
} }
/** /**
* @param \Shopware\Storefront\Page\Checkout\Confirm\CheckoutConfirmPageLoadedEvent $event * @param CheckoutConfirmPageLoadedEvent $event
*/ * @return void
public function onConfirmPageLoaded(CheckoutConfirmPageLoadedEvent $event): void */
{ public function onCheckoutConfirmLoaded(CheckoutConfirmPageLoadedEvent $event): void
try { {
$salesChannelContext = $event->getSalesChannelContext(); try {
$settings = $this->settingsService->getValidSettings($salesChannelContext->getSalesChannel()->getId()); $salesChannelContext = $event->getSalesChannelContext();
if (is_null($settings)) { $settings = $this->settingsService->getValidSettings($salesChannelContext->getSalesChannel()->getId());
$this->logger->notice('Removing payment methods because settings are invalid'); if (is_null($settings)) {
$this->removeVRPaymentPaymentMethodFromConfirmPage($event); $this->logger->notice('Removing payment methods because settings are invalid');
} $this->removeVRPaymentPaymentMethodFromConfirmPage($event);
}
$createdTransactionId = $this->transactionService->createPendingTransaction($salesChannelContext, $event); $createdTransactionId = $this->transactionService->createPendingTransaction($salesChannelContext, $event);
$this->updateTempTransactionIfNeeded($salesChannelContext, $createdTransactionId); $this->updateTempTransactionIfNeeded($salesChannelContext, $createdTransactionId);
$this->getAvailablePaymentMethods($settings, $createdTransactionId); $this->getAvailablePaymentMethods($settings, $createdTransactionId, $salesChannelContext);
$this->setPossiblePaymentMethods($settings->getSpaceId(), $event); $this->setPossiblePaymentMethods($settings->getSpaceId(), $event);
} catch (\Exception $e) { } catch (\Exception $e) {
$this->logger->error($e->getMessage()); $this->logger->error($e->getMessage());
$this->removeVRPaymentPaymentMethodFromConfirmPage($event); $this->removeVRPaymentPaymentMethodFromConfirmPage($event);
} }
} }
/** /**
* @param \Shopware\Storefront\Page\Checkout\Confirm\CheckoutConfirmPageLoadedEvent $event * @param AccountEditOrderPageLoadedEvent $event
*/ * @return void
private function removeVRPaymentPaymentMethodFromConfirmPage(CheckoutConfirmPageLoadedEvent $event): void */
{ public function onAccountOrderEditLoaded(AccountEditOrderPageLoadedEvent $event): void
$paymentMethodCollection = $event->getPage()->getPaymentMethods(); {
$paymentMethodIds = $this->paymentMethodUtil->getVRPaymentPaymentMethodIds($event->getContext()); try {
foreach ($paymentMethodIds as $paymentMethodId) { $this->handlePaymentMethodFiltering($event);
$paymentMethodCollection->remove($paymentMethodId); } catch (\Throwable $e) {
} $this->logger->error($e->getMessage());
} $this->removeVRPaymentPaymentMethodFromConfirmPage($event);
}
}
/** /**
* @param Settings $settings * @param AccountPaymentMethodPageLoadedEvent $event
* @param int $createdTransactionId * @return void
* @return void */
*/ public function onAccountPaymentMethodLoaded(AccountPaymentMethodPageLoadedEvent $event): void
private function getAvailablePaymentMethods(Settings $settings, int $createdTransactionId): void {
{ try {
$transactionService = $settings->getApiClient()->getTransactionService(); $this->handlePaymentMethodFiltering($event);
$possiblePaymentMethods = $transactionService->fetchPaymentMethods( } catch (\Throwable $e) {
$settings->getSpaceId(), $this->logger->error($e->getMessage());
$createdTransactionId, $this->removeVRPaymentPaymentMethodFromConfirmPage($event);
$settings->getIntegration() }
); }
$arrayOfPossibleMethods = [];
foreach ($possiblePaymentMethods as $possiblePaymentMethod) {
$arrayOfPossibleMethods[] = $possiblePaymentMethod->getid();
}
$_SESSION['arrayOfPossibleMethods'] = $arrayOfPossibleMethods;
}
/** /**
* @param int $spaceId * @param \Shopware\Storefront\Page\Checkout\Confirm\CheckoutConfirmPageLoadedEvent $event
* @param CheckoutConfirmPageLoadedEvent $event */
* @return void public function onConfirmPageLoaded(CheckoutConfirmPageLoadedEvent $event): void
*/ {
private function setPossiblePaymentMethods(int $spaceId, CheckoutConfirmPageLoadedEvent $event): void try {
{ $this->handlePaymentMethodFiltering($event);
$localPaymentMethods = []; } catch (\Throwable $e) {
$paymentMethodConfigurations = $this->paymentMethodConfigurationService->getAllPaymentMethodConfigurations($spaceId, $event->getSalesChannelContext()->getContext()); $this->logger->error($e->getMessage());
foreach ($paymentMethodConfigurations as $paymentMethodConfiguration) { $this->removeVRPaymentPaymentMethodFromConfirmPage($event);
$localPaymentMethods[$paymentMethodConfiguration->getId()] = $paymentMethodConfiguration->getPaymentMethodConfigurationId(); }
} }
$paymentMethodCollection = $event->getPage()->getPaymentMethods(); /**
foreach ($paymentMethodCollection as $paymentMethodCollectionItem) { * @param $event
$isVRPaymentPM = VRPaymentPaymentHandler::class == $paymentMethodCollectionItem->getHandlerIdentifier(); * @return void
if (!$isVRPaymentPM) { */
continue; private function handlePaymentMethodFiltering($event): void
} {
$salesChannelContext = $event->getSalesChannelContext();
$settings = $this->settingsService->getValidSettings($salesChannelContext->getSalesChannel()->getId());
$paymentMethodConfigurationId = $localPaymentMethods[$paymentMethodCollectionItem->getId()]; if (is_null($settings)) {
if (!\in_array($paymentMethodConfigurationId, $_SESSION['arrayOfPossibleMethods'])) { $this->logger->notice('Removing payment methods because settings are invalid');
$paymentMethodCollection->remove($paymentMethodCollectionItem->getId()); $this->removeVRPaymentPaymentMethodFromConfirmPage($event);
} return;
} }
}
/** $createdTransactionId = $this->transactionService->createPendingTransaction($salesChannelContext, $event);
* @param SalesChannelContext $salesChannelContext $this->updateTempTransactionIfNeeded($salesChannelContext, $createdTransactionId);
* @param int $createdTransactionId
* @return void
*/
private function updateTempTransactionIfNeeded(SalesChannelContext $salesChannelContext, int $createdTransactionId): void
{
$addressCheck = $_SESSION['addressCheck'] ?? null;
$currencyCheck = $_SESSION['currencyCheck'] ?? null;
$customer = $salesChannelContext->getCustomer(); $this->getAvailablePaymentMethods($settings, $createdTransactionId, $salesChannelContext);
$addressHash = md5(json_encode((array)$customer)); $this->setPossiblePaymentMethods($settings->getSpaceId(), $event);
$currency = $salesChannelContext->getCurrency()->getIsoCode(); }
if (($addressCheck && $currencyCheck) && $addressCheck !== $addressHash || $currencyCheck !== $currency) {
if ($createdTransactionId) { /**
$this->transactionService->updateTempTransaction($salesChannelContext, $createdTransactionId); * @param $event
} * @return void
$_SESSION['arrayOfPossibleMethods'] = null; */
$_SESSION['addressCheck'] = $addressHash; private function removeVRPaymentPaymentMethodFromConfirmPage($event): void
$_SESSION['currencyCheck'] = $currency; {
} $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, SalesChannelContext $salesChannelContext): void
{
$transactionService = $settings->getApiClient()->getTransactionService();
$possiblePaymentMethods = $transactionService->fetchPaymentMethods(
$settings->getSpaceId(),
$createdTransactionId,
$settings->getIntegration()
);
$arrayOfPossibleMethods = [];
foreach ($possiblePaymentMethods as $possiblePaymentMethod) {
$arrayOfPossibleMethods[] = $possiblePaymentMethod->getId();
}
$salesChannelContext->getContext()->addExtension(
'possibleMethods',
new ArrayEntity(['ids' => $arrayOfPossibleMethods])
);
}
/**
* @param int $spaceId
* @param CheckoutConfirmPageLoadedEvent $event
* @return void
*/
private function setPossiblePaymentMethods(int $spaceId, $event): void
{
$paymentIds = [];
$paymentMethodCollection = $event->getPage()->getPaymentMethods();
foreach ($paymentMethodCollection as $paymentMethodCollectionItem) {
$isVRPaymentPM = VRPaymentPaymentHandler::class === $paymentMethodCollectionItem->getHandlerIdentifier();
if (!$isVRPaymentPM) {
$paymentIds[] = $paymentMethodCollectionItem->getId();
}
}
$allowedWLMethods = [];
$paymentMethodConfigurations = $this->paymentMethodConfigurationService
->getAllPaymentMethodConfigurations($spaceId, $event->getSalesChannelContext()->getContext());
foreach ($paymentMethodConfigurations as $paymentMethodConfiguration) {
if ($paymentMethodConfiguration->getPaymentMethod() === null) {
continue;
}
$pmId = $paymentMethodConfiguration->getPaymentMethod()->getId();
$pmConfigId = $paymentMethodConfiguration->getPaymentMethodConfigurationId();
$allowedIds = $this->getAllowedPaymentMethodIds($event->getSalesChannelContext());
if ($paymentMethodConfiguration->getSpaceId() === $spaceId
&& \in_array($pmConfigId, $allowedIds, true)) {
$allowedWLMethods[] = $pmId;
}
}
$allPaymentIds = array_unique(array_merge($paymentIds, $allowedWLMethods));
$collection = new PaymentMethodCollection();
if (!empty($allPaymentIds)) {
$criteria = new Criteria($allPaymentIds);
$criteria->addFilter(new EqualsFilter('active', true));
$criteria->addFilter(
new EqualsFilter('salesChannels.id', $event->getSalesChannelContext()->getSalesChannelId())
);
$criteria->addSorting(new FieldSorting('position', FieldSorting::ASCENDING));
$criteria->addAssociation('media');
$result = $this->paymentMethodRepository->search($criteria, $event->getContext());
foreach ($result->getEntities() as $method) {
if (!$collection->has($method->getId())) {
$collection->add($method);
}
}
}
$event->getPage()->setPaymentMethods($collection);
}
/**
* @param SalesChannelContext $salesChannelContext
* @param int $createdTransactionId
* @return void
*/
private function updateTempTransactionIfNeeded(SalesChannelContext $salesChannelContext, int $createdTransactionId): void
{
$ctx = $salesChannelContext->getContext();
/** @var ArrayEntity|null $ext */
$ext = $ctx->getExtension('checkoutState');
$oldAddressHash = $ext instanceof ArrayEntity ? $ext->get('addressHash') : null;
$oldCurrency = $ext instanceof ArrayEntity ? $ext->get('currency') : null;
$customer = $salesChannelContext->getCustomer();
$addressHash = md5(json_encode((array) $customer));
$currency = $salesChannelContext->getCurrency()->getIsoCode();
$needsUpdate = ($oldAddressHash !== $addressHash) || ($oldCurrency !== $currency);
if ($needsUpdate) {
if ($createdTransactionId) {
$this->transactionService->updateTempTransaction($salesChannelContext, $createdTransactionId);
}
$ctx->addExtension('possibleMethods', new ArrayEntity(['ids' => []]));
$ctx->addExtension(
'checkoutState',
new ArrayEntity([
'addressHash' => $addressHash,
'currency' => $currency,
])
);
}
}
/**
* @param SalesChannelContext $salesChannelContext
* @return array
*/
private function getAllowedPaymentMethodIds(SalesChannelContext $salesChannelContext): array
{
$ext = $salesChannelContext->getContext()->getExtension('possibleMethods');
return $ext instanceof ArrayEntity ? ($ext->get('ids') ?? []) : [];
}
} }
+61 -6
View File
@@ -3,6 +3,7 @@
namespace VRPaymentPayment\Core\Util\Analytics; namespace VRPaymentPayment\Core\Util\Analytics;
use VRPayment\Sdk\ApiClient; use VRPayment\Sdk\ApiClient;
use Shopware\Core\Kernel;
/** /**
* Class Analytics * Class Analytics
@@ -14,29 +15,83 @@ class Analytics {
public const SHOP_SYSTEM = 'x-meta-shop-system'; public const SHOP_SYSTEM = 'x-meta-shop-system';
public const SHOP_SYSTEM_VERSION = 'x-meta-shop-system-version'; public const SHOP_SYSTEM_VERSION = 'x-meta-shop-system-version';
public const SHOP_SYSTEM_AND_VERSION = 'x-meta-shop-system-and-version'; public const SHOP_SYSTEM_AND_VERSION = 'x-meta-shop-system-and-version';
public const PLUGIN_SYSTEM_VERSION = 'x-meta-plugin-version';
/** /**
* @return array * @return array
*/ */
public static function getDefaultData() public static function getDefaultData(): array
{ {
$shopwareVersion = self::getShopwareVersion();
return [ return [
self::SHOP_SYSTEM => 'shopware', self::SHOP_SYSTEM => 'shopware',
self::SHOP_SYSTEM_VERSION => '6', self::SHOP_SYSTEM_VERSION => $shopwareVersion,
self::SHOP_SYSTEM_AND_VERSION => 'shopware-6', self::SHOP_SYSTEM_AND_VERSION => 'shopware-' . $shopwareVersion,
self::PLUGIN_SYSTEM_VERSION => '6.2.1',
]; ];
} }
/** /**
* @param \VRPayment\Sdk\ApiClient $apiClient * @param \VRPayment\Sdk\ApiClient $apiClient
*/ */
public static function addHeaders(ApiClient &$apiClient) public static function addHeaders(ApiClient &$apiClient): void
{ {
$data = self::getDefaultData(); $data = self::getDefaultData();
foreach ($data as $key => $value) { foreach ($data as $key => $value) {
$apiClient->addDefaultHeader($key, $value); $apiClient->addDefaultHeader($key, $value);
} }
} }
/**
* Reads Shopware version and caches it for performance.
*
* @return string
*/
public static function getShopwareVersion(): string
{
static $cachedVersion = null;
if ($cachedVersion !== null) {
return $cachedVersion;
}
$basePath = dirname(__DIR__, 7);
$installedFile = $basePath . '/vendor/composer/installed.php';
if (is_file($installedFile)) {
$installed = include $installedFile;
$packages = [];
if (isset($installed['versions'])) {
$packages = $installed['versions'];
} elseif (is_array($installed)) {
foreach ($installed as $section) {
if (isset($section['versions'])) {
$packages = $section['versions'];
break;
}
}
}
if (isset($packages['shopware/core']['pretty_version'])) {
return $cachedVersion = ltrim($packages['shopware/core']['pretty_version'], 'v');
}
}
$lockFile = $basePath . '/composer.lock';
if (is_file($lockFile)) {
$data = json_decode((string) file_get_contents($lockFile), true);
if (!empty($data['packages'])) {
foreach ($data['packages'] as $package) {
if (($package['name'] ?? '') === 'shopware/core') {
return $cachedVersion = ltrim($package['version'], 'v');
}
}
}
}
return $cachedVersion = Kernel::SHOPWARE_FALLBACK_VERSION;
}
} }
@@ -0,0 +1,8 @@
<?php declare(strict_types=1);
namespace VRPaymentPayment\Core\Util\Exception;
class RefundNotSupportedException extends \LogicException{
}
+51 -26
View File
@@ -12,6 +12,7 @@ use Shopware\Core\{Checkout\Cart\Tax\Struct\CalculatedTaxCollection,
Framework\DataAbstractionLayer\Search\Criteria, Framework\DataAbstractionLayer\Search\Criteria,
System\SalesChannel\SalesChannelContext System\SalesChannel\SalesChannelContext
}; };
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface;
use VRPayment\Sdk\{Model\AddressCreate, use VRPayment\Sdk\{Model\AddressCreate,
@@ -193,10 +194,16 @@ class TransactionPayload extends AbstractPayload
->setShippingAddress($shippingAddress) ->setShippingAddress($shippingAddress)
->setShippingMethod($transactionData['shipping_method']); ->setShippingMethod($transactionData['shipping_method']);
$paymentConfiguration = $this->getPaymentConfiguration($this->salesChannelContext->getPaymentMethod()->getId()); $paymentConfiguration = $this->getPaymentConfiguration(
$this->salesChannelContext->getPaymentMethod()->getId(),
$transactionPayload->setAllowedPaymentMethodConfigurations([$paymentConfiguration->getPaymentMethodConfigurationId()]); $this->settings->getSpaceId()
);
if ($paymentConfiguration) {
$transactionPayload->setAllowedPaymentMethodConfigurations([
$paymentConfiguration->getPaymentMethodConfigurationId()
]);
}
$successUrl = $this->transaction->getReturnUrl() . '&status=paid'; $successUrl = $this->transaction->getReturnUrl() . '&status=paid';
$failedUrl = $this->getFailUrl($this->transaction->getOrder()->getId()) . '&status=fail'; $failedUrl = $this->getFailUrl($this->transaction->getOrder()->getId()) . '&status=fail';
$transactionPayload->setSuccessUrl($successUrl) $transactionPayload->setSuccessUrl($successUrl)
@@ -210,6 +217,23 @@ class TransactionPayload extends AbstractPayload
return $transactionPayload; return $transactionPayload;
} }
/**
* @param string $paymentMethodId
* @param int $spaceId
* @return PaymentMethodConfigurationEntity|null
*/
protected function getPaymentConfiguration(string $paymentMethodId, int $spaceId): ?PaymentMethodConfigurationEntity
{
$criteria = new Criteria();
$criteria->addFilter(new EqualsFilter('paymentMethodId', $paymentMethodId));
$criteria->addFilter(new EqualsFilter('spaceId', $spaceId));
return $this->container->get('vrpayment_payment_method_configuration.repository')
->search($criteria, $this->salesChannelContext->getContext())
->first();
}
/** /**
* Get transaction line items * Get transaction line items
* *
@@ -295,7 +319,9 @@ class TransactionPayload extends AbstractPayload
}); });
if ($discounts) { if ($discounts) {
$this->addDiscountLineItem(current($discounts), $lineItems); foreach ($discounts as $discount) {
$this->addDiscountLineItem($discount, $lineItems);
}
} }
} }
@@ -312,13 +338,14 @@ class TransactionPayload extends AbstractPayload
$lineItem = new LineItemCreate(); $lineItem = new LineItemCreate();
$amount = $this->calculateDiscountAmount($calculatedTax); $amount = $this->calculateDiscountAmount($calculatedTax);
$discountName = $discount->getLabel();
$lineItem->setAmountIncludingTax($amount) $lineItem->setAmountIncludingTax($amount)
->setName(sprintf('DISCOUNT: %s (%s%% tax)', $discount->getLabel(), $rate)) ->setName(sprintf('DISCOUNT: %s (%s%% tax)', $discount->getLabel(), $rate))
->setQuantity(1) ->setQuantity(1)
->setShippingRequired(false) ->setShippingRequired(false)
->setSku('sku-discount-' . $rate, 200) ->setSku('sku-discount-' . $rate . '-' . $discountName, 200)
->setType(LineItemType::DISCOUNT) ->setType(LineItemType::DISCOUNT)
->setUniqueId('coupon-sku-discount-' . $rate . '-' . $rate); ->setUniqueId('coupon-sku-discount-' . $rate . '-' . $rate . '-' . $discountName . '-' . $discount->getId());
$taxRate = new TaxCreate(['title' => 'Discount Tax: ' . $rate, 'rate' => $rate]); $taxRate = new TaxCreate(['title' => 'Discount Tax: ' . $rate, 'rate' => $rate]);
$lineItem->setTaxes([$taxRate]); $lineItem->setTaxes([$taxRate]);
@@ -354,7 +381,9 @@ class TransactionPayload extends AbstractPayload
*/ */
protected function addOptionalLineItems(array &$lineItems): void protected function addOptionalLineItems(array &$lineItems): void
{ {
if (count($this->transaction->getOrder()->getShippingCosts()->getCalculatedTaxes()) === 1) { $shippingCosts = $this->transaction->getOrder()->getShippingCosts();
if ($shippingCosts && $this->transaction->getOrder()->getShippingTotal() > 0) {
if ($shippingLineItem = $this->getShippingLineItem()) { if ($shippingLineItem = $this->getShippingLineItem()) {
$lineItems[] = $shippingLineItem; $lineItems[] = $shippingLineItem;
} }
@@ -616,12 +645,22 @@ class TransactionPayload extends AbstractPayload
{ {
$lineItem = null; $lineItem = null;
$lineItemPriceTotal = array_sum(array_map(static function (LineItemCreate $lineItem) { // Calculate total of all current line items
return $lineItem->getAmountIncludingTax(); $lineItemPriceTotal = array_sum(array_map(static fn(LineItemCreate $li) => $li->getAmountIncludingTax(), $lineItems));
}, $lineItems));
$adjustmentPrice = $this->transaction->getOrder()->getAmountTotal() - $lineItemPriceTotal; $this->logger->debug("LineItem price total before adjustment: $lineItemPriceTotal");
$adjustmentPrice = self::round($adjustmentPrice);
// Get shipping total including taxes from the order
$shippingCosts = $this->transaction->getOrder()->getShippingCosts();
$shippingTotal = $shippingCosts ? self::round($shippingCosts->getTotalPrice()) : 0.0;
// Add shipping to the line items total if it's not already included
$hasShippingLineItem = array_filter($lineItems, static fn(LineItemCreate $li) => $li->getType() === LineItemType::SHIPPING);
if (!$hasShippingLineItem && $shippingTotal > 0) {
$lineItemPriceTotal += $shippingTotal;
}
$adjustmentPrice = self::round($this->transaction->getOrder()->getAmountTotal() - $lineItemPriceTotal);
if (abs($adjustmentPrice) != 0) { if (abs($adjustmentPrice) != 0) {
if ($this->settings->isLineItemConsistencyEnabled()) { if ($this->settings->isLineItemConsistencyEnabled()) {
@@ -777,20 +816,6 @@ class TransactionPayload extends AbstractPayload
return $addressPayload; 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 * Get failure URL
* *
@@ -31,7 +31,7 @@ class Migration1590156974TransactionEntity extends MigrationStep {
public function update(Connection $connection): void public function update(Connection $connection): void
{ {
$connection->executeStatement(' $connection->executeStatement('
CREATE TABLE IF NOT EXISTS `vrpayment_transaction` ( CREATE TABLE IF NOT EXISTS `vrpayment_transaction_tmp` (
`id` BINARY(16) NOT NULL, `id` BINARY(16) NOT NULL,
`data` JSON NOT NULL, `data` JSON NOT NULL,
`payment_method_id` BINARY(16) NOT NULL, `payment_method_id` BINARY(16) NOT NULL,
@@ -42,7 +42,7 @@ class Migration1590646356RefundEntity extends MigrationStep {
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
UNIQUE KEY `refund_id_UNIQUE` (`refund_id`), UNIQUE KEY `refund_id_UNIQUE` (`refund_id`),
KEY `fk.vrp_refund.transaction_id` (`transaction_id`), KEY `fk.vrp_refund.transaction_id` (`transaction_id`),
CONSTRAINT `fk.vrp_refund.transaction_id` FOREIGN KEY (`transaction_id`) REFERENCES `vrpayment_transaction` (`transaction_id`) ON DELETE CASCADE CONSTRAINT `fk.vrp_refund.transaction_id` FOREIGN KEY (`transaction_id`) REFERENCES `vrpayment_transaction_tmp` (`transaction_id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
'); ');
} }
@@ -30,7 +30,7 @@ class Migration1590646356TransactionEntity extends MigrationStep {
public function update(Connection $connection): void public function update(Connection $connection): void
{ {
try { try {
$connection->executeStatement('ALTER TABLE `vrpayment_transaction` ADD COLUMN `confirmation_email_sent` TINYINT(1) NOT NULL DEFAULT 0 AFTER `id`;'); $connection->executeStatement('ALTER TABLE `vrpayment_transaction_tmp` ADD COLUMN `confirmation_email_sent` TINYINT(1) NOT NULL DEFAULT 0 AFTER `id`;');
}catch (\Exception $exception){ }catch (\Exception $exception){
// column probably exists // column probably exists
} }
@@ -33,19 +33,19 @@ class Migration1605701048TransactionEntity extends MigrationStep
try { try {
$connection->executeStatement(' $connection->executeStatement('
ALTER TABLE `vrpayment_transaction` ALTER TABLE `vrpayment_transaction_tmp`
ADD `order_version_id` binary(16) NOT NULL AFTER `transaction_id`; ADD `order_version_id` binary(16) NOT NULL AFTER `transaction_id`;
'); ');
$connection->executeStatement(' $connection->executeStatement('
UPDATE `vrpayment_transaction` t1 UPDATE `vrpayment_transaction_tmp` t1
INNER JOIN `order` t2 INNER JOIN `order` t2
ON t1.order_id = t2.id ON t1.order_id = t2.id
SET t1.order_version_id = t2.version_id; SET t1.order_version_id = t2.version_id;
'); ');
$connection->executeStatement(' $connection->executeStatement('
ALTER TABLE `vrpayment_transaction` ALTER TABLE `vrpayment_transaction_tmp`
DROP FOREIGN KEY `fk.vrp_transaction.order_id`, DROP FOREIGN KEY `fk.vrp_transaction.order_id`,
DROP FOREIGN KEY `fk.vrp_transaction.order_transaction_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.payment_method_id`,
@@ -53,7 +53,7 @@ class Migration1605701048TransactionEntity extends MigrationStep
'); ');
$connection->executeStatement(' $connection->executeStatement('
ALTER TABLE `vrpayment_transaction` ALTER TABLE `vrpayment_transaction_tmp`
ADD CONSTRAINT `fk.vrp_transaction_order_id` FOREIGN KEY (`order_id`, `order_version_id`) 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, REFERENCES `order` (`id`, `version_id`) ON DELETE CASCADE ON UPDATE CASCADE,
ADD CONSTRAINT `fk.vrp_transaction_payment_method_id` FOREIGN KEY (`payment_method_id`) ADD CONSTRAINT `fk.vrp_transaction_payment_method_id` FOREIGN KEY (`payment_method_id`)
@@ -30,7 +30,7 @@ class Migration1684240994TransactionEntity extends MigrationStep {
public function update(Connection $connection): void public function update(Connection $connection): void
{ {
try { try {
$connection->executeStatement('ALTER TABLE `vrpayment_transaction` ADD COLUMN `erp_merchant_id` VARCHAR(255) DEFAULT NULL AFTER `confirmation_email_sent`;'); $connection->executeStatement('ALTER TABLE `vrpayment_transaction_tmp` ADD COLUMN `erp_merchant_id` VARCHAR(255) DEFAULT NULL AFTER `confirmation_email_sent`;');
}catch (\Exception $exception){ }catch (\Exception $exception){
// column probably exists // column probably exists
} }
@@ -0,0 +1,324 @@
<?php declare(strict_types=1);
namespace VRPaymentPayment\Migration;
use Doctrine\DBAL\Connection;
use Shopware\Core\Framework\Migration\MigrationStep;
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
/**
* Class Migration1766067106TransactionEntity
*
* @package VRPaymentPayment\Migration
*/
class Migration1766067106TransactionEntity extends MigrationStep
{
/**
* get creation timestamp
*
* @return int
*/
public function getCreationTimestamp(): int
{
return 1766067106;
}
/**
* update non-destructive changes
*
* @param \Doctrine\DBAL\Connection $connection
*/
public function update(Connection $connection): void
{
$oldTableName = 'vrpayment_transaction';
$tempTableName = 'vrpayment_transaction_tmp';
$realTableName = 'vrpayment_transaction_data';
$logger = new Logger('vrpayment_migration');
$logger->pushHandler(new StreamHandler(dirname(__DIR__, 5) . '/var/log/vrpayment-migration.log'));
$logger->info(
'Migration start', [
'old_table_exists' => $this->tableExists($connection, $oldTableName),
'temp_table_exists' => $this->tableExists($connection, $tempTableName),
'real_table_exists' => $this->tableExists($connection, $realTableName),
]
);
if ($this->tableExists($connection, $tempTableName)) {
// If _temp table exists, it means that this is a fresh installation.
$logger->info('Fresh installation detected.');
$connection->executeStatement(
sprintf('RENAME TABLE `%s` TO `%s`', $tempTableName, $realTableName)
);
$logger->info('Fresh installation finished.');
} else {
// If _temp does not exist, it means that this could be a version upgrade.
$logger->info('Possible plugin upgrade detected.');
if ($this->tableExists($connection, $oldTableName) && !$this->isOldPluginTable($connection, $oldTableName)) {
$logger->info('Old vrpayment_transaction table detected.');
// If vrpayment_transaction already exists and does not belong to old plugin,
// it means that this is indeed a version update.
$this->syncTransactionTable($connection, $oldTableName);
$logger->info('Old vrpayment_transaction table sync finished.');
$this->syncRefundTable($connection, $oldTableName);
$logger->info('Old vrpayment_refund table sync finished.');
$connection->executeStatement(
sprintf('RENAME TABLE `%s` TO `%s`', $oldTableName, $realTableName)
);
$logger->info('Old vrpayment_transaction table renaming completed.');
}
$logger->info('Possible plugin upgrade finished.');
// If vrpayment_transaction exists and it does belong to old plugin,
// it means we must run it in parallel.
}
$logger->info('Migration finished.');
return;
}
/**
* Check if table exists.
*
* @param \Doctrine\DBAL\Connection $connection
* @param string $table
*
* @return bool
*/
public function tableExists(Connection $connection, string $table): bool {
$result = $connection->fetchOne('SHOW TABLES LIKE :table', ['table' => $table]);
return $result !== false && $result !== null;
}
/**
* Check if table belongs to old plugin.
*
* @param \Doctrine\DBAL\Connection $connection
* @param string $table
*
* @return bool
*/
public function isOldPluginTable(Connection $connection, string $table): bool {
$oldTableExclusiveColumns = [
'finalized_at' => 'datetime',
'refunded_at' => 'datetime',
'initial_transaction_mode' => 'varchar',
'manual_capture' => 'tinyint',
'partial_refunded_at' => 'datetime',
'refunded_amount' => 'double',
'amount_to_refund' => 'double',
];
$resultColumns = $connection->fetchAllAssociative(
'SELECT LOWER(COLUMN_NAME) AS column_name, LOWER(DATA_TYPE) AS data_type
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = :table',
['table' => $table]
);
$dbColumns = [];
foreach($resultColumns as $column) {
$dbColumns[$column['column_name']] = $column['data_type'];
}
$oldPluginTable = true;
foreach($oldTableExclusiveColumns as $columnName => $columnType) {
if(!isset($dbColumns[$columnName])) {
$oldPluginTable = false;
break;
}
if ($dbColumns[$columnName] !== $columnType) {
$oldPluginTable = false;
break;
}
}
return $oldPluginTable;
}
/**
* Synchronizes the transaction table with the current/latest version.
*
* @param \Doctrine\DBAL\Connection $connection
* @param string $table
*/
private function syncTransactionTable(Connection $connection, string $table): void {
$this->addColumnIfMissing($connection, $table, 'confirmation_email_sent', "TINYINT(1) NOT NULL DEFAULT 0 AFTER `id`");
$this->addColumnIfMissing($connection, $table, 'erp_merchant_id', "VARCHAR(255) DEFAULT NULL AFTER `confirmation_email_sent`");
$this->addColumnIfMissing($connection, $table, 'data', "LONGTEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL CHECK (json_valid(`data`)) AFTER `erp_merchant_id`");
$this->addColumnIfMissing($connection, $table, 'payment_method_id', "BINARY(16) NOT NULL");
$this->addColumnIfMissing($connection, $table, 'order_id', "BINARY(16) NOT NULL");
$this->addColumnIfMissing($connection, $table, 'order_transaction_id', "BINARY(16) NOT NULL");
$this->addColumnIfMissing($connection, $table, 'space_id', "INT(10) UNSIGNED NOT NULL");
$this->addColumnIfMissing($connection, $table, 'state', "VARCHAR(255) NOT NULL");
$this->addColumnIfMissing($connection, $table, 'sales_channel_id', "BINARY(16) NOT NULL");
$this->addColumnIfMissing($connection, $table, 'transaction_id', "INT(10) UNSIGNED NOT NULL");
$this->addColumnIfMissing($connection, $table, 'order_version_id', "BINARY(16) NOT NULL AFTER `transaction_id`");
$this->addColumnIfMissing($connection, $table, 'created_at', "DATETIME(3) NOT NULL");
$this->addColumnIfMissing($connection, $table, 'updated_at', "DATETIME(3) DEFAULT NULL");
$this->ensureIndexBySql($connection, $table, 'fk.vrp_transaction.order_id', "KEY `fk.vrp_transaction.order_id` (`order_id`)");
$this->ensureIndexBySql($connection, $table, 'fk.vrp_transaction.order_transaction_id', "KEY `fk.vrp_transaction.order_transaction_id` (`order_transaction_id`)");
$this->ensureIndexBySql($connection, $table, 'fk.vrp_transaction.payment_method_id', "KEY `fk.vrp_transaction.payment_method_id` (`payment_method_id`)");
$this->ensureIndexBySql($connection, $table, 'fk.vrp_transaction.sales_channel_id', "KEY `fk.vrp_transaction.sales_channel_id` (`sales_channel_id`)");
$this->ensureIndexBySql($connection, $table, 'fk.vrp_transaction', "KEY `fk.vrp_transaction` (`order_id`,`order_version_id`)");
$this->ensureForeignKey(
$connection,
$table,
'fk.vrp_transaction_order_id',
['order_id', 'order_version_id'],
'order',
['id', 'version_id'],
'CASCADE',
'CASCADE'
);
$this->ensureForeignKey(
$connection,
$table,
'fk.vrp_transaction_payment_method_id',
['payment_method_id'],
'payment_method',
['id'],
'RESTRICT',
'CASCADE'
);
$this->ensureForeignKey(
$connection,
$table,
'fk.vrp_transaction_sales_channel_id',
['sales_channel_id'],
'sales_channel',
['id'],
'RESTRICT',
'CASCADE'
);
}
/**
* Synchronizes the parts of the refund table related to transactions with the current/latest version.
*
* @param \Doctrine\DBAL\Connection $connection
* @param string $table
*/
private function syncRefundTable(Connection $connection, string $table): void {
$refundTable = 'vrpayment_refund';
$this->ensureIndexBySql($connection, $refundTable, 'fk.vrp_refund.transaction_id', "KEY `fk.vrp_refund.transaction_id` (`transaction_id`)");
$this->ensureForeignKey(
$connection,
$refundTable,
'fk.vrp_refund.transaction_id',
['transaction_id'],
$table,
['transaction_id'],
'CASCADE',
null
);
}
/**
* Adds column to the table if it's missing.
*
* @param \Doctrine\DBAL\Connection $connection
* @param string $table
* @param string $column
* @param string $sqlFragment
*/
private function addColumnIfMissing(Connection $connection, string $table, string $column, string $sqlFragment): void {
if ($this->columnExists($connection, $table, $column)) {
return;
}
$connection->executeStatement(
sprintf("ALTER TABLE `%s` ADD COLUMN `%s` %s", $table, $column, $sqlFragment)
);
}
/**
* Adds index to the table if it's missing.
*
* @param \Doctrine\DBAL\Connection $connection
* @param string $table
* @param string $indexName
* @param string $sqlFragment
*/
private function ensureIndexBySql(Connection $connection, string $table, string $indexName, string $sqlFragment): void {
if ($this->indexExists($connection, $table, $indexName)) {
return;
}
$connection->executeStatement(
sprintf("ALTER TABLE `%s` ADD %s", $table, $sqlFragment)
);
}
/**
* Adds foreign key constraint to the table if it's missing.
*
* @param \Doctrine\DBAL\Connection $connection
* @param string $table
* @param string $constraintName
* @param string $columns
* @param string $refTable
* @param string $refColumns
* @param string|null $onDelete
* @param string|null $onUpdate
*/
private function ensureForeignKey(
Connection $connection,
string $table,
string $constraintName,
array $columns,
string $refTable,
array $refColumns,
?string $onDelete,
?string $onUpdate
): void {
if ($this->foreignKeyExists($connection, $table, $constraintName)) {
return;
}
$columnsList = '`' . implode('`,`', $columns) . '`';
$refColumnsList = '`' . implode('`,`', $refColumns) . '`';
$connection->executeStatement(
sprintf(
"ALTER TABLE `%s`
ADD CONSTRAINT `%s` FOREIGN KEY (%s)
REFERENCES `%s` (%s)%s%s",
$table,
$constraintName,
$columnsList,
$refTable,
$refColumnsList,
$onDelete ? " ON DELETE {$onDelete}" : "",
$onUpdate ? " ON UPDATE {$onUpdate}" : ""
)
);
}
/**
* Check if foreign key constraint exists.
*
* @param \Doctrine\DBAL\Connection $connection
* @param string $table
* @param string $constraintName
*
* @return bool
*/
private function foreignKeyExists(Connection $connection, string $table, $constraintName): bool {
$result = $connection->fetchOne(
"SELECT 1 FROM information_schema.referential_constraints
WHERE constraint_schema = DATABASE()
AND table_name = ?
AND constraint_name = ?
LIMIT 1",
[$table,$constraintName]
);
return $result !== false && $result !== null;
}
/**
* update destructive changes
*
* @param \Doctrine\DBAL\Connection $connection
*/
public function updateDestructive(Connection $connection): void
{
// implement update destructive
}
}
BIN
View File
Binary file not shown.
@@ -70,9 +70,24 @@ Component.register('vrpayment-order-action-refund-by-amount', {
}); });
}).catch((errorResponse) => { }).catch((errorResponse) => {
try { try {
var errorTitle = errorResponse?.response?.data?.errors?.[0]?.title ?? this.$tc('vrpayment-order.refundAction.refundCreateError.errorTitle')
var errorMessage;
switch(errorResponse.response.data) {
case 'refundAmountZero':
errorMessage = this.$tc('vrpayment-order.refundAction.refundCreateError.messageRefundAmountIsZero');
break;
case 'refundExceedsAmount':
errorMessage = this.$tc('vrpayment-order.refundAction.refundCreateError.messageRefundAmountExceedsAvailableBalance');
break;
case 'methodDoesNotSupportRefund':
errorMessage = this.$tc('vrpayment-order.refundAction.refundCreateError.messagePaymentMethodDoesNotSupportRefund');
break;
default:
errorMessage = errorResponse.response.data.errors[0].detail;
}
this.createNotificationError({ this.createNotificationError({
title: errorResponse.response.data.errors[0].title, title: errorTitle,
message: errorResponse.response.data.errors[0].detail, message: errorMessage,
autoClose: false autoClose: false
}); });
} catch (e) { } catch (e) {
@@ -69,9 +69,18 @@ Component.register('vrpayment-order-action-refund-partial', {
}); });
}).catch((errorResponse) => { }).catch((errorResponse) => {
try { try {
var errorTitle = errorResponse?.response?.data?.errors?.[0]?.title ?? this.$tc('vrpayment-order.refundAction.refundCreateError.errorTitle')
var errorMessage;
switch(errorResponse.response.data) {
case 'methodDoesNotSupportRefund':
errorMessage = this.$tc('vrpayment-order.refundAction.refundCreateError.messagePaymentMethodDoesNotSupportRefund');
break;
default:
errorMessage = errorResponse.response.data.errors[0].detail;
}
this.createNotificationError({ this.createNotificationError({
title: errorResponse.response.data.errors[0].title, title: errorTitle,
message: errorResponse.response.data.errors[0].detail, message: errorMessage,
autoClose: false autoClose: false
}); });
} catch (e) { } catch (e) {
@@ -70,9 +70,18 @@ Component.register('vrpayment-order-action-refund-selected', {
}); });
}).catch((errorResponse) => { }).catch((errorResponse) => {
try { try {
var errorTitle = errorResponse?.response?.data?.errors?.[0]?.title ?? this.$tc('vrpayment-order.refundAction.refundCreateError.errorTitle')
var errorMessage;
switch(errorResponse.response.data) {
case 'methodDoesNotSupportRefund':
errorMessage = this.$tc('vrpayment-order.refundAction.refundCreateError.messagePaymentMethodDoesNotSupportRefund');
break;
default:
errorMessage = errorResponse.response.data.errors[0].detail;
}
this.createNotificationError({ this.createNotificationError({
title: errorResponse.response.data.errors[0].title, title: errorTitle,
message: errorResponse.response.data.errors[0].detail, message: errorMessage,
autoClose: false autoClose: false
}); });
} catch (e) { } catch (e) {
@@ -9,6 +9,7 @@
:max="this.$parent.$parent.itemRefundableQuantity" :max="this.$parent.$parent.itemRefundableQuantity"
:min="0" :min="0"
v-model:value="refundQuantity" v-model:value="refundQuantity"
number-type="int"
:label="$tc('vrpayment-order.refund.refundQuantity.label')"> :label="$tc('vrpayment-order.refund.refundQuantity.label')">
</sw-number-field> </sw-number-field>
@@ -68,9 +68,24 @@ Component.register('vrpayment-order-action-refund', {
}); });
}).catch((errorResponse) => { }).catch((errorResponse) => {
try { try {
var errorTitle = errorResponse?.response?.data?.errors?.[0]?.title ?? this.$tc('vrpayment-order.refundAction.refundCreateError.errorTitle')
var errorMessage;
switch(errorResponse.response.data) {
case 'refundQuantityZero':
errorMessage = this.$tc('vrpayment-order.refundAction.refundCreateError.messageRefundQuantityIsZero');
break;
case 'refundExceedsQuantity':
errorMessage = this.$tc('vrpayment-order.refundAction.refundCreateError.messageRefundQuantityExceedsAvailableBalance');
break;
case 'methodDoesNotSupportRefund':
errorMessage = this.$tc('vrpayment-order.refundAction.refundCreateError.messagePaymentMethodDoesNotSupportRefund');
break;
default:
errorMessage = errorResponse.response.data.errors[0].detail;
}
this.createNotificationError({ this.createNotificationError({
title: errorResponse.response.data.errors[0].title, title: errorTitle,
message: errorResponse.response.data.errors[0].detail, message: errorMessage,
autoClose: false autoClose: false
}); });
} catch (e) { } catch (e) {
@@ -98,7 +98,7 @@
<template #actions="{ item }"> <template #actions="{ item }">
<sw-context-menu-item <sw-context-menu-item
:disabled="transaction.state != 'FULFILL' || item.refundableQuantity != item.quantity || item.refundableAmount == 0 || item.itemRefundedAmount > 0 || item.itemRefundedQuantity > 0" :disabled="transaction.state != 'FULFILL' || item.refundableQuantity != item.quantity || item.refundableAmount == 0 || item.itemRefundedAmount > 0 || item.itemRefundedQuantity > 0"
@click="lineItemRefund(item.uniqueId)"> @click="lineItemRefund(item.uniqueId, item.quantity)">
{{ $tc('vrpayment-order.buttons.label.refund-whole-line-item') }} {{ $tc('vrpayment-order.buttons.label.refund-whole-line-item') }}
</sw-context-menu-item> </sw-context-menu-item>
@@ -332,12 +332,12 @@ Component.register('vrpayment-order-detail', {
this.modalType = ''; this.modalType = '';
}, },
lineItemRefund(lineItemId) { lineItemRefund(lineItemId, itemQuantity) {
this.isLoading = true; this.isLoading = true;
this.VRPaymentRefundService.createRefund( this.VRPaymentRefundService.createRefund(
this.transactionData.transactions[0].metaData.salesChannelId, this.transactionData.transactions[0].metaData.salesChannelId,
this.transactionData.transactions[0].id, this.transactionData.transactions[0].id,
0, itemQuantity,
lineItemId lineItemId
).then(() => { ).then(() => {
this.createNotificationSuccess({ this.createNotificationSuccess({
@@ -351,9 +351,18 @@ Component.register('vrpayment-order-detail', {
}); });
}).catch((errorResponse) => { }).catch((errorResponse) => {
try { try {
var errorTitle = errorResponse?.response?.data?.errors?.[0]?.title ?? this.$tc('vrpayment-order.refundAction.refundCreateError.errorTitle')
var errorMessage;
switch(errorResponse.response.data) {
case 'methodDoesNotSupportRefund':
errorMessage = this.$tc('vrpayment-order.refundAction.refundCreateError.messagePaymentMethodDoesNotSupportRefund');
break;
default:
errorMessage = errorResponse.response.data.errors[0].detail;
}
this.createNotificationError({ this.createNotificationError({
title: errorResponse.response.data.errors[0].title, title: errorTitle,
message: errorResponse.response.data.errors[0].detail, message: errorMessage,
autoClose: false autoClose: false
}); });
} catch (e) { } catch (e) {
@@ -385,7 +394,7 @@ Component.register('vrpayment-order-detail', {
// Force the DOM to update before proceeding with the asynchronous operations // Force the DOM to update before proceeding with the asynchronous operations
this.$nextTick(() => { this.$nextTick(() => {
const refundPromises = this.selectedItems.map((item) => { const refundPromises = this.selectedItems.map((item) => {
return this.lineItemRefundBulk(item.uniqueId); // Simulated refund action with delay return this.lineItemRefundBulk(item.uniqueId, item.quantity); // Simulated refund action with delay
}); });
// Wait for all refund promises to complete // Wait for all refund promises to complete
@@ -410,7 +419,7 @@ Component.register('vrpayment-order-detail', {
}); });
} }
}, },
lineItemRefundBulk(lineItemId) { lineItemRefundBulk(lineItemId, itemQuantity) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.VRPaymentRefundService.createRefund( this.VRPaymentRefundService.createRefund(
this.transactionData.transactions[0].metaData.salesChannelId, this.transactionData.transactions[0].metaData.salesChannelId,
@@ -427,11 +436,20 @@ Component.register('vrpayment-order-detail', {
}) })
.catch((errorResponse) => { .catch((errorResponse) => {
try { try {
this.createNotificationError({ var errorTitle = errorResponse?.response?.data?.errors?.[0]?.title ?? this.$tc('vrpayment-order.refundAction.refundCreateError.errorTitle')
title: errorResponse.response.data.errors[0].title, var errorMessage;
message: errorResponse.response.data.errors[0].detail, switch(errorResponse.response.data) {
autoClose: false case 'methodDoesNotSupportRefund':
}); errorMessage = this.$tc('vrpayment-order.refundAction.refundCreateError.messagePaymentMethodDoesNotSupportRefund');
break;
default:
errorMessage = errorResponse.response.data.errors[0].detail;
}
this.createNotificationError({
title: errorTitle,
message: errorMessage,
autoClose: false
});
} catch (e) { } catch (e) {
this.createNotificationError({ this.createNotificationError({
title: errorResponse.title, title: errorResponse.title,
@@ -77,7 +77,15 @@
"successMessage": "Ihre Rückerstattung war erfolgreich", "successMessage": "Ihre Rückerstattung war erfolgreich",
"successTitle": "Erfolg", "successTitle": "Erfolg",
"maxAvailableItemsToRefund": "Maximal Verfügbare Artikel zum Erstatten", "maxAvailableItemsToRefund": "Maximal Verfügbare Artikel zum Erstatten",
"maxAvailableAmountToRefund": "Maximal verfügbarer Erstattungsbetrag" "maxAvailableAmountToRefund": "Maximal verfügbarer Erstattungsbetrag",
"refundCreateError": {
"errorTitle": "Fehler beim Erstellen der Rückerstattung.",
"messageRefundAmountExceedsAvailableBalance": "Der Rückerstattungsbetrag übersteigt das verfügbare Guthaben.",
"messageRefundAmountIsZero": "Der Rückerstattungsbetrag muss größer als 0 sein.",
"messageRefundQuantityExceedsAvailableBalance": "Rückerstattung nach Menge überschreitet die maximal verfügbare Anzahl an Artikeln zur Rückerstattung.",
"messageRefundQuantityIsZero": "Rückerstattung nach Menge muss größer als 0 sein.",
"messagePaymentMethodDoesNotSupportRefund": "Die Zahlungsmethode unterstützt keine Online-Rückerstattungen."
}
}, },
"transactionHistory": { "transactionHistory": {
"cardTitle": "Einzelheiten", "cardTitle": "Einzelheiten",
@@ -9,7 +9,6 @@
"void": "Cancel authorization", "void": "Cancel authorization",
"refund-whole-line-item": "Refund whole line item", "refund-whole-line-item": "Refund whole line item",
"refund-line-item-by-quantity": "Refund by quantity", "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-selected": "Refund selected",
"refund-line-item-parial": "Partial refund" "refund-line-item-parial": "Partial refund"
} }
@@ -78,7 +77,15 @@
"successMessage": "Your refund was successful.", "successMessage": "Your refund was successful.",
"successTitle": "Success", "successTitle": "Success",
"maxAvailableItemsToRefund": "Maximum available items to refund", "maxAvailableItemsToRefund": "Maximum available items to refund",
"maxAvailableAmountToRefund": "Maximum available amount to refund" "maxAvailableAmountToRefund": "Maximum available amount to refund",
"refundCreateError": {
"errorTitle": "Error while creating the refund.",
"messageRefundAmountExceedsAvailableBalance": "Refund amount exceeds available balance.",
"messageRefundAmountIsZero": "Refund amount must be greater than 0.",
"messageRefundQuantityExceedsAvailableBalance": "Refund by quantity exceeds maximum available items to refund.",
"messageRefundQuantityIsZero": "Refund by quantity must be greater than 0.",
"messagePaymentMethodDoesNotSupportRefund": "Payment method does not support online refunds."
}
}, },
"transactionHistory": { "transactionHistory": {
"cardTitle": "Details", "cardTitle": "Details",
@@ -77,7 +77,15 @@
"successMessage": "Votre remboursement a été effectué avec succès.", "successMessage": "Votre remboursement a été effectué avec succès.",
"successTitle": "Succès", "successTitle": "Succès",
"maxAvailableItemsToRefund": "Nombre maximum d'articles disponibles pour le remboursement", "maxAvailableItemsToRefund": "Nombre maximum d'articles disponibles pour le remboursement",
"maxAvailableAmountToRefund": "Montant maximal disponible pour le remboursement" "maxAvailableAmountToRefund": "Montant maximal disponible pour le remboursement",
"refundCreateError": {
"errorTitle": "Erreur lors de la création du remboursement.",
"messageRefundAmountExceedsAvailableBalance": "Le montant du remboursement dépasse le solde disponible.",
"messageRefundAmountIsZero": "Le montant du remboursement doit être supérieur à 0.",
"messageRefundQuantityExceedsAvailableBalance": "Le remboursement par quantité dépasse le nombre maximal darticles remboursables.",
"messageRefundQuantityIsZero": "Le remboursement par quantité doit être supérieur à 0.",
"messagePaymentMethodDoesNotSupportRefund": "Le mode de paiement ne prend pas en charge les remboursements en ligne."
}
}, },
"transactionHistory": { "transactionHistory": {
"cardTitle": "Détails", "cardTitle": "Détails",
@@ -77,7 +77,15 @@
"successMessage": "Il tuo rimborso è andato a buon fine.", "successMessage": "Il tuo rimborso è andato a buon fine.",
"successTitle": "Successo", "successTitle": "Successo",
"maxAvailableItemsToRefund": "Numero massimo di articoli disponibili da rimborsare", "maxAvailableItemsToRefund": "Numero massimo di articoli disponibili da rimborsare",
"maxAvailableAmountToRefund": "Importo massimo disponibile per il rimborso" "maxAvailableAmountToRefund": "Importo massimo disponibile per il rimborso",
"refundCreateError": {
"errorTitle": "Errore durante la creazione del rimborso.",
"messageRefundAmountExceedsAvailableBalance": "LL'importo del rimborso supera il saldo disponibile.",
"messageRefundAmountIsZero": "L'importo del rimborso deve essere superiore a 0.",
"messageRefundQuantityExceedsAvailableBalance": "Il rimborso per quantità supera il numero massimo di articoli rimborsabili.",
"messageRefundQuantityIsZero": "Il rimborso per quantità deve essere maggiore di 0.",
"messagePaymentMethodDoesNotSupportRefund": "Il metodo di pagamento non supporta i rimborsi online."
}
}, },
"transactionHistory": { "transactionHistory": {
"cardTitle": "Dettagli", "cardTitle": "Dettagli",
@@ -14,7 +14,7 @@
{% block vrpayment_settings_content_card_channel_config_credentials_card_container_settings_space_id %} {% block vrpayment_settings_content_card_channel_config_credentials_card_container_settings_space_id %}
<sw-inherit-wrapper <sw-inherit-wrapper
v-model:value="actualConfigData[CONFIG_SPACE_ID]" v-model:value="actualConfigData[CONFIG_SPACE_ID]"
:inheritedValue="selectedSalesChannelId === null ? null : allConfigs['null'][CONFIG_SPACE_ID]" :inheritedValue="getInheritedValue(CONFIG_SPACE_ID)"
:customInheritationCheckFunction="checkNumberFieldInheritance"> :customInheritationCheckFunction="checkNumberFieldInheritance">
<template #content="props"> <template #content="props">
<sw-number-field <sw-number-field
@@ -23,7 +23,7 @@
:mapInheritance="props" :mapInheritance="props"
:label="$tc('vrpayment-settings.settingForm.credentials.spaceId.label')" :label="$tc('vrpayment-settings.settingForm.credentials.spaceId.label')"
:helpText="$tc('vrpayment-settings.settingForm.credentials.spaceId.tooltipText')" :helpText="$tc('vrpayment-settings.settingForm.credentials.spaceId.tooltipText')"
:disabled="props.isInherited || !acl.can('vrpayment.editor')" :disabled="!acl.can('vrpayment.editor')"
:value="props.currentValue" :value="props.currentValue"
:error="spaceIdErrorState" :error="spaceIdErrorState"
@update:value="props.updateCurrentValue"> @update:value="props.updateCurrentValue">
@@ -35,7 +35,7 @@
{% block vrpayment_settings_content_card_channel_config_credentials_card_container_settings_user_id %} {% block vrpayment_settings_content_card_channel_config_credentials_card_container_settings_user_id %}
<sw-inherit-wrapper <sw-inherit-wrapper
v-model:value="actualConfigData[CONFIG_USER_ID]" v-model:value="actualConfigData[CONFIG_USER_ID]"
:inheritedValue="selectedSalesChannelId === null ? null : allConfigs['null'][CONFIG_USER_ID]" :inheritedValue="getInheritedValue(CONFIG_USER_ID)"
:customInheritationCheckFunction="checkNumberFieldInheritance"> :customInheritationCheckFunction="checkNumberFieldInheritance">
<template #content="props"> <template #content="props">
<sw-number-field <sw-number-field
@@ -44,7 +44,7 @@
:mapInheritance="props" :mapInheritance="props"
:label="$tc('vrpayment-settings.settingForm.credentials.userId.label')" :label="$tc('vrpayment-settings.settingForm.credentials.userId.label')"
:helpText="$tc('vrpayment-settings.settingForm.credentials.userId.tooltipText')" :helpText="$tc('vrpayment-settings.settingForm.credentials.userId.tooltipText')"
:disabled="props.isInherited || !acl.can('vrpayment.editor')" :disabled="!acl.can('vrpayment.editor')"
:value="props.currentValue" :value="props.currentValue"
:error="userIdErrorState" :error="userIdErrorState"
@update:value="props.updateCurrentValue"> @update:value="props.updateCurrentValue">
@@ -56,7 +56,7 @@
{% block vrpayment_settings_content_card_channel_config_credentials_card_container_settings_application_key %} {% block vrpayment_settings_content_card_channel_config_credentials_card_container_settings_application_key %}
<sw-inherit-wrapper <sw-inherit-wrapper
v-model:value="actualConfigData[CONFIG_APPLICATION_KEY]" v-model:value="actualConfigData[CONFIG_APPLICATION_KEY]"
:inheritedValue="selectedSalesChannelId === null ? null : allConfigs['null'][CONFIG_APPLICATION_KEY]" :inheritedValue="getInheritedValue(CONFIG_APPLICATION_KEY)"
:customInheritationCheckFunction="checkTextFieldInheritance"> :customInheritationCheckFunction="checkTextFieldInheritance">
<template #content="props"> <template #content="props">
<sw-password-field <sw-password-field
@@ -66,7 +66,7 @@
:mapInheritance="props" :mapInheritance="props"
:label="$tc('vrpayment-settings.settingForm.credentials.applicationKey.label')" :label="$tc('vrpayment-settings.settingForm.credentials.applicationKey.label')"
:helpText="$tc('vrpayment-settings.settingForm.credentials.applicationKey.tooltipText')" :helpText="$tc('vrpayment-settings.settingForm.credentials.applicationKey.tooltipText')"
:disabled="props.isInherited || !acl.can('vrpayment.editor')" :disabled="!acl.can('vrpayment.editor')"
:value="props.currentValue" :value="props.currentValue"
:error="applicationKeyErrorState" :error="applicationKeyErrorState"
@update:value="props.updateCurrentValue"> @update:value="props.updateCurrentValue">
@@ -6,7 +6,7 @@ import constants from '../../page/vrpayment-settings/configuration-constants'
const {Component, Mixin} = Shopware; const {Component, Mixin} = Shopware;
Component.register('sw-vrpayment-credentials', { Component.register('sw-vrpayment-credentials', {
template: template, template,
name: 'VRPaymentCredentials', name: 'VRPaymentCredentials',
@@ -29,7 +29,9 @@ Component.register('sw-vrpayment-credentials', {
}, },
selectedSalesChannelId: { selectedSalesChannelId: {
required: true type: [String, null],
required: false,
default: null
}, },
spaceIdFilled: { spaceIdFilled: {
type: Boolean, type: Boolean,
@@ -68,38 +70,44 @@ Component.register('sw-vrpayment-credentials', {
}; };
}, },
computed: {
currentConfig() {
if (this.selectedSalesChannelId && this.allConfigs[this.selectedSalesChannelId]) {
return this.allConfigs[this.selectedSalesChannelId];
}
return this.allConfigs['null'] || {};
}
},
methods: { methods: {
checkTextFieldInheritance(value) { checkTextFieldInheritance(value) {
if (typeof value !== 'string') { return !value || value.length <= 0;
return true; },
}
return value.length <= 0; checkNumberFieldInheritance(value) {
}, return value == null || value === '';
},
checkNumberFieldInheritance(value) { checkBoolFieldInheritance(value) {
if (typeof value !== 'number') { return typeof value !== 'boolean';
return true; },
}
return value.length <= 0;
},
checkBoolFieldInheritance(value) {
return typeof value !== 'boolean';
},
// Emits the 'check-api-connection-event' with the current API connection parameters. // Emits the 'check-api-connection-event' with the current API connection parameters.
// Used to trigger API connection testing from this component. // Used to trigger API connection testing from this component.
emitCheckApiConnectionEvent() { emitCheckApiConnectionEvent() {
const apiConnectionParams = { const apiConnectionParams = {
spaceId: this.actualConfigData[constants.CONFIG_SPACE_ID], spaceId: this.currentConfig[constants.CONFIG_SPACE_ID],
userId: this.actualConfigData[constants.CONFIG_USER_ID], userId: this.currentConfig[constants.CONFIG_USER_ID],
applicationKey: this.actualConfigData[constants.CONFIG_APPLICATION_KEY] applicationKey: this.currentConfig[constants.CONFIG_APPLICATION_KEY]
}; };
this.$emit('check-api-connection-event', apiConnectionParams); this.$emit('check-api-connection-event', apiConnectionParams);
},
getInheritedValue(key) {
return this.allConfigs['null']?.[key] ?? null;
} }
} }
}); });
@@ -1,145 +1,145 @@
{% block vrpayment_settings %} {% block vrpayment_settings %}
<sw-page class="vrpayment-settings">
{% block vrpayment_settings_header %} <sw-page class="vrpayment-settings">
<template #smart-bar-header> {% block vrpayment_settings_header %}
<h2> <template #smart-bar-header>
{{ $tc('sw-settings.index.title') }} <h2>
<sw-icon name="small-arrow-medium-right" small></sw-icon> {{ $tc('sw-settings.index.title') }}
{{ $tc('vrpayment-settings.header') }} <mt-icon name="small-arrow-medium-right" size="16px"></mt-icon>
</h2> {{ $tc('vrpayment-settings.header') }}
</template> </h2>
{% endblock %} </template>
{% block vrpayment_settings_actions %}
<template #smart-bar-actions>
{% block vrpayment_settings_actions_save %}
<sw-button-process
v-model:value="isSaveSuccessful"
class="sw-settings-login-registration__save-action"
variant="primary"
:isLoading="isLoading"
:disabled="isLoading"
@click="onSave">
{{ $tc('vrpayment-settings.settingForm.save') }}
</sw-button-process>
{% endblock %}
</template>
{% endblock %}
{% block vrpayment_settings_content %}
<template #content>
{% block vrpayment_settings_content_card %}
<sw-card-view>
{% block vrpayment_settings_content_card_channel_config %}
<sw-sales-channel-config v-model:value="config"
ref="configComponent"
:domain="CONFIG_DOMAIN">
{% block vrpayment_settings_content_card_channel_config_sales_channel %}
<template #select="{ onInput, selectedSalesChannelId, salesChannel }">
{% block vrpayment_settings_content_card_channel_config_sales_channel_card %}
<sw-card title="Sales Channel Switch">
{% block vrpayment_settings_content_card_channel_config_sales_channel_card_title %}
<sw-single-select
v-model:value="selectedSalesChannelId"
labelProperty="translated.name"
valueProperty="id"
:mapInheritance="props"
:isLoading="isLoading"
:options="salesChannel"
@update:value="onInput">
</sw-single-select>
{% endblock %}
{% block vrpayment_settings_content_card_channel_config_sales_channel_card_footer %}
<template #footer>
{% block vrpayment_settings_content_card_channel_config_sales_channel_card_footer_container %}
<sw-container columns="2fr 1fr" gap="0px 30px">
{% block vrpayment_settings_content_card_channel_config_sales_channel_card_footer_container_text %}
<p>{{ $tc('vrpayment-settings.salesChannelCard.button.description') }}</p>
{% endblock %}
{% block vrpayment_settings_content_card_channel_config_sales_channel_card_footer_container_button %}
<sw-button-process
v-model:value="isSetDefaultPaymentSuccessful"
:isLoading="isSettingDefaultPaymentMethods"
@click="onSetPaymentMethodDefault">
{{ $tc('vrpayment-settings.salesChannelCard.button.label') }}
</sw-button-process>
{% endblock %}
</sw-container>
{% endblock %}
</template>
{% endblock %}
</sw-card>
{% endblock %}
</template>
{% endblock %}
{% block vrpayment_settings_content_card_channel_config_cards %}
<template #content="{ actualConfigData, allConfigs, selectedSalesChannelId }">
<div v-if="actualConfigData">
<sw-vrpayment-credentials
:actualConfigData="actualConfigData"
:allConfigs="allConfigs"
:selectedSalesChannelId="selectedSalesChannelId"
:spaceIdErrorState="spaceIdErrorState"
:userIdErrorState="userIdErrorState"
:applicationKeyErrorState="applicationKeyErrorState"
:spaceIdFilled="spaceIdFilled"
:userIdFilled="userIdFilled"
:applicationKeyFilled="applicationKeyFilled"
:isLoading="isLoading"
:isTesting="isTesting"
@check-api-connection-event="onCheckApiConnection"
></sw-vrpayment-credentials>
<sw-vrpayment-options
:actualConfigData="actualConfigData"
:allConfigs="allConfigs"
:isLoading="isLoading"
:selectedSalesChannelId="selectedSalesChannelId"
>
</sw-vrpayment-options>
<sw-vrpayment-storefront-options
:actualConfigData="actualConfigData"
:allConfigs="allConfigs"
:isLoading="isLoading"
:selectedSalesChannelId="selectedSalesChannelId"
>
</sw-vrpayment-storefront-options>
<sw-vrpayment-advanced-options
:actualConfigData="actualConfigData"
:allConfigs="allConfigs"
:isLoading="isLoading"
:selectedSalesChannelId="selectedSalesChannelId"
>
</sw-vrpayment-advanced-options>
</div>
</template>
{% endblock %}
</sw-sales-channel-config>
{% endblock %}
{% block vrpayment_settings_content_card_loading %}
<sw-loader v-if="isLoading"></sw-loader>
{% endblock %}
</sw-card-view>
{% endblock %} {% endblock %}
</template> {% block vrpayment_settings_actions %}
{% endblock %} <template #smart-bar-actions>
</sw-page> {% block vrpayment_settings_actions_save %}
<mt-button
v-model:value="isSaveSuccessful"
class="sw-settings-login-registration__save-action"
variant="primary"
:isLoading="isLoading"
:disabled="isLoading"
@click="onSave">
{{ $tc('vrpayment-settings.settingForm.save') }}
</mt-button>
{% endblock %}
</template>
{% endblock %}
{% block vrpayment_settings_content %}
<template #content>
{% block vrpayment_settings_content_card %}
<mt-card-view>
{% block vrpayment_settings_content_card_channel_config %}
<sw-sales-channel-config v-model:value="config"
ref="configComponent"
:domain="CONFIG_DOMAIN">
{% block vrpayment_settings_content_card_channel_config_sales_channel %}
<template #select="{ onInput, selectedSalesChannelId, salesChannel }">
{% block vrpayment_settings_content_card_channel_config_sales_channel_card %}
<mt-card title="Sales Channel Switch">
{% block vrpayment_settings_content_card_channel_config_sales_channel_card_title %}
<sw-single-select
:value="selectedSalesChannelId"
:options="salesChannel.map(sc => ({ id: sc.id, name: sc.translated.name }))"
labelProperty="name"
valueProperty="id"
:isLoading="isLoading"
@update:value="onInput"
/>
{% endblock %}
{% block vrpayment_settings_content_card_channel_config_sales_channel_card_footer %}
<template #footer>
{% block vrpayment_settings_content_card_channel_config_sales_channel_card_footer_container %}
<sw-container columns="2fr 1fr" gap="0px 30px">
{% block vrpayment_settings_content_card_channel_config_sales_channel_card_footer_container_text %}
<p>{{ $tc('vrpayment-settings.salesChannelCard.button.description') }}</p>
{% endblock %}
{% block vrpayment_settings_content_card_channel_config_sales_channel_card_footer_container_button %}
<sw-button
variant="primary"
v-model:value="isSetDefaultPaymentSuccessful"
:isLoading="isSettingDefaultPaymentMethods"
@click="onSetPaymentMethodDefault">
{{ $tc('vrpayment-settings.salesChannelCard.button.label') }}
</sw-button>
{% endblock %}
</sw-container>
{% endblock %}
</template>
{% endblock %}
</mt-card>
{% endblock %}
</template>
{% endblock %}
{% block vrpayment_settings_content_card_channel_config_cards %}
<template #content="{ actualConfigData, allConfigs, selectedSalesChannelId }">
<div v-if="actualConfigData">
<sw-vrpayment-credentials
:actualConfigData="actualConfigData"
:allConfigs="allConfigs"
:selectedSalesChannelId="selectedSalesChannelId"
:spaceIdErrorState="spaceIdErrorState"
:userIdErrorState="userIdErrorState"
:applicationKeyErrorState="applicationKeyErrorState"
:spaceIdFilled="spaceIdFilled"
:userIdFilled="userIdFilled"
:applicationKeyFilled="applicationKeyFilled"
:isLoading="isLoading"
:isTesting="isTesting"
@check-api-connection-event="onCheckApiConnection"
></sw-vrpayment-credentials>
<sw-vrpayment-options
:actualConfigData="actualConfigData"
:allConfigs="allConfigs"
:isLoading="isLoading"
:selectedSalesChannelId="selectedSalesChannelId"
>
</sw-vrpayment-options>
<sw-vrpayment-storefront-options
:actualConfigData="actualConfigData"
:allConfigs="allConfigs"
:isLoading="isLoading"
:selectedSalesChannelId="selectedSalesChannelId"
>
</sw-vrpayment-storefront-options>
<sw-vrpayment-advanced-options
:actualConfigData="actualConfigData"
:allConfigs="allConfigs"
:isLoading="isLoading"
:selectedSalesChannelId="selectedSalesChannelId"
>
</sw-vrpayment-advanced-options>
</div>
</template>
{% endblock %}
</sw-sales-channel-config>
{% endblock %}
{% block vrpayment_settings_content_card_loading %}
<mt-loader v-if="isLoading"></mt-loader>
{% endblock %}
</mt-card-view>
{% endblock %}
</template>
{% endblock %}
</sw-page>
{% endblock %} {% endblock %}
@@ -80,7 +80,7 @@ Component.register('vrpayment-settings', {
watch: { watch: {
config: { config: {
handler(configData) { handler(configData) {
const defaultConfig = this.$refs.configComponent.allConfigs.null; const defaultConfig = (this.$refs.configComponent.allConfigs || {}).null || {};
const salesChannelId = this.$refs.configComponent.selectedSalesChannelId; const salesChannelId = this.$refs.configComponent.selectedSalesChannelId;
if (salesChannelId === null) { if (salesChannelId === null) {
File diff suppressed because one or more lines are too long
@@ -10,6 +10,7 @@
<service id="VRPaymentPayment\Core\Api\Refund\Controller\RefundController" public="true"> <service id="VRPaymentPayment\Core\Api\Refund\Controller\RefundController" public="true">
<argument type="service" id="VRPaymentPayment\Core\Api\Refund\Service\RefundService"/> <argument type="service" id="VRPaymentPayment\Core\Api\Refund\Service\RefundService"/>
<argument type="service" id="VRPaymentPayment\Core\Settings\Service\SettingsService"/> <argument type="service" id="VRPaymentPayment\Core\Settings\Service\SettingsService"/>
<argument type="service" id="VRPaymentPayment\Core\Api\Transaction\Service\TransactionService"/>
<call method="setLogger"> <call method="setLogger">
<argument type="service" id="monolog.logger.vrpayment_payment"/> <argument type="service" id="monolog.logger.vrpayment_payment"/>
</call> </call>
@@ -13,6 +13,9 @@
<argument type="service" id="VRPaymentPayment\Core\Api\Transaction\Service\TransactionService"/> <argument type="service" id="VRPaymentPayment\Core\Api\Transaction\Service\TransactionService"/>
<argument type="service" id="Shopware\Storefront\Page\GenericPageLoader"/> <argument type="service" id="Shopware\Storefront\Page\GenericPageLoader"/>
<argument type="service" id="Shopware\Core\Checkout\Order\SalesChannel\OrderRoute"/> <argument type="service" id="Shopware\Core\Checkout\Order\SalesChannel\OrderRoute"/>
<argument type="service" id="Shopware\Core\Checkout\Order\Aggregate\OrderTransaction\OrderTransactionStateHandler"/>
<argument type="service" id="Shopware\Core\System\StateMachine\StateMachineRegistry"/>
<argument type="service" id="Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface"/>
<call method="setLogger"> <call method="setLogger">
<argument type="service" id="monolog.logger.vrpayment_payment"/> <argument type="service" id="monolog.logger.vrpayment_payment"/>
</call> </call>
@@ -30,6 +33,7 @@
<argument id="VRPaymentPayment\Core\Api\Transaction\Service\TransactionService" type="service"/> <argument id="VRPaymentPayment\Core\Api\Transaction\Service\TransactionService" type="service"/>
<argument id="VRPaymentPayment\Core\Settings\Service\SettingsService" type="service"/> <argument id="VRPaymentPayment\Core\Settings\Service\SettingsService" type="service"/>
<argument id="VRPaymentPayment\Core\Util\PaymentMethodUtil" type="service"/> <argument id="VRPaymentPayment\Core\Util\PaymentMethodUtil" type="service"/>
<argument id="payment_method.repository" type="service"/>
<call method="setLogger"> <call method="setLogger">
<argument type="service" id="monolog.logger.vrpayment_payment"/> <argument type="service" id="monolog.logger.vrpayment_payment"/>
</call> </call>
BIN
View File
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long