|
@@ -0,0 +1,267 @@
|
|
|
|
|
+<?php
|
|
|
|
|
+
|
|
|
|
|
+namespace Longyi\Core\Tests\Unit;
|
|
|
|
|
+
|
|
|
|
|
+use Illuminate\Http\Request;
|
|
|
|
|
+use Illuminate\Support\Facades\DB;
|
|
|
|
|
+use Illuminate\Support\Facades\Schema;
|
|
|
|
|
+use Longyi\Core\Models\ProductOption;
|
|
|
|
|
+use Longyi\Core\Services\ProductBasicInfoSyncService;
|
|
|
|
|
+use Longyi\Core\Support\ProductBasicInfoColumns;
|
|
|
|
|
+use Longyi\Core\Tests\TestCase;
|
|
|
|
|
+use Webkul\Product\Models\Product;
|
|
|
|
|
+
|
|
|
|
|
+class ProductBasicInfoSyncServiceTest extends TestCase
|
|
|
|
|
+{
|
|
|
|
|
+ public function test_updates_name_price_and_status_for_existing_sku(): void
|
|
|
|
|
+ {
|
|
|
|
|
+ $product = $this->createSimpleProduct(['sku' => 'SYNC-CORE-1']);
|
|
|
|
|
+
|
|
|
|
|
+ $result = $this->syncService()->sync([[
|
|
|
|
|
+ 'sku' => 'SYNC-CORE-1',
|
|
|
|
|
+ 'locale' => 'en',
|
|
|
|
|
+ 'name' => 'Updated Name',
|
|
|
|
|
+ 'price' => 29.99,
|
|
|
|
|
+ 'status' => 0,
|
|
|
|
|
+ ]], false);
|
|
|
|
|
+
|
|
|
|
|
+ $this->assertSame(1, $result->updated);
|
|
|
|
|
+ $this->assertSame(0, $result->failed);
|
|
|
|
|
+ $this->assertSame('Updated Name', $this->attributeText($product->id, 'name'));
|
|
|
|
|
+ $this->assertEqualsWithDelta(29.99, (float) $this->attributeText($product->id, 'price'), 0.001);
|
|
|
|
|
+ $this->assertSame(0, (int) $this->attributeText($product->id, 'status'));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public function test_unknown_sku_empty_name_and_unknown_category_fail_without_writing(): void
|
|
|
|
|
+ {
|
|
|
|
|
+ $emptyNameProduct = $this->createSimpleProduct(['sku' => 'SYNC-CORE-2']);
|
|
|
|
|
+ $categoryProduct = $this->createSimpleProduct(['sku' => 'SYNC-CORE-2B']);
|
|
|
|
|
+ $originalEmptyName = $this->attributeText($emptyNameProduct->id, 'name');
|
|
|
|
|
+ $originalCategoryName = $this->attributeText($categoryProduct->id, 'name');
|
|
|
|
|
+
|
|
|
|
|
+ $result = $this->syncService()->sync([
|
|
|
|
|
+ [
|
|
|
|
|
+ 'sku' => 'MISSING-SKU',
|
|
|
|
|
+ 'locale' => 'en',
|
|
|
|
|
+ 'name' => 'Should Not Exist',
|
|
|
|
|
+ ],
|
|
|
|
|
+ [
|
|
|
|
|
+ 'sku' => 'SYNC-CORE-2',
|
|
|
|
|
+ 'locale' => 'en',
|
|
|
|
|
+ 'name' => '',
|
|
|
|
|
+ ],
|
|
|
|
|
+ [
|
|
|
|
|
+ 'sku' => 'SYNC-CORE-2B',
|
|
|
|
|
+ 'locale' => 'en',
|
|
|
|
|
+ 'name' => 'Changed Anyway',
|
|
|
|
|
+ 'categories' => 'No Such Category',
|
|
|
|
|
+ ],
|
|
|
|
|
+ ], false);
|
|
|
|
|
+
|
|
|
|
|
+ $this->assertSame(0, $result->updated);
|
|
|
|
|
+ $this->assertSame(3, $result->failed);
|
|
|
|
|
+ $this->assertSame($originalEmptyName, $this->attributeText($emptyNameProduct->id, 'name'));
|
|
|
|
|
+ $this->assertSame($originalCategoryName, $this->attributeText($categoryProduct->id, 'name'));
|
|
|
|
|
+ $this->assertFalse(
|
|
|
|
|
+ DB::table('products')->where('sku', 'MISSING-SKU')->exists()
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public function test_empty_qty_and_categories_do_not_overwrite_existing_values(): void
|
|
|
|
|
+ {
|
|
|
|
|
+ $product = $this->createSimpleProduct(['sku' => 'SYNC-CORE-3']);
|
|
|
|
|
+ $this->ensureInventory($product, 42);
|
|
|
|
|
+
|
|
|
|
|
+ $category = $this->createNamedCategory('Kept Category');
|
|
|
|
|
+ $product->categories()->sync([$category->id => ['position' => 0]]);
|
|
|
|
|
+
|
|
|
|
|
+ $result = $this->syncService()->sync([[
|
|
|
|
|
+ 'sku' => 'SYNC-CORE-3',
|
|
|
|
|
+ 'locale' => 'en',
|
|
|
|
|
+ 'name' => 'Name After Empty Columns',
|
|
|
|
|
+ 'qty' => '',
|
|
|
|
|
+ 'categories' => '',
|
|
|
|
|
+ ]], false);
|
|
|
|
|
+
|
|
|
|
|
+ $this->assertSame(1, $result->updated);
|
|
|
|
|
+ $this->assertSame('Name After Empty Columns', $this->attributeText($product->id, 'name'));
|
|
|
|
|
+ $this->assertSame(42, (int) DB::table('product_inventories')->where('product_id', $product->id)->value('qty'));
|
|
|
|
|
+ $this->assertTrue($product->fresh()->categories->contains('id', $category->id));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public function test_replaces_categories_when_names_are_updated(): void
|
|
|
|
|
+ {
|
|
|
|
|
+ $product = $this->createSimpleProduct(['sku' => 'SYNC-CAT-1']);
|
|
|
|
|
+ $original = $this->createNamedCategory('Original Category');
|
|
|
|
|
+ $target = $this->createNamedCategory('Target Category');
|
|
|
|
|
+ $product->categories()->sync([$original->id => ['position' => 0]]);
|
|
|
|
|
+
|
|
|
|
|
+ $result = $this->syncService()->sync([[
|
|
|
|
|
+ 'sku' => 'SYNC-CAT-1',
|
|
|
|
|
+ 'locale' => 'en',
|
|
|
|
|
+ 'categories' => 'Target Category',
|
|
|
|
|
+ ]], false);
|
|
|
|
|
+
|
|
|
|
|
+ $this->assertSame(1, $result->updated, json_encode($result->errors));
|
|
|
|
|
+ $this->assertSame(0, $result->failed);
|
|
|
|
|
+ $fresh = $product->fresh()->categories->pluck('id')->all();
|
|
|
|
|
+ $this->assertEqualsCanonicalizing([$target->id], $fresh);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public function test_resolves_category_name_from_another_locale(): void
|
|
|
|
|
+ {
|
|
|
|
|
+ $product = $this->createSimpleProduct(['sku' => 'SYNC-CAT-2']);
|
|
|
|
|
+ $category = $this->createNamedCategory('假发', 'zh_CN');
|
|
|
|
|
+
|
|
|
|
|
+ $result = $this->syncService()->sync([[
|
|
|
|
|
+ 'sku' => 'SYNC-CAT-2',
|
|
|
|
|
+ 'locale' => 'en',
|
|
|
|
|
+ 'categories' => '假发',
|
|
|
|
|
+ ]], false);
|
|
|
|
|
+
|
|
|
|
|
+ $this->assertSame(1, $result->updated, json_encode($result->errors));
|
|
|
|
|
+ $this->assertTrue($product->fresh()->categories->contains('id', $category->id));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public function test_keeps_comma_inside_category_name(): void
|
|
|
|
|
+ {
|
|
|
|
|
+ $product = $this->createSimpleProduct(['sku' => 'SYNC-CAT-3']);
|
|
|
|
|
+ $category = $this->createNamedCategory('13x4 Lace Front Wigs, Human Hair');
|
|
|
|
|
+
|
|
|
|
|
+ $result = $this->syncService()->sync([[
|
|
|
|
|
+ 'sku' => 'SYNC-CAT-3',
|
|
|
|
|
+ 'locale' => 'en',
|
|
|
|
|
+ 'categories' => '13x4 Lace Front Wigs, Human Hair',
|
|
|
|
|
+ ]], false);
|
|
|
|
|
+
|
|
|
|
|
+ $this->assertSame(1, $result->updated, json_encode($result->errors));
|
|
|
|
|
+ $this->assertTrue($product->fresh()->categories->contains('id', $category->id));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public function test_map_table_accepts_category_heading_alias(): void
|
|
|
|
|
+ {
|
|
|
|
|
+ $rows = $this->syncService()->mapTableToRows([
|
|
|
|
|
+ ['sku', 'category'],
|
|
|
|
|
+ ['ABC', 'Wigs'],
|
|
|
|
|
+ ]);
|
|
|
|
|
+
|
|
|
|
|
+ $this->assertSame('Wigs', $rows[0]['categories']);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public function test_whitelist_update_does_not_remove_images_or_options(): void
|
|
|
|
|
+ {
|
|
|
|
|
+ $product = $this->createSimpleProduct(['sku' => 'SYNC-CORE-4']);
|
|
|
|
|
+
|
|
|
|
|
+ DB::table('product_images')->insert([
|
|
|
|
|
+ 'type' => 'images',
|
|
|
|
|
+ 'path' => 'catalog/product/keep-me.jpg',
|
|
|
|
|
+ 'product_id' => $product->id,
|
|
|
|
|
+ 'position' => 1,
|
|
|
|
|
+ ]);
|
|
|
|
|
+
|
|
|
|
|
+ $optionCount = null;
|
|
|
|
|
+
|
|
|
|
|
+ if (Schema::hasTable('product_options') && Schema::hasTable('product_product_options')) {
|
|
|
|
|
+ $option = ProductOption::query()->create([
|
|
|
|
|
+ 'label' => 'Keep Option',
|
|
|
|
|
+ 'code' => 'keep_option_'.$product->id,
|
|
|
|
|
+ 'type' => 'select',
|
|
|
|
|
+ 'position' => 0,
|
|
|
|
|
+ ]);
|
|
|
|
|
+
|
|
|
|
|
+ $product->options()->attach($option->id, [
|
|
|
|
|
+ 'position' => 0,
|
|
|
|
|
+ 'is_required' => true,
|
|
|
|
|
+ ]);
|
|
|
|
|
+
|
|
|
|
|
+ $optionCount = DB::table('product_product_options')->where('product_id', $product->id)->count();
|
|
|
|
|
+ $this->assertSame(1, $optionCount);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ $imageCount = DB::table('product_images')->where('product_id', $product->id)->count();
|
|
|
|
|
+
|
|
|
|
|
+ $result = $this->syncService()->sync([[
|
|
|
|
|
+ 'sku' => 'SYNC-CORE-4',
|
|
|
|
|
+ 'name' => 'Still Has Media',
|
|
|
|
|
+ 'price' => 12.5,
|
|
|
|
|
+ ]], false);
|
|
|
|
|
+
|
|
|
|
|
+ $this->assertSame(1, $result->updated);
|
|
|
|
|
+ $this->assertSame($imageCount, DB::table('product_images')->where('product_id', $product->id)->count());
|
|
|
|
|
+
|
|
|
|
|
+ if ($optionCount !== null) {
|
|
|
|
|
+ $this->assertSame(
|
|
|
|
|
+ $optionCount,
|
|
|
|
|
+ DB::table('product_product_options')->where('product_id', $product->id)->count()
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public function test_map_table_requires_sku_column(): void
|
|
|
|
|
+ {
|
|
|
|
|
+ $this->expectExceptionMessage(trans('longyi::app.product-sync.errors.sku-column-missing'));
|
|
|
|
|
+
|
|
|
|
|
+ $this->syncService()->mapTableToRows([
|
|
|
|
|
+ ['name', 'price'],
|
|
|
|
|
+ ['Wig', '10'],
|
|
|
|
|
+ ]);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public function test_headings_match_export_contract(): void
|
|
|
|
|
+ {
|
|
|
|
|
+ $this->assertSame([
|
|
|
|
|
+ 'sku',
|
|
|
|
|
+ 'locale',
|
|
|
|
|
+ 'name',
|
|
|
|
|
+ 'url_key',
|
|
|
|
|
+ 'short_description',
|
|
|
|
|
+ 'description',
|
|
|
|
|
+ 'price',
|
|
|
|
|
+ 'special_price',
|
|
|
|
|
+ 'weight',
|
|
|
|
|
+ 'status',
|
|
|
|
|
+ 'qty',
|
|
|
|
|
+ 'categories',
|
|
|
|
|
+ 'meta_title',
|
|
|
|
|
+ 'meta_keywords',
|
|
|
|
|
+ 'meta_description',
|
|
|
|
|
+ ], ProductBasicInfoColumns::HEADINGS);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public function test_duplicate_sku_rows_are_rejected(): void
|
|
|
|
|
+ {
|
|
|
|
|
+ $this->createSimpleProduct(['sku' => 'SYNC-CORE-5']);
|
|
|
|
|
+
|
|
|
|
|
+ $result = $this->syncService()->sync([
|
|
|
|
|
+ [
|
|
|
|
|
+ 'sku' => 'SYNC-CORE-5',
|
|
|
|
|
+ 'name' => 'First',
|
|
|
|
|
+ ],
|
|
|
|
|
+ [
|
|
|
|
|
+ 'sku' => 'SYNC-CORE-5',
|
|
|
|
|
+ 'name' => 'Second',
|
|
|
|
|
+ ],
|
|
|
|
|
+ ], false);
|
|
|
|
|
+
|
|
|
|
|
+ $this->assertSame(1, $result->updated);
|
|
|
|
|
+ $this->assertSame(1, $result->failed);
|
|
|
|
|
+ $this->assertSame('First', $this->attributeText(
|
|
|
|
|
+ (int) Product::query()->where('sku', 'SYNC-CORE-5')->value('id'),
|
|
|
|
|
+ 'name'
|
|
|
|
|
+ ));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public function test_export_route_is_not_captured_by_product_file_download(): void
|
|
|
|
|
+ {
|
|
|
|
|
+ $route = app('router')->getRoutes()->match(
|
|
|
|
|
+ Request::create('/admin/catalog/products/sync/export', 'GET')
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ $this->assertSame('admin.catalog.products.sync.export', $route->getName());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ protected function syncService(): ProductBasicInfoSyncService
|
|
|
|
|
+ {
|
|
|
|
|
+ return app(ProductBasicInfoSyncService::class);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|