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