| 1 |
<?php |
| 2 |
|
| 3 |
namespace StoreEngine\Classes; |
| 4 |
|
| 5 |
use Exception; |
| 6 |
use stdClass; |
| 7 |
use StoreEngine\Classes\Exceptions\StoreEngineException; |
| 8 |
use StoreEngine\Classes\Exceptions\StoreEngineInvalidArgumentException; |
| 9 |
use StoreEngine\Classes\Order\OrderItemCoupon; |
| 10 |
use StoreEngine\Classes\OrderStatus\OrderStatus; |
| 11 |
use StoreEngine\Hooks; |
| 12 |
use StoreEngine\Payment\Gateways\PaymentGateway; |
| 13 |
use StoreEngine\Utils\ArrayUtil; |
| 14 |
use StoreEngine\Utils\Caching; |
| 15 |
use StoreEngine\Utils\Formatting; |
| 16 |
use StoreEngine\Utils\Helper; |
| 17 |
use StoreEngine\Utils\PaymentUtil; |
| 18 |
use StoreEngine\Utils\TaxUtil; |
| 19 |
use WP_Comment; |
| 20 |
|
| 21 |
if ( ! defined( 'ABSPATH' ) ) { |
| 22 |
exit; |
| 23 |
} |
| 24 |
|
| 25 |
/** |
| 26 |
* Order model backed by a custom database table data store. |
| 27 |
*/ |
| 28 |
class Order extends AbstractOrder { |
| 29 |
|
| 30 |
protected array $extra_data = [ |
| 31 |
'order_placed_date_gmt' => null, |
| 32 |
'order_placed_date' => null, |
| 33 |
'paid_status' => null, |
| 34 |
// Addresses |
| 35 |
'billing' => [ |
| 36 |
'first_name' => '', |
| 37 |
'last_name' => '', |
| 38 |
'company' => '', |
| 39 |
'address_1' => '', |
| 40 |
'address_2' => '', |
| 41 |
'city' => '', |
| 42 |
'state' => '', |
| 43 |
'postcode' => '', |
| 44 |
'country' => '', |
| 45 |
'email' => '', |
| 46 |
'phone' => '', |
| 47 |
'address_type' => 'billing', |
| 48 |
], |
| 49 |
'shipping' => [ |
| 50 |
'first_name' => '', |
| 51 |
'last_name' => '', |
| 52 |
'company' => '', |
| 53 |
'address_1' => '', |
| 54 |
'address_2' => '', |
| 55 |
'city' => '', |
| 56 |
'state' => '', |
| 57 |
'postcode' => '', |
| 58 |
'country' => '', |
| 59 |
'email' => '', |
| 60 |
'phone' => '', |
| 61 |
'address_type' => 'shipping', |
| 62 |
], |
| 63 |
'download_permissions_granted' => false, |
| 64 |
'auto_complete_digital_order' => false, |
| 65 |
]; |
| 66 |
|
| 67 |
/** |
| 68 |
* Stores data about status changes so relevant hooks can be fired. |
| 69 |
* |
| 70 |
* @var bool|array |
| 71 |
*/ |
| 72 |
protected $status_transition = false; |
| 73 |
|
| 74 |
public function __construct( $read = 0 ) { |
| 75 |
$this->internal_meta_keys[] = '_order_placed_date_gmt'; |
| 76 |
$this->internal_meta_keys[] = '_order_placed_date'; |
| 77 |
$this->internal_meta_keys[] = '_download_permissions_granted'; |
| 78 |
$this->internal_meta_keys[] = '_auto_complete_digital_order'; |
| 79 |
$this->meta_key_to_props['_order_placed_date_gmt'] = 'order_placed_date_gmt'; |
| 80 |
$this->meta_key_to_props['_order_placed_date'] = 'order_placed_date'; |
| 81 |
$this->meta_key_to_props['_paid_status'] = 'paid_status'; |
| 82 |
$this->meta_key_to_props['_download_permissions_granted'] = 'download_permissions_granted'; |
| 83 |
$this->meta_key_to_props['_auto_complete_digital_order'] = 'auto_complete_digital_order'; |
| 84 |
parent::__construct( $read ); |
| 85 |
} |
| 86 |
|
| 87 |
protected function read_db_data( $value, string $field = 'id' ): array { |
| 88 |
return array_merge( |
| 89 |
parent::read_db_data( $value, $field ), |
| 90 |
[ |
| 91 |
'order_placed_date_gmt' => $this->get_metadata( '_order_placed_date_gmt' ), |
| 92 |
'order_placed_date' => $this->get_metadata( '_order_placed_date' ), |
| 93 |
'paid_status' => $this->get_metadata( '_paid_status' ), |
| 94 |
'download_permissions_granted' => $this->get_metadata( '_download_permissions_granted' ), |
| 95 |
'auto_complete_digital_order' => $this->get_metadata( '_auto_complete_digital_order' ), |
| 96 |
] |
| 97 |
); |
| 98 |
} |
| 99 |
|
| 100 |
public function save() { |
| 101 |
$this->maybe_set_user_billing_email(); |
| 102 |
|
| 103 |
$saved = parent::save(); |
| 104 |
|
| 105 |
if ( is_wp_error( $saved ) ) { |
| 106 |
return $saved; |
| 107 |
} |
| 108 |
|
| 109 |
$this->status_transition(); |
| 110 |
|
| 111 |
return $this->get_id(); |
| 112 |
} |
| 113 |
|
| 114 |
public function create() { |
| 115 |
parent::create(); |
| 116 |
|
| 117 |
if ( ! $this->is_type( 'refund' ) && ( array_key_exists( 'billing', $this->data ) || array_key_exists( 'shipping', $this->data ) ) ) { |
| 118 |
foreach ( [ 'billing', 'shipping' ] as $type ) { |
| 119 |
$address = $this->get_address( $type ); |
| 120 |
|
| 121 |
if ( ! $this->{'has_' . $type . '_address'}( 'edit' ) ) { |
| 122 |
continue; |
| 123 |
} |
| 124 |
|
| 125 |
$address['order_id'] = $this->get_id(); |
| 126 |
$formats = array_fill( 0, count( $address ), '%s' ); |
| 127 |
|
| 128 |
$this->wpdb->insert( "{$this->wpdb->prefix}storeengine_order_addresses", $address, $formats ); |
| 129 |
|
| 130 |
if ( $this->wpdb->last_error ) { |
| 131 |
throw new StoreEngineException( wp_kses_post( $this->wpdb->last_error ), 'db-error-insert-record' ); |
| 132 |
} |
| 133 |
} |
| 134 |
} |
| 135 |
} |
| 136 |
|
| 137 |
public function update() { |
| 138 |
parent::update(); |
| 139 |
|
| 140 |
foreach ( [ 'billing', 'shipping' ] as $type ) { |
| 141 |
$address = $this->get_address( $type ); |
| 142 |
// phpcs:disable PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- query prepared. |
| 143 |
$this->wpdb->query( |
| 144 |
$this->wpdb->prepare( |
| 145 |
" |
| 146 |
INSERT INTO `{$this->wpdb->prefix}storeengine_order_addresses` |
| 147 |
(`order_id`, `address_type`, `first_name`, `last_name`, `company`, `address_1`, `address_2`, `city`, `state`, `postcode`, `country`, `email`, `phone`) |
| 148 |
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) |
| 149 |
ON DUPLICATE KEY UPDATE |
| 150 |
`order_id` = VALUES(`order_id`), |
| 151 |
`first_name` = VALUES(`first_name`), |
| 152 |
`last_name` = VALUES(`last_name`), |
| 153 |
`company` = VALUES(`company`), |
| 154 |
`address_1` = VALUES(`address_1`), |
| 155 |
`address_2` = VALUES(`address_2`), |
| 156 |
`city` = VALUES(`city`), |
| 157 |
`state` = VALUES(`state`), |
| 158 |
`postcode` = VALUES(`postcode`), |
| 159 |
`country` = VALUES(`country`), |
| 160 |
`email` = VALUES(`email`), |
| 161 |
`phone` = VALUES(`phone`); |
| 162 |
", |
| 163 |
$this->get_id(), |
| 164 |
$type, |
| 165 |
$address['first_name'], |
| 166 |
$address['last_name'], |
| 167 |
$address['company'], |
| 168 |
$address['address_1'], |
| 169 |
$address['address_2'], |
| 170 |
$address['city'], |
| 171 |
$address['state'], |
| 172 |
$address['postcode'], |
| 173 |
$address['country'], |
| 174 |
$address['email'], |
| 175 |
$address['phone'] |
| 176 |
) |
| 177 |
); |
| 178 |
// phpcs:enable PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- query prepared. |
| 179 |
|
| 180 |
if ( $this->wpdb->last_error ) { |
| 181 |
throw new StoreEngineException( wp_kses_post( $this->wpdb->last_error ), 'db-error-update-record' ); |
| 182 |
} |
| 183 |
} |
| 184 |
} |
| 185 |
|
| 186 |
public function delete( bool $force_delete = false ): bool { |
| 187 |
if ( ! $force_delete && $this->is_trashable() ) { |
| 188 |
$this->set_status( 'trash' ); |
| 189 |
$this->save(); |
| 190 |
|
| 191 |
return true; |
| 192 |
} |
| 193 |
|
| 194 |
$refunds = $this->get_refunds(); |
| 195 |
|
| 196 |
if ( ! empty( $refunds ) ) { |
| 197 |
foreach ( $refunds as $refund ) { |
| 198 |
$refund->delete( true ); |
| 199 |
} |
| 200 |
} |
| 201 |
|
| 202 |
if ( $this->has_address( 'edit' ) ) { |
| 203 |
$this->wpdb->delete( "{$this->wpdb->prefix}storeengine_order_addresses", [ 'order_id' => $this->get_id() ], [ '%d' ] ); |
| 204 |
|
| 205 |
if ( $this->wpdb->last_error ) { |
| 206 |
throw new StoreEngineException( wp_kses_post( $this->wpdb->last_error ), 'db-error-delete-order_addresses' ); |
| 207 |
} |
| 208 |
} |
| 209 |
|
| 210 |
return parent::delete( true ); |
| 211 |
} |
| 212 |
|
| 213 |
/** |
| 214 |
* Log an error about this order is exception is encountered. |
| 215 |
* |
| 216 |
* @param StoreEngineException $e Exception object. |
| 217 |
* @param string $message Message regarding exception thrown. |
| 218 |
*/ |
| 219 |
protected function handle_exception( StoreEngineException $e, string $message = 'Error' ) { |
| 220 |
$this->add_order_note( $message . ' ' . $e->getMessage() ); |
| 221 |
} |
| 222 |
|
| 223 |
/** |
| 224 |
* When a payment is complete this function is called. |
| 225 |
* |
| 226 |
* Most of the time this should mark an order as 'processing' so that admin can process/post the items. |
| 227 |
* If the cart contains only downloadable items then the order is 'completed' since the admin needs to take no action. |
| 228 |
* Stock levels are reduced at this point. |
| 229 |
* Sales are also recorded for products. |
| 230 |
* Finally, record the date of payment. |
| 231 |
* |
| 232 |
* @param string $transaction_id Optional transaction id to store in post meta. |
| 233 |
* |
| 234 |
* @return bool success |
| 235 |
*/ |
| 236 |
public function payment_complete( string $transaction_id = '' ): bool { |
| 237 |
if ( ! $this->get_id() ) { // Order must exist. |
| 238 |
return false; |
| 239 |
} |
| 240 |
|
| 241 |
try { |
| 242 |
$order_id = $this->get_id(); |
| 243 |
/** |
| 244 |
* Fires before payment complete process of an order. |
| 245 |
* |
| 246 |
* @param int $order_id Order id. |
| 247 |
* @param string $transaction_id Transaction id. |
| 248 |
*/ |
| 249 |
do_action( 'storeengine/pre_payment_complete', $order_id, $transaction_id ); |
| 250 |
|
| 251 |
/** |
| 252 |
* Filters the valid order statuses for payment complete. |
| 253 |
* |
| 254 |
* @param array $valid_completed_statuses Array of valid order statuses for payment complete. |
| 255 |
* @param Order $this Order object. |
| 256 |
*/ |
| 257 |
$valid_completed_statuses = apply_filters( 'storeengine/valid_order_statuses_for_payment_complete', [ |
| 258 |
OrderStatus::ON_HOLD, |
| 259 |
OrderStatus::PAYMENT_PENDING, |
| 260 |
OrderStatus::PAYMENT_FAILED, |
| 261 |
OrderStatus::CANCELLED, |
| 262 |
], $this ); |
| 263 |
|
| 264 |
if ( $this->has_status( $valid_completed_statuses ) ) { |
| 265 |
if ( ! empty( $transaction_id ) ) { |
| 266 |
$this->set_transaction_id( $transaction_id ); |
| 267 |
} |
| 268 |
|
| 269 |
if ( ! $this->get_date_paid_gmt( 'edit' ) ) { |
| 270 |
$this->set_date_paid_gmt( current_time( 'mysql', 1 ) ); |
| 271 |
} |
| 272 |
|
| 273 |
/** |
| 274 |
* Filters the order status to set after payment complete. |
| 275 |
* |
| 276 |
* @param string $status Order status. |
| 277 |
* @param int $order_id Order ID. |
| 278 |
* @param Order $this Order object. |
| 279 |
*/ |
| 280 |
$this->set_status( apply_filters( 'storeengine/payment_complete_order_status', $this->needs_processing() ? OrderStatus::PROCESSING : OrderStatus::COMPLETED, $this->get_id(), $this ) ); |
| 281 |
$this->save(); |
| 282 |
|
| 283 |
/** |
| 284 |
* Fires after payment complete process of an order. |
| 285 |
* |
| 286 |
* @param int $order_id Order id. |
| 287 |
* @param string $transaction_id Transaction id. |
| 288 |
*/ |
| 289 |
do_action( 'storeengine/payment_complete', $order_id, $transaction_id ); |
| 290 |
} else { |
| 291 |
$status = $this->get_status(); |
| 292 |
/** |
| 293 |
* If order status isn't valid for mark order as processing/completed, then fire this hook. |
| 294 |
* |
| 295 |
* @param int $order_id Order ID. |
| 296 |
* @param array $transaction_id Transaction ID. |
| 297 |
*/ |
| 298 |
do_action( "storeengine/payment_complete_order_status_{$status}", $order_id, $transaction_id ); |
| 299 |
} |
| 300 |
} catch ( Exception $e ) { |
| 301 |
Helper::log_error( $e ); |
| 302 |
|
| 303 |
$this->add_order_note( __( 'Payment complete event failed.', 'storeengine' ) . ' ' . $e->getMessage() ); |
| 304 |
|
| 305 |
return false; |
| 306 |
} |
| 307 |
|
| 308 |
return true; |
| 309 |
} |
| 310 |
|
| 311 |
/** |
| 312 |
* Forcibly mark an order as paid and advance it to a paid state, running the |
| 313 |
* full payment-complete flow (status transition, digital auto-complete, paid |
| 314 |
* status + date, downstream hooks/emails). |
| 315 |
* |
| 316 |
* Unlike payment_complete() this handles EVERY starting status — including |
| 317 |
* `draft`/`auto-draft` (a Paddle order stranded by the client-side flow) — by |
| 318 |
* first "placing" the order, then advancing it. Idempotent: a no-op that |
| 319 |
* returns true if the order is already paid. |
| 320 |
* |
| 321 |
* Shared seam used by both the Paddle webhook reconciliation |
| 322 |
* (GatewayPaddle::complete_order_from_transaction) and the admin |
| 323 |
* "Mark as paid" action (Ajax\Order::mark_order_as_paid). |
| 324 |
* |
| 325 |
* @param string $note Order note attached to the status transition. |
| 326 |
* |
| 327 |
* @return bool True if the order ended in a paid state. |
| 328 |
*/ |
| 329 |
public function mark_as_paid_force( string $note = '' ): bool { |
| 330 |
if ( ! $this->get_id() ) { |
| 331 |
return false; |
| 332 |
} |
| 333 |
|
| 334 |
if ( $this->is_paid() ) { |
| 335 |
return true; |
| 336 |
} |
| 337 |
|
| 338 |
try { |
| 339 |
$status = $this->get_status(); |
| 340 |
|
| 341 |
// Draft orders must be "placed" first (draft -> pending_payment) before |
| 342 |
// they can be processed. order_placed resets paid_status to unpaid, so |
| 343 |
// we (re)assert paid below for the pending_payment branch. |
| 344 |
if ( in_array( $status, [ OrderStatus::DRAFT, OrderStatus::AUTO_DRAFT ], true ) ) { |
| 345 |
( new OrderContext( $status ) )->proceed_to_next_status( 'order_placed', $this, [ 'note' => $note ] ); |
| 346 |
$status = $this->get_status(); |
| 347 |
} |
| 348 |
|
| 349 |
if ( OrderStatus::PAYMENT_PENDING === $status ) { |
| 350 |
// Mark paid BEFORE advancing so OrderContext's digital auto-complete |
| 351 |
// branch (which checks paid_status === 'paid') fires correctly. |
| 352 |
$this->set_paid_status( 'paid' ); |
| 353 |
( new OrderContext( $status ) )->proceed_to_next_status( 'process_order', $this, [ 'note' => $note ] ); |
| 354 |
} elseif ( in_array( $status, [ OrderStatus::ON_HOLD, OrderStatus::PAYMENT_FAILED, OrderStatus::CANCELLED ], true ) ) { |
| 355 |
// payment_complete() accepts exactly these statuses and moves the |
| 356 |
// order to processing/completed based on needs_processing(). |
| 357 |
$this->payment_complete( $this->get_transaction_id() ); |
| 358 |
|
| 359 |
// set_status() only auto-marks paid for COMPLETED/PAYMENT_CONFIRMED, |
| 360 |
// so assert it for the processing case too. |
| 361 |
if ( 'paid' !== $this->get_paid_status( 'edit' ) ) { |
| 362 |
$this->set_paid_status( 'paid' ); |
| 363 |
} |
| 364 |
} else { |
| 365 |
// Already in a post-payment status (processing/payment_confirmed/ |
| 366 |
// completed) but flagged unpaid — just assert the paid flag. |
| 367 |
$this->set_paid_status( 'paid' ); |
| 368 |
} |
| 369 |
|
| 370 |
$this->save(); |
| 371 |
} catch ( \Throwable $e ) { |
| 372 |
Helper::log_error( $e ); |
| 373 |
$this->add_order_note( __( 'Force mark as paid failed.', 'storeengine' ) . ' ' . $e->getMessage() ); |
| 374 |
|
| 375 |
return false; |
| 376 |
} |
| 377 |
|
| 378 |
return $this->is_paid(); |
| 379 |
} |
| 380 |
|
| 381 |
/** |
| 382 |
* Gets order total - formatted for display. |
| 383 |
* |
| 384 |
* @param string $tax_display Type of tax display. |
| 385 |
* @param bool $display_refunded If should include refunded value. |
| 386 |
* |
| 387 |
* @return string |
| 388 |
*/ |
| 389 |
public function get_formatted_order_total( string $tax_display = '', bool $display_refunded = true ): string { |
| 390 |
$formatted_total = Formatting::price( $this->get_total(), [ 'currency' => $this->get_currency() ] ); |
| 391 |
$order_total = $this->get_total(); |
| 392 |
$total_refunded = $this->get_total_refunded(); |
| 393 |
$tax_string = ''; |
| 394 |
|
| 395 |
// Tax for inclusive prices. |
| 396 |
if ( TaxUtil::is_tax_enabled() && 'incl' === $tax_display ) { |
| 397 |
$tax_string_array = []; |
| 398 |
$tax_totals = $this->get_tax_totals(); |
| 399 |
|
| 400 |
if ( 'itemized' === Helper::get_settings( 'tax_total_display' ) ) { |
| 401 |
foreach ( $tax_totals as $code => $tax ) { |
| 402 |
$tax_amount = ( $total_refunded && $display_refunded ) ? Formatting::price( Tax::round( $tax->amount - $this->get_total_tax_refunded_by_rate_id( $tax->rate_id ) ), [ 'currency' => $this->get_currency() ] ) : $tax->formatted_amount; |
| 403 |
$tax_string_array[] = sprintf( '%s %s', $tax_amount, $tax->label ); |
| 404 |
} |
| 405 |
} elseif ( ! empty( $tax_totals ) ) { |
| 406 |
$tax_amount = ( $total_refunded && $display_refunded ) ? $this->get_total_tax() - $this->get_total_tax_refunded() : $this->get_total_tax(); |
| 407 |
$tax_string_array[] = sprintf( '%s %s', Formatting::price( $tax_amount, [ 'currency' => $this->get_currency() ] ), Countries::init()->tax_or_vat() ); |
| 408 |
} |
| 409 |
|
| 410 |
if ( ! empty( $tax_string_array ) ) { |
| 411 |
/* translators: %s: tax amounts */ |
| 412 |
$tax_string = ' <small class="includes_tax">' . sprintf( __( '(includes %s)', 'storeengine' ), implode( ', ', $tax_string_array ) ) . '</small>'; |
| 413 |
} |
| 414 |
} |
| 415 |
|
| 416 |
if ( $total_refunded && $display_refunded ) { |
| 417 |
$current_total = Formatting::price( $order_total - $total_refunded, [ 'currency' => $this->get_currency() ] ); |
| 418 |
// Strikethrough pricing. |
| 419 |
$formatted_total = '<del aria-hidden="true">' . $formatted_total . '</del> '; |
| 420 |
|
| 421 |
// For accessibility (a11y) we'll also display that information to screen readers. |
| 422 |
$formatted_total .= '<span class="screen-reader-text"> '; |
| 423 |
// translators: %s is total order amount without refund. |
| 424 |
$formatted_total .= esc_html( sprintf( __( 'Original amount was: %s.', 'storeengine' ), wp_strip_all_tags( $formatted_total ) ) ); |
| 425 |
$formatted_total .= '</span>'; |
| 426 |
|
| 427 |
// Add the sale price. |
| 428 |
$formatted_total .= ' <ins aria-hidden="true">' . $current_total . $tax_string . '</ins> '; |
| 429 |
|
| 430 |
// For accessibility (a11y) we'll also display that information to screen readers. |
| 431 |
$formatted_total .= '<span class="screen-reader-text"> '; |
| 432 |
// translators: %s is total order amount after refund. |
| 433 |
$formatted_total .= esc_html( sprintf( __( 'Current amount is: %s.', 'storeengine' ), wp_strip_all_tags( $current_total ) ) ); |
| 434 |
$formatted_total .= '</span>'; |
| 435 |
} else { |
| 436 |
$formatted_total .= $tax_string; |
| 437 |
} |
| 438 |
|
| 439 |
/** |
| 440 |
* Filter StoreEngine formatted order total. |
| 441 |
* |
| 442 |
* @param string $formatted_total Total to display. |
| 443 |
* @param Order $order Order data. |
| 444 |
* @param string $tax_display Type of tax display. |
| 445 |
* @param bool $display_refunded If should include refunded value. |
| 446 |
*/ |
| 447 |
return apply_filters( 'storeengine/get_formatted_order_total', $formatted_total, $this, $tax_display, $display_refunded ); |
| 448 |
} |
| 449 |
|
| 450 |
/** |
| 451 |
* Set order status. |
| 452 |
* |
| 453 |
* @param string $new_status Status to change the order to. No internal wc- prefix is required. |
| 454 |
* @param string $note |
| 455 |
* @param bool $manual_update |
| 456 |
* |
| 457 |
* @return array |
| 458 |
*/ |
| 459 |
public function set_status( string $new_status, string $note = '', bool $manual_update = false ): array { |
| 460 |
$result = parent::set_status( $new_status ); |
| 461 |
|
| 462 |
if ( true === $this->object_read && ! empty( $result['from'] ) && $result['from'] !== $result['to'] ) { |
| 463 |
$this->status_transition = [ |
| 464 |
'from' => ! empty( $this->status_transition['from'] ) ? $this->status_transition['from'] : $result['from'], |
| 465 |
'to' => $result['to'], |
| 466 |
'note' => $note, |
| 467 |
'manual' => $manual_update, |
| 468 |
]; |
| 469 |
|
| 470 |
if ( $manual_update ) { |
| 471 |
$order_id = $this->get_id(); |
| 472 |
$new_order_status = $result['to']; |
| 473 |
/** |
| 474 |
* Fires during set new status when manual update is set to true. |
| 475 |
* |
| 476 |
* @param int $order_id Order ID. |
| 477 |
* @param string $new_order_status New Order Status. |
| 478 |
*/ |
| 479 |
do_action( 'storeengine/order_edit_status', $order_id, $new_order_status ); |
| 480 |
} |
| 481 |
|
| 482 |
if ( $this->is_type( 'order' ) ) { |
| 483 |
// Maybe set order-placed-date |
| 484 |
if ( ! $this->get_order_placed_date_gmt( 'edit' ) && $this->has_status( [ |
| 485 |
OrderStatus::PROCESSING, |
| 486 |
OrderStatus::PAYMENT_CONFIRMED, |
| 487 |
OrderStatus::COMPLETED |
| 488 |
] ) ) { |
| 489 |
$this->set_order_placed_date_gmt(); |
| 490 |
$this->set_order_placed_date(); |
| 491 |
} |
| 492 |
|
| 493 |
// Maybe set paid status. |
| 494 |
if ( ! $this->is_paid( 'edit' ) && $this->has_status( OrderStatus::COMPLETED ) && 'paid' !== $this->get_paid_status( 'edit' ) ) { |
| 495 |
$this->set_paid_status( 'paid' ); |
| 496 |
$this->add_order_note( __( 'Order marked as paid automatically as order is completed.', 'storeengine' ) ); |
| 497 |
} |
| 498 |
|
| 499 |
if ( ! $this->is_paid( 'edit' ) && $this->has_status( OrderStatus::PAYMENT_CONFIRMED ) && 'paid' !== $this->get_paid_status( 'edit' ) ) { |
| 500 |
$this->set_paid_status( 'paid' ); |
| 501 |
$this->add_order_note( __( 'Order marked as paid as order status set to payment confirmed.', 'storeengine' ) ); |
| 502 |
} |
| 503 |
|
| 504 |
if ( 'paid' === $this->get_paid_status( 'edit' ) && $this->has_status( OrderStatus::PAYMENT_PENDING ) ) { |
| 505 |
$this->set_paid_status( 'unpaid' ); |
| 506 |
$this->set_date_paid_gmt( 0 ); |
| 507 |
$this->add_order_note( __( 'Payment status marked as unpaid as order status set to payment pending.', 'storeengine' ) ); |
| 508 |
} |
| 509 |
|
| 510 |
if ( 'paid' === $this->get_paid_status( 'edit' ) && $this->has_status( OrderStatus::ON_HOLD ) ) { |
| 511 |
$this->set_paid_status( 'on_hold' ); |
| 512 |
$this->set_date_paid_gmt( 0 ); |
| 513 |
$this->add_order_note( __( 'Payment status marked as oh-hold as order status set to payment on-hold.', 'storeengine' ) ); |
| 514 |
} |
| 515 |
|
| 516 |
if ( 'paid' === $this->get_paid_status( 'edit' ) && $this->has_status( 'refunded' ) ) { |
| 517 |
$this->set_paid_status( 'refunded' ); |
| 518 |
$this->set_date_paid_gmt( 0 ); |
| 519 |
$this->add_order_note( __( 'Payment status marked as refunded as order status set to payment refunded.', 'storeengine' ) ); |
| 520 |
} |
| 521 |
|
| 522 |
$this->maybe_set_date_completed(); |
| 523 |
} |
| 524 |
} |
| 525 |
|
| 526 |
return $result; |
| 527 |
} |
| 528 |
|
| 529 |
/** |
| 530 |
* Maybe set date paid. |
| 531 |
* |
| 532 |
* Sets the date paid variable when transitioning to the payment complete |
| 533 |
* order status. This is either processing or completed. This is not filtered |
| 534 |
* to avoid infinite loops e.g. if loading an order via the filter. |
| 535 |
* |
| 536 |
* Date paid is set once in this manner - only when it is not already set. |
| 537 |
* This ensures the data exists even if a gateway does not use the |
| 538 |
* `payment_complete` method. |
| 539 |
* |
| 540 |
* @deprecated use paid_status |
| 541 |
*/ |
| 542 |
public function maybe_set_date_paid() { |
| 543 |
// This logic only runs if the date_paid prop has not been set yet. |
| 544 |
if ( ! $this->get_date_paid_gmt( 'edit' ) ) { |
| 545 |
$paid_statuses = [ OrderStatus::PAYMENT_CONFIRMED, OrderStatus::PROCESSING, OrderStatus::COMPLETED ]; |
| 546 |
if ( $this->has_status( $paid_statuses ) ) { |
| 547 |
// If payment complete status is reached, set paid now. |
| 548 |
$this->set_date_paid_gmt( current_time( 'mysql', 1 ) ); |
| 549 |
$this->set_prop( 'paid_status', 'paid' ); |
| 550 |
} else { |
| 551 |
$this->set_date_paid_gmt( 0 ); |
| 552 |
$this->set_prop( 'paid_status', 'unpaid' ); |
| 553 |
} |
| 554 |
} |
| 555 |
|
| 556 |
$unpaid_statuses = [ OrderStatus::PAYMENT_PENDING, OrderStatus::AUTO_DRAFT, OrderStatus::DRAFT ]; |
| 557 |
if ( $this->get_date_paid_gmt( 'edit' ) && $this->has_status( $unpaid_statuses ) ) { |
| 558 |
$this->set_date_paid_gmt( 0 ); |
| 559 |
$this->set_prop( 'paid_status', 'unpaid' ); |
| 560 |
} |
| 561 |
} |
| 562 |
|
| 563 |
/** |
| 564 |
* @param string $status |
| 565 |
* @param string|null $transaction_id |
| 566 |
* |
| 567 |
* @return void |
| 568 |
* @throws StoreEngineException |
| 569 |
*/ |
| 570 |
public function set_paid_status( string $status, ?string $transaction_id = null ) { |
| 571 |
$paid_stati = [ 'paid', 'partially_paid', 'unpaid', 'failed', 'on_hold', 'refunded' ]; |
| 572 |
if ( ! in_array( $status, $paid_stati, true ) ) { |
| 573 |
throw StoreEngineInvalidArgumentException::create( 1, 'status', $paid_stati, $status ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped |
| 574 |
} |
| 575 |
|
| 576 |
if ( $transaction_id ) { |
| 577 |
$this->set_transaction_id( $transaction_id ); |
| 578 |
} |
| 579 |
|
| 580 |
$old_status = $this->get_paid_status(); |
| 581 |
|
| 582 |
if ( 'paid' === $status ) { |
| 583 |
if ( ! $this->get_date_paid_gmt( 'edit' ) || 'partially_paid' === $old_status ) { |
| 584 |
$this->set_date_paid_gmt( current_time( 'mysql', 1 ) ); |
| 585 |
} |
| 586 |
} elseif ( 'partially_paid' === $status ) { |
| 587 |
$this->set_date_paid_gmt( current_time( 'mysql', 1 ) ); |
| 588 |
} else { |
| 589 |
$this->set_date_paid_gmt( 0 ); |
| 590 |
} |
| 591 |
|
| 592 |
$this->set_prop( 'paid_status', $status ); |
| 593 |
|
| 594 |
if ( true === $this->object_read && ! empty( $old_status ) && $old_status !== $status ) { |
| 595 |
do_action_ref_array( 'storeengine/order/payment_status_changed', [ &$this, $status, $old_status ] ); |
| 596 |
} |
| 597 |
} |
| 598 |
|
| 599 |
public function get_paid_status( string $context = 'view' ): ?string { |
| 600 |
$status = $this->get_prop( 'paid_status', $context ); |
| 601 |
|
| 602 |
if ( ! $status && 'view' === $context ) { |
| 603 |
$status = 'unpaid'; |
| 604 |
} |
| 605 |
|
| 606 |
return $status; |
| 607 |
} |
| 608 |
|
| 609 |
public function is_paid( $context = 'view' ): bool { |
| 610 |
return (bool) apply_filters( 'storeengine/order/is_paid', 'paid' === $this->get_paid_status( $context ) ); |
| 611 |
} |
| 612 |
|
| 613 |
// /** |
| 614 |
// * Returns if an order has been paid for based on the order status. |
| 615 |
// * |
| 616 |
// * @return bool |
| 617 |
// */ |
| 618 |
// public function is_paid(): bool { |
| 619 |
// return apply_filters( 'storeengine/order_is_paid', $this->has_status( OrderStatus::get_is_paid_statuses() ), $this ); |
| 620 |
// } |
| 621 |
|
| 622 |
/** |
| 623 |
* Maybe set date completed. |
| 624 |
* |
| 625 |
* Sets the date completed variable when transitioning to completed status. |
| 626 |
*/ |
| 627 |
protected function maybe_set_date_completed() { |
| 628 |
if ( $this->has_status( OrderStatus::COMPLETED ) ) { |
| 629 |
$this->set_date_completed_gmt( time() ); |
| 630 |
} |
| 631 |
} |
| 632 |
|
| 633 |
/** |
| 634 |
* Updates status of order immediately. |
| 635 |
* |
| 636 |
* @param string $new_status Status to change the order to. No internal wc- prefix is required. |
| 637 |
* @param string $note Optional note to add. |
| 638 |
* @param bool $manual Is this a manual order status change?. |
| 639 |
* |
| 640 |
* @return bool |
| 641 |
* @uses self::set_status() |
| 642 |
*/ |
| 643 |
public function update_status( string $new_status, string $note = '', bool $manual = false ): bool { |
| 644 |
if ( ! $this->get_id() ) { // Order must exist. |
| 645 |
return false; |
| 646 |
} |
| 647 |
|
| 648 |
try { |
| 649 |
$this->set_status( $new_status, $note, $manual ); |
| 650 |
$this->save(); |
| 651 |
} catch ( Exception $e ) { |
| 652 |
Helper::log_error( $e ); |
| 653 |
$this->add_order_note( __( 'Update status event failed.', 'storeengine' ) . ' ' . $e->getMessage() ); |
| 654 |
|
| 655 |
return false; |
| 656 |
} |
| 657 |
|
| 658 |
return true; |
| 659 |
} |
| 660 |
|
| 661 |
/** |
| 662 |
* Handle the status transition. |
| 663 |
*/ |
| 664 |
protected function status_transition() { |
| 665 |
$status_transition = $this->status_transition; |
| 666 |
|
| 667 |
// Reset status transition variable. |
| 668 |
$this->status_transition = false; |
| 669 |
|
| 670 |
if ( $status_transition ) { |
| 671 |
try { |
| 672 |
$new_status = $status_transition['to']; |
| 673 |
$order_id = $this->get_id(); |
| 674 |
/** |
| 675 |
* Fires when order status is changed. |
| 676 |
* |
| 677 |
* @param int $order_id Order ID. |
| 678 |
* @param Order $this Order object. |
| 679 |
* @param array $status_transition Status transition data. |
| 680 |
*/ |
| 681 |
do_action( "storeengine/order_status_{$new_status}", $order_id, $this, $status_transition ); |
| 682 |
|
| 683 |
if ( ! empty( $status_transition['from'] ) ) { |
| 684 |
/* translators: 1: old order status 2: new order status */ |
| 685 |
$transition_note = sprintf( __( 'Order status changed from %1$s to %2$s.', 'storeengine' ), OrderStatus::get_order_status_name( $status_transition['from'] ), OrderStatus::get_order_status_name( $status_transition['to'] ) ); |
| 686 |
|
| 687 |
// Note the transition occurred. |
| 688 |
$this->add_status_transition_note( $transition_note, $status_transition ); |
| 689 |
|
| 690 |
$old_status = $status_transition['from']; |
| 691 |
|
| 692 |
/** |
| 693 |
* Fires when order status is changed. |
| 694 |
* |
| 695 |
* @param int $order_id Order ID. |
| 696 |
* @param Order $this Order object. |
| 697 |
*/ |
| 698 |
do_action( "storeengine/order_status_{$old_status}_to_{$new_status}", $order_id, $this ); |
| 699 |
|
| 700 |
/** |
| 701 |
* Fires when order status is changed. |
| 702 |
* |
| 703 |
* @param int $order_id Order ID. |
| 704 |
* @param string $old_status Old Status. |
| 705 |
* @param string $new_status New Status. |
| 706 |
* @param Order $this Order object. |
| 707 |
*/ |
| 708 |
do_action( 'storeengine/order/status_changed', $order_id, $old_status, $new_status, $this ); |
| 709 |
|
| 710 |
/** |
| 711 |
* Fires when order status is changed. |
| 712 |
* |
| 713 |
* @param int $order_id Order ID. |
| 714 |
* @param string $old_status Old Status. |
| 715 |
* @param Order $this Order object. |
| 716 |
*/ |
| 717 |
do_action( "storeengine/order/status_{$new_status}", $order_id, $old_status, $this ); |
| 718 |
|
| 719 |
// Work out if this was for a payment, and trigger a payment_status hook instead. |
| 720 |
/** |
| 721 |
* Filter the valid order statuses for payment. |
| 722 |
* |
| 723 |
* @param array $valid_order_statuses Array of valid order statuses for payment. |
| 724 |
* @param Order $order Order object. |
| 725 |
*/ |
| 726 |
$check_transition_from = in_array( $status_transition['from'], apply_filters( 'storeengine/valid_order_statuses_for_payment', [ |
| 727 |
OrderStatus::PAYMENT_PENDING, |
| 728 |
OrderStatus::PAYMENT_FAILED, |
| 729 |
], $this ), true ); |
| 730 |
$check_transition_to = in_array( $status_transition['to'], OrderStatus::get_is_paid_statuses(), true ); |
| 731 |
if ( $check_transition_from && $check_transition_to ) { |
| 732 |
/** |
| 733 |
* Fires when the order progresses from a pending payment status to a paid one. |
| 734 |
* |
| 735 |
* @param int $order_id Order ID. |
| 736 |
* @param Order $this Order object. |
| 737 |
*/ |
| 738 |
do_action( 'storeengine/order_payment_status_changed', $order_id, $this ); |
| 739 |
} |
| 740 |
} else { |
| 741 |
/* translators: %s: new order status */ |
| 742 |
$transition_note = sprintf( __( 'Order status set to %s.', 'storeengine' ), OrderStatus::get_order_status_name( $status_transition['to'] ) ); |
| 743 |
|
| 744 |
// Note the transition occurred. |
| 745 |
$this->add_status_transition_note( $transition_note, $status_transition ); |
| 746 |
} |
| 747 |
} catch ( Exception $e ) { |
| 748 |
Helper::log_error( $e ); |
| 749 |
$this->add_order_note( __( 'Error during status transition.', 'storeengine' ) . ' ' . $e->getMessage() ); |
| 750 |
} |
| 751 |
} |
| 752 |
} |
| 753 |
|
| 754 |
/* |
| 755 |
|-------------------------------------------------------------------------- |
| 756 |
| Getters |
| 757 |
|-------------------------------------------------------------------------- |
| 758 |
| |
| 759 |
| Methods for getting data from the order object. |
| 760 |
| |
| 761 |
*/ |
| 762 |
|
| 763 |
/** |
| 764 |
* Get basic order data in array format. |
| 765 |
* |
| 766 |
* @return array |
| 767 |
*/ |
| 768 |
public function get_base_data(): array { |
| 769 |
return array_merge( |
| 770 |
[ 'id' => $this->get_id() ], |
| 771 |
$this->data, |
| 772 |
[ 'number' => $this->get_order_number() ] |
| 773 |
); |
| 774 |
} |
| 775 |
|
| 776 |
/** |
| 777 |
* Get all class data in array format. |
| 778 |
* |
| 779 |
* @return array |
| 780 |
*/ |
| 781 |
public function get_data(): array { |
| 782 |
return array_merge( |
| 783 |
$this->get_base_data(), |
| 784 |
[ |
| 785 |
'meta_data' => $this->get_meta_data(), |
| 786 |
'line_items' => $this->get_items( 'line_item' ), |
| 787 |
'tax_lines' => $this->get_items( 'tax' ), |
| 788 |
'shipping_lines' => $this->get_items( 'shipping' ), |
| 789 |
'fee_lines' => $this->get_items( 'fee' ), |
| 790 |
'coupon_lines' => $this->get_items( 'coupon' ), |
| 791 |
] |
| 792 |
); |
| 793 |
} |
| 794 |
|
| 795 |
/** |
| 796 |
* Expands the shipping and billing information in the changes array. |
| 797 |
*/ |
| 798 |
public function get_changes(): array { |
| 799 |
$changed_props = parent::get_changes(); |
| 800 |
$subs = [ 'shipping', 'billing' ]; |
| 801 |
foreach ( $subs as $sub ) { |
| 802 |
if ( ! empty( $changed_props[ $sub ] ) ) { |
| 803 |
foreach ( $changed_props[ $sub ] as $sub_prop => $value ) { |
| 804 |
$changed_props[ $sub . '_' . $sub_prop ] = $value; |
| 805 |
} |
| 806 |
} |
| 807 |
} |
| 808 |
if ( isset( $changed_props['customer_note'] ) ) { |
| 809 |
$changed_props['post_excerpt'] = $changed_props['customer_note']; |
| 810 |
} |
| 811 |
|
| 812 |
return $changed_props; |
| 813 |
} |
| 814 |
|
| 815 |
/** |
| 816 |
* Gets the order number for display (by default, order ID). |
| 817 |
* |
| 818 |
* @return string |
| 819 |
*/ |
| 820 |
public function get_order_number(): string { |
| 821 |
return (string) apply_filters( 'storeengine/order_number', $this->get_id(), $this ); |
| 822 |
} |
| 823 |
|
| 824 |
/** |
| 825 |
* Gets a prop for a getter method. |
| 826 |
* |
| 827 |
* @param string $prop Name of prop to get. |
| 828 |
* @param string $address_type Type of address; 'billing' or 'shipping'. |
| 829 |
* @param string $context What the value is for. Valid values are view and edit. |
| 830 |
* |
| 831 |
* @return ?string |
| 832 |
*/ |
| 833 |
protected function get_address_prop( string $prop, string $address_type = 'billing', string $context = 'view' ): ?string { |
| 834 |
$value = null; |
| 835 |
|
| 836 |
if ( array_key_exists( $prop, $this->data[ $address_type ] ) ) { |
| 837 |
$value = $this->changes[ $address_type ][ $prop ] ?? $this->data[ $address_type ][ $prop ]; |
| 838 |
|
| 839 |
if ( 'view' === $context ) { |
| 840 |
/** |
| 841 |
* Filter: 'storeengine/order_get_[billing|shipping]_[prop]' |
| 842 |
* |
| 843 |
* Allow developers to change the returned value for any order address property. |
| 844 |
* |
| 845 |
* @param string $value The address property value. |
| 846 |
* @param Order $order The order object being read. |
| 847 |
* |
| 848 |
* @ignore Ignore from HookParser. |
| 849 |
*/ |
| 850 |
$value = apply_filters( $this->get_hook_prefix( $address_type . '_' . $prop ), $value, $this ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound |
| 851 |
} |
| 852 |
} |
| 853 |
|
| 854 |
return $value; |
| 855 |
} |
| 856 |
|
| 857 |
/** |
| 858 |
* Get billing first name. |
| 859 |
* |
| 860 |
* @param string $context What the value is for. Valid values are view and edit. |
| 861 |
* |
| 862 |
* @return ?string |
| 863 |
*/ |
| 864 |
public function get_billing_first_name( string $context = 'view' ): ?string { |
| 865 |
return $this->get_address_prop( 'first_name', 'billing', $context ); |
| 866 |
} |
| 867 |
|
| 868 |
/** |
| 869 |
* Get billing last name. |
| 870 |
* |
| 871 |
* @param ?string $context What the value is for. Valid values are view and edit. |
| 872 |
* |
| 873 |
* @return string |
| 874 |
*/ |
| 875 |
public function get_billing_last_name( string $context = 'view' ): ?string { |
| 876 |
return $this->get_address_prop( 'last_name', 'billing', $context ); |
| 877 |
} |
| 878 |
|
| 879 |
/** |
| 880 |
* Get billing company. |
| 881 |
* |
| 882 |
* @param string $context What the value is for. Valid values are view and edit. |
| 883 |
* |
| 884 |
* @return ?string |
| 885 |
*/ |
| 886 |
public function get_billing_company( string $context = 'view' ): ?string { |
| 887 |
return $this->get_address_prop( 'company', 'billing', $context ); |
| 888 |
} |
| 889 |
|
| 890 |
/** |
| 891 |
* Get billing address line 1. |
| 892 |
* |
| 893 |
* @param string $context What the value is for. Valid values are view and edit. |
| 894 |
* |
| 895 |
* @return ?string |
| 896 |
*/ |
| 897 |
public function get_billing_address_1( string $context = 'view' ): ?string { |
| 898 |
return $this->get_address_prop( 'address_1', 'billing', $context ); |
| 899 |
} |
| 900 |
|
| 901 |
/** |
| 902 |
* Get billing address line 2. |
| 903 |
* |
| 904 |
* @param string $context What the value is for. Valid values are view and edit. |
| 905 |
* |
| 906 |
* @return ?string |
| 907 |
*/ |
| 908 |
public function get_billing_address_2( string $context = 'view' ): ?string { |
| 909 |
return $this->get_address_prop( 'address_2', 'billing', $context ); |
| 910 |
} |
| 911 |
|
| 912 |
/** |
| 913 |
* Get billing city. |
| 914 |
* |
| 915 |
* @param string $context What the value is for. Valid values are view and edit. |
| 916 |
* |
| 917 |
* @return ?string |
| 918 |
*/ |
| 919 |
public function get_billing_city( string $context = 'view' ): ?string { |
| 920 |
return $this->get_address_prop( 'city', 'billing', $context ); |
| 921 |
} |
| 922 |
|
| 923 |
/** |
| 924 |
* Get billing state. |
| 925 |
* |
| 926 |
* @param string $context What the value is for. Valid values are view and edit. |
| 927 |
* |
| 928 |
* @return ?string |
| 929 |
*/ |
| 930 |
public function get_billing_state( string $context = 'view' ): ?string { |
| 931 |
return $this->get_address_prop( 'state', 'billing', $context ); |
| 932 |
} |
| 933 |
|
| 934 |
/** |
| 935 |
* Get billing postcode. |
| 936 |
* |
| 937 |
* @param string $context What the value is for. Valid values are view and edit. |
| 938 |
* |
| 939 |
* @return ?string |
| 940 |
*/ |
| 941 |
public function get_billing_postcode( string $context = 'view' ): ?string { |
| 942 |
return $this->get_address_prop( 'postcode', 'billing', $context ); |
| 943 |
} |
| 944 |
|
| 945 |
/** |
| 946 |
* Get billing country. |
| 947 |
* |
| 948 |
* @param string $context What the value is for. Valid values are view and edit. |
| 949 |
* |
| 950 |
* @return ?string |
| 951 |
*/ |
| 952 |
public function get_billing_country( string $context = 'view' ): ?string { |
| 953 |
return $this->get_address_prop( 'country', 'billing', $context ); |
| 954 |
} |
| 955 |
|
| 956 |
/** |
| 957 |
* Get billing email. |
| 958 |
* |
| 959 |
* @param string $context What the value is for. Valid values are view and edit. |
| 960 |
* |
| 961 |
* @return ?string |
| 962 |
*/ |
| 963 |
public function get_billing_email( string $context = 'view' ): ?string { |
| 964 |
return $this->get_address_prop( 'email', 'billing', $context ); |
| 965 |
} |
| 966 |
|
| 967 |
/** |
| 968 |
* Get billing phone. |
| 969 |
* |
| 970 |
* @param string $context What the value is for. Valid values are view and edit. |
| 971 |
* |
| 972 |
* @return ?string |
| 973 |
*/ |
| 974 |
public function get_billing_phone( string $context = 'view' ): ?string { |
| 975 |
return $this->get_address_prop( 'phone', 'billing', $context ); |
| 976 |
} |
| 977 |
|
| 978 |
/** |
| 979 |
* Get shipping first name. |
| 980 |
* |
| 981 |
* @param string $context What the value is for. Valid values are view and edit. |
| 982 |
* |
| 983 |
* @return ?string |
| 984 |
*/ |
| 985 |
public function get_shipping_first_name( string $context = 'view' ): ?string { |
| 986 |
return $this->get_address_prop( 'first_name', 'shipping', $context ); |
| 987 |
} |
| 988 |
|
| 989 |
/** |
| 990 |
* Get shipping_last_name. |
| 991 |
* |
| 992 |
* @param string $context What the value is for. Valid values are view and edit. |
| 993 |
* |
| 994 |
* @return ?string |
| 995 |
*/ |
| 996 |
public function get_shipping_last_name( string $context = 'view' ): ?string { |
| 997 |
return $this->get_address_prop( 'last_name', 'shipping', $context ); |
| 998 |
} |
| 999 |
|
| 1000 |
/** |
| 1001 |
* Get shipping company. |
| 1002 |
* |
| 1003 |
* @param string $context What the value is for. Valid values are view and edit. |
| 1004 |
* |
| 1005 |
* @return ?string |
| 1006 |
*/ |
| 1007 |
public function get_shipping_company( string $context = 'view' ): ?string { |
| 1008 |
return $this->get_address_prop( 'company', 'shipping', $context ); |
| 1009 |
} |
| 1010 |
|
| 1011 |
/** |
| 1012 |
* Get shipping address line 1. |
| 1013 |
* |
| 1014 |
* @param string $context What the value is for. Valid values are view and edit. |
| 1015 |
* |
| 1016 |
* @return? string |
| 1017 |
*/ |
| 1018 |
public function get_shipping_address_1( string $context = 'view' ): ?string { |
| 1019 |
return $this->get_address_prop( 'address_1', 'shipping', $context ); |
| 1020 |
} |
| 1021 |
|
| 1022 |
/** |
| 1023 |
* Get shipping address line 2. |
| 1024 |
* |
| 1025 |
* @param string $context What the value is for. Valid values are view and edit. |
| 1026 |
* |
| 1027 |
* @return ?string |
| 1028 |
*/ |
| 1029 |
public function get_shipping_address_2( string $context = 'view' ): ?string { |
| 1030 |
return $this->get_address_prop( 'address_2', 'shipping', $context ); |
| 1031 |
} |
| 1032 |
|
| 1033 |
/** |
| 1034 |
* Get shipping city. |
| 1035 |
* |
| 1036 |
* @param string $context What the value is for. Valid values are view and edit. |
| 1037 |
* |
| 1038 |
* @return string |
| 1039 |
*/ |
| 1040 |
public function get_shipping_city( string $context = 'view' ): ?string { |
| 1041 |
return $this->get_address_prop( 'city', 'shipping', $context ); |
| 1042 |
} |
| 1043 |
|
| 1044 |
/** |
| 1045 |
* Get shipping state. |
| 1046 |
* |
| 1047 |
* @param string $context What the value is for. Valid values are view and edit. |
| 1048 |
* |
| 1049 |
* @return ?string |
| 1050 |
*/ |
| 1051 |
public function get_shipping_state( string $context = 'view' ): ?string { |
| 1052 |
return $this->get_address_prop( 'state', 'shipping', $context ); |
| 1053 |
} |
| 1054 |
|
| 1055 |
/** |
| 1056 |
* Get shipping postcode. |
| 1057 |
* |
| 1058 |
* @param string $context What the value is for. Valid values are view and edit. |
| 1059 |
* |
| 1060 |
* @return ?string |
| 1061 |
*/ |
| 1062 |
public function get_shipping_postcode( string $context = 'view' ): ?string { |
| 1063 |
return $this->get_address_prop( 'postcode', 'shipping', $context ); |
| 1064 |
} |
| 1065 |
|
| 1066 |
/** |
| 1067 |
* Get shipping country. |
| 1068 |
* |
| 1069 |
* @param string $context What the value is for. Valid values are view and edit. |
| 1070 |
* |
| 1071 |
* @return ?string |
| 1072 |
*/ |
| 1073 |
public function get_shipping_country( string $context = 'view' ): ?string { |
| 1074 |
return $this->get_address_prop( 'country', 'shipping', $context ); |
| 1075 |
} |
| 1076 |
|
| 1077 |
/** |
| 1078 |
* Get shipping country. |
| 1079 |
* |
| 1080 |
* @param string $context What the value is for. Valid values are view and edit. |
| 1081 |
* |
| 1082 |
* @return ?string |
| 1083 |
*/ |
| 1084 |
public function get_shipping_email( string $context = 'view' ): ?string { |
| 1085 |
return $this->get_address_prop( 'email', 'shipping', $context ); |
| 1086 |
} |
| 1087 |
|
| 1088 |
/** |
| 1089 |
* Get shipping phone. |
| 1090 |
* |
| 1091 |
* @param string $context What the value is for. Valid values are view and edit. |
| 1092 |
* |
| 1093 |
* @return ?string |
| 1094 |
*/ |
| 1095 |
public function get_shipping_phone( string $context = 'view' ): ?string { |
| 1096 |
return $this->get_address_prop( 'phone', 'shipping', $context ); |
| 1097 |
} |
| 1098 |
|
| 1099 |
/** |
| 1100 |
* Get the payment method. |
| 1101 |
* |
| 1102 |
* @param string $context What the value is for. Valid values are view and edit. |
| 1103 |
* |
| 1104 |
* @return string |
| 1105 |
*/ |
| 1106 |
public function get_payment_method( string $context = 'view' ) { |
| 1107 |
return $this->get_prop( 'payment_method', $context ); |
| 1108 |
} |
| 1109 |
|
| 1110 |
/** |
| 1111 |
* Get payment method title. |
| 1112 |
* |
| 1113 |
* @param string $context What the value is for. Valid values are view and edit. |
| 1114 |
* |
| 1115 |
* @return string |
| 1116 |
*/ |
| 1117 |
public function get_payment_method_title( string $context = 'view' ) { |
| 1118 |
return $this->get_prop( 'payment_method_title', $context ); |
| 1119 |
} |
| 1120 |
|
| 1121 |
/** |
| 1122 |
* Get transaction id. |
| 1123 |
* |
| 1124 |
* @param string $context What the value is for. Valid values are view and edit. |
| 1125 |
* |
| 1126 |
* @return ?string |
| 1127 |
*/ |
| 1128 |
public function get_transaction_id( string $context = 'view' ) { |
| 1129 |
return $this->get_prop( 'transaction_id', $context ); |
| 1130 |
} |
| 1131 |
|
| 1132 |
/** |
| 1133 |
* Get customer ip address. |
| 1134 |
* |
| 1135 |
* @param string $context What the value is for. Valid values are view and edit. |
| 1136 |
* |
| 1137 |
* @return ?string |
| 1138 |
*/ |
| 1139 |
public function get_ip_address( string $context = 'view' ) { |
| 1140 |
return $this->get_prop( 'ip_address', $context ); |
| 1141 |
} |
| 1142 |
|
| 1143 |
/** |
| 1144 |
* Get customer user agent. |
| 1145 |
* |
| 1146 |
* @param string $context What the value is for. Valid values are view and edit. |
| 1147 |
* |
| 1148 |
* @return ?string |
| 1149 |
*/ |
| 1150 |
public function get_user_agent( string $context = 'view' ) { |
| 1151 |
return $this->get_prop( 'user_agent', $context ); |
| 1152 |
} |
| 1153 |
|
| 1154 |
/** |
| 1155 |
* Get created via. |
| 1156 |
* |
| 1157 |
* @param string $context What the value is for. Valid values are view and edit. |
| 1158 |
* |
| 1159 |
* @return ?string |
| 1160 |
*/ |
| 1161 |
public function get_created_via( string $context = 'view' ) { |
| 1162 |
return $this->get_prop( 'created_via', $context ); |
| 1163 |
} |
| 1164 |
|
| 1165 |
/** |
| 1166 |
* Get customer note. |
| 1167 |
* |
| 1168 |
* @param string $context What the value is for. Valid values are view and edit. |
| 1169 |
* |
| 1170 |
* @return string |
| 1171 |
*/ |
| 1172 |
public function get_customer_note( string $context = 'view' ) { |
| 1173 |
return $this->get_prop( 'customer_note', $context ); |
| 1174 |
} |
| 1175 |
|
| 1176 |
/** |
| 1177 |
* Get cart hash. |
| 1178 |
* |
| 1179 |
* @param string $context What the value is for. Valid values are view and edit. |
| 1180 |
* |
| 1181 |
* @return string |
| 1182 |
*/ |
| 1183 |
public function get_cart_hash( string $context = 'view' ) { |
| 1184 |
return $this->get_hash( $context ); |
| 1185 |
} |
| 1186 |
|
| 1187 |
/** |
| 1188 |
* Get cart hash. |
| 1189 |
* |
| 1190 |
* @param string $context What the value is for. Valid values are view and edit. |
| 1191 |
* |
| 1192 |
* @return string |
| 1193 |
*/ |
| 1194 |
public function get_hash( string $context = 'view' ) { |
| 1195 |
return $this->get_prop( 'hash', $context ); |
| 1196 |
} |
| 1197 |
|
| 1198 |
/** |
| 1199 |
* Returns the requested address in raw, non-formatted way. |
| 1200 |
* Note: Merges raw data with get_prop data so changes are returned too. |
| 1201 |
* |
| 1202 |
* @param string $address_type Type of address; 'billing' or 'shipping'. |
| 1203 |
* |
| 1204 |
* @return array The stored address after filter. |
| 1205 |
*/ |
| 1206 |
public function get_address( $address_type = 'billing' ) { |
| 1207 |
/** |
| 1208 |
* Filter: 'storeengine/get_order_address' |
| 1209 |
* |
| 1210 |
* Allow developers to change the returned value for an order's billing or shipping address. |
| 1211 |
* |
| 1212 |
* @param array $address_data The raw address data merged with the data from get_prop. |
| 1213 |
* @param string $address_type Type of address; 'billing' or 'shipping'. |
| 1214 |
*/ |
| 1215 |
return apply_filters( 'storeengine/get_order_address', array_merge( $this->data[ $address_type ], $this->get_prop( $address_type, 'view' ) ), $address_type, $this ); |
| 1216 |
} |
| 1217 |
|
| 1218 |
/** |
| 1219 |
* Get a formatted shipping address for the order. |
| 1220 |
* |
| 1221 |
* @return string |
| 1222 |
*/ |
| 1223 |
public function get_shipping_address_map_url() { |
| 1224 |
$address = $this->get_address( 'shipping' ); |
| 1225 |
|
| 1226 |
// Remove name and company before generate the Google Maps URL. |
| 1227 |
unset( $address['first_name'], $address['last_name'], $address['company'], $address['phone'] ); |
| 1228 |
|
| 1229 |
$address = apply_filters( 'storeengine/shipping_address_map_url_parts', $address, $this ); |
| 1230 |
|
| 1231 |
return apply_filters( 'storeengine/shipping_address_map_url', 'https://maps.google.com/maps?&q=' . rawurlencode( implode( ', ', $address ) ) . '&z=16', $this ); |
| 1232 |
} |
| 1233 |
|
| 1234 |
/** |
| 1235 |
* Get a formatted billing full name. |
| 1236 |
* |
| 1237 |
* @return string |
| 1238 |
*/ |
| 1239 |
public function get_formatted_billing_full_name() { |
| 1240 |
/* translators: 1: first name 2: last name */ |
| 1241 |
return sprintf( _x( '%1$s %2$s', 'full name', 'storeengine' ), $this->get_billing_first_name(), $this->get_billing_last_name() ); |
| 1242 |
} |
| 1243 |
|
| 1244 |
/** |
| 1245 |
* Get a formatted shipping full name. |
| 1246 |
* |
| 1247 |
* @return string |
| 1248 |
*/ |
| 1249 |
public function get_formatted_shipping_full_name() { |
| 1250 |
/* translators: 1: first name 2: last name */ |
| 1251 |
return sprintf( _x( '%1$s %2$s', 'full name', 'storeengine' ), $this->get_shipping_first_name(), $this->get_shipping_last_name() ); |
| 1252 |
} |
| 1253 |
|
| 1254 |
/** |
| 1255 |
* Get a formatted billing address for the order. |
| 1256 |
* |
| 1257 |
* @param string $empty_content Content to show if no address is present. |
| 1258 |
* |
| 1259 |
* @return string |
| 1260 |
*/ |
| 1261 |
public function get_formatted_billing_address( $empty_content = '' ) { |
| 1262 |
$raw_address = apply_filters( 'storeengine/order_formatted_billing_address', $this->get_address( 'billing' ), $this ); |
| 1263 |
$address = Countries::init()->get_formatted_address( $raw_address ); |
| 1264 |
|
| 1265 |
/** |
| 1266 |
* Filter orders formatted billing address. |
| 1267 |
* |
| 1268 |
* @param string $address Formatted billing address string. |
| 1269 |
* @param array $raw_address Raw billing address. |
| 1270 |
* @param Order $order Order data. |
| 1271 |
*/ |
| 1272 |
return apply_filters( 'storeengine/order_get_formatted_billing_address', $address ? $address : $empty_content, $raw_address, $this ); |
| 1273 |
} |
| 1274 |
|
| 1275 |
/** |
| 1276 |
* Get a formatted shipping address for the order. |
| 1277 |
* |
| 1278 |
* @param string $empty_content Content to show if no address is present. |
| 1279 |
* |
| 1280 |
* @return string |
| 1281 |
*/ |
| 1282 |
public function get_formatted_shipping_address( $empty_content = '' ) { |
| 1283 |
$address = ''; |
| 1284 |
$raw_address = $this->get_address( 'shipping' ); |
| 1285 |
|
| 1286 |
if ( $this->has_shipping_address() ) { |
| 1287 |
$raw_address = apply_filters( 'storeengine/order_formatted_shipping_address', $raw_address, $this ); |
| 1288 |
$address = Countries::init()->get_formatted_address( $raw_address ); |
| 1289 |
} |
| 1290 |
|
| 1291 |
/** |
| 1292 |
* Filter orders formatted shipping address. |
| 1293 |
* |
| 1294 |
* @param string $address Formatted shipping address string. |
| 1295 |
* @param array $raw_address Raw shipping address. |
| 1296 |
* @param Order $order Order data. |
| 1297 |
*/ |
| 1298 |
return apply_filters( 'storeengine/order_get_formatted_shipping_address', $address ? $address : $empty_content, $raw_address, $this ); |
| 1299 |
} |
| 1300 |
|
| 1301 |
/** |
| 1302 |
* Returns true if the order has a billing address. |
| 1303 |
* |
| 1304 |
* @param string $context |
| 1305 |
* |
| 1306 |
* @return boolean |
| 1307 |
*/ |
| 1308 |
public function has_billing_address( string $context = 'view' ): bool { |
| 1309 |
return $this->get_billing_address_1( $context ) || $this->get_billing_address_2( $context ); |
| 1310 |
} |
| 1311 |
|
| 1312 |
/** |
| 1313 |
* Returns true if the order has a shipping address. |
| 1314 |
* |
| 1315 |
* @param string $context |
| 1316 |
* |
| 1317 |
* @return boolean |
| 1318 |
*/ |
| 1319 |
public function has_shipping_address( string $context = 'view' ): bool { |
| 1320 |
return $this->get_shipping_address_1( $context ) || $this->get_shipping_address_2( $context ); |
| 1321 |
} |
| 1322 |
|
| 1323 |
/** |
| 1324 |
* Gets information about whether stock was reduced. |
| 1325 |
* |
| 1326 |
* @param string $context What the value is for. Valid values are view and edit. |
| 1327 |
* |
| 1328 |
* @return bool |
| 1329 |
*/ |
| 1330 |
public function get_order_stock_reduced( string $context = 'view' ): bool { |
| 1331 |
return Formatting::string_to_bool( $this->get_prop( 'order_stock_reduced', $context ) ); |
| 1332 |
} |
| 1333 |
|
| 1334 |
/** |
| 1335 |
* Gets information about whether permissions were generated yet. |
| 1336 |
* |
| 1337 |
* @param string $context What the value is for. Valid values are view and edit. |
| 1338 |
* |
| 1339 |
* @return bool True if permissions were generated, false otherwise. |
| 1340 |
*/ |
| 1341 |
public function get_download_permissions_granted( string $context = 'view' ): bool { |
| 1342 |
return Formatting::string_to_bool( $this->get_prop( 'download_permissions_granted', $context ) ); |
| 1343 |
} |
| 1344 |
|
| 1345 |
public function get_auto_complete_digital_order( string $context = 'view' ): bool { |
| 1346 |
return Formatting::string_to_bool( $this->get_prop( 'auto_complete_digital_order', $context ) ); |
| 1347 |
} |
| 1348 |
|
| 1349 |
/** |
| 1350 |
* Whether email have been sent for this order. |
| 1351 |
* |
| 1352 |
* @param string $context What the value is for. Valid values are view and edit. |
| 1353 |
* |
| 1354 |
* @return bool |
| 1355 |
*/ |
| 1356 |
public function get_new_order_email_sent( string $context = 'view' ): bool { |
| 1357 |
return Formatting::string_to_bool( $this->get_prop( 'new_order_email_sent', $context ) ); |
| 1358 |
} |
| 1359 |
|
| 1360 |
/** |
| 1361 |
* Gets information about whether sales were recorded. |
| 1362 |
* |
| 1363 |
* @param string $context What the value is for. Valid values are view and edit. |
| 1364 |
* |
| 1365 |
* @return bool True if sales were recorded, false otherwise. |
| 1366 |
*/ |
| 1367 |
public function get_recorded_sales( string $context = 'view' ): bool { |
| 1368 |
return Formatting::string_to_bool( $this->get_prop( 'recorded_sales', $context ) ); |
| 1369 |
} |
| 1370 |
|
| 1371 |
/** |
| 1372 |
* @param string $context |
| 1373 |
* |
| 1374 |
* @return null|StoreengineDatetime |
| 1375 |
*/ |
| 1376 |
public function get_order_placed_date_gmt( string $context = 'view' ): ?StoreengineDatetime { |
| 1377 |
return $this->get_prop( 'order_placed_date_gmt', $context ); |
| 1378 |
} |
| 1379 |
|
| 1380 |
/** |
| 1381 |
* @param string $context |
| 1382 |
* |
| 1383 |
* @return null|StoreengineDatetime |
| 1384 |
*/ |
| 1385 |
public function get_order_placed_date( string $context = 'view' ): ?StoreengineDatetime { |
| 1386 |
return $this->get_prop( 'order_placed_date', $context ); |
| 1387 |
} |
| 1388 |
|
| 1389 |
/* |
| 1390 |
|-------------------------------------------------------------------------- |
| 1391 |
| Setters |
| 1392 |
|-------------------------------------------------------------------------- |
| 1393 |
| |
| 1394 |
| Functions for setting order data. These should not update anything in the |
| 1395 |
| database itself and should only change what is stored in the class |
| 1396 |
| object. However, for backwards compatibility pre 3.0.0 some of these |
| 1397 |
| setters may handle both. |
| 1398 |
| |
| 1399 |
*/ |
| 1400 |
|
| 1401 |
/** |
| 1402 |
* Sets a prop for a setter method. |
| 1403 |
* |
| 1404 |
* @param string $prop Name of prop to set. |
| 1405 |
* @param string $address_type Type of address; 'billing' or 'shipping'. |
| 1406 |
* @param ?string $value Value of the prop. |
| 1407 |
*/ |
| 1408 |
protected function set_address_prop( $prop, string $address_type, ?string $value ) { |
| 1409 |
if ( isset( $this->data[ $address_type ] ) && array_key_exists( $prop, $this->data[ $address_type ] ) ) { |
| 1410 |
if ( true === $this->object_read ) { |
| 1411 |
if ( $value !== $this->data[ $address_type ][ $prop ] || ( isset( $this->changes[ $address_type ] ) && array_key_exists( $prop, $this->changes[ $address_type ] ) ) ) { |
| 1412 |
$this->changes[ $address_type ][ $prop ] = $value; |
| 1413 |
} |
| 1414 |
} else { |
| 1415 |
$this->data[ $address_type ][ $prop ] = $value; |
| 1416 |
} |
| 1417 |
} |
| 1418 |
} |
| 1419 |
|
| 1420 |
/** |
| 1421 |
* Setter for billing address, expects the $address parameter to be key value pairs for individual address props. |
| 1422 |
* |
| 1423 |
* @param array $address Address to set. |
| 1424 |
* |
| 1425 |
* @return void |
| 1426 |
*/ |
| 1427 |
public function set_billing_address( array $address ) { |
| 1428 |
foreach ( $address as $key => $value ) { |
| 1429 |
$this->set_address_prop( $key, 'billing', $value ); |
| 1430 |
} |
| 1431 |
} |
| 1432 |
|
| 1433 |
/** |
| 1434 |
* Shortcut for calling set_billing_address. |
| 1435 |
* |
| 1436 |
* This is useful in scenarios where set_$prop_name is invoked, and since we store the billing address as 'billing' prop in data, it can be called directly. |
| 1437 |
* |
| 1438 |
* @param array $address Address to set. |
| 1439 |
* |
| 1440 |
* @return void |
| 1441 |
*/ |
| 1442 |
public function set_billing( array $address ) { |
| 1443 |
$this->set_billing_address( $address ); |
| 1444 |
} |
| 1445 |
|
| 1446 |
/** |
| 1447 |
* Setter for shipping address, expects the $address parameter to be key value pairs for individual address props. |
| 1448 |
* |
| 1449 |
* @param array $address Address to set. |
| 1450 |
* |
| 1451 |
* @return void |
| 1452 |
*/ |
| 1453 |
public function set_shipping_address( array $address ) { |
| 1454 |
foreach ( $address as $key => $value ) { |
| 1455 |
$this->set_address_prop( $key, 'shipping', $value ); |
| 1456 |
} |
| 1457 |
} |
| 1458 |
|
| 1459 |
/** |
| 1460 |
* Shortcut for calling set_shipping_address. This is useful in scenarios where set_$prop_name is invoked, and since we store the shipping address as 'shipping' prop in data, it can be called directly. |
| 1461 |
* |
| 1462 |
* @param array $address Address to set. |
| 1463 |
* |
| 1464 |
* @return void |
| 1465 |
*/ |
| 1466 |
public function set_shipping( array $address ) { |
| 1467 |
$this->set_shipping_address( $address ); |
| 1468 |
} |
| 1469 |
|
| 1470 |
/** |
| 1471 |
* Set billing first name. |
| 1472 |
* |
| 1473 |
* @param ?string $value Billing first name. |
| 1474 |
*/ |
| 1475 |
public function set_billing_first_name( ?string $value ) { |
| 1476 |
$this->set_address_prop( 'first_name', 'billing', $value ); |
| 1477 |
} |
| 1478 |
|
| 1479 |
/** |
| 1480 |
* Set billing last name. |
| 1481 |
* |
| 1482 |
* @param ?string $value Billing last name. |
| 1483 |
*/ |
| 1484 |
public function set_billing_last_name( ?string $value ) { |
| 1485 |
$this->set_address_prop( 'last_name', 'billing', $value ); |
| 1486 |
} |
| 1487 |
|
| 1488 |
/** |
| 1489 |
* Set billing company. |
| 1490 |
* |
| 1491 |
* @param ?string $value Billing company. |
| 1492 |
*/ |
| 1493 |
public function set_billing_company( ?string $value ) { |
| 1494 |
$this->set_address_prop( 'company', 'billing', $value ); |
| 1495 |
} |
| 1496 |
|
| 1497 |
/** |
| 1498 |
* Set billing address line 1. |
| 1499 |
* |
| 1500 |
* @param ?string $value Billing address line 1. |
| 1501 |
*/ |
| 1502 |
public function set_billing_address_1( ?string $value ) { |
| 1503 |
$this->set_address_prop( 'address_1', 'billing', $value ); |
| 1504 |
} |
| 1505 |
|
| 1506 |
/** |
| 1507 |
* Set billing address line 2. |
| 1508 |
* |
| 1509 |
* @param ?string $value Billing address line 2. |
| 1510 |
*/ |
| 1511 |
public function set_billing_address_2( ?string $value ) { |
| 1512 |
$this->set_address_prop( 'address_2', 'billing', $value ); |
| 1513 |
} |
| 1514 |
|
| 1515 |
/** |
| 1516 |
* Set billing city. |
| 1517 |
* |
| 1518 |
* @param ?string $value Billing city. |
| 1519 |
*/ |
| 1520 |
public function set_billing_city( ?string $value ) { |
| 1521 |
$this->set_address_prop( 'city', 'billing', $value ); |
| 1522 |
} |
| 1523 |
|
| 1524 |
/** |
| 1525 |
* Set billing state. |
| 1526 |
* |
| 1527 |
* @param ?string $value Billing state. |
| 1528 |
*/ |
| 1529 |
public function set_billing_state( ?string $value ) { |
| 1530 |
$this->set_address_prop( 'state', 'billing', $value ); |
| 1531 |
} |
| 1532 |
|
| 1533 |
/** |
| 1534 |
* Set billing postcode. |
| 1535 |
* |
| 1536 |
* @param ?string $value Billing postcode. |
| 1537 |
*/ |
| 1538 |
public function set_billing_postcode( ?string $value ) { |
| 1539 |
$this->set_address_prop( 'postcode', 'billing', $value ); |
| 1540 |
} |
| 1541 |
|
| 1542 |
/** |
| 1543 |
* Set billing country. |
| 1544 |
* |
| 1545 |
* @param ?string $value Billing country. |
| 1546 |
*/ |
| 1547 |
public function set_billing_country( ?string $value ) { |
| 1548 |
$this->set_address_prop( 'country', 'billing', $value ); |
| 1549 |
} |
| 1550 |
|
| 1551 |
/** |
| 1552 |
* Maybe set empty billing email to that of the user who owns the order. |
| 1553 |
*/ |
| 1554 |
protected function maybe_set_user_billing_email() { |
| 1555 |
$user = $this->get_user(); |
| 1556 |
if ( ! $this->get_billing_email() && $user ) { |
| 1557 |
try { |
| 1558 |
$this->set_billing_email( $user->user_email ); |
| 1559 |
} catch ( Exception $e ) { |
| 1560 |
unset( $e ); |
| 1561 |
} |
| 1562 |
} |
| 1563 |
} |
| 1564 |
|
| 1565 |
/** |
| 1566 |
* Set billing email. |
| 1567 |
* |
| 1568 |
* @param ?string $value Billing email. |
| 1569 |
* |
| 1570 |
* @throws StoreEngineException |
| 1571 |
*/ |
| 1572 |
public function set_billing_email( ?string $value = '' ) { |
| 1573 |
$value = $value ?? ''; |
| 1574 |
if ( $value && ! is_email( $value ) ) { |
| 1575 |
$this->error( 'order_invalid_billing_email', __( 'Invalid billing email address', 'storeengine' ) ); |
| 1576 |
} |
| 1577 |
|
| 1578 |
$this->set_address_prop( 'email', 'billing', sanitize_email( $value ) ); |
| 1579 |
} |
| 1580 |
|
| 1581 |
/** |
| 1582 |
* Set billing phone. |
| 1583 |
* |
| 1584 |
* @param ?string $value Billing phone. |
| 1585 |
*/ |
| 1586 |
public function set_billing_phone( ?string $value ) { |
| 1587 |
$this->set_address_prop( 'phone', 'billing', $value ); |
| 1588 |
} |
| 1589 |
|
| 1590 |
/** |
| 1591 |
* Set shipping first name. |
| 1592 |
* |
| 1593 |
* @param ?string $value Shipping first name. |
| 1594 |
*/ |
| 1595 |
public function set_shipping_first_name( ?string $value ) { |
| 1596 |
$this->set_address_prop( 'first_name', 'shipping', $value ); |
| 1597 |
} |
| 1598 |
|
| 1599 |
/** |
| 1600 |
* Set shipping last name. |
| 1601 |
* |
| 1602 |
* @param ?string $value Shipping last name. |
| 1603 |
*/ |
| 1604 |
public function set_shipping_last_name( ?string $value ) { |
| 1605 |
$this->set_address_prop( 'last_name', 'shipping', $value ); |
| 1606 |
} |
| 1607 |
|
| 1608 |
/** |
| 1609 |
* Set shipping company. |
| 1610 |
* |
| 1611 |
* @param ?string $value Shipping company. |
| 1612 |
*/ |
| 1613 |
public function set_shipping_company( ?string $value ) { |
| 1614 |
$this->set_address_prop( 'company', 'shipping', $value ); |
| 1615 |
} |
| 1616 |
|
| 1617 |
/** |
| 1618 |
* Set shipping address line 1. |
| 1619 |
* |
| 1620 |
* @param ?string $value Shipping address line 1. |
| 1621 |
*/ |
| 1622 |
public function set_shipping_address_1( ?string $value ) { |
| 1623 |
$this->set_address_prop( 'address_1', 'shipping', $value ); |
| 1624 |
} |
| 1625 |
|
| 1626 |
/** |
| 1627 |
* Set shipping address line 2. |
| 1628 |
* |
| 1629 |
* @param ?string $value Shipping address line 2. |
| 1630 |
*/ |
| 1631 |
public function set_shipping_address_2( ?string $value ) { |
| 1632 |
$this->set_address_prop( 'address_2', 'shipping', $value ); |
| 1633 |
} |
| 1634 |
|
| 1635 |
/** |
| 1636 |
* Set shipping city. |
| 1637 |
* |
| 1638 |
* @param ?string $value Shipping city. |
| 1639 |
*/ |
| 1640 |
public function set_shipping_city( ?string $value ) { |
| 1641 |
$this->set_address_prop( 'city', 'shipping', $value ); |
| 1642 |
} |
| 1643 |
|
| 1644 |
/** |
| 1645 |
* Set shipping state. |
| 1646 |
* |
| 1647 |
* @param ?string $value Shipping state. |
| 1648 |
*/ |
| 1649 |
public function set_shipping_state( ?string $value ) { |
| 1650 |
$this->set_address_prop( 'state', 'shipping', $value ); |
| 1651 |
} |
| 1652 |
|
| 1653 |
/** |
| 1654 |
* Set shipping postcode. |
| 1655 |
* |
| 1656 |
* @param ?string $value Shipping postcode. |
| 1657 |
*/ |
| 1658 |
public function set_shipping_postcode( ?string $value ) { |
| 1659 |
$this->set_address_prop( 'postcode', 'shipping', $value ); |
| 1660 |
} |
| 1661 |
|
| 1662 |
/** |
| 1663 |
* Set shipping country. |
| 1664 |
* |
| 1665 |
* @param ?string $value Shipping country. |
| 1666 |
*/ |
| 1667 |
public function set_shipping_country( ?string $value ) { |
| 1668 |
$this->set_address_prop( 'country', 'shipping', $value ); |
| 1669 |
} |
| 1670 |
|
| 1671 |
/** |
| 1672 |
* Set shipping phone. |
| 1673 |
* |
| 1674 |
* @param ?string $value Shipping phone. |
| 1675 |
*/ |
| 1676 |
public function set_shipping_phone( ?string $value ) { |
| 1677 |
$this->set_address_prop( 'phone', 'shipping', $value ); |
| 1678 |
} |
| 1679 |
|
| 1680 |
/** |
| 1681 |
* Set shipping phone. |
| 1682 |
* |
| 1683 |
* @param ?string $value Shipping phone. |
| 1684 |
* |
| 1685 |
* @throws StoreEngineException |
| 1686 |
*/ |
| 1687 |
public function set_shipping_email( ?string $value ) { |
| 1688 |
$value = $value ?? ''; |
| 1689 |
if ( $value && ! is_email( $value ) ) { |
| 1690 |
$this->error( 'order_invalid_shipping_email', __( 'Invalid shipping email address', 'storeengine' ) ); |
| 1691 |
} |
| 1692 |
|
| 1693 |
$this->set_address_prop( 'email', 'shipping', sanitize_email( $value ) ); |
| 1694 |
} |
| 1695 |
|
| 1696 |
/** |
| 1697 |
* Set the payment method. |
| 1698 |
* |
| 1699 |
* @param string|PaymentGateway $payment_method Supports a payment-gateway object for bw compatibility with < 3.0. |
| 1700 |
*/ |
| 1701 |
public function set_payment_method( $payment_method = '' ) { |
| 1702 |
if ( is_object( $payment_method ) ) { |
| 1703 |
$this->set_payment_method( $payment_method->id ); |
| 1704 |
$this->set_payment_method_title( $payment_method->get_title() ); |
| 1705 |
} elseif ( '' === $payment_method ) { |
| 1706 |
$this->set_prop( 'payment_method', '' ); |
| 1707 |
$this->set_prop( 'payment_method_title', '' ); |
| 1708 |
} else { |
| 1709 |
$this->set_prop( 'payment_method', $payment_method ); |
| 1710 |
} |
| 1711 |
} |
| 1712 |
|
| 1713 |
/** |
| 1714 |
* Set payment method title. |
| 1715 |
* |
| 1716 |
* @param ?string $value Payment method title. |
| 1717 |
*/ |
| 1718 |
public function set_payment_method_title( ?string $value ) { |
| 1719 |
$this->set_prop( 'payment_method_title', $value ); |
| 1720 |
} |
| 1721 |
|
| 1722 |
/** |
| 1723 |
* Check if the subscription has a payment gateway. |
| 1724 |
* |
| 1725 |
* @return bool |
| 1726 |
*/ |
| 1727 |
public function has_payment_gateway(): bool { |
| 1728 |
return (bool) Helper::get_payment_gateway_by_order( $this ); |
| 1729 |
} |
| 1730 |
|
| 1731 |
/** |
| 1732 |
* Set transaction id. |
| 1733 |
* |
| 1734 |
* @param ?string $value Transaction id. |
| 1735 |
*/ |
| 1736 |
public function set_transaction_id( ?string $value ) { |
| 1737 |
$this->set_prop( 'transaction_id', $value ); |
| 1738 |
} |
| 1739 |
|
| 1740 |
/** |
| 1741 |
* Set customer ip address. |
| 1742 |
* |
| 1743 |
* @param ?string $value Customer ip address. |
| 1744 |
*/ |
| 1745 |
public function set_ip_address( ?string $value ) { |
| 1746 |
$this->set_prop( 'ip_address', $value ); |
| 1747 |
} |
| 1748 |
|
| 1749 |
/** |
| 1750 |
* Set customer user agent. |
| 1751 |
* |
| 1752 |
* @param ?string $value Customer user agent. |
| 1753 |
*/ |
| 1754 |
public function set_user_agent( ?string $value ) { |
| 1755 |
$this->set_prop( 'user_agent', $value ); |
| 1756 |
} |
| 1757 |
|
| 1758 |
/** |
| 1759 |
* Set created via. |
| 1760 |
* |
| 1761 |
* @param ?string $value Created via. |
| 1762 |
*/ |
| 1763 |
public function set_created_via( ?string $value ) { |
| 1764 |
$this->set_prop( 'created_via', $value ); |
| 1765 |
} |
| 1766 |
|
| 1767 |
/** |
| 1768 |
* Set customer note. |
| 1769 |
* |
| 1770 |
* @param ?string $value Customer note. |
| 1771 |
*/ |
| 1772 |
public function set_customer_note( ?string $value ) { |
| 1773 |
$this->set_prop( 'customer_note', $value ); |
| 1774 |
} |
| 1775 |
|
| 1776 |
/** |
| 1777 |
* Set cart hash. |
| 1778 |
* |
| 1779 |
* @param string $value Cart hash. |
| 1780 |
*/ |
| 1781 |
public function set_cart_hash( $value ) { |
| 1782 |
$this->set_hash( $value ); |
| 1783 |
} |
| 1784 |
|
| 1785 |
/** |
| 1786 |
* Set cart hash. |
| 1787 |
* |
| 1788 |
* @param string $value Cart hash. |
| 1789 |
*/ |
| 1790 |
public function set_hash( $value ) { |
| 1791 |
$this->set_prop( 'hash', $value ); |
| 1792 |
} |
| 1793 |
|
| 1794 |
/** |
| 1795 |
* Stores information about whether stock was reduced. |
| 1796 |
* |
| 1797 |
* @param bool|string $value True if stock was reduced, false if not. |
| 1798 |
* |
| 1799 |
* @return void |
| 1800 |
*/ |
| 1801 |
public function set_order_stock_reduced( $value ) { |
| 1802 |
$this->set_prop( 'order_stock_reduced', Formatting::string_to_bool( $value ) ); |
| 1803 |
} |
| 1804 |
|
| 1805 |
/** |
| 1806 |
* Stores information about whether permissions were generated yet. |
| 1807 |
* |
| 1808 |
* @param bool|string $value True if permissions were generated, false if not. |
| 1809 |
* |
| 1810 |
* @return void |
| 1811 |
*/ |
| 1812 |
public function set_download_permissions_granted( $value ) { |
| 1813 |
$this->set_prop( 'download_permissions_granted', Formatting::string_to_bool( $value ) ); |
| 1814 |
} |
| 1815 |
|
| 1816 |
public function set_auto_complete_digital_order( $value ) { |
| 1817 |
$this->set_prop( 'auto_complete_digital_order', Formatting::string_to_bool( $value ) ); |
| 1818 |
} |
| 1819 |
|
| 1820 |
/** |
| 1821 |
* Stores information about whether email was sent. |
| 1822 |
* |
| 1823 |
* @param bool|string $value True if email was sent, false if not. |
| 1824 |
* |
| 1825 |
* @return void |
| 1826 |
*/ |
| 1827 |
public function set_new_order_email_sent( $value ) { |
| 1828 |
$this->set_prop( 'new_order_email_sent', Formatting::string_to_bool( $value ) ); |
| 1829 |
} |
| 1830 |
|
| 1831 |
/** |
| 1832 |
* Stores information about whether sales were recorded. |
| 1833 |
* |
| 1834 |
* @param bool|string $value True if sales were recorded, false if not. |
| 1835 |
* |
| 1836 |
* @return void |
| 1837 |
*/ |
| 1838 |
public function set_recorded_sales( $value ) { |
| 1839 |
$this->set_prop( 'recorded_sales', Formatting::string_to_bool( $value ) ); |
| 1840 |
} |
| 1841 |
|
| 1842 |
/* |
| 1843 |
|-------------------------------------------------------------------------- |
| 1844 |
| Conditionals |
| 1845 |
|-------------------------------------------------------------------------- |
| 1846 |
| |
| 1847 |
| Checks if a condition is true or false. |
| 1848 |
| |
| 1849 |
*/ |
| 1850 |
|
| 1851 |
/** |
| 1852 |
* Check if an order key is valid. |
| 1853 |
* |
| 1854 |
* @param string $key Order key. |
| 1855 |
* |
| 1856 |
* @return bool |
| 1857 |
*/ |
| 1858 |
public function key_is_valid( $key ) { |
| 1859 |
return hash_equals( $this->get_order_key(), $key ); |
| 1860 |
} |
| 1861 |
|
| 1862 |
/** |
| 1863 |
* See if order matches cart_hash. |
| 1864 |
* |
| 1865 |
* @param string $cart_hash Cart hash. |
| 1866 |
* |
| 1867 |
* @return bool |
| 1868 |
*/ |
| 1869 |
public function has_cart_hash( $cart_hash = '' ) { |
| 1870 |
return hash_equals( $this->get_cart_hash(), $cart_hash ); |
| 1871 |
} |
| 1872 |
|
| 1873 |
/** |
| 1874 |
* Checks if an order can be edited, specifically for use on the Edit Order screen. |
| 1875 |
* |
| 1876 |
* @return bool |
| 1877 |
*/ |
| 1878 |
public function is_editable(): bool { |
| 1879 |
$editable_statuses = [ |
| 1880 |
OrderStatus::PAYMENT_PENDING, |
| 1881 |
OrderStatus::ON_HOLD, |
| 1882 |
OrderStatus::AUTO_DRAFT, |
| 1883 |
]; |
| 1884 |
|
| 1885 |
/** |
| 1886 |
* Filter to check if an order is editable. |
| 1887 |
* |
| 1888 |
* @param bool $is_editable Is the order editable. |
| 1889 |
* @param Order $order Order object. |
| 1890 |
*/ |
| 1891 |
return apply_filters( |
| 1892 |
'storeengine/order/is_editable', |
| 1893 |
in_array( $this->get_status(), $editable_statuses, true ), |
| 1894 |
$this |
| 1895 |
); |
| 1896 |
} |
| 1897 |
|
| 1898 |
/** |
| 1899 |
* Checks if product download is permitted. |
| 1900 |
* |
| 1901 |
* @return bool |
| 1902 |
*/ |
| 1903 |
public function is_download_permitted(): bool { |
| 1904 |
/** |
| 1905 |
* Filter to check if an order is downloadable. |
| 1906 |
* |
| 1907 |
* @param bool $is_download_permitted Is the order downloadable. |
| 1908 |
* @param Order $this Order object. |
| 1909 |
*/ |
| 1910 |
return apply_filters( 'storeengine/order_is_download_permitted', $this->has_status( OrderStatus::COMPLETED ) || ( 'yes' === get_option( 'storeengine/downloads_grant_access_after_payment' ) && $this->has_status( OrderStatus::PROCESSING ) ), $this ); |
| 1911 |
} |
| 1912 |
|
| 1913 |
/** |
| 1914 |
* Checks if an order needs display the shipping address, based on shipping method. |
| 1915 |
* |
| 1916 |
* @return bool |
| 1917 |
*/ |
| 1918 |
public function needs_shipping_address(): bool { |
| 1919 |
if ( 'no' === get_option( 'storeengine/calc_shipping' ) ) { |
| 1920 |
return false; |
| 1921 |
} |
| 1922 |
|
| 1923 |
$hide = apply_filters( 'storeengine/order_hide_shipping_address', [ 'local_pickup' ], $this ); |
| 1924 |
$needs_address = false; |
| 1925 |
|
| 1926 |
foreach ( $this->get_shipping_methods() as $shipping_method ) { |
| 1927 |
$shipping_method_id = $shipping_method->get_method_id(); |
| 1928 |
|
| 1929 |
if ( ! in_array( $shipping_method_id, $hide, true ) ) { |
| 1930 |
$needs_address = true; |
| 1931 |
break; |
| 1932 |
} |
| 1933 |
} |
| 1934 |
|
| 1935 |
return apply_filters( 'storeengine/order_needs_shipping_address', $needs_address, $hide, $this ); |
| 1936 |
} |
| 1937 |
|
| 1938 |
/** |
| 1939 |
* Returns true if the order contains a downloadable product. |
| 1940 |
* |
| 1941 |
* @return bool |
| 1942 |
*/ |
| 1943 |
public function has_downloadable_item() { |
| 1944 |
foreach ( $this->get_items() as $item ) { |
| 1945 |
if ( $item->is_type( 'line_item' ) ) { |
| 1946 |
$product = $item->get_product(); |
| 1947 |
|
| 1948 |
if ( $product && $product->has_file() ) { |
| 1949 |
return true; |
| 1950 |
} |
| 1951 |
} |
| 1952 |
} |
| 1953 |
|
| 1954 |
return false; |
| 1955 |
} |
| 1956 |
|
| 1957 |
/** |
| 1958 |
* Get downloads from all line items for this order. |
| 1959 |
* |
| 1960 |
* @return array |
| 1961 |
*/ |
| 1962 |
public function get_downloadable_items(): array { |
| 1963 |
$downloads = []; |
| 1964 |
|
| 1965 |
foreach ( $this->get_items() as $item ) { |
| 1966 |
if ( ! is_object( $item ) ) { |
| 1967 |
continue; |
| 1968 |
} |
| 1969 |
|
| 1970 |
// Check item refunds. |
| 1971 |
$refunded_qty = abs( $this->get_qty_refunded_for_item( $item->get_id() ) ); |
| 1972 |
if ( $refunded_qty && $item->get_quantity() === $refunded_qty ) { |
| 1973 |
continue; |
| 1974 |
} |
| 1975 |
|
| 1976 |
if ( $item->is_type( 'line_item' ) ) { |
| 1977 |
$item_downloads = $item->get_item_downloads(); |
| 1978 |
$product = $item->get_product(); |
| 1979 |
if ( $product && $item_downloads ) { |
| 1980 |
foreach ( $item_downloads as $file ) { |
| 1981 |
$downloads[] = [ |
| 1982 |
'download_url' => $file['download_url'], |
| 1983 |
'download_id' => $file['id'], |
| 1984 |
'product_id' => $product->get_id(), |
| 1985 |
'product_name' => $product->get_name(), |
| 1986 |
'product_url' => $product->is_visible() ? $product->get_permalink() : '', |
| 1987 |
'download_name' => $file['name'], |
| 1988 |
'order_id' => $this->get_id(), |
| 1989 |
'order_key' => $this->get_order_key(), |
| 1990 |
'downloads_remaining' => $file['downloads_remaining'], |
| 1991 |
'access_expires' => $file['access_expires'], |
| 1992 |
'file' => [ |
| 1993 |
'name' => $file['name'], |
| 1994 |
'file' => $file['file'], |
| 1995 |
], |
| 1996 |
]; |
| 1997 |
} |
| 1998 |
} |
| 1999 |
} |
| 2000 |
} |
| 2001 |
|
| 2002 |
return apply_filters( 'storeengine/order_get_downloadable_items', $downloads, $this ); |
| 2003 |
} |
| 2004 |
|
| 2005 |
/** |
| 2006 |
* Checks if an order needs payment, based on status and order total. |
| 2007 |
* |
| 2008 |
* @return bool |
| 2009 |
*/ |
| 2010 |
public function needs_payment(): bool { |
| 2011 |
/** |
| 2012 |
* Filter the valid order statuses for payment. |
| 2013 |
* |
| 2014 |
* @param array $valid_order_statuses Array of valid order statuses for payment. |
| 2015 |
* @param Order $order Order object. |
| 2016 |
*/ |
| 2017 |
$paid_status = $this->get_paid_status(); |
| 2018 |
$valid_unpaid_statuses = [ 'unpaid', 'failed' ]; |
| 2019 |
$valid_order_statuses = apply_filters( 'storeengine/order/valid_unpaid_statuses', $valid_unpaid_statuses, $this ); |
| 2020 |
$valid_statuses_for_payment = [ OrderStatus::PAYMENT_PENDING, OrderStatus::PAYMENT_FAILED ]; |
| 2021 |
$valid_statuses_for_payment = apply_filters( 'storeengine/order/valid_statuses_for_payment', $valid_statuses_for_payment, $this ); |
| 2022 |
$need_payment = ( |
| 2023 |
in_array( $paid_status, $valid_order_statuses, true ) && |
| 2024 |
in_array( $this->get_status(), $valid_statuses_for_payment, true ) && |
| 2025 |
$this->get_total() > 0 |
| 2026 |
); |
| 2027 |
|
| 2028 |
return apply_filters( 'storeengine/order_needs_payment', $need_payment, $this, $valid_order_statuses ); |
| 2029 |
} |
| 2030 |
|
| 2031 |
/** |
| 2032 |
* See if the order needs processing before it can be completed. |
| 2033 |
* |
| 2034 |
* Orders which only contain virtual, downloadable items do not need admin |
| 2035 |
* intervention. |
| 2036 |
* |
| 2037 |
* Uses a transient so these calls are not repeated multiple times, and because |
| 2038 |
* once the order is processed this code/transient does not need to persist. |
| 2039 |
* |
| 2040 |
* @return bool |
| 2041 |
*/ |
| 2042 |
public function needs_processing(): bool { |
| 2043 |
$transient_name = 'storeengine/order_' . $this->get_id() . '_needs_processing'; |
| 2044 |
$needs_processing = get_transient( $transient_name ); |
| 2045 |
|
| 2046 |
if ( false === $needs_processing ) { |
| 2047 |
$needs_processing = 0; |
| 2048 |
|
| 2049 |
if ( count( $this->get_items() ) > 0 ) { |
| 2050 |
foreach ( $this->get_items() as $item ) { |
| 2051 |
if ( $item->is_type( 'line_item' ) ) { |
| 2052 |
/** @var $product AbstractProduct */ |
| 2053 |
$product = $item->get_product(); |
| 2054 |
|
| 2055 |
if ( ! $product ) { |
| 2056 |
continue; |
| 2057 |
} |
| 2058 |
|
| 2059 |
$virtual_downloadable_item = $product->is_downloadable() && $product->is_virtual(); |
| 2060 |
|
| 2061 |
if ( apply_filters( 'storeengine/order/item_needs_processing', ! $virtual_downloadable_item, $product, $this->get_id() ) ) { |
| 2062 |
$needs_processing = 1; |
| 2063 |
break; |
| 2064 |
} |
| 2065 |
} |
| 2066 |
} |
| 2067 |
} |
| 2068 |
|
| 2069 |
set_transient( $transient_name, $needs_processing, DAY_IN_SECONDS ); |
| 2070 |
} |
| 2071 |
|
| 2072 |
return 1 === absint( $needs_processing ); |
| 2073 |
} |
| 2074 |
|
| 2075 |
/* |
| 2076 |
|-------------------------------------------------------------------------- |
| 2077 |
| URLs and Endpoints |
| 2078 |
|-------------------------------------------------------------------------- |
| 2079 |
*/ |
| 2080 |
|
| 2081 |
/** |
| 2082 |
* Generates a URL so that a customer can pay for their (unpaid - pending) order. Pass 'true' for the checkout version which doesn't offer gateway choices. |
| 2083 |
* |
| 2084 |
* @param bool $on_checkout If on checkout. |
| 2085 |
* |
| 2086 |
* @return string |
| 2087 |
*/ |
| 2088 |
public function get_checkout_payment_url( bool $on_checkout = false ): string { |
| 2089 |
$pay_url = Helper::get_endpoint_url( 'order-pay', $this->get_id(), Helper::get_checkout_url() ); |
| 2090 |
|
| 2091 |
if ( $on_checkout ) { |
| 2092 |
$pay_url = add_query_arg( 'key', $this->get_order_key(), $pay_url ); |
| 2093 |
} else { |
| 2094 |
$pay_url = add_query_arg( [ |
| 2095 |
'pay_for_order' => 'true', |
| 2096 |
'key' => $this->get_order_key(), |
| 2097 |
], $pay_url ); |
| 2098 |
} |
| 2099 |
|
| 2100 |
return apply_filters( 'storeengine/get_checkout_payment_url', $pay_url, $this ); |
| 2101 |
} |
| 2102 |
|
| 2103 |
/** |
| 2104 |
* Generates a URL for the thanks page (order received). |
| 2105 |
* |
| 2106 |
* @return string |
| 2107 |
*/ |
| 2108 |
public function get_checkout_order_received_url(): string { |
| 2109 |
$order_received_url = add_query_arg( 'order_hash', $this->get_order_key(), Helper::get_thankyou_page_url() ); |
| 2110 |
|
| 2111 |
return apply_filters( 'storeengine/order/get_checkout_order_received_url', $order_received_url, $this ); |
| 2112 |
} |
| 2113 |
|
| 2114 |
/** |
| 2115 |
* Generates a URL so that a customer can cancel their (unpaid - pending) order. |
| 2116 |
* |
| 2117 |
* @param string $redirect Redirect URL. |
| 2118 |
* |
| 2119 |
* @return string |
| 2120 |
*/ |
| 2121 |
public function get_cancel_order_url( string $redirect = '' ): string { |
| 2122 |
/** |
| 2123 |
* Filter the URL to cancel the order in the frontend. |
| 2124 |
* |
| 2125 |
* @param string $url |
| 2126 |
* @param Order $order Order data. |
| 2127 |
* @param string $redirect Redirect URL. |
| 2128 |
*/ |
| 2129 |
return apply_filters( |
| 2130 |
'storeengine/order/get_cancel_order_url', |
| 2131 |
wp_nonce_url( |
| 2132 |
add_query_arg( |
| 2133 |
[ |
| 2134 |
'cancel_order' => 'true', |
| 2135 |
'order' => $this->get_order_key(), |
| 2136 |
'order_id' => $this->get_id(), |
| 2137 |
'redirect' => $redirect, |
| 2138 |
], |
| 2139 |
$this->get_cancel_endpoint() |
| 2140 |
), |
| 2141 |
'storeengine-cancel_order' |
| 2142 |
), |
| 2143 |
$this, |
| 2144 |
$redirect |
| 2145 |
); |
| 2146 |
} |
| 2147 |
|
| 2148 |
/** |
| 2149 |
* Generates a raw (unescaped) cancel-order URL for use by payment gateways. |
| 2150 |
* |
| 2151 |
* @param string $redirect Redirect URL. |
| 2152 |
* |
| 2153 |
* @return string The unescaped cancel-order URL. |
| 2154 |
*/ |
| 2155 |
public function get_cancel_order_url_raw( string $redirect = '' ): string { |
| 2156 |
/** |
| 2157 |
* Filter the raw URL to cancel the order in the frontend. |
| 2158 |
* |
| 2159 |
* @param string $url |
| 2160 |
* @param Order $order Order data. |
| 2161 |
* @param string $redirect Redirect URL. |
| 2162 |
*/ |
| 2163 |
return apply_filters( |
| 2164 |
'storeengine/order/get_cancel_order_url_raw', |
| 2165 |
add_query_arg( |
| 2166 |
[ |
| 2167 |
'cancel_order' => 'true', |
| 2168 |
'order' => $this->get_order_key(), |
| 2169 |
'order_id' => $this->get_id(), |
| 2170 |
'redirect' => $redirect, |
| 2171 |
'_wpnonce' => wp_create_nonce( 'storeengine-cancel_order' ), |
| 2172 |
], |
| 2173 |
$this->get_cancel_endpoint() |
| 2174 |
), |
| 2175 |
$this, |
| 2176 |
$redirect |
| 2177 |
); |
| 2178 |
} |
| 2179 |
|
| 2180 |
/** |
| 2181 |
* Helper method to return the cancel endpoint. |
| 2182 |
* |
| 2183 |
* @return string the cancel endpoint; either the cart page or the home page. |
| 2184 |
*/ |
| 2185 |
public function get_cancel_endpoint(): string { |
| 2186 |
$cancel_endpoint = Helper::get_cart_url(); |
| 2187 |
if ( ! $cancel_endpoint ) { |
| 2188 |
$cancel_endpoint = home_url(); |
| 2189 |
} |
| 2190 |
|
| 2191 |
if ( false === strpos( $cancel_endpoint, '?' ) ) { |
| 2192 |
$cancel_endpoint = trailingslashit( $cancel_endpoint ); |
| 2193 |
} |
| 2194 |
|
| 2195 |
return $cancel_endpoint; |
| 2196 |
} |
| 2197 |
|
| 2198 |
/** |
| 2199 |
* Generates a URL to view an order from the myaccount page. |
| 2200 |
* |
| 2201 |
* @return string |
| 2202 |
*/ |
| 2203 |
public function get_view_order_url(): string { |
| 2204 |
return apply_filters( 'storeengine/order/get_view_url', Helper::get_account_endpoint_url( 'orders', $this->get_id() ), $this ); |
| 2205 |
} |
| 2206 |
|
| 2207 |
/** |
| 2208 |
* Get the URL to edit the order in the backend. |
| 2209 |
* |
| 2210 |
* @return string |
| 2211 |
*/ |
| 2212 |
public function get_edit_order_url(): string { |
| 2213 |
$edit_url = admin_url( 'admin.php?page=storeengine-orders&id=' . $this->get_id() . '&action=edit' ); |
| 2214 |
|
| 2215 |
/** |
| 2216 |
* Filter the URL to edit the order in the backend. |
| 2217 |
*/ |
| 2218 |
return apply_filters( 'storeengine/order/get_edit_url', $edit_url, $this ); |
| 2219 |
} |
| 2220 |
|
| 2221 |
/* |
| 2222 |
|-------------------------------------------------------------------------- |
| 2223 |
| Order notes. |
| 2224 |
|-------------------------------------------------------------------------- |
| 2225 |
*/ |
| 2226 |
|
| 2227 |
/** |
| 2228 |
* Adds a note (comment) to the order. Order must exist. |
| 2229 |
* |
| 2230 |
* @param string $note Note to add. |
| 2231 |
* @param int|string $is_customer_note Is this a note for the customer?. |
| 2232 |
* @param bool $added_by_user Was the note added by a user?. |
| 2233 |
* |
| 2234 |
* @return int|false Comment ID. |
| 2235 |
*/ |
| 2236 |
public function add_order_note( string $note, $is_customer_note = 0, bool $added_by_user = false ) { |
| 2237 |
if ( ! $this->get_id() ) { |
| 2238 |
return 0; |
| 2239 |
} |
| 2240 |
|
| 2241 |
$is_customer_note = absint( $is_customer_note ); |
| 2242 |
|
| 2243 |
// @TODO edit_shop_orders cap doesn't exists in storeengine. |
| 2244 |
|
| 2245 |
if ( is_user_logged_in() && current_user_can( 'edit_shop_orders', $this->get_id() ) && $added_by_user ) { |
| 2246 |
$user = get_user_by( 'id', get_current_user_id() ); |
| 2247 |
$comment_author = $user->display_name; |
| 2248 |
$comment_author_email = $user->user_email; |
| 2249 |
} else { |
| 2250 |
$comment_author = _x( 'StoreEngine', 'System Comment Author', 'storeengine' ); |
| 2251 |
$comment_author_email = strtolower( $comment_author ) . '@' . wp_parse_url( get_site_url(), PHP_URL_HOST ); |
| 2252 |
$comment_author_email = sanitize_email( $comment_author_email ); |
| 2253 |
} |
| 2254 |
|
| 2255 |
$commentdata = apply_filters( |
| 2256 |
'storeengine/new_order_note_data', |
| 2257 |
[ |
| 2258 |
'comment_post_ID' => $this->get_id(), |
| 2259 |
'comment_author' => $comment_author, |
| 2260 |
'comment_author_email' => $comment_author_email, |
| 2261 |
'comment_author_url' => '', |
| 2262 |
'comment_content' => $note, |
| 2263 |
'comment_agent' => 'StoreEngine', |
| 2264 |
'comment_type' => 'order_note', |
| 2265 |
'comment_parent' => 0, |
| 2266 |
'comment_approved' => 1, |
| 2267 |
], |
| 2268 |
[ |
| 2269 |
'order_id' => $this->get_id(), |
| 2270 |
'is_customer_note' => $is_customer_note, |
| 2271 |
] |
| 2272 |
); |
| 2273 |
|
| 2274 |
$comment_id = wp_insert_comment( $commentdata ); |
| 2275 |
|
| 2276 |
if ( ! $comment_id ) { |
| 2277 |
return false; |
| 2278 |
} |
| 2279 |
|
| 2280 |
if ( $is_customer_note ) { |
| 2281 |
add_comment_meta( $comment_id, 'is_customer_note', 1 ); |
| 2282 |
|
| 2283 |
/** |
| 2284 |
* Action hook fired after an order note is added for the customer. |
| 2285 |
* |
| 2286 |
* @param string $note Comment data. |
| 2287 |
* @param Order $this Comment data. |
| 2288 |
*/ |
| 2289 |
do_action( 'storeengine/order/new_customer_note', $note, $this ); |
| 2290 |
} |
| 2291 |
|
| 2292 |
/** |
| 2293 |
* Action hook fired after an order note is added. |
| 2294 |
* |
| 2295 |
* @param int $comment_id Order note ID. |
| 2296 |
* @param Order $this Order object. |
| 2297 |
*/ |
| 2298 |
do_action( 'storeengine/order/note_added', $comment_id, $this ); |
| 2299 |
|
| 2300 |
return $comment_id; |
| 2301 |
} |
| 2302 |
|
| 2303 |
/** |
| 2304 |
* Add an order note for status transition |
| 2305 |
* |
| 2306 |
* @param string $note Note to be added giving status transition from and to details. |
| 2307 |
* @param bool $transition Details of the status transition. |
| 2308 |
* |
| 2309 |
* @return int Comment ID. |
| 2310 |
* @uses self::add_order_note() |
| 2311 |
*/ |
| 2312 |
protected function add_status_transition_note( $note, $transition ) { |
| 2313 |
return $this->add_order_note( trim( $transition['note'] . ' ' . $note ), 0, $transition['manual'] ); |
| 2314 |
} |
| 2315 |
|
| 2316 |
/** |
| 2317 |
* List order notes (public) for the customer. |
| 2318 |
* |
| 2319 |
* @return WP_Comment[] |
| 2320 |
*/ |
| 2321 |
public function get_customer_order_notes(): array { |
| 2322 |
return $this->get_order_notes( 'customer' ); |
| 2323 |
} |
| 2324 |
|
| 2325 |
/** |
| 2326 |
* List order notes. |
| 2327 |
* |
| 2328 |
* @param string $customer_notes switch for customer (public) notes or internal (admin) notes. Default all notes. |
| 2329 |
* @param bool $ids |
| 2330 |
* |
| 2331 |
* @return WP_Comment[] |
| 2332 |
*/ |
| 2333 |
public function get_order_notes( string $customer_notes = '', bool $ids = false ): array { |
| 2334 |
$notes = []; |
| 2335 |
|
| 2336 |
if ( ! $this->get_id() ) { |
| 2337 |
return $notes; |
| 2338 |
} |
| 2339 |
|
| 2340 |
$args = [ |
| 2341 |
'post_id' => $this->get_id(), |
| 2342 |
'orderby' => 'comment_ID', |
| 2343 |
'order' => 'DESC', |
| 2344 |
'approve' => 'approve', |
| 2345 |
'meta_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query |
| 2346 |
'relation' => 'AND', |
| 2347 |
], |
| 2348 |
]; |
| 2349 |
|
| 2350 |
// type->order_note & author->StoreEngine conditions are added via filter below. |
| 2351 |
|
| 2352 |
if ( $ids ) { |
| 2353 |
$args['fields'] = 'ids'; |
| 2354 |
} |
| 2355 |
|
| 2356 |
if ( 'customer' === $customer_notes ) { |
| 2357 |
$args['meta_query'][] = [ |
| 2358 |
'key' => 'is_customer_note', |
| 2359 |
'value' => 1, |
| 2360 |
'compare' => '=', |
| 2361 |
'type' => 'UNSIGNED', |
| 2362 |
]; |
| 2363 |
} elseif ( 'internal' === $customer_notes ) { |
| 2364 |
$args['meta_query'][] = [ |
| 2365 |
'key' => 'is_customer_note', |
| 2366 |
'compare' => 'NOT EXISTS', |
| 2367 |
]; |
| 2368 |
} |
| 2369 |
|
| 2370 |
remove_filter( 'comments_clauses', [ Hooks::class, 'exclude_order_comments' ] ); |
| 2371 |
add_filter( 'comments_clauses', [ Hooks::class, 'include_order_comments' ] ); |
| 2372 |
|
| 2373 |
$comments = get_comments( $args ); |
| 2374 |
|
| 2375 |
foreach ( $comments as $comment ) { |
| 2376 |
$comment->comment_content = make_clickable( $comment->comment_content ); |
| 2377 |
$notes[] = $comment; |
| 2378 |
} |
| 2379 |
|
| 2380 |
remove_filter( 'comments_clauses', [ Hooks::class, 'include_order_comments' ] ); |
| 2381 |
add_filter( 'comments_clauses', [ Hooks::class, 'exclude_order_comments' ] ); |
| 2382 |
|
| 2383 |
return array_filter( array_map( [ __CLASS__, 'get_order_note' ], $notes ) ); |
| 2384 |
} |
| 2385 |
|
| 2386 |
/** |
| 2387 |
* Get an order note. |
| 2388 |
* |
| 2389 |
* @param int|WP_Comment $data Note ID (or WP_Comment instance for internal use only). |
| 2390 |
* |
| 2391 |
* @return stdClass|null Object with order note details or null when does not exists. |
| 2392 |
* @throws StoreEngineException |
| 2393 |
*/ |
| 2394 |
public static function get_order_note( $data ) { |
| 2395 |
if ( is_numeric( $data ) ) { |
| 2396 |
$data = get_comment( $data ); |
| 2397 |
} |
| 2398 |
|
| 2399 |
if ( ! is_a( $data, 'WP_Comment' ) ) { |
| 2400 |
return null; |
| 2401 |
} |
| 2402 |
|
| 2403 |
// @TODO use OrderNote object. |
| 2404 |
return (object) apply_filters( 'storeengine/order/get_order_note', [ |
| 2405 |
'id' => (int) $data->comment_ID, |
| 2406 |
'date_created' => $data->comment_date_gmt, |
| 2407 |
//'date_created' => Formatting::string_to_datetime( $data->comment_date ), |
| 2408 |
'content' => $data->comment_content, |
| 2409 |
'customer_note' => (bool) get_comment_meta( $data->comment_ID, 'is_customer_note', true ), |
| 2410 |
'added_by' => __( 'StoreEngine', 'storeengine' ) === $data->comment_author ? 'system' : $data->comment_author, |
| 2411 |
'order_id' => absint( $data->comment_post_ID ), |
| 2412 |
], $data ); |
| 2413 |
} |
| 2414 |
|
| 2415 |
/** |
| 2416 |
* Delete an order note. |
| 2417 |
* |
| 2418 |
* @param int $note_id Order note. |
| 2419 |
* |
| 2420 |
* @return bool True on success, false on failure. |
| 2421 |
* @throws StoreEngineException |
| 2422 |
*/ |
| 2423 |
public static function delete_order_note( int $note_id ): bool { |
| 2424 |
$note = self::get_order_note( $note_id ); |
| 2425 |
if ( $note && wp_delete_comment( $note_id, true ) ) { |
| 2426 |
/** |
| 2427 |
* Action hook fired after an order note is deleted. |
| 2428 |
* |
| 2429 |
* @param int $note_id Order note ID. |
| 2430 |
* @param stdClass $note Object with the deleted order note details. |
| 2431 |
*/ |
| 2432 |
do_action( 'storeengine/order/note_deleted', $note_id, $note ); |
| 2433 |
|
| 2434 |
return true; |
| 2435 |
} |
| 2436 |
|
| 2437 |
return false; |
| 2438 |
} |
| 2439 |
|
| 2440 |
/* |
| 2441 |
|-------------------------------------------------------------------------- |
| 2442 |
| Refunds |
| 2443 |
|-------------------------------------------------------------------------- |
| 2444 |
*/ |
| 2445 |
|
| 2446 |
/** |
| 2447 |
* Get order refunds. |
| 2448 |
* |
| 2449 |
* @return Refund[] of Order_Refund objects |
| 2450 |
* @throws StoreEngineException |
| 2451 |
*/ |
| 2452 |
public function get_refunds(): array { |
| 2453 |
$cache_key = Caching::get_cache_prefix( 'orders' ) . 'refunds' . $this->get_id(); |
| 2454 |
$ids = wp_cache_get( $cache_key, $this->cache_group ); |
| 2455 |
$refunds = []; |
| 2456 |
|
| 2457 |
if ( false === $ids || ! is_array( $ids ) ) { |
| 2458 |
$query = ( new Refund() )->query(); |
| 2459 |
// @TODO cache properly to prevent 2x query while creating refund object. |
| 2460 |
// phpcs:disable PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared -- query prepared in refund class. |
| 2461 |
$results = $this->wpdb->get_results( $this->wpdb->prepare( "$query WHERE o.parent_order_id = %d AND o.type = %s GROUP BY o_id;", $this->get_id(), 'refund_order' ), ARRAY_A ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- common query prepared |
| 2462 |
// phpcs:enable PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared -- query prepared in refund class. |
| 2463 |
|
| 2464 |
if ( ! $results ) { |
| 2465 |
return $refunds; |
| 2466 |
} |
| 2467 |
|
| 2468 |
$ids = array_unique( array_filter( array_map( 'absint', array_column( $results, 'o_id' ) ) ) ); |
| 2469 |
wp_cache_set( $cache_key, $ids, $this->cache_group ); |
| 2470 |
} |
| 2471 |
|
| 2472 |
foreach ( $ids as $id ) { |
| 2473 |
$refunds[] = new Refund( $id ); |
| 2474 |
} |
| 2475 |
|
| 2476 |
return $refunds; |
| 2477 |
} |
| 2478 |
|
| 2479 |
/** |
| 2480 |
* Get amount already refunded. |
| 2481 |
* |
| 2482 |
* @param bool $refresh |
| 2483 |
* |
| 2484 |
* @return float|int |
| 2485 |
*/ |
| 2486 |
public function get_total_refunded( bool $refresh = false ) { |
| 2487 |
$cache_key = Caching::get_cache_prefix( 'orders' ) . 'total_refunded' . $this->get_id(); |
| 2488 |
$cached_data = wp_cache_get( $cache_key, $this->cache_group ); |
| 2489 |
|
| 2490 |
if ( false !== $cached_data && ! $refresh ) { |
| 2491 |
return (float) $cached_data; |
| 2492 |
} |
| 2493 |
|
| 2494 |
// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $this->table is hardcoded. |
| 2495 |
$total_refunded = $this->wpdb->get_var( |
| 2496 |
$this->wpdb->prepare( |
| 2497 |
"SELECT SUM( total_amount ) FROM $this->table WHERE type = %s AND parent_order_id = %d;", |
| 2498 |
'refund_order', |
| 2499 |
$this->get_id() |
| 2500 |
) |
| 2501 |
) ?? 0; |
| 2502 |
// phpcs:enable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $this->table is hardcoded. |
| 2503 |
|
| 2504 |
$total_refunded = - 1 * floatval( $total_refunded ); |
| 2505 |
|
| 2506 |
wp_cache_set( $cache_key, $total_refunded, $this->cache_group ); |
| 2507 |
|
| 2508 |
return $total_refunded; |
| 2509 |
} |
| 2510 |
|
| 2511 |
/** |
| 2512 |
* Get the total tax refunded. |
| 2513 |
* |
| 2514 |
* @return float |
| 2515 |
*/ |
| 2516 |
public function get_total_tax_refunded(): float { |
| 2517 |
$cache_key = Caching::get_cache_prefix( 'orders' ) . 'total_tax_refunded' . $this->get_id(); |
| 2518 |
$cached_data = wp_cache_get( $cache_key, $this->cache_group ); |
| 2519 |
|
| 2520 |
if ( false !== $cached_data ) { |
| 2521 |
return $cached_data; |
| 2522 |
} |
| 2523 |
|
| 2524 |
// phpcs:disable PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared -- query prepared. |
| 2525 |
$total_refunded = $this->wpdb->get_var( |
| 2526 |
$this->wpdb->prepare( |
| 2527 |
"SELECT SUM( order_item_meta.meta_value ) |
| 2528 |
FROM {$this->wpdb->prefix}storeengine_order_item_meta AS order_item_meta |
| 2529 |
INNER JOIN $this->table AS orders ON ( orders.type = 'shop_order_refund' AND orders.parent_order_id = %d ) |
| 2530 |
INNER JOIN {$this->wpdb->prefix}storeengine_order_items AS order_items ON ( order_items.order_id = orders.id AND order_items.order_item_type = 'tax' ) |
| 2531 |
WHERE order_item_meta.order_item_id = order_items.order_item_id |
| 2532 |
AND order_item_meta.meta_key IN ('tax_amount', 'shipping_tax_amount')", |
| 2533 |
$this->get_id() |
| 2534 |
) |
| 2535 |
) ?? 0; |
| 2536 |
// phpcs:enable PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared -- query prepared |
| 2537 |
|
| 2538 |
$total_refunded = floatval( $total_refunded ); |
| 2539 |
|
| 2540 |
wp_cache_set( $cache_key, $total_refunded, $this->cache_group ); |
| 2541 |
|
| 2542 |
return $total_refunded; |
| 2543 |
} |
| 2544 |
|
| 2545 |
/** |
| 2546 |
* Get the total shipping refunded. |
| 2547 |
* |
| 2548 |
* @return float |
| 2549 |
*/ |
| 2550 |
public function get_total_shipping_refunded() { |
| 2551 |
$cache_key = Caching::get_cache_prefix( 'orders' ) . 'total_shipping_refunded' . $this->get_id(); |
| 2552 |
$cached_data = wp_cache_get( $cache_key, $this->cache_group ); |
| 2553 |
|
| 2554 |
if ( false !== $cached_data ) { |
| 2555 |
return $cached_data; |
| 2556 |
} |
| 2557 |
|
| 2558 |
// phpcs:disable PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared -- query prepared |
| 2559 |
$total_refunded = $this->wpdb->get_var( |
| 2560 |
$this->wpdb->prepare( |
| 2561 |
"SELECT SUM( order_item_meta.meta_value ) |
| 2562 |
FROM {$this->wpdb->prefix}storeengine_order_item_meta AS order_item_meta |
| 2563 |
INNER JOIN $this->table AS orders ON ( orders.type = 'shop_order_refund' AND orders.parent_order_id = %d ) |
| 2564 |
INNER JOIN {$this->wpdb->prefix}storeengine_order_items AS order_items ON ( order_items.order_id = orders.id AND order_items.order_item_type = 'shipping' ) |
| 2565 |
WHERE order_item_meta.order_item_id = order_items.order_item_id |
| 2566 |
AND order_item_meta.meta_key IN ('cost')", |
| 2567 |
$this->get_id() |
| 2568 |
) |
| 2569 |
) ?? 0; |
| 2570 |
// phpcs:enable PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared -- query prepared |
| 2571 |
|
| 2572 |
$total_refunded = floatval( $total_refunded ); |
| 2573 |
|
| 2574 |
wp_cache_set( $cache_key, $total_refunded, $this->cache_group ); |
| 2575 |
|
| 2576 |
return $total_refunded; |
| 2577 |
} |
| 2578 |
|
| 2579 |
/** |
| 2580 |
* Gets the count of order items of a certain type that have been refunded. |
| 2581 |
* |
| 2582 |
* @param string $item_type Item type. |
| 2583 |
* |
| 2584 |
* @return int |
| 2585 |
*/ |
| 2586 |
public function get_item_count_refunded( $item_type = '' ): int { |
| 2587 |
if ( empty( $item_type ) ) { |
| 2588 |
$item_type = [ 'line_item' ]; |
| 2589 |
} |
| 2590 |
if ( ! is_array( $item_type ) ) { |
| 2591 |
$item_type = [ $item_type ]; |
| 2592 |
} |
| 2593 |
$count = 0; |
| 2594 |
|
| 2595 |
foreach ( $this->get_refunds() as $refund ) { |
| 2596 |
foreach ( $refund->get_items( $item_type ) as $refunded_item ) { |
| 2597 |
$count += abs( $refunded_item->get_quantity() ); |
| 2598 |
} |
| 2599 |
} |
| 2600 |
|
| 2601 |
return apply_filters( 'storeengine/get_item_count_refunded', $count, $item_type, $this ); |
| 2602 |
} |
| 2603 |
|
| 2604 |
/** |
| 2605 |
* Get the total number of items refunded. |
| 2606 |
* |
| 2607 |
* @param string $item_type Type of the item we're checking, if not a line_item. |
| 2608 |
* |
| 2609 |
* @return int |
| 2610 |
*/ |
| 2611 |
public function get_total_qty_refunded( $item_type = 'line_item' ) { |
| 2612 |
$qty = 0; |
| 2613 |
foreach ( $this->get_refunds() as $refund ) { |
| 2614 |
foreach ( $refund->get_items( $item_type ) as $refunded_item ) { |
| 2615 |
$qty += $refunded_item->get_quantity(); |
| 2616 |
} |
| 2617 |
} |
| 2618 |
|
| 2619 |
return $qty; |
| 2620 |
} |
| 2621 |
|
| 2622 |
/** |
| 2623 |
* Get the refunded amount for a line item. |
| 2624 |
* |
| 2625 |
* @param int $item_id ID of the item we're checking. |
| 2626 |
* @param string $item_type Type of the item we're checking, if not a line_item. |
| 2627 |
* |
| 2628 |
* @return int |
| 2629 |
*/ |
| 2630 |
public function get_qty_refunded_for_item( $item_id, $item_type = 'line_item' ) { |
| 2631 |
$qty = 0; |
| 2632 |
foreach ( $this->get_refunds() as $refund ) { |
| 2633 |
foreach ( $refund->get_items( $item_type ) as $refunded_item ) { |
| 2634 |
if ( absint( $refunded_item->get_meta( '_refunded_item_id' ) ) === $item_id ) { |
| 2635 |
$qty += $refunded_item->get_quantity(); |
| 2636 |
} |
| 2637 |
} |
| 2638 |
} |
| 2639 |
|
| 2640 |
return $qty; |
| 2641 |
} |
| 2642 |
|
| 2643 |
/** |
| 2644 |
* Get the refunded amount for a line item. |
| 2645 |
* |
| 2646 |
* @param int $item_id ID of the item we're checking. |
| 2647 |
* @param string $item_type Type of the item we're checking, if not a line_item. |
| 2648 |
* |
| 2649 |
* @return int |
| 2650 |
*/ |
| 2651 |
public function get_total_refunded_for_item( $item_id, $item_type = 'line_item' ) { |
| 2652 |
$total = 0; |
| 2653 |
foreach ( $this->get_refunds() as $refund ) { |
| 2654 |
foreach ( $refund->get_items( $item_type ) as $refunded_item ) { |
| 2655 |
if ( absint( $refunded_item->get_meta( '_refunded_item_id' ) ) === $item_id ) { |
| 2656 |
$total += $refunded_item->get_total(); |
| 2657 |
} |
| 2658 |
} |
| 2659 |
} |
| 2660 |
|
| 2661 |
return $total * - 1; |
| 2662 |
} |
| 2663 |
|
| 2664 |
/** |
| 2665 |
* Get the refunded tax amount for a line item. |
| 2666 |
* |
| 2667 |
* @param int $item_id ID of the item we're checking. |
| 2668 |
* @param int $tax_id ID of the tax we're checking. |
| 2669 |
* @param string $item_type Type of the item we're checking, if not a line_item. |
| 2670 |
* |
| 2671 |
* @return double |
| 2672 |
*/ |
| 2673 |
public function get_tax_refunded_for_item( $item_id, $tax_id, $item_type = 'line_item' ) { |
| 2674 |
$total = 0; |
| 2675 |
foreach ( $this->get_refunds() as $refund ) { |
| 2676 |
foreach ( $refund->get_items( $item_type ) as $refunded_item ) { |
| 2677 |
$refunded_item_id = (int) $refunded_item->get_meta( '_refunded_item_id' ); |
| 2678 |
if ( $refunded_item_id === $item_id ) { |
| 2679 |
$taxes = $refunded_item->get_taxes(); |
| 2680 |
// Add to total. |
| 2681 |
$total += isset( $taxes['total'][ $tax_id ] ) ? (float) $taxes['total'][ $tax_id ] : 0; |
| 2682 |
break; |
| 2683 |
} |
| 2684 |
} |
| 2685 |
} |
| 2686 |
|
| 2687 |
return Formatting::round_tax_total( $total ) * - 1; |
| 2688 |
} |
| 2689 |
|
| 2690 |
/** |
| 2691 |
* Get total tax refunded by rate ID. |
| 2692 |
* |
| 2693 |
* @param int $rate_id Rate ID. |
| 2694 |
* |
| 2695 |
* @return float |
| 2696 |
*/ |
| 2697 |
public function get_total_tax_refunded_by_rate_id( $rate_id ) { |
| 2698 |
$total = 0; |
| 2699 |
foreach ( $this->get_refunds() as $refund ) { |
| 2700 |
foreach ( $refund->get_items( 'tax' ) as $refunded_item ) { |
| 2701 |
if ( absint( $refunded_item->get_rate_id() ) === $rate_id ) { |
| 2702 |
$total += abs( $refunded_item->get_tax_total() ) + abs( $refunded_item->get_shipping_tax_total() ); |
| 2703 |
} |
| 2704 |
} |
| 2705 |
} |
| 2706 |
|
| 2707 |
return $total; |
| 2708 |
} |
| 2709 |
|
| 2710 |
/** |
| 2711 |
* How much money is left to refund? |
| 2712 |
* |
| 2713 |
* @return float |
| 2714 |
*/ |
| 2715 |
public function get_remaining_refund_amount(): float { |
| 2716 |
return (float) Formatting::format_decimal( $this->get_total() - $this->get_total_refunded(), Formatting::get_price_decimals() ); |
| 2717 |
} |
| 2718 |
|
| 2719 |
/** |
| 2720 |
* How many items are left to refund? |
| 2721 |
* |
| 2722 |
* @return int |
| 2723 |
*/ |
| 2724 |
public function get_remaining_refund_items() { |
| 2725 |
return absint( $this->get_item_count() - $this->get_item_count_refunded() ); |
| 2726 |
} |
| 2727 |
|
| 2728 |
/** |
| 2729 |
* Add total row for the payment method. |
| 2730 |
* |
| 2731 |
* @param array $total_rows Total rows. |
| 2732 |
* @param string $tax_display Tax to display. |
| 2733 |
*/ |
| 2734 |
protected function add_order_item_totals_payment_method_row( array &$total_rows ) { |
| 2735 |
if ( $this->get_total() > 0 && $this->get_payment_method_title() ) { |
| 2736 |
$total_rows['payment_method'] = [ |
| 2737 |
'type' => 'payment_method', |
| 2738 |
'label' => __( 'Payment method:', 'storeengine' ), |
| 2739 |
'value' => $this->get_payment_method_to_display( 'customer' ), |
| 2740 |
]; |
| 2741 |
} |
| 2742 |
} |
| 2743 |
|
| 2744 |
/** |
| 2745 |
* Add total row for refunds. |
| 2746 |
* |
| 2747 |
* @param array $total_rows Total rows. |
| 2748 |
* @param string $tax_display Tax to display. |
| 2749 |
*/ |
| 2750 |
protected function add_order_item_totals_refund_rows( &$total_rows, $tax_display ) { |
| 2751 |
$refunds = $this->get_refunds(); |
| 2752 |
if ( $refunds ) { |
| 2753 |
foreach ( $refunds as $id => $refund ) { |
| 2754 |
$reason = trim( $refund->get_reason() ); |
| 2755 |
|
| 2756 |
if ( strlen( $reason ) > 0 ) { |
| 2757 |
$reason = "<br><small>$reason</small>"; |
| 2758 |
} |
| 2759 |
|
| 2760 |
$total_rows[ 'refund_' . $id ] = [ |
| 2761 |
'type' => 'refund', |
| 2762 |
'label' => __( 'Refund', 'storeengine' ) . ':', |
| 2763 |
'value' => Formatting::price( $refund->get_total_amount(), [ 'currency' => $this->get_currency() ] ) . $reason, |
| 2764 |
]; |
| 2765 |
} |
| 2766 |
} |
| 2767 |
} |
| 2768 |
|
| 2769 |
/** |
| 2770 |
* Get totals for display on pages and in emails. |
| 2771 |
* |
| 2772 |
* @param string $tax_display Tax to display. |
| 2773 |
* |
| 2774 |
* @return array |
| 2775 |
*/ |
| 2776 |
public function get_order_item_totals( $tax_display = '' ) { |
| 2777 |
$tax_display = $tax_display ? $tax_display : Helper::get_settings( 'tax_display_cart' ); |
| 2778 |
$total_rows = []; |
| 2779 |
|
| 2780 |
$this->add_order_item_totals_subtotal_row( $total_rows, $tax_display ); |
| 2781 |
$this->add_order_item_totals_discount_row( $total_rows, $tax_display ); |
| 2782 |
$this->add_order_item_totals_shipping_row( $total_rows, $tax_display ); |
| 2783 |
$this->add_order_item_totals_fee_rows( $total_rows, $tax_display ); |
| 2784 |
$this->add_order_item_totals_tax_rows( $total_rows, $tax_display ); |
| 2785 |
$this->add_order_item_totals_refund_rows( $total_rows, $tax_display ); |
| 2786 |
$this->add_order_item_totals_total_row( $total_rows, $tax_display ); |
| 2787 |
$this->add_order_item_totals_payment_method_row( $total_rows, $tax_display ); |
| 2788 |
|
| 2789 |
return apply_filters( 'storeengine/get_order_item_totals', $total_rows, $this, $tax_display ); |
| 2790 |
} |
| 2791 |
|
| 2792 |
/** |
| 2793 |
* Check if order has been created via admin, checkout, or in another way. |
| 2794 |
* |
| 2795 |
* @param string $modus Way of creating the order to test for. |
| 2796 |
* |
| 2797 |
* @return bool |
| 2798 |
*/ |
| 2799 |
public function is_created_via( $modus ) { |
| 2800 |
return apply_filters( 'storeengine/order_is_created_via', $modus === $this->get_created_via(), $this, $modus ); |
| 2801 |
} |
| 2802 |
|
| 2803 |
/** |
| 2804 |
* Indicates that regular orders have an associated Cost of Goods Sold value. |
| 2805 |
* Note that this is true even if the order has no line items with COGS values (in that case the COGS value for the order will be zero)- |
| 2806 |
* |
| 2807 |
* @return bool Always true. |
| 2808 |
*/ |
| 2809 |
public function has_cogs(): bool { |
| 2810 |
return true; |
| 2811 |
} |
| 2812 |
|
| 2813 |
// ----------------------- |
| 2814 |
|
| 2815 |
/** |
| 2816 |
* Coupons array. |
| 2817 |
* |
| 2818 |
* @var OrderItemCoupon[] |
| 2819 |
*/ |
| 2820 |
protected array $coupons = []; |
| 2821 |
|
| 2822 |
/** |
| 2823 |
* Determine how the payment method should be displayed for a subscription. |
| 2824 |
* |
| 2825 |
* @param string $context The context the payment method is being displayed in. Can be 'admin' or 'customer'. Default 'admin'. |
| 2826 |
*/ |
| 2827 |
public function get_payment_method_to_display( string $context = 'admin' ) { |
| 2828 |
$is_unknown = ! $this->get_payment_method() || 'other' === $this->get_payment_method(); |
| 2829 |
$payment_method_to_display = $this->get_payment_method_title(); |
| 2830 |
|
| 2831 |
if ( ! $is_unknown && $payment_method_to_display ) { |
| 2832 |
$card_info = $this->get_payment_card_info(); |
| 2833 |
if ( isset( $card_info['last4'] ) && $card_info['last4'] ) { |
| 2834 |
$payment_method_to_display .= sprintf( |
| 2835 |
// translators: %1$s: Payment method title. %2$s: Last 4 digits of the card. |
| 2836 |
_x('%1$s - %2$s', 'Card info with payment method name', 'storeengine' ), |
| 2837 |
$payment_method_to_display, |
| 2838 |
$card_info['last4'] |
| 2839 |
); |
| 2840 |
} |
| 2841 |
} elseif ( false !== ( $payment_gateway = Helper::get_payment_gateway_by_order( $this ) ) ) { |
| 2842 |
$payment_method_to_display = $payment_gateway->get_title(); |
| 2843 |
} else { |
| 2844 |
// Fallback to the title of the payment method when the order was created |
| 2845 |
$payment_method_to_display = ''; |
| 2846 |
} |
| 2847 |
|
| 2848 |
if ( 'customer' === $context ) { |
| 2849 |
if ( $payment_method_to_display ) { |
| 2850 |
// translators: %s: payment method. |
| 2851 |
$payment_method_to_display = sprintf( __( 'Via %s', 'storeengine' ), $payment_method_to_display ); |
| 2852 |
} |
| 2853 |
|
| 2854 |
$payment_method_to_display = PaymentUtil::maybe_display_my_payment_method( $payment_method_to_display, $this ); |
| 2855 |
} |
| 2856 |
|
| 2857 |
return apply_filters( |
| 2858 |
"storeengine/{$this->object_type}/payment_method_to_display", |
| 2859 |
$payment_method_to_display, |
| 2860 |
$this, |
| 2861 |
$context |
| 2862 |
); |
| 2863 |
} |
| 2864 |
|
| 2865 |
/** |
| 2866 |
* @param int $customer_id |
| 2867 |
* @param null $deprecated |
| 2868 |
* @param bool $create |
| 2869 |
* |
| 2870 |
* @return $this|false|Order |
| 2871 |
*/ |
| 2872 |
public function get_recent_draft_order( int $customer_id = 0, $deprecated = null, bool $create = true ) { |
| 2873 |
$cart_hash = Helper::get_cart_hash_from_cookie(); |
| 2874 |
if ( 0 === $customer_id ) { |
| 2875 |
$customer_id = get_current_user_id(); |
| 2876 |
} |
| 2877 |
|
| 2878 |
if ( ! $cart_hash && ! $customer_id ) { |
| 2879 |
return $this; |
| 2880 |
} |
| 2881 |
|
| 2882 |
try { |
| 2883 |
$cache_key = 'order:draft:' . $cart_hash; |
| 2884 |
$id = wp_cache_get( $cache_key, $this->cache_group ); |
| 2885 |
|
| 2886 |
if ( false !== $id && false !== wp_cache_get( $id, $this->cache_group ) ) { |
| 2887 |
$this->set_id( $id ); |
| 2888 |
$this->read(); |
| 2889 |
|
| 2890 |
return $this; |
| 2891 |
} |
| 2892 |
|
| 2893 |
$data = $this->read_db_data( [ $cart_hash, $customer_id ], 'cart_hash' ); |
| 2894 |
|
| 2895 |
wp_cache_set( $cache_key, $data['id'], $this->cache_group ); |
| 2896 |
wp_cache_set( $data['id'], $data, $this->cache_group ); |
| 2897 |
|
| 2898 |
$this->set_id( $data['id'] ); |
| 2899 |
$this->read(); |
| 2900 |
|
| 2901 |
return $this; |
| 2902 |
} catch ( Exception $e ) { |
| 2903 |
if ( 404 !== $e->getCode() ) { |
| 2904 |
Helper::log_error( $e ); |
| 2905 |
} |
| 2906 |
} |
| 2907 |
|
| 2908 |
if ( $create ) { |
| 2909 |
return self::create_draft_order( [ |
| 2910 |
'cart_hash' => $cart_hash, |
| 2911 |
'customer_id' => $customer_id, |
| 2912 |
] ); |
| 2913 |
} |
| 2914 |
|
| 2915 |
return false; |
| 2916 |
} |
| 2917 |
|
| 2918 |
public static function create_draft_order( array $args = [] ): Order { |
| 2919 |
$args = wp_parse_args( $args, [ |
| 2920 |
'customer_id' => get_current_user_id(), |
| 2921 |
'ip_address' => Helper::get_user_ip(), |
| 2922 |
'user_agent' => Helper::get_user_agent(), |
| 2923 |
'cart_hash' => Helper::get_cart_hash_from_cookie(), |
| 2924 |
'prices_include_tax' => TaxUtil::prices_include_tax(), |
| 2925 |
] ); |
| 2926 |
|
| 2927 |
$order = new self(); |
| 2928 |
$order->set_props( $args ); |
| 2929 |
$order->set_prop( 'status', OrderStatus::DRAFT ); |
| 2930 |
$order->save(); |
| 2931 |
|
| 2932 |
if ( $order->get_id() ) { |
| 2933 |
$cache_key = 'order:draft:' . Helper::get_cart_hash_from_cookie(); |
| 2934 |
wp_cache_set( $cache_key, $order->get_id(), 'storeengine_orders' ); |
| 2935 |
} |
| 2936 |
|
| 2937 |
return $order; |
| 2938 |
} |
| 2939 |
|
| 2940 |
public function has_address( string $context = 'view' ): bool { |
| 2941 |
return $this->has_shipping_address( $context ) || $this->has_billing_address( $context ); |
| 2942 |
} |
| 2943 |
|
| 2944 |
/** |
| 2945 |
* @return DownloadPermission[] |
| 2946 |
*/ |
| 2947 |
public function get_downloadable_permissions(): array { |
| 2948 |
return Helper::get_download_permissions_by_order_id( $this->get_id() ); |
| 2949 |
} |
| 2950 |
|
| 2951 |
public function get_tax_amount( string $context = 'view' ) { |
| 2952 |
return $this->get_total_tax( $context ); |
| 2953 |
} |
| 2954 |
|
| 2955 |
public function get_total_amount( string $context = 'view' ) { |
| 2956 |
return $this->get_total( $context ); |
| 2957 |
} |
| 2958 |
|
| 2959 |
public function set_total_amount( $amount ) { |
| 2960 |
$this->set_total( $amount ); |
| 2961 |
} |
| 2962 |
|
| 2963 |
public function set_order_placed_date_gmt( $value = null ) { |
| 2964 |
if ( null === $value ) { |
| 2965 |
$value = current_time( 'mysql', 1 ); |
| 2966 |
} |
| 2967 |
|
| 2968 |
$this->set_date_prop( 'order_placed_date_gmt', $value ); |
| 2969 |
} |
| 2970 |
|
| 2971 |
/** |
| 2972 |
* @param $value |
| 2973 |
* |
| 2974 |
* @return void |
| 2975 |
* @see get_date_from_gmt can be used. |
| 2976 |
* |
| 2977 |
*/ |
| 2978 |
public function set_order_placed_date( $value = null ) { |
| 2979 |
if ( null === $value ) { |
| 2980 |
$value = current_time( 'mysql', false ); |
| 2981 |
} |
| 2982 |
|
| 2983 |
$this->set_date_prop( 'order_placed_date', $value ); |
| 2984 |
} |
| 2985 |
|
| 2986 |
public function maybe_set_digital_auto_complete() { |
| 2987 |
$items= $this->get_line_product_items(); |
| 2988 |
$this->set_auto_complete_digital_order( |
| 2989 |
$items && |
| 2990 |
ArrayUtil::every( |
| 2991 |
$items, |
| 2992 |
fn( $item ) => 'digital' === $item->get_shipping_type() && $item->get_digital_auto_complete() |
| 2993 |
) |
| 2994 |
); |
| 2995 |
} |
| 2996 |
} |
| 2997 |
|