[Back]
<?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;
	}
}