$context extra placeholders (e.g. coupon_code) */ public static function send(int $subscriptionId, string $template, array $context = []): void { $sub = self::subscription($subscriptionId); if (!$sub) { return; } [$email, $name] = self::recipient($sub); if ($email === '') { return; } $tpl = self::templates()[$template] ?? null; if ($tpl === null) { return; } $vars = array_merge([ 'customer_name' => $name, 'amount' => self::formatAmount((float) $sub['amount'], (string) ($sub['currency'] ?? 'EUR')), 'manage_url' => (string) apply_filters('abovault_manage_url', self::manageUrl(), $subscriptionId), ], $context); $subject = self::render($tpl['subject'], $vars); $bodyHtml = self::render($tpl['body'], $vars); if (!empty($tpl['cta']) && $vars['manage_url'] !== '') { $bodyHtml .= self::button((string) $tpl['cta'], (string) $vars['manage_url']); } $body = self::wrap($bodyHtml, $subject); $headers = ['Content-Type: text/html; charset=UTF-8']; // Brand the sender: reuse WooCommerce's configured "from" identity so lifecycle // emails match the store's transactional mail (not the generic "WordPress" default). $fromName = get_option('woocommerce_email_from_name', ''); if (!is_string($fromName) || $fromName === '') { $fromName = get_bloginfo('name'); } $fromEmail = get_option('woocommerce_email_from_address', ''); if (!is_string($fromEmail) || !is_email($fromEmail)) { $fromEmail = get_option('admin_email'); } if (is_string($fromEmail) && is_email($fromEmail)) { $headers[] = sprintf('From: %s <%s>', $fromName, $fromEmail); } wp_mail($email, $subject, $body, $headers); do_action('abovault_email_sent', $subscriptionId, $template, $email); } /** * Format a money amount for the email's *current* locale — not the store's fixed * WooCommerce number format. The email is rendered in the recipient's language * (switch_to_locale before send), so the amount must match: German → "19,00 €", * English → "€19.00", French → "1 234,50 €". * * Uses the intl extension (locale-correct grouping + symbol placement) when present, * with a language-based fallback for hosts without ext-intl. */ private static function formatAmount(float $amount, string $currency): string { $currency = $currency !== '' ? strtoupper($currency) : 'EUR'; $locale = function_exists('determine_locale') ? determine_locale() : get_locale(); // Preferred: locale-aware currency formatting (correct separators + symbol side). if (class_exists('\NumberFormatter')) { $formatter = new \NumberFormatter($locale, \NumberFormatter::CURRENCY); $formatted = $formatter->formatCurrency($amount, $currency); if (is_string($formatted) && $formatted !== '') { return $formatted; } } // Fallback without ext-intl: pick separators + symbol placement by language. $symbol = function_exists('get_woocommerce_currency_symbol') ? html_entity_decode(get_woocommerce_currency_symbol($currency), ENT_QUOTES, 'UTF-8') : $currency; $lang = strtolower(substr($locale, 0, 2)); // Comma-decimal / symbol-after languages (EU continental) vs. English default. $commaDecimal = in_array($lang, ['de', 'fr', 'es', 'it', 'nl', 'pt', 'da', 'fi', 'sv', 'pl', 'cs', 'sk', 'hu', 'ro', 'el', 'bg', 'hr', 'lt', 'lv', 'sl', 'et'], true); if ($commaDecimal) { return number_format($amount, 2, ',', '.') . ' ' . $symbol; } return $symbol . number_format($amount, 2, '.', ','); } /** * Localized templates. English source strings; German via de_DE translation. * * @return array */ private static function templates(): array { $templates = [ 'dunning_1' => [ 'subject' => __('Your payment didn’t go through', 'abovault'), 'body' => __('Hi {customer_name},

We couldn’t collect {amount} for your subscription. We’ll try again automatically — no action needed yet. If your bank details changed, you can update them below.', 'abovault'), 'cta' => __('Update payment details', 'abovault'), ], 'dunning_2' => [ 'subject' => __('Still can’t collect your payment', 'abovault'), 'body' => __('Hi {customer_name},

Your payment of {amount} is still failing. Please update your SEPA details to keep your subscription active.', 'abovault'), 'cta' => __('Update SEPA details', 'abovault'), ], 'dunning_final' => [ 'subject' => __('Last reminder before we pause your subscription', 'abovault'), 'body' => __('Hi {customer_name},

This is the final reminder. If we can’t collect {amount}, your subscription will be paused. Please update your payment method now.', 'abovault'), 'cta' => __('Update payment method', 'abovault'), ], 'renewal_reminder' => [ 'subject' => __('Your subscription renews soon', 'abovault'), 'body' => __('Hi {customer_name},

Your subscription ({amount}) renews in a few days via SEPA Direct Debit. Nothing to do — we’ll handle it.', 'abovault'), 'cta' => __('Manage subscription', 'abovault'), ], 'win_back_offer' => [ 'subject' => __('Before you cancel — a little something', 'abovault'), 'body' => __('Hi {customer_name},

Sorry to see you thinking about leaving. You can pause instead of cancelling, or keep your benefits.', 'abovault'), 'cta' => __('See your options', 'abovault'), ], 'win_back_1' => [ 'subject' => __('Sorry to see you go', 'abovault'), 'body' => __('Hi {customer_name},

Your subscription has been cancelled. You’re welcome back anytime — reactivate in a couple of clicks.', 'abovault'), 'cta' => __('Reactivate subscription', 'abovault'), ], 'win_back_coupon' => [ 'subject' => __('Come back — here’s a discount', 'abovault'), 'body' => __('Hi {customer_name},

We’d love to have you back. Use code {coupon_code} for a discount on your next subscription.', 'abovault'), 'cta' => __('Claim your discount', 'abovault'), ], 'loyalty' => [ 'subject' => __('Thank you for staying with us', 'abovault'), 'body' => __('Hi {customer_name},

Thanks for being a loyal subscriber! Here’s a reward: use {coupon_code} on your next order.', 'abovault'), 'cta' => __('View your account', 'abovault'), ], ]; /** * Register additional lifecycle email templates (e.g. Abovault Pro adds extra * dunning / win-back touches). Each entry: ['subject'=>string,'body'=>string,'cta'?=>string]. * * @param array $templates */ return (array) apply_filters('abovault_email_templates', $templates); } /** * Email-safe CTA button (bulletproof table layout + inline styles → renders in * Outlook/Gmail/Apple Mail which strip