From 3948295a94e7b583d5fb329b1dec86ed70849c34 Mon Sep 17 00:00:00 2001 From: Tomas Ilginis Date: Wed, 24 Sep 2025 15:54:10 +0300 Subject: [PATCH] adds background re-index job --- src/Models/ReindexCancelResponse.php | 45 +++++ src/Models/ReindexStatusResponse.php | 73 +++++++ src/Models/ReindexTaskResponse.php | 35 ++++ src/SynchronizationApiSdk.php | 35 +++- tests/Adapters/PrestaShopAdapterTest.php | 76 ++++++- tests/Models/ReindexResponseModelsTest.php | 224 +++++++++++++++++++++ 6 files changed, 475 insertions(+), 13 deletions(-) create mode 100644 src/Models/ReindexCancelResponse.php create mode 100644 src/Models/ReindexStatusResponse.php create mode 100644 src/Models/ReindexTaskResponse.php create mode 100644 tests/Models/ReindexResponseModelsTest.php diff --git a/src/Models/ReindexCancelResponse.php b/src/Models/ReindexCancelResponse.php new file mode 100644 index 0000000..7990084 --- /dev/null +++ b/src/Models/ReindexCancelResponse.php @@ -0,0 +1,45 @@ +status === 'success'; + } + + public function isError(): bool + { + return $this->status === 'error'; + } + + public function toArray(): array + { + return array_filter([ + 'status' => $this->status, + 'message' => $this->message, + 'task_id' => $this->taskId, + 'error' => $this->error, + ], fn($value) => $value !== null); + } + + public static function fromArray(array $data): self + { + return new self( + status: $data['status'], + message: $data['message'], + taskId: $data['task_id'], + error: $data['error'] ?? null + ); + } +} \ No newline at end of file diff --git a/src/Models/ReindexStatusResponse.php b/src/Models/ReindexStatusResponse.php new file mode 100644 index 0000000..5226c5d --- /dev/null +++ b/src/Models/ReindexStatusResponse.php @@ -0,0 +1,73 @@ +status === 'in_progress'; + } + + public function isCompleted(): bool + { + return $this->status === 'completed'; + } + + public function isFailed(): bool + { + return $this->status === 'failed'; + } + + public function isFinished(): bool + { + return $this->isCompleted() || $this->isFailed(); + } + + public function toArray(): array + { + return array_filter([ + 'task_id' => $this->taskId, + 'status' => $this->status, + 'message' => $this->message, + 'progress' => $this->progress, + 'result' => $this->result, + 'error' => $this->error, + 'started_at' => $this->startedAt, + 'updated_at' => $this->updatedAt, + 'completed_at' => $this->completedAt, + 'failed_at' => $this->failedAt, + ], fn($value) => $value !== null); + } + + public static function fromArray(array $data): self + { + return new self( + taskId: $data['task_id'], + status: $data['status'], + message: $data['message'], + progress: $data['progress'] ?? null, + result: $data['result'] ?? null, + error: $data['error'] ?? null, + startedAt: $data['started_at'] ?? null, + updatedAt: $data['updated_at'] ?? null, + completedAt: $data['completed_at'] ?? null, + failedAt: $data['failed_at'] ?? null + ); + } +} \ No newline at end of file diff --git a/src/Models/ReindexTaskResponse.php b/src/Models/ReindexTaskResponse.php new file mode 100644 index 0000000..446e8a0 --- /dev/null +++ b/src/Models/ReindexTaskResponse.php @@ -0,0 +1,35 @@ + $this->status, + 'message' => $this->message, + 'task_id' => $this->taskId, + 'status_url' => $this->statusUrl, + ]; + } + + public static function fromArray(array $data): self + { + return new self( + status: $data['status'], + message: $data['message'], + taskId: $data['task_id'], + statusUrl: $data['status_url'] + ); + } +} \ No newline at end of file diff --git a/src/SynchronizationApiSdk.php b/src/SynchronizationApiSdk.php index 18f31c6..d51ba73 100644 --- a/src/SynchronizationApiSdk.php +++ b/src/SynchronizationApiSdk.php @@ -7,6 +7,9 @@ use BradSearch\SyncSdk\Client\HttpClient; use BradSearch\SyncSdk\Config\SyncConfig; use BradSearch\SyncSdk\Models\FieldConfig; +use BradSearch\SyncSdk\Models\ReindexTaskResponse; +use BradSearch\SyncSdk\Models\ReindexStatusResponse; +use BradSearch\SyncSdk\Models\ReindexCancelResponse; use BradSearch\SyncSdk\Validators\DataValidator; use BradSearch\SyncSdk\Exceptions\ValidationException; use BradSearch\SyncSdk\Enums\FieldType; @@ -70,13 +73,14 @@ public function createIndex(string $index): void } /** - * Copy source index to target index + * Copy source index to target index (async operation) * * @param string $sourceIndex * @param string $targetIndex + * @return ReindexTaskResponse * @throws ValidationException */ - public function copyIndex(string $sourceIndex, string $targetIndex): void + public function copyIndex(string $sourceIndex, string $targetIndex): ReindexTaskResponse { $this->validator->validateIndex($sourceIndex); @@ -89,7 +93,8 @@ public function copyIndex(string $sourceIndex, string $targetIndex): void 'target_index' => $targetIndex ]; - $this->httpClient->post("{$this->apiStartUrl}sync/reindex", $data); + $response = $this->httpClient->post("{$this->apiStartUrl}sync/reindex", $data); + return ReindexTaskResponse::fromArray($response); } /** @@ -291,4 +296,28 @@ private function buildEmbeddableFields(): array return $embeddableFields; } + + /** + * Get the status of a reindex task + * + * @param string $taskId + * @return ReindexStatusResponse + */ + public function getReindexStatus(string $taskId): ReindexStatusResponse + { + $response = $this->httpClient->get("{$this->apiStartUrl}sync/reindex/status/{$taskId}"); + return ReindexStatusResponse::fromArray($response); + } + + /** + * Cancel a running reindex task + * + * @param string $taskId + * @return ReindexCancelResponse + */ + public function cancelReindexTask(string $taskId): ReindexCancelResponse + { + $response = $this->httpClient->delete("{$this->apiStartUrl}sync/reindex/{$taskId}"); + return ReindexCancelResponse::fromArray($response); + } } diff --git a/tests/Adapters/PrestaShopAdapterTest.php b/tests/Adapters/PrestaShopAdapterTest.php index 2cc9fb0..458f30b 100644 --- a/tests/Adapters/PrestaShopAdapterTest.php +++ b/tests/Adapters/PrestaShopAdapterTest.php @@ -811,7 +811,9 @@ public function testTransformWithInvalidDescriptionTypes(): void 'remoteId' => '1807', 'sku' => 'M0E20000000EAAK', 'price' => '99.99', - 'formattedPrice' => '$99.99', + 'basePrice' => '9.99', + 'priceTaxExcluded' => '1.00', + 'basePriceTaxExcluded' => '8.44', 'localizedNames' => [ 'en-US' => 'Test Product' ], @@ -841,7 +843,9 @@ public function testTransformWithInvalidImageUrlTypes(): void 'remoteId' => '1807', 'sku' => 'M0E20000000EAAK', 'price' => '99.99', - 'formattedPrice' => '$99.99', + 'basePrice' => '9.99', + 'priceTaxExcluded' => '1.00', + 'basePriceTaxExcluded' => '8.44', 'localizedNames' => [ 'en-US' => 'Test Product' ], @@ -853,7 +857,9 @@ public function testTransformWithInvalidImageUrlTypes(): void 'remoteId' => '1808', 'sku' => 'M0E20000000EAAL', 'price' => '99.99', - 'formattedPrice' => '$99.99', + 'basePrice' => '9.99', + 'priceTaxExcluded' => '1.00', + 'basePriceTaxExcluded' => '8.44', 'localizedNames' => [ 'en-US' => 'Test Product 2' ], @@ -888,7 +894,9 @@ public function testTransformWithInvalidProductUrlTypes(): void 'remoteId' => '1807', 'sku' => 'M0E20000000EAAK', 'price' => '99.99', - 'formattedPrice' => '$99.99', + 'basePrice' => '9.99', + 'priceTaxExcluded' => '1.00', + 'basePriceTaxExcluded' => '8.44', 'localizedNames' => [ 'en-US' => 'Test Product' ], @@ -900,7 +908,9 @@ public function testTransformWithInvalidProductUrlTypes(): void 'remoteId' => '1808', 'sku' => 'M0E20000000EAAL', 'price' => '99.99', - 'formattedPrice' => '$99.99', + 'basePrice' => '9.99', + 'priceTaxExcluded' => '1.00', + 'basePriceTaxExcluded' => '8.44', 'localizedNames' => [ 'en-US' => 'Test Product 2' ], @@ -936,7 +946,9 @@ public function testTransformWithInvalidCategoriesTypes(): void 'remoteId' => '1807', 'sku' => 'M0E20000000EAAK', 'price' => '99.99', - 'formattedPrice' => '$99.99', + 'basePrice' => '9.99', + 'priceTaxExcluded' => '1.00', + 'basePriceTaxExcluded' => '8.44', 'localizedNames' => [ 'en-US' => 'Test Product' ], @@ -948,7 +960,9 @@ public function testTransformWithInvalidCategoriesTypes(): void 'remoteId' => '1808', 'sku' => 'M0E20000000EAAL', 'price' => '99.99', - 'formattedPrice' => '$99.99', + 'basePrice' => '9.99', + 'priceTaxExcluded' => '1.00', + 'basePriceTaxExcluded' => '8.44', 'localizedNames' => [ 'en-US' => 'Test Product 2' ], @@ -1013,7 +1027,9 @@ public function testTransformWithInvalidLocalizedValues(): void 'remoteId' => '1807', 'sku' => 'M0E20000000EAAK', 'price' => '99.99', - 'formattedPrice' => '$99.99', + 'basePrice' => '9.99', + 'priceTaxExcluded' => '1.00', + 'basePriceTaxExcluded' => '8.44', 'localizedNames' => [ 'en-US' => null, // Null name 'lt-LT' => '', // Empty name @@ -1074,7 +1090,9 @@ public function testTransformWithInvalidFeaturesTypes(): void 'remoteId' => '1807', 'sku' => 'M0E20000000EAAK', 'price' => '99.99', - 'formattedPrice' => '$99.99', + 'basePrice' => '9.99', + 'priceTaxExcluded' => '1.00', + 'basePriceTaxExcluded' => '8.44', 'localizedNames' => [ 'en-US' => 'Test Product' ], @@ -1129,7 +1147,9 @@ public function testTransformWithInvalidVariantsTypes(): void 'remoteId' => '1807', 'sku' => 'M0E20000000EAAK', 'price' => '99.99', - 'formattedPrice' => '$99.99', + 'basePrice' => '9.99', + 'priceTaxExcluded' => '1.00', + 'basePriceTaxExcluded' => '8.44', 'localizedNames' => [ 'en-US' => 'Test Product' ], @@ -1159,4 +1179,40 @@ public function testTransformWithInvalidVariantsTypes(): void $this->assertCount(1, $result["products"]); $this->assertEmpty($this->getProductFromResult($result)['variants']); // All variants invalid or filtered out } + + /** + * Test that zero price values are handled correctly and not skipped + */ + public function testTransformWithZeroPriceValues(): void + { + $prestaShopData = [ + 'products' => [ + [ + 'remoteId' => '1807', + 'sku' => 'FREE-PRODUCT', + 'price' => '0', // Zero price as string + 'basePrice' => 0, // Zero price as number + 'priceTaxExcluded' => '0.00', // Zero price with decimal + 'basePriceTaxExcluded' => 0.0, // Zero price as float + 'localizedNames' => [ + 'en-US' => 'Free Product' + ], + 'categories' => [], + 'variants' => [] + ] + ] + ]; + + $result = $this->adapter->transform($prestaShopData); + + $this->assertCount(1, $result['products']); + $product = $this->getProductFromResult($result); + + // Verify that all zero price values are correctly handled + $this->assertEquals('0', $product['price']); + $this->assertEquals('0', $product['basePrice']); + $this->assertEquals('0.00', $product['priceTaxExcluded']); + $this->assertEquals('0', $product['basePriceTaxExcluded']); + $this->assertEquals('Free Product', $product['name']); + } } diff --git a/tests/Models/ReindexResponseModelsTest.php b/tests/Models/ReindexResponseModelsTest.php new file mode 100644 index 0000000..f5c50f8 --- /dev/null +++ b/tests/Models/ReindexResponseModelsTest.php @@ -0,0 +1,224 @@ +assertEquals('accepted', $response->status); + $this->assertEquals('Reindex operation initiated', $response->message); + $this->assertEquals('reindex_abc123def456', $response->taskId); + $this->assertEquals('/api/v1/sync/reindex/status/reindex_abc123def456', $response->statusUrl); + } + + public function testReindexTaskResponseFromArray(): void + { + $data = [ + 'status' => 'accepted', + 'message' => 'Reindex operation initiated', + 'task_id' => 'reindex_abc123def456', + 'status_url' => '/api/v1/sync/reindex/status/reindex_abc123def456' + ]; + + $response = ReindexTaskResponse::fromArray($data); + + $this->assertEquals('accepted', $response->status); + $this->assertEquals('Reindex operation initiated', $response->message); + $this->assertEquals('reindex_abc123def456', $response->taskId); + $this->assertEquals('/api/v1/sync/reindex/status/reindex_abc123def456', $response->statusUrl); + } + + public function testReindexTaskResponseToArray(): void + { + $response = new ReindexTaskResponse( + status: 'accepted', + message: 'Reindex operation initiated', + taskId: 'reindex_abc123def456', + statusUrl: '/api/v1/sync/reindex/status/reindex_abc123def456' + ); + + $array = $response->toArray(); + $expected = [ + 'status' => 'accepted', + 'message' => 'Reindex operation initiated', + 'task_id' => 'reindex_abc123def456', + 'status_url' => '/api/v1/sync/reindex/status/reindex_abc123def456' + ]; + + $this->assertEquals($expected, $array); + } + + public function testReindexStatusResponseInProgress(): void + { + $data = [ + 'task_id' => 'reindex_abc123def456', + 'status' => 'in_progress', + 'message' => 'Reindex operation is running', + 'progress' => [ + 'total_docs' => 10000, + 'processed_docs' => 3500, + 'percentage' => 35.0, + 'estimated_remaining_seconds' => 120 + ], + 'started_at' => '1642244200', + 'updated_at' => '1642244500' + ]; + + $response = ReindexStatusResponse::fromArray($data); + + $this->assertEquals('reindex_abc123def456', $response->taskId); + $this->assertEquals('in_progress', $response->status); + $this->assertTrue($response->isInProgress()); + $this->assertFalse($response->isCompleted()); + $this->assertFalse($response->isFailed()); + $this->assertFalse($response->isFinished()); + $this->assertNotNull($response->progress); + $this->assertEquals(10000, $response->progress['total_docs']); + } + + public function testReindexStatusResponseCompleted(): void + { + $data = [ + 'task_id' => 'reindex_abc123def456', + 'status' => 'completed', + 'message' => 'Reindex operation completed successfully', + 'result' => [ + 'total_docs' => 10000, + 'indexed_docs' => 10000, + 'failed_docs' => 0, + 'took_milliseconds' => 45000 + ], + 'started_at' => '1642244200', + 'completed_at' => '1642244275' + ]; + + $response = ReindexStatusResponse::fromArray($data); + + $this->assertEquals('completed', $response->status); + $this->assertFalse($response->isInProgress()); + $this->assertTrue($response->isCompleted()); + $this->assertFalse($response->isFailed()); + $this->assertTrue($response->isFinished()); + $this->assertNotNull($response->result); + $this->assertEquals(10000, $response->result['total_docs']); + } + + public function testReindexStatusResponseFailed(): void + { + $data = [ + 'task_id' => 'reindex_abc123def456', + 'status' => 'failed', + 'message' => 'Reindex operation failed', + 'error' => 'Target index mapping incompatible', + 'started_at' => '1642244200', + 'failed_at' => '1642244205' + ]; + + $response = ReindexStatusResponse::fromArray($data); + + $this->assertEquals('failed', $response->status); + $this->assertFalse($response->isInProgress()); + $this->assertFalse($response->isCompleted()); + $this->assertTrue($response->isFailed()); + $this->assertTrue($response->isFinished()); + $this->assertEquals('Target index mapping incompatible', $response->error); + } + + public function testReindexStatusResponseToArray(): void + { + $response = new ReindexStatusResponse( + taskId: 'reindex_abc123def456', + status: 'completed', + message: 'Reindex operation completed successfully', + result: [ + 'total_docs' => 10000, + 'indexed_docs' => 10000, + 'failed_docs' => 0 + ], + startedAt: '1642244200', + completedAt: '1642244275' + ); + + $array = $response->toArray(); + + $this->assertEquals('reindex_abc123def456', $array['task_id']); + $this->assertEquals('completed', $array['status']); + $this->assertEquals('Reindex operation completed successfully', $array['message']); + $this->assertArrayHasKey('result', $array); + $this->assertEquals(10000, $array['result']['total_docs']); + $this->assertEquals('1642244200', $array['started_at']); + $this->assertEquals('1642244275', $array['completed_at']); + // Null values should be filtered out + $this->assertArrayNotHasKey('progress', $array); + $this->assertArrayNotHasKey('error', $array); + } + + public function testReindexCancelResponseSuccess(): void + { + $data = [ + 'status' => 'success', + 'message' => 'Reindex task cancelled successfully', + 'task_id' => 'reindex_abc123def456' + ]; + + $response = ReindexCancelResponse::fromArray($data); + + $this->assertEquals('success', $response->status); + $this->assertTrue($response->isSuccess()); + $this->assertFalse($response->isError()); + $this->assertEquals('reindex_abc123def456', $response->taskId); + $this->assertNull($response->error); + } + + public function testReindexCancelResponseError(): void + { + $data = [ + 'status' => 'error', + 'message' => 'Cannot cancel completed task', + 'task_id' => 'reindex_abc123def456', + 'error' => 'task has already completed' + ]; + + $response = ReindexCancelResponse::fromArray($data); + + $this->assertEquals('error', $response->status); + $this->assertFalse($response->isSuccess()); + $this->assertTrue($response->isError()); + $this->assertEquals('task has already completed', $response->error); + } + + public function testReindexCancelResponseToArray(): void + { + $response = new ReindexCancelResponse( + status: 'success', + message: 'Reindex task cancelled successfully', + taskId: 'reindex_abc123def456' + ); + + $array = $response->toArray(); + $expected = [ + 'status' => 'success', + 'message' => 'Reindex task cancelled successfully', + 'task_id' => 'reindex_abc123def456' + ]; + + $this->assertEquals($expected, $array); + // Null error should be filtered out + $this->assertArrayNotHasKey('error', $array); + } +} \ No newline at end of file