id = 'abovault_sepa'; $this->method_title = __('SEPA Direct Debit (Abovault)', 'abovault'); $this->method_description = __('SEPA-first recurring payments. Mandate stored locally — no external subscription cloud.', 'abovault'); $this->has_fields = true; $this->supports = ['products', 'refunds']; // 'subscriptions' driven by our own engine $this->init_form_fields(); $this->init_settings(); $this->title = $this->get_option('title', __('SEPA Direct Debit', 'abovault')); $this->enabled = $this->get_option('enabled', 'no'); add_action('woocommerce_update_options_payment_gateways_' . $this->id, [$this, 'process_admin_options']); add_action('wp_enqueue_scripts', [$this, 'enqueueCheckout']); } /** Active gateway provider (stripe built-in; add-ons register others). */ private function provider(): string { return (string) get_option('abovault_gateway_provider', 'stripe'); } /** IBAN field — Stripe Elements mounts here; other providers render their own. */ public function payment_fields(): void { if ($this->description) { echo wp_kses_post(wpautop($this->description)); } echo '
'; $provider = $this->provider(); if ($provider === 'stripe') { echo '
'; echo ''; } else { /** * Non-Stripe providers (e.g. Abovault Pro's Mollie gateway) render their * own checkout field here — typically a plain IBAN input. * * @param string $provider */ do_action('abovault_payment_fields', $provider); } echo '

' . esc_html__('By providing your IBAN you authorise a SEPA Direct Debit mandate. Your payment data is processed by your payment provider; your subscription data stays on this site.', 'abovault') . '

'; echo '
'; } public function enqueueCheckout(): void { if (!function_exists('is_checkout') || !is_checkout() || $this->enabled !== 'yes') { return; } // Stripe.js + Elements are only needed for the Stripe provider. Other providers // (e.g. Mollie) collect a plain IBAN that posts normally — no client-side JS. if ($this->provider() !== 'stripe') { return; } $pk = $this->get_option('stripe_publishable_key', ''); if ($pk === '') { return; } wp_enqueue_script('stripe-js', 'https://js.stripe.com/v3/', [], ABOVAULT_VERSION, true); // Version from filemtime → every deploy busts browser + CDN cache automatically. $jsPath = ABOVAULT_PATH . 'assets/js/checkout.js'; $ver = is_file($jsPath) ? (string) filemtime($jsPath) : ABOVAULT_VERSION; wp_enqueue_script( 'abovault-checkout', ABOVAULT_URL . 'assets/js/checkout.js', ['stripe-js', 'jquery', 'wc-checkout'], $ver, true ); wp_localize_script('abovault-checkout', 'abovaultCheckout', [ 'pk' => $pk, 'i18n' => [ 'notReady' => __('Payment field not ready. Please reload the page.', 'abovault'), 'network' => __('We couldn’t reach the payment provider. Please check your connection and try again.', 'abovault'), ], ]); } public function init_form_fields(): void { $this->form_fields = [ 'enabled' => [ 'title' => __('Enable/Disable', 'abovault'), 'type' => 'checkbox', 'label' => __('Enable SEPA Direct Debit', 'abovault'), 'default' => 'no', ], 'title' => [ 'title' => __('Title', 'abovault'), 'type' => 'text', 'default' => __('SEPA Direct Debit', 'abovault'), ], 'creditor_id' => [ 'title' => __('Creditor Identifier (Gläubiger-ID)', 'abovault'), 'type' => 'text', 'description' => __('Your SEPA creditor identifier — required for the mandate.', 'abovault'), ], 'stripe_publishable_key' => [ 'title' => __('Stripe Publishable Key', 'abovault'), 'type' => 'text', 'description' => __('Used by the checkout IBAN field (Stripe Elements).', 'abovault'), ], 'stripe_secret_key' => [ 'title' => __('Stripe Secret Key (SEPA processing)', 'abovault'), 'type' => 'password', ], 'webhook_secret' => [ 'title' => __('Stripe Webhook Signing Secret', 'abovault'), 'type' => 'password', 'description' => sprintf( /* translators: %s: webhook URL */ __('Point a Stripe webhook at %s and paste its signing secret here.', 'abovault'), esc_url(rest_url('abovault/v1/stripe-webhook')) ), ], ]; } /** * @param int $order_id * @return array */ public function process_payment($order_id): array { $order = wc_get_order($order_id); $gateway = GatewayFactory::active(); if (!$order || $gateway === null) { wc_add_notice(__('SEPA payments are not configured.', 'abovault'), 'error'); return ['result' => 'failure']; } // Build the mandate context. WooCommerce verifies the checkout nonce before // process_payment runs, so the posted payment field is trusted here. $ctx = [ 'email' => $order->get_billing_email(), 'name' => $order->get_formatted_billing_full_name(), 'metadata' => ['abovault_order_id' => (string) $order->get_id()], 'ip' => $order->get_customer_ip_address() ?: '0.0.0.0', 'ua' => $order->get_customer_user_agent() ?: 'unknown', ]; // Stripe (default): a PaymentMethod token created client-side before submit. // phpcs:ignore WordPress.Security.NonceVerification.Missing if (isset($_POST['abovault_pm_id'])) { // phpcs:ignore WordPress.Security.NonceVerification.Missing $ctx['pm_id'] = sanitize_text_field(wp_unslash($_POST['abovault_pm_id'])); } /** * Other providers (e.g. Abovault Pro's Mollie gateway) add their own payment * field to the mandate context here — e.g. a raw IBAN read from the request. * * @param array $ctx */ $ctx = (array) apply_filters('abovault_checkout_mandate_context', $ctx); if (empty($ctx['pm_id']) && empty($ctx['iban'])) { wc_add_notice(__('Please enter your IBAN to authorise the SEPA mandate.', 'abovault'), 'error'); return ['result' => 'failure']; } $mandate = $gateway->registerMandate($ctx); if (!$mandate['ok']) { wc_add_notice(__('Could not set up the SEPA mandate. Please check your IBAN and try again.', 'abovault'), 'error'); return ['result' => 'failure']; } $customerRef = (string) ($mandate['customer_ref'] ?? ''); // "cus|pm" — mandate ready now // Create the local subscription rows with the mandate attached (data stays local). $subIds = SubscriptionFactory::createFromOrder($order, $customerRef); $order->update_meta_data('_abovault_customer_ref', $customerRef); $order->update_meta_data('_abovault_subscription_ids', implode(',', $subIds)); // SEPA settles asynchronously — mark on-hold until the first charge confirms. $order->update_status('on-hold', __('Awaiting SEPA Direct Debit settlement.', 'abovault')); $order->save(); WC()->cart->empty_cart(); return [ 'result' => 'success', 'redirect' => $this->get_return_url($order), ]; } /** * Charge a stored mandate for a renewal — thin delegate used by legacy callers. * Dunning\RetryPolicy now talks to StripeGateway directly. * * @param array $subscription * @return array{status:string, reference?:string, reason?:string} */ public static function chargeRenewal(array $subscription): array { $gateway = GatewayFactory::active(); if ($gateway === null) { return ['status' => 'failed', 'reason' => 'gateway-not-configured']; } return $gateway->chargeOffSession($subscription); } }