Commit 4307da81 authored by Michael Iseard's avatar Michael Iseard
Browse files

Improvements to donation form and fix recurring payment webhook

parent 0fbbd169
......@@ -2,6 +2,8 @@
namespace Kudos;
use Mollie\Api\Resources\Subscription;
use Mollie_API_Object_Customer_Subscription;
use WP_REST_Server;
/**
......@@ -147,6 +149,19 @@ class Kudos_Admin {
);
add_action( "admin_print_scripts-{$donors_page_hook_suffix}", [$this, 'kudos_subscriptions_page_assets'] );
// Add debug menu
if(WP_DEBUG) {
add_submenu_page(
'kudos-settings',
'Kudos Debug',
'Kudos Debug',
'manage_options',
'kudos-debug',
[$this, 'kudos_debug']
);
}
}
/**
......@@ -355,6 +370,98 @@ class Kudos_Admin {
wp_die();
}
/**
* Debug page render
*
* @since 1.1.0
*/
public function kudos_debug() {
$kudos_donor = new Kudos_Donor();
$kudos_mollie = new Kudos_Mollie();
echo '<div class="wrap">';
$donors = $kudos_donor->get_all();
if($donors) {
foreach($donors as $donor) {
$subscriptions = $kudos_mollie->get_subscriptions($donor->customer_id);
if(!count($subscriptions)) {
continue;
}
echo "<h3><strong>" . $donor->email . "</strong> <span>(" . $donor->customer_id . ")</span></h3>";
echo "<form action=". admin_url( 'admin-post.php' ) ." method='post'>";
wp_nonce_field('cancel_subscription', '_wpnonce');
echo "<input type='hidden' name='action' value='cancel_subscription'>";
echo "<input type='hidden' name='customerId' value='". $donor->customer_id ."'>";
/** @var Subscription $subscription */
foreach ($subscriptions as $subscription) {
echo "<table class='widefat'>";
echo "<tbody>";
echo "<tr>";
echo "<td class='row-title'>id</td>";
echo "<td>" . $subscription->id . "</td> ";
echo "</tr>";
echo "<tr class='alternate'>";
echo "<td class='row-title'>status</td>";
echo "<td>$subscription->status" . ($subscription->status !== 'canceled' ? " <button name='subscriptionId' type='submit' value='$subscription->id'>Cancel</button>" : "") . "</td>";
echo "</tr>";
echo "<tr>";
echo "<td class='row-title'>amount</td>";
echo "<td>" . $subscription->amount->value . "</td>";
echo "</tr> ";
echo "<tr class='alternate'>";
echo "<td class='row-title'>interval</td>";
echo "<td>" . $subscription->interval . "</td>";
echo "</tr> ";
echo "<tr>";
echo "<td class='row-title'>times</td>" ;
echo "<td>". $subscription->times . "</td>";
echo "</tr>";
echo "<tr class='alternate'>";
echo "<td class='row-title'>next payment</td>";
echo "<td>". ($subscription->nextPaymentDate ?? 'n/a') . "</td>";
echo "</tr>";
echo "<tr>";
echo "<td class='row-title'>webhookUrl</td>" ;
echo "<td>". $subscription->webhookUrl . "</td>";
echo "</tr>";
echo "</tbody>";
echo "</table>";
echo "<br class='clear'>";
}
echo "</form>";
}
}
echo '</div>';
}
public function cancel_subscription() {
if(!wp_verify_nonce($_REQUEST['_wpnonce'], 'cancel_subscription')) {
echo "Nope!";
die;
}
$kudos_mollie = new Kudos_Mollie();
$subscription = $kudos_mollie->cancel_subscription($_REQUEST['subscriptionId'], $_REQUEST['customerId']);
if($subscription) {
echo "Subscription canceled";
}
}
/**
* Register the plugin settings
*
......
......@@ -138,6 +138,7 @@ class Subscriptions_Table extends WP_List_Table {
return [
'subscription_created'=>__('Date', 'kudos-donations'),
'frequency'=>__('Frequency', 'kudos-donations'),
'years' => __('Years', 'kudos-donations'),
'name' => __('Name', 'kudos-donations'),
'email'=>__('E-mail', 'kudos-donations'),
'value'=>__('Amount', 'kudos-donations'),
......@@ -166,13 +167,19 @@ class Subscriptions_Table extends WP_List_Table {
$count = count($this->count_records('frequency', '12 months'));
$yearly_url = add_query_arg('frequency','12 months');
$class = ($current == '12 months' ? ' class="current"' :'');
$views['test'] = "<a href='{$yearly_url}' {$class} >". __('Yearly', 'kudos-donations') ." ($count)</a>";
$views['yearl'] = "<a href='{$yearly_url}' {$class} >". __('Yearly', 'kudos-donations') ." ($count)</a>";
//Quarterly link
$count = count($this->count_records('frequency', '3 months'));
$yearly_url = add_query_arg('frequency','3 months');
$class = ($current == '3 months' ? ' class="current"' :'');
$views['quarterly'] = "<a href='{$yearly_url}' {$class} >". __('Quarterly', 'kudos-donations') ." ($count)</a>";
//Monthly link
$count = count($this->count_records('frequency', '1 month'));
$monthly_url = add_query_arg('frequency','1 month');
$class = ($current == '1 month' ? ' class="current"' :'');
$views['live'] = "<a href='{$monthly_url}' {$class} >". __('Monthly', 'kudos-donations') ." ($count)</a>";
$views['monthly'] = "<a href='{$monthly_url}' {$class} >". __('Monthly', 'kudos-donations') ." ($count)</a>";
return $views;
}
......@@ -206,6 +213,10 @@ class Subscriptions_Table extends WP_List_Table {
'value' => [
'value',
false
],
'status' => [
'status',
false
]
];
}
......@@ -244,14 +255,23 @@ class Subscriptions_Table extends WP_List_Table {
return $title . $this->row_actions( $actions );
}
/**
* @since 1.1.0
* @param $item
* @return string|void
*/
function column_frequency($item) {
switch ($item['frequency']) {
case '12 months':
return __('Yearly', 'kudos-donations');
case '1 month':
return __('Monthly', 'kudos-donations');
}
return false;
return get_frequency_name($item['frequency']);
}
/**
* @since 1.1.0
* @param $item
* @return string|void
*/
function column_years($item) {
return ($item['years'] > 0 ? $item['years'] : __('Continuous', 'kudos-donations'));
}
/**
......@@ -304,9 +324,7 @@ class Subscriptions_Table extends WP_List_Table {
$status = $item['status'];
}
$frequency = $item['frequency'] === 'test' ? ' ('. $item['frequency'] .')' : '';
return $status . ' ' . $frequency;
return $status;
}
/**
......@@ -329,25 +347,9 @@ class Subscriptions_Table extends WP_List_Table {
*/
public static function cancel_subscription( $subscription_id ) {
$mollie = new Kudos_Mollie();
$kudos_subscription = new Kudos_Subscription();
$customer = $kudos_subscription->get_by(['subscription_id' => $subscription_id]);
$kudos_mollie = new Kudos_Mollie();
$kudos_mollie->cancel_subscription($subscription_id);
if(empty($customer)) {
return;
}
$customer_id = $customer->customer_id;
$subscription = $mollie->cancel_subscription($customer_id, $subscription_id);
if($subscription) {
$kudos_subscription->update([
'status' => 'cancelled'
], [
'subscription_id' => $subscription_id
]);
}
}
/**
......
......@@ -358,7 +358,7 @@ class Transactions_Table extends WP_List_Table {
}
function column_type($item) {
return sequence_type($item['sequence_type']);
return get_sequence_type($item['sequence_type']);
}
/**
......
......@@ -59,7 +59,8 @@ class Kudos_Activator {
sequence_type VARCHAR(255) NOT NULL,
customer_id VARCHAR(255),
order_id VARCHAR(255) NOT NULL,
transaction_id VARCHAR(255),
transaction_id VARCHAR(255),
subscription_id VARCHAR(255),
PRIMARY KEY (id)
) $charset_collate;";
......@@ -107,12 +108,12 @@ class Kudos_Activator {
$table_name = Kudos_Subscription::getTableName(); //get the database table prefix to create my new table
$sql = "CREATE TABLE $table_name (
id mediumint(9) NOT NULL AUTO_INCREMENT,
id MEDIUMINT(9) NOT NULL AUTO_INCREMENT,
subscription_created datetime DEFAULT '0000-00-00 00:00:00' NOT NULL,
value DECIMAL(7,2) NOT NULL,
currency VARCHAR(255),
frequency VARCHAR(255) NOT NULL,
times VARCHAR(255) NOT NULL,
years MEDIUMINT(2) NOT NULL,
customer_id VARCHAR(255),
transaction_id VARCHAR(255),
k_subscription_id VARCHAR(255),
......
......@@ -83,7 +83,7 @@ class Kudos_Invoice
$invoiceArray = [
'logo' => 'data:image/svg+xml;base64,'. base64_encode('<svg xmlns="http://www.w3.org/2000/svg" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2" clip-rule="evenodd" viewBox="0 0 555 449"><defs/><path fill="#2ec4b6" d="M0-.003h130.458v448.355H.001z"/><path fill="#ff9f1c" d="M489.887 224.178c78.407 47.195 78.407 141.59 39.201 188.784-39.2 47.194-117.612 47.194-196.019 0-58.809-33.04-117.612-117.992-156.818-188.784 39.206-70.793 98.01-155.744 156.818-188.781 78.407-47.196 156.818-47.196 196.02 0 39.205 47.195 39.205 141.587-39.202 188.781z"/></svg>'),
'date' => $transaction->transaction_created,
'description' => sequence_type($transaction->sequence_type),
'description' => get_sequence_type($transaction->sequence_type),
'amount' => (!empty($transaction->currency) ? html_entity_decode(get_currency_symbol($transaction->currency)) : '') . number_format_i18n($transaction->value, 2),
'order_id' => $order_id,
'company_name' => get_option('_kudos_invoice_company_name'),
......
......@@ -4,9 +4,11 @@ namespace Kudos;
use Mollie\Api\Exceptions\ApiException;
use Mollie\Api\MollieApiClient;
use Mollie\Api\Resources\BaseCollection;
use Mollie\Api\Resources\Customer;
use Mollie\Api\Resources\Mandate;
use Mollie\Api\Resources\MandateCollection;
use Mollie\Api\Resources\Payment;
use Mollie\Api\Resources\Subscription;
use WP_Error;
use WP_HTTP_Response;
use WP_REST_Request;
......@@ -39,6 +41,10 @@ class Kudos_Mollie
* @var mixed
*/
private $apiKey;
/**
* @var Kudos_Subscription
*/
private $subscription;
/**
......@@ -49,6 +55,7 @@ class Kudos_Mollie
public function __construct() {
$this->logger = new Kudos_Logger();
$this->transaction = new Kudos_Transaction();
$this->subscription = new Kudos_Subscription();
$this->invoice = new Kudos_Invoice();
$this->mollieApi = new MollieApiClient();
$this->apiMode = get_option('_kudos_mollie_api_mode');
......@@ -111,7 +118,7 @@ class Kudos_Mollie
*
* @param $value
* @param string $interval
* @param string $times
* @param string $years
* @param string $redirectUrl
* @param string|null $name
* @param string|null $email
......@@ -120,7 +127,7 @@ class Kudos_Mollie
* @return bool|object
* @since 1.0.0
*/
public function create_payment($value, $interval, $times, $redirectUrl, $name=null, $email=null, $customerId=null) {
public function create_payment($value, $interval, $years, $redirectUrl, $name=null, $email=null, $customerId=null) {
$mollieApi = $this->mollieApi;
$order_id = 'kdo_'.time();
......@@ -134,24 +141,8 @@ class Kudos_Mollie
}
// Set payment frequency
switch ($interval) {
case "12 months":
$frequency_text = __('Yearly', 'kudos-donations');
$sequenceType = 'first';
break;
case "3 months":
$frequency_text = __('Quarterly', 'kudos-donations');
$sequenceType = 'first';
break;
case "1 month":
$frequency_text = __('Monthly', 'kudos-donations');
$sequenceType = 'first';
break;
case "oneoff":
default:
$frequency_text = __('One off', 'kudos-donations');
$sequenceType = 'oneoff';
}
$frequency_text = get_frequency_name($interval);
$sequenceType = ($interval === 'oneoff' ? 'oneoff' : 'first');
$paymentArray = [
"amount" => [
......@@ -161,13 +152,13 @@ class Kudos_Mollie
"redirectUrl" => $redirectUrl,
// "webhookUrl" => rest_url('kudos/v1/mollie/payment/webhook'),
"sequenceType" => $sequenceType,
"webhookUrl" => 'http://194e94884213.ngrok.io/wp-json/kudos/v1/mollie/payment/webhook',
"webhookUrl" => 'https://2744ce05c57f.ngrok.io/wp-json/kudos/v1/mollie/payment/webhook',
/* translators: %s: The order id */
"description" => sprintf(__("Kudos Donation (%s) - %s", 'kudos-donations'), $frequency_text, $order_id),
'metadata' => [
'order_id' => $order_id,
'interval' => $interval,
'times' => $times,
'years' => $years,
'email' => $email,
'name' => $name
]
......@@ -182,7 +173,14 @@ class Kudos_Mollie
$payment = $mollieApi->payments->create($paymentArray);
$transaction = $this->transaction;
$transaction->insert_transaction($order_id, $customerId, $value, $currency, $payment->status, $payment->sequenceType);
$transaction->insert_transaction([
'order_id' => $order_id,
'customer_id' => $customerId,
'value' => $value,
'currency' => $currency,
'status' => $payment->status,
'sequence_type' => $payment->sequenceType
]);
return $payment;
......@@ -193,17 +191,41 @@ class Kudos_Mollie
}
/**
* Returns all subscriptions for customer
*
* @since 1.1.0
* @param $customerId
*
* @return BaseCollection|bool
*/
public function get_subscriptions($customerId) {
$mollieApi = $this->mollieApi;
try {
/** @var Customer $customer */
$customer = $mollieApi->customers->get($customerId);
return $customer->subscriptions();
} catch (ApiException $e) {
$this->logger->log($e->getMessage(), 'CRITICAL');
return false;
}
}
/**
* Create a subscription
*
* @param object $transaction
* @param $mandateId
* @param $interval
* @param null $times
* @param $years
*
* @return bool|object
* @since 1.1.0
*/
public function create_subscription($transaction, $interval, $times=null) {
public function create_subscription($transaction, $mandateId, $interval, $years) {
$mollieApi = $this->mollieApi;
$customer_id = $transaction->customer_id;
......@@ -217,28 +239,36 @@ class Kudos_Mollie
"value" => $value,
"currency" => $currency
],
"mandateId" => $mandateId,
"interval" => $interval,
"startDate" => $startDate,
// "startDate" => $startDate, // Disable for test mode
"description" => sprintf(__('Kudos Subscription (%s) - %s', 'kudos-donations'), $interval, $k_subscription_id),
// "webhookUrl" => rest_url('kudos/v1/mollie/subscription/webhook'),
"webhookUrl" => 'http://194e94884213.ngrok.io/wp-json/kudos/v1/mollie/subscription/webhook',
"webhookUrl" => 'https://2744ce05c57f.ngrok.io/wp-json/kudos/v1/mollie/payment/webhook',
"metadata" => [
"subscription_id" => $k_subscription_id
]
];
if($times) {
$subscriptionArray["times"] = $times;
if($years && $years > 0) {
$subscriptionArray["times"] = get_times_from_years($years, $interval);
}
try {
/** @var Customer $customer */
$customer = $mollieApi->customers->get($customer_id);
$mandate = $mollieApi->mandates->getFor($customer, $mandateId);
if(!$mandate->status === 'pending' || !$mandate->status === 'valid') {
$this->logger->log('Cannot create subscription as customer has no valid mandates.', 'CRITICAL', [$customer_id]);
return false;
}
$subscription = $customer->createSubscription($subscriptionArray);
if($subscription) {
$kudos_subscription = new Kudos_Subscription();
$kudos_subscription->insert_subscription($transaction->transaction_id, $customer_id, $interval, $times, $value, $currency, $k_subscription_id, $subscription->id, $subscription->status);
$kudos_subscription->insert_subscription($transaction->transaction_id, $customer_id, $interval, $years, $value, $currency, $k_subscription_id, $subscription->id, $subscription->status);
return $subscription;
}
......@@ -278,13 +308,42 @@ class Kudos_Mollie
}
public function cancel_subscription($customerId, $subscriptionId) {
/**
* Cancel the specified subscription
*
* @param $subscriptionId
* @param null|string $customerId
*
* @return bool
*/
public function cancel_subscription($subscriptionId, $customerId=null) {
$mollieApi = $this->mollieApi;
if(!$customerId) {
$kudos_subscription = new Kudos_Subscription();
$customer = $kudos_subscription->get_by(['subscription_id' => $subscriptionId]);
if(empty($customer)) {
$this->logger->log("Could not find a customer for " . $subscriptionId, 'DEBUG', [$subscriptionId]);
return false;
}
$customerId = $customer->customer_id;
}
try {
$customer = $mollieApi->customers->get($customerId);
return $customer->cancelSubscription($subscriptionId);
$this->logger->log($subscriptionId . " cancelled.", 'DEBUG', [$customerId, $subscriptionId]);
$subscription = $customer->cancelSubscription($subscriptionId);
if($subscription) {
$kudos_subscription = $this->subscription;
$kudos_subscription->update([
'status' => 'cancelled'
], [
'subscription_id' => $subscription->id
]);
}
} catch (ApiException $e) {
$this->logger->log($e->getMessage(), 'CRITICAL', [$customerId, $subscriptionId]);
return false;
......@@ -310,17 +369,6 @@ class Kudos_Mollie
]
]
] );
// Subscription webhook
register_rest_route( 'kudos/v1', 'mollie/subscription/webhook', [
'methods' => 'POST',
'callback' => [$this, 'rest_api_mollie_webhook'],
'args' => [
'id' => [
'required' => true
]
]
] );
}
/**
......@@ -401,6 +449,7 @@ class Kudos_Mollie
$response->add_link( 'self', rest_url( $request->get_route() ) );
/** @var Payment $payment */
$payment = $this->get_payment($id);
if ( null === $payment ) {
......@@ -414,16 +463,45 @@ class Kudos_Mollie
return $response;
}
// Update payment.
$order_id = $payment->metadata->order_id;
$transaction_id = $payment->id;
$status = $payment->status;
$sequence_type = $payment->sequenceType;
$this->transaction->update_transaction($order_id, [
'status' => $status,
'transaction_id' => $transaction_id,
'method' => $payment->method
]);
$sequence_type = $payment->sequenceType;
$transaction_id = $payment->id;
/* translators: %s: Mollie */
$note = sprintf(__( 'Webhook requested by %s.', 'kudos-donations' ),'Mollie');
$this->logger->log($note, 'INFO', ['transaction_id' => $id, 'status' => $status, 'sequence_type' => $sequence_type]);
$kudos_transaction = $this->transaction;
if($sequence_type === 'recurring') {
// Insert payment
$order_id = $order_id = 'kdo_'.time();
$customer_id = $payment->customerId;
$amount = $payment->amount;
$kudos_transaction->insert_transaction([
'order_id' => $order_id,
'customer_id' => $customer_id,
'value' => $amount->value,
'currency' => $amount->currency,
'status' => $payment->status,
'sequence_type' => $sequence_type,
'transaction_id' => $payment->id,
'method' => $payment->method,
'subscription_id' => $payment->subscriptionId
]);
} else {
// Update payment.
$order_id = $payment->metadata->order_id;
$kudos_transaction->update_transaction($order_id, [
'status' => $status,
'transaction_id' => $transaction_id,
'method' => $payment->method
]);
}
// Send email receipt on success
$mailer = new Kudos_Mailer();
......@@ -442,15 +520,11 @@ class Kudos_Mollie
// Set up recurring payment if sequence is first
if($sequence_type === 'first') {
return $this->create_subscription($transaction, $payment->metadata->interval, $payment->metadata->times);
return $this->create_subscription($transaction, $payment->mandateId, $payment->metadata->interval, $payment->metadata->years);
}
}
}
/* translators: %s: Mollie */
$note = sprintf(__( 'Webhook requested by %s.', 'kudos-donations' ),'Mollie');
$this->logger->log($note, 'INFO', ['order_id' => $order_id, 'status' => $status, 'sequence_type' => $sequence_type]);
return $response;
}
}
\ No newline at end of file