PluginProbe ʕ •ᴥ•ʔ
LatePoint – Calendar Booking Plugin for Appointments and Events / 5.6.3
LatePoint – Calendar Booking Plugin for Appointments and Events v5.6.3
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 3 months ago agent_meta_model.php 3 months ago agent_model.php 3 months ago booking_meta_model.php 3 months ago booking_model.php 1 week ago bundle_meta_model.php 3 months ago bundle_model.php 1 week ago cart_item_model.php 3 months ago cart_meta_model.php 3 months ago cart_model.php 2 weeks ago connector_model.php 3 months ago customer_meta_model.php 3 months ago customer_model.php 1 month ago invoice_model.php 2 weeks ago join_bundles_services_model.php 3 months ago location_category_model.php 3 months ago location_model.php 3 months ago meta_model.php 3 months ago model.php 3 months ago off_period_model.php 3 months ago order_intent_meta_model.php 3 months ago order_intent_model.php 1 week ago order_item_model.php 3 months ago order_meta_model.php 3 months ago order_model.php 1 month ago otp_model.php 3 months ago payment_request_model.php 3 months ago process_job_model.php 3 months ago process_model.php 1 month ago recurrence_model.php 3 months ago service_category_model.php 3 months ago service_meta_model.php 3 months ago service_model.php 3 months ago session_model.php 3 months ago settings_model.php 3 months ago step_settings_model.php 3 months ago transaction_intent_model.php 3 months ago transaction_model.php 3 months ago transaction_refund_model.php 3 months ago work_period_model.php 3 months ago
order_intent_model.php
607 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 is_bookable( array $settings = [] ): bool {
135 $cart = $this->build_cart_object();
136 // loop items and check if bookings are still available
137 foreach ( $cart->get_items() as $cart_item ) {
138 switch ( $cart_item->variant ) {
139 case LATEPOINT_ITEM_VARIANT_BOOKING:
140 $booking = $cart_item->build_original_object_from_item_data();
141 if ( ! $booking->is_bookable( $settings ) ) {
142 $redirect_step = $booking->get_error_data( 'send_to_step' ) ?: 'booking__datepicker';
143 $this->add_error( 'send_to_step', $booking->get_error_messages(), $redirect_step );
144
145 return false;
146 }
147 break;
148 case LATEPOINT_ITEM_VARIANT_BUNDLE:
149 break;
150 }
151 }
152
153 return true;
154 }
155
156 public function is_processing(): bool {
157 return $this->status == LATEPOINT_ORDER_INTENT_STATUS_PROCESSING;
158 }
159
160 public function is_failed(): bool {
161 return $this->status == LATEPOINT_ORDER_INTENT_STATUS_FAILED;
162 }
163
164
165 public function mark_as_failed() {
166 $this->update_attributes( [ 'status' => LATEPOINT_ORDER_INTENT_STATUS_FAILED ] );
167 /**
168 * Order intent is marked as failed
169 *
170 * @param {OsOrderIntentModel} $order_intent Instance of order intent model that has failed
171 *
172 * @since 5.2.0
173 * @hook latepoint_order_intent_failed
174 *
175 */
176 do_action( 'latepoint_order_intent_failed', $this );
177 }
178
179 public function wait_for_transaction_completion(): OsOrderIntentModel {
180 $attempts = 0;
181 $max_attempts = 6;
182 $delay_seconds = 2;
183
184 while ( $attempts < $max_attempts ) {
185 if ( ! $this->is_processing() ) {
186 return $this;
187 }
188 sleep( $delay_seconds );
189 $attempts++;
190 $this->load_by_id( $this->id );
191 }
192 if ( $this->is_processing() ) {
193 // if it's still processing after waiting - mark as failed
194 $this->mark_as_failed();
195 }
196 return $this;
197 }
198
199 public function convert_to_order() {
200 if ( $this->is_converted() ) {
201 return $this->order_id;
202 }
203
204 if ( $this->is_processing() ) {
205
206 $this->wait_for_transaction_completion();
207 if ( $this->is_failed() ) {
208 $this->add_error( 'transaction_intent_error', __( 'Can not convert to transaction, because transaction intent conversion is being processed', 'latepoint' ) );
209 return false;
210 } elseif ( $this->is_converted() ) {
211 return $this->order_id;
212 }
213 }
214
215 $this->mark_as_processing();
216
217 try {
218
219 // process is cart -> order intent -> order
220 if ( ! $this->is_bookable() ) {
221 $this->mark_as_new();
222 return false;
223 }
224
225 // process payment if there is amount due
226 $transaction = OsPaymentsHelper::process_payment_for_order_intent( $this );
227
228 // payment processing can take a while, make sure to check if the intent wasn't converted already in the meantime
229 $converted_order_id = OsOrderIntentHelper::is_converted( $this->id );
230 if ( $converted_order_id ) {
231 $order = new OsOrderModel( $converted_order_id );
232 $this->mark_as_converted( $order );
233 return $converted_order_id;
234 }
235
236 if ( $this->get_error() ) {
237 OsDebugHelper::log( 'Error converting intent to an order', 'order_error', $this->get_error_messages() );
238
239 $this->mark_as_new();
240 return false;
241 }
242
243
244 $cart_from_intent = $this->build_cart_object();
245
246 $order = new OsOrderModel();
247 $order->customer_id = $this->customer_id;
248 $order->total = $this->total;
249 $order->subtotal = $this->subtotal;
250 $order->coupon_code = $this->coupon_code;
251 $order->coupon_discount = $this->coupon_discount;
252 $order->tax_total = $this->tax_total;
253 $order->source_url = $this->booking_form_page_url;
254 $order->customer_comment = $this->customer->notes;
255 $order_initial_payment_data_arr = json_decode( $this->payment_data, true );
256 $order_initial_payment_data_arr['charge_amount'] = $this->charge_amount;
257 $order->initial_payment_data = wp_json_encode( $order_initial_payment_data_arr );
258 // 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
259 $order->price_breakdown = wp_json_encode( $cart_from_intent->generate_price_breakdown_rows( [ 'balance', 'total', 'subtotal' ] ) );
260
261 /**
262 * Filters order right before it's about to be saved when converting from an order intent
263 *
264 * @param {OsOrderModel} $order Order to be filtered
265 * @returns {OsOrderModel} The filtered order
266 *
267 * @since 5.0.0
268 * @hook latepoint_before_order_save_from_order_intent
269 *
270 */
271 $order = apply_filters( 'latepoint_before_order_save_from_order_intent', $order );
272
273
274 if ( $order->save() ) {
275 $this->mark_as_converted( $order );
276 OsInvoicesHelper::create_invoices_for_new_order( $order );
277
278
279 foreach ( $cart_from_intent->get_items() as $cart_item ) {
280 $order_item = OsOrdersHelper::create_order_item_from_cart_item( $cart_item );
281 $order_item->order_id = $order->id;
282 $order_item->save();
283 }
284
285 if ( $transaction ) {
286 $transaction->order_id = $order->id;
287 $invoice = OsInvoicesHelper::get_matching_invoice_for_transaction( $transaction );
288 if ( ! $invoice->is_new_record() ) {
289 $transaction->invoice_id = $invoice->id;
290 }
291 if ( $transaction->save() ) {
292
293 /**
294 * Transaction was created
295 *
296 * @param {OsTransactionModel} $transaction instance of transaction model that was created
297 *
298 * @since 5.1.0
299 * @hook latepoint_transaction_created
300 *
301 */
302 do_action( 'latepoint_transaction_created', $transaction );
303 if ( ! $invoice->is_new_record() ) {
304 $old_invoice = clone $invoice;
305 $invoice->update_attributes( [ 'status' => LATEPOINT_INVOICE_STATUS_PAID ] );
306 /**
307 * Invoice was updated
308 *
309 * @param {OsInvoiceModel} $invoice instance of invoice model after it was updated
310 * @param {OsInvoiceModel} $old_invoice instance of invoice model before it was updated
311 *
312 * @since 5.1.0
313 * @hook latepoint_invoice_updated
314 *
315 */
316 do_action( 'latepoint_invoice_updated', $invoice, $old_invoice );
317 // 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
318 $other_invoices = new OsInvoiceModel();
319 $other_invoices = $other_invoices->where(
320 [
321 'status' => LATEPOINT_INVOICE_STATUS_DRAFT,
322 'order_id' => $order->id,
323 ]
324 )->get_results_as_models();
325 if ( $other_invoices ) {
326 foreach ( $other_invoices as $invoice ) {
327 $data = json_decode( $invoice->data, true );
328 $data['totals']['payments'] = $order->get_total_amount_paid_from_transactions( true );
329 $invoice->update_attributes( [ 'data' => json_encode( $data ) ] );
330 }
331 }
332 }
333 } else {
334 OsDebugHelper::log( 'Error creating transaction', 'transaction_error', $transaction->get_error_messages() );
335 }
336 }
337 $order_bookings = $order->get_bookings_from_order_items( true );
338 if ( $order_bookings ) {
339 foreach ( $order_bookings as $order_item_id => $order_booking ) {
340 $order_booking->order_item_id = $order_item_id;
341 $order_booking->customer_id = $order->customer_id;
342 $order_booking->end_time = $order_booking->calculate_end_time();
343 $order_booking->end_date = $order_booking->calculate_end_date();
344 $order_booking->set_utc_datetimes();
345 $service = new OsServiceModel( $order_booking->service_id );
346 $order_booking->buffer_before = $service->buffer_before;
347 $order_booking->buffer_after = $service->buffer_after;
348 $order_booking->customer_comment = $order->customer->notes;
349 if ( $order_booking->save() ) {
350
351 /**
352 * Booking was created
353 *
354 * @param {OsBookingModel} $booking instance of booking model that was created
355 *
356 * @since 5.0.0
357 * @hook latepoint_booking_created
358 *
359 */
360 do_action( 'latepoint_booking_created', $order_booking );
361 // set booking id to the one that was created for item data property
362 $order_item = new OsOrderItemModel( $order_item_id );
363 $item_data = json_decode( $order_item->item_data, true );
364 $item_data['id'] = $order_booking->id;
365 $order_item->update_attributes( [ 'item_data' => wp_json_encode( $item_data ) ] );
366 } else {
367 OsDebugHelper::log( 'Unable to save booking', 'booking_save_error', $order_booking->get_error_messages() );
368 }
369 }
370 }
371 // update connected cart with created order id
372 $this->mark_cart_converted();
373 $order->determine_payment_status();
374 // update with latest info
375 $order->get_items( true );
376
377 /**
378 * Order was created
379 *
380 * @param {OsOrderModel} $order instance of order model that was created
381 *
382 * @since 5.0.0
383 * @hook latepoint_order_created
384 *
385 */
386 do_action( 'latepoint_order_created', $order );
387
388 return $order->id;
389 } else {
390 $this->add_error( 'order_error', $order->get_error_messages() );
391
392 $this->mark_as_new();
393 return false;
394 }
395 } catch ( Exception $e ) {
396 $this->mark_as_new();
397 // translators: %s is the error description
398 $this->add_error( 'order_error', sprintf( __( 'Error: %s', 'latepoint' ), $e->getMessage() ) );
399 OsDebugHelper::log( 'Error converting intent to an order', 'order_error', $e->getMessage() );
400 return false;
401 }
402 }
403
404 public function get_by_intent_key( $intent_key ) {
405 return $this->where( [ 'intent_key' => $intent_key ] )->set_limit( 1 )->get_results_as_models();
406 }
407
408 public function mark_as_converted( OsOrderModel $order ) {
409 if ( empty( $order->id ) ) {
410 return false;
411 }
412
413 $this->update_attributes(
414 [
415 'order_id' => $order->id,
416 'status' => LATEPOINT_ORDER_INTENT_STATUS_CONVERTED,
417 ]
418 );
419 /**
420 * Order intent is converted to order
421 *
422 * @param {OsOrderIntentModel} $order_intent Instance of order intent model that has been converted to order
423 * @param {OsOrderModel} $order Instance of order model that order intent was converted to
424 *
425 * @since 5.0.0
426 * @hook latepoint_order_intent_converted
427 *
428 */
429 do_action( 'latepoint_order_intent_converted', $this, $order );
430 }
431
432 public function mark_as_processing() {
433 $this->update_attributes( [ 'status' => LATEPOINT_ORDER_INTENT_STATUS_PROCESSING ] );
434 /**
435 * Order intent is marked as processing
436 *
437 * @param {OsOrderIntentModel} $order_intent Instance of order intent model that has started processing
438 *
439 * @since 5.0.0
440 * @hook latepoint_order_intent_processing
441 *
442 */
443 do_action( 'latepoint_order_intent_processing', $this );
444 }
445
446 public function mark_as_new() {
447 $this->update_attributes( [ 'status' => LATEPOINT_ORDER_INTENT_STATUS_NEW ] );
448 /**
449 * Order intent is marked as new
450 *
451 * @param {OsOrderIntentModel} $order_intent Instance of order intent model that is being marked as new
452 *
453 * @since 5.0.0
454 * @hook latepoint_order_intent_new
455 *
456 */
457 do_action( 'latepoint_order_intent_new', $this );
458 }
459
460 // Determines if order intent has been converted into a order already
461 public function is_converted(): bool {
462 if ( empty( $this->order_id ) ) {
463 return false;
464 } else {
465 return true;
466 }
467 }
468
469 public function generate_data_vars(): array {
470 $vars = [
471 'id' => $this->id,
472 'intent_key' => $this->intent_key,
473 'customer_id' => $this->customer_id,
474 'booking_form_page_url' => $this->booking_form_page_url,
475 'cart_items_data' => ! empty( $this->cart_items_data ) ? json_decode( $this->cart_items_data, true ) : [],
476 'restrictions_data' => ! empty( $this->restrictions_data ) ? json_decode( $this->restrictions_data, true ) : [],
477 'presets_data' => ! empty( $this->presets_data ) ? json_decode( $this->presets_data, true ) : [],
478 'payment_data' => ! empty( $this->payment_data ) ? json_decode( $this->payment_data, true ) : [],
479 'other_data' => ! empty( $this->other_data ) ? json_decode( $this->other_data, true ) : [],
480 'order_id' => $this->order_id,
481 'coupon_code' => $this->coupon_code,
482 'coupon_discount' => $this->coupon_discount,
483 'tax_total' => $this->tax_total,
484 'updated_at' => $this->updated_at,
485 'created_at' => $this->created_at,
486 ];
487
488 return $vars;
489 }
490
491 public function get_page_url_with_intent() {
492 $booking_page_url = $this->booking_form_page_url;
493 $existing_var_position = strpos( $booking_page_url, 'latepoint_order_intent_key=' );
494 if ( $existing_var_position === false ) {
495 // no intent variable in url
496 $question_position = strpos( $booking_page_url, '?' );
497 if ( $question_position === false ) {
498 // no ?query params
499 $hash_position = strpos( $booking_page_url, '#' );
500 if ( $hash_position === false ) {
501 // no hashtag in url
502 $booking_page_url = $booking_page_url . '?latepoint_order_intent_key=' . $this->intent_key;
503 } else {
504 // hashtag in url and no ?query, prepend the hashtag with query
505 $booking_page_url = substr_replace( $booking_page_url, '?latepoint_order_intent_key=' . $this->intent_key . '#', $hash_position, 1 );
506 }
507 } else {
508 // ?query string exists, add intent key to it
509 $booking_page_url = substr_replace( $booking_page_url, '?latepoint_order_intent_key=' . $this->intent_key . '&', $question_position, 1 );
510 }
511 } else {
512 // intent key variable exist in url
513 preg_match( '/latepoint_order_intent_key=([\d,\w]*)/', $booking_page_url, $matches );
514 if ( isset( $matches[1] ) ) {
515 $booking_page_url = str_replace( 'latepoint_order_intent_key=' . $matches[1], 'latepoint_order_intent_key=' . $this->intent_key, $booking_page_url );
516 }
517 }
518
519 return $booking_page_url;
520 }
521
522
523 protected function before_create() {
524 if ( empty( $this->intent_key ) ) {
525 $this->intent_key = bin2hex( openssl_random_pseudo_bytes( 10 ) );
526 }
527 if ( empty( $this->status ) ) {
528 $this->status = LATEPOINT_ORDER_INTENT_STATUS_NEW;
529 }
530 }
531
532 protected function allowed_params( $role = 'admin' ) {
533 $allowed_params = array(
534 'customer_id',
535 'cart_items_data',
536 'restrictions_data',
537 'presets_data',
538 'payment_data',
539 'other_data',
540 'booking_form_page_url',
541 'intent_key',
542 'order_id',
543 'coupon_code',
544 'coupon_discount',
545 'tax_total',
546 'status',
547 );
548
549 return $allowed_params;
550 }
551
552
553 protected function params_to_save( $role = 'admin' ) {
554 $params_to_save = array(
555 'customer_id',
556 'cart_items_data',
557 'restrictions_data',
558 'presets_data',
559 'payment_data',
560 'other_data',
561 'booking_form_page_url',
562 'intent_key',
563 'total',
564 'subtotal',
565 'charge_amount',
566 'specs_charge_amount',
567 'price_breakdown',
568 'order_id',
569 'coupon_code',
570 'coupon_discount',
571 'tax_total',
572 'status',
573 );
574
575 return $params_to_save;
576 }
577
578
579 protected function properties_to_validate() {
580 $validations = array(
581 'customer_id' => array( 'presence' ),
582 );
583
584 return $validations;
585 }
586
587 public function mark_cart_converted( ?OsCartModel $cart = null ): bool {
588 if ( $this->is_new_record() || empty( $this->order_id ) ) {
589 return false;
590 }
591 if ( ! empty( $cart ) ) {
592 $cart->order_id = $this->order_id;
593 $cart->save();
594 } else {
595 $carts = new OsCartModel();
596 $carts = $carts->where( [ 'order_intent_id' => $this->id ] )->get_results_as_models();
597 if ( ! empty( $carts ) ) {
598 foreach ( $carts as $cart ) {
599 $cart->order_id = $this->order_id;
600 $cart->save();
601 }
602 }
603 }
604 return true;
605 }
606 }
607