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' ) ); } } }