'object' ); AgentAdapt_Capabilities::register( 'orders.list', array( 'summary' => __( 'List the customer\'s orders', 'agent-adapt' ), 'description' => __( 'The signed-in customer\'s orders, newest first.', 'agent-adapt' ), 'method' => 'GET', 'path' => '/orders', 'scopes' => $read, 'params' => array( 'page' => array( 'type' => 'integer', 'minimum' => 1, 'default' => 1 ), 'per_page' => array( 'type' => 'integer', 'minimum' => 1, 'maximum' => 50, 'default' => 10 ), ), 'response' => array( 'type' => 'object', 'properties' => array( 'orders' => array( 'type' => 'array', 'items' => $order ), 'total' => array( 'type' => 'integer' ), 'total_pages' => array( 'type' => 'integer' ) ) ), 'handler' => array( __CLASS__, 'list_orders' ), ) ); AgentAdapt_Capabilities::register( 'orders.get', array( 'summary' => __( 'Get an order', 'agent-adapt' ), 'description' => __( 'One of the signed-in customer\'s orders in full: items, totals, status, addresses, and whether it can be cancelled.', 'agent-adapt' ), 'method' => 'GET', 'path' => '/orders/{id}', 'scopes' => $read, 'params' => array( 'id' => array( 'type' => 'integer', 'minimum' => 1, 'required' => true, 'in' => 'path' ) ), 'response' => $order, 'handler' => array( __CLASS__, 'get_order' ), ) ); AgentAdapt_Capabilities::register( 'orders.cancel_unpaid', array( 'summary' => __( 'Cancel an unpaid order', 'agent-adapt' ), 'description' => __( 'Cancel one of the customer\'s orders that has not been paid (the same orders WooCommerce lets a customer cancel from My Account).', 'agent-adapt' ), 'method' => 'POST', 'path' => '/orders/{id}/cancel', 'scopes' => $write, 'params' => array( 'id' => array( 'type' => 'integer', 'minimum' => 1, 'required' => true, 'in' => 'path' ) ), 'response' => $order, 'handler' => array( __CLASS__, 'cancel_order' ), ) ); AgentAdapt_Capabilities::register( 'orders.reorder', array( 'summary' => __( 'Put a past order back in the cart', 'agent-adapt' ), 'description' => __( 'Add the items of one of the customer\'s past orders to this Session\'s cart, at today\'s prices. Items no longer sold are skipped.', 'agent-adapt' ), 'method' => 'POST', 'path' => '/orders/{id}/reorder', 'scopes' => $read, 'params' => array( 'id' => array( 'type' => 'integer', 'minimum' => 1, 'required' => true, 'in' => 'path' ) ), 'response' => array( 'type' => 'object' ), 'handler' => array( __CLASS__, 'reorder' ), ) ); AgentAdapt_Capabilities::register( 'account.addresses', array( 'summary' => __( 'Saved addresses', 'agent-adapt' ), 'description' => __( 'The signed-in customer\'s saved billing and shipping addresses.', 'agent-adapt' ), 'method' => 'GET', 'path' => '/account/addresses', 'scopes' => $read, 'response' => array( 'type' => 'object', 'properties' => array( 'billing' => array( 'type' => 'object' ), 'shipping' => array( 'type' => 'object' ) ) ), 'handler' => array( __CLASS__, 'addresses' ), ) ); } /** * orders.list * * @param AgentAdapt_Context $c Context. * @param array $p Params. * @return array */ public static function list_orders( AgentAdapt_Context $c, array $p ): array { $res = wc_get_orders( array( 'customer_id' => $c->user_id, 'type' => 'shop_order', 'limit' => (int) ( $p['per_page'] ?? 10 ), 'page' => (int) ( $p['page'] ?? 1 ), 'paginate' => true, 'orderby' => 'date', 'order' => 'DESC', ) ); AgentAdapt_Events::record( 'orders.listed', $c, array( 'value' => (int) $res->total ) ); return array( 'orders' => array_map( static fn( $o ) => self::summary( $o ), (array) $res->orders ), 'total' => (int) $res->total, 'total_pages' => (int) $res->max_num_pages, ); } /** * orders.get * * @param AgentAdapt_Context $c Context. * @param array $p Params. * @return array|WP_Error */ public static function get_order( AgentAdapt_Context $c, array $p ) { $order = self::own( $c, (int) $p['id'] ); if ( ! $order ) { return self::not_found(); } AgentAdapt_Events::record( 'order.viewed', $c, array( 'object_id' => $order->get_id() ) ); return self::detail( $order ); } /** * orders.cancel_unpaid * * @param AgentAdapt_Context $c Context. * @param array $p Params. * @return array|WP_Error */ public static function cancel_order( AgentAdapt_Context $c, array $p ) { $order = self::own( $c, (int) $p['id'] ); if ( ! $order ) { return self::not_found(); } if ( ! self::cancellable( $order ) ) { return new WP_Error( 'not_cancellable', 'this order can\'t be cancelled here; it may already be paid', array( 'status' => 409 ) ); } $order->update_status( 'cancelled', /* translators: %s: the personal agent's name */ sprintf( __( 'Cancelled by the customer through their personal agent (%s).', 'agent-adapt' ), '' !== $c->agent_name ? $c->agent_name : wp_parse_url( $c->client_id, PHP_URL_HOST ) ) ); // The same action WooCommerce fires when a customer cancels from My // Account, so integrations that listen for it see this cancellation too. do_action( 'woocommerce_cancelled_order', $order->get_id() ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WooCommerce's own hook. AgentAdapt_Events::record( 'order.cancelled', $c, array( 'object_id' => $order->get_id(), 'value' => (float) $order->get_total() ) ); return self::detail( wc_get_order( $order->get_id() ) ); } /** * orders.reorder * * @param AgentAdapt_Context $c Context. * @param array $p Params. * @return array|WP_Error */ public static function reorder( AgentAdapt_Context $c, array $p ) { $order = self::own( $c, (int) $p['id'] ); if ( ! $order ) { return self::not_found(); } return AgentAdapt_Cart::run( $c, static function () use ( $c, $order ) { $added = 0; foreach ( $order->get_items() as $item ) { if ( ! $item instanceof WC_Order_Item_Product ) { continue; } $id = $item->get_variation_id() ?: $item->get_product_id(); if ( $id && AgentAdapt_Catalog::resolvable( (int) $id ) && WC()->cart->add_to_cart( (int) $item->get_product_id(), max( 1, (int) $item->get_quantity() ), (int) $item->get_variation_id() ) ) { ++$added; } } AgentAdapt_Events::record( 'cart.reordered', $c, array( 'object_id' => $order->get_id(), 'value' => $added ) ); return null; } ); } /** * account.addresses * * @param AgentAdapt_Context $c Context. * @param array $p Params. * @return array */ public static function addresses( AgentAdapt_Context $c, array $p ): array { $customer = new WC_Customer( $c->user_id ); AgentAdapt_Events::record( 'account.addresses', $c ); return array( 'billing' => self::address( $customer->get_billing() ), 'shipping' => self::address( $customer->get_shipping() ), ); } /** * The signed-in customer's own order, or null. * * @param AgentAdapt_Context $c Context. * @param int $id Order ID. */ public static function own( AgentAdapt_Context $c, int $id ): ?WC_Order { $order = wc_get_order( $id ); if ( ! $order instanceof WC_Order || 'shop_order' !== $order->get_type() || $c->user_id < 1 || (int) $order->get_customer_id() !== $c->user_id ) { return null; } return $order; } /** * One answer for missing, someone else's, and not-an-order. */ public static function not_found(): WP_Error { return new WP_Error( 'not_found', 'no such order', array( 'status' => 404 ) ); } /** * Whether WooCommerce lets the customer cancel it. * * @param WC_Order $order Order. */ public static function cancellable( WC_Order $order ): bool { $allowed = (array) apply_filters( 'woocommerce_valid_order_statuses_for_cancel', array( 'pending', 'failed' ), $order ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WooCommerce's own filter, read so we follow the store's rule. return $order->has_status( $allowed ); } /** * Order summary. * * @param WC_Order $o Order. * @return array */ private static function summary( WC_Order $o ): array { return array( 'id' => $o->get_id(), 'number' => (string) $o->get_order_number(), 'status' => $o->get_status(), 'status_label' => wc_get_order_status_name( $o->get_status() ), 'created' => $o->get_date_created() ? $o->get_date_created()->format( 'c' ) : null, 'total' => AgentAdapt_Money::from_decimal( $o->get_total(), $o->get_currency() ), 'item_count' => (int) $o->get_item_count(), 'url' => $o->get_view_order_url(), ); } /** * Order detail. * * @param WC_Order $o Order. * @return array */ public static function detail( WC_Order $o ): array { $cur = $o->get_currency(); $m = static fn( $v ) => AgentAdapt_Money::from_decimal( $v, $cur ); $items = array(); foreach ( $o->get_items() as $item ) { if ( ! $item instanceof WC_Order_Item_Product ) { continue; } $items[] = array( 'item_id' => (int) $item->get_id(), 'product_id' => (int) $item->get_product_id(), 'variation_id' => (int) $item->get_variation_id(), 'name' => AgentAdapt_Catalog::text( $item->get_name() ), 'quantity' => (int) $item->get_quantity(), 'subtotal' => $m( $item->get_subtotal() ), 'total' => $m( $item->get_total() ), ); } return self::summary( $o ) + array( 'items' => $items, 'totals' => array( 'subtotal' => $m( $o->get_subtotal() ), 'discount' => $m( $o->get_discount_total() ), 'shipping' => $m( $o->get_shipping_total() ), 'tax' => $m( $o->get_total_tax() ), 'total' => $m( $o->get_total() ), ), 'payment_method' => AgentAdapt_Catalog::text( (string) $o->get_payment_method_title() ), 'paid' => $o->get_date_paid() ? $o->get_date_paid()->format( 'c' ) : null, 'completed' => $o->get_date_completed() ? $o->get_date_completed()->format( 'c' ) : null, 'shipping_method' => AgentAdapt_Catalog::text( (string) $o->get_shipping_method() ), 'billing' => self::address( $o->get_address( 'billing' ) ), 'shipping' => self::address( $o->get_address( 'shipping' ) ), 'can_cancel' => self::cancellable( $o ), ); } /** * An address with empty fields dropped. * * @param array $a Address. * @return array */ public static function address( array $a ): array { $keep = array( 'first_name', 'last_name', 'company', 'address_1', 'address_2', 'city', 'state', 'postcode', 'country', 'email', 'phone' ); $out = array(); foreach ( $keep as $k ) { if ( isset( $a[ $k ] ) && '' !== (string) $a[ $k ] ) { $out[ $k ] = (string) $a[ $k ]; } } return $out; } }