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 / cart_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
cart_model.php
593 lines
1 <?php
2 /*
3 * Copyright (c) 2023 LatePoint LLC. All rights reserved.
4 */
5
6 class OsCartModel extends OsModel {
7 public $items; // should NOT be set by default, it means they are not loaded, to avoid queries to DB
8
9 public $id,
10 $uuid,
11 $order_id,
12 $coupon_code = '',
13 $order_intent_id,
14 $payment_method,
15 $payment_portion,
16 $payment_time,
17 $payment_token,
18 $payment_processor,
19 $source_id = '',
20 $order_forced_customer_id = false, // only used for when you creating a cart from an order
21 $subtotal = 0,
22 $total = 0,
23 $coupon_discount = 0,
24 $tax_total = 0,
25 $updated_at,
26 $created_at;
27
28 function __construct( $id = false ) {
29 parent::__construct();
30 $this->table_name = LATEPOINT_TABLE_CARTS;
31
32 if ( $id ) {
33 $this->load_by_id( $id );
34 }
35 }
36
37
38 public function get_total() {
39
40 /**
41 * Get total of a cart
42 *
43 * @param {float} $total Total amount in database format 1999.0000
44 * @param {OsCartModel} $cart Cart that total is assessed on
45 * @returns {float} The filtered "total" amount
46 *
47 * @since 5.0.0
48 * @hook latepoint_cart_get_total
49 *
50 */
51 $amount = apply_filters( 'latepoint_cart_get_total', $this->total, $this );
52
53 return OsMoneyHelper::pad_to_db_format( $amount );
54 }
55
56 public function get_order_intent(): OsOrderIntentModel {
57 return new OsOrderIntentModel( $this->order_intent_id );
58 }
59
60
61 public function get_subtotal() {
62
63 /**
64 * Get subtotal of a cart
65 *
66 * @param {float} $subtotal Subtotal amount in database format 1999.0000
67 * @param {OsCartModel} $cart Cart that subtotal is assessed on
68 * @returns {float} The filtered "subtotal" amount
69 *
70 * @since 5.0.0
71 * @hook latepoint_cart_get_subtotal
72 *
73 */
74 $amount = apply_filters( 'latepoint_cart_get_subtotal', $this->subtotal, $this );
75
76 return OsMoneyHelper::pad_to_db_format( $amount );
77 }
78
79 public function get_coupon_discount() {
80
81 /**
82 * Get coupon discount of a cart
83 *
84 * @param {float} $discount_amount Coupon discount amount in database format 1999.0000
85 * @param {OsCartModel} $cart Cart that coupon discount is assessed on
86 * @returns {float} The filtered "coupon discount" amount
87 *
88 * @since 5.0.0
89 * @hook latepoint_cart_get_coupon_discount
90 *
91 */
92 $amount = apply_filters( 'latepoint_cart_get_coupon_discount', $this->coupon_discount, $this );
93
94 return OsMoneyHelper::pad_to_db_format( $amount );
95 }
96
97
98 public function get_tax_total() {
99
100 /**
101 * Get Total Tax amount of a cart
102 *
103 * @param {float} $tax_total Total amount of tax for a cart in database format 1999.0000
104 * @param {OsCartModel} $cart Cart that tax total is requested for
105 * @returns {float} The filtered "tax_total" amount
106 *
107 * @since 5.0.0
108 * @hook latepoint_cart_get_tax_total
109 *
110 */
111 $amount = apply_filters( 'latepoint_cart_get_tax_total', $this->tax_total, $this );
112
113 return OsMoneyHelper::pad_to_db_format( $amount );
114 }
115
116 public function get_coupon_code() {
117 /**
118 * Get coupon code of a cart
119 *
120 * @param {string} $coupon_code Coupon code
121 * @param {OsCartItemModel} $cart Cart Item that coupon code is requested for
122 * @returns {string} The filtered "coupon code" value
123 *
124 * @since 5.0.0
125 * @hook latepoint_cart_get_coupon_code
126 *
127 */
128 return apply_filters( 'latepoint_cart_get_coupon_code', $this->coupon_code, $this );
129 }
130
131
132 public function set_coupon_code( string $coupon_code ) {
133 $this->coupon_code = $coupon_code;
134 if ( ! $this->is_new_record() ) {
135 $this->update_attributes( [ 'coupon_code' => $coupon_code ] );
136 }
137 }
138
139
140 public function clear_coupon_code() {
141 $this->coupon_code = '';
142 if ( ! $this->is_new_record() ) {
143 $this->update_attributes( [ 'coupon_code' => '' ] );
144 }
145 }
146
147 /**
148 * @return OsBookingModel[]
149 */
150 public function get_bookings_from_cart_items(): array {
151 $cart_bookings = [];
152 foreach ( $this->get_items() as $cart_item ) {
153 if ( $cart_item->is_booking() ) {
154 $cart_bookings[ $cart_item->id ] = $cart_item->build_original_object_from_item_data();
155 }
156 }
157
158 return $cart_bookings;
159 }
160
161
162 /**
163 * @return OsBundleModel[]
164 */
165 public function get_bundles_from_cart_items(): array {
166 $cart_bundles = [];
167 foreach ( $this->get_items() as $cart_item ) {
168 if ( $cart_item->is_bundle() ) {
169 $cart_bundles[ $cart_item->id ] = $cart_item->build_original_object_from_item_data();
170 }
171 }
172
173 return $cart_bundles;
174 }
175
176 public function is_empty(): bool {
177 return ! $this->get_items();
178 }
179
180
181 public function delete_meta_by_key( $meta_key ) {
182 if ( $this->is_new_record() ) {
183 return false;
184 }
185
186 $meta = new OsCartMetaModel();
187
188 return $meta->delete_by_key( $meta_key, $this->id );
189 }
190
191 public function get_meta_by_key( $meta_key, $default = false ) {
192 if ( $this->is_new_record() ) {
193 return $default;
194 }
195
196 $meta = new OsCartMetaModel();
197
198 return $meta->get_by_key( $meta_key, $this->id, $default );
199 }
200
201 public function save_meta_by_key( $meta_key, $meta_value ) {
202 if ( $this->is_new_record() ) {
203 return false;
204 }
205
206 $meta = new OsCartMetaModel();
207
208 return $meta->save_by_key( $meta_key, $meta_value, $this->id );
209 }
210
211
212 public function clear(): void {
213 // remove current cart items
214 foreach ( $this->get_items() as $cart_item ) {
215 $cart_item->delete();
216 }
217 // Reset the in-memory cache to an empty array instead of unsetting it. If we unset, the next add_item() triggers
218 // get_items() to re-query the DB (which now contains the row just saved by add_item), and the caller then
219 // appends the same item again in memory, producing N+1 items.
220 $this->items = [];
221 }
222
223 /** ?
224 *
225 * @return OsCartItemModel[]
226 */
227 public function get_items(): array {
228 // only call DB when needed
229 if ( ! isset( $this->items ) && ! empty( $this->id ) ) {
230 $this->items = OsCartsHelper::get_items_for_cart_id( $this->id );
231 }
232
233 if ( empty( $this->items ) ) {
234 $this->items = [];
235 }
236
237 return $this->items;
238 }
239
240 /**
241 * @param array $rows_to_hide
242 *
243 * @return array[]
244 */
245 public function generate_price_breakdown_rows( array $rows_to_hide = [] ): array {
246 $rows = [
247 'before_subtotal' => [],
248 'subtotal' => [],
249 'after_subtotal' => [],
250 'total' => [],
251 'balance' => [],
252 ];
253
254 $items = $this->get_items();
255
256
257 // payments and balance have to always be recalculated, even if requested for existing booking
258 if ( ! in_array( 'balance', $rows_to_hide ) ) {
259 $balance_due_amount = $this->total;
260 $rows['balance'] = [
261 'label' => __( 'Balance Due', 'latepoint' ),
262 'raw_value' => OsMoneyHelper::pad_to_db_format( $balance_due_amount ),
263 'value' => OsMoneyHelper::format_price( $balance_due_amount, true, false ),
264 'style' => 'total',
265 ];
266 }
267
268 foreach ( $items as $item ) {
269 switch ( $item->variant ) {
270 case LATEPOINT_ITEM_VARIANT_BOOKING:
271 $booking = $item->build_original_object_from_item_data();
272
273 // recalculations are below this point
274 $service_row = [
275 'heading' => __( 'Service', 'latepoint' ),
276 'items' => [],
277 ];
278 $item_subtotal = OsBookingHelper::calculate_full_amount_for_service( $booking );
279 $service_row_item = [
280 'label' => $booking->service->name,
281 'raw_value' => OsMoneyHelper::pad_to_db_format( $item_subtotal ),
282 'value' => OsMoneyHelper::format_price( $item_subtotal, true, false ),
283 ];
284 $service_row['items'][] = $service_row_item;
285 $service_row = apply_filters( 'latepoint_price_breakdown_service_row_for_booking', $service_row, $booking );
286 $rows['before_subtotal'][] = $service_row;
287 break;
288 case LATEPOINT_ITEM_VARIANT_BUNDLE:
289 // TODO Merge somehow this case with the booking case as they are reusing a lot of code
290 // recalculations are below this point
291 $bundle = $item->build_original_object_from_item_data();
292 $service_row = [
293 'heading' => __( 'Bundle', 'latepoint' ),
294 'items' => [],
295 ];
296 $item_subtotal = OsBundlesHelper::calculate_full_amount_for_bundle( $bundle );
297 $service_row_item = [
298 'label' => $bundle->name,
299 'raw_value' => OsMoneyHelper::pad_to_db_format( $item_subtotal ),
300 'value' => OsMoneyHelper::format_price( $item_subtotal, true, false ),
301 ];
302 $service_row['items'][] = $service_row_item;
303 $service_row = apply_filters( 'latepoint_price_breakdown_service_row_for_bundle', $service_row, $bundle );
304 $rows['before_subtotal'][] = $service_row;
305 break;
306 }
307 }
308
309
310 if ( ! in_array( 'subtotal', $rows_to_hide ) ) {
311 $subtotal_amount = $this->subtotal;
312 $rows['subtotal'] = [
313 'label' => __( 'Sub Total', 'latepoint' ),
314 'style' => 'strong',
315 'raw_value' => OsMoneyHelper::pad_to_db_format( $subtotal_amount ),
316 'value' => OsMoneyHelper::format_price( $subtotal_amount, true, false ),
317 ];
318 }
319
320 if ( ! in_array( 'total', $rows_to_hide ) ) {
321 $total_amount = $this->total;
322 $rows['total'] = [
323 'label' => __( 'Total Price', 'latepoint' ),
324 'style' => in_array( 'balance', $rows_to_hide ) ? 'total' : 'strong',
325 'raw_value' => OsMoneyHelper::pad_to_db_format( $total_amount ),
326 'value' => OsMoneyHelper::format_price( $total_amount, true, false ),
327 ];
328 }
329
330 // filter only applies when recalculating rows, do not apply it to the existing data, since it has already ran
331 return apply_filters( 'latepoint_cart_price_breakdown_rows', $rows, $this, $rows_to_hide );
332 }
333
334
335 /**
336 * @param array $options
337 *
338 * @return mixed|void
339 *
340 * Returns amount to charge depending on a portion set in database format 1999.0000
341 *
342 */
343 public function amount_to_charge( array $options = [] ) {
344 $amount = ( $this->payment_portion == LATEPOINT_PAYMENT_PORTION_DEPOSIT ) ? $this->deposit_amount_to_charge( $options ) : $this->full_amount_to_charge( $options );
345
346 return apply_filters( 'latepoint_cart_amount_to_charge', $amount, $this, $options );
347 }
348
349
350 /**
351 * @param array $options
352 *
353 * @return mixed|void
354 *
355 * Returns deposit amount to charge in database format 1999.0000
356 *
357 */
358 public function deposit_amount_to_charge( array $options = [] ) {
359 $default_options = [
360 'apply_coupons' => false,
361 'apply_taxes' => false,
362 ];
363 $options = array_merge( $default_options, $options );
364 $amount = 0;
365 $items = $this->get_items();
366 if ( empty( $items ) ) {
367 return $amount;
368 }
369 foreach ( $items as $item ) {
370 $amount += $item->deposit_amount_to_charge( $options );
371 }
372
373 /**
374 * Filter deposit amount to charge on the cart object
375 *
376 * @param {float} $amount The amount to charge on the cart
377 * @param {OsCartModel} $cart Cart object that deposit amount is calculated on
378 * @param {array} $options Array of options that determine if taxes and coupons should be applied
379 * @returns {float} The filtered amount to charge on the cart
380 *
381 * @since 5.0.0
382 * @hook latepoint_cart_deposit_amount_to_charge
383 *
384 */
385 return apply_filters( 'latepoint_cart_deposit_amount_to_charge', $amount, $this, $options );
386 }
387
388 public function deposit_amount_to_charge_formatted( array $options = [] ) {
389 $amount = $this->deposit_amount_to_charge( $options );
390
391 return OsMoneyHelper::format_price( $amount, true, false );
392 }
393
394 /**
395 * @param array $options
396 *
397 * @return mixed|void
398 *
399 * Returns full amount to charge in database format 1999.0000
400 *
401 */
402 public function full_amount_to_charge( array $options = [] ) {
403 /**
404 * Get full amount to charge
405 *
406 * @param {float} $total Full amount to charge database format 1999.0000
407 * @param {OsCartModel} $cart Cart that total is assessed on
408 * @returns {float} The filtered full amount to charge
409 *
410 * @since 5.0.0
411 * @hook latepoint_cart_full_amount_to_charge
412 *
413 */
414 $amount = apply_filters( 'latepoint_cart_full_amount_to_charge', $this->get_total(), $this, $options );
415
416 return OsMoneyHelper::pad_to_db_format( $amount );
417 }
418
419
420 public function specs_calculate_amount_to_charge() {
421 if ( $this->payment_portion == LATEPOINT_PAYMENT_PORTION_DEPOSIT ) {
422 return $this->specs_calculate_deposit_amount_to_charge();
423 } else {
424 return $this->specs_calculate_full_amount_to_charge();
425 }
426 }
427
428 public function specs_calculate_full_amount_to_charge() {
429 return OsPaymentsHelper::convert_charge_amount_to_requirements( $this->get_total(), $this );
430 }
431
432 public function specs_calculate_deposit_amount_to_charge() {
433 return OsPaymentsHelper::convert_charge_amount_to_requirements( $this->deposit_amount_to_charge(), $this );
434 }
435
436 public function get_total_formatted() {
437 return OsMoneyHelper::format_price( $this->get_total(), true, false );
438 }
439
440 public function set_payment_portion() {
441 if ( ! empty( $this->payment_time ) ) {
442 if ( $this->payment_time == LATEPOINT_PAYMENT_TIME_LATER ) {
443 $this->payment_portion = LATEPOINT_PAYMENT_PORTION_FULL;
444 } else {
445 $deposit_amount = $this->deposit_amount_to_charge();
446 $this->payment_portion = ( $deposit_amount > 0 ) ? LATEPOINT_PAYMENT_PORTION_DEPOSIT : LATEPOINT_PAYMENT_PORTION_FULL;
447 }
448 }
449 }
450
451 public function set_payment_processor() {
452 if ( empty( $this->payment_processor ) && ! empty( $this->payment_time ) && ! empty( $this->payment_method ) ) {
453 $enabled_processors = OsPaymentsHelper::get_enabled_payment_processors_for_payment_time_and_method( $this->payment_time, $this->payment_method );
454 if ( count( $enabled_processors ) == 1 ) {
455 $this->payment_processor = array_key_first( $enabled_processors );
456 }
457 }
458 }
459
460 public function set_payment_time() {
461 if ( empty( $this->payment_time ) ) {
462 $enabled_payment_times = OsPaymentsHelper::get_enabled_payment_times();
463 if ( count( $enabled_payment_times ) == 1 ) {
464 $this->payment_time = array_key_first( $enabled_payment_times );
465 }
466 }
467 }
468
469 public function set_payment_method() {
470 if ( ! empty( $this->payment_time ) ) {
471 $enabled_payment_methods = OsPaymentsHelper::get_enabled_payment_methods_for_payment_time( $this->payment_time );
472 if ( count( $enabled_payment_methods ) == 1 ) {
473 $this->payment_method = array_key_first( $enabled_payment_methods );
474 }
475 }
476 }
477
478 public function set_singular_payment_attributes() {
479 $this->set_payment_time();
480 $this->set_payment_portion();
481 $this->set_payment_method();
482 $this->set_payment_processor();
483 }
484
485 public function remove_item( OsCartItemModel $item, bool $remove_connected_items = true ) {
486 if ( $item->id && $this->id == $item->cart_id ) {
487 if ( $remove_connected_items ) {
488 if ( ! empty( $item->connected_cart_item_id ) ) {
489 $cart_items = new OsCartItemModel();
490 $cart_items->delete_where( [ 'id' => $item->connected_cart_item_id ] );
491 }
492 // search for connected cart items
493 $cart_items = new OsCartItemModel();
494 $cart_items->delete_where( [ 'connected_cart_item_id' => $item->id ] );
495 }
496 $item->delete();
497 $this->items = OsCartsHelper::get_items_for_cart_id( $this->id );
498 }
499 $this->calculate_prices();
500
501 return true;
502 }
503
504 public function add_item( OsCartItemModel $item, bool $permanent = true, bool $calculate_prices = true ) {
505 if ( $permanent ) {
506 // save cart itself if not saved yet, since it's a permanent addition to cart
507 if ( empty( $this->id ) ) {
508 $this->save();
509 }
510 $item->cart_id = $this->id;
511 if ( $item->save() ) {
512 // we are doing this - to modify a copy of $items, to avoid modifying the getter's return value
513 $items = $this->get_items();
514 $items[] = $item;
515 $this->items = $items;
516 }
517 } else {
518 // we are doing this - to modify a copy of $items, to avoid modifying the getter's return value
519 $items = $this->get_items();
520 $items[] = $item;
521 $this->items = $items;
522 }
523 if ( $calculate_prices ) {
524 $this->calculate_prices();
525 }
526
527 return true;
528 }
529
530 public function calculate_prices() {
531
532 // calculate subtotal for all items
533 foreach ( $this->get_items() as $item ) {
534 $item->subtotal = $item->full_amount_to_charge();
535 $item->total = $item->subtotal;
536 }
537
538
539 // do cart subtotal
540 $this->subtotal = 0;
541 foreach ( $this->get_items() as $item ) {
542 $this->subtotal = $this->subtotal += $item->subtotal;
543 }
544 // do cart total
545 $this->total = 0;
546 foreach ( $this->get_items() as $item ) {
547 $this->total = $this->total += $item->total;
548 }
549
550
551 /**
552 * Triggers when cart prices are being calculated
553 *
554 * @param {OsCartModel} $cart Cart model for which prices are being generated
555 *
556 * @since 5.0.0
557 * @hook latepoint_cart_calculate_prices
558 *
559 */
560 do_action( 'latepoint_cart_calculate_prices', $this );
561 }
562
563
564 protected function allowed_params( $role = 'admin' ) {
565 $allowed_params = array(
566 'payment_method',
567 'payment_portion',
568 'payment_processor',
569 'payment_time',
570 'coupon_code',
571 'payment_token',
572 'source_id',
573 );
574
575 return $allowed_params;
576 }
577
578
579 protected function params_to_save( $role = 'admin' ) {
580 $params_to_save = array(
581 'id',
582 'uuid',
583 'order_intent_id',
584 'order_id',
585 'coupon_code',
586 'updated_at',
587 'created_at',
588 );
589
590 return $params_to_save;
591 }
592 }
593