PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / 1.10.20
WCPOS – Point of Sale (POS) plugin for WooCommerce v1.10.20
1.10.20 1.10.19 1.10.18 1.10.17 1.10.16 1.10.15 1.10.13 1.10.14 1.10.12 1.10.11 1.10.10 1.10.9 1.10.8 untagged-3d9b7ccddc54df87c672 1.10.7 1.10.6 1.10.5 1.10.3 1.10.4 1.10.2 1.10.1 1.10.0 1.9.17 1.9.15 1.9.16 All 164 releases
← All changes | includes/Orders.php +166 -0 1.10.15 → 1.10.20 View file →
@@ -61,8 +61,9 @@
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 );
65 66 add_filter( 'woocommerce_hidden_order_itemmeta', array( $this, 'hidden_order_itemmeta' ) );
66 67 add_filter( 'woocommerce_order_item_product', array( $this, 'order_item_product' ), 10, 2 );
67 68 add_filter( 'woocommerce_order_get_tax_location', array( $this, 'get_tax_location' ), 10, 2 );
68 69 add_action( 'woocommerce_order_item_after_calculate_taxes', array( $this, 'order_item_after_calculate_taxes' ) );
@@ -213,8 +214,173 @@
213 214
214 215 return \in_array( $normalized_status, $valid_statuses, true )
215 216 ? $normalized_status
216 217 : $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 + : '';
217 383 }
218 384
219 385 /**
220 386 * Resolve the configured POS order status for a given payment gateway.