Skip to content

Commit

Permalink
* Add support for ExpiredPaidLate order status.
Browse files Browse the repository at this point in the history
* Register and listen to PaymentSettled webhook events.
* Add UpdateManager to process and perform needed updates once.
* Add update routines for order states and update Webhook event config on BTCPay Server.
* Bump version.
  • Loading branch information
ndeet committed Aug 17, 2022
1 parent d8d0d64 commit 1d2cbb2
Show file tree
Hide file tree
Showing 8 changed files with 252 additions and 34 deletions.
14 changes: 11 additions & 3 deletions btcpay-greenfield-for-woocommerce.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@
* Author URI: https://btcpayserver.org
* Text Domain: btcpay-greenfield-for-woocommerce
* Domain Path: /languages
* Version: 1.0.2
* Version: 1.0.3
* Requires PHP: 7.4
* Tested up to: 5.9
* Tested up to: 6.0
* Requires at least: 5.2
* WC requires at least: 5.0.0
* WC tested up to: 6.8
*/

use BTCPayServer\WC\Admin\Notice;
Expand All @@ -21,7 +23,8 @@

defined( 'ABSPATH' ) || exit();

define( 'BTCPAYSERVER_VERSION', '1.0.2' );
define( 'BTCPAYSERVER_VERSION', '1.0.3' );
define( 'BTCPAYSERVER_VERSION_KEY', 'btcpay_gf_version' );
define( 'BTCPAYSERVER_PLUGIN_FILE_PATH', plugin_dir_path( __FILE__ ) );
define( 'BTCPAYSERVER_PLUGIN_URL', plugin_dir_url(__FILE__ ) );
define( 'BTCPAYSERVER_PLUGIN_ID', 'btcpay-greenfield-for-woocommerce' );
Expand All @@ -35,6 +38,9 @@ public function __construct() {

add_action('woocommerce_thankyou_btcpaygf_default', [$this, 'orderStatusThankYouPage'], 10, 1);

// Run the updates.
\BTCPayServer\WC\Helper\UpdateManager::processUpdates();

if (is_admin()) {
// Register our custom global settings page.
add_filter(
Expand Down Expand Up @@ -254,6 +260,7 @@ function init_btcpay_greenfield() {
flush_rewrite_rules(false);
update_option('btcpaygf_permalinks_flushed', 1);
}

});

// Action links on plugin overview.
Expand Down Expand Up @@ -339,6 +346,7 @@ function init_btcpay_greenfield() {
// Installation routine.
register_activation_hook( __FILE__, function() {
update_option('btcpaygf_permalinks_flushed', 0);
update_option( BTCPAYSERVER_VERSION_KEY, BTCPAYSERVER_VERSION );
});

// Initialize payment gateways and plugin.
Expand Down
14 changes: 12 additions & 2 deletions readme.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ Contributors: ndeet, kukks, nicolasdorier
Donate link: https://btcpayserver.org/donate/
Tags: bitcoin, btcpay, BTCPay Server, btcpayserver, WooCommerce, payment gateway, accept bitcoin, bitcoin plugin, bitcoin payment processor, bitcoin e-commerce, Lightning Network, Litecoin, cryptocurrency
Requires at least: 5.2
Tested up to: 5.9
Tested up to: 6.0
Requires PHP: 7.4
Stable tag: 1.0.2
Stable tag: 1.0.3
License: MIT
License URI: https://github.com/btcpayserver/woocommerce-greenfield-plugin/blob/master/license.txt

Expand Down Expand Up @@ -103,6 +103,16 @@ You'll find extensive documentation and answers to many of your questions on [BT
6. Example of the PoS app you can launch.

== Changelog ==
= 1.0.3 :: 2022-08-17 =
* New order state: Payment received after invoice has been expired.
* Order metadata restructure, also list multiple payments separated.
* Add plugin action links for settings, logs, docs, support.
* Show notice when BTCPay Server is not fully synched yet.
* Add BTCPay Server info to debug log.
* Update Readme with development instructions.
* Docker: Update to latest WP and WC versions.
* Pin BTCPay Server PHP library stable version.

= 1.0.2 :: 2022-04-08 =
* Fix plugin meta docblock version update, pump version once more.

Expand Down
69 changes: 56 additions & 13 deletions src/Gateway/AbstractGateway.php
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,15 @@ public function process_payment( $orderId ) {

Logger::debug( 'Invoice creation successful, redirecting user.' );

$url = $invoice->getData()['checkoutLink'];
// Todo: needs testing, support for .onion URLs, see https://github.com/btcpayserver/woocommerce-greenfield-plugin/issues/4
/* if ( preg_match( "/^([a-zA-Z0-9][a-zA-Z0-9-]{1,61}[a-zA-Z0-9]\.)?[a-zA-Z0-9][a-zA-Z0-9-]{1,61}[a-zA-Z0-9]\.onion$/", $_SERVER['SERVER_NAME'] ) ){
$url = str_replace($this->apiHelper->url, $_SERVER['SERVER_NAME'], $url);
} */

return [
'result' => 'success',
'redirect' => $invoice->getData()['checkoutLink'],
'redirect' => $url,
];
}
}
Expand Down Expand Up @@ -289,13 +295,39 @@ protected function processOrderStatus(\WC_Order $order, \stdClass $webhookData)
switch ($webhookData->type) {
case 'InvoiceReceivedPayment':
if ($webhookData->afterExpiration) {
if ($order->get_status() === $configuredOrderStates[OrderStates::EXPIRED]) {
$this->updateWCOrderStatus($order, $configuredOrderStates[OrderStates::EXPIRED_PAID_PARTIAL]);
$order->add_order_note(__('Invoice (partial) payment incoming (unconfirmed) after invoice was already expired.', 'btcpay-greenfield-for-woocommerce'));
} else {
// No need to change order status here, only leave a note.
$order->add_order_note(__('Invoice (partial) payment incoming (unconfirmed). Waiting for settlement.', 'btcpay-greenfield-for-woocommerce'));
}

// Store payment data (exchange rate, address).
$this->updateWCOrderPayments($order);

break;
case 'InvoicePaymentSettled':
// We can't use $webhookData->afterExpiration here as there is a bug affecting all version prior to
// BTCPay Server v1.7.0.0, see https://github.com/btcpayserver/btcpayserver/issues/
// Therefore we check if the invoice is in expired or expired paid partial status, instead.
$orderStatus = $order->get_status();
if ($orderStatus === str_replace('wc-', '', $configuredOrderStates[OrderStates::EXPIRED]) ||
$orderStatus === str_replace('wc-', '', $configuredOrderStates[OrderStates::EXPIRED_PAID_PARTIAL])
) {
// Check if also the invoice is now fully paid.
if (GreenfieldApiHelper::invoiceIsFullyPaid($webhookData->invoiceId)) {
Logger::debug('Invoice fully paid.');
$this->updateWCOrderStatus($order, $configuredOrderStates[OrderStates::EXPIRED_PAID_LATE]);
$order->add_order_note(__('Invoice fully settled after invoice was already expired. Needs manual checking.', 'btcpay-greenfield-for-woocommerce'));
//$order->payment_complete();
} else {
Logger::debug('Invoice NOT fully paid.');
$this->updateWCOrderStatus($order, $configuredOrderStates[OrderStates::EXPIRED_PAID_PARTIAL]);
$order->add_order_note(__('Invoice payment received after invoice was already expired.', 'btcpay-greenfield-for-woocommerce'));
$order->add_order_note(__('(Partial) payment settled but invoice not settled yet (could be more transactions incoming). Needs manual checking.', 'btcpay-greenfield-for-woocommerce'));
}
} else {
// No need to change order status here, only leave a note.
$order->add_order_note(__('Invoice (partial) payment received. Waiting for full payment.', 'btcpay-greenfield-for-woocommerce'));
$order->add_order_note(__('Invoice (partial) payment settled.', 'btcpay-greenfield-for-woocommerce'));
}

// Store payment data (exchange rate, address).
Expand Down Expand Up @@ -402,6 +434,7 @@ public function markInvoiceInvalid($invoiceId): void {
*/
public function updateWCOrderStatus(\WC_Order $order, string $status): void {
if ($status !== OrderStates::IGNORE) {
Logger::debug('Updating order status from ' . $order->get_status() . ' to ' . $status);
$order->update_status($status);
}
}
Expand All @@ -414,17 +447,27 @@ public function updateWCOrderPayments(\WC_Order $order): void {

foreach ($allPaymentData as $payment) {
// Only continue if the payment method has payments made.
if ((float) $payment->getTotalPaid() > 0.0) {
$paymentMethod = $payment->getPaymentMethod();
// Update order meta data.
update_post_meta( $order->get_id(), "BTCPay_{$paymentMethod}_destination", $payment->getDestination() ?? '' );
update_post_meta( $order->get_id(), "BTCPay_{$paymentMethod}_amount", $payment->getAmount() ?? '' );
update_post_meta( $order->get_id(), "BTCPay_{$paymentMethod}_paid", $payment->getTotalPaid() ?? '' );
update_post_meta( $order->get_id(), "BTCPay_{$paymentMethod}_networkFee", $payment->getNetworkFee() ?? '' );
update_post_meta( $order->get_id(), "BTCPay_{$paymentMethod}_rate", $payment->getRate() ?? '' );
if ((float) $payment->getPaymentMethodPaid() > 0.0) {
$paymentMethodName = $payment->getPaymentMethod();
// Update order meta data with payment methods and transactions.
update_post_meta( $order->get_id(), "BTCPay_{$paymentMethodName}_total_paid", $payment->getTotalPaid() ?? '' );
update_post_meta( $order->get_id(), "BTCPay_{$paymentMethodName}_total_amount", $payment->getAmount() ?? '' );
update_post_meta( $order->get_id(), "BTCPay_{$paymentMethodName}_total_due", $payment->getDue() ?? '' );
update_post_meta( $order->get_id(), "BTCPay_{$paymentMethodName}_total_fee", $payment->getNetworkFee() ?? '' );
update_post_meta( $order->get_id(), "BTCPay_{$paymentMethodName}_rate", $payment->getRate() ?? '' );
if ((float) $payment->getRate() > 0.0) {
$formattedRate = number_format((float) $payment->getRate(), wc_get_price_decimals(), wc_get_price_decimal_separator(), wc_get_price_thousand_separator());
update_post_meta( $order->get_id(), "BTCPay_{$paymentMethod}_rateFormatted", $formattedRate );
update_post_meta( $order->get_id(), "BTCPay_{$paymentMethodName}_rateFormatted", $formattedRate );
}

// For each actual payment make a separate entry to make sense of it.
foreach ($payment->getPayments() as $index => $trx) {
update_post_meta( $order->get_id(), "BTCPay_{$paymentMethodName}_{$index}_id", $trx->getTransactionId() ?? '' );
update_post_meta( $order->get_id(), "BTCPay_{$paymentMethodName}_{$index}_timestamp", $trx->getReceivedTimestamp() ?? '' );
update_post_meta( $order->get_id(), "BTCPay_{$paymentMethodName}_{$index}_destination", $trx->getDestination() ?? '' );
update_post_meta( $order->get_id(), "BTCPay_{$paymentMethodName}_{$index}_amount", $trx->getValue() ?? '' );
update_post_meta( $order->get_id(), "BTCPay_{$paymentMethodName}_{$index}_status", $trx->getStatus() ?? '' );
update_post_meta( $order->get_id(), "BTCPay_{$paymentMethodName}_{$index}_networkFee", $trx->getFee() ?? '' );
}
}
}
Expand Down
20 changes: 19 additions & 1 deletion src/Helper/GreenfieldApiHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace BTCPayServer\WC\Helper;

use BTCPayServer\Client\Invoice;
use BTCPayServer\Client\Server;
use BTCPayServer\Client\Store;
use BTCPayServer\Client\StorePaymentMethod;
Expand Down Expand Up @@ -38,7 +39,7 @@ public static function getConfig(): array {
$key = get_option('btcpay_gf_api_key');
if ($url && $key) {
return [
'url' => $url,
'url' => rtrim($url, '/'),
'api_key' => $key,
'store_id' => get_option('btcpay_gf_store_id', null),
'webhook' => get_option('btcpay_gf_webhook', null)
Expand Down Expand Up @@ -159,4 +160,21 @@ public static function apiCredentialsExist(string $apiUrl, string $apiKey, strin
return false;
}

/**
* Checks if a given invoice id has status of fully paid (settled) or paid late.
*/
public static function invoiceIsFullyPaid(string $invoiceId): bool {
if ($config = self::getConfig()) {
$client = new Invoice($config['url'], $config['api_key']);
try {
$invoice = $client->getInvoice($config['store_id'], $invoiceId);
return $invoice->isFullyPaid() || $invoice->isPaidLate();
} catch (\Throwable $e) {
Logger::debug('Exception while checking if invoice settled '. $invoiceId . ': ' . $e->getMessage());
}
}

return false;
}

}
62 changes: 61 additions & 1 deletion src/Helper/GreenfieldApiWebhook.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
class GreenfieldApiWebhook {
public const WEBHOOK_EVENTS = [
'InvoiceReceivedPayment',
'InvoicePaymentSettled',
'InvoiceProcessing',
'InvoiceExpired',
'InvoiceSettled',
Expand Down Expand Up @@ -64,7 +65,66 @@ public static function registerWebhook(string $apiUrl, $apiKey, $storeId): ?Webh

return $webhook;
} catch (\Throwable $e) {
Logger::debug('Error fetching existing Webhook from BTCPay Server.');
Logger::debug('Error creating a new webhook on BTCPay Server instance: ' . $e->getMessage());
}

return null;
}

/**
* Update an existing webhook on BTCPay Server.
*/
public static function updateWebhook(
string $webhookId,
string $webhookUrl,
string $secret,
bool $enabled,
bool $automaticRedelivery,
?array $events
): ?WebhookResult {

if ($config = GreenfieldApiHelper::getConfig()) {
try {
$whClient = new Webhook( $config['url'], $config['api_key'] );
$webhook = $whClient->updateWebhook(
$config['store_id'],
$webhookUrl,
$webhookId,
$events ?? self::WEBHOOK_EVENTS,
$enabled,
$automaticRedelivery,
$secret
);

return $webhook;
} catch (\Throwable $e) {
Logger::debug('Error updating existing Webhook from BTCPay Server: ' . $e->getMessage());
return null;
}
} else {
Logger::debug('Plugin not configured, aborting updating webhook.');
}

return null;
}

/**
* Load existing webhook data from BTCPay Server, defaults to locally stored webhook.
*/
public static function getWebhook(?string $webhookId): ?WebhookResult {
$existingWebhook = get_option('btcpay_gf_webhook');
$config = GreenfieldApiHelper::getConfig();

try {
$whClient = new Webhook( $config['url'], $config['api_key'] );
$webhook = $whClient->getWebhook(
$config['store_id'],
$webhookId ?? $existingWebhook['id'],
);

return $webhook;
} catch (\Throwable $e) {
Logger::debug('Error fetching existing Webhook from BTCPay Server: ' . $e->getMessage());
}

return null;
Expand Down
31 changes: 17 additions & 14 deletions src/Helper/OrderStates.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,29 +15,32 @@ class OrderStates {
const INVALID = 'Invalid';
const EXPIRED = 'Expired';
const EXPIRED_PAID_PARTIAL = 'ExpiredPaidPartial';
const EXPIRED_PAID_LATE = 'ExpiredPaidLate';
const IGNORE = 'BTCPAY_IGNORE';

public function getDefaultOrderStateMappings(): array {
return [
self::NEW => 'wc-pending',
self::PROCESSING => 'wc-on-hold',
self::SETTLED => self::IGNORE,
self::SETTLED_PAID_OVER => 'wc-processing',
self::INVALID => 'wc-failed',
self::EXPIRED => 'wc-cancelled',
self::EXPIRED_PAID_PARTIAL => 'wc-failed'
self::NEW => 'wc-pending',
self::PROCESSING => 'wc-on-hold',
self::SETTLED => self::IGNORE,
self::SETTLED_PAID_OVER => 'wc-processing',
self::INVALID => 'wc-failed',
self::EXPIRED => 'wc-cancelled',
self::EXPIRED_PAID_PARTIAL => 'wc-failed',
self::EXPIRED_PAID_LATE => 'wc-processing'
];
}

public function getOrderStateLabels(): array {
return [
self::NEW => _x('New', 'global_settings', 'btcpay-greenfield-for-woocommerce'),
self::PROCESSING => _x('Paid', 'global_settings', 'btcpay-greenfield-for-woocommerce'),
self::SETTLED => _x('Settled', 'global_settings', 'btcpay-greenfield-for-woocommerce'),
self::SETTLED_PAID_OVER => _x('Settled (paid over)', 'global_settings', 'btcpay-greenfield-for-woocommerce'),
self::INVALID => _x('Invalid', 'global_settings', 'btcpay-greenfield-for-woocommerce'),
self::EXPIRED => _x('Expired', 'global_settings', 'btcpay-greenfield-for-woocommerce'),
self::EXPIRED_PAID_PARTIAL => _x('Expired with partial payment', 'global_settings', 'btcpay-greenfield-for-woocommerce')
self::NEW => _x('New', 'global_settings', 'btcpay-greenfield-for-woocommerce'),
self::PROCESSING => _x('Paid', 'global_settings', 'btcpay-greenfield-for-woocommerce'),
self::SETTLED => _x('Settled', 'global_settings', 'btcpay-greenfield-for-woocommerce'),
self::SETTLED_PAID_OVER => _x('Settled (paid over)', 'global_settings', 'btcpay-greenfield-for-woocommerce'),
self::INVALID => _x('Invalid', 'global_settings', 'btcpay-greenfield-for-woocommerce'),
self::EXPIRED => _x('Expired', 'global_settings', 'btcpay-greenfield-for-woocommerce'),
self::EXPIRED_PAID_PARTIAL => _x('Expired with partial payment', 'global_settings', 'btcpay-greenfield-for-woocommerce'),
self::EXPIRED_PAID_LATE => _x('Expired (paid late)', 'global_settings', 'btcpay-greenfield-for-woocommerce')
];
}

Expand Down
40 changes: 40 additions & 0 deletions src/Helper/UpdateManager.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

namespace BTCPayServer\WC\Helper;

use BTCPayServer\WC\Admin\Notice;

class UpdateManager {

private static $updates = [
'1.0.3' => 'update-1.0.3.php'
];

/**
* Runs updates if available or just updates the stored version.
*/
public static function processUpdates() {

// Check stored version to see if update is needed, will only run once.
$runningVersion = get_option( BTCPAYSERVER_VERSION_KEY, '1.0.2' );

if ( version_compare( $runningVersion, BTCPAYSERVER_VERSION, '<' ) ) {

// Run update scripts if there are any.
foreach ( self::$updates as $updateVersion => $filename ) {
if ( version_compare( $runningVersion, $updateVersion, '<' ) ) {
$file = BTCPAYSERVER_PLUGIN_FILE_PATH . 'updates/' . $filename;
if ( file_exists( $file ) ) {
include $file;
}
$runningVersion = $updateVersion;
update_option( BTCPAYSERVER_VERSION_KEY, $updateVersion );
Notice::addNotice('success', 'BTCPay Server: successfully ran updates to version ' . $runningVersion, true);
}
}

update_option( BTCPAYSERVER_VERSION_KEY, BTCPAYSERVER_VERSION );
}
}

}
Loading

0 comments on commit 1d2cbb2

Please sign in to comment.