<?php /* * Copyright (c) 2023 LatePoint LLC. All rights reserved. */ class OsOrderModel extends OsModel { public $items; public $id, $subtotal = 0, $total = 0, $confirmation_code, $status, $fulfillment_status, $payment_status, $source_id, $source_url, $customer_id, $customer_comment, $price_breakdown, $initial_payment_data = '', $initial_payment_data_arr, $coupon_code, $coupon_discount = 0, $ip_address, $tax_total = 0, $keys_to_manage = [], $created_at, $updated_at; function __construct( $id = false ) { parent::__construct(); $this->table_name = LATEPOINT_TABLE_ORDERS; if ( $id ) { $this->load_by_id( $id ); } } public function should_not_be_cancelled() { return $this->where( [ $this->table_name . '.status !=' => LATEPOINT_ORDER_STATUS_CANCELLED ] ); } public function get_initial_payment_data_value( string $key ): string { if ( ! isset( $this->initial_payment_data_arr ) ) { $this->initial_payment_data_arr = json_decode( $this->initial_payment_data, true ); } return $this->initial_payment_data_arr[ $key ] ?? ''; } public function get_nice_status_name(): string { return OsOrdersHelper::get_nice_order_status_name( $this->status ); } public function get_nice_payment_status_name(): string { return OsOrdersHelper::get_nice_order_payment_status_name( $this->payment_status ); } public function get_nice_fulfillment_status_name(): string { return OsOrdersHelper::get_nice_order_fulfillment_status_name( $this->fulfillment_status ); } /** * @param array $statuses * * @return OsInvoiceModel|OsInvoiceModel[] */ public function get_invoices( array $statuses = [] ) { $invoices = new OsInvoiceModel(); if ( ! empty( $statuses ) ) { $invoices = $invoices->where_in( 'status', $statuses ); } return $invoices->where( [ 'order_id' => $this->id ] )->get_results_as_models(); } public function manage_by_key_url( string $for = 'customer' ): string { return OsOrdersHelper::generate_direct_manage_order_url( $this, $for ); } public function set_initial_payment_data_value( string $key, string $value, bool $save = true ) { $this->initial_payment_data_arr = json_decode( $this->initial_payment_data, true ); $this->initial_payment_data_arr[ $key ] = $value; $this->initial_payment_data = wp_json_encode( $this->initial_payment_data_arr ); if ( $save && !$this->is_new_record() ) { $this->update_attributes( [ 'initial_payment_data' => $this->initial_payment_data ] ); } } public function is_single_booking(): bool { $order_items = $this->get_items(); return ( count( $order_items ) == 1 && $order_items[0]->is_booking() ); } public function filter_allowed_records(): OsModel { if ( ! OsRolesHelper::are_all_records_allowed() ) { $this->select( LATEPOINT_TABLE_ORDERS . '.*' )->join( LATEPOINT_TABLE_ORDER_ITEMS, [ 'order_id' => LATEPOINT_TABLE_ORDERS . '.id' ] )->join( LATEPOINT_TABLE_BOOKINGS, [ 'order_item_id' => LATEPOINT_TABLE_ORDER_ITEMS . '.id' ] )->group_by( LATEPOINT_TABLE_ORDERS . '.id' ); if ( ! OsRolesHelper::are_all_records_allowed( 'agent' ) ) { $this->filter_where_conditions( [ LATEPOINT_TABLE_BOOKINGS . '.agent_id' => OsRolesHelper::get_allowed_records( 'agent' ) ] ); } if ( ! OsRolesHelper::are_all_records_allowed( 'location' ) ) { $this->filter_where_conditions( [ LATEPOINT_TABLE_BOOKINGS . '.location_id' => OsRolesHelper::get_allowed_records( 'location' ) ] ); } if ( ! OsRolesHelper::are_all_records_allowed( 'service' ) ) { $this->filter_where_conditions( [ LATEPOINT_TABLE_BOOKINGS . '.service_id' => OsRolesHelper::get_allowed_records( 'service' ) ] ); } } return $this; } /** * @return OsBundleModel[] */ public function get_bundles_from_order_items(bool $force_db_call = false): array { $order_bundles = []; foreach ( $this->get_items($force_db_call) as $order_item ) { if ( $order_item->is_bundle() ) { $order_bundles[ $order_item->id ] = $order_item->build_original_object_from_item_data(); } } return $order_bundles; } /** * @return OsBookingModel[] */ public function get_bookings_from_order_items(bool $force_db_call = false): array { $order_bookings = []; foreach ( $this->get_items($force_db_call) as $order_item ) { if ( $order_item->is_booking() ) { $item_data = json_decode( $order_item->item_data, true ); if ( ! empty( $item_data['id'] ) ) { // existing booking $order_bookings[ $order_item->id ] = $order_item->retrieve_original_object(); } else { $order_bookings[ $order_item->get_form_id() ] = $order_item->build_original_object_from_item_data(); } } } return $order_bookings; } public function get_total_amount_paid_from_transactions($in_database_format = false) { if ( $this->is_new_record() ) { return 0; } $transactions_model = new OsTransactionModel(); $transactions = $transactions_model->select( 'amount' )->where( [ 'order_id' => $this->id ] )->get_results(); $total = 0; foreach ( $transactions as $transaction ) { $total += (float) $transaction->amount; } if($in_database_format){ $total = OsMoneyHelper::pad_to_db_format( $total ); } return $total; } public function delete( $id = false ) { if ( ! $id && isset( $this->id ) ) { $id = $this->id; } $transactions = new OsTransactionModel(); $transactions->delete_where( [ 'order_id' => $id ] ); $order_items = new OsOrderItemModel(); $order_items = $order_items->where( [ 'order_id' => $id ] )->get_results_as_models(); if ( ! empty( $order_items ) ) { foreach ( $order_items as $order_item ) { $bookings = new OsBookingModel(); $bookings_to_delete = $bookings->where( [ 'order_item_id' => $order_item->id ] )->get_results_as_models(); if ( $bookings_to_delete ) { foreach ( $bookings_to_delete as $booking_to_delete ) { $booking_id_to_delete = $booking_to_delete->id; /** * Fires right before a booking is about to be deleted * * @param {integer} $booking_id ID of the booking that will be deleted * * @since 5.0.0 * @hook latepoint_booking_will_be_deleted * */ do_action( 'latepoint_booking_will_be_deleted', $booking_id_to_delete ); $booking_to_delete->delete(); } } } } $order_items = new OsOrderItemModel(); $order_items->delete_where( [ 'order_id' => $id ] ); $order_metas = new OsOrderMetaModel(); $order_metas->delete_where( [ 'object_id' => $id ] ); $process_jobs = new OsProcessJobModel(); $process_jobs->delete_where( [ 'object_id' => $id, 'object_model_type' => 'order' ] ); return parent::delete( $id ); } public function delete_meta_by_key( $meta_key ) { if ( $this->is_new_record() ) { return false; } $meta = new OsOrderMetaModel(); 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 OsOrderMetaModel(); 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 OsOrderMetaModel(); return $meta->save_by_key( $meta_key, $meta_value, $this->id ); } public function determine_payment_status() { if ( $this->total > 0 ) { $total_paid = $this->get_total_amount_paid_from_transactions(); if ( empty( $total_paid ) ) { $this->update_attributes( [ 'payment_status' => LATEPOINT_ORDER_PAYMENT_STATUS_NOT_PAID ] ); } else if ( $total_paid < $this->total ) { $this->update_attributes( [ 'payment_status' => LATEPOINT_ORDER_PAYMENT_STATUS_PARTIALLY_PAID ] ); } else { $this->update_attributes( [ 'payment_status' => LATEPOINT_ORDER_PAYMENT_STATUS_FULLY_PAID ] ); } } } public function get_default_order_status(): string { return OsOrdersHelper::get_default_order_status(); } public function get_default_payment_status(): string { return LATEPOINT_ORDER_PAYMENT_STATUS_NOT_PAID; } public function get_default_payment_time(): string { return LATEPOINT_PAYMENT_TIME_LATER; } public function get_default_fulfillment_status(): string { return LATEPOINT_ORDER_FULFILLMENT_STATUS_NOT_FULFILLED; } protected function before_save() { // TODO check for uniqueness if ( empty( $this->confirmation_code ) ) { $this->confirmation_code = strtoupper( OsUtilHelper::random_text( 'distinct', 7 ) ); } if ( empty( $this->payment_status ) ) { $this->payment_status = $this->get_default_payment_status(); } if ( empty( $this->payment_time ) ) { $this->payment_time = $this->get_default_payment_time(); } if ( empty( $this->fulfillment_status ) ) { $this->fulfillment_status = $this->get_default_fulfillment_status(); } if ( empty( $this->source_url ) ) { $this->source_url = OsUtilHelper::get_referrer(); } if ( empty( $this->status ) ) { $this->status = $this->get_default_order_status(); } if ( empty( $this->ip_address ) && OsSettingsHelper::is_on('log_customer_ip_address') ) { $this->ip_address = OsUtilHelper::get_user_ip(); } } public function get_total_balance_due( bool $recalculate_total = false ) { $total = $this->get_total( $recalculate_total ); $payments = $this->get_total_amount_paid_from_transactions(); return $total - $payments; } public function get_total( bool $recalculate = false, array $options = [ 'apply_taxes' => true, 'apply_coupons' => true ] ) { if ( $recalculate ) { $this->total = $this->recalculate_total(); } return $this->total; } public function get_subtotal( bool $recalculate = false ) { if ( $recalculate ) { $this->subtotal = $this->recalculate_subtotal(); } return $this->subtotal; } public function get_deposit_amount_to_charge( array $options = [] ) { $cart = $this->view_as_cart(); return $cart->deposit_amount_to_charge( $options ); } public function recalculate_total() { $cart = $this->view_as_cart(); $cart->calculate_prices(); return $cart->get_total(); } public function recalculate_subtotal() { $cart = $this->view_as_cart(); $cart->calculate_prices(); return $cart->get_subtotal(); } public function view_as_cart(): OsCartModel { $cart = new OsCartModel(); $cart->coupon_code = $this->coupon_code ?? ''; if ( ! empty( $this->id ) ) { $cart->order_id = $this->id; } $cart->order_forced_customer_id = empty( $this->customer_id ) ? 'new' : $this->customer_id; $items = $this->get_items(); foreach ( $items as $item ) { $cart->add_item( $item->view_as_cart_item(), false ); } return $cart; } public function generate_first_level_data_vars(): array { $vars = [ 'id' => $this->id, 'confirmation_code' => $this->confirmation_code, 'customer_comment' => $this->customer_comment, 'status' => $this->status, 'fulfillment_status' => $this->fulfillment_status, 'payment_status' => $this->payment_status, 'source_id' => $this->source_id, 'source_url' => $this->source_url, 'total' => OsMoneyHelper::format_price( $this->get_total() ), 'subtotal' => OsMoneyHelper::format_price( $this->get_subtotal() ), 'created_datetime' => $this->format_created_datetime_rfc3339(), ]; return $vars; } public function properties_to_query(): array { return [ 'status' => __( 'Order Status', 'latepoint' ), 'fulfillment_status' => __( 'Fulfillment Status', 'latepoint' ), 'payment_status' => __( 'Payment Status', 'latepoint' ), ]; } public function generate_data_vars(): array { $vars = $this->get_first_level_data_vars(); $vars['customer'] = $this->customer->get_data_vars(); $vars['transactions'] = []; $transactions = $this->get_transactions(); if ( $transactions ) { foreach ( $transactions as $transaction ) { $vars['transactions'][] = $transaction->get_data_vars(); } } $order_items = $this->get_items(); if ( $order_items ) { foreach ( $order_items as $order_item ) { $vars['order_items'][] = $order_item->get_data_vars(); } } return $vars; } public function get_key_to_manage_for(string $for): string { if($this->is_new_record()) return ''; if(!empty($this->keys_to_manage[$for])) return $this->keys_to_manage[$for]; $key = OsMetaHelper::get_order_meta_by_key( 'key_to_manage_for_' . $for, $this->id ); if ( empty( $key ) ) { $key = OsUtilHelper::generate_key_to_manage(); OsMetaHelper::save_order_meta_by_key( 'key_to_manage_for_' . $for, $key, $this->id ); } $this->keys_to_manage[$for] = $key; return $key; } protected function params_to_sanitize() { return [ 'subtotal' => 'money', 'total' => 'money', 'coupon_discount' => 'money', 'tax_total' => 'money', ]; } public function get_transactions(): array { $transactions_model = new OsTransactionModel(); $transactions = $transactions_model->where( [ 'order_id' => $this->id ] )->get_results_as_models(); return $transactions; } /** * @param bool $force_recalculate * @param array $rows_to_hide * * @return array[] */ public function generate_price_breakdown_rows( array $rows_to_hide = [], bool $force_recalculate = false ): array { $rows = [ 'before_subtotal' => [], 'subtotal' => [], 'after_subtotal' => [], 'total' => [], 'payments' => [], 'balance' => [] ]; $existing_rows = []; // try to get existing price breakdown from order record if ( ! $force_recalculate && ! $this->is_new_record() ) { $existing_price_breakdown = empty( $this->price_breakdown ) ? '' : $this->price_breakdown; $existing_rows = json_decode( $existing_price_breakdown, true ); } if ( ! empty( $existing_rows ) ) { // merge existing rows with balance and payments $rows = array_merge( $rows, $existing_rows ); } else { // recalculate because there is nothing existing or because it's forced $cart = $this->view_as_cart(); $recalculated_rows = $cart->generate_price_breakdown_rows( $rows_to_hide ); $rows = array_merge( $rows, $recalculated_rows ); } // payments and balance have to always be recalculated, even if requested for existing booking if ( ! in_array( 'payments', $rows_to_hide ) ) { $total_payments_amount = $this->get_total_amount_paid_from_transactions(); $rows['payments'] = [ [ 'label' => __( 'Payments and Credits', 'latepoint' ), 'raw_value' => OsMoneyHelper::pad_to_db_format( $total_payments_amount ), 'value' => ( ( $total_payments_amount > 0 ) ? '-' : '' ) . OsMoneyHelper::format_price( $total_payments_amount, true, false ), 'type' => ( $total_payments_amount > 0 ) ? 'credit' : '' ] ]; } if ( ! in_array( 'balance', $rows_to_hide ) ) { $balance_due_amount = $this->get_total_balance_due( $this->is_new_record() ); $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' ]; } /** * Filters rows for price breakdown of the order * * @param {array} $rows Price breakdown rows to be filtered * @param {OsOrderModel} $order Order model for which price breakdown rows are requested * @param {array} $rows_to_hide Rows to hide on the breakdown * @param {bool} $force_recalculate tells whether price rows should be recalculated * * @returns {array} Filtered array of price breakdown rows * @since 5.0.0 * @hook latepoint_order_price_breakdown_rows * */ return apply_filters( 'latepoint_order_price_breakdown_rows', $rows, $this, $rows_to_hide, $force_recalculate ); } public function get_customer() : OsCustomerModel { if ( $this->customer_id ) { if ( ! isset( $this->customer ) || ( isset( $this->customer ) && ( $this->customer->id != $this->customer_id ) ) ) { $this->customer = new OsCustomerModel( $this->customer_id ); } } else { $this->customer = new OsCustomerModel(); } return $this->customer; } /** * @return OsOrderItemModel[] */ public function get_items( bool $pull_from_db = false ): array { // only call DB when needed if ( ( ! isset( $this->items ) || ( $pull_from_db ) && ! empty( $this->id ) ) ) { $this->items = OsOrdersHelper::get_items_for_order_id( $this->id ); } if ( empty( $this->items ) ) { $this->items = []; } return $this->items; } public function get_print_link( $key = false ) { return ( $key ) ? OsRouterHelper::build_admin_post_link( [ 'manage_order_by_key', 'print' ], [ 'key' => $key ] ) : OsRouterHelper::build_admin_post_link( [ 'customer_cabinet', 'print_order_info' ], [ 'latepoint_order_id' => $this->id ] ); } protected function allowed_params( $role = 'admin' ) { $allowed_params = array( 'id', 'subtotal', 'total', 'status', 'fulfillment_status', 'payment_status', 'source_id', 'confirmation_code', 'source_url', 'initial_payment_data', 'customer_id', 'customer_comment', 'coupon_code', 'coupon_discount', 'tax_total', 'ip_address', 'updated_at', 'created_at' ); return $allowed_params; } protected function params_to_save( $role = 'admin' ) { $params_to_save = array( 'id', 'subtotal', 'total', 'status', 'fulfillment_status', 'payment_status', 'source_id', 'confirmation_code', 'source_url', 'initial_payment_data', 'customer_id', 'customer_comment', 'price_breakdown', 'coupon_code', 'coupon_discount', 'tax_total', 'ip_address', 'updated_at', 'created_at' ); return $params_to_save; } protected function properties_to_validate() { $validations = array( 'customer_id' => array( 'presence' ), 'status' => array( 'presence' ), ); return $validations; } }