get_meta( '_pokit_released' ) ) {
return false;
}
return $enabled;
}
/**
* Register the "Pre-ordered" order status (wc-pokit-preorder).
*/
public static function register_status() {
register_post_status(
'wc-pokit-preorder',
array(
'label' => _x( 'Pre-ordered', 'Order status', 'shipclock-pre-orders' ),
'public' => false,
'exclude_from_search' => false,
'show_in_admin_all_list' => true,
'show_in_admin_status_list' => true,
/* translators: %s: count */
'label_count' => _n_noop( 'Pre-ordered (%s)', 'Pre-ordered (%s)', 'shipclock-pre-orders' ),
)
);
}
/**
* Add to the status list.
*
* @param array $statuses Statuses.
* @return array
*/
public static function order_statuses( $statuses ) {
$new = array();
foreach ( $statuses as $k => $v ) {
$new[ $k ] = $v;
if ( 'wc-processing' === $k ) {
$new['wc-pokit-preorder'] = _x( 'Pre-ordered', 'Order status', 'shipclock-pre-orders' );
}
}
if ( ! isset( $new['wc-pokit-preorder'] ) ) {
$new['wc-pokit-preorder'] = _x( 'Pre-ordered', 'Order status', 'shipclock-pre-orders' );
}
return $new;
}
/**
* Pre-ordered counts as paid (for reports and download permissions).
*
* @param array $statuses Statuses.
* @return array
*/
public static function paid_statuses( $statuses ) {
$statuses[] = 'pokit-preorder';
return array_unique( $statuses );
}
/**
* After payment, pre-order-only orders go to Pre-ordered; balance orders go straight to Completed.
*
* @param string $status Status.
* @param int $order_id ID.
* @param WC_Order $order Order.
* @return string
*/
public static function payment_complete_status( $status, $order_id, $order = null ) {
$order = $order ? $order : wc_get_order( $order_id );
if ( ! $order ) {
return $status;
}
if ( pokit_is_balance_order( $order ) ) {
return 'completed';
}
if ( pokit_order_is_preorder_only( $order ) && 'yes' !== $order->get_meta( '_pokit_released' ) ) {
return 'pokit-preorder';
}
return $status;
}
/**
* Keep records in sync with order status.
*
* @param int $order_id Order ID.
* @param string $from From.
* @param string $to To.
* @param WC_Order $order Order.
*/
public static function status_changed( $order_id, $from, $to, $order ) {
if ( pokit_is_balance_order( $order ) ) {
do_action( 'pokit_balance_order_status', $order, $from, $to );
return;
}
if ( ! pokit_order_preorder_items( $order ) ) {
return;
}
if ( in_array( $to, array( 'cancelled', 'refunded', 'failed' ), true ) ) {
self::close_order_records( $order_id );
return;
}
if ( in_array( $to, array( 'processing', 'on-hold', 'completed', 'pokit-preorder' ), true ) ) {
self::create_records( $order );
self::reopen_order_records( $order );
if ( $order->is_paid() ) {
self::sync_paid_amounts( $order );
}
}
if ( self::$internal ) {
return;
}
// Admin (or a gateway) moved a pre-order-only order to Processing before release: keep it Pre-ordered.
if ( 'processing' === $to && in_array( $from, array( 'on-hold', 'pending', 'failed', 'cancelled' ), true ) && pokit_order_is_preorder_only( $order ) && 'yes' !== $order->get_meta( '_pokit_released' ) && self::open_records( $order_id ) ) {
self::set_status( $order, 'pokit-preorder', __( 'Payment received. Order kept as Pre-ordered until release.', 'shipclock-pre-orders' ) );
$order = wc_get_order( $order_id );
$to = 'pokit-preorder';
}
if ( 'pokit-preorder' === $to && 'yes' !== $order->get_meta( '_pokit_confirm_sent' ) ) {
$order->update_meta_data( '_pokit_confirm_sent', 'yes' );
$order->save();
do_action( 'pokit_email_customer_confirmed', $order->get_id() );
}
// Admin marked the order Completed manually: treat fully-paid pre-orders as released/shipped.
if ( 'completed' === $to ) {
foreach ( POKIT_Records::query( array( 'order_id' => $order_id, 'status' => array( 'active', 'cancel_requested' ) ) ) as $r ) {
if ( (float) $r->balance_due <= 0 ) {
POKIT_Records::update( $r->id, array( 'status' => 'completed', 'released_at' => pokit_now() ) );
}
}
}
}
/**
* Order cancelled/refunded/failed: close its open records. Runs early (priority 1) so it happens
* before WooCommerce restocks the items – otherwise the restock could release other customers.
*
* @param int $order_id Order ID.
*/
public static function close_order_records( $order_id ) {
$order = wc_get_order( $order_id );
if ( ! $order || pokit_is_balance_order( $order ) ) {
return;
}
$closed = array_filter( array_map( 'intval', (array) $order->get_meta( '_pokit_auto_cancelled' ) ) );
$found = false;
foreach ( POKIT_Records::query( array( 'order_id' => $order_id, 'status' => pokit_open_statuses() ) ) as $r ) {
POKIT_Records::update( $r->id, array( 'status' => 'cancelled' ) );
$closed[] = (int) $r->id;
$found = true;
if ( $r->balance_order_id ) {
$bal = wc_get_order( $r->balance_order_id );
if ( $bal && $bal->has_status( array( 'pending', 'on-hold', 'failed' ) ) ) {
self::set_status( $bal, 'cancelled', __( 'Parent pre-order was cancelled.', 'shipclock-pre-orders' ) );
}
}
}
if ( $found ) {
$order->update_meta_data( '_pokit_auto_cancelled', array_values( array_unique( $closed ) ) );
$order->save();
}
}
/**
* Hook targets for the early close.
*
* @param int $order_id Order ID.
*/
public static function early_close( $order_id ) {
if ( pokit_order_preorder_items( wc_get_order( $order_id ) ) ) {
self::close_order_records( $order_id );
}
}
/**
* An order that was failed/cancelled comes back (e.g. paid after a decline): reopen the records
* that were closed by the status change (never those cancelled on purpose).
*
* @param WC_Order $order Order.
*/
private static function reopen_order_records( $order ) {
$ids = array_filter( array_map( 'intval', (array) $order->get_meta( '_pokit_auto_cancelled' ) ) );
if ( ! $ids ) {
return;
}
foreach ( POKIT_Records::query( array( 'ids' => $ids, 'status' => 'cancelled' ) ) as $r ) {
POKIT_Records::update( $r->id, array( 'status' => $r->balance_order_id ? 'awaiting_payment' : 'active' ) );
}
$order->delete_meta_data( '_pokit_auto_cancelled' );
$order->save();
$order->add_order_note( __( 'Order reopened: its pre-orders are active again.', 'shipclock-pre-orders' ) );
}
/**
* Record what was actually paid once the order is paid (on-hold orders start at 0).
*
* @param WC_Order $order Order.
*/
private static function sync_paid_amounts( $order ) {
foreach ( POKIT_Records::query( array( 'order_id' => $order->get_id(), 'status' => pokit_open_statuses() ) ) as $r ) {
if ( (float) $r->amount_paid > 0 || $r->balance_order_id ) {
continue;
}
$item = $order->get_item( $r->order_item_id );
if ( ! $item ) {
continue;
}
$paid = (float) $item->get_total() + (float) $item->get_total_tax();
if ( $paid > 0 ) {
POKIT_Records::update( $r->id, array( 'amount_paid' => wc_format_decimal( $paid, wc_get_price_decimals() ) ) );
}
}
}
/**
* Open record count for an order.
*
* @param int $order_id Order.
* @return int
*/
public static function open_records( $order_id ) {
return (int) POKIT_Records::query( array( 'order_id' => $order_id, 'status' => pokit_open_statuses(), 'count' => true ) );
}
/**
* Change status from inside the plugin without our own sync reacting to it.
*
* @param WC_Order $order Order.
* @param string $status Status.
* @param string $note Note.
*/
public static function set_status( $order, $status, $note = '' ) {
self::$internal = true;
$order->update_status( $status, $note );
self::$internal = false;
}
/**
* Create one record per pre-order line (idempotent).
*
* @param WC_Order $order Order.
*/
public static function create_records( $order ) {
if ( 'yes' === $order->get_meta( '_pokit_records_created' ) ) {
return;
}
$created = 0;
foreach ( pokit_order_preorder_items( $order ) as $item_id => $item ) {
if ( POKIT_Records::query( array( 'order_item_id' => $item_id, 'count' => true ) ) ) {
continue;
}
$product = $item->get_product();
$balance = (float) $item->get_meta( '_pokit_balance' );
$display_b = $balance;
if ( $balance > 0 && $product ) {
$display_b = (float) wc_get_price_including_tax( $product, array( 'price' => $balance ) );
}
$new_id = POKIT_Records::insert(
array(
'order_id' => $order->get_id(),
'order_item_id' => $item_id,
'product_id' => $item->get_product_id(),
'variation_id' => $item->get_variation_id(),
'target_id' => (int) $item->get_meta( '_pokit_target' ),
'customer_id' => $order->get_customer_id(),
'email' => $order->get_billing_email(),
'qty' => $item->get_quantity(),
'mode' => $item->get_meta( '_pokit_mode' ),
'status' => 'active',
'release_ts' => (int) $item->get_meta( '_pokit_release_ts' ),
'amount_paid' => $order->is_paid() ? wc_format_decimal( (float) $item->get_total() + (float) $item->get_total_tax(), wc_get_price_decimals() ) : 0,
'balance_due' => wc_format_decimal( $display_b, wc_get_price_decimals() ),
)
);
if ( $new_id ) {
++$created;
}
}
$order->update_meta_data( '_pokit_records_created', 'yes' );
$order->update_meta_data( '_pokit_has_preorder', 'yes' );
$order->save();
if ( $created ) {
$order->add_order_note(
sprintf(
/* translators: %d: count */
_n( '%d pre-order line registered with Shipclock.', '%d pre-order lines registered with Shipclock.', $created, 'shipclock-pre-orders' ),
$created
)
);
do_action( 'pokit_email_admin_new', $order->get_id() );
do_action( 'pokit_records_created', $order );
}
}
/**
* Legacy (posts) order storage: only react to shop orders.
*
* @param int $post_id Post ID.
*/
public static function post_trashed( $post_id ) {
if ( 'shop_order' === get_post_type( $post_id ) ) {
self::order_deleted( $post_id );
}
}
/**
* Cancel open records when an order is trashed/deleted (no customer email).
*
* @param int $order_id Order ID.
*/
public static function order_deleted( $order_id ) {
if ( ! $order_id ) {
return;
}
foreach ( POKIT_Records::query( array( 'order_id' => $order_id, 'status' => pokit_open_statuses() ) ) as $r ) {
POKIT_Records::update( $r->id, array( 'status' => 'cancelled' ) );
}
}
}