PluginProbe ʕ •ᴥ•ʔ
Appointment Booking Plugin – LatePoint | Calendar & Scheduling for WordPress / 5.6.10
Appointment Booking Plugin – LatePoint | Calendar & Scheduling for WordPress v5.6.10
5.6.10 5.6.9 5.6.8 5.6.7 5.6.6 5.6.5 5.6.4 5.6.3 5.6.2 5.6.1 5.6.0 5.5.2 5.5.1 5.5.0 5.4.2 trunk 5.1.0 5.1.1 5.1.2 5.1.3 5.1.4 5.1.5 5.1.6 5.1.7 5.1.8 5.1.9 5.1.91 5.1.92 5.1.93 5.1.94 5.2.0 5.2.1 5.2.10 5.2.11 5.2.2 5.2.3 5.2.4 5.2.5 5.2.6 5.2.7 5.2.8 5.2.9 5.3.0 5.3.1 5.3.2 5.4.0 5.4.1
latepoint / lib / helpers / paypal_connect_helper.php
latepoint / lib / helpers Last commit date
activities_helper.php 5 months ago admin_nudges_helper.php 3 weeks ago agent_helper.php 5 months ago analytics_helper.php 3 weeks ago auth_helper.php 1 month ago blocks_helper.php 1 month ago booking_helper.php 1 month ago bricks_helper.php 5 months ago bundles_helper.php 1 month ago calendar_helper.php 3 weeks ago carts_helper.php 5 months ago connector_helper.php 5 months ago csv_helper.php 5 months ago customer_helper.php 2 months ago customer_import_helper.php 2 months ago database_helper.php 1 month ago debug_helper.php 5 months ago defaults_helper.php 5 months ago elementor_helper.php 5 months ago email_helper.php 5 months ago encrypt_helper.php 5 months ago events_helper.php 4 months ago form_helper.php 5 months ago icalendar_helper.php 5 months ago image_helper.php 5 months ago invoices_helper.php 1 month ago license_helper.php 5 months ago location_helper.php 5 months ago marketing_systems_helper.php 5 months ago meeting_systems_helper.php 5 months ago menu_helper.php 1 month ago meta_helper.php 5 months ago migrations_helper.php 5 months ago money_helper.php 4 months ago notifications_helper.php 5 months ago nps_survey_helper.php 5 months ago order_intent_helper.php 3 months ago orders_helper.php 1 month ago otp_helper.php 4 months ago pages_helper.php 5 months ago params_helper.php 5 months ago payments_helper.php 1 month ago paypal_connect_helper.php 3 weeks ago plugin_version_update_helper.php 3 months ago price_breakdown_helper.php 5 months ago process_jobs_helper.php 3 weeks ago processes_helper.php 5 months ago razorpay_connect_helper.php 3 weeks ago replacer_helper.php 5 months ago resource_helper.php 1 month ago roles_helper.php 3 weeks ago router_helper.php 5 months ago service_helper.php 5 months ago sessions_helper.php 5 months ago settings_helper.php 1 month ago short_links_systems_helper.php 5 months ago shortcodes_helper.php 1 month ago sms_helper.php 5 months ago steps_helper.php 1 week ago stripe_connect_helper.php 1 month ago styles_helper.php 5 months ago support_topics_helper.php 5 months ago time_helper.php 4 months ago timeline_helper.php 3 months ago transaction_helper.php 2 months ago transaction_intent_helper.php 5 months ago util_helper.php 3 weeks ago version_specific_updates_helper.php 1 month ago whatsapp_helper.php 2 months ago work_periods_helper.php 4 months ago wp_datetime.php 5 months ago wp_user_helper.php 5 months ago
paypal_connect_helper.php
749 lines
1 <?php
2
3 class OsPaypalConnectHelper {
4 public static $default_currency_iso_code = 'usd';
5 public static $processor_code = 'paypal_ppcp';
6
7
8 public static function add_all_payment_methods_to_payment_times( array $payment_times ): array {
9 $payment_methods = self::get_supported_payment_methods();
10 foreach ( $payment_methods as $payment_method_code => $payment_method_info ) {
11 $payment_times[ LATEPOINT_PAYMENT_TIME_NOW ][ $payment_method_code ][ self::$processor_code ] = $payment_method_info;
12 }
13 return $payment_times;
14 }
15
16 public static function add_enabled_payment_methods_to_payment_times( array $payment_times ): array {
17 if ( OsPaymentsHelper::is_payment_processor_enabled( self::$processor_code ) ) {
18 $payment_times = self::add_all_payment_methods_to_payment_times( $payment_times );
19 }
20 return $payment_times;
21 }
22
23 public static function is_supported_payment_method( $method ): bool {
24 return in_array( $method, array( 'paypal_buttons', 'paypal_card' ), true );
25 }
26
27 public static function process_refund( $transaction_refund, OsTransactionModel $transaction, $custom_amount = null ) {
28 if ( $transaction->processor != self::$processor_code ) {
29 return $transaction_refund;
30 }
31
32 if ( ! $transaction->can_refund() ) {
33 throw new Exception( 'Invalid Transaction' );
34 }
35
36 $refund_data = [
37 'capture_id' => $transaction->token,
38 'currency' => strtoupper( self::get_currency_iso_code() ),
39 ];
40 if ( $custom_amount ) {
41 $refund_data['custom_amount'] = self::convert_amount_to_specs( $custom_amount );
42 }
43
44 $response = self::do_account_request( 'refunds', OsSettingsHelper::get_payments_environment(), '', 'POST', $refund_data );
45
46 if ( empty( $response['data'] ) ) {
47 throw new Exception( __( 'Error Refunding', 'latepoint' ) );
48 }
49
50 $transaction_refund = new OsTransactionRefundModel();
51 $transaction_refund->transaction_id = $transaction->id;
52 $transaction_refund->amount = $response['data']['amount'];
53 $transaction_refund->token = $response['data']['id'];
54 if ( $transaction_refund->save() ) {
55 do_action( 'latepoint_transaction_refund_created', $transaction_refund );
56 return $transaction_refund;
57 } else {
58 throw new Exception( implode( ', ', $transaction_refund->get_error_messages() ) );
59 }
60 }
61
62 public static function convert_transaction_intent_charge_amount_to_specs( $amount, OsTransactionIntentModel $transaction_intent ) {
63 if ( OsPaymentsHelper::should_processor_handle_payment_for_transaction_intent( self::$processor_code, $transaction_intent ) ) {
64 $amount = self::convert_amount_to_specs( $amount );
65 }
66 return $amount;
67 }
68
69 public static function process_payment_for_transaction_intent( $result, OsTransactionIntentModel $transaction_intent ) {
70 if ( ! OsPaymentsHelper::should_processor_handle_payment_for_transaction_intent( self::$processor_code, $transaction_intent ) ) {
71 return $result;
72 }
73
74 if ( self::is_supported_payment_method( $transaction_intent->get_payment_data_value( 'method' ) ) ) {
75 if ( $transaction_intent->get_payment_data_value( 'token' ) ) {
76 $capture = self::retrieve_capture( $transaction_intent->get_payment_data_value( 'token' ) );
77 if ( ! empty( $capture ) && ( $capture['status'] ?? '' ) === 'COMPLETED' ) {
78 $expected_order_id = $transaction_intent->get_payment_data_value( 'paypal_order_id' );
79 if ( self::validate_capture_amount( $capture, $transaction_intent->charge_amount )
80 && self::validate_capture_binding( $capture, 'TRANS_INTENT_', $transaction_intent->intent_key )
81 && self::validate_capture_order_binding( $capture, $expected_order_id ) ) {
82 $result['status'] = LATEPOINT_STATUS_SUCCESS;
83 $result['processor'] = self::$processor_code;
84 $result['charge_id'] = $capture['id'];
85 $result['kind'] = LATEPOINT_TRANSACTION_KIND_CAPTURE;
86 } else {
87 $result['status'] = LATEPOINT_STATUS_ERROR;
88 $result['message'] = __( 'Payment verification failed', 'latepoint' );
89 OsDebugHelper::log( 'PayPal capture verification failed (amount/currency/binding) for transaction intent ' . $transaction_intent->id, 'paypal_connect_error' );
90 $transaction_intent->add_error( 'send_to_step', $result['message'], 'payment' );
91 }
92 } else {
93 $result['status'] = LATEPOINT_STATUS_ERROR;
94 $result['message'] = __( 'Payment Error', 'latepoint' );
95 $transaction_intent->add_error( 'send_to_step', $result['message'], 'payment' );
96 }
97 } else {
98 $result['status'] = LATEPOINT_STATUS_ERROR;
99 $result['message'] = __( 'Payment Error', 'latepoint' );
100 $transaction_intent->add_error( 'send_to_step', $result['message'], 'payment' );
101 }
102 }
103 return $result;
104 }
105
106 public static function process_payment( $result, OsOrderIntentModel $order_intent ) {
107 if ( ! OsPaymentsHelper::should_processor_handle_payment_for_order_intent( self::$processor_code, $order_intent ) ) {
108 return $result;
109 }
110
111 if ( self::is_supported_payment_method( $order_intent->get_payment_data_value( 'method' ) ) ) {
112 if ( $order_intent->get_payment_data_value( 'token' ) ) {
113 $capture = self::retrieve_capture( $order_intent->get_payment_data_value( 'token' ) );
114 if ( ! empty( $capture ) && ( $capture['status'] ?? '' ) === 'COMPLETED' ) {
115 $expected_order_id = $order_intent->get_other_data_value( 'paypal_order_id' );
116 if ( self::validate_capture_amount( $capture, $order_intent->charge_amount )
117 && self::validate_capture_binding( $capture, 'ORDER_INTENT_', $order_intent->intent_key )
118 && self::validate_capture_order_binding( $capture, $expected_order_id ) ) {
119 $result['status'] = LATEPOINT_STATUS_SUCCESS;
120 $result['processor'] = self::$processor_code;
121 $result['charge_id'] = $capture['id'];
122 $result['kind'] = LATEPOINT_TRANSACTION_KIND_CAPTURE;
123 } else {
124 $result['status'] = LATEPOINT_STATUS_ERROR;
125 $result['message'] = __( 'Payment verification failed', 'latepoint' );
126 OsDebugHelper::log( 'PayPal capture verification failed (amount/currency/binding) for order intent ' . $order_intent->id, 'paypal_connect_error' );
127 $order_intent->add_error( 'payment_error', $result['message'] );
128 $order_intent->add_error( 'send_to_step', $result['message'], 'payment' );
129 }
130 } else {
131 $result['status'] = LATEPOINT_STATUS_ERROR;
132 $result['message'] = __( 'Payment Error', 'latepoint' );
133 $order_intent->add_error( 'payment_error', $result['message'] );
134 $order_intent->add_error( 'send_to_step', $result['message'], 'payment' );
135 }
136 } else {
137 $result['status'] = LATEPOINT_STATUS_ERROR;
138 $result['message'] = __( 'Payment Error', 'latepoint' );
139 $order_intent->add_error( 'payment_error', $result['message'] );
140 $order_intent->add_error( 'send_to_step', $result['message'], 'payment' );
141 }
142 }
143
144 return $result;
145 }
146
147 public static function convert_charge_amount_to_requirements( $charge_amount, OsCartModel $cart ) {
148 if ( OsPaymentsHelper::should_processor_handle_payment_for_cart( self::$processor_code, $cart ) ) {
149 $charge_amount = self::convert_amount_to_specs( $charge_amount );
150 }
151 return $charge_amount;
152 }
153
154 /**
155 * PayPal uses decimal strings ("50.00"), not integer cents like Stripe
156 */
157 public static function convert_amount_to_specs( $charge_amount ) {
158 $iso_code = self::get_currency_iso_code();
159 if ( in_array( strtolower( $iso_code ), self::zero_decimal_currencies_list() ) ) {
160 $charge_amount = (string) round( $charge_amount );
161 } else {
162 $number_of_decimals = OsSettingsHelper::get_settings_value( 'number_of_decimals', '2' );
163 $charge_amount = number_format( (float) $charge_amount, $number_of_decimals, '.', '' );
164 }
165 return $charge_amount;
166 }
167
168 public static function convert_amount_back_from_specs_to_db_format( $charge_amount ) {
169 return OsMoneyHelper::pad_to_db_format( $charge_amount );
170 }
171
172 public static function retrieve_capture( string $capture_id ): array {
173 $response = self::do_account_request( 'captures/' . $capture_id, OsSettingsHelper::get_payments_environment() );
174 return $response['data'] ?? [];
175 }
176
177 public static function validate_capture_amount( array $capture, string $expected_charge_amount ): bool {
178 $expected = self::convert_amount_to_specs( $expected_charge_amount );
179 $actual = $capture['amount']['value'] ?? '';
180 return abs( (float) $expected - (float) $actual ) < 0.01;
181 }
182
183 public static function validate_capture_currency( array $capture ): bool {
184 $expected = strtoupper( self::get_currency_iso_code() );
185 $actual = strtoupper( $capture['amount']['currency_code'] ?? '' );
186 return ! empty( $actual ) && $actual === $expected;
187 }
188
189 public static function validate_capture_binding( array $capture, string $prefix, string $expected_intent_key ): bool {
190 if ( empty( $expected_intent_key ) ) {
191 return false;
192 }
193 $custom_id = $capture['custom_id'] ?? '';
194 $actual_intent_key = str_starts_with( $custom_id, $prefix ) ? substr( $custom_id, strlen( $prefix ) ) : '';
195 return ! empty( $actual_intent_key ) && hash_equals( $expected_intent_key, $actual_intent_key );
196 }
197
198 public static function validate_capture_order_binding( array $capture, string $expected_order_id ): bool {
199 if ( empty( $expected_order_id ) ) {
200 return false;
201 }
202 $actual_order_id = $capture['supplementary_data']['related_ids']['order_id'] ?? '';
203 return ! empty( $actual_order_id ) && $actual_order_id === $expected_order_id;
204 }
205
206 public static function output_order_payment_pay_contents( OsTransactionIntentModel $transaction_intent ) {
207 if ( ! OsPaymentsHelper::should_processor_handle_payment_for_transaction_intent( self::$processor_code, $transaction_intent ) ) {
208 return;
209 }
210 self::render_paypal_payment_view( $transaction_intent->specs_charge_amount );
211 }
212
213 public static function output_payment_step_contents( OsCartModel $cart ) {
214 if ( ! OsPaymentsHelper::should_processor_handle_payment_for_cart( self::$processor_code, $cart ) ) {
215 return;
216 }
217 self::render_paypal_payment_view( self::convert_amount_to_specs( $cart->get_total() ) );
218 }
219
220 private static function render_paypal_payment_view( string $charge_amount = '' ): void {
221 echo '<div class="lp-payment-method-content" data-payment-method="paypal_buttons"'
222 . ( $charge_amount !== '' ? ' data-charge-amount="' . esc_attr( $charge_amount ) . '"' : '' )
223 . '>';
224 echo '<div class="lp-payment-method-content-i">';
225 echo '<div class="lp-paypal-connect-button-container">';
226 echo '<paypal-button id="latepoint-paypal-button" type="pay" hidden></paypal-button>';
227 echo '<venmo-button id="latepoint-venmo-button" type="pay" hidden></venmo-button>';
228 echo '<paypal-pay-later-button id="latepoint-paylater-button" type="pay" hidden></paypal-pay-later-button>';
229 echo '</div>';
230 if ( self::get_acdc_eligible() ) {
231 echo '<div class="lp-paypal-card-separator"><span>' . esc_html__( 'or pay with card', 'latepoint' ) . '</span></div>';
232
233 echo '<div id="card-form" class="card_container lp-paypal-card-fields-container">';
234 echo '<div id="card-number-field-container" class="lp-card-number-field"></div>';
235
236 echo '<div class="lp-card-fields-row">';
237 echo '<div id="card-expiry-field-container" class="lp-card-expiry-field"></div>';
238 echo '<div id="card-cvv-field-container" class="lp-card-cvv-field"></div>';
239 echo '</div>';
240
241 echo '<button id="card-field-submit-button" type="button" class="lp-card-submit-btn latepoint-btn latepoint-btn-primary">'
242 . esc_html__( 'Pay with Card', 'latepoint' )
243 . '</button>';
244 echo '</div>';
245
246 // echo '<div class="lp-paypal-card-disclosure">';
247 // echo '<img src="https://www.paypalobjects.com/paypal-ui/logos/svg/paypal-mark-color.svg" alt="PayPal" height="16" /> ';
248 // echo esc_html__( 'Payments powered by PayPal', 'latepoint' );
249 // echo '</div>';
250 }
251 echo '</div>';
252 echo '</div>';
253 }
254
255 public static function get_acdc_eligible(): bool {
256 return OsSettingsHelper::get_settings_value(
257 OsSettingsHelper::append_payment_env_key( 'paypal_connect_acdc_vetting_status' ),
258 ''
259 ) === 'SUBSCRIBED';
260 }
261
262 private static function map_payment_method_for_api( string $method ): string {
263 switch ( $method ) {
264 case 'paypal_card':
265 return 'paypal_card';
266 default:
267 return 'paypal_buttons';
268 }
269 }
270
271 public static function get_supported_payment_methods(): array {
272 return [
273 'paypal_buttons' => [
274 'name' => __( 'PayPal', 'latepoint' ),
275 'label' => __( 'PayPal', 'latepoint' ),
276 'image_url' => LATEPOINT_IMAGES_URL . 'payment_paypal.png',
277 ],
278 ];
279 }
280
281 public static function register_payment_processor( array $payment_processors ): array {
282 $payment_processors[ self::$processor_code ] = [
283 'code' => self::$processor_code,
284 'name' => __( 'PayPal Checkout', 'latepoint' ),
285 'front_name' => __( 'PayPal Checkout', 'latepoint' ),
286 'image_url' => LATEPOINT_IMAGES_URL . 'processor-paypal.png',
287 ];
288 return $payment_processors;
289 }
290
291 public static function transaction_is_refund_available( $result, OsTransactionModel $transaction_model ) {
292 if ( $transaction_model->processor == self::$processor_code ) {
293 $result = true;
294 }
295 return $result;
296 }
297
298 /**
299 * Outputs a payment source badge on the booking confirmation step.
300 * The actual method (PayPal / Venmo / Pay Later) is read from sessionStorage
301 * in the browser — set there by the JS capture handler.
302 *
303 * @param OsOrderModel $order
304 */
305 public static function output_confirmation_payment_source( $order ) {
306 // Only show for PayPal-processed orders.
307 $transactions = $order->get_transactions();
308 $is_paypal = false;
309 foreach ( $transactions as $transaction ) {
310 if ( $transaction->processor === self::$processor_code ) {
311 $is_paypal = true;
312 break;
313 }
314 }
315 if ( ! $is_paypal ) {
316 return;
317 }
318 $labels = [
319 'paypal' => __( 'PayPal', 'latepoint' ),
320 'venmo' => __( 'Venmo', 'latepoint' ),
321 'paylater' => __( 'Pay Later', 'latepoint' ),
322 'card' => __( 'Credit/Debit Card', 'latepoint' ),
323 ];
324 $labels_json = wp_json_encode( $labels );
325 ?>
326 <div class="lp-paypal-payment-source" style="display:none;">
327 <span class="lp-paypal-payment-source-label"></span>
328 </div>
329 <script>
330 (function() {
331 var source = sessionStorage.getItem('lp_paypal_payment_source') || 'paypal';
332 var labels = <?php echo $labels_json; ?>;
333 var label = labels[source] || labels['paypal'];
334 var wrapper = document.querySelector('.lp-paypal-payment-source');
335 if (wrapper) {
336 wrapper.querySelector('.lp-paypal-payment-source-label').textContent = label;
337 wrapper.style.display = '';
338 sessionStorage.removeItem('lp_paypal_payment_source');
339 }
340 })();
341 </script>
342 <?php
343 }
344
345 public static function add_settings_fields( $processor_code ) {
346 if ( $processor_code != self::$processor_code ) {
347 return false;
348 } ?>
349 <div class="sub-section-row">
350 <div class="sub-section-label">
351 <h3><?php esc_html_e( 'Connect (Live)', 'latepoint' ); ?></h3>
352 </div>
353 <div class="sub-section-content">
354 <div data-env="<?php echo esc_attr( LATEPOINT_PAYMENTS_ENV_LIVE ); ?>"
355 class="payment-processor-connect-status-wrapper paypal-connect-status-wrapper"
356 data-route-name="<?php echo esc_attr( OsRouterHelper::build_route_name( 'paypal_connect', 'check_connect_status' ) ); ?>">
357 <div class="os-loading-spinner"></div>
358 </div>
359 </div>
360 </div>
361 <div class="sub-section-row">
362 <div class="sub-section-label">
363 <h3><?php esc_html_e( 'Connect (Dev)', 'latepoint' ); ?></h3>
364 </div>
365 <div class="sub-section-content">
366 <div data-env="<?php echo esc_attr( LATEPOINT_PAYMENTS_ENV_DEV ); ?>"
367 class="payment-processor-connect-status-wrapper paypal-connect-status-wrapper"
368 data-route-name="<?php echo esc_attr( OsRouterHelper::build_route_name( 'paypal_connect', 'check_connect_status' ) ); ?>">
369 <div class="os-loading-spinner"></div>
370 </div>
371 </div>
372 </div>
373 <div class="sub-section-row">
374 <div class="sub-section-label">
375 <h3><?php esc_html_e( 'Other Settings', 'latepoint' ); ?></h3>
376 </div>
377 <div class="sub-section-content">
378 <?php
379 $selected_paypal_country_code = OsSettingsHelper::get_settings_value( 'paypal_connect_country_code', 'US' );
380 $selected_paypal_currency_iso_code = OsSettingsHelper::get_settings_value( 'paypal_connect_currency_iso_code', 'usd' ); ?>
381 <div class="os-row os-mb-2">
382 <div class="os-col-6">
383 <?php echo OsFormHelper::select_field( 'settings[paypal_connect_country_code]', __( 'Country', 'latepoint' ), self::load_countries_list(), $selected_paypal_country_code ); ?>
384 </div>
385 <div class="os-col-6">
386 <?php echo OsFormHelper::select_field( 'settings[paypal_connect_currency_iso_code]', __( 'Currency Code', 'latepoint' ), self::load_all_currencies_list(), $selected_paypal_currency_iso_code ); ?>
387 </div>
388 </div>
389 <div class="os-row os-mb-2">
390 <div class="os-col-12">
391 <?php echo OsFormHelper::toggler_field(
392 'settings[paypal_connect_disable_pay_later]',
393 __( 'Disable Pay Later', 'latepoint' ),
394 OsSettingsHelper::is_on( 'paypal_connect_disable_pay_later' ),
395 false,
396 false,
397 [ 'sub_label' => __( 'Hide PayPal Pay Later option from the payment buttons.', 'latepoint' ) ]
398 ); ?>
399 </div>
400 </div>
401 </div>
402 </div>
403 <?php
404 }
405
406 public static function get_currency_iso_code() {
407 return OsSettingsHelper::get_settings_value( 'paypal_connect_currency_iso_code', self::$default_currency_iso_code );
408 }
409
410 public static function get_server_token( string $force_env = '' ): string {
411 $key = OsSettingsHelper::append_payment_env_key( 'server_token_for_paypal_connect', $force_env );
412 $server_token = OsSettingsHelper::get_settings_value( $key, '' );
413 if ( empty( $server_token ) ) {
414 $server_token = OsUtilHelper::generate_uuid();
415 OsSettingsHelper::save_setting_by_name( $key, $server_token );
416 }
417 return $server_token;
418 }
419
420 public static function reset_server_token( string $force_env = '' ): string {
421 $key = OsSettingsHelper::append_payment_env_key( 'server_token_for_paypal_connect', $force_env );
422 $new_server_token = OsUtilHelper::generate_uuid();
423 OsSettingsHelper::save_setting_by_name( $key, $new_server_token );
424 return $new_server_token;
425 }
426
427 public static function get_connect_url( string $env = '' ) {
428 $url = LATEPOINT_PAYPAL_CONNECT_URL . '/wp/paypal-connection/' . $env . '/start/';
429 $url .= self::get_server_token( $env ) . '/' . base64_encode( implode( '|||', [ get_bloginfo( 'name' ), get_site_icon_url(), OsUtilHelper::get_site_url() ] ) );
430 return $url;
431 }
432
433 public static function get_connect_merchant_id( string $env = '' ): string {
434 return OsSettingsHelper::get_settings_value( OsSettingsHelper::append_payment_env_key( 'paypal_connect_merchant_id', $env ), '' );
435 }
436
437 public static function get_partner_client_id(): string {
438 $env = OsSettingsHelper::get_payments_environment();
439 $key = OsSettingsHelper::append_payment_env_key( 'paypal_connect_partner_client_id', $env );
440 $client_id = OsSettingsHelper::get_settings_value( $key, '' );
441 if ( empty( $client_id ) ) {
442 $response = self::do_request( 'partner-client-id/' );
443 $client_id = $response['data']['client_id'] ?? '';
444 if ( ! empty( $client_id ) ) {
445 OsSettingsHelper::save_setting_by_name( $key, $client_id );
446 }
447 }
448 return $client_id;
449 }
450
451 public static function generate_client_token(): string {
452 $token = get_transient( 'latepoint_paypal_client_token' );
453 if ( ! empty( $token ) ) {
454 return $token;
455 }
456 try {
457 $response = self::do_account_request( 'client-token/', OsSettingsHelper::get_payments_environment(), '', 'POST' );
458 $token = $response['data']['client_token'] ?? '';
459 if ( ! empty( $token ) ) {
460 set_transient( 'latepoint_paypal_client_token', $token, 3000 );
461 }
462 return $token;
463 } catch ( \Exception $e ) {
464 OsDebugHelper::log( 'Error generating PayPal client token: ' . $e->getMessage(), 'paypal_connect_error' );
465 return '';
466 }
467 }
468
469 public static function do_account_request( string $path, string $env = '', string $connection_data = '', string $method = 'GET', array $vars = [], array $headers = [] ) {
470 if ( empty( $env ) ) {
471 $env = OsSettingsHelper::get_payments_environment();
472 }
473 $path = self::get_connect_merchant_id( $env ) . '/' . $path;
474 try {
475 return self::do_request( $path, $connection_data, $method, $vars, $headers );
476 } catch ( \Exception $e ) {
477 OsDebugHelper::log( 'Error processing request to PayPal: ' . $e->getMessage(), 'paypal_connect_error' );
478 return [];
479 }
480 }
481
482 public static function do_request( string $path, string $connection_data = '', string $method = 'GET', array $vars = [], array $headers = [], string $force_env = '' ) {
483 $default_vars = [];
484 $default_headers = [
485 'latepoint-version' => LATEPOINT_VERSION,
486 'latepoint-domain' => OsUtilHelper::get_site_url(),
487 'latepoint-license-key' => OsLicenseHelper::get_license_key(),
488 ];
489
490 if ( ! empty( $connection_data ) ) {
491 $default_headers['connection-data'] = $connection_data;
492 }
493
494 $args = array(
495 'timeout' => 15,
496 'headers' => array_merge( $default_headers, $headers ),
497 'body' => array_merge( $default_vars, $vars ),
498 'sslverify' => false,
499 'method' => $method,
500 );
501
502 if ( ! empty( $force_env ) && in_array( $force_env, [ LATEPOINT_PAYMENTS_ENV_DEV, LATEPOINT_PAYMENTS_ENV_LIVE ] ) ) {
503 $env = ( $force_env == LATEPOINT_PAYMENTS_ENV_DEV ) ? 'test' : 'live';
504 } else {
505 $env = ( OsSettingsHelper::is_env_payments_dev() ? 'test' : 'live' );
506 }
507 $url = LATEPOINT_PAYPAL_CONNECT_URL . "/api/wp/v1/paypal-connect/{$env}/{$path}";
508
509 $response = wp_remote_request( $url, $args );
510
511 if ( ! is_wp_error( $response ) ) {
512 $data = json_decode( wp_remote_retrieve_body( $response ), true );
513 $data['status'] = $response['response'];
514 return $data;
515 } else {
516 $error_message = $response->get_error_message();
517 throw new Exception( $error_message );
518 }
519 }
520
521 public static function create_order_for_order_intent( OsOrderIntentModel $order_intent, string $payment_method = 'paypal_buttons' ): array {
522 $order_data = [
523 'order_options' => [
524 'amount' => self::convert_amount_to_specs( $order_intent->charge_amount ),
525 'currency' => self::get_currency_iso_code(),
526 'item_name' => __( 'Appointment Booking', 'latepoint' ),
527 'brand_name' => get_bloginfo( 'name' ),
528 'payment_method' => self::map_payment_method_for_api( $payment_method ),
529 'metadata' => [
530 'order_intent_key' => $order_intent->intent_key,
531 ],
532 ],
533 ];
534
535 $response = self::do_account_request( 'orders', OsSettingsHelper::get_payments_environment(), '', 'POST', $order_data );
536
537 if ( ! empty( $response['error'] ) ) {
538 throw new Exception( $response['error'] );
539 }
540
541 if ( ! empty( $response['data']['order_id'] ) ) {
542 $order_intent->set_other_data_value( 'paypal_order_id', $response['data']['order_id'] );
543 }
544
545 return [
546 'order_id' => $response['data']['order_id'] ?? '',
547 'status' => $response['data']['status'] ?? '',
548 ];
549 }
550
551 public static function create_order_for_transaction_intent( OsTransactionIntentModel $transaction_intent, string $payment_method = 'paypal_buttons' ): array {
552 $order_data = [
553 'order_options' => [
554 'amount' => $transaction_intent->specs_charge_amount,
555 'currency' => self::get_currency_iso_code(),
556 'item_name' => __( 'Appointment Booking', 'latepoint' ),
557 'brand_name' => get_bloginfo( 'name' ),
558 'payment_method' => self::map_payment_method_for_api( $payment_method ),
559 'metadata' => [
560 'transaction_intent_key' => $transaction_intent->intent_key,
561 ],
562 ],
563 ];
564
565 $response = self::do_account_request( 'orders', OsSettingsHelper::get_payments_environment(), '', 'POST', $order_data );
566
567 if ( ! empty( $response['error'] ) ) {
568 throw new Exception( $response['error'] );
569 }
570
571 if ( ! empty( $response['data']['order_id'] ) ) {
572 $transaction_intent->set_payment_data_value( 'paypal_order_id', $response['data']['order_id'] );
573 }
574
575 return [
576 'order_id' => $response['data']['order_id'] ?? '',
577 'status' => $response['data']['status'] ?? '',
578 ];
579 }
580
581 public static function capture_order( string $paypal_order_id ): array {
582 $response = self::do_account_request( 'orders/' . $paypal_order_id . '/capture', OsSettingsHelper::get_payments_environment(), '', 'POST' );
583
584 if ( ! empty( $response['error'] ) ) {
585 throw new Exception( $response['error'] );
586 }
587
588 return [
589 'order_id' => $response['data']['order_id'] ?? '',
590 'capture_id' => $response['data']['capture_id'] ?? '',
591 'status' => $response['data']['status'] ?? '',
592 'payment_source_type' => $response['data']['payment_source_type'] ?? 'paypal',
593 ];
594 }
595
596 public static function get_connection_buttons_and_status( string $env = '' ) {
597 $paypal_connect_merchant_id = OsSettingsHelper::get_settings_value( OsSettingsHelper::append_payment_env_key( 'paypal_connect_merchant_id', $env ), false );
598 $html = '';
599 $duplicate_token_activations = OsSettingsHelper::get_settings_value( OsSettingsHelper::append_payment_env_key( 'paypal_connect_duplicate_token_activations', $env ), '' );
600 if ( ! empty( $duplicate_token_activations ) ) {
601 $html .= '<div class="latepoint-fix-records-warning"><i class="latepoint-icon latepoint-icon-info"></i><div>';
602 $html .= '<div>' . __( 'The following websites are using the same server token. This can happen if a site was cloned from one server to another. To fix this, disconnect each site and reconnect it.', 'latepoint' ) . '</div>';
603 $html .= '<div class="latepoint-fix-records-values">' . esc_html( $duplicate_token_activations ) . '</div>';
604 $html .= '</div></div>';
605 }
606 $html .= '<div class="payment-processor-connect-status-inner">';
607 if ( $paypal_connect_merchant_id ) {
608 $payments_enabled = OsSettingsHelper::is_on( OsSettingsHelper::append_payment_env_key( 'paypal_connect_payments_enabled', $env ) );
609 $payments_receivable = OsSettingsHelper::is_on( OsSettingsHelper::append_payment_env_key( 'paypal_connect_payments_receivable', $env ) );
610 $primary_email_confirmed = OsSettingsHelper::is_on( OsSettingsHelper::append_payment_env_key( 'paypal_connect_primary_email_confirmed', $env ) );
611 $oauth_valid = OsSettingsHelper::is_on( OsSettingsHelper::append_payment_env_key( 'paypal_connect_oauth_integrations_valid', $env ) );
612 $acdc_vetting_status = OsSettingsHelper::get_settings_value( OsSettingsHelper::append_payment_env_key( 'paypal_connect_acdc_vetting_status', $env ), '' );
613
614 $disconnect_link = '<a class="payment-processor-disconnect-link" href="#"
615 data-os-pass-response="yes"
616 data-os-pass-this="yes"
617 data-os-before-after="none"
618 data-os-after-call="latepointPaypalConnectAdmin.reload_connect_status_wrapper"
619 data-os-params="' . OsUtilHelper::build_os_params( [ 'env' => $env ] ) . '"
620 data-os-action="' . OsRouterHelper::build_route_name( 'paypal_connect', 'disconnect_connect_account' ) . '"
621 ><i class="latepoint-icon latepoint-icon-x"></i><span>' . __( 'disconnect', 'latepoint' ) . '</span></a>';
622 if ( $payments_enabled ) {
623 $html .= '<div class="payment-processor-status-connected"><i class="latepoint-icon latepoint-icon-check"></i><span>' . __( 'Connected', 'latepoint' ) . '</span></div>';
624 $html .= $disconnect_link;
625 $html .= '<div class="paypal-connect-account-info">' . __( 'Merchant: ', 'latepoint' ) . esc_html( $paypal_connect_merchant_id ) . '</div>';
626 if ( $env == 'live' && ! empty( OsSettingsHelper::get_settings_value( 'paypal_connect_transaction_fee_info', '' ) ) ) {
627 $html .= '<div class="fee-disclosure-wrapper">
628 <div class="fee-disclosure">' . esc_html( OsSettingsHelper::get_settings_value( 'paypal_connect_transaction_fee_info', '' ) ) . ' transaction fee. <a target="_blank" href="https://wpdocs.latepoint.com/understanding-payment-processing-fees/"><span>Pricing</span> <i class="latepoint-icon latepoint-icon-external-link"></i></a></div>
629 </div>';
630 }
631 // Advisory notice: oauth_integrations not yet propagated. Payments still work.
632 if ( ! $oauth_valid ) {
633 $html .= '<div class="paypal-connect-status-info-wrapper">';
634 $html .= '<div class="paypal-connect-status-item paypal-connect-status-item--info"><i class="latepoint-icon latepoint-icon-info"></i> ' . __( 'Payment permission scopes not yet detected. Payments are active, but you may want to reconnect if issues arise.', 'latepoint' ) . '</div>';
635 $html .= '</div>';
636 }
637 } else {
638 $html .= '<div class="payment-processor-status-charges-disabled"><i class="latepoint-icon latepoint-icon-clock"></i><span>' . __( 'Pending Action', 'latepoint' ) . '</span></div>';
639
640 // Show which conditions are blocking payments so seller can act.
641 $html .= '<div class="paypal-connect-status-checklist">';
642 if ( ! $primary_email_confirmed ) {
643 $html .= '<div class="paypal-connect-status-item paypal-connect-status-item--warning"><i class="latepoint-icon latepoint-icon-alert-triangle"></i> ' . __( 'PayPal account email not yet confirmed. Please confirm your PayPal email address.', 'latepoint' ) . '</div>';
644 }
645 if ( ! $payments_receivable ) {
646 $html .= '<div class="paypal-connect-status-item paypal-connect-status-item--warning"><i class="latepoint-icon latepoint-icon-alert-triangle"></i> ' . __( 'Your PayPal account cannot receive payments yet. Please complete your account setup on PayPal.', 'latepoint' ) . '</div>';
647 }
648 $html .= '</div>';
649
650 // $html .= '<a data-env="' . esc_attr( $env ) . '" data-route-name="' . esc_attr( OsRouterHelper::build_route_name( 'paypal_connect', 'start_connect_process' ) ) . '" href="#" class="payment-start-connecting"><span>' . __( 'Continue Setup', 'latepoint' ) . '</span><i class="latepoint-icon latepoint-icon-arrow-right"></i></a>';
651 $html .= '<div class="paypal-connect-account-info">';
652 $html .= '<div>' . esc_html( $paypal_connect_merchant_id ) . '</div>';
653 $html .= $disconnect_link;
654 $html .= '</div>';
655 }
656
657 // ACDC vetting status (shown regardless of payments_enabled).
658 // if ( ! empty( $acdc_vetting_status ) && 'SUBSCRIBED' !== $acdc_vetting_status ) {
659 // $acdc_labels = [
660 // 'NEED_MORE_DATA' => __( 'Advanced card processing: additional information required.', 'latepoint' ),
661 // 'IN_REVIEW' => __( 'Advanced card processing: your account is under review.', 'latepoint' ),
662 // 'DENIED' => __( 'Advanced card processing: your application was denied.', 'latepoint' ),
663 // ];
664 // $acdc_message = $acdc_labels[ $acdc_vetting_status ] ?? '';
665 // if ( ! empty( $acdc_message ) ) {
666 // $html .= '<div class="paypal-connect-status-item paypal-connect-status-item--info"><i class="latepoint-icon latepoint-icon-info"></i> ' . esc_html( $acdc_message ) . '</div>';
667 // }
668 // }
669 } else {
670 $html .= '<a data-env="' . esc_attr( $env ) . '" data-route-name="' . esc_attr( OsRouterHelper::build_route_name( 'paypal_connect', 'start_connect_process' ) ) . '" href="#" class="payment-start-connecting"><span>' . __( 'Start Connecting', 'latepoint' ) . '</span><i class="latepoint-icon latepoint-icon-arrow-right"></i></a>';
671 }
672 $html .= '</div>';
673 return $html;
674 }
675
676 public static function zero_decimal_currencies_list() {
677 return [ 'huf', 'jpy', 'twd' ];
678 }
679
680 public static function load_countries_list() {
681 return [
682 'US' => 'United States',
683 'AU' => 'Australia',
684 'AT' => 'Austria',
685 'BE' => 'Belgium',
686 'BR' => 'Brazil',
687 'CA' => 'Canada',
688 'CZ' => 'Czech Republic',
689 'DK' => 'Denmark',
690 'FI' => 'Finland',
691 'FR' => 'France',
692 'DE' => 'Germany',
693 'HK' => 'Hong Kong',
694 'HU' => 'Hungary',
695 'IN' => 'India',
696 'IL' => 'Israel',
697 'IT' => 'Italy',
698 'JP' => 'Japan',
699 'MY' => 'Malaysia',
700 'MX' => 'Mexico',
701 'NL' => 'Netherlands',
702 'NZ' => 'New Zealand',
703 'NO' => 'Norway',
704 'PH' => 'Philippines',
705 'PL' => 'Poland',
706 'PT' => 'Portugal',
707 'RU' => 'Russia',
708 'SG' => 'Singapore',
709 'ES' => 'Spain',
710 'SE' => 'Sweden',
711 'CH' => 'Switzerland',
712 'TW' => 'Taiwan',
713 'TH' => 'Thailand',
714 'GB' => 'United Kingdom',
715 ];
716 }
717
718 public static function load_all_currencies_list() {
719 return [
720 'aud' => 'AUD',
721 'brl' => 'BRL',
722 'cad' => 'CAD',
723 'cop' => 'COP',
724 'czk' => 'CZK',
725 'dkk' => 'DKK',
726 'eur' => 'EUR',
727 'hkd' => 'HKD',
728 'huf' => 'HUF',
729 'inr' => 'INR',
730 'ils' => 'ILS',
731 'jpy' => 'JPY',
732 'myr' => 'MYR',
733 'mxn' => 'MXN',
734 'twd' => 'TWD',
735 'nzd' => 'NZD',
736 'nok' => 'NOK',
737 'php' => 'PHP',
738 'pln' => 'PLN',
739 'gbp' => 'GBP',
740 'rub' => 'RUB',
741 'sgd' => 'SGD',
742 'sek' => 'SEK',
743 'chf' => 'CHF',
744 'thb' => 'THB',
745 'usd' => 'USD',
746 ];
747 }
748 }
749