From 8443ff5ba9138982b53c4d24b2baeae9dfe79d88 Mon Sep 17 00:00:00 2001 From: 021-projects <20326979+021-projects@users.noreply.github.com> Date: Tue, 19 Mar 2024 15:53:18 +0200 Subject: [PATCH] feat: new webhooks handling --- .../BtcPayProvider/Payment/BTCPayServer.php | 24 ++-- .../Payment/Concerns/Webhook.php | 114 ++++++++++++++++++ src/addons/BS/BtcPayProvider/helpers.php | 8 ++ 3 files changed, 135 insertions(+), 11 deletions(-) create mode 100644 src/addons/BS/BtcPayProvider/Payment/Concerns/Webhook.php create mode 100644 src/addons/BS/BtcPayProvider/helpers.php diff --git a/src/addons/BS/BtcPayProvider/Payment/BTCPayServer.php b/src/addons/BS/BtcPayProvider/Payment/BTCPayServer.php index a4d30ff..84437b5 100644 --- a/src/addons/BS/BtcPayProvider/Payment/BTCPayServer.php +++ b/src/addons/BS/BtcPayProvider/Payment/BTCPayServer.php @@ -15,6 +15,8 @@ class BTCPayServer extends AbstractProvider { + use Concerns\Webhook; + public function getTitle() { return 'BTCPay Server'; @@ -87,6 +89,7 @@ public function setupCallback(\XF\Http\Request $request) $state->requestKey = $metadata['request_key'] ?? ''; $state->transactionId = $payload['invoiceId'] ?? ''; $state->hookType = $payload['type'] ?? ''; + $state->payload = $payload; return $state; } @@ -103,14 +106,11 @@ public function validateCallback(CallbackState $state) $secret = $paymentProfile->options['secret']; - if ($state->hookType !== 'InvoiceSettled') { - $state->logType = false; - $state->logMessage = 'Invalid hook type.'; - $state->httpCode = 200; - return false; - } - - if (! Webhook::isIncomingWebhookRequestValid($state->inputRaw, $state->signature, $secret)) { + if (! Webhook::isIncomingWebhookRequestValid( + $state->inputRaw, + $state->signature, + $secret + )) { $state->logType = 'error'; $state->logMessage = 'Invalid signature.'; return false; @@ -156,11 +156,13 @@ public function validateTransaction(CallbackState $state) public function getPaymentResult(CallbackState $state) { - if ($state->hookType !== 'InvoiceSettled') { - return; + $result = $this->getWebhookPaymentResult($state); + if (! $result) { + return null; } - $state->paymentResult = CallbackState::PAYMENT_RECEIVED; + $state->paymentResult = $result; + return $result; } public function prepareLogData(CallbackState $state) diff --git a/src/addons/BS/BtcPayProvider/Payment/Concerns/Webhook.php b/src/addons/BS/BtcPayProvider/Payment/Concerns/Webhook.php new file mode 100644 index 0000000..386c00e --- /dev/null +++ b/src/addons/BS/BtcPayProvider/Payment/Concerns/Webhook.php @@ -0,0 +1,114 @@ +payload ?? []; + + switch ($state->hookType) { + case 'InvoiceSettled': + if ($payload['overPaid'] ?? false) { + $state->logType = 'info'; + $state->logMessage = 'Invoice payment settled but was overpaid.'; + } + + return CallbackState::PAYMENT_RECEIVED; + + case 'InvoicePaymentSettled': + if (! data_get($state->purchaseRequest->extra_data, 'invoiceExpired')) { + $state->logType = 'info'; + $state->logMessage = 'Invoice (partial) payment settled.'; + return null; + } + + // here invoice expired + $state->logType = 'info'; + if ($this->invoiceIsFullyPaid($state->paymentProfile->options, $state->transactionId)) { + $state->logMessage = 'Invoice fully settled after invoice was already expired. Needs manual checking.'; + } else { + $state->logMessage = '(Partial) payment settled but invoice not settled yet (could be more transactions incoming). Needs manual checking.'; + } + return null; + + case 'InvoiceReceivedPayment': + $state->logType = 'info'; + if (data_get($payload, 'afterExpiration')) { + $state->logMessage = 'Invoice (partial) payment incoming (unconfirmed) after invoice was already expired.'; + } else { + $state->logMessage = 'Invoice (partial) payment incoming (unconfirmed). Waiting for settlement.'; + } + return null; + + case 'InvoiceProcessing': + $state->logType = 'info'; + if (data_get($payload, 'overPaid')) { + $state->logMessage = 'Invoice payment received fully with overpayment, waiting for settlement.'; + } else { + $state->logMessage = 'Invoice payment received fully, waiting for settlement.'; + } + return null; + + case 'InvoiceExpired': + $state->logType = 'info'; + if (data_get($payload, 'partiallyPaid')) { + $state->logMessage = 'Invoice expired but was paid partially, please check.'; + } else { + $state->logMessage = 'Invoice expired. No action to take.'; + } + $this->updateStatePurchaseRequestExtraData($state, 'invoiceExpired', true); + return null; + + case 'InvoiceInvalid': + $state->logType = 'info'; + if (data_get($payload, 'manuallyMarked')) { + $state->logMessage = 'Invoice manually marked invalid.'; + } else { + $state->logMessage = 'Invoice became invalid.'; + } + return null; + + default: + return null; + } + } + + protected function updateStatePurchaseRequestExtraData( + CallbackState $state, + array|string $key, + mixed $value = null + ): void { + $request = $state->purchaseRequest; + $extraData = $request->extra_data; + + if (is_array($key)) { + $extraData = array_merge($extraData, $key); + } else { + $extraData[$key] = $value; + } + + $request->extra_data = $extraData; + $request->save(); + } + + protected function invoiceIsFullyPaid(array $options, string $invoiceId): bool + { + $client = new Invoice($options['host'], $options['api_key']); + + try { + return $client->getInvoice($options['store_id'], $invoiceId) + ->isSettled(); + } catch (\Throwable $e) { + \XF::logException($e, false, 'BTCPay Server invoice retrieval failed: '); + } + + return false; + } +} diff --git a/src/addons/BS/BtcPayProvider/helpers.php b/src/addons/BS/BtcPayProvider/helpers.php new file mode 100644 index 0000000..8cbdaba --- /dev/null +++ b/src/addons/BS/BtcPayProvider/helpers.php @@ -0,0 +1,8 @@ +