| @@ -16,12 +16,14 @@ | ||
| 16 | 16 | use WC_Order; |
| 17 | 17 | use WC_Order_Item; |
| 18 | 18 | use WC_Order_Item_Product; |
| 19 | 19 | use WC_Order_Item_Shipping; |
| 20 | +use WC_Payment_Gateway; | |
| 20 | 21 | use WC_Discounts; |
| 21 | 22 | use WC_Product; |
| 22 | 23 | use WC_Product_Simple; |
| 23 | 24 | use WC_Tax; |
| 25 | +use WCPOS\WooCommercePOS\Payments\Gateway_Contract; | |
| 24 | 26 | |
| 25 | 27 | /** |
| 26 | 28 | * Orders Class |
| 27 | 29 | * - runs for all life cycles. |
| @@ -49,8 +51,15 @@ | ||
| 49 | 51 | */ |
| 50 | 52 | private static $temp_id_counter = 0; |
| 51 | 53 | |
| 52 | 54 | /** |
| 55 | + * Armed date_paid suppressions, keyed by order id — see arm_paid_date_suppression(). | |
| 56 | + * | |
| 57 | + * @var array<int, callable> | |
| 58 | + */ | |
| 59 | + private $paid_date_suppressions = array(); | |
| 60 | + | |
| 61 | + /** | |
| 53 | 62 | * Constructor. |
| 54 | 63 | */ |
| 55 | 64 | public function __construct() { |
| 56 | 65 | $this->register_order_status(); |
| @@ -61,9 +70,13 @@ | ||
| 61 | 70 | add_filter( 'woocommerce_payment_complete_order_status', array( $this, 'payment_complete_order_status' ), 10, 3 ); |
| 62 | 71 | add_filter( 'woocommerce_bacs_process_payment_order_status', array( $this, 'offline_process_payment_order_status' ), 10, 2 ); |
| 63 | 72 | add_filter( 'woocommerce_cheque_process_payment_order_status', array( $this, 'offline_process_payment_order_status' ), 10, 2 ); |
| 64 | 73 | add_filter( 'woocommerce_cod_process_payment_order_status', array( $this, 'offline_process_payment_order_status' ), 10, 2 ); |
| 65 | - add_filter( 'woocommerce_payment_successful_result', array( $this, 'apply_unpaid_gateway_order_status' ), 10, 2 ); | |
| 74 | + // PHP_INT_MAX: the redirect is the decision input and gateways rewrite it on | |
| 75 | + // this same filter — Stripe swaps get_return_url() for a #confirm-pi hash at | |
| 76 | + // 99999 when the intent needs 3DS. Reading it earlier closes a sale the | |
| 77 | + // customer is still confirming. | |
| 78 | + add_filter( 'woocommerce_payment_successful_result', array( $this, 'apply_unpaid_gateway_order_status' ), PHP_INT_MAX, 2 ); | |
| 66 | 79 | add_filter( 'woocommerce_hidden_order_itemmeta', array( $this, 'hidden_order_itemmeta' ) ); |
| 67 | 80 | add_filter( 'woocommerce_order_item_product', array( $this, 'order_item_product' ), 10, 2 ); |
| 68 | 81 | add_filter( 'woocommerce_order_get_tax_location', array( $this, 'get_tax_location' ), 10, 2 ); |
| 69 | 82 | add_action( 'woocommerce_order_item_after_calculate_taxes', array( $this, 'order_item_after_calculate_taxes' ) ); |
| @@ -177,12 +190,105 @@ | ||
| 177 | 190 | if ( ! woocommerce_pos_is_pos_order( $order ) ) { |
| 178 | 191 | return $status; |
| 179 | 192 | } |
| 180 | 193 | |
| 181 | - return $this->normalize_status( $this->get_gateway_order_status( $order->get_payment_method() ), $status ); | |
| 194 | + $configured = $this->normalize_status( $this->get_gateway_order_status( $order->get_payment_method() ), $status ); | |
| 195 | + | |
| 196 | + /* | |
| 197 | + * The gateway writes this status itself with update_status(), and no money | |
| 198 | + * has been taken at the till. payment_complete_order_status() reports the | |
| 199 | + * configured status as this order's paid status, so set_status() would | |
| 200 | + * stamp date_paid the moment the order lands on it — booking a pending | |
| 201 | + * "pay by invoice" or on-hold bank-transfer order as revenue. Suppress the | |
| 202 | + * stamp for that write when the configured status is one WooCommerce | |
| 203 | + * does not itself call paid; a paid status keeps WooCommerce's own | |
| 204 | + * behaviour (core stamps date_paid for COD landing on processing). | |
| 205 | + * | |
| 206 | + * This filter runs before the gateway's update_status(), so the | |
| 207 | + * suppression is armed here and released once the order object is saved | |
| 208 | + * — see release_paid_date_suppression_after_save(). | |
| 209 | + */ | |
| 210 | + if ( $order instanceof WC_Order && ! \in_array( $configured, wc_get_is_paid_statuses(), true ) ) { | |
| 211 | + $this->arm_paid_date_suppression( $order->get_id() ); | |
| 212 | + } | |
| 213 | + | |
| 214 | + return $configured; | |
| 182 | 215 | } |
| 183 | 216 | |
| 184 | 217 | /** |
| 218 | + * Suppress the date_paid stamp for one order until its next save. | |
| 219 | + * | |
| 220 | + * Scoped to the order id: a status-transition handler can call | |
| 221 | + * payment_complete() on a *different* order while this filter is live | |
| 222 | + * (subscriptions, bundles and gift-card plugins all do), and an | |
| 223 | + * unconditional '' would reach that order too — set_status() rejects an | |
| 224 | + * unknown status and falls back to 'pending', leaving an order that was | |
| 225 | + * just paid sitting unpaid. | |
| 226 | + * | |
| 227 | + * Released on `woocommerce_after_order_object_save` for that order, which | |
| 228 | + * fires whether or not the status actually changed, and on `shutdown` as a | |
| 229 | + * backstop so a write that never happens cannot leave it armed. | |
| 230 | + * | |
| 231 | + * @param int $order_id Order ID. | |
| 232 | + */ | |
| 233 | + private function arm_paid_date_suppression( int $order_id ): void { | |
| 234 | + if ( isset( $this->paid_date_suppressions[ $order_id ] ) ) { | |
| 235 | + return; | |
| 236 | + } | |
| 237 | + | |
| 238 | + $suppress = static function ( $payment_status, $filtered_order_id ) use ( $order_id ) { | |
| 239 | + return (int) $filtered_order_id === $order_id ? '' : $payment_status; | |
| 240 | + }; | |
| 241 | + | |
| 242 | + $this->paid_date_suppressions[ $order_id ] = $suppress; | |
| 243 | + | |
| 244 | + add_filter( 'woocommerce_payment_complete_order_status', $suppress, PHP_INT_MAX, 2 ); | |
| 245 | + add_action( 'woocommerce_after_order_object_save', array( $this, 'release_paid_date_suppression_after_save' ), 10, 1 ); | |
| 246 | + add_action( 'shutdown', array( $this, 'release_all_paid_date_suppressions' ) ); | |
| 247 | + } | |
| 248 | + | |
| 249 | + /** | |
| 250 | + * Release the date_paid suppression once the order it was armed for is saved. | |
| 251 | + * | |
| 252 | + * @param WC_Abstract_Order $order The saved order. | |
| 253 | + */ | |
| 254 | + public function release_paid_date_suppression_after_save( $order ): void { | |
| 255 | + if ( ! $order instanceof WC_Abstract_Order ) { | |
| 256 | + return; | |
| 257 | + } | |
| 258 | + | |
| 259 | + $this->release_paid_date_suppression( $order->get_id() ); | |
| 260 | + } | |
| 261 | + | |
| 262 | + /** | |
| 263 | + * Release every armed date_paid suppression. | |
| 264 | + */ | |
| 265 | + public function release_all_paid_date_suppressions(): void { | |
| 266 | + foreach ( array_keys( $this->paid_date_suppressions ) as $order_id ) { | |
| 267 | + $this->release_paid_date_suppression( (int) $order_id ); | |
| 268 | + } | |
| 269 | + } | |
| 270 | + | |
| 271 | + /** | |
| 272 | + * Remove one order's date_paid suppression from the filter. | |
| 273 | + * | |
| 274 | + * @param int $order_id Order ID. | |
| 275 | + */ | |
| 276 | + private function release_paid_date_suppression( int $order_id ): void { | |
| 277 | + if ( ! isset( $this->paid_date_suppressions[ $order_id ] ) ) { | |
| 278 | + return; | |
| 279 | + } | |
| 280 | + | |
| 281 | + remove_filter( 'woocommerce_payment_complete_order_status', $this->paid_date_suppressions[ $order_id ], PHP_INT_MAX ); | |
| 282 | + unset( $this->paid_date_suppressions[ $order_id ] ); | |
| 283 | + | |
| 284 | + if ( empty( $this->paid_date_suppressions ) ) { | |
| 285 | + remove_action( 'woocommerce_after_order_object_save', array( $this, 'release_paid_date_suppression_after_save' ), 10 ); | |
| 286 | + remove_action( 'shutdown', array( $this, 'release_all_paid_date_suppressions' ) ); | |
| 287 | + } | |
| 288 | + } | |
| 289 | + | |
| 290 | + /** | |
| 185 | 291 | * Normalise a configured gateway order status for the WooCommerce status filters. |
| 186 | 292 | * |
| 187 | 293 | * Both `woocommerce_payment_complete_order_status` and the offline gateway |
| 188 | 294 | * `*_process_payment_order_status` filters expect a status *without* the `wc-` |
| @@ -240,8 +346,17 @@ | ||
| 240 | 346 | * - the gateway must be enabled for POS *and* carry an explicitly stored |
| 241 | 347 | * status. The settings view synthesizes `wc-completed` for every installed |
| 242 | 348 | * gateway it has never seen, so trusting the computed value would mark an |
| 243 | 349 | * unconfigured third-party gateway Completed with no money taken. |
| 350 | + * - the gateway's redirect must be the order's own received page. That is | |
| 351 | + * the gateway saying it is finished; any other target means it is still | |
| 352 | + * collecting the money — see redirect_targets_order_received(). | |
| 353 | + * - the gateway must not be one that moves money, read from the capability | |
| 354 | + * WooCommerce has every gateway declare: refunds. A gateway that can give | |
| 355 | + * money back takes money; if it hands the customer to the received page | |
| 356 | + * without having taken it, the payment is pending (a bank transfer, an | |
| 357 | + * async method) and its webhook owns the status. A quote, invoice or | |
| 358 | + * purchase-order gateway cannot refund, because nothing was ever paid. | |
| 244 | 359 | * |
| 245 | 360 | * @param array $result Gateway result, passed through untouched. |
| 246 | 361 | * @param int $order_id Order ID. |
| 247 | 362 | * |
| @@ -261,9 +376,17 @@ | ||
| 261 | 376 | if ( ! $order->has_status( 'pos-open' ) || $order->get_date_paid( 'edit' ) ) { |
| 262 | 377 | return $result; |
| 263 | 378 | } |
| 264 | 379 | |
| 380 | + if ( ! \is_array( $result ) || ! $this->redirect_targets_order_received( $result, $order ) ) { | |
| 381 | + return $result; | |
| 382 | + } | |
| 383 | + | |
| 265 | 384 | $gateway_id = $order->get_payment_method(); |
| 385 | + | |
| 386 | + if ( $this->gateway_moves_money( $gateway_id ) ) { | |
| 387 | + return $result; | |
| 388 | + } | |
| 266 | 389 | $configured = $this->get_stored_gateway_order_status( $gateway_id ); |
| 267 | 390 | |
| 268 | 391 | if ( '' === $configured ) { |
| 269 | 392 | return $result; |
| @@ -322,65 +445,190 @@ | ||
| 322 | 445 | return $result; |
| 323 | 446 | } |
| 324 | 447 | |
| 325 | 448 | /** |
| 326 | - * Read the explicitly stored per-gateway order status. | |
| 449 | + * Whether a successful gateway result sends the customer to the order's received page. | |
| 327 | 450 | * |
| 328 | - * Reads the raw options rather than the settings service, because the service | |
| 329 | - * rebuilds its view from the installed gateways and synthesizes a default | |
| 330 | - * status for gateways the merchant has never configured. Only a status the | |
| 331 | - * merchant actually chose, on a gateway they enabled for POS, counts as intent. | |
| 451 | + * The redirect is the one thing every gateway declares about what happens | |
| 452 | + * next, and it separates the two shapes that both "return success and leave | |
| 453 | + * the order open": | |
| 332 | 454 | * |
| 333 | - * Two places hold such a choice, matching Payment_Gateways_Section::read(): | |
| 334 | - * the per-gateway entry, and — on sites upgraded from before per-gateway | |
| 335 | - * statuses — the legacy global `checkout.order_status`, which that section | |
| 336 | - * still applies in memory to any gateway with no explicit status of its own | |
| 337 | - * until the merchant next saves. | |
| 455 | + * - A gateway that is finished sends the customer to the received page, | |
| 456 | + * `get_return_url( $order )`. Quotes, invoices, purchase orders, BACS, | |
| 457 | + * cheque and COD all do. No money will ever move through it, so the | |
| 458 | + * configured POS status is the only thing that closes the sale. | |
| 459 | + * - A gateway that still has to collect the money sends them somewhere else: | |
| 460 | + * a hosted checkout off-site (Dintero, Mollie, PayPal, Klarna), or an | |
| 461 | + * on-site pay or receipt page that posts a form to one. It settles the | |
| 462 | + * order later, from its callback, through payment_complete(). Acting on | |
| 463 | + * this shape marked the order Completed while the cashier was still | |
| 464 | + * looking at the hosted checkout (1.10.20–1.10.22): Dintero's capture | |
| 465 | + * handler then found no transaction and bounced the order to on-hold, and | |
| 466 | + * the till — which reads any status outside its open/unpaid set as a | |
| 467 | + * finished sale — opened the receipt before a payment method was chosen. | |
| 338 | 468 | * |
| 339 | - * @param string $gateway_id The payment gateway ID. | |
| 469 | + * Compared without scheme or trailing slash: `get_return_url()` may upgrade to | |
| 470 | + * https, and a gateway may append its own arguments. Both the order's received | |
| 471 | + * URL and its `woocommerce_get_return_url`-filtered form are accepted, so a | |
| 472 | + * plugin that moves the thank-you page still matches — see is_same_page() for | |
| 473 | + * what "same page" means on plain permalinks, where the page is in the query. | |
| 340 | 474 | * |
| 341 | - * @return string The stored status (may include the wc- prefix), or '' when absent. | |
| 475 | + * A redirect carrying a fragment never matches. A fragment on a thank-you URL | |
| 476 | + * is a gateway's instruction to its own script to do something before the sale | |
| 477 | + * is done (Stripe's and WooPayments' `#confirm-pi…` 3DS hand-off); a gateway | |
| 478 | + * that is finished has no reason to add one. Nor does a missing or relative | |
| 479 | + * redirect match: WooCommerce's pay handler would redirect to it unchanged, | |
| 480 | + * and nothing here can tell what it is. | |
| 481 | + * | |
| 482 | + * @param array $result Gateway result from process_payment(). | |
| 483 | + * @param WC_Order $order The order being paid. | |
| 484 | + * | |
| 485 | + * @return bool | |
| 342 | 486 | */ |
| 343 | - private function get_stored_gateway_order_status( string $gateway_id ): string { | |
| 344 | - if ( '' === $gateway_id ) { | |
| 345 | - return ''; | |
| 487 | + private function redirect_targets_order_received( array $result, WC_Order $order ): bool { | |
| 488 | + $redirect = isset( $result['redirect'] ) && \is_string( $result['redirect'] ) ? trim( $result['redirect'] ) : ''; | |
| 489 | + | |
| 490 | + if ( '' === $redirect || false !== strpos( $redirect, '#' ) ) { | |
| 491 | + return false; | |
| 346 | 492 | } |
| 347 | 493 | |
| 348 | - $stored = get_option( 'woocommerce_pos_settings_payment_gateways', array() ); | |
| 494 | + $received_url = $order->get_checkout_order_received_url(); | |
| 349 | 495 | |
| 350 | - if ( ! \is_array( $stored ) || ! isset( $stored['gateways'][ $gateway_id ] ) || ! \is_array( $stored['gateways'][ $gateway_id ] ) ) { | |
| 496 | + /** This filter is documented in woocommerce/includes/abstracts/abstract-wc-payment-gateway.php */ | |
| 497 | + $return_url = apply_filters( 'woocommerce_get_return_url', $received_url, $order ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WooCommerce core hook. | |
| 498 | + | |
| 499 | + foreach ( array_unique( array( $received_url, $return_url ) ) as $candidate ) { | |
| 500 | + if ( \is_string( $candidate ) && '' !== $candidate && $this->is_same_page( $redirect, $candidate ) ) { | |
| 501 | + return true; | |
| 502 | + } | |
| 503 | + } | |
| 504 | + | |
| 505 | + return false; | |
| 506 | + } | |
| 507 | + | |
| 508 | + /** | |
| 509 | + * Whether a gateway's redirect lands on the given page. | |
| 510 | + * | |
| 511 | + * Host (case-insensitive), port and path (without a trailing slash) must | |
| 512 | + * match; the scheme is ignored, and a port is compared only when it is not | |
| 513 | + * the scheme's default, so http and https forms of one origin agree while | |
| 514 | + * two services on one host do not. | |
| 515 | + * | |
| 516 | + * The query is compared one way: every argument the page carries must be on | |
| 517 | + * the redirect with the same value, except `key`, which a gateway may drop. | |
| 518 | + * Extra arguments on the redirect are fine (`utm_nooverride`, a gateway's own | |
| 519 | + * tracking). This is what makes plain permalinks safe, where every | |
| 520 | + * WooCommerce page shares the path `/` and the page is its query: the | |
| 521 | + * received page carries `page_id` and the received endpoint (`order-received`, | |
| 522 | + * or whatever the store renamed it to, read from the URL itself rather than | |
| 523 | + * assumed), so the checkout page, the pay page and any other page of the same | |
| 524 | + * site fail to carry one of them. | |
| 525 | + * | |
| 526 | + * @param string $redirect The gateway's redirect. | |
| 527 | + * @param string $page The page it must land on. | |
| 528 | + * | |
| 529 | + * @return bool | |
| 530 | + */ | |
| 531 | + private function is_same_page( string $redirect, string $page ): bool { | |
| 532 | + $parts_r = wp_parse_url( $redirect ); | |
| 533 | + $parts_p = wp_parse_url( $page ); | |
| 534 | + | |
| 535 | + if ( ! \is_array( $parts_r ) || ! \is_array( $parts_p ) ) { | |
| 536 | + return false; | |
| 537 | + } | |
| 538 | + | |
| 539 | + $host_r = strtolower( (string) ( $parts_r['host'] ?? '' ) ); | |
| 540 | + $host_p = strtolower( (string) ( $parts_p['host'] ?? '' ) ); | |
| 541 | + | |
| 542 | + if ( '' === $host_r || $host_r !== $host_p ) { | |
| 543 | + return false; | |
| 544 | + } | |
| 545 | + | |
| 546 | + if ( $this->explicit_port( $parts_r ) !== $this->explicit_port( $parts_p ) ) { | |
| 547 | + return false; | |
| 548 | + } | |
| 549 | + | |
| 550 | + $path_r = untrailingslashit( (string) ( $parts_r['path'] ?? '/' ) ); | |
| 551 | + $path_p = untrailingslashit( (string) ( $parts_p['path'] ?? '/' ) ); | |
| 552 | + | |
| 553 | + if ( $path_r !== $path_p ) { | |
| 554 | + return false; | |
| 555 | + } | |
| 556 | + | |
| 557 | + parse_str( (string) ( $parts_r['query'] ?? '' ), $query_r ); | |
| 558 | + parse_str( (string) ( $parts_p['query'] ?? '' ), $query_p ); | |
| 559 | + | |
| 560 | + foreach ( $query_p as $name => $value ) { | |
| 561 | + if ( 'key' === $name ) { | |
| 562 | + continue; | |
| 563 | + } | |
| 564 | + | |
| 565 | + if ( ! isset( $query_r[ $name ] ) || (string) $query_r[ $name ] !== (string) $value ) { | |
| 566 | + return false; | |
| 567 | + } | |
| 568 | + } | |
| 569 | + | |
| 570 | + return true; | |
| 571 | + } | |
| 572 | + | |
| 573 | + /** | |
| 574 | + * The port of a parsed URL, or '' when it is the scheme's default. | |
| 575 | + * | |
| 576 | + * @param array $parts Output of wp_parse_url(). | |
| 577 | + * | |
| 578 | + * @return string | |
| 579 | + */ | |
| 580 | + private function explicit_port( array $parts ): string { | |
| 581 | + if ( ! isset( $parts['port'] ) ) { | |
| 351 | 582 | return ''; |
| 352 | 583 | } |
| 353 | 584 | |
| 354 | - $gateway = $stored['gateways'][ $gateway_id ]; | |
| 585 | + $port = (int) $parts['port']; | |
| 586 | + $scheme = strtolower( (string) ( $parts['scheme'] ?? '' ) ); | |
| 355 | 587 | |
| 356 | - if ( ! isset( $gateway['enabled'] ) || ! wc_string_to_bool( $gateway['enabled'] ) ) { | |
| 588 | + if ( ( 'http' === $scheme && 80 === $port ) || ( 'https' === $scheme && 443 === $port ) ) { | |
| 357 | 589 | return ''; |
| 358 | 590 | } |
| 359 | 591 | |
| 360 | - if ( isset( $gateway['order_status'] ) && \is_string( $gateway['order_status'] ) && '' !== $gateway['order_status'] ) { | |
| 361 | - return $gateway['order_status']; | |
| 592 | + return (string) $port; | |
| 593 | + } | |
| 594 | + | |
| 595 | + /** | |
| 596 | + * Whether a gateway declares WooCommerce's refund capability. | |
| 597 | + * | |
| 598 | + * The declaration is the gateway's own (`$supports`, through | |
| 599 | + * `WC_Payment_Gateway::supports()` and its filter). A gateway WooCommerce | |
| 600 | + * does not know is taken as not moving money: the merchant stored a POS status | |
| 601 | + * for it, and that choice is the only thing left to act on. | |
| 602 | + * | |
| 603 | + * @param string $gateway_id The payment gateway ID. | |
| 604 | + * | |
| 605 | + * @return bool | |
| 606 | + */ | |
| 607 | + private function gateway_moves_money( string $gateway_id ): bool { | |
| 608 | + if ( '' === $gateway_id ) { | |
| 609 | + return false; | |
| 362 | 610 | } |
| 363 | 611 | |
| 364 | - return $this->get_legacy_checkout_order_status(); | |
| 612 | + $gateways = WC()->payment_gateways()->payment_gateways(); | |
| 613 | + $gateway = $gateways[ $gateway_id ] ?? null; | |
| 614 | + | |
| 615 | + return $gateway instanceof WC_Payment_Gateway && $gateway->supports( 'refunds' ); | |
| 365 | 616 | } |
| 366 | 617 | |
| 367 | 618 | /** |
| 368 | - * Read the legacy global checkout order status. | |
| 619 | + * Read the explicitly stored per-gateway order status. | |
| 369 | 620 | * |
| 370 | - * Pre-dates per-gateway statuses. Payment_Gateways_Section::read() still seeds | |
| 371 | - * it in memory for gateways with no explicit status, and leaves the key in | |
| 372 | - * place until the merchant saves, so an upgraded site can have an enabled | |
| 373 | - * gateway whose only configured status lives here. | |
| 621 | + * The received page and the POS gateway catalog read the same value, so the | |
| 622 | + * reader lives on Gateway_Contract — see get_stored_order_status() there for | |
| 623 | + * why the raw option, and not the settings view, is the source of intent. | |
| 374 | 624 | * |
| 375 | - * @return string The stored legacy status, or '' when absent. | |
| 625 | + * @param string $gateway_id The payment gateway ID. | |
| 626 | + * | |
| 627 | + * @return string The stored status (may include the wc- prefix), or '' when absent. | |
| 376 | 628 | */ |
| 377 | - private function get_legacy_checkout_order_status(): string { | |
| 378 | - $checkout = get_option( 'woocommerce_pos_settings_checkout', array() ); | |
| 379 | - | |
| 380 | - return \is_array( $checkout ) && isset( $checkout['order_status'] ) && \is_string( $checkout['order_status'] ) | |
| 381 | - ? $checkout['order_status'] | |
| 382 | - : ''; | |
| 629 | + private function get_stored_gateway_order_status( string $gateway_id ): string { | |
| 630 | + return ( new Gateway_Contract() )->get_stored_order_status( $gateway_id ); | |
| 383 | 631 | } |
| 384 | 632 | |
| 385 | 633 | /** |
| 386 | 634 | * Resolve the configured POS order status for a given payment gateway. |