PluginProbe
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses / 4.4.9
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses v4.4.9
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
← All changes | inc/gateways/class-lp-gateway-abstract.php +1160 -1147 4.3.9 → 4.4.9 View file →
@@ -1,1147 +1,1160 @@
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 -}
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 + /**
1149 + * Refund payment
1150 + *
1151 + *
1152 + * @since 4.4.0
1153 + * @version 1.0.0
1154 + * @return void
1155 + * @throws Exception
1156 + */
1157 + public function refund( $lp_order, float $amount = 0, string $note = '' ) {
1158 + throw new Exception( __( 'This gateway does not support refund.', 'learnpress' ) );
1159 + }
1160 +}