<?php /* * Copyright (c) 2023 LatePoint LLC. All rights reserved. */ class OsCartModel extends OsModel { public $items; // should NOT be set by default, it means they are not loaded, to avoid queries to DB public $id, $uuid, $order_id, $coupon_code = '', $order_intent_id, $payment_method, $payment_portion, $payment_time, $payment_token, $payment_processor, $source_id = '', $order_forced_customer_id = false, // only used for when you creating a cart from an order $subtotal = 0, $total = 0, $coupon_discount = 0, $tax_total = 0, $updated_at, $created_at; function __construct( $id = false ) { parent::__construct(); $this->table_name = LATEPOINT_TABLE_CARTS; if ( $id ) { $this->load_by_id( $id ); } } public function get_total() { /** * Get total of a cart * * @param {float} $total Total amount in database format 1999.0000 * @param {OsCartModel} $cart Cart that total is assessed on * @returns {float} The filtered "total" amount * * @since 5.0.0 * @hook latepoint_cart_get_total * */ $amount = apply_filters( 'latepoint_cart_get_total', $this->total, $this ); return OsMoneyHelper::pad_to_db_format( $amount ); } public function get_order_intent() : OsOrderIntentModel { return new OsOrderIntentModel($this->order_intent_id); } public function get_subtotal() { /** * Get subtotal of a cart * * @param {float} $subtotal Subtotal amount in database format 1999.0000 * @param {OsCartModel} $cart Cart that subtotal is assessed on * @returns {float} The filtered "subtotal" amount * * @since 5.0.0 * @hook latepoint_cart_get_subtotal * */ $amount = apply_filters( 'latepoint_cart_get_subtotal', $this->subtotal, $this ); return OsMoneyHelper::pad_to_db_format( $amount ); } public function get_coupon_discount() { /** * Get coupon discount of a cart * * @param {float} $discount_amount Coupon discount amount in database format 1999.0000 * @param {OsCartModel} $cart Cart that coupon discount is assessed on * @returns {float} The filtered "coupon discount" amount * * @since 5.0.0 * @hook latepoint_cart_get_coupon_discount * */ $amount = apply_filters( 'latepoint_cart_get_coupon_discount', $this->coupon_discount, $this ); return OsMoneyHelper::pad_to_db_format( $amount ); } public function get_tax_total() { /** * Get Total Tax amount of a cart * * @param {float} $tax_total Total amount of tax for a cart in database format 1999.0000 * @param {OsCartModel} $cart Cart that tax total is requested for * @returns {float} The filtered "tax_total" amount * * @since 5.0.0 * @hook latepoint_cart_get_tax_total * */ $amount = apply_filters( 'latepoint_cart_get_tax_total', $this->tax_total, $this ); return OsMoneyHelper::pad_to_db_format( $amount ); } public function get_coupon_code() { /** * Get coupon code of a cart * * @param {string} $coupon_code Coupon code * @param {OsCartItemModel} $cart Cart Item that coupon code is requested for * @returns {string} The filtered "coupon code" value * * @since 5.0.0 * @hook latepoint_cart_get_coupon_code * */ return apply_filters( 'latepoint_cart_get_coupon_code', $this->coupon_code, $this ); } public function set_coupon_code( string $coupon_code ) { $this->coupon_code = $coupon_code; if ( ! $this->is_new_record() ) { $this->update_attributes( [ 'coupon_code' => $coupon_code ] ); } } public function clear_coupon_code() { $this->coupon_code = ''; if ( ! $this->is_new_record() ) { $this->update_attributes( [ 'coupon_code' => '' ] ); } } /** * @return OsBookingModel[] */ public function get_bookings_from_cart_items(): array { $cart_bookings = []; foreach ( $this->get_items() as $cart_item ) { if ( $cart_item->is_booking() ) { $cart_bookings[ $cart_item->id ] = $cart_item->build_original_object_from_item_data(); } } return $cart_bookings; } /** * @return OsBundleModel[] */ public function get_bundles_from_cart_items(): array { $cart_bundles = []; foreach ( $this->get_items() as $cart_item ) { if ( $cart_item->is_bundle() ) { $cart_bundles[ $cart_item->id ] = $cart_item->build_original_object_from_item_data(); } } return $cart_bundles; } public function is_empty(): bool { return ! $this->get_items(); } public function delete_meta_by_key( $meta_key ) { if ( $this->is_new_record() ) { return false; } $meta = new OsCartMetaModel(); return $meta->delete_by_key( $meta_key, $this->id ); } public function get_meta_by_key( $meta_key, $default = false ) { if ( $this->is_new_record() ) { return $default; } $meta = new OsCartMetaModel(); return $meta->get_by_key( $meta_key, $this->id, $default ); } public function save_meta_by_key( $meta_key, $meta_value ) { if ( $this->is_new_record() ) { return false; } $meta = new OsCartMetaModel(); return $meta->save_by_key( $meta_key, $meta_value, $this->id ); } public function clear(): void { // remove current cart items foreach ( $this->get_items() as $cart_item ) { $cart_item->delete(); } unset( $this->items ); // important to unset, to avoid db queries } /** ? * * @return OsCartItemModel[] */ public function get_items(): array { // only call DB when needed if ( ! isset( $this->items ) && ! empty( $this->id ) ) { $this->items = OsCartsHelper::get_items_for_cart_id( $this->id ); } if ( empty( $this->items ) ) { $this->items = []; } return $this->items; } /** * @param array $rows_to_hide * * @return array[] */ public function generate_price_breakdown_rows( array $rows_to_hide = [] ): array { $rows = [ 'before_subtotal' => [], 'subtotal' => [], 'after_subtotal' => [], 'total' => [], 'balance' => [] ]; $items = $this->get_items(); // payments and balance have to always be recalculated, even if requested for existing booking if ( ! in_array( 'balance', $rows_to_hide ) ) { $balance_due_amount = $this->total; $rows['balance'] = [ 'label' => __( 'Balance Due', 'latepoint' ), 'raw_value' => OsMoneyHelper::pad_to_db_format( $balance_due_amount ), 'value' => OsMoneyHelper::format_price( $balance_due_amount, true, false ), 'style' => 'total' ]; } foreach ( $items as $item ) { switch ( $item->variant ) { case LATEPOINT_ITEM_VARIANT_BOOKING: $booking = $item->build_original_object_from_item_data(); // recalculations are below this point $service_row = [ 'heading' => __( 'Service', 'latepoint' ), 'items' => [] ]; $item_subtotal = OsBookingHelper::calculate_full_amount_for_service( $booking ); $service_row_item = [ 'label' => $booking->service->name, 'raw_value' => OsMoneyHelper::pad_to_db_format( $item_subtotal ), 'value' => OsMoneyHelper::format_price( $item_subtotal, true, false ) ]; $service_row['items'][] = $service_row_item; $service_row = apply_filters( 'latepoint_price_breakdown_service_row_for_booking', $service_row, $booking ); $rows['before_subtotal'][] = $service_row; break; case LATEPOINT_ITEM_VARIANT_BUNDLE: // TODO Merge somehow this case with the booking case as they are reusing a lot of code // recalculations are below this point $bundle = $item->build_original_object_from_item_data(); $service_row = [ 'heading' => __( 'Bundle', 'latepoint' ), 'items' => [] ]; $item_subtotal = OsBundlesHelper::calculate_full_amount_for_bundle( $bundle ); $service_row_item = [ 'label' => $bundle->name, 'raw_value' => OsMoneyHelper::pad_to_db_format( $item_subtotal ), 'value' => OsMoneyHelper::format_price( $item_subtotal, true, false ) ]; $service_row['items'][] = $service_row_item; $service_row = apply_filters( 'latepoint_price_breakdown_service_row_for_bundle', $service_row, $bundle ); $rows['before_subtotal'][] = $service_row; break; } } if ( ! in_array( 'subtotal', $rows_to_hide ) ) { $subtotal_amount = $this->subtotal; $rows['subtotal'] = [ 'label' => __( 'Sub Total', 'latepoint' ), 'style' => 'strong', 'raw_value' => OsMoneyHelper::pad_to_db_format( $subtotal_amount ), 'value' => OsMoneyHelper::format_price( $subtotal_amount, true, false ) ]; } if ( ! in_array( 'total', $rows_to_hide ) ) { $total_amount = $this->total; $rows['total'] = [ 'label' => __( 'Total Price', 'latepoint' ), 'style' => in_array( 'balance', $rows_to_hide ) ? 'total' : 'strong', 'raw_value' => OsMoneyHelper::pad_to_db_format( $total_amount ), 'value' => OsMoneyHelper::format_price( $total_amount, true, false ) ]; } // filter only applies when recalculating rows, do not apply it to the existing data, since it has already ran return apply_filters( 'latepoint_cart_price_breakdown_rows', $rows, $this, $rows_to_hide ); } /** * @param array $options * * @return mixed|void * * Returns amount to charge depending on a portion set in database format 1999.0000 * */ public function amount_to_charge( array $options = [] ) { $amount = ( $this->payment_portion == LATEPOINT_PAYMENT_PORTION_DEPOSIT ) ? $this->deposit_amount_to_charge( $options ) : $this->full_amount_to_charge( $options ); return apply_filters( 'latepoint_cart_amount_to_charge', $amount, $this, $options ); } /** * @param array $options * * @return mixed|void * * Returns deposit amount to charge in database format 1999.0000 * */ public function deposit_amount_to_charge( array $options = [] ) { $default_options = [ 'apply_coupons' => false, 'apply_taxes' => false ]; $options = array_merge( $default_options, $options ); $amount = 0; $items = $this->get_items(); if ( empty( $items ) ) { return $amount; } foreach ( $items as $item ) { $amount += $item->deposit_amount_to_charge( $options ); } /** * Filter deposit amount to charge on the cart object * * @param {float} $amount The amount to charge on the cart * @param {OsCartModel} $cart Cart object that deposit amount is calculated on * @param {array} $options Array of options that determine if taxes and coupons should be applied * @returns {float} The filtered amount to charge on the cart * * @since 5.0.0 * @hook latepoint_cart_deposit_amount_to_charge * */ return apply_filters( 'latepoint_cart_deposit_amount_to_charge', $amount, $this, $options ); } public function deposit_amount_to_charge_formatted( array $options = [] ) { $amount = $this->deposit_amount_to_charge( $options ); return OsMoneyHelper::format_price( $amount, true, false ); } /** * @param array $options * * @return mixed|void * * Returns full amount to charge in database format 1999.0000 * */ public function full_amount_to_charge( array $options = [] ) { /** * Get full amount to charge * * @param {float} $total Full amount to charge database format 1999.0000 * @param {OsCartModel} $cart Cart that total is assessed on * @returns {float} The filtered full amount to charge * * @since 5.0.0 * @hook latepoint_cart_full_amount_to_charge * */ $amount = apply_filters( 'latepoint_cart_full_amount_to_charge', $this->get_total(), $this, $options ); return OsMoneyHelper::pad_to_db_format( $amount ); } public function specs_calculate_amount_to_charge() { if ( $this->payment_portion == LATEPOINT_PAYMENT_PORTION_DEPOSIT ) { return $this->specs_calculate_deposit_amount_to_charge(); } else { return $this->specs_calculate_full_amount_to_charge(); } } public function specs_calculate_full_amount_to_charge() { return OsPaymentsHelper::convert_charge_amount_to_requirements( $this->get_total(), $this ); } public function specs_calculate_deposit_amount_to_charge() { return OsPaymentsHelper::convert_charge_amount_to_requirements( $this->deposit_amount_to_charge(), $this ); } public function get_total_formatted() { return OsMoneyHelper::format_price( $this->get_total(), true, false ); } public function set_payment_portion() { if ( ! empty( $this->payment_time ) ) { if ( $this->payment_time == LATEPOINT_PAYMENT_TIME_LATER ) { $this->payment_portion = LATEPOINT_PAYMENT_PORTION_FULL; } else { $deposit_amount = $this->deposit_amount_to_charge(); $this->payment_portion = ( $deposit_amount > 0 ) ? LATEPOINT_PAYMENT_PORTION_DEPOSIT : LATEPOINT_PAYMENT_PORTION_FULL; } } } public function set_payment_processor() { if ( empty( $this->payment_processor ) && ! empty( $this->payment_time ) && ! empty( $this->payment_method ) ) { $enabled_processors = OsPaymentsHelper::get_enabled_payment_processors_for_payment_time_and_method( $this->payment_time, $this->payment_method ); if ( count( $enabled_processors ) == 1 ) { $this->payment_processor = array_key_first( $enabled_processors ); } } } public function set_payment_time() { if ( empty( $this->payment_time ) ) { $enabled_payment_times = OsPaymentsHelper::get_enabled_payment_times(); if ( count( $enabled_payment_times ) == 1 ) { $this->payment_time = array_key_first( $enabled_payment_times ); } } } public function set_payment_method() { if ( ! empty( $this->payment_time ) ) { $enabled_payment_methods = OsPaymentsHelper::get_enabled_payment_methods_for_payment_time( $this->payment_time ); if ( count( $enabled_payment_methods ) == 1 ) { $this->payment_method = array_key_first( $enabled_payment_methods ); } } } public function set_singular_payment_attributes() { $this->set_payment_time(); $this->set_payment_portion(); $this->set_payment_method(); $this->set_payment_processor(); } public function remove_item( OsCartItemModel $item, bool $remove_connected_items = true ) { if ( $item->id && $this->id == $item->cart_id ) { if($remove_connected_items){ if(!empty($item->connected_cart_item_id)){ $cart_items = new OsCartItemModel(); $cart_items->delete_where(['id' => $item->connected_cart_item_id]); } // search for connected cart items $cart_items = new OsCartItemModel(); $cart_items->delete_where(['connected_cart_item_id' => $item->id]); } $item->delete(); $this->items = OsCartsHelper::get_items_for_cart_id( $this->id ); } $this->calculate_prices(); return true; } public function add_item( OsCartItemModel $item, bool $permanent = true, bool $calculate_prices = true ) { if ( $permanent ) { // save cart itself if not saved yet, since it's a permanent addition to cart if ( empty( $this->id ) ) { $this->save(); } $item->cart_id = $this->id; if ( $item->save() ) { // we are doing this - to modify a copy of $items, to avoid modifying the getter's return value $items = $this->get_items(); $items[] = $item; $this->items = $items; } } else { // we are doing this - to modify a copy of $items, to avoid modifying the getter's return value $items = $this->get_items(); $items[] = $item; $this->items = $items; } if ( $calculate_prices ) { $this->calculate_prices(); } return true; } public function calculate_prices() { // calculate subtotal for all items foreach ( $this->get_items() as $item ) { $item->subtotal = $item->full_amount_to_charge(); $item->total = $item->subtotal; } // do cart subtotal $this->subtotal = 0; foreach ( $this->get_items() as $item ) { $this->subtotal = $this->subtotal += $item->subtotal; } // do cart total $this->total = 0; foreach ( $this->get_items() as $item ) { $this->total = $this->total += $item->total; } /** * Triggers when cart prices are being calculated * * @param {OsCartModel} $cart Cart model for which prices are being generated * * @since 5.0.0 * @hook latepoint_cart_calculate_prices * */ do_action( 'latepoint_cart_calculate_prices', $this ); } protected function allowed_params( $role = 'admin' ) { $allowed_params = array( 'payment_method', 'payment_portion', 'payment_processor', 'payment_time', 'coupon_code', 'payment_token', 'source_id' ); return $allowed_params; } protected function params_to_save( $role = 'admin' ) { $params_to_save = array( 'id', 'uuid', 'order_intent_id', 'order_id', 'coupon_code', 'updated_at', 'created_at' ); return $params_to_save; } }