MigrateReviewJob.php 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. <?php
  2. namespace App\Jobs;
  3. use App\Services\Asteria\MagentoPrimaryKey;
  4. use Illuminate\Bus\Queueable;
  5. use Illuminate\Contracts\Queue\ShouldQueue;
  6. use Illuminate\Foundation\Bus\Dispatchable;
  7. use Illuminate\Queue\InteractsWithQueue;
  8. use Illuminate\Queue\SerializesModels;
  9. use Illuminate\Support\Facades\DB;
  10. use Illuminate\Support\Facades\Log;
  11. /**
  12. * Processes a single batch of Magento reviews and writes them into the
  13. * Bagisto product_reviews table.
  14. *
  15. * Each job receives an array of pre-fetched review rows so no remote DB
  16. * connection is held across queue serialization/unserialization.
  17. */
  18. class MigrateReviewJob implements ShouldQueue
  19. {
  20. use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
  21. /** Retry up to 3 times before marking as failed. */
  22. public int $tries = 3;
  23. /** Allow 5 minutes per job for large batches. */
  24. public int $timeout = 300;
  25. /**
  26. * @param array<int, array<string, mixed>> $reviews
  27. * Each element contains:
  28. * review_id, created_at, magento_product_id, status_id,
  29. * title, comment, name, customer_email, avg_rating
  30. * @param array<int, int> $productIdMap magento_product_id → bagisto product_id
  31. * @param array<string, int> $customerEmailMap email → bagisto customer_id
  32. */
  33. public function __construct(
  34. private readonly array $reviews,
  35. private readonly array $productIdMap,
  36. private readonly array $customerEmailMap,
  37. ) {}
  38. public function handle(): void
  39. {
  40. $statusMap = [
  41. 1 => 'approved',
  42. 2 => 'pending',
  43. 3 => 'disapproved',
  44. ];
  45. $now = now();
  46. $inserted = 0;
  47. $skipped = 0;
  48. foreach ($this->reviews as $row) {
  49. $bagistoProductId = $this->productIdMap[$row['magento_product_id']] ?? null;
  50. if ($bagistoProductId === null) {
  51. Log::warning('MigrateReviewJob: no Bagisto product for Magento product_id='.$row['magento_product_id'].', review_id='.$row['review_id'].'. Skipping.');
  52. $skipped++;
  53. continue;
  54. }
  55. // Deduplicate: skip if already migrated (keyed on migrated_from_asteria_id).
  56. $reviewId = (int) $row['review_id'];
  57. if ($reviewId < 1) {
  58. $skipped++;
  59. continue;
  60. }
  61. $exists = DB::table('product_reviews')
  62. ->where('migrated_from_asteria_id', $reviewId)
  63. ->exists();
  64. if ($exists) {
  65. $skipped++;
  66. continue;
  67. }
  68. if (isset(MagentoPrimaryKey::occupiedIds('product_reviews', [$reviewId])[$reviewId])) {
  69. Log::warning("MigrateReviewJob: product_reviews.id {$reviewId} already occupied; skipping Magento review {$reviewId}.");
  70. $skipped++;
  71. continue;
  72. }
  73. $rating = $this->normaliseRating($row['avg_rating']);
  74. $status = $statusMap[$row['status_id']] ?? 'pending';
  75. $customerId = $row['customer_email']
  76. ? ($this->customerEmailMap[strtolower($row['customer_email'])] ?? null)
  77. : null;
  78. $title = $this->truncate((string) ($row['title'] ?? ''), 255) ?: '(no title)';
  79. $comment = $this->truncate((string) ($row['comment'] ?? ''), 65535);
  80. $name = $this->truncate((string) ($row['name'] ?? ''), 255) ?: 'Guest';
  81. DB::table('product_reviews')->insert([
  82. 'id' => $reviewId,
  83. 'title' => $title,
  84. 'comment' => $comment,
  85. 'rating' => $rating,
  86. 'status' => $status,
  87. 'product_id' => $bagistoProductId,
  88. 'customer_id' => $customerId,
  89. 'name' => $name,
  90. 'migrated_from_asteria_id' => $reviewId,
  91. 'created_at' => $row['created_at'] ?? $now,
  92. 'updated_at' => $now,
  93. ]);
  94. // Download and store review images from review_media_image.url
  95. if (! empty($row['images'])) {
  96. $this->migrateImages($reviewId, $row['images']);
  97. }
  98. $inserted++;
  99. }
  100. if ($inserted > 0) {
  101. MagentoPrimaryKey::bumpAutoIncrement('product_reviews');
  102. }
  103. Log::info("MigrateReviewJob: inserted={$inserted}, skipped={$skipped}");
  104. }
  105. /**
  106. * Store image URLs from Asteria directly as attachment paths.
  107. * Files will be uploaded to S3 in bulk separately.
  108. *
  109. * @param int $bagistoReviewId
  110. * @param list<string> $urls
  111. */
  112. private function migrateImages(int $bagistoReviewId, array $urls): void
  113. {
  114. foreach ($urls as $url) {
  115. if (empty($url)) {
  116. continue;
  117. }
  118. DB::table('product_review_attachments')->insert([
  119. 'review_id' => $bagistoReviewId,
  120. 'type' => 'image',
  121. 'mime_type' => null,
  122. 'path' => $url,
  123. ]);
  124. }
  125. }
  126. /**
  127. * Map Magento's average star value (1–5 float) to a Bagisto integer 1–5.
  128. * Falls back to 5 when null/zero (treat missing rating as best-case).
  129. */
  130. private function normaliseRating(mixed $avgRating): int
  131. {
  132. if ($avgRating === null || $avgRating == 0) {
  133. return 5;
  134. }
  135. return (int) max(1, min(5, round((float) $avgRating)));
  136. }
  137. private function truncate(string $value, int $maxLength): string
  138. {
  139. return mb_strlen($value) > $maxLength
  140. ? mb_substr($value, 0, $maxLength)
  141. : $value;
  142. }
  143. }