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);
}
}