<?php /* * Copyright (c) 2024 LatePoint LLC. All rights reserved. */ class OsTransactionIntentModel extends OsModel { var $id, $intent_key, $order_id, $customer_id, $invoice_id, $transaction_id, $payment_data = '', $payment_data_arr, $other_data, $charge_amount, $specs_charge_amount, $status, $order_form_page_url, $updated_at, $created_at; function __construct( $id = false ) { parent::__construct(); $this->table_name = LATEPOINT_TABLE_TRANSACTION_INTENTS; if ( $id ) { $this->load_by_id( $id ); } } public function calculate_specs_charge_amount() { /** * Convert transaction intent charge amount to specs * * @param {string} $charge_amount original charge amount of a transaction intent * @param {OsTransactionIntentModel} $transaction_intent transaction intent model * * @returns {string} The filtered to specs charge amount * * @since 5.1.3 * @hook latepoint_transaction_intent_specs_charge_amount * */ $this->specs_charge_amount = apply_filters( 'latepoint_transaction_intent_specs_charge_amount', $this->charge_amount, $this ); } protected function params_to_sanitize() { return [ 'charge_amount' => 'money', ]; } 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_processing(): bool { return $this->status == LATEPOINT_TRANSACTION_INTENT_STATUS_PROCESSING; } public function is_failed(): bool { return $this->status == LATEPOINT_TRANSACTION_INTENT_STATUS_FAILED; } public function wait_for_transaction_completion() : OsTransactionIntentModel { $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_transaction() { 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; } } if($this->is_converted()){ return $this->transaction_id; } $this->mark_as_processing(); try { // process payment if there is amount due $transaction = OsPaymentsHelper::process_payment_for_transaction_intent( $this ); if(!$transaction || $transaction->status != LATEPOINT_TRANSACTION_STATUS_SUCCEEDED){ if(!$transaction){ $this->add_error('transaction_intent_error', __('No payment processor available to process this transaction intent', 'latepoint')); }else{ if ( $transaction->get_error() ) { $this->add_error('transaction_intent_error', $transaction->get_error_messages()); } } $this->mark_as_new(); return false; } /** * Filters transaction right before it's about to be saved when converting from a transaction intent * * @param {OsTransactionModel} $transaction Transaction to be filtered * @returns {OsTransactionModel} The filtered transaction * * @since 5.0.0 * @hook latepoint_before_transaction_save_from_transaction_intent * */ $transaction = apply_filters( 'latepoint_before_transaction_save_from_transaction_intent', $transaction ); if ( $transaction->save() ) { $this->mark_as_converted( $transaction ); /** * Transaction was created * * @param {OsTransactionModel} $transaction instance of transaction model that was created * * @since 5.0.0 * @hook latepoint_transaction_created * */ do_action( 'latepoint_transaction_created', $transaction ); if($transaction->invoice_id){ $invoice = new OsInvoiceModel($transaction->invoice_id); if($invoice && !$invoice->is_new_record()){ $invoice->change_status(LATEPOINT_INVOICE_STATUS_PAID); OsOrdersHelper::check_if_order_invoices_paid_full_balance($this->order_id); } } return $transaction->id; } else { $this->add_error( 'transaction_intent_error', $transaction->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( 'transaction_intent_error', sprintf(__('Error: %s', 'latepoint'), $e->getMessage() )); OsDebugHelper::log( 'Error converting transaction intent to a transaction', 'transaction_intent_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( OsTransactionModel $transaction ) : bool { if ( empty( $transaction->id ) ) { return false; } $this->transaction_id = $transaction->id; $this->status = LATEPOINT_TRANSACTION_INTENT_STATUS_CONVERTED; if($this->save()){ /** * Transaction intent is converted to transaction * * @param {OsTransactionIntentModel} $transaction_intent Instance of transaction intent model that has been converted to transaction * @param {OsTransactionModel} $transaction Instance of transaction model that transaction intent was converted to * * @since 5.0.0 * @hook latepoint_transaction_intent_converted * */ do_action( 'latepoint_transaction_intent_converted', $this, $transaction ); return true; }else{ return false; } } public function mark_as_failed() { $this->update_attributes( [ 'status' => LATEPOINT_TRANSACTION_INTENT_STATUS_FAILED ] ); /** * Transaction intent is marked as failed * * @param {OsTransactionIntentModel} $transaction_intent Instance of order intent model that has failed * * @since 5.2.0 * @hook latepoint_transaction_intent_failed * */ do_action( 'latepoint_transaction_intent_failed', $this ); } public function mark_as_processing() { $this->update_attributes( [ 'status' => LATEPOINT_TRANSACTION_INTENT_STATUS_PROCESSING ] ); /** * Transaction intent is marked as processing * * @param {OsTransactionIntentModel} $transaction_intent Instance of order intent model that has started processing * * @since 5.0.0 * @hook latepoint_transaction_intent_processing * */ do_action( 'latepoint_transaction_intent_processing', $this ); } public function mark_as_new() { $this->update_attributes( [ 'status' => LATEPOINT_TRANSACTION_INTENT_STATUS_NEW ] ); /** * Order intent is marked as new * * @param {OsTransactionIntentModel} $transaction_intent Instance of order intent model that is being marked as new * * @since 5.0.0 * @hook latepoint_transaction_intent_new * */ do_action( 'latepoint_transaction_intent_new', $this ); } // Determines if order intent has been converted into a order already public function is_converted() : bool { if ( empty( $this->transaction_id ) ) { return false; } else { return true; } } public function generate_data_vars(): array { $vars = [ 'id' => $this->id, 'intent_key' => $this->intent_key, 'payment_data' => !empty($this->payment_data) ? json_decode( $this->payment_data, true ) : [], 'order_id' => $this->order_id, 'transaction_id' => $this->transaction_id, 'order_form_page_url' => $this->order_form_page_url, 'updated_at' => $this->updated_at, 'created_at' => $this->created_at, ]; return $vars; } public function get_page_url_with_intent() { $order_form_page_url = $this->order_form_page_url; $existing_var_position = strpos( $order_form_page_url, 'latepoint_transaction_intent_key=' ); if ( $existing_var_position === false ) { // no intent variable in url $question_position = strpos( $order_form_page_url, '?' ); if ( $question_position === false ) { // no ?query params $hash_position = strpos( $order_form_page_url, '#' ); if ( $hash_position === false ) { // no hashtag in url $order_form_page_url = $order_form_page_url . '?latepoint_transaction_intent_key=' . $this->intent_key; } else { // hashtag in url and no ?query, prepend the hashtag with query $order_form_page_url = substr_replace( $order_form_page_url, '?latepoint_transaction_intent_key=' . $this->intent_key . '#', $hash_position, 1 ); } } else { // ?query string exists, add intent key to it $order_form_page_url = substr_replace( $order_form_page_url, '?latepoint_transaction_intent_key=' . $this->intent_key . '&', $question_position, 1 ); } } else { // intent key variable exist in url preg_match( '/latepoint_transaction_intent_key=([\d,\w]*)/', $order_form_page_url, $matches ); if ( isset( $matches[1] ) ) { $order_form_page_url = str_replace( 'latepoint_transaction_intent_key=' . $matches[1], 'latepoint_transaction_intent_key=' . $this->intent_key, $order_form_page_url ); } } return $order_form_page_url; } public function generate_intent_key() { $this->intent_key = bin2hex( openssl_random_pseudo_bytes( 10 ) ); } 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; } 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_TRANSACTION_INTENT_STATUS_NEW; } } protected function allowed_params( $role = 'admin' ) { $allowed_params = array( 'payment_data', 'intent_key', 'order_id', 'customer_id', 'invoice_id', 'transaction_id', 'order_form_page_url', 'status', ); return $allowed_params; } protected function params_to_save( $role = 'admin' ) { $params_to_save = array( 'payment_data', 'intent_key', 'charge_amount', 'specs_charge_amount', 'order_id', 'customer_id', 'invoice_id', 'transaction_id', 'status', ); return $params_to_save; } protected function properties_to_validate() { $validations = array( 'order_id' => array( 'presence' ), ); return $validations; } }