PluginProbe
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses / 4.3.7
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses v4.3.7
4.4.9 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 All 140 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.7, at inc/gateways/class-lp-gateway-abstract.php

1,075 lines 31.0 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 */
196 public function is_subscription_order( LP_Order $order ): bool {
197
198 $order_id = $order->get_id();
199 $saved_price_id = sanitize_text_field( (string) get_post_meta( $order_id, self::META_SUBSCRIPTION_PLAN_ID, true ) );
200 if ( ! empty( $saved_price_id ) ) {
201 return true;
202 }
203 $is_subscription = (bool) apply_filters(
204 'learn-press/gateway/subscription-order',
205 false,
206 $order,
207 $this
208 );
209
210 return $is_subscription;
211 }
212
213 /**
214 * Get subscription context for provider APIs.
215 *
216 * Returns a gateway-agnostic payload that child gateways can pass to
217 * `pay_subscription()` after optional gateway-specific normalization.
218 *
219 * @param LP_Order $order
220 *
221 * @return array
222 */
223 public function get_subscription_context( LP_Order $order ): array {
224 $order_id = $order->get_id();
225
226 $context = array(
227 'price_id' => get_post_meta( $order_id, self::META_SUBSCRIPTION_PLAN_ID, true ),
228 'quantity' => (int) get_post_meta( $order_id, self::META_SUBSCRIPTION_QUANTITY, true ),
229 'success_url' => $this->get_return_url( $order ),
230 'cancel_url' => learn_press_get_page_link( 'checkout' ),
231 'metadata' => array(
232 'lp_order_id' => (string) $order_id,
233 'lp_order_key' => (string) $order->get_order_key(),
234 'lp_gateway' => $this->get_id(),
235 'lp_user_id' => (string) $order->get_user_id(),
236 'lp_order_type' => 'subscription',
237 ),
238 );
239
240 if ( empty( $context['quantity'] ) ) {
241 $context['quantity'] = 1;
242 }
243
244 return (array) apply_filters( 'learn-press/gateway/subscription-context', $context, $order, $this );
245 }
246
247 /**
248 * Persist subscription identifiers to order before payment execution.
249 *
250 * This allows payment methods to operate only on normalized parameters and
251 * avoid re-detecting custom integration attributes inside gateway code.
252 *
253 * @param LP_Order $order
254 * @param array $data
255 *
256 * @return void
257 */
258 protected function persist_subscription_payment_identifiers( LP_Order $order, array $data ) {
259
260 $order_id = $order->get_id();
261
262 update_post_meta( $order_id, self::META_SUBSCRIPTION_PLAN_ID, sanitize_text_field( (string) ( $data['price_id'] ?? '' ) ) );
263 update_post_meta( $order_id, self::META_SUBSCRIPTION_QUANTITY, max( 1, absint( $data['quantity'] ?? 1 ) ) );
264 }
265 /**
266 * Resolve normalized subscription payment params from order context.
267 *
268 * Behavior:
269 * - If order contains persisted subscription identifiers, treat it as
270 * subscription payment.
271 * - If order is marked subscription by integration filter but has no
272 * `price_id`, return a validation error early.
273 * - Otherwise return empty array (one-time payment flow).
274 *
275 * @param LP_Order $order
276 *
277 * @return array
278 * @throws Exception
279 */
280 public function resolve_subscription_payment_data( LP_Order $order ): array {
281 $context = $this->get_subscription_context( $order );
282 $context = wp_parse_args(
283 $context,
284 array(
285 'price_id' => '',
286 'quantity' => 1,
287 'success_url' => '',
288 'cancel_url' => '',
289 'metadata' => array(),
290 )
291 );
292
293 $context['price_id'] = sanitize_text_field( (string) $context['price_id'] );
294 $context['quantity'] = max( 1, absint( $context['quantity'] ) );
295 $context['metadata'] = is_array( $context['metadata'] ) ? $context['metadata'] : array();
296
297 if ( ! empty( $context['price_id'] ) ) {
298 $this->persist_subscription_payment_identifiers( $order, $context );
299 return $context;
300 }
301
302 if ( $this->is_subscription_order( $order ) ) {
303 throw new Exception( __( 'Missing subscription price id.', 'learnpress' ) );
304 }
305
306 return array();
307 }
308
309 /**
310 * Normalize and validate the shared subscription checkout payload.
311 *
312 * This method is intentionally gateway-agnostic and is used by child gateways
313 * (e.g. Stripe/PayPal) before they build provider-specific API requests.
314 *
315 * Payload contract:
316 * - `price_id` (string, required): provider-side configured recurring price/plan id.
317 * - `quantity` (int): defaults to 1 when missing/invalid/zero.
318 * - `success_url` / `cancel_url` (string, required): absolute callback URLs.
319 * - `metadata` (array): optional identifiers (order/user/etc.) for reconciliation.
320 *
321 * @param array $data
322 *
323 * @return array Normalized payload array.
324 * @throws Exception
325 */
326 protected function validate_subscription_payload( array $data ): array {
327 // Apply safe defaults to guarantee a stable input shape.
328 $data = wp_parse_args(
329 $data,
330 array(
331 'price_id' => '',
332 'quantity' => 1,
333 'success_url' => '',
334 'cancel_url' => '',
335 'metadata' => array(),
336 )
337 );
338
339 // Scalar sanitation/coercion for fields commonly coming from request context.
340 $data['price_id'] = sanitize_text_field( wp_unslash( (string) $data['price_id'] ) );
341 $data['quantity'] = absint( $data['quantity'] );
342 if ( empty( $data['quantity'] ) ) {
343 $data['quantity'] = 1;
344 }
345
346 // URLs are stored in raw form for outbound provider API requests.
347 $data['success_url'] = esc_url_raw( (string) $data['success_url'] );
348 $data['cancel_url'] = esc_url_raw( (string) $data['cancel_url'] );
349
350 // Defensive normalization for optional structured fields.
351 $data['metadata'] = is_array( $data['metadata'] ) ? $data['metadata'] : array();
352
353 // price_id is the minimum provider binding required for subscription checkout.
354 if ( empty( $data['price_id'] ) ) {
355 throw new Exception( __( 'Missing subscription price id.', 'learnpress' ) );
356 }
357
358 // Redirect URLs are mandatory for provider-hosted checkout flows.
359 if ( empty( $data['success_url'] ) || empty( $data['cancel_url'] ) ) {
360 throw new Exception( __( 'Missing subscription return URLs.', 'learnpress' ) );
361 }
362
363 return $data;
364 }
365
366 /**
367 * Generic subscription checkout flow.
368 *
369 * Child gateways should override this method and return a payload with at
370 * least `status` and `redirect_url` on success.
371 *
372 * @param array $data
373 *
374 * @return array
375 * @throws Exception
376 */
377 public function pay_subscription( array $data ): array {
378 throw new Exception( sprintf( __( 'Gateway %s does not support subscription payment.', 'learnpress' ), $this->get_id() ) );
379 }
380
381 /**
382 * Normalize and validate shared plan-creation payload.
383 *
384 * Common payload contract:
385 * - `name` (string, required when `product_id` is empty)
386 * - `amount` (float, required, > 0)
387 * - `currency` (string, required)
388 * - `interval` (day|week|month|year)
389 * - `interval_count` (int, default 1)
390 * - `setup_fee` (float, optional, >= 0)
391 * - `product_id` (string, optional)
392 * - `metadata` (array, optional)
393 *
394 * @param array $data
395 *
396 * @return array
397 * @throws Exception
398 */
399 protected function validate_data_plan_payload( array $data ): array {
400 $data = wp_parse_args(
401 $data,
402 array(
403 'name' => '',
404 'amount' => 0,
405 'currency' => learn_press_get_currency(),
406 'interval' => 'month',
407 'interval_count' => 1,
408 'setup_fee' => 0,
409 'product_id' => '',
410 'metadata' => array(),
411 )
412 );
413 $data['name'] = sanitize_text_field( wp_unslash( (string) $data['name'] ) );
414 $data['amount'] = (float) $data['amount'];
415 $data['currency'] = strtoupper( sanitize_text_field( wp_unslash( (string) $data['currency'] ) ) );
416 $data['interval'] = strtolower( sanitize_key( (string) $data['interval'] ) );
417 $data['interval_count'] = max( 1, absint( $data['interval_count'] ) );
418 $data['setup_fee'] = (float) $data['setup_fee'];
419 $data['product_id'] = sanitize_text_field( wp_unslash( (string) $data['product_id'] ) );
420 $data['metadata'] = is_array( $data['metadata'] ) ? $data['metadata'] : array();
421 if ( empty( $data['product_id'] ) && empty( $data['name'] ) ) {
422 throw new Exception( __( 'Missing subscription plan name.', 'learnpress' ) );
423 }
424
425 if ( $data['amount'] <= 0 ) {
426 throw new Exception( __( 'Invalid subscription amount.', 'learnpress' ) );
427 }
428
429 if ( $data['setup_fee'] < 0 ) {
430 throw new Exception( __( 'Invalid subscription setup fee.', 'learnpress' ) );
431 }
432
433 if ( empty( $data['currency'] ) ) {
434 throw new Exception( __( 'Missing subscription currency.', 'learnpress' ) );
435 }
436 $allowed_intervals = array( 'day', 'week', 'month', 'year' );
437 if ( ! in_array( $data['interval'], $allowed_intervals, true ) ) {
438 throw new Exception( __( 'Invalid subscription interval.', 'learnpress' ) );
439 }
440
441 return $data;
442 }
443
444 /**
445 * Generic provider plan/price creation flow.
446 *
447 * Child gateways should override and return created provider identifiers,
448 * typically a `price_id`/`plan_id` to be used later by `pay_subscription()`.
449 *
450 * @param array $data
451 *
452 * @return array
453 * @throws Exception
454 */
455 public function create_plan( array $data ): array {
456
457 throw new Exception( sprintf( __( 'Gateway %s does not support subscription plan creation.', 'learnpress' ), $this->get_id() ) );
458 }
459
460 /**
461 * List provider plans/prices with optional filtering/pagination args.
462 *
463 * @param array $args
464 *
465 * @return array
466 * @throws Exception
467 */
468 public function list_plans( array $args = array() ): array {
469
470 throw new Exception( sprintf( __( 'Gateway %s does not support listing subscription plans.', 'learnpress' ), $this->get_id() ) );
471 }
472
473 /**
474 * Fetch provider plan/price details by plan id.
475 *
476 * Child gateways should override and return at least:
477 * - `plan`: raw provider response
478 * - `summary`: normalized fields used by integrations to compare updates
479 * (amount/currency/interval/interval_count/setup_fee/status)
480 *
481 * @param string $plan_id
482 *
483 * @return array
484 * @throws Exception
485 */
486 public function get_plan( string $plan_id ): array {
487
488 throw new Exception( sprintf( __( 'Gateway %s does not support fetching subscription plan.', 'learnpress' ), $this->get_id() ) );
489 }
490
491 /**
492 * Update provider plan details by plan id.
493 *
494 * @param string $plan_id
495 * @param array $data
496 *
497 * @return array
498 * @throws Exception
499 */
500 public function update_plan( string $plan_id, array $data ): array {
501
502 throw new Exception( sprintf( __( 'Gateway %s does not support updating subscription plan.', 'learnpress' ), $this->get_id() ) );
503 }
504
505 /**
506 * Delete/deactivate provider plan by plan id.
507 *
508 * @param string $plan_id
509 *
510 * @return array
511 * @throws Exception
512 */
513 public function delete_plan( string $plan_id ): array {
514
515 throw new Exception( sprintf( __( 'Gateway %s does not support deleting subscription plan.', 'learnpress' ), $this->get_id() ) );
516 }
517 /**
518 * Generic subscription webhook listener.
519 *
520 * Child gateways should override and orchestrate:
521 * verify -> normalize -> manager dispatch.
522 *
523 * @param WP_REST_Request $request
524 *
525 * @return array
526 * @throws Exception
527 */
528 public function listen_webhook_subscription( WP_REST_Request $request ): array {
529 throw new Exception( sprintf( __( 'Gateway %s does not support subscription webhook.', 'learnpress' ), $this->get_id() ) );
530 }
531
532 /**
533 * Receive subscription webhook from provider.
534 *
535 * @throws Exception
536 *
537 * @since 4.3.7
538 * @version 1.0.0
539 */
540 public function capture_subscription_webhook( WP_REST_Request $request ) {
541 throw new Exception(
542 sprintf(
543 __( 'Gateway %s does not support subscription webhook.', 'learnpress' ),
544 $this->get_id()
545 )
546 );
547 }
548
549 /**
550 * Verify subscription webhook payload/signature with provider.
551 *
552 * Child gateways should return verified provider event payload/object.
553 *
554 * @param array $webhook_data Generic webhook data extracted from transport layer.
555 *
556 * @return array|object
557 * @throws Exception
558 */
559 public function verify_subscription_webhook( array $webhook_data ) {
560
561 throw new Exception( sprintf( __( 'Gateway %s does not support subscription webhook verification.', 'learnpress' ), $this->get_id() ) );
562 }
563
564 /**
565 * Build normalized webhook data array from transport-specific REST request.
566 *
567 * Contract:
568 * - raw_body: raw payload string (for signature verification like Stripe).
569 * - body: decoded JSON array when $decode_body is true, otherwise null.
570 * - headers: map of required header keys (lowercase) to raw header values.
571 *
572 * @param WP_REST_Request $request
573 * @param array $required_headers
574 * @param bool $decode_body
575 *
576 * @return array
577 */
578 protected function build_webhook_data_from_request( WP_REST_Request $request, array $required_headers = array(), bool $decode_body = true ): array {
579
580 $raw_body = (string) $request->get_body();
581 $headers = array();
582
583 foreach ( $required_headers as $required_header ) {
584 $required_header = strtolower( sanitize_key( (string) $required_header ) );
585 $headers[ $required_header ] = (string) $request->get_header( $required_header );
586 }
587
588 $body = null;
589 if ( $decode_body ) {
590 $body = LP_Helper::json_decode( $raw_body, true );
591 }
592
593 return array(
594 'raw_body' => $raw_body,
595 'body' => is_array( $body ) ? $body : null,
596 'headers' => $headers,
597 );
598 }
599
600 /**
601 * Validate normalized webhook payload contract before provider verification.
602 *
603 * This centralizes fail-fast checks so each gateway does not re-implement
604 * required key/header validation and accidentally diverge key names.
605 *
606 * @param array $webhook_data
607 * @param array $required_top_level_keys Allowed: raw_body, body, headers.
608 * @param array $required_headers
609 *
610 * @return void
611 * @throws Exception
612 */
613 protected function validate_webhook_data_contract( array $webhook_data, array $required_top_level_keys = array(), array $required_headers = array() ) {
614
615 $missing = array();
616
617 foreach ( $required_top_level_keys as $required_key ) {
618 $required_key = sanitize_key( (string) $required_key );
619
620 switch ( $required_key ) {
621 case 'raw_body':
622 if ( empty( $webhook_data['raw_body'] ) || ! is_string( $webhook_data['raw_body'] ) ) {
623 $missing[] = 'raw_body';
624 }
625 break;
626 case 'body':
627 if ( empty( $webhook_data['body'] ) || ! is_array( $webhook_data['body'] ) ) {
628 $missing[] = 'body';
629 }
630 break;
631 case 'headers':
632 if ( ! isset( $webhook_data['headers'] ) || ! is_array( $webhook_data['headers'] ) ) {
633 $missing[] = 'headers';
634 }
635 break;
636 }
637 }
638
639 $headers_map = is_array( $webhook_data['headers'] ?? null ) ? $webhook_data['headers'] : array();
640 foreach ( $required_headers as $required_header ) {
641 $required_header = strtolower( sanitize_key( (string) $required_header ) );
642 $header_value = sanitize_text_field( (string) ( $headers_map[ $required_header ] ?? '' ) );
643 if ( '' === $header_value ) {
644 $missing[] = 'headers.' . $required_header;
645 }
646 }
647
648 if ( ! empty( $missing ) ) {
649 throw new Exception(
650 sprintf(
651 /* translators: %s: comma separated required webhook fields. */
652 __( 'Invalid webhook request data: missing %s.', 'learnpress' ),
653 implode( ', ', array_unique( $missing ) )
654 ),
655 400
656 );
657 }
658 }
659
660 /**
661 * Normalize provider webhook event to LP event payload.
662 *
663 * Child gateways should map provider-specific event types/fields into this
664 * canonical schema so Subscription Manager can process consistently.
665 *
666 * @param array|object $provider_event
667 *
668 * @return array
669 */
670 public function normalize_subscription_event( $provider_event ): array {
671 $event = array(
672 'event_id' => '',
673 'event_type' => '',
674 'subscription_id' => '',
675 'customer_id' => '',
676 'price_id' => '',
677 'parent_order_id' => 0,
678 'transaction_id' => '',
679 'amount' => 0,
680 'currency' => '',
681 'status' => '',
682 'metadata' => array(),
683 'raw' => $provider_event,
684 );
685
686 return (array) apply_filters( 'learn-press/gateway/subscription/event', $event, $provider_event, $this );
687 }
688
689 /**
690 * Normalize provider webhook event to LP event payload.
691 *
692 * Merge data from provider webhook event to LP event payload.
693 *
694 * @param array $webhook_data [lp_order_id, plan_id, subscription_id, subscription_status]
695 *
696 * @return void
697 */
698 public function normalize_subscription_data( array &$webhook_data = [] ) {
699 $webhook_data = array_merge(
700 array(
701 'lp_order_id' => 0,
702 'lp_plan_id' => '',
703 'lp_subscription_id' => '',
704 'lp_subscription_status' => '',
705 ),
706 $webhook_data
707 );
708 }
709
710 /**
711 * Process subscription by status.
712 *
713 * Status flow:
714 * - trial/activated: triggers once on the parent LP Order (first payment).
715 * - renewed: triggers on each renewal, creating a child LP Order.
716 * - created/expired/canceled/suspended: logged, do_action only, no order state change.
717 *
718 * @param LP_Order $lp_order
719 * @param string $lp_subscription_status_set_to_handle Status you want set to handle by case
720 * @param array $webhook_data
721 *
722 * @return void
723 * @throws Exception
724 * @since 4.3.7
725 * @version 1.0.0
726 */
727 public function process_subscription_by_status(
728 $lp_order,
729 string $lp_subscription_status_set_to_handle,
730 array $webhook_data = []
731 ) {
732 LP_Debug::log_to_comment( 'Progress wit data: ' . json_encode( $webhook_data, JSON_UNESCAPED_UNICODE ) );
733
734 switch ( $lp_subscription_status_set_to_handle ) {
735 case LP_Subscription_Manager::STATUS_TRIAL:
736 // For trial, update LP order to complete, set subscription status to trial
737 $lp_order->add_note(
738 sprintf(
739 'LP Order: %s %s: %s. %s. %s, %s',
740 sprintf(
741 '<a href="%s">%s</a>',
742 $lp_order->get_edit_link(),
743 $lp_order->get_order_number()
744 ),
745 __( 'Started trialing created at', 'learnpress' ),
746 $webhook_data['create_time'] ?? '',
747 sprintf( '%s %s', __( 'Next billing time', 'learnpress' ), $webhook_data['next_billing_time'] ?? '' ),
748 sprintf(
749 __( 'Subscription ID: %s', 'learnpress' ),
750 $webhook_data['lp_subscription_id']
751 ),
752 sprintf(
753 __( 'Plan ID: %s', 'learnpress' ),
754 $webhook_data['lp_plan_id']
755 )
756 )
757 );
758 $this->process_subscription_when_payment_first( $lp_order, LP_Subscription_Manager::STATUS_TRIAL, $webhook_data );
759 do_action( 'learn-press/subscription/trial', $this, $lp_order, $webhook_data );
760 break;
761 case LP_Subscription_Manager::STATUS_ACTIVATED:
762 // For payment plan first success, update LP order to complete, set subscription status to activated
763 $this->process_subscription_when_payment_first( $lp_order, $lp_subscription_status_set_to_handle, $webhook_data );
764 $lp_order->add_note(
765 sprintf(
766 'LP Order: %s %s: %s. %s. %s, %s',
767 sprintf(
768 '<a href="%s">%s</a>',
769 $lp_order->get_edit_link(),
770 $lp_order->get_order_number()
771 ),
772 __( 'Activated created at', 'learnpress' ),
773 $webhook_data['create_time'] ?? '',
774 sprintf( '%s: %s', __( 'Next billing time', 'learnpress' ), $webhook_data['next_billing_time'] ?? '' ),
775 sprintf(
776 __( 'Subscription ID: %s', 'learnpress' ),
777 $webhook_data['lp_subscription_id']
778 ),
779 sprintf(
780 __( 'Plan ID: %s', 'learnpress' ),
781 $webhook_data['lp_plan_id']
782 )
783 )
784 );
785 do_action( 'learn-press/subscription/active', $this, $lp_order, $webhook_data );
786 break;
787 case LP_Subscription_Manager::STATUS_RENEWED:
788 // For payment plan renew success, parent order is completed and payment renew success
789 $this->process_subscription_when_payment_renew_success( $lp_order, $webhook_data );
790 $lp_order->add_note(
791 sprintf(
792 'LP Order: %s %s: %s. %s. %s, %s',
793 sprintf(
794 '<a href="%s">%s</a>',
795 $lp_order->get_edit_link(),
796 $lp_order->get_order_number()
797 ),
798 __( 'Renew created at', 'learnpress' ),
799 $webhook_data['create_time'] ?? '',
800 sprintf( '%s: %s', __( 'Next billing time', 'learnpress' ), $webhook_data['next_billing_time'] ?? '' ),
801 sprintf(
802 __( 'Subscription ID: %s', 'learnpress' ),
803 $webhook_data['lp_subscription_id']
804 ),
805 sprintf(
806 __( 'Plan ID: %s', 'learnpress' ),
807 $webhook_data['lp_plan_id']
808 )
809 )
810 );
811 do_action( 'learn-press/subscription/renew', $this, $lp_order, $webhook_data );
812 break;
813 case LP_Subscription_Manager::STATUS_EXPIRED:
814 // For payment plan expired, not impact orders
815 $lp_order->add_note(
816 sprintf(
817 'LP Order: %s %s: %s. %s, %s',
818 sprintf(
819 '<a href="%s">%s</a>',
820 $lp_order->get_edit_link(),
821 $lp_order->get_order_number()
822 ),
823 __( 'Expired created at', 'learnpress' ),
824 $webhook_data['create_time'] ?? '',
825 sprintf(
826 __( 'Subscription ID: %s', 'learnpress' ),
827 $webhook_data['lp_subscription_id']
828 ),
829 sprintf(
830 __( 'Plan ID: %s', 'learnpress' ),
831 $webhook_data['lp_plan_id']
832 )
833 )
834 );
835 do_action( 'learn-press/subscription/expired', $this, $lp_order, $webhook_data );
836 break;
837 case LP_Subscription_Manager::STATUS_SUSPENDED:
838 // For payment plan suspended, not impact orders
839 $lp_order->add_note(
840 sprintf(
841 'LP Order: %s %s: %s. %s, %s',
842 sprintf(
843 '<a href="%s">%s</a>',
844 $lp_order->get_edit_link(),
845 $lp_order->get_order_number()
846 ),
847 __( 'Suspended at', 'learnpress' ),
848 $webhook_data['create_time'] ?? '',
849 sprintf(
850 __( 'Subscription ID: %s', 'learnpress' ),
851 $webhook_data['lp_subscription_id']
852 ),
853 sprintf(
854 __( 'Plan ID: %s', 'learnpress' ),
855 $webhook_data['lp_plan_id']
856 )
857 )
858 );
859 do_action( 'learn-press/subscription/suspended', $this, $lp_order, $webhook_data );
860 break;
861 case LP_Subscription_Manager::STATUS_CANCELLED:
862 // For payment plan canceled, not impact orders
863 $lp_order->add_note(
864 sprintf(
865 'LP Order: %s %s: %s. %s, %s',
866 sprintf(
867 '<a href="%s">%s</a>',
868 $lp_order->get_edit_link(),
869 $lp_order->get_order_number()
870 ),
871 __( 'Canceled at', 'learnpress' ),
872 $webhook_data['create_time'] ?? '',
873 sprintf(
874 __( 'Subscription ID: %s', 'learnpress' ),
875 $webhook_data['lp_subscription_id']
876 ),
877 sprintf(
878 __( 'Plan ID: %s', 'learnpress' ),
879 $webhook_data['lp_plan_id']
880 )
881 )
882 );
883 do_action( 'learn-press/subscription/cancelled', $this, $lp_order, $webhook_data );
884 break;
885 }
886
887 do_action( 'learn-press/subscription/process', $this, $lp_order, $webhook_data );
888 }
889
890 /**
891 * Process order when subscription payment first.
892 *
893 * @since 4.3.7
894 * @version 1.0.0
895 */
896 public function process_subscription_when_payment_first(
897 LP_Order $order,
898 string $lp_subscription_status_set_to_handle,
899 $webhook_data
900 ) {
901 $lp_subscription_amount = $webhook_data['lp_subscription_amount'] ?? 0;
902 $lp_subscription_currency = $webhook_data['lp_subscription_currency'] ?? '';
903 $order->update_status( LP_ORDER_COMPLETED );
904 update_post_meta(
905 $order->get_id(),
906 self::META_SUBSCRIPTION_STATUS,
907 $lp_subscription_status_set_to_handle
908 );
909 update_post_meta(
910 $order->get_id(),
911 'lp_subscription_amount',
912 $lp_subscription_amount
913 );
914 update_post_meta(
915 $order->get_id(),
916 'lp_subscription_currency',
917 $lp_subscription_currency
918 );
919 }
920
921 /**
922 * Process order when payment recurring success.
923 *
924 * @throws Exception
925 * @since 4.3.7
926 * @version 1.0.0
927 */
928 public function process_subscription_when_payment_renew_success( LP_Order $lp_order_parent, $webhook_data ) {
929 error_log( 'renew pay' );
930
931 $lp_subscription_amount = $webhook_data['lp_subscription_amount'] ?? 0;
932 $lp_subscription_currency = $webhook_data['lp_subscription_currency'] ?? '';
933
934 // Create new Order child
935 $order_renew = new LP_Order();
936 $order_renew->set_parent_id( $lp_order_parent->get_id() );
937 $order_renew->set_user_id( $lp_order_parent->get_user_id() );
938 $order_renew->set_checkout_email( $lp_order_parent->get_checkout_email() );
939 $order_renew->set_status( LP_ORDER_COMPLETED );
940 $order_renew->set_created_via( 'subscription' );
941 $order_renew->set_currency( $lp_subscription_currency ?? $lp_order_parent->get_currency() );
942 $order_renew->set_total( $lp_subscription_amount );
943 $order_renew->set_subtotal( $lp_subscription_amount );
944 $order_renew->set_data( 'payment_method', $lp_order_parent->get_data( 'payment_method' ) );
945 $order_renew->set_data( 'payment_method_title', $lp_order_parent->get_payment_method_title() );
946 $order_renew->save();
947
948 error_log( 'renew ' . json_encode( $webhook_data, JSON_UNESCAPED_UNICODE ) );
949
950 update_post_meta(
951 $order_renew->get_id(),
952 self::META_SUBSCRIPTION_DATA_RECEIVER,
953 wp_json_encode( $webhook_data, JSON_UNESCAPED_UNICODE )
954 );
955 update_post_meta(
956 $order_renew->get_id(),
957 'lp_subscription_amount',
958 $lp_subscription_amount
959 );
960 update_post_meta(
961 $order_renew->get_id(),
962 'lp_subscription_currency',
963 $lp_subscription_currency
964 );
965 }
966
967 /**
968 * Get provider manage subscription URL for order.
969 *
970 * Child gateways can override when provider offers customer portal pages.
971 *
972 * @param LP_Order $order
973 *
974 * @return string
975 */
976 public function get_manage_subscription_url( LP_Order $order ): string {
977 $url = get_post_meta( $order->get_id(), self::META_SUBSCRIPTION_MANAGE_URL, true );
978 if ( ! is_string( $url ) ) {
979 $url = '';
980 }
981
982 return (string) apply_filters( 'learn-press/gateway/subscription/manage-url', $url, $order, $this );
983 }
984
985 /**
986 * Get the icon of payment displays in front end.
987 *
988 * @return mixed
989 */
990 public function get_icon() {
991 $size = apply_filters( 'learn-press/default-payment-gateway-icon-sizes', null ); // array( 52, 32 ) is low quatity.
992
993 if ( $size ) {
994 $icon_size = sprintf( 'width: %dpx; height: %dpx', $size[0], $size[1] );
995 } else {
996 $icon_size = '';
997 }
998
999 $icon = $this->icon ? '<img class="gateway-icon" src="' . $this->icon . '" alt="' . esc_attr( $this->get_title() ) . '" style="' . $icon_size . '" />' : '';
1000
1001 return apply_filters( 'learn_press_gateway_icon', $icon, $this->id );
1002 }
1003
1004 /**
1005 * Return the form where user can input payment details or anything else.
1006 *
1007 * @return string
1008 */
1009 public function get_payment_form() {
1010 return '';
1011 }
1012
1013 /**
1014 * Validate required field before submitting fields.
1015 *
1016 * @return bool
1017 */
1018 public function validate_fields() {
1019 // TODO: validate fields if needed
1020 return true;
1021 }
1022
1023 /**
1024 * @param LP_Order $order
1025 *
1026 * @return mixed
1027 */
1028 public function get_return_url( $order = null ) {
1029 if ( $order ) {
1030 $return_url = $order->get_checkout_order_received_url();
1031 } else {
1032 $return_url = learn_press_get_endpoint_url( 'lp-order-received', '', learn_press_get_page_link( 'checkout' ) );
1033 }
1034
1035 return apply_filters( 'learn_press_get_return_url', $return_url, $order );
1036 }
1037
1038 public function __get( $prop ) {
1039 switch ( $prop ) {
1040 case 'method_title':
1041 case 'method_description':
1042 case 'id':
1043 _deprecated_argument( $prop, '3.0.0', sprintf( __( '%s has been deprecated. Please use % instead of.', 'learnpress' ), $prop, "get_{$prop}" ) );
1044
1045 return call_user_func( array( $this, "get_{$prop}" ) );
1046 default:
1047 return property_exists( $this, $prop ) ? $this->{$prop} : false;
1048 }
1049 }
1050
1051 /**
1052 * @since 3.0.0
1053 *
1054 * return bool
1055 * @deprecated 4.2.3.5
1056 */
1057 public function is_display() {
1058 _deprecated_function( __METHOD__, '4.2.3.5' );
1059 $display = apply_filters( 'learn-press/payment-method/display', true, $this->id );
1060 $display = apply_filters( 'learn-press/payment-method-' . $this->id . '/display', $display );
1061
1062 // @deprecated
1063 $display = apply_filters( 'learn_press_display_payment_method', $display, $this->id );
1064
1065 return $display;
1066 }
1067
1068 /**
1069 * @return string
1070 */
1071 public function __toString() {
1072 return $this->method_title;
1073 }
1074 }
1075