PluginProbe
Better Payment – Instant Payments, Donations, Fundraising with Subscriptions & More / 2.3.3
Better Payment – Instant Payments, Donations, Fundraising with Subscriptions & More v2.3.3
2.3.4 2.3.3 2.3.2 2.3.1 2.3.0 2.2.2 2.2.1 2.2.0 2.1.2 2.1.1 trunk 0.0.1 0.0.2 0.0.3 0.0.4 0.0.5 0.0.6 0.0.7 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 All 66 releases
better-payment / includes / WooCommerce / Gateway.php

Gateway.php in Better Payment – Instant Payments, Donations, Fundraising with Subscriptions & More 2.3.3, at includes/WooCommerce/Gateway.php

395 lines 17.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Better_Payment\Lite\WooCommerce;
4
5 use Better_Payment\Lite\Admin\DB;
6 use Better_Payment\Lite\Classes\Handler;
7 use Better_Payment\Lite\Classes\StripeService;
8
9 /**
10 * Exit if accessed directly
11 */
12 if ( ! defined( 'ABSPATH' ) ) {
13 exit;
14 }
15
16 /**
17 * Better Payment (Stripe) — WooCommerce payment gateway.
18 *
19 * A redirect gateway over the Better Payment payment engine. It owns only
20 * the WooCommerce side: settings (enable/title/description — Stripe
21 * credentials stay in Better Payment → Settings → Stripe), order mapping,
22 * and the redirect. Payment creation, verification, transactions, hooks and
23 * the payment lifecycle are the engine's (see OrderHandler + Classes\*).
24 *
25 * Only loaded when WooCommerce is active (see Loader::register()).
26 *
27 * @since 2.4.0
28 */
29 class Gateway extends \WC_Payment_Gateway {
30
31 /**
32 * Gateway id.
33 */
34 const GATEWAY_ID = 'better_payment_stripe';
35
36 /**
37 * Constructor.
38 */
39 public function __construct() {
40 $this->id = self::GATEWAY_ID;
41 $this->has_fields = false;
42 $this->method_title = __( 'Better Payment (Stripe)', 'better-payment' );
43 $this->method_description = __( 'Accept payments through Better Payment\'s existing Stripe integration. Stripe credentials are managed in Better Payment → Settings → Stripe.', 'better-payment' );
44 $this->supports = array( 'products' );
45 $this->icon = '';
46
47 $this->init_form_fields();
48 $this->init_settings();
49
50 $this->title = $this->get_option( 'title' );
51 $this->description = $this->get_option( 'description' );
52
53 add_action(
54 'woocommerce_update_options_payment_gateways_' . $this->id,
55 function () {
56 $this->process_admin_options();
57 }
58 );
59 add_action( 'woocommerce_receipt_' . $this->id, array( $this, 'receipt_page' ) );
60 }
61
62 /**
63 * Minimal settings — deliberately no Stripe credentials here. The
64 * gateway consumes Better Payment's existing Stripe configuration.
65 *
66 * @return void
67 */
68 public function init_form_fields() {
69 $this->form_fields = array(
70 'enabled' => array(
71 'title' => __( 'Enable/Disable', 'better-payment' ),
72 'type' => 'checkbox',
73 'label' => __( 'Enable Better Payment (Stripe)', 'better-payment' ),
74 'default' => 'no',
75 ),
76 'title' => array(
77 'title' => __( 'Title', 'better-payment' ),
78 'type' => 'text',
79 'description' => __( 'The payment method title customers see at checkout.', 'better-payment' ),
80 'default' => __( 'Better Payment', 'better-payment' ),
81 'desc_tip' => true,
82 ),
83 'description' => array(
84 'title' => __( 'Description', 'better-payment' ),
85 'type' => 'textarea',
86 'description' => __( 'The payment method description customers see at checkout.', 'better-payment' ),
87 'default' => __( 'Pay securely using Stripe via Better Payment.', 'better-payment' ),
88 'desc_tip' => true,
89 ),
90 );
91 }
92
93 /**
94 * URL of the Better Payment Stripe settings screen.
95 *
96 * @return string
97 */
98 public static function bp_stripe_settings_url() {
99 return admin_url( 'admin.php?page=better-payment-admin&tab=settings&section=stripe' );
100 }
101
102 /**
103 * Settings screen: configuration notice + the standard options table.
104 *
105 * @return void
106 */
107 public function admin_options() {
108 ?>
109 <h2><?php echo esc_html( $this->get_method_title() ); ?></h2>
110 <p><?php echo esc_html( $this->get_method_description() ); ?></p>
111 <div class="notice notice-info inline" style="margin: 12px 0; padding: 12px;">
112 <p style="margin: 0 0 8px;">
113 <strong><?php esc_html_e( 'This gateway uses Better Payment\'s existing Stripe configuration.', 'better-payment' ); ?></strong><br>
114 <?php esc_html_e( 'Manage your Stripe account from Better Payment → Settings → Stripe.', 'better-payment' ); ?>
115 </p>
116 <?php if ( ! StripeService::is_configured() ) : ?>
117 <p style="margin: 0 0 8px; color: #b32d2e;">
118 <?php esc_html_e( 'Stripe keys are not configured yet — the gateway will stay hidden at checkout until they are.', 'better-payment' ); ?>
119 </p>
120 <?php endif; ?>
121 <a href="<?php echo esc_url( self::bp_stripe_settings_url() ); ?>" class="button button-secondary">
122 <?php esc_html_e( 'Open Better Payment Stripe Settings', 'better-payment' ); ?>
123 </a>
124 </div>
125 <table class="form-table">
126 <?php $this->generate_settings_html(); ?>
127 </table>
128 <?php
129 }
130
131 /**
132 * Available only when enabled AND Better Payment's Stripe keys exist
133 * for the currently selected mode.
134 *
135 * @return bool
136 */
137 public function is_available() {
138 return parent::is_available() && StripeService::is_configured();
139 }
140
141 /**
142 * Create the Better Payment payment request for the order and redirect
143 * to Stripe Checkout.
144 *
145 * @param int $order_id WooCommerce order id.
146 * @return array
147 */
148 public function process_payment( $order_id ) {
149 $order = wc_get_order( $order_id );
150
151 if ( ! $order ) {
152 wc_add_notice( __( 'Unable to process the order. Please try again.', 'better-payment' ), 'error' );
153 return array( 'result' => 'failure' );
154 }
155
156 $keys = StripeService::get_global_keys();
157
158 if ( empty( $keys['secret_key'] ) || empty( $keys['public_key'] ) ) {
159 wc_add_notice( __( 'This payment method is not configured. Please choose another payment method.', 'better-payment' ), 'error' );
160 OrderHandler::log( 'process_payment blocked for order #' . $order->get_id() . ': Stripe keys missing.', 'error' );
161 return array( 'result' => 'failure' );
162 }
163
164 // A $0 subscription order is a free-trial checkout: there is nothing
165 // to charge, but the subscription must still activate — and, when
166 // automatic renewal is on, the card must still be collected (via a
167 // Stripe setup-mode session) so the first payment can be charged
168 // off-session when the trial ends.
169 $is_subscription_order = Subscriptions::order_contains_subscription( $order );
170 $is_free_subscription_order = $is_subscription_order && (float) $order->get_total() < 0.01;
171
172 if ( $is_free_subscription_order && ! Subscriptions::should_save_payment_method() ) {
173 // Site-wide manual renewal policy: no card is ever stored, so
174 // there is nothing to send the customer to Stripe for. Complete
175 // the free order and activate the subscription directly; the
176 // first payment is invoiced when the trial ends (the same
177 // manual-renewal path every renewal takes on this policy).
178 $order->add_order_note( __( 'Better Payment: free trial checkout — nothing to charge. Renewal payments will be invoiced for manual payment.', 'better-payment' ) );
179 $order->payment_complete();
180 $order->save();
181
182 if ( function_exists( 'wc_empty_cart' ) ) {
183 wc_empty_cart();
184 }
185
186 OrderHandler::log( 'Free trial order #' . $order->get_id() . ' completed without a Stripe session (manual renewal policy).' );
187
188 // The module's own completion contract — activates the
189 // subscription (see Subscriptions::on_order_paid()).
190 do_action( 'better_payment/woocommerce/payment_complete', $order, null );
191
192 return array(
193 'result' => 'success',
194 'redirect' => $this->get_return_url( $order ),
195 );
196 }
197
198 // Same id scheme as every existing Better Payment Stripe surface.
199 $bp_order_id = 'stripe_' . uniqid();
200
201 $success_url = add_query_arg(
202 array(
203 'better_payment_stripe_status' => 'success',
204 'better_payment_stripe_id' => $bp_order_id,
205 ),
206 $this->get_return_url( $order )
207 );
208
209 $stripe_customer_id = '';
210
211 if ( $is_free_subscription_order ) {
212 // Setup-mode session: collect + save the card without charging.
213 // The customer object is created up front — a setup session only
214 // attaches the payment method to a customer the caller supplies.
215 $customer = StripeService::create_customer(
216 array(
217 'email' => $order->get_billing_email(),
218 'name' => trim( $order->get_billing_first_name() . ' ' . $order->get_billing_last_name() ),
219 'metadata' => array( 'wc_order_id' => (string) $order->get_id() ),
220 ),
221 $keys['secret_key']
222 );
223
224 if ( is_wp_error( $customer ) ) {
225 wc_add_notice( __( 'The payment could not be started. Please try again or choose another payment method.', 'better-payment' ), 'error' );
226 OrderHandler::log( 'Stripe customer creation failed for free trial order #' . $order->get_id() . ': ' . $customer->get_error_message(), 'error' );
227 return array( 'result' => 'failure' );
228 }
229
230 $stripe_customer_id = (string) $customer->id;
231
232 $session_request = Subscriptions::build_setup_session_request(
233 array(
234 'bp_order_id' => $bp_order_id,
235 'wc_order_id' => $order->get_id(),
236 'success_url' => $success_url,
237 'cancel_url' => $order->get_cancel_order_url_raw(),
238 'customer' => $stripe_customer_id,
239 )
240 );
241 } else {
242 $session_request = OrderHandler::build_session_request(
243 array(
244 'bp_order_id' => $bp_order_id,
245 'wc_order_id' => $order->get_id(),
246 'order_number' => $order->get_order_number(),
247 'amount' => (float) $order->get_total(),
248 'currency' => $order->get_currency(),
249 'success_url' => $success_url,
250 'cancel_url' => $order->get_cancel_order_url_raw(),
251 'customer_email' => $order->get_billing_email(),
252 'customer_name' => trim( $order->get_billing_first_name() . ' ' . $order->get_billing_last_name() ),
253 'site_name' => get_bloginfo( 'name' ),
254 )
255 );
256
257 // Subscription carts additionally ask Stripe to keep the payment
258 // method reusable for off-session renewals (see Subscriptions) —
259 // but only while automatic renewal is enabled site-wide. With a
260 // manual renewal policy the card would be stored without ever being
261 // charged, so it is not stored at all; renewals are invoiced instead.
262 if ( $is_subscription_order && Subscriptions::should_save_payment_method() ) {
263 $session_request = Subscriptions::add_off_session_setup( $session_request );
264 }
265 }
266
267 OrderHandler::log( 'Payment started for order #' . $order->get_id() . ' (' . $bp_order_id . ').' );
268
269 $session = StripeService::create_checkout_session( $session_request, $keys['secret_key'] );
270
271 if ( is_wp_error( $session ) ) {
272 wc_add_notice( __( 'The payment could not be started. Please try again or choose another payment method.', 'better-payment' ), 'error' );
273 $order->add_order_note(
274 sprintf(
275 /* translators: %s: error message */
276 __( 'Better Payment: could not create the Stripe Checkout Session — %s', 'better-payment' ),
277 $session->get_error_message()
278 )
279 );
280 OrderHandler::log( 'Checkout Session creation failed for order #' . $order->get_id() . ': ' . $session->get_error_message(), 'error' );
281 return array( 'result' => 'failure' );
282 }
283
284 // Persist the transaction through the engine's own writer.
285 $transaction_data = OrderHandler::build_transaction_data(
286 array(
287 'bp_order_id' => $bp_order_id,
288 'wc_order_id' => $order->get_id(),
289 'order_number' => $order->get_order_number(),
290 'amount' => (float) $order->get_total(),
291 'currency' => $order->get_currency(),
292 'customer_email' => $order->get_billing_email(),
293 'customer_name' => trim( $order->get_billing_first_name() . ' ' . $order->get_billing_last_name() ),
294 ),
295 $session
296 );
297
298 if ( $is_free_subscription_order ) {
299 // A setup-mode session reports payment_status
300 // 'no_payment_required' from the moment it is CREATED — before
301 // the customer has entered any card. The row must start 'unpaid'
302 // so the return-side verification
303 // (OrderHandler::verify_setup_return()) is the single-shot flip.
304 $transaction_data['status'] = 'unpaid';
305 }
306
307 $transaction_row_id = Handler::payment_create( $transaction_data );
308
309 if ( ! $transaction_row_id ) {
310 wc_add_notice( __( 'The payment could not be started. Please try again or choose another payment method.', 'better-payment' ), 'error' );
311 OrderHandler::log( 'Transaction row insert failed for order #' . $order->get_id() . '.', 'error' );
312 return array( 'result' => 'failure' );
313 }
314
315 // Integration metadata only — no new tables.
316 $order->update_meta_data( '_bp_transaction_id', (string) $transaction_row_id );
317 $order->update_meta_data( '_bp_payment_id', $bp_order_id );
318 $order->update_meta_data( '_bp_gateway', 'stripe' );
319 $order->update_meta_data( '_bp_payment_status', $is_free_subscription_order || empty( $session->payment_status ) ? 'unpaid' : sanitize_text_field( $session->payment_status ) );
320
321 if ( '' !== $stripe_customer_id ) {
322 // The customer id is known now; the payment method arrives with
323 // the completed setup session (harvest_payment_method()).
324 $order->update_meta_data( Subscriptions::CUSTOMER_META, sanitize_text_field( $stripe_customer_id ) );
325 }
326 $order->add_order_note(
327 sprintf(
328 /* translators: 1: Better Payment order id, 2: Stripe Checkout Session id */
329 __( 'Better Payment: redirecting to Stripe Checkout. Payment ID: %1$s, Session: %2$s.', 'better-payment' ),
330 $bp_order_id,
331 sanitize_text_field( $session->id )
332 )
333 );
334 $order->save();
335
336 if ( function_exists( 'wc_empty_cart' ) ) {
337 wc_empty_cart();
338 }
339
340 // Newer Stripe API versions return a hosted URL; the pinned legacy
341 // version may not — fall back to the receipt page, which redirects
342 // via Stripe.js exactly like every existing Better Payment surface.
343 $redirect = ! empty( $session->url ) ? $session->url : $order->get_checkout_payment_url( true );
344
345 OrderHandler::log( 'Redirecting order #' . $order->get_id() . ' to Stripe Checkout (' . ( ! empty( $session->url ) ? 'hosted url' : 'Stripe.js' ) . ').' );
346
347 return array(
348 'result' => 'success',
349 'redirect' => $redirect,
350 );
351 }
352
353 /**
354 * Receipt page — Stripe.js redirect fallback for sessions without a
355 * hosted URL. Mirrors the redirectToCheckout mechanism the existing
356 * surfaces use.
357 *
358 * @param int $order_id WooCommerce order id.
359 * @return void
360 */
361 public function receipt_page( $order_id ) {
362 $order = wc_get_order( $order_id );
363
364 if ( ! $order || ! $order->needs_payment() ) {
365 return;
366 }
367
368 $row_id = (int) $order->get_meta( '_bp_transaction_id' );
369 $row = $row_id ? DB::get_transaction( $row_id ) : null;
370 $keys = StripeService::get_global_keys();
371
372 if ( empty( $row->obj_id ) || empty( $keys['public_key'] ) ) {
373 echo '<p>' . esc_html__( 'The payment session could not be found. Please try placing the order again.', 'better-payment' ) . '</p>';
374 return;
375 }
376
377 if ( wp_script_is( 'better-payment-stripe', 'registered' ) ) {
378 wp_enqueue_script( 'better-payment-stripe' );
379 } else {
380 // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion -- external SDK, unversioned by design (matches Assets.php).
381 wp_enqueue_script( 'better-payment-stripe', 'https://js.stripe.com/v3/', array(), null, true );
382 }
383
384 $inline = sprintf(
385 '(function(){if(typeof Stripe==="undefined"){return;}Stripe(%s).redirectToCheckout({sessionId:%s});})();',
386 wp_json_encode( $keys['public_key'] ),
387 wp_json_encode( $row->obj_id )
388 );
389 wp_add_inline_script( 'better-payment-stripe', $inline );
390
391 echo '<p>' . esc_html__( 'Redirecting to secure Stripe Checkout…', 'better-payment' ) . '</p>';
392 echo '<p><a href="' . esc_url( $order->get_checkout_payment_url( true ) ) . '" class="button">' . esc_html__( 'Click here if you are not redirected automatically.', 'better-payment' ) . '</a></p>';
393 }
394 }
395