diff --git a/client-mu-plugins/goodbids/composer.json b/client-mu-plugins/goodbids/composer.json index 208b1ba25..1c0277f2f 100644 --- a/client-mu-plugins/goodbids/composer.json +++ b/client-mu-plugins/goodbids/composer.json @@ -48,6 +48,7 @@ "ext-mbstring": "*", "composer/installers": "^1 || ^2", "cweagans/composer-patches": "^1.7", + "illuminate/collections": "^10.38", "oomphinc/composer-installers-extender": "^2.0", "vlucas/phpdotenv": "^5.5", "wpackagist-plugin/accessibility-checker": "^1.6", diff --git a/client-mu-plugins/goodbids/composer.lock b/client-mu-plugins/goodbids/composer.lock index 8b26a8ebc..867ba0041 100644 --- a/client-mu-plugins/goodbids/composer.lock +++ b/client-mu-plugins/goodbids/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "dcd86d96e7ccf5087eb8265a5f1ae291", + "content-hash": "d494006c8f40bed2d1fdbc56666e2f0b", "packages": [ { "name": "composer/installers", @@ -261,6 +261,201 @@ ], "time": "2023-11-12T22:16:48+00:00" }, + { + "name": "illuminate/collections", + "version": "v10.38.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/collections.git", + "reference": "2677b3962a88640f92dba8a1f4ed38dcaaf13dad" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/collections/zipball/2677b3962a88640f92dba8a1f4ed38dcaaf13dad", + "reference": "2677b3962a88640f92dba8a1f4ed38dcaaf13dad", + "shasum": "" + }, + "require": { + "illuminate/conditionable": "^10.0", + "illuminate/contracts": "^10.0", + "illuminate/macroable": "^10.0", + "php": "^8.1" + }, + "suggest": { + "symfony/var-dumper": "Required to use the dump method (^6.2)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "10.x-dev" + } + }, + "autoload": { + "files": [ + "helpers.php" + ], + "psr-4": { + "Illuminate\\Support\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Collections package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2023-12-15T18:25:00+00:00" + }, + { + "name": "illuminate/conditionable", + "version": "v10.38.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/conditionable.git", + "reference": "d0958e4741fc9d6f516a552060fd1b829a85e009" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/conditionable/zipball/d0958e4741fc9d6f516a552060fd1b829a85e009", + "reference": "d0958e4741fc9d6f516a552060fd1b829a85e009", + "shasum": "" + }, + "require": { + "php": "^8.0.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "10.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Support\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Conditionable package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2023-02-03T08:06:17+00:00" + }, + { + "name": "illuminate/contracts", + "version": "v10.38.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/contracts.git", + "reference": "f6bf37a272fda164f6c451407c99f820eb1eb95b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/contracts/zipball/f6bf37a272fda164f6c451407c99f820eb1eb95b", + "reference": "f6bf37a272fda164f6c451407c99f820eb1eb95b", + "shasum": "" + }, + "require": { + "php": "^8.1", + "psr/container": "^1.1.1|^2.0.1", + "psr/simple-cache": "^1.0|^2.0|^3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "10.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Contracts\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Contracts package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2023-10-30T00:59:22+00:00" + }, + { + "name": "illuminate/macroable", + "version": "v10.38.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/macroable.git", + "reference": "dff667a46ac37b634dcf68909d9d41e94dc97c27" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/macroable/zipball/dff667a46ac37b634dcf68909d9d41e94dc97c27", + "reference": "dff667a46ac37b634dcf68909d9d41e94dc97c27", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "10.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Support\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Macroable package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2023-06-05T12:46:42+00:00" + }, { "name": "oomphinc/composer-installers-extender", "version": "2.0.1", @@ -393,6 +588,110 @@ ], "time": "2023-11-12T21:59:55+00:00" }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/simple-cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/simple-cache.git", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/764e0b3939f5ca87cb904f570ef9be2d78a07865", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\SimpleCache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interfaces for simple caching", + "keywords": [ + "cache", + "caching", + "psr", + "psr-16", + "simple-cache" + ], + "support": { + "source": "https://github.com/php-fig/simple-cache/tree/3.0.0" + }, + "time": "2021-10-29T13:26:27+00:00" + }, { "name": "symfony/polyfill-ctype", "version": "v1.28.0", diff --git a/client-mu-plugins/goodbids/src/classes/Auctions/Auctions.php b/client-mu-plugins/goodbids/src/classes/Auctions/Auctions.php index 03835425d..831f45324 100644 --- a/client-mu-plugins/goodbids/src/classes/Auctions/Auctions.php +++ b/client-mu-plugins/goodbids/src/classes/Auctions/Auctions.php @@ -8,6 +8,8 @@ namespace GoodBids\Auctions; +use WC_Order; + /** * Class for Auctions * @@ -33,6 +35,18 @@ class Auctions { */ const SINGULAR_SLUG = 'auction'; + /** + * @since 1.0.0 + * @var string + */ + const BID_COUNT_TRANSIENT = 'gb:bid-count:%d'; + + /** + * @since 1.0.0 + * @var string + */ + const TOTAL_RAISED_TRANSIENT = 'gb:total-raised:%d'; + /** * @since 1.0.0 * @var Bids @@ -54,8 +68,17 @@ public function __construct() { // Init Rewards Category. $this->init_rewards_category(); + // Add Auction Metrics Meta Box. + $this->add_metrics_meta_box(); + + // Add custom Admin Columns for Auctions. + $this->add_admin_columns(); + // Update Bid Product when Auction is updated. $this->update_bid_product_on_auction_update(); + + // Clear metric transients on new Bid Order. + $this->maybe_clear_metric_transients(); } /** @@ -85,7 +108,7 @@ function () { 'update_item' => __( 'Update Auction', 'goodbids' ), 'view_item' => __( 'View Auction', 'goodbids' ), 'view_items' => __( 'View Auctions', 'goodbids' ), - 'search_items' => __( 'Search Auction', 'goodbids' ), + 'search_items' => __( 'Search Auctions', 'goodbids' ), 'not_found' => __( 'Not found', 'goodbids' ), 'not_found_in_trash' => __( 'Not found in Trash', 'goodbids' ), 'featured_image' => __( 'Featured Image', 'goodbids' ), @@ -445,7 +468,7 @@ function ( int $post_id ) { $bid_price = intval( $bid_product->get_price( 'edit' ) ); if ( $starting_bid && $bid_price !== $starting_bid ) { - // Update postmeta. + // Update post meta. update_post_meta( $bid_product_id, '_price', $starting_bid ); // Update current instance. @@ -481,4 +504,297 @@ public function get_product_type( int $product_id ): ?string { return null; } + + /** + * Get Bid Order IDs for an Auction + * + * @since 1.0.0 + * + * @param int $auction_id + * @param int $limit + * + * @return int[] + */ + public function get_bid_order_ids( int $auction_id, int $limit = -1 ): array { + $args = [ + 'limit' => $limit, + 'status' => [ 'processing', 'completed' ], + 'return' => 'ids', + 'orderby' => 'date', + 'order' => 'DESC', + ]; + + $orders = wc_get_orders( $args ); + $return = []; + + foreach ( $orders as $order_id ) { + // We need to filter the orders out here, for some reason. + // meta_query doesn't seem to work with the following filter hooks: + // - woocommerce_order_query_args + // - woocommerce_order_data_store_cpt_get_orders_query + if ( ! goodbids()->woocommerce->is_bid_order( $order_id ) ) { + continue; + } + + if ( $auction_id !== goodbids()->woocommerce->get_order_auction_id( $order_id ) ) { + continue; + } + + $return[] = $order_id; + } + + return $return; + } + + /** + * Get Bid Order objects for an Auction + * + * @since 1.0.0 + * + * @param int $auction_id + * @param int $limit + * + * @return WC_Order[] + */ + public function get_bid_orders( int $auction_id, int $limit = -1 ): array { + $orders = $this->get_bid_order_ids( $auction_id, $limit ); + + return array_map( + fn ( $order ) => wc_get_order( $order ), + $orders + ); + } + + /** + * Get the Auction Bid Count + * + * @since 1.0.0 + * + * @param int $auction_id + * + * @return int + */ + public function get_bid_count( int $auction_id ): int { + $transient = sprintf( self::BID_COUNT_TRANSIENT, $auction_id ); + $bid_count = get_transient( $transient ); + + if ( $bid_count ) { + return $bid_count; + } + + $orders = $this->get_bid_order_ids( $auction_id ); + $bid_count = count( $orders ); + + set_transient( $transient, $bid_count, HOUR_IN_SECONDS ); + + return $bid_count; + } + + /** + * Get the Auction Total Raised + * + * @since 1.0.0 + * + * @param int $auction_id + * + * @return float + */ + public function get_total_raised( int $auction_id ): float { + $transient = sprintf( self::TOTAL_RAISED_TRANSIENT, $auction_id ); + $total = get_transient( $transient ); + + if ( $total ) { + return $total; + } + + $total = collect( $this->get_bid_orders( $auction_id ) ) + ->sum( fn( $order ) => $order->get_total( 'edit' ) ); + + set_transient( $transient, $total, HOUR_IN_SECONDS ); + + return $total; + } + + /** + * Get the last bid order for an Auction. + * + * @since 1.0.0 + * + * @param int $auction_id + * + * @return ?WC_Order + */ + public function get_last_bid( int $auction_id ): ?WC_Order { + $orders = $this->get_bid_orders( $auction_id, 1 ); + + if ( empty( $orders ) ) { + return null; + } + + return $orders[0]; + } + + /** + * Add a meta box to show Auction metrics. + * + * @since 1.0.0 + * + * @return void + */ + private function add_metrics_meta_box(): void { + add_action( + 'current_screen', + function (): void { + $screen = get_current_screen(); + + if ( $this->get_post_type() !== $screen->id ) { + return; + } + + add_meta_box( + 'goodbids-auction-metrics', + __( 'Auction Metrics', 'goodbids' ), + [ $this, 'metrics_meta_box' ], + $screen->id, + 'side' + ); + } + ); + } + + /** + * Clear Metric Transients on new Bid Order. + * + * @since 1.0.0 + * + * @return void + */ + private function maybe_clear_metric_transients(): void { + add_action( + 'goodbids_order_payment_complete', + function ( int $order_id, int $auction_id ) { + // Don't clear if this isn't a Bid order. + if ( ! goodbids()->woocommerce->is_bid_order( $order_id ) ) { + return; + } + + $transients = [ + sprintf( self::BID_COUNT_TRANSIENT, $auction_id ), + sprintf( self::TOTAL_RAISED_TRANSIENT, $auction_id ), + ]; + + foreach ( $transients as $transient ) { + delete_transient( $transient ); + } + }, + 11, + 2 + ); + } + + /** + * Display the Auction Metrics + * + * @since 1.0.0 + * + * @return void + */ + public function metrics_meta_box(): void { + $auction_id = $this->get_auction_id(); + + printf( + '

%s
%s

', + esc_html__( 'Total Bids', 'goodbids' ), + esc_html( $this->get_bid_count( $auction_id ) ) + ); + + printf( + '

%s
%s

', + esc_html__( 'Total Raised', 'goodbids' ), + wp_kses_post( wc_price( $this->get_total_raised( $auction_id ) ) ) + ); + + $bid_product = wc_get_product( $this->get_bid_product_id( $auction_id ) ); + + printf( + '

%s
%s

', + esc_html__( 'Current Bid', 'goodbids' ), + wp_kses_post( wc_price( $bid_product->get_price() ) ) + ); + + $last_bid = $this->get_last_bid( $auction_id ); + + if ( $last_bid ) { + printf( + '

%s
%s

', + esc_html__( 'Last Bid', 'goodbids' ), + esc_url( get_edit_post_link( $last_bid->get_id() ) ), + wp_kses_post( wc_price( $last_bid->get_total() ) ) + ); + } + } + + private function add_admin_columns(): void { + add_filter( + 'manage_' . $this->get_post_type() . '_posts_columns', + function ( array $columns ): array { + $new_columns = []; + + foreach ( $columns as $column => $label ) { + $new_columns[ $column ] = $label; + + // Insert Custom Columns after the Title column. + if ( 'title' === $column ) { + $new_columns['starting_bid'] = __( 'Starting Bid', 'goodbids' ); + $new_columns['bid_increment'] = __( 'Bid Increment', 'goodbids' ); + $new_columns['total_bids'] = __( 'Total Bids', 'goodbids' ); + $new_columns['total_raised'] = __( 'Total Raised', 'goodbids' ); + $new_columns['last_bid'] = __( 'Last Bid', 'goodbids' ); + $new_columns['current_bid'] = __( 'Current Bid', 'goodbids' ); + } + } + + return $new_columns; + } + ); + + add_action( + 'manage_' . $this->get_post_type() . '_posts_custom_column', + function ( $column, $post_id ) { + $bid_cols = [ + 'starting_bid', + 'bid_increment', + 'total_bids', + 'total_raised', + 'last_bid', + 'current_bid', + ]; + + // Bail early if Auction isn't published. + if ( in_array( $column, $bid_cols, true ) && 'publish' !== get_post_status( $post_id ) ) { + echo '—'; + return; + } + + // Output the column values. + if ( 'starting_bid' === $column ) { + echo wp_kses_post( wc_price( $this->calculate_starting_bid( $post_id ) ) ); + } elseif ( 'bid_increment' === $column ) { + echo wp_kses_post( wc_price( $this->get_bid_increment( $post_id ) ) ); + } elseif ( 'total_bids' === $column ) { + echo esc_html( $this->get_bid_count( $post_id ) ); + } elseif ( 'total_raised' === $column ) { + echo wp_kses_post( wc_price( $this->get_total_raised( $post_id ) ) ); + } elseif ( 'last_bid' === $column ) { + $last_bid = $this->get_last_bid( $post_id ); + echo $last_bid ? wp_kses_post( wc_price( $last_bid->get_total() ) ) : '—'; + } elseif ( 'current_bid' === $column ) { + $bid_product = wc_get_product( $this->get_bid_product_id( $post_id ) ); + echo wp_kses_post( wc_price( $bid_product->get_price() ) ); + } + }, + 10, + 2 + ); + } } diff --git a/client-mu-plugins/goodbids/src/classes/Plugins/WooCommerce.php b/client-mu-plugins/goodbids/src/classes/Plugins/WooCommerce.php index 36c054ce2..448935518 100644 --- a/client-mu-plugins/goodbids/src/classes/Plugins/WooCommerce.php +++ b/client-mu-plugins/goodbids/src/classes/Plugins/WooCommerce.php @@ -21,6 +21,12 @@ class WooCommerce { */ const AUCTION_META_KEY = '_goodbids_auction_id'; + /** + * @since 1.0.0 + * @var string + */ + const TYPE_META_KEY = '_goodbids_product_type'; + /** * @since 1.0.0 * @var string @@ -47,7 +53,9 @@ public function __construct() { $this->store_auction_id_in_cart(); $this->store_auction_id_on_checkout(); - $this->auction_meta_box(); + $this->redirect_after_bid_checkout(); + + $this->add_auction_meta_box(); } /** @@ -314,6 +322,7 @@ function ( \WC_Order_Item_Product $item, string $cart_item_key, array $values, \ } $item->update_meta_data( self::AUCTION_META_KEY, $auction_id ); + $item->update_meta_data( self::TYPE_META_KEY, $product_type ); }, 10, 4 @@ -331,32 +340,62 @@ private function store_auction_id_on_checkout(): void { add_action( 'woocommerce_payment_complete', function ( int $order_id ) { - $order = wc_get_order( $order_id ); + $order = wc_get_order( $order_id ); + $auction_id = false; + $order_type = false; // Find order items with Auction Meta. foreach ( $order->get_items() as $item ) { try { $auction_id = wc_get_order_item_meta( $item->get_id(), self::AUCTION_META_KEY ); + $order_type = wc_get_order_item_meta( $item->get_id(), self::TYPE_META_KEY ); } catch (\Exception $e) { continue; } - if ( ! $auction_id ) { - continue; + if ( $auction_id && $order_type ) { + break; } + } - update_post_meta( $order_id, self::AUCTION_META_KEY, $auction_id ); + if ( ! $auction_id || ! $order_type ) { + // TODO: Log warning. + return; + } - do_action( 'goodbids_order_payment_complete', $order_id, $auction_id ); + update_post_meta( $order_id, self::AUCTION_META_KEY, $auction_id ); + update_post_meta( $order_id, self::TYPE_META_KEY, $order_type ); - if ( is_admin() || ( defined( 'DOING_AJAX' ) && DOING_AJAX ) || headers_sent() ) { - return; - } + do_action( 'goodbids_order_payment_complete', $order_id, $auction_id ); + } + ); + } - // TODO: Check if Auction is over. - wp_safe_redirect( get_permalink( $auction_id ) ); - exit; + /** + * Redirect back to Auction after Checkout. + * + * @since 1.0.0 + * + * @return void + */ + private function redirect_after_bid_checkout(): void { + add_action( + 'woocommerce_thankyou', + function ( int $order_id ): void { + if ( is_admin() || ( defined( 'DOING_AJAX' ) && DOING_AJAX ) || headers_sent() ) { + return; + } + + if ( ! $this->is_bid_order( $order_id ) ) { + return; } + + $auction_id = $this->get_order_auction_id( $order_id ); + + // TODO: Check if Auction has ended. + + wp_safe_redirect( get_permalink( $auction_id ) ); + exit; } ); } @@ -389,7 +428,7 @@ public function get_order_auction_id( int $order_id = 0 ): ?int { * * @return void */ - private function auction_meta_box(): void { + private function add_auction_meta_box(): void { add_action( 'current_screen', function (): void { @@ -408,7 +447,7 @@ function (): void { add_meta_box( 'goodbids-auction-info', __( 'Auction Info', 'goodbids' ), - [ $this, 'auction_info_meta_box' ], + [ $this, 'auction_meta_box' ], $screen->id, 'side' ); @@ -423,7 +462,7 @@ function (): void { * * @return void */ - public function auction_info_meta_box(): void { + public function auction_meta_box(): void { $order_id = ! empty( $_GET['id'] ) ? intval( sanitize_text_field( $_GET['id'] ) ) : false; // phpcs:ignore $auction_id = $this->get_order_auction_id( $order_id ); @@ -445,4 +484,44 @@ public function auction_info_meta_box(): void { esc_html( $reward_id ) ); } + + /** + * Get the Order Type. + * + * @since 1.0.0 + * + * @param int $order_id + * + * @return string + */ + public function get_order_type( int $order_id ): string { + $type = get_post_meta( $order_id, self::TYPE_META_KEY, true ); + return $type ?: 'unknown'; + } + + /** + * Check if an Order is a Bid Order. + * + * @since 1.0.0 + * + * @param int $order_id + * + * @return bool + */ + public function is_bid_order( int $order_id ): bool { + return 'bids' === $this->get_order_type( $order_id ); + } + + /** + * Check if an Order is a Reward Order. + * + * @since 1.0.0 + * + * @param int $order_id + * + * @return bool + */ + public function is_reward_order( int $order_id ): bool { + return 'rewards' === $this->get_order_type( $order_id ); + } }