[Back]
<?php
/*
 * Copyright (c) 2024 LatePoint LLC. All rights reserved.
 */

class OsOrderIntentModel extends OsModel {
	var $id,
		$intent_key,
		$customer_id,
		$booking_form_page_url,
		$cart_items_data,
		$restrictions_data,
		$presets_data,
		$payment_data = '',
		$payment_data_arr,
		$other_data,
		$charge_amount,
		$specs_charge_amount,
		$coupon_code,
		$coupon_discount,
		$total,
		$subtotal,
		$price_breakdown,
		$order_id,
		$tax_total,
		$status,
		$updated_at,
		$created_at;

	function __construct( $id = false ) {
		parent::__construct();
		$this->table_name = LATEPOINT_TABLE_ORDER_INTENTS;

		if ( $id ) {
			$this->load_by_id( $id );
		}
	}


	protected function params_to_sanitize() {
		return [
			'charge_amount'        => 'money',
			'total'        => 'money',
			'subtotal'        => 'money',
			'tax_total'        => 'money',
		];
	}


	public function delete_meta_by_key( $meta_key ) {
		if ( $this->is_new_record() ) {
			return false;
		}

		$meta = new OsOrderIntentMetaModel();

		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 OsOrderIntentMetaModel();

		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 OsOrderIntentMetaModel();

		return $meta->save_by_key( $meta_key, $meta_value, $this->id );
	}

	public function get_customer() {
		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;
	}

	public function build_cart_object(): OsCartModel {
		$cart                  = new OsCartModel();
		$cart->total           = $this->total;
		$cart->subtotal        = $this->subtotal;
		$cart->coupon_code     = $this->coupon_code;
		$cart->coupon_discount = $this->coupon_discount;
		$cart->tax_total       = $this->tax_total;

		// add items from intent
		$intent_cart_items = json_decode( $this->cart_items_data, true );
		foreach ( $intent_cart_items as $cart_item_data ) {
			$cart->add_item( OsCartsHelper::create_cart_item_from_item_data( $cart_item_data ), false );
		}

		// restore payment info
		$payment_data            = json_decode( $this->payment_data, true );
		$cart->payment_method    = $payment_data['method'];
		$cart->payment_time      = $payment_data['time'];
		$cart->payment_portion   = $payment_data['portion'];
		$cart->payment_token     = $payment_data['token'];
		$cart->payment_processor = $payment_data['processor'];

		return $cart;
	}

	public function get_payment_data_value( string $key ): string {
		if ( ! isset( $this->payment_data_arr ) ) {
			$this->payment_data_arr = json_decode( $this->payment_data, true );
		}

		return $this->payment_data_arr[ $key ] ?? '';
	}

	public function set_payment_data_value( string $key, string $value, bool $save = true ) {
		$this->payment_data_arr         = json_decode( $this->payment_data, true );
		$this->payment_data_arr[ $key ] = $value;
		$this->payment_data             = wp_json_encode( $this->payment_data_arr );
		if ( $save ) {
			$this->update_attributes( [ 'payment_data' => $this->payment_data ] );
		}
	}

	public function is_bookable( array $settings = []): bool {
		$cart = $this->build_cart_object();
		// loop items and check if bookings are still available
		foreach ( $cart->get_items() as $cart_item ) {
			switch ( $cart_item->variant ) {
				case LATEPOINT_ITEM_VARIANT_BOOKING:
					$booking = $cart_item->build_original_object_from_item_data();
					if ( ! $booking->is_bookable($settings) ) {
						$this->add_error( 'send_to_step', $booking->get_error_messages(), 'booking__datepicker' );

						return false;
					}
					break;
				case LATEPOINT_ITEM_VARIANT_BUNDLE:
					break;
			}
		}

		return true;
	}

	public function is_processing(): bool {
		return $this->status == LATEPOINT_ORDER_INTENT_STATUS_PROCESSING;
	}

	public function is_failed(): bool {
		return $this->status == LATEPOINT_ORDER_INTENT_STATUS_FAILED;
	}


	public function mark_as_failed() {
		$this->update_attributes( [ 'status' => LATEPOINT_ORDER_INTENT_STATUS_FAILED ] );
		/**
		 * Order intent is marked as failed
		 *
		 * @param {OsOrderIntentModel} $order_intent Instance of order intent model that has failed
		 *
		 * @since 5.2.0
		 * @hook latepoint_order_intent_failed
		 *
		 */
		do_action( 'latepoint_order_intent_failed', $this );
	}

	public function wait_for_transaction_completion() : OsOrderIntentModel {
		$attempts = 0;
		$max_attempts = 6;
		$delay_seconds = 2;

		while ($attempts < $max_attempts) {
			if (!$this->is_processing()) {
				return $this;
			}
			sleep($delay_seconds);
			$attempts++;
			$this->load_by_id($this->id);
		}
		if($this->is_processing()){
			// if it's still processing after waiting - mark as failed
			$this->mark_as_failed();
		}
		return $this;
	}

	public function convert_to_order() {
		if($this->is_converted()){
			return $this->order_id;
		}

		if($this->is_processing()){

			$this->wait_for_transaction_completion();
			if($this->is_failed()){
				$this->add_error( 'transaction_intent_error', __('Can not convert to transaction, because transaction intent conversion is being processed', 'latepoint') );
				return false;
			}else if($this->is_converted()){
				return $this->order_id;
			}
		}

		$this->mark_as_processing();

		try {

			// process is cart -> order intent -> order
			if ( ! $this->is_bookable() ) {
				$this->mark_as_new();
				return false;
			}

			// process payment if there is amount due
			$transaction = OsPaymentsHelper::process_payment_for_order_intent( $this );

			// payment processing can take a while, make sure to check if the intent wasn't converted already in the meantime
			$converted_order_id = OsOrderIntentHelper::is_converted( $this->id );
			if ( $converted_order_id ) {
				$order = new OsOrderModel($converted_order_id);
				$this->mark_as_converted($order);
				return $converted_order_id;
			}

			if ( $this->get_error() ) {
				OsDebugHelper::log( 'Error converting intent to an order', 'order_error', $this->get_error_messages() );

				$this->mark_as_new();
				return false;
			}


			$cart_from_intent = $this->build_cart_object();

			$order                      = new OsOrderModel();
			$order->customer_id         = $this->customer_id;
			$order->total               = $this->total;
			$order->subtotal            = $this->subtotal;
			$order->coupon_code         = $this->coupon_code;
			$order->coupon_discount     = $this->coupon_discount;
			$order->tax_total           = $this->tax_total;
			$order->source_url          = $this->booking_form_page_url;
			$order->customer_comment    = $this->customer->notes;
			$order_initial_payment_data_arr = json_decode( $this->payment_data, true );
			$order_initial_payment_data_arr['charge_amount'] = $this->charge_amount;
			$order->initial_payment_data        = wp_json_encode($order_initial_payment_data_arr);
			// 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
			$order->price_breakdown = wp_json_encode( $cart_from_intent->generate_price_breakdown_rows(['balance', 'total', 'subtotal']));

			/**
			 * Filters order right before it's about to be saved when converting from an order intent
			 *
			 * @param {OsOrderModel} $order Order to be filtered
			 * @returns {OsOrderModel} The filtered order
			 *
			 * @since 5.0.0
			 * @hook latepoint_before_order_save_from_order_intent
			 *
			 */
			$order = apply_filters( 'latepoint_before_order_save_from_order_intent', $order );


			if ( $order->save() ) {
				$this->mark_as_converted( $order );
				OsInvoicesHelper::create_invoices_for_new_order($order);


				foreach ( $cart_from_intent->get_items() as $cart_item ) {
					$order_item           = OsOrdersHelper::create_order_item_from_cart_item( $cart_item );
					$order_item->order_id = $order->id;
					$order_item->save();
				}

				if ( $transaction ) {
					$transaction->order_id = $order->id;
					$invoice = OsInvoicesHelper::get_matching_invoice_for_transaction($transaction);
					if(!$invoice->is_new_record()) $transaction->invoice_id = $invoice->id;
					if ( $transaction->save() ) {

						/**
						 * Transaction was created
						 *
						 * @param {OsTransactionModel} $transaction instance of transaction model that was created
						 *
						 * @since 5.1.0
						 * @hook latepoint_transaction_created
						 *
						 */
						do_action( 'latepoint_transaction_created', $transaction );
						if(!$invoice->is_new_record()){
							$old_invoice = clone $invoice;
							$invoice->update_attributes(['status' => LATEPOINT_INVOICE_STATUS_PAID]);
							/**
							 * Invoice was updated
							 *
							 * @param {OsInvoiceModel} $invoice instance of invoice model after it was updated
							 * @param {OsInvoiceModel} $old_invoice instance of invoice model before it was updated
							 *
							 * @since 5.1.0
							 * @hook latepoint_invoice_updated
							 *
							 */
							do_action( 'latepoint_invoice_updated', $invoice, $old_invoice );
							// 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
							$other_invoices = new OsInvoiceModel();
							$other_invoices = $other_invoices->where(['status' => LATEPOINT_INVOICE_STATUS_DRAFT, 'order_id' => $order->id])->get_results_as_models();
							if($other_invoices){
								foreach($other_invoices as $invoice){
									$data = json_decode($invoice->data, true);
									$data['totals']['payments'] = $order->get_total_amount_paid_from_transactions(true);
									$invoice->update_attributes(['data' => json_encode($data)]);
								}
							}

						}

					} else {
						OsDebugHelper::log( 'Error creating transaction', 'transaction_error', $transaction->get_error_messages() );
					}
				}
				$order_bookings = $order->get_bookings_from_order_items(true);
				if ( $order_bookings ) {
					foreach ( $order_bookings as $order_item_id => $order_booking ) {
						$order_booking->order_item_id = $order_item_id;
						$order_booking->customer_id   = $order->customer_id;
						$order_booking->end_time      = $order_booking->calculate_end_time();
						$order_booking->end_date      = $order_booking->calculate_end_date();
						$order_booking->set_utc_datetimes();
						$service                         = new OsServiceModel( $order_booking->service_id );
						$order_booking->buffer_before    = $service->buffer_before;
						$order_booking->buffer_after     = $service->buffer_after;
						$order_booking->customer_comment = $order->customer->notes;
						if ( $order_booking->save() ) {

							/**
							 * Booking was created
							 *
							 * @param {OsBookingModel} $booking instance of booking model that was created
							 *
							 * @since 5.0.0
							 * @hook latepoint_booking_created
							 *
							 */
							do_action( 'latepoint_booking_created', $order_booking );
							// set booking id to the one that was created for item data property
							$order_item      = new OsOrderItemModel( $order_item_id );
							$item_data       = json_decode( $order_item->item_data, true );
							$item_data['id'] = $order_booking->id;
							$order_item->update_attributes( [ 'item_data' => wp_json_encode( $item_data ) ] );
						} else {
							OsDebugHelper::log( 'Unable to save booking', 'booking_save_error', $order_booking->get_error_messages() );
						}
					}
				}
				// update connected cart with created order id
				$this->mark_cart_converted();
				$order->determine_payment_status();
				// update with latest info
				$order->get_items(true);

				/**
				 * Order was created
				 *
				 * @param {OsOrderModel} $order instance of order model that was created
				 *
				 * @since 5.0.0
				 * @hook latepoint_order_created
				 *
				 */
				do_action( 'latepoint_order_created', $order );

				return $order->id;
			} else {
				$this->add_error( 'order_error', $order->get_error_messages() );

				$this->mark_as_new();
				return false;
			}
		} catch ( Exception $e ) {
			$this->mark_as_new();
			// translators: %s is the error description
			$this->add_error( 'order_error', sprintf(__('Error: %s', 'latepoint'), $e->getMessage() ));
			OsDebugHelper::log( 'Error converting intent to an order', 'order_error', $e->getMessage() );
			return false;
		}
	}

	public function get_by_intent_key( $intent_key ) {
		return $this->where( [ 'intent_key' => $intent_key ] )->set_limit( 1 )->get_results_as_models();
	}

	public function mark_as_converted( OsOrderModel $order ) {
		if ( empty( $order->id ) ) {
			return false;
		}

		$this->update_attributes( [ 'order_id' => $order->id, 'status' => LATEPOINT_ORDER_INTENT_STATUS_CONVERTED ] );
		/**
		 * Order intent is converted to order
		 *
		 * @param {OsOrderIntentModel} $order_intent Instance of order intent model that has been converted to order
		 * @param {OsOrderModel} $order Instance of order model that order intent was converted to
		 *
		 * @since 5.0.0
		 * @hook latepoint_order_intent_converted
		 *
		 */
		do_action( 'latepoint_order_intent_converted', $this, $order );
	}

	public function mark_as_processing() {
		$this->update_attributes( [ 'status' => LATEPOINT_ORDER_INTENT_STATUS_PROCESSING ] );
		/**
		 * Order intent is marked as processing
		 *
		 * @param {OsOrderIntentModel} $order_intent Instance of order intent model that has started processing
		 *
		 * @since 5.0.0
		 * @hook latepoint_order_intent_processing
		 *
		 */
		do_action( 'latepoint_order_intent_processing', $this );
	}

	public function mark_as_new() {
		$this->update_attributes( [ 'status' => LATEPOINT_ORDER_INTENT_STATUS_NEW ] );
		/**
		 * Order intent is marked as new
		 *
		 * @param {OsOrderIntentModel} $order_intent Instance of order intent model that is being marked as new
		 *
		 * @since 5.0.0
		 * @hook latepoint_order_intent_new
		 *
		 */
		do_action( 'latepoint_order_intent_new', $this );
	}

	// Determines if order intent has been converted into a order already
	public function is_converted() : bool {
		if ( empty( $this->order_id ) ) {
			return false;
		} else {
			return true;
		}
	}

	public function generate_data_vars(): array {
		$vars = [
			'id'                    => $this->id,
			'intent_key'            => $this->intent_key,
			'customer_id'           => $this->customer_id,
			'booking_form_page_url' => $this->booking_form_page_url,
			'cart_items_data'       => !empty($this->cart_items_data) ? json_decode( $this->cart_items_data, true ) : [],
			'restrictions_data'     => !empty($this->restrictions_data) ? json_decode( $this->restrictions_data, true ) : [],
			'presets_data'          => !empty($this->presets_data) ? json_decode( $this->presets_data, true ) : [],
			'payment_data'          => !empty($this->payment_data) ? json_decode( $this->payment_data, true ) : [],
			'other_data'            => !empty($this->other_data) ? json_decode( $this->other_data, true ) : [],
			'order_id'              => $this->order_id,
			'coupon_code'           => $this->coupon_code,
			'coupon_discount'       => $this->coupon_discount,
			'tax_total'             => $this->tax_total,
			'updated_at'            => $this->updated_at,
			'created_at'            => $this->created_at,
		];

		return $vars;
	}

	public function get_page_url_with_intent() {
		$booking_page_url      = $this->booking_form_page_url;
		$existing_var_position = strpos( $booking_page_url, 'latepoint_order_intent_key=' );
		if ( $existing_var_position === false ) {
			// no intent variable in url
			$question_position = strpos( $booking_page_url, '?' );
			if ( $question_position === false ) {
				// no ?query params
				$hash_position = strpos( $booking_page_url, '#' );
				if ( $hash_position === false ) {
					// no hashtag in url
					$booking_page_url = $booking_page_url . '?latepoint_order_intent_key=' . $this->intent_key;
				} else {
					// hashtag in url and no ?query, prepend the hashtag with query
					$booking_page_url = substr_replace( $booking_page_url, '?latepoint_order_intent_key=' . $this->intent_key . '#', $hash_position, 1 );
				}
			} else {
				// ?query string exists, add intent key to it
				$booking_page_url = substr_replace( $booking_page_url, '?latepoint_order_intent_key=' . $this->intent_key . '&', $question_position, 1 );
			}
		} else {
			// intent key variable exist in url
			preg_match( '/latepoint_order_intent_key=([\d,\w]*)/', $booking_page_url, $matches );
			if ( isset( $matches[1] ) ) {
				$booking_page_url = str_replace( 'latepoint_order_intent_key=' . $matches[1], 'latepoint_order_intent_key=' . $this->intent_key, $booking_page_url );
			}
		}

		return $booking_page_url;
	}


	protected function before_create() {
		if ( empty( $this->intent_key ) ) {
			$this->intent_key = bin2hex( openssl_random_pseudo_bytes( 10 ) );
		}
		if ( empty( $this->status ) ) {
			$this->status = LATEPOINT_ORDER_INTENT_STATUS_NEW;
		}
	}

	protected function allowed_params( $role = 'admin' ) {
		$allowed_params = array(
			'customer_id',
			'cart_items_data',
			'restrictions_data',
			'presets_data',
			'payment_data',
			'other_data',
			'booking_form_page_url',
			'intent_key',
			'order_id',
			'coupon_code',
			'coupon_discount',
			'tax_total',
			'status',
		);

		return $allowed_params;
	}


	protected function params_to_save( $role = 'admin' ) {
		$params_to_save = array(
			'customer_id',
			'cart_items_data',
			'restrictions_data',
			'presets_data',
			'payment_data',
			'other_data',
			'booking_form_page_url',
			'intent_key',
			'total',
			'subtotal',
			'charge_amount',
			'specs_charge_amount',
			'price_breakdown',
			'order_id',
			'coupon_code',
			'coupon_discount',
			'tax_total',
			'status',
		);

		return $params_to_save;
	}


	protected function properties_to_validate() {
		$validations = array(
			'customer_id' => array( 'presence' ),
		);

		return $validations;
	}

	public function mark_cart_converted(?OsCartModel $cart = null) : bool {
		if($this->is_new_record() || empty($this->order_id)){
			return false;
		}
		if(!empty($cart)){
			$cart->order_id = $this->order_id;
			$cart->save();
		}else{
			$carts = new OsCartModel();
			$carts = $carts->where( [ 'order_intent_id' => $this->id ] )->get_results_as_models();
			if ( ! empty( $carts ) ) {
				foreach ( $carts as $cart ) {
					$cart->order_id = $this->order_id;
					$cart->save();
				}
			}
		}
		return true;
	}
}