763 lines
29 KiB
PHP
763 lines
29 KiB
PHP
<?php
|
|
|
|
/**
|
|
* Subscriptions API Controller
|
|
*
|
|
* REST API endpoints for subscription management.
|
|
*
|
|
* @package WooNooW\Api
|
|
*/
|
|
|
|
namespace WooNooW\Api;
|
|
|
|
if (!defined('ABSPATH')) exit;
|
|
|
|
use WP_REST_Request;
|
|
use WP_REST_Response;
|
|
use WP_Error;
|
|
use WooNooW\Core\ModuleRegistry;
|
|
use WooNooW\Modules\Subscription\SubscriptionManager;
|
|
use WooNooW\Modules\Subscription\GatewayCapabilities;
|
|
|
|
class SubscriptionsController
|
|
{
|
|
|
|
/**
|
|
* Register REST routes
|
|
*/
|
|
public static function register_routes()
|
|
{
|
|
// Check if module is enabled
|
|
if (!ModuleRegistry::is_enabled('subscription')) {
|
|
return;
|
|
}
|
|
|
|
// Admin routes
|
|
register_rest_route('woonoow/v1', '/subscriptions', [
|
|
'methods' => 'GET',
|
|
'callback' => [__CLASS__, 'get_subscriptions'],
|
|
'permission_callback' => function () {
|
|
return current_user_can('manage_woocommerce');
|
|
},
|
|
]);
|
|
|
|
// M3 — Bulk operations. Body shape: { action: 'cancel' | 'export_csv', ids: number[] }.
|
|
// For 'cancel' we return { ok: int, failed: [{id, error}] }. For 'export_csv' the
|
|
// response is a text/csv body with Content-Disposition.
|
|
register_rest_route('woonoow/v1', '/subscriptions/bulk', [
|
|
'methods' => 'POST',
|
|
'callback' => [__CLASS__, 'bulk_action'],
|
|
'permission_callback' => function () {
|
|
return current_user_can('manage_woocommerce');
|
|
},
|
|
]);
|
|
|
|
register_rest_route('woonoow/v1', '/subscriptions/(?P<id>\d+)', [
|
|
'methods' => 'GET',
|
|
'callback' => [__CLASS__, 'get_subscription'],
|
|
'permission_callback' => function () {
|
|
return current_user_can('manage_woocommerce');
|
|
},
|
|
]);
|
|
|
|
register_rest_route('woonoow/v1', '/subscriptions/(?P<id>\d+)', [
|
|
'methods' => 'PUT',
|
|
'callback' => [__CLASS__, 'update_subscription'],
|
|
'permission_callback' => function () {
|
|
return current_user_can('manage_woocommerce');
|
|
},
|
|
]);
|
|
|
|
register_rest_route('woonoow/v1', '/subscriptions/(?P<id>\d+)/cancel', [
|
|
'methods' => 'POST',
|
|
'callback' => [__CLASS__, 'cancel_subscription'],
|
|
'permission_callback' => function () {
|
|
return current_user_can('manage_woocommerce');
|
|
},
|
|
]);
|
|
|
|
register_rest_route('woonoow/v1', '/subscriptions/(?P<id>\d+)/renew', [
|
|
'methods' => 'POST',
|
|
'callback' => [__CLASS__, 'renew_subscription'],
|
|
'permission_callback' => function () {
|
|
return current_user_can('manage_woocommerce');
|
|
},
|
|
]);
|
|
|
|
register_rest_route('woonoow/v1', '/subscriptions/(?P<id>\d+)/pause', [
|
|
'methods' => 'POST',
|
|
'callback' => [__CLASS__, 'pause_subscription'],
|
|
'permission_callback' => function () {
|
|
return current_user_can('manage_woocommerce');
|
|
},
|
|
]);
|
|
|
|
register_rest_route('woonoow/v1', '/subscriptions/(?P<id>\d+)/resume', [
|
|
'methods' => 'POST',
|
|
'callback' => [__CLASS__, 'resume_subscription'],
|
|
'permission_callback' => function () {
|
|
return current_user_can('manage_woocommerce');
|
|
},
|
|
]);
|
|
|
|
// Customer routes
|
|
register_rest_route('woonoow/v1', '/account/subscriptions', [
|
|
'methods' => 'GET',
|
|
'callback' => [__CLASS__, 'get_customer_subscriptions'],
|
|
'permission_callback' => function () {
|
|
return is_user_logged_in();
|
|
},
|
|
]);
|
|
|
|
register_rest_route('woonoow/v1', '/account/subscriptions/(?P<id>\d+)', [
|
|
'methods' => 'GET',
|
|
'callback' => [__CLASS__, 'get_customer_subscription'],
|
|
'permission_callback' => function () {
|
|
return is_user_logged_in();
|
|
},
|
|
]);
|
|
|
|
register_rest_route('woonoow/v1', '/account/subscriptions/(?P<id>\d+)/cancel', [
|
|
'methods' => 'POST',
|
|
'callback' => [__CLASS__, 'customer_cancel'],
|
|
'permission_callback' => function () {
|
|
return is_user_logged_in();
|
|
},
|
|
]);
|
|
|
|
register_rest_route('woonoow/v1', '/account/subscriptions/(?P<id>\d+)/pause', [
|
|
'methods' => 'POST',
|
|
'callback' => [__CLASS__, 'customer_pause'],
|
|
'permission_callback' => function () {
|
|
return is_user_logged_in();
|
|
},
|
|
]);
|
|
|
|
register_rest_route('woonoow/v1', '/account/subscriptions/(?P<id>\d+)/resume', [
|
|
'methods' => 'POST',
|
|
'callback' => [__CLASS__, 'customer_resume'],
|
|
'permission_callback' => function () {
|
|
return is_user_logged_in();
|
|
},
|
|
]);
|
|
|
|
register_rest_route('woonoow/v1', '/account/subscriptions/(?P<id>\d+)/renew', [
|
|
'methods' => 'POST',
|
|
'callback' => [__CLASS__, 'customer_renew'],
|
|
'permission_callback' => function () {
|
|
return is_user_logged_in();
|
|
},
|
|
]);
|
|
|
|
// §9 — Gateway capability matrix (admin)
|
|
register_rest_route('woonoow/v1', '/subscriptions/gateway-capabilities', [
|
|
'methods' => 'GET',
|
|
'callback' => [__CLASS__, 'get_gateway_capabilities'],
|
|
'permission_callback' => function () {
|
|
return current_user_can('manage_woocommerce');
|
|
},
|
|
]);
|
|
|
|
register_rest_route('woonoow/v1', '/subscriptions/gateway-capabilities', [
|
|
'methods' => 'POST',
|
|
'callback' => [__CLASS__, 'update_gateway_capabilities'],
|
|
'permission_callback' => function () {
|
|
return current_user_can('manage_woocommerce');
|
|
},
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Get all subscriptions (admin)
|
|
*/
|
|
public static function get_subscriptions(WP_REST_Request $request)
|
|
{
|
|
$args = [
|
|
'status' => $request->get_param('status'),
|
|
'product_id' => $request->get_param('product_id'),
|
|
'user_id' => $request->get_param('user_id'),
|
|
'search' => $request->get_param('search'),
|
|
'limit' => $request->get_param('per_page') ?: 20,
|
|
'offset' => (($request->get_param('page') ?: 1) - 1) * ($request->get_param('per_page') ?: 20),
|
|
];
|
|
|
|
$subscriptions = SubscriptionManager::get_all($args);
|
|
$total = SubscriptionManager::count([
|
|
'status' => $args['status'],
|
|
'search' => $args['search'],
|
|
]);
|
|
|
|
// Enrich with product and user info
|
|
$enriched = [];
|
|
foreach ($subscriptions as $subscription) {
|
|
$enriched[] = self::enrich_subscription($subscription);
|
|
}
|
|
|
|
return new WP_REST_Response([
|
|
'subscriptions' => $enriched,
|
|
'total' => $total,
|
|
'page' => $request->get_param('page') ?: 1,
|
|
'per_page' => $args['limit'],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Get single subscription (admin)
|
|
*/
|
|
public static function get_subscription(WP_REST_Request $request)
|
|
{
|
|
$subscription = SubscriptionManager::get($request->get_param('id'));
|
|
|
|
if (!$subscription) {
|
|
return new WP_Error('not_found', __('Subscription not found', 'woonoow'), ['status' => 404]);
|
|
}
|
|
|
|
$enriched = self::enrich_subscription($subscription);
|
|
$enriched['orders'] = SubscriptionManager::get_orders($subscription->id);
|
|
|
|
return new WP_REST_Response($enriched);
|
|
}
|
|
|
|
/**
|
|
* Update subscription (admin)
|
|
*/
|
|
public static function update_subscription(WP_REST_Request $request)
|
|
{
|
|
global $wpdb;
|
|
|
|
$subscription = SubscriptionManager::get($request->get_param('id'));
|
|
|
|
if (!$subscription) {
|
|
return new WP_Error('not_found', __('Subscription not found', 'woonoow'), ['status' => 404]);
|
|
}
|
|
|
|
$data = $request->get_json_params();
|
|
$allowed_fields = ['status', 'next_payment_date', 'end_date', 'billing_period', 'billing_interval'];
|
|
|
|
$update_data = [];
|
|
$format = [];
|
|
|
|
foreach ($allowed_fields as $field) {
|
|
if (isset($data[$field])) {
|
|
$update_data[$field] = $data[$field];
|
|
$format[] = is_numeric($data[$field]) ? '%d' : '%s';
|
|
}
|
|
}
|
|
|
|
if (empty($update_data)) {
|
|
return new WP_Error('no_data', __('No valid fields to update', 'woonoow'), ['status' => 400]);
|
|
}
|
|
|
|
$table = $wpdb->prefix . 'woonoow_subscriptions';
|
|
$updated = $wpdb->update($table, $update_data, ['id' => $subscription->id], $format, ['%d']);
|
|
|
|
if ($updated === false) {
|
|
return new WP_Error('update_failed', __('Failed to update subscription', 'woonoow'), ['status' => 500]);
|
|
}
|
|
|
|
return new WP_REST_Response(['success' => true]);
|
|
}
|
|
|
|
/**
|
|
* Cancel subscription (admin)
|
|
*/
|
|
public static function cancel_subscription(WP_REST_Request $request)
|
|
{
|
|
$data = $request->get_json_params();
|
|
$reason = $data['reason'] ?? 'Cancelled by admin';
|
|
|
|
$result = SubscriptionManager::cancel($request->get_param('id'), $reason);
|
|
|
|
if (!$result) {
|
|
return new WP_Error('cancel_failed', __('Failed to cancel subscription', 'woonoow'), ['status' => 500]);
|
|
}
|
|
|
|
return new WP_REST_Response(['success' => true]);
|
|
}
|
|
|
|
/**
|
|
* Renew subscription (admin - force immediate renewal)
|
|
*
|
|
* M2 — supports `?charge_now=true` to bypass the per-gateway capability
|
|
* gate. With the flag, the auto-debit path is attempted even on gateways
|
|
* that are normally manual-only; on failure the order is marked failed
|
|
* (no manual fallback) so the admin can see the charge couldn't go
|
|
* through.
|
|
*/
|
|
public static function renew_subscription(WP_REST_Request $request)
|
|
{
|
|
$charge_now = filter_var($request->get_param('charge_now'), FILTER_VALIDATE_BOOLEAN);
|
|
$result = SubscriptionManager::renew($request->get_param('id'), $charge_now);
|
|
|
|
if (!$result) {
|
|
return new WP_Error('renew_failed', __('Failed to process renewal', 'woonoow'), ['status' => 500]);
|
|
}
|
|
|
|
return new WP_REST_Response([
|
|
'success' => true,
|
|
'order_id' => $result['order_id'],
|
|
'status' => $result['status'] ?? 'complete',
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Pause subscription (admin)
|
|
*/
|
|
public static function pause_subscription(WP_REST_Request $request)
|
|
{
|
|
$result = SubscriptionManager::pause($request->get_param('id'));
|
|
|
|
if (!$result) {
|
|
return new WP_Error('pause_failed', __('Failed to pause subscription', 'woonoow'), ['status' => 500]);
|
|
}
|
|
|
|
return new WP_REST_Response(['success' => true]);
|
|
}
|
|
|
|
/**
|
|
* Resume subscription (admin)
|
|
*/
|
|
public static function resume_subscription(WP_REST_Request $request)
|
|
{
|
|
$result = SubscriptionManager::resume($request->get_param('id'));
|
|
|
|
if (!$result) {
|
|
return new WP_Error('resume_failed', __('Failed to resume subscription', 'woonoow'), ['status' => 500]);
|
|
}
|
|
|
|
return new WP_REST_Response(['success' => true]);
|
|
}
|
|
|
|
/**
|
|
* M3 — Bulk action endpoint.
|
|
*
|
|
* Body: { action: 'cancel' | 'export_csv', ids: number[] }
|
|
*
|
|
* - 'cancel' returns JSON `{ ok: int, failed: [{id, error}] }`. Per-subscription
|
|
* errors do not abort the batch — the admin sees the per-row outcome.
|
|
* - 'export_csv' streams a CSV download. We don't use WP_REST_Response's
|
|
* download flag because we want to set a custom filename.
|
|
*
|
|
* Hard cap of 500 ids per call to avoid runaway batches. A real implementation
|
|
* would dispatch this via Action Scheduler; for now we run inline because
|
|
* 500 cancels is <1s of DB writes.
|
|
*/
|
|
public static function bulk_action(WP_REST_Request $request)
|
|
{
|
|
$action = (string) $request->get_param('action');
|
|
$ids = $request->get_param('ids');
|
|
|
|
if (!is_array($ids) || empty($ids)) {
|
|
return new WP_Error('bad_request', __('ids must be a non-empty array', 'woonoow'), ['status' => 400]);
|
|
}
|
|
|
|
// Coerce to ints, drop non-numeric junk, dedupe.
|
|
$ids = array_values(array_unique(array_filter(array_map('intval', $ids), function ($i) { return $i > 0; })));
|
|
if (empty($ids)) {
|
|
return new WP_Error('bad_request', __('ids must contain at least one positive integer', 'woonoow'), ['status' => 400]);
|
|
}
|
|
if (count($ids) > 500) {
|
|
return new WP_Error('batch_too_large', __('Maximum 500 ids per bulk request', 'woonoow'), ['status' => 400]);
|
|
}
|
|
|
|
if ($action === 'cancel') {
|
|
$ok = 0;
|
|
$failed = [];
|
|
foreach ($ids as $id) {
|
|
$result = SubscriptionManager::cancel($id);
|
|
if ($result === false || $result === null) {
|
|
$failed[] = ['id' => $id, 'error' => __('Cancel returned false', 'woonoow')];
|
|
} else {
|
|
$ok++;
|
|
}
|
|
}
|
|
return new WP_REST_Response(['ok' => $ok, 'failed' => $failed]);
|
|
}
|
|
|
|
if ($action === 'export_csv') {
|
|
$rows = [];
|
|
foreach ($ids as $id) {
|
|
$sub = SubscriptionManager::get($id);
|
|
if (!$sub) {
|
|
$rows[] = [
|
|
'id' => $id, 'status' => 'missing', 'user_name' => '', 'user_email' => '',
|
|
'product_name' => '', 'billing_period' => '', 'billing_interval' => '',
|
|
'recurring_amount' => '', 'next_payment_date' => '', 'start_date' => '',
|
|
'end_date' => '', 'payment_method' => '',
|
|
];
|
|
continue;
|
|
}
|
|
$rows[] = [
|
|
'id' => (int) $sub->id,
|
|
'status' => (string) $sub->status,
|
|
'user_name' => (string) ($sub->user_name ?? ''),
|
|
'user_email' => (string) ($sub->user_email ?? ''),
|
|
'product_name' => (string) ($sub->product_name ?? ''),
|
|
'billing_period' => (string) $sub->billing_period,
|
|
'billing_interval' => (int) $sub->billing_interval,
|
|
'recurring_amount' => (string) $sub->recurring_amount,
|
|
'next_payment_date' => (string) ($sub->next_payment_date ?? ''),
|
|
'start_date' => (string) $sub->start_date,
|
|
'end_date' => (string) ($sub->end_date ?? ''),
|
|
'payment_method' => (string) ($sub->payment_method ?? ''),
|
|
];
|
|
}
|
|
|
|
$filename = 'woonoow-subscriptions-' . gmdate('Ymd-His') . '.csv';
|
|
header('Content-Type: text/csv; charset=utf-8');
|
|
header('Content-Disposition: attachment; filename="' . $filename . '"');
|
|
$out = fopen('php://output', 'w');
|
|
if (!empty($rows)) {
|
|
fputcsv($out, array_keys($rows[0]));
|
|
foreach ($rows as $r) {
|
|
fputcsv($out, $r);
|
|
}
|
|
}
|
|
fclose($out);
|
|
exit;
|
|
}
|
|
|
|
return new WP_Error('unknown_action', __('Unknown bulk action', 'woonoow'), ['status' => 400]);
|
|
}
|
|
|
|
/**
|
|
* Get customer's subscriptions
|
|
*/
|
|
public static function get_customer_subscriptions(WP_REST_Request $request)
|
|
{
|
|
$user_id = get_current_user_id();
|
|
$subscriptions = SubscriptionManager::get_by_user($user_id, [
|
|
'status' => $request->get_param('status'),
|
|
'limit' => $request->get_param('per_page') ?: 20,
|
|
'offset' => (($request->get_param('page') ?: 1) - 1) * ($request->get_param('per_page') ?: 20),
|
|
]);
|
|
|
|
// Enrich each subscription
|
|
$enriched = [];
|
|
foreach ($subscriptions as $subscription) {
|
|
$enriched[] = self::enrich_subscription($subscription);
|
|
}
|
|
|
|
return new WP_REST_Response($enriched);
|
|
}
|
|
|
|
/**
|
|
* Get customer's subscription detail
|
|
*/
|
|
public static function get_customer_subscription(WP_REST_Request $request)
|
|
{
|
|
$user_id = get_current_user_id();
|
|
$subscription = SubscriptionManager::get($request->get_param('id'));
|
|
|
|
if (!$subscription || $subscription->user_id != $user_id) {
|
|
return new WP_Error('not_found', __('Subscription not found', 'woonoow'), ['status' => 404]);
|
|
}
|
|
|
|
$enriched = self::enrich_subscription($subscription);
|
|
$enriched['orders'] = SubscriptionManager::get_orders($subscription->id);
|
|
|
|
return new WP_REST_Response($enriched);
|
|
}
|
|
|
|
/**
|
|
* Customer cancel their own subscription
|
|
*/
|
|
public static function customer_cancel(WP_REST_Request $request)
|
|
{
|
|
// Check if customer cancellation is allowed
|
|
$settings = ModuleRegistry::get_settings('subscription');
|
|
if (empty($settings['allow_customer_cancel'])) {
|
|
return new WP_Error('not_allowed', __('Customer cancellation is not allowed', 'woonoow'), ['status' => 403]);
|
|
}
|
|
|
|
$user_id = get_current_user_id();
|
|
$subscription = SubscriptionManager::get($request->get_param('id'));
|
|
|
|
if (!$subscription || $subscription->user_id != $user_id) {
|
|
return new WP_Error('not_found', __('Subscription not found', 'woonoow'), ['status' => 404]);
|
|
}
|
|
|
|
$data = $request->get_json_params();
|
|
$reason = $data['reason'] ?? 'Cancelled by customer';
|
|
|
|
$result = SubscriptionManager::cancel($subscription->id, $reason);
|
|
|
|
if (!$result) {
|
|
return new WP_Error('cancel_failed', __('Failed to cancel subscription', 'woonoow'), ['status' => 500]);
|
|
}
|
|
|
|
return new WP_REST_Response(['success' => true]);
|
|
}
|
|
|
|
/**
|
|
* Customer pause their own subscription
|
|
*/
|
|
public static function customer_pause(WP_REST_Request $request)
|
|
{
|
|
// Check if customer pause is allowed
|
|
$settings = ModuleRegistry::get_settings('subscription');
|
|
if (empty($settings['allow_customer_pause'])) {
|
|
return new WP_Error('not_allowed', __('Customer pause is not allowed', 'woonoow'), ['status' => 403]);
|
|
}
|
|
|
|
$user_id = get_current_user_id();
|
|
$subscription = SubscriptionManager::get($request->get_param('id'));
|
|
|
|
if (!$subscription || $subscription->user_id != $user_id) {
|
|
return new WP_Error('not_found', __('Subscription not found', 'woonoow'), ['status' => 404]);
|
|
}
|
|
|
|
$result = SubscriptionManager::pause($subscription->id);
|
|
|
|
if (!$result) {
|
|
return new WP_Error('pause_failed', __('Failed to pause subscription. Maximum pauses may have been reached.', 'woonoow'), ['status' => 500]);
|
|
}
|
|
|
|
return new WP_REST_Response(['success' => true]);
|
|
}
|
|
|
|
/**
|
|
* Customer resume their own subscription
|
|
*/
|
|
public static function customer_resume(WP_REST_Request $request)
|
|
{
|
|
$user_id = get_current_user_id();
|
|
$subscription = SubscriptionManager::get($request->get_param('id'));
|
|
|
|
if (!$subscription || $subscription->user_id != $user_id) {
|
|
return new WP_Error('not_found', __('Subscription not found', 'woonoow'), ['status' => 404]);
|
|
}
|
|
|
|
$result = SubscriptionManager::resume($subscription->id);
|
|
|
|
if (!$result) {
|
|
return new WP_Error('resume_failed', __('Failed to resume subscription', 'woonoow'), ['status' => 500]);
|
|
}
|
|
|
|
return new WP_REST_Response(['success' => true]);
|
|
}
|
|
|
|
/**
|
|
* Customer renew their own subscription (Early Renewal)
|
|
*/
|
|
public static function customer_renew(WP_REST_Request $request)
|
|
{
|
|
$user_id = get_current_user_id();
|
|
$subscription = SubscriptionManager::get($request->get_param('id'));
|
|
|
|
if (!$subscription || $subscription->user_id != $user_id) {
|
|
return new WP_Error('not_found', __('Subscription not found', 'woonoow'), ['status' => 404]);
|
|
}
|
|
|
|
// Check if subscription is active (for early renewal) or on-hold with no pending payment
|
|
if ($subscription->status !== 'active' && $subscription->status !== 'on-hold') {
|
|
return new WP_Error('not_allowed', __('Only active subscriptions can be renewed early', 'woonoow'), ['status' => 403]);
|
|
}
|
|
|
|
// Trigger renewal
|
|
$result = SubscriptionManager::renew($subscription->id);
|
|
|
|
// SubscriptionManager::renew returns array (success) or false (failed)
|
|
if (!$result) {
|
|
return new WP_Error('renew_failed', __('Failed to create renewal order', 'woonoow'), ['status' => 500]);
|
|
}
|
|
|
|
return new WP_REST_Response([
|
|
'success' => true,
|
|
'order_id' => $result['order_id'],
|
|
'status' => $result['status']
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Enrich subscription with product and user info
|
|
*/
|
|
private static function enrich_subscription($subscription)
|
|
{
|
|
$enriched = (array) $subscription;
|
|
|
|
// Add product info
|
|
$product_id = $subscription->variation_id ?: $subscription->product_id;
|
|
$product = wc_get_product($product_id);
|
|
$enriched['product_name'] = $product ? $product->get_name() : __('Unknown Product', 'woonoow');
|
|
$enriched['product_image'] = $product ? wp_get_attachment_url($product->get_image_id()) : '';
|
|
|
|
// Add user info
|
|
$user = get_userdata($subscription->user_id);
|
|
$enriched['user_email'] = $user ? $user->user_email : '';
|
|
$enriched['user_name'] = $user ? $user->display_name : __('Unknown User', 'woonoow');
|
|
|
|
// Add computed fields
|
|
$enriched['is_active'] = $subscription->status === 'active';
|
|
|
|
// Surface pause-limit context to the client (H2). The server-side pause handler in
|
|
// SubscriptionManager::pause() already enforces the limit; this just tells the UI how
|
|
// many pauses remain so the button can be disabled with a tooltip before the customer
|
|
// hits the wall and gets a generic 500.
|
|
$settings = \WooNooW\Core\ModuleRegistry::get_settings('subscription');
|
|
$max_pause_count = isset($settings['max_pause_count']) ? (int) $settings['max_pause_count'] : 3;
|
|
$enriched['max_pause_count'] = $max_pause_count;
|
|
$enriched['pauses_remaining'] = $max_pause_count > 0
|
|
? max(0, $max_pause_count - (int) $subscription->pause_count)
|
|
: null; // null = unlimited
|
|
|
|
// Whether this customer is actually allowed to pause, incorporating:
|
|
// - feature toggle (allow_customer_pause setting)
|
|
// - subscription status
|
|
// - lifetime pause limit
|
|
$allow_pause_feature = !empty($settings['allow_customer_pause']);
|
|
$pause_limit_ok = ($max_pause_count <= 0) || ($subscription->pause_count < $max_pause_count);
|
|
$enriched['can_pause'] = $subscription->status === 'active' && $allow_pause_feature && $pause_limit_ok;
|
|
$enriched['can_resume'] = in_array($subscription->status, ['on-hold', 'pending-cancel']);
|
|
$enriched['can_cancel'] = in_array($subscription->status, ['active', 'on-hold', 'pending']);
|
|
|
|
// Expose paused_at so the UI can show when the subscription was paused
|
|
$enriched['paused_at'] = $subscription->paused_at ?? null;
|
|
|
|
// Format billing info
|
|
$period_labels = [
|
|
'day' => __('day', 'woonoow'),
|
|
'week' => __('week', 'woonoow'),
|
|
'month' => __('month', 'woonoow'),
|
|
'year' => __('year', 'woonoow'),
|
|
];
|
|
$interval = $subscription->billing_interval > 1 ? $subscription->billing_interval . ' ' : '';
|
|
$period = $period_labels[$subscription->billing_period] ?? $subscription->billing_period;
|
|
if ($subscription->billing_interval > 1) {
|
|
$period .= 's'; // Pluralize
|
|
}
|
|
$enriched['billing_schedule'] = sprintf(__('Every %s%s', 'woonoow'), $interval, $period);
|
|
|
|
// Add payment method title
|
|
$payment_title = $subscription->payment_method; // Default to ID
|
|
|
|
// 1. Try from payment_meta (stored snapshot)
|
|
if (!empty($subscription->payment_meta)) {
|
|
$meta = json_decode($subscription->payment_meta, true);
|
|
if (isset($meta['method_title']) && !empty($meta['method_title'])) {
|
|
$payment_title = $meta['method_title'];
|
|
}
|
|
}
|
|
|
|
// 2. If it looks like an ID (no spaces, lowercase), try to get fresh title from gateway
|
|
if ($payment_title === $subscription->payment_method && function_exists('WC')) {
|
|
$gateways_handler = WC()->payment_gateways();
|
|
if ($gateways_handler) {
|
|
$gateways = $gateways_handler->payment_gateways();
|
|
if (isset($gateways[$subscription->payment_method])) {
|
|
$gw = $gateways[$subscription->payment_method];
|
|
$payment_title = $gw->get_title() ?: $gw->method_title;
|
|
}
|
|
}
|
|
}
|
|
|
|
$enriched['payment_method_title'] = $payment_title;
|
|
|
|
// §9 — Tell the client whether the stored gateway is declared to support
|
|
// subscription auto-renew. The renewal flow uses this for messaging and the
|
|
// admin uses it for at-a-glance status.
|
|
$enriched['gateway_supports_auto_renew'] = !empty($subscription->payment_method)
|
|
? GatewayCapabilities::should_attempt_auto_renew($subscription->payment_method)
|
|
: false;
|
|
$enriched['gateway_force_manual'] = GatewayCapabilities::force_manual();
|
|
|
|
return $enriched;
|
|
}
|
|
|
|
/**
|
|
* §9 — List the merged gateway capability matrix for the admin UI.
|
|
*
|
|
* Returns a row per available WC payment gateway with:
|
|
* id, title, description, enabled (site-enabled),
|
|
* auto_renew (effective — capability table + kill switch),
|
|
* override (the merchant-set override, or null if using default),
|
|
* default (the built-in default for this gateway ID)
|
|
*/
|
|
public static function get_gateway_capabilities(WP_REST_Request $request)
|
|
{
|
|
if (!function_exists('WC')) {
|
|
return new WP_Error('wc_missing', __('WooCommerce is not active.', 'woonoow'), ['status' => 500]);
|
|
}
|
|
|
|
$gateways = WC()->payment_gateways()->payment_gateways();
|
|
$stored = get_option(GatewayCapabilities::OPTION_KEY, []);
|
|
if (!is_array($stored)) {
|
|
$stored = [];
|
|
}
|
|
$defaults = GatewayCapabilities::default_capabilities();
|
|
$kill_switch = GatewayCapabilities::force_manual();
|
|
|
|
$rows = [];
|
|
foreach ($gateways as $id => $gateway) {
|
|
$default = isset($defaults[$id]) ? (bool) $defaults[$id]['subscription_auto_renew'] : false;
|
|
$override = array_key_exists($id, $stored) ? (bool) $stored[$id]['subscription_auto_renew'] : null;
|
|
$effective = GatewayCapabilities::should_attempt_auto_renew($id);
|
|
$rows[] = [
|
|
'id' => $id,
|
|
'title' => $gateway->get_title() ?: $gateway->method_title ?: $id,
|
|
'description' => $gateway->get_description(),
|
|
'enabled' => $gateway->enabled === 'yes',
|
|
'default' => $default,
|
|
'override' => $override,
|
|
'auto_renew' => $effective,
|
|
'forced_manual' => $kill_switch,
|
|
];
|
|
}
|
|
|
|
return new WP_REST_Response([
|
|
'gateways' => array_values($rows),
|
|
'kill_switch' => $kill_switch,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* §9 — Persist merchant overrides for the per-gateway capability table.
|
|
*
|
|
* Body shape: { overrides: { '<gateway_id>': bool | null, ... } }
|
|
* - bool => explicit override (true = auto-renew, false = manual)
|
|
* - null => clear override, fall back to default
|
|
*
|
|
* The kill switch is NOT set here — it lives in the standard module
|
|
* settings under `force_manual_renewal` (use the generic settings endpoint).
|
|
*/
|
|
public static function update_gateway_capabilities(WP_REST_Request $request)
|
|
{
|
|
$body = $request->get_json_params();
|
|
if (!is_array($body) || !isset($body['overrides']) || !is_array($body['overrides'])) {
|
|
return new WP_Error('bad_request', __('overrides map is required.', 'woonoow'), ['status' => 400]);
|
|
}
|
|
|
|
$stored = get_option(GatewayCapabilities::OPTION_KEY, []);
|
|
if (!is_array($stored)) {
|
|
$stored = [];
|
|
}
|
|
|
|
$defaults = GatewayCapabilities::default_capabilities();
|
|
$valid_ids = $defaults;
|
|
if (function_exists('WC')) {
|
|
foreach (WC()->payment_gateways()->payment_gateways() as $id => $gw) {
|
|
$valid_ids[$id] = ['subscription_auto_renew' => false];
|
|
}
|
|
}
|
|
|
|
foreach ($body['overrides'] as $id => $value) {
|
|
$id = sanitize_key((string) $id);
|
|
if ($id === '' || !array_key_exists($id, $valid_ids)) {
|
|
continue; // unknown gateway — ignore
|
|
}
|
|
if ($value === null) {
|
|
unset($stored[$id]);
|
|
} else {
|
|
$stored[$id] = ['subscription_auto_renew' => (bool) $value];
|
|
}
|
|
}
|
|
|
|
update_option(GatewayCapabilities::OPTION_KEY, $stored);
|
|
|
|
return new WP_REST_Response([
|
|
'success' => true,
|
|
'overrides' => $stored,
|
|
]);
|
|
}
|
|
}
|