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 / 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 4 months ago booking_meta_model.php 5 months ago booking_model.php 1 month ago bundle_meta_model.php 5 months ago bundle_model.php 1 month 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 2 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 1 month ago off_period_model.php 5 months ago order_intent_meta_model.php 5 months ago order_intent_model.php 3 weeks ago order_item_model.php 3 weeks ago order_meta_model.php 5 months ago order_model.php 2 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 2 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
624 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
236 // process is cart -> order intent -> order
237 if ( ! $this->is_bookable() ) {
238 $this->mark_as_new();
239 return false;
240 }
241
242 // process payment if there is amount due
243 $transaction = OsPaymentsHelper::process_payment_for_order_intent( $this );
244
245 // payment processing can take a while, make sure to check if the intent wasn't converted already in the meantime
246 $converted_order_id = OsOrderIntentHelper::is_converted( $this->id );
247 if ( $converted_order_id ) {
248 $order = new OsOrderModel( $converted_order_id );
249 $this->mark_as_converted( $order );
250 return $converted_order_id;
251 }
252
253 if ( $this->get_error() ) {
254 OsDebugHelper::log( 'Error converting intent to an order', 'order_error', $this->get_error_messages() );
255
256 $this->mark_as_new();
257 return false;
258 }
259
260
261 $cart_from_intent = $this->build_cart_object();
262
263 $order = new OsOrderModel();
264 $order->customer_id = $this->customer_id;
265 $order->total = $this->total;
266 $order->subtotal = $this->subtotal;
267 $order->coupon_code = $this->coupon_code;
268 $order->coupon_discount = $this->coupon_discount;
269 $order->tax_total = $this->tax_total;
270 $order->source_url = $this->booking_form_page_url;
271 $order->customer_comment = $this->customer->notes;
272 $order_initial_payment_data_arr = json_decode( $this->payment_data, true );
273 $order_initial_payment_data_arr['charge_amount'] = $this->charge_amount;
274 $order->initial_payment_data = wp_json_encode( $order_initial_payment_data_arr );
275 // 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
276 $order->price_breakdown = wp_json_encode( $cart_from_intent->generate_price_breakdown_rows( [ 'balance', 'total', 'subtotal' ] ) );
277
278 /**
279 * Filters order right before it's about to be saved when converting from an order intent
280 *
281 * @param {OsOrderModel} $order Order to be filtered
282 * @returns {OsOrderModel} The filtered order
283 *
284 * @since 5.0.0
285 * @hook latepoint_before_order_save_from_order_intent
286 *
287 */
288 $order = apply_filters( 'latepoint_before_order_save_from_order_intent', $order );
289
290
291 if ( $order->save() ) {
292 $this->mark_as_converted( $order );
293 OsInvoicesHelper::create_invoices_for_new_order( $order );
294
295
296 foreach ( $cart_from_intent->get_items() as $cart_item ) {
297 $order_item = OsOrdersHelper::create_order_item_from_cart_item( $cart_item );
298 $order_item->order_id = $order->id;
299 $order_item->save();
300 }
301
302 if ( $transaction ) {
303 $transaction->order_id = $order->id;
304 $invoice = OsInvoicesHelper::get_matching_invoice_for_transaction( $transaction );
305 if ( ! $invoice->is_new_record() ) {
306 $transaction->invoice_id = $invoice->id;
307 }
308 if ( $transaction->save() ) {
309
310 /**
311 * Transaction was created
312 *
313 * @param {OsTransactionModel} $transaction instance of transaction model that was created
314 *
315 * @since 5.1.0
316 * @hook latepoint_transaction_created
317 *
318 */
319 do_action( 'latepoint_transaction_created', $transaction );
320 if ( ! $invoice->is_new_record() ) {
321 $old_invoice = clone $invoice;
322 $invoice->update_attributes( [ 'status' => LATEPOINT_INVOICE_STATUS_PAID ] );
323 /**
324 * Invoice was updated
325 *
326 * @param {OsInvoiceModel} $invoice instance of invoice model after it was updated
327 * @param {OsInvoiceModel} $old_invoice instance of invoice model before it was updated
328 *
329 * @since 5.1.0
330 * @hook latepoint_invoice_updated
331 *
332 */
333 do_action( 'latepoint_invoice_updated', $invoice, $old_invoice );
334 // 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
335 $other_invoices = new OsInvoiceModel();
336 $other_invoices = $other_invoices->where(
337 [
338 'status' => LATEPOINT_INVOICE_STATUS_DRAFT,
339 'order_id' => $order->id,
340 ]
341 )->get_results_as_models();
342 if ( $other_invoices ) {
343 foreach ( $other_invoices as $invoice ) {
344 $data = json_decode( $invoice->data, true );
345 $data['totals']['payments'] = $order->get_total_amount_paid_from_transactions( true );
346 $invoice->update_attributes( [ 'data' => json_encode( $data ) ] );
347 }
348 }
349 }
350 } else {
351 OsDebugHelper::log( 'Error creating transaction', 'transaction_error', $transaction->get_error_messages() );
352 }
353 }
354 $order_bookings = $order->get_bookings_from_order_items( true );
355 if ( $order_bookings ) {
356 foreach ( $order_bookings as $order_item_id => $order_booking ) {
357 $order_booking->order_item_id = $order_item_id;
358 $order_booking->customer_id = $order->customer_id;
359 $order_booking->end_time = $order_booking->calculate_end_time();
360 $order_booking->end_date = $order_booking->calculate_end_date();
361 $order_booking->set_utc_datetimes();
362 $service = new OsServiceModel( $order_booking->service_id );
363 $order_booking->buffer_before = $service->buffer_before;
364 $order_booking->buffer_after = $service->buffer_after;
365 $order_booking->customer_comment = $order->customer->notes;
366 if ( $order_booking->save() ) {
367
368 /**
369 * Booking was created
370 *
371 * @param {OsBookingModel} $booking instance of booking model that was created
372 *
373 * @since 5.0.0
374 * @hook latepoint_booking_created
375 *
376 */
377 do_action( 'latepoint_booking_created', $order_booking );
378 // set booking id to the one that was created for item data property
379 $order_item = new OsOrderItemModel( $order_item_id );
380 $item_data = json_decode( $order_item->item_data, true );
381 $item_data['id'] = $order_booking->id;
382 $order_item->update_attributes( [ 'item_data' => wp_json_encode( $item_data ) ] );
383 } else {
384 OsDebugHelper::log( 'Unable to save booking', 'booking_save_error', $order_booking->get_error_messages() );
385 }
386 }
387 }
388 // update connected cart with created order id
389 $this->mark_cart_converted();
390 $order->determine_payment_status();
391 // update with latest info
392 $order->get_items( true );
393
394 /**
395 * Order was created
396 *
397 * @param {OsOrderModel} $order instance of order model that was created
398 *
399 * @since 5.0.0
400 * @hook latepoint_order_created
401 *
402 */
403 do_action( 'latepoint_order_created', $order );
404
405 return $order->id;
406 } else {
407 $this->add_error( 'order_error', $order->get_error_messages() );
408
409 $this->mark_as_new();
410 return false;
411 }
412 } catch ( Exception $e ) {
413 $this->mark_as_new();
414 // translators: %s is the error description
415 $this->add_error( 'order_error', sprintf( __( 'Error: %s', 'latepoint' ), $e->getMessage() ) );
416 OsDebugHelper::log( 'Error converting intent to an order', 'order_error', $e->getMessage() );
417 return false;
418 }
419 }
420
421 public function get_by_intent_key( $intent_key ) {
422 return $this->where( [ 'intent_key' => $intent_key ] )->set_limit( 1 )->get_results_as_models();
423 }
424
425 public function mark_as_converted( OsOrderModel $order ) {
426 if ( empty( $order->id ) ) {
427 return false;
428 }
429
430 $this->update_attributes(
431 [
432 'order_id' => $order->id,
433 'status' => LATEPOINT_ORDER_INTENT_STATUS_CONVERTED,
434 ]
435 );
436 /**
437 * Order intent is converted to order
438 *
439 * @param {OsOrderIntentModel} $order_intent Instance of order intent model that has been converted to order
440 * @param {OsOrderModel} $order Instance of order model that order intent was converted to
441 *
442 * @since 5.0.0
443 * @hook latepoint_order_intent_converted
444 *
445 */
446 do_action( 'latepoint_order_intent_converted', $this, $order );
447 }
448
449 public function mark_as_processing() {
450 $this->update_attributes( [ 'status' => LATEPOINT_ORDER_INTENT_STATUS_PROCESSING ] );
451 /**
452 * Order intent is marked as processing
453 *
454 * @param {OsOrderIntentModel} $order_intent Instance of order intent model that has started processing
455 *
456 * @since 5.0.0
457 * @hook latepoint_order_intent_processing
458 *
459 */
460 do_action( 'latepoint_order_intent_processing', $this );
461 }
462
463 public function mark_as_new() {
464 $this->update_attributes( [ 'status' => LATEPOINT_ORDER_INTENT_STATUS_NEW ] );
465 /**
466 * Order intent is marked as new
467 *
468 * @param {OsOrderIntentModel} $order_intent Instance of order intent model that is being marked as new
469 *
470 * @since 5.0.0
471 * @hook latepoint_order_intent_new
472 *
473 */
474 do_action( 'latepoint_order_intent_new', $this );
475 }
476
477 // Determines if order intent has been converted into a order already
478 public function is_converted(): bool {
479 if ( empty( $this->order_id ) ) {
480 return false;
481 } else {
482 return true;
483 }
484 }
485
486 public function generate_data_vars(): array {
487 $vars = [
488 'id' => $this->id,
489 'intent_key' => $this->intent_key,
490 'customer_id' => $this->customer_id,
491 'booking_form_page_url' => $this->booking_form_page_url,
492 'cart_items_data' => ! empty( $this->cart_items_data ) ? json_decode( $this->cart_items_data, true ) : [],
493 'restrictions_data' => ! empty( $this->restrictions_data ) ? json_decode( $this->restrictions_data, true ) : [],
494 'presets_data' => ! empty( $this->presets_data ) ? json_decode( $this->presets_data, true ) : [],
495 'payment_data' => ! empty( $this->payment_data ) ? json_decode( $this->payment_data, true ) : [],
496 'other_data' => ! empty( $this->other_data ) ? json_decode( $this->other_data, true ) : [],
497 'order_id' => $this->order_id,
498 'coupon_code' => $this->coupon_code,
499 'coupon_discount' => $this->coupon_discount,
500 'tax_total' => $this->tax_total,
501 'updated_at' => $this->updated_at,
502 'created_at' => $this->created_at,
503 ];
504
505 return $vars;
506 }
507
508 public function get_page_url_with_intent() {
509 $booking_page_url = $this->booking_form_page_url;
510 $existing_var_position = strpos( $booking_page_url, 'latepoint_order_intent_key=' );
511 if ( $existing_var_position === false ) {
512 // no intent variable in url
513 $question_position = strpos( $booking_page_url, '?' );
514 if ( $question_position === false ) {
515 // no ?query params
516 $hash_position = strpos( $booking_page_url, '#' );
517 if ( $hash_position === false ) {
518 // no hashtag in url
519 $booking_page_url = $booking_page_url . '?latepoint_order_intent_key=' . $this->intent_key;
520 } else {
521 // hashtag in url and no ?query, prepend the hashtag with query
522 $booking_page_url = substr_replace( $booking_page_url, '?latepoint_order_intent_key=' . $this->intent_key . '#', $hash_position, 1 );
523 }
524 } else {
525 // ?query string exists, add intent key to it
526 $booking_page_url = substr_replace( $booking_page_url, '?latepoint_order_intent_key=' . $this->intent_key . '&', $question_position, 1 );
527 }
528 } else {
529 // intent key variable exist in url
530 preg_match( '/latepoint_order_intent_key=([\d,\w]*)/', $booking_page_url, $matches );
531 if ( isset( $matches[1] ) ) {
532 $booking_page_url = str_replace( 'latepoint_order_intent_key=' . $matches[1], 'latepoint_order_intent_key=' . $this->intent_key, $booking_page_url );
533 }
534 }
535
536 return $booking_page_url;
537 }
538
539
540 protected function before_create() {
541 if ( empty( $this->intent_key ) ) {
542 $this->intent_key = bin2hex( openssl_random_pseudo_bytes( 10 ) );
543 }
544 if ( empty( $this->status ) ) {
545 $this->status = LATEPOINT_ORDER_INTENT_STATUS_NEW;
546 }
547 }
548
549 protected function allowed_params( $role = 'admin' ) {
550 $allowed_params = array(
551 'customer_id',
552 'cart_items_data',
553 'restrictions_data',
554 'presets_data',
555 'payment_data',
556 'other_data',
557 'booking_form_page_url',
558 'intent_key',
559 'order_id',
560 'coupon_code',
561 'coupon_discount',
562 'tax_total',
563 'status',
564 );
565
566 return $allowed_params;
567 }
568
569
570 protected function params_to_save( $role = 'admin' ) {
571 $params_to_save = array(
572 'customer_id',
573 'cart_items_data',
574 'restrictions_data',
575 'presets_data',
576 'payment_data',
577 'other_data',
578 'booking_form_page_url',
579 'intent_key',
580 'total',
581 'subtotal',
582 'charge_amount',
583 'specs_charge_amount',
584 'price_breakdown',
585 'order_id',
586 'coupon_code',
587 'coupon_discount',
588 'tax_total',
589 'status',
590 );
591
592 return $params_to_save;
593 }
594
595
596 protected function properties_to_validate() {
597 $validations = array(
598 'customer_id' => array( 'presence' ),
599 );
600
601 return $validations;
602 }
603
604 public function mark_cart_converted( ?OsCartModel $cart = null ): bool {
605 if ( $this->is_new_record() || empty( $this->order_id ) ) {
606 return false;
607 }
608 if ( ! empty( $cart ) ) {
609 $cart->order_id = $this->order_id;
610 $cart->save();
611 } else {
612 $carts = new OsCartModel();
613 $carts = $carts->where( [ 'order_intent_id' => $this->id ] )->get_results_as_models();
614 if ( ! empty( $carts ) ) {
615 foreach ( $carts as $cart ) {
616 $cart->order_id = $this->order_id;
617 $cart->save();
618 }
619 }
620 }
621 return true;
622 }
623 }
624