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 / razorpay_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 3 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
razorpay_connect_helper.php
696 lines
1 <?php
2 /*
3 * Copyright (c) 2024 LatePoint LLC. All rights reserved.
4 */
5
6 if ( ! defined( 'ABSPATH' ) ) {
7 exit; // Exit if accessed directly.
8 }
9
10 class OsRazorpayConnectHelper {
11 public static $default_currency_iso_code = 'INR';
12 public static $processor_code = 'razorpay_connect';
13
14 // -------------------------------------------------------------------------
15 // Payment processor registration
16 // -------------------------------------------------------------------------
17
18 public static function register_payment_processor( array $payment_processors ): array {
19 $payment_processors[ self::$processor_code ] = [
20 'code' => self::$processor_code,
21 'name' => __( 'Razorpay Checkout', 'latepoint' ),
22 'front_name' => __( 'Razorpay Checkout', 'latepoint' ),
23 'image_url' => LATEPOINT_IMAGES_URL . 'processor-razorpay-logo.png',
24 ];
25 return $payment_processors;
26 }
27
28 public static function add_settings_fields( $processor_code ) {
29 if ( $processor_code !== self::$processor_code ) {
30 return false;
31 }
32 ?>
33 <div class="sub-section-row">
34 <div class="sub-section-label">
35 <h3><?php esc_html_e( 'Connect (Live)', 'latepoint' ); ?></h3>
36 </div>
37 <div class="sub-section-content">
38 <div data-env="<?php echo esc_attr( LATEPOINT_PAYMENTS_ENV_LIVE ); ?>"
39 class="payment-processor-connect-status-wrapper razorpay-connect-status-wrapper"
40 data-route-name="<?php echo esc_attr( OsRouterHelper::build_route_name( 'razorpay_connect', 'check_connect_status' ) ); ?>">
41 <div class="os-loading-spinner"></div>
42 </div>
43 </div>
44 </div>
45 <div class="sub-section-row">
46 <div class="sub-section-label">
47 <h3><?php esc_html_e( 'Connect (Dev)', 'latepoint' ); ?></h3>
48 </div>
49 <div class="sub-section-content">
50 <div data-env="<?php echo esc_attr( LATEPOINT_PAYMENTS_ENV_DEV ); ?>"
51 class="payment-processor-connect-status-wrapper razorpay-connect-status-wrapper"
52 data-route-name="<?php echo esc_attr( OsRouterHelper::build_route_name( 'razorpay_connect', 'check_connect_status' ) ); ?>">
53 <div class="os-loading-spinner"></div>
54 </div>
55 </div>
56 </div>
57 <div class="sub-section-row">
58 <div class="sub-section-label">
59 <h3><?php esc_html_e( 'Currency Settings', 'latepoint' ); ?></h3>
60 </div>
61 <div class="sub-section-content">
62 <?php
63 $selected_country_code = OsSettingsHelper::get_settings_value( 'razorpay_connect_country_code', 'IN' );
64 $selected_currency_code = OsSettingsHelper::get_settings_value( 'razorpay_connect_currency_iso_code', self::$default_currency_iso_code );
65 ?>
66 <div class="os-row">
67 <div class="os-col-6">
68 <?php echo OsFormHelper::select_field( 'settings[razorpay_connect_country_code]', __( 'Country', 'latepoint' ), self::load_countries_list(), $selected_country_code ); ?>
69 </div>
70 <div class="os-col-6">
71 <?php echo OsFormHelper::select_field( 'settings[razorpay_connect_currency_iso_code]', __( 'Currency Code', 'latepoint' ), self::load_currencies_list(), $selected_currency_code ); ?>
72 </div>
73 </div>
74 </div>
75 </div>
76 <div class="sub-section-row">
77 <div class="sub-section-label">
78 <h3><?php esc_html_e( 'Other Settings', 'latepoint' ); ?></h3>
79 </div>
80 <div class="sub-section-content">
81 <div class="os-row os-mb-1">
82 <div class="os-col-6">
83 <?php echo OsFormHelper::text_field( 'settings[razorpay_connect_company_name]', __( 'Company Name (Appears on Payment Modal)', 'latepoint' ), OsSettingsHelper::get_settings_value( 'razorpay_connect_company_name' ), [ 'theme' => 'simple' ] ); ?>
84 </div>
85 <div class="os-col-6">
86 <?php echo OsFormHelper::color_picker( 'settings[razorpay_connect_theme_color]', __( 'Color for Payment Modal', 'latepoint' ), OsSettingsHelper::get_settings_value( 'razorpay_connect_theme_color', '#4366ff' ) ); ?>
87 </div>
88 </div>
89 <div class="os-row os-mb-2">
90 <div class="os-col-12">
91 <?php echo OsFormHelper::media_uploader_field( 'settings[razorpay_connect_logo_image_id]', 0, __( 'Logo for Payment Modal', 'latepoint' ), __( 'Remove Logo', 'latepoint' ), OsSettingsHelper::get_settings_value( 'razorpay_connect_logo_image_id' ) ); ?>
92 </div>
93 </div>
94 </div>
95 </div>
96 <?php
97 }
98
99 // -------------------------------------------------------------------------
100 // Payment methods
101 // -------------------------------------------------------------------------
102
103 public static function get_supported_payment_methods(): array {
104 return [
105 'razorpay_checkout' => [
106 'name' => __( 'Razorpay Checkout', 'latepoint' ),
107 'label' => __( 'Pay with Razorpay', 'latepoint' ),
108 'image_url' => LATEPOINT_IMAGES_URL . 'processor-razorpay-logo.png',
109 ],
110 ];
111 }
112
113 public static function add_all_payment_methods_to_payment_times( array $payment_times ): array {
114 $payment_methods = self::get_supported_payment_methods();
115 foreach ( $payment_methods as $payment_method_code => $payment_method_info ) {
116 $payment_times[ LATEPOINT_PAYMENT_TIME_NOW ][ $payment_method_code ][ self::$processor_code ] = $payment_method_info;
117 }
118 return $payment_times;
119 }
120
121 public static function add_enabled_payment_methods_to_payment_times( array $payment_times ): array {
122 if ( OsPaymentsHelper::is_payment_processor_enabled( self::$processor_code ) ) {
123 $payment_times = self::add_all_payment_methods_to_payment_times( $payment_times );
124 }
125 return $payment_times;
126 }
127
128 // -------------------------------------------------------------------------
129 // Payment step UI
130 // -------------------------------------------------------------------------
131
132 public static function output_payment_step_contents( OsCartModel $cart ) {
133 if ( ! OsPaymentsHelper::should_processor_handle_payment_for_cart( self::$processor_code, $cart ) ) {
134 return;
135 }
136 echo '<div class="lp-payment-method-content" data-payment-method="razorpay_checkout">';
137 echo '<div class="lp-payment-method-content-i">';
138 echo '<div class="razorpay-payment-trigger"></div>';
139 echo '</div>';
140 echo '</div>';
141 }
142
143 public static function output_order_payment_pay_contents( OsTransactionIntentModel $transaction_intent ) {
144 if ( ! OsPaymentsHelper::should_processor_handle_payment_for_transaction_intent( self::$processor_code, $transaction_intent ) ) {
145 return;
146 }
147 echo '<div class="lp-payment-method-content" data-payment-method="razorpay_checkout">';
148 echo '<div class="lp-payment-method-content-i">';
149 echo '<div class="razorpay-payment-trigger"></div>';
150 echo '</div>';
151 echo '</div>';
152 }
153
154 // -------------------------------------------------------------------------
155 // Amount conversion
156 // -------------------------------------------------------------------------
157
158 public static function convert_charge_amount_to_requirements( $charge_amount, OsCartModel $cart ) {
159 if ( OsPaymentsHelper::should_processor_handle_payment_for_cart( self::$processor_code, $cart ) ) {
160 $charge_amount = self::convert_amount_to_specs( $charge_amount );
161 }
162 return $charge_amount;
163 }
164
165 public static function convert_transaction_intent_charge_amount_to_specs( $amount, OsTransactionIntentModel $transaction_intent ) {
166 if ( OsPaymentsHelper::should_processor_handle_payment_for_transaction_intent( self::$processor_code, $transaction_intent ) ) {
167 $amount = self::convert_amount_to_specs( $amount );
168 }
169 return $amount;
170 }
171
172 public static function convert_amount_to_specs( $charge_amount ) {
173 $iso_code = self::get_currency_iso_code();
174 if ( in_array( strtolower( $iso_code ), self::zero_decimal_currencies_list() ) ) {
175 $charge_amount = round( $charge_amount );
176 } else {
177 $number_of_decimals = OsSettingsHelper::get_settings_value( 'number_of_decimals', '2' );
178 $charge_amount = number_format( (float) $charge_amount, $number_of_decimals, '.', '' ) * pow( 10, $number_of_decimals );
179 }
180 return (int) $charge_amount;
181 }
182
183 public static function convert_amount_back_from_specs_to_db_format( $charge_amount ) {
184 $iso_code = self::get_currency_iso_code();
185 $number_of_decimals = OsSettingsHelper::get_settings_value( 'number_of_decimals', '2' );
186 if ( ! in_array( strtolower( $iso_code ), self::zero_decimal_currencies_list() ) && ! empty( $number_of_decimals ) ) {
187 $charge_amount = $charge_amount / pow( 10, $number_of_decimals );
188 $charge_amount = number_format( (float) $charge_amount, 4, '.', '' );
189 } else {
190 $charge_amount = OsMoneyHelper::pad_to_db_format( $charge_amount );
191 }
192 return $charge_amount;
193 }
194
195 public static function zero_decimal_currencies_list(): array {
196 return array( 'bif', 'clp', 'djf', 'gnf', 'jpy', 'kmf', 'krw', 'mga', 'pyg', 'rwf', 'ugx', 'vnd', 'vuv', 'xaf', 'xof', 'xpf' );
197 }
198
199 // -------------------------------------------------------------------------
200 // Payment verification
201 // -------------------------------------------------------------------------
202
203 // Reject a payment whose amount doesn't match the expected charge (blocks underpayment). Mirrors Stripe's validate_payment_intent_amount().
204 public static function validate_payment_amount( array $payment, string $expected_charge_amount ): bool {
205 $expected_in_specs = (int) self::convert_amount_to_specs( $expected_charge_amount );
206 $actual_from_razorpay = isset( $payment['amount'] ) ? (int) $payment['amount'] : 0;
207 return abs( $expected_in_specs - $actual_from_razorpay ) <= 1;
208 }
209
210 // Reject a payment whose currency doesn't match the configured currency; fails closed when currency is missing.
211 public static function validate_payment_currency( array $payment ): bool {
212 $expected = strtoupper( self::get_currency_iso_code() );
213 $actual = isset( $payment['currency'] ) ? strtoupper( (string) $payment['currency'] ) : '';
214 return ! empty( $actual ) && $actual === $expected;
215 }
216
217 // First (lightweight) bind: the payment's Razorpay notes must reference THIS intent. We stamp the intent key into
218 // the notes ($order_intent->intent_key under 'order_intent_key' / 'transaction_intent_key') when creating the
219 // order/checkout and Razorpay echoes it back on the retrieved payment. Notes originate from client-facing Checkout
220 // options, so this alone is not sufficient — the authoritative bind is validate_payment_order_binding() below.
221 // Fails closed when the note is absent.
222 public static function validate_payment_intent_notes( array $payment, string $note_key, string $expected_intent_key ): bool {
223 if ( empty( $expected_intent_key ) ) {
224 return false;
225 }
226 $notes = ( isset( $payment['notes'] ) && is_array( $payment['notes'] ) ) ? $payment['notes'] : [];
227 $actual_intent_key = isset( $notes[ $note_key ] ) ? (string) $notes[ $note_key ] : '';
228 return ! empty( $actual_intent_key ) && hash_equals( $expected_intent_key, $actual_intent_key );
229 }
230
231 // Authoritative bind: the payment must belong to the Razorpay order we created server-side for THIS intent. The
232 // payment's order_id (assigned by Razorpay when we created the order through our connected account) must equal the
233 // id we persisted at order-creation time. Unlike the notes, an attacker cannot make Razorpay attach our order_id
234 // to a payment they made against a different order, so this blocks a same-amount/same-currency payment whose
235 // order_id belongs to another order. Fails closed when the expected id is unknown or the order_id differs.
236 public static function validate_payment_order_binding( array $payment, string $expected_order_id ): bool {
237 if ( empty( $expected_order_id ) ) {
238 return false;
239 }
240 $actual_order_id = isset( $payment['order_id'] ) ? (string) $payment['order_id'] : '';
241 return ! empty( $actual_order_id ) && hash_equals( $expected_order_id, $actual_order_id );
242 }
243
244 // -------------------------------------------------------------------------
245 // Payment processing
246 // -------------------------------------------------------------------------
247
248 public static function process_payment( $result, OsOrderIntentModel $order_intent ) {
249 if ( ! OsPaymentsHelper::should_processor_handle_payment_for_order_intent( self::$processor_code, $order_intent ) ) {
250 return $result;
251 }
252 switch ( $order_intent->get_payment_data_value( 'method' ) ) {
253 case 'razorpay_checkout':
254 if ( $order_intent->get_payment_data_value( 'token' ) ) {
255 $payment_data = self::retrieve_payment( $order_intent->get_payment_data_value( 'token' ) );
256 // Require a *settled* payment: accept only 'captured', not 'authorized'. An authorization is just a hold,
257 // and we never capture it server-side, so an authorized-but-uncaptured payment would be auto-voided by
258 // Razorpay while the booking stays fulfilled — paid-looking but never actually settled.
259 if ( ! empty( $payment_data['data'] ) && $payment_data['data']['status'] === 'captured' ) {
260 // Verify amount, currency, that the payment's notes reference this intent, and — authoritatively —
261 // that the payment is bound to the Razorpay order we created for this intent. Not just status.
262 // Fail closed on any mismatch (blocks underpayment and cross-order reuse/replay).
263 $expected_order_id = $order_intent->get_other_data_value( 'razorpay_order_id' );
264 if ( self::validate_payment_amount( $payment_data['data'], $order_intent->charge_amount )
265 && self::validate_payment_currency( $payment_data['data'] )
266 && self::validate_payment_intent_notes( $payment_data['data'], 'order_intent_key', $order_intent->intent_key )
267 && self::validate_payment_order_binding( $payment_data['data'], $expected_order_id ) ) {
268 $result['status'] = LATEPOINT_STATUS_SUCCESS;
269 $result['processor'] = self::$processor_code;
270 $result['charge_id'] = $payment_data['data']['id'];
271 $result['amount'] = $payment_data['data']['amount'];
272 $result['kind'] = LATEPOINT_TRANSACTION_KIND_CAPTURE;
273 } else {
274 $result['status'] = LATEPOINT_STATUS_ERROR;
275 $result['message'] = __( 'Payment verification failed', 'latepoint' );
276 OsDebugHelper::log( 'Razorpay payment verification failed (amount/currency/notes/order binding) for order intent ' . $order_intent->id, 'razorpay_connect_error' );
277 $order_intent->add_error( 'payment_error', $result['message'] );
278 $order_intent->add_error( 'send_to_step', $result['message'], 'payment' );
279 }
280 } else {
281 $result['status'] = LATEPOINT_STATUS_ERROR;
282 $result['message'] = __( 'Payment Error', 'latepoint' );
283 $order_intent->add_error( 'payment_error', $result['message'] );
284 $order_intent->add_error( 'send_to_step', $result['message'], 'payment' );
285 }
286 } else {
287 $result['status'] = LATEPOINT_STATUS_ERROR;
288 $result['message'] = __( 'Payment Error — token missing', 'latepoint' );
289 $order_intent->add_error( 'payment_error', $result['message'] );
290 }
291 break;
292 }
293 return $result;
294 }
295
296 public static function process_payment_for_transaction_intent( $result, OsTransactionIntentModel $transaction_intent ) {
297 if ( ! OsPaymentsHelper::should_processor_handle_payment_for_transaction_intent( self::$processor_code, $transaction_intent ) ) {
298 return $result;
299 }
300 switch ( $transaction_intent->get_payment_data_value( 'method' ) ) {
301 case 'razorpay_checkout':
302 if ( $transaction_intent->get_payment_data_value( 'token' ) ) {
303 $payment_data = self::retrieve_payment( $transaction_intent->get_payment_data_value( 'token' ) );
304 // Require a *settled* payment: accept only 'captured', not 'authorized'. An authorization is just a hold,
305 // and we never capture it server-side, so an authorized-but-uncaptured payment would be auto-voided by
306 // Razorpay while the invoice stays fulfilled — paid-looking but never actually settled.
307 if ( ! empty( $payment_data['data'] ) && $payment_data['data']['status'] === 'captured' ) {
308 // Verify amount, currency, that the payment's notes reference this intent, and — authoritatively —
309 // that the payment is bound to the Razorpay order we created for this intent. Not just status.
310 // Fail closed on any mismatch (blocks underpayment and cross-order reuse/replay).
311 $expected_order_id = $transaction_intent->get_payment_data_value( 'razorpay_order_id' );
312 if ( self::validate_payment_amount( $payment_data['data'], $transaction_intent->charge_amount )
313 && self::validate_payment_currency( $payment_data['data'] )
314 && self::validate_payment_intent_notes( $payment_data['data'], 'transaction_intent_key', $transaction_intent->intent_key )
315 && self::validate_payment_order_binding( $payment_data['data'], $expected_order_id ) ) {
316 $result['status'] = LATEPOINT_STATUS_SUCCESS;
317 $result['processor'] = self::$processor_code;
318 $result['charge_id'] = $payment_data['data']['id'];
319 $result['amount'] = $payment_data['data']['amount'];
320 $result['kind'] = LATEPOINT_TRANSACTION_KIND_CAPTURE;
321 } else {
322 $result['status'] = LATEPOINT_STATUS_ERROR;
323 $result['message'] = __( 'Payment verification failed', 'latepoint' );
324 OsDebugHelper::log( 'Razorpay payment verification failed (amount/currency/notes/order binding) for transaction intent ' . $transaction_intent->id, 'razorpay_connect_error' );
325 $transaction_intent->add_error( 'send_to_step', $result['message'], 'payment' );
326 }
327 } else {
328 $result['status'] = LATEPOINT_STATUS_ERROR;
329 $result['message'] = __( 'Payment Error', 'latepoint' );
330 $transaction_intent->add_error( 'send_to_step', $result['message'], 'payment' );
331 }
332 } else {
333 $result['status'] = LATEPOINT_STATUS_ERROR;
334 $result['message'] = __( 'Payment Error — token missing', 'latepoint' );
335 $transaction_intent->add_error( 'payment_error', $result['message'] );
336 }
337 break;
338 }
339 return $result;
340 }
341
342 public static function transaction_is_refund_available( $result, OsTransactionModel $transaction_model ): bool {
343 if ( OsPaymentsHelper::is_payment_processor_enabled( self::$processor_code ) && $transaction_model->processor === self::$processor_code ) {
344 $result = true;
345 }
346 return $result;
347 }
348
349 public static function process_refund( $transaction_refund, OsTransactionModel $transaction, $custom_amount = null ) {
350 if ( $transaction->processor !== self::$processor_code ) {
351 return $transaction_refund;
352 }
353 if ( ! $transaction->can_refund() ) {
354 throw new Exception( __( 'Invalid Transaction', 'latepoint' ) );
355 }
356
357 $refund_data = [
358 'payment_id' => $transaction->token,
359 ];
360 if ( $custom_amount ) {
361 $refund_data['custom_amount'] = self::convert_amount_to_specs( $custom_amount );
362 }
363
364 $response = self::do_account_request( 'refunds', OsSettingsHelper::get_payments_environment(), '', 'POST', $refund_data );
365
366 if ( empty( $response['data'] ) ) {
367 throw new Exception( __( 'Error Refunding', 'latepoint' ) );
368 }
369
370 $transaction_refund = new OsTransactionRefundModel();
371 $transaction_refund->transaction_id = $transaction->id;
372 $transaction_refund->amount = self::convert_amount_back_from_specs_to_db_format( $response['data']['amount'] );
373 $transaction_refund->token = $response['data']['id'];
374 if ( $transaction_refund->save() ) {
375 do_action( 'latepoint_transaction_refund_created', $transaction_refund );
376 return $transaction_refund;
377 } else {
378 throw new Exception( implode( ', ', $transaction_refund->get_error_messages() ) );
379 }
380 }
381
382 // -------------------------------------------------------------------------
383 // Middleware requests
384 // -------------------------------------------------------------------------
385
386 public static function generate_razorpay_order_for_order_intent( OsOrderIntentModel $order_intent ): array {
387 $options = [
388 'amount' => $order_intent->specs_charge_amount,
389 'currency' => strtoupper( self::get_currency_iso_code() ),
390 'notes' => [
391 'order_intent_key' => $order_intent->intent_key,
392 ],
393 ];
394 $result = self::do_account_request(
395 'orders',
396 OsSettingsHelper::get_payments_environment(),
397 '',
398 'POST',
399 [ 'order_options' => $options ]
400 );
401 if ( empty( $result['data'] ) ) {
402 $error_message = ! empty( $result['error'] ) ? sprintf( __( 'Payment Error: %s', 'latepoint' ), esc_html( $result['error'] ) ) : __( 'Error generating Razorpay order', 'latepoint' );
403 OsDebugHelper::log( $error_message );
404 throw new Exception( $error_message );
405 }
406 // Persist the server-created order id (in the intent's other_data) so payment verification can bind the
407 // returned payment to the exact order we created for this intent.
408 if ( ! empty( $result['data']['order_id'] ) ) {
409 $order_intent->set_other_data_value( 'razorpay_order_id', $result['data']['order_id'] );
410 }
411 return $result['data'];
412 }
413
414 public static function generate_razorpay_order_for_transaction_intent( OsTransactionIntentModel $transaction_intent ): array {
415 $options = [
416 'amount' => $transaction_intent->specs_charge_amount,
417 'currency' => strtoupper( self::get_currency_iso_code() ),
418 'notes' => [
419 'transaction_intent_key' => $transaction_intent->intent_key,
420 ],
421 ];
422 $result = self::do_account_request(
423 'orders',
424 OsSettingsHelper::get_payments_environment(),
425 '',
426 'POST',
427 [ 'order_options' => $options ]
428 );
429 if ( empty( $result['data'] ) ) {
430 $error_message = ! empty( $result['error'] ) ? sprintf( __( 'Payment Error: %s', 'latepoint' ), esc_html( $result['error'] ) ) : __( 'Error generating Razorpay order for transaction', 'latepoint' );
431 OsDebugHelper::log( $error_message );
432 throw new Exception( $error_message );
433 }
434 // Persist the server-created order id (in the intent's payment_data) so payment verification can bind the
435 // returned payment to the exact order we created for this intent.
436 if ( ! empty( $result['data']['order_id'] ) ) {
437 $transaction_intent->set_payment_data_value( 'razorpay_order_id', $result['data']['order_id'] );
438 }
439 return $result['data'];
440 }
441
442 public static function retrieve_payment( string $payment_id ): array {
443 $env = OsSettingsHelper::get_payments_environment();
444 return self::do_account_request( 'payments/' . $payment_id, $env );
445 }
446
447 public static function do_account_request( string $path, string $env = '', string $connection_data = '', string $method = 'GET', array $vars = [], array $headers = [] ) {
448 if ( empty( $env ) ) {
449 $env = OsSettingsHelper::get_payments_environment();
450 }
451 $path = self::get_connect_account_id( $env ) . '/' . $path;
452 try {
453 return self::do_request( $path, $connection_data, $method, $vars, $headers, $env );
454 } catch ( Exception $e ) {
455 OsDebugHelper::log( 'Error processing request to Razorpay middleware: ' . $e->getMessage(), 'razorpay_connect_error' );
456 return [];
457 }
458 }
459
460 public static function do_request( string $path, string $connection_data = '', string $method = 'GET', array $vars = [], array $headers = [], string $force_env = '' ) {
461 $default_headers = [
462 'latepoint-version' => LATEPOINT_VERSION,
463 'latepoint-domain' => OsUtilHelper::get_site_url(),
464 'latepoint-license-key' => OsLicenseHelper::get_license_key(),
465 ];
466
467 if ( ! empty( $connection_data ) ) {
468 $default_headers['connection-data'] = $connection_data;
469 }
470
471 $args = array(
472 'timeout' => 15,
473 'headers' => array_merge( $default_headers, $headers ),
474 'body' => $vars,
475 'sslverify' => false,
476 'method' => $method,
477 );
478
479 if ( ! empty( $force_env ) && in_array( $force_env, [ LATEPOINT_PAYMENTS_ENV_DEV, LATEPOINT_PAYMENTS_ENV_LIVE ] ) ) {
480 $env = ( $force_env === LATEPOINT_PAYMENTS_ENV_DEV ) ? 'test' : 'live';
481 } else {
482 $env = OsSettingsHelper::is_env_payments_dev() ? 'test' : 'live';
483 }
484
485 $url = LATEPOINT_RAZORPAY_CONNECT_URL . "/api/wp/v1/razorpay-connect/{$env}/{$path}";
486 $response = wp_remote_request( $url, $args );
487
488 if ( ! is_wp_error( $response ) ) {
489 $data = json_decode( wp_remote_retrieve_body( $response ), true );
490 $data['status'] = $response['response'];
491 return $data;
492 } else {
493 $error_message = $response->get_error_message();
494 throw new Exception( $error_message );
495 }
496 }
497
498 // -------------------------------------------------------------------------
499 // Connection management
500 // -------------------------------------------------------------------------
501
502 public static function get_server_token( string $force_env = '' ): string {
503 $key = OsSettingsHelper::append_payment_env_key( 'server_token_for_razorpay_connect', $force_env );
504 $server_token = OsSettingsHelper::get_settings_value( $key, '' );
505 if ( empty( $server_token ) ) {
506 $server_token = OsUtilHelper::generate_uuid();
507 OsSettingsHelper::save_setting_by_name( $key, $server_token );
508 }
509 return $server_token;
510 }
511
512 public static function reset_server_token( string $force_env = '' ): string {
513 $key = OsSettingsHelper::append_payment_env_key( 'server_token_for_razorpay_connect', $force_env );
514 $new_server_token = OsUtilHelper::generate_uuid();
515 OsSettingsHelper::save_setting_by_name( $key, $new_server_token );
516 return $new_server_token;
517 }
518
519 public static function get_connect_url( string $env = '' ): string {
520 $url = LATEPOINT_RAZORPAY_CONNECT_URL . '/wp/razorpay-connection/' . $env . '/start/';
521 $url .= self::get_server_token( $env ) . '/' . base64_encode( implode( '|||', [ get_bloginfo( 'name' ), get_site_icon_url(), OsUtilHelper::get_site_url() ] ) );
522 return $url;
523 }
524
525 public static function get_connect_account_id( string $env = '' ): string {
526 if ( empty( $env ) ) {
527 $env = OsSettingsHelper::get_payments_environment();
528 }
529 return OsSettingsHelper::get_settings_value( OsSettingsHelper::append_payment_env_key( 'razorpay_connect_account_id', $env ), '' );
530 }
531
532 public static function get_connect_public_token( string $env = '' ): string {
533 if ( empty( $env ) ) {
534 $env = OsSettingsHelper::get_payments_environment();
535 }
536 return OsSettingsHelper::get_settings_value( OsSettingsHelper::append_payment_env_key( 'razorpay_connect_public_token', $env ), '' );
537 }
538
539 public static function get_connection_buttons_and_status( string $env = '' ): string {
540 $razorpay_connect_account_id = OsSettingsHelper::get_settings_value( OsSettingsHelper::append_payment_env_key( 'razorpay_connect_account_id', $env ), false );
541 $html = '';
542 $duplicate_token_activations = OsSettingsHelper::get_settings_value( OsSettingsHelper::append_payment_env_key( 'razorpay_connect_duplicate_token_activations', $env ), '' );
543
544 if ( ! empty( $duplicate_token_activations ) ) {
545 $html .= '<div class="latepoint-fix-records-warning"><i class="latepoint-icon latepoint-icon-info"></i><div>';
546 $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>';
547 $html .= '<div class="latepoint-fix-records-values">' . esc_html( $duplicate_token_activations ) . '</div>';
548 $html .= '</div></div>';
549 }
550
551 $html .= '<div class="payment-processor-connect-status-inner">';
552 if ( $razorpay_connect_account_id ) {
553 $charges_enabled = OsSettingsHelper::is_on( OsSettingsHelper::append_payment_env_key( 'razorpay_connect_charges_enabled', $env ) );
554 $disconnect_link = '<a class="payment-processor-disconnect-link" href="#"
555 data-os-pass-response="yes"
556 data-os-pass-this="yes"
557 data-os-before-after="none"
558 data-os-after-call="latepointRazorpayConnectAdmin.reload_connect_status_wrapper"
559 data-os-params="' . esc_attr( OsUtilHelper::build_os_params( [ 'env' => $env ], 'razorpay_disconnect_account' ) ) . '"
560 data-os-action="' . OsRouterHelper::build_route_name( 'razorpay_connect', 'disconnect_connect_account' ) . '"
561 ><i class="latepoint-icon latepoint-icon-x"></i><span>' . __( 'disconnect', 'latepoint' ) . '</span></a>';
562 if ( $charges_enabled ) {
563 $html .= '<div class="payment-processor-status-connected"><i class="latepoint-icon latepoint-icon-check"></i><span>' . __( 'Connected', 'latepoint' ) . '</span></div>';
564 $html .= $disconnect_link;
565 $html .= '<div class="razorpay-connect-account-info">' . __( 'Account: ', 'latepoint' ) . esc_html( $razorpay_connect_account_id ) . '</div>';
566 if ( $env === LATEPOINT_PAYMENTS_ENV_LIVE && ! empty( OsSettingsHelper::get_settings_value( 'razorpay_connect_transaction_fee_info', '' ) ) ) {
567 $html .= '<div class="fee-disclosure-wrapper">
568 <div class="fee-disclosure">' . esc_html( OsSettingsHelper::get_settings_value( 'razorpay_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>
569 </div>';
570 }
571 } else {
572 $html .= '<div class="payment-processor-status-charges-disabled"><i class="latepoint-icon latepoint-icon-clock"></i><span>' . __( 'Pending Action', 'latepoint' ) . '</span></div>';
573 $html .= '<a data-env="' . esc_attr( $env ) . '" data-route-name="' . esc_attr( OsRouterHelper::build_route_name( 'razorpay_connect', 'start_connect_process' ) ) . '" href="#" class="payment-start-connecting"><span>' . __( 'Continue Setup', 'latepoint' ) . '</span><i class="latepoint-icon latepoint-icon-arrow-right"></i></a>';
574 $html .= '<div class="razorpay-connect-account-info">';
575 $html .= '<div>' . esc_html( $razorpay_connect_account_id ) . '</div>';
576 $html .= $disconnect_link;
577 $html .= '</div>';
578 }
579 } else {
580 $html .= '<a data-env="' . esc_attr( $env ) . '" data-route-name="' . esc_attr( OsRouterHelper::build_route_name( 'razorpay_connect', 'start_connect_process' ) ) . '" href="#" class="payment-start-connecting"><span>' . __( 'Start Connecting', 'latepoint' ) . '</span><i class="latepoint-icon latepoint-icon-arrow-right"></i></a>';
581 }
582 $html .= '</div>';
583 return $html;
584 }
585
586 // -------------------------------------------------------------------------
587 // Settings helpers
588 // -------------------------------------------------------------------------
589
590 public static function get_currency_iso_code(): string {
591 return OsSettingsHelper::get_settings_value( 'razorpay_connect_currency_iso_code', self::$default_currency_iso_code );
592 }
593
594 public static function load_countries_list(): array {
595 return [ 'IN' => 'India' ];
596 }
597
598 public static function load_currencies_list(): array {
599 return [
600 'AED' => 'United Arab Emirates Dirham',
601 'ALL' => 'Albanian Lek',
602 'AMD' => 'Armenian Dram',
603 'ARS' => 'Argentine Peso',
604 'AUD' => 'Australian Dollar',
605 'AWG' => 'Aruban Florin',
606 'BBD' => 'Barbadian Dollar',
607 'BDT' => 'Bangladeshi Taka',
608 'BMD' => 'Bermudian Dollar',
609 'BND' => 'Brunei Dollar',
610 'BOB' => 'Bolivian Boliviano',
611 'BSD' => 'Bahamian Dollar',
612 'BWP' => 'Botswana Pula',
613 'BZD' => 'Belize Dollar',
614 'CAD' => 'Canadian Dollar',
615 'CHF' => 'Swiss Franc',
616 'CNY' => 'Chinese Yuan',
617 'COP' => 'Colombian Peso',
618 'CRC' => 'Costa Rican Colón',
619 'CUP' => 'Cuban Peso',
620 'CZK' => 'Czech Koruna',
621 'DKK' => 'Danish Krone',
622 'DOP' => 'Dominican Peso',
623 'DZD' => 'Algerian Dinar',
624 'EGP' => 'Egyptian Pound',
625 'ETB' => 'Ethiopian Birr',
626 'EUR' => 'Euro',
627 'FJD' => 'Fijian Dollar',
628 'GBP' => 'Pound Sterling',
629 'GHS' => 'Ghanaian Cedi',
630 'GIP' => 'Gibraltar Pound',
631 'GMD' => 'Gambian Dalasi',
632 'GTQ' => 'Guatemalan Quetzal',
633 'GYD' => 'Guyanese Dollar',
634 'HKD' => 'Hong Kong Dollar',
635 'HNL' => 'Honduran Lempira',
636 'HRK' => 'Croatian Kuna',
637 'HTG' => 'Haitian Gourde',
638 'HUF' => 'Hungarian Forint',
639 'IDR' => 'Indonesian Rupiah',
640 'ILS' => 'Israeli New Shekel',
641 'INR' => 'Indian Rupee',
642 'JMD' => 'Jamaican Dollar',
643 'KES' => 'Kenyan Shilling',
644 'KGS' => 'Kyrgyzstani Som',
645 'KHR' => 'Cambodian Riel',
646 'KYD' => 'Cayman Islands Dollar',
647 'KZT' => 'Kazakhstani Tenge',
648 'LAK' => 'Lao Kip',
649 'LBP' => 'Lebanese Pound',
650 'LKR' => 'Sri Lankan Rupee',
651 'LRD' => 'Liberian Dollar',
652 'LSL' => 'Lesotho Loti',
653 'MAD' => 'Moroccan Dirham',
654 'MDL' => 'Moldovan Leu',
655 'MKD' => 'Macedonian Denar',
656 'MMK' => 'Myanmar Kyat',
657 'MNT' => 'Mongolian Tögrög',
658 'MOP' => 'Macanese Pataca',
659 'MUR' => 'Mauritian Rupee',
660 'MVR' => 'Maldivian Rufiyaa',
661 'MWK' => 'Malawian Kwacha',
662 'MXN' => 'Mexican Peso',
663 'MYR' => 'Malaysian Ringgit',
664 'NAD' => 'Namibian Dollar',
665 'NGN' => 'Nigerian Naira',
666 'NIO' => 'Nicaraguan Córdoba',
667 'NOK' => 'Norwegian Krone',
668 'NPR' => 'Nepalese Rupee',
669 'NZD' => 'New Zealand Dollar',
670 'PEN' => 'Peruvian Sol',
671 'PGK' => 'Papua New Guinean Kina',
672 'PHP' => 'Philippine Peso',
673 'PKR' => 'Pakistani Rupee',
674 'QAR' => 'Qatari Riyal',
675 'RUB' => 'Russian Ruble',
676 'SAR' => 'Saudi Riyal',
677 'SCR' => 'Seychellois Rupee',
678 'SEK' => 'Swedish Krona',
679 'SGD' => 'Singapore Dollar',
680 'SLL' => 'Sierra Leonean Leone',
681 'SOS' => 'Somali Shilling',
682 'SSP' => 'South Sudanese Pound',
683 'SVC' => 'Salvadoran Colón',
684 'SZL' => 'Swazi Lilangeni',
685 'THB' => 'Thai Baht',
686 'TTD' => 'Trinidad and Tobago Dollar',
687 'TZS' => 'Tanzanian Shilling',
688 'USD' => 'United States Dollar',
689 'UYU' => 'Uruguayan Peso',
690 'UZS' => 'Uzbekistani Som',
691 'YER' => 'Yemeni Rial',
692 'ZAR' => 'South African Rand',
693 ];
694 }
695 }
696