| @@ -61,9 +61,8 @@ | ||
| 61 | 61 | add_filter( 'woocommerce_payment_complete_order_status', array( $this, 'payment_complete_order_status' ), 10, 3 ); |
| 62 | 62 | add_filter( 'woocommerce_bacs_process_payment_order_status', array( $this, 'offline_process_payment_order_status' ), 10, 2 ); |
| 63 | 63 | add_filter( 'woocommerce_cheque_process_payment_order_status', array( $this, 'offline_process_payment_order_status' ), 10, 2 ); |
| 64 | 64 | 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 ); | |
| 66 | 65 | add_filter( 'woocommerce_hidden_order_itemmeta', array( $this, 'hidden_order_itemmeta' ) ); |
| 67 | 66 | add_filter( 'woocommerce_order_item_product', array( $this, 'order_item_product' ), 10, 2 ); |
| 68 | 67 | add_filter( 'woocommerce_order_get_tax_location', array( $this, 'get_tax_location' ), 10, 2 ); |
| 69 | 68 | add_action( 'woocommerce_order_item_after_calculate_taxes', array( $this, 'order_item_after_calculate_taxes' ) ); |
| @@ -214,173 +213,8 @@ | ||
| 214 | 213 | |
| 215 | 214 | return \in_array( $normalized_status, $valid_statuses, true ) |
| 216 | 215 | ? $normalized_status |
| 217 | 216 | : $fallback; |
| 218 | - } | |
| 219 | - | |
| 220 | - /** | |
| 221 | - * Apply the configured POS order status when a gateway settles without payment. | |
| 222 | - * | |
| 223 | - * The generic form of offline_process_payment_order_status(). Those three | |
| 224 | - * gateways are hooked by name only because BACS, cheque and COD each expose a | |
| 225 | - * `woocommerce_{id}_process_payment_order_status` filter. A gateway that takes | |
| 226 | - * no money at the till and exposes no such filter — a quote, invoice or | |
| 227 | - * purchase-order gateway — returned success while leaving the order at | |
| 228 | - * pos-open, so the configured status was never applied and the till never | |
| 229 | - * finished the sale. `woocommerce_payment_successful_result` is the seam every | |
| 230 | - * gateway passes through: WooCommerce applies it after any successful | |
| 231 | - * process_payment(), including on the POS pay page. | |
| 232 | - * | |
| 233 | - * Deliberately narrow, because "returned success but left the order open" is | |
| 234 | - * also what a gateway awaiting an async confirmation looks like: | |
| 235 | - * | |
| 236 | - * - `pos-open` only, never `pos-partial` — a partial tender is still owed | |
| 237 | - * money, and closing it would lose that. | |
| 238 | - * - no `date_paid` — money moving means the payment_complete path owns the | |
| 239 | - * status. | |
| 240 | - * - the gateway must be enabled for POS *and* carry an explicitly stored | |
| 241 | - * status. The settings view synthesizes `wc-completed` for every installed | |
| 242 | - * gateway it has never seen, so trusting the computed value would mark an | |
| 243 | - * unconfigured third-party gateway Completed with no money taken. | |
| 244 | - * | |
| 245 | - * @param array $result Gateway result, passed through untouched. | |
| 246 | - * @param int $order_id Order ID. | |
| 247 | - * | |
| 248 | - * @return array | |
| 249 | - */ | |
| 250 | - public function apply_unpaid_gateway_order_status( $result, $order_id ) { | |
| 251 | - if ( ! woocommerce_pos_request() ) { | |
| 252 | - return $result; | |
| 253 | - } | |
| 254 | - | |
| 255 | - $order = wc_get_order( $order_id ); | |
| 256 | - | |
| 257 | - if ( ! $order instanceof WC_Order || ! woocommerce_pos_is_pos_order( $order ) ) { | |
| 258 | - return $result; | |
| 259 | - } | |
| 260 | - | |
| 261 | - if ( ! $order->has_status( 'pos-open' ) || $order->get_date_paid( 'edit' ) ) { | |
| 262 | - return $result; | |
| 263 | - } | |
| 264 | - | |
| 265 | - $gateway_id = $order->get_payment_method(); | |
| 266 | - $configured = $this->get_stored_gateway_order_status( $gateway_id ); | |
| 267 | - | |
| 268 | - if ( '' === $configured ) { | |
| 269 | - return $result; | |
| 270 | - } | |
| 271 | - | |
| 272 | - $status = $this->normalize_status( $configured, '' ); | |
| 273 | - | |
| 274 | - if ( '' === $status || $order->has_status( $status ) ) { | |
| 275 | - return $result; | |
| 276 | - } | |
| 277 | - | |
| 278 | - /* | |
| 279 | - * payment_complete_order_status() reports the configured status as this | |
| 280 | - * order's paid status, which makes WC_Order::set_status() stamp date_paid | |
| 281 | - * the moment the status changes — booking an unpaid order as revenue. No | |
| 282 | - * payment was taken here, so suppress it for this transition only. | |
| 283 | - * | |
| 284 | - * Scoped to this order id: a status-transition handler can call | |
| 285 | - * payment_complete() on a *different* order while this filter is live | |
| 286 | - * (subscriptions, bundles and gift-card plugins all do), and an | |
| 287 | - * unconditional '' would reach that order too — set_status() rejects an | |
| 288 | - * unknown status and falls back to 'pending', leaving an order that was | |
| 289 | - * just paid sitting unpaid. | |
| 290 | - */ | |
| 291 | - $target_id = $order->get_id(); | |
| 292 | - $suppress_paid_date = static function ( $payment_status, $filtered_order_id ) use ( $target_id ) { | |
| 293 | - return (int) $filtered_order_id === $target_id ? '' : $payment_status; | |
| 294 | - }; | |
| 295 | - | |
| 296 | - add_filter( 'woocommerce_payment_complete_order_status', $suppress_paid_date, PHP_INT_MAX, 2 ); | |
| 297 | - | |
| 298 | - try { | |
| 299 | - /* | |
| 300 | - * update_status()'s return value is deliberately not checked. It reports | |
| 301 | - * false only when the order has no id — impossible here — because | |
| 302 | - * WC_Abstract_Order::save() and WC_Order::status_transition() each catch | |
| 303 | - * Exception themselves and handle_exception() does not rethrow, so a | |
| 304 | - * throwing hook never reaches update_status()'s own catch and it still | |
| 305 | - * returns true. Nor could the checkout be aborted from here: | |
| 306 | - * WC_Form_Handler::pay_action() applies this filter inside its | |
| 307 | - * `'success' === $result['result']` branch and redirects unconditionally | |
| 308 | - * on the next line. | |
| 309 | - */ | |
| 310 | - $order->update_status( | |
| 311 | - $status, | |
| 312 | - /* translators: %s: payment gateway title. */ | |
| 313 | - sprintf( __( 'Order status set by %s; no payment was taken at the till.', 'woocommerce-pos' ), $order->get_payment_method_title() ) | |
| 314 | - ); | |
| 315 | - } finally { | |
| 316 | - // Must come off even if a status-change handler throws: left in place it | |
| 317 | - // would suppress the configured status, and date_paid, for every later | |
| 318 | - // payment in this request. | |
| 319 | - remove_filter( 'woocommerce_payment_complete_order_status', $suppress_paid_date, PHP_INT_MAX ); | |
| 320 | - } | |
| 321 | - | |
| 322 | - return $result; | |
| 323 | - } | |
| 324 | - | |
| 325 | - /** | |
| 326 | - * Read the explicitly stored per-gateway order status. | |
| 327 | - * | |
| 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. | |
| 332 | - * | |
| 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. | |
| 338 | - * | |
| 339 | - * @param string $gateway_id The payment gateway ID. | |
| 340 | - * | |
| 341 | - * @return string The stored status (may include the wc- prefix), or '' when absent. | |
| 342 | - */ | |
| 343 | - private function get_stored_gateway_order_status( string $gateway_id ): string { | |
| 344 | - if ( '' === $gateway_id ) { | |
| 345 | - return ''; | |
| 346 | - } | |
| 347 | - | |
| 348 | - $stored = get_option( 'woocommerce_pos_settings_payment_gateways', array() ); | |
| 349 | - | |
| 350 | - if ( ! \is_array( $stored ) || ! isset( $stored['gateways'][ $gateway_id ] ) || ! \is_array( $stored['gateways'][ $gateway_id ] ) ) { | |
| 351 | - return ''; | |
| 352 | - } | |
| 353 | - | |
| 354 | - $gateway = $stored['gateways'][ $gateway_id ]; | |
| 355 | - | |
| 356 | - if ( ! isset( $gateway['enabled'] ) || ! wc_string_to_bool( $gateway['enabled'] ) ) { | |
| 357 | - return ''; | |
| 358 | - } | |
| 359 | - | |
| 360 | - if ( isset( $gateway['order_status'] ) && \is_string( $gateway['order_status'] ) && '' !== $gateway['order_status'] ) { | |
| 361 | - return $gateway['order_status']; | |
| 362 | - } | |
| 363 | - | |
| 364 | - return $this->get_legacy_checkout_order_status(); | |
| 365 | - } | |
| 366 | - | |
| 367 | - /** | |
| 368 | - * Read the legacy global checkout order status. | |
| 369 | - * | |
| 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. | |
| 374 | - * | |
| 375 | - * @return string The stored legacy status, or '' when absent. | |
| 376 | - */ | |
| 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 | - : ''; | |
| 383 | 217 | } |
| 384 | 218 | |
| 385 | 219 | /** |
| 386 | 220 | * Resolve the configured POS order status for a given payment gateway. |