Changeset 3798535
- Timestamp:
- 11/12/2026 02:39:47 PM (13 hours ago)
- Location:
- netdebit-for-paid-memberships-pro
- Files:
-
- 8 edited
- 11 copied
-
tags/1.1.0 (copied) (copied from netdebit-for-paid-memberships-pro/trunk )
-
tags/1.1.0/assets/js/netdebit-checkout.js (copied) (copied from netdebit-for-paid-memberships-pro/trunk/assets/js/netdebit-checkout.js )
-
tags/1.1.0/includes/Admin/SettingsPage.php (copied) (copied from netdebit-for-paid-memberships-pro/trunk/includes/Admin/SettingsPage.php )
-
tags/1.1.0/includes/Gateway/PMProNetDebitGateway.php (copied) (copied from netdebit-for-paid-memberships-pro/trunk/includes/Gateway/PMProNetDebitGateway.php ) (2 diffs)
-
tags/1.1.0/includes/Integration/CheckoutRedirect.php (copied) (copied from netdebit-for-paid-memberships-pro/trunk/includes/Integration/CheckoutRedirect.php ) (2 diffs)
-
tags/1.1.0/includes/Integration/GatewayForcer.php (modified) (2 diffs)
-
tags/1.1.0/includes/Integration/LevelSanitizer.php (copied) (copied from netdebit-for-paid-memberships-pro/trunk/includes/Integration/LevelSanitizer.php )
-
tags/1.1.0/includes/Plugin.php (copied) (copied from netdebit-for-paid-memberships-pro/trunk/includes/Plugin.php ) (4 diffs)
-
tags/1.1.0/includes/WebHooks/PMProHandler.php (copied) (copied from netdebit-for-paid-memberships-pro/trunk/includes/WebHooks/PMProHandler.php )
-
tags/1.1.0/includes/WebHooks/WebHookHandler.php (copied) (copied from netdebit-for-paid-memberships-pro/trunk/includes/WebHooks/WebHookHandler.php ) (2 diffs)
-
tags/1.1.0/netdebit-for-paid-memberships-pro.php (copied) (copied from netdebit-for-paid-memberships-pro/trunk/netdebit-for-paid-memberships-pro.php ) (2 diffs)
-
tags/1.1.0/readme.txt (copied) (copied from netdebit-for-paid-memberships-pro/trunk/readme.txt ) (5 diffs)
-
trunk/includes/Gateway/PMProNetDebitGateway.php (modified) (2 diffs)
-
trunk/includes/Integration/CheckoutRedirect.php (modified) (2 diffs)
-
trunk/includes/Integration/GatewayForcer.php (modified) (2 diffs)
-
trunk/includes/Plugin.php (modified) (4 diffs)
-
trunk/includes/WebHooks/WebHookHandler.php (modified) (2 diffs)
-
trunk/netdebit-for-paid-memberships-pro.php (modified) (2 diffs)
-
trunk/readme.txt (modified) (5 diffs)
Legend:
- Unmodified
- Added
- Removed
-
netdebit-for-paid-memberships-pro/tags/1.1.0/includes/Gateway/PMProNetDebitGateway.php
r3674556 r3798535 6 6 use MemberOrder; 7 7 use NetDebit\PMPro\Admin\SettingsPage; 8 use NetDebit\PMPro\Integration\FreeMembershipCheck; 8 9 use NetDebit\SDK\Billing\Access\Rate; 9 10 use NetDebit\SDK\Billing\Membership; … … 39 40 try { 40 41 $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 41 52 $recurring = $this->isRecurring($order); 42 53 $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; 44 60 45 61 if ($has_trial || $has_setup_fee) { -
netdebit-for-paid-memberships-pro/tags/1.1.0/includes/Integration/CheckoutRedirect.php
r3674556 r3798535 13 13 14 14 use NetDebit\PMPro\Gateway\PMProNetDebitGateway; 15 use NetDebit\PMPro\Integration\FreeMembershipCheck; 15 16 16 17 class 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 17 38 /** 18 39 * Register hooks. … … 121 142 } 122 143 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 123 150 $order = new \MemberOrder(); 124 151 $order->membership_id = $level->id; -
netdebit-for-paid-memberships-pro/tags/1.1.0/includes/Integration/GatewayForcer.php
r3659034 r3798535 20 20 */ 21 21 public function registerHooks(): void { 22 // Define the PMPro gateway class alias once PMPro is loaded. 23 add_action('plugins_loaded', [ $this, 'defineGatewayClassAlias' ], 5); 24 22 25 // Force NetDebit gateway selection 23 26 add_filter('pmpro_get_gateway', [ $this, 'forceGateway' ]); … … 43 46 add_filter('pmpro_include_billing_fields', '__return_false', Constants::FILTER_OVERRIDE); 44 47 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'; 45 75 } 46 76 -
netdebit-for-paid-memberships-pro/tags/1.1.0/includes/Plugin.php
r3674556 r3798535 8 8 use NetDebit\PMPro\Core\Constants; 9 9 use NetDebit\PMPro\Integration\CheckoutRedirect; 10 use NetDebit\PMPro\Integration\FreeMembershipCheck; 10 11 use NetDebit\PMPro\Integration\GatewayForcer; 11 12 use NetDebit\PMPro\Integration\LevelSanitizer; 12 13 use NetDebit\PMPro\Integration\MemberActionModifier; 13 14 use NetDebit\PMPro\Integration\SecurityConfig; 15 use NetDebit\PMPro\Setup\BusinessSettings; 14 16 use NetDebit\PMPro\Setup\DatabaseMigration; 15 17 use NetDebit\PMPro\WebHooks\WebHookHandler; … … 132 134 global $pmpro_level; 133 135 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; 134 141 135 142 $price_label = function_exists('pmpro_formatPrice') ? pmpro_formatPrice($pmpro_level->initial_payment) : $pmpro_level->initial_payment; … … 188 195 { 189 196 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 ) ) { 190 204 return; 191 205 } … … 410 424 411 425 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(); 412 431 } 413 432 414 433 public static function deactivate(): void 415 434 { 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(); 417 439 } 418 440 } -
netdebit-for-paid-memberships-pro/tags/1.1.0/includes/WebHooks/WebHookHandler.php
r3674556 r3798535 51 51 } 52 52 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 53 76 public function handleBooking( WP_REST_Request $request ) { 54 77 global $wpdb; … … 71 94 72 95 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' ); 75 98 } 99 100 $body = 'OK=90'; 101 $status = 400; 76 102 77 103 try { 78 104 $this->validate_netdebit_gw_response_params( $payload ); 79 105 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(); 80 112 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() ?: ''; 103 118 } 104 119 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 } 105 129 } 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 ); 111 131 } finally { 112 // Release the advisory lock . No-op if the connection already died113 // (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). 114 134 $wpdb->query( $wpdb->prepare( 'SELECT RELEASE_LOCK(%s)', $lock_name ) ); 115 135 } 136 137 $this->respondRaw( $body, $status ); 116 138 } 117 139 -
netdebit-for-paid-memberships-pro/tags/1.1.0/netdebit-for-paid-memberships-pro.php
r3674556 r3798535 4 4 * Plugin URI: https://wordpress.org/plugins/netdebit-for-pmpro/ 5 5 * Description: Integrates NetDebit Payment with PaidMembership Pro 6 * Version: 1. 0.16 * Version: 1.1.0 7 7 * Requires at least: 6.0 8 8 * Requires PHP: 8.0 … … 24 24 if ( ! defined( 'NETDEBIT_PMPRO_PLUGIN_URL' ) ) { 25 25 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' ); 27 27 define( 'NETDEBIT_PMPRO_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); 28 28 define( 'NETDEBIT_PMPRO_BASENAME', plugin_basename( __FILE__ ) ); -
netdebit-for-paid-memberships-pro/tags/1.1.0/readme.txt
r3674556 r3798535 5 5 Tested up to: 7.1 6 6 Requires PHP: 8.0 7 Stable tag: 1. 0.17 Stable tag: 1.1.0 8 8 License: GPL-2.0+ 9 9 License URI: http://www.gnu.org/licenses/gpl-2.0.txt … … 31 31 * Clean UI: Removes unnecessary form fields for a higher conversion rate. 32 32 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 33 37 * Users with active chargebacks are automatically blocked from accessing content. 34 38 … … 44 48 45 49 == Frequently Asked Questions == 50 51 = Can I offer free memberships? = 52 53 Yes. 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. 46 54 47 55 = Do I need a NetDebit partner account? = … … 65 73 66 74 == Upgrade Notice == 75 = 1.1.0 = 76 Free 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 = 79 The webhook now answers with the raw expected body (OK=100) instead of a JSON-encoded string. 80 81 = 1.0.2 = 82 Fixes 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 67 84 = 1.0.1 = 68 85 Tested with WordPress 7.1 and Paid Memberships Pro 3.7.2. … … 90 107 91 108 == 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). 92 125 93 126 = 1.0.1 = -
netdebit-for-paid-memberships-pro/trunk/includes/Gateway/PMProNetDebitGateway.php
r3674556 r3798535 6 6 use MemberOrder; 7 7 use NetDebit\PMPro\Admin\SettingsPage; 8 use NetDebit\PMPro\Integration\FreeMembershipCheck; 8 9 use NetDebit\SDK\Billing\Access\Rate; 9 10 use NetDebit\SDK\Billing\Membership; … … 39 40 try { 40 41 $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 41 52 $recurring = $this->isRecurring($order); 42 53 $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; 44 60 45 61 if ($has_trial || $has_setup_fee) { -
netdebit-for-paid-memberships-pro/trunk/includes/Integration/CheckoutRedirect.php
r3674556 r3798535 13 13 14 14 use NetDebit\PMPro\Gateway\PMProNetDebitGateway; 15 use NetDebit\PMPro\Integration\FreeMembershipCheck; 15 16 16 17 class 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 17 38 /** 18 39 * Register hooks. … … 121 142 } 122 143 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 123 150 $order = new \MemberOrder(); 124 151 $order->membership_id = $level->id; -
netdebit-for-paid-memberships-pro/trunk/includes/Integration/GatewayForcer.php
r3659034 r3798535 20 20 */ 21 21 public function registerHooks(): void { 22 // Define the PMPro gateway class alias once PMPro is loaded. 23 add_action('plugins_loaded', [ $this, 'defineGatewayClassAlias' ], 5); 24 22 25 // Force NetDebit gateway selection 23 26 add_filter('pmpro_get_gateway', [ $this, 'forceGateway' ]); … … 43 46 add_filter('pmpro_include_billing_fields', '__return_false', Constants::FILTER_OVERRIDE); 44 47 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'; 45 75 } 46 76 -
netdebit-for-paid-memberships-pro/trunk/includes/Plugin.php
r3674556 r3798535 8 8 use NetDebit\PMPro\Core\Constants; 9 9 use NetDebit\PMPro\Integration\CheckoutRedirect; 10 use NetDebit\PMPro\Integration\FreeMembershipCheck; 10 11 use NetDebit\PMPro\Integration\GatewayForcer; 11 12 use NetDebit\PMPro\Integration\LevelSanitizer; 12 13 use NetDebit\PMPro\Integration\MemberActionModifier; 13 14 use NetDebit\PMPro\Integration\SecurityConfig; 15 use NetDebit\PMPro\Setup\BusinessSettings; 14 16 use NetDebit\PMPro\Setup\DatabaseMigration; 15 17 use NetDebit\PMPro\WebHooks\WebHookHandler; … … 132 134 global $pmpro_level; 133 135 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; 134 141 135 142 $price_label = function_exists('pmpro_formatPrice') ? pmpro_formatPrice($pmpro_level->initial_payment) : $pmpro_level->initial_payment; … … 188 195 { 189 196 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 ) ) { 190 204 return; 191 205 } … … 410 424 411 425 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(); 412 431 } 413 432 414 433 public static function deactivate(): void 415 434 { 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(); 417 439 } 418 440 } -
netdebit-for-paid-memberships-pro/trunk/includes/WebHooks/WebHookHandler.php
r3674556 r3798535 51 51 } 52 52 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 53 76 public function handleBooking( WP_REST_Request $request ) { 54 77 global $wpdb; … … 71 94 72 95 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' ); 75 98 } 99 100 $body = 'OK=90'; 101 $status = 400; 76 102 77 103 try { 78 104 $this->validate_netdebit_gw_response_params( $payload ); 79 105 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(); 80 112 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() ?: ''; 103 118 } 104 119 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 } 105 129 } 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 ); 111 131 } finally { 112 // Release the advisory lock . No-op if the connection already died113 // (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). 114 134 $wpdb->query( $wpdb->prepare( 'SELECT RELEASE_LOCK(%s)', $lock_name ) ); 115 135 } 136 137 $this->respondRaw( $body, $status ); 116 138 } 117 139 -
netdebit-for-paid-memberships-pro/trunk/netdebit-for-paid-memberships-pro.php
r3674556 r3798535 4 4 * Plugin URI: https://wordpress.org/plugins/netdebit-for-pmpro/ 5 5 * Description: Integrates NetDebit Payment with PaidMembership Pro 6 * Version: 1. 0.16 * Version: 1.1.0 7 7 * Requires at least: 6.0 8 8 * Requires PHP: 8.0 … … 24 24 if ( ! defined( 'NETDEBIT_PMPRO_PLUGIN_URL' ) ) { 25 25 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' ); 27 27 define( 'NETDEBIT_PMPRO_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); 28 28 define( 'NETDEBIT_PMPRO_BASENAME', plugin_basename( __FILE__ ) ); -
netdebit-for-paid-memberships-pro/trunk/readme.txt
r3674556 r3798535 5 5 Tested up to: 7.1 6 6 Requires PHP: 8.0 7 Stable tag: 1. 0.17 Stable tag: 1.1.0 8 8 License: GPL-2.0+ 9 9 License URI: http://www.gnu.org/licenses/gpl-2.0.txt … … 31 31 * Clean UI: Removes unnecessary form fields for a higher conversion rate. 32 32 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 33 37 * Users with active chargebacks are automatically blocked from accessing content. 34 38 … … 44 48 45 49 == Frequently Asked Questions == 50 51 = Can I offer free memberships? = 52 53 Yes. 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. 46 54 47 55 = Do I need a NetDebit partner account? = … … 65 73 66 74 == Upgrade Notice == 75 = 1.1.0 = 76 Free 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 = 79 The webhook now answers with the raw expected body (OK=100) instead of a JSON-encoded string. 80 81 = 1.0.2 = 82 Fixes 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 67 84 = 1.0.1 = 68 85 Tested with WordPress 7.1 and Paid Memberships Pro 3.7.2. … … 90 107 91 108 == 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). 92 125 93 126 = 1.0.1 =
Note:
See TracChangeset
for help on using the changeset viewer.