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