PluginProbe ʕ •ᴥ•ʔ
LatePoint – Calendar Booking Plugin for Appointments and Events / 5.2.2
LatePoint – Calendar Booking Plugin for Appointments and Events v5.2.2
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 1 year ago agent_meta_model.php 1 year ago agent_model.php 1 year ago booking_meta_model.php 1 year ago booking_model.php 1 year ago bundle_meta_model.php 1 year ago bundle_model.php 1 year ago cart_item_model.php 1 year ago cart_meta_model.php 1 year ago cart_model.php 1 year ago connector_model.php 1 year ago customer_meta_model.php 1 year ago customer_model.php 9 months ago invoice_model.php 1 year ago join_bundles_services_model.php 1 year ago location_category_model.php 1 year ago location_model.php 1 year ago meta_model.php 1 year ago model.php 9 months ago off_period_model.php 1 year ago order_intent_meta_model.php 1 year ago order_intent_model.php 9 months ago order_item_model.php 1 year ago order_meta_model.php 1 year ago order_model.php 1 year ago otp_model.php 9 months ago payment_request_model.php 1 year ago process_job_model.php 1 year ago process_model.php 1 year ago recurrence_model.php 1 year ago service_category_model.php 9 months ago service_meta_model.php 1 year ago service_model.php 9 months ago session_model.php 1 year ago settings_model.php 1 year ago step_settings_model.php 1 year ago transaction_intent_model.php 1 year ago transaction_model.php 1 year ago transaction_refund_model.php 1 year ago work_period_model.php 1 year ago
order_intent_model.php
596 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 }else if($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()) $transaction->invoice_id = $invoice->id;
288 if ( $transaction->save() ) {
289
290 /**
291 * Transaction was created
292 *
293 * @param {OsTransactionModel} $transaction instance of transaction model that was created
294 *
295 * @since 5.1.0
296 * @hook latepoint_transaction_created
297 *
298 */
299 do_action( 'latepoint_transaction_created', $transaction );
300 if(!$invoice->is_new_record()){
301 $old_invoice = clone $invoice;
302 $invoice->update_attributes(['status' => LATEPOINT_INVOICE_STATUS_PAID]);
303 /**
304 * Invoice was updated
305 *
306 * @param {OsInvoiceModel} $invoice instance of invoice model after it was updated
307 * @param {OsInvoiceModel} $old_invoice instance of invoice model before it was updated
308 *
309 * @since 5.1.0
310 * @hook latepoint_invoice_updated
311 *
312 */
313 do_action( 'latepoint_invoice_updated', $invoice, $old_invoice );
314 // 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
315 $other_invoices = new OsInvoiceModel();
316 $other_invoices = $other_invoices->where(['status' => LATEPOINT_INVOICE_STATUS_DRAFT, 'order_id' => $order->id])->get_results_as_models();
317 if($other_invoices){
318 foreach($other_invoices as $invoice){
319 $data = json_decode($invoice->data, true);
320 $data['totals']['payments'] = $order->get_total_amount_paid_from_transactions(true);
321 $invoice->update_attributes(['data' => json_encode($data)]);
322 }
323 }
324
325 }
326
327 } else {
328 OsDebugHelper::log( 'Error creating transaction', 'transaction_error', $transaction->get_error_messages() );
329 }
330 }
331 $order_bookings = $order->get_bookings_from_order_items(true);
332 if ( $order_bookings ) {
333 foreach ( $order_bookings as $order_item_id => $order_booking ) {
334 $order_booking->order_item_id = $order_item_id;
335 $order_booking->customer_id = $order->customer_id;
336 $order_booking->end_time = $order_booking->calculate_end_time();
337 $order_booking->end_date = $order_booking->calculate_end_date();
338 $order_booking->set_utc_datetimes();
339 $service = new OsServiceModel( $order_booking->service_id );
340 $order_booking->buffer_before = $service->buffer_before;
341 $order_booking->buffer_after = $service->buffer_after;
342 $order_booking->customer_comment = $order->customer->notes;
343 if ( $order_booking->save() ) {
344
345 /**
346 * Booking was created
347 *
348 * @param {OsBookingModel} $booking instance of booking model that was created
349 *
350 * @since 5.0.0
351 * @hook latepoint_booking_created
352 *
353 */
354 do_action( 'latepoint_booking_created', $order_booking );
355 // set booking id to the one that was created for item data property
356 $order_item = new OsOrderItemModel( $order_item_id );
357 $item_data = json_decode( $order_item->item_data, true );
358 $item_data['id'] = $order_booking->id;
359 $order_item->update_attributes( [ 'item_data' => wp_json_encode( $item_data ) ] );
360 } else {
361 OsDebugHelper::log( 'Unable to save booking', 'booking_save_error', $order_booking->get_error_messages() );
362 }
363 }
364 }
365 // update connected cart with created order id
366 $this->mark_cart_converted();
367 $order->determine_payment_status();
368 // update with latest info
369 $order->get_items(true);
370
371 /**
372 * Order was created
373 *
374 * @param {OsOrderModel} $order instance of order model that was created
375 *
376 * @since 5.0.0
377 * @hook latepoint_order_created
378 *
379 */
380 do_action( 'latepoint_order_created', $order );
381
382 return $order->id;
383 } else {
384 $this->add_error( 'order_error', $order->get_error_messages() );
385
386 $this->mark_as_new();
387 return false;
388 }
389 } catch ( Exception $e ) {
390 $this->mark_as_new();
391 // translators: %s is the error description
392 $this->add_error( 'order_error', sprintf(__('Error: %s', 'latepoint'), $e->getMessage() ));
393 OsDebugHelper::log( 'Error converting intent to an order', 'order_error', $e->getMessage() );
394 return false;
395 }
396 }
397
398 public function get_by_intent_key( $intent_key ) {
399 return $this->where( [ 'intent_key' => $intent_key ] )->set_limit( 1 )->get_results_as_models();
400 }
401
402 public function mark_as_converted( OsOrderModel $order ) {
403 if ( empty( $order->id ) ) {
404 return false;
405 }
406
407 $this->update_attributes( [ 'order_id' => $order->id, 'status' => LATEPOINT_ORDER_INTENT_STATUS_CONVERTED ] );
408 /**
409 * Order intent is converted to order
410 *
411 * @param {OsOrderIntentModel} $order_intent Instance of order intent model that has been converted to order
412 * @param {OsOrderModel} $order Instance of order model that order intent was converted to
413 *
414 * @since 5.0.0
415 * @hook latepoint_order_intent_converted
416 *
417 */
418 do_action( 'latepoint_order_intent_converted', $this, $order );
419 }
420
421 public function mark_as_processing() {
422 $this->update_attributes( [ 'status' => LATEPOINT_ORDER_INTENT_STATUS_PROCESSING ] );
423 /**
424 * Order intent is marked as processing
425 *
426 * @param {OsOrderIntentModel} $order_intent Instance of order intent model that has started processing
427 *
428 * @since 5.0.0
429 * @hook latepoint_order_intent_processing
430 *
431 */
432 do_action( 'latepoint_order_intent_processing', $this );
433 }
434
435 public function mark_as_new() {
436 $this->update_attributes( [ 'status' => LATEPOINT_ORDER_INTENT_STATUS_NEW ] );
437 /**
438 * Order intent is marked as new
439 *
440 * @param {OsOrderIntentModel} $order_intent Instance of order intent model that is being marked as new
441 *
442 * @since 5.0.0
443 * @hook latepoint_order_intent_new
444 *
445 */
446 do_action( 'latepoint_order_intent_new', $this );
447 }
448
449 // Determines if order intent has been converted into a order already
450 public function is_converted() : bool {
451 if ( empty( $this->order_id ) ) {
452 return false;
453 } else {
454 return true;
455 }
456 }
457
458 public function generate_data_vars(): array {
459 $vars = [
460 'id' => $this->id,
461 'intent_key' => $this->intent_key,
462 'customer_id' => $this->customer_id,
463 'booking_form_page_url' => $this->booking_form_page_url,
464 'cart_items_data' => !empty($this->cart_items_data) ? json_decode( $this->cart_items_data, true ) : [],
465 'restrictions_data' => !empty($this->restrictions_data) ? json_decode( $this->restrictions_data, true ) : [],
466 'presets_data' => !empty($this->presets_data) ? json_decode( $this->presets_data, true ) : [],
467 'payment_data' => !empty($this->payment_data) ? json_decode( $this->payment_data, true ) : [],
468 'other_data' => !empty($this->other_data) ? json_decode( $this->other_data, true ) : [],
469 'order_id' => $this->order_id,
470 'coupon_code' => $this->coupon_code,
471 'coupon_discount' => $this->coupon_discount,
472 'tax_total' => $this->tax_total,
473 'updated_at' => $this->updated_at,
474 'created_at' => $this->created_at,
475 ];
476
477 return $vars;
478 }
479
480 public function get_page_url_with_intent() {
481 $booking_page_url = $this->booking_form_page_url;
482 $existing_var_position = strpos( $booking_page_url, 'latepoint_order_intent_key=' );
483 if ( $existing_var_position === false ) {
484 // no intent variable in url
485 $question_position = strpos( $booking_page_url, '?' );
486 if ( $question_position === false ) {
487 // no ?query params
488 $hash_position = strpos( $booking_page_url, '#' );
489 if ( $hash_position === false ) {
490 // no hashtag in url
491 $booking_page_url = $booking_page_url . '?latepoint_order_intent_key=' . $this->intent_key;
492 } else {
493 // hashtag in url and no ?query, prepend the hashtag with query
494 $booking_page_url = substr_replace( $booking_page_url, '?latepoint_order_intent_key=' . $this->intent_key . '#', $hash_position, 1 );
495 }
496 } else {
497 // ?query string exists, add intent key to it
498 $booking_page_url = substr_replace( $booking_page_url, '?latepoint_order_intent_key=' . $this->intent_key . '&', $question_position, 1 );
499 }
500 } else {
501 // intent key variable exist in url
502 preg_match( '/latepoint_order_intent_key=([\d,\w]*)/', $booking_page_url, $matches );
503 if ( isset( $matches[1] ) ) {
504 $booking_page_url = str_replace( 'latepoint_order_intent_key=' . $matches[1], 'latepoint_order_intent_key=' . $this->intent_key, $booking_page_url );
505 }
506 }
507
508 return $booking_page_url;
509 }
510
511
512 protected function before_create() {
513 if ( empty( $this->intent_key ) ) {
514 $this->intent_key = bin2hex( openssl_random_pseudo_bytes( 10 ) );
515 }
516 if ( empty( $this->status ) ) {
517 $this->status = LATEPOINT_ORDER_INTENT_STATUS_NEW;
518 }
519 }
520
521 protected function allowed_params( $role = 'admin' ) {
522 $allowed_params = array(
523 'customer_id',
524 'cart_items_data',
525 'restrictions_data',
526 'presets_data',
527 'payment_data',
528 'other_data',
529 'booking_form_page_url',
530 'intent_key',
531 'order_id',
532 'coupon_code',
533 'coupon_discount',
534 'tax_total',
535 'status',
536 );
537
538 return $allowed_params;
539 }
540
541
542 protected function params_to_save( $role = 'admin' ) {
543 $params_to_save = array(
544 'customer_id',
545 'cart_items_data',
546 'restrictions_data',
547 'presets_data',
548 'payment_data',
549 'other_data',
550 'booking_form_page_url',
551 'intent_key',
552 'total',
553 'subtotal',
554 'charge_amount',
555 'specs_charge_amount',
556 'price_breakdown',
557 'order_id',
558 'coupon_code',
559 'coupon_discount',
560 'tax_total',
561 'status',
562 );
563
564 return $params_to_save;
565 }
566
567
568 protected function properties_to_validate() {
569 $validations = array(
570 'customer_id' => array( 'presence' ),
571 );
572
573 return $validations;
574 }
575
576 public function mark_cart_converted(?OsCartModel $cart = null) : bool {
577 if($this->is_new_record() || empty($this->order_id)){
578 return false;
579 }
580 if(!empty($cart)){
581 $cart->order_id = $this->order_id;
582 $cart->save();
583 }else{
584 $carts = new OsCartModel();
585 $carts = $carts->where( [ 'order_intent_id' => $this->id ] )->get_results_as_models();
586 if ( ! empty( $carts ) ) {
587 foreach ( $carts as $cart ) {
588 $cart->order_id = $this->order_id;
589 $cart->save();
590 }
591 }
592 }
593 return true;
594 }
595 }
596