| @@ -11,8 +11,9 @@ | ||
| 11 | 11 | use WCPOS\WooCommercePOS\Services\Permission_Rules; |
| 12 | 12 | use WCPOS\WooCommercePOS\Services\Tax_Id_Types; |
| 13 | 13 | use WCPOS\WooCommercePOS\Sync\Api; |
| 14 | 14 | use WCPOS\WooCommercePOS\Sync\Collections; |
| 15 | +use WCPOS\WooCommercePOS\Sync\Create_Identity; | |
| 15 | 16 | use WCPOS\WooCommercePOS\Sync\Endpoint_Permissions; |
| 16 | 17 | use WCPOS\WooCommercePOS\Sync\Header_Mirror; |
| 17 | 18 | use WCPOS\WooCommercePOS\Sync\Meta_Normalizer; |
| 18 | 19 | use WCPOS\WooCommercePOS\Sync\Mutation_Store; |
| @@ -52,9 +53,12 @@ | ||
| 52 | 53 | |
| 53 | 54 | /** @var mixed Duck-typed mutation store; tests inject an in-memory implementation. */ |
| 54 | 55 | private $store; |
| 55 | 56 | |
| 57 | + /** @var Create_Identity Shared proof for fresh and poisoned creates. */ | |
| 58 | + private Create_Identity $identity; | |
| 56 | 59 | |
| 60 | + | |
| 57 | 61 | /** |
| 58 | 62 | * collection => wc/v3 route + how its uuid→id is resolved. ONE table, not |
| 59 | 63 | * per-collection controllers. Only collections whose resolver is correct AND |
| 60 | 64 | * exercised are exposed; the rest stay out until their phase: |
| @@ -99,8 +103,9 @@ | ||
| 99 | 103 | } |
| 100 | 104 | |
| 101 | 105 | public function __construct( $store = null ) { |
| 102 | 106 | $this->store = $store ? $store : new Mutation_Store(); |
| 107 | + $this->identity = new Create_Identity( $this->store ); | |
| 103 | 108 | } |
| 104 | 109 | |
| 105 | 110 | /** Resolve the collection-specific writer for registry metadata. */ |
| 106 | 111 | private function writer( array $meta ) { |
| @@ -220,9 +225,14 @@ | ||
| 220 | 225 | return $mismatch; |
| 221 | 226 | } |
| 222 | 227 | } |
| 223 | 228 | if ( is_array( $hit ) && 'poison' === ( $hit['status'] ?? '' ) ) { |
| 224 | - return $this->retry_identity_stamp( $meta, $m, $hit ); | |
| 229 | + $writer = $this->writer( $meta ); | |
| 230 | + $recovered = $this->identity->recover( $meta, $m, $hit, $writer ); | |
| 231 | + if ( is_wp_error( $recovered ) ) { | |
| 232 | + return $recovered; | |
| 233 | + } | |
| 234 | + return $this->envelope_document( $this->document_for( $meta, $recovered['id'] ), $m['recordId'], $meta, $recovered['id'], $recovered['status'], $writer ); | |
| 225 | 235 | } |
| 226 | 236 | if ( is_array( $hit ) && in_array( ( $hit['status'] ?? '' ), array( 'done', 'applied' ), true ) ) { |
| 227 | 237 | if ( 'applied' === $hit['status'] && ! $this->store->finalize( $m['mutationId'], (int) $hit['remote_id'] ) ) { |
| 228 | 238 | return $this->finalize_error(); |
| @@ -406,33 +416,12 @@ | ||
| 406 | 416 | $this->store->mark_indeterminate( $m['mutationId'], 0, $response->get_status() ); |
| 407 | 417 | return new WP_Error( 'woo_rxdb_sync_create_no_id', 'Create returned no server id.', array( 'status' => 502 ) ); |
| 408 | 418 | } |
| 409 | 419 | |
| 410 | - // Poison checkpoint, UUID persistence, and finalization remain shared here. | |
| 411 | - $checkpointed = $this->store->mark_poison( $m['mutationId'], $new_id, $response->get_status() ); | |
| 412 | - $writer->persist( 'create_before_identity', $new_id, $m['payload'] ); | |
| 413 | - $identity_error = null; | |
| 414 | - if ( ! $this->store->persist_uuid( $meta['id_type'], $new_id, $m['recordId'] ) ) { | |
| 415 | - $identity_error = new WP_Error( 'woo_rxdb_sync_identity_persistence_failed', 'Unable to persist created record identity.', array( 'status' => 500 ) ); | |
| 416 | - } else { | |
| 417 | - $resolved = $this->store->resolve_id_by_uuid( $meta['id_type'], $m['recordId'], $meta ); | |
| 418 | - if ( is_wp_error( $resolved ) ) { | |
| 419 | - $identity_error = $resolved; | |
| 420 | - } elseif ( $resolved !== $new_id ) { | |
| 421 | - $identity_error = new WP_Error( 'woo_rxdb_sync_identity_persistence_failed', 'Unable to persist created record identity.', array( 'status' => 500 ) ); | |
| 422 | - } | |
| 420 | + $stamped = $this->identity->stamp( $meta, $m, $new_id, $response->get_status(), $writer ); | |
| 421 | + if ( is_wp_error( $stamped ) ) { | |
| 422 | + return $stamped; | |
| 423 | 423 | } |
| 424 | - if ( ! $checkpointed ) { | |
| 425 | - $this->store->mark_indeterminate( $m['mutationId'], $new_id, $response->get_status() ); | |
| 426 | - return $this->finalize_error(); | |
| 427 | - } | |
| 428 | - if ( $identity_error ) { | |
| 429 | - return $identity_error; | |
| 430 | - } | |
| 431 | - $writer->persist( 'create_after_identity', $new_id, $m['payload'] ); | |
| 432 | - if ( ! $this->store->finalize_poison( $m['mutationId'], $new_id ) ) { | |
| 433 | - return $this->finalize_error(); | |
| 434 | - } | |
| 435 | 424 | return $this->envelope_document( $this->document_for( $meta, $new_id ), $m['recordId'], $meta, $new_id, $response->get_status(), $writer ); |
| 436 | 425 | } |
| 437 | 426 | |
| 438 | 427 | /** |
| @@ -519,9 +508,9 @@ | ||
| 519 | 508 | if ( $response->get_status() >= 400 ) { |
| 520 | 509 | return new WP_REST_Response( $response->get_data(), $response->get_status() ); |
| 521 | 510 | } |
| 522 | 511 | $data = $response->get_data(); |
| 523 | - $writer->persist( 'update', $id, $m['payload'], $current_bare, is_array( $data ) ? $data : array(), $prepared['context'] ); | |
| 512 | + $writer->after_update( $id, $m['payload'], $current_bare, is_array( $data ) ? $data : array(), $prepared['context'] ); | |
| 524 | 513 | |
| 525 | 514 | $this->store->persist_uuid( $meta['id_type'], $id, $m['recordId'] ); |
| 526 | 515 | $finalized = $this->checkpoint_and_finalize( $m['mutationId'], $id, $response->get_status() ); |
| 527 | 516 | if ( is_wp_error( $finalized ) ) { |
| @@ -639,43 +628,8 @@ | ||
| 639 | 628 | ? (int) $hit['response_status'] |
| 640 | 629 | : ( 'create' === ( $hit['operation'] ?? '' ) ? 201 : null ); |
| 641 | 630 | $writer = $this->writer( $meta ); |
| 642 | 631 | return $this->envelope_document( $this->document_for( $meta, $remote_id ), $expected, $meta, $remote_id, $status, $writer ); |
| 643 | - } | |
| 644 | - | |
| 645 | - private function retry_identity_stamp( array $meta, array $m, array $hit ) { | |
| 646 | - $remote_id = (int) ( $hit['remote_id'] ?? 0 ); | |
| 647 | - $record_uuid = (string) ( $hit['record_uuid'] ?? '' ); | |
| 648 | - if ( $record_uuid !== $m['recordId'] ) { | |
| 649 | - return new WP_Error( 'woo_rxdb_sync_identity_conflict', 'recordId disagrees with the stored mutation identity.', array( 'status' => 422 ) ); | |
| 650 | - } | |
| 651 | - if ( 'create' !== ( $hit['operation'] ?? '' ) || $remote_id <= 0 ) { | |
| 652 | - return new WP_Error( 'woo_rxdb_sync_identity_persistence_failed', 'Created record identity cannot be recovered safely.', array( 'status' => 500 ) ); | |
| 653 | - } | |
| 654 | - $resolved = $this->store->resolve_id_by_uuid( $meta['id_type'], $record_uuid, $meta ); | |
| 655 | - if ( is_wp_error( $resolved ) ) { | |
| 656 | - return $resolved; | |
| 657 | - } | |
| 658 | - if ( $resolved > 0 && $resolved !== $remote_id ) { | |
| 659 | - return new WP_Error( 'woo_rxdb_sync_identity_persistence_failed', 'Stored create identity points at a different record.', array( 'status' => 500 ) ); | |
| 660 | - } | |
| 661 | - if ( ! $this->store->persist_uuid( $meta['id_type'], $remote_id, $record_uuid ) ) { | |
| 662 | - return new WP_Error( 'woo_rxdb_sync_identity_persistence_failed', 'Unable to persist created record identity.', array( 'status' => 500 ) ); | |
| 663 | - } | |
| 664 | - $verified = $this->store->resolve_id_by_uuid( $meta['id_type'], $record_uuid, $meta ); | |
| 665 | - if ( is_wp_error( $verified ) ) { | |
| 666 | - return $verified; | |
| 667 | - } | |
| 668 | - if ( $verified !== $remote_id ) { | |
| 669 | - return new WP_Error( 'woo_rxdb_sync_identity_persistence_failed', 'Unable to persist created record identity.', array( 'status' => 500 ) ); | |
| 670 | - } | |
| 671 | - $writer = $this->writer( $meta ); | |
| 672 | - $writer->persist( 'create_recovery', $remote_id, $m['payload'] ); | |
| 673 | - if ( ! $this->store->finalize_poison( $m['mutationId'], $remote_id ) ) { | |
| 674 | - return $this->finalize_error(); | |
| 675 | - } | |
| 676 | - $status = isset( $hit['response_status'] ) ? (int) $hit['response_status'] : 201; | |
| 677 | - return $this->envelope_document( $this->document_for( $meta, $remote_id ), $record_uuid, $meta, $remote_id, $status, $writer ); | |
| 678 | 632 | } |
| 679 | 633 | |
| 680 | 634 | /** |
| 681 | 635 | * Validate a client-submitted `tax_ids` payload against the v1 schema. |