From 0cf75f514343c73a34aa99a9fdea9389aac5fca2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20M=C3=BCller?= <p.mueller@pcsg.de> Date: Thu, 20 Feb 2025 12:28:48 +0000 Subject: [PATCH] feat: sepa payment --- .phive/phars.xml | 10 +- ajax/confirmSubscription.php | 12 +- ajax/createSubscription.php | 21 +- bin/controls/PaymentDisplay.js | 73 +- bin/controls/Recurring/PaymentDisplay.js | 7 +- composer.json | 4 + cron.xml | 14 + locale.xml | 148 +++- phpstan-baseline.neon | 664 +----------------- settings.xml | 67 +- .../Payments/Stripe/AbstractBasePayment.php | 70 +- .../Payments/Stripe/ChargeDisputeService.php | 155 ++++ .../Stripe/Cron/PendingPaymentsService.php | 140 ++++ src/QUI/ERP/Payments/Stripe/Events.php | 16 +- .../Stripe/OrderProcessingService.php | 244 +++++++ .../OrderProcessingServiceInterface.php | 19 + .../PaymentMethods/AbstractBrowserPay.php | 30 +- .../Stripe/PaymentMethods/ApplePay.php | 2 +- .../Payments/Stripe/PaymentMethods/Card.php | 5 +- .../Stripe/PaymentMethods/GooglePay.php | 2 +- .../Stripe/PaymentMethods/MicrosoftPay.php | 2 +- .../AbstractBaseRecurringPayment.php | 40 +- .../Recurring/AbstractRecurringBrowserPay.php | 24 + .../PaymentMethods/Recurring/ApplePay.php | 2 +- .../Stripe/PaymentMethods/Recurring/Card.php | 5 +- .../PaymentMethods/Recurring/GooglePay.php | 2 +- .../PaymentMethods/Recurring/MicrosoftPay.php | 2 +- .../PaymentMethods/Recurring/SepaDebit.php | 207 ++++++ .../Recurring/Subscriptions.php | 172 +++-- .../Stripe/PaymentMethods/SepaDebit.php | 179 +++++ src/QUI/ERP/Payments/Stripe/Provider.php | 79 ++- src/QUI/ERP/Payments/Stripe/Utils.php | 193 ++++- .../ERP/Payments/Stripe/WebhookHandler.php | 66 ++ 33 files changed, 1817 insertions(+), 859 deletions(-) create mode 100644 src/QUI/ERP/Payments/Stripe/ChargeDisputeService.php create mode 100644 src/QUI/ERP/Payments/Stripe/Cron/PendingPaymentsService.php create mode 100644 src/QUI/ERP/Payments/Stripe/OrderProcessingService.php create mode 100644 src/QUI/ERP/Payments/Stripe/OrderProcessingServiceInterface.php create mode 100644 src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/SepaDebit.php create mode 100644 src/QUI/ERP/Payments/Stripe/PaymentMethods/SepaDebit.php diff --git a/.phive/phars.xml b/.phive/phars.xml index 5bfa092..083bf35 100644 --- a/.phive/phars.xml +++ b/.phive/phars.xml @@ -1,8 +1,8 @@ <?xml version="1.0" encoding="UTF-8"?> <phive xmlns="https://phar.io/phive"> - <phar name="phpstan" version="1.11.8" installed="1.11.8" location="./tools/phpstan" copy="false"/> - <phar name="phpunit" version="^10.5.20" installed="10.5.20" location="./tools/phpunit" copy="false"/> - <phar name="phpcs" version="^3.10.1" installed="3.10.1" location="./tools/phpcs" copy="false"/> - <phar name="phpcbf" version="^3.10.1" installed="3.10.1" location="./tools/phpcbf" copy="false"/> - <phar name="captainhook" version="^5.23.3" installed="5.23.3" location="./tools/captainhook" copy="false"/> + <phar name="phpstan" version="1.11.8" installed="1.11.8" location="./tools/phpstan" copy="true"/> + <phar name="phpunit" version="^10.5.20" installed="10.5.20" location="./tools/phpunit" copy="true"/> + <phar name="phpcs" version="^3.10.1" installed="3.10.1" location="./tools/phpcs" copy="true"/> + <phar name="phpcbf" version="^3.10.1" installed="3.10.1" location="./tools/phpcbf" copy="true"/> + <phar name="captainhook" version="^5.23.3" installed="5.23.3" location="./tools/captainhook" copy="true"/> </phive> diff --git a/ajax/confirmSubscription.php b/ajax/confirmSubscription.php index 81f0577..62da9f2 100644 --- a/ajax/confirmSubscription.php +++ b/ajax/confirmSubscription.php @@ -10,6 +10,7 @@ use QUI\ERP\Payments\Stripe\PaymentMethods\Recurring\Subscriptions; use QUI\ERP\Payments\Stripe\AbstractBasePayment; use Stripe\Exception\CardException; +use QUI\ERP\Payments\Stripe\PaymentMethods\Recurring\AbstractBaseRecurringPayment; /** * Confirm a Stripe Subscription @@ -23,9 +24,18 @@ function ($orderHash) { try { $orderHash = Orthos::clear($orderHash); $Order = Handler::getInstance()->getOrderByHash($orderHash); + $paymentType = $Order->getPayment()->getPaymentType(); + + if (!($paymentType instanceof AbstractBaseRecurringPayment)) { + throw new StripeException("Cannot create Stripe subscription. Wrong order payment type.", 0, [ + 'orderUuid' => $Order->getUUID(), + 'paymentTypeActual' => $paymentType::class + ]); + } return Subscriptions::confirmSubscription( - $Order->getPaymentDataEntry(AbstractBasePayment::ATTR_STRIPE_SUBSCRIPTION_ID) + $Order->getPaymentDataEntry(AbstractBasePayment::ATTR_STRIPE_SUBSCRIPTION_ID), + $paymentType ); } catch (Exception $Exception) { if ($Exception instanceof CardException) { diff --git a/ajax/createSubscription.php b/ajax/createSubscription.php index a2b0e25..68cb11a 100644 --- a/ajax/createSubscription.php +++ b/ajax/createSubscription.php @@ -5,15 +5,15 @@ */ use QUI\ERP\Order\Handler; -use QUI\ERP\Payments\Stripe\PaymentMethods\Recurring\AbstractBaseRecurringPayment; -use QUI\Utils\Security\Orthos; -use QUI\ERP\Payments\Stripe\StripeException; use QUI\ERP\Payments\Stripe\AbstractBasePayment; +use QUI\ERP\Payments\Stripe\PaymentMethods\Recurring\AbstractBaseRecurringPayment; use QUI\ERP\Payments\Stripe\PaymentMethods\Recurring\Subscriptions; -use Stripe\Subscription as StripeSubscription; -use Stripe\Invoice as StripeInvoice; +use QUI\ERP\Payments\Stripe\StripeException; use QUI\ERP\Payments\Stripe\Utils; +use QUI\Utils\Security\Orthos; use Stripe\Exception\CardException; +use Stripe\Invoice as StripeInvoice; +use Stripe\Subscription as StripeSubscription; /** * Create a Stripe PaymentIntent @@ -29,6 +29,15 @@ function ($orderHash, $paymentMethodId, $resetPaymentMethod = null) { $paymentMethodId = Orthos::clear($paymentMethodId); $Order = Handler::getInstance()->getOrderByHash($orderHash); + $paymentType = $Order->getPayment()->getPaymentType(); + + if (!($paymentType instanceof AbstractBaseRecurringPayment)) { + throw new StripeException("Cannot create Stripe subscription. Wrong order payment type.", 0, [ + 'orderUuid' => $Order->getUUID(), + 'paymentTypeActual' => $paymentType::class + ]); + } + /** @var AbstractBaseRecurringPayment $Payment */ $Payment = $Order->getPayment()->getPaymentType(); @@ -66,7 +75,7 @@ function ($orderHash, $paymentMethodId, $resetPaymentMethod = null) { } } - return Subscriptions::confirmSubscription($subscriptionId); + return Subscriptions::confirmSubscription($subscriptionId, $Payment); } catch (Exception $Exception) { if ($Exception instanceof CardException) { QUI\System\Log::writeDebugException($Exception); diff --git a/bin/controls/PaymentDisplay.js b/bin/controls/PaymentDisplay.js index 80966ed..60fc8a5 100644 --- a/bin/controls/PaymentDisplay.js +++ b/bin/controls/PaymentDisplay.js @@ -39,7 +39,8 @@ define('package/quiqqer/payment-stripe/bin/controls/PaymentDisplay', [ '$showMsg', '$onGeneralError', '$getPayBtn', - '$onBrowserPaySubmit' + '$onBrowserPaySubmit', + 'getPaymentMode' ], options: { @@ -111,9 +112,7 @@ define('package/quiqqer/payment-stripe/bin/controls/PaymentDisplay', [ var self = this; var onStripeJsLoaded = function () { - self.$Stripe = Stripe(self.getAttribute('publickey')); - self.$Elements = self.$Stripe.elements(); - + self.$Stripe = Stripe(self.getAttribute('publickey')); self.$showStripeForm(); }; @@ -144,16 +143,17 @@ define('package/quiqqer/payment-stripe/bin/controls/PaymentDisplay', [ * Show Stripe payment method form */ $showStripeForm: function () { - var self = this; - this.$OrderProcess.Loader.show(); - var PayBtnElm = this.$Elm.getElement('.quiqqer-payment-stripe-submit'), - PaymentForm = this.$Elm.getElement('.quiqqer-payment-stripe-form'); + const PayBtnElm = this.$Elm.getElement('.quiqqer-payment-stripe-submit'); + const PaymentForm = this.$Elm.getElement('.quiqqer-payment-stripe-form'); + const paymentMethod = this.getAttribute('paymentmethod'); - switch (this.getAttribute('paymentmethod')) { + let elements; + + switch (paymentMethod) { case 'browser_pay': - var PaymentRequest = self.$Stripe.paymentRequest({ + var PaymentRequest = this.$Stripe.paymentRequest({ country : this.getAttribute('country'), currency: this.getAttribute('currency'), total : { @@ -166,7 +166,8 @@ define('package/quiqqer/payment-stripe/bin/controls/PaymentDisplay', [ //requestPayerEmail: true, }); - this.$StripeFormElement = this.$Elements.create('paymentRequestButton', { + elements = this.$Stripe.elements(); + this.$StripeFormElement = elements.create('paymentRequestButton', { paymentRequest: PaymentRequest, style : { base : { @@ -187,15 +188,15 @@ define('package/quiqqer/payment-stripe/bin/controls/PaymentDisplay', [ } }); - PaymentRequest.canMakePayment().then(function (canMakePayment) { + PaymentRequest.canMakePayment().then((canMakePayment) => { if (!canMakePayment) { - var browser = self.getAttribute('browser'); + var browser = this.getAttribute('browser'); switch (browser) { case 'Chrome': case 'Safari': case 'Edge': - self.$showErrorMsg( + this.$showErrorMsg( QUILocale.get( pkg, 'controls.PaymentDisplay.browser_pay.error.cannot_use.' + browser @@ -204,7 +205,7 @@ define('package/quiqqer/payment-stripe/bin/controls/PaymentDisplay', [ break; default: - self.$showErrorMsg( + this.$showErrorMsg( QUILocale.get( pkg, 'controls.PaymentDisplay.browser_pay.error.cannot_use' @@ -212,27 +213,25 @@ define('package/quiqqer/payment-stripe/bin/controls/PaymentDisplay', [ ); } - self.fireEvent('processingError', [self]); + this.fireEvent('processingError', [this]); PaymentForm.destroy(); return; } - PaymentRequest.on('paymentmethod', self.$onBrowserPaySubmit); + PaymentRequest.on('paymentmethod', this.$onBrowserPaySubmit); - self.$StripeFormElement.mount(PaymentForm); - self.$OrderProcess.Loader.hide(); + this.$StripeFormElement.mount(PaymentForm); + this.$OrderProcess.Loader.hide(); }); break; - case 'card': default: - //this.$showMsg(QUILocale.get(pkg, 'PaymentDisplay.card.info')); - - this.$PayBtn = this.$getPayBtn().inject(PayBtnElm); + let paymentMethodTypes = [paymentMethod]; + let elementType; - this.$StripeFormElement = this.$Elements.create('card', { + const elementOptions = { style: { base : { color : '#13151b', @@ -250,14 +249,35 @@ define('package/quiqqer/payment-stripe/bin/controls/PaymentDisplay', [ } } } + }; + + switch (paymentMethod) { + case 'sepa_debit': + elementOptions.supportedCountries = ['SEPA']; + break; + } + + this.$PayBtn = this.$getPayBtn().inject(PayBtnElm); + + elements = this.$Stripe.elements({ + mode : this.getPaymentMode(), + paymentMethodTypes: paymentMethodTypes, + amount : this.getAttribute('amount'), + currency : this.getAttribute('currency') }); + this.$StripeFormElement = elements.create('payment', elementOptions); + this.$StripeFormElement.mount(PaymentForm); this.$OrderProcess.Loader.hide(); break; } }, + getPaymentMode: function () { + return 'payment'; + }, + /** * Handle special Stripe paymentRequest submit (Browser Pay) * @@ -365,7 +385,10 @@ define('package/quiqqer/payment-stripe/bin/controls/PaymentDisplay', [ var self = this; this.$OrderProcess.Loader.show(); - this.$Stripe.handleCardAction(Confirmation.clientSecret).then(function (result) { + + this.$Stripe.handleNextAction({ + clientSecret: Confirmation.clientSecret + }).then(function (result) { if (result.error) { self.$onStripeError(result.error); return; diff --git a/bin/controls/Recurring/PaymentDisplay.js b/bin/controls/Recurring/PaymentDisplay.js index 7c449ab..47be043 100644 --- a/bin/controls/Recurring/PaymentDisplay.js +++ b/bin/controls/Recurring/PaymentDisplay.js @@ -27,7 +27,8 @@ define('package/quiqqer/payment-stripe/bin/controls/Recurring/PaymentDisplay', [ Binds: [ '$onPaymentFormSubmit', '$getPayBtn', - '$handleSubscriptionConfirmation' + '$handleSubscriptionConfirmation', + 'getPaymentMode' ], initialize: function (options) { @@ -161,6 +162,10 @@ define('package/quiqqer/payment-stripe/bin/controls/Recurring/PaymentDisplay', [ } }, + getPaymentMode: function () { + return 'subscription'; + }, + /** * Get payment button * diff --git a/composer.json b/composer.json index 115e53b..d51010b 100644 --- a/composer.json +++ b/composer.json @@ -24,6 +24,10 @@ "stripe/stripe-php": "^15", "cbschuld/browser.php": "^1" }, + "suggest": { + "quiqqer/order": "Provide Stripe payments for ecoyn orders and subscriptions.", + "quiqqer/invoice": "Checks invoice payments with Stripe transactions." + }, "autoload": { "psr-4": { "QUI\\ERP\\Payments\\Stripe\\": "src/QUI/ERP/Payments/Stripe" diff --git a/cron.xml b/cron.xml index 47225a2..dd33292 100644 --- a/cron.xml +++ b/cron.xml @@ -15,4 +15,18 @@ </autocreate> </cron> + <cron exec="\QUI\ERP\Payments\Stripe\Cron\PendingPaymentsService::execute"> + <title> + <locale group="quiqqer/payment-stripe" var="cron.PendingPaymentsService.title"/> + </title> + <description> + <locale group="quiqqer/payment-stripe" var="cron.PendingPaymentsService.description"/> + </description> + + <autocreate> + <interval>0 * * * *</interval> + <active>1</active> + </autocreate> + </cron> + </crons> \ No newline at end of file diff --git a/locale.xml b/locale.xml index fc63bb9..aa93f33 100644 --- a/locale.xml +++ b/locale.xml @@ -77,6 +77,22 @@ <de><![CDATA[Wiederkehrende Zahlungen mit Microsoft Pay (nur Edge Browser)]]></de> <en><![CDATA[Recurring payments with Microsoft Pay (Edge browser only)]]></en> </locale> + <locale name="payment.SepaDebit.title"> + <de><![CDATA[Stripe - SEPA-Lastschrift]]></de> + <en><![CDATA[Stripe - SEPA Direct Debit]]></en> + </locale> + <locale name="payment.SepaDebit.description"> + <de><![CDATA[SEPA-Lastschrift über Stripe]]></de> + <en><![CDATA[SEPA Direct Debit via Stripe]]></en> + </locale> + <locale name="payment.Recurring.SepaDebit.title"> + <de><![CDATA[Stripe - SEPA-Lastschrift (Abonnements)]]></de> + <en><![CDATA[Stripe - SEPA Direct Debit (Subscriptions)]]></en> + </locale> + <locale name="payment.Recurring.SepaDebit.description"> + <de><![CDATA[SEPA-Lastschrift über Stripe]]></de> + <en><![CDATA[SEPA Direct Debit via Stripe]]></en> + </locale> <locale name="payment.frontend.Card.title"> <de><![CDATA[Kreditkarte]]></de> @@ -86,6 +102,14 @@ <de><![CDATA[Sichere Kreditkartenzahlung für die gängigsten Karten wie Visa/Visa Electron/Visa Debit, MasterCard/Maestro, Diners Club, Discover, China Union Pay, AMEX und JCB.]]></de> <en><![CDATA[Secure credit card payment for the most popular cards such as Visa/Visa Electron/Visa Debit, MasterCard/Maestro, Diners Club, Discover, China Union Pay, AMEX and JCB.]]></en> </locale> + <locale name="payment.frontend.SepaDebit.title"> + <de><![CDATA[SEPA-Lastschrift]]></de> + <en><![CDATA[SEPA Direct Debit]]></en> + </locale> + <locale name="payment.frontend.SepaDebit.description"> + <de><![CDATA[Zahlung mittels SEPA-Lastschriftverfahren]]></de> + <en><![CDATA[Payment via SEPA Direct Debit]]></en> + </locale> <locale name="payment.frontend.GooglePay.title"> <de><![CDATA[Google Pay]]></de> <en><![CDATA[Google Pay]]></en> @@ -143,6 +167,15 @@ <de><![CDATA[Sichere Kreditkartenzahlung für die gängigsten Karten wie Visa/Visa Electron/Visa Debit, MasterCard/Maestro, Diners Club, Discover, China Union Pay, AMEX und JCB.]]></de> <en><![CDATA[Secure credit card payment for the most popular cards such as Visa/Visa Electron/Visa Debit, MasterCard/Maestro, Diners Club, Discover, China Union Pay, AMEX and JCB.]]></en> </locale> + <locale name="payment.frontend.Recurring.SepaDebit.title"> + <de><![CDATA[SEPA-Lastschrift]]></de> + <en><![CDATA[SEPA Direct Debit]]></en> + </locale> + <locale name="payment.frontend.Recurring.SepaDebit.description"> + <de><![CDATA[Zahlung mittels SEPA-Lastschriftverfahren]]></de> + <en><![CDATA[Payment via SEPA Direct Debit]]></en> + </locale> + <locale name="payment.PaymentStep.title.ApplePay"> <de><![CDATA[Bezahlung mit Apple Pay]]></de> @@ -180,6 +213,22 @@ <de><![CDATA[Bitte geben Sie Ihre Kreditkarten-Daten im sicheren Formular an und klicken auf <b>Regelmäßige Zahlung autorisieren</b>, um den Zahlungsvorgang zu starten.]]></de> <en><![CDATA[Please provide your credit card information in the secure form and click <b>Authorize recurring payment</b> to start the payment process.]]></en> </locale> + <locale name="payment.PaymentStep.title.SepaDebit"> + <de><![CDATA[Bezahlung mit SEPA-Lastschrift (über Stripe)]]></de> + <en><![CDATA[Payment with SEPA Direct Debit (via Stripe)]]></en> + </locale> + <locale name="payment.PaymentStep.info.Card" html="true"> + <de><![CDATA[Bitte geben Sie Ihre Kontodaten im sicheren Formular an und klicken auf <b>Jetzt bezahlen</b>, um den Zahlungsvorgang zu starten.]]></de> + <en><![CDATA[Please provide your bank account information in the secure form and click <b>Authorize recurring payment</b> to start the payment process.]]></en> + </locale> + <locale name="payment.Recurring.PaymentStep.info.Card" html="true"> + <de><![CDATA[Bitte geben Sie Ihre Kreditkarten-Daten im sicheren Formular an und klicken auf <b>Regelmäßige Zahlung autorisieren</b>, um den Zahlungsvorgang zu starten.]]></de> + <en><![CDATA[Please provide your credit card information in the secure form and click <b>Authorize recurring payment</b> to start the payment process.]]></en> + </locale> + <locale name="payment.Recurring.PaymentStep.info.SepaDebit"> + <de><![CDATA[Bitte geben Sie Ihre Kontodaten im sicheren Formular an und klicken auf <b>Regelmäßige Zahlung autorisieren</b>, um den Zahlungsvorgang zu starten.]]></de> + <en><![CDATA[Please provide your bank account information in the secure form and click <b>Authorize recurring payment</b> to start the payment process.]]></en> + </locale> <locale name="additional_invoice_text.MicrosoftPay"> <de><![CDATA[Der Betrag wird automatisch über Ihr Microsoft Pay Konto eingezogen.]]></de> @@ -197,6 +246,10 @@ <de><![CDATA[Der Betrag wird automatisch über Ihre Kreditkarte eingezogen.]]></de> <en><![CDATA[The amount will be automatically collected via your credit card.]]></en> </locale> + <locale name="additional_invoice_text.SepaDebit"> + <de><![CDATA[Der Betrag wird automatisch per SEPA-Lastschriftverfahren von Ihrem angegebenen Bankkonto eingezogen.]]></de> + <en><![CDATA[The amount will be collected automatically by SEPA Direct Debit from your specified bank account.]]></en> + </locale> <!-- Recurring payments --> <locale name="history.invoice.add_paymill_transaction"> @@ -215,6 +268,10 @@ <de><![CDATA[Die Stripe Subscription [subscriptionId] konnte nicht gekündigt werden. Bitte prüfen Sie die Fehler-Logs.]]></de> <en><![CDATA[The Stripe Subscription [subscriptionId] could not be cancelled. Please check the error logs.]]></en> </locale> + <locale name="history.Invoice.stripe_charge_dispute_lost"> + <de><![CDATA[Stripe Abonnement-Rechnungstransaktion "[stripeInvoiceId]" wurde angefochten (Stripe Anfechtungs ID: "[stripeDisputeId]"). Die Anfechtung wurde verloren. Die zugehörige ecoyn Transaktion "[quiqqerTransactionId]" wurde als fehlerhaft markiert.]]></de> + <en><![CDATA[Stripe subscription invoice transaction "[stripeInvoiceId]" was disputed (Stripe Dispute ID: "[stripeDisputeId]". The dispute was lost. The associated eyocn Transaktion "[quiqqerTransactionId]" was marked as erroneous.]]></en> + </locale> <!-- Settings --> <locale name="settings.menu.title"> @@ -314,6 +371,33 @@ <li><b>{orderId}</b> - Order number (with prefix)</li> </ul>]]></en> </locale> + <locale name="settings.general.statement_descriptor.title"> + <de><![CDATA[Zahlungsbeschreibung in Abrechnungen]]></de> + <en><![CDATA[Statement descriptor]]></en> + </locale> + <locale name="settings.general.statement_descriptor.description" html="true"> + <de><![CDATA[Zahlungsbeschreibung, der in Abrechnungsvorgängen dem Benutzer angezeigt wird (z.B. auf dem Kontoauszug). <b>Gilt nicht</b> für Kartenzahlungen! + <br><br>Verfügbare Platzhalter: +<ul> +<li><b>{orderId}</b> - Bestellnummer (mit Präfix)</li> +</ul> +<p><b>Maximal 22 Zeichen inkl. konkreten Platzhalter-Werten!</b></p>]]></de> + <en><![CDATA[Statement descriptor that is shown to the user in billing procedures (e.g. account statement). <b>Not used</b> for credit card payments! +<br><br>Available placeholders: +<ul> +<li><b>{orderId}</b> - Order number (with prefix)</li> +</ul> +<p><b>Maximum 22 characters including concrete placeholder values!</b></p> +]]></en> + </locale> + <locale name="settings.general.notify_about_failed_payment.title"> + <de><![CDATA[Benachrichtigung bei fehlgeschlagener Zahlung]]></de> + <en><![CDATA[Notification about failed payment]]></en> + </locale> + <locale name="settings.general.notify_about_failed_payment.description"> + <de><![CDATA[Es soll eine Benachrichtigung per E-Mail an den Administrator gesendet werden, wenn eine Stripe-Zahlung fehlgeschlagen ist (Einmalzahlung / Abonnement-Zahlung).]]></de> + <en><![CDATA[An e-mail notification should be sent to the administrator if a Stripe payment has failed (one-time payment / subscription payment).]]></en> + </locale> <!-- Cron --> <locale name="cron.processUnpaidInvoices.title"> @@ -324,6 +408,14 @@ <de><![CDATA[Für alle offenen Rechnungen mit wiederkehrenden Zahlungen die passenden Stripe-Transaktionen (Invoices) finden und intern buchen]]></de> <en><![CDATA[For all open invoices with recurring payments, find the matching Stripe transactions (Invoices) and book them internally]]></en> </locale> + <locale name="cron.PendingPaymentsService.title"> + <de><![CDATA[Stripe: Ausstehende Zahlungen prüfen]]></de> + <en><![CDATA[Stripe: Check pending payments]]></en> + </locale> + <locale name="cron.PendingPaymentsService.description"> + <de><![CDATA[Fragt ausstehende Transaktionen bei Stripe ab und bucht diese im ecoyn-System]]></de> + <en><![CDATA[Queries pending transactions from Stripe and books them in the ecoyn system]]></en> + </locale> <!-- Permissions --> <locale name="permission.quiqqer.payments.stripe._header"> @@ -359,6 +451,54 @@ <groups name="quiqqer/payment-stripe" datatype="php"> + <!-- Mails --> + <locale name="mail.payment_failed.order.subject"> + <de><![CDATA[Stripe-Zahlung für Bestellung [orderId] fehlgeschlagen]]></de> + <en><![CDATA[Stripe payment for order [orderId] failed]]></en> + </locale> + <locale name="mail.payment_failed.order.body" html="true"> + <de><![CDATA[ +<p>Stripe hat die Zahlung für Bestellung <b>[orderId]</b> als fehlgeschlagen gemeldet. Die Stripe Transaktions-ID lautet: [stripePaymentIntentId] (<a href="[paymentIntentUrl]" target="_blank">Ansicht im Stripe Dashboard</a>).</p> +<p><b>Fehlermeldung:</b> [errorMsg]</p> +]]></de> + <en><![CDATA[ + <p>Stripe has reported the payment for order <b>[orderId]</b> as failed. The Stripe transaction ID is: [stripePaymentIntentId] (<a href=“[paymentIntentUrl]" target=“_blankâ€>view in the Stripe dashboard</a>).</p> +<p><b>Error message:</b> [errorMsg]</p> +]]></en> + </locale> + <locale name="mail.payment_failed.invoice.subject"> + <de><![CDATA[Stripe-Zahlung für Rechnung [invoiceNo] fehlgeschlagen]]></de> + <en><![CDATA[Stripe payment for invoice [invoiceNo] failed]]></en> + </locale> + <locale name="mail.payment_failed.invoice.body" html="true"> + <de><![CDATA[ +<p>Stripe hat die Zahlung für Rechnung <b>[invoiceNo]</b> als fehlgeschlagen gemeldet. Die Stripe Transaktions-ID lautet: [stripeInvoiceId] (<a href="[stripeInvoiceUrl]" target="_blank">Ansicht im Stripe Dashboard</a>).</p> +<p><b>Fehlermeldung:</b> [errorMsg]</p> +<p><b>Vertrag:</b> [contractNo]</p> +]]></de> + <en><![CDATA[ + <p>Stripe has reported the payment for invoice <b>[invoiceNo]</b> as failed. The Stripe transaction ID is: [stripeInvoiceId] ().</p> +<p><b>Error message:</b> [errorMsg]</p> +<p><b>Contract:</b> [contractNo]</p> +]]></en> + </locale> + <locale name="mail.disputed_transaction.subject"> + <de><![CDATA[Anfechtung einer Stripe-Transaktion!]]></de> + <en><![CDATA[Dispute of a Stripe transaction]]></en> + </locale> + <locale name="mail.disputed_transaction.body" html="true"> + <de><![CDATA[ +<p>Eine Stripe-Transaktion i.H.v. [amount] [currency] wurde angefochten. <a href="[chargeUrl]" target="_blank">Ansicht im Stripe Dashboard</a>.</p> +<p><b>Verknüpfte Bestellung:</b> [orderId]</p> +<p><b>Begründungs Code:</b> [reason] (<a href="https://docs.stripe.com/disputes/categories" target="_blank">weitere Informationen</a>)</p> +]]></de> + <en><![CDATA[ +<p>A stripe transaction in the amount of [amount] [currency] has been disputed. <a href="[chargeUrl]" target="_blank">View in Stripe dashboard</a>.</p> +<p><b>Associated order:</b> [orderId]</p> +<p><b>Reason code:</b> [reason] (<a href="https://docs.stripe.com/disputes/categories" target="_blank">additional information</a>)</p> +]]></en> + </locale> + <!-- Ajax messages --> <locale name="message.ajax.backend.settings.deleteBillingPlan.success"> <de><![CDATA[Der Abrechnungsplan [planId] wurde erfolgreich gelöscht.]]></de> @@ -521,12 +661,12 @@ <en><![CDATA[Click here to pay your order with a value of [display_price] with your credit card]]></en> </locale> <locale name="PaymentDisplay.checkout_process"> - <de><![CDATA[Zahlung mit Kreditkarte wird durchgeführt]]></de> - <en><![CDATA[Credit card payment is being processed]]></en> + <de><![CDATA[Zahlung wird durchgeführt]]></de> + <en><![CDATA[Payment is being processed]]></en> </locale> <locale name="PaymentDisplay.validation_error"> - <de><![CDATA[Bei der Validierung Ihrer Kreditkarten-Daten ist ein Fehler aufgetreten. Bitte überprüfen Sie Ihre Eingaben noch einmal und korrigieren diese ggf.]]></de> - <en><![CDATA[An error occurred during the validation of your credit card data. Please check your entries again and correct them if necessary.]]></en> + <de><![CDATA[Bei der Validierung Ihrer Zahlungsdaten ist ein Fehler aufgetreten. Bitte überprüfen Sie Ihre Eingaben noch einmal und korrigieren diese ggf.]]></de> + <en><![CDATA[An error occurred during the validation of your payment data data. Please check your entries again and correct them if necessary.]]></en> </locale> <locale name="PaymentDisplay.service_provider_error"> <de> diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 02fd72b..b321498 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1,683 +1,23 @@ parameters: ignoreErrors: - - - message: "#^Call to static method getInstance\\(\\) on an unknown class QUI\\\\ERP\\\\Order\\\\Handler\\.$#" - count: 1 - path: ajax/confirmPaymentIntent.php - - - - message: "#^Call to static method getInstance\\(\\) on an unknown class QUI\\\\ERP\\\\Order\\\\Handler\\.$#" - count: 1 - path: ajax/confirmSubscription.php - - - - message: "#^Call to static method getInstance\\(\\) on an unknown class QUI\\\\ERP\\\\Order\\\\Handler\\.$#" - count: 1 - path: ajax/createPaymentIntent.php - - - - message: "#^Call to static method getInstance\\(\\) on an unknown class QUI\\\\ERP\\\\Order\\\\Handler\\.$#" - count: 1 - path: ajax/createSubscription.php - - message: "#^Access to an undefined property Stripe\\\\StripeObject\\:\\:\\$type\\.$#" count: 1 path: src/QUI/ERP/Payments/Stripe/AbstractBasePayment.php - - - message: "#^Call to method addHistory\\(\\) on an unknown class QUI\\\\ERP\\\\Order\\\\AbstractOrder\\.$#" - count: 1 - path: src/QUI/ERP/Payments/Stripe/AbstractBasePayment.php - - - - message: "#^Call to method getPaymentDataEntry\\(\\) on an unknown class QUI\\\\ERP\\\\Order\\\\AbstractOrder\\.$#" - count: 4 - path: src/QUI/ERP/Payments/Stripe/AbstractBasePayment.php - - - - message: "#^Call to method getUUID\\(\\) on an unknown class QUI\\\\ERP\\\\Order\\\\AbstractOrder\\.$#" - count: 1 - path: src/QUI/ERP/Payments/Stripe/AbstractBasePayment.php - - - - message: "#^Call to method setContent\\(\\) on an unknown class QUI\\\\ERP\\\\Order\\\\Controls\\\\OrderProcess\\\\Processing\\.$#" - count: 1 - path: src/QUI/ERP/Payments/Stripe/AbstractBasePayment.php - - - - message: "#^Call to method setPaymentData\\(\\) on an unknown class QUI\\\\ERP\\\\Order\\\\AbstractOrder\\.$#" - count: 2 - path: src/QUI/ERP/Payments/Stripe/AbstractBasePayment.php - - - - message: "#^Call to method setSuccessfulStatus\\(\\) on an unknown class QUI\\\\ERP\\\\Order\\\\AbstractOrder\\.$#" - count: 1 - path: src/QUI/ERP/Payments/Stripe/AbstractBasePayment.php - - - - message: "#^Call to method setTitle\\(\\) on an unknown class QUI\\\\ERP\\\\Order\\\\Controls\\\\OrderProcess\\\\Processing\\.$#" - count: 1 - path: src/QUI/ERP/Payments/Stripe/AbstractBasePayment.php - - - - message: "#^Call to method update\\(\\) on an unknown class QUI\\\\ERP\\\\Order\\\\AbstractOrder\\.$#" - count: 1 - path: src/QUI/ERP/Payments/Stripe/AbstractBasePayment.php - - - - message: "#^Call to static method getInstance\\(\\) on an unknown class QUI\\\\ERP\\\\Order\\\\Handler\\.$#" - count: 1 - path: src/QUI/ERP/Payments/Stripe/AbstractBasePayment.php - - - - message: "#^Parameter \\$Order of method QUI\\\\ERP\\\\Payments\\\\Stripe\\\\AbstractBasePayment\\:\\:addOrderHistoryEntry\\(\\) has invalid type QUI\\\\ERP\\\\Order\\\\AbstractOrder\\.$#" - count: 2 - path: src/QUI/ERP/Payments/Stripe/AbstractBasePayment.php - - - - message: "#^Parameter \\$Order of method QUI\\\\ERP\\\\Payments\\\\Stripe\\\\AbstractBasePayment\\:\\:confirmPaymentIntent\\(\\) has invalid type QUI\\\\ERP\\\\Order\\\\AbstractOrder\\.$#" - count: 2 - path: src/QUI/ERP/Payments/Stripe/AbstractBasePayment.php - - - - message: "#^Parameter \\$Order of method QUI\\\\ERP\\\\Payments\\\\Stripe\\\\AbstractBasePayment\\:\\:createPaymentIntent\\(\\) has invalid type QUI\\\\ERP\\\\Order\\\\AbstractOrder\\.$#" - count: 2 - path: src/QUI/ERP/Payments/Stripe/AbstractBasePayment.php - - - - message: "#^Parameter \\$Order of method QUI\\\\ERP\\\\Payments\\\\Stripe\\\\AbstractBasePayment\\:\\:createPaymentIntentForOrder\\(\\) has invalid type QUI\\\\ERP\\\\Order\\\\AbstractOrder\\.$#" - count: 2 - path: src/QUI/ERP/Payments/Stripe/AbstractBasePayment.php - - - - message: "#^Parameter \\$Order of method QUI\\\\ERP\\\\Payments\\\\Stripe\\\\AbstractBasePayment\\:\\:getGatewayDisplay\\(\\) has invalid type QUI\\\\ERP\\\\Order\\\\AbstractOrder\\.$#" - count: 2 - path: src/QUI/ERP/Payments/Stripe/AbstractBasePayment.php - - - - message: "#^Parameter \\$Order of method QUI\\\\ERP\\\\Payments\\\\Stripe\\\\AbstractBasePayment\\:\\:getPaymentIntentByOrder\\(\\) has invalid type QUI\\\\ERP\\\\Order\\\\AbstractOrder\\.$#" - count: 2 - path: src/QUI/ERP/Payments/Stripe/AbstractBasePayment.php - - - - message: "#^Parameter \\$Order of method QUI\\\\ERP\\\\Payments\\\\Stripe\\\\AbstractBasePayment\\:\\:saveOrder\\(\\) has invalid type QUI\\\\ERP\\\\Order\\\\AbstractOrder\\.$#" - count: 2 - path: src/QUI/ERP/Payments/Stripe/AbstractBasePayment.php - - - - message: "#^Parameter \\$Step of method QUI\\\\ERP\\\\Payments\\\\Stripe\\\\AbstractBasePayment\\:\\:getGatewayDisplay\\(\\) has invalid type QUI\\\\ERP\\\\Order\\\\Controls\\\\OrderProcess\\\\Processing\\.$#" - count: 1 - path: src/QUI/ERP/Payments/Stripe/AbstractBasePayment.php - - - - message: "#^Parameter \\$Order of method QUI\\\\ERP\\\\Payments\\\\Stripe\\\\Events\\:\\:onPaymentsCanUsedInOrder\\(\\) has invalid type QUI\\\\ERP\\\\Order\\\\OrderInterface\\.$#" - count: 2 - path: src/QUI/ERP/Payments/Stripe/Events.php - - - - message: "#^Call to method getCurrency\\(\\) on an unknown class QUI\\\\ERP\\\\Order\\\\AbstractOrder\\.$#" - count: 1 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/AbstractBrowserPay.php - - - - message: "#^Parameter \\#1 \\$Source of static method QUI\\\\ERP\\\\Payments\\\\Stripe\\\\Utils\\:\\:getCentAmount\\(\\) expects QUI\\\\ERP\\\\ErpEntityInterface, QUI\\\\ERP\\\\Order\\\\AbstractOrder given\\.$#" - count: 1 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/AbstractBrowserPay.php - - - - message: "#^Parameter \\$Order of method QUI\\\\ERP\\\\Payments\\\\Stripe\\\\PaymentMethods\\\\AbstractBrowserPay\\:\\:createPaymentIntentForOrder\\(\\) has invalid type QUI\\\\ERP\\\\Order\\\\AbstractOrder\\.$#" - count: 2 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/AbstractBrowserPay.php - - - - message: "#^Call to method getCustomer\\(\\) on an unknown class QUI\\\\ERP\\\\Accounting\\\\Invoice\\\\Invoice\\.$#" - count: 1 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/ApplePay.php - - - - message: "#^Call to method getCustomer\\(\\) on an unknown class QUI\\\\ERP\\\\Accounting\\\\Invoice\\\\InvoiceTemporary\\.$#" - count: 1 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/ApplePay.php - - - - message: "#^Call to method getCustomer\\(\\) on an unknown class QUI\\\\ERP\\\\Accounting\\\\Invoice\\\\InvoiceView\\.$#" - count: 1 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/ApplePay.php - - - - message: "#^PHPDoc tag @return with type mixed is not subtype of native type string\\.$#" - count: 1 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/ApplePay.php - - - - message: "#^Parameter \\$Invoice of method QUI\\\\ERP\\\\Payments\\\\Stripe\\\\PaymentMethods\\\\ApplePay\\:\\:getInvoiceInformationText\\(\\) has invalid type QUI\\\\ERP\\\\Accounting\\\\Invoice\\\\Invoice\\.$#" - count: 2 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/ApplePay.php - - - - message: "#^Parameter \\$Invoice of method QUI\\\\ERP\\\\Payments\\\\Stripe\\\\PaymentMethods\\\\ApplePay\\:\\:getInvoiceInformationText\\(\\) has invalid type QUI\\\\ERP\\\\Accounting\\\\Invoice\\\\InvoiceTemporary\\.$#" - count: 2 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/ApplePay.php - - - - message: "#^Parameter \\$Invoice of method QUI\\\\ERP\\\\Payments\\\\Stripe\\\\PaymentMethods\\\\ApplePay\\:\\:getInvoiceInformationText\\(\\) has invalid type QUI\\\\ERP\\\\Accounting\\\\Invoice\\\\InvoiceView\\.$#" - count: 2 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/ApplePay.php - - - - message: "#^Call to method getCurrency\\(\\) on an unknown class QUI\\\\ERP\\\\Order\\\\AbstractOrder\\.$#" - count: 1 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/Card.php - - - - message: "#^Call to method getCustomer\\(\\) on an unknown class QUI\\\\ERP\\\\Accounting\\\\Invoice\\\\Invoice\\.$#" - count: 1 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/Card.php - - - - message: "#^Call to method getCustomer\\(\\) on an unknown class QUI\\\\ERP\\\\Accounting\\\\Invoice\\\\InvoiceTemporary\\.$#" - count: 1 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/Card.php - - - - message: "#^Call to method getCustomer\\(\\) on an unknown class QUI\\\\ERP\\\\Accounting\\\\Invoice\\\\InvoiceView\\.$#" - count: 1 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/Card.php - - - - message: "#^PHPDoc tag @return with type mixed is not subtype of native type string\\.$#" - count: 1 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/Card.php - - - - message: "#^Parameter \\#1 \\$Source of static method QUI\\\\ERP\\\\Payments\\\\Stripe\\\\Utils\\:\\:getCentAmount\\(\\) expects QUI\\\\ERP\\\\ErpEntityInterface, QUI\\\\ERP\\\\Order\\\\AbstractOrder given\\.$#" - count: 1 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/Card.php - - - - message: "#^Parameter \\$Invoice of method QUI\\\\ERP\\\\Payments\\\\Stripe\\\\PaymentMethods\\\\Card\\:\\:getInvoiceInformationText\\(\\) has invalid type QUI\\\\ERP\\\\Accounting\\\\Invoice\\\\Invoice\\.$#" - count: 2 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/Card.php - - - - message: "#^Parameter \\$Invoice of method QUI\\\\ERP\\\\Payments\\\\Stripe\\\\PaymentMethods\\\\Card\\:\\:getInvoiceInformationText\\(\\) has invalid type QUI\\\\ERP\\\\Accounting\\\\Invoice\\\\InvoiceTemporary\\.$#" - count: 2 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/Card.php - - - - message: "#^Parameter \\$Invoice of method QUI\\\\ERP\\\\Payments\\\\Stripe\\\\PaymentMethods\\\\Card\\:\\:getInvoiceInformationText\\(\\) has invalid type QUI\\\\ERP\\\\Accounting\\\\Invoice\\\\InvoiceView\\.$#" - count: 2 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/Card.php - - - - message: "#^Parameter \\$Order of method QUI\\\\ERP\\\\Payments\\\\Stripe\\\\PaymentMethods\\\\Card\\:\\:createPaymentIntentForOrder\\(\\) has invalid type QUI\\\\ERP\\\\Order\\\\AbstractOrder\\.$#" - count: 2 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/Card.php - - - - message: "#^Call to method getCustomer\\(\\) on an unknown class QUI\\\\ERP\\\\Accounting\\\\Invoice\\\\Invoice\\.$#" - count: 1 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/GooglePay.php - - - - message: "#^Call to method getCustomer\\(\\) on an unknown class QUI\\\\ERP\\\\Accounting\\\\Invoice\\\\InvoiceTemporary\\.$#" - count: 1 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/GooglePay.php - - - - message: "#^Call to method getCustomer\\(\\) on an unknown class QUI\\\\ERP\\\\Accounting\\\\Invoice\\\\InvoiceView\\.$#" - count: 1 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/GooglePay.php - - - - message: "#^PHPDoc tag @return with type mixed is not subtype of native type string\\.$#" - count: 1 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/GooglePay.php - - - - message: "#^Parameter \\$Invoice of method QUI\\\\ERP\\\\Payments\\\\Stripe\\\\PaymentMethods\\\\GooglePay\\:\\:getInvoiceInformationText\\(\\) has invalid type QUI\\\\ERP\\\\Accounting\\\\Invoice\\\\Invoice\\.$#" - count: 2 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/GooglePay.php - - - - message: "#^Parameter \\$Invoice of method QUI\\\\ERP\\\\Payments\\\\Stripe\\\\PaymentMethods\\\\GooglePay\\:\\:getInvoiceInformationText\\(\\) has invalid type QUI\\\\ERP\\\\Accounting\\\\Invoice\\\\InvoiceTemporary\\.$#" - count: 2 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/GooglePay.php - - - - message: "#^Parameter \\$Invoice of method QUI\\\\ERP\\\\Payments\\\\Stripe\\\\PaymentMethods\\\\GooglePay\\:\\:getInvoiceInformationText\\(\\) has invalid type QUI\\\\ERP\\\\Accounting\\\\Invoice\\\\InvoiceView\\.$#" - count: 2 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/GooglePay.php - - - - message: "#^Call to method getCustomer\\(\\) on an unknown class QUI\\\\ERP\\\\Accounting\\\\Invoice\\\\Invoice\\.$#" - count: 1 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/MicrosoftPay.php - - - - message: "#^Call to method getCustomer\\(\\) on an unknown class QUI\\\\ERP\\\\Accounting\\\\Invoice\\\\InvoiceTemporary\\.$#" - count: 1 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/MicrosoftPay.php - - - - message: "#^Call to method getCustomer\\(\\) on an unknown class QUI\\\\ERP\\\\Accounting\\\\Invoice\\\\InvoiceView\\.$#" - count: 1 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/MicrosoftPay.php - - - - message: "#^PHPDoc tag @return with type mixed is not subtype of native type string\\.$#" - count: 1 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/MicrosoftPay.php - - - - message: "#^Parameter \\$Invoice of method QUI\\\\ERP\\\\Payments\\\\Stripe\\\\PaymentMethods\\\\MicrosoftPay\\:\\:getInvoiceInformationText\\(\\) has invalid type QUI\\\\ERP\\\\Accounting\\\\Invoice\\\\Invoice\\.$#" - count: 2 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/MicrosoftPay.php - - - - message: "#^Parameter \\$Invoice of method QUI\\\\ERP\\\\Payments\\\\Stripe\\\\PaymentMethods\\\\MicrosoftPay\\:\\:getInvoiceInformationText\\(\\) has invalid type QUI\\\\ERP\\\\Accounting\\\\Invoice\\\\InvoiceTemporary\\.$#" - count: 2 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/MicrosoftPay.php - - - - message: "#^Parameter \\$Invoice of method QUI\\\\ERP\\\\Payments\\\\Stripe\\\\PaymentMethods\\\\MicrosoftPay\\:\\:getInvoiceInformationText\\(\\) has invalid type QUI\\\\ERP\\\\Accounting\\\\Invoice\\\\InvoiceView\\.$#" - count: 2 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/MicrosoftPay.php - - - - message: "#^Call to method getCurrency\\(\\) on an unknown class QUI\\\\ERP\\\\Order\\\\AbstractOrder\\.$#" - count: 1 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/AbstractBaseRecurringPayment.php - - - - message: "#^Call to method getUUID\\(\\) on an unknown class QUI\\\\ERP\\\\Order\\\\AbstractOrder\\.$#" - count: 1 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/AbstractBaseRecurringPayment.php - - - - message: "#^Call to method setContent\\(\\) on an unknown class QUI\\\\ERP\\\\Order\\\\Controls\\\\OrderProcess\\\\Processing\\.$#" - count: 1 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/AbstractBaseRecurringPayment.php - - - - message: "#^Call to method setTitle\\(\\) on an unknown class QUI\\\\ERP\\\\Order\\\\Controls\\\\OrderProcess\\\\Processing\\.$#" - count: 1 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/AbstractBaseRecurringPayment.php - - - - message: "#^Method QUI\\\\ERP\\\\Payments\\\\Stripe\\\\PaymentMethods\\\\Recurring\\\\AbstractBaseRecurringPayment\\:\\:createSubscription\\(\\) should return string\\|null but returns int\\|false\\.$#" - count: 1 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/AbstractBaseRecurringPayment.php - - - - message: "#^Parameter \\#1 \\$Source of static method QUI\\\\ERP\\\\Payments\\\\Stripe\\\\Utils\\:\\:getCentAmount\\(\\) expects QUI\\\\ERP\\\\ErpEntityInterface, QUI\\\\ERP\\\\Order\\\\AbstractOrder given\\.$#" - count: 1 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/AbstractBaseRecurringPayment.php - - - - message: "#^Parameter \\$Invoice of method QUI\\\\ERP\\\\Payments\\\\Stripe\\\\PaymentMethods\\\\Recurring\\\\AbstractBaseRecurringPayment\\:\\:captureSubscription\\(\\) has invalid type QUI\\\\ERP\\\\Accounting\\\\Invoice\\\\Invoice\\.$#" - count: 2 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/AbstractBaseRecurringPayment.php - - - - message: "#^Parameter \\$Order of method QUI\\\\ERP\\\\Payments\\\\Stripe\\\\PaymentMethods\\\\Recurring\\\\AbstractBaseRecurringPayment\\:\\:createPaymentIntentForOrder\\(\\) has invalid type QUI\\\\ERP\\\\Order\\\\AbstractOrder\\.$#" - count: 2 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/AbstractBaseRecurringPayment.php - - - - message: "#^Parameter \\$Order of method QUI\\\\ERP\\\\Payments\\\\Stripe\\\\PaymentMethods\\\\Recurring\\\\AbstractBaseRecurringPayment\\:\\:createSubscription\\(\\) has invalid type QUI\\\\ERP\\\\Order\\\\AbstractOrder\\.$#" - count: 2 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/AbstractBaseRecurringPayment.php - - - - message: "#^Parameter \\$Order of method QUI\\\\ERP\\\\Payments\\\\Stripe\\\\PaymentMethods\\\\Recurring\\\\AbstractBaseRecurringPayment\\:\\:getGatewayDisplay\\(\\) has invalid type QUI\\\\ERP\\\\Order\\\\AbstractOrder\\.$#" - count: 2 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/AbstractBaseRecurringPayment.php - - - - message: "#^Parameter \\$Order of method QUI\\\\ERP\\\\Payments\\\\Stripe\\\\PaymentMethods\\\\Recurring\\\\AbstractBaseRecurringPayment\\:\\:getSubscriptionIdByOrder\\(\\) has invalid type QUI\\\\ERP\\\\Order\\\\AbstractOrder\\.$#" - count: 2 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/AbstractBaseRecurringPayment.php - - - - message: "#^Parameter \\$Step of method QUI\\\\ERP\\\\Payments\\\\Stripe\\\\PaymentMethods\\\\Recurring\\\\AbstractBaseRecurringPayment\\:\\:getGatewayDisplay\\(\\) has invalid type QUI\\\\ERP\\\\Order\\\\Controls\\\\OrderProcess\\\\Processing\\.$#" - count: 1 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/AbstractBaseRecurringPayment.php - - - - message: "#^Call to method getCustomer\\(\\) on an unknown class QUI\\\\ERP\\\\Accounting\\\\Invoice\\\\Invoice\\.$#" - count: 1 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/ApplePay.php - - - - message: "#^Call to method getCustomer\\(\\) on an unknown class QUI\\\\ERP\\\\Accounting\\\\Invoice\\\\InvoiceTemporary\\.$#" - count: 1 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/ApplePay.php - - - - message: "#^Call to method getCustomer\\(\\) on an unknown class QUI\\\\ERP\\\\Accounting\\\\Invoice\\\\InvoiceView\\.$#" - count: 1 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/ApplePay.php - - - - message: "#^PHPDoc tag @return with type mixed is not subtype of native type string\\.$#" - count: 1 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/ApplePay.php - - - - message: "#^Parameter \\$Invoice of method QUI\\\\ERP\\\\Payments\\\\Stripe\\\\PaymentMethods\\\\Recurring\\\\ApplePay\\:\\:getInvoiceInformationText\\(\\) has invalid type QUI\\\\ERP\\\\Accounting\\\\Invoice\\\\Invoice\\.$#" - count: 2 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/ApplePay.php - - - - message: "#^Parameter \\$Invoice of method QUI\\\\ERP\\\\Payments\\\\Stripe\\\\PaymentMethods\\\\Recurring\\\\ApplePay\\:\\:getInvoiceInformationText\\(\\) has invalid type QUI\\\\ERP\\\\Accounting\\\\Invoice\\\\InvoiceTemporary\\.$#" - count: 2 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/ApplePay.php - - - - message: "#^Parameter \\$Invoice of method QUI\\\\ERP\\\\Payments\\\\Stripe\\\\PaymentMethods\\\\Recurring\\\\ApplePay\\:\\:getInvoiceInformationText\\(\\) has invalid type QUI\\\\ERP\\\\Accounting\\\\Invoice\\\\InvoiceView\\.$#" - count: 2 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/ApplePay.php - - - - message: "#^Call to method getArticles\\(\\) on an unknown class QUI\\\\ERP\\\\Order\\\\AbstractOrder\\.$#" - count: 2 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/BillingPlans.php - - - - message: "#^Call to method getCurrency\\(\\) on an unknown class QUI\\\\ERP\\\\Order\\\\AbstractOrder\\.$#" - count: 1 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/BillingPlans.php - - - - message: "#^Call to method getCustomer\\(\\) on an unknown class QUI\\\\ERP\\\\Order\\\\AbstractOrder\\.$#" - count: 2 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/BillingPlans.php - - - - message: "#^Call to method getPaymentDataEntry\\(\\) on an unknown class QUI\\\\ERP\\\\Order\\\\AbstractOrder\\.$#" - count: 2 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/BillingPlans.php - - - - message: "#^Call to method getPriceCalculation\\(\\) on an unknown class QUI\\\\ERP\\\\Order\\\\AbstractOrder\\.$#" - count: 1 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/BillingPlans.php - - - - message: "#^Call to method getUUID\\(\\) on an unknown class QUI\\\\ERP\\\\Order\\\\AbstractOrder\\.$#" - count: 1 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/BillingPlans.php - - - - message: "#^Parameter \\#1 \\$Source of static method QUI\\\\ERP\\\\Payments\\\\Stripe\\\\Utils\\:\\:getCentAmount\\(\\) expects QUI\\\\ERP\\\\ErpEntityInterface, QUI\\\\ERP\\\\Order\\\\AbstractOrder given\\.$#" - count: 1 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/BillingPlans.php - - - - message: "#^Parameter \\$Order of method QUI\\\\ERP\\\\Payments\\\\Stripe\\\\PaymentMethods\\\\Recurring\\\\BillingPlans\\:\\:createBillingPlanFromOrder\\(\\) has invalid type QUI\\\\ERP\\\\Order\\\\AbstractOrder\\.$#" - count: 2 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/BillingPlans.php - - - - message: "#^Parameter \\$Order of method QUI\\\\ERP\\\\Payments\\\\Stripe\\\\PaymentMethods\\\\Recurring\\\\BillingPlans\\:\\:getIdentificationHash\\(\\) has invalid type QUI\\\\ERP\\\\Order\\\\AbstractOrder\\.$#" - count: 2 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/BillingPlans.php - - - - message: "#^Parameter \\$Order of method QUI\\\\ERP\\\\Payments\\\\Stripe\\\\PaymentMethods\\\\Recurring\\\\BillingPlans\\:\\:getStripeBillingPlanIdByOrder\\(\\) has invalid type QUI\\\\ERP\\\\Order\\\\AbstractOrder\\.$#" - count: 2 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/BillingPlans.php - - - - message: "#^Call to method getCustomer\\(\\) on an unknown class QUI\\\\ERP\\\\Accounting\\\\Invoice\\\\Invoice\\.$#" - count: 1 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/Card.php - - - - message: "#^Call to method getCustomer\\(\\) on an unknown class QUI\\\\ERP\\\\Accounting\\\\Invoice\\\\InvoiceView\\.$#" - count: 1 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/Card.php - - - - message: "#^PHPDoc tag @return with type mixed is not subtype of native type string\\.$#" - count: 1 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/Card.php - - - - message: "#^Parameter \\$Invoice of method QUI\\\\ERP\\\\Payments\\\\Stripe\\\\PaymentMethods\\\\Recurring\\\\Card\\:\\:getInvoiceInformationText\\(\\) has invalid type QUI\\\\ERP\\\\Accounting\\\\Invoice\\\\Invoice\\.$#" - count: 1 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/Card.php - - - - message: "#^Parameter \\$Invoice of method QUI\\\\ERP\\\\Payments\\\\Stripe\\\\PaymentMethods\\\\Recurring\\\\Card\\:\\:getInvoiceInformationText\\(\\) has invalid type QUI\\\\ERP\\\\Accounting\\\\Invoice\\\\InvoiceView\\.$#" - count: 1 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/Card.php - - - - message: "#^Call to method getCustomer\\(\\) on an unknown class QUI\\\\ERP\\\\Accounting\\\\Invoice\\\\Invoice\\.$#" - count: 1 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/GooglePay.php - - - - message: "#^Call to method getCustomer\\(\\) on an unknown class QUI\\\\ERP\\\\Accounting\\\\Invoice\\\\InvoiceTemporary\\.$#" - count: 1 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/GooglePay.php - - - - message: "#^Call to method getCustomer\\(\\) on an unknown class QUI\\\\ERP\\\\Accounting\\\\Invoice\\\\InvoiceView\\.$#" - count: 1 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/GooglePay.php - - - - message: "#^PHPDoc tag @return with type mixed is not subtype of native type string\\.$#" - count: 1 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/GooglePay.php - - - - message: "#^Parameter \\$Invoice of method QUI\\\\ERP\\\\Payments\\\\Stripe\\\\PaymentMethods\\\\Recurring\\\\GooglePay\\:\\:getInvoiceInformationText\\(\\) has invalid type QUI\\\\ERP\\\\Accounting\\\\Invoice\\\\Invoice\\.$#" - count: 2 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/GooglePay.php - - - - message: "#^Parameter \\$Invoice of method QUI\\\\ERP\\\\Payments\\\\Stripe\\\\PaymentMethods\\\\Recurring\\\\GooglePay\\:\\:getInvoiceInformationText\\(\\) has invalid type QUI\\\\ERP\\\\Accounting\\\\Invoice\\\\InvoiceTemporary\\.$#" - count: 2 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/GooglePay.php - - - - message: "#^Parameter \\$Invoice of method QUI\\\\ERP\\\\Payments\\\\Stripe\\\\PaymentMethods\\\\Recurring\\\\GooglePay\\:\\:getInvoiceInformationText\\(\\) has invalid type QUI\\\\ERP\\\\Accounting\\\\Invoice\\\\InvoiceView\\.$#" - count: 2 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/GooglePay.php - - - - message: "#^Call to method getCustomer\\(\\) on an unknown class QUI\\\\ERP\\\\Accounting\\\\Invoice\\\\Invoice\\.$#" - count: 1 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/MicrosoftPay.php - - - - message: "#^Call to method getCustomer\\(\\) on an unknown class QUI\\\\ERP\\\\Accounting\\\\Invoice\\\\InvoiceTemporary\\.$#" - count: 1 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/MicrosoftPay.php - - - - message: "#^Call to method getCustomer\\(\\) on an unknown class QUI\\\\ERP\\\\Accounting\\\\Invoice\\\\InvoiceView\\.$#" - count: 1 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/MicrosoftPay.php - - - - message: "#^PHPDoc tag @return with type mixed is not subtype of native type string\\.$#" - count: 1 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/MicrosoftPay.php - - - - message: "#^Parameter \\$Invoice of method QUI\\\\ERP\\\\Payments\\\\Stripe\\\\PaymentMethods\\\\Recurring\\\\MicrosoftPay\\:\\:getInvoiceInformationText\\(\\) has invalid type QUI\\\\ERP\\\\Accounting\\\\Invoice\\\\Invoice\\.$#" - count: 2 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/MicrosoftPay.php - - - - message: "#^Parameter \\$Invoice of method QUI\\\\ERP\\\\Payments\\\\Stripe\\\\PaymentMethods\\\\Recurring\\\\MicrosoftPay\\:\\:getInvoiceInformationText\\(\\) has invalid type QUI\\\\ERP\\\\Accounting\\\\Invoice\\\\InvoiceTemporary\\.$#" - count: 2 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/MicrosoftPay.php - - - - message: "#^Parameter \\$Invoice of method QUI\\\\ERP\\\\Payments\\\\Stripe\\\\PaymentMethods\\\\Recurring\\\\MicrosoftPay\\:\\:getInvoiceInformationText\\(\\) has invalid type QUI\\\\ERP\\\\Accounting\\\\Invoice\\\\InvoiceView\\.$#" - count: 2 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/MicrosoftPay.php - - - - message: "#^Call to method addHistory\\(\\) on an unknown class QUI\\\\ERP\\\\Accounting\\\\Invoice\\\\Invoice\\.$#" - count: 2 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/Subscriptions.php - - - - message: "#^Call to method addHistory\\(\\) on an unknown class QUI\\\\ERP\\\\Order\\\\AbstractOrder\\.$#" - count: 2 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/Subscriptions.php - - - - message: "#^Call to method addTransaction\\(\\) on an unknown class QUI\\\\ERP\\\\Accounting\\\\Invoice\\\\Invoice\\.$#" - count: 2 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/Subscriptions.php - - - - message: "#^Call to method calculatePayments\\(\\) on an unknown class QUI\\\\ERP\\\\Accounting\\\\Invoice\\\\Invoice\\.$#" - count: 2 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/Subscriptions.php - - - - message: "#^Call to method getAttribute\\(\\) on an unknown class QUI\\\\ERP\\\\Accounting\\\\Invoice\\\\Invoice\\.$#" - count: 2 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/Subscriptions.php - - - - message: "#^Call to method getCurrency\\(\\) on an unknown class QUI\\\\ERP\\\\Accounting\\\\Invoice\\\\Invoice\\.$#" - count: 4 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/Subscriptions.php - - - - message: "#^Call to method getCustomer\\(\\) on an unknown class QUI\\\\ERP\\\\Order\\\\AbstractOrder\\.$#" - count: 2 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/Subscriptions.php - - - - message: "#^Call to method getGlobalProcessId\\(\\) on an unknown class QUI\\\\ERP\\\\Accounting\\\\Invoice\\\\Invoice\\.$#" - count: 2 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/Subscriptions.php - - - - message: "#^Call to method getPaymentDataEntry\\(\\) on an unknown class QUI\\\\ERP\\\\Accounting\\\\Invoice\\\\Invoice\\.$#" - count: 2 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/Subscriptions.php - - - - message: "#^Call to method getPaymentDataEntry\\(\\) on an unknown class QUI\\\\ERP\\\\Order\\\\AbstractOrder\\.$#" - count: 4 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/Subscriptions.php - - - - message: "#^Call to method getUUID\\(\\) on an unknown class QUI\\\\ERP\\\\Accounting\\\\Invoice\\\\Invoice\\.$#" - count: 3 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/Subscriptions.php - - - - message: "#^Call to method getUUID\\(\\) on an unknown class QUI\\\\ERP\\\\Order\\\\AbstractOrder\\.$#" - count: 1 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/Subscriptions.php - - - - message: "#^Call to method setPaymentData\\(\\) on an unknown class QUI\\\\ERP\\\\Order\\\\AbstractOrder\\.$#" - count: 4 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/Subscriptions.php - - - - message: "#^Call to method update\\(\\) on an unknown class QUI\\\\ERP\\\\Order\\\\AbstractOrder\\.$#" - count: 3 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/Subscriptions.php - - - - message: "#^Call to static method getInstance\\(\\) on an unknown class QUI\\\\ERP\\\\Accounting\\\\Invoice\\\\Handler\\.$#" - count: 1 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/Subscriptions.php - - - - message: "#^Method QUI\\\\ERP\\\\Payments\\\\Stripe\\\\PaymentMethods\\\\Recurring\\\\Subscriptions\\:\\:createSubscription\\(\\) should return int\\|false but returns string\\.$#" - count: 1 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/Subscriptions.php - - - - message: "#^Parameter \\#1 \\$Source of static method QUI\\\\ERP\\\\Payments\\\\Stripe\\\\Utils\\:\\:getCentAmount\\(\\) expects QUI\\\\ERP\\\\ErpEntityInterface, QUI\\\\ERP\\\\Accounting\\\\Invoice\\\\Invoice given\\.$#" - count: 2 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/Subscriptions.php - - - - message: "#^Parameter \\$Invoice of method QUI\\\\ERP\\\\Payments\\\\Stripe\\\\PaymentMethods\\\\Recurring\\\\Subscriptions\\:\\:billSubscriptionBalance\\(\\) has invalid type QUI\\\\ERP\\\\Accounting\\\\Invoice\\\\Invoice\\.$#" - count: 2 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/Subscriptions.php - - - - message: "#^Parameter \\$Invoice of method QUI\\\\ERP\\\\Payments\\\\Stripe\\\\PaymentMethods\\\\Recurring\\\\Subscriptions\\:\\:processDeniedTransactions\\(\\) has invalid type QUI\\\\ERP\\\\Accounting\\\\Invoice\\\\Invoice\\.$#" - count: 2 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/Subscriptions.php - - - - message: "#^Parameter \\$Order of method QUI\\\\ERP\\\\Payments\\\\Stripe\\\\PaymentMethods\\\\Recurring\\\\Subscriptions\\:\\:createSubscription\\(\\) has invalid type QUI\\\\ERP\\\\Order\\\\AbstractOrder\\.$#" - count: 2 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/Subscriptions.php - - message: "#^Variable \\$Payment in PHPDoc tag @var does not match assigned variable \\$paymentMethodData\\.$#" - count: 2 - path: src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/Subscriptions.php - - - - message: "#^Call to method getAttribute\\(\\) on an unknown class QUI\\\\ERP\\\\Accounting\\\\Invoice\\\\Invoice\\.$#" - count: 1 - path: src/QUI/ERP/Payments/Stripe/Utils.php - - - - message: "#^Call to method getCurrency\\(\\) on an unknown class QUI\\\\ERP\\\\Accounting\\\\Invoice\\\\Invoice\\.$#" count: 1 - path: src/QUI/ERP/Payments/Stripe/Utils.php - - - - message: "#^Call to method getCustomer\\(\\) on an unknown class QUI\\\\ERP\\\\Order\\\\AbstractOrder\\.$#" - count: 1 - path: src/QUI/ERP/Payments/Stripe/Utils.php - - - - message: "#^Call to method getPrefixedId\\(\\) on an unknown class QUI\\\\ERP\\\\Order\\\\AbstractOrder\\.$#" - count: 2 - path: src/QUI/ERP/Payments/Stripe/Utils.php - - - - message: "#^Call to method getPriceCalculation\\(\\) on an unknown class QUI\\\\ERP\\\\Order\\\\AbstractOrder\\.$#" - count: 1 - path: src/QUI/ERP/Payments/Stripe/Utils.php - - - - message: "#^Class QUI\\\\ERP\\\\Accounting\\\\Invoice\\\\Invoice not found\\.$#" - count: 1 - path: src/QUI/ERP/Payments/Stripe/Utils.php - - - - message: "#^Class QUI\\\\ERP\\\\Order\\\\AbstractOrder not found\\.$#" - count: 1 - path: src/QUI/ERP/Payments/Stripe/Utils.php + path: src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/Subscriptions.php - message: "#^Parameter \\#1 \\$precision of method QUI\\\\ERP\\\\Accounting\\\\CalculationValue\\:\\:precision\\(\\) expects bool, int given\\.$#" count: 1 path: src/QUI/ERP/Payments/Stripe/Utils.php - - - message: "#^Parameter \\$Order of method QUI\\\\ERP\\\\Payments\\\\Stripe\\\\Utils\\:\\:getPaymentDescriptionForOrder\\(\\) has invalid type QUI\\\\ERP\\\\Order\\\\AbstractOrder\\.$#" - count: 2 - path: src/QUI/ERP/Payments/Stripe/Utils.php - - message: "#^Access to an undefined property Stripe\\\\StripeObject\\:\\:\\$object\\.$#" - count: 3 + count: 5 path: src/QUI/ERP/Payments/Stripe/WebhookHandler.php - diff --git a/settings.xml b/settings.xml index 127cc52..1a63a4e 100644 --- a/settings.xml +++ b/settings.xml @@ -8,6 +8,17 @@ <conf name="payment_description"> <type><![CDATA[string]]></type> </conf> + <conf name="statement_descriptor"> + <type><![CDATA[string]]></type> + </conf> + <conf name="notify_about_failed_payment"> + <type><![CDATA[boolean]]></type> + <defaultvalue>1</defaultvalue> + </conf> + <conf name="notify_about_failed_invoice_payment"> + <type><![CDATA[boolean]]></type> + <defaultvalue>1</defaultvalue> + </conf> </section> <section name="api"> @@ -34,7 +45,6 @@ <type><![CDATA[string]]></type> </conf> </section> - </config> <window> @@ -60,10 +70,34 @@ <input conf="general.payment_description" type="text"> <text> - <locale group="quiqqer/payment-stripe" var="settings.general.payment_description.title"/> + <locale group="quiqqer/payment-stripe" + var="settings.general.payment_description.title"/> + </text> + <description> + <locale group="quiqqer/payment-stripe" + var="settings.general.payment_description.description"/> + </description> + </input> + + <input conf="general.statement_descriptor" type="text" maxlength="22"> + <text> + <locale group="quiqqer/payment-stripe" + var="settings.general.statement_descriptor.title"/> + </text> + <description> + <locale group="quiqqer/payment-stripe" + var="settings.general.statement_descriptor.description"/> + </description> + </input> + + <input conf="general.notify_about_failed_payment" type="checkbox"> + <text> + <locale group="quiqqer/payment-stripe" + var="settings.general.notify_about_failed_payment.title"/> </text> <description> - <locale group="quiqqer/payment-stripe" var="settings.general.payment_description.description"/> + <locale group="quiqqer/payment-stripe" + var="settings.general.notify_about_failed_payment.description"/> </description> </input> @@ -103,7 +137,8 @@ <locale group="quiqqer/payment-stripe" var="settings.api.sandbox_public_key.title"/> </text> <description> - <locale group="quiqqer/payment-stripe" var="settings.api.sandbox_public_key.description"/> + <locale group="quiqqer/payment-stripe" + var="settings.api.sandbox_public_key.description"/> </description> </input> @@ -112,7 +147,8 @@ <locale group="quiqqer/payment-stripe" var="settings.api.sandbox_client_secret.title"/> </text> <description> - <locale group="quiqqer/payment-stripe" var="settings.api.sandbox_client_secret.description"/> + <locale group="quiqqer/payment-stripe" + var="settings.api.sandbox_client_secret.description"/> </description> </input> @@ -132,12 +168,15 @@ <locale group="quiqqer/payment-stripe" var="settings.payment.title"/> </title> - <input conf="payment.apple_pay_domains" type="text" data-qui="package/quiqqer/payment-stripe/bin/controls/backend/settings/ApplePayRegister" label="false"> + <input conf="payment.apple_pay_domains" type="text" + data-qui="package/quiqqer/payment-stripe/bin/controls/backend/settings/ApplePayRegister" + label="false"> <text> <locale group="quiqqer/payment-stripe" var="settings.payment.apple_pay_domains.title"/> </text> <description> - <locale group="quiqqer/payment-stripe" var="settings.payment.apple_pay_domains.description"/> + <locale group="quiqqer/payment-stripe" + var="settings.payment.apple_pay_domains.description"/> </description> </input> @@ -155,7 +194,9 @@ <locale group="quiqqer/payment-stripe" var="settings.category.stripe_billing_plans.title"/> </title> - <input type="hidden" data-qui="package/quiqqer/payment-stripe/bin/controls/backend/settings/BillingPlans" label="false"> + <input type="hidden" + data-qui="package/quiqqer/payment-stripe/bin/controls/backend/settings/BillingPlans" + label="false"> </input> </settings> @@ -164,15 +205,19 @@ <category name="stripe_billing_plan_subscriptions"> <icon>fa fa-cc-stripe</icon> <title> - <locale group="quiqqer/payment-stripe" var="settings.category.stripe_billing_plan_subscriptions.title"/> + <locale group="quiqqer/payment-stripe" + var="settings.category.stripe_billing_plan_subscriptions.title"/> </title> <settings name="stripe_billing_plan_subscriptions" title="stripe_billing_plan_subscriptions"> <title> - <locale group="quiqqer/payment-stripe" var="settings.category.stripe_billing_plan_subscriptions.title"/> + <locale group="quiqqer/payment-stripe" + var="settings.category.stripe_billing_plan_subscriptions.title"/> </title> - <input type="hidden" data-qui="package/quiqqer/payment-stripe/bin/controls/backend/settings/Subscriptions" label="false"> + <input type="hidden" + data-qui="package/quiqqer/payment-stripe/bin/controls/backend/settings/Subscriptions" + label="false"> </input> </settings> diff --git a/src/QUI/ERP/Payments/Stripe/AbstractBasePayment.php b/src/QUI/ERP/Payments/Stripe/AbstractBasePayment.php index 119c420..d5d1268 100644 --- a/src/QUI/ERP/Payments/Stripe/AbstractBasePayment.php +++ b/src/QUI/ERP/Payments/Stripe/AbstractBasePayment.php @@ -4,11 +4,11 @@ use Exception; use QUI; -use QUI\ERP\Accounting\Payments\Gateway\Gateway; use QUI\ERP\Accounting\Payments\Transactions\Factory as TransactionFactory; use QUI\ERP\Accounting\Payments\Transactions\Transaction; use QUI\ERP\Order\AbstractOrder; use QUI\ERP\Order\Handler as OrderHandler; +use QUI\ERP\Order\OrderInterface; use Stripe\Exception\ApiErrorException; use Stripe\PaymentIntent as StripePaymentIntent; use Stripe\Refund as StripeRefund; @@ -135,9 +135,9 @@ public function refundSupport(): bool */ public function refund( Transaction $Transaction, - float|int $amount, + float | int $amount, string $message = '', - bool|string $hash = false + bool | string $hash = false ): void { try { if ($hash === false) { @@ -210,7 +210,7 @@ public function getGatewayDisplay(AbstractOrder $Order, $Step = null): string * @throws ApiErrorException * @throws StripeException */ - public function createPaymentIntent(AbstractOrder $Order, string $paymentMethodId): StripePaymentIntent|bool + public function createPaymentIntent(AbstractOrder $Order, string $paymentMethodId): StripePaymentIntent | bool { if (!empty($Order->getPaymentDataEntry(self::ATTR_STRIPE_PAYMENT_INTENT_ID))) { $PaymentIntent = $this->getPaymentIntentByOrder($Order); @@ -275,47 +275,30 @@ public function confirmPaymentIntent(AbstractOrder $Order, StripePaymentIntent $ if ( $PaymentIntent->status === $PaymentIntent::STATUS_REQUIRES_ACTION - && $PaymentIntent->next_action->type === 'use_stripe_sdk' + && $PaymentIntent->next_action?->type === 'use_stripe_sdk' ) { $confirmData['status'] = 'action_required'; $confirmData['clientSecret'] = $PaymentIntent->client_secret; $this->addOrderHistoryEntry($Order, 'Additional user action required for PaymentIntent confirmation.'); - } elseif ($PaymentIntent->status === $PaymentIntent::STATUS_SUCCEEDED) { - $confirmData['status'] = 'success'; - - $this->addOrderHistoryEntry($Order, 'PaymentIntent successully confirmed.'); - - try { - $Order->setSuccessfulStatus(); - $Order->setPaymentData(self::ATTR_STRIPE_ORDER_SUCCESSFUL, true); - - $this->saveOrder($Order); - - $Transaction = Gateway::getInstance()->purchase( - $PaymentIntent->amount_received / 100, - QUI\ERP\Currency\Handler::getCurrency(mb_strtoupper($PaymentIntent->currency)), - $Order, - $this - ); - - $Transaction->setData( - self::ATTR_STRIPE_PAYMENT_INTENT_ID, - $Order->getPaymentDataEntry(self::ATTR_STRIPE_PAYMENT_INTENT_ID) - ); - - $Transaction->updateData(); - } catch (Exception $Exception) { - QUI\System\Log::writeException($Exception); - - $this->addOrderHistoryEntry( - $Order, - 'Error while trying to set order as successful: ' . $Exception->getMessage() - ); - } } else { - $confirmData['status'] = 'error'; - $this->addOrderHistoryEntry($Order, 'Confirmation error.'); + $orderProcessingService = new OrderProcessingService($Order); + $orderProcessingService->processPaymentIntent($PaymentIntent); + + switch ($PaymentIntent->status) { + case StripePaymentIntent::STATUS_PROCESSING: +// $confirmData['status'] = 'pending'; + $confirmData['status'] = 'success'; // TODO: special 'pending' status necessary? + break; + + case StripePaymentIntent::STATUS_SUCCEEDED: + $confirmData['status'] = 'success'; + break; + + default: + $confirmData['status'] = 'error'; + break; + } } return $confirmData; @@ -330,7 +313,7 @@ public function confirmPaymentIntent(AbstractOrder $Order, StripePaymentIntent $ * @throws ApiErrorException * @throws StripeException */ - public function getPaymentIntentByOrder(AbstractOrder $Order): StripePaymentIntent|bool + public function getPaymentIntentByOrder(AbstractOrder $Order): StripePaymentIntent | bool { $paymentIntentId = $Order->getPaymentDataEntry(self::ATTR_STRIPE_PAYMENT_INTENT_ID); @@ -362,7 +345,7 @@ public function getPaymentIntentByOrder(AbstractOrder $Order): StripePaymentInte public function refundPayment( Transaction $Transaction, string $refundHash, - float|int $amount, + float | int $amount, string $reason = '' ): void { $Process = new QUI\ERP\Process($Transaction->getGlobalProcessId()); @@ -508,6 +491,11 @@ public function refundPayment( } } + public function canBeUsedInOrder(OrderInterface $order): bool + { + return true; + } + /** * Add history entry for current Order * diff --git a/src/QUI/ERP/Payments/Stripe/ChargeDisputeService.php b/src/QUI/ERP/Payments/Stripe/ChargeDisputeService.php new file mode 100644 index 0000000..084a71c --- /dev/null +++ b/src/QUI/ERP/Payments/Stripe/ChargeDisputeService.php @@ -0,0 +1,155 @@ +<?php + +namespace QUI\ERP\Payments\Stripe; + +use Doctrine\DBAL\Connection; +use QUI; +use QUI\ERP\Accounting\Payments\Transactions\Handler as TransactionHandler; +use QUI\ERP\Accounting\Payments\Transactions\Transaction as EcoynTransaction; +use QUI\ERP\Order\AbstractOrder; +use QUI\ERP\Order\Handler as OrderHandler; +use Stripe\Dispute; +use Stripe\PaymentIntent; + +class ChargeDisputeService +{ + public function __construct( + private ?TransactionHandler $transactionHandler = null, + private ?OrderHandler $orderHandler = null, + private ?Connection $connection = null + ) { + if (is_null($this->transactionHandler)) { + $this->transactionHandler = TransactionHandler::getInstance(); + } + + if (is_null($this->orderHandler)) { + $this->orderHandler = OrderHandler::getInstance(); + } + + if (is_null($this->connection)) { + $this->connection = QUI::getDataBaseConnection(); + } + + Provider::setupApi(); + } + + public function processDispute(Dispute $dispute): void + { + $order = $this->getOrderByDispute($dispute); + Utils::sendNotificationAboutDisputedTransaction($dispute, $order); + + if ($order) { + try { + $order->addHistory( + 'Stripe :: Order transaction disputed. Dispute ID: ' . $dispute->id + ); + $order->update(QUI::getUsers()->getSystemUser()); + } catch (\Exception $exception) { + QUI\System\Log::writeException($exception); + } + } + + // If the dispute is lost, mark the corresponding ecoyn transaction as erroneous. + if ($dispute->status === Dispute::STATUS_LOST) { + $ecoynTransaction = $this->getEcoynTransactionByDispute($dispute); + + try { + $ecoynTransaction?->changeStatus(TransactionHandler::STATUS_ERROR); + } catch (\Exception $exception) { + QUI\System\Log::writeException($exception); + } + } + // TODO: ggf. weiteres Vorgehen wie Transaktion rückgängig machen bzw. negative Transaktion erstellen + } + + /** + * @param Dispute $dispute + * @return EcoynTransaction|null + */ + private function getEcoynTransactionByDispute(Dispute $dispute): ?EcoynTransaction + { + if (!$dispute->payment_intent) { + return null; + } + + try { + $paymentIntent = PaymentIntent::retrieve($dispute->payment_intent); + } catch (\Exception $exception) { + QUI\System\Log::writeException($exception); + return null; + } + + if (empty($paymentIntent->invoice)) { + return null; + } + + try { + $result = $this->connection + ->createQueryBuilder() + ->select('quiqqer_transaction_id') + ->from(Provider::getStripeBillingSubscriptionsTransactionsTable()) + ->where('stripe_invoice_id = :stripe_invoice_id') + ->setParameter('stripe_invoice_id', $paymentIntent->invoice) + ->fetchAssociative(); + + if (empty($result['quiqqer_transaction_id'])) { + return null; + } + + return $this->transactionHandler->get($result['quiqqer_transaction_id']); + } catch (\Exception | \Throwable $exception) { + if ($exception instanceof \Exception) { + QUI\System\Log::writeException($exception); + } + return null; + } + } + + /** + * @param Dispute $dispute + * @return AbstractOrder|null + */ + private function getOrderByDispute(Dispute $dispute): ?AbstractOrder + { + if (!$dispute->payment_intent) { + return null; + } + + try { + $paymentIntent = PaymentIntent::retrieve([ + 'id' => $dispute->payment_intent, + 'expand' => [ + 'invoice.subscription' + ] + ]); + } catch (\Exception $exception) { + QUI\System\Log::writeException($exception); + return null; + } + + $metaData = $paymentIntent->metadata->toArray(); + $orderUuid = null; + + if (!empty($metaData['orderUuid'])) { + $orderUuid = $metaData['orderUuid']; + } elseif (!empty($paymentIntent->invoice?->subscription)) { + $subscriptionMetaData = $paymentIntent->invoice->subscription->metadata->toArray(); + + if (!empty($subscriptionMetaData['orderUuid'])) { + $orderUuid = $subscriptionMetaData['orderUuid']; + } + } + + if (is_null($orderUuid)) { + return null; + } + + try { + return $this->orderHandler->get($orderUuid); + } catch (\Exception $exception) { + QUI\System\Log::writeException($exception); + } + + return null; + } +} diff --git a/src/QUI/ERP/Payments/Stripe/Cron/PendingPaymentsService.php b/src/QUI/ERP/Payments/Stripe/Cron/PendingPaymentsService.php new file mode 100644 index 0000000..1cf5d52 --- /dev/null +++ b/src/QUI/ERP/Payments/Stripe/Cron/PendingPaymentsService.php @@ -0,0 +1,140 @@ +<?php + +namespace QUI\ERP\Payments\Stripe\Cron; + +use Doctrine\DBAL\Connection; +use Doctrine\DBAL\Exception; +use QUI; +use QUI\ERP\Accounting\Payments\Payments; +use QUI\ERP\Accounting\Payments\Types\Payment as PaymentType; +use QUI\ERP\Constants as ErpConstants; +use QUI\ERP\Order\Handler as OrderHandler; +use QUI\ERP\Order\Order; +use QUI\ERP\Payments\Stripe\AbstractBasePayment; +use QUI\ERP\Payments\Stripe\OrderProcessingService; +use QUI\ERP\Payments\Stripe\Provider; +use Stripe\PaymentIntent as StripePaymentIntent; + +use function implode; + +class PendingPaymentsService +{ + public function __construct( + private ?Payments $payments = null, + private ?OrderHandler $orderHandler = null, + private ?Connection $dbConnection = null + ) { + if (is_null($this->payments)) { + $this->payments = Payments::getInstance(); + } + + if (is_null($this->orderHandler)) { + $this->orderHandler = OrderHandler::getInstance(); + } + + if (is_null($this->dbConnection)) { + $this->dbConnection = QUI::getDataBaseConnection(); + } + + Provider::setupApi(); + } + + public static function execute(): void + { + $service = new PendingPaymentsService(); + $service->checkPendingOrderPayments(); + } + + /** + * Check all pending stripe order payments. + * + * @return void + * @throws Exception + */ + public function checkPendingOrderPayments(): void + { + $stripePaymentTypeIds = $this->getStripePaymentTypeIds(); + + // If no + if (empty($stripePaymentTypeIds)) { + return; + } + + $paidStatusCodes = + [ + ErpConstants::PAYMENT_STATUS_OPEN, + ErpConstants::PAYMENT_STATUS_PART, + ErpConstants::PAYMENT_STATUS_DEBIT + ]; + + // Manuay query here because query builder did not work for some reason (peat) + $sql = "SELECT `id` FROM {$this->orderHandler->table()}"; + $sql .= " WHERE `paid_status` IN ('" . implode(',', $paidStatusCodes) . "')"; + $sql .= " AND `payment_id` IN ('" . implode("','", $stripePaymentTypeIds) . "')"; + $result = $this->dbConnection->fetchAllAssociative($sql); + + foreach ($result as $row) { + try { + $orderId = $row['id']; + $order = $this->orderHandler->getOrderById($orderId); + $this->processOrder($order); + } catch (\Exception $exception) { + QUI\System\Log::writeException($exception); + } + } + } + + /** + * Process an unpaid order. + * + * Checks if the associated Stripe PaymentIntent is (un)successful and creates/updates the order payment + * transaction accordingly. + * + * @param Order $order + * @return void + * @throws \Stripe\Exception\ApiErrorException + */ + private function processOrder(Order $order): void + { + if ($order->isPaid()) { + return; + } + + $stripePaymentIntentId = $order->getPaymentDataEntry(AbstractBasePayment::ATTR_STRIPE_PAYMENT_INTENT_ID); + + if (empty($stripePaymentIntentId)) { + return; + } + + $paymentIntent = StripePaymentIntent::retrieve($stripePaymentIntentId); + + $orderProcessingService = new OrderProcessingService($order); + $orderProcessingService->processPaymentIntent($paymentIntent); + } + + /** + * @return int[] + */ + private function getStripePaymentTypeIds(): array + { + // Determine payment type IDs + $payments = $this->payments->getPayments([ + 'select' => ['id'], + 'where' => [ + 'payment_type' => [ + 'type' => 'IN', + 'value' => Provider::getNonRecurringPaymentTypes() + ] + ] + ]); + + $paymentTypeIds = []; + + /** @var PaymentType $Payment */ + foreach ($payments as $Payment) { + $paymentTypeIds[] = $Payment->getId(); + } + + return $paymentTypeIds; + } +} diff --git a/src/QUI/ERP/Payments/Stripe/Events.php b/src/QUI/ERP/Payments/Stripe/Events.php index bd8f015..417b448 100644 --- a/src/QUI/ERP/Payments/Stripe/Events.php +++ b/src/QUI/ERP/Payments/Stripe/Events.php @@ -9,6 +9,7 @@ use QUI\ERP\Accounting\Payments\Types\Payment; use QUI\ERP\Order\OrderInterface; use QUI\Package\Package; +use QUI\ERP\Payments\Stripe\AbstractBasePayment as StripeBasePayment; use function in_array; @@ -51,20 +52,7 @@ public static function onPaymentsCanUsedInOrder(Payment $Payment, OrderInterface return; } - if ( - !($PaymentType instanceof QUI\ERP\Payments\Stripe\PaymentMethods\AbstractBrowserPay) - && !($PaymentType instanceof QUI\ERP\Payments\Stripe\PaymentMethods\Recurring\AbstractRecurringBrowserPay) - ) { - return; - } - - $B = new Browser(); - - if (!$B->isMobile()) { - throw new PaymentCanNotBeUsedException(); - } - - if (!in_array($B->getBrowser(), $PaymentType->getBrowserTypes())) { + if ($PaymentType instanceof StripeBasePayment && !$PaymentType->canBeUsedInOrder($Order)) { throw new PaymentCanNotBeUsedException(); } } diff --git a/src/QUI/ERP/Payments/Stripe/OrderProcessingService.php b/src/QUI/ERP/Payments/Stripe/OrderProcessingService.php new file mode 100644 index 0000000..deb0b7f --- /dev/null +++ b/src/QUI/ERP/Payments/Stripe/OrderProcessingService.php @@ -0,0 +1,244 @@ +<?php + +namespace QUI\ERP\Payments\Stripe; + +use Exception; +use QUI; +use QUI\ERP\Accounting\Payments\Transactions\Factory as TransactionFactory; +use QUI\ERP\Accounting\Payments\Transactions\Handler as TransactionHandler; +use QUI\ERP\Accounting\Payments\Transactions\Transaction; +use QUI\ERP\Order\AbstractOrder; +use Stripe\PaymentIntent; + +class OrderProcessingService implements OrderProcessingServiceInterface +{ + const ORDER_PAYMENT_ATTR_TRANSACTION_TX_ID = 'stripe_transaction_tx_id'; + + public function __construct( + private readonly AbstractOrder $order, + private ?TransactionHandler $transactionHandler = null + ) { + if (is_null($this->transactionHandler)) { + $this->transactionHandler = TransactionHandler::getInstance(); + } + } + + /** + * Processes a Stripe PaymentIntent for this order. + * + * This may set the order to SUCESSFUL or FAILED, depending on the status. + * This also creates transactions for successful orders. + * + * @param PaymentIntent $paymentIntent + * @return void + */ + public function processPaymentIntent(PaymentIntent $paymentIntent): void + { + switch ($paymentIntent->status) { + case PaymentIntent::STATUS_PROCESSING: + $this->processPendingPaymentIntent($paymentIntent); + break; + + case PaymentIntent::STATUS_SUCCEEDED: + $this->processSuccessfulPaymentIntent($paymentIntent); + break; + + case PaymentIntent::STATUS_CANCELED: + case PaymentIntent::STATUS_REQUIRES_PAYMENT_METHOD: + $this->processFailedPaymentIntent($paymentIntent); + break; + } + } + + /** + * Process a successful Stripe PaymentIntent + * + * @param PaymentIntent $paymentIntent + * @return void + */ + private function processPendingPaymentIntent(PaymentIntent $paymentIntent): void + { + try { + if (is_null($this->getPaymentIntentTransaction())) { + $this->createPaymentIntentTransaction($paymentIntent); + } + + $this->order->setSuccessfulStatus(); + $this->order->setPaymentData(AbstractBasePayment::ATTR_STRIPE_ORDER_SUCCESSFUL, true); + + $this->addOrderHistoryEntry( + 'PaymentIntent transaction is PENDING. Waiting for successful status from Stripe. Order is successful.' + ); + } catch (Exception $Exception) { + QUI\System\Log::writeException($Exception); + + $this->addOrderHistoryEntry( + 'Error while trying to process PENDING PaymentIntent: ' . $Exception->getMessage() + ); + } + } + + /** + * Process a successful Stripe PaymentIntent + * + * @param PaymentIntent $paymentIntent + * @return void + */ + private function processSuccessfulPaymentIntent(PaymentIntent $paymentIntent): void + { + try { + $transaction = $this->getPaymentIntentTransaction(); + + if (is_null($transaction)) { + $transaction = $this->createPaymentIntentTransaction($paymentIntent); + } + + $transactionAmount = (int)($transaction->getAmount() * 100); + + if ($transactionAmount !== $paymentIntent->amount_received) { + $transaction->changeStatus(TransactionHandler::STATUS_ERROR); + $transaction = $this->createPaymentIntentTransaction($paymentIntent); + } + + $transaction->changeStatus(TransactionHandler::STATUS_COMPLETE); + $this->order->setSuccessfulStatus(); + $this->order->setPaymentData(AbstractBasePayment::ATTR_STRIPE_ORDER_SUCCESSFUL, true); + + $this->addOrderHistoryEntry('PaymentIntent confirmed. Payment successful.'); + } catch (Exception $Exception) { + QUI\System\Log::writeException($Exception); + + $this->addOrderHistoryEntry( + 'Error while trying to set order as successful: ' . $Exception->getMessage() + ); + } + } + + /** + * Process a failed Stripe PaymentIntent + * + * @param PaymentIntent $paymentIntent + * @return void + */ + private function processFailedPaymentIntent(PaymentIntent $paymentIntent): void + { + try { + $transaction = $this->getPaymentIntentTransaction(); + + if (is_null($transaction)) { + $transaction = $this->createPaymentIntentTransaction($paymentIntent); + } + + $transaction->changeStatus(TransactionHandler::STATUS_ERROR); + + $this->addOrderHistoryEntry( + "PaymentIntent transaction {$transaction->getTxId()} FAILED." + ); + + if (Provider::shouldNotifyOnFailedPayment()) { + $lastError = $paymentIntent->last_payment_error->toArray(); + $errorMsg = ''; + + if (!empty($lastError['message'])) { + $errorMsg = $lastError['message']; + } + + if (!empty($lastError['type'])) { + $errorMsg .= ' (' . $lastError['type'] . ')'; + } + + if (!empty($lastError['decline_code'])) { + $errorMsg .= ' (' . $lastError['decline_code'] . ')'; + } + + Utils::sendNotificationAboutFailedOrderPayment($this->order, $errorMsg); + } + + $this->order->setPaymentStatus(QUI\ERP\Constants::PAYMENT_STATUS_ERROR); + } catch (Exception $Exception) { + QUI\System\Log::writeException($Exception); + + $this->addOrderHistoryEntry( + 'Error while trying to process FAILED PaymentIntent: ' . $Exception->getMessage() + ); + } + } + + /** + * Add history entry for current Order + * + * @param string $msg + * @return void + */ + private function addOrderHistoryEntry(string $msg): void + { + try { + $this->order->addHistory('Stripe :: ' . $msg); + $this->saveOrder(); + } catch (\Exception | \Throwable $exception) { + if ($exception instanceof \Exception) { + QUI\System\Log::writeException($exception); + } + } + } + + /** + * @param PaymentIntent $paymentIntent + * @return Transaction + * + * @throws QUI\ERP\Accounting\Payments\Exception + * @throws QUI\ERP\Accounting\Payments\Transactions\Exception + * @throws QUI\Exception + */ + private function createPaymentIntentTransaction(PaymentIntent $paymentIntent): Transaction + { + $transaction = TransactionFactory::createPaymentTransaction( + $paymentIntent->amount / 100, + QUI\ERP\Currency\Handler::getCurrency(mb_strtoupper($paymentIntent->currency)), + $this->order->getUUID(), + $this->order->getPayment()->getPaymentType()->getName(), + [ + AbstractBasePayment::ATTR_STRIPE_PAYMENT_INTENT_ID => $paymentIntent->id + ], + null, + false, + false, + TransactionHandler::STATUS_PENDING + ); + + $this->addOrderHistoryEntry("PaymentIntent and transaction {$transaction->getTxId()} created."); + $this->order->setPaymentData(self::ORDER_PAYMENT_ATTR_TRANSACTION_TX_ID, $transaction->getTxId()); + $this->saveOrder(); + + return $transaction; + } + + /** + * @return Transaction|null + */ + private function getPaymentIntentTransaction(): ?Transaction + { + $transactionId = $this->order->getPaymentDataEntry(self::ORDER_PAYMENT_ATTR_TRANSACTION_TX_ID); + + if (empty($transactionId)) { + return null; + } + + foreach ($this->order->getTransactions() as $transaction) { + if ($transaction->getTxId() === $transactionId) { + return $transaction; + } + } + + return null; + } + + /** + * @return void + * @throws QUI\Exception + */ + private function saveOrder(): void + { + $this->order->update(QUI::getUsers()->getSystemUser()); + } +} diff --git a/src/QUI/ERP/Payments/Stripe/OrderProcessingServiceInterface.php b/src/QUI/ERP/Payments/Stripe/OrderProcessingServiceInterface.php new file mode 100644 index 0000000..f3e1390 --- /dev/null +++ b/src/QUI/ERP/Payments/Stripe/OrderProcessingServiceInterface.php @@ -0,0 +1,19 @@ +<?php + +namespace QUI\ERP\Payments\Stripe; + +use Stripe\PaymentIntent; + +interface OrderProcessingServiceInterface +{ + /** + * Processes a Stripe PaymentIntent for this order. + * + * This may set the order to SUCESSFUL or FAILED, depending on the status. + * This also creates transactions for successful orders. + * + * @param PaymentIntent $paymentIntent + * @return void + */ + public function processPaymentIntent(PaymentIntent $paymentIntent): void; +} diff --git a/src/QUI/ERP/Payments/Stripe/PaymentMethods/AbstractBrowserPay.php b/src/QUI/ERP/Payments/Stripe/PaymentMethods/AbstractBrowserPay.php index 2ab92ed..0e97863 100644 --- a/src/QUI/ERP/Payments/Stripe/PaymentMethods/AbstractBrowserPay.php +++ b/src/QUI/ERP/Payments/Stripe/PaymentMethods/AbstractBrowserPay.php @@ -5,10 +5,13 @@ use QUI; use QUI\ERP\Accounting\Payments\Payments; use QUI\ERP\Order\AbstractOrder; +use QUI\ERP\Order\OrderInterface; use QUI\ERP\Payments\Stripe\AbstractBasePayment; +use QUI\ERP\Payments\Stripe\Utils; use Stripe\Exception\ApiErrorException; use Stripe\PaymentIntent as StripePaymentIntent; -use QUI\ERP\Payments\Stripe\Utils; +use Browser; +use QUI\ERP\Accounting\Payments\Exceptions\PaymentCanNotBeUsed as PaymentCanNotBeUsedException; /** * Class BrowserPay @@ -61,16 +64,31 @@ public function getPaymentMethodType(): string protected function createPaymentIntentForOrder(AbstractOrder $Order, string $paymentMethodId): StripePaymentIntent { return StripePaymentIntent::create([ - 'payment_method' => $paymentMethodId, - 'amount' => Utils::getCentAmount($Order), - 'currency' => mb_strtolower($Order->getCurrency()->getCode()), + 'payment_method' => $paymentMethodId, + 'amount' => Utils::getCentAmount($Order), + 'currency' => mb_strtolower($Order->getCurrency()->getCode()), // 'confirmation_method' => 'manual', - 'confirm' => true, - 'description' => Utils::getPaymentDescriptionForOrder($Order), + 'confirm' => true, + 'description' => Utils::getPaymentDescriptionForOrder($Order), 'automatic_payment_methods' => [ 'enabled' => true, 'allow_redirects' => 'never' ] ]); } + + public function canBeUsedInOrder(OrderInterface $order): bool + { + $B = new Browser(); + + if (!$B->isMobile()) { + return false; + } + + if (!in_array($B->getBrowser(), $this->getBrowserTypes())) { + return false; + } + + return true; + } } diff --git a/src/QUI/ERP/Payments/Stripe/PaymentMethods/ApplePay.php b/src/QUI/ERP/Payments/Stripe/PaymentMethods/ApplePay.php index fe1d001..1359fa9 100644 --- a/src/QUI/ERP/Payments/Stripe/PaymentMethods/ApplePay.php +++ b/src/QUI/ERP/Payments/Stripe/PaymentMethods/ApplePay.php @@ -104,7 +104,7 @@ public function getIcon(): string * Return the extra text for the invoice * * @param Invoice|InvoiceTemporary|InvoiceView $Invoice |InvoiceView $Invoice - * @return mixed + * @return string */ public function getInvoiceInformationText(Invoice|InvoiceTemporary|InvoiceView $Invoice): string { diff --git a/src/QUI/ERP/Payments/Stripe/PaymentMethods/Card.php b/src/QUI/ERP/Payments/Stripe/PaymentMethods/Card.php index 5aa8ff6..51b66de 100644 --- a/src/QUI/ERP/Payments/Stripe/PaymentMethods/Card.php +++ b/src/QUI/ERP/Payments/Stripe/PaymentMethods/Card.php @@ -13,6 +13,7 @@ use Stripe\Exception\ApiErrorException; use Stripe\PaymentIntent as StripePaymentIntent; use QUI\ERP\Payments\Stripe\Utils; +use Stripe\PaymentMethod; /** * Class Card @@ -97,7 +98,7 @@ public function getIcon(): string */ public function getPaymentMethodType(): string { - return \Stripe\Card::OBJECT_NAME; + return PaymentMethod::TYPE_CARD; } /** @@ -130,7 +131,7 @@ protected function createPaymentIntentForOrder(AbstractOrder $Order, string $pay * Return the extra text for the invoice * * @param Invoice|InvoiceTemporary|InvoiceView $Invoice |InvoiceView $Invoice - * @return mixed + * @return string */ public function getInvoiceInformationText(Invoice|InvoiceTemporary|InvoiceView $Invoice): string { diff --git a/src/QUI/ERP/Payments/Stripe/PaymentMethods/GooglePay.php b/src/QUI/ERP/Payments/Stripe/PaymentMethods/GooglePay.php index 583400c..90e2ca5 100644 --- a/src/QUI/ERP/Payments/Stripe/PaymentMethods/GooglePay.php +++ b/src/QUI/ERP/Payments/Stripe/PaymentMethods/GooglePay.php @@ -102,7 +102,7 @@ public function getIcon(): string * Return the extra text for the invoice * * @param Invoice|InvoiceTemporary|InvoiceView $Invoice |InvoiceView $Invoice - * @return mixed + * @return string */ public function getInvoiceInformationText(Invoice|InvoiceTemporary|InvoiceView $Invoice): string { diff --git a/src/QUI/ERP/Payments/Stripe/PaymentMethods/MicrosoftPay.php b/src/QUI/ERP/Payments/Stripe/PaymentMethods/MicrosoftPay.php index 30591a9..bcef294 100644 --- a/src/QUI/ERP/Payments/Stripe/PaymentMethods/MicrosoftPay.php +++ b/src/QUI/ERP/Payments/Stripe/PaymentMethods/MicrosoftPay.php @@ -102,7 +102,7 @@ public function getIcon(): string * Return the extra text for the invoice * * @param Invoice|InvoiceTemporary|InvoiceView $Invoice |InvoiceView $Invoice - * @return mixed + * @return string */ public function getInvoiceInformationText(Invoice|InvoiceTemporary|InvoiceView $Invoice): string { diff --git a/src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/AbstractBaseRecurringPayment.php b/src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/AbstractBaseRecurringPayment.php index 130f7ac..c7a2451 100644 --- a/src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/AbstractBaseRecurringPayment.php +++ b/src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/AbstractBaseRecurringPayment.php @@ -5,15 +5,17 @@ use Exception; use QUI; use QUI\ERP\Accounting\Invoice\Invoice; +use QUI\ERP\Accounting\Payments\Types\RecurringPaymentInterface; use QUI\ERP\Order\AbstractOrder; use QUI\ERP\Payments\Stripe\AbstractBasePayment; +use QUI\ERP\Payments\Stripe\Provider; use QUI\ERP\Payments\Stripe\StripeException; +use QUI\ERP\Payments\Stripe\Utils; use Stripe\Exception\ApiErrorException; +use Stripe\PaymentIntent; use Stripe\PaymentIntent as StripePaymentIntent; -use QUI\ERP\Accounting\Payments\Types\RecurringPaymentInterface; -use QUI\ERP\Payments\Stripe\Provider; +use Stripe\Subscription; use Stripe\Subscription as StripeSubscription; -use QUI\ERP\Payments\Stripe\Utils; use function array_column; @@ -83,7 +85,7 @@ public function captureSubscription(Invoice $Invoice): void * * @throws Exception */ - public function cancelSubscription(int|string $subscriptionId, string $reason = ''): void + public function cancelSubscription(int | string $subscriptionId, string $reason = ''): void { Subscriptions::cancelSubscription($subscriptionId, $reason); } @@ -99,7 +101,7 @@ public function cancelSubscription(int|string $subscriptionId, string $reason = * @throws ApiErrorException * @throws StripeException */ - public function suspendSubscription(int|string $subscriptionId, string $note = null): void + public function suspendSubscription(int | string $subscriptionId, string $note = null): void { Subscriptions::suspendSubscription($subscriptionId); } @@ -115,7 +117,7 @@ public function suspendSubscription(int|string $subscriptionId, string $note = n * @throws ApiErrorException * @throws StripeException */ - public function resumeSubscription(int|string $subscriptionId, string $note = null): void + public function resumeSubscription(int | string $subscriptionId, string $note = null): void { Subscriptions::resumeSubscription($subscriptionId); } @@ -126,7 +128,7 @@ public function resumeSubscription(int|string $subscriptionId, string $note = nu * @param int|string $subscriptionId * @return bool */ - public function isSuspended(int|string $subscriptionId): bool + public function isSuspended(int | string $subscriptionId): bool { return Subscriptions::isSuspended($subscriptionId); } @@ -140,7 +142,7 @@ public function isSuspended(int|string $subscriptionId): bool * @param int|string $subscriptionId * @return void */ - public function setSubscriptionAsInactive(int|string $subscriptionId): void + public function setSubscriptionAsInactive(int | string $subscriptionId): void { Subscriptions::setSubscriptionAsInactive($subscriptionId); } @@ -163,7 +165,7 @@ public function isSubscriptionEditable(): bool * @param AbstractOrder $Order * @return int|string|false - ID or false of no ID associated */ - public function getSubscriptionIdByOrder(AbstractOrder $Order): bool|int|string + public function getSubscriptionIdByOrder(AbstractOrder $Order): bool | int | string { try { $result = QUI::getDataBase()->fetch([ @@ -191,7 +193,7 @@ public function getSubscriptionIdByOrder(AbstractOrder $Order): bool|int|string * @param int|string $subscriptionId * @return bool */ - public function isSubscriptionActiveAtPaymentProvider(int|string $subscriptionId): bool + public function isSubscriptionActiveAtPaymentProvider(int | string $subscriptionId): bool { try { $data = Subscriptions::getSubscriptionDetails($subscriptionId); @@ -217,7 +219,7 @@ public function isSubscriptionActiveAtPaymentProvider(int|string $subscriptionId * @param string|int $subscriptionId - Payment provider subscription ID * @return bool */ - public function isSubscriptionActiveAtQuiqqer(int|string $subscriptionId): bool + public function isSubscriptionActiveAtQuiqqer(int | string $subscriptionId): bool { try { $result = QUI::getDataBase()->fetch([ @@ -273,7 +275,7 @@ public function getSubscriptionIds(bool $includeInactive = false): array * @param string|int $subscriptionId * @return string|false */ - public function getSubscriptionGlobalProcessingId(int|string $subscriptionId): bool|string + public function getSubscriptionGlobalProcessingId(int | string $subscriptionId): bool | string { try { $result = QUI::getDataBase()->fetch([ @@ -339,4 +341,18 @@ public function supportsRecurringPaymentsOnly(): bool { return true; } + + /** + * Confirms a PaymentIntent with status "requires_confirmation" for a subscription. + * + * @param StripeSubscription $subscription + * @param StripePaymentIntent $paymentIntent + * @return void + */ + public function confirmPaymentIntentForSubscription( + Subscription $subscription, + PaymentIntent $paymentIntent + ): void { + // nothing by default + } } diff --git a/src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/AbstractRecurringBrowserPay.php b/src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/AbstractRecurringBrowserPay.php index b8bcaca..f4817af 100644 --- a/src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/AbstractRecurringBrowserPay.php +++ b/src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/AbstractRecurringBrowserPay.php @@ -2,6 +2,15 @@ namespace QUI\ERP\Payments\Stripe\PaymentMethods\Recurring; +use Browser; +use QUI\ERP\Order\OrderInterface; +use Stripe\PaymentIntent; +use Stripe\PaymentIntent as StripePaymentIntent; +use Stripe\Subscription; +use Stripe\Subscription as StripeSubscription; + +use function in_array; + /** * Class AbstractRecurringBrowserPay * @@ -25,4 +34,19 @@ public function getPaymentMethodType(): string { return 'browser_pay'; } + + public function canBeUsedInOrder(OrderInterface $order): bool + { + $B = new Browser(); + + if (!$B->isMobile()) { + return false; + } + + if (!in_array($B->getBrowser(), $this->getBrowserTypes())) { + return false; + } + + return true; + } } diff --git a/src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/ApplePay.php b/src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/ApplePay.php index de97a16..95250ba 100644 --- a/src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/ApplePay.php +++ b/src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/ApplePay.php @@ -104,7 +104,7 @@ public function getIcon(): string * Return the extra text for the invoice * * @param Invoice|InvoiceView|InvoiceTemporary $Invoice |InvoiceView $Invoice - * @return mixed + * @return string */ public function getInvoiceInformationText(Invoice|InvoiceView|InvoiceTemporary $Invoice): string { diff --git a/src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/Card.php b/src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/Card.php index 4aeed5b..a1255ae 100644 --- a/src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/Card.php +++ b/src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/Card.php @@ -7,6 +7,7 @@ use QUI\ERP\Accounting\Invoice\Invoice; use QUI\ERP\Accounting\Invoice\InvoiceView; use QUI\ERP\Accounting\Payments\Payments; +use Stripe\PaymentMethod; /** * Class Card @@ -91,14 +92,14 @@ public function getIcon(): string */ public function getPaymentMethodType(): string { - return \Stripe\Card::OBJECT_NAME; + return PaymentMethod::TYPE_CARD; } /** * Return the extra text for the invoice * * @param Invoice|InvoiceView $Invoice - * @return mixed + * @return string */ public function getInvoiceInformationText($Invoice): string { diff --git a/src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/GooglePay.php b/src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/GooglePay.php index b2ceff4..4a38f46 100644 --- a/src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/GooglePay.php +++ b/src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/GooglePay.php @@ -102,7 +102,7 @@ public function getIcon(): string * Return the extra text for the invoice * * @param Invoice|InvoiceView|InvoiceTemporary $Invoice |InvoiceView $Invoice - * @return mixed + * @return string */ public function getInvoiceInformationText(Invoice|InvoiceView|InvoiceTemporary $Invoice): string { diff --git a/src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/MicrosoftPay.php b/src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/MicrosoftPay.php index bb81953..90dc245 100644 --- a/src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/MicrosoftPay.php +++ b/src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/MicrosoftPay.php @@ -101,7 +101,7 @@ public function getIcon(): string * Return the extra text for the invoice * * @param Invoice|InvoiceView|InvoiceTemporary $Invoice |InvoiceView $Invoice - * @return mixed + * @return string */ public function getInvoiceInformationText(Invoice|InvoiceView|InvoiceTemporary $Invoice): string { diff --git a/src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/SepaDebit.php b/src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/SepaDebit.php new file mode 100644 index 0000000..20cbb71 --- /dev/null +++ b/src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/SepaDebit.php @@ -0,0 +1,207 @@ +<?php + +namespace QUI\ERP\Payments\Stripe\PaymentMethods\Recurring; + +use Exception; +use QUI; +use QUI\ERP\Accounting\Invoice\Invoice; +use QUI\ERP\Accounting\Invoice\InvoiceView; +use QUI\ERP\Accounting\Payments\Payments; +use QUI\ERP\Order\AbstractOrder; +use QUI\ERP\Order\OrderInterface; +use QUI\ERP\Payments\Stripe\Provider; +use QUI\ERP\Payments\Stripe\Utils; +use Stripe\Exception\ApiErrorException; +use Stripe\PaymentIntent; +use Stripe\PaymentIntent as StripePaymentIntent; +use Stripe\PaymentMethod; +use Stripe\Subscription; +use Stripe\Subscription as StripeSubscription; + +/** + * Stripe payment with SEPA Direct Debit for recurring payments + */ +class SepaDebit extends AbstractBaseRecurringPayment +{ + /** + * @return string + */ + public function getTitle(): string + { + return $this->getLocale()->get('quiqqer/payment-stripe', 'payment.Recurring.SepaDebit.title'); + } + + /** + * @return string + */ + public function getDescription(): string + { + return $this->getLocale()->get('quiqqer/payment-stripe', 'payment.Recurring.SepaDebit.description'); + } + + /** + * Get title for frontend + * + * @return string + */ + public function getFrontendTitle(): string + { + return $this->getLocale()->get('quiqqer/payment-stripe', 'payment.frontend.Recurring.SepaDebit.title'); + } + + /** + * Get description for frontend + * + * @return string + */ + public function getFrontendDescription(): string + { + return $this->getLocale()->get('quiqqer/payment-stripe', 'payment.frontend.Recurring.SepaDebit.description'); + } + + /** + * Get title for the Payment step (OrderProcess) + * + * @return string + */ + public function getPaymentStepTitle(): string + { + return $this->getLocale()->get('quiqqer/payment-stripe', 'payment.PaymentStep.title.SepaDebit'); + } + + /** + * Get description step for the Payment step (OrderProcess) + * + * @return string + */ + public function getPaymentStepInfo(): string + { + return $this->getLocale()->get('quiqqer/payment-stripe', 'payment.Recurring.PaymentStep.info.SepaDebit'); + } + +// /** +// * Return the payment icon (the URL path) +// * Can be overwritten +// * +// * @return string +// */ +// public function getIcon(): string +// { +// return Payments::getInstance()->getHost() . +// URL_OPT_DIR . +// 'quiqqer/payment-stripe/bin/images/Payment_Card.png'; // TODO: richtiges Logo +// } + + /** + * Get type string of Stripe PaymentMethod + * + * @return string + */ + public function getPaymentMethodType(): string + { + return PaymentMethod::TYPE_SEPA_DEBIT; + } + + /** + * Return the extra text for the invoice + * + * @param Invoice|InvoiceView $Invoice + * @return string + */ + public function getInvoiceInformationText($Invoice): string + { + try { + return $Invoice->getCustomer()->getLocale()->get( + 'quiqqer/payment-stripe', + 'additional_invoice_text.SepaDebit' + ); + } catch (Exception $Exception) { + QUI\System\Log::writeException($Exception); + return ''; + } + } + + /** + * Return attributes for creating a PaymentIntent + * + * @param AbstractOrder $Order + * @param string $paymentMethodId - Stripe PaymentMethod ID + * @return StripePaymentIntent + * + * @throws ApiErrorException + * @throws QUI\Exception + */ + protected function createPaymentIntentForOrder(AbstractOrder $Order, string $paymentMethodId): StripePaymentIntent + { + return StripePaymentIntent::create([ + 'payment_method' => $paymentMethodId, + 'amount' => Utils::getCentAmount($Order), + 'currency' => mb_strtolower($Order->getCurrency()->getCode()), + 'confirmation_method' => 'manual', + 'confirm' => true, + 'setup_future_usage' => 'off_session', + 'use_stripe_sdk' => true, + 'description' => Utils::getPaymentDescriptionForOrder($Order), + 'statement_descriptor' => Provider::getStatementDescriptor($Order), + 'metadata' => [ + 'orderUuid' => $Order->getUUID() + ], + 'mandate_data' => [ + 'customer_acceptance' => [ + 'type' => 'online', + 'online' => [ + 'ip_address' => $_SERVER['REMOTE_ADDR'], + 'user_agent' => $_SERVER['HTTP_USER_AGENT'], + ], + ], + ] + ]); + } + + public function canBeUsedInOrder(OrderInterface $order): bool + { + // Currency must be EUR + if ($order->getCurrency()->getCode() !== 'EUR') { + return false; + } + + if ($order->getInvoiceAddress()->getCountry()->isEU()) { + return true; + } + + // Some Non-EU countries are also part of SEPA + return match ($order->getInvoiceAddress()->getCountry()->getCode()) { + 'CH', 'GB', 'SM', 'VA', 'AD', 'MC', 'IS', 'NO', 'LI' => true, + default => false, + }; + } + + /** + * Confirms a PaymentIntent with status "requires_confirmation" for a subscription. + * + * @param StripeSubscription $subscription + * @param StripePaymentIntent $paymentIntent + * @return void + * @throws ApiErrorException + */ + public function confirmPaymentIntentForSubscription( + Subscription $subscription, + PaymentIntent $paymentIntent + ): void { + Provider::setupApi(); + + $data = [ + 'mandate_data' => [ + 'customer_acceptance' => [ + 'type' => 'online', + 'online' => [ + 'ip_address' => $_SERVER['REMOTE_ADDR'], + 'user_agent' => $_SERVER['HTTP_USER_AGENT'], + ], + ], + ] + ]; + + $paymentIntent->confirm($data); + } +} diff --git a/src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/Subscriptions.php b/src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/Subscriptions.php index b651fe3..d70a032 100644 --- a/src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/Subscriptions.php +++ b/src/QUI/ERP/Payments/Stripe/PaymentMethods/Recurring/Subscriptions.php @@ -5,26 +5,28 @@ use Exception; use PDO; use QUI; -use QUI\ERP\Order\AbstractOrder; +use QUI\ERP\Accounting\Invoice\Handler as InvoiceHandler; use QUI\ERP\Accounting\Invoice\Invoice; +use QUI\ERP\Accounting\Payments\Payments; +use QUI\ERP\Accounting\Payments\Transactions\Factory as TransactionFactory; +use QUI\ERP\Accounting\Payments\Transactions\Handler as TransactionHandler; +use QUI\ERP\Order\AbstractOrder; +use QUI\ERP\Payments\Stripe\AbstractBasePayment; +use QUI\ERP\Payments\Stripe\Provider; +use QUI\ERP\Payments\Stripe\Utils; use QUI\ExceptionStack; use QUI\Interfaces\Users\User; use QUI\System\Log; use QUI\Utils\Security\Orthos; -use QUI\ERP\Accounting\Payments\Transactions\Factory as TransactionFactory; -use QUI\ERP\Accounting\Invoice\Handler as InvoiceHandler; -use QUI\ERP\Accounting\Payments\Payments; -use QUI\ERP\Accounting\Payments\Transactions\Handler as TransactionHandler; -use QUI\ERP\Payments\Stripe\AbstractBasePayment; -use Stripe\Exception\ApiErrorException; -use Stripe\Subscription as StripeSubscription; use Stripe\Customer as StripeCustomer; +use Stripe\Dispute; +use Stripe\Exception\ApiErrorException; use Stripe\Invoice as StripeInvoice; use Stripe\PaymentIntent as StripePaymentIntent; -use QUI\ERP\Payments\Stripe\Provider; -use QUI\ERP\Payments\Stripe\Utils; +use Stripe\Subscription as StripeSubscription; use function array_keys; +use function array_pop; use function json_decode; use function json_encode; use function mb_strtoupper; @@ -50,17 +52,22 @@ class Subscriptions * Create a Stripe subscription form an order * * @param AbstractOrder $Order - * @return int|false - Subscription ID + * @return string|null - Subscription ID * * @throws Exception */ - public static function createSubscription(AbstractOrder $Order): bool|int + public static function createSubscription(AbstractOrder $Order): ?string { $SystemUser = QUI::getUsers()->getSystemUser(); if ($Order->getPaymentDataEntry(AbstractBasePayment::ATTR_STRIPE_SUBSCRIPTION_ID)) { + /** @var AbstractBaseRecurringPayment $paymentType */ + $paymentType = $Order->getPayment()->getPaymentType(); + + $confirmData = self::confirmSubscription( - $Order->getPaymentDataEntry(AbstractBasePayment::ATTR_STRIPE_SUBSCRIPTION_ID) + $Order->getPaymentDataEntry(AbstractBasePayment::ATTR_STRIPE_SUBSCRIPTION_ID), + $paymentType ); switch ($confirmData['status']) { @@ -91,7 +98,7 @@ public static function createSubscription(AbstractOrder $Order): bool|int $paymentMethodId = $Order->getPaymentDataEntry(AbstractBasePayment::ATTR_STRIPE_PAYMENT_METHOD_ID); if (empty($paymentMethodId)) { - return false; + return null; } // Create Stripe customer for subscription @@ -107,8 +114,12 @@ public static function createSubscription(AbstractOrder $Order): bool|int 'plan' => $billingPlanId ] ], - 'payment_behavior' => 'allow_incomplete', - 'default_payment_method' => $paymentMethodId +// 'payment_behavior' => 'allow_incomplete', + 'payment_behavior' => 'default_incomplete', + 'default_payment_method' => $paymentMethodId, + 'metadata' => [ + 'orderUuid' => $Order->getUUID() + ] ]); try { @@ -142,10 +153,11 @@ public static function createSubscription(AbstractOrder $Order): bool|int /** * Confirm subscription status (was creation and payment successful?) * - * @param string|int $subscriptionId + * @param string $subscriptionId + * @param AbstractBaseRecurringPayment $payment * @return array */ - public static function confirmSubscription(string|int $subscriptionId): array + public static function confirmSubscription(string $subscriptionId, AbstractBaseRecurringPayment $payment): array { Provider::setupApi(); @@ -177,6 +189,19 @@ public static function confirmSubscription(string|int $subscriptionId): array $data['status'] = 'retry_payment_method'; break; + case StripePaymentIntent::STATUS_REQUIRES_CONFIRMATION: + $paymentIntent = $StripeSubscription->latest_invoice->payment_intent; + + try { + $payment->confirmPaymentIntentForSubscription($StripeSubscription, $paymentIntent); + $StripeSubscription->refresh(); + $data['status'] = $StripeSubscription->status; + } catch (\Exception $exception) { + QUI\System\Log::writeException($exception); + $data['status'] = 'error'; + } + break; + default: $data['status'] = 'error'; } @@ -192,7 +217,7 @@ public static function confirmSubscription(string|int $subscriptionId): array * @return string - Stripe Invoice ID * @throws ApiErrorException */ - public static function getLatestSubscriptionInvoiceId(int|string $subscriptionId): string + public static function getLatestSubscriptionInvoiceId(int | string $subscriptionId): string { Provider::setupApi(); @@ -238,15 +263,15 @@ public static function billSubscriptionBalance(Invoice $Invoice): void // $data = self::getSubscriptionDetails($subscriptionId); // } - // Check if a Paymill transaction matches the Invoice + // Check if a Stripe transaction matches the Invoice $unprocessedTransactions = self::getUnprocessedTransactions($subscriptionId); $Invoice->calculatePayments(); $invoiceAmount = Utils::getCentAmount($Invoice); $invoiceCurrency = $Invoice->getCurrency()->getCode(); - /** @var AbstractBaseRecurringPayment $Payment */ $paymentMethodData = json_decode($Invoice->getAttribute('payment_method_data'), true); + /** @var AbstractBaseRecurringPayment $Payment */ $Payment = Payments::getInstance()->getPayment($paymentMethodData['id'])->getPaymentType(); foreach ($unprocessedTransactions as $transaction) { @@ -276,6 +301,26 @@ public static function billSubscriptionBalance(Invoice $Invoice): void $Invoice->getGlobalProcessId() ); + // Check if associated charge is disputed and dispute is lost + if (!empty($transaction['charge']['dispute'])) { + $dispute = $transaction['charge']['dispute']; + + if ($dispute['status'] === Dispute::STATUS_LOST) { + $InvoiceTransaction->changeStatus(TransactionHandler::STATUS_ERROR); + $Invoice->addHistory( + QUI::getSystemLocale()->get( + 'quiqqer/payment-stripe', + 'history.Invoice.stripe_charge_dispute_lost', + [ + 'quiqqerTransactionId' => $InvoiceTransaction->getTxId(), + 'stripeInvoiceId' => $transaction['id'], + 'stripeDisputeId' => $dispute['id'] + ] + ) + ); + } + } + $Invoice->addTransaction($InvoiceTransaction); QUI::getDataBase()->update( @@ -315,7 +360,7 @@ public static function billSubscriptionBalance(Invoice $Invoice): void * @return array|int * @throws QUI\Exception */ - public static function getSubscriptionList(array $searchParams, bool $countOnly = false): array|int + public static function getSubscriptionList(array $searchParams, bool $countOnly = false): array | int { $Grid = new QUI\Utils\Grid($searchParams); $gridParams = $Grid->parseDBParams($searchParams); @@ -397,7 +442,7 @@ public static function getSubscriptionList(array $searchParams, bool $countOnly * @return array * @throws ApiErrorException */ - public static function getSubscriptionDetails(int|string $subscriptionId): array + public static function getSubscriptionDetails(int | string $subscriptionId): array { Provider::setupApi(); return StripeSubscription::retrieve($subscriptionId)->toArray(); @@ -413,7 +458,7 @@ public static function getSubscriptionDetails(int|string $subscriptionId): array * @throws QUI\ERP\Payments\Stripe\StripeException * @throws ApiErrorException */ - public static function cancelSubscription(int|string $subscriptionId, string $reason = ''): void + public static function cancelSubscription(int | string $subscriptionId, string $reason = ''): void { $data = self::getSubscriptionData($subscriptionId); @@ -455,7 +500,7 @@ public static function cancelSubscription(int|string $subscriptionId, string $re * @throws QUI\ERP\Payments\Stripe\StripeException * @throws ApiErrorException */ - public static function suspendSubscription(int|string $subscriptionId): void + public static function suspendSubscription(int | string $subscriptionId): void { $data = self::getSubscriptionData($subscriptionId); @@ -500,7 +545,7 @@ public static function suspendSubscription(int|string $subscriptionId): void * @throws QUI\ERP\Payments\Stripe\StripeException * @throws ApiErrorException */ - public static function resumeSubscription(int|string $subscriptionId): void + public static function resumeSubscription(int | string $subscriptionId): void { $data = self::getSubscriptionData($subscriptionId); @@ -539,7 +584,7 @@ public static function resumeSubscription(int|string $subscriptionId): void * @return bool * @throws ApiErrorException */ - public static function isSuspended(int|string $subscriptionId): bool + public static function isSuspended(int | string $subscriptionId): bool { $data = self::getSubscriptionDetails($subscriptionId); @@ -556,7 +601,7 @@ public static function isSuspended(int|string $subscriptionId): bool * @param int|string $subscriptionId - Stripe Subscription ID * @return void */ - public static function setSubscriptionAsInactive(int|string $subscriptionId): void + public static function setSubscriptionAsInactive(int | string $subscriptionId): void { try { QUI::getDataBase()->update( @@ -572,33 +617,46 @@ public static function setSubscriptionAsInactive(int|string $subscriptionId): vo /** * Fetches subscription transactions (invoices) from Stripe * - * @param int|string $subscriptionId - Stripe Subscription ID + * @param string $stripeSubscriptionId - Stripe Subscription ID * @param string|null $startAfterInvoiceId (optional) - Only fetch invoices after this Stripe Invoice ID - * @return array + * @return StripeInvoice[] * * @throws ApiErrorException */ public static function getSubscriptionTransactions( - int|string $subscriptionId, + string $stripeSubscriptionId, string $startAfterInvoiceId = null ): array { $searchParams = [ 'limit' => 100, - 'subscription' => $subscriptionId + 'subscription' => $stripeSubscriptionId, + 'expand' => [ + 'data.charge.dispute' + ] ]; -// if ($startAfterInvoiceId) { -// $searchParams['starting_after'] = $startAfterInvoiceId; -// } + if ($startAfterInvoiceId) { + $searchParams['starting_after'] = $startAfterInvoiceId; + } - $result = StripeInvoice::all($searchParams); - $transactions = []; + $list = []; - foreach ($result->autoPagingIterator() as $Transaction) { - $transactions[] = $Transaction->toArray(); - } + do { + $result = StripeInvoice::all($searchParams); - return $result['data']; + if (empty($result['data'])) { + break; + } + + $list = array_merge($list, $result['data']); + + /** @var StripeInvoice $last */ + $last = $result->last(); + + $searchParams['starting_after'] = $last->id; + } while (!empty($result['has_more'])); + + return $list; } /** @@ -781,6 +839,10 @@ public static function processDeniedTransactions(Invoice $Invoice): void $InvoiceTransaction->changeStatus(TransactionHandler::STATUS_ERROR); + if (Provider::shouldNotifyOnFailedPayment()) { + Utils::sendNotificationAboutFailedInvoicePayment($Invoice, $transaction['id']); + } + $Invoice->addTransaction($InvoiceTransaction); QUI::getDataBase()->update( @@ -818,7 +880,7 @@ public static function processDeniedTransactions(Invoice $Invoice): void * @throws QUI\Database\Exception * @throws ApiErrorException */ - protected static function refreshTransactionList(int|string $subscriptionId): void + protected static function refreshTransactionList(int | string $subscriptionId): void { if (isset(self::$transactionsRefreshed[$subscriptionId])) { return; @@ -847,6 +909,17 @@ protected static function refreshTransactionList(int|string $subscriptionId): vo $transactions = self::getSubscriptionTransactions($data['stripe_id']); foreach ($transactions as $transaction) { + // Only save invoices with final state + switch ($transaction->status) { + case StripeInvoice::STATUS_PAID: + case StripeInvoice::STATUS_UNCOLLECTIBLE: + case StripeInvoice::STATUS_VOID: + break; + + default: + continue 2; + } + $TransactionTime = date_create('@' . $transaction['created']); if (empty($TransactionTime)) { @@ -886,23 +959,23 @@ protected static function refreshTransactionList(int|string $subscriptionId): vo /** * Get all completed Subscription transactions that are unprocessed by QUIQQER ERP * - * @param int|string $subscriptionId + * @param string $stripeSubscriptionId * @param string $status (optional) - Get transactions with this status [default: "paid"] * @return array + * @throws ApiErrorException * @throws QUI\Database\Exception - * @throws Exception */ protected static function getUnprocessedTransactions( - int|string $subscriptionId, + string $stripeSubscriptionId, string $status = StripeInvoice::STATUS_PAID ): array { - self::refreshTransactionList($subscriptionId); + self::refreshTransactionList($stripeSubscriptionId); $result = QUI::getDataBase()->fetch([ 'select' => ['stripe_invoice_data'], 'from' => Provider::getStripeBillingSubscriptionsTransactionsTable(), 'where' => [ - 'stripe_subscription_id' => $subscriptionId, + 'stripe_subscription_id' => $stripeSubscriptionId, 'quiqqer_transaction_id' => null ] ]); @@ -928,7 +1001,7 @@ protected static function getUnprocessedTransactions( * @param int|string $subscriptionId - Stripe Subscription ID * @return array|false */ - protected static function getSubscriptionData(int|string $subscriptionId): bool|array + protected static function getSubscriptionData(int | string $subscriptionId): bool | array { try { $result = QUI::getDataBase()->fetch([ @@ -994,7 +1067,8 @@ public static function createStripeCustomerFromQuiqqerUser(QUI\Interfaces\Users\ ], 'email' => $quiqqerCustomerEmail ?: '', 'description' => QUI::conf('globals', 'host'), - 'preferred_locales' => [str_replace('_', '-', $userLocaleList[0])] + 'preferred_locales' => [str_replace('_', '-', $userLocaleList[0])], + 'name' => $User->getName() // transform strings like "de_DE" to "de-DE" ]); diff --git a/src/QUI/ERP/Payments/Stripe/PaymentMethods/SepaDebit.php b/src/QUI/ERP/Payments/Stripe/PaymentMethods/SepaDebit.php new file mode 100644 index 0000000..52cb75f --- /dev/null +++ b/src/QUI/ERP/Payments/Stripe/PaymentMethods/SepaDebit.php @@ -0,0 +1,179 @@ +<?php + +namespace QUI\ERP\Payments\Stripe\PaymentMethods; + +use Exception; +use QUI; +use QUI\ERP\Accounting\Invoice\Invoice; +use QUI\ERP\Accounting\Invoice\InvoiceTemporary; +use QUI\ERP\Accounting\Invoice\InvoiceView; +use QUI\ERP\Accounting\Payments\Payments; +use QUI\ERP\Order\AbstractOrder; +use QUI\ERP\Order\OrderInterface; +use QUI\ERP\Payments\Stripe\AbstractBasePayment; +use QUI\ERP\Payments\Stripe\Utils; +use Stripe\Exception\ApiErrorException; +use Stripe\PaymentIntent as StripePaymentIntent; +use Stripe\PaymentMethod; +use QUI\ERP\Payments\Stripe\Provider; + +/** + * Stripe payment with SEPA Direct Debit + */ +class SepaDebit extends AbstractBasePayment +{ + /** + * @return string + */ + public function getTitle(): string + { + return $this->getLocale()->get('quiqqer/payment-stripe', 'payment.SepaDebit.title'); + } + + /** + * @return string + */ + public function getDescription(): string + { + return $this->getLocale()->get('quiqqer/payment-stripe', 'payment.SepaDebit.description'); + } + + /** + * Get title for frontend + * + * @return string + */ + public function getFrontendTitle(): string + { + return $this->getLocale()->get('quiqqer/payment-stripe', 'payment.frontend.SepaDebit.title'); + } + + /** + * Get description for frontend + * + * @return string + */ + public function getFrontendDescription(): string + { + return $this->getLocale()->get('quiqqer/payment-stripe', 'payment.frontend.SepaDebit.description'); + } + + /** + * Get title for the Payment step (OrderProcess) + * + * @return string + */ + public function getPaymentStepTitle(): string + { + return $this->getLocale()->get('quiqqer/payment-stripe', 'payment.PaymentStep.title.Card'); + } + + /** + * Get description step for the Payment step (OrderProcess) + * + * @return string + */ + public function getPaymentStepInfo(): string + { + return $this->getLocale()->get('quiqqer/payment-stripe', 'payment.PaymentStep.info.Card'); + } + +// /** +// * Return the payment icon (the URL path) +// * Can be overwritten +// * +// * @return string +// */ +// public function getIcon(): string +// { +// return Payments::getInstance()->getHost() . +// URL_OPT_DIR . +// 'quiqqer/payment-stripe/bin/images/Payment_Card.png'; // TODO: richtiges Logo verwenden +// } + + /** + * Get type string of Stripe PaymentMethod + * + * @return string + */ + public function getPaymentMethodType(): string + { + return PaymentMethod::TYPE_SEPA_DEBIT; + } + + /** + * Return attributes for creating a PaymentIntent + * + * @param AbstractOrder $Order + * @param string $paymentMethodId - Stripe PaymentMethod ID + * @return StripePaymentIntent + * + * @throws ApiErrorException + * @throws QUI\Exception + */ + protected function createPaymentIntentForOrder(AbstractOrder $Order, string $paymentMethodId): StripePaymentIntent + { + return StripePaymentIntent::create([ + 'payment_method' => $paymentMethodId, + 'amount' => Utils::getCentAmount($Order), + 'currency' => mb_strtolower($Order->getCurrency()->getCode()), +// 'confirmation_method' => 'manual', + 'confirm' => true, + 'description' => Utils::getPaymentDescriptionForOrder($Order), + 'statement_descriptor' => Provider::getStatementDescriptor($Order), + 'automatic_payment_methods' => [ + 'enabled' => true, + 'allow_redirects' => 'never' + ], + 'metadata' => [ + 'orderUuid' => $Order->getUUID() + ], + 'mandate_data' => [ + 'customer_acceptance' => [ + 'type' => 'online', + 'online' => [ + 'ip_address' => $_SERVER['REMOTE_ADDR'], + 'user_agent' => $_SERVER['HTTP_USER_AGENT'], + ], + ], + ] + ]); + } + + /** + * Return the extra text for the invoice + * + * @param Invoice|InvoiceTemporary|InvoiceView $Invoice |InvoiceView $Invoice + * @return string + */ + public function getInvoiceInformationText(Invoice | InvoiceTemporary | InvoiceView $Invoice): string + { + try { + return $Invoice->getCustomer()->getLocale()->get( + 'quiqqer/payment-stripe', + 'additional_invoice_text.Card' + ); + } catch (Exception $Exception) { + QUI\System\Log::writeException($Exception); + return ''; + } + } + + public function canBeUsedInOrder(OrderInterface $order): bool + { + // Currency must be EUR + if ($order->getCurrency()->getCode() !== 'EUR') { + return false; + } + + if ($order->getInvoiceAddress()->getCountry()->isEU()) { + return true; + } + + // Some Non-EU countries are also part of SEPA + return match ($order->getInvoiceAddress()->getCountry()->getCode()) { + 'CH', 'GB', 'SM', 'VA', 'AD', 'MC', 'IS', 'NO', 'LI' => true, + default => false, + }; + } +} diff --git a/src/QUI/ERP/Payments/Stripe/Provider.php b/src/QUI/ERP/Payments/Stripe/Provider.php index d937590..29942e8 100644 --- a/src/QUI/ERP/Payments/Stripe/Provider.php +++ b/src/QUI/ERP/Payments/Stripe/Provider.php @@ -6,16 +6,19 @@ use QUI; use QUI\ERP\Accounting\Payments\Api\AbstractPaymentProvider; use QUI\ERP\Accounting\Payments\Types\Factory; -use QUI\ERP\Payments\Stripe\PaymentMethods\Card; -use Stripe\Stripe as StripeApiClient; -use QUI\ERP\Payments\Stripe\PaymentMethods\Recurring\Card as CardRecurring; +use QUI\ERP\Order\OrderInterface; use QUI\ERP\Payments\Stripe\PaymentMethods\ApplePay; +use QUI\ERP\Payments\Stripe\PaymentMethods\Card; use QUI\ERP\Payments\Stripe\PaymentMethods\GooglePay; use QUI\ERP\Payments\Stripe\PaymentMethods\MicrosoftPay; use QUI\ERP\Payments\Stripe\PaymentMethods\Recurring\ApplePay as ApplePayRecurring; +use QUI\ERP\Payments\Stripe\PaymentMethods\Recurring\Card as CardRecurring; use QUI\ERP\Payments\Stripe\PaymentMethods\Recurring\GooglePay as GooglePayRecurring; use QUI\ERP\Payments\Stripe\PaymentMethods\Recurring\MicrosoftPay as MicrosoftPayRecurring; +use QUI\ERP\Payments\Stripe\PaymentMethods\SepaDebit; use QUI\System\VhostManager; +use Stripe\Stripe as StripeApiClient; +use QUI\ERP\Payments\Stripe\PaymentMethods\Recurring\SepaDebit as SepaDebitRecurring; use function array_flip; use function array_map; @@ -23,6 +26,7 @@ use function file_exists; use function file_get_contents; use function file_put_contents; +use function mb_strlen; /** * Class Provider @@ -47,10 +51,22 @@ public function getPaymentTypes(): array Card::class, GooglePay::class, ApplePay::class, - MicrosoftPay::class + MicrosoftPay::class, + SepaDebit::class ], self::getRecurringPaymentTypes()); } + public static function getNonRecurringPaymentTypes(): array + { + return [ + Card::class, + GooglePay::class, + ApplePay::class, + MicrosoftPay::class, + SepaDebit::class + ]; + } + /** * @return array */ @@ -60,7 +76,8 @@ public static function getRecurringPaymentTypes(): array CardRecurring::class, ApplePayRecurring::class, GooglePayRecurring::class, - MicrosoftPayRecurring::class + MicrosoftPayRecurring::class, + SepaDebitRecurring::class ]; } @@ -70,7 +87,7 @@ public static function getRecurringPaymentTypes(): array * @param string $setting - Setting name * @return bool|string */ - public static function getApiSetting(string $setting): bool|string + public static function getApiSetting(string $setting): bool | string { try { $Conf = QUI::getPackage('quiqqer/payment-stripe')->getConfig(); @@ -89,7 +106,7 @@ public static function getApiSetting(string $setting): bool|string * @param string $setting - Setting name * @return bool|string */ - public static function getGeneralSetting(string $setting): bool|string + public static function getGeneralSetting(string $setting): bool | string { try { $Conf = QUI::getPackage('quiqqer/payment-stripe')->getConfig(); @@ -102,12 +119,56 @@ public static function getGeneralSetting(string $setting): bool|string return $Conf->get('general', $setting); } + /** + * The statement descriptor is shown in the account statement for a Stripe transaction. + * + * E.g. on the bank statement of a SEPA payment. + * + * @param OrderInterface $order + * @return string|null + */ + public static function getStatementDescriptor(OrderInterface $order): ?string + { + $descriptor = self::getGeneralSetting('statement_descriptor'); + + if (empty($descriptor)) { + return null; + } + + $descriptor = str_replace( + [ + '{orderId}' + ], + [ + $order->getPrefixedNumber() + ], + $descriptor + ); + + // Max 22 characters + if (mb_strlen($descriptor) <= 22) { + return $descriptor; + } + + return mb_substr($descriptor, 0, 21) . '*'; + } + + /** + * Should the system admin be notified if a Stripe payment fails? + * + * @return bool + */ + public static function shouldNotifyOnFailedPayment(): bool + { + return !empty(self::getGeneralSetting('notify_about_failed_payment')); + } + /** * Get Stripe public key ("Publishable key") * * @return bool|string */ - public static function getPublicKey(): bool|string + public static function getPublicKey(): bool | string { if (self::isSandbox()) { return self::getApiSetting('sandbox_public_key'); @@ -122,7 +183,7 @@ public static function getPublicKey(): bool|string * @param bool $considerSandbox (optional) - Considers sandbox settings when retrieving key [default: true] * @return bool|string */ - public static function getClientSecret(bool $considerSandbox = true): bool|string + public static function getClientSecret(bool $considerSandbox = true): bool | string { if ($considerSandbox && self::isSandbox()) { return self::getApiSetting('sandbox_client_secret'); diff --git a/src/QUI/ERP/Payments/Stripe/Utils.php b/src/QUI/ERP/Payments/Stripe/Utils.php index e76ebac..74ec27c 100644 --- a/src/QUI/ERP/Payments/Stripe/Utils.php +++ b/src/QUI/ERP/Payments/Stripe/Utils.php @@ -7,9 +7,13 @@ use QUI\ERP\ErpEntityInterface; use QUI\ERP\Exception; use QUI\ERP\Order\AbstractOrder; -use Stripe\PaymentMethod as StripePaymentMethod; -use Stripe\Exception\ApiErrorException; +use QUI\ERP\Order\Handler as OrderHandler; +use QUI\ERP\Order\OrderInterface; use QUI\Interfaces\Users\User as UserInterface; +use Stripe\Dispute; +use Stripe\Exception\ApiErrorException; +use Stripe\PaymentIntent; +use Stripe\PaymentMethod as StripePaymentMethod; use function mb_strpos; use function str_replace; @@ -31,7 +35,7 @@ class Utils * @throws QUI\Exception * @throws StripeException */ - public static function getCentAmount(QUI\ERP\ErpEntityInterface $Source): float|int + public static function getCentAmount(QUI\ERP\ErpEntityInterface $Source): float | int { if ($Source instanceof AbstractOrder) { $PriceCalculation = $Source->getPriceCalculation(); @@ -143,4 +147,187 @@ public static function getPaymentDescriptionForOrder(AbstractOrder $Order): stri return $defaultDescription; } } + + /** + * @param Invoice $invoice + * @param string $stripeInvoiceId + * @param string $errorMsg + * @return void + */ + public static function sendNotificationAboutFailedInvoicePayment( + Invoice $invoice, + string $stripeInvoiceId, + string $errorMsg = '' + ): void { + try { + $locale = QUI::getSystemLocale(); + $mailer = QUI::getMailManager()->getMailer(); + $mailer->addRecipient(QUI::conf('mail', 'admin_mail')); + $mailer->setSubject( + $locale->get( + 'quiqqer/payment-stripe', + 'mail.payment_failed.invoice.subject', + [ + 'invoiceNo' => $invoice->getPrefixedNumber() + ] + ) + ); + + $contractNo = '-'; + $contractData = $invoice->getData('contract'); + + if (!empty($contractData['prefixedNumber'])) { + $contractNo = $contractData['prefixedNumber']; + } + + $mailer->setBody( + $locale->get( + 'quiqqer/payment-stripe', + 'mail.payment_failed.invoice.body', + [ + 'invoiceNo' => $invoice->getPrefixedNumber(), + 'invoiceAmount' => $invoice->getPriceCalculation()->getSum()->formatted($locale), + 'contractNo' => $contractNo, + 'stripeInvoiceId' => $stripeInvoiceId, + 'stripeInvoiceUrl' => self::getStripeDashboardSearchUrl($stripeInvoiceId), + 'errorMsg' => $errorMsg ?: '-' + ] + ) + ); + $mailer->send(); + } catch (\Exception $exception) { + QUI\System\Log::writeException($exception); + } + } + + /** + * @param AbstractOrder $order + * @param string $errorMsg + * @return void + */ + public static function sendNotificationAboutFailedOrderPayment(AbstractOrder $order, string $errorMsg = ''): void + { + try { + $locale = QUI::getSystemLocale(); + $mailer = QUI::getMailManager()->getMailer(); + $mailer->addRecipient(QUI::conf('mail', 'admin_mail')); + $mailer->setSubject( + $locale->get( + 'quiqqer/payment-stripe', + 'mail.payment_failed.order.subject', + [ + 'orderId' => $order->getPrefixedNumber() + ] + ) + ); + + $paymentIntentId = $order->getPaymentDataEntry(AbstractBasePayment::ATTR_STRIPE_PAYMENT_INTENT_ID); + $paymentIntentUrl = 'https://dashboard.stripe.com/'; + + if (Provider::isSandbox()) { + $paymentIntentUrl .= 'test/'; + } + + $paymentIntentUrl .= 'payments/' . $paymentIntentId; + + $mailer->setBody( + $locale->get( + 'quiqqer/payment-stripe', + 'mail.payment_failed.order.body', + [ + 'orderId' => $order->getPrefixedNumber(), + 'orderAmount' => $order->getPriceCalculation()->getSum()->formatted($locale), + 'stripePaymentIntentId' => $order->getPaymentDataEntry( + AbstractBasePayment::ATTR_STRIPE_PAYMENT_INTENT_ID + ), + 'paymentIntentUrl' => $paymentIntentUrl, + 'errorMsg' => $errorMsg ?: '-' + ] + ) + ); + $mailer->send(); + } catch (\Exception $exception) { + QUI\System\Log::writeException($exception); + } + } + + /** + * @param Dispute $dispute + * @param OrderInterface|null $order + * @return void + */ + public static function sendNotificationAboutDisputedTransaction( + Dispute $dispute, + OrderInterface $order = null + ): void { + try { + $locale = QUI::getSystemLocale(); + $mailer = QUI::getMailManager()->getMailer(); + $mailer->addRecipient(QUI::conf('mail', 'admin_mail')); + $mailer->setSubject( + $locale->get( + 'quiqqer/payment-stripe', + 'mail.disputed_transaction.subject' + ) + ); + + $mailer->setBody( + $locale->get( + 'quiqqer/payment-stripe', + 'mail.disputed_transaction.body', + [ + 'amount' => $dispute->amount / 100, + 'currency' => $dispute->currency, + 'chargeUrl' => self::getStripeDashboardSearchUrl($dispute->charge), + 'reason' => $dispute->reason, + 'orderId' => $order ? $order->getPrefixedNumber() : '-' + ] + ) + ); + $mailer->send(); + } catch (\Exception $exception) { + QUI\System\Log::writeException($exception); + } + } + + /** + * @param PaymentIntent $paymentIntent + * @return AbstractOrder|null + */ + public static function getOrderByPaymentIntent(PaymentIntent $paymentIntent): ?AbstractOrder + { + $metaData = $paymentIntent->metadata->toArray(); + + if (empty($metaData['orderUuid'])) { + return null; + } + + $orderUuid = $metaData['orderUuid']; + + try { + return OrderHandler::getInstance()->get($orderUuid); + } catch (\Exception $exception) { + QUI\System\Log::writeException($exception); + } + + return null; + } + + /** + * Get direct link to Stripe dashboard with an entity id (invoice, dispute, payment intent etc.). + * + * @param string $entityId + * @return string + */ + public static function getStripeDashboardSearchUrl(string $entityId): string + { + $url = 'https://dashboard.stripe.com/'; + + if (Provider::isSandbox()) { + $url .= 'test/'; + } + + $url .= 'search?query=' . $entityId; + return $url; + } } diff --git a/src/QUI/ERP/Payments/Stripe/WebhookHandler.php b/src/QUI/ERP/Payments/Stripe/WebhookHandler.php index 0c709c1..31a57d1 100644 --- a/src/QUI/ERP/Payments/Stripe/WebhookHandler.php +++ b/src/QUI/ERP/Payments/Stripe/WebhookHandler.php @@ -4,11 +4,14 @@ use Exception; use QUI; +use Stripe\Dispute; use Stripe\Event as StripeEvent; +use Stripe\PaymentIntent; use Stripe\Refund as StripeRefund; use Stripe\Subscription as StripeSubscription; use QUI\ERP\Accounting\Payments\Transactions\Handler as TransactionsHandler; use QUI\ERP\Payments\Stripe\PaymentMethods\Recurring\Subscriptions; +use QUI\ERP\Order\Handler as OrderHandler; /** * Class WebhookHandler @@ -41,6 +44,16 @@ public static function handleEvent(StripeEvent $Event): void case StripeEvent::CUSTOMER_SUBSCRIPTION_UPDATED: self::onCustomerSubscriptionUpdated($Event); break; + + case StripeEvent::PAYMENT_INTENT_SUCCEEDED: + case StripeEvent::PAYMENT_INTENT_CANCELED: + // currently checked synchronously via cron +// self::onPaymentIntentEvent($Event); + break; + + case StripeEvent::CHARGE_DISPUTE_CREATED: + self::onChargeDisputeCreated($Event); + break; } } @@ -124,4 +137,57 @@ protected static function onChargeRefunded(StripeEvent $Event): void QUI\System\Log::writeException($Exception); } } + + /** + * Stripe webhook: customer.subscription.deleted + * + * @param StripeEvent $event + * @return void + */ + protected static function onPaymentIntentEvent(StripeEvent $event): void + { + $paymentIntent = $event->data->object; + + if (!($paymentIntent instanceof PaymentIntent)) { + return; + } + + $metaData = $paymentIntent->metadata->toArray(); + + if (empty($metaData['orderUuid'])) { + return; + } + + try { + $order = OrderHandler::getInstance()->get($metaData['orderUuid']); + } catch (\Exception $exception) { + QUI\System\Log::writeDebugException($exception); + return; + } + + $orderProcessingService = new OrderProcessingService($order); + $orderProcessingService->processPaymentIntent($paymentIntent); + } + + /** + * Stripe webhook: customer.subscription.deleted + * + * @param StripeEvent $event + * @return void + */ + protected static function onChargeDisputeCreated(StripeEvent $event): void + { + $dispute = $event->data->object; + + if (!($dispute instanceof Dispute)) { + return; + } + + try { + $chargeDisputeService = new ChargeDisputeService(); + $chargeDisputeService->processDispute($dispute); + } catch (\Exception $exception) { + QUI\System\Log::writeException($exception); + } + } } -- GitLab