PluginProbe ʕ •ᴥ•ʔ
Appointment Booking Plugin – LatePoint | Calendar & Scheduling for WordPress / 5.6.11
Appointment Booking Plugin – LatePoint | Calendar & Scheduling for WordPress v5.6.11
5.6.11 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 / models / order_intent_model.php
latepoint / lib / models Last commit date
activity_model.php 5 months ago agent_meta_model.php 5 months ago agent_model.php 5 months ago booking_meta_model.php 5 months ago booking_model.php 1 day ago bundle_meta_model.php 5 months ago bundle_model.php 2 months ago cart_item_model.php 5 months ago cart_meta_model.php 5 months ago cart_model.php 1 month ago connector_model.php 5 months ago customer_meta_model.php 5 months ago customer_model.php 3 months ago invoice_model.php 1 month ago join_bundles_services_model.php 5 months ago location_category_model.php 5 months ago location_model.php 5 months ago meta_model.php 5 months ago model.php 2 months ago off_period_model.php 5 months ago order_intent_meta_model.php 5 months ago order_intent_model.php 1 day ago order_item_model.php 1 month ago order_meta_model.php 5 months ago order_model.php 3 months ago otp_model.php 5 months ago payment_request_model.php 5 months ago process_job_model.php 5 months ago process_model.php 3 months ago recurrence_model.php 5 months ago service_category_model.php 5 months ago service_meta_model.php 5 months ago service_model.php 5 months ago session_model.php 5 months ago settings_model.php 5 months ago step_settings_model.php 5 months ago transaction_intent_model.php 5 months ago transaction_model.php 5 months ago transaction_refund_model.php 5 months ago work_period_model.php 5 months ago
order_intent_model.php
625 lines
1 <?php
2 /*
3 * Copyright (c) 2024 LatePoint LLC. All rights reserved.
4 */
5
6 class OsOrderIntentModel extends OsModel {
7 var $id,
8 $intent_key,
9 $customer_id,
10 $booking_form_page_url,
11 $cart_items_data,
12 $restrictions_data,
13 $presets_data,
14 $payment_data = '',
15 $payment_data_arr,
16 $other_data,
17 $charge_amount,
18 $specs_charge_amount,
19 $coupon_code,
20 $coupon_discount,
21 $total,
22 $subtotal,
23 $price_breakdown,
24 $order_id,
25 $tax_total,
26 $status,
27 $updated_at,
28 $created_at;
29
30 function __construct( $id = false ) {
31 parent::__construct();
32 $this->table_name = LATEPOINT_TABLE_ORDER_INTENTS;
33
34 if ( $id ) {
35 $this->load_by_id( $id );
36 }
37 }
38
39
40 protected function params_to_sanitize() {
41 return [
42 'charge_amount' => 'money',
43 'total' => 'money',
44 'subtotal' => 'money',
45 'tax_total' => 'money',
46 ];
47 }
48
49
50 public function delete_meta_by_key( $meta_key ) {
51 if ( $this->is_new_record() ) {
52 return false;
53 }
54
55 $meta = new OsOrderIntentMetaModel();
56
57 return $meta->delete_by_key( $meta_key, $this->id );
58 }
59
60 public function get_meta_by_key( $meta_key, $default = false ) {
61 if ( $this->is_new_record() ) {
62 return $default;
63 }
64
65 $meta = new OsOrderIntentMetaModel();
66
67 return $meta->get_by_key( $meta_key, $this->id, $default );
68 }
69
70 public function save_meta_by_key( $meta_key, $meta_value ) {
71 if ( $this->is_new_record() ) {
72 return false;
73 }
74
75 $meta = new OsOrderIntentMetaModel();
76
77 return $meta->save_by_key( $meta_key, $meta_value, $this->id );
78 }
79
80 public function get_customer() {
81 if ( $this->customer_id ) {
82 if ( ! isset( $this->customer ) || ( isset( $this->customer ) && ( $this->customer->id != $this->customer_id ) ) ) {
83 $this->customer = new OsCustomerModel( $this->customer_id );
84 }
85 } else {
86 $this->customer = new OsCustomerModel();
87 }
88
89 return $this->customer;
90 }
91
92 public function build_cart_object(): OsCartModel {
93 $cart = new OsCartModel();
94 $cart->total = $this->total;
95 $cart->subtotal = $this->subtotal;
96 $cart->coupon_code = $this->coupon_code;
97 $cart->coupon_discount = $this->coupon_discount;
98 $cart->tax_total = $this->tax_total;
99
100 // add items from intent
101 $intent_cart_items = json_decode( $this->cart_items_data, true );
102 foreach ( $intent_cart_items as $cart_item_data ) {
103 $cart->add_item( OsCartsHelper::create_cart_item_from_item_data( $cart_item_data ), false );
104 }
105
106 // restore payment info
107 $payment_data = json_decode( $this->payment_data, true );
108 $cart->payment_method = $payment_data['method'];
109 $cart->payment_time = $payment_data['time'];
110 $cart->payment_portion = $payment_data['portion'];
111 $cart->payment_token = $payment_data['token'];
112 $cart->payment_processor = $payment_data['processor'];
113
114 return $cart;
115 }
116
117 public function get_payment_data_value( string $key ): string {
118 if ( ! isset( $this->payment_data_arr ) ) {
119 $this->payment_data_arr = json_decode( $this->payment_data, true );
120 }
121
122 return $this->payment_data_arr[ $key ] ?? '';
123 }
124
125 public function set_payment_data_value( string $key, string $value, bool $save = true ) {
126 $this->payment_data_arr = json_decode( $this->payment_data, true );
127 $this->payment_data_arr[ $key ] = $value;
128 $this->payment_data = wp_json_encode( $this->payment_data_arr );
129 if ( $save ) {
130 $this->update_attributes( [ 'payment_data' => $this->payment_data ] );
131 }
132 }
133
134 public function get_other_data_value( string $key ): string {
135 if ( ! isset( $this->other_data_arr ) ) {
136 $this->other_data_arr = json_decode( $this->other_data, true ) ?: [];
137 }
138
139 return $this->other_data_arr[ $key ] ?? '';
140 }
141
142 public function set_other_data_value( string $key, string $value, bool $save = true ) {
143 $this->other_data_arr = json_decode( $this->other_data, true ) ?: [];
144 $this->other_data_arr[ $key ] = $value;
145 $this->other_data = wp_json_encode( $this->other_data_arr );
146 if ( $save ) {
147 $this->update_attributes( [ 'other_data' => $this->other_data ] );
148 }
149 }
150
151 public function is_bookable( array $settings = [] ): bool {
152 $cart = $this->build_cart_object();
153 // loop items and check if bookings are still available
154 foreach ( $cart->get_items() as $cart_item ) {
155 switch ( $cart_item->variant ) {
156 case LATEPOINT_ITEM_VARIANT_BOOKING:
157 $booking = $cart_item->build_original_object_from_item_data();
158 if ( ! $booking->is_bookable( $settings ) ) {
159 $redirect_step = $booking->get_error_data( 'send_to_step' ) ?: 'booking__datepicker';
160 $this->add_error( 'send_to_step', $booking->get_error_messages(), $redirect_step );
161
162 return false;
163 }
164 break;
165 case LATEPOINT_ITEM_VARIANT_BUNDLE:
166 break;
167 }
168 }
169
170 return true;
171 }
172
173 public function is_processing(): bool {
174 return $this->status == LATEPOINT_ORDER_INTENT_STATUS_PROCESSING;
175 }
176
177 public function is_failed(): bool {
178 return $this->status == LATEPOINT_ORDER_INTENT_STATUS_FAILED;
179 }
180
181
182 public function mark_as_failed() {
183 $this->update_attributes( [ 'status' => LATEPOINT_ORDER_INTENT_STATUS_FAILED ] );
184 /**
185 * Order intent is marked as failed
186 *
187 * @param {OsOrderIntentModel} $order_intent Instance of order intent model that has failed
188 *
189 * @since 5.2.0
190 * @hook latepoint_order_intent_failed
191 *
192 */
193 do_action( 'latepoint_order_intent_failed', $this );
194 }
195
196 public function wait_for_transaction_completion(): OsOrderIntentModel {
197 $attempts = 0;
198 $max_attempts = 6;
199 $delay_seconds = 2;
200
201 while ( $attempts < $max_attempts ) {
202 if ( ! $this->is_processing() ) {
203 return $this;
204 }
205 sleep( $delay_seconds );
206 $attempts++;
207 $this->load_by_id( $this->id );
208 }
209 if ( $this->is_processing() ) {
210 // if it's still processing after waiting - mark as failed
211 $this->mark_as_failed();
212 }
213 return $this;
214 }
215
216 public function convert_to_order() {
217 if ( $this->is_converted() ) {
218 return $this->order_id;
219 }
220
221 if ( $this->is_processing() ) {
222
223 $this->wait_for_transaction_completion();
224 if ( $this->is_failed() ) {
225 $this->add_error( 'transaction_intent_error', __( 'Can not convert to transaction, because transaction intent conversion is being processed', 'latepoint' ) );
226 return false;
227 } elseif ( $this->is_converted() ) {
228 return $this->order_id;
229 }
230 }
231
232 $this->mark_as_processing();
233
234 try {
235 remove_filter( 'latepoint_get_booked_periods', [ 'OsBookingHelper', 'inject_active_hold_booked_periods' ], 10 );
236
237 // process is cart -> order intent -> order
238 if ( ! $this->is_bookable() ) {
239 $this->mark_as_new();
240 return false;
241 }
242
243 // process payment if there is amount due
244 $transaction = OsPaymentsHelper::process_payment_for_order_intent( $this );
245
246 // payment processing can take a while, make sure to check if the intent wasn't converted already in the meantime
247 $converted_order_id = OsOrderIntentHelper::is_converted( $this->id );
248 if ( $converted_order_id ) {
249 $order = new OsOrderModel( $converted_order_id );
250 $this->mark_as_converted( $order );
251 return $converted_order_id;
252 }
253
254 if ( $this->get_error() ) {
255 OsDebugHelper::log( 'Error converting intent to an order', 'order_error', $this->get_error_messages() );
256
257 $this->mark_as_new();
258 return false;
259 }
260
261
262 $cart_from_intent = $this->build_cart_object();
263
264 $order = new OsOrderModel();
265 $order->customer_id = $this->customer_id;
266 $order->total = $this->total;
267 $order->subtotal = $this->subtotal;
268 $order->coupon_code = $this->coupon_code;
269 $order->coupon_discount = $this->coupon_discount;
270 $order->tax_total = $this->tax_total;
271 $order->source_url = $this->booking_form_page_url;
272 $order->customer_comment = $this->customer->notes;
273 $order_initial_payment_data_arr = json_decode( $this->payment_data, true );
274 $order_initial_payment_data_arr['charge_amount'] = $this->charge_amount;
275 $order->initial_payment_data = wp_json_encode( $order_initial_payment_data_arr );
276 // order's price breakdown should only hold cart items, and never holds total, subtotal, balance variables because those are stored on order model itself and/or generated on the fly
277 $order->price_breakdown = wp_json_encode( $cart_from_intent->generate_price_breakdown_rows( [ 'balance', 'total', 'subtotal' ] ) );
278
279 /**
280 * Filters order right before it's about to be saved when converting from an order intent
281 *
282 * @param {OsOrderModel} $order Order to be filtered
283 * @returns {OsOrderModel} The filtered order
284 *
285 * @since 5.0.0
286 * @hook latepoint_before_order_save_from_order_intent
287 *
288 */
289 $order = apply_filters( 'latepoint_before_order_save_from_order_intent', $order );
290
291
292 if ( $order->save() ) {
293 $this->mark_as_converted( $order );
294 OsInvoicesHelper::create_invoices_for_new_order( $order );
295
296
297 foreach ( $cart_from_intent->get_items() as $cart_item ) {
298 $order_item = OsOrdersHelper::create_order_item_from_cart_item( $cart_item );
299 $order_item->order_id = $order->id;
300 $order_item->save();
301 }
302
303 if ( $transaction ) {
304 $transaction->order_id = $order->id;
305 $invoice = OsInvoicesHelper::get_matching_invoice_for_transaction( $transaction );
306 if ( ! $invoice->is_new_record() ) {
307 $transaction->invoice_id = $invoice->id;
308 }
309 if ( $transaction->save() ) {
310
311 /**
312 * Transaction was created
313 *
314 * @param {OsTransactionModel} $transaction instance of transaction model that was created
315 *
316 * @since 5.1.0
317 * @hook latepoint_transaction_created
318 *
319 */
320 do_action( 'latepoint_transaction_created', $transaction );
321 if ( ! $invoice->is_new_record() ) {
322 $old_invoice = clone $invoice;
323 $invoice->update_attributes( [ 'status' => LATEPOINT_INVOICE_STATUS_PAID ] );
324 /**
325 * Invoice was updated
326 *
327 * @param {OsInvoiceModel} $invoice instance of invoice model after it was updated
328 * @param {OsInvoiceModel} $old_invoice instance of invoice model before it was updated
329 *
330 * @since 5.1.0
331 * @hook latepoint_invoice_updated
332 *
333 */
334 do_action( 'latepoint_invoice_updated', $invoice, $old_invoice );
335 // update other invoices with this paid amount, for example if we charge a deposit - then this transaction should also be reflected in draft invoices for the remaining amount that were created earlier
336 $other_invoices = new OsInvoiceModel();
337 $other_invoices = $other_invoices->where(
338 [
339 'status' => LATEPOINT_INVOICE_STATUS_DRAFT,
340 'order_id' => $order->id,
341 ]
342 )->get_results_as_models();
343 if ( $other_invoices ) {
344 foreach ( $other_invoices as $invoice ) {
345 $data = json_decode( $invoice->data, true );
346 $data['totals']['payments'] = $order->get_total_amount_paid_from_transactions( true );
347 $invoice->update_attributes( [ 'data' => json_encode( $data ) ] );
348 }
349 }
350 }
351 } else {
352 OsDebugHelper::log( 'Error creating transaction', 'transaction_error', $transaction->get_error_messages() );
353 }
354 }
355 $order_bookings = $order->get_bookings_from_order_items( true );
356 if ( $order_bookings ) {
357 foreach ( $order_bookings as $order_item_id => $order_booking ) {
358 $order_booking->order_item_id = $order_item_id;
359 $order_booking->customer_id = $order->customer_id;
360 $order_booking->end_time = $order_booking->calculate_end_time();
361 $order_booking->end_date = $order_booking->calculate_end_date();
362 $order_booking->set_utc_datetimes();
363 $service = new OsServiceModel( $order_booking->service_id );
364 $order_booking->buffer_before = $service->buffer_before;
365 $order_booking->buffer_after = $service->buffer_after;
366 $order_booking->customer_comment = $order->customer->notes;
367 if ( $order_booking->save() ) {
368
369 /**
370 * Booking was created
371 *
372 * @param {OsBookingModel} $booking instance of booking model that was created
373 *
374 * @since 5.0.0
375 * @hook latepoint_booking_created
376 *
377 */
378 do_action( 'latepoint_booking_created', $order_booking );
379 // set booking id to the one that was created for item data property
380 $order_item = new OsOrderItemModel( $order_item_id );
381 $item_data = json_decode( $order_item->item_data, true );
382 $item_data['id'] = $order_booking->id;
383 $order_item->update_attributes( [ 'item_data' => wp_json_encode( $item_data ) ] );
384 } else {
385 OsDebugHelper::log( 'Unable to save booking', 'booking_save_error', $order_booking->get_error_messages() );
386 }
387 }
388 }
389 // update connected cart with created order id
390 $this->mark_cart_converted();
391 $order->determine_payment_status();
392 // update with latest info
393 $order->get_items( true );
394
395 /**
396 * Order was created
397 *
398 * @param {OsOrderModel} $order instance of order model that was created
399 *
400 * @since 5.0.0
401 * @hook latepoint_order_created
402 *
403 */
404 do_action( 'latepoint_order_created', $order );
405
406 return $order->id;
407 } else {
408 $this->add_error( 'order_error', $order->get_error_messages() );
409
410 $this->mark_as_new();
411 return false;
412 }
413 } catch ( Exception $e ) {
414 $this->mark_as_new();
415 // translators: %s is the error description
416 $this->add_error( 'order_error', sprintf( __( 'Error: %s', 'latepoint' ), $e->getMessage() ) );
417 OsDebugHelper::log( 'Error converting intent to an order', 'order_error', $e->getMessage() );
418 return false;
419 }
420 }
421
422 public function get_by_intent_key( $intent_key ) {
423 return $this->where( [ 'intent_key' => $intent_key ] )->set_limit( 1 )->get_results_as_models();
424 }
425
426 public function mark_as_converted( OsOrderModel $order ) {
427 if ( empty( $order->id ) ) {
428 return false;
429 }
430
431 $this->update_attributes(
432 [
433 'order_id' => $order->id,
434 'status' => LATEPOINT_ORDER_INTENT_STATUS_CONVERTED,
435 ]
436 );
437 /**
438 * Order intent is converted to order
439 *
440 * @param {OsOrderIntentModel} $order_intent Instance of order intent model that has been converted to order
441 * @param {OsOrderModel} $order Instance of order model that order intent was converted to
442 *
443 * @since 5.0.0
444 * @hook latepoint_order_intent_converted
445 *
446 */
447 do_action( 'latepoint_order_intent_converted', $this, $order );
448 }
449
450 public function mark_as_processing() {
451 $this->update_attributes( [ 'status' => LATEPOINT_ORDER_INTENT_STATUS_PROCESSING ] );
452 /**
453 * Order intent is marked as processing
454 *
455 * @param {OsOrderIntentModel} $order_intent Instance of order intent model that has started processing
456 *
457 * @since 5.0.0
458 * @hook latepoint_order_intent_processing
459 *
460 */
461 do_action( 'latepoint_order_intent_processing', $this );
462 }
463
464 public function mark_as_new() {
465 $this->update_attributes( [ 'status' => LATEPOINT_ORDER_INTENT_STATUS_NEW ] );
466 /**
467 * Order intent is marked as new
468 *
469 * @param {OsOrderIntentModel} $order_intent Instance of order intent model that is being marked as new
470 *
471 * @since 5.0.0
472 * @hook latepoint_order_intent_new
473 *
474 */
475 do_action( 'latepoint_order_intent_new', $this );
476 }
477
478 // Determines if order intent has been converted into a order already
479 public function is_converted(): bool {
480 if ( empty( $this->order_id ) ) {
481 return false;
482 } else {
483 return true;
484 }
485 }
486
487 public function generate_data_vars(): array {
488 $vars = [
489 'id' => $this->id,
490 'intent_key' => $this->intent_key,
491 'customer_id' => $this->customer_id,
492 'booking_form_page_url' => $this->booking_form_page_url,
493 'cart_items_data' => ! empty( $this->cart_items_data ) ? json_decode( $this->cart_items_data, true ) : [],
494 'restrictions_data' => ! empty( $this->restrictions_data ) ? json_decode( $this->restrictions_data, true ) : [],
495 'presets_data' => ! empty( $this->presets_data ) ? json_decode( $this->presets_data, true ) : [],
496 'payment_data' => ! empty( $this->payment_data ) ? json_decode( $this->payment_data, true ) : [],
497 'other_data' => ! empty( $this->other_data ) ? json_decode( $this->other_data, true ) : [],
498 'order_id' => $this->order_id,
499 'coupon_code' => $this->coupon_code,
500 'coupon_discount' => $this->coupon_discount,
501 'tax_total' => $this->tax_total,
502 'updated_at' => $this->updated_at,
503 'created_at' => $this->created_at,
504 ];
505
506 return $vars;
507 }
508
509 public function get_page_url_with_intent() {
510 $booking_page_url = $this->booking_form_page_url;
511 $existing_var_position = strpos( $booking_page_url, 'latepoint_order_intent_key=' );
512 if ( $existing_var_position === false ) {
513 // no intent variable in url
514 $question_position = strpos( $booking_page_url, '?' );
515 if ( $question_position === false ) {
516 // no ?query params
517 $hash_position = strpos( $booking_page_url, '#' );
518 if ( $hash_position === false ) {
519 // no hashtag in url
520 $booking_page_url = $booking_page_url . '?latepoint_order_intent_key=' . $this->intent_key;
521 } else {
522 // hashtag in url and no ?query, prepend the hashtag with query
523 $booking_page_url = substr_replace( $booking_page_url, '?latepoint_order_intent_key=' . $this->intent_key . '#', $hash_position, 1 );
524 }
525 } else {
526 // ?query string exists, add intent key to it
527 $booking_page_url = substr_replace( $booking_page_url, '?latepoint_order_intent_key=' . $this->intent_key . '&', $question_position, 1 );
528 }
529 } else {
530 // intent key variable exist in url
531 preg_match( '/latepoint_order_intent_key=([\d,\w]*)/', $booking_page_url, $matches );
532 if ( isset( $matches[1] ) ) {
533 $booking_page_url = str_replace( 'latepoint_order_intent_key=' . $matches[1], 'latepoint_order_intent_key=' . $this->intent_key, $booking_page_url );
534 }
535 }
536
537 return $booking_page_url;
538 }
539
540
541 protected function before_create() {
542 if ( empty( $this->intent_key ) ) {
543 $this->intent_key = bin2hex( openssl_random_pseudo_bytes( 10 ) );
544 }
545 if ( empty( $this->status ) ) {
546 $this->status = LATEPOINT_ORDER_INTENT_STATUS_NEW;
547 }
548 }
549
550 protected function allowed_params( $role = 'admin' ) {
551 $allowed_params = array(
552 'customer_id',
553 'cart_items_data',
554 'restrictions_data',
555 'presets_data',
556 'payment_data',
557 'other_data',
558 'booking_form_page_url',
559 'intent_key',
560 'order_id',
561 'coupon_code',
562 'coupon_discount',
563 'tax_total',
564 'status',
565 );
566
567 return $allowed_params;
568 }
569
570
571 protected function params_to_save( $role = 'admin' ) {
572 $params_to_save = array(
573 'customer_id',
574 'cart_items_data',
575 'restrictions_data',
576 'presets_data',
577 'payment_data',
578 'other_data',
579 'booking_form_page_url',
580 'intent_key',
581 'total',
582 'subtotal',
583 'charge_amount',
584 'specs_charge_amount',
585 'price_breakdown',
586 'order_id',
587 'coupon_code',
588 'coupon_discount',
589 'tax_total',
590 'status',
591 );
592
593 return $params_to_save;
594 }
595
596
597 protected function properties_to_validate() {
598 $validations = array(
599 'customer_id' => array( 'presence' ),
600 );
601
602 return $validations;
603 }
604
605 public function mark_cart_converted( ?OsCartModel $cart = null ): bool {
606 if ( $this->is_new_record() || empty( $this->order_id ) ) {
607 return false;
608 }
609 if ( ! empty( $cart ) ) {
610 $cart->order_id = $this->order_id;
611 $cart->save();
612 } else {
613 $carts = new OsCartModel();
614 $carts = $carts->where( [ 'order_intent_id' => $this->id ] )->get_results_as_models();
615 if ( ! empty( $carts ) ) {
616 foreach ( $carts as $cart ) {
617 $cart->order_id = $this->order_id;
618 $cart->save();
619 }
620 }
621 }
622 return true;
623 }
624 }
625