| @@ -9,12 +9,12 @@ | ||
| 9 | 9 | |
| 10 | 10 | namespace WCPOS\WooCommercePOS\API\V2\Writers; |
| 11 | 11 | |
| 12 | 12 | use WCPOS\WooCommercePOS\Services\Order_Notes; |
| 13 | +use WCPOS\WooCommercePOS\Services\Order_Write_Intent; | |
| 13 | 14 | use WCPOS\WooCommercePOS\Services\Pos_Order_Audit; |
| 14 | 15 | use WCPOS\WooCommercePOS\Services\Settings as SettingsService; |
| 15 | 16 | use WCPOS\WooCommercePOS\Services\Stock_Validator; |
| 16 | -use WCPOS\WooCommercePOS\Services\Tax_Id_Writer; | |
| 17 | 17 | use WCPOS\WooCommercePOS\Sync\Meta_Entry; |
| 18 | 18 | use WCPOS\WooCommercePOS\Sync\Order_Serializer; |
| 19 | 19 | use WCPOS\WooCommercePOS\Sync\Order_Write_Payload; |
| 20 | 20 | use WCPOS\WooCommercePOS\Sync\Pos_Uuid; |
| @@ -45,9 +45,9 @@ | ||
| 45 | 45 | } |
| 46 | 46 | |
| 47 | 47 | /** Prepare an order create and its create-only hook policy. */ |
| 48 | 48 | public function prepare_create( array $meta, array $payload, callable $validate_tax_ids ) { |
| 49 | - $created_gmt = $this->validate_client_created_gmt( $payload ); | |
| 49 | + $created_gmt = $this->order_payload->validate_client_created_gmt( $payload ); | |
| 50 | 50 | if ( is_wp_error( $created_gmt ) ) { |
| 51 | 51 | return $created_gmt; |
| 52 | 52 | } |
| 53 | 53 | $error = $validate_tax_ids( $payload ); |
| @@ -119,9 +119,21 @@ | ||
| 119 | 119 | } |
| 120 | 120 | |
| 121 | 121 | /** Forward within the named order hook lifecycle. */ |
| 122 | 122 | public function forward( array $prepared, callable $forward ) { |
| 123 | - return $this->forward_with_reserved_stock( $prepared, $forward ); | |
| 123 | + $declared = $prepared['context']; | |
| 124 | + if ( ! in_array( $declared['operation'] ?? '', array( 'create', 'update' ), true ) ) { | |
| 125 | + return $this->forward_with_reserved_stock( $prepared, $forward ); | |
| 126 | + } | |
| 127 | + $payload = $prepared['payload']; | |
| 128 | + $declared['requested_status'] = isset( $payload['status'] ) ? (string) $payload['status'] : ''; | |
| 129 | + $declared['set_paid'] = isset( $payload['set_paid'] ) && rest_sanitize_boolean( $payload['set_paid'] ); | |
| 130 | + return Order_Write_Intent::open( | |
| 131 | + $declared, | |
| 132 | + function () use ( $prepared, $forward ) { | |
| 133 | + return $this->forward_with_reserved_stock( $prepared, $forward ); | |
| 134 | + } | |
| 135 | + ); | |
| 124 | 136 | } |
| 125 | 137 | |
| 126 | 138 | /** |
| 127 | 139 | * Wrap the create forward in the shared create-pending -> reserve -> complete |
| @@ -176,32 +188,39 @@ | ||
| 176 | 188 | // right, and it is the same order throughout. |
| 177 | 189 | return $response; |
| 178 | 190 | } |
| 179 | 191 | |
| 180 | - /** Persist the order behavior assigned to a controller-owned protocol phase. */ | |
| 181 | - public function persist( string $phase, int $id, array $payload, array $current = array(), array $response_data = array(), array $context = array() ): void { | |
| 182 | - if ( 'create_before_identity' === $phase ) { | |
| 183 | - $this->persist_tax_ids( $id, $payload, true ); | |
| 184 | - } elseif ( 'create_after_identity' === $phase ) { | |
| 185 | - $this->stamp_order_audit( $id, $payload, true ); | |
| 192 | + /** Persist tax IDs before the identity proof. */ | |
| 193 | + public function after_create( int $id, array $payload ): void { | |
| 194 | + $this->order_payload->persist_tax_ids( $id, $payload, true ); | |
| 195 | + } | |
| 196 | + | |
| 197 | + /** Stamp audit metadata and add the creation note after the identity proof. */ | |
| 198 | + public function after_identity( int $id, array $payload ): void { | |
| 199 | + $this->stamp_order_audit( $id, $payload, true ); | |
| 200 | + $order = wc_get_order( $id ); | |
| 201 | + if ( $order ) { | |
| 202 | + Order_Notes::add_creation_note( $order, get_current_user_id(), $order->get_meta( '_pos_store' ) ); | |
| 203 | + } | |
| 204 | + } | |
| 205 | + | |
| 206 | + /** Restore audit metadata and tax IDs after recovery. */ | |
| 207 | + public function after_recovery( int $id, array $payload ): void { | |
| 208 | + $this->stamp_order_audit( $id, $payload, false ); | |
| 209 | + $this->order_payload->persist_tax_ids( $id, $payload, true ); | |
| 210 | + } | |
| 211 | + | |
| 212 | + /** Persist till metadata, tax IDs, reassignment, and email changes after update. */ | |
| 213 | + public function after_update( int $id, array $payload, array $current, array $response_data, array $context ): void { | |
| 214 | + $this->stamp_order_till_meta( $id, $payload ); | |
| 215 | + $this->order_payload->persist_tax_ids( $id, $payload, false ); | |
| 216 | + $this->persist_cashier_store_reassignment( $id, $current, $response_data, $context ); | |
| 217 | + if ( ! empty( $context['clear_email'] ) ) { | |
| 186 | 218 | $order = wc_get_order( $id ); |
| 187 | 219 | if ( $order ) { |
| 188 | - Order_Notes::add_creation_note( $order, get_current_user_id(), $order->get_meta( '_pos_store' ) ); | |
| 220 | + $order->set_billing_email( '' ); | |
| 221 | + $order->get_data_store()->update( $order ); | |
| 189 | 222 | } |
| 190 | - } elseif ( 'create_recovery' === $phase ) { | |
| 191 | - $this->stamp_order_audit( $id, $payload, false ); | |
| 192 | - $this->persist_tax_ids( $id, $payload, true ); | |
| 193 | - } elseif ( 'update' === $phase ) { | |
| 194 | - $this->stamp_order_till_meta( $id, $payload ); | |
| 195 | - $this->persist_tax_ids( $id, $payload, false ); | |
| 196 | - $this->persist_cashier_store_reassignment( $id, $current, $response_data, $context ); | |
| 197 | - if ( ! empty( $context['clear_email'] ) ) { | |
| 198 | - $order = wc_get_order( $id ); | |
| 199 | - if ( $order ) { | |
| 200 | - $order->set_billing_email( '' ); | |
| 201 | - $order->get_data_store()->update( $order ); | |
| 202 | - } | |
| 203 | - } | |
| 204 | 223 | } |
| 205 | 224 | } |
| 206 | 225 | |
| 207 | 226 | /** Execute delete with the named stock restore/rollback lifecycle. */ |
| @@ -237,35 +256,15 @@ | ||
| 237 | 256 | } |
| 238 | 257 | |
| 239 | 258 | /** Apply create/update hook policies around one exact forwarded order. */ |
| 240 | 259 | private function forward_with_order_lifecycle( array $prepared, callable $forward ) { |
| 241 | - $context = $prepared['context']; | |
| 242 | - $forwarded_order = null; | |
| 243 | - $pre_insert = static function ( $order, $request, $creating ) use ( $context, &$forwarded_order ) { | |
| 244 | - $is_create = 'create' === $context['operation']; | |
| 245 | - if ( $is_create && $creating && $order instanceof \WC_Order && null === $forwarded_order ) { | |
| 246 | - $forwarded_order = $order; | |
| 247 | - } | |
| 248 | - $target = $is_create ? ( $creating && $order === $forwarded_order ) : ( $order instanceof \WC_Order && $context['id'] === $order->get_id() ); | |
| 249 | - if ( $target ) { | |
| 250 | - foreach ( $context['fill_meta'] as $key => $value ) { | |
| 251 | - $order->update_meta_data( $key, $value ); | |
| 252 | - } | |
| 253 | - } | |
| 254 | - if ( $is_create && $creating && null !== $context['created_gmt'] && $order instanceof \WC_Order ) { | |
| 255 | - $order->set_date_created( $context['created_gmt'] ); | |
| 256 | - } | |
| 257 | - return $order; | |
| 258 | - }; | |
| 259 | - $created_via = static function ( $order ) use ( &$forwarded_order ) { | |
| 260 | - if ( $order instanceof \WC_Order && $order === $forwarded_order && 'woocommerce-pos' !== $order->get_created_via() ) { | |
| 260 | + $context = $prepared['context']; | |
| 261 | + $created_via = static function ( $order ) { | |
| 262 | + $intent = Order_Write_Intent::current(); | |
| 263 | + if ( $order instanceof \WC_Order && null !== $intent && $intent->is_subject( $order ) && 'woocommerce-pos' !== $order->get_created_via() ) { | |
| 261 | 264 | $order->set_created_via( 'woocommerce-pos' ); |
| 262 | 265 | } |
| 263 | 266 | }; |
| 264 | - $use_filter = 'create' === $context['operation'] || array() !== $context['fill_meta']; | |
| 265 | - if ( $use_filter ) { | |
| 266 | - add_filter( 'woocommerce_rest_pre_insert_shop_order_object', $pre_insert, 10, 3 ); | |
| 267 | - } | |
| 268 | 267 | if ( 'create' === $context['operation'] ) { |
| 269 | 268 | add_action( 'woocommerce_before_order_object_save', $created_via ); |
| 270 | 269 | } |
| 271 | 270 | try { |
| @@ -270,11 +269,8 @@ | ||
| 270 | 269 | } |
| 271 | 270 | try { |
| 272 | 271 | return $forward( $prepared['method'], $prepared['route'], $prepared['payload'] ); |
| 273 | 272 | } finally { |
| 274 | - if ( $use_filter ) { | |
| 275 | - remove_filter( 'woocommerce_rest_pre_insert_shop_order_object', $pre_insert, 10 ); | |
| 276 | - } | |
| 277 | 273 | if ( 'create' === $context['operation'] ) { |
| 278 | 274 | remove_action( 'woocommerce_before_order_object_save', $created_via ); |
| 279 | 275 | } |
| 280 | 276 | } |
| @@ -403,21 +399,8 @@ | ||
| 403 | 399 | Order_Notes::add_pos_customer_change_note( $order, $current['customer_id'], $data['customer_id'] ); |
| 404 | 400 | } |
| 405 | 401 | } |
| 406 | 402 | |
| 407 | - /** Persist order tax IDs or the create-time customer snapshot. */ | |
| 408 | - private function persist_tax_ids( int $id, array $payload, bool $is_create ): void { | |
| 409 | - $order = wc_get_order( $id ); | |
| 410 | - if ( ! $order ) { | |
| 411 | - return; | |
| 412 | - } | |
| 413 | - if ( is_array( $payload['tax_ids'] ?? null ) ) { | |
| 414 | - ( new Tax_Id_Writer() )->write_for_order( $order, $payload['tax_ids'] ); | |
| 415 | - } elseif ( $is_create && $order->get_customer_id() > 0 ) { | |
| 416 | - ( new Tax_Id_Writer() )->snapshot_from_user_to_order( $order, $order->get_customer_id() ); | |
| 417 | - } | |
| 418 | - } | |
| 419 | - | |
| 420 | 403 | /** Persist server-owned order audit metadata. */ |
| 421 | 404 | private function stamp_order_audit( int $id, array $payload, bool $stamp_version ): void { |
| 422 | 405 | $meta = array( '_pos_user' => (string) get_current_user_id() ); |
| 423 | 406 | // Create-shaped phases only (create_after_identity, create_recovery, the |
| @@ -454,35 +437,8 @@ | ||
| 454 | 437 | private function without_pos_audit_meta( array $payload, int $id = 0 ): array { |
| 455 | 438 | $meta = is_array( $payload['meta_data'] ?? null ) ? $payload['meta_data'] : array(); |
| 456 | 439 | $protected = $id > 0 ? Pos_Order_Audit::audit_meta_ids( wc_get_order( $id ) ) : array(); |
| 457 | 440 | return Pos_Order_Audit::strip_audit_meta( $meta, $protected ); |
| 458 | - } | |
| 459 | - | |
| 460 | - /** Validate and normalize the optional client create timestamp. */ | |
| 461 | - private function validate_client_created_gmt( array $payload ) { | |
| 462 | - if ( ! isset( $payload['date_created_gmt'] ) ) { | |
| 463 | - return null; | |
| 464 | - } | |
| 465 | - if ( ! is_scalar( $payload['date_created_gmt'] ) ) { | |
| 466 | - return $this->invalid_created_gmt(); | |
| 467 | - } | |
| 468 | - $value = wc_clean( wp_unslash( (string) $payload['date_created_gmt'] ) ); | |
| 469 | - if ( '' === $value ) { | |
| 470 | - return null; | |
| 471 | - } | |
| 472 | - $timestamp = 1 === preg_match( '/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z?$/i', $value ) | |
| 473 | - ? rest_parse_date( 'Z' === strtoupper( substr( $value, -1 ) ) ? $value : $value . 'Z', true ) : false; | |
| 474 | - if ( false === $timestamp ) { | |
| 475 | - return $this->invalid_created_gmt(); | |
| 476 | - } | |
| 477 | - return $timestamp > time() + DAY_IN_SECONDS | |
| 478 | - ? new WP_Error( 'woocommerce_pos_rest_future_date_created_gmt', __( 'date_created_gmt cannot be more than 24 hours in the future.', 'woocommerce-pos' ), array( 'status' => 400 ) ) | |
| 479 | - : $timestamp; | |
| 480 | - } | |
| 481 | - | |
| 482 | - /** Build the stable invalid create timestamp error. */ | |
| 483 | - private function invalid_created_gmt(): WP_Error { | |
| 484 | - return new WP_Error( 'woocommerce_pos_rest_invalid_date_created_gmt', __( 'date_created_gmt must be a valid ISO 8601 UTC date.', 'woocommerce-pos' ), array( 'status' => 400 ) ); | |
| 485 | 441 | } |
| 486 | 442 | |
| 487 | 443 | /** Whether order stock was actually reduced before delete. */ |
| 488 | 444 | private function order_stock_reduced( int $id ): bool { |