Plugin Directory

Changeset 3798535


Ignore:
Timestamp:
11/12/2026 02:39:47 PM (13 hours ago)
Author:
netdebitdev
Message:

Release version 1.1.0

Location:
netdebit-for-paid-memberships-pro
Files:
8 edited
11 copied

Legend:

Unmodified
Added
Removed
  • netdebit-for-paid-memberships-pro/tags/1.1.0/includes/Gateway/PMProNetDebitGateway.php

    r3674556 r3798535  
    66use MemberOrder;
    77use NetDebit\PMPro\Admin\SettingsPage;
     8use NetDebit\PMPro\Integration\FreeMembershipCheck;
    89use NetDebit\SDK\Billing\Access\Rate;
    910use NetDebit\SDK\Billing\Membership;
    … …  
    3940        try {
    4041            $level = $order->getMembershipLevel();
     42
     43            // Gratis-Membership: PMPro wickelt freie Levels nativ ab (Order-Gateway
     44            // 'free' → PMPro-Basisklasse, pmpro_complete_checkout()). Sollte eine
     45            // freie Order dennoch hier ankommen (exotische Pfade), signalisieren wir
     46            // nur "erfolgreich": PMPro setzt dann selbst Order-Status (success) und
     47            // Membership. Es darf niemals eine 0-€-Buchung an NetDebit gehen.
     48            if (FreeMembershipCheck::isLevelFree($level)) {
     49                return true;
     50            }
     51
    4152            $recurring = $this->isRecurring($order);
    4253            $has_trial = !empty($level->trial_limit) && (int)$level->trial_limit > 0;
    43             $has_setup_fee = $recurring && (float)$level->initial_payment !== (float)$level->billing_amount;
     54            // Setup-Fee / Gratis-Start: Jede Abweichung zwischen Startzahlung und
     55            // Abo-Rate ist unzulässig — auch bei inkonsistent konfigurierten Levels
     56            // ohne Abo-Zyklus (billing_amount > 0, cycle_number = 0), die sonst als
     57            // 0-€-Buchung an NetDebit weitergereicht würden.
     58            $has_setup_fee = (float)$level->billing_amount > 0
     59                && (float)$level->initial_payment !== (float)$level->billing_amount;
    4460
    4561            if ($has_trial || $has_setup_fee) {
  • netdebit-for-paid-memberships-pro/tags/1.1.0/includes/Integration/CheckoutRedirect.php

    r3674556 r3798535  
    1313
    1414use NetDebit\PMPro\Gateway\PMProNetDebitGateway;
     15use NetDebit\PMPro\Integration\FreeMembershipCheck;
    1516
    1617class CheckoutRedirect {
     18
     19        /**
     20         * Gratis-Checkouts laufen nativ über PMPro (Order-Gateway 'free',
     21         * pmpro_complete_checkout). Unsere Hook läuft VOR PMPro's Order-Erstellung
     22         * und darf einen freien Level daher nie in den NetDebit-Pfad ziehen — auch
     23         * dann nicht, wenn ein veraltetes Formular (Cache) oder ein manueller POST
     24         * gateway=netdebit mitschickt.
     25         */
     26        private function isFreeCheckout(): bool
     27        {
     28                // Autoritativ: der von PMPro aufgelöste Checkout-Level (inkl.
     29                // Discount-Code-Anpassungen) liegt im Global vor.
     30                global $pmpro_level;
     31                if ( ! empty( $pmpro_level ) && FreeMembershipCheck::isLevelFree( $pmpro_level ) ) {
     32                        return true;
     33                }
     34
     35                return false;
     36        }
     37
    1738        /**
    1839         * Register hooks.
    … …  
    121142                }
    122143
     144                // Gratis-Membership: hier nie eingreifen — PMPro verarbeitet das
     145                // Level nativ (keine NetDebit-Buchung, keine 0-€-Buchung).
     146                if ( FreeMembershipCheck::isLevelFree( $level ) || $this->isFreeCheckout() ) {
     147                        return;
     148                }
     149
    123150                $order = new \MemberOrder();
    124151                $order->membership_id = $level->id;
  • netdebit-for-paid-memberships-pro/tags/1.1.0/includes/Integration/GatewayForcer.php

    r3659034 r3798535  
    2020     */
    2121    public function registerHooks(): void {
     22        // Define the PMPro gateway class alias once PMPro is loaded.
     23        add_action('plugins_loaded', [ $this, 'defineGatewayClassAlias' ], 5);
     24
    2225        // Force NetDebit gateway selection
    2326        add_filter('pmpro_get_gateway', [ $this, 'forceGateway' ]);
    … …  
    4346        add_filter('pmpro_include_billing_fields', '__return_false', Constants::FILTER_OVERRIDE);
    4447        add_filter('pmpro_include_payment_information_fields', '__return_false', Constants::FILTER_OVERRIDE);
     48    }
     49
     50    /**
     51     * Define the gateway class PMPro expects.
     52     *
     53     * PMPro resolves the gateway class by convention: setGateway() looks for
     54     * a class named "PMProGateway_{gateway}" (i.e. PMProGateway_netdebit) and
     55     * sets MemberOrder::$Gateway to null when it is missing. In that state
     56     * MemberOrder::process() returns null without an error message and PMPro
     57     * shows its generic "Unknown error generating account" fallback — exactly
     58     * what happens whenever a checkout submission bypasses the JS purchase box
     59     * (Enter key, original submit button, cached page, JS conflict) and PMPro
     60     * processes the request itself.
     61     *
     62     * Declaring this alias makes PMPro's own processing path work: it loads
     63     * PMProNetDebitGateway, which creates the pending order and redirects to
     64     * the NetDebit payment page — identical to the JS-driven flow.
     65     *
     66     * Must run after plugins_loaded because the parent class PMProGateway is
     67     * only defined once PMPro's plugin file has been loaded.
     68     */
     69    public function defineGatewayClassAlias(): void {
     70        if ( ! class_exists( 'PMProGateway' ) ) {
     71            return; // PMPro not active — nothing to alias.
     72        }
     73
     74        require_once NETDEBIT_PMPRO_PLUGIN_DIR . 'includes/Gateway/pmpro-gateway-netdebit-alias.php';
    4575    }
    4676
  • netdebit-for-paid-memberships-pro/tags/1.1.0/includes/Plugin.php

    r3674556 r3798535  
    88use NetDebit\PMPro\Core\Constants;
    99use NetDebit\PMPro\Integration\CheckoutRedirect;
     10use NetDebit\PMPro\Integration\FreeMembershipCheck;
    1011use NetDebit\PMPro\Integration\GatewayForcer;
    1112use NetDebit\PMPro\Integration\LevelSanitizer;
    1213use NetDebit\PMPro\Integration\MemberActionModifier;
    1314use NetDebit\PMPro\Integration\SecurityConfig;
     15use NetDebit\PMPro\Setup\BusinessSettings;
    1416use NetDebit\PMPro\Setup\DatabaseMigration;
    1517use NetDebit\PMPro\WebHooks\WebHookHandler;
    … …  
    132134        global $pmpro_level;
    133135        if (empty($pmpro_level)) return;
     136
     137        // Gratis-Checkout: PMPro's nativer Submit-Button bleibt erhalten —
     138        // die Kaufbox darf hier weder gerendert werden noch gateway=netdebit
     139        // in das Formular schreiben.
     140        if (FreeMembershipCheck::isLevelFree($pmpro_level)) return;
    134141
    135142        $price_label = function_exists('pmpro_formatPrice') ? pmpro_formatPrice($pmpro_level->initial_payment) : $pmpro_level->initial_payment;
    … …  
    188195    {
    189196        if ( ! function_exists( 'pmpro_is_checkout' ) || ! pmpro_is_checkout() ) {
     197            return;
     198        }
     199
     200        // Gratis-Checkout: keine NetDebit-Assets (CSS/JS) — der native
     201        // PMPro-Button übernimmt und PMPro verarbeitet das Level nativ.
     202        global $pmpro_level;
     203        if ( ! empty( $pmpro_level ) && FreeMembershipCheck::isLevelFree( $pmpro_level ) ) {
    190204            return;
    191205        }
    … …  
    410424
    411425        DatabaseMigration::install();
     426
     427        // NetDebit is the merchant of record: while this plugin is active,
     428        // PMPro's Business Settings (invoice "Pay to" block) carry the
     429        // NetDebit letterhead. Deactivation clears the fields again.
     430        BusinessSettings::install();
    412431    }
    413432
    414433    public static function deactivate(): void
    415434    {
    416         // Deactivation logic.
     435        // Clear the NetDebit letterhead from PMPro's Business Settings —
     436        // without this plugin the site owner is responsible for their own
     437        // invoice details (invoice falls back to the blog name).
     438        BusinessSettings::remove();
    417439    }
    418440}
  • netdebit-for-paid-memberships-pro/tags/1.1.0/includes/WebHooks/WebHookHandler.php

    r3674556 r3798535  
    5151    }
    5252
     53    /**
     54     * NetDebit erwartet als Antwort den ROHEN Body (z.B. "OK=100"), exakt wie
     55     * in der SDK-Referenz (Handle_Notifications.php), die den Processor direkt
     56     * echo't. Ein WP_REST_Response würde vom REST-Server JSON-kodiert
     57     * ("\"OK=100\"") — deshalb geben wir selbst aus und beenden den Request.
     58     *
     59     * Wird erst nach dem RELEASE_LOCK aufgerufen (siehe handleBooking), damit
     60     * der Advisory Lock nicht von exit- Semantiken abhängig ist.
     61     */
     62    private function respondRaw( string $body, int $status = 200 ): void {
     63        status_header( $status );
     64        header( 'Content-Type: text/plain; charset=utf-8' );
     65        echo $body;
     66        exit;
     67    }
     68
     69    private function logWebhookError( Exception $e ): void {
     70        if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
     71            // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
     72            error_log( '[NetDebit Webhook Error] ' . $e->getMessage() );
     73        }
     74    }
     75
    5376    public function handleBooking( WP_REST_Request $request ) {
    5477        global $wpdb;
    … …  
    7194
    7295        if ( ! $lock_acquired ) {
    73             // Already being processed – return OK to avoid retries.
    74             return new WP_REST_Response( 'OK=30', 200 );
     96            // Already being processed – OK=30 avoids a retry storm.
     97            $this->respondRaw( 'OK=30' );
    7598        }
     99
     100        $body   = 'OK=90';
     101        $status = 400;
    76102
    77103        try {
    78104            $this->validate_netdebit_gw_response_params( $payload );
    79105
     106            $content = Content::with(
     107                ContentIdentifier::with( $options['con'] ?? throw new \Exception( 'Con (Content ID) not configured' ) ),
     108                ContentSecret::with( $options['gw_pw'] )
     109            );
     110
     111            ob_start();
    80112            try {
    81                 $content = Content::with(
    82                     ContentIdentifier::with( $options['con'] ?? throw new \Exception('Con (Content ID) not configured') ),
    83                     ContentSecret::with( $options['gw_pw'] )
    84                 );
    85 
    86                 ob_start();
    87                 try {
    88                     $processor = new NotificationProcessor( $content );
    89                     $processor->register( new PMProHandler() );
    90                     $processor->processRequest( $payload );
    91                 } finally {
    92                     $sdkResponse = ob_get_clean() ?: '';
    93                 }
    94 
    95                 return new WP_REST_Response( $sdkResponse, 200 );
    96 
    97             } catch ( Exception $e ) {
    98                 if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
    99                     // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
    100                     error_log( '[NetDebit Webhook Error] ' . $e->getMessage() );
    101                 }
    102                 return new WP_REST_Response( 'OK=90', 400 );
     113                $processor = new NotificationProcessor( $content );
     114                $processor->register( new PMProHandler() );
     115                $processor->processRequest( $payload );
     116            } finally {
     117                $sdkResponse = ob_get_clean() ?: '';
    103118            }
    104119
     120            if ( '' === $sdkResponse ) {
     121                // Unbekannter Notification-Typ (Processor hat nichts geantwortet):
     122                // nicht quittieren → NetDebit wiederholt die Zustellung.
     123                $body   = 'OK=90';
     124                $status = 400;
     125            } else {
     126                $body   = $sdkResponse;
     127                $status = 200;
     128            }
    105129        } catch ( Exception $e ) {
    106             if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
    107                 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
    108                 error_log( '[NetDebit Webhook Error] ' . $e->getMessage() );
    109             }
    110             return new WP_REST_Response( 'OK=90', 400 );
     130            $this->logWebhookError( $e );
    111131        } finally {
    112             // Release the advisory lock. No-op if the connection already died
    113             // (MySQL released it automatically).
     132            // Release the advisory lock BEFORE responding (respondRaw exits).
     133            // No-op if the connection already died (MySQL released it then).
    114134            $wpdb->query( $wpdb->prepare( 'SELECT RELEASE_LOCK(%s)', $lock_name ) );
    115135        }
     136
     137        $this->respondRaw( $body, $status );
    116138    }
    117139
  • netdebit-for-paid-memberships-pro/tags/1.1.0/netdebit-for-paid-memberships-pro.php

    r3674556 r3798535  
    44 * Plugin URI:          https://wordpress.org/plugins/netdebit-for-pmpro/
    55 * Description:         Integrates NetDebit Payment with PaidMembership Pro
    6  * Version:             1.0.1
     6 * Version:             1.1.0
    77 * Requires at least:   6.0
    88 * Requires PHP:        8.0
    … …  
    2424if ( ! defined( 'NETDEBIT_PMPRO_PLUGIN_URL' ) ) {
    2525    define( 'NETDEBIT_PMPRO_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
    26     define( 'NETDEBIT_PMPRO_VERSION', '1.0.1' );
     26    define( 'NETDEBIT_PMPRO_VERSION', '1.1.0' );
    2727    define( 'NETDEBIT_PMPRO_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
    2828    define( 'NETDEBIT_PMPRO_BASENAME', plugin_basename( __FILE__ ) );
  • netdebit-for-paid-memberships-pro/tags/1.1.0/readme.txt

    r3674556 r3798535  
    55Tested up to: 7.1
    66Requires PHP: 8.0
    7 Stable tag: 1.0.1
     7Stable tag: 1.1.0
    88License: GPL-2.0+
    99License URI: http://www.gnu.org/licenses/gpl-2.0.txt
    … …  
    3131* Clean UI: Removes unnecessary form fields for a higher conversion rate.
    3232
     33* Native checkout for free membership levels: gratis memberships are processed by PMPro itself, without any NetDebit redirect.
     34
     35* NetDebit letterhead: while the plugin is active, PMPro's Business Settings carry the NetDebit company address (Merchant of Record) for the invoice page. Cleared again on deactivation.
     36
    3337* Users with active chargebacks are automatically blocked from accessing content.
    3438
    … …  
    4448
    4549== Frequently Asked Questions ==
     50
     51= Can I offer free memberships? =
     52
     53Yes. Membership levels without any payment (initial payment 0, no recurring billing, no trial) are processed natively by Paid Memberships Pro: the NetDebit purchase box is not shown, the checkout completes without a redirect and no booking is created at NetDebit. Paid levels keep using the NetDebit checkout as before. Levels with "first month free" (0 start payment plus paid subscription) or setup fees are rejected, as they are not compatible with direct debit.
    4654
    4755= Do I need a NetDebit partner account? =
    … …  
    6573
    6674== Upgrade Notice ==
     75= 1.1.0 =
     76Free membership levels are now checked out natively via PMPro without any NetDebit redirect. Levels whose start payment differs from the recurring rate — including malformed levels with a billing amount but no cycle — can no longer create 0 bookings at NetDebit.
     77
     78= 1.0.3 =
     79The webhook now answers with the raw expected body (OK=100) instead of a JSON-encoded string.
     80
     81= 1.0.2 =
     82Fixes the "Unknown error generating account" checkout error: PMPro can now process the NetDebit gateway itself when the checkout form is submitted without the gateway field (Enter key, original submit button, cached page, JavaScript conflicts).
     83
    6784= 1.0.1 =
    6885Tested with WordPress 7.1 and Paid Memberships Pro 3.7.2.
    … …  
    90107
    91108== Changelog ==
     109
     110= 1.1.0 =
     111
     112* Added: Free membership levels (initial payment 0, no recurring billing, no trial) are processed natively by PMPro: no NetDebit purchase box, no redirect, no 0 booking. Also applies when a stale form or a manual request posts gateway=netdebit for a free level, and when a 100% discount code makes a paid level free.
     113* Fixed: Levels whose start payment differs from the recurring rate can no longer create 0 bookings at NetDebit. This now also covers inconsistent levels configured with a billing amount but no cycle, which previously bypassed the setup-fee check and produced a 0 booking.
     114* Added: Central FreeMembershipCheck helper used by checkout redirect, frontend assets and the gateway itself, honoring PMPro's pmpro_is_level_free filter.
     115* Added: On activation the plugin fills PMPro's Business Settings (Settings → Advanced → Business Settings) with the NetDebit letterhead — NetDebit is the merchant of record, so the invoice "Pay to" block shows the NetDebit company address. Deactivating the plugin clears these fields again.
     116
     117= 1.0.3 =
     118
     119* Fixed: The booking webhook now answers with the raw body expected by NetDebit (exactly `OK=100`, Content-Type text/plain) instead of a JSON-encoded REST response. Unknown notification types are no longer acknowledged (OK=90) so NetDebit retries.
     120
     121= 1.0.2 =
     122
     123* Fixed: Checkout no longer fails with PMPro's generic "Unknown error generating account" message when a submission reaches PMPro without the NetDebit gateway field.
     124* Added: The gateway class `PMProGateway_netdebit` is now declared as an alias of the plugin's gateway, so PMPro's own order processing redirects to the NetDebit payment page as a fallback (also improves compatibility with custom/older checkout templates).
    92125
    93126= 1.0.1 =
  • netdebit-for-paid-memberships-pro/trunk/includes/Gateway/PMProNetDebitGateway.php

    r3674556 r3798535  
    66use MemberOrder;
    77use NetDebit\PMPro\Admin\SettingsPage;
     8use NetDebit\PMPro\Integration\FreeMembershipCheck;
    89use NetDebit\SDK\Billing\Access\Rate;
    910use NetDebit\SDK\Billing\Membership;
    … …  
    3940        try {
    4041            $level = $order->getMembershipLevel();
     42
     43            // Gratis-Membership: PMPro wickelt freie Levels nativ ab (Order-Gateway
     44            // 'free' → PMPro-Basisklasse, pmpro_complete_checkout()). Sollte eine
     45            // freie Order dennoch hier ankommen (exotische Pfade), signalisieren wir
     46            // nur "erfolgreich": PMPro setzt dann selbst Order-Status (success) und
     47            // Membership. Es darf niemals eine 0-€-Buchung an NetDebit gehen.
     48            if (FreeMembershipCheck::isLevelFree($level)) {
     49                return true;
     50            }
     51
    4152            $recurring = $this->isRecurring($order);
    4253            $has_trial = !empty($level->trial_limit) && (int)$level->trial_limit > 0;
    43             $has_setup_fee = $recurring && (float)$level->initial_payment !== (float)$level->billing_amount;
     54            // Setup-Fee / Gratis-Start: Jede Abweichung zwischen Startzahlung und
     55            // Abo-Rate ist unzulässig — auch bei inkonsistent konfigurierten Levels
     56            // ohne Abo-Zyklus (billing_amount > 0, cycle_number = 0), die sonst als
     57            // 0-€-Buchung an NetDebit weitergereicht würden.
     58            $has_setup_fee = (float)$level->billing_amount > 0
     59                && (float)$level->initial_payment !== (float)$level->billing_amount;
    4460
    4561            if ($has_trial || $has_setup_fee) {
  • netdebit-for-paid-memberships-pro/trunk/includes/Integration/CheckoutRedirect.php

    r3674556 r3798535  
    1313
    1414use NetDebit\PMPro\Gateway\PMProNetDebitGateway;
     15use NetDebit\PMPro\Integration\FreeMembershipCheck;
    1516
    1617class CheckoutRedirect {
     18
     19        /**
     20         * Gratis-Checkouts laufen nativ über PMPro (Order-Gateway 'free',
     21         * pmpro_complete_checkout). Unsere Hook läuft VOR PMPro's Order-Erstellung
     22         * und darf einen freien Level daher nie in den NetDebit-Pfad ziehen — auch
     23         * dann nicht, wenn ein veraltetes Formular (Cache) oder ein manueller POST
     24         * gateway=netdebit mitschickt.
     25         */
     26        private function isFreeCheckout(): bool
     27        {
     28                // Autoritativ: der von PMPro aufgelöste Checkout-Level (inkl.
     29                // Discount-Code-Anpassungen) liegt im Global vor.
     30                global $pmpro_level;
     31                if ( ! empty( $pmpro_level ) && FreeMembershipCheck::isLevelFree( $pmpro_level ) ) {
     32                        return true;
     33                }
     34
     35                return false;
     36        }
     37
    1738        /**
    1839         * Register hooks.
    … …  
    121142                }
    122143
     144                // Gratis-Membership: hier nie eingreifen — PMPro verarbeitet das
     145                // Level nativ (keine NetDebit-Buchung, keine 0-€-Buchung).
     146                if ( FreeMembershipCheck::isLevelFree( $level ) || $this->isFreeCheckout() ) {
     147                        return;
     148                }
     149
    123150                $order = new \MemberOrder();
    124151                $order->membership_id = $level->id;
  • netdebit-for-paid-memberships-pro/trunk/includes/Integration/GatewayForcer.php

    r3659034 r3798535  
    2020     */
    2121    public function registerHooks(): void {
     22        // Define the PMPro gateway class alias once PMPro is loaded.
     23        add_action('plugins_loaded', [ $this, 'defineGatewayClassAlias' ], 5);
     24
    2225        // Force NetDebit gateway selection
    2326        add_filter('pmpro_get_gateway', [ $this, 'forceGateway' ]);
    … …  
    4346        add_filter('pmpro_include_billing_fields', '__return_false', Constants::FILTER_OVERRIDE);
    4447        add_filter('pmpro_include_payment_information_fields', '__return_false', Constants::FILTER_OVERRIDE);
     48    }
     49
     50    /**
     51     * Define the gateway class PMPro expects.
     52     *
     53     * PMPro resolves the gateway class by convention: setGateway() looks for
     54     * a class named "PMProGateway_{gateway}" (i.e. PMProGateway_netdebit) and
     55     * sets MemberOrder::$Gateway to null when it is missing. In that state
     56     * MemberOrder::process() returns null without an error message and PMPro
     57     * shows its generic "Unknown error generating account" fallback — exactly
     58     * what happens whenever a checkout submission bypasses the JS purchase box
     59     * (Enter key, original submit button, cached page, JS conflict) and PMPro
     60     * processes the request itself.
     61     *
     62     * Declaring this alias makes PMPro's own processing path work: it loads
     63     * PMProNetDebitGateway, which creates the pending order and redirects to
     64     * the NetDebit payment page — identical to the JS-driven flow.
     65     *
     66     * Must run after plugins_loaded because the parent class PMProGateway is
     67     * only defined once PMPro's plugin file has been loaded.
     68     */
     69    public function defineGatewayClassAlias(): void {
     70        if ( ! class_exists( 'PMProGateway' ) ) {
     71            return; // PMPro not active — nothing to alias.
     72        }
     73
     74        require_once NETDEBIT_PMPRO_PLUGIN_DIR . 'includes/Gateway/pmpro-gateway-netdebit-alias.php';
    4575    }
    4676
  • netdebit-for-paid-memberships-pro/trunk/includes/Plugin.php

    r3674556 r3798535  
    88use NetDebit\PMPro\Core\Constants;
    99use NetDebit\PMPro\Integration\CheckoutRedirect;
     10use NetDebit\PMPro\Integration\FreeMembershipCheck;
    1011use NetDebit\PMPro\Integration\GatewayForcer;
    1112use NetDebit\PMPro\Integration\LevelSanitizer;
    1213use NetDebit\PMPro\Integration\MemberActionModifier;
    1314use NetDebit\PMPro\Integration\SecurityConfig;
     15use NetDebit\PMPro\Setup\BusinessSettings;
    1416use NetDebit\PMPro\Setup\DatabaseMigration;
    1517use NetDebit\PMPro\WebHooks\WebHookHandler;
    … …  
    132134        global $pmpro_level;
    133135        if (empty($pmpro_level)) return;
     136
     137        // Gratis-Checkout: PMPro's nativer Submit-Button bleibt erhalten —
     138        // die Kaufbox darf hier weder gerendert werden noch gateway=netdebit
     139        // in das Formular schreiben.
     140        if (FreeMembershipCheck::isLevelFree($pmpro_level)) return;
    134141
    135142        $price_label = function_exists('pmpro_formatPrice') ? pmpro_formatPrice($pmpro_level->initial_payment) : $pmpro_level->initial_payment;
    … …  
    188195    {
    189196        if ( ! function_exists( 'pmpro_is_checkout' ) || ! pmpro_is_checkout() ) {
     197            return;
     198        }
     199
     200        // Gratis-Checkout: keine NetDebit-Assets (CSS/JS) — der native
     201        // PMPro-Button übernimmt und PMPro verarbeitet das Level nativ.
     202        global $pmpro_level;
     203        if ( ! empty( $pmpro_level ) && FreeMembershipCheck::isLevelFree( $pmpro_level ) ) {
    190204            return;
    191205        }
    … …  
    410424
    411425        DatabaseMigration::install();
     426
     427        // NetDebit is the merchant of record: while this plugin is active,
     428        // PMPro's Business Settings (invoice "Pay to" block) carry the
     429        // NetDebit letterhead. Deactivation clears the fields again.
     430        BusinessSettings::install();
    412431    }
    413432
    414433    public static function deactivate(): void
    415434    {
    416         // Deactivation logic.
     435        // Clear the NetDebit letterhead from PMPro's Business Settings —
     436        // without this plugin the site owner is responsible for their own
     437        // invoice details (invoice falls back to the blog name).
     438        BusinessSettings::remove();
    417439    }
    418440}
  • netdebit-for-paid-memberships-pro/trunk/includes/WebHooks/WebHookHandler.php

    r3674556 r3798535  
    5151    }
    5252
     53    /**
     54     * NetDebit erwartet als Antwort den ROHEN Body (z.B. "OK=100"), exakt wie
     55     * in der SDK-Referenz (Handle_Notifications.php), die den Processor direkt
     56     * echo't. Ein WP_REST_Response würde vom REST-Server JSON-kodiert
     57     * ("\"OK=100\"") — deshalb geben wir selbst aus und beenden den Request.
     58     *
     59     * Wird erst nach dem RELEASE_LOCK aufgerufen (siehe handleBooking), damit
     60     * der Advisory Lock nicht von exit- Semantiken abhängig ist.
     61     */
     62    private function respondRaw( string $body, int $status = 200 ): void {
     63        status_header( $status );
     64        header( 'Content-Type: text/plain; charset=utf-8' );
     65        echo $body;
     66        exit;
     67    }
     68
     69    private function logWebhookError( Exception $e ): void {
     70        if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
     71            // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
     72            error_log( '[NetDebit Webhook Error] ' . $e->getMessage() );
     73        }
     74    }
     75
    5376    public function handleBooking( WP_REST_Request $request ) {
    5477        global $wpdb;
    … …  
    7194
    7295        if ( ! $lock_acquired ) {
    73             // Already being processed – return OK to avoid retries.
    74             return new WP_REST_Response( 'OK=30', 200 );
     96            // Already being processed – OK=30 avoids a retry storm.
     97            $this->respondRaw( 'OK=30' );
    7598        }
     99
     100        $body   = 'OK=90';
     101        $status = 400;
    76102
    77103        try {
    78104            $this->validate_netdebit_gw_response_params( $payload );
    79105
     106            $content = Content::with(
     107                ContentIdentifier::with( $options['con'] ?? throw new \Exception( 'Con (Content ID) not configured' ) ),
     108                ContentSecret::with( $options['gw_pw'] )
     109            );
     110
     111            ob_start();
    80112            try {
    81                 $content = Content::with(
    82                     ContentIdentifier::with( $options['con'] ?? throw new \Exception('Con (Content ID) not configured') ),
    83                     ContentSecret::with( $options['gw_pw'] )
    84                 );
    85 
    86                 ob_start();
    87                 try {
    88                     $processor = new NotificationProcessor( $content );
    89                     $processor->register( new PMProHandler() );
    90                     $processor->processRequest( $payload );
    91                 } finally {
    92                     $sdkResponse = ob_get_clean() ?: '';
    93                 }
    94 
    95                 return new WP_REST_Response( $sdkResponse, 200 );
    96 
    97             } catch ( Exception $e ) {
    98                 if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
    99                     // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
    100                     error_log( '[NetDebit Webhook Error] ' . $e->getMessage() );
    101                 }
    102                 return new WP_REST_Response( 'OK=90', 400 );
     113                $processor = new NotificationProcessor( $content );
     114                $processor->register( new PMProHandler() );
     115                $processor->processRequest( $payload );
     116            } finally {
     117                $sdkResponse = ob_get_clean() ?: '';
    103118            }
    104119
     120            if ( '' === $sdkResponse ) {
     121                // Unbekannter Notification-Typ (Processor hat nichts geantwortet):
     122                // nicht quittieren → NetDebit wiederholt die Zustellung.
     123                $body   = 'OK=90';
     124                $status = 400;
     125            } else {
     126                $body   = $sdkResponse;
     127                $status = 200;
     128            }
    105129        } catch ( Exception $e ) {
    106             if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
    107                 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
    108                 error_log( '[NetDebit Webhook Error] ' . $e->getMessage() );
    109             }
    110             return new WP_REST_Response( 'OK=90', 400 );
     130            $this->logWebhookError( $e );
    111131        } finally {
    112             // Release the advisory lock. No-op if the connection already died
    113             // (MySQL released it automatically).
     132            // Release the advisory lock BEFORE responding (respondRaw exits).
     133            // No-op if the connection already died (MySQL released it then).
    114134            $wpdb->query( $wpdb->prepare( 'SELECT RELEASE_LOCK(%s)', $lock_name ) );
    115135        }
     136
     137        $this->respondRaw( $body, $status );
    116138    }
    117139
  • netdebit-for-paid-memberships-pro/trunk/netdebit-for-paid-memberships-pro.php

    r3674556 r3798535  
    44 * Plugin URI:          https://wordpress.org/plugins/netdebit-for-pmpro/
    55 * Description:         Integrates NetDebit Payment with PaidMembership Pro
    6  * Version:             1.0.1
     6 * Version:             1.1.0
    77 * Requires at least:   6.0
    88 * Requires PHP:        8.0
    … …  
    2424if ( ! defined( 'NETDEBIT_PMPRO_PLUGIN_URL' ) ) {
    2525    define( 'NETDEBIT_PMPRO_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
    26     define( 'NETDEBIT_PMPRO_VERSION', '1.0.1' );
     26    define( 'NETDEBIT_PMPRO_VERSION', '1.1.0' );
    2727    define( 'NETDEBIT_PMPRO_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
    2828    define( 'NETDEBIT_PMPRO_BASENAME', plugin_basename( __FILE__ ) );
  • netdebit-for-paid-memberships-pro/trunk/readme.txt

    r3674556 r3798535  
    55Tested up to: 7.1
    66Requires PHP: 8.0
    7 Stable tag: 1.0.1
     7Stable tag: 1.1.0
    88License: GPL-2.0+
    99License URI: http://www.gnu.org/licenses/gpl-2.0.txt
    … …  
    3131* Clean UI: Removes unnecessary form fields for a higher conversion rate.
    3232
     33* Native checkout for free membership levels: gratis memberships are processed by PMPro itself, without any NetDebit redirect.
     34
     35* NetDebit letterhead: while the plugin is active, PMPro's Business Settings carry the NetDebit company address (Merchant of Record) for the invoice page. Cleared again on deactivation.
     36
    3337* Users with active chargebacks are automatically blocked from accessing content.
    3438
    … …  
    4448
    4549== Frequently Asked Questions ==
     50
     51= Can I offer free memberships? =
     52
     53Yes. Membership levels without any payment (initial payment 0, no recurring billing, no trial) are processed natively by Paid Memberships Pro: the NetDebit purchase box is not shown, the checkout completes without a redirect and no booking is created at NetDebit. Paid levels keep using the NetDebit checkout as before. Levels with "first month free" (0 start payment plus paid subscription) or setup fees are rejected, as they are not compatible with direct debit.
    4654
    4755= Do I need a NetDebit partner account? =
    … …  
    6573
    6674== Upgrade Notice ==
     75= 1.1.0 =
     76Free membership levels are now checked out natively via PMPro without any NetDebit redirect. Levels whose start payment differs from the recurring rate — including malformed levels with a billing amount but no cycle — can no longer create 0 bookings at NetDebit.
     77
     78= 1.0.3 =
     79The webhook now answers with the raw expected body (OK=100) instead of a JSON-encoded string.
     80
     81= 1.0.2 =
     82Fixes the "Unknown error generating account" checkout error: PMPro can now process the NetDebit gateway itself when the checkout form is submitted without the gateway field (Enter key, original submit button, cached page, JavaScript conflicts).
     83
    6784= 1.0.1 =
    6885Tested with WordPress 7.1 and Paid Memberships Pro 3.7.2.
    … …  
    90107
    91108== Changelog ==
     109
     110= 1.1.0 =
     111
     112* Added: Free membership levels (initial payment 0, no recurring billing, no trial) are processed natively by PMPro: no NetDebit purchase box, no redirect, no 0 booking. Also applies when a stale form or a manual request posts gateway=netdebit for a free level, and when a 100% discount code makes a paid level free.
     113* Fixed: Levels whose start payment differs from the recurring rate can no longer create 0 bookings at NetDebit. This now also covers inconsistent levels configured with a billing amount but no cycle, which previously bypassed the setup-fee check and produced a 0 booking.
     114* Added: Central FreeMembershipCheck helper used by checkout redirect, frontend assets and the gateway itself, honoring PMPro's pmpro_is_level_free filter.
     115* Added: On activation the plugin fills PMPro's Business Settings (Settings → Advanced → Business Settings) with the NetDebit letterhead — NetDebit is the merchant of record, so the invoice "Pay to" block shows the NetDebit company address. Deactivating the plugin clears these fields again.
     116
     117= 1.0.3 =
     118
     119* Fixed: The booking webhook now answers with the raw body expected by NetDebit (exactly `OK=100`, Content-Type text/plain) instead of a JSON-encoded REST response. Unknown notification types are no longer acknowledged (OK=90) so NetDebit retries.
     120
     121= 1.0.2 =
     122
     123* Fixed: Checkout no longer fails with PMPro's generic "Unknown error generating account" message when a submission reaches PMPro without the NetDebit gateway field.
     124* Added: The gateway class `PMProGateway_netdebit` is now declared as an alias of the plugin's gateway, so PMPro's own order processing redirects to the NetDebit payment page as a fallback (also improves compatibility with custom/older checkout templates).
    92125
    93126= 1.0.1 =
Note: See TracChangeset for help on using the changeset viewer.