PluginProbe
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses / 4.3.9.1
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses v4.3.9.1
4.4.8 4.4.7 4.4.6 4.4.5 4.4.4 4.4.3 4.4.2 4.4.1 4.4.0 4.3.9.1 4.3.9 4.3.8 4.3.7 4.1.6.9 4.1.6.9.1 4.1.6.9.2 4.1.6.9.3 4.1.6.9.4 4.1.7 4.1.7.1 4.1.7.2 4.1.7.3 4.1.7.3.1 4.1.7.3.2 4.2.0 All 139 releases
learnpress / inc / gateways / class-lp-gateway-abstract.php

class-lp-gateway-abstract.php in LearnPress – WordPress LMS Plugin for Create and Sell Online Courses 4.3.9.1, at inc/gateways/class-lp-gateway-abstract.php

1,148 lines 35.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Class LP_Gateway_Abstract
5 *
6 * @author ThimPress
7 * @package LearnPress/Classes
8 * @version 1.0
9 */
10
11 if ( ! defined( 'ABSPATH' ) ) {
12 exit;
13 }
14
15 class LP_Gateway_Abstract extends LP_Abstract_Settings {
16
17 /**
18 * Shared subscription order meta keys.
19 */
20 const META_SUBSCRIPTION_ID = '_lp_subscription_id';
21 const META_SUBSCRIPTION_CUSTOMER_ID = '_lp_subscription_customer_id';
22 const META_SUBSCRIPTION_PLAN_ID = '_lp_subscription_plan_id';
23 const META_SUBSCRIPTION_QUANTITY = '_lp_subscription_quantity';
24 const META_SUBSCRIPTION_STATUS = '_lp_subscription_status';
25 const META_SUBSCRIPTION_STATUS_TMP = '_lp_subscription_status_tmp';
26 const META_SUBSCRIPTION_RENEWAL_KEY = '_lp_subscription_renewal_key';
27 const META_SUBSCRIPTION_LAST_EVENT_ID = '_lp_subscription_last_event_id';
28 const META_SUBSCRIPTION_EVENT_ID = '_lp_subscription_event_id';
29 const META_SUBSCRIPTION_MANAGE_URL = '_lp_subscription_manage_url';
30 const META_SUBSCRIPTION_DATA_RECEIVER = '_lp_subscription_data_receiver';
31 const META_SUBSCRIPTION_DATA_PAYMENT_SUCCESS = '_lp_subscription_data_payment_success';
32
33 /**
34 * @var null|string
35 */
36 public $id = null;
37 /**
38 * @var LP_Settings
39 */
40 protected $settings;
41 /**
42 * Name of gateway will be displayed in admin settings.
43 *
44 * @var string
45 */
46 protected $method_title = '';
47
48 /**
49 * Description of gateway will be displayed in admin settings.
50 *
51 * @var string
52 */
53 protected $method_description = '';
54
55 /**
56 * @var string
57 */
58 public $order_button_text = '';
59
60 /**
61 * This payment is turn on or off?
62 *
63 * @var string
64 */
65 public $enabled = 'no';
66
67 /**
68 * @var null
69 */
70 public $title = null;
71
72 /**
73 * @var null
74 */
75 public $description = null;
76
77 /**
78 * @var string
79 */
80 protected $icon = '';
81 /**
82 * @var bool set default select when checkout
83 */
84 public $is_selected = false;
85
86 /**
87 * Constructor
88 */
89 public function __construct() {
90 /*
91 if ( ! $this->admin_name ) {
92 $this->admin_name = preg_replace( '!LP_Gateway_!', '', get_class( $this ) );
93 }*/
94
95 if ( ! $this->id ) {
96 $this->id = sanitize_title( $this->title );
97 }
98
99 $this->settings = LP_Settings::instance()->get_group( $this->id );
100 $this->enabled = $this->settings->get( 'enable', 'no' );
101
102 add_filter( 'learn-press/admin/get-settings/admin-options-' . $this->id, array( $this, 'get_settings' ) );
103 }
104
105 /**
106 * Return unique Id of payment
107 *
108 * @return null|string
109 */
110 public function get_id() {
111 return $this->id;
112 }
113
114 /**
115 * Return method title.
116 *
117 * @return string
118 */
119 public function get_method_title() {
120 return $this->method_title;
121 }
122
123 /**
124 * Return method description.
125 *
126 * @return string
127 */
128 public function get_method_description() {
129 return $this->method_description;
130 }
131
132 /**
133 * Return method title displays in front end.
134 *
135 * @return string
136 */
137 public function get_title() {
138 return apply_filters( 'learn_press_gateway_title', $this->title, $this->id );
139 }
140
141 /**
142 * Return method description displays in front end.
143 *
144 * @return string
145 */
146 public function get_description() {
147 return apply_filters( 'learn_press_gateway_description', $this->description, $this->id );
148 }
149
150 /**
151 * Payment is turn on or off?
152 *
153 * @return bool
154 */
155 public function is_enabled() {
156 return $this->enabled === 'yes';
157 }
158
159 public function enable( $status ) {
160 if ( is_bool( $status ) ) {
161 $this->enabled = $status;
162
163 $options = get_option( 'learn_press_' . $this->get_id() );
164
165 if ( ! $options ) {
166 $options = array();
167 }
168
169 $options['enable'] = $status ? 'yes' : 'no';
170 update_option( 'learn_press_' . $this->get_id(), $options );
171 }
172
173 return $this->enabled == 'yes';
174 }
175
176 /**
177 * Process the payment.
178 *
179 * @param $order_id
180 *
181 * @return array
182 */
183 public function process_payment( $order_id ) {
184 return array();
185 }
186
187 /**
188 * Check if order should use subscription flow.
189 *
190 * Integrations decide via filter `learn-press/gateway/subscription-order`.
191 *
192 * @param LP_Order $order
193 *
194 * @return bool
195 * @deprecated 4.3.8 Use is_data_for_payment_subscription() instead.
196 */
197 /*public function is_subscription_order( LP_Order $order ): bool {
198
199 $order_id = $order->get_id();
200 $saved_price_id = sanitize_text_field( (string) get_post_meta( $order_id, self::META_SUBSCRIPTION_PLAN_ID, true ) );
201 if ( ! empty( $saved_price_id ) ) {
202 return true;
203 }
204 $is_subscription = (bool) apply_filters(
205 'learn-press/gateway/subscription-order',
206 false,
207 $order,
208 $this
209 );
210
211 return $is_subscription;
212 }*/
213
214 /**
215 * Get subscription context for provider APIs.
216 *
217 * Returns a gateway-agnostic payload that child gateways can pass to
218 * `pay_subscription()` after optional gateway-specific normalization.
219 *
220 * @param LP_Order $order
221 *
222 * @return array
223 * @deprecated 4.3.8
224 */
225 /*public function get_subscription_context( LP_Order $order ): array {
226 $order_id = $order->get_id();
227
228 $context = array(
229 'price_id' => get_post_meta( $order_id, self::META_SUBSCRIPTION_PLAN_ID, true ),
230 'quantity' => (int) get_post_meta( $order_id, self::META_SUBSCRIPTION_QUANTITY, true ),
231 'success_url' => $this->get_return_url( $order ),
232 'cancel_url' => learn_press_get_page_link( 'checkout' ),
233 'metadata' => array(
234 'lp_order_id' => (string) $order_id,
235 'lp_order_key' => (string) $order->get_order_key(),
236 'lp_gateway' => $this->get_id(),
237 'lp_user_id' => (string) $order->get_user_id(),
238 'lp_order_type' => 'subscription',
239 ),
240 );
241
242 if ( empty( $context['quantity'] ) ) {
243 $context['quantity'] = 1;
244 }
245
246 return (array) apply_filters( 'learn-press/gateway/subscription-context', $context, $order, $this );
247 }*/
248
249 /**
250 * Persist subscription identifiers to order before payment execution.
251 *
252 * This allows payment methods to operate only on normalized parameters and
253 * avoid re-detecting custom integration attributes inside gateway code.
254 *
255 * @param LP_Order $order
256 * @param array $data
257 *
258 * @return void
259 * @deprecated 4.3.8
260 */
261 /*protected function persist_subscription_payment_identifiers( LP_Order $order, array $data ) {
262
263 $order_id = $order->get_id();
264
265 update_post_meta( $order_id, self::META_SUBSCRIPTION_PLAN_ID, sanitize_text_field( (string) ( $data['price_id'] ?? '' ) ) );
266 update_post_meta( $order_id, self::META_SUBSCRIPTION_QUANTITY, max( 1, absint( $data['quantity'] ?? 1 ) ) );
267 }*/
268 /**
269 * Resolve normalized subscription payment params from order context.
270 *
271 * Behavior:
272 * - If order contains persisted subscription identifiers, treat it as
273 * subscription payment.
274 * - If order is marked subscription by integration filter but has no
275 * `price_id`, return a validation error early.
276 * - Otherwise return empty array (one-time payment flow).
277 *
278 * @param LP_Order $order
279 *
280 * @return array
281 * @throws Exception
282 * @deprecated 4.3.8 Use is_data_for_payment_subscription() instead.
283 */
284 /*public function resolve_subscription_payment_data( LP_Order $order ): array {
285 $context = $this->get_subscription_context( $order );
286 $context = wp_parse_args(
287 $context,
288 array(
289 'price_id' => '',
290 'quantity' => 1,
291 'success_url' => '',
292 'cancel_url' => '',
293 'metadata' => array(),
294 )
295 );
296
297 $context['price_id'] = sanitize_text_field( (string) $context['price_id'] );
298 $context['quantity'] = max( 1, absint( $context['quantity'] ) );
299 $context['metadata'] = is_array( $context['metadata'] ) ? $context['metadata'] : array();
300
301 if ( ! empty( $context['price_id'] ) ) {
302 $this->persist_subscription_payment_identifiers( $order, $context );
303 return $context;
304 }
305
306 if ( $this->is_subscription_order( $order ) ) {
307 throw new Exception( __( 'Missing subscription price id.', 'learnpress' ) );
308 }
309
310 return array();
311 }*/
312
313 /**
314 * Check data is type payment for subscription
315 * If LP order has data plan id, return data subscription
316 *
317 * @return false|array
318 * @since 4.3.8
319 * @version 1.0.0
320 */
321 public function is_data_for_payment_subscription( LP_Order $lp_order ) {
322 $data_subscription = [
323 'plan_id' => '',
324 'success_url' => $this->get_return_url( $lp_order ),
325 'cancel_url' => LP_Helper::get_link_no_cache( learn_press_get_page_link( 'checkout' ) ),
326 ];
327
328 // Check LP order has data plan id
329 $plan_id = get_post_meta( $lp_order->get_id(), self::META_SUBSCRIPTION_PLAN_ID, true );
330 if ( empty( $plan_id ) ) {
331 return false;
332 }
333
334 $data_subscription['plan_id'] = $plan_id;
335
336 return apply_filters(
337 'learn-press/gateway/subscription-payment-data',
338 $data_subscription,
339 $lp_order,
340 $this
341 );
342 }
343
344 /**
345 * Normalize and validate the shared subscription checkout payload.
346 *
347 * This method is intentionally gateway-agnostic and is used by child gateways
348 * (e.g. Stripe/PayPal) before they build provider-specific API requests.
349 *
350 * Payload contract:
351 * - `price_id` (string, required): provider-side configured recurring price/plan id.
352 * - `quantity` (int): defaults to 1 when missing/invalid/zero.
353 * - `success_url` / `cancel_url` (string, required): absolute callback URLs.
354 * - `metadata` (array): optional identifiers (order/user/etc.) for reconciliation.
355 *
356 * @param array $data
357 *
358 * @return array Normalized payload array.
359 * @throws Exception
360 * @deprecated 4.3.8
361 */
362 /*protected function validate_subscription_payload( array $data ): array {
363 // Apply safe defaults to guarantee a stable input shape.
364 $data = wp_parse_args(
365 $data,
366 array(
367 'price_id' => '',
368 'quantity' => 1,
369 'success_url' => '',
370 'cancel_url' => '',
371 'metadata' => array(),
372 )
373 );
374
375 // Scalar sanitation/coercion for fields commonly coming from request context.
376 $data['price_id'] = sanitize_text_field( wp_unslash( (string) $data['price_id'] ) );
377 $data['quantity'] = absint( $data['quantity'] );
378 if ( empty( $data['quantity'] ) ) {
379 $data['quantity'] = 1;
380 }
381
382 // URLs are stored in raw form for outbound provider API requests.
383 $data['success_url'] = esc_url_raw( (string) $data['success_url'] );
384 $data['cancel_url'] = esc_url_raw( (string) $data['cancel_url'] );
385
386 // Defensive normalization for optional structured fields.
387 $data['metadata'] = is_array( $data['metadata'] ) ? $data['metadata'] : array();
388
389 // price_id is the minimum provider binding required for subscription checkout.
390 if ( empty( $data['price_id'] ) ) {
391 throw new Exception( __( 'Missing subscription price id.', 'learnpress' ) );
392 }
393
394 // Redirect URLs are mandatory for provider-hosted checkout flows.
395 if ( empty( $data['success_url'] ) || empty( $data['cancel_url'] ) ) {
396 throw new Exception( __( 'Missing subscription return URLs.', 'learnpress' ) );
397 }
398
399 return $data;
400 }*/
401
402 /**
403 * Generic subscription checkout flow.
404 *
405 * Child gateways should override this method and return a payload with at
406 * least `status` and `redirect_url` on success.
407 *
408 * @param array $data
409 *
410 * @return array
411 * @throws Exception
412 */
413 /*public function pay_subscription( array $data ): array {
414 throw new Exception( sprintf( __( 'Gateway %s does not support subscription payment.', 'learnpress' ), $this->get_id() ) );
415 }*/
416
417 /**
418 * Generic subscription checkout flow.
419 *
420 * Child gateways should override this method and return a payload with at
421 * least `status` and `redirect_url` on success.
422 * $data required key: plan_id
423 *
424 * @param LP_Order $lp_order
425 * @param array $data
426 *
427 * @return array
428 * @throws Exception
429 * @since 4.3.8
430 * @version 1.0.0
431 */
432 public function pay_via_subscription( LP_Order $lp_order, array $data ): array {
433 throw new Exception( sprintf( __( 'Gateway %s does not support subscription payment.', 'learnpress' ), $this->get_id() ) );
434 }
435
436 /**
437 * Normalize and validate shared plan-creation payload.
438 *
439 * Common payload contract:
440 * - `name` (string, required when `product_id` is empty)
441 * - `amount` (float, required, > 0)
442 * - `currency` (string, required)
443 * - `interval` (day|week|month|year)
444 * - `interval_count` (int, default 1)
445 * - `setup_fee` (float, optional, >= 0)
446 * - `product_id` (string, optional)
447 * - `metadata` (array, optional)
448 *
449 * @param array $data
450 *
451 * @return array
452 * @throws Exception
453 * @since 4.3.7
454 * @version 1.0.1
455 */
456 protected function validate_data_plan_payload( array $data ): array {
457 $data = wp_parse_args(
458 $data,
459 array(
460 'name' => '', // Name of plan and product (if product_id is empty)
461 'amount' => 0,
462 'currency' => learn_press_get_currency(),
463 'interval' => 'month',
464 'interval_count' => 1,
465 'setup_fee' => 0,
466 'product_id' => '', // if empty, will create product, then create plan with product created
467 'metadata' => array(),
468 )
469 );
470 $data['name'] = LP_Helper::sanitize_params_submitted( $data['name'] );
471 $data['amount'] = (float) $data['amount'];
472 $data['currency'] = LP_Helper::sanitize_params_submitted( $data['currency'], 'key' );
473 $data['interval'] = LP_Helper::sanitize_params_submitted( $data['interval'], 'key' );
474 $data['interval_count'] = max( 1, absint( $data['interval_count'] ) );
475 $data['setup_fee'] = (float) $data['setup_fee'];
476 $data['product_id'] = LP_Helper::sanitize_params_submitted( $data['product_id'] );
477 $data['metadata'] = is_array( $data['metadata'] ) ? $data['metadata'] : array();
478
479 if ( empty( $data['name'] ) ) {
480 throw new Exception( __( 'Missing subscription plan name.', 'learnpress' ) );
481 }
482
483 if ( $data['amount'] <= 0 ) {
484 throw new Exception( __( 'Invalid subscription amount.', 'learnpress' ) );
485 }
486
487 if ( $data['setup_fee'] < 0 ) {
488 throw new Exception( __( 'Invalid subscription setup fee.', 'learnpress' ) );
489 }
490
491 if ( empty( $data['currency'] ) ) {
492 throw new Exception( __( 'Missing subscription currency.', 'learnpress' ) );
493 }
494
495 $allowed_intervals = array( 'day', 'week', 'month', 'year' );
496 if ( ! in_array( $data['interval'], $allowed_intervals, true ) ) {
497 throw new Exception( __( 'Invalid subscription interval.', 'learnpress' ) );
498 }
499
500 return $data;
501 }
502
503 /**
504 * Create plan of Payment provider.
505 *
506 * @param array $data
507 *
508 * @return array
509 * @throws Exception
510 */
511 public function create_plan( array $data ): array {
512 throw new Exception( sprintf( __( 'Gateway %s does not support subscription plan creation.', 'learnpress' ), $this->get_id() ) );
513 }
514
515 /**
516 * List provider plans/prices with optional filtering/pagination args.
517 *
518 * @param array $args
519 *
520 * @return array
521 * @throws Exception
522 */
523 public function list_plans( array $args = array() ): array {
524
525 throw new Exception( sprintf( __( 'Gateway %s does not support listing subscription plans.', 'learnpress' ), $this->get_id() ) );
526 }
527
528 /**
529 * Fetch provider plan/price details by plan id.
530 *
531 * Child gateways should override and return at least:
532 * - `plan`: raw provider response
533 * - `summary`: normalized fields used by integrations to compare updates
534 * (amount/currency/interval/interval_count/setup_fee/status)
535 *
536 * @param string $plan_id
537 *
538 * @return array
539 * @throws Exception
540 */
541 public function get_plan( string $plan_id ): array {
542 throw new Exception( sprintf( __( 'Gateway %s does not support fetching subscription plan.', 'learnpress' ), $this->get_id() ) );
543 }
544
545 /**
546 * Update provider plan details by plan id.
547 *
548 * @uses LP_Gateway_Paypal::update_plan
549 *
550 * @param string $plan_id
551 * @param array $data
552 *
553 * @return array
554 * @throws Exception
555 */
556 public function update_plan( string $plan_id, array $data ): array {
557 throw new Exception( sprintf( __( 'Gateway %s does not support updating subscription plan.', 'learnpress' ), $this->get_id() ) );
558 }
559
560 /**
561 * Delete/deactivate provider plan by plan id.
562 *
563 * @param string $plan_id
564 *
565 * @return array
566 * @throws Exception
567 */
568 public function delete_plan( string $plan_id ): array {
569 throw new Exception( sprintf( __( 'Gateway %s does not support deleting subscription plan.', 'learnpress' ), $this->get_id() ) );
570 }
571
572 /**
573 * Generic subscription webhook listener.
574 *
575 * Child gateways should override and orchestrate:
576 * verify -> normalize -> manager dispatch.
577 *
578 * @param WP_REST_Request $request
579 *
580 * @return array
581 * @throws Exception
582 * @deprecated 4.3.8 Use capture_subscription_webhook instead.
583 */
584 /*public function listen_webhook_subscription( WP_REST_Request $request ): array {
585 throw new Exception( sprintf( __( 'Gateway %s does not support subscription webhook.', 'learnpress' ), $this->get_id() ) );
586 }*/
587
588 /**
589 * Receive subscription webhook from provider.
590 *
591 * @throws Exception
592 *
593 * @since 4.3.7
594 * @version 1.0.0
595 */
596 public function capture_subscription_webhook( WP_REST_Request $request ) {
597 throw new Exception(
598 sprintf(
599 __( 'Gateway %s does not support subscription webhook.', 'learnpress' ),
600 $this->get_id()
601 )
602 );
603 }
604
605 /**
606 * Verify subscription webhook payload/signature with provider.
607 *
608 * Child gateways should return verified provider event payload/object.
609 *
610 * @param array $webhook_data Generic webhook data extracted from transport layer.
611 *
612 * @return array|object
613 * @throws Exception
614 * @deprecated 4.3.8
615 */
616 /*public function verify_subscription_webhook( array $webhook_data ) {
617
618 throw new Exception( sprintf( __( 'Gateway %s does not support subscription webhook verification.', 'learnpress' ), $this->get_id() ) );
619 }*/
620
621 /**
622 * Build normalized webhook data array from transport-specific REST request.
623 *
624 * Contract:
625 * - raw_body: raw payload string (for signature verification like Stripe).
626 * - body: decoded JSON array when $decode_body is true, otherwise null.
627 * - headers: map of required header keys (lowercase) to raw header values.
628 *
629 * @param WP_REST_Request $request
630 * @param array $required_headers
631 * @param bool $decode_body
632 *
633 * @return array
634 * @deprecated 4.3.8
635 */
636 /*protected function build_webhook_data_from_request( WP_REST_Request $request, array $required_headers = array(), bool $decode_body = true ): array {
637
638 $raw_body = (string) $request->get_body();
639 $headers = array();
640
641 foreach ( $required_headers as $required_header ) {
642 $required_header = strtolower( sanitize_key( (string) $required_header ) );
643 $headers[ $required_header ] = (string) $request->get_header( $required_header );
644 }
645
646 $body = null;
647 if ( $decode_body ) {
648 $body = LP_Helper::json_decode( $raw_body, true );
649 }
650
651 return array(
652 'raw_body' => $raw_body,
653 'body' => is_array( $body ) ? $body : null,
654 'headers' => $headers,
655 );
656 }*/
657
658 /**
659 * Validate normalized webhook payload contract before provider verification.
660 *
661 * This centralizes fail-fast checks so each gateway does not re-implement
662 * required key/header validation and accidentally diverge key names.
663 *
664 * @param array $webhook_data
665 * @param array $required_top_level_keys Allowed: raw_body, body, headers.
666 * @param array $required_headers
667 *
668 * @return void
669 * @throws Exception
670 * @deprecated 4.3.8
671 */
672 // protected function validate_webhook_data_contract( array $webhook_data, array $required_top_level_keys = array(), array $required_headers = array() ) {
673 //
674 // $missing = array();
675 //
676 // foreach ( $required_top_level_keys as $required_key ) {
677 // $required_key = sanitize_key( (string) $required_key );
678 //
679 // switch ( $required_key ) {
680 // case 'raw_body':
681 // if ( empty( $webhook_data['raw_body'] ) || ! is_string( $webhook_data['raw_body'] ) ) {
682 // $missing[] = 'raw_body';
683 // }
684 // break;
685 // case 'body':
686 // if ( empty( $webhook_data['body'] ) || ! is_array( $webhook_data['body'] ) ) {
687 // $missing[] = 'body';
688 // }
689 // break;
690 // case 'headers':
691 // if ( ! isset( $webhook_data['headers'] ) || ! is_array( $webhook_data['headers'] ) ) {
692 // $missing[] = 'headers';
693 // }
694 // break;
695 // }
696 // }
697 //
698 // $headers_map = is_array( $webhook_data['headers'] ?? null ) ? $webhook_data['headers'] : array();
699 // foreach ( $required_headers as $required_header ) {
700 // $required_header = strtolower( sanitize_key( (string) $required_header ) );
701 // $header_value = sanitize_text_field( (string) ( $headers_map[ $required_header ] ?? '' ) );
702 // if ( '' === $header_value ) {
703 // $missing[] = 'headers.' . $required_header;
704 // }
705 // }
706 //
707 // if ( ! empty( $missing ) ) {
708 // throw new Exception(
709 // sprintf(
710 // /* translators: %s: comma separated required webhook fields. */
711 // __( 'Invalid webhook request data: missing %s.', 'learnpress' ),
712 // implode( ', ', array_unique( $missing ) )
713 // ),
714 // 400
715 // );
716 // }
717 // }
718
719 /**
720 * Normalize provider webhook event to LP event payload.
721 *
722 * Child gateways should map provider-specific event types/fields into this
723 * canonical schema so Subscription Manager can process consistently.
724 *
725 * @param array|object $provider_event
726 *
727 * @return array
728 * @deprecated 4.3.8 Use normalize_subscription_data instead.
729 */
730 /*public function normalize_subscription_event( $provider_event ): array {
731 $event = array(
732 'event_id' => '',
733 'event_type' => '',
734 'subscription_id' => '',
735 'customer_id' => '',
736 'price_id' => '',
737 'parent_order_id' => 0,
738 'transaction_id' => '',
739 'amount' => 0,
740 'currency' => '',
741 'status' => '',
742 'metadata' => array(),
743 'raw' => $provider_event,
744 );
745
746 return (array) apply_filters( 'learn-press/gateway/subscription/event', $event, $provider_event, $this );
747 }*/
748
749 /**
750 * Define key of system LearnPress for webhook data.
751 *
752 * @param array $webhook_data [lp_order_id, plan_id, subscription_id, subscription_status]
753 *
754 * @return void
755 */
756 final public function normalize_subscription_data( array &$webhook_data = [] ) {
757 $webhook_data = array_merge(
758 array(
759 'lp_order_id' => 0,
760 'lp_plan_id' => '', // Plan id of payment, not membership plan id.
761 'lp_subscription_id' => '', // Subscription id of payment plan id.
762 'lp_subscription_status' => '', // Subscription status of payment plan id.
763 ),
764 $webhook_data
765 );
766 }
767
768 /**
769 * Process subscription by status.
770 *
771 * Status flow:
772 * - trial/activated: triggers once on the parent LP Order (first payment).
773 * - renewed: triggers on each renewal, creating a child LP Order.
774 * - created/expired/canceled/suspended: logged, do_action only, no order state change.
775 *
776 * @param LP_Order $lp_order
777 * @param string $lp_subscription_status_set_to_handle Status you want set to handle by case
778 * @param array $webhook_data
779 *
780 * @return void
781 * @throws Exception
782 * @since 4.3.7
783 * @version 1.0.0
784 */
785 final public function process_subscription_by_status(
786 $lp_order,
787 string $lp_subscription_status_set_to_handle,
788 array $webhook_data = []
789 ) {
790 LP_Debug::log_to_comment( 'Progress wit data: ' . json_encode( $webhook_data, JSON_UNESCAPED_UNICODE ) );
791
792 switch ( $lp_subscription_status_set_to_handle ) {
793 case LP_Subscription_Manager::STATUS_TRIAL:
794 // For trial, update LP order to complete, set subscription status to trial
795 $this->process_subscription_when_payment_first( $lp_order, LP_Subscription_Manager::STATUS_TRIAL, $webhook_data );
796
797 // Set user is using plan trial
798 $order_user_ids = $lp_order->get_users();
799 $plan_id = get_post_meta( $lp_order->get_id(), self::META_SUBSCRIPTION_PLAN_ID, true );
800 foreach ( $order_user_ids as $user_id ) {
801 update_user_meta( $user_id, 'user_plan_trial', $plan_id );
802 }
803
804 $lp_order->add_note(
805 sprintf(
806 'LP Order: %s %s: %s. %s. %s, %s',
807 sprintf(
808 '<a href="%s">%s</a>',
809 $lp_order->get_edit_link(),
810 $lp_order->get_order_number()
811 ),
812 __( 'Started trialing created at', 'learnpress' ),
813 $webhook_data['create_time'] ?? '',
814 sprintf( '%s %s', __( 'Next billing time', 'learnpress' ), $webhook_data['next_billing_time'] ?? '' ),
815 sprintf(
816 __( 'Subscription ID: %s', 'learnpress' ),
817 $webhook_data['lp_subscription_id']
818 ),
819 sprintf(
820 __( 'Plan ID: %s', 'learnpress' ),
821 $webhook_data['lp_plan_id']
822 )
823 )
824 );
825 do_action( 'learn-press/subscription/trial', $this, $lp_order, $webhook_data );
826 break;
827 case LP_Subscription_Manager::STATUS_ACTIVATED:
828 // For payment plan first success, update LP order to complete, set subscription status to activated
829 $this->process_subscription_when_payment_first( $lp_order, $lp_subscription_status_set_to_handle, $webhook_data );
830 $lp_order->add_note(
831 sprintf(
832 'LP Order: %s %s: %s. %s. %s, %s',
833 sprintf(
834 '<a href="%s">%s</a>',
835 $lp_order->get_edit_link(),
836 $lp_order->get_order_number()
837 ),
838 __( 'Activated created at', 'learnpress' ),
839 $webhook_data['create_time'] ?? '',
840 sprintf( '%s: %s', __( 'Next billing time', 'learnpress' ), $webhook_data['next_billing_time'] ?? '' ),
841 sprintf(
842 __( 'Subscription ID: %s', 'learnpress' ),
843 $webhook_data['lp_subscription_id']
844 ),
845 sprintf(
846 __( 'Plan ID: %s', 'learnpress' ),
847 $webhook_data['lp_plan_id']
848 )
849 )
850 );
851 do_action( 'learn-press/subscription/active', $this, $lp_order, $webhook_data );
852 break;
853 case LP_Subscription_Manager::STATUS_RENEWED:
854 // For payment plan renew success, parent order is completed and payment renew success
855 $this->process_subscription_when_payment_renew_success( $lp_order, $webhook_data );
856 $lp_order->add_note(
857 sprintf(
858 'LP Order: %s %s: %s. %s. %s, %s',
859 sprintf(
860 '<a href="%s">%s</a>',
861 $lp_order->get_edit_link(),
862 $lp_order->get_order_number()
863 ),
864 __( 'Renew created at', 'learnpress' ),
865 $webhook_data['create_time'] ?? '',
866 sprintf( '%s: %s', __( 'Next billing time', 'learnpress' ), $webhook_data['next_billing_time'] ?? '' ),
867 sprintf(
868 __( 'Subscription ID: %s', 'learnpress' ),
869 $webhook_data['lp_subscription_id']
870 ),
871 sprintf(
872 __( 'Plan ID: %s', 'learnpress' ),
873 $webhook_data['lp_plan_id']
874 )
875 )
876 );
877 do_action( 'learn-press/subscription/renew', $this, $lp_order, $webhook_data );
878 break;
879 case LP_Subscription_Manager::STATUS_EXPIRED:
880 // For payment plan expired, not impact orders
881 $lp_order->add_note(
882 sprintf(
883 'LP Order: %s %s: %s. %s, %s',
884 sprintf(
885 '<a href="%s">%s</a>',
886 $lp_order->get_edit_link(),
887 $lp_order->get_order_number()
888 ),
889 __( 'Expired created at', 'learnpress' ),
890 $webhook_data['create_time'] ?? '',
891 sprintf(
892 __( 'Subscription ID: %s', 'learnpress' ),
893 $webhook_data['lp_subscription_id']
894 ),
895 sprintf(
896 __( 'Plan ID: %s', 'learnpress' ),
897 $webhook_data['lp_plan_id']
898 )
899 )
900 );
901 do_action( 'learn-press/subscription/expired', $this, $lp_order, $webhook_data );
902 break;
903 case LP_Subscription_Manager::STATUS_SUSPENDED:
904 // For payment plan suspended, not impact orders
905 $lp_order->add_note(
906 sprintf(
907 'LP Order: %s %s: %s. %s, %s',
908 sprintf(
909 '<a href="%s">%s</a>',
910 $lp_order->get_edit_link(),
911 $lp_order->get_order_number()
912 ),
913 __( 'Suspended at', 'learnpress' ),
914 $webhook_data['create_time'] ?? '',
915 sprintf(
916 __( 'Subscription ID: %s', 'learnpress' ),
917 $webhook_data['lp_subscription_id']
918 ),
919 sprintf(
920 __( 'Plan ID: %s', 'learnpress' ),
921 $webhook_data['lp_plan_id']
922 )
923 )
924 );
925 do_action( 'learn-press/subscription/suspended', $this, $lp_order, $webhook_data );
926 break;
927 case LP_Subscription_Manager::STATUS_CANCELLED:
928 // For payment plan canceled, not impact orders
929 $lp_order->add_note(
930 sprintf(
931 'LP Order: %s %s: %s. %s, %s',
932 sprintf(
933 '<a href="%s">%s</a>',
934 $lp_order->get_edit_link(),
935 $lp_order->get_order_number()
936 ),
937 __( 'Canceled at', 'learnpress' ),
938 $webhook_data['create_time'] ?? '',
939 sprintf(
940 __( 'Subscription ID: %s', 'learnpress' ),
941 $webhook_data['lp_subscription_id']
942 ),
943 sprintf(
944 __( 'Plan ID: %s', 'learnpress' ),
945 $webhook_data['lp_plan_id']
946 )
947 )
948 );
949 do_action( 'learn-press/subscription/cancelled', $this, $lp_order, $webhook_data );
950 break;
951 }
952
953 do_action( 'learn-press/subscription/process', $this, $lp_order, $webhook_data );
954 }
955
956 /**
957 * Process order when subscription payment first.
958 * Update status of order parent to completed
959 * Save META_SUBSCRIPTION_STATUS, lp_subscription_amount, lp_subscription_currency
960 *
961 * @since 4.3.7
962 * @version 1.0.1
963 */
964 private function process_subscription_when_payment_first(
965 LP_Order $lp_order,
966 string $lp_subscription_status_set_to_handle,
967 $webhook_data
968 ) {
969 $lp_subscription_amount = $webhook_data['lp_subscription_amount'] ?? 0;
970 $lp_subscription_currency = $webhook_data['lp_subscription_currency'] ?? '';
971 $lp_order->update_status( LP_ORDER_COMPLETED );
972 update_post_meta(
973 $lp_order->get_id(),
974 self::META_SUBSCRIPTION_STATUS,
975 $lp_subscription_status_set_to_handle
976 );
977 update_post_meta(
978 $lp_order->get_id(),
979 'lp_subscription_amount',
980 $lp_subscription_amount
981 );
982 update_post_meta(
983 $lp_order->get_id(),
984 'lp_subscription_currency',
985 $lp_subscription_currency
986 );
987 update_post_meta(
988 $lp_order->get_id(),
989 self::META_SUBSCRIPTION_DATA_RECEIVER,
990 wp_json_encode( $webhook_data, JSON_UNESCAPED_UNICODE )
991 );
992
993 do_action( 'learn-press/subscription/order/success', $lp_order, $webhook_data );
994 }
995
996 /**
997 * Process order when payment recurring success.
998 * Create new order child
999 * Save META_SUBSCRIPTION_DATA_RECEIVER, lp_subscription_amount, lp_subscription_currency
1000 *
1001 * @throws Exception
1002 * @since 4.3.7
1003 * @version 1.0.1
1004 */
1005 private function process_subscription_when_payment_renew_success( LP_Order $lp_order_parent, $webhook_data ) {
1006 $lp_subscription_amount = $webhook_data['lp_subscription_amount'] ?? 0;
1007 $lp_subscription_currency = $webhook_data['lp_subscription_currency'] ?? '';
1008
1009 // Create new Order child
1010 $order_renew = new LP_Order();
1011 $order_renew->set_parent_id( $lp_order_parent->get_id() );
1012 $order_renew->set_user_id( $lp_order_parent->get_user_id() );
1013 $order_renew->set_checkout_email( $lp_order_parent->get_checkout_email() );
1014 $order_renew->set_status( LP_ORDER_COMPLETED );
1015 $order_renew->set_created_via( 'subscription' );
1016 $order_renew->set_currency( $lp_subscription_currency ?? $lp_order_parent->get_currency() );
1017 $order_renew->set_total( $lp_subscription_amount );
1018 $order_renew->set_subtotal( $lp_subscription_amount );
1019 $order_renew->set_data( 'payment_method', $lp_order_parent->get_data( 'payment_method' ) );
1020 $order_renew->set_data( 'payment_method_title', $lp_order_parent->get_payment_method_title() );
1021 $order_renew->save();
1022
1023 //error_log( 'renew ' . json_encode( $webhook_data, JSON_UNESCAPED_UNICODE ) );
1024 // Add item to order renew
1025 foreach ( $lp_order_parent->get_all_items() as $item ) {
1026 $item['subtotal'] = $lp_subscription_amount;
1027 $item['total'] = $lp_subscription_amount;
1028 $order_renew->add_item( $item );
1029 }
1030
1031 update_post_meta(
1032 $order_renew->get_id(),
1033 self::META_SUBSCRIPTION_DATA_RECEIVER,
1034 wp_json_encode( $webhook_data, JSON_UNESCAPED_UNICODE )
1035 );
1036 update_post_meta(
1037 $order_renew->get_id(),
1038 'lp_subscription_amount',
1039 $lp_subscription_amount
1040 );
1041 update_post_meta(
1042 $order_renew->get_id(),
1043 'lp_subscription_currency',
1044 $lp_subscription_currency
1045 );
1046
1047 do_action( 'learn-press/subscription/order/renew-success', $order_renew, $webhook_data );
1048 }
1049
1050 /**
1051 * Get provider manage subscription URL for order.
1052 *
1053 * Child gateways can override when provider offers customer portal pages.
1054 *
1055 * @param LP_Order $order
1056 *
1057 * @return string
1058 */
1059 public function get_manage_subscription_url( LP_Order $order ): string {
1060 $url = get_post_meta( $order->get_id(), self::META_SUBSCRIPTION_MANAGE_URL, true );
1061 if ( ! is_string( $url ) ) {
1062 $url = '';
1063 }
1064
1065 return (string) apply_filters( 'learn-press/gateway/subscription/manage-url', $url, $order, $this );
1066 }
1067
1068 /**
1069 * Get the icon of payment displays in front end.
1070 *
1071 * @return mixed
1072 */
1073 public function get_icon() {
1074 $size = apply_filters( 'learn-press/default-payment-gateway-icon-sizes', null ); // array( 52, 32 ) is low quatity.
1075
1076 if ( $size ) {
1077 $icon_size = sprintf( 'width: %dpx; height: %dpx', $size[0], $size[1] );
1078 } else {
1079 $icon_size = '';
1080 }
1081
1082 $icon = $this->icon ? '<img class="gateway-icon" src="' . $this->icon . '" alt="' . esc_attr( $this->get_title() ) . '" style="' . $icon_size . '" />' : '';
1083
1084 return apply_filters( 'learn_press_gateway_icon', $icon, $this->id );
1085 }
1086
1087 /**
1088 * Return the form where user can input payment details or anything else.
1089 *
1090 * @return string
1091 */
1092 public function get_payment_form() {
1093 return apply_filters( 'learn_press_gateway_payment_form', '', $this );
1094 }
1095
1096 /**
1097 * Validate required field before submitting fields.
1098 *
1099 * @return bool
1100 */
1101 public function validate_fields() {
1102 // TODO: validate fields if needed
1103 return true;
1104 }
1105
1106 /**
1107 * @param LP_Order $order
1108 *
1109 * @return mixed
1110 */
1111 public function get_return_url( $order = null ) {
1112 if ( $order ) {
1113 $return_url = $order->get_checkout_order_received_url();
1114 } else {
1115 $return_url = learn_press_get_endpoint_url( 'lp-order-received', '', learn_press_get_page_link( 'checkout' ) );
1116 }
1117
1118 return apply_filters( 'learn_press_get_return_url', $return_url, $order );
1119 }
1120
1121 /**
1122 * @param string $prop
1123 *
1124 * @deprecated 4.3.9
1125 */
1126 public function __get( $prop ) {
1127 _deprecated_function( __METHOD__, '4.3.9' );
1128 return false;
1129 switch ( $prop ) {
1130 case 'method_title':
1131 case 'method_description':
1132 case 'id':
1133 _deprecated_argument( $prop, '3.0.0', sprintf( __( '%s has been deprecated. Please use % instead of.', 'learnpress' ), $prop, "get_{$prop}" ) );
1134
1135 return call_user_func( array( $this, "get_{$prop}" ) );
1136 default:
1137 return property_exists( $this, $prop ) ? $this->{$prop} : false;
1138 }
1139 }
1140
1141 /**
1142 * @return string
1143 */
1144 public function __toString() {
1145 return $this->method_title;
1146 }
1147 }
1148