Compare commits

...

10 Commits

Author SHA1 Message Date
andrewrowanwallee 219a13af6e Release 6.2.2 2026-05-04 15:16:50 +02:00
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
75 changed files with 8248 additions and 938 deletions
+37 -1
View File
@@ -1,3 +1,39 @@
# 6.2.2
- Fixed issue with payment method availability rules being ignored
# 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
- Implement payment page integration.
- Fixed bug with duplicate payment methods appearing
@@ -141,7 +177,7 @@
- Added settings to control update of webhooks and payment methods
# 4.0.15
- Adjust VRPay/SW6 documentation - how to do refunds
- Adjust VR Payment/SW6 documentation - how to do refunds
# 4.0.14
- Support for Shopware 6.4.6
+37 -1
View File
@@ -1,3 +1,39 @@
# 6.2.2
- Problem behoben, bei dem die Verfügbarkeitsregeln für Zahlungsmethoden ignoriert wurden
# 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
- Integration der Zahlungsseite implementieren.
- Fehler mit doppelten angezeigten Zahlungsmethoden behoben
@@ -139,7 +175,7 @@
- Einstellungen zur Steuerung der Aktualisierung von Webhooks und Zahlungsmethoden hinzugefügt
# 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
- 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
identification within third-party archives.
Copyright 2025 VR Payment GmbH
Copyright 2026 VR Payment GmbH
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
+87 -46
View File
@@ -1,79 +1,120 @@
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.
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).
## **Overview**
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
- Shopware 6.5.x or Shopware 6.6.x. See table below.
- PHP minimum version supported by the each shop version.
- **Shopware Version:** 6.5.x or 6.6.x (see [compatibility table](#compatibility)).
- **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
___________________________________________________________________________________
| Shopware 6 version | Plugin major version | Supported until |
|-------------------------------|------------------------|------------------------|
| Shopware 6.6.x | 6.x | Further notice |
| Shopware 6.5.x | 5.x | October 2024 |
-----------------------------------------------------------------------------------
## Documentation
- For English documentation click [here](https://docs.plugin-documentation.vr-payment.de/vr-payment/shopware-6/6.2.2/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.2/docs/de/documentation.html)
- Pour la documentation Française, cliquez [ici](https://docs.plugin-documentation.vr-payment.de/vr-payment/shopware-6/6.2.2/docs/fr/documentation.html)
- Per la documentazione in tedesco, clicca [qui](https://docs.plugin-documentation.vr-payment.de/vr-payment/shopware-6/6.2.2/docs/it/documentation.html)
## Installation
You can use **Composer** or **install manually**
### Composer
The preferred method is via [composer](https://getcomposer.org). Follow the
[installation instructions](https://getcomposer.org/doc/00-intro.md) if you do not already have
composer installed.
Once composer is installed, execute the following command from the shop root to install the plugin:
### **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
```
#### 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:
1. Download the latest [Release](../../releases)
2. Extract the ZIP to custom/plugins/VRPaymentPayment.
```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
Copy
bin/console plugin:refresh
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.
## Update
### Logs and debugging
To view the logs please run the command below:
### 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
cd shopware/install/dir
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.6.x | 6.x | December 2026 |
| Shopware 6.5.x | 5.x | October 2024 |
-----------------------------------------------------------------------------------
### Troubleshooting
**Logs**: Check payment logs with:
```bash
Copy
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
+10 -10
View File
@@ -2,7 +2,7 @@
"authors": [
{
"homepage": "https://www.vr-payment.de/",
"name": "VRPay"
"name": "VR Payment"
}
],
"autoload": {
@@ -12,15 +12,15 @@
},
"description": "VRPayment integration for Shopware 6",
"extra": {
"copyright": "(c) by VRPay",
"copyright": "(c) by VR Payment",
"description": {
"de-DE": "VRPayment integration f\u00fcr Shopware 6",
"de-DE": "VRPayment integration für 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"
},
"label": {
"de-DE": "VRPayment Produkte f\u00fcr Shopware 6",
"de-DE": "VRPayment Produkte für Shopware 6",
"en-GB": "VRPayment Products for Shopware 6",
"fr-FR": "VRPayment Produits for Shopware 6",
"it-IT": "VRPayment Prodotti per Shopware 6"
@@ -41,7 +41,7 @@
},
"homepage": "https://www.vr-payment.de//",
"keywords": [
"VRPay",
"VR Payment",
"payment",
"php",
"shopware"
@@ -53,11 +53,11 @@
"ext-json": "*",
"ext-mbstring": "*",
"php": ">=8.2",
"shopware/core": "6.6.*",
"shopware/core": "~6.6.0",
"shopware/administration": "~6.6.0",
"shopware/storefront": "6.6.*",
"vrpayment/sdk": "4.6.0"
"shopware/storefront":"~6.6.0",
"vrpayment/sdk": "^4.0.0"
},
"type": "shopware-platform-plugin",
"version": "6.1.11"
"version": "6.2.2"
}
+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
$settings = $this->settingsService->getSettings($this->getSalesChannelId());
$this->setSpaceId($settings->getSpaceId())
->setApiClient($settings->getApiClient());
$this->setSpaceId($settings->getSpaceId())->setApiClient($settings->getApiClient());
$this->disablePaymentMethodConfigurations($context);
$this->enablePaymentMethodConfigurations($context);
@@ -252,11 +251,16 @@ class PaymentMethodConfigurationService {
{
$data = [];
$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)
->getEntities();
@@ -272,19 +276,14 @@ class PaymentMethodConfigurationService {
];
$paymentMethodData[] = [
'id' => $paymentMethodConfigurationEntity->getId(),
'id' => $paymentMethodConfigurationEntity->getPaymentMethodId(),
'active' => false,
];
$salesChannelPaymentMethodData[] = [
'paymentMethodId' => $paymentMethodConfigurationEntity->getId(),
];
}
try {
$this->vRPaymentPaymentMethodConfigurationRepository->update($data, $context);
$this->paymentMethodRepository->update($paymentMethodData, $context);
$this->salesChannelPaymentRepository->delete($salesChannelPaymentMethodData, $context);
} catch (\Exception $exception) {
$this->logger->critical($exception->getMessage());
}
@@ -350,28 +349,67 @@ class PaymentMethodConfigurationService {
*/
foreach ($paymentMethodConfigurations as $paymentMethodConfiguration) {
$paymentMethodConfigurationEntity = $this->getPaymentMethodConfigurationEntity(
$entity = $this->getPaymentMethodConfigurationEntity(
$paymentMethodConfiguration->getSpaceId(),
$paymentMethodConfiguration->getId(),
$context
);
$id = is_null($paymentMethodConfigurationEntity) ? Uuid::randomHex() : $paymentMethodConfigurationEntity->getId();
$configId = $entity ? $entity->getId() : Uuid::randomHex();
$technicalName = $paymentMethodConfiguration->getName();
$paymentMethodId = $this->getOrCreatePaymentMethodId(
$technicalName,
VRPaymentPaymentHandler::class,
$context
);
$data = [
'id' => $id,
'id' => $configId,
'paymentMethodConfigurationId' => $paymentMethodConfiguration->getId(),
'paymentMethodId' => $id,
'paymentMethodId' => $paymentMethodId,
'data' => json_decode(strval($paymentMethodConfiguration), true),
'sortOrder' => $paymentMethodConfiguration->getSortOrder(),
'spaceId' => $paymentMethodConfiguration->getSpaceId(),
'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
@@ -477,8 +515,7 @@ class PaymentMethodConfigurationService {
string $id,
PaymentMethodConfiguration $paymentMethodConfiguration,
Context $context
): void
{
): void {
/** @var PluginIdProvider $pluginIdProvider */
$pluginIdProvider = $this->container->get(PluginIdProvider::class);
$pluginId = $pluginIdProvider->getPluginIdByBaseClass(
@@ -494,13 +531,19 @@ class PaymentMethodConfigurationService {
'afterOrderEnabled' => true,
'active' => true,
'translations' => $this->getPaymentMethodConfigurationTranslation($paymentMethodConfiguration, $context),
'technicalName' => $paymentMethodConfiguration->getName(),
];
$data['mediaId'] = $this->upsertMedia($id, $paymentMethodConfiguration, $context);
$data = array_filter($data);
$mediaId = $this->upsertMedia($id, $paymentMethodConfiguration, $context);
if ($mediaId) {
$data['mediaId'] = $mediaId;
}
try {
$this->paymentMethodRepository->upsert([$data], $context);
} catch (\Exception $e) {
$this->logger->error($e->getMessage(), [$e->getTraceAsString()]);
}
}
/**
@@ -602,46 +645,58 @@ class PaymentMethodConfigurationService {
*
* @return string|null
*/
/**
* Upload or update Payment Method icons
*/
protected function upsertMedia(string $id, PaymentMethodConfiguration $paymentMethodConfiguration, Context $context): ?string
{
try {
$existingRecord = $this->getMediaDefaultFolderForPaymentMethod($paymentMethodConfiguration, $context);
$folderKey = 'payment_method_' . $paymentMethodConfiguration->getId();
if ($existingRecord->count() > 0) {
$id = $existingRecord->first()->getId();
// Check existing default folder
$criteria = new Criteria();
$criteria->addFilter(new EqualsFilter('entity', $folderKey));
$existingFolder = $this->mediaDefaultFolderRepository->search($criteria, $context);
$folderId = $id;
if ($existingFolder->count() > 0) {
$folderId = $existingFolder->first()->getId();
}
// Ensure default folder
$this->mediaDefaultFolderRepository->upsert([
[
'id' => $id,
'id' => $folderId,
'associationFields' => [],
'entity' => 'payment_method_' . $paymentMethodConfiguration->getId(),
'entity' => $folderKey,
],
], $context);
// Ensure media folder
$this->mediaFolderRepository->upsert([
[
'id' => $id,
'defaultFolderId' => $id,
'id' => $folderId,
'defaultFolderId' => $folderId,
'name' => $paymentMethodConfiguration->getName(),
'useParentConfiguration' => false,
'configuration' => [],
],
], $context);
/**
* @var \Shopware\Core\Content\Media\MediaDefinition
*/
// Media insert/update
$mediaDefinition = $this->container->get(MediaDefinition::class);
$this->mediaSerializer->setRegistry($this->serializerRegistry);
$data = [
'id' => $id,
'title' => $paymentMethodConfiguration->getName(),
'url' => $paymentMethodConfiguration->getResolvedImageUrl(),
'mediaFolderId' => $id,
'mediaFolderId' => $folderId,
];
$data = $this->mediaSerializer->deserialize(new Config([], [], []), $mediaDefinition, $data);
$this->mediaRepository->upsert([$data], $context);
return $id;
} catch (\Exception $e) {
$this->logger->critical($e->getMessage(), [$e->getTraceAsString()]);
@@ -13,7 +13,9 @@ use Symfony\Component\{
};
use VRPaymentPayment\Core\{
Api\Refund\Service\RefundService,
Settings\Service\SettingsService
Api\Transaction\Service\TransactionService,
Settings\Service\SettingsService,
Util\Exception\RefundNotSupportedException
};
/**
@@ -41,16 +43,23 @@ class RefundController extends AbstractController
*/
protected $logger;
/**
* @var \VRPaymentPayment\Core\Api\Transaction\Service\TransactionService
*/
protected $transactionService;
/**
* RefundController constructor.
*
* @param \VRPaymentPayment\Core\Api\Refund\Service\RefundService $refundService
* @param \VRPaymentPayment\Core\Settings\Service\SettingsService $settingsService
* @param \VRPaymentPayment\Core\Api\Transaction\Service\TransactionService $transactionService
*/
public function __construct(RefundService $refundService, SettingsService $settingsService)
public function __construct(RefundService $refundService, SettingsService $settingsService, TransactionService $transactionService)
{
$this->settingsService = $settingsService;
$this->refundService = $refundService;
$this->transactionService = $transactionService;
}
/**
@@ -82,11 +91,28 @@ class RefundController extends AbstractController
$quantity = (int)$request->request->get('quantity');
$lineItemId = $request->request->get('lineItemId');
if ($quantity === null || $quantity <= 0) {
return new Response('refundQuantityZero', Response::HTTP_BAD_REQUEST);
}
$settings = $this->settingsService->getSettings($salesChannelId);
$apiClient = $settings->getApiClient();
$transaction = $apiClient->getTransactionService()->read($settings->getSpaceId(), $transactionId);
$maxQuantity = $this->refundService->getMaxRefundableQuantity($transaction, $context, $lineItemId);
if ($quantity > $maxQuantity) {
return new Response('refundExceedsQuantity', Response::HTTP_BAD_REQUEST);
}
try {
$refund = $this->refundService->create($transaction, $context, $lineItemId, $quantity);
} catch (RefundNotSupportedException $exception) {
$this->logger->info('Payment method does not support online refunds for transaction: ' . $transactionId);
return new Response('methodDoesNotSupportRefund', Response::HTTP_BAD_REQUEST);
}
if ($refund === null) {
return new Response('Refund was not created. Please check the refund amound or if the item was not refunded before', Response::HTTP_BAD_REQUEST);
}
@@ -111,11 +137,33 @@ class RefundController extends AbstractController
$transactionId = $request->request->get('transactionId');
$refundableAmount = $request->request->get('refundableAmount');
if ($refundableAmount === null || $refundableAmount <= 0.0) {
return new Response('refundAmountZero', Response::HTTP_BAD_REQUEST);
}
$settings = $this->settingsService->getSettings($salesChannelId);
$apiClient = $settings->getApiClient();
$transaction = $apiClient->getTransactionService()->read($settings->getSpaceId(), $transactionId);
$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);
}
@@ -142,7 +190,13 @@ class RefundController extends AbstractController
$apiClient = $settings->getApiClient();
$transaction = $apiClient->getTransactionService()->read($settings->getSpaceId(), $transactionId);
$this->refundService->createPartialRefund($transaction, $context, $lineItemId, $refundableAmount);
try {
$refund = $this->refundService->createPartialRefund($transaction, $context, $lineItemId, $refundableAmount);
} catch (RefundNotSupportedException $exception) {
$this->logger->info('Payment method does not support online refunds for transaction: ' . $transactionId);
return new Response('methodDoesNotSupportRefund', Response::HTTP_BAD_REQUEST);
}
return new Response(null, Response::HTTP_NO_CONTENT);
}
+89 -2
View File
@@ -12,14 +12,20 @@ use Shopware\Core\{
};
use VRPayment\Sdk\{
Model\Refund,
Model\Transaction
Model\Transaction,
Model\CriteriaOperator,
Model\EntityQueryFilter,
Model\EntityQueryFilterType,
Model\EntityQuery,
ApiException
};
use VRPaymentPayment\Core\{
Api\Refund\Entity\RefundEntity,
Api\Transaction\Entity\TransactionEntity,
Api\Transaction\Entity\TransactionEntityDefinition,
Settings\Service\SettingsService,
Util\Payload\RefundPayload
Util\Payload\RefundPayload,
Util\Exception\RefundNotSupportedException
};
/**
@@ -99,6 +105,12 @@ class RefundService
$this->upsert($refund, $context);
return $refund;
}
} catch (ApiException $exception) {
$message = $exception->getMessage();
$this->logger->critical($message);
if ($exception->getCode() === 442 && str_contains($message, 'does not support online refunds')) {
throw new RefundNotSupportedException($message, 0, $exception);
}
} catch (\Exception $exception) {
$this->logger->critical($exception->getMessage());
}
@@ -134,6 +146,12 @@ class RefundService
$this->upsert($refund, $context);
return $refund;
}
} catch (ApiException $exception) {
$message = $exception->getMessage();
$this->logger->critical($message);
if ($exception->getCode() === 442 && str_contains($message, 'does not support online refunds')) {
throw new RefundNotSupportedException($message, 0, $exception);
}
} catch (\Exception $exception) {
$this->logger->critical($exception->getMessage());
}
@@ -170,6 +188,12 @@ class RefundService
$this->upsert($refund, $context);
return $refund;
}
} catch (ApiException $exception) {
$message = $exception->getMessage();
$this->logger->critical($message);
if ($exception->getCode() === 442 && str_contains($message, 'does not support online refunds')) {
throw new RefundNotSupportedException($message, 0, $exception);
}
} catch (\Exception $exception) {
$this->logger->critical($exception->getMessage());
}
@@ -241,4 +265,67 @@ class RefundService
->first();
}
/**
* Get total refunded quantity for transaction's line item by lineItemId.
*
* @param \VRPayment\Sdk\Model\Transaction $transaction
* @param \Shopware\Core\Framework\Context $context
* @param string $lineItemId
*
* @return int
*/
public function getRefundedQuantity(Transaction $transaction, Context $context, string $lineItemId): int {
$transactionEntity = $this->getTransactionEntityByTransactionId($transaction->getId(), $context);
$settings = $this->settingsService->getSettings($transactionEntity->getSalesChannel()->getId());
$apiClient = $settings->getApiClient();
$entityQueryFilter = (new EntityQueryFilter())
->setType(EntityQueryFilterType::LEAF)
->setOperator(CriteriaOperator::EQUALS)
->setFieldName('transaction.id')
->setValue($transaction->getId());
$query = (new EntityQuery())->setFilter($entityQueryFilter);
$refunds = $apiClient->getRefundService()->search($settings->getSpaceId(), $query);
$refundedQuantity = 0;
foreach ($refunds as $refund) {
foreach ($refund->getReductions() as $reduction) {
if ($reduction->getLineItemUniqueId() === $lineItemId) {
$refundedQuantity += (int) $reduction->getQuantityReduction();
}
}
}
return $refundedQuantity;
}
/**
* Get maximum quantity of available items to refund for line item.
*
* @param \VRPayment\Sdk\Model\Transaction $transaction
* @param \Shopware\Core\Framework\Context $context
* @param string $lineItemId
*
* @return int
*/
public function getMaxRefundableQuantity(Transaction $transaction, Context $context, string $lineItemId): int {
$originalQuantity = 0;
foreach ($transaction->getLineItems() as $lineItem) {
if ($lineItem->getUniqueId() === $lineItemId) {
$originalQuantity = (int) $lineItem->getQuantity();
break;
}
}
$refundedQuantity = $this->getRefundedQuantity($transaction, $context, $lineItemId);
$maxQuantity = $originalQuantity - $refundedQuantity;
return $maxQuantity;
}
}
@@ -32,7 +32,7 @@ use VRPaymentPayment\Core\Api\Refund\Entity\RefundEntityDefinition;
*/
class TransactionEntityDefinition extends EntityDefinition {
public const ENTITY_NAME = 'vrpayment_transaction';
public const ENTITY_NAME = 'vrpayment_transaction_data';
/**
* @return string
@@ -15,6 +15,7 @@ use Shopware\Core\{
System\SalesChannel\SalesChannelContext
};
use Shopware\Storefront\Page\Checkout\Confirm\CheckoutConfirmPageLoadedEvent;
use Shopware\Storefront\Page\Account\Order\AccountEditOrderPageLoadedEvent;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use VRPayment\Sdk\{
Model\AddressCreate,
@@ -46,6 +47,9 @@ use VRPaymentPayment\Core\{
Util\Payload\TransactionPayload
};
use Shopware\Core\Checkout\Order\Aggregate\OrderLineItem\OrderLineItemEntity;
use Shopware\Core\Framework\Struct\ArrayEntity;
/**
* Class TransactionService
*
@@ -132,13 +136,12 @@ class TransactionService
$settings = $this->settingsService->getSettings($salesChannelId);
$apiClient = $settings->getApiClient();
$failedStates = [
TransactionState::DECLINE,
TransactionState::FAILED,
TransactionState::VOIDED,
];
$transactionId = $_SESSION['transactionId'] ?? null;
if ($transactionId !== null) {
$pendingTransaction = $this->read($_SESSION['transactionId'], $salesChannelId);
if (in_array($pendingTransaction->getState(), $failedStates)) {
}
if ($transactionId === null || $pendingTransaction === null || $pendingTransaction->getState() !== TransactionState::PENDING) {
unset($_SESSION['transactionId']);
$pendingTransactionId = $this->createPendingTransaction($salesChannelContext);
$pendingTransaction = $this->read($pendingTransactionId, $salesChannelId);
@@ -181,10 +184,19 @@ class TransactionService
$transaction->getOrderTransaction()->getPaymentMethodId(),
$transaction->getOrder()->getSalesChannelId()
);
$_SESSION['transactionId'] = null;
$_SESSION['arrayOfPossibleMethods'] = null;
$_SESSION['addressCheck'] = null;
$_SESSION['currencyCheck'] = null;
$salesChannelContext->getContext()->addExtension(
'checkoutState',
new ArrayEntity([
'transactionId' => null,
'addressHash' => null,
'currency' => null,
])
);
$salesChannelContext->getContext()->addExtension(
'possibleMethods',
new ArrayEntity(['ids' => []])
);
$this->holdDelivery($transaction->getOrder()->getId(), $salesChannelContext->getContext());
@@ -479,14 +491,18 @@ class TransactionService
/**
* @param SalesChannelContext $salesChannelContext
* @param CheckoutConfirmPageLoadedEvent|null $event
* @param $event
* @return int
*/
public function createPendingTransaction(SalesChannelContext $salesChannelContext, ?CheckoutConfirmPageLoadedEvent $event = null): int
public function createPendingTransaction(SalesChannelContext $salesChannelContext, $event = null): int
{
$expiredTransaction = true;
$transactionId = $_SESSION['transactionId'] ?? null;
$settings = $this->settingsService->getValidSettings($salesChannelContext->getSalesChannel()->getId());
if (!$settings) {
throw new \Exception('Space settings not configured');
}
if ($transactionId) {
$transactionService = $settings->getApiClient()->getTransactionService();
@@ -495,6 +511,7 @@ class TransactionService
TransactionState::DECLINE,
TransactionState::FAILED,
TransactionState::VOIDED,
null
];
if (!in_array($pendingTransaction->getState(), $failedStates)) {
$expiredTransaction = false;
@@ -568,6 +585,7 @@ class TransactionService
$lineItems = [];
if ($event) {
if ($event instanceof CheckoutConfirmPageLoadedEvent) {
$cartLineItems = $event->getPage()->getCart()->getLineItems()->getElements();
foreach ($cartLineItems as $cartLineItem) {
if ($cartLineItem->getType() === CustomProductsLineItemTypes::LINE_ITEM_TYPE_CUSTOMIZED_PRODUCTS) {
@@ -575,6 +593,12 @@ class TransactionService
}
$lineItems[] = $this->createTempLineItem($cartLineItem);
}
} elseif ($event instanceof AccountEditOrderPageLoadedEvent) {
$order = $event->getPage()->getOrder();
foreach ($order->getLineItems() as $orderLineItem) {
$lineItems[] = $this->createTempLineItem($orderLineItem);
}
}
}
$customerId = "";
@@ -585,10 +609,13 @@ class TransactionService
$protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https://' : 'http://';
$homeUrl = $protocol . $_SERVER['HTTP_HOST'];
$currency = $salesChannelContext->getCurrency()->getIsoCode();
$language = $this->localeCodeProvider->getLocaleCodeFromContext($salesChannelContext->getContext());
$transactionPayload = (new TransactionCreate())
->setBillingAddress($billingAddress)
->setLineItems($lineItems)
->setCurrency($currency)
->setLanguage($language)
->setSpaceViewId($settings->getSpaceViewId())
->setAutoConfirmationEnabled(false)
->setChargeRetryEnabled(false)
@@ -637,7 +664,10 @@ class TransactionService
$billingAddress->setOrganizationName($customerBillingAddress->getCompany());
$currency = $salesChannelContext->getCurrency()->getIsoCode();
$language = $this->localeCodeProvider->getLocaleCodeFromContext($salesChannelContext->getContext());
$pendingTransaction->setCurrency($currency);
$pendingTransaction->setLanguage($language);
$pendingTransaction->setBillingAddress($billingAddress);
$settings->getApiClient()->getTransactionService()
@@ -732,14 +762,26 @@ class TransactionService
* @param LineItem $productData
* @return LineItemCreate
*/
private function createTempLineItem(LineItem $productData): LineItemCreate
private function createTempLineItem($productData): LineItemCreate
{
$lineItem = new LineItemCreate();
if ($productData instanceof LineItem) {
$lineItem->setName($productData->getLabel());
$lineItem->setUniqueId($productData->getId());
$lineItem->setSku($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);
$signature = $request->server->get('HTTP_X_SIGNATURE');
$requestJson = json_decode($request->getContent(), true);
if ($requestJson['eventId'] == null && $requestJson['entityId'] == null && $requestJson['listenerEntityId'] == null && $requestJson['listenerEntityId'] == null && $requestJson['listenerEntityTechnicalName'] == null && $requestJson['spaceId'] == null) {
throw new \InvalidArgumentException('Empty webhook');
}
if (!$this->settings->getSpaceId() || !$this->settings->getUserId() || !$this->settings->getApplicationKey()) {
throw new \InvalidArgumentException('Not correct webhook configuration for salesChannelId: ' . $salesChannelId . ' Debug: ' . var_dump($requestJson));
}
$apiClient = $this->settings->getApiClient();
$callBackData->assign($requestJson);
@@ -638,25 +646,46 @@ class WebHookController extends AbstractController {
private function unholdDelivery(string $orderId, Context $context): void
{
try {
/**
* @var OrderDeliveryStateHandler $orderDeliveryStateHandler
*/
$order = $this->getOrderEntity($orderId, $context);
/**
* @var OrderDeliveryEntity $orderDelivery
*/
$criteria = new Criteria([$orderId]);
$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();
if (null === $orderDelivery) {
$this->logger->info('No deliveries found for order: ' . $orderId);
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;
}
$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->unhold($orderDelivery->getId(), $context);
$this->logger->info('Successfully unheld order delivery for order: ' . $orderId);
} 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);
if(!empty($orderId)) {
$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);
if (
in_array(
$orderTransaction->getStateMachineState()?->getTechnicalName(),
[
OrderTransactionStates::STATE_PAID,
OrderTransactionStates::STATE_PARTIALLY_PAID,
]
) &&
($request->getState() == RefundState::SUCCESSFUL)
) {
if ($refund->getAmount() == $orderTransaction->getAmount()->getTotalPrice()) {
$this->orderTransactionStateHandler->refund($orderTransactionId, $context);
} else {
if ($refund->getAmount() < $orderTransaction->getAmount()->getTotalPrice()) {
$this->orderTransactionStateHandler->refundPartially($orderTransactionId, $context);
}
}
} elseif ($orderTransaction->getStateMachineState()?->getTechnicalName() ===
OrderTransactionStates::STATE_PARTIALLY_REFUNDED &&
($request->getState() == RefundState::SUCCESSFUL)
) {
$transactionByOrderTransactionId = $this->transactionService->getByOrderTransactionId($orderTransactionId, $context);
$totalRefundedAmount = $this->getTotalRefundedAmount($transactionByOrderTransactionId->getTransactionId(), $context);
if (floatval($orderTransaction->getAmount()->getTotalPrice()) - $totalRefundedAmount <= 0) {
$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);
}
}
});
}
@@ -306,6 +306,14 @@ abstract class WebHookStrategyBase implements WebHookStrategyInterface {
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.
*
@@ -317,22 +325,46 @@ abstract class WebHookStrategyBase implements WebHookStrategyInterface {
protected function unholdDelivery(string $orderId, Context $context): void
{
try {
$order = $this->getOrderEntity($orderId, $context);
/** @var OrderDeliveryEntity $orderDelivery */
$criteria = new Criteria([$orderId]);
$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();
if (null === $orderDelivery) {
$this->logger->info('No deliveries found for order: ' . $orderId);
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;
}
$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->unhold($orderDelivery->getId(), $context);
$this->logger->info('Successfully unheld order delivery for order: ' . $orderId);
} 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\Exception\AsyncPaymentFinalizeException,
Checkout\Payment\Exception\AsyncPaymentProcessException,
Checkout\Payment\PaymentException,
Checkout\Payment\Exception\CustomerCanceledAsyncPaymentException,
Framework\Validation\DataBag\RequestDataBag,
System\SalesChannel\SalesChannelContext
@@ -140,7 +141,7 @@ class VRPaymentPaymentHandler implements AsynchronousPaymentHandlerInterface
]);
unset($_SESSION['transactionId']);
$this->logger->info($errorMessage);
throw new \Exception($transaction->getOrder()->getId());
throw PaymentException::customerCanceled($transaction->getOrderTransaction()->getId(), $errorMessage);
}
} else {
$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_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
*/
@@ -132,7 +157,13 @@ class SettingsService {
if ($property === '') {
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]));
}
$propertyValuePairs[$property] = $value;
@@ -2,15 +2,21 @@
namespace VRPaymentPayment\Core\Storefront\Checkout\Controller;
use Psr\Log\LoggerInterface;
use Psr\{
Log\LoggerInterface,
Cache\CacheItemPoolInterface
};
use Shopware\Core\{
Checkout\Payment\PaymentException,
Checkout\Cart\Cart,
Checkout\Cart\CartException,
Checkout\Cart\LineItemFactoryRegistry,
Checkout\Cart\SalesChannel\CartService,
Checkout\Order\Aggregate\OrderLineItem\OrderLineItemCollection,
Checkout\Order\Aggregate\OrderLineItem\OrderLineItemEntity,
Checkout\Order\Aggregate\OrderTransaction\OrderTransactionStateHandler,
Checkout\Order\OrderEntity,
Checkout\Order\OrderDefinition,
Checkout\Order\SalesChannel\AbstractOrderRoute,
Framework\Context,
Framework\DataAbstractionLayer\Search\Criteria,
@@ -21,7 +27,9 @@ use Shopware\Core\{
Framework\Uuid\Uuid,
Framework\Uuid\Exception\InvalidUuidException,
Framework\Validation\DataBag\RequestDataBag,
System\SalesChannel\SalesChannelContext
System\SalesChannel\SalesChannelContext,
System\StateMachine\StateMachineRegistry,
System\StateMachine\Transition,
};
use Shopware\Storefront\{
Controller\StorefrontController,
@@ -31,9 +39,13 @@ use Shopware\Storefront\{
use Symfony\Component\{
HttpFoundation\Request,
HttpFoundation\Response,
HttpFoundation\RedirectResponse,
Routing\Attribute\Route,
Routing\Generator\UrlGeneratorInterface
Routing\Generator\UrlGeneratorInterface,
Cache\Adapter\FilesystemAdapter,
DependencyInjection\ParameterBag\ParameterBagInterface
};
use Symfony\Contracts\Cache\ItemInterface;
use VRPayment\Sdk\{
Model\Transaction,
Model\TransactionState
@@ -43,10 +55,11 @@ use VRPaymentPayment\Core\{
Settings\Options\Integration,
Settings\Service\SettingsService,
Storefront\Checkout\Struct\CheckoutPageData,
Util\Payload\CustomProducts\CustomProductsLineItemTypes
Util\LocaleCodeProvider,
Util\Payload\CustomProducts\CustomProductsLineItemTypes,
Util\Payload\TransactionPayload
};
/**
* Class CheckoutController
*
@@ -57,6 +70,18 @@ use VRPaymentPayment\Core\{
#[Route(defaults: ['_routeScope' => ['storefront']])]
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
*/
@@ -97,6 +122,16 @@ class CheckoutController extends StorefrontController {
*/
private $orderRoute;
/**
* @var \Psr\Cache\CacheItemPoolInterface
*/
private CacheItemPoolInterface $cache;
/**
* @var LocaleCodeProvider
*/
private LocaleCodeProvider $localeCodeProvider;
/**
* PaymentController constructor.
*
@@ -106,6 +141,9 @@ class CheckoutController extends StorefrontController {
* @param \VRPaymentPayment\Core\Api\Transaction\Service\TransactionService $transactionService
* @param \Shopware\Storefront\Page\GenericPageLoaderInterface $genericLoader
* @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(
LineItemFactoryRegistry $lineItemFactoryRegistry,
@@ -113,7 +151,11 @@ class CheckoutController extends StorefrontController {
SettingsService $settingsService,
TransactionService $transactionService,
GenericPageLoaderInterface $genericLoader,
AbstractOrderRoute $orderRoute
AbstractOrderRoute $orderRoute,
OrderTransactionStateHandler $orderTransactionStateHandler,
StateMachineRegistry $stateMachineRegistry,
ParameterBagInterface $params,
LocaleCodeProvider $localeCodeProvider
)
{
$this->cartService = $cartService;
@@ -122,6 +164,10 @@ class CheckoutController extends StorefrontController {
$this->transactionService = $transactionService;
$this->lineItemFactoryRegistry = $lineItemFactoryRegistry;
$this->orderRoute = $orderRoute;
$this->orderTransactionStateHandler = $orderTransactionStateHandler;
$this->stateMachineRegistry = $stateMachineRegistry;
$this->cache = new FilesystemAdapter('vrpayment', 0, rtrim($params->get('kernel.cache_dir'), '/') . '/vrpayment-cache');
$this->localeCodeProvider = $localeCodeProvider;
}
/**
@@ -205,7 +251,9 @@ class CheckoutController extends StorefrontController {
return $this->redirect($recreateCartUrl, Response::HTTP_MOVED_PERMANENTLY);
}
$javascriptUrl = $this->getTransactionJavaScriptUrl($transaction->getId());
$localeCode = $this->localeCodeProvider->getLocaleCodeFromContext($salesChannelContext->getContext());
$paymentPageLocale = $this->localeCodeProvider->mapToPaymentPageLocale($localeCode);
$javascriptUrl = $this->getTransactionJavaScriptUrl($transaction->getId(), $paymentPageLocale);
// Set Checkout Page Data
$checkoutPageData = (new CheckoutPageData())
@@ -232,13 +280,14 @@ class CheckoutController extends StorefrontController {
* Get transaction Javascript URL
*
* @param int $transactionId
* @param string $paymentPageLocale The payment page locale.
*
* @return string
* @throws \VRPayment\Sdk\ApiException
* @throws \VRPayment\Sdk\Http\ConnectionException
* @throws \VRPayment\Sdk\VersioningException
*/
private function getTransactionJavaScriptUrl(int $transactionId): string
private function getTransactionJavaScriptUrl(int $transactionId, string $paymentPageLocale = ''): string
{
$javascriptUrl = '';
switch ($this->settings->getIntegration()) {
@@ -254,6 +303,12 @@ class CheckoutController extends StorefrontController {
$this->logger->critical(strtr('invalid integration : :integration', [':integration' => $this->settings->getIntegration()]));
}
if ($javascriptUrl && $paymentPageLocale) {
$separator = str_contains($javascriptUrl, '?') ? '&' : '?';
$javascriptUrl .= $separator . 'language=' . $paymentPageLocale;
}
return $javascriptUrl;
}
@@ -354,6 +409,45 @@ class CheckoutController extends StorefrontController {
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 {
$this->cartService->deleteCart($salesChannelContext);
$cart = $this->cartService->createNew($salesChannelContext->getToken());
@@ -367,6 +461,7 @@ class CheckoutController extends StorefrontController {
}
$transaction = $this->getTransaction($orderId, $salesChannelContext->getContext());
$orderTransactionId = $transaction->getMetaData()[TransactionPayload::VRPAYMENT_METADATA_ORDER_TRANSACTION_ID];
if (!empty($transaction->getUserFailureMessage())) {
$this->addFlash('danger', $transaction->getUserFailureMessage());
}
@@ -401,6 +496,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) {
$this->addFlash('danger', $this->trans('error.addToCartError'));
$this->logger->critical($exception->getMessage());
@@ -410,6 +517,74 @@ class CheckoutController extends StorefrontController {
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
*
@@ -7,11 +7,14 @@ use Shopware\Core\{Checkout\Order\Aggregate\OrderTransaction\OrderTransactionCol
Checkout\Order\Aggregate\OrderTransaction\OrderTransactionStates,
Checkout\Order\OrderEntity,
Content\MailTemplate\Service\Event\MailBeforeValidateEvent};
use Shopware\Core\Checkout\Payment\PaymentMethodCollection;
use Shopware\Core\System\SalesChannel\SalesChannelContext;
use Shopware\Storefront\Page\Account\Order\AccountEditOrderPageLoadedEvent;
use Shopware\Storefront\Page\Account\PaymentMethod\AccountPaymentMethodPageLoadedEvent;
use Shopware\Storefront\Page\Checkout\Confirm\CheckoutConfirmPageLoadedEvent;
use Shopware\Storefront\Page\Checkout\Finish\CheckoutFinishPageLoadedEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use VRPaymentPayment\Core\{Api\Transaction\Service\OrderMailService,
Api\Transaction\Service\TransactionService,
use VRPaymentPayment\Core\{Api\Transaction\Service\TransactionService,
Checkout\PaymentHandler\VRPaymentPaymentHandler,
Settings\Service\SettingsService,
Settings\Struct\Settings,
@@ -31,6 +34,7 @@ use VRPaymentPayment\Sdk\{Model\AddressCreate,
Model\Transaction,
Model\TransactionCreate,
Model\TransactionPending};
use Shopware\Core\Framework\Struct\ArrayEntity;
/**
* Class CheckoutSubscriber
@@ -99,7 +103,9 @@ class CheckoutSubscriber implements EventSubscriberInterface
public static function getSubscribedEvents(): array
{
return [
CheckoutConfirmPageLoadedEvent::class => ['onConfirmPageLoaded', 1],
CheckoutConfirmPageLoadedEvent::class => 'onCheckoutConfirmLoaded',
AccountEditOrderPageLoadedEvent::class => 'onAccountOrderEditLoaded',
AccountPaymentMethodPageLoadedEvent::class => 'onAccountPaymentMethodLoaded',
MailBeforeValidateEvent::class => ['onMailBeforeValidate', 1],
];
}
@@ -153,9 +159,10 @@ 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();
@@ -168,7 +175,7 @@ class CheckoutSubscriber implements EventSubscriberInterface
$createdTransactionId = $this->transactionService->createPendingTransaction($salesChannelContext, $event);
$this->updateTempTransactionIfNeeded($salesChannelContext, $createdTransactionId);
$this->getAvailablePaymentMethods($settings, $createdTransactionId);
$this->getAvailablePaymentMethods($settings, $createdTransactionId, $salesChannelContext);
$this->setPossiblePaymentMethods($settings->getSpaceId(), $event);
} catch (\Exception $e) {
$this->logger->error($e->getMessage());
@@ -176,10 +183,74 @@ class CheckoutSubscriber implements EventSubscriberInterface
}
}
/**
* @param AccountEditOrderPageLoadedEvent $event
* @return void
*/
public function onAccountOrderEditLoaded(AccountEditOrderPageLoadedEvent $event): void
{
try {
$this->handlePaymentMethodFiltering($event);
} catch (\Throwable $e) {
$this->logger->error($e->getMessage());
$this->removeVRPaymentPaymentMethodFromConfirmPage($event);
}
}
/**
* @param AccountPaymentMethodPageLoadedEvent $event
* @return void
*/
public function onAccountPaymentMethodLoaded(AccountPaymentMethodPageLoadedEvent $event): void
{
try {
$this->handlePaymentMethodFiltering($event);
} catch (\Throwable $e) {
$this->logger->error($e->getMessage());
$this->removeVRPaymentPaymentMethodFromConfirmPage($event);
}
}
/**
* @param \Shopware\Storefront\Page\Checkout\Confirm\CheckoutConfirmPageLoadedEvent $event
*/
private function removeVRPaymentPaymentMethodFromConfirmPage(CheckoutConfirmPageLoadedEvent $event): void
public function onConfirmPageLoaded(CheckoutConfirmPageLoadedEvent $event): void
{
try {
$this->handlePaymentMethodFiltering($event);
} catch (\Throwable $e) {
$this->logger->error($e->getMessage());
$this->removeVRPaymentPaymentMethodFromConfirmPage($event);
}
}
/**
* @param $event
* @return void
*/
private function handlePaymentMethodFiltering($event): void
{
$salesChannelContext = $event->getSalesChannelContext();
$settings = $this->settingsService->getValidSettings($salesChannelContext->getSalesChannel()->getId());
if (is_null($settings)) {
$this->logger->notice('Removing payment methods because settings are invalid');
$this->removeVRPaymentPaymentMethodFromConfirmPage($event);
return;
}
$createdTransactionId = $this->transactionService->createPendingTransaction($salesChannelContext, $event);
$this->updateTempTransactionIfNeeded($salesChannelContext, $createdTransactionId);
$this->getAvailablePaymentMethods($settings, $createdTransactionId, $salesChannelContext);
$this->setPossiblePaymentMethods($settings->getSpaceId(), $event);
}
/**
* @param $event
* @return void
*/
private function removeVRPaymentPaymentMethodFromConfirmPage($event): void
{
$paymentMethodCollection = $event->getPage()->getPaymentMethods();
$paymentMethodIds = $this->paymentMethodUtil->getVRPaymentPaymentMethodIds($event->getContext());
@@ -193,7 +264,7 @@ class CheckoutSubscriber implements EventSubscriberInterface
* @param int $createdTransactionId
* @return void
*/
private function getAvailablePaymentMethods(Settings $settings, int $createdTransactionId): void
private function getAvailablePaymentMethods(Settings $settings, int $createdTransactionId, SalesChannelContext $salesChannelContext): void
{
$transactionService = $settings->getApiClient()->getTransactionService();
$possiblePaymentMethods = $transactionService->fetchPaymentMethods(
@@ -203,36 +274,71 @@ class CheckoutSubscriber implements EventSubscriberInterface
);
$arrayOfPossibleMethods = [];
foreach ($possiblePaymentMethods as $possiblePaymentMethod) {
$arrayOfPossibleMethods[] = $possiblePaymentMethod->getid();
$arrayOfPossibleMethods[] = $possiblePaymentMethod->getId();
}
$_SESSION['arrayOfPossibleMethods'] = $arrayOfPossibleMethods;
$salesChannelContext->getContext()->addExtension(
'possibleMethods',
new ArrayEntity(['ids' => $arrayOfPossibleMethods])
);
}
/**
* Filters the original payment method collection (which already has Shopware's availability rules applied)
* to only include WhitelabelMachineName methods that are also allowed by the API.
* Non-WhitelabelMachineName methods are kept as-is.
*
* @param int $spaceId
* @param CheckoutConfirmPageLoadedEvent $event
* @param $event
* @return void
*/
private function setPossiblePaymentMethods(int $spaceId, CheckoutConfirmPageLoadedEvent $event): void
private function setPossiblePaymentMethods(int $spaceId, $event): void
{
$localPaymentMethods = [];
$paymentMethodConfigurations = $this->paymentMethodConfigurationService->getAllPaymentMethodConfigurations($spaceId, $event->getSalesChannelContext()->getContext());
foreach ($paymentMethodConfigurations as $paymentMethodConfiguration) {
$localPaymentMethods[$paymentMethodConfiguration->getId()] = $paymentMethodConfiguration->getPaymentMethodConfigurationId();
}
$paymentMethodCollection = $event->getPage()->getPaymentMethods();
foreach ($paymentMethodCollection as $paymentMethodCollectionItem) {
$isVRPaymentPM = VRPaymentPaymentHandler::class == $paymentMethodCollectionItem->getHandlerIdentifier();
if (!$isVRPaymentPM) {
$paymentMethodConfigurations = $this->paymentMethodConfigurationService
->getAllPaymentMethodConfigurations($spaceId, $event->getSalesChannelContext()->getContext());
$allowedIds = $this->getAllowedPaymentMethodIds($event->getSalesChannelContext());
// Build a map of Shopware payment method ID => configuration for methods allowed by the API.
$allowedWLConfigByPmId = [];
foreach ($paymentMethodConfigurations as $paymentMethodConfiguration) {
if ($paymentMethodConfiguration->getPaymentMethod() === null) {
continue;
}
$paymentMethodConfigurationId = $localPaymentMethods[$paymentMethodCollectionItem->getId()];
if (!\in_array($paymentMethodConfigurationId, $_SESSION['arrayOfPossibleMethods'])) {
$paymentMethodCollection->remove($paymentMethodCollectionItem->getId());
$pmId = $paymentMethodConfiguration->getPaymentMethod()->getId();
$pmConfigId = $paymentMethodConfiguration->getPaymentMethodConfigurationId();
if ($paymentMethodConfiguration->getSpaceId() === $spaceId
&& \in_array($pmConfigId, $allowedIds, true)) {
$allowedWLConfigByPmId[$pmId] = $paymentMethodConfiguration;
}
}
// Filter the original collection to preserve Shopware's availability rule filtering.
// Non-WLM methods pass through unchanged; WLM methods are kept only if allowed by the API.
$collection = new PaymentMethodCollection();
foreach ($paymentMethodCollection as $method) {
$isVRPaymentPM = VRPaymentPaymentHandler::class === $method->getHandlerIdentifier();
if (!$isVRPaymentPM) {
$collection->add($method);
continue;
}
if (isset($allowedWLConfigByPmId[$method->getId()])) {
$method->addExtension('vrpayment_config', $allowedWLConfigByPmId[$method->getId()]);
$collection->add($method);
}
}
$collection->sort(function ($a, $b) {
return ($a->getPosition() ?? 0) <=> ($b->getPosition() ?? 0);
});
$event->getPage()->setPaymentMethods($collection);
}
/**
@@ -242,19 +348,43 @@ class CheckoutSubscriber implements EventSubscriberInterface
*/
private function updateTempTransactionIfNeeded(SalesChannelContext $salesChannelContext, int $createdTransactionId): void
{
$addressCheck = $_SESSION['addressCheck'] ?? null;
$currencyCheck = $_SESSION['currencyCheck'] ?? null;
$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));
$addressHash = md5(json_encode((array) $customer));
$currency = $salesChannelContext->getCurrency()->getIsoCode();
if (($addressCheck && $currencyCheck) && $addressCheck !== $addressHash || $currencyCheck !== $currency) {
$needsUpdate = ($oldAddressHash !== $addressHash) || ($oldCurrency !== $currency);
if ($needsUpdate) {
if ($createdTransactionId) {
$this->transactionService->updateTempTransaction($salesChannelContext, $createdTransactionId);
}
$_SESSION['arrayOfPossibleMethods'] = null;
$_SESSION['addressCheck'] = $addressHash;
$_SESSION['currencyCheck'] = $currency;
$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') ?? []) : [];
}
}
+60 -5
View File
@@ -3,6 +3,7 @@
namespace VRPaymentPayment\Core\Util\Analytics;
use VRPayment\Sdk\ApiClient;
use Shopware\Core\Kernel;
/**
* Class Analytics
@@ -14,29 +15,83 @@ class Analytics {
public const SHOP_SYSTEM = 'x-meta-shop-system';
public const SHOP_SYSTEM_VERSION = 'x-meta-shop-system-version';
public const SHOP_SYSTEM_AND_VERSION = 'x-meta-shop-system-and-version';
public const PLUGIN_SYSTEM_VERSION = 'x-meta-plugin-version';
/**
* @return array
*/
public static function getDefaultData()
public static function getDefaultData(): array
{
$shopwareVersion = self::getShopwareVersion();
return [
self::SHOP_SYSTEM => 'shopware',
self::SHOP_SYSTEM_VERSION => '6',
self::SHOP_SYSTEM_AND_VERSION => 'shopware-6',
self::SHOP_SYSTEM_VERSION => $shopwareVersion,
self::SHOP_SYSTEM_AND_VERSION => 'shopware-' . $shopwareVersion,
self::PLUGIN_SYSTEM_VERSION => '6.2.2',
];
}
/**
* @param \VRPayment\Sdk\ApiClient $apiClient
*/
public static function addHeaders(ApiClient &$apiClient)
public static function addHeaders(ApiClient &$apiClient): void
{
$data = self::getDefaultData();
foreach ($data as $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{
}
+20
View File
@@ -91,6 +91,26 @@ class LocaleCodeProvider {
return $language->getLocale() ? $language->getLocale()->getCode() : $defaultLocale;
}
/**
* Maps a locale code to a VRPayment-supported payment page locale by matching the language prefix.
* E.g. de-CH -> de-DE, fr-CH -> fr-FR, en-US -> en-GB, it-CH -> it-IT.
*
* @param string $localeCode
* @return string
*/
public function mapToPaymentPageLocale(string $localeCode): string
{
$supportedLocales = [
'de' => self::LOCALE_GERMANY_GERMAN,
'fr' => self::LOCALE_FRANCE_FRENCH,
'it' => self::LOCALE_ITALY_ITALIAN,
'en' => self::LOCALE_GREAT_BRITAIN_ENGLISH,
];
$languagePrefix = substr($localeCode, 0, 2);
return $supportedLocales[$languagePrefix] ?? self::LOCALE_GREAT_BRITAIN_ENGLISH;
}
/**
* @param \Shopware\Core\Framework\Context $context
+51 -26
View File
@@ -12,6 +12,7 @@ use Shopware\Core\{Checkout\Cart\Tax\Struct\CalculatedTaxCollection,
Framework\DataAbstractionLayer\Search\Criteria,
System\SalesChannel\SalesChannelContext
};
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use VRPayment\Sdk\{Model\AddressCreate,
@@ -193,10 +194,16 @@ class TransactionPayload extends AbstractPayload
->setShippingAddress($shippingAddress)
->setShippingMethod($transactionData['shipping_method']);
$paymentConfiguration = $this->getPaymentConfiguration($this->salesChannelContext->getPaymentMethod()->getId());
$transactionPayload->setAllowedPaymentMethodConfigurations([$paymentConfiguration->getPaymentMethodConfigurationId()]);
$paymentConfiguration = $this->getPaymentConfiguration(
$this->salesChannelContext->getPaymentMethod()->getId(),
$this->settings->getSpaceId()
);
if ($paymentConfiguration) {
$transactionPayload->setAllowedPaymentMethodConfigurations([
$paymentConfiguration->getPaymentMethodConfigurationId()
]);
}
$successUrl = $this->transaction->getReturnUrl() . '&status=paid';
$failedUrl = $this->getFailUrl($this->transaction->getOrder()->getId()) . '&status=fail';
$transactionPayload->setSuccessUrl($successUrl)
@@ -210,6 +217,23 @@ class TransactionPayload extends AbstractPayload
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
*
@@ -295,7 +319,9 @@ class TransactionPayload extends AbstractPayload
});
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();
$amount = $this->calculateDiscountAmount($calculatedTax);
$discountName = $discount->getLabel();
$lineItem->setAmountIncludingTax($amount)
->setName(sprintf('DISCOUNT: %s (%s%% tax)', $discount->getLabel(), $rate))
->setQuantity(1)
->setShippingRequired(false)
->setSku('sku-discount-' . $rate, 200)
->setSku('sku-discount-' . $rate . '-' . $discountName, 200)
->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]);
$lineItem->setTaxes([$taxRate]);
@@ -354,7 +381,9 @@ class TransactionPayload extends AbstractPayload
*/
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()) {
$lineItems[] = $shippingLineItem;
}
@@ -616,12 +645,22 @@ class TransactionPayload extends AbstractPayload
{
$lineItem = null;
$lineItemPriceTotal = array_sum(array_map(static function (LineItemCreate $lineItem) {
return $lineItem->getAmountIncludingTax();
}, $lineItems));
// Calculate total of all current line items
$lineItemPriceTotal = array_sum(array_map(static fn(LineItemCreate $li) => $li->getAmountIncludingTax(), $lineItems));
$adjustmentPrice = $this->transaction->getOrder()->getAmountTotal() - $lineItemPriceTotal;
$adjustmentPrice = self::round($adjustmentPrice);
$this->logger->debug("LineItem price total before adjustment: $lineItemPriceTotal");
// 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 ($this->settings->isLineItemConsistencyEnabled()) {
@@ -777,20 +816,6 @@ class TransactionPayload extends AbstractPayload
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
*
@@ -31,7 +31,7 @@ class Migration1590156974TransactionEntity extends MigrationStep {
public function update(Connection $connection): void
{
$connection->executeStatement('
CREATE TABLE IF NOT EXISTS `vrpayment_transaction` (
CREATE TABLE IF NOT EXISTS `vrpayment_transaction_tmp` (
`id` BINARY(16) NOT NULL,
`data` JSON NOT NULL,
`payment_method_id` BINARY(16) NOT NULL,
@@ -42,7 +42,7 @@ class Migration1590646356RefundEntity extends MigrationStep {
PRIMARY KEY (`id`),
UNIQUE KEY `refund_id_UNIQUE` (`refund_id`),
KEY `fk.vrp_refund.transaction_id` (`transaction_id`),
CONSTRAINT `fk.vrp_refund.transaction_id` FOREIGN KEY (`transaction_id`) REFERENCES `vrpayment_transaction` (`transaction_id`) ON DELETE CASCADE
CONSTRAINT `fk.vrp_refund.transaction_id` FOREIGN KEY (`transaction_id`) REFERENCES `vrpayment_transaction_tmp` (`transaction_id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
');
}
@@ -30,7 +30,7 @@ class Migration1590646356TransactionEntity extends MigrationStep {
public function update(Connection $connection): void
{
try {
$connection->executeStatement('ALTER TABLE `vrpayment_transaction` ADD COLUMN `confirmation_email_sent` TINYINT(1) NOT NULL DEFAULT 0 AFTER `id`;');
$connection->executeStatement('ALTER TABLE `vrpayment_transaction_tmp` ADD COLUMN `confirmation_email_sent` TINYINT(1) NOT NULL DEFAULT 0 AFTER `id`;');
}catch (\Exception $exception){
// column probably exists
}
@@ -33,19 +33,19 @@ class Migration1605701048TransactionEntity extends MigrationStep
try {
$connection->executeStatement('
ALTER TABLE `vrpayment_transaction`
ALTER TABLE `vrpayment_transaction_tmp`
ADD `order_version_id` binary(16) NOT NULL AFTER `transaction_id`;
');
$connection->executeStatement('
UPDATE `vrpayment_transaction` t1
UPDATE `vrpayment_transaction_tmp` t1
INNER JOIN `order` t2
ON t1.order_id = t2.id
SET t1.order_version_id = t2.version_id;
');
$connection->executeStatement('
ALTER TABLE `vrpayment_transaction`
ALTER TABLE `vrpayment_transaction_tmp`
DROP FOREIGN KEY `fk.vrp_transaction.order_id`,
DROP FOREIGN KEY `fk.vrp_transaction.order_transaction_id`,
DROP FOREIGN KEY `fk.vrp_transaction.payment_method_id`,
@@ -53,7 +53,7 @@ class Migration1605701048TransactionEntity extends MigrationStep
');
$connection->executeStatement('
ALTER TABLE `vrpayment_transaction`
ALTER TABLE `vrpayment_transaction_tmp`
ADD CONSTRAINT `fk.vrp_transaction_order_id` FOREIGN KEY (`order_id`, `order_version_id`)
REFERENCES `order` (`id`, `version_id`) ON DELETE CASCADE ON UPDATE CASCADE,
ADD CONSTRAINT `fk.vrp_transaction_payment_method_id` FOREIGN KEY (`payment_method_id`)
@@ -30,7 +30,7 @@ class Migration1684240994TransactionEntity extends MigrationStep {
public function update(Connection $connection): void
{
try {
$connection->executeStatement('ALTER TABLE `vrpayment_transaction` ADD COLUMN `erp_merchant_id` VARCHAR(255) DEFAULT NULL AFTER `confirmation_email_sent`;');
$connection->executeStatement('ALTER TABLE `vrpayment_transaction_tmp` ADD COLUMN `erp_merchant_id` VARCHAR(255) DEFAULT NULL AFTER `confirmation_email_sent`;');
}catch (\Exception $exception){
// column probably exists
}
@@ -0,0 +1,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) => {
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({
title: errorResponse.response.data.errors[0].title,
message: errorResponse.response.data.errors[0].detail,
title: errorTitle,
message: errorMessage,
autoClose: false
});
} catch (e) {
@@ -69,9 +69,18 @@ Component.register('vrpayment-order-action-refund-partial', {
});
}).catch((errorResponse) => {
try {
var errorTitle = errorResponse?.response?.data?.errors?.[0]?.title ?? this.$tc('vrpayment-order.refundAction.refundCreateError.errorTitle')
var errorMessage;
switch(errorResponse.response.data) {
case 'methodDoesNotSupportRefund':
errorMessage = this.$tc('vrpayment-order.refundAction.refundCreateError.messagePaymentMethodDoesNotSupportRefund');
break;
default:
errorMessage = errorResponse.response.data.errors[0].detail;
}
this.createNotificationError({
title: errorResponse.response.data.errors[0].title,
message: errorResponse.response.data.errors[0].detail,
title: errorTitle,
message: errorMessage,
autoClose: false
});
} catch (e) {
@@ -70,9 +70,18 @@ Component.register('vrpayment-order-action-refund-selected', {
});
}).catch((errorResponse) => {
try {
var errorTitle = errorResponse?.response?.data?.errors?.[0]?.title ?? this.$tc('vrpayment-order.refundAction.refundCreateError.errorTitle')
var errorMessage;
switch(errorResponse.response.data) {
case 'methodDoesNotSupportRefund':
errorMessage = this.$tc('vrpayment-order.refundAction.refundCreateError.messagePaymentMethodDoesNotSupportRefund');
break;
default:
errorMessage = errorResponse.response.data.errors[0].detail;
}
this.createNotificationError({
title: errorResponse.response.data.errors[0].title,
message: errorResponse.response.data.errors[0].detail,
title: errorTitle,
message: errorMessage,
autoClose: false
});
} catch (e) {
@@ -9,6 +9,7 @@
:max="this.$parent.$parent.itemRefundableQuantity"
:min="0"
v-model:value="refundQuantity"
number-type="int"
:label="$tc('vrpayment-order.refund.refundQuantity.label')">
</sw-number-field>
@@ -68,9 +68,24 @@ Component.register('vrpayment-order-action-refund', {
});
}).catch((errorResponse) => {
try {
var errorTitle = errorResponse?.response?.data?.errors?.[0]?.title ?? this.$tc('vrpayment-order.refundAction.refundCreateError.errorTitle')
var errorMessage;
switch(errorResponse.response.data) {
case 'refundQuantityZero':
errorMessage = this.$tc('vrpayment-order.refundAction.refundCreateError.messageRefundQuantityIsZero');
break;
case 'refundExceedsQuantity':
errorMessage = this.$tc('vrpayment-order.refundAction.refundCreateError.messageRefundQuantityExceedsAvailableBalance');
break;
case 'methodDoesNotSupportRefund':
errorMessage = this.$tc('vrpayment-order.refundAction.refundCreateError.messagePaymentMethodDoesNotSupportRefund');
break;
default:
errorMessage = errorResponse.response.data.errors[0].detail;
}
this.createNotificationError({
title: errorResponse.response.data.errors[0].title,
message: errorResponse.response.data.errors[0].detail,
title: errorTitle,
message: errorMessage,
autoClose: false
});
} catch (e) {
@@ -98,7 +98,7 @@
<template #actions="{ item }">
<sw-context-menu-item
:disabled="transaction.state != 'FULFILL' || item.refundableQuantity != item.quantity || item.refundableAmount == 0 || item.itemRefundedAmount > 0 || item.itemRefundedQuantity > 0"
@click="lineItemRefund(item.uniqueId)">
@click="lineItemRefund(item.uniqueId, item.quantity)">
{{ $tc('vrpayment-order.buttons.label.refund-whole-line-item') }}
</sw-context-menu-item>
@@ -332,12 +332,12 @@ Component.register('vrpayment-order-detail', {
this.modalType = '';
},
lineItemRefund(lineItemId) {
lineItemRefund(lineItemId, itemQuantity) {
this.isLoading = true;
this.VRPaymentRefundService.createRefund(
this.transactionData.transactions[0].metaData.salesChannelId,
this.transactionData.transactions[0].id,
0,
itemQuantity,
lineItemId
).then(() => {
this.createNotificationSuccess({
@@ -351,9 +351,18 @@ Component.register('vrpayment-order-detail', {
});
}).catch((errorResponse) => {
try {
var errorTitle = errorResponse?.response?.data?.errors?.[0]?.title ?? this.$tc('vrpayment-order.refundAction.refundCreateError.errorTitle')
var errorMessage;
switch(errorResponse.response.data) {
case 'methodDoesNotSupportRefund':
errorMessage = this.$tc('vrpayment-order.refundAction.refundCreateError.messagePaymentMethodDoesNotSupportRefund');
break;
default:
errorMessage = errorResponse.response.data.errors[0].detail;
}
this.createNotificationError({
title: errorResponse.response.data.errors[0].title,
message: errorResponse.response.data.errors[0].detail,
title: errorTitle,
message: errorMessage,
autoClose: false
});
} catch (e) {
@@ -385,7 +394,7 @@ Component.register('vrpayment-order-detail', {
// Force the DOM to update before proceeding with the asynchronous operations
this.$nextTick(() => {
const refundPromises = this.selectedItems.map((item) => {
return this.lineItemRefundBulk(item.uniqueId); // Simulated refund action with delay
return this.lineItemRefundBulk(item.uniqueId, item.quantity); // Simulated refund action with delay
});
// Wait for all refund promises to complete
@@ -410,7 +419,7 @@ Component.register('vrpayment-order-detail', {
});
}
},
lineItemRefundBulk(lineItemId) {
lineItemRefundBulk(lineItemId, itemQuantity) {
return new Promise((resolve, reject) => {
this.VRPaymentRefundService.createRefund(
this.transactionData.transactions[0].metaData.salesChannelId,
@@ -427,9 +436,18 @@ Component.register('vrpayment-order-detail', {
})
.catch((errorResponse) => {
try {
var errorTitle = errorResponse?.response?.data?.errors?.[0]?.title ?? this.$tc('vrpayment-order.refundAction.refundCreateError.errorTitle')
var errorMessage;
switch(errorResponse.response.data) {
case 'methodDoesNotSupportRefund':
errorMessage = this.$tc('vrpayment-order.refundAction.refundCreateError.messagePaymentMethodDoesNotSupportRefund');
break;
default:
errorMessage = errorResponse.response.data.errors[0].detail;
}
this.createNotificationError({
title: errorResponse.response.data.errors[0].title,
message: errorResponse.response.data.errors[0].detail,
title: errorTitle,
message: errorMessage,
autoClose: false
});
} catch (e) {
@@ -77,7 +77,15 @@
"successMessage": "Ihre Rückerstattung war erfolgreich",
"successTitle": "Erfolg",
"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": {
"cardTitle": "Einzelheiten",
@@ -9,7 +9,6 @@
"void": "Cancel authorization",
"refund-whole-line-item": "Refund whole line item",
"refund-line-item-by-quantity": "Refund by quantity",
"refund-line-item-selected": "Rembourser sélectionnés",
"refund-line-item-selected": "Refund selected",
"refund-line-item-parial": "Partial refund"
}
@@ -78,7 +77,15 @@
"successMessage": "Your refund was successful.",
"successTitle": "Success",
"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": {
"cardTitle": "Details",
@@ -77,7 +77,15 @@
"successMessage": "Votre remboursement a été effectué avec succès.",
"successTitle": "Succès",
"maxAvailableItemsToRefund": "Nombre maximum d'articles disponibles pour le remboursement",
"maxAvailableAmountToRefund": "Montant maximal disponible pour le remboursement"
"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": {
"cardTitle": "Détails",
@@ -77,7 +77,15 @@
"successMessage": "Il tuo rimborso è andato a buon fine.",
"successTitle": "Successo",
"maxAvailableItemsToRefund": "Numero massimo di articoli disponibili da rimborsare",
"maxAvailableAmountToRefund": "Importo massimo disponibile per il rimborso"
"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": {
"cardTitle": "Dettagli",
@@ -14,7 +14,7 @@
{% block vrpayment_settings_content_card_channel_config_credentials_card_container_settings_space_id %}
<sw-inherit-wrapper
v-model:value="actualConfigData[CONFIG_SPACE_ID]"
:inheritedValue="selectedSalesChannelId === null ? null : allConfigs['null'][CONFIG_SPACE_ID]"
:inheritedValue="getInheritedValue(CONFIG_SPACE_ID)"
:customInheritationCheckFunction="checkNumberFieldInheritance">
<template #content="props">
<sw-number-field
@@ -23,7 +23,7 @@
:mapInheritance="props"
:label="$tc('vrpayment-settings.settingForm.credentials.spaceId.label')"
:helpText="$tc('vrpayment-settings.settingForm.credentials.spaceId.tooltipText')"
:disabled="props.isInherited || !acl.can('vrpayment.editor')"
:disabled="!acl.can('vrpayment.editor')"
:value="props.currentValue"
:error="spaceIdErrorState"
@update:value="props.updateCurrentValue">
@@ -35,7 +35,7 @@
{% block vrpayment_settings_content_card_channel_config_credentials_card_container_settings_user_id %}
<sw-inherit-wrapper
v-model:value="actualConfigData[CONFIG_USER_ID]"
:inheritedValue="selectedSalesChannelId === null ? null : allConfigs['null'][CONFIG_USER_ID]"
:inheritedValue="getInheritedValue(CONFIG_USER_ID)"
:customInheritationCheckFunction="checkNumberFieldInheritance">
<template #content="props">
<sw-number-field
@@ -44,7 +44,7 @@
:mapInheritance="props"
:label="$tc('vrpayment-settings.settingForm.credentials.userId.label')"
:helpText="$tc('vrpayment-settings.settingForm.credentials.userId.tooltipText')"
:disabled="props.isInherited || !acl.can('vrpayment.editor')"
:disabled="!acl.can('vrpayment.editor')"
:value="props.currentValue"
:error="userIdErrorState"
@update:value="props.updateCurrentValue">
@@ -56,7 +56,7 @@
{% block vrpayment_settings_content_card_channel_config_credentials_card_container_settings_application_key %}
<sw-inherit-wrapper
v-model:value="actualConfigData[CONFIG_APPLICATION_KEY]"
:inheritedValue="selectedSalesChannelId === null ? null : allConfigs['null'][CONFIG_APPLICATION_KEY]"
:inheritedValue="getInheritedValue(CONFIG_APPLICATION_KEY)"
:customInheritationCheckFunction="checkTextFieldInheritance">
<template #content="props">
<sw-password-field
@@ -66,7 +66,7 @@
:mapInheritance="props"
:label="$tc('vrpayment-settings.settingForm.credentials.applicationKey.label')"
:helpText="$tc('vrpayment-settings.settingForm.credentials.applicationKey.tooltipText')"
:disabled="props.isInherited || !acl.can('vrpayment.editor')"
:disabled="!acl.can('vrpayment.editor')"
:value="props.currentValue"
:error="applicationKeyErrorState"
@update:value="props.updateCurrentValue">
@@ -6,7 +6,7 @@ import constants from '../../page/vrpayment-settings/configuration-constants'
const {Component, Mixin} = Shopware;
Component.register('sw-vrpayment-credentials', {
template: template,
template,
name: 'VRPaymentCredentials',
@@ -29,7 +29,9 @@ Component.register('sw-vrpayment-credentials', {
},
selectedSalesChannelId: {
required: true
type: [String, null],
required: false,
default: null
},
spaceIdFilled: {
type: Boolean,
@@ -68,22 +70,24 @@ Component.register('sw-vrpayment-credentials', {
};
},
computed: {
currentConfig() {
if (this.selectedSalesChannelId && this.allConfigs[this.selectedSalesChannelId]) {
return this.allConfigs[this.selectedSalesChannelId];
}
return this.allConfigs['null'] || {};
}
},
methods: {
checkTextFieldInheritance(value) {
if (typeof value !== 'string') {
return true;
}
return value.length <= 0;
return !value || value.length <= 0;
},
checkNumberFieldInheritance(value) {
if (typeof value !== 'number') {
return true;
}
return value.length <= 0;
return value == null || value === '';
},
checkBoolFieldInheritance(value) {
@@ -94,12 +98,16 @@ Component.register('sw-vrpayment-credentials', {
// Used to trigger API connection testing from this component.
emitCheckApiConnectionEvent() {
const apiConnectionParams = {
spaceId: this.actualConfigData[constants.CONFIG_SPACE_ID],
userId: this.actualConfigData[constants.CONFIG_USER_ID],
applicationKey: this.actualConfigData[constants.CONFIG_APPLICATION_KEY]
spaceId: this.currentConfig[constants.CONFIG_SPACE_ID],
userId: this.currentConfig[constants.CONFIG_USER_ID],
applicationKey: this.currentConfig[constants.CONFIG_APPLICATION_KEY]
};
this.$emit('check-api-connection-event', apiConnectionParams);
},
getInheritedValue(key) {
return this.allConfigs['null']?.[key] ?? null;
}
}
});
@@ -1,11 +1,11 @@
{% block vrpayment_settings %}
<sw-page class="vrpayment-settings">
<sw-page class="vrpayment-settings">
{% block vrpayment_settings_header %}
<template #smart-bar-header>
<h2>
{{ $tc('sw-settings.index.title') }}
<sw-icon name="small-arrow-medium-right" small></sw-icon>
<mt-icon name="small-arrow-medium-right" size="16px"></mt-icon>
{{ $tc('vrpayment-settings.header') }}
</h2>
</template>
@@ -14,7 +14,7 @@
{% block vrpayment_settings_actions %}
<template #smart-bar-actions>
{% block vrpayment_settings_actions_save %}
<sw-button-process
<mt-button
v-model:value="isSaveSuccessful"
class="sw-settings-login-registration__save-action"
variant="primary"
@@ -22,7 +22,7 @@
:disabled="isLoading"
@click="onSave">
{{ $tc('vrpayment-settings.settingForm.save') }}
</sw-button-process>
</mt-button>
{% endblock %}
</template>
{% endblock %}
@@ -31,7 +31,7 @@
<template #content>
{% block vrpayment_settings_content_card %}
<sw-card-view>
<mt-card-view>
{% block vrpayment_settings_content_card_channel_config %}
<sw-sales-channel-config v-model:value="config"
@@ -42,18 +42,17 @@
<template #select="{ onInput, selectedSalesChannelId, salesChannel }">
{% block vrpayment_settings_content_card_channel_config_sales_channel_card %}
<sw-card title="Sales Channel Switch">
<mt-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"
:value="selectedSalesChannelId"
:options="salesChannel.map(sc => ({ id: sc.id, name: sc.translated.name }))"
labelProperty="name"
valueProperty="id"
:mapInheritance="props"
:isLoading="isLoading"
:options="salesChannel"
@update:value="onInput">
</sw-single-select>
@update:value="onInput"
/>
{% endblock %}
{% block vrpayment_settings_content_card_channel_config_sales_channel_card_footer %}
<template #footer>
@@ -66,18 +65,19 @@
{% endblock %}
{% block vrpayment_settings_content_card_channel_config_sales_channel_card_footer_container_button %}
<sw-button-process
<sw-button
variant="primary"
v-model:value="isSetDefaultPaymentSuccessful"
:isLoading="isSettingDefaultPaymentMethods"
@click="onSetPaymentMethodDefault">
{{ $tc('vrpayment-settings.salesChannelCard.button.label') }}
</sw-button-process>
</sw-button>
{% endblock %}
</sw-container>
{% endblock %}
</template>
{% endblock %}
</sw-card>
</mt-card>
{% endblock %}
</template>
{% endblock %}
@@ -134,12 +134,12 @@
{% endblock %}
{% block vrpayment_settings_content_card_loading %}
<sw-loader v-if="isLoading"></sw-loader>
<mt-loader v-if="isLoading"></mt-loader>
{% endblock %}
</sw-card-view>
</mt-card-view>
{% endblock %}
</template>
{% endblock %}
</sw-page>
</sw-page>
{% endblock %}
@@ -80,7 +80,7 @@ Component.register('vrpayment-settings', {
watch: {
config: {
handler(configData) {
const defaultConfig = this.$refs.configComponent.allConfigs.null;
const defaultConfig = (this.$refs.configComponent.allConfigs || {}).null || {};
const salesChannelId = this.$refs.configComponent.selectedSalesChannelId;
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">
<argument type="service" id="VRPaymentPayment\Core\Api\Refund\Service\RefundService"/>
<argument type="service" id="VRPaymentPayment\Core\Settings\Service\SettingsService"/>
<argument type="service" id="VRPaymentPayment\Core\Api\Transaction\Service\TransactionService"/>
<call method="setLogger">
<argument type="service" id="monolog.logger.vrpayment_payment"/>
</call>
@@ -13,6 +13,10 @@
<argument type="service" id="VRPaymentPayment\Core\Api\Transaction\Service\TransactionService"/>
<argument type="service" id="Shopware\Storefront\Page\GenericPageLoader"/>
<argument type="service" id="Shopware\Core\Checkout\Order\SalesChannel\OrderRoute"/>
<argument type="service" id="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"/>
<argument type="service" id="VRPaymentPayment\Core\Util\LocaleCodeProvider"/>
<call method="setLogger">
<argument type="service" id="monolog.logger.vrpayment_payment"/>
</call>
BIN
View File
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long