| 1 |
<?php |
| 2 |
/** |
| 3 |
* OpenStation — My WordPress: WooCommerce integration. |
| 4 |
* |
| 5 |
* Everything in this file is inert unless WooCommerce is active. It |
| 6 |
* makes the shop folder behave like a shop rather than like a pile of |
| 7 |
* post types: |
| 8 |
* |
| 9 |
* - The folder is labelled **Woo** and carries WooCommerce's own |
| 10 |
* mark. ("WooCommerce" wraps onto two lines under a 88px tile.) |
| 11 |
* - **Orders** are served through `wc_get_orders()` instead of a |
| 12 |
* `WP_Query`. WooCommerce's High-Performance Order Storage moves |
| 13 |
* orders out of `wp_posts` into its own tables, so the generic |
| 14 |
* post-type path returns an empty folder on any modern store. |
| 15 |
* Going through WooCommerce's own API covers both storages. |
| 16 |
* - The right pane gets merchant facts — a product's price, stock |
| 17 |
* and units sold; an order's total, customer and line items; a |
| 18 |
* coupon's validity and usage — and the folder itself gets store |
| 19 |
* totals. |
| 20 |
* |
| 21 |
* REST surface (all read-only, all capability-gated): |
| 22 |
* |
| 23 |
* GET desktop-mode/v1/woocommerce/orders |
| 24 |
* GET desktop-mode/v1/woocommerce/orders/<id> |
| 25 |
* GET desktop-mode/v1/woocommerce/summary/<type>/<id> |
| 26 |
* GET desktop-mode/v1/woocommerce/store |
| 27 |
* |
| 28 |
* @package OpenStation |
| 29 |
*/ |
| 30 |
|
| 31 |
defined( 'ABSPATH' ) || exit; |
| 32 |
|
| 33 |
/** |
| 34 |
* Whether WooCommerce is active and its API is loaded. |
| 35 |
* |
| 36 |
* @return bool |
| 37 |
*/ |
| 38 |
function openstation_my_wordpress_woo_active() { |
| 39 |
return class_exists( 'WooCommerce' ) && function_exists( 'wc_get_orders' ); |
| 40 |
} |
| 41 |
|
| 42 |
/** |
| 43 |
* WooCommerce's mark as a `currentColor` SVG data URI. |
| 44 |
* |
| 45 |
* WooCommerce builds the same glyph inline in `WC_Admin_Menus::admin_menu()` |
| 46 |
* with a hard-coded grey fill, and it's a local variable there — not |
| 47 |
* reachable, and not tintable. Re-emitting it with `currentColor` lets |
| 48 |
* `renderIcon()` mask it, so the folder icon follows the desktop theme |
| 49 |
* the way every other icon does. |
| 50 |
* |
| 51 |
* @return string Data URI. |
| 52 |
*/ |
| 53 |
function openstation_my_wordpress_woo_icon() { |
| 54 |
$svg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 85.9 47.6">' |
| 55 |
. '<path fill="currentColor" d="M77.4,0.1c-4.3,0-7.1,1.4-9.6,6.1L56.4,27.7V8.6c0-5.7-2.7-8.5-7.7-8.5' |
| 56 |
. 's-7.1,1.7-9.6,6.5L28.3,27.7V8.8c0-6.1-2.5-8.7-8.6-8.7H7.3C2.6,0.1,0,2.3,0,6.3s2.5,6.4,7.1,6.4h5.1v24.1' |
| 57 |
. 'c0,6.8,4.6,10.8,11.2,10.8S33,45,36.3,38.9l7.2-13.5v11.4c0,6.7,4.4,10.8,11.1,10.8s9.2-2.3,13-8.7l16.6-28' |
| 58 |
. 'c3.6-6.1,1.1-10.8-6.9-10.8C77.3,0.1,77.3,0.1,77.4,0.1z"/></svg>'; |
| 59 |
|
| 60 |
return 'data:image/svg+xml;base64,' . base64_encode( $svg ); |
| 61 |
} |
| 62 |
|
| 63 |
/** |
| 64 |
* Label and icon for the WooCommerce folder. |
| 65 |
* |
| 66 |
* @param array|null $group Resolved group. |
| 67 |
* @param string $post_type Post type slug. |
| 68 |
* @return array|null |
| 69 |
*/ |
| 70 |
function openstation_my_wordpress_woo_group( $group, $post_type ) { |
| 71 |
unset( $post_type ); |
| 72 |
if ( ! is_array( $group ) || 'plugin:woocommerce' !== ( $group['id'] ?? '' ) ) { |
| 73 |
return $group; |
| 74 |
} |
| 75 |
|
| 76 |
// "WooCommerce" wraps to two lines in an 88px tile and reads badly. |
| 77 |
$group['label'] = _x( 'Woo', 'WooCommerce folder name', 'desktop-mode' ); |
| 78 |
$group['icon'] = openstation_my_wordpress_woo_icon(); |
| 79 |
// Ahead of other plugin folders — for a shop this is the folder |
| 80 |
// the merchant opens all day. |
| 81 |
$group['order'] = 15; |
| 82 |
|
| 83 |
return $group; |
| 84 |
} |
| 85 |
|
| 86 |
/** |
| 87 |
* Replace the generic Orders section with one backed by |
| 88 |
* `wc_get_orders()`. |
| 89 |
* |
| 90 |
* Registered at priority 5 — ahead of the generic post-type pass, |
| 91 |
* which skips any type an existing section already declares via |
| 92 |
* `post_type`. Same trick a plugin would use to hand-roll a section. |
| 93 |
* |
| 94 |
* @param array[] $entities Entity descriptors. |
| 95 |
* @return array[] |
| 96 |
*/ |
| 97 |
function openstation_my_wordpress_woo_entities( $entities ) { |
| 98 |
if ( ! is_array( $entities ) || ! openstation_my_wordpress_woo_active() ) { |
| 99 |
return $entities; |
| 100 |
} |
| 101 |
|
| 102 |
$orders = get_post_type_object( 'shop_order' ); |
| 103 |
if ( ! $orders instanceof WP_Post_Type ) { |
| 104 |
return $entities; |
| 105 |
} |
| 106 |
if ( empty( $orders->cap->edit_posts ) || ! current_user_can( $orders->cap->edit_posts ) ) { |
| 107 |
return $entities; |
| 108 |
} |
| 109 |
|
| 110 |
$group = openstation_my_wordpress_woo_group( |
| 111 |
array( |
| 112 |
'id' => 'plugin:woocommerce', |
| 113 |
'label' => 'WooCommerce', |
| 114 |
'icon' => 'dashicons-admin-plugins', |
| 115 |
'order' => 20, |
| 116 |
), |
| 117 |
'shop_order' |
| 118 |
); |
| 119 |
|
| 120 |
$entities[] = array( |
| 121 |
'id' => 'wc-orders', |
| 122 |
'label' => __( 'Orders', 'desktop-mode' ), |
| 123 |
'icon' => 'dashicons-cart', |
| 124 |
'restPath' => 'desktop-mode/v1/woocommerce/orders', |
| 125 |
'kind' => 'post', |
| 126 |
// Claims `shop_order` so the generic post-type pass skips it, |
| 127 |
// and drives the `os.shop_order.changed` broadcast. |
| 128 |
'post_type' => 'shop_order', |
| 129 |
'thumbnails' => false, |
| 130 |
// Keeps `wcStatus` from being filtered out of the list rows. |
| 131 |
'listFields' => array( 'wcStatus' ), |
| 132 |
'group' => $group['id'], |
| 133 |
'groupLabel' => $group['label'], |
| 134 |
'groupIcon' => $group['icon'], |
| 135 |
'groupOrder' => $group['order'], |
| 136 |
); |
| 137 |
|
| 138 |
return $entities; |
| 139 |
} |
| 140 |
|
| 141 |
/** |
| 142 |
* Give WooCommerce's post types icons that mean something. |
| 143 |
* |
| 144 |
* These types are submenu entries under the WooCommerce menu, so they |
| 145 |
* carry no `menu_icon` and fall back to the generic post pin — a pin |
| 146 |
* for a coupon reads as a mistake. |
| 147 |
* |
| 148 |
* @param array $entity Entity descriptor. |
| 149 |
* @param WP_Post_Type $post_type Post type object. |
| 150 |
* @return array |
| 151 |
*/ |
| 152 |
function openstation_my_wordpress_woo_entity_icon( $entity, $post_type ) { |
| 153 |
$icons = array( |
| 154 |
'product' => 'dashicons-products', |
| 155 |
'shop_coupon' => 'dashicons-tickets-alt', |
| 156 |
'shop_order' => 'dashicons-cart', |
| 157 |
); |
| 158 |
|
| 159 |
/** |
| 160 |
* Filter the section icons used for WooCommerce post types. |
| 161 |
* |
| 162 |
* **Status: Experimental** |
| 163 |
* |
| 164 |
* @param array $icons Post type slug => dashicon class. |
| 165 |
*/ |
| 166 |
$icons = (array) apply_filters( 'openstation_my_wordpress_woo_section_icons', $icons ); |
| 167 |
|
| 168 |
$name = isset( $post_type->name ) ? (string) $post_type->name : ''; |
| 169 |
if ( isset( $icons[ $name ] ) ) { |
| 170 |
$entity['icon'] = (string) $icons[ $name ]; |
| 171 |
} |
| 172 |
|
| 173 |
// Coupons carry no featured image; a thumbnail column would just |
| 174 |
// be a grid of fallback icons. |
| 175 |
if ( 'shop_coupon' === $name ) { |
| 176 |
$entity['thumbnails'] = false; |
| 177 |
$entity['listFields'] = array( 'openstation_woo' ); |
| 178 |
$entity['listQuery'] = array( OPENSTATION_WOO_BANDED_PARAM => '1' ); |
| 179 |
} |
| 180 |
|
| 181 |
// Products band by stock and category, both of which ride the |
| 182 |
// `openstation_woo` REST field — declared here so `_fields` |
| 183 |
// doesn't strip it out of the list rows. |
| 184 |
if ( 'product' === $name ) { |
| 185 |
$entity['listFields'] = array( 'openstation_woo' ); |
| 186 |
$entity['listQuery'] = array( OPENSTATION_WOO_BANDED_PARAM => '1' ); |
| 187 |
// A catalogue is looked at, not read — the product photo is |
| 188 |
// the thing being scanned, and it earns the bigger tile. |
| 189 |
$entity['tileSize'] = 'large'; |
| 190 |
} |
| 191 |
|
| 192 |
return $entity; |
| 193 |
} |
| 194 |
|
| 195 |
/** |
| 196 |
* Status bands for the Orders section, ordered so the ones a merchant |
| 197 |
* has to act on come first. |
| 198 |
* |
| 199 |
* Anything WooCommerce (or a plugin) registers that isn't listed here |
| 200 |
* lands in the trailing "Other" band rather than being dropped. |
| 201 |
* |
| 202 |
* @return array[] Each entry: `id`, `label`, `order`, `statuses`. |
| 203 |
*/ |
| 204 |
function openstation_my_wordpress_woo_order_bands() { |
| 205 |
$labels = wc_get_order_statuses(); |
| 206 |
|
| 207 |
$label_for = static function ( $status ) use ( $labels ) { |
| 208 |
return $labels[ 'wc-' . $status ] ?? ucfirst( str_replace( '-', ' ', $status ) ); |
| 209 |
}; |
| 210 |
|
| 211 |
$bands = array( |
| 212 |
array( |
| 213 |
'id' => 'needs-action', |
| 214 |
'label' => __( 'Needs attention', 'desktop-mode' ), |
| 215 |
'order' => 10, |
| 216 |
'tone' => 'warn', |
| 217 |
'statuses' => array( 'processing', 'on-hold', 'pending' ), |
| 218 |
), |
| 219 |
array( |
| 220 |
'id' => 'problem', |
| 221 |
'label' => __( 'Problems', 'desktop-mode' ), |
| 222 |
'order' => 20, |
| 223 |
'tone' => 'danger', |
| 224 |
'statuses' => array( 'failed' ), |
| 225 |
), |
| 226 |
array( |
| 227 |
'id' => 'completed', |
| 228 |
'label' => $label_for( 'completed' ), |
| 229 |
'order' => 30, |
| 230 |
'statuses' => array( 'completed' ), |
| 231 |
), |
| 232 |
array( |
| 233 |
'id' => 'closed', |
| 234 |
'label' => __( 'Cancelled & refunded', 'desktop-mode' ), |
| 235 |
'order' => 40, |
| 236 |
'statuses' => array( 'cancelled', 'refunded' ), |
| 237 |
), |
| 238 |
array( |
| 239 |
'id' => 'other', |
| 240 |
'label' => __( 'Other', 'desktop-mode' ), |
| 241 |
'order' => 50, |
| 242 |
'statuses' => array(), |
| 243 |
), |
| 244 |
); |
| 245 |
|
| 246 |
/** |
| 247 |
* Filter the status bands the Orders section groups tiles into. |
| 248 |
* |
| 249 |
* Each entry declares `id`, `label`, `order` (lower renders |
| 250 |
* first), and `statuses` — WooCommerce status slugs *without* the |
| 251 |
* `wc-` prefix. The last band in the list catches every status no |
| 252 |
* other band claims, so keep a catch-all at the end. |
| 253 |
* |
| 254 |
* **Status: Experimental** |
| 255 |
* |
| 256 |
* @param array[] $bands Default bands. |
| 257 |
*/ |
| 258 |
return (array) apply_filters( 'openstation_my_wordpress_woo_order_bands', $bands ); |
| 259 |
} |
| 260 |
|
| 261 |
/** |
| 262 |
* Bands for the Products section: anything out of stock first, then a |
| 263 |
* band per product category. |
| 264 |
* |
| 265 |
* Two groupings at once rather than a mode switch — a merchant scanning |
| 266 |
* the catalogue wants the empty shelves surfaced regardless of which |
| 267 |
* category they sit in, and everything else filed where they'd look |
| 268 |
* for it. |
| 269 |
* |
| 270 |
* @return array[] Each entry: `id`, `label`, `order`, and either |
| 271 |
* `stock` (a status slug) or `category` (a term slug). |
| 272 |
*/ |
| 273 |
function openstation_my_wordpress_woo_product_bands() { |
| 274 |
return openstation_my_wordpress_woo_count_product_bands( |
| 275 |
openstation_my_wordpress_woo_product_band_defs() |
| 276 |
); |
| 277 |
} |
| 278 |
|
| 279 |
/** |
| 280 |
* The product band definitions, without counts. |
| 281 |
* |
| 282 |
* Split from the counted version because the per-product band |
| 283 |
* resolver needs the definitions and the counter needs the resolver — |
| 284 |
* calling one function for both would recurse. |
| 285 |
* |
| 286 |
* @return array[] |
| 287 |
*/ |
| 288 |
function openstation_my_wordpress_woo_product_band_defs() { |
| 289 |
// Category bands only exist when the catalogue is small enough to |
| 290 |
// be ordered by band server-side. Offering them without that |
| 291 |
// ordering is worse than not offering them: rows arrive in date |
| 292 |
// order, so a category band materialises above whatever the user |
| 293 |
// is reading every time a stray row for it turns up. A capped |
| 294 |
// store gets stock bands only, which the meta-key fallback orders |
| 295 |
// correctly. |
| 296 |
$with_categories = ! openstation_my_wordpress_woo_catalogue_is_capped(); |
| 297 |
|
| 298 |
$bands = array( |
| 299 |
array( |
| 300 |
'id' => 'stock:outofstock', |
| 301 |
'label' => __( 'Out of stock', 'desktop-mode' ), |
| 302 |
'order' => 10, |
| 303 |
'tone' => 'danger', |
| 304 |
'stock' => 'outofstock', |
| 305 |
), |
| 306 |
array( |
| 307 |
'id' => 'stock:onbackorder', |
| 308 |
'label' => __( 'On backorder', 'desktop-mode' ), |
| 309 |
'order' => 20, |
| 310 |
'tone' => 'warn', |
| 311 |
'stock' => 'onbackorder', |
| 312 |
), |
| 313 |
); |
| 314 |
|
| 315 |
if ( $with_categories ) { |
| 316 |
$terms = get_terms( |
| 317 |
array( |
| 318 |
'taxonomy' => 'product_cat', |
| 319 |
'hide_empty' => true, |
| 320 |
'orderby' => 'name', |
| 321 |
'order' => 'ASC', |
| 322 |
) |
| 323 |
); |
| 324 |
|
| 325 |
$order = 100; |
| 326 |
foreach ( ( is_wp_error( $terms ) ? array() : (array) $terms ) as $term ) { |
| 327 |
$bands[] = array( |
| 328 |
'id' => 'cat:' . $term->slug, |
| 329 |
'label' => $term->name, |
| 330 |
'order' => $order, |
| 331 |
'category' => $term->slug, |
| 332 |
); |
| 333 |
$order += 10; |
| 334 |
} |
| 335 |
} |
| 336 |
|
| 337 |
// Catch-all: everything no earlier band claimed — uncategorised |
| 338 |
// products, or simply "in stock" on a capped catalogue. |
| 339 |
$bands[] = array( |
| 340 |
'id' => 'cat:__none', |
| 341 |
'label' => $with_categories |
| 342 |
? __( 'Uncategorised', 'desktop-mode' ) |
| 343 |
: __( 'In stock', 'desktop-mode' ), |
| 344 |
'order' => PHP_INT_MAX, |
| 345 |
); |
| 346 |
|
| 347 |
/** |
| 348 |
* Filter the bands the Products section groups tiles into. |
| 349 |
* |
| 350 |
* Each entry declares `id`, `label`, `order` (lower renders |
| 351 |
* first), and one matcher: `stock` (a WooCommerce stock-status |
| 352 |
* slug) or `category` (a `product_cat` slug). Keep a matcher-less |
| 353 |
* catch-all last — it collects everything no other band claims. |
| 354 |
* |
| 355 |
* **Status: Experimental** |
| 356 |
* |
| 357 |
* @param array[] $bands Default bands. |
| 358 |
*/ |
| 359 |
return (array) apply_filters( 'openstation_my_wordpress_woo_product_bands', $bands ); |
| 360 |
} |
| 361 |
|
| 362 |
/** |
| 363 |
* Attach a row count to every product band. |
| 364 |
* |
| 365 |
* The bundle lays out every band that has rows *before* the first page |
| 366 |
* lands, so bands never appear or reshuffle while the user scrolls — |
| 367 |
* they only fill. That needs counts up front, which is one cheap |
| 368 |
* count query per band, cached because this runs on every admin page |
| 369 |
* load. |
| 370 |
* |
| 371 |
* Counts are the number of products a band *would* claim on its own. |
| 372 |
* A product that is out of stock is claimed by the stock band and |
| 373 |
* skipped by its category band client-side, so a category count can |
| 374 |
* read high by however many of its products are out of stock — an |
| 375 |
* over-estimate that only ever leaves a band laid out and empty, never |
| 376 |
* a band appearing late. |
| 377 |
* |
| 378 |
* @param array[] $bands Band descriptors. |
| 379 |
* @return array[] Bands with a `count` key. |
| 380 |
*/ |
| 381 |
function openstation_my_wordpress_woo_count_product_bands( $bands ) { |
| 382 |
$plan = openstation_my_wordpress_woo_product_plan(); |
| 383 |
foreach ( $bands as $i => $band ) { |
| 384 |
$bands[ $i ]['count'] = (int) ( $plan['counts'][ $band['id'] ] ?? 0 ); |
| 385 |
} |
| 386 |
return $bands; |
| 387 |
} |
| 388 |
|
| 389 |
/** |
| 390 |
* Largest catalogue this integration will band-order. |
| 391 |
* |
| 392 |
* Ordering works by handing WP_Query the full list of product ids in |
| 393 |
* band order, which becomes a `post__in` clause. That's cheap for a |
| 394 |
* normal catalogue and silly for a huge one, so past this size the |
| 395 |
* ordering falls back to stock-status only and the category bands fill |
| 396 |
* progressively. |
| 397 |
*/ |
| 398 |
const OPENSTATION_WOO_MAX_ORDERED_PRODUCTS = 20000; |
| 399 |
|
| 400 |
/** |
| 401 |
* How many products the catalogue holds. Memoized and cached — both |
| 402 |
* the band definitions and the ordering plan need it, and it must not |
| 403 |
* recurse into either. |
| 404 |
* |
| 405 |
* @return int |
| 406 |
*/ |
| 407 |
function openstation_my_wordpress_woo_product_total() { |
| 408 |
static $total = null; |
| 409 |
if ( null !== $total ) { |
| 410 |
return $total; |
| 411 |
} |
| 412 |
|
| 413 |
$cached = get_transient( 'desktop_mode_woo_product_total' ); |
| 414 |
if ( false !== $cached ) { |
| 415 |
$total = (int) $cached; |
| 416 |
return $total; |
| 417 |
} |
| 418 |
|
| 419 |
$result = wc_get_products( |
| 420 |
array( |
| 421 |
'limit' => 1, |
| 422 |
'paginate' => true, |
| 423 |
'return' => 'ids', |
| 424 |
'status' => array( 'publish', 'private', 'draft', 'pending', 'future' ), |
| 425 |
) |
| 426 |
); |
| 427 |
$total = isset( $result->total ) ? (int) $result->total : 0; |
| 428 |
set_transient( 'desktop_mode_woo_product_total', $total, 5 * MINUTE_IN_SECONDS ); |
| 429 |
|
| 430 |
return $total; |
| 431 |
} |
| 432 |
|
| 433 |
/** |
| 434 |
* Whether the catalogue is too large to band-order server-side. |
| 435 |
* |
| 436 |
* @return bool |
| 437 |
*/ |
| 438 |
function openstation_my_wordpress_woo_catalogue_is_capped() { |
| 439 |
return openstation_my_wordpress_woo_product_total() > OPENSTATION_WOO_MAX_ORDERED_PRODUCTS; |
| 440 |
} |
| 441 |
|
| 442 |
/** |
| 443 |
* The band-ordered product id list, plus an exact row count per band. |
| 444 |
* |
| 445 |
* Bands only stop reshuffling if rows *arrive* in band order — laying |
| 446 |
* the bands out ahead of time isn't enough, because a band that fills |
| 447 |
* late still expands above whatever the user is reading. WordPress |
| 448 |
* can't express "order by stock, then by category" in one query, so |
| 449 |
* the order is computed once here (one id query per band, deduped so |
| 450 |
* a product in two categories lands in the first that claims it) and |
| 451 |
* replayed as `post__in` on every page request. |
| 452 |
* |
| 453 |
* Cached, because it runs on every admin page load; flushed whenever a |
| 454 |
* product changes. |
| 455 |
* |
| 456 |
* @return array{ids: int[], counts: array<string,int>, capped: bool} |
| 457 |
*/ |
| 458 |
function openstation_my_wordpress_woo_product_plan() { |
| 459 |
static $memo = null; |
| 460 |
if ( null !== $memo ) { |
| 461 |
return $memo; |
| 462 |
} |
| 463 |
|
| 464 |
$cache_key = 'desktop_mode_woo_product_plan'; |
| 465 |
$cached = get_transient( $cache_key ); |
| 466 |
if ( is_array( $cached ) && isset( $cached['ids'], $cached['counts'] ) ) { |
| 467 |
$memo = $cached; |
| 468 |
return $memo; |
| 469 |
} |
| 470 |
|
| 471 |
$statuses = array( 'publish', 'private', 'draft', 'pending', 'future' ); |
| 472 |
$total = openstation_my_wordpress_woo_product_total(); |
| 473 |
|
| 474 |
if ( openstation_my_wordpress_woo_catalogue_is_capped() ) { |
| 475 |
$memo = array( |
| 476 |
'ids' => array(), |
| 477 |
'counts' => array(), |
| 478 |
'capped' => true, |
| 479 |
'products' => $total, |
| 480 |
); |
| 481 |
set_transient( $cache_key, $memo, 5 * MINUTE_IN_SECONDS ); |
| 482 |
return $memo; |
| 483 |
} |
| 484 |
|
| 485 |
$ordered = array(); |
| 486 |
$counts = array(); |
| 487 |
$seen = array(); |
| 488 |
|
| 489 |
foreach ( openstation_my_wordpress_woo_product_band_defs() as $band ) { |
| 490 |
$args = array( |
| 491 |
'limit' => -1, |
| 492 |
'return' => 'ids', |
| 493 |
'orderby' => 'date', |
| 494 |
'order' => 'DESC', |
| 495 |
'status' => $statuses, |
| 496 |
); |
| 497 |
if ( ! empty( $band['stock'] ) ) { |
| 498 |
$args['stock_status'] = $band['stock']; |
| 499 |
} elseif ( ! empty( $band['category'] ) ) { |
| 500 |
$args['category'] = array( $band['category'] ); |
| 501 |
} else { |
| 502 |
// Catch-all: whatever no earlier band claimed. Resolved |
| 503 |
// below from the remainder rather than by query — there's |
| 504 |
// no cheap way to ask for "has no product_cat term". |
| 505 |
$args = null; |
| 506 |
} |
| 507 |
|
| 508 |
$ids = null === $args ? array() : (array) wc_get_products( $args ); |
| 509 |
|
| 510 |
$claimed = 0; |
| 511 |
foreach ( $ids as $id ) { |
| 512 |
$id = (int) $id; |
| 513 |
if ( isset( $seen[ $id ] ) ) { |
| 514 |
continue; |
| 515 |
} |
| 516 |
$seen[ $id ] = true; |
| 517 |
$ordered[] = $id; |
| 518 |
++$claimed; |
| 519 |
} |
| 520 |
$counts[ $band['id'] ] = $claimed; |
| 521 |
} |
| 522 |
|
| 523 |
// Anything no band claimed — uncategorised, in stock — trails the |
| 524 |
// list under the catch-all band. |
| 525 |
$remainder = wc_get_products( |
| 526 |
array( |
| 527 |
'limit' => -1, |
| 528 |
'return' => 'ids', |
| 529 |
'orderby' => 'date', |
| 530 |
'order' => 'DESC', |
| 531 |
'status' => $statuses, |
| 532 |
) |
| 533 |
); |
| 534 |
$trailing = 0; |
| 535 |
foreach ( (array) $remainder as $id ) { |
| 536 |
$id = (int) $id; |
| 537 |
if ( isset( $seen[ $id ] ) ) { |
| 538 |
continue; |
| 539 |
} |
| 540 |
$seen[ $id ] = true; |
| 541 |
$ordered[] = $id; |
| 542 |
++$trailing; |
| 543 |
} |
| 544 |
$counts['cat:__none'] = ( $counts['cat:__none'] ?? 0 ) + $trailing; |
| 545 |
|
| 546 |
$memo = array( |
| 547 |
'ids' => $ordered, |
| 548 |
'counts' => $counts, |
| 549 |
'capped' => false, |
| 550 |
'products' => $total, |
| 551 |
); |
| 552 |
set_transient( $cache_key, $memo, 5 * MINUTE_IN_SECONDS ); |
| 553 |
|
| 554 |
return $memo; |
| 555 |
} |
| 556 |
|
| 557 |
/** |
| 558 |
* Query parameter the site window's list requests carry so the band |
| 559 |
* ordering filters can scope themselves. Declared on the section |
| 560 |
* descriptor as `listQuery`, sent by `fetchEntityList()`. |
| 561 |
* |
| 562 |
* The VALUE keeps its pre-rebrand spelling on purpose: it is a |
| 563 |
* persisted or externally-visible identifier, so renaming it would |
| 564 |
* orphan data already written by live installs (or break a live |
| 565 |
* URL). The mismatch between this constant's name and its value is |
| 566 |
* deliberate — it is NOT a half-finished rename. |
| 567 |
*/ |
| 568 |
const OPENSTATION_WOO_BANDED_PARAM = 'desktop_mode_bands'; |
| 569 |
|
| 570 |
/** |
| 571 |
* Whether a REST request asked for band ordering. |
| 572 |
* |
| 573 |
* `rest_product_query` / `rest_shop_coupon_query` fire for every caller |
| 574 |
* of those collections, not just us — the Product Collection block |
| 575 |
* renders through the same filter. Without this check a storefront's |
| 576 |
* chosen sort order would be silently replaced by ours. |
| 577 |
* |
| 578 |
* @param WP_REST_Request $request Request. |
| 579 |
* @return bool |
| 580 |
*/ |
| 581 |
function openstation_my_wordpress_woo_is_banded_request( $request ) { |
| 582 |
if ( ! $request instanceof WP_REST_Request ) { |
| 583 |
return false; |
| 584 |
} |
| 585 |
return '1' === (string) $request->get_param( OPENSTATION_WOO_BANDED_PARAM ); |
| 586 |
} |
| 587 |
|
| 588 |
/** |
| 589 |
* A readable summary of whether the Products collection is being |
| 590 |
* band-ordered, for diagnosing a section whose bands look wrong. |
| 591 |
* |
| 592 |
* @return array{mode: string, products: int, ordered: int, limit: int} |
| 593 |
*/ |
| 594 |
function openstation_my_wordpress_woo_ordering_state() { |
| 595 |
$plan = openstation_my_wordpress_woo_product_plan(); |
| 596 |
return array( |
| 597 |
'mode' => ! empty( $plan['capped'] ) ? 'capped' : 'ordered', |
| 598 |
'products' => (int) ( $plan['products'] ?? 0 ), |
| 599 |
'ordered' => count( (array) ( $plan['ids'] ?? array() ) ), |
| 600 |
'limit' => OPENSTATION_WOO_MAX_ORDERED_PRODUCTS, |
| 601 |
); |
| 602 |
} |
| 603 |
|
| 604 |
/** |
| 605 |
* Which band a single product belongs to. Stock first, then the first |
| 606 |
* category that claims it, then the catch-all. |
| 607 |
* |
| 608 |
* @param WC_Product $product Product. |
| 609 |
* @return string Band id. |
| 610 |
*/ |
| 611 |
function openstation_my_wordpress_woo_product_band_id( $product ) { |
| 612 |
$defs = openstation_my_wordpress_woo_product_band_defs(); |
| 613 |
$stock = $product->get_stock_status(); |
| 614 |
|
| 615 |
foreach ( $defs as $band ) { |
| 616 |
if ( ! empty( $band['stock'] ) && $band['stock'] === $stock ) { |
| 617 |
return (string) $band['id']; |
| 618 |
} |
| 619 |
} |
| 620 |
|
| 621 |
$slugs = wp_get_post_terms( |
| 622 |
$product->get_id(), |
| 623 |
'product_cat', |
| 624 |
array( 'fields' => 'slugs' ) |
| 625 |
); |
| 626 |
$slugs = is_wp_error( $slugs ) ? array() : (array) $slugs; |
| 627 |
|
| 628 |
foreach ( $defs as $band ) { |
| 629 |
if ( ! empty( $band['category'] ) && in_array( $band['category'], $slugs, true ) ) { |
| 630 |
return (string) $band['id']; |
| 631 |
} |
| 632 |
} |
| 633 |
|
| 634 |
return 'cat:__none'; |
| 635 |
} |
| 636 |
|
| 637 |
/** |
| 638 |
* Drop the cached band counts when the catalogue changes, so a newly |
| 639 |
* emptied shelf shows up on the next load rather than five minutes |
| 640 |
* later. |
| 641 |
* |
| 642 |
* @return void |
| 643 |
*/ |
| 644 |
function openstation_my_wordpress_woo_flush_band_counts() { |
| 645 |
delete_transient( 'desktop_mode_woo_product_plan' ); |
| 646 |
delete_transient( 'desktop_mode_woo_product_total' ); |
| 647 |
delete_transient( 'desktop_mode_woo_coupon_plan' ); |
| 648 |
} |
| 649 |
add_action( 'woocommerce_update_product', 'openstation_my_wordpress_woo_flush_band_counts' ); |
| 650 |
add_action( 'woocommerce_new_product', 'openstation_my_wordpress_woo_flush_band_counts' ); |
| 651 |
add_action( 'woocommerce_product_set_stock_status', 'openstation_my_wordpress_woo_flush_band_counts' ); |
| 652 |
add_action( 'woocommerce_new_coupon', 'openstation_my_wordpress_woo_flush_band_counts' ); |
| 653 |
add_action( 'woocommerce_update_coupon', 'openstation_my_wordpress_woo_flush_band_counts' ); |
| 654 |
add_action( 'created_product_cat', 'openstation_my_wordpress_woo_flush_band_counts' ); |
| 655 |
add_action( 'edited_product_cat', 'openstation_my_wordpress_woo_flush_band_counts' ); |
| 656 |
add_action( 'delete_product_cat', 'openstation_my_wordpress_woo_flush_band_counts' ); |
| 657 |
|
| 658 |
/** |
| 659 |
* Order the Products collection so empty shelves come first. |
| 660 |
* |
| 661 |
* `wp/v2/product` is core's collection, so ordering has to be pushed |
| 662 |
* in through its query filter. `_stock_status` sorts |
| 663 |
* `outofstock` > `onbackorder` > `instock` descending, which is |
| 664 |
* exactly the band order, so a band's rows arrive together instead of |
| 665 |
* trickling in across pages. |
| 666 |
* |
| 667 |
* @param array $args Query args. |
| 668 |
* @return array |
| 669 |
*/ |
| 670 |
function openstation_my_wordpress_woo_order_products( $args, $request ) { |
| 671 |
if ( ! openstation_my_wordpress_woo_active() ) { |
| 672 |
return $args; |
| 673 |
} |
| 674 |
// Only the site window's own requests. `rest_product_query` fires |
| 675 |
// for every `wp/v2/product` caller — WooCommerce Blocks' Product |
| 676 |
// Collection renders through it, so rewriting `orderby` |
| 677 |
// unconditionally would silently replace a storefront's chosen |
| 678 |
// sort with our band order. |
| 679 |
if ( ! openstation_my_wordpress_woo_is_banded_request( $request ) ) { |
| 680 |
return $args; |
| 681 |
} |
| 682 |
// A search is the user asking for relevance, not for the band |
| 683 |
// order — and it would fight the `post__in` clause. |
| 684 |
if ( ! empty( $request['search'] ) ) { |
| 685 |
return $args; |
| 686 |
} |
| 687 |
|
| 688 |
$plan = openstation_my_wordpress_woo_product_plan(); |
| 689 |
if ( empty( $plan['ids'] ) ) { |
| 690 |
// Catalogue too large to order this way — fall back to stock |
| 691 |
// status, which at least floats empty shelves to the top. |
| 692 |
$args['meta_key'] = '_stock_status'; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key |
| 693 |
$args['orderby'] = array( |
| 694 |
'meta_value' => 'DESC', |
| 695 |
'date' => 'DESC', |
| 696 |
); |
| 697 |
return $args; |
| 698 |
} |
| 699 |
|
| 700 |
$args['post__in'] = $plan['ids']; |
| 701 |
$args['orderby'] = 'post__in'; |
| 702 |
unset( $args['order'] ); |
| 703 |
|
| 704 |
return $args; |
| 705 |
} |
| 706 |
// Priority 99, not the default 10. WooCommerce Blocks' own |
| 707 |
// `ProductQuery::update_rest_query` also hooks this filter at 10 and |
| 708 |
// ends with `array_merge( $args, …, $orderby_query, … )`, where |
| 709 |
// `$orderby_query` is rebuilt from the request's `orderby` param — |
| 710 |
// which defaults to `date`. At equal priority it runs after us and |
| 711 |
// silently puts `orderby` back, so `post__in` was set but never |
| 712 |
// honoured and the bands arrived in date order. |
| 713 |
add_filter( 'rest_product_query', 'openstation_my_wordpress_woo_order_products', 99, 2 ); |
| 714 |
|
| 715 |
/** |
| 716 |
* Bands for the Coupons section: the ones still worth handing out |
| 717 |
* first, the dead ones last. |
| 718 |
* |
| 719 |
* @return array[] |
| 720 |
*/ |
| 721 |
function openstation_my_wordpress_woo_coupon_bands() { |
| 722 |
$bands = array( |
| 723 |
array( |
| 724 |
'id' => 'coupon:active', |
| 725 |
'label' => __( 'Active', 'desktop-mode' ), |
| 726 |
'order' => 10, |
| 727 |
), |
| 728 |
array( |
| 729 |
'id' => 'coupon:expiring', |
| 730 |
'label' => __( 'Expiring soon', 'desktop-mode' ), |
| 731 |
'order' => 20, |
| 732 |
'tone' => 'warn', |
| 733 |
), |
| 734 |
array( |
| 735 |
'id' => 'coupon:used-up', |
| 736 |
'label' => __( 'Usage limit reached', 'desktop-mode' ), |
| 737 |
'order' => 30, |
| 738 |
'tone' => 'danger', |
| 739 |
), |
| 740 |
array( |
| 741 |
'id' => 'coupon:expired', |
| 742 |
'label' => __( 'Expired', 'desktop-mode' ), |
| 743 |
'order' => 40, |
| 744 |
), |
| 745 |
); |
| 746 |
|
| 747 |
/** |
| 748 |
* Filter the bands the Coupons section groups tiles into. |
| 749 |
* |
| 750 |
* **Status: Experimental** |
| 751 |
* |
| 752 |
* @param array[] $bands Default bands. |
| 753 |
*/ |
| 754 |
return (array) apply_filters( 'openstation_my_wordpress_woo_coupon_bands', $bands ); |
| 755 |
} |
| 756 |
|
| 757 |
/** |
| 758 |
* Which band a coupon belongs to. |
| 759 |
* |
| 760 |
* "Expiring soon" is within 30 days — near enough that a merchant |
| 761 |
* might want to extend or replace it before it lapses. |
| 762 |
* |
| 763 |
* @param WC_Coupon $coupon Coupon. |
| 764 |
* @return string Band id. |
| 765 |
*/ |
| 766 |
function openstation_my_wordpress_woo_coupon_band_id( $coupon ) { |
| 767 |
$expiry = $coupon->get_date_expires(); |
| 768 |
$limit = (int) $coupon->get_usage_limit(); |
| 769 |
$used = (int) $coupon->get_usage_count(); |
| 770 |
|
| 771 |
if ( $expiry && $expiry->getTimestamp() < time() ) { |
| 772 |
return 'coupon:expired'; |
| 773 |
} |
| 774 |
if ( $limit > 0 && $used >= $limit ) { |
| 775 |
return 'coupon:used-up'; |
| 776 |
} |
| 777 |
if ( $expiry && $expiry->getTimestamp() < time() + ( 30 * DAY_IN_SECONDS ) ) { |
| 778 |
return 'coupon:expiring'; |
| 779 |
} |
| 780 |
return 'coupon:active'; |
| 781 |
} |
| 782 |
|
| 783 |
/** |
| 784 |
* The band-ordered coupon id list plus per-band counts. |
| 785 |
* |
| 786 |
* Coupon validity lives in postmeta with no single sortable key, so — |
| 787 |
* as with products — the order is computed once in PHP and replayed as |
| 788 |
* `post__in`. Stores have tens of coupons, not thousands, so the whole |
| 789 |
* set is walked. |
| 790 |
* |
| 791 |
* @return array{ids: int[], counts: array<string,int>} |
| 792 |
*/ |
| 793 |
function openstation_my_wordpress_woo_coupon_plan() { |
| 794 |
static $memo = null; |
| 795 |
if ( null !== $memo ) { |
| 796 |
return $memo; |
| 797 |
} |
| 798 |
|
| 799 |
$cache_key = 'desktop_mode_woo_coupon_plan'; |
| 800 |
$cached = get_transient( $cache_key ); |
| 801 |
if ( is_array( $cached ) && isset( $cached['ids'], $cached['counts'] ) ) { |
| 802 |
$memo = $cached; |
| 803 |
return $memo; |
| 804 |
} |
| 805 |
|
| 806 |
$ids = get_posts( |
| 807 |
array( |
| 808 |
'post_type' => 'shop_coupon', |
| 809 |
'post_status' => array( 'publish', 'private', 'draft', 'pending', 'future' ), |
| 810 |
'posts_per_page' => 500, |
| 811 |
'fields' => 'ids', |
| 812 |
'orderby' => 'title', |
| 813 |
'order' => 'ASC', |
| 814 |
) |
| 815 |
); |
| 816 |
|
| 817 |
$buckets = array(); |
| 818 |
foreach ( openstation_my_wordpress_woo_coupon_bands() as $band ) { |
| 819 |
$buckets[ $band['id'] ] = array(); |
| 820 |
} |
| 821 |
|
| 822 |
foreach ( (array) $ids as $id ) { |
| 823 |
$coupon = new WC_Coupon( (int) $id ); |
| 824 |
if ( ! $coupon->get_id() ) { |
| 825 |
continue; |
| 826 |
} |
| 827 |
$band = openstation_my_wordpress_woo_coupon_band_id( $coupon ); |
| 828 |
if ( ! isset( $buckets[ $band ] ) ) { |
| 829 |
$buckets[ $band ] = array(); |
| 830 |
} |
| 831 |
$buckets[ $band ][] = (int) $id; |
| 832 |
} |
| 833 |
|
| 834 |
$ordered = array(); |
| 835 |
$counts = array(); |
| 836 |
foreach ( $buckets as $band_id => $band_ids ) { |
| 837 |
$counts[ $band_id ] = count( $band_ids ); |
| 838 |
$ordered = array_merge( $ordered, $band_ids ); |
| 839 |
} |
| 840 |
|
| 841 |
$memo = array( |
| 842 |
'ids' => $ordered, |
| 843 |
'counts' => $counts, |
| 844 |
); |
| 845 |
set_transient( $cache_key, $memo, 5 * MINUTE_IN_SECONDS ); |
| 846 |
|
| 847 |
return $memo; |
| 848 |
} |
| 849 |
|
| 850 |
/** |
| 851 |
* Coupon bands with their counts attached, for the bundle. |
| 852 |
* |
| 853 |
* @return array[] |
| 854 |
*/ |
| 855 |
function openstation_my_wordpress_woo_coupon_bands_with_counts() { |
| 856 |
$plan = openstation_my_wordpress_woo_coupon_plan(); |
| 857 |
$bands = openstation_my_wordpress_woo_coupon_bands(); |
| 858 |
foreach ( $bands as $i => $band ) { |
| 859 |
$bands[ $i ]['count'] = (int) ( $plan['counts'][ $band['id'] ] ?? 0 ); |
| 860 |
} |
| 861 |
return $bands; |
| 862 |
} |
| 863 |
|
| 864 |
/** |
| 865 |
* Order the Coupons collection to match the band order. |
| 866 |
* |
| 867 |
* The bridge controller runs Core's `get_items()`, which applies |
| 868 |
* `rest_{$post_type}_query` — so the same `post__in` trick works here |
| 869 |
* even though the collection lives under `desktop-mode/v1`. |
| 870 |
* |
| 871 |
* @param array $args Query args. |
| 872 |
* @param WP_REST_Request $request Request. |
| 873 |
* @return array |
| 874 |
*/ |
| 875 |
function openstation_my_wordpress_woo_order_coupons( $args, $request ) { |
| 876 |
if ( ! openstation_my_wordpress_woo_active() || ! empty( $request['search'] ) ) { |
| 877 |
return $args; |
| 878 |
} |
| 879 |
if ( ! openstation_my_wordpress_woo_is_banded_request( $request ) ) { |
| 880 |
return $args; |
| 881 |
} |
| 882 |
$plan = openstation_my_wordpress_woo_coupon_plan(); |
| 883 |
if ( empty( $plan['ids'] ) ) { |
| 884 |
return $args; |
| 885 |
} |
| 886 |
$args['post__in'] = $plan['ids']; |
| 887 |
$args['orderby'] = 'post__in'; |
| 888 |
unset( $args['order'] ); |
| 889 |
return $args; |
| 890 |
} |
| 891 |
add_filter( 'rest_shop_coupon_query', 'openstation_my_wordpress_woo_order_coupons', 99, 2 ); |
| 892 |
|
| 893 |
/** |
| 894 |
* Expose each coupon's band on its REST row. |
| 895 |
* |
| 896 |
* @return void |
| 897 |
*/ |
| 898 |
function openstation_my_wordpress_woo_register_coupon_field() { |
| 899 |
if ( ! openstation_my_wordpress_woo_active() || ! post_type_exists( 'shop_coupon' ) ) { |
| 900 |
return; |
| 901 |
} |
| 902 |
register_rest_field( |
| 903 |
'shop_coupon', |
| 904 |
'openstation_woo', |
| 905 |
array( |
| 906 |
'get_callback' => static function ( $post ) { |
| 907 |
$coupon = new WC_Coupon( isset( $post['id'] ) ? (int) $post['id'] : 0 ); |
| 908 |
if ( ! $coupon->get_id() ) { |
| 909 |
return null; |
| 910 |
} |
| 911 |
return array( |
| 912 |
'band' => openstation_my_wordpress_woo_coupon_band_id( $coupon ), |
| 913 |
); |
| 914 |
}, |
| 915 |
'schema' => array( |
| 916 |
'description' => __( 'Coupon band used by the site window tiles.', 'desktop-mode' ), |
| 917 |
'type' => array( 'object', 'null' ), |
| 918 |
'context' => array( 'view', 'edit' ), |
| 919 |
'readonly' => true, |
| 920 |
), |
| 921 |
) |
| 922 |
); |
| 923 |
} |
| 924 |
add_action( 'rest_api_init', 'openstation_my_wordpress_woo_register_coupon_field' ); |
| 925 |
|
| 926 |
/** |
| 927 |
* Expose the few product facts the site window's tiles need — stock |
| 928 |
* status for the out-of-stock band and badge, category slugs for the |
| 929 |
* category bands. |
| 930 |
* |
| 931 |
* A REST field rather than extra work in the bundle: products come |
| 932 |
* from core's `wp/v2/product` collection, so this is the only way to |
| 933 |
* widen that payload. The section declares the field in `listFields` |
| 934 |
* so `_fields` doesn't strip it back out. |
| 935 |
* |
| 936 |
* @return void |
| 937 |
*/ |
| 938 |
function openstation_my_wordpress_woo_register_rest_field() { |
| 939 |
if ( ! openstation_my_wordpress_woo_active() || ! post_type_exists( 'product' ) ) { |
| 940 |
return; |
| 941 |
} |
| 942 |
|
| 943 |
register_rest_field( |
| 944 |
'product', |
| 945 |
'openstation_woo', |
| 946 |
array( |
| 947 |
'get_callback' => static function ( $post ) { |
| 948 |
$product = wc_get_product( isset( $post['id'] ) ? (int) $post['id'] : 0 ); |
| 949 |
if ( ! $product ) { |
| 950 |
return null; |
| 951 |
} |
| 952 |
$slugs = wp_get_post_terms( |
| 953 |
$product->get_id(), |
| 954 |
'product_cat', |
| 955 |
array( 'fields' => 'slugs' ) |
| 956 |
); |
| 957 |
return array( |
| 958 |
// The band this row belongs to, decided server-side |
| 959 |
// by the same rules that ordered the collection, so |
| 960 |
// the two can't disagree. |
| 961 |
'band' => openstation_my_wordpress_woo_product_band_id( $product ), |
| 962 |
'stockStatus' => $product->get_stock_status(), |
| 963 |
'stockLevel' => $product->managing_stock() |
| 964 |
? (int) $product->get_stock_quantity() |
| 965 |
: null, |
| 966 |
'onSale' => $product->is_on_sale(), |
| 967 |
'categories' => is_wp_error( $slugs ) ? array() : array_values( $slugs ), |
| 968 |
); |
| 969 |
}, |
| 970 |
'schema' => array( |
| 971 |
'description' => __( 'Stock and category facts used by the site window tiles.', 'desktop-mode' ), |
| 972 |
'type' => array( 'object', 'null' ), |
| 973 |
'context' => array( 'view', 'edit' ), |
| 974 |
'readonly' => true, |
| 975 |
), |
| 976 |
) |
| 977 |
); |
| 978 |
} |
| 979 |
add_action( 'rest_api_init', 'openstation_my_wordpress_woo_register_rest_field' ); |
| 980 |
|
| 981 |
/** |
| 982 |
* Boot the integration's hooks. Called from the module bootstrap; |
| 983 |
|
| 984 |
/** |
| 985 |
* Boot the integration's hooks. Called from the module bootstrap; |
| 986 |
* every callback re-checks `openstation_my_wordpress_woo_active()` |
| 987 |
* because WooCommerce loads on `plugins_loaded`, after this file. |
| 988 |
* |
| 989 |
* @return void |
| 990 |
*/ |
| 991 |
function openstation_my_wordpress_woo_boot() { |
| 992 |
add_filter( 'openstation_my_wordpress_post_type_group', 'openstation_my_wordpress_woo_group', 10, 2 ); |
| 993 |
add_filter( 'openstation_my_wordpress_entities', 'openstation_my_wordpress_woo_entities', 5 ); |
| 994 |
add_filter( 'openstation_my_wordpress_post_type_entity', 'openstation_my_wordpress_woo_entity_icon', 10, 2 ); |
| 995 |
} |
| 996 |
openstation_my_wordpress_woo_boot(); |
| 997 |
|
| 998 |
/* |
| 999 |
------------------------------------------------------------------- |
| 1000 |
* REST |
| 1001 |
* ---------------------------------------------------------------- |
| 1002 |
*/ |
| 1003 |
|
| 1004 |
/** |
| 1005 |
* Whether the current user may read order data. |
| 1006 |
* |
| 1007 |
* @return true|WP_Error |
| 1008 |
*/ |
| 1009 |
function openstation_my_wordpress_woo_orders_permission() { |
| 1010 |
$orders = get_post_type_object( 'shop_order' ); |
| 1011 |
$cap = $orders instanceof WP_Post_Type && ! empty( $orders->cap->edit_posts ) |
| 1012 |
? $orders->cap->edit_posts |
| 1013 |
: 'manage_woocommerce'; |
| 1014 |
|
| 1015 |
if ( ! current_user_can( $cap ) ) { |
| 1016 |
return new WP_Error( |
| 1017 |
'openstation_woo_forbidden', |
| 1018 |
__( 'Sorry, you are not allowed to view orders.', 'desktop-mode' ), |
| 1019 |
array( 'status' => rest_authorization_required_code() ) |
| 1020 |
); |
| 1021 |
} |
| 1022 |
return true; |
| 1023 |
} |
| 1024 |
|
| 1025 |
/** |
| 1026 |
* Shape an order as a post-shaped row so the site window's existing |
| 1027 |
* list and detail fetchers consume it unchanged. |
| 1028 |
* |
| 1029 |
* @param WC_Abstract_Order $order Order. |
| 1030 |
* @param bool $full Include the `content` field (detail view). |
| 1031 |
* @return array |
| 1032 |
*/ |
| 1033 |
function openstation_my_wordpress_woo_order_row( $order, $full = false ) { |
| 1034 |
$total = html_entity_decode( |
| 1035 |
wp_strip_all_tags( wc_price( $order->get_total(), array( 'currency' => $order->get_currency() ) ) ), |
| 1036 |
ENT_QUOTES, |
| 1037 |
get_bloginfo( 'charset' ) |
| 1038 |
); |
| 1039 |
|
| 1040 |
$row = array( |
| 1041 |
'id' => $order->get_id(), |
| 1042 |
// Tiles show a single line — number and total are what a |
| 1043 |
// merchant scans for; everything else lives in the pane. |
| 1044 |
'title' => array( |
| 1045 |
'rendered' => sprintf( |
| 1046 |
/* translators: 1: order number, 2: formatted order total. */ |
| 1047 |
__( '#%1$s · %2$s', 'desktop-mode' ), |
| 1048 |
$order->get_order_number(), |
| 1049 |
$total |
| 1050 |
), |
| 1051 |
), |
| 1052 |
'excerpt' => array( 'rendered' => '' ), |
| 1053 |
'date' => $order->get_date_created() |
| 1054 |
? $order->get_date_created()->date( 'c' ) |
| 1055 |
: '', |
| 1056 |
// Deliberately `publish`: the tile's status ribbon only speaks |
| 1057 |
// draft/pending/private/future, and a `wc-processing` value |
| 1058 |
// would paint a meaningless ribbon on every order. The real |
| 1059 |
// status is in the pane. |
| 1060 |
'status' => 'publish', |
| 1061 |
// Refunds and custom order types don't carry an edit URL. |
| 1062 |
'link' => method_exists( $order, 'get_edit_order_url' ) |
| 1063 |
? $order->get_edit_order_url() |
| 1064 |
: '', |
| 1065 |
'featured_media' => 0, |
| 1066 |
// The real status, for the tile bands. Kept out of `status` |
| 1067 |
// above on purpose — see the note there. Declared in the |
| 1068 |
// section's `listFields` so `_fields` doesn't strip it. |
| 1069 |
'wcStatus' => $order->get_status(), |
| 1070 |
); |
| 1071 |
|
| 1072 |
if ( $full ) { |
| 1073 |
$row['content'] = array( 'rendered' => '' ); |
| 1074 |
$row['modified'] = $order->get_date_modified() |
| 1075 |
? $order->get_date_modified()->date( 'c' ) |
| 1076 |
: $row['date']; |
| 1077 |
} |
| 1078 |
|
| 1079 |
return $row; |
| 1080 |
} |
| 1081 |
|
| 1082 |
/** |
| 1083 |
* Per-band counts for the Orders section, in display order. |
| 1084 |
* |
| 1085 |
* Each entry is `array( 'statuses' => string[], 'count' => int )`. The |
| 1086 |
* catch-all band (the one declaring no statuses) collects every status |
| 1087 |
* no earlier band claimed, so nothing is dropped and nothing is |
| 1088 |
* counted twice. |
| 1089 |
* |
| 1090 |
* @param array $base Shared `wc_get_orders()` args (search, ordering). |
| 1091 |
* @return array[] |
| 1092 |
*/ |
| 1093 |
function openstation_my_wordpress_woo_order_band_slices( $base ) { |
| 1094 |
$all = array_map( |
| 1095 |
static function ( $status ) { |
| 1096 |
return substr( $status, 3 ); // Strip the `wc-` prefix. |
| 1097 |
}, |
| 1098 |
array_keys( wc_get_order_statuses() ) |
| 1099 |
); |
| 1100 |
$claimed = array(); |
| 1101 |
$slices = array(); |
| 1102 |
|
| 1103 |
foreach ( openstation_my_wordpress_woo_order_bands() as $band ) { |
| 1104 |
$statuses = array_values( |
| 1105 |
array_intersect( (array) ( $band['statuses'] ?? array() ), $all ) |
| 1106 |
); |
| 1107 |
if ( empty( $statuses ) ) { |
| 1108 |
// Catch-all: whatever no earlier band took. |
| 1109 |
$statuses = array_values( array_diff( $all, $claimed ) ); |
| 1110 |
} |
| 1111 |
$claimed = array_merge( $claimed, $statuses ); |
| 1112 |
if ( empty( $statuses ) ) { |
| 1113 |
continue; |
| 1114 |
} |
| 1115 |
|
| 1116 |
$counted = wc_get_orders( |
| 1117 |
array_merge( |
| 1118 |
$base, |
| 1119 |
array( |
| 1120 |
'status' => array_map( |
| 1121 |
static function ( $s ) { |
| 1122 |
return 'wc-' . $s; |
| 1123 |
}, |
| 1124 |
$statuses |
| 1125 |
), |
| 1126 |
'limit' => 1, |
| 1127 |
'paginate' => true, |
| 1128 |
'return' => 'ids', |
| 1129 |
) |
| 1130 |
) |
| 1131 |
); |
| 1132 |
|
| 1133 |
$slices[] = array( |
| 1134 |
'statuses' => array_map( |
| 1135 |
static function ( $s ) { |
| 1136 |
return 'wc-' . $s; |
| 1137 |
}, |
| 1138 |
$statuses |
| 1139 |
), |
| 1140 |
'count' => isset( $counted->total ) ? (int) $counted->total : 0, |
| 1141 |
); |
| 1142 |
} |
| 1143 |
|
| 1144 |
return $slices; |
| 1145 |
} |
| 1146 |
|
| 1147 |
/** |
| 1148 |
* `GET /woocommerce/orders` — paginated, post-shaped order list. |
| 1149 |
* |
| 1150 |
* @param WP_REST_Request $request Request. |
| 1151 |
* @return WP_REST_Response |
| 1152 |
*/ |
| 1153 |
function openstation_my_wordpress_woo_orders( $request ) { |
| 1154 |
$per_page = max( 1, min( 100, (int) ( $request['per_page'] ?? 24 ) ) ); |
| 1155 |
$page = max( 1, (int) ( $request['page'] ?? 1 ) ); |
| 1156 |
$search = (string) ( $request['search'] ?? '' ); |
| 1157 |
|
| 1158 |
$base = array( |
| 1159 |
'orderby' => 'date', |
| 1160 |
'order' => 'DESC', |
| 1161 |
); |
| 1162 |
if ( '' !== $search ) { |
| 1163 |
$base['s'] = $search; |
| 1164 |
} |
| 1165 |
|
| 1166 |
/** |
| 1167 |
* Filter the query args for the site window's Orders section. |
| 1168 |
* |
| 1169 |
* Merged into every per-band query — `status`, `limit`, `offset` |
| 1170 |
* and `paginate` are set by the band walker and will be |
| 1171 |
* overwritten. |
| 1172 |
* |
| 1173 |
* **Status: Experimental** |
| 1174 |
* |
| 1175 |
* @param array $args `wc_get_orders()` args. |
| 1176 |
* @param WP_REST_Request $request The request. |
| 1177 |
*/ |
| 1178 |
$base = (array) apply_filters( 'openstation_my_wordpress_woo_order_args', $base, $request ); |
| 1179 |
|
| 1180 |
// Walk the status bands in display order and slice the requested |
| 1181 |
// page out of the concatenation. Without this the client gets a |
| 1182 |
// date-ordered page and bands materialise in whatever order their |
| 1183 |
// first row happens to arrive, so the grouping visibly reshuffles |
| 1184 |
// as the user scrolls. Ordering server-side means each band's rows |
| 1185 |
// arrive together, in band order, and a band never appears above |
| 1186 |
// content the user has already scrolled past. |
| 1187 |
$slices = openstation_my_wordpress_woo_order_band_slices( $base ); |
| 1188 |
|
| 1189 |
$total = 0; |
| 1190 |
foreach ( $slices as $slice ) { |
| 1191 |
$total += $slice['count']; |
| 1192 |
} |
| 1193 |
$pages = $per_page > 0 ? (int) ceil( $total / $per_page ) : 1; |
| 1194 |
$offset = ( $page - 1 ) * $per_page; |
| 1195 |
|
| 1196 |
$orders = array(); |
| 1197 |
$remaining = $per_page; |
| 1198 |
$cursor = 0; |
| 1199 |
foreach ( $slices as $slice ) { |
| 1200 |
if ( $remaining <= 0 ) { |
| 1201 |
break; |
| 1202 |
} |
| 1203 |
$count = $slice['count']; |
| 1204 |
if ( 0 === $count ) { |
| 1205 |
continue; |
| 1206 |
} |
| 1207 |
// Skip bands that end before the requested offset. |
| 1208 |
if ( $offset >= $cursor + $count ) { |
| 1209 |
$cursor += $count; |
| 1210 |
continue; |
| 1211 |
} |
| 1212 |
$within = max( 0, $offset - $cursor ); |
| 1213 |
$take = min( $remaining, $count - $within ); |
| 1214 |
|
| 1215 |
$page_args = array_merge( |
| 1216 |
$base, |
| 1217 |
array( |
| 1218 |
'status' => $slice['statuses'], |
| 1219 |
'limit' => $take, |
| 1220 |
'offset' => $within, |
| 1221 |
'paginate' => false, |
| 1222 |
) |
| 1223 |
); |
| 1224 |
$batch = wc_get_orders( $page_args ); |
| 1225 |
$batch = is_object( $batch ) && isset( $batch->orders ) |
| 1226 |
? (array) $batch->orders |
| 1227 |
: (array) $batch; |
| 1228 |
|
| 1229 |
$orders = array_merge( $orders, $batch ); |
| 1230 |
$remaining -= count( $batch ); |
| 1231 |
$cursor += $count; |
| 1232 |
$offset = $cursor; |
| 1233 |
} |
| 1234 |
|
| 1235 |
$results = (object) array( |
| 1236 |
'orders' => $orders, |
| 1237 |
'total' => $total, |
| 1238 |
'max_num_pages' => max( 1, $pages ), |
| 1239 |
); |
| 1240 |
|
| 1241 |
// `wc_get_orders()` returns a plain array unless `paginate` is |
| 1242 |
// honoured by the active data store. Handle both so a store with |
| 1243 |
// a custom order data store can't collapse the folder to empty. |
| 1244 |
if ( is_object( $results ) && isset( $results->orders ) ) { |
| 1245 |
$orders = (array) $results->orders; |
| 1246 |
$total = isset( $results->total ) ? (int) $results->total : count( $orders ); |
| 1247 |
$pages = isset( $results->max_num_pages ) ? (int) $results->max_num_pages : 1; |
| 1248 |
} else { |
| 1249 |
$orders = (array) $results; |
| 1250 |
$total = count( $orders ); |
| 1251 |
$pages = 1; |
| 1252 |
} |
| 1253 |
|
| 1254 |
$rows = array(); |
| 1255 |
$skipped = 0; |
| 1256 |
foreach ( $orders as $maybe_order ) { |
| 1257 |
// A data store may hand back ids rather than objects. |
| 1258 |
$order = is_scalar( $maybe_order ) ? wc_get_order( (int) $maybe_order ) : $maybe_order; |
| 1259 |
|
| 1260 |
// `WC_Abstract_Order`, not `WC_Order`: that's the type every |
| 1261 |
// order class actually extends, including HPOS's overrides and |
| 1262 |
// whatever a custom order type registers. Testing against |
| 1263 |
// `WC_Order` silently dropped every row on some stores while |
| 1264 |
// `total` still reported hundreds — an empty folder with a |
| 1265 |
// confident count on its tile. |
| 1266 |
if ( ! $order instanceof WC_Abstract_Order ) { |
| 1267 |
++$skipped; |
| 1268 |
continue; |
| 1269 |
} |
| 1270 |
try { |
| 1271 |
$rows[] = openstation_my_wordpress_woo_order_row( $order ); |
| 1272 |
} catch ( Throwable $e ) { |
| 1273 |
++$skipped; |
| 1274 |
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log |
| 1275 |
error_log( |
| 1276 |
sprintf( |
| 1277 |
'[openstation] Skipped order %d in the site window: %s', |
| 1278 |
is_object( $order ) ? (int) $order->get_id() : 0, |
| 1279 |
$e->getMessage() |
| 1280 |
) |
| 1281 |
); |
| 1282 |
} |
| 1283 |
} |
| 1284 |
|
| 1285 |
$response = rest_ensure_response( $rows ); |
| 1286 |
$response->header( 'X-WP-Total', (string) $total ); |
| 1287 |
$response->header( 'X-WP-TotalPages', (string) $pages ); |
| 1288 |
// Surfaced so an empty-folder-with-a-count can be diagnosed from |
| 1289 |
// the network tab instead of guessed at. |
| 1290 |
$response->header( 'X-Desktop-Mode-Woo-Rows', (string) count( $rows ) ); |
| 1291 |
$response->header( 'X-Desktop-Mode-Woo-Skipped', (string) $skipped ); |
| 1292 |
return $response; |
| 1293 |
} |
| 1294 |
|
| 1295 |
/** |
| 1296 |
* `GET /woocommerce/orders/<id>` — one post-shaped order. |
| 1297 |
* |
| 1298 |
* @param WP_REST_Request $request Request. |
| 1299 |
* @return WP_REST_Response|WP_Error |
| 1300 |
*/ |
| 1301 |
function openstation_my_wordpress_woo_order( $request ) { |
| 1302 |
$order = wc_get_order( (int) $request['id'] ); |
| 1303 |
if ( ! $order instanceof WC_Abstract_Order ) { |
| 1304 |
return new WP_Error( |
| 1305 |
'openstation_woo_no_order', |
| 1306 |
__( 'Order not found.', 'desktop-mode' ), |
| 1307 |
array( 'status' => 404 ) |
| 1308 |
); |
| 1309 |
} |
| 1310 |
return rest_ensure_response( openstation_my_wordpress_woo_order_row( $order, true ) ); |
| 1311 |
} |
| 1312 |
|
| 1313 |
/** |
| 1314 |
* Format an amount in the store's currency, entity-decoded so the |
| 1315 |
* bundle can render it as text. |
| 1316 |
* |
| 1317 |
* @param float $amount Amount. |
| 1318 |
* @param string|null $currency Currency code. |
| 1319 |
* @return string |
| 1320 |
*/ |
| 1321 |
function openstation_my_wordpress_woo_price( $amount, $currency = null ) { |
| 1322 |
$args = $currency ? array( 'currency' => $currency ) : array(); |
| 1323 |
return html_entity_decode( |
| 1324 |
wp_strip_all_tags( wc_price( (float) $amount, $args ) ), |
| 1325 |
ENT_QUOTES, |
| 1326 |
get_bloginfo( 'charset' ) |
| 1327 |
); |
| 1328 |
} |
| 1329 |
|
| 1330 |
/** |
| 1331 |
* Merchant facts for one product. |
| 1332 |
* |
| 1333 |
* @param int $id Product id. |
| 1334 |
* @return array|WP_Error |
| 1335 |
*/ |
| 1336 |
function openstation_my_wordpress_woo_product_summary( $id ) { |
| 1337 |
$product = wc_get_product( $id ); |
| 1338 |
if ( ! $product ) { |
| 1339 |
return new WP_Error( |
| 1340 |
'openstation_woo_no_product', |
| 1341 |
__( 'Product not found.', 'desktop-mode' ), |
| 1342 |
array( 'status' => 404 ) |
| 1343 |
); |
| 1344 |
} |
| 1345 |
|
| 1346 |
$on_sale = $product->is_on_sale(); |
| 1347 |
$stock_status = $product->get_stock_status(); |
| 1348 |
$stock_labels = array( |
| 1349 |
'instock' => __( 'In stock', 'desktop-mode' ), |
| 1350 |
'outofstock' => __( 'Out of stock', 'desktop-mode' ), |
| 1351 |
'onbackorder' => __( 'On backorder', 'desktop-mode' ), |
| 1352 |
); |
| 1353 |
|
| 1354 |
$categories = wp_get_post_terms( $product->get_id(), 'product_cat', array( 'fields' => 'names' ) ); |
| 1355 |
|
| 1356 |
// `wc_get_product_types()` is the registry — there is no |
| 1357 |
// per-type label helper. |
| 1358 |
$types = function_exists( 'wc_get_product_types' ) ? wc_get_product_types() : array(); |
| 1359 |
$type = $product->get_type(); |
| 1360 |
$type_label = isset( $types[ $type ] ) ? (string) $types[ $type ] : ucfirst( (string) $type ); |
| 1361 |
|
| 1362 |
return array( |
| 1363 |
'type' => 'product', |
| 1364 |
'sku' => $product->get_sku(), |
| 1365 |
'price' => openstation_my_wordpress_woo_price( $product->get_price() ), |
| 1366 |
'regular' => $on_sale ? openstation_my_wordpress_woo_price( $product->get_regular_price() ) : '', |
| 1367 |
'onSale' => $on_sale, |
| 1368 |
// Raw slug alongside the label: the bundle tints the stock |
| 1369 |
// pill from the slug, which no translation can break. |
| 1370 |
'stockStatus' => $stock_status, |
| 1371 |
'stockLabel' => $stock_labels[ $stock_status ] ?? $stock_status, |
| 1372 |
'stockLevel' => $product->managing_stock() ? (int) $product->get_stock_quantity() : null, |
| 1373 |
'sold' => (int) $product->get_total_sales(), |
| 1374 |
'rating' => (float) $product->get_average_rating(), |
| 1375 |
'reviews' => (int) $product->get_review_count(), |
| 1376 |
'productType' => $type_label, |
| 1377 |
'variations' => $product->is_type( 'variable' ) ? count( $product->get_children() ) : 0, |
| 1378 |
'categories' => is_wp_error( $categories ) ? array() : array_values( $categories ), |
| 1379 |
'permalink' => (string) $product->get_permalink(), |
| 1380 |
'editUrl' => (string) get_edit_post_link( $product->get_id(), 'raw' ), |
| 1381 |
); |
| 1382 |
} |
| 1383 |
|
| 1384 |
/** |
| 1385 |
* Merchant facts for one order. |
| 1386 |
* |
| 1387 |
* @param int $id Order id. |
| 1388 |
* @return array|WP_Error |
| 1389 |
*/ |
| 1390 |
function openstation_my_wordpress_woo_order_summary( $id ) { |
| 1391 |
$order = wc_get_order( $id ); |
| 1392 |
if ( ! $order instanceof WC_Abstract_Order ) { |
| 1393 |
return new WP_Error( |
| 1394 |
'openstation_woo_no_order', |
| 1395 |
__( 'Order not found.', 'desktop-mode' ), |
| 1396 |
array( 'status' => 404 ) |
| 1397 |
); |
| 1398 |
} |
| 1399 |
|
| 1400 |
$statuses = wc_get_order_statuses(); |
| 1401 |
$status = 'wc-' . $order->get_status(); |
| 1402 |
|
| 1403 |
$items = array(); |
| 1404 |
foreach ( $order->get_items() as $item ) { |
| 1405 |
$product_id = method_exists( $item, 'get_product_id' ) ? (int) $item->get_product_id() : 0; |
| 1406 |
// Variations edit through their parent product's screen. |
| 1407 |
$edit_id = $product_id; |
| 1408 |
$items[] = array( |
| 1409 |
'name' => $item->get_name(), |
| 1410 |
'quantity' => (int) $item->get_quantity(), |
| 1411 |
'total' => openstation_my_wordpress_woo_price( $item->get_total(), $order->get_currency() ), |
| 1412 |
'id' => $product_id, |
| 1413 |
// A line item whose product has since been deleted has no |
| 1414 |
// edit screen to link to; the bundle renders plain text. |
| 1415 |
'editUrl' => $edit_id && get_post( $edit_id ) |
| 1416 |
? (string) get_edit_post_link( $edit_id, 'raw' ) |
| 1417 |
: '', |
| 1418 |
); |
| 1419 |
} |
| 1420 |
|
| 1421 |
// Refunds and custom order types don't carry billing accessors. |
| 1422 |
$name = method_exists( $order, 'get_formatted_billing_full_name' ) |
| 1423 |
? trim( $order->get_formatted_billing_full_name() ) |
| 1424 |
: ''; |
| 1425 |
$customer_id = method_exists( $order, 'get_customer_id' ) |
| 1426 |
? (int) $order->get_customer_id() |
| 1427 |
: 0; |
| 1428 |
|
| 1429 |
return array( |
| 1430 |
'type' => 'order', |
| 1431 |
'number' => $order->get_order_number(), |
| 1432 |
'status' => $order->get_status(), |
| 1433 |
'statusLabel' => $statuses[ $status ] ?? $order->get_status(), |
| 1434 |
'total' => openstation_my_wordpress_woo_price( $order->get_total(), $order->get_currency() ), |
| 1435 |
'subtotal' => openstation_my_wordpress_woo_price( $order->get_subtotal(), $order->get_currency() ), |
| 1436 |
'shipping' => (float) $order->get_shipping_total() > 0 |
| 1437 |
? openstation_my_wordpress_woo_price( $order->get_shipping_total(), $order->get_currency() ) |
| 1438 |
: '', |
| 1439 |
'discount' => (float) $order->get_discount_total() > 0 |
| 1440 |
? openstation_my_wordpress_woo_price( $order->get_discount_total(), $order->get_currency() ) |
| 1441 |
: '', |
| 1442 |
'coupons' => array_values( |
| 1443 |
array_map( |
| 1444 |
static function ( $coupon ) { |
| 1445 |
return $coupon->get_code(); |
| 1446 |
}, |
| 1447 |
$order->get_items( 'coupon' ) |
| 1448 |
) |
| 1449 |
), |
| 1450 |
'paymentVia' => $order->get_payment_method_title(), |
| 1451 |
'datePaid' => $order->get_date_paid() ? $order->get_date_paid()->date( 'c' ) : '', |
| 1452 |
'placed' => $order->get_date_created() ? $order->get_date_created()->date( 'c' ) : '', |
| 1453 |
'customer' => '' !== $name ? $name : __( 'Guest', 'desktop-mode' ), |
| 1454 |
'customerUrl' => $customer_id && current_user_can( 'edit_user', $customer_id ) |
| 1455 |
? (string) get_edit_user_link( $customer_id ) |
| 1456 |
: '', |
| 1457 |
'email' => $order->get_billing_email(), |
| 1458 |
'itemCount' => $order->get_item_count(), |
| 1459 |
'items' => $items, |
| 1460 |
'editUrl' => method_exists( $order, 'get_edit_order_url' ) |
| 1461 |
? $order->get_edit_order_url() |
| 1462 |
: '', |
| 1463 |
); |
| 1464 |
} |
| 1465 |
|
| 1466 |
/** |
| 1467 |
* Total discount a coupon has actually given customers — the number a |
| 1468 |
* merchant wants and WooCommerce never surfaces. |
| 1469 |
* |
| 1470 |
* There's no aggregate for this, so it means walking paid orders and |
| 1471 |
* summing the matching coupon line items. Bounded to the most recent |
| 1472 |
* 500 orders and cached, because the coupon preview pane hits this on |
| 1473 |
* every selection and the scan is by far the most expensive thing in |
| 1474 |
* the summary. |
| 1475 |
* |
| 1476 |
* @param WC_Coupon $coupon Coupon. |
| 1477 |
* @return float Total discount given. |
| 1478 |
*/ |
| 1479 |
function openstation_my_wordpress_woo_coupon_discount_given( $coupon ) { |
| 1480 |
$cache_key = 'openstation_woo_coupon_given_' . $coupon->get_id(); |
| 1481 |
$cached = get_transient( $cache_key ); |
| 1482 |
if ( false !== $cached ) { |
| 1483 |
return (float) $cached; |
| 1484 |
} |
| 1485 |
|
| 1486 |
$granted = 0.0; |
| 1487 |
$code = strtolower( $coupon->get_code() ); |
| 1488 |
$orders = wc_get_orders( |
| 1489 |
array( |
| 1490 |
'limit' => 500, |
| 1491 |
'status' => array( 'wc-processing', 'wc-completed' ), |
| 1492 |
'return' => 'objects', |
| 1493 |
) |
| 1494 |
); |
| 1495 |
foreach ( (array) $orders as $maybe_order ) { |
| 1496 |
$order = is_scalar( $maybe_order ) ? wc_get_order( (int) $maybe_order ) : $maybe_order; |
| 1497 |
if ( ! $order instanceof WC_Abstract_Order ) { |
| 1498 |
continue; |
| 1499 |
} |
| 1500 |
foreach ( $order->get_items( 'coupon' ) as $line ) { |
| 1501 |
if ( strtolower( $line->get_code() ) === $code ) { |
| 1502 |
$granted += (float) $line->get_discount(); |
| 1503 |
} |
| 1504 |
} |
| 1505 |
} |
| 1506 |
|
| 1507 |
set_transient( $cache_key, $granted, 5 * MINUTE_IN_SECONDS ); |
| 1508 |
|
| 1509 |
return $granted; |
| 1510 |
} |
| 1511 |
|
| 1512 |
/** |
| 1513 |
* Merchant facts for one coupon. |
| 1514 |
* |
| 1515 |
* @param int $id Coupon id. |
| 1516 |
* @return array|WP_Error |
| 1517 |
*/ |
| 1518 |
function openstation_my_wordpress_woo_coupon_summary( $id ) { |
| 1519 |
$coupon = new WC_Coupon( $id ); |
| 1520 |
if ( ! $coupon->get_id() ) { |
| 1521 |
return new WP_Error( |
| 1522 |
'openstation_woo_no_coupon', |
| 1523 |
__( 'Coupon not found.', 'desktop-mode' ), |
| 1524 |
array( 'status' => 404 ) |
| 1525 |
); |
| 1526 |
} |
| 1527 |
|
| 1528 |
$expiry = $coupon->get_date_expires(); |
| 1529 |
$limit = (int) $coupon->get_usage_limit(); |
| 1530 |
$used = (int) $coupon->get_usage_count(); |
| 1531 |
|
| 1532 |
// "Active" is expiry + usage-limit only. WooCommerce's full |
| 1533 |
// validity check needs a cart to run against, and a coupon that |
| 1534 |
// merely doesn't apply to the current (empty) cart isn't inactive. |
| 1535 |
$expired = $expiry && $expiry->getTimestamp() < time(); |
| 1536 |
$used_up = $limit > 0 && $used >= $limit; |
| 1537 |
|
| 1538 |
$type_label = 'percent' === $coupon->get_discount_type() |
| 1539 |
? sprintf( |
| 1540 |
/* translators: %s: percentage off. */ |
| 1541 |
__( '%s%% off', 'desktop-mode' ), |
| 1542 |
wc_format_localized_decimal( $coupon->get_amount() ) |
| 1543 |
) |
| 1544 |
: sprintf( |
| 1545 |
/* translators: %s: formatted discount amount. */ |
| 1546 |
__( '%s off', 'desktop-mode' ), |
| 1547 |
openstation_my_wordpress_woo_price( $coupon->get_amount() ) |
| 1548 |
); |
| 1549 |
|
| 1550 |
// Resolve product / category restrictions to names with links — |
| 1551 |
// WooCommerce's own coupon screen shows these as bare token |
| 1552 |
// fields you have to click into. |
| 1553 |
$link_terms = static function ( array $ids, $taxonomy ) { |
| 1554 |
$out = array(); |
| 1555 |
foreach ( $ids as $id ) { |
| 1556 |
$term = get_term( (int) $id, $taxonomy ); |
| 1557 |
if ( $term && ! is_wp_error( $term ) ) { |
| 1558 |
$out[] = array( |
| 1559 |
'label' => $term->name, |
| 1560 |
'editUrl' => (string) get_edit_term_link( $term->term_id, $taxonomy ), |
| 1561 |
); |
| 1562 |
} |
| 1563 |
} |
| 1564 |
return $out; |
| 1565 |
}; |
| 1566 |
|
| 1567 |
$link_products = static function ( array $ids ) { |
| 1568 |
$out = array(); |
| 1569 |
foreach ( $ids as $id ) { |
| 1570 |
$product = wc_get_product( (int) $id ); |
| 1571 |
if ( $product ) { |
| 1572 |
$out[] = array( |
| 1573 |
'label' => $product->get_name(), |
| 1574 |
'editUrl' => (string) get_edit_post_link( $product->get_id(), 'raw' ), |
| 1575 |
); |
| 1576 |
} |
| 1577 |
} |
| 1578 |
return $out; |
| 1579 |
}; |
| 1580 |
|
| 1581 |
$granted = openstation_my_wordpress_woo_coupon_discount_given( $coupon ); |
| 1582 |
|
| 1583 |
return array( |
| 1584 |
'type' => 'coupon', |
| 1585 |
'code' => $coupon->get_code(), |
| 1586 |
'active' => ! $expired && ! $used_up, |
| 1587 |
'inactiveWhy' => $expired |
| 1588 |
? __( 'Expired', 'desktop-mode' ) |
| 1589 |
: ( $used_up ? __( 'Usage limit reached', 'desktop-mode' ) : '' ), |
| 1590 |
'discount' => $type_label, |
| 1591 |
'description' => $coupon->get_description(), |
| 1592 |
'used' => $used, |
| 1593 |
'usageLimit' => $limit, |
| 1594 |
'perUserLimit' => (int) $coupon->get_usage_limit_per_user(), |
| 1595 |
'limitToItems' => (int) $coupon->get_limit_usage_to_x_items(), |
| 1596 |
'granted' => $granted > 0 ? openstation_my_wordpress_woo_price( $granted ) : '', |
| 1597 |
'created' => $coupon->get_date_created() ? $coupon->get_date_created()->date( 'c' ) : '', |
| 1598 |
'expires' => $expiry ? $expiry->date( 'c' ) : '', |
| 1599 |
'minSpend' => $coupon->get_minimum_amount() |
| 1600 |
? openstation_my_wordpress_woo_price( $coupon->get_minimum_amount() ) |
| 1601 |
: '', |
| 1602 |
'maxSpend' => $coupon->get_maximum_amount() |
| 1603 |
? openstation_my_wordpress_woo_price( $coupon->get_maximum_amount() ) |
| 1604 |
: '', |
| 1605 |
'freeShipping' => (bool) $coupon->get_free_shipping(), |
| 1606 |
'individualUse' => (bool) $coupon->get_individual_use(), |
| 1607 |
'excludeSale' => (bool) $coupon->get_exclude_sale_items(), |
| 1608 |
'products' => $link_products( (array) $coupon->get_product_ids() ), |
| 1609 |
'excluded' => $link_products( (array) $coupon->get_excluded_product_ids() ), |
| 1610 |
'categories' => $link_terms( (array) $coupon->get_product_categories(), 'product_cat' ), |
| 1611 |
'emails' => array_values( (array) $coupon->get_email_restrictions() ), |
| 1612 |
'editUrl' => (string) get_edit_post_link( $coupon->get_id(), 'raw' ), |
| 1613 |
); |
| 1614 |
} |
| 1615 |
|
| 1616 |
/** |
| 1617 |
* `GET /woocommerce/summary/<type>/<id>`. |
| 1618 |
* |
| 1619 |
* @param WP_REST_Request $request Request. |
| 1620 |
* @return WP_REST_Response|WP_Error |
| 1621 |
*/ |
| 1622 |
function openstation_my_wordpress_woo_summary( $request ) { |
| 1623 |
$type = (string) $request['type']; |
| 1624 |
$id = (int) $request['id']; |
| 1625 |
|
| 1626 |
switch ( $type ) { |
| 1627 |
case 'product': |
| 1628 |
$data = openstation_my_wordpress_woo_product_summary( $id ); |
| 1629 |
break; |
| 1630 |
case 'order': |
| 1631 |
$data = openstation_my_wordpress_woo_order_summary( $id ); |
| 1632 |
break; |
| 1633 |
case 'coupon': |
| 1634 |
$data = openstation_my_wordpress_woo_coupon_summary( $id ); |
| 1635 |
break; |
| 1636 |
default: |
| 1637 |
/** |
| 1638 |
* Filter in a summary payload for a type this route |
| 1639 |
* doesn't handle itself. |
| 1640 |
* |
| 1641 |
* The route is the one place the site window asks "tell |
| 1642 |
* me about this shop object", so a new object type — a |
| 1643 |
* customer, a subscription, a booking — joins here rather |
| 1644 |
* than needing its own endpoint and its own client |
| 1645 |
* transport. Return `null` (the default) to leave the |
| 1646 |
* type unknown, which answers 400. |
| 1647 |
* |
| 1648 |
* A subscriber MUST also gate its type in |
| 1649 |
* `openstation_my_wordpress_woo_summary_capability`, |
| 1650 |
* which decides who may ask. |
| 1651 |
* |
| 1652 |
* **Status: Experimental** |
| 1653 |
* |
| 1654 |
* @param array|null $data Summary payload, or `null`. |
| 1655 |
* @param string $type The requested type. |
| 1656 |
* @param int $id Object id. |
| 1657 |
*/ |
| 1658 |
$data = apply_filters( 'openstation_my_wordpress_woo_summary_type', null, $type, $id ); |
| 1659 |
|
| 1660 |
if ( ! is_array( $data ) && ! is_wp_error( $data ) ) { |
| 1661 |
return new WP_Error( |
| 1662 |
'openstation_woo_bad_type', |
| 1663 |
__( 'Unknown summary type.', 'desktop-mode' ), |
| 1664 |
array( 'status' => 400 ) |
| 1665 |
); |
| 1666 |
} |
| 1667 |
break; |
| 1668 |
} |
| 1669 |
|
| 1670 |
if ( is_wp_error( $data ) ) { |
| 1671 |
return $data; |
| 1672 |
} |
| 1673 |
|
| 1674 |
/** |
| 1675 |
* Filter the merchant summary shown in the site window's right |
| 1676 |
* pane for a product, order, or coupon. |
| 1677 |
* |
| 1678 |
* **Status: Experimental** |
| 1679 |
* |
| 1680 |
* @param array $data Summary payload. |
| 1681 |
* @param string $type One of `product`, `order`, `coupon`. |
| 1682 |
* @param int $id Object id. |
| 1683 |
*/ |
| 1684 |
return rest_ensure_response( |
| 1685 |
(array) apply_filters( 'openstation_my_wordpress_woo_summary', $data, $type, $id ) |
| 1686 |
); |
| 1687 |
} |
| 1688 |
|
| 1689 |
/** |
| 1690 |
* Permission check for a summary request — the capability depends on |
| 1691 |
* what is being summarised. |
| 1692 |
* |
| 1693 |
* @param WP_REST_Request $request Request. |
| 1694 |
* @return true|WP_Error |
| 1695 |
*/ |
| 1696 |
function openstation_my_wordpress_woo_summary_permission( $request ) { |
| 1697 |
$type = (string) $request['type']; |
| 1698 |
$id = (int) $request['id']; |
| 1699 |
|
| 1700 |
if ( 'order' === $type ) { |
| 1701 |
return openstation_my_wordpress_woo_orders_permission(); |
| 1702 |
} |
| 1703 |
|
| 1704 |
/** |
| 1705 |
* Filter the permission check for a summary type this route |
| 1706 |
* doesn't handle itself. |
| 1707 |
* |
| 1708 |
* Return `true` to allow, a `WP_Error` to deny, or `null` (the |
| 1709 |
* default) to fall through to the post-capability check below — |
| 1710 |
* which is only meaningful for types whose id IS a post id. Any |
| 1711 |
* type added through |
| 1712 |
* `openstation_my_wordpress_woo_summary_type` must answer here |
| 1713 |
* too, or it inherits a capability check that means nothing for |
| 1714 |
* it. |
| 1715 |
* |
| 1716 |
* **Status: Experimental** |
| 1717 |
* |
| 1718 |
* @param true|WP_Error|null $allowed Permission verdict. |
| 1719 |
* @param string $type The requested type. |
| 1720 |
* @param int $id Object id. |
| 1721 |
*/ |
| 1722 |
$allowed = apply_filters( 'openstation_my_wordpress_woo_summary_capability', null, $type, $id ); |
| 1723 |
if ( true === $allowed || is_wp_error( $allowed ) ) { |
| 1724 |
return $allowed; |
| 1725 |
} |
| 1726 |
|
| 1727 |
if ( ! current_user_can( 'edit_post', $id ) ) { |
| 1728 |
return new WP_Error( |
| 1729 |
'openstation_woo_forbidden', |
| 1730 |
__( 'Sorry, you are not allowed to view this item.', 'desktop-mode' ), |
| 1731 |
array( 'status' => rest_authorization_required_code() ) |
| 1732 |
); |
| 1733 |
} |
| 1734 |
return true; |
| 1735 |
} |
| 1736 |
|
| 1737 |
/** |
| 1738 |
* `GET /woocommerce/store` — headline numbers for the Woo folder. |
| 1739 |
* |
| 1740 |
* Deliberately three cheap queries: a revenue sum over paid orders |
| 1741 |
* this month, a count of orders awaiting action, and a low-stock |
| 1742 |
* count. Anything heavier belongs in WooCommerce Analytics. |
| 1743 |
* |
| 1744 |
* @return WP_REST_Response |
| 1745 |
*/ |
| 1746 |
function openstation_my_wordpress_woo_store() { |
| 1747 |
$month_start = gmdate( 'Y-m-01 00:00:00', current_time( 'timestamp' ) ); |
| 1748 |
|
| 1749 |
$paid = wc_get_orders( |
| 1750 |
array( |
| 1751 |
'limit' => -1, |
| 1752 |
'status' => array( 'wc-processing', 'wc-completed' ), |
| 1753 |
'date_created' => '>=' . $month_start, |
| 1754 |
'return' => 'objects', |
| 1755 |
) |
| 1756 |
); |
| 1757 |
|
| 1758 |
$revenue = 0.0; |
| 1759 |
foreach ( (array) $paid as $maybe_order ) { |
| 1760 |
$order = is_scalar( $maybe_order ) ? wc_get_order( (int) $maybe_order ) : $maybe_order; |
| 1761 |
if ( $order instanceof WC_Abstract_Order ) { |
| 1762 |
$revenue += (float) $order->get_total(); |
| 1763 |
} |
| 1764 |
} |
| 1765 |
|
| 1766 |
// Read the statuses straight off the "Needs attention" band rather |
| 1767 |
// than repeating them. They had drifted: the band counted |
| 1768 |
// pending + processing + on-hold while this counted only the last |
| 1769 |
// two, so the folder and the panel disagreed by every pending |
| 1770 |
// order on the store. |
| 1771 |
$attention = array(); |
| 1772 |
foreach ( openstation_my_wordpress_woo_order_bands() as $band ) { |
| 1773 |
if ( 'needs-action' === ( $band['id'] ?? '' ) ) { |
| 1774 |
$attention = array_map( |
| 1775 |
static function ( $status ) { |
| 1776 |
return 'wc-' . $status; |
| 1777 |
}, |
| 1778 |
(array) ( $band['statuses'] ?? array() ) |
| 1779 |
); |
| 1780 |
break; |
| 1781 |
} |
| 1782 |
} |
| 1783 |
if ( empty( $attention ) ) { |
| 1784 |
$attention = array( 'wc-processing', 'wc-on-hold', 'wc-pending' ); |
| 1785 |
} |
| 1786 |
|
| 1787 |
$processing = wc_get_orders( |
| 1788 |
array( |
| 1789 |
'limit' => 1, |
| 1790 |
'paginate' => true, |
| 1791 |
'return' => 'ids', |
| 1792 |
'status' => $attention, |
| 1793 |
) |
| 1794 |
); |
| 1795 |
|
| 1796 |
$low_stock = wc_get_products( |
| 1797 |
array( |
| 1798 |
'limit' => -1, |
| 1799 |
'return' => 'ids', |
| 1800 |
'stock_status' => 'outofstock', |
| 1801 |
) |
| 1802 |
); |
| 1803 |
|
| 1804 |
$data = array( |
| 1805 |
'revenue' => openstation_my_wordpress_woo_price( $revenue ), |
| 1806 |
'revenueRaw' => round( $revenue, 2 ), |
| 1807 |
'processing' => isset( $processing->total ) ? (int) $processing->total : 0, |
| 1808 |
'outOfStock' => is_array( $low_stock ) ? count( $low_stock ) : 0, |
| 1809 |
'ordersUrl' => admin_url( 'admin.php?page=wc-orders' ), |
| 1810 |
); |
| 1811 |
|
| 1812 |
/** |
| 1813 |
* Filter the store headline numbers shown on the Woo folder. |
| 1814 |
* |
| 1815 |
* **Status: Experimental** |
| 1816 |
* |
| 1817 |
* @param array $data Store totals. |
| 1818 |
*/ |
| 1819 |
return rest_ensure_response( |
| 1820 |
(array) apply_filters( 'openstation_my_wordpress_woo_store', $data ) |
| 1821 |
); |
| 1822 |
} |
| 1823 |
|
| 1824 |
/** |
| 1825 |
* Register the integration's REST routes. |
| 1826 |
* |
| 1827 |
* @return void |
| 1828 |
*/ |
| 1829 |
function openstation_my_wordpress_woo_register_routes() { |
| 1830 |
if ( ! openstation_my_wordpress_woo_active() ) { |
| 1831 |
return; |
| 1832 |
} |
| 1833 |
|
| 1834 |
register_rest_route( |
| 1835 |
'desktop-mode/v1', |
| 1836 |
'/woocommerce/orders', |
| 1837 |
array( |
| 1838 |
'methods' => WP_REST_Server::READABLE, |
| 1839 |
'callback' => 'openstation_my_wordpress_woo_orders', |
| 1840 |
'permission_callback' => 'openstation_my_wordpress_woo_orders_permission', |
| 1841 |
'args' => array( |
| 1842 |
'page' => array( |
| 1843 |
'type' => 'integer', |
| 1844 |
'default' => 1, |
| 1845 |
), |
| 1846 |
'per_page' => array( |
| 1847 |
'type' => 'integer', |
| 1848 |
'default' => 24, |
| 1849 |
), |
| 1850 |
'search' => array( 'type' => 'string' ), |
| 1851 |
), |
| 1852 |
) |
| 1853 |
); |
| 1854 |
|
| 1855 |
register_rest_route( |
| 1856 |
'desktop-mode/v1', |
| 1857 |
'/woocommerce/orders/(?P<id>\d+)', |
| 1858 |
array( |
| 1859 |
'methods' => WP_REST_Server::READABLE, |
| 1860 |
'callback' => 'openstation_my_wordpress_woo_order', |
| 1861 |
'permission_callback' => 'openstation_my_wordpress_woo_orders_permission', |
| 1862 |
'args' => array( |
| 1863 |
'id' => array( 'type' => 'integer' ), |
| 1864 |
), |
| 1865 |
) |
| 1866 |
); |
| 1867 |
|
| 1868 |
register_rest_route( |
| 1869 |
'desktop-mode/v1', |
| 1870 |
'/woocommerce/summary/(?P<type>[a-z]+)/(?P<id>\d+)', |
| 1871 |
array( |
| 1872 |
'methods' => WP_REST_Server::READABLE, |
| 1873 |
'callback' => 'openstation_my_wordpress_woo_summary', |
| 1874 |
'permission_callback' => 'openstation_my_wordpress_woo_summary_permission', |
| 1875 |
'args' => array( |
| 1876 |
'type' => array( 'type' => 'string' ), |
| 1877 |
'id' => array( 'type' => 'integer' ), |
| 1878 |
), |
| 1879 |
) |
| 1880 |
); |
| 1881 |
|
| 1882 |
register_rest_route( |
| 1883 |
'desktop-mode/v1', |
| 1884 |
'/woocommerce/store', |
| 1885 |
array( |
| 1886 |
'methods' => WP_REST_Server::READABLE, |
| 1887 |
'callback' => 'openstation_my_wordpress_woo_store', |
| 1888 |
'permission_callback' => 'openstation_my_wordpress_woo_orders_permission', |
| 1889 |
) |
| 1890 |
); |
| 1891 |
} |
| 1892 |
add_action( 'rest_api_init', 'openstation_my_wordpress_woo_register_routes' ); |
| 1893 |
|
| 1894 |
/* |
| 1895 |
------------------------------------------------------------------- |
| 1896 |
* Assets |
| 1897 |
* ---------------------------------------------------------------- |
| 1898 |
*/ |
| 1899 |
|
| 1900 |
/** |
| 1901 |
* Register the integration bundle. |
| 1902 |
* |
| 1903 |
* @return void |
| 1904 |
*/ |
| 1905 |
function openstation_my_wordpress_woo_register_assets() { |
| 1906 |
wp_register_script( |
| 1907 |
'os-my-wordpress-woocommerce', |
| 1908 |
OPENSTATION_URL . 'assets/js/my-wordpress-woocommerce' . ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min' ) . '.js', |
| 1909 |
array( 'wp-hooks' ), |
| 1910 |
OPENSTATION_VERSION, |
| 1911 |
true |
| 1912 |
); |
| 1913 |
wp_set_script_translations( 'os-my-wordpress-woocommerce', 'desktop-mode' ); |
| 1914 |
|
| 1915 |
wp_register_style( |
| 1916 |
'os-my-wordpress-woocommerce', |
| 1917 |
OPENSTATION_URL . 'assets/css/my-wordpress-woocommerce.css', |
| 1918 |
array( 'desktop-mode-my-wordpress' ), |
| 1919 |
OPENSTATION_VERSION |
| 1920 |
); |
| 1921 |
} |
| 1922 |
add_action( 'init', 'openstation_my_wordpress_woo_register_assets', 5 ); |
| 1923 |
|
| 1924 |
/** |
| 1925 |
* Attach the integration's config to its script handle. |
| 1926 |
* |
| 1927 |
* The bundle itself is NOT enqueued here, and that is the point. It |
| 1928 |
* subscribes to the WP Explorer window's `preview-extras` / |
| 1929 |
* `group-extras` actions, so it has to be in the tab before that |
| 1930 |
* window's bundle paints — but not one moment sooner. It travels as |
| 1931 |
* a companion of `desktop-mode-my-wordpress` (see the `scripts` arg |
| 1932 |
* on that window's registration), which means the shell loads it |
| 1933 |
* when the window first opens and a merchant who never opens WP |
| 1934 |
* Explorer never downloads it at all. |
| 1935 |
* |
| 1936 |
* Only for users who can open the site window on a store — everyone |
| 1937 |
* else pays nothing. |
| 1938 |
* |
| 1939 |
* Runs at priority 5, ahead of `openstation_enqueue_assets()` at 10: |
| 1940 |
* that is where the boot payload is built, and it harvests this |
| 1941 |
* inline blob off the registered handle so the lazy loader can |
| 1942 |
* replay it around the script tag. Attaching later would ship the |
| 1943 |
* bundle with no config. |
| 1944 |
* |
| 1945 |
* @return void |
| 1946 |
*/ |
| 1947 |
function openstation_my_wordpress_woo_enqueue() { |
| 1948 |
if ( ! openstation_my_wordpress_woo_active() ) { |
| 1949 |
return; |
| 1950 |
} |
| 1951 |
if ( ! function_exists( 'openstation_is_enabled' ) || ! openstation_is_enabled() ) { |
| 1952 |
return; |
| 1953 |
} |
| 1954 |
if ( ! openstation_my_wordpress_user_can_use() ) { |
| 1955 |
return; |
| 1956 |
} |
| 1957 |
|
| 1958 |
// The stylesheet stays eager: it is a few KB, it has no parse |
| 1959 |
// cost worth deferring, and the window's own CSS is enqueued the |
| 1960 |
// same way. |
| 1961 |
wp_enqueue_style( 'os-my-wordpress-woocommerce' ); |
| 1962 |
wp_add_inline_script( |
| 1963 |
'os-my-wordpress-woocommerce', |
| 1964 |
sprintf( |
| 1965 |
'window.openStationWooConfig=%s;', |
| 1966 |
wp_json_encode( |
| 1967 |
array( |
| 1968 |
'restRoot' => esc_url_raw( rest_url( 'desktop-mode/v1/woocommerce/' ) ), |
| 1969 |
'restNonce' => wp_create_nonce( 'wp_rest' ), |
| 1970 |
'canOrders' => true === openstation_my_wordpress_woo_orders_permission(), |
| 1971 |
'canCustomers' => true === openstation_my_wordpress_woo_customers_permission(), |
| 1972 |
'orderBands' => openstation_my_wordpress_woo_order_bands(), |
| 1973 |
'productBands' => openstation_my_wordpress_woo_product_bands(), |
| 1974 |
'couponBands' => openstation_my_wordpress_woo_coupon_bands_with_counts(), |
| 1975 |
// Only built for a viewer who may see them — the |
| 1976 |
// band counts are money, and the plan behind them |
| 1977 |
// is a full pass over the user base. |
| 1978 |
'customerBands' => true === openstation_my_wordpress_woo_customers_permission() |
| 1979 |
? openstation_my_wordpress_woo_customer_bands_with_counts() |
| 1980 |
: array(), |
| 1981 |
// Whether the catalogue is small enough to be |
| 1982 |
// band-ordered server-side. Read it from the |
| 1983 |
// console (`window.openStationWooConfig.ordering`) |
| 1984 |
// when the Products bands look wrong: `capped` |
| 1985 |
// means rows arrive stock-ordered only and the |
| 1986 |
// category bands fill progressively. |
| 1987 |
'ordering' => openstation_my_wordpress_woo_ordering_state(), |
| 1988 |
) |
| 1989 |
) |
| 1990 |
), |
| 1991 |
'before' |
| 1992 |
); |
| 1993 |
} |
| 1994 |
add_action( 'admin_enqueue_scripts', 'openstation_my_wordpress_woo_enqueue', 5 ); |
| 1995 |
|
| 1996 |
/** |
| 1997 |
* Attach the bundle to the WP Explorer window as a companion script. |
| 1998 |
* |
| 1999 |
* `scripts` handles load in order immediately before the window's own |
| 2000 |
* `script`, so the integration is listening to `preview-extras` / |
| 2001 |
* `group-extras` by the time the window bundle fires them — the same |
| 2002 |
* guarantee the old boot-time enqueue gave, at the cost of nothing |
| 2003 |
* until the window opens. |
| 2004 |
* |
| 2005 |
* @param array $window_args Args passed to `openstation_register_window()`. |
| 2006 |
* @return array |
| 2007 |
*/ |
| 2008 |
function openstation_my_wordpress_woo_window_args( $window_args ) { |
| 2009 |
if ( ! is_array( $window_args ) || ! openstation_my_wordpress_woo_active() ) { |
| 2010 |
return $window_args; |
| 2011 |
} |
| 2012 |
|
| 2013 |
$scripts = isset( $window_args['scripts'] ) ? (array) $window_args['scripts'] : array(); |
| 2014 |
$scripts[] = 'os-my-wordpress-woocommerce'; |
| 2015 |
|
| 2016 |
$window_args['scripts'] = $scripts; |
| 2017 |
|
| 2018 |
return $window_args; |
| 2019 |
} |
| 2020 |
add_filter( 'openstation_my_wordpress_window_args', 'openstation_my_wordpress_woo_window_args' ); |
| 2021 |
|