MigrateAsteriaCustomersCommandTest.php 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532
  1. <?php
  2. namespace Webkul\BagistoApi\Tests\Unit\Migration;
  3. use Illuminate\Support\Facades\Cache;
  4. use Illuminate\Support\Facades\DB;
  5. use Illuminate\Support\Facades\Hash;
  6. use Illuminate\Support\Facades\Schema;
  7. use Webkul\BagistoApi\Tests\BagistoApiTestCase;
  8. use Webkul\Core\Models\SubscribersList;
  9. use Webkul\Customer\Models\Customer;
  10. use Webkul\Customer\Models\CustomerAddress;
  11. use Webkul\Customer\Models\CustomerGroup;
  12. class MigrateAsteriaCustomersCommandTest extends BagistoApiTestCase
  13. {
  14. private string $sqlitePath;
  15. private string $connection = 'asteria_test';
  16. /** Magento entity_ids in a range unlikely to collide with a populated Bagisto catalog. */
  17. private const MID = 881900000;
  18. public function setUp(): void
  19. {
  20. parent::setUp();
  21. if (! Schema::hasColumn('customers', 'migrated_from_asteria_id')
  22. || ! Schema::hasColumn('customers', 'legacy_password')
  23. || ! Schema::hasColumn('customers', 'customer_source')) {
  24. $this->markTestSkipped('Run php artisan migrate to add Asteria customer columns.');
  25. }
  26. $this->seedRequiredData();
  27. $this->forgetLeakedAsteriaCustomers();
  28. Cache::forget('migrate_asteria_customers_last_id');
  29. $this->sqlitePath = sys_get_temp_dir().'/asteria_cmd_'.uniqid('', true).'.sqlite';
  30. touch($this->sqlitePath);
  31. config()->set('database.connections.'.$this->connection, [
  32. 'driver' => 'sqlite',
  33. 'database' => $this->sqlitePath,
  34. 'prefix' => '',
  35. 'foreign_key_constraints' => false,
  36. ]);
  37. DB::purge($this->connection);
  38. MagentoSchema::create($this->connection);
  39. }
  40. public function tearDown(): void
  41. {
  42. DB::purge($this->connection);
  43. if (isset($this->sqlitePath) && is_file($this->sqlitePath)) {
  44. @unlink($this->sqlitePath);
  45. }
  46. parent::tearDown();
  47. }
  48. public function test_it_migrates_a_customer_and_address_into_the_general_group(): void
  49. {
  50. MagentoSchema::seedCustomer($this->connection, [
  51. 'entity_id' => self::MID + 10,
  52. 'email' => 'mig-pk-jane@example.test',
  53. 'firstname' => 'Jane',
  54. 'lastname' => 'Doe',
  55. 'telephone' => '5558819010',
  56. 'gender' => 1,
  57. 'dob' => '1990-05-01 00:00:00',
  58. 'password_hash' => md5('abcsecret12').':abc',
  59. 'group_id' => 99,
  60. 'default_billing' => self::MID + 21,
  61. 'default_shipping' => self::MID + 21,
  62. 'source' => 20,
  63. ]);
  64. MagentoSchema::seedAddress($this->connection, [
  65. 'entity_id' => self::MID + 21,
  66. 'parent_id' => self::MID + 10,
  67. 'firstname' => 'Jane',
  68. 'lastname' => 'Doe',
  69. 'street' => "123 Main St\nApt 4",
  70. 'city' => 'Austin',
  71. 'region' => 'TX',
  72. 'postcode' => '78701',
  73. 'country_id' => 'US',
  74. 'telephone' => '5558819010',
  75. 'company' => 'Acme',
  76. ]);
  77. $this->artisan('customers:migrate-asteria', [
  78. '--connection' => $this->connection,
  79. '--reset-progress' => true,
  80. ])->assertSuccessful();
  81. $customer = Customer::query()->where('email', 'mig-pk-jane@example.test')->first();
  82. $this->assertNotNull($customer);
  83. $this->assertSame(self::MID + 10, (int) $customer->id);
  84. $this->assertSame(self::MID + 10, (int) $customer->migrated_from_asteria_id);
  85. $this->assertSame((int) $customer->id, (int) $customer->migrated_from_asteria_id);
  86. $this->assertSame('Jane', $customer->first_name);
  87. $this->assertSame('Doe', $customer->last_name);
  88. $this->assertSame('Male', $customer->gender);
  89. $this->assertSame('1990-05-01', $customer->date_of_birth);
  90. $this->assertSame('5558819010', $customer->phone);
  91. $this->assertSame(md5('abcsecret12').':abc', $customer->legacy_password);
  92. $this->assertSame(1, (int) $customer->is_verified);
  93. $this->assertSame(0, (int) $customer->subscribed_to_news_letter);
  94. $this->assertSame(20, (int) $customer->customer_source);
  95. $generalId = CustomerGroup::query()->where('code', 'general')->value('id');
  96. $this->assertSame((int) $generalId, (int) $customer->customer_group_id);
  97. $address = CustomerAddress::query()->where('customer_id', $customer->id)->first();
  98. $this->assertNotNull($address);
  99. $this->assertSame('123 Main St, Apt 4', $address->address);
  100. $this->assertSame('Austin', $address->city);
  101. $this->assertSame('US', $address->country);
  102. $this->assertTrue((bool) $address->default_address);
  103. $this->assertTrue((bool) $address->use_for_shipping);
  104. $this->assertSame(self::MID + 21, $this->asteriaAddressId($address));
  105. }
  106. public function test_dry_run_does_not_write_customers(): void
  107. {
  108. MagentoSchema::seedCustomer($this->connection, [
  109. 'entity_id' => self::MID + 10,
  110. 'email' => 'dry-run@example.com',
  111. 'firstname' => 'Dry',
  112. 'lastname' => 'Run',
  113. ]);
  114. $before = Customer::query()->count();
  115. $this->artisan('customers:migrate-asteria', [
  116. '--connection' => $this->connection,
  117. '--dry-run' => true,
  118. '--reset-progress' => true,
  119. ])->assertSuccessful();
  120. $this->assertSame($before, Customer::query()->count());
  121. $this->assertNull(Customer::query()->where('email', 'dry-run@example.com')->first());
  122. }
  123. public function test_it_links_an_existing_email_without_overwriting_the_password(): void
  124. {
  125. $existing = $this->createCustomer([
  126. 'email' => 'mig-pk-existing@example.test',
  127. 'password' => Hash::make('bagisto-secret'),
  128. 'first_name'=> 'Keep',
  129. ]);
  130. MagentoSchema::seedCustomer($this->connection, [
  131. 'entity_id' => self::MID + 44,
  132. 'email' => 'mig-pk-existing@example.test',
  133. 'firstname' => 'Magento',
  134. 'lastname' => 'Name',
  135. 'password_hash' => md5('abcsecret12').':abc',
  136. ]);
  137. MagentoSchema::seedAddress($this->connection, [
  138. 'entity_id' => self::MID + 45,
  139. 'parent_id' => self::MID + 44,
  140. 'firstname' => 'Magento',
  141. 'lastname' => 'Name',
  142. 'street' => '9 Oak Rd',
  143. 'city' => 'Dallas',
  144. 'country_id' => 'US',
  145. ]);
  146. $bagistoId = (int) $existing->id;
  147. $this->artisan('customers:migrate-asteria', [
  148. '--connection' => $this->connection,
  149. '--reset-progress' => true,
  150. ])->assertSuccessful();
  151. $existing->refresh();
  152. $this->assertSame($bagistoId, (int) $existing->id);
  153. $this->assertSame(self::MID + 44, (int) $existing->migrated_from_asteria_id);
  154. $this->assertSame('Keep', $existing->first_name);
  155. $this->assertTrue(Hash::check('bagisto-secret', $existing->password));
  156. $this->assertNull($existing->legacy_password);
  157. $this->assertSame(1, Customer::query()->where('email', 'mig-pk-existing@example.test')->count());
  158. $this->assertSame(1, CustomerAddress::query()->where('customer_id', $existing->id)->count());
  159. }
  160. public function test_it_nulls_a_conflicting_phone_number(): void
  161. {
  162. $this->createCustomer([
  163. 'email' => 'mig-pk-owner@example.test',
  164. 'phone' => '5558819070',
  165. ]);
  166. MagentoSchema::seedCustomer($this->connection, [
  167. 'entity_id' => self::MID + 70,
  168. 'email' => 'mig-pk-other@example.test',
  169. 'firstname' => 'Other',
  170. 'lastname' => 'Person',
  171. 'telephone' => '5558819070',
  172. ]);
  173. $this->artisan('customers:migrate-asteria', [
  174. '--connection' => $this->connection,
  175. '--reset-progress' => true,
  176. ])->assertSuccessful();
  177. $migrated = Customer::query()->where('email', 'mig-pk-other@example.test')->first();
  178. $this->assertNotNull($migrated);
  179. $this->assertNull($migrated->phone);
  180. }
  181. public function test_it_is_idempotent_on_a_second_run(): void
  182. {
  183. MagentoSchema::seedCustomer($this->connection, [
  184. 'entity_id' => self::MID + 80,
  185. 'email' => 'mig-pk-once@example.test',
  186. 'firstname' => 'Once',
  187. 'lastname' => 'Only',
  188. ]);
  189. MagentoSchema::seedAddress($this->connection, [
  190. 'entity_id' => self::MID + 81,
  191. 'parent_id' => self::MID + 80,
  192. 'firstname' => 'Once',
  193. 'lastname' => 'Only',
  194. 'street' => '1 Repeat Ln',
  195. 'city' => 'Miami',
  196. 'country_id' => 'US',
  197. ]);
  198. $options = [
  199. '--connection' => $this->connection,
  200. '--reset-progress' => true,
  201. ];
  202. $this->artisan('customers:migrate-asteria', $options)->assertSuccessful();
  203. $this->artisan('customers:migrate-asteria', $options)->assertSuccessful();
  204. $this->assertSame(1, Customer::query()->where('email', 'mig-pk-once@example.test')->count());
  205. $customer = Customer::query()->where('email', 'mig-pk-once@example.test')->first();
  206. $this->assertSame(self::MID + 80, (int) $customer->id);
  207. $this->assertSame(self::MID + 80, (int) $customer->migrated_from_asteria_id);
  208. $this->assertSame(1, CustomerAddress::query()->where('customer_id', $customer->id)->count());
  209. }
  210. public function test_it_migrates_a_subscribed_customer_into_subscribers_list(): void
  211. {
  212. MagentoSchema::seedCustomer($this->connection, [
  213. 'entity_id' => 880000090,
  214. 'email' => 'asteria-nl-sub@nshop.test',
  215. 'firstname' => 'Jane',
  216. 'lastname' => 'Sub',
  217. ]);
  218. MagentoSchema::seedSubscriber($this->connection, [
  219. 'subscriber_id' => 1,
  220. 'customer_id' => 880000090,
  221. 'subscriber_email' => 'asteria-nl-sub@nshop.test',
  222. 'subscriber_status' => 1,
  223. 'subscriber_confirm_code' => 'magento-token',
  224. ]);
  225. $this->artisan('customers:migrate-asteria', [
  226. '--connection' => $this->connection,
  227. '--reset-progress' => true,
  228. ])->assertSuccessful();
  229. $customer = Customer::query()->where('email', 'asteria-nl-sub@nshop.test')->first();
  230. $this->assertNotNull($customer);
  231. $this->assertSame(1, (int) $customer->subscribed_to_news_letter);
  232. $subscription = SubscribersList::query()->where('email', 'asteria-nl-sub@nshop.test')->first();
  233. $this->assertNotNull($subscription);
  234. $this->assertSame(1, (int) $subscription->is_subscribed);
  235. $this->assertSame($customer->id, (int) $subscription->customer_id);
  236. $this->assertSame('magento-token', $subscription->token);
  237. $this->assertSame(1, SubscribersList::query()->where('email', 'asteria-nl-sub@nshop.test')->count());
  238. }
  239. public function test_it_records_an_unsubscribed_magento_customer(): void
  240. {
  241. MagentoSchema::seedCustomer($this->connection, [
  242. 'entity_id' => 880000091,
  243. 'email' => 'asteria-nl-unsub@nshop.test',
  244. 'firstname' => 'Un',
  245. 'lastname' => 'Sub',
  246. ]);
  247. MagentoSchema::seedSubscriber($this->connection, [
  248. 'subscriber_id' => 2,
  249. 'customer_id' => 880000091,
  250. 'subscriber_email' => 'asteria-nl-unsub@nshop.test',
  251. 'subscriber_status' => 3,
  252. ]);
  253. $this->artisan('customers:migrate-asteria', [
  254. '--connection' => $this->connection,
  255. '--reset-progress' => true,
  256. ])->assertSuccessful();
  257. $customer = Customer::query()->where('email', 'asteria-nl-unsub@nshop.test')->first();
  258. $this->assertNotNull($customer);
  259. $this->assertSame(0, (int) $customer->subscribed_to_news_letter);
  260. $subscription = SubscribersList::query()->where('email', 'asteria-nl-unsub@nshop.test')->first();
  261. $this->assertNotNull($subscription);
  262. $this->assertSame(0, (int) $subscription->is_subscribed);
  263. $this->assertSame($customer->id, (int) $subscription->customer_id);
  264. }
  265. public function test_it_syncs_subscription_when_linking_an_existing_customer(): void
  266. {
  267. $existing = $this->createCustomer([
  268. 'email' => 'asteria-nl-keep@nshop.test',
  269. 'first_name' => 'Keep',
  270. 'subscribed_to_news_letter' => 0,
  271. ]);
  272. MagentoSchema::seedCustomer($this->connection, [
  273. 'entity_id' => 880000092,
  274. 'email' => 'asteria-nl-keep@nshop.test',
  275. 'firstname' => 'Magento',
  276. 'lastname' => 'Name',
  277. ]);
  278. MagentoSchema::seedSubscriber($this->connection, [
  279. 'subscriber_id' => 3,
  280. 'customer_id' => 880000092,
  281. 'subscriber_email' => 'asteria-nl-keep@nshop.test',
  282. 'subscriber_status' => 1,
  283. ]);
  284. $this->artisan('customers:migrate-asteria', [
  285. '--connection' => $this->connection,
  286. '--reset-progress' => true,
  287. ])->assertSuccessful();
  288. $existing->refresh();
  289. $this->assertSame('Keep', $existing->first_name);
  290. $this->assertSame(1, (int) $existing->subscribed_to_news_letter);
  291. $this->assertSame(1, SubscribersList::query()->where('email', 'asteria-nl-keep@nshop.test')->where('is_subscribed', 1)->count());
  292. }
  293. public function test_it_backfills_subscriptions_without_rescanning_customers(): void
  294. {
  295. MagentoSchema::seedCustomer($this->connection, [
  296. 'entity_id' => 880000093,
  297. 'email' => 'asteria-nl-later@nshop.test',
  298. 'firstname' => 'Later',
  299. 'lastname' => 'Sub',
  300. ]);
  301. $this->artisan('customers:migrate-asteria', [
  302. '--connection' => $this->connection,
  303. '--reset-progress' => true,
  304. ])->assertSuccessful();
  305. $customer = Customer::query()->where('email', 'asteria-nl-later@nshop.test')->first();
  306. $this->assertNotNull($customer);
  307. $this->assertSame(0, (int) $customer->subscribed_to_news_letter);
  308. MagentoSchema::seedSubscriber($this->connection, [
  309. 'subscriber_id' => 4,
  310. 'customer_id' => 880000093,
  311. 'subscriber_email' => 'asteria-nl-later@nshop.test',
  312. 'subscriber_status' => 1,
  313. ]);
  314. $this->artisan('customers:migrate-asteria', [
  315. '--connection' => $this->connection,
  316. ])->assertSuccessful();
  317. $customer->refresh();
  318. $this->assertSame(1, (int) $customer->subscribed_to_news_letter);
  319. $this->assertSame(1, SubscribersList::query()->where('email', 'asteria-nl-later@nshop.test')->where('customer_id', $customer->id)->count());
  320. }
  321. public function test_it_imports_guest_newsletter_subscribers(): void
  322. {
  323. MagentoSchema::seedSubscriber($this->connection, [
  324. 'subscriber_id' => 5,
  325. 'customer_id' => 0,
  326. 'subscriber_email' => 'asteria-nl-guest@nshop.test',
  327. 'subscriber_status' => 1,
  328. ]);
  329. MagentoSchema::seedSubscriber($this->connection, [
  330. 'subscriber_id' => 6,
  331. 'customer_id' => 0,
  332. 'subscriber_email' => 'asteria-nl-guest-unsub@nshop.test',
  333. 'subscriber_status' => 3,
  334. ]);
  335. $this->artisan('customers:migrate-asteria', [
  336. '--connection' => $this->connection,
  337. '--reset-progress' => true,
  338. ])->assertSuccessful();
  339. $guest = SubscribersList::query()->where('email', 'asteria-nl-guest@nshop.test')->first();
  340. $this->assertNotNull($guest);
  341. $this->assertSame(1, (int) $guest->is_subscribed);
  342. $this->assertNull($guest->customer_id);
  343. $this->assertNull(SubscribersList::query()->where('email', 'asteria-nl-guest-unsub@nshop.test')->first());
  344. }
  345. public function test_it_does_not_duplicate_subscribers_on_a_second_run(): void
  346. {
  347. MagentoSchema::seedCustomer($this->connection, [
  348. 'entity_id' => 880000094,
  349. 'email' => 'asteria-nl-twice@nshop.test',
  350. 'firstname' => 'Twice',
  351. 'lastname' => 'Sub',
  352. ]);
  353. MagentoSchema::seedSubscriber($this->connection, [
  354. 'subscriber_id' => 7,
  355. 'customer_id' => 880000094,
  356. 'subscriber_email' => 'asteria-nl-twice@nshop.test',
  357. 'subscriber_status' => 1,
  358. ]);
  359. $options = [
  360. '--connection' => $this->connection,
  361. '--reset-progress' => true,
  362. ];
  363. $this->artisan('customers:migrate-asteria', $options)->assertSuccessful();
  364. $this->artisan('customers:migrate-asteria', $options)->assertSuccessful();
  365. $this->assertSame(1, SubscribersList::query()->where('email', 'asteria-nl-twice@nshop.test')->count());
  366. }
  367. public function test_it_skips_newsletter_when_the_magento_table_is_missing(): void
  368. {
  369. Schema::connection($this->connection)->drop('newsletter_subscriber');
  370. MagentoSchema::seedCustomer($this->connection, [
  371. 'entity_id' => 880000095,
  372. 'email' => 'asteria-nl-notable@nshop.test',
  373. 'firstname' => 'No',
  374. 'lastname' => 'Table',
  375. ]);
  376. $this->artisan('customers:migrate-asteria', [
  377. '--connection' => $this->connection,
  378. '--reset-progress' => true,
  379. ])->assertSuccessful();
  380. $customer = Customer::query()->where('email', 'asteria-nl-notable@nshop.test')->first();
  381. $this->assertNotNull($customer);
  382. $this->assertSame(0, (int) $customer->subscribed_to_news_letter);
  383. $this->assertSame(0, SubscribersList::query()->where('email', 'asteria-nl-notable@nshop.test')->count());
  384. }
  385. public function test_it_migrates_customer_source(): void
  386. {
  387. MagentoSchema::seedCustomer($this->connection, [
  388. 'entity_id' => 880000096,
  389. 'email' => 'asteria-src@nshop.test',
  390. 'firstname' => 'Src',
  391. 'lastname' => 'User',
  392. 'source' => 22,
  393. ]);
  394. $this->artisan('customers:migrate-asteria', [
  395. '--connection' => $this->connection,
  396. '--reset-progress' => true,
  397. ])->assertSuccessful();
  398. $customer = Customer::query()->where('email', 'asteria-src@nshop.test')->first();
  399. $this->assertNotNull($customer);
  400. $this->assertSame(22, (int) $customer->customer_source);
  401. }
  402. public function test_it_backfills_customer_source_on_an_already_migrated_customer(): void
  403. {
  404. MagentoSchema::seedCustomer($this->connection, [
  405. 'entity_id' => 880000097,
  406. 'email' => 'asteria-src-later@nshop.test',
  407. 'firstname' => 'Later',
  408. 'lastname' => 'Src',
  409. ]);
  410. $this->artisan('customers:migrate-asteria', [
  411. '--connection' => $this->connection,
  412. '--reset-progress' => true,
  413. ])->assertSuccessful();
  414. $customer = Customer::query()->where('email', 'asteria-src-later@nshop.test')->first();
  415. $this->assertNotNull($customer);
  416. $this->assertNull($customer->customer_source);
  417. DB::connection($this->connection)->table('customer_entity_int')->insert([
  418. 'entity_id' => 880000097,
  419. 'attribute_id' => 89,
  420. 'value' => 15,
  421. ]);
  422. $this->artisan('customers:migrate-asteria', [
  423. '--connection' => $this->connection,
  424. '--reset-progress' => true,
  425. ])->assertSuccessful();
  426. $customer->refresh();
  427. $this->assertSame(15, (int) $customer->customer_source);
  428. }
  429. private function asteriaAddressId(CustomerAddress $address): ?int
  430. {
  431. $additional = $address->additional;
  432. if (is_string($additional) && $additional !== '') {
  433. $additional = json_decode($additional, true);
  434. }
  435. return is_array($additional) ? (int) ($additional['asteria_address_id'] ?? 0) : null;
  436. }
  437. private function forgetLeakedAsteriaCustomers(): void
  438. {
  439. $ids = Customer::query()
  440. ->where('email', 'like', 'mig-pk-%')
  441. ->orWhere('email', 'like', 'asteria-%')
  442. ->pluck('id');
  443. if ($ids->isEmpty()) {
  444. return;
  445. }
  446. $emails = Customer::query()->whereIn('id', $ids)->pluck('email');
  447. CustomerAddress::query()->whereIn('customer_id', $ids)->delete();
  448. SubscribersList::query()->whereIn('email', $emails)->delete();
  449. Customer::query()->whereIn('id', $ids)->delete();
  450. }
  451. }