From 85b9eff3b3fa68c1d874cf38f9bb6dec664a369a Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Tue, 10 Jun 2025 13:28:55 +0000 Subject: [PATCH 01/24] Apply fixes from StyleCI --- app/Exceptions/ReviewServiceException.php | 2 +- app/Http/Controllers/Controller.php | 3 +- app/Http/Kernel.php | 20 +- .../Middleware/RedirectIfAuthenticated.php | 2 +- app/Livewire/ReviewAnalyzer.php | 106 +++--- app/Models/AsinData.php | 51 +-- app/Models/User.php | 6 +- app/Services/Amazon/AmazonFetchService.php | 113 +++--- app/Services/CaptchaService.php | 14 +- app/Services/LoggingService.php | 24 +- app/Services/OpenAIService.php | 117 +++---- app/Services/ReviewAnalysisService.php | 324 +++++++++--------- app/Services/ReviewService.php | 13 +- config/auth.php | 10 +- config/broadcasting.php | 22 +- config/cache.php | 34 +- config/database.php | 90 ++--- config/filesystems.php | 28 +- config/hashing.php | 6 +- config/logging.php | 60 ++-- config/mail.php | 26 +- config/queue.php | 48 +-- config/sanctum.php | 4 +- config/services.php | 14 +- database/factories/UserFactory.php | 8 +- .../2014_10_12_000000_create_users_table.php | 3 +- ...000_create_password_reset_tokens_table.php | 3 +- ..._08_19_000000_create_failed_jobs_table.php | 3 +- ...01_create_personal_access_tokens_table.php | 3 +- ...05_27_085749_create_asin_reviews_table.php | 2 +- ..._05_27_180704_add_openai_result_column.php | 3 +- ...d_rating_columns_to_asin_reviews_table.php | 2 +- ...rename_asin_reviews_table_to_asin_data.php | 6 +- ...10_remove_columns_from_asin_data_table.php | 7 +- ...3_remove_analysis_from_asin_data_table.php | 5 +- ...025_06_09_190029_create_sessions_table.php | 3 +- tests/Feature/ReviewAnalyzerLivewireTest.php | 62 ++-- tests/Unit/AmazonFetchServiceTest.php | 67 ++-- tests/Unit/AsinDataModelTest.php | 270 +++++++-------- tests/Unit/LoggingServiceTest.php | 48 +-- tests/Unit/OpenAIServiceTest.php | 84 ++--- 41 files changed, 855 insertions(+), 861 deletions(-) diff --git a/app/Exceptions/ReviewServiceException.php b/app/Exceptions/ReviewServiceException.php index ac278e7..5883acf 100644 --- a/app/Exceptions/ReviewServiceException.php +++ b/app/Exceptions/ReviewServiceException.php @@ -4,4 +4,4 @@ class ReviewServiceException extends \Exception { -} \ No newline at end of file +} diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php index 77ec359..f1406be 100644 --- a/app/Http/Controllers/Controller.php +++ b/app/Http/Controllers/Controller.php @@ -8,5 +8,6 @@ class Controller extends BaseController { - use AuthorizesRequests, ValidatesRequests; + use AuthorizesRequests; + use ValidatesRequests; } diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index 494c050..d3a2c6d 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -53,16 +53,16 @@ class Kernel extends HttpKernel * @var array */ protected $middlewareAliases = [ - 'auth' => \App\Http\Middleware\Authenticate::class, - 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, - 'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class, - 'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class, - 'can' => \Illuminate\Auth\Middleware\Authorize::class, - 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class, + 'auth' => \App\Http\Middleware\Authenticate::class, + 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, + 'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class, + 'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class, + 'can' => \Illuminate\Auth\Middleware\Authorize::class, + 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class, 'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class, - 'precognitive' => \Illuminate\Foundation\Http\Middleware\HandlePrecognitiveRequests::class, - 'signed' => \App\Http\Middleware\ValidateSignature::class, - 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, - 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, + 'precognitive' => \Illuminate\Foundation\Http\Middleware\HandlePrecognitiveRequests::class, + 'signed' => \App\Http\Middleware\ValidateSignature::class, + 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, + 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, ]; } diff --git a/app/Http/Middleware/RedirectIfAuthenticated.php b/app/Http/Middleware/RedirectIfAuthenticated.php index afc78c4..dae8398 100644 --- a/app/Http/Middleware/RedirectIfAuthenticated.php +++ b/app/Http/Middleware/RedirectIfAuthenticated.php @@ -13,7 +13,7 @@ class RedirectIfAuthenticated /** * Handle an incoming request. * - * @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next + * @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next */ public function handle(Request $request, Closure $next, string ...$guards): Response { diff --git a/app/Livewire/ReviewAnalyzer.php b/app/Livewire/ReviewAnalyzer.php index 84b1ead..9fc0ac3 100644 --- a/app/Livewire/ReviewAnalyzer.php +++ b/app/Livewire/ReviewAnalyzer.php @@ -2,13 +2,10 @@ namespace App\Livewire; -use Livewire\Component; -use App\Services\ReviewAnalysisService; use App\Services\CaptchaService; -use Illuminate\Support\Facades\Log; -use App\Services\Amazon\AmazonFetchService; -use App\Services\OpenAIService; use App\Services\LoggingService; +use App\Services\ReviewAnalysisService; +use Livewire\Component; /** * Livewire component for Amazon product review analysis interface. @@ -43,15 +40,15 @@ class ReviewAnalyzer extends Component // Progress tracking properties public $progressStep = 0; - public $progressSteps = [ - 1 => 'Validating product URL...', - 2 => 'Authenticating request...', - 3 => 'Checking product database...', - 4 => 'Gathering review information...', - 5 => 'Analyzing reviews with AI...', - 6 => 'Calculating final metrics...', - 7 => 'Finalizing results...' - ]; + public $progressSteps = [ + 1 => 'Validating product URL...', + 2 => 'Authenticating request...', + 3 => 'Checking product database...', + 4 => 'Gathering review information...', + 5 => 'Analyzing reviews with AI...', + 6 => 'Calculating final metrics...', + 7 => 'Finalizing results...', + ]; public $progressPercentage = 0; public $totalReviewsFound = 0; public $currentlyProcessing = ''; @@ -70,7 +67,7 @@ class ReviewAnalyzer extends Component public function mount() { $this->hcaptchaKey = uniqid(); - + // Initialize progress variables to ensure they're available in the template $this->progressStep = 0; $this->progressPercentage = 0; @@ -82,7 +79,7 @@ public function mount() public function analyze() { LoggingService::log('=== LIVEWIRE ANALYZE METHOD STARTED ==='); - + try { // Loading state and progress are already initialized by initializeProgress() // Just clear previous results @@ -100,7 +97,7 @@ public function analyze() $this->asinReview = null; $this->adjusted_rating = 0.00; $this->isAnalyzed = false; - + // Validate input $this->validate([ 'productUrl' => 'required|url', @@ -111,7 +108,7 @@ public function analyze() $captchaService = app(CaptchaService::class); $provider = $captchaService->getProvider(); $clientIp = request()->ip(); - + if ($provider === 'recaptcha' && !empty($this->g_recaptcha_response)) { if (!$captchaService->verify($this->g_recaptcha_response, $clientIp)) { throw new \Exception('Captcha verification failed. Please try again.'); @@ -127,34 +124,33 @@ public function analyze() $analysisService = app(ReviewAnalysisService::class); $productInfo = $analysisService->checkProductExists($this->productUrl); - + $asinData = $productInfo['asin_data']; - + // Gather reviews if needed if ($productInfo['needs_fetching']) { $asinData = $analysisService->fetchReviews( - $productInfo['asin'], - $productInfo['country'], + $productInfo['asin'], + $productInfo['country'], $productInfo['product_url'] ); } - + // Analyze with OpenAI if needed if ($productInfo['needs_openai']) { $asinData = $analysisService->analyzeWithOpenAI($asinData); } - + // Calculate final metrics and set results $analysisResult = $analysisService->calculateFinalMetrics($asinData); $this->setResults($analysisResult); - + // Set final state $this->isAnalyzed = true; LoggingService::log('=== LIVEWIRE ANALYZE METHOD COMPLETED SUCCESSFULLY ==='); - } catch (\Exception $e) { - LoggingService::log('Exception in analyze method: ' . $e->getMessage()); + LoggingService::log('Exception in analyze method: '.$e->getMessage()); $this->error = LoggingService::handleException($e); $this->resetAnalysisState(); } @@ -172,52 +168,46 @@ private function resetAnalysisState() $this->isAnalyzed = false; } - - private function setResults($analysisResult) { - LoggingService::log('Setting results with data: ' . json_encode($analysisResult)); - + LoggingService::log('Setting results with data: '.json_encode($analysisResult)); + $this->result = $analysisResult; $this->fake_percentage = $analysisResult['fake_percentage'] ?? 0; $this->amazon_rating = $analysisResult['amazon_rating'] ?? 0; $this->adjusted_rating = (float) $analysisResult['adjusted_rating']; $this->grade = $analysisResult['grade'] ?? 'N/A'; $this->explanation = $analysisResult['explanation'] ?? null; - - LoggingService::log('Results set - amazon_rating: ' . $this->amazon_rating . ', fake_percentage: ' . $this->fake_percentage . ', adjusted_rating: ' . $this->adjusted_rating); - LoggingService::log('Adjusted rating type: ' . gettype($this->adjusted_rating) . ', value: ' . var_export($this->adjusted_rating, true)); - } - + LoggingService::log('Results set - amazon_rating: '.$this->amazon_rating.', fake_percentage: '.$this->fake_percentage.', adjusted_rating: '.$this->adjusted_rating); + LoggingService::log('Adjusted rating type: '.gettype($this->adjusted_rating).', value: '.var_export($this->adjusted_rating, true)); + } public function render() { return view('livewire.review-analyzer'); } - - public function getGradeColor() { - return match($this->grade) { - 'A' => 'text-green-600', - 'B' => 'text-yellow-600', - 'C' => 'text-orange-600', - 'D' => 'text-red-600', - 'F' => 'text-red-800', + return match ($this->grade) { + 'A' => 'text-green-600', + 'B' => 'text-yellow-600', + 'C' => 'text-orange-600', + 'D' => 'text-red-600', + 'F' => 'text-red-800', default => 'text-gray-600' }; } public function getGradeBgColor() { - return match($this->grade) { - 'A' => 'bg-green-100', - 'B' => 'bg-yellow-100', - 'C' => 'bg-orange-100', - 'D' => 'bg-red-100', - 'F' => 'bg-red-200', + return match ($this->grade) { + 'A' => 'bg-green-100', + 'B' => 'bg-yellow-100', + 'C' => 'bg-orange-100', + 'D' => 'bg-red-100', + 'F' => 'bg-red-200', default => 'bg-gray-100' }; } @@ -240,14 +230,14 @@ public function clearPreviousResults() $this->asinReview = null; $this->adjusted_rating = 0.00; $this->isAnalyzed = false; - + // Also reset progress state for clean start $this->loading = false; $this->progressStep = 0; $this->progressPercentage = 0; $this->currentlyProcessing = ''; $this->progress = 0; - + // Force Livewire to re-render immediately $this->dispatch('resultsCleared'); } @@ -263,7 +253,7 @@ public function initializeProgress() $this->currentlyProcessing = 'Starting analysis...'; $this->progress = 0; $this->isAnalyzed = false; - + LoggingService::log('=== INITIALIZE PROGRESS CALLED ==='); LoggingService::log('Progress initialized - loading: true, step: 0'); } @@ -271,13 +261,11 @@ public function initializeProgress() public function startAnalysis() { LoggingService::log('=== START ANALYSIS CALLED ==='); - - // Clear previous results + + // Clear previous results $this->clearPreviousResults(); - + // Run the analysis (JavaScript will handle progress simulation) $this->analyze(); } - - -} \ No newline at end of file +} diff --git a/app/Models/AsinData.php b/app/Models/AsinData.php index b1c93cb..f4706d3 100644 --- a/app/Models/AsinData.php +++ b/app/Models/AsinData.php @@ -38,7 +38,7 @@ class AsinData extends Model * @var array */ protected $casts = [ - 'reviews' => 'array', + 'reviews' => 'array', 'openai_result' => 'array', ]; @@ -52,20 +52,20 @@ public function getFakePercentageAttribute(): ?float $reviews = $this->getReviewsArray(); $openaiResultJson = is_string($this->openai_result) ? $this->openai_result : json_encode($this->openai_result); $openaiResult = json_decode($openaiResultJson, true); - + if (empty($reviews) || !$openaiResult || !isset($openaiResult['detailed_scores'])) { return null; } $totalReviews = count($reviews); $fakeCount = 0; - + foreach ($openaiResult['detailed_scores'] as $reviewId => $score) { if ($score >= 70) { $fakeCount++; } } - + return $totalReviews > 0 ? round(($fakeCount / $totalReviews) * 100, 1) : 0; } @@ -77,11 +77,11 @@ public function getFakePercentageAttribute(): ?float public function getGradeAttribute(): ?string { $fakePercentage = $this->fake_percentage; - + if ($fakePercentage === null) { return null; } - + if ($fakePercentage >= 50) { return 'F'; } elseif ($fakePercentage >= 30) { @@ -104,28 +104,28 @@ public function getExplanationAttribute(): ?string { $reviews = $this->getReviewsArray(); $fakePercentage = $this->fake_percentage; - + if (empty($reviews) || $fakePercentage === null) { return null; } - + $totalReviews = count($reviews); $fakeCount = round(($fakePercentage / 100) * $totalReviews); - + $explanation = "Analysis of {$totalReviews} reviews found {$fakeCount} potentially fake reviews ({$fakePercentage}%). "; - + if ($fakePercentage >= 50) { - $explanation .= "This product has an extremely high percentage of fake reviews. Avoid purchasing."; + $explanation .= 'This product has an extremely high percentage of fake reviews. Avoid purchasing.'; } elseif ($fakePercentage >= 30) { - $explanation .= "This product has a high percentage of fake reviews. Consider looking for alternatives."; + $explanation .= 'This product has a high percentage of fake reviews. Consider looking for alternatives.'; } elseif ($fakePercentage >= 20) { - $explanation .= "This product has moderate fake review activity. Exercise some caution."; + $explanation .= 'This product has moderate fake review activity. Exercise some caution.'; } elseif ($fakePercentage >= 10) { - $explanation .= "This product has some fake review activity but is generally trustworthy."; + $explanation .= 'This product has some fake review activity but is generally trustworthy.'; } else { - $explanation .= "This product appears to have genuine reviews with minimal fake activity."; + $explanation .= 'This product appears to have genuine reviews with minimal fake activity.'; } - + return $explanation; } @@ -137,12 +137,13 @@ public function getExplanationAttribute(): ?string public function getAmazonRatingAttribute(): float { $reviews = $this->getReviewsArray(); - + if (empty($reviews)) { return 0; } $totalRating = collect($reviews)->sum('rating'); + return round($totalRating / count($reviews), 2); } @@ -155,7 +156,7 @@ public function getAdjustedRatingAttribute(): float { $reviews = $this->getReviewsArray(); $openaiResult = is_array($this->openai_result) ? $this->openai_result : json_decode($this->openai_result ?? '[]', true); - + if (empty($reviews) || !$openaiResult || !isset($openaiResult['results'])) { return $this->amazon_rating; } @@ -166,7 +167,7 @@ public function getAdjustedRatingAttribute(): float foreach ($reviews as $index => $review) { $score = $results[$index]['score'] ?? 0; - + // Scores < 80 are considered genuine if ($score < 80) { $genuineRatingSum += $review['rating']; @@ -189,18 +190,19 @@ public function getAdjustedRatingAttribute(): float public function getReviewsArray(): array { $reviews = $this->reviews; - + // If it's already an array, return it if (is_array($reviews)) { return $reviews; } - + // If it's a string, decode it if (is_string($reviews)) { $decoded = json_decode($reviews, true); + return is_array($decoded) ? $decoded : []; } - + // Default to empty array return []; } @@ -214,8 +216,9 @@ public function isAnalyzed(): bool { // Check if we have OpenAI results $openaiResult = is_array($this->openai_result) ? $this->openai_result : json_decode($this->openai_result ?? '[]', true); - return !empty($openaiResult) && + + return !empty($openaiResult) && is_array($openaiResult) && !empty($this->getReviewsArray()); } -} \ No newline at end of file +} diff --git a/app/Models/User.php b/app/Models/User.php index 4d7f70f..7b42bef 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -10,7 +10,9 @@ class User extends Authenticatable { - use HasApiTokens, HasFactory, Notifiable; + use HasApiTokens; + use HasFactory; + use Notifiable; /** * The attributes that are mass assignable. @@ -40,6 +42,6 @@ class User extends Authenticatable */ protected $casts = [ 'email_verified_at' => 'datetime', - 'password' => 'hashed', + 'password' => 'hashed', ]; } diff --git a/app/Services/Amazon/AmazonFetchService.php b/app/Services/Amazon/AmazonFetchService.php index c17ede2..29f020b 100644 --- a/app/Services/Amazon/AmazonFetchService.php +++ b/app/Services/Amazon/AmazonFetchService.php @@ -3,11 +3,9 @@ namespace App\Services\Amazon; use App\Models\AsinData; -use App\Exceptions\ReviewServiceException; +use App\Services\LoggingService; use GuzzleHttp\Client; -use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Http; -use App\Services\LoggingService; /** * Service for fetching Amazon product reviews via Unwrangle API. @@ -22,7 +20,7 @@ class AmazonFetchService public function __construct() { $this->httpClient = new Client([ - 'timeout' => 30, + 'timeout' => 30, 'http_errors' => false, ]); } @@ -30,37 +28,40 @@ public function __construct() /** * Fetch Amazon reviews and save to database. * - * @param string $asin Amazon Standard Identification Number - * @param string $country Two-letter country code + * @param string $asin Amazon Standard Identification Number + * @param string $country Two-letter country code * @param string $productUrl Full Amazon product URL - * @return AsinData The created database record + * * @throws \Exception If product doesn't exist or fetching fails + * + * @return AsinData The created database record */ public function fetchReviewsAndSave(string $asin, string $country, string $productUrl): AsinData { // Fetch reviews from Amazon $reviewsData = $this->fetchReviews($asin, $country); - + // Check if fetching failed (empty reviews data means validation failed) if (empty($reviewsData) || !isset($reviewsData['reviews'])) { throw new \Exception('Product does not exist on Amazon.com (US) site. Please check the URL and try again.'); } - + // Save to database - NO OpenAI analysis yet (will be done separately) return AsinData::create([ - 'asin' => $asin, - 'country' => $country, + 'asin' => $asin, + 'country' => $country, 'product_description' => $reviewsData['description'] ?? '', - 'reviews' => json_encode($reviewsData['reviews']), - 'openai_result' => null, // Will be populated later + 'reviews' => json_encode($reviewsData['reviews']), + 'openai_result' => null, // Will be populated later ]); } /** * Fetch reviews from Amazon using Unwrangle API. * - * @param string $asin Amazon Standard Identification Number + * @param string $asin Amazon Standard Identification Number * @param string $country Two-letter country code (defaults to 'us') + * * @return array Array containing reviews, description, and total count */ public function fetchReviews(string $asin, string $country = 'us'): array @@ -68,9 +69,10 @@ public function fetchReviews(string $asin, string $country = 'us'): array // Check if the ASIN exists on Amazon US before calling Unwrangle API if (!$this->validateAsinExists($asin)) { LoggingService::log('ASIN validation failed - product does not exist on amazon.com', [ - 'asin' => $asin, - 'url_checked' => "https://www.amazon.com/dp/{$asin}" + 'asin' => $asin, + 'url_checked' => "https://www.amazon.com/dp/{$asin}", ]); + return []; } @@ -79,14 +81,14 @@ public function fetchReviews(string $asin, string $country = 'us'): array $baseUrl = 'https://data.unwrangle.com/api/getter/'; $maxPages = 10; $country = 'us'; // Always use US country for session cookie match - + $query = [ - 'platform' => 'amazon_reviews', - 'asin' => $asin, + 'platform' => 'amazon_reviews', + 'asin' => $asin, 'country_code' => $country, - 'max_pages' => $maxPages, - 'api_key' => $apiKey, - 'cookie' => $cookie, + 'max_pages' => $maxPages, + 'api_key' => $apiKey, + 'cookie' => $cookie, ]; try { @@ -95,15 +97,16 @@ public function fetchReviews(string $asin, string $country = 'us'): array $body = $response->getBody()->getContents(); LoggingService::log('Unwrangle API response', [ - 'status' => $status, - 'has_data' => !empty($body) + 'status' => $status, + 'has_data' => !empty($body), ]); if ($status !== 200) { LoggingService::log('Unwrangle API non-200 response', [ 'status' => $status, - 'body' => $body + 'body' => $body, ]); + return []; } @@ -111,8 +114,9 @@ public function fetchReviews(string $asin, string $country = 'us'): array if (empty($data) || empty($data['success'])) { LoggingService::log('Unwrangle API returned error', [ - 'error' => $data['error'] ?? 'Unknown error' + 'error' => $data['error'] ?? 'Unknown error', ]); + return []; } @@ -121,16 +125,16 @@ public function fetchReviews(string $asin, string $country = 'us'): array $allReviews = $data['reviews'] ?? []; return [ - 'reviews' => $allReviews, - 'description' => $description, + 'reviews' => $allReviews, + 'description' => $description, 'total_reviews' => $totalReviews, ]; - } catch (\Exception $e) { LoggingService::log('Unwrangle API request exception', [ 'error' => $e->getMessage(), - 'asin' => $asin + 'asin' => $asin, ]); + return []; } } @@ -139,50 +143,49 @@ public function fetchReviews(string $asin, string $country = 'us'): array * Validate that an ASIN exists on Amazon US by checking the product page. * * @param string $asin Amazon Standard Identification Number + * * @return bool True if product exists (returns 200), false otherwise */ private function validateAsinExists(string $asin): bool { $url = "https://www.amazon.com/dp/{$asin}"; - + try { $response = $this->httpClient->request('GET', $url, [ - 'timeout' => 10, + 'timeout' => 10, 'allow_redirects' => false, // Don't follow redirects to catch geo-redirects - 'headers' => [ - 'User-Agent' => 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36', - 'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8', - 'Accept-Language' => 'en-US,en;q=0.9', - 'Accept-Encoding' => 'gzip, deflate, br', - 'Cache-Control' => 'no-cache', - 'Pragma' => 'no-cache', + 'headers' => [ + 'User-Agent' => 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36', + 'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8', + 'Accept-Language' => 'en-US,en;q=0.9', + 'Accept-Encoding' => 'gzip, deflate, br', + 'Cache-Control' => 'no-cache', + 'Pragma' => 'no-cache', 'Upgrade-Insecure-Requests' => '1', - 'Sec-Fetch-Dest' => 'document', - 'Sec-Fetch-Mode' => 'navigate', - 'Sec-Fetch-Site' => 'none', - 'Sec-Fetch-User' => '?1' + 'Sec-Fetch-Dest' => 'document', + 'Sec-Fetch-Mode' => 'navigate', + 'Sec-Fetch-Site' => 'none', + 'Sec-Fetch-User' => '?1', ], - 'http_errors' => false // Don't throw exceptions on 4xx/5xx + 'http_errors' => false, // Don't throw exceptions on 4xx/5xx ]); - + $statusCode = $response->getStatusCode(); - + LoggingService::log('ASIN validation check', [ - 'asin' => $asin, - 'url' => $url, - 'status_code' => $statusCode + 'asin' => $asin, + 'url' => $url, + 'status_code' => $statusCode, ]); - + return $statusCode === 200; - } catch (\Exception $e) { LoggingService::log('ASIN validation failed with exception', [ - 'asin' => $asin, - 'error' => $e->getMessage() + 'asin' => $asin, + 'error' => $e->getMessage(), ]); + return false; } } - - } diff --git a/app/Services/CaptchaService.php b/app/Services/CaptchaService.php index 48f8538..485a53f 100644 --- a/app/Services/CaptchaService.php +++ b/app/Services/CaptchaService.php @@ -5,7 +5,7 @@ use Illuminate\Support\Facades\Http; /** - * Captcha Service for handling multiple CAPTCHA providers + * Captcha Service for handling multiple CAPTCHA providers. * * This service provides a unified interface for both reCAPTCHA and hCaptcha * verification systems, automatically using the configured provider. @@ -20,6 +20,7 @@ class CaptchaService public function getSiteKey(): string { $provider = config('captcha.provider'); + return config("captcha.{$provider}.site_key"); } @@ -36,15 +37,16 @@ public function getProvider(): string /** * Verify a captcha response token with the configured provider. * - * @param string $token The captcha response token from the client - * @param string|null $ip Optional client IP address for verification + * @param string $token The captcha response token from the client + * @param string|null $ip Optional client IP address for verification + * * @return bool True if verification successful, false otherwise */ - public function verify(string $token, string $ip = null): bool + public function verify(string $token, ?string $ip = null): bool { $provider = config('captcha.provider'); - $secret = config("captcha.{$provider}.secret_key"); - $url = config("captcha.{$provider}.verify_url"); + $secret = config("captcha.{$provider}.secret_key"); + $url = config("captcha.{$provider}.verify_url"); $response = Http::asForm()->post($url, array_filter([ 'secret' => $secret, diff --git a/app/Services/LoggingService.php b/app/Services/LoggingService.php index b33d904..b50ca95 100644 --- a/app/Services/LoggingService.php +++ b/app/Services/LoggingService.php @@ -15,36 +15,36 @@ class LoggingService const ERROR_TYPES = [ 'TIMEOUT' => [ 'patterns' => ['cURL error 28', 'Operation timed out'], - 'message' => 'The request took too long to complete. Please try again.' + 'message' => 'The request took too long to complete. Please try again.', ], 'PRODUCT_NOT_FOUND' => [ 'patterns' => ['Product does not exist on Amazon.com (US) site'], - 'message' => 'Product does not exist on Amazon.com (US) site. Please check the URL and try again.' + 'message' => 'Product does not exist on Amazon.com (US) site. Please check the URL and try again.', ], 'DATA_TYPE_ERROR' => [ 'patterns' => ['count(): Argument #1 ($value) must be of type Countable|array', 'TypeError'], - 'message' => 'Data processing error occurred. Please try again.' + 'message' => 'Data processing error occurred. Please try again.', ], 'FETCHING_FAILED' => [ 'patterns' => ['Failed to fetch reviews'], - 'message' => 'Unable to fetch reviews at this time. Please try again later.' + 'message' => 'Unable to fetch reviews at this time. Please try again later.', ], 'OPENAI_ERROR' => [ 'patterns' => ['OpenAI API request failed'], - 'message' => 'Analysis service is temporarily unavailable. Please try again later.' + 'message' => 'Analysis service is temporarily unavailable. Please try again later.', ], 'INVALID_URL' => [ 'patterns' => ['Invalid Amazon URL', 'ASIN not found', 'Could not extract ASIN from URL'], - 'message' => 'Please provide a valid Amazon product URL.' + 'message' => 'Please provide a valid Amazon product URL.', ], 'REDIRECT_FAILED' => [ 'patterns' => ['Failed to follow redirect', 'Redirect does not lead to Amazon domain'], - 'message' => 'Unable to resolve the shortened URL. Please try using the full Amazon product URL instead.' - ] + 'message' => 'Unable to resolve the shortened URL. Please try using the full Amazon product URL instead.', + ], ]; /** - * Log a message with context + * Log a message with context. */ public static function log(string $message, array $context = [], string $level = self::INFO): void { @@ -52,7 +52,7 @@ public static function log(string $message, array $context = [], string $level = } /** - * Log an exception and return user-friendly message + * Log an exception and return user-friendly message. */ public static function handleException(\Exception $e): string { @@ -73,10 +73,10 @@ public static function handleException(\Exception $e): string } /** - * Log progress updates + * Log progress updates. */ public static function logProgress(string $step, string $message): void { self::log("Progress: {$step} - {$message}"); } -} \ No newline at end of file +} diff --git a/app/Services/OpenAIService.php b/app/Services/OpenAIService.php index 03dd4f9..0d46692 100644 --- a/app/Services/OpenAIService.php +++ b/app/Services/OpenAIService.php @@ -4,7 +4,6 @@ use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; -use App\Services\LoggingService; class OpenAIService { @@ -17,7 +16,7 @@ public function __construct() $this->apiKey = config('services.openai.api_key') ?? ''; $this->model = config('services.openai.model', 'gpt-4'); $this->baseUrl = config('services.openai.base_url', 'https://api.openai.com/v1'); - + if (empty($this->apiKey)) { throw new \InvalidArgumentException('OpenAI API key is not configured. Please set OPENAI_API_KEY in your .env file.'); } @@ -30,17 +29,18 @@ public function analyzeReviews(array $reviews): array } // Log the number of reviews being sent - LoggingService::log("Sending " . count($reviews) . " reviews to OpenAI for analysis"); - + LoggingService::log('Sending '.count($reviews).' reviews to OpenAI for analysis'); + $prompt = $this->buildPrompt($reviews); - + // Log prompt size for debugging $promptSize = strlen($prompt); LoggingService::log("Prompt size: {$promptSize} characters"); - + // Only chunk if prompt is extremely large (>100k chars) or too many reviews (>100) if ($promptSize > 100000 || count($reviews) > 100) { - LoggingService::log("Very large payload detected, processing in chunks to avoid API limits"); + LoggingService::log('Very large payload detected, processing in chunks to avoid API limits'); + return $this->analyzeReviewsInChunks($reviews); } @@ -48,7 +48,7 @@ public function analyzeReviews(array $reviews): array // Extract the endpoint from base_url if it includes the full path $endpoint = $this->baseUrl; if (!str_ends_with($endpoint, '/chat/completions')) { - $endpoint = rtrim($endpoint, '/') . '/chat/completions'; + $endpoint = rtrim($endpoint, '/').'/chat/completions'; } LoggingService::log("Making OpenAI API request to: {$endpoint}"); @@ -58,45 +58,47 @@ public function analyzeReviews(array $reviews): array LoggingService::log("Using max_tokens: {$maxTokens} for model: {$this->model}"); $response = Http::withHeaders([ - 'Authorization' => 'Bearer ' . $this->apiKey, - 'Content-Type' => 'application/json', - 'User-Agent' => 'ReviewAnalyzer/1.0', + 'Authorization' => 'Bearer '.$this->apiKey, + 'Content-Type' => 'application/json', + 'User-Agent' => 'ReviewAnalyzer/1.0', ])->timeout(300)->connectTimeout(60)->retry(3, 2000)->post($endpoint, [ - 'model' => $this->model, + 'model' => $this->model, 'messages' => [ [ - 'role' => 'system', - 'content' => 'You are an expert at detecting fake reviews. Analyze each review and provide a score from 0-100 where 0 is genuine and 100 is definitely fake. Return ONLY a JSON array with objects containing "id" and "score" fields.' + 'role' => 'system', + 'content' => 'You are an expert at detecting fake reviews. Analyze each review and provide a score from 0-100 where 0 is genuine and 100 is definitely fake. Return ONLY a JSON array with objects containing "id" and "score" fields.', ], [ - 'role' => 'user', - 'content' => $prompt - ] + 'role' => 'user', + 'content' => $prompt, + ], ], 'temperature' => 0.1, // Lower for more consistent results - 'max_tokens' => $maxTokens, + 'max_tokens' => $maxTokens, ]); if ($response->successful()) { - LoggingService::log("OpenAI API request successful"); + LoggingService::log('OpenAI API request successful'); $result = $response->json(); + return $this->parseOpenAIResponse($result, $reviews); } else { Log::error('OpenAI API error', [ 'status' => $response->status(), - 'body' => $response->body() + 'body' => $response->body(), ]); - throw new \Exception('OpenAI API request failed: ' . $response->status()); + + throw new \Exception('OpenAI API request failed: '.$response->status()); } } catch (\Exception $e) { LoggingService::log('OpenAI service error', ['error' => $e->getMessage()]); - + // Check if it's a timeout with 0 bytes - this suggests connection issues if (str_contains($e->getMessage(), 'cURL error 28') && str_contains($e->getMessage(), '0 bytes received')) { throw new \Exception('Unable to connect to OpenAI service. Please check your internet connection and try again.'); } - - throw new \Exception('Failed to analyze reviews: ' . $e->getMessage()); + + throw new \Exception('Failed to analyze reviews: '.$e->getMessage()); } } @@ -105,26 +107,25 @@ private function analyzeReviewsInChunks(array $reviews): array $chunkSize = 25; // Process 25 reviews at a time $chunks = array_chunk($reviews, $chunkSize); $allDetailedScores = []; - + foreach ($chunks as $index => $chunk) { - LoggingService::log("Processing chunk " . ($index + 1) . " of " . count($chunks) . " (" . count($chunk) . " reviews)"); - + LoggingService::log('Processing chunk '.($index + 1).' of '.count($chunks).' ('.count($chunk).' reviews)'); + try { $chunkResult = $this->analyzeReviews($chunk); - + if (isset($chunkResult['detailed_scores'])) { $allDetailedScores = array_merge($allDetailedScores, $chunkResult['detailed_scores']); } - + // Small delay between chunks to avoid rate limiting usleep(500000); // 0.5 seconds - } catch (\Exception $e) { - LoggingService::log("Error processing chunk " . ($index + 1) . ": " . $e->getMessage()); + LoggingService::log('Error processing chunk '.($index + 1).': '.$e->getMessage()); // Continue with other chunks even if one fails } } - + return ['detailed_scores' => $allDetailedScores]; } @@ -138,7 +139,7 @@ private function buildPrompt($reviews): string foreach ($reviews as $review) { $verification = isset($review['meta_data']['verified_purchase']) && $review['meta_data']['verified_purchase'] ? 'Verified' : 'Unverified'; $vine = isset($review['meta_data']['is_vine_voice']) && $review['meta_data']['is_vine_voice'] ? 'Vine' : 'Regular'; - + $prompt .= "ID:{$review['id']} {$review['rating']}/5 {$verification} {$vine}\n"; $prompt .= "Title: {$review['review_title']}\n"; $prompt .= "Text: {$review['review_text']}\n\n"; @@ -150,42 +151,42 @@ private function buildPrompt($reviews): string private function parseOpenAIResponse($response, $reviews): array { $content = $response['choices'][0]['message']['content'] ?? ''; - - LoggingService::log('Raw OpenAI response content: ' . json_encode(['content' => $content])); - + + LoggingService::log('Raw OpenAI response content: '.json_encode(['content' => $content])); + // Try to extract JSON from the content if (preg_match('/\[[\s\S]*\]/', $content, $matches)) { $jsonString = $matches[0]; - + if ($jsonString) { - LoggingService::log('Extracted JSON string: ' . json_encode(['json' => substr($jsonString, 0, 100) . '...'])); - + LoggingService::log('Extracted JSON string: '.json_encode(['json' => substr($jsonString, 0, 100).'...'])); + try { $results = json_decode($jsonString, true); - + if (json_last_error() === JSON_ERROR_NONE && is_array($results)) { $detailedScores = []; foreach ($results as $result) { if (isset($result['id']) && isset($result['score'])) { - $detailedScores[$result['id']] = (int)$result['score']; + $detailedScores[$result['id']] = (int) $result['score']; } } - + return [ - 'detailed_scores' => $detailedScores + 'detailed_scores' => $detailedScores, ]; } } catch (\Exception $e) { - LoggingService::log('Failed to parse OpenAI JSON response: ' . $e->getMessage()); + LoggingService::log('Failed to parse OpenAI JSON response: '.$e->getMessage()); } } } - + LoggingService::log('Failed to parse OpenAI response, using fallback'); - + // Fallback: return empty detailed scores return [ - 'detailed_scores' => [] + 'detailed_scores' => [], ]; } @@ -193,17 +194,17 @@ private function getMaxTokensForModel(string $model): int { // Map models to their max completion tokens $modelLimits = [ - 'gpt-4' => 4096, - 'gpt-4-0613' => 4096, - 'gpt-4-32k' => 32768, - 'gpt-4-32k-0613' => 32768, - 'gpt-4-turbo' => 4096, - 'gpt-4-turbo-preview' => 4096, - 'gpt-4-1106-preview' => 4096, - 'gpt-4-0125-preview' => 4096, - 'gpt-3.5-turbo' => 4096, - 'gpt-3.5-turbo-16k' => 16384, - 'gpt-3.5-turbo-0613' => 4096, + 'gpt-4' => 4096, + 'gpt-4-0613' => 4096, + 'gpt-4-32k' => 32768, + 'gpt-4-32k-0613' => 32768, + 'gpt-4-turbo' => 4096, + 'gpt-4-turbo-preview' => 4096, + 'gpt-4-1106-preview' => 4096, + 'gpt-4-0125-preview' => 4096, + 'gpt-3.5-turbo' => 4096, + 'gpt-3.5-turbo-16k' => 16384, + 'gpt-3.5-turbo-0613' => 4096, 'gpt-3.5-turbo-16k-0613' => 16384, ]; diff --git a/app/Services/ReviewAnalysisService.php b/app/Services/ReviewAnalysisService.php index 399d4fe..38c898c 100644 --- a/app/Services/ReviewAnalysisService.php +++ b/app/Services/ReviewAnalysisService.php @@ -4,9 +4,6 @@ use App\Models\AsinData; use App\Services\Amazon\AmazonFetchService; -use App\Services\OpenAIService; -use App\Services\LoggingService; -use GuzzleHttp\Client; class ReviewAnalysisService { @@ -31,30 +28,31 @@ private function extractAsinFromUrl($url): string $url = $this->followRedirect($url); LoggingService::log("Followed redirect to: {$url}"); } - + // Extract ASIN from various Amazon URL patterns $patterns = [ '/\/dp\/([A-Z0-9]{10})/', // /dp/ASIN - '/\/product\/([A-Z0-9]{10})/', // /product/ASIN + '/\/product\/([A-Z0-9]{10})/', // /product/ASIN '/\/product-reviews\/([A-Z0-9]{10})/', // /product-reviews/ASIN '/\/gp\/product\/([A-Z0-9]{10})/', // /gp/product/ASIN '/ASIN=([A-Z0-9]{10})/', // ASIN=ASIN parameter '/\/([A-Z0-9]{10})(?:\/|\?|$)/', // ASIN in path ]; - + foreach ($patterns as $pattern) { if (preg_match($pattern, $url, $matches)) { LoggingService::log("Extracted ASIN '{$matches[1]}' using pattern: {$pattern}"); + return $matches[1]; } } - + // If it's already just an ASIN if (preg_match('/^[A-Z0-9]{10}$/', $url)) { return $url; } - - throw new \Exception('Could not extract ASIN from URL: ' . $url); + + throw new \Exception('Could not extract ASIN from URL: '.$url); } private function followRedirect(string $url): string @@ -63,55 +61,56 @@ private function followRedirect(string $url): string if (!preg_match('/^https?:\/\/a\.co\//', $url)) { throw new \Exception('Redirect following only allowed for a.co domains'); } - + try { LoggingService::log("Following redirect for URL: {$url}"); - + // Track redirects manually using on_redirect callback $redirectUrls = []; $client = new \GuzzleHttp\Client([ - 'timeout' => 10, + 'timeout' => 10, 'allow_redirects' => [ - 'max' => 5, - 'strict' => true, - 'referer' => true, + 'max' => 5, + 'strict' => true, + 'referer' => true, 'on_redirect' => function ($request, $response, $uri) use (&$redirectUrls) { $redirectUrls[] = (string) $uri; - } + }, ], 'headers' => [ - 'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', - 'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', - 'Accept-Language' => 'en-US,en;q=0.5', - 'Accept-Encoding' => 'gzip, deflate', - 'Connection' => 'keep-alive', + 'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', + 'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', + 'Accept-Language' => 'en-US,en;q=0.5', + 'Accept-Encoding' => 'gzip, deflate', + 'Connection' => 'keep-alive', 'Upgrade-Insecure-Requests' => '1', - ] + ], ]); - + $response = $client->get($url); $finalUrl = !empty($redirectUrls) ? end($redirectUrls) : $url; - - LoggingService::log("Redirect resolved", [ - 'original_url' => $url, - 'final_url' => $finalUrl, - 'status_code' => $response->getStatusCode(), - 'redirect_chain' => $redirectUrls + + LoggingService::log('Redirect resolved', [ + 'original_url' => $url, + 'final_url' => $finalUrl, + 'status_code' => $response->getStatusCode(), + 'redirect_chain' => $redirectUrls, ]); - + // Verify the redirect goes to Amazon if (preg_match('/^https?:\/\/(?:www\.)?amazon\.(com|co\.uk|ca|com\.au|de|fr|it|es|in|co\.jp|com\.mx|com\.br|nl|sg|com\.tr|ae|sa|se|pl|eg|be)/', $finalUrl)) { return $finalUrl; } else { - throw new \Exception('Redirect does not lead to Amazon domain: ' . $finalUrl); + throw new \Exception('Redirect does not lead to Amazon domain: '.$finalUrl); } - } catch (\GuzzleHttp\Exception\RequestException $e) { - LoggingService::log("Redirect following failed: " . $e->getMessage()); - throw new \Exception('Failed to follow redirect: ' . $e->getMessage()); + LoggingService::log('Redirect following failed: '.$e->getMessage()); + + throw new \Exception('Failed to follow redirect: '.$e->getMessage()); } catch (\Exception $e) { - LoggingService::log("Redirect following failed: " . $e->getMessage()); - throw new \Exception('Failed to follow redirect: ' . $e->getMessage()); + LoggingService::log('Redirect following failed: '.$e->getMessage()); + + throw new \Exception('Failed to follow redirect: '.$e->getMessage()); } } @@ -125,55 +124,55 @@ public function analyzeProduct(string $asin, string $country = 'us'): array $asin = $this->extractAsinFromUrl($asin); $country = 'us'; // Default country $productUrl = "https://www.amazon.com/dp/{$asin}"; - + // Step 1: Check if analysis already exists in database $asinData = AsinData::where('asin', $asin)->where('country', $country)->first(); - + if (!$asinData) { - LoggingService::log('Product not found in database, starting fetch process for ASIN: ' . $asin); + LoggingService::log('Product not found in database, starting fetch process for ASIN: '.$asin); LoggingService::log('Progress: 1/4 - Gathering Amazon reviews...'); - + // Step 2: Fetch reviews from Amazon $asinData = $this->fetchService->fetchReviewsAndSave($asin, $country, $productUrl); - + if (!$asinData) { throw new \Exception("This product (ASIN: {$asin}) does not exist on Amazon US. Please verify the product URL and ensure it's available on amazon.com."); } - - LoggingService::log("Gathered " . count($asinData->getReviewsArray()) . " reviews, now sending to OpenAI for analysis"); - + + LoggingService::log('Gathered '.count($asinData->getReviewsArray()).' reviews, now sending to OpenAI for analysis'); + // Step 4: Analyze with OpenAI and update the same record $openaiResult = $this->openAIService->analyzeReviews($asinData->getReviewsArray()); - + // Step 5: Update the record with OpenAI analysis $asinData->update([ 'openai_result' => json_encode($openaiResult), - 'status' => 'completed', + 'status' => 'completed', ]); - - LoggingService::log("Updated database record with OpenAI analysis results"); + + LoggingService::log('Updated database record with OpenAI analysis results'); } else { LoggingService::log("Found existing analysis in database for ASIN: {$asin}"); - + // Check if OpenAI analysis is missing and complete it if needed if (!$asinData->openai_result) { LoggingService::log("OpenAI analysis missing, completing analysis for ASIN: {$asin}"); - + $reviews = $asinData->getReviewsArray(); if (empty($reviews)) { throw new \Exception('No reviews found in database for analysis'); } - + LoggingService::logProgress('Completing analysis', 'Analyzing reviews with OpenAI...'); $openaiResult = $this->openAIService->analyzeReviews($reviews); - + $asinData->update([ 'openai_result' => json_encode($openaiResult), - 'status' => 'completed', + 'status' => 'completed', ]); - + LoggingService::log("Completed missing OpenAI analysis for ASIN: {$asin}"); - + // Refresh the model to get updated data $asinData = $asinData->fresh(); } @@ -193,56 +192,56 @@ public function analyzeProduct(string $asin, string $country = 'us'): array if (is_string($openaiResult)) { $openaiResult = json_decode($openaiResult, true); } - + $detailedScores = $openaiResult['detailed_scores'] ?? []; $reviews = $asinData->getReviewsArray(); - + $totalReviews = count($reviews); $fakeCount = 0; $amazonRatingSum = 0; $genuineRatingSum = 0; $genuineCount = 0; - - LoggingService::log("=== STARTING CALCULATION DEBUG ==="); + + LoggingService::log('=== STARTING CALCULATION DEBUG ==='); LoggingService::log("Total reviews found: {$totalReviews}"); - LoggingService::log("Detailed scores count: " . count($detailedScores)); - + LoggingService::log('Detailed scores count: '.count($detailedScores)); + $fakeReviews = []; $genuineReviews = []; - + foreach ($reviews as $review) { $reviewId = $review['id']; $rating = $review['rating']; $amazonRatingSum += $rating; - + $fakeScore = $detailedScores[$reviewId] ?? 0; - + if ($fakeScore >= 70) { $fakeCount++; $fakeReviews[] = [ - 'id' => $reviewId, - 'rating' => $rating, - 'fake_score' => $fakeScore + 'id' => $reviewId, + 'rating' => $rating, + 'fake_score' => $fakeScore, ]; LoggingService::log("FAKE REVIEW: ID={$reviewId}, Rating={$rating}, Score={$fakeScore}"); } else { $genuineRatingSum += $rating; $genuineCount++; $genuineReviews[] = [ - 'id' => $reviewId, - 'rating' => $rating, - 'fake_score' => $fakeScore + 'id' => $reviewId, + 'rating' => $rating, + 'fake_score' => $fakeScore, ]; } } - - LoggingService::log("=== FAKE REVIEWS SUMMARY ==="); + + LoggingService::log('=== FAKE REVIEWS SUMMARY ==='); LoggingService::log("Total fake reviews: {$fakeCount}"); foreach ($fakeReviews as $fake) { LoggingService::log("Fake: {$fake['id']} - Rating: {$fake['rating']} - Score: {$fake['fake_score']}"); } - - LoggingService::log("=== GENUINE REVIEWS SUMMARY ==="); + + LoggingService::log('=== GENUINE REVIEWS SUMMARY ==='); LoggingService::log("Total genuine reviews: {$genuineCount}"); $genuineRatingCounts = [1 => 0, 2 => 0, 3 => 0, 4 => 0, 5 => 0]; foreach ($genuineReviews as $genuine) { @@ -251,46 +250,45 @@ public function analyzeProduct(string $asin, string $country = 'us'): array foreach ($genuineRatingCounts as $rating => $count) { LoggingService::log("Genuine {$rating}-star reviews: {$count}"); } - + $fakePercentage = $totalReviews > 0 ? ($fakeCount / $totalReviews) * 100 : 0; $amazonRating = $totalReviews > 0 ? $amazonRatingSum / $totalReviews : 0; $adjustedRating = $this->calculateAdjustedRating($genuineReviews); - - LoggingService::log("=== FINAL CALCULATIONS ==="); + + LoggingService::log('=== FINAL CALCULATIONS ==='); LoggingService::log("Amazon rating sum: {$amazonRatingSum}"); LoggingService::log("Amazon rating average: {$amazonRating}"); LoggingService::log("Genuine rating sum: {$genuineRatingSum}"); LoggingService::log("Genuine rating average: {$adjustedRating}"); LoggingService::log("Fake percentage: {$fakePercentage}%"); - + $grade = $this->calculateGrade($fakePercentage); $explanation = $this->generateExplanation($totalReviews, $fakeCount, $fakePercentage); - + // Update the database with calculated values $asinData->update([ 'fake_percentage' => round($fakePercentage, 1), - 'amazon_rating' => round($amazonRating, 2), + 'amazon_rating' => round($amazonRating, 2), 'adjusted_rating' => round($adjustedRating, 2), - 'grade' => $grade, - 'explanation' => $explanation, + 'grade' => $grade, + 'explanation' => $explanation, ]); return [ 'fake_percentage' => round($fakePercentage, 1), - 'amazon_rating' => round($amazonRating, 2), + 'amazon_rating' => round($amazonRating, 2), 'adjusted_rating' => round($adjustedRating, 2), - 'grade' => $grade, - 'explanation' => $explanation, - 'asin_review' => $asinData->fresh(), + 'grade' => $grade, + 'explanation' => $explanation, + 'asin_review' => $asinData->fresh(), ]; } catch (\Exception $e) { $userMessage = LoggingService::handleException($e); + throw new \Exception($userMessage); } } - - private function calculateGrade($fakePercentage): string { if ($fakePercentage <= 10) { @@ -308,104 +306,106 @@ private function calculateGrade($fakePercentage): string private function generateExplanation($totalReviews, $fakeCount, $fakePercentage): string { - return "Analysis of {$totalReviews} reviews found {$fakeCount} potentially fake reviews (" . round($fakePercentage, 1) . "%). " . - ($fakePercentage <= 10 ? "This product has very low fake review activity and appears highly trustworthy." : - ($fakePercentage <= 20 ? "This product has low fake review activity and appears trustworthy." : - ($fakePercentage <= 35 ? "This product has moderate fake review activity. Exercise some caution." : - ($fakePercentage <= 50 ? "This product has high fake review activity. Exercise caution when purchasing." : - "This product has very high fake review activity. We recommend avoiding this product.")))); + return "Analysis of {$totalReviews} reviews found {$fakeCount} potentially fake reviews (".round($fakePercentage, 1).'%). '. + ($fakePercentage <= 10 ? 'This product has very low fake review activity and appears highly trustworthy.' : + ($fakePercentage <= 20 ? 'This product has low fake review activity and appears trustworthy.' : + ($fakePercentage <= 35 ? 'This product has moderate fake review activity. Exercise some caution.' : + ($fakePercentage <= 50 ? 'This product has high fake review activity. Exercise caution when purchasing.' : + 'This product has very high fake review activity. We recommend avoiding this product.')))); } private function calculateAdjustedRating($genuineReviews) { - LoggingService::log('calculateAdjustedRating called with ' . count($genuineReviews) . ' genuine reviews'); - + LoggingService::log('calculateAdjustedRating called with '.count($genuineReviews).' genuine reviews'); + if (empty($genuineReviews)) { LoggingService::log('No genuine reviews found, returning 0'); + return 0; } - + $totalRating = 0; foreach ($genuineReviews as $review) { $totalRating += $review['rating']; - LoggingService::log('Adding rating: ' . $review['rating'] . ', total so far: ' . $totalRating); + LoggingService::log('Adding rating: '.$review['rating'].', total so far: '.$totalRating); } - + $adjustedRating = $totalRating / count($genuineReviews); $roundedRating = round($adjustedRating, 2); - - LoggingService::log('Final calculation: ' . $totalRating . ' / ' . count($genuineReviews) . ' = ' . $adjustedRating); - LoggingService::log('Rounded result: ' . $roundedRating); - + + LoggingService::log('Final calculation: '.$totalRating.' / '.count($genuineReviews).' = '.$adjustedRating); + LoggingService::log('Rounded result: '.$roundedRating); + return $roundedRating; } /** - * Phase 1: Extract ASIN and check if product exists in database + * Phase 1: Extract ASIN and check if product exists in database. */ public function checkProductExists(string $productUrl): array { $asin = $this->extractAsinFromUrl($productUrl); $country = 'us'; $productUrl = "https://www.amazon.com/dp/{$asin}"; - + $asinData = AsinData::where('asin', $asin)->where('country', $country)->first(); - + return [ - 'asin' => $asin, - 'country' => $country, - 'product_url' => $productUrl, - 'exists' => $asinData !== null, - 'asin_data' => $asinData, + 'asin' => $asin, + 'country' => $country, + 'product_url' => $productUrl, + 'exists' => $asinData !== null, + 'asin_data' => $asinData, 'needs_fetching' => $asinData === null, - 'needs_openai' => $asinData === null || !$asinData->openai_result + 'needs_openai' => $asinData === null || !$asinData->openai_result, ]; } - + /** - * Phase 2: Fetch reviews from Amazon (if needed) + * Phase 2: Fetch reviews from Amazon (if needed). */ public function fetchReviews(string $asin, string $country, string $productUrl): AsinData { LoggingService::log("Starting scrape process for ASIN: {$asin}"); - + $asinData = $this->fetchService->fetchReviewsAndSave($asin, $country, $productUrl); - + if (!$asinData) { throw new \Exception("This product (ASIN: {$asin}) does not exist on Amazon US. Please verify the product URL and ensure it's available on amazon.com."); } - - LoggingService::log("Gathered " . count($asinData->getReviewsArray()) . " reviews"); + + LoggingService::log('Gathered '.count($asinData->getReviewsArray()).' reviews'); + return $asinData; } - + /** - * Phase 3: Analyze reviews with OpenAI (if needed) + * Phase 3: Analyze reviews with OpenAI (if needed). */ public function analyzeWithOpenAI(AsinData $asinData): AsinData { $reviews = $asinData->getReviewsArray(); - + if (empty($reviews)) { throw new \Exception('No reviews found for analysis'); } - - LoggingService::log("Sending " . count($reviews) . " reviews to OpenAI for analysis"); - + + LoggingService::log('Sending '.count($reviews).' reviews to OpenAI for analysis'); + $openaiResult = $this->openAIService->analyzeReviews($reviews); - + $asinData->update([ 'openai_result' => json_encode($openaiResult), - 'status' => 'completed', + 'status' => 'completed', ]); - - LoggingService::log("Updated database record with OpenAI analysis results"); - + + LoggingService::log('Updated database record with OpenAI analysis results'); + return $asinData->fresh(); } - + /** - * Phase 4: Calculate final metrics and grades + * Phase 4: Calculate final metrics and grades. */ public function calculateFinalMetrics(AsinData $asinData): array { @@ -418,56 +418,56 @@ public function calculateFinalMetrics(AsinData $asinData): array if (is_string($openaiResult)) { $openaiResult = json_decode($openaiResult, true); } - + $detailedScores = $openaiResult['detailed_scores'] ?? []; $reviews = $asinData->getReviewsArray(); - + $totalReviews = count($reviews); $fakeCount = 0; $amazonRatingSum = 0; $genuineRatingSum = 0; $genuineCount = 0; - - LoggingService::log("=== STARTING CALCULATION DEBUG ==="); + + LoggingService::log('=== STARTING CALCULATION DEBUG ==='); LoggingService::log("Total reviews found: {$totalReviews}"); - LoggingService::log("Detailed scores count: " . count($detailedScores)); - + LoggingService::log('Detailed scores count: '.count($detailedScores)); + $fakeReviews = []; $genuineReviews = []; - + foreach ($reviews as $review) { $reviewId = $review['id']; $rating = $review['rating']; $amazonRatingSum += $rating; - + $fakeScore = $detailedScores[$reviewId] ?? 0; - + if ($fakeScore >= 70) { $fakeCount++; $fakeReviews[] = [ - 'id' => $reviewId, - 'rating' => $rating, - 'fake_score' => $fakeScore + 'id' => $reviewId, + 'rating' => $rating, + 'fake_score' => $fakeScore, ]; LoggingService::log("FAKE REVIEW: ID={$reviewId}, Rating={$rating}, Score={$fakeScore}"); } else { $genuineRatingSum += $rating; $genuineCount++; $genuineReviews[] = [ - 'id' => $reviewId, - 'rating' => $rating, - 'fake_score' => $fakeScore + 'id' => $reviewId, + 'rating' => $rating, + 'fake_score' => $fakeScore, ]; } } - - LoggingService::log("=== FAKE REVIEWS SUMMARY ==="); + + LoggingService::log('=== FAKE REVIEWS SUMMARY ==='); LoggingService::log("Total fake reviews: {$fakeCount}"); foreach ($fakeReviews as $fake) { LoggingService::log("Fake: {$fake['id']} - Rating: {$fake['rating']} - Score: {$fake['fake_score']}"); } - - LoggingService::log("=== GENUINE REVIEWS SUMMARY ==="); + + LoggingService::log('=== GENUINE REVIEWS SUMMARY ==='); LoggingService::log("Total genuine reviews: {$genuineCount}"); $genuineRatingCounts = [1 => 0, 2 => 0, 3 => 0, 4 => 0, 5 => 0]; foreach ($genuineReviews as $genuine) { @@ -476,37 +476,37 @@ public function calculateFinalMetrics(AsinData $asinData): array foreach ($genuineRatingCounts as $rating => $count) { LoggingService::log("Genuine {$rating}-star reviews: {$count}"); } - + $fakePercentage = $totalReviews > 0 ? ($fakeCount / $totalReviews) * 100 : 0; $amazonRating = $totalReviews > 0 ? $amazonRatingSum / $totalReviews : 0; $adjustedRating = $this->calculateAdjustedRating($genuineReviews); - - LoggingService::log("=== FINAL CALCULATIONS ==="); + + LoggingService::log('=== FINAL CALCULATIONS ==='); LoggingService::log("Amazon rating sum: {$amazonRatingSum}"); LoggingService::log("Amazon rating average: {$amazonRating}"); LoggingService::log("Genuine rating sum: {$genuineRatingSum}"); LoggingService::log("Genuine rating average: {$adjustedRating}"); LoggingService::log("Fake percentage: {$fakePercentage}%"); - + $grade = $this->calculateGrade($fakePercentage); $explanation = $this->generateExplanation($totalReviews, $fakeCount, $fakePercentage); - + // Update the database with calculated values $asinData->update([ 'fake_percentage' => round($fakePercentage, 1), - 'amazon_rating' => round($amazonRating, 2), + 'amazon_rating' => round($amazonRating, 2), 'adjusted_rating' => round($adjustedRating, 2), - 'grade' => $grade, - 'explanation' => $explanation, + 'grade' => $grade, + 'explanation' => $explanation, ]); return [ 'fake_percentage' => round($fakePercentage, 1), - 'amazon_rating' => round($amazonRating, 2), + 'amazon_rating' => round($amazonRating, 2), 'adjusted_rating' => round($adjustedRating, 2), - 'grade' => $grade, - 'explanation' => $explanation, - 'asin_review' => $asinData->fresh(), + 'grade' => $grade, + 'explanation' => $explanation, + 'asin_review' => $asinData->fresh(), ]; } } diff --git a/app/Services/ReviewService.php b/app/Services/ReviewService.php index 75e2457..169cb6b 100644 --- a/app/Services/ReviewService.php +++ b/app/Services/ReviewService.php @@ -5,7 +5,7 @@ use App\Models\AsinData; /** - * Review Service for Amazon product URL processing + * Review Service for Amazon product URL processing. * * This service handles the extraction of ASINs from Amazon URLs, * country detection, and database lookups for existing analyses. @@ -16,8 +16,10 @@ class ReviewService * Extract the 10-character ASIN from the Amazon product URL. * * @param string $url The Amazon product URL - * @return string The extracted ASIN + * * @throws \InvalidArgumentException If ASIN cannot be extracted + * + * @return string The extracted ASIN */ public function extractAsin(string $url): string { @@ -27,6 +29,7 @@ public function extractAsin(string $url): string if (preg_match('/\/product\/([A-Z0-9]{10})/', $url, $matches)) { return $matches[1]; } + throw new \InvalidArgumentException('Invalid Amazon product URL'); } @@ -34,12 +37,13 @@ public function extractAsin(string $url): string * Extract country code from Amazon URL based on domain. * * @param string $url The Amazon product URL + * * @return string Two-letter country code (defaults to 'us') */ public function extractCountryFromUrl(string $url): string { $host = parse_url($url, PHP_URL_HOST); - + $countryMap = [ 'amazon.com' => 'us', 'amazon.co.uk' => 'gb', @@ -76,8 +80,9 @@ public function extractCountryFromUrl(string $url): string /** * Find existing analysis in database by ASIN and country. * - * @param string $asin The Amazon Standard Identification Number + * @param string $asin The Amazon Standard Identification Number * @param string $country Two-letter country code + * * @return AsinData|null The existing analysis or null if not found */ public function findExistingAnalysis(string $asin, string $country): ?AsinData diff --git a/config/auth.php b/config/auth.php index 9548c15..b2850bc 100644 --- a/config/auth.php +++ b/config/auth.php @@ -14,7 +14,7 @@ */ 'defaults' => [ - 'guard' => 'web', + 'guard' => 'web', 'passwords' => 'users', ], @@ -37,7 +37,7 @@ 'guards' => [ 'web' => [ - 'driver' => 'session', + 'driver' => 'session', 'provider' => 'users', ], ], @@ -62,7 +62,7 @@ 'providers' => [ 'users' => [ 'driver' => 'eloquent', - 'model' => App\Models\User::class, + 'model' => App\Models\User::class, ], // 'users' => [ @@ -93,8 +93,8 @@ 'passwords' => [ 'users' => [ 'provider' => 'users', - 'table' => 'password_reset_tokens', - 'expire' => 60, + 'table' => 'password_reset_tokens', + 'expire' => 60, 'throttle' => 60, ], ], diff --git a/config/broadcasting.php b/config/broadcasting.php index 2410485..5a79816 100644 --- a/config/broadcasting.php +++ b/config/broadcasting.php @@ -31,17 +31,17 @@ 'connections' => [ 'pusher' => [ - 'driver' => 'pusher', - 'key' => env('PUSHER_APP_KEY'), - 'secret' => env('PUSHER_APP_SECRET'), - 'app_id' => env('PUSHER_APP_ID'), + 'driver' => 'pusher', + 'key' => env('PUSHER_APP_KEY'), + 'secret' => env('PUSHER_APP_SECRET'), + 'app_id' => env('PUSHER_APP_ID'), 'options' => [ - 'cluster' => env('PUSHER_APP_CLUSTER'), - 'host' => env('PUSHER_HOST') ?: 'api-'.env('PUSHER_APP_CLUSTER', 'mt1').'.pusher.com', - 'port' => env('PUSHER_PORT', 443), - 'scheme' => env('PUSHER_SCHEME', 'https'), + 'cluster' => env('PUSHER_APP_CLUSTER'), + 'host' => env('PUSHER_HOST') ?: 'api-'.env('PUSHER_APP_CLUSTER', 'mt1').'.pusher.com', + 'port' => env('PUSHER_PORT', 443), + 'scheme' => env('PUSHER_SCHEME', 'https'), 'encrypted' => true, - 'useTLS' => env('PUSHER_SCHEME', 'https') === 'https', + 'useTLS' => env('PUSHER_SCHEME', 'https') === 'https', ], 'client_options' => [ // Guzzle client options: https://docs.guzzlephp.org/en/stable/request-options.html @@ -50,11 +50,11 @@ 'ably' => [ 'driver' => 'ably', - 'key' => env('ABLY_KEY'), + 'key' => env('ABLY_KEY'), ], 'redis' => [ - 'driver' => 'redis', + 'driver' => 'redis', 'connection' => 'default', ], diff --git a/config/cache.php b/config/cache.php index d4171e2..f48ce85 100644 --- a/config/cache.php +++ b/config/cache.php @@ -38,27 +38,27 @@ ], 'array' => [ - 'driver' => 'array', + 'driver' => 'array', 'serialize' => false, ], 'database' => [ - 'driver' => 'database', - 'table' => 'cache', - 'connection' => null, + 'driver' => 'database', + 'table' => 'cache', + 'connection' => null, 'lock_connection' => null, ], 'file' => [ - 'driver' => 'file', - 'path' => storage_path('framework/cache/data'), + 'driver' => 'file', + 'path' => storage_path('framework/cache/data'), 'lock_path' => storage_path('framework/cache/data'), ], 'memcached' => [ - 'driver' => 'memcached', + 'driver' => 'memcached', 'persistent_id' => env('MEMCACHED_PERSISTENT_ID'), - 'sasl' => [ + 'sasl' => [ env('MEMCACHED_USERNAME'), env('MEMCACHED_PASSWORD'), ], @@ -67,25 +67,25 @@ ], 'servers' => [ [ - 'host' => env('MEMCACHED_HOST', '127.0.0.1'), - 'port' => env('MEMCACHED_PORT', 11211), + 'host' => env('MEMCACHED_HOST', '127.0.0.1'), + 'port' => env('MEMCACHED_PORT', 11211), 'weight' => 100, ], ], ], 'redis' => [ - 'driver' => 'redis', - 'connection' => 'cache', + 'driver' => 'redis', + 'connection' => 'cache', 'lock_connection' => 'default', ], 'dynamodb' => [ - 'driver' => 'dynamodb', - 'key' => env('AWS_ACCESS_KEY_ID'), - 'secret' => env('AWS_SECRET_ACCESS_KEY'), - 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), - 'table' => env('DYNAMODB_CACHE_TABLE', 'cache'), + 'driver' => 'dynamodb', + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + 'table' => env('DYNAMODB_CACHE_TABLE', 'cache'), 'endpoint' => env('DYNAMODB_ENDPOINT'), ], diff --git a/config/database.php b/config/database.php index 137ad18..b5e3797 100644 --- a/config/database.php +++ b/config/database.php @@ -36,58 +36,58 @@ 'connections' => [ 'sqlite' => [ - 'driver' => 'sqlite', - 'url' => env('DATABASE_URL'), - 'database' => env('DB_DATABASE', database_path('database.sqlite')), - 'prefix' => '', + 'driver' => 'sqlite', + 'url' => env('DATABASE_URL'), + 'database' => env('DB_DATABASE', database_path('database.sqlite')), + 'prefix' => '', 'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true), ], 'mysql' => [ - 'driver' => 'mysql', - 'url' => env('DATABASE_URL'), - 'host' => env('DB_HOST', '127.0.0.1'), - 'port' => env('DB_PORT', '3306'), - 'database' => env('DB_DATABASE', 'forge'), - 'username' => env('DB_USERNAME', 'forge'), - 'password' => env('DB_PASSWORD', ''), - 'unix_socket' => env('DB_SOCKET', ''), - 'charset' => 'utf8mb4', - 'collation' => 'utf8mb4_unicode_ci', - 'prefix' => '', + 'driver' => 'mysql', + 'url' => env('DATABASE_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '3306'), + 'database' => env('DB_DATABASE', 'forge'), + 'username' => env('DB_USERNAME', 'forge'), + 'password' => env('DB_PASSWORD', ''), + 'unix_socket' => env('DB_SOCKET', ''), + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'prefix' => '', 'prefix_indexes' => true, - 'strict' => true, - 'engine' => null, - 'options' => extension_loaded('pdo_mysql') ? array_filter([ + 'strict' => true, + 'engine' => null, + 'options' => extension_loaded('pdo_mysql') ? array_filter([ PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), ]) : [], ], 'pgsql' => [ - 'driver' => 'pgsql', - 'url' => env('DATABASE_URL'), - 'host' => env('DB_HOST', '127.0.0.1'), - 'port' => env('DB_PORT', '5432'), - 'database' => env('DB_DATABASE', 'forge'), - 'username' => env('DB_USERNAME', 'forge'), - 'password' => env('DB_PASSWORD', ''), - 'charset' => 'utf8', - 'prefix' => '', + 'driver' => 'pgsql', + 'url' => env('DATABASE_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '5432'), + 'database' => env('DB_DATABASE', 'forge'), + 'username' => env('DB_USERNAME', 'forge'), + 'password' => env('DB_PASSWORD', ''), + 'charset' => 'utf8', + 'prefix' => '', 'prefix_indexes' => true, - 'search_path' => 'public', - 'sslmode' => 'prefer', + 'search_path' => 'public', + 'sslmode' => 'prefer', ], 'sqlsrv' => [ - 'driver' => 'sqlsrv', - 'url' => env('DATABASE_URL'), - 'host' => env('DB_HOST', 'localhost'), - 'port' => env('DB_PORT', '1433'), - 'database' => env('DB_DATABASE', 'forge'), - 'username' => env('DB_USERNAME', 'forge'), - 'password' => env('DB_PASSWORD', ''), - 'charset' => 'utf8', - 'prefix' => '', + 'driver' => 'sqlsrv', + 'url' => env('DATABASE_URL'), + 'host' => env('DB_HOST', 'localhost'), + 'port' => env('DB_PORT', '1433'), + 'database' => env('DB_DATABASE', 'forge'), + 'username' => env('DB_USERNAME', 'forge'), + 'password' => env('DB_PASSWORD', ''), + 'charset' => 'utf8', + 'prefix' => '', 'prefix_indexes' => true, // 'encrypt' => env('DB_ENCRYPT', 'yes'), // 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'), @@ -125,24 +125,24 @@ 'options' => [ 'cluster' => env('REDIS_CLUSTER', 'redis'), - 'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_database_'), + 'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_database_'), ], 'default' => [ - 'url' => env('REDIS_URL'), - 'host' => env('REDIS_HOST', '127.0.0.1'), + 'url' => env('REDIS_URL'), + 'host' => env('REDIS_HOST', '127.0.0.1'), 'username' => env('REDIS_USERNAME'), 'password' => env('REDIS_PASSWORD'), - 'port' => env('REDIS_PORT', '6379'), + 'port' => env('REDIS_PORT', '6379'), 'database' => env('REDIS_DB', '0'), ], 'cache' => [ - 'url' => env('REDIS_URL'), - 'host' => env('REDIS_HOST', '127.0.0.1'), + 'url' => env('REDIS_URL'), + 'host' => env('REDIS_HOST', '127.0.0.1'), 'username' => env('REDIS_USERNAME'), 'password' => env('REDIS_PASSWORD'), - 'port' => env('REDIS_PORT', '6379'), + 'port' => env('REDIS_PORT', '6379'), 'database' => env('REDIS_CACHE_DB', '1'), ], diff --git a/config/filesystems.php b/config/filesystems.php index e9d9dbd..416c147 100644 --- a/config/filesystems.php +++ b/config/filesystems.php @@ -32,28 +32,28 @@ 'local' => [ 'driver' => 'local', - 'root' => storage_path('app'), - 'throw' => false, + 'root' => storage_path('app'), + 'throw' => false, ], 'public' => [ - 'driver' => 'local', - 'root' => storage_path('app/public'), - 'url' => env('APP_URL').'/storage', + 'driver' => 'local', + 'root' => storage_path('app/public'), + 'url' => env('APP_URL').'/storage', 'visibility' => 'public', - 'throw' => false, + 'throw' => false, ], 's3' => [ - 'driver' => 's3', - 'key' => env('AWS_ACCESS_KEY_ID'), - 'secret' => env('AWS_SECRET_ACCESS_KEY'), - 'region' => env('AWS_DEFAULT_REGION'), - 'bucket' => env('AWS_BUCKET'), - 'url' => env('AWS_URL'), - 'endpoint' => env('AWS_ENDPOINT'), + 'driver' => 's3', + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION'), + 'bucket' => env('AWS_BUCKET'), + 'url' => env('AWS_URL'), + 'endpoint' => env('AWS_ENDPOINT'), 'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false), - 'throw' => false, + 'throw' => false, ], ], diff --git a/config/hashing.php b/config/hashing.php index 0e8a0bb..7629086 100644 --- a/config/hashing.php +++ b/config/hashing.php @@ -45,10 +45,10 @@ */ 'argon' => [ - 'memory' => 65536, + 'memory' => 65536, 'threads' => 1, - 'time' => 4, - 'verify' => true, + 'time' => 4, + 'verify' => true, ], ]; diff --git a/config/logging.php b/config/logging.php index c44d276..7e528e2 100644 --- a/config/logging.php +++ b/config/logging.php @@ -33,7 +33,7 @@ 'deprecations' => [ 'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'), - 'trace' => false, + 'trace' => false, ], /* @@ -53,73 +53,73 @@ 'channels' => [ 'stack' => [ - 'driver' => 'stack', - 'channels' => ['single'], + 'driver' => 'stack', + 'channels' => ['single'], 'ignore_exceptions' => false, ], 'single' => [ - 'driver' => 'single', - 'path' => storage_path('logs/laravel.log'), - 'level' => env('LOG_LEVEL', 'debug'), + 'driver' => 'single', + 'path' => storage_path('logs/laravel.log'), + 'level' => env('LOG_LEVEL', 'debug'), 'replace_placeholders' => true, ], 'daily' => [ - 'driver' => 'daily', - 'path' => storage_path('logs/laravel.log'), - 'level' => env('LOG_LEVEL', 'debug'), - 'days' => 14, + 'driver' => 'daily', + 'path' => storage_path('logs/laravel.log'), + 'level' => env('LOG_LEVEL', 'debug'), + 'days' => 14, 'replace_placeholders' => true, ], 'slack' => [ - 'driver' => 'slack', - 'url' => env('LOG_SLACK_WEBHOOK_URL'), - 'username' => 'Laravel Log', - 'emoji' => ':boom:', - 'level' => env('LOG_LEVEL', 'critical'), + 'driver' => 'slack', + 'url' => env('LOG_SLACK_WEBHOOK_URL'), + 'username' => 'Laravel Log', + 'emoji' => ':boom:', + 'level' => env('LOG_LEVEL', 'critical'), 'replace_placeholders' => true, ], 'papertrail' => [ - 'driver' => 'monolog', - 'level' => env('LOG_LEVEL', 'debug'), - 'handler' => env('LOG_PAPERTRAIL_HANDLER', SyslogUdpHandler::class), + 'driver' => 'monolog', + 'level' => env('LOG_LEVEL', 'debug'), + 'handler' => env('LOG_PAPERTRAIL_HANDLER', SyslogUdpHandler::class), 'handler_with' => [ - 'host' => env('PAPERTRAIL_URL'), - 'port' => env('PAPERTRAIL_PORT'), + 'host' => env('PAPERTRAIL_URL'), + 'port' => env('PAPERTRAIL_PORT'), 'connectionString' => 'tls://'.env('PAPERTRAIL_URL').':'.env('PAPERTRAIL_PORT'), ], 'processors' => [PsrLogMessageProcessor::class], ], 'stderr' => [ - 'driver' => 'monolog', - 'level' => env('LOG_LEVEL', 'debug'), - 'handler' => StreamHandler::class, + 'driver' => 'monolog', + 'level' => env('LOG_LEVEL', 'debug'), + 'handler' => StreamHandler::class, 'formatter' => env('LOG_STDERR_FORMATTER'), - 'with' => [ + 'with' => [ 'stream' => 'php://stderr', ], 'processors' => [PsrLogMessageProcessor::class], ], 'syslog' => [ - 'driver' => 'syslog', - 'level' => env('LOG_LEVEL', 'debug'), - 'facility' => LOG_USER, + 'driver' => 'syslog', + 'level' => env('LOG_LEVEL', 'debug'), + 'facility' => LOG_USER, 'replace_placeholders' => true, ], 'errorlog' => [ - 'driver' => 'errorlog', - 'level' => env('LOG_LEVEL', 'debug'), + 'driver' => 'errorlog', + 'level' => env('LOG_LEVEL', 'debug'), 'replace_placeholders' => true, ], 'null' => [ - 'driver' => 'monolog', + 'driver' => 'monolog', 'handler' => NullHandler::class, ], diff --git a/config/mail.php b/config/mail.php index e894b2e..a17d4ea 100644 --- a/config/mail.php +++ b/config/mail.php @@ -35,14 +35,14 @@ 'mailers' => [ 'smtp' => [ - 'transport' => 'smtp', - 'url' => env('MAIL_URL'), - 'host' => env('MAIL_HOST', 'smtp.mailgun.org'), - 'port' => env('MAIL_PORT', 587), - 'encryption' => env('MAIL_ENCRYPTION', 'tls'), - 'username' => env('MAIL_USERNAME'), - 'password' => env('MAIL_PASSWORD'), - 'timeout' => null, + 'transport' => 'smtp', + 'url' => env('MAIL_URL'), + 'host' => env('MAIL_HOST', 'smtp.mailgun.org'), + 'port' => env('MAIL_PORT', 587), + 'encryption' => env('MAIL_ENCRYPTION', 'tls'), + 'username' => env('MAIL_USERNAME'), + 'password' => env('MAIL_PASSWORD'), + 'timeout' => null, 'local_domain' => env('MAIL_EHLO_DOMAIN'), ], @@ -67,12 +67,12 @@ 'sendmail' => [ 'transport' => 'sendmail', - 'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'), + 'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'), ], 'log' => [ 'transport' => 'log', - 'channel' => env('MAIL_LOG_CHANNEL'), + 'channel' => env('MAIL_LOG_CHANNEL'), ], 'array' => [ @@ -81,7 +81,7 @@ 'failover' => [ 'transport' => 'failover', - 'mailers' => [ + 'mailers' => [ 'smtp', 'log', ], @@ -89,7 +89,7 @@ 'roundrobin' => [ 'transport' => 'roundrobin', - 'mailers' => [ + 'mailers' => [ 'ses', 'postmark', ], @@ -109,7 +109,7 @@ 'from' => [ 'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'), - 'name' => env('MAIL_FROM_NAME', 'Example'), + 'name' => env('MAIL_FROM_NAME', 'Example'), ], /* diff --git a/config/queue.php b/config/queue.php index 01c6b05..1c01f21 100644 --- a/config/queue.php +++ b/config/queue.php @@ -35,39 +35,39 @@ ], 'database' => [ - 'driver' => 'database', - 'table' => 'jobs', - 'queue' => 'default', - 'retry_after' => 90, + 'driver' => 'database', + 'table' => 'jobs', + 'queue' => 'default', + 'retry_after' => 90, 'after_commit' => false, ], 'beanstalkd' => [ - 'driver' => 'beanstalkd', - 'host' => 'localhost', - 'queue' => 'default', - 'retry_after' => 90, - 'block_for' => 0, + 'driver' => 'beanstalkd', + 'host' => 'localhost', + 'queue' => 'default', + 'retry_after' => 90, + 'block_for' => 0, 'after_commit' => false, ], 'sqs' => [ - 'driver' => 'sqs', - 'key' => env('AWS_ACCESS_KEY_ID'), - 'secret' => env('AWS_SECRET_ACCESS_KEY'), - 'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'), - 'queue' => env('SQS_QUEUE', 'default'), - 'suffix' => env('SQS_SUFFIX'), - 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + 'driver' => 'sqs', + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'), + 'queue' => env('SQS_QUEUE', 'default'), + 'suffix' => env('SQS_SUFFIX'), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), 'after_commit' => false, ], 'redis' => [ - 'driver' => 'redis', - 'connection' => 'default', - 'queue' => env('REDIS_QUEUE', 'default'), - 'retry_after' => 90, - 'block_for' => null, + 'driver' => 'redis', + 'connection' => 'default', + 'queue' => env('REDIS_QUEUE', 'default'), + 'retry_after' => 90, + 'block_for' => null, 'after_commit' => false, ], @@ -86,7 +86,7 @@ 'batching' => [ 'database' => env('DB_CONNECTION', 'mysql'), - 'table' => 'job_batches', + 'table' => 'job_batches', ], /* @@ -101,9 +101,9 @@ */ 'failed' => [ - 'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'), + 'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'), 'database' => env('DB_CONNECTION', 'mysql'), - 'table' => 'failed_jobs', + 'table' => 'failed_jobs', ], ]; diff --git a/config/sanctum.php b/config/sanctum.php index 764a82f..4c7d412 100644 --- a/config/sanctum.php +++ b/config/sanctum.php @@ -76,8 +76,8 @@ 'middleware' => [ 'authenticate_session' => Laravel\Sanctum\Http\Middleware\AuthenticateSession::class, - 'encrypt_cookies' => Illuminate\Cookie\Middleware\EncryptCookies::class, - 'validate_csrf_token' => Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class, + 'encrypt_cookies' => Illuminate\Cookie\Middleware\EncryptCookies::class, + 'validate_csrf_token' => Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class, ], ]; diff --git a/config/services.php b/config/services.php index 20ab06a..f04e972 100644 --- a/config/services.php +++ b/config/services.php @@ -15,10 +15,10 @@ */ 'mailgun' => [ - 'domain' => env('MAILGUN_DOMAIN'), - 'secret' => env('MAILGUN_SECRET'), + 'domain' => env('MAILGUN_DOMAIN'), + 'secret' => env('MAILGUN_SECRET'), 'endpoint' => env('MAILGUN_ENDPOINT', 'api.mailgun.net'), - 'scheme' => 'https', + 'scheme' => 'https', ], 'postmark' => [ @@ -26,19 +26,19 @@ ], 'ses' => [ - 'key' => env('AWS_ACCESS_KEY_ID'), + 'key' => env('AWS_ACCESS_KEY_ID'), 'secret' => env('AWS_SECRET_ACCESS_KEY'), 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), ], 'rapidapi' => [ - 'key' => env('RAPIDAPI_KEY'), + 'key' => env('RAPIDAPI_KEY'), 'host' => env('RAPIDAPI_HOST', 'amazon23.p.rapidapi.com'), ], 'openai' => [ - 'api_key' => env('OPENAI_API_KEY'), - 'model' => env('OPENAI_MODEL', 'gpt-4'), + 'api_key' => env('OPENAI_API_KEY'), + 'model' => env('OPENAI_MODEL', 'gpt-4'), 'base_url' => env('OPENAI_BASE_URL', 'https://api.openai.com/v1'), ], diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php index 584104c..e0c9536 100644 --- a/database/factories/UserFactory.php +++ b/database/factories/UserFactory.php @@ -24,11 +24,11 @@ class UserFactory extends Factory public function definition(): array { return [ - 'name' => fake()->name(), - 'email' => fake()->unique()->safeEmail(), + 'name' => fake()->name(), + 'email' => fake()->unique()->safeEmail(), 'email_verified_at' => now(), - 'password' => static::$password ??= Hash::make('password'), - 'remember_token' => Str::random(10), + 'password' => static::$password ??= Hash::make('password'), + 'remember_token' => Str::random(10), ]; } diff --git a/database/migrations/2014_10_12_000000_create_users_table.php b/database/migrations/2014_10_12_000000_create_users_table.php index 444fafb..2fb647b 100644 --- a/database/migrations/2014_10_12_000000_create_users_table.php +++ b/database/migrations/2014_10_12_000000_create_users_table.php @@ -4,8 +4,7 @@ use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; -return new class extends Migration -{ +return new class() extends Migration { /** * Run the migrations. */ diff --git a/database/migrations/2014_10_12_100000_create_password_reset_tokens_table.php b/database/migrations/2014_10_12_100000_create_password_reset_tokens_table.php index 81a7229..e354188 100644 --- a/database/migrations/2014_10_12_100000_create_password_reset_tokens_table.php +++ b/database/migrations/2014_10_12_100000_create_password_reset_tokens_table.php @@ -4,8 +4,7 @@ use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; -return new class extends Migration -{ +return new class() extends Migration { /** * Run the migrations. */ diff --git a/database/migrations/2019_08_19_000000_create_failed_jobs_table.php b/database/migrations/2019_08_19_000000_create_failed_jobs_table.php index 249da81..6876828 100644 --- a/database/migrations/2019_08_19_000000_create_failed_jobs_table.php +++ b/database/migrations/2019_08_19_000000_create_failed_jobs_table.php @@ -4,8 +4,7 @@ use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; -return new class extends Migration -{ +return new class() extends Migration { /** * Run the migrations. */ diff --git a/database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php b/database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php index e828ad8..e637aaf 100644 --- a/database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php +++ b/database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php @@ -4,8 +4,7 @@ use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; -return new class extends Migration -{ +return new class() extends Migration { /** * Run the migrations. */ diff --git a/database/migrations/2025_05_27_085749_create_asin_reviews_table.php b/database/migrations/2025_05_27_085749_create_asin_reviews_table.php index e12c11c..dc35594 100644 --- a/database/migrations/2025_05_27_085749_create_asin_reviews_table.php +++ b/database/migrations/2025_05_27_085749_create_asin_reviews_table.php @@ -34,4 +34,4 @@ public function down(): void { Schema::dropIfExists('asin_reviews'); } -} \ No newline at end of file +} diff --git a/database/migrations/2025_05_27_180704_add_openai_result_column.php b/database/migrations/2025_05_27_180704_add_openai_result_column.php index 78958d6..f310b33 100644 --- a/database/migrations/2025_05_27_180704_add_openai_result_column.php +++ b/database/migrations/2025_05_27_180704_add_openai_result_column.php @@ -4,8 +4,7 @@ use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; -return new class extends Migration -{ +return new class() extends Migration { /** * Run the migrations. */ diff --git a/database/migrations/2025_05_29_181643_add_rating_columns_to_asin_reviews_table.php b/database/migrations/2025_05_29_181643_add_rating_columns_to_asin_reviews_table.php index 18a979e..fe86b0e 100644 --- a/database/migrations/2025_05_29_181643_add_rating_columns_to_asin_reviews_table.php +++ b/database/migrations/2025_05_29_181643_add_rating_columns_to_asin_reviews_table.php @@ -20,4 +20,4 @@ public function down() $table->dropColumn(['amazon_rating', 'adjusted_rating']); }); } -} \ No newline at end of file +} diff --git a/database/migrations/2025_05_29_184402_rename_asin_reviews_table_to_asin_data.php b/database/migrations/2025_05_29_184402_rename_asin_reviews_table_to_asin_data.php index e1ffb08..4631d70 100644 --- a/database/migrations/2025_05_29_184402_rename_asin_reviews_table_to_asin_data.php +++ b/database/migrations/2025_05_29_184402_rename_asin_reviews_table_to_asin_data.php @@ -1,11 +1,9 @@ decimal('adjusted_rating', 3, 2)->default(0); }); } -}; \ No newline at end of file +}; diff --git a/database/migrations/2025_05_30_183203_remove_analysis_from_asin_data_table.php b/database/migrations/2025_05_30_183203_remove_analysis_from_asin_data_table.php index d89185b..cb70dca 100644 --- a/database/migrations/2025_05_30_183203_remove_analysis_from_asin_data_table.php +++ b/database/migrations/2025_05_30_183203_remove_analysis_from_asin_data_table.php @@ -4,8 +4,7 @@ use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; -return new class extends Migration -{ +return new class() extends Migration { public function up() { Schema::table('asin_data', function (Blueprint $table) { @@ -19,4 +18,4 @@ public function down() $table->json('analysis')->nullable(); }); } -}; \ No newline at end of file +}; diff --git a/database/migrations/2025_06_09_190029_create_sessions_table.php b/database/migrations/2025_06_09_190029_create_sessions_table.php index f60625b..01e84f5 100644 --- a/database/migrations/2025_06_09_190029_create_sessions_table.php +++ b/database/migrations/2025_06_09_190029_create_sessions_table.php @@ -4,8 +4,7 @@ use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; -return new class extends Migration -{ +return new class() extends Migration { /** * Run the migrations. */ diff --git a/tests/Feature/ReviewAnalyzerLivewireTest.php b/tests/Feature/ReviewAnalyzerLivewireTest.php index 939eb17..cbeb7fc 100644 --- a/tests/Feature/ReviewAnalyzerLivewireTest.php +++ b/tests/Feature/ReviewAnalyzerLivewireTest.php @@ -2,14 +2,14 @@ namespace Tests\Feature; -use Tests\TestCase; -use Livewire\Livewire; use App\Livewire\ReviewAnalyzer; -use App\Services\ReviewAnalysisService; -use App\Services\CaptchaService; use App\Models\AsinData; +use App\Services\CaptchaService; +use App\Services\ReviewAnalysisService; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\App; +use Livewire\Livewire; +use Tests\TestCase; class ReviewAnalyzerLivewireTest extends TestCase { @@ -18,13 +18,13 @@ class ReviewAnalyzerLivewireTest extends TestCase protected function setUp(): void { parent::setUp(); - + // Set up test environment config([ - 'captcha.provider' => 'recaptcha', - 'captcha.recaptcha.site_key' => 'test_site_key', + 'captcha.provider' => 'recaptcha', + 'captcha.recaptcha.site_key' => 'test_site_key', 'captcha.recaptcha.secret_key' => 'test_secret_key', - 'captcha.recaptcha.verify_url' => 'https://www.google.com/recaptcha/api/siteverify' + 'captcha.recaptcha.verify_url' => 'https://www.google.com/recaptcha/api/siteverify', ]); } @@ -173,19 +173,19 @@ public function test_captcha_validation_success_in_production() { // Create existing analysis data $asinData = AsinData::create([ - 'asin' => 'B08N5WRWNW', - 'country' => 'us', + 'asin' => 'B08N5WRWNW', + 'country' => 'us', 'product_url' => 'https://www.amazon.com/dp/B08N5WRWNW', - 'reviews' => json_encode([ - ['id' => 0, 'rating' => 5, 'review_title' => 'Great!', 'review_text' => 'Great product', 'author' => 'John'] + 'reviews' => json_encode([ + ['id' => 0, 'rating' => 5, 'review_title' => 'Great!', 'review_text' => 'Great product', 'author' => 'John'], ]), - 'openai_result' => json_encode(['detailed_scores' => [0 => 25]]), + 'openai_result' => json_encode(['detailed_scores' => [0 => 25]]), 'fake_percentage' => 0.0, - 'amazon_rating' => 5.0, + 'amazon_rating' => 5.0, 'adjusted_rating' => 5.0, - 'grade' => 'A', - 'explanation' => 'Test explanation', - 'status' => 'completed' + 'grade' => 'A', + 'explanation' => 'Test explanation', + 'status' => 'completed', ]); // Mock CaptchaService - success @@ -199,23 +199,23 @@ public function test_captcha_validation_success_in_production() $mockAnalysisService = $this->createMock(ReviewAnalysisService::class); $mockAnalysisService->method('checkProductExists') ->willReturn([ - 'asin' => 'B08N5WRWNW', - 'country' => 'us', - 'product_url' => 'https://www.amazon.com/dp/B08N5WRWNW', - 'exists' => true, - 'asin_data' => $asinData, + 'asin' => 'B08N5WRWNW', + 'country' => 'us', + 'product_url' => 'https://www.amazon.com/dp/B08N5WRWNW', + 'exists' => true, + 'asin_data' => $asinData, 'needs_fetching' => false, - 'needs_openai' => false + 'needs_openai' => false, ]); $mockAnalysisService->method('calculateFinalMetrics') ->willReturn([ 'fake_percentage' => 0.0, - 'amazon_rating' => 5.0, + 'amazon_rating' => 5.0, 'adjusted_rating' => 5.0, - 'grade' => 'A', - 'explanation' => 'Test explanation', - 'asin_review' => $asinData + 'grade' => 'A', + 'explanation' => 'Test explanation', + 'asin_review' => $asinData, ]); App::instance(ReviewAnalysisService::class, $mockAnalysisService); @@ -278,7 +278,7 @@ public function test_grade_color_methods() $this->assertEquals('text-green-600', $gradeColor); $this->assertEquals('bg-green-100', $gradeBgColor); - // Test F grade + // Test F grade $component->set('grade', 'F'); $gradeColor = $component->instance()->getGradeColor(); $gradeBgColor = $component->instance()->getGradeBgColor(); @@ -322,8 +322,8 @@ public function test_hcaptcha_provider_handling() { // Configure for hCaptcha config([ - 'captcha.provider' => 'hcaptcha', - 'captcha.hcaptcha.site_key' => 'test_hcaptcha_key' + 'captcha.provider' => 'hcaptcha', + 'captcha.hcaptcha.site_key' => 'test_hcaptcha_key', ]); // Mock CaptchaService for hCaptcha @@ -370,4 +370,4 @@ public function test_reset_analysis_state() ->assertSet('progressPercentage', 0) ->assertSet('isAnalyzed', false); } -} \ No newline at end of file +} diff --git a/tests/Unit/AmazonFetchServiceTest.php b/tests/Unit/AmazonFetchServiceTest.php index 3040bc9..737e7f4 100644 --- a/tests/Unit/AmazonFetchServiceTest.php +++ b/tests/Unit/AmazonFetchServiceTest.php @@ -2,17 +2,16 @@ namespace Tests\Unit; -use Tests\TestCase; -use App\Services\Amazon\AmazonFetchService; use App\Models\AsinData; +use App\Services\Amazon\AmazonFetchService; use GuzzleHttp\Client; +use GuzzleHttp\Exception\RequestException; use GuzzleHttp\Handler\MockHandler; use GuzzleHttp\HandlerStack; -use GuzzleHttp\Psr7\Response; -use GuzzleHttp\Exception\RequestException; use GuzzleHttp\Psr7\Request; +use GuzzleHttp\Psr7\Response; use Illuminate\Foundation\Testing\RefreshDatabase; -use Illuminate\Support\Facades\Log; +use Tests\TestCase; class AmazonFetchServiceTest extends TestCase { @@ -24,19 +23,19 @@ class AmazonFetchServiceTest extends TestCase protected function setUp(): void { parent::setUp(); - + // Set up environment variables for testing config(['app.env' => 'testing']); putenv('UNWRANGLE_API_KEY=test_api_key'); putenv('UNWRANGLE_AMAZON_COOKIE=test_cookie'); - + $this->mockHandler = new MockHandler(); $handlerStack = HandlerStack::create($this->mockHandler); $mockClient = new Client(['handler' => $handlerStack]); - + // Create service instance and inject mock client $this->service = new AmazonFetchService(); - + // Use reflection to inject the mock client $reflection = new \ReflectionClass($this->service); $property = $reflection->getProperty('httpClient'); @@ -48,16 +47,16 @@ public function test_fetch_reviews_and_save_success() { // Mock Amazon validation (product exists) $this->mockHandler->append(new Response(200, [], 'Amazon product page')); - + // Mock Unwrangle API response $unwrangleResponse = [ - 'success' => true, + 'success' => true, 'total_results' => 2, - 'description' => 'Test Product Description', - 'reviews' => [ + 'description' => 'Test Product Description', + 'reviews' => [ ['rating' => 5, 'text' => 'Great product', 'author' => 'John'], - ['rating' => 4, 'text' => 'Good product', 'author' => 'Jane'] - ] + ['rating' => 4, 'text' => 'Good product', 'author' => 'Jane'], + ], ]; $this->mockHandler->append(new Response(200, [], json_encode($unwrangleResponse))); @@ -86,17 +85,17 @@ public function test_fetch_reviews_success() { // Mock Amazon validation (product exists) $this->mockHandler->append(new Response(200, [], 'Amazon product page')); - + // Mock Unwrangle API response $unwrangleResponse = [ - 'success' => true, + 'success' => true, 'total_results' => 3, - 'description' => 'Test Product', - 'reviews' => [ + 'description' => 'Test Product', + 'reviews' => [ ['rating' => 5, 'text' => 'Excellent'], ['rating' => 4, 'text' => 'Good'], - ['rating' => 3, 'text' => 'Average'] - ] + ['rating' => 3, 'text' => 'Average'], + ], ]; $this->mockHandler->append(new Response(200, [], json_encode($unwrangleResponse))); @@ -125,7 +124,7 @@ public function test_fetch_reviews_unwrangle_api_error() { // Mock Amazon validation (product exists) $this->mockHandler->append(new Response(200, [], 'Amazon product page')); - + // Mock Unwrangle API error response $this->mockHandler->append(new Response(500, [], 'Internal Server Error')); @@ -138,11 +137,11 @@ public function test_fetch_reviews_unwrangle_api_returns_error() { // Mock Amazon validation (product exists) $this->mockHandler->append(new Response(200, [], 'Amazon product page')); - + // Mock Unwrangle API response with error $unwrangleResponse = [ 'success' => false, - 'error' => 'API limit exceeded' + 'error' => 'API limit exceeded', ]; $this->mockHandler->append(new Response(200, [], json_encode($unwrangleResponse))); @@ -155,7 +154,7 @@ public function test_fetch_reviews_unwrangle_api_exception() { // Mock Amazon validation (product exists) $this->mockHandler->append(new Response(200, [], 'Amazon product page')); - + // Mock Unwrangle API exception $this->mockHandler->append(new RequestException( 'Connection timeout', @@ -234,7 +233,7 @@ public function test_fetch_reviews_with_empty_response() { // Mock Amazon validation (product exists) $this->mockHandler->append(new Response(200, [], 'Amazon product page')); - + // Mock Unwrangle API empty response $this->mockHandler->append(new Response(200, [], '')); @@ -247,7 +246,7 @@ public function test_fetch_reviews_with_invalid_json() { // Mock Amazon validation (product exists) $this->mockHandler->append(new Response(200, [], 'Amazon product page')); - + // Mock Unwrangle API invalid JSON response $this->mockHandler->append(new Response(200, [], 'invalid json')); @@ -260,13 +259,13 @@ public function test_fetch_reviews_uses_correct_api_parameters() { // Mock Amazon validation (product exists) $this->mockHandler->append(new Response(200, [], 'Amazon product page')); - + // Mock Unwrangle API response $unwrangleResponse = [ - 'success' => true, + 'success' => true, 'total_results' => 1, - 'description' => 'Test Product', - 'reviews' => [['rating' => 5, 'text' => 'Great']] + 'description' => 'Test Product', + 'reviews' => [['rating' => 5, 'text' => 'Great']], ]; $this->mockHandler->append(new Response(200, [], json_encode($unwrangleResponse))); @@ -278,7 +277,7 @@ public function test_fetch_reviews_uses_correct_api_parameters() $this->assertArrayHasKey('description', $result); $this->assertEquals('Test Product', $result['description']); $this->assertCount(1, $result['reviews']); - + // Verify both mock responses were consumed $this->assertCount(0, $this->mockHandler); } @@ -288,7 +287,7 @@ protected function tearDown(): void // Clean up environment variables putenv('UNWRANGLE_API_KEY'); putenv('UNWRANGLE_AMAZON_COOKIE'); - + parent::tearDown(); } -} \ No newline at end of file +} diff --git a/tests/Unit/AsinDataModelTest.php b/tests/Unit/AsinDataModelTest.php index 64be7a5..857be63 100644 --- a/tests/Unit/AsinDataModelTest.php +++ b/tests/Unit/AsinDataModelTest.php @@ -2,9 +2,9 @@ namespace Tests\Unit; -use Tests\TestCase; use App\Models\AsinData; use Illuminate\Foundation\Testing\RefreshDatabase; +use Tests\TestCase; class AsinDataModelTest extends TestCase { @@ -13,21 +13,21 @@ class AsinDataModelTest extends TestCase public function test_can_create_asin_data() { $asinData = AsinData::create([ - 'asin' => 'B08N5WRWNW', - 'country' => 'us', + 'asin' => 'B08N5WRWNW', + 'country' => 'us', 'product_description' => 'Test Product', - 'reviews' => [ + 'reviews' => [ ['rating' => 5, 'text' => 'Great product'], ['rating' => 4, 'text' => 'Good product'], - ['rating' => 1, 'text' => 'Bad product'] + ['rating' => 1, 'text' => 'Bad product'], ], 'openai_result' => [ 'detailed_scores' => [ 0 => 30, // genuine 1 => 45, // genuine - 2 => 85 // fake - ] - ] + 2 => 85, // fake + ], + ], ]); $this->assertInstanceOf(AsinData::class, $asinData); @@ -39,23 +39,23 @@ public function test_can_create_asin_data() public function test_fake_percentage_calculation() { $asinData = AsinData::create([ - 'asin' => 'B08N5WRWNW', - 'country' => 'us', + 'asin' => 'B08N5WRWNW', + 'country' => 'us', 'product_description' => 'Test Product', - 'reviews' => [ + 'reviews' => [ ['rating' => 5, 'text' => 'Great product'], ['rating' => 4, 'text' => 'Good product'], ['rating' => 1, 'text' => 'Bad product'], - ['rating' => 2, 'text' => 'Another bad product'] + ['rating' => 2, 'text' => 'Another bad product'], ], 'openai_result' => [ 'detailed_scores' => [ 0 => 30, // genuine 1 => 45, // genuine 2 => 85, // fake (>= 70) - 3 => 75 // fake (>= 70) - ] - ] + 3 => 75, // fake (>= 70) + ], + ], ]); // 2 fake out of 4 total = 50% @@ -65,11 +65,11 @@ public function test_fake_percentage_calculation() public function test_fake_percentage_returns_null_when_no_data() { $asinData = AsinData::create([ - 'asin' => 'B08N5WRWNW', - 'country' => 'us', + 'asin' => 'B08N5WRWNW', + 'country' => 'us', 'product_description' => 'Test Product', - 'reviews' => [], - 'openai_result' => null + 'reviews' => [], + 'openai_result' => null, ]); $this->assertNull($asinData->fake_percentage); @@ -79,12 +79,12 @@ public function test_grade_calculation() { // Test Grade A (< 10% fake) $asinData = AsinData::create([ - 'asin' => 'B08N5WRWNW', - 'country' => 'us', - 'reviews' => array_fill(0, 10, ['rating' => 5, 'text' => 'Great']), + 'asin' => 'B08N5WRWNW', + 'country' => 'us', + 'reviews' => array_fill(0, 10, ['rating' => 5, 'text' => 'Great']), 'openai_result' => [ - 'detailed_scores' => array_fill(0, 10, 30) // all genuine - ] + 'detailed_scores' => array_fill(0, 10, 30), // all genuine + ], ]); $this->assertEquals('A', $asinData->grade); @@ -94,8 +94,8 @@ public function test_grade_calculation() 'detailed_scores' => array_merge( array_fill(0, 9, 30), // 9 genuine [75] // 1 fake = 10% - ) - ] + ), + ], ]); $this->assertEquals('B', $asinData->fresh()->grade); @@ -105,8 +105,8 @@ public function test_grade_calculation() 'detailed_scores' => array_merge( array_fill(0, 8, 30), // 8 genuine array_fill(0, 2, 75) // 2 fake = 20% - ) - ] + ), + ], ]); $this->assertEquals('C', $asinData->fresh()->grade); @@ -116,16 +116,16 @@ public function test_grade_calculation() 'detailed_scores' => array_merge( array_fill(0, 7, 30), // 7 genuine array_fill(0, 3, 75) // 3 fake = 30% - ) - ] + ), + ], ]); $this->assertEquals('D', $asinData->fresh()->grade); // Test Grade F (>= 50% fake) $asinData->update([ 'openai_result' => [ - 'detailed_scores' => array_fill(0, 10, 75) // all fake = 100% - ] + 'detailed_scores' => array_fill(0, 10, 75), // all fake = 100% + ], ]); $this->assertEquals('F', $asinData->fresh()->grade); } @@ -133,15 +133,15 @@ public function test_grade_calculation() public function test_explanation_generation() { $asinData = AsinData::create([ - 'asin' => 'B08N5WRWNW', - 'country' => 'us', - 'reviews' => array_fill(0, 10, ['rating' => 5, 'text' => 'Great']), + 'asin' => 'B08N5WRWNW', + 'country' => 'us', + 'reviews' => array_fill(0, 10, ['rating' => 5, 'text' => 'Great']), 'openai_result' => [ 'detailed_scores' => array_merge( array_fill(0, 8, 30), // 8 genuine array_fill(0, 2, 75) // 2 fake = 20% - ) - ] + ), + ], ]); $explanation = $asinData->explanation; @@ -154,15 +154,15 @@ public function test_explanation_generation() public function test_amazon_rating_calculation() { $asinData = AsinData::create([ - 'asin' => 'B08N5WRWNW', + 'asin' => 'B08N5WRWNW', 'country' => 'us', 'reviews' => [ ['rating' => 5, 'text' => 'Great'], ['rating' => 4, 'text' => 'Good'], ['rating' => 3, 'text' => 'OK'], - ['rating' => 2, 'text' => 'Bad'] + ['rating' => 2, 'text' => 'Bad'], ], - 'openai_result' => [] + 'openai_result' => [], ]); // (5 + 4 + 3 + 2) / 4 = 3.5 @@ -172,22 +172,22 @@ public function test_amazon_rating_calculation() public function test_adjusted_rating_calculation() { $asinData = AsinData::create([ - 'asin' => 'B08N5WRWNW', + 'asin' => 'B08N5WRWNW', 'country' => 'us', 'reviews' => [ ['rating' => 5, 'text' => 'Great'], // genuine (score 30) ['rating' => 4, 'text' => 'Good'], // genuine (score 45) ['rating' => 1, 'text' => 'Bad'], // fake (score 85) - ['rating' => 1, 'text' => 'Awful'] // fake (score 90) + ['rating' => 1, 'text' => 'Awful'], // fake (score 90) ], 'openai_result' => [ 'results' => [ ['score' => 30], // genuine ['score' => 45], // genuine ['score' => 85], // fake - ['score' => 90] // fake - ] - ] + ['score' => 90], // fake + ], + ], ]); // Only genuine reviews: (5 + 4) / 2 = 4.5 @@ -198,13 +198,13 @@ public function test_get_reviews_array_method() { $reviews = [ ['rating' => 5, 'text' => 'Great'], - ['rating' => 4, 'text' => 'Good'] + ['rating' => 4, 'text' => 'Good'], ]; $asinData = AsinData::create([ - 'asin' => 'B08N5WRWNW', + 'asin' => 'B08N5WRWNW', 'country' => 'us', - 'reviews' => $reviews + 'reviews' => $reviews, ]); $this->assertEquals($reviews, $asinData->getReviewsArray()); @@ -214,13 +214,13 @@ public function test_get_reviews_array_handles_string_json() { $reviews = [ ['rating' => 5, 'text' => 'Great'], - ['rating' => 4, 'text' => 'Good'] + ['rating' => 4, 'text' => 'Good'], ]; $asinData = new AsinData([ - 'asin' => 'B08N5WRWNW', + 'asin' => 'B08N5WRWNW', 'country' => 'us', - 'reviews' => json_encode($reviews) // Store as JSON string + 'reviews' => json_encode($reviews), // Store as JSON string ]); $this->assertEquals($reviews, $asinData->getReviewsArray()); @@ -230,10 +230,10 @@ public function test_is_analyzed_method() { // Test with no OpenAI result $asinData = AsinData::create([ - 'asin' => 'B08N5WRWNW', - 'country' => 'us', - 'reviews' => [['rating' => 5, 'text' => 'Great']], - 'openai_result' => null + 'asin' => 'B08N5WRWNW', + 'country' => 'us', + 'reviews' => [['rating' => 5, 'text' => 'Great']], + 'openai_result' => null, ]); $this->assertFalse($asinData->isAnalyzed()); @@ -247,10 +247,10 @@ public function test_is_analyzed_method() // Test with OpenAI result but no reviews $asinDataNoReviews = AsinData::create([ - 'asin' => 'B08N5WRWN1', - 'country' => 'us', - 'reviews' => [], - 'openai_result' => ['detailed_scores' => []] + 'asin' => 'B08N5WRWN1', + 'country' => 'us', + 'reviews' => [], + 'openai_result' => ['detailed_scores' => []], ]); $this->assertFalse($asinDataNoReviews->isAnalyzed()); } @@ -258,28 +258,28 @@ public function test_is_analyzed_method() public function test_unique_constraint_on_asin_and_country() { AsinData::create([ - 'asin' => 'B08N5WRWNW', - 'country' => 'us' + 'asin' => 'B08N5WRWNW', + 'country' => 'us', ]); $this->expectException(\Illuminate\Database\QueryException::class); - + AsinData::create([ - 'asin' => 'B08N5WRWNW', - 'country' => 'us' + 'asin' => 'B08N5WRWNW', + 'country' => 'us', ]); } public function test_can_create_same_asin_different_country() { $asinData1 = AsinData::create([ - 'asin' => 'B08N5WRWNW', - 'country' => 'us' + 'asin' => 'B08N5WRWNW', + 'country' => 'us', ]); $asinData2 = AsinData::create([ - 'asin' => 'B08N5WRWNW', - 'country' => 'uk' + 'asin' => 'B08N5WRWNW', + 'country' => 'uk', ]); $this->assertNotEquals($asinData1->id, $asinData2->id); @@ -290,18 +290,18 @@ public function test_can_create_same_asin_different_country() public function test_fake_percentage_with_string_openai_result() { $asinData = AsinData::create([ - 'asin' => 'B08N5WRWNW', + 'asin' => 'B08N5WRWNW', 'country' => 'us', 'reviews' => [ ['rating' => 5, 'text' => 'Great'], - ['rating' => 1, 'text' => 'Bad'] + ['rating' => 1, 'text' => 'Bad'], ], 'openai_result' => json_encode([ 'detailed_scores' => [ 0 => 30, // genuine - 1 => 85 // fake - ] - ]) + 1 => 85, // fake + ], + ]), ]); // 1 fake out of 2 total = 50% @@ -311,10 +311,10 @@ public function test_fake_percentage_with_string_openai_result() public function test_fake_percentage_with_missing_detailed_scores() { $asinData = AsinData::create([ - 'asin' => 'B08N5WRWNW', - 'country' => 'us', - 'reviews' => [['rating' => 5, 'text' => 'Great']], - 'openai_result' => ['other_data' => 'value'] // No detailed_scores + 'asin' => 'B08N5WRWNW', + 'country' => 'us', + 'reviews' => [['rating' => 5, 'text' => 'Great']], + 'openai_result' => ['other_data' => 'value'], // No detailed_scores ]); $this->assertNull($asinData->fake_percentage); @@ -323,10 +323,10 @@ public function test_fake_percentage_with_missing_detailed_scores() public function test_grade_returns_null_when_no_fake_percentage() { $asinData = AsinData::create([ - 'asin' => 'B08N5WRWNW', - 'country' => 'us', - 'reviews' => [], - 'openai_result' => null + 'asin' => 'B08N5WRWNW', + 'country' => 'us', + 'reviews' => [], + 'openai_result' => null, ]); $this->assertNull($asinData->grade); @@ -335,10 +335,10 @@ public function test_grade_returns_null_when_no_fake_percentage() public function test_explanation_returns_null_when_no_data() { $asinData = AsinData::create([ - 'asin' => 'B08N5WRWNW', - 'country' => 'us', - 'reviews' => [], - 'openai_result' => null + 'asin' => 'B08N5WRWNW', + 'country' => 'us', + 'reviews' => [], + 'openai_result' => null, ]); $this->assertNull($asinData->explanation); @@ -348,12 +348,12 @@ public function test_explanation_different_fake_percentage_ranges() { // Test >= 50% fake (F grade) $asinData = AsinData::create([ - 'asin' => 'B08N5WRWNW', - 'country' => 'us', - 'reviews' => array_fill(0, 10, ['rating' => 5, 'text' => 'Great']), + 'asin' => 'B08N5WRWNW', + 'country' => 'us', + 'reviews' => array_fill(0, 10, ['rating' => 5, 'text' => 'Great']), 'openai_result' => [ - 'detailed_scores' => array_fill(0, 10, 75) // all fake = 100% - ] + 'detailed_scores' => array_fill(0, 10, 75), // all fake = 100% + ], ]); $this->assertStringContainsString('extremely high percentage', $asinData->explanation); $this->assertStringContainsString('Avoid purchasing', $asinData->explanation); @@ -364,8 +364,8 @@ public function test_explanation_different_fake_percentage_ranges() 'detailed_scores' => array_merge( array_fill(0, 6, 30), // 6 genuine array_fill(0, 4, 75) // 4 fake = 40% - ) - ] + ), + ], ]); $explanation = $asinData->fresh()->explanation; $this->assertStringContainsString('high percentage', $explanation); @@ -377,8 +377,8 @@ public function test_explanation_different_fake_percentage_ranges() 'detailed_scores' => array_merge( array_fill(0, 8, 30), // 8 genuine array_fill(0, 2, 75) // 2 fake = 20% - ) - ] + ), + ], ]); $explanation = $asinData->fresh()->explanation; $this->assertStringContainsString('moderate fake review activity', $explanation); @@ -390,8 +390,8 @@ public function test_explanation_different_fake_percentage_ranges() 'detailed_scores' => array_merge( array_fill(0, 9, 30), // 9 genuine [75] // 1 fake = 10% - ) - ] + ), + ], ]); $explanation = $asinData->fresh()->explanation; $this->assertStringContainsString('some fake review activity', $explanation); @@ -403,8 +403,8 @@ public function test_explanation_different_fake_percentage_ranges() 'detailed_scores' => array_merge( array_fill(0, 10, 30), // 10 genuine [] // 0 fake = 0% - ) - ] + ), + ], ]); $explanation = $asinData->fresh()->explanation; $this->assertStringContainsString('genuine reviews', $explanation); @@ -414,9 +414,9 @@ public function test_explanation_different_fake_percentage_ranges() public function test_amazon_rating_with_empty_reviews() { $asinData = AsinData::create([ - 'asin' => 'B08N5WRWNW', + 'asin' => 'B08N5WRWNW', 'country' => 'us', - 'reviews' => [] + 'reviews' => [], ]); $this->assertEquals(0, $asinData->amazon_rating); @@ -425,13 +425,13 @@ public function test_amazon_rating_with_empty_reviews() public function test_adjusted_rating_with_no_openai_results() { $asinData = AsinData::create([ - 'asin' => 'B08N5WRWNW', + 'asin' => 'B08N5WRWNW', 'country' => 'us', 'reviews' => [ ['rating' => 5, 'text' => 'Great'], - ['rating' => 3, 'text' => 'OK'] + ['rating' => 3, 'text' => 'OK'], ], - 'openai_result' => null + 'openai_result' => null, ]); // Should fall back to amazon_rating @@ -441,13 +441,13 @@ public function test_adjusted_rating_with_no_openai_results() public function test_adjusted_rating_with_missing_results_key() { $asinData = AsinData::create([ - 'asin' => 'B08N5WRWNW', + 'asin' => 'B08N5WRWNW', 'country' => 'us', 'reviews' => [ ['rating' => 5, 'text' => 'Great'], - ['rating' => 3, 'text' => 'OK'] + ['rating' => 3, 'text' => 'OK'], ], - 'openai_result' => ['other_data' => 'value'] // No 'results' key + 'openai_result' => ['other_data' => 'value'], // No 'results' key ]); // Should fall back to amazon_rating @@ -457,18 +457,18 @@ public function test_adjusted_rating_with_missing_results_key() public function test_adjusted_rating_with_all_fake_reviews() { $asinData = AsinData::create([ - 'asin' => 'B08N5WRWNW', + 'asin' => 'B08N5WRWNW', 'country' => 'us', 'reviews' => [ ['rating' => 5, 'text' => 'Great'], - ['rating' => 5, 'text' => 'Amazing'] + ['rating' => 5, 'text' => 'Amazing'], ], 'openai_result' => [ 'results' => [ ['score' => 85], // fake - ['score' => 90] // fake - ] - ] + ['score' => 90], // fake + ], + ], ]); // All reviews are fake, should fall back to amazon_rating @@ -478,18 +478,18 @@ public function test_adjusted_rating_with_all_fake_reviews() public function test_adjusted_rating_with_string_openai_result() { $asinData = AsinData::create([ - 'asin' => 'B08N5WRWNW', + 'asin' => 'B08N5WRWNW', 'country' => 'us', 'reviews' => [ ['rating' => 5, 'text' => 'Great'], - ['rating' => 1, 'text' => 'Bad'] + ['rating' => 1, 'text' => 'Bad'], ], 'openai_result' => json_encode([ 'results' => [ ['score' => 30], // genuine - ['score' => 85] // fake - ] - ]) + ['score' => 85], // fake + ], + ]), ]); // Only genuine review: 5/1 = 5.0 @@ -499,9 +499,9 @@ public function test_adjusted_rating_with_string_openai_result() public function test_get_reviews_array_with_invalid_json() { $asinData = new AsinData([ - 'asin' => 'B08N5WRWNW', + 'asin' => 'B08N5WRWNW', 'country' => 'us', - 'reviews' => 'invalid json string' + 'reviews' => 'invalid json string', ]); $this->assertEquals([], $asinData->getReviewsArray()); @@ -510,9 +510,9 @@ public function test_get_reviews_array_with_invalid_json() public function test_get_reviews_array_with_null_reviews() { $asinData = new AsinData([ - 'asin' => 'B08N5WRWNW', + 'asin' => 'B08N5WRWNW', 'country' => 'us', - 'reviews' => null + 'reviews' => null, ]); $this->assertEquals([], $asinData->getReviewsArray()); @@ -521,10 +521,10 @@ public function test_get_reviews_array_with_null_reviews() public function test_is_analyzed_with_string_openai_result() { $asinData = AsinData::create([ - 'asin' => 'B08N5WRWNW', - 'country' => 'us', - 'reviews' => [['rating' => 5, 'text' => 'Great']], - 'openai_result' => json_encode(['detailed_scores' => [0 => 25]]) + 'asin' => 'B08N5WRWNW', + 'country' => 'us', + 'reviews' => [['rating' => 5, 'text' => 'Great']], + 'openai_result' => json_encode(['detailed_scores' => [0 => 25]]), ]); $this->assertTrue($asinData->isAnalyzed()); @@ -533,10 +533,10 @@ public function test_is_analyzed_with_string_openai_result() public function test_is_analyzed_with_invalid_json_openai_result() { $asinData = new AsinData([ - 'asin' => 'B08N5WRWNW', - 'country' => 'us', - 'reviews' => [['rating' => 5, 'text' => 'Great']], - 'openai_result' => 'invalid json' + 'asin' => 'B08N5WRWNW', + 'country' => 'us', + 'reviews' => [['rating' => 5, 'text' => 'Great']], + 'openai_result' => 'invalid json', ]); $this->assertFalse($asinData->isAnalyzed()); @@ -545,10 +545,10 @@ public function test_is_analyzed_with_invalid_json_openai_result() public function test_model_casts() { $asinData = AsinData::create([ - 'asin' => 'B08N5WRWNW', - 'country' => 'us', - 'reviews' => [['rating' => 5, 'text' => 'Great']], - 'openai_result' => ['detailed_scores' => [0 => 25]] + 'asin' => 'B08N5WRWNW', + 'country' => 'us', + 'reviews' => [['rating' => 5, 'text' => 'Great']], + 'openai_result' => ['detailed_scores' => [0 => 25]], ]); // Test that arrays are properly cast @@ -559,13 +559,13 @@ public function test_model_casts() public function test_fillable_attributes() { $fillable = (new AsinData())->getFillable(); - + $expectedFillable = [ 'asin', 'country', 'product_description', 'reviews', - 'openai_result' + 'openai_result', ]; $this->assertEquals($expectedFillable, $fillable); @@ -576,4 +576,4 @@ public function test_table_name() $asinData = new AsinData(); $this->assertEquals('asin_data', $asinData->getTable()); } -} \ No newline at end of file +} diff --git a/tests/Unit/LoggingServiceTest.php b/tests/Unit/LoggingServiceTest.php index 46d57e2..09c6652 100644 --- a/tests/Unit/LoggingServiceTest.php +++ b/tests/Unit/LoggingServiceTest.php @@ -2,16 +2,16 @@ namespace Tests\Unit; -use Tests\TestCase; use App\Services\LoggingService; use Illuminate\Support\Facades\Log; +use Tests\TestCase; class LoggingServiceTest extends TestCase { protected function setUp(): void { parent::setUp(); - + // Clear any previous log expectations Log::spy(); } @@ -56,11 +56,11 @@ public function test_log_with_context() public function test_handle_exception_timeout_error() { $exception = new \Exception('cURL error 28: Operation timed out after 30 seconds'); - + $result = LoggingService::handleException($exception); $this->assertEquals('The request took too long to complete. Please try again.', $result); - + Log::shouldHaveReceived('error') ->once() ->with('cURL error 28: Operation timed out after 30 seconds', \Mockery::type('array')); @@ -69,84 +69,84 @@ public function test_handle_exception_timeout_error() public function test_handle_exception_product_not_found() { $exception = new \Exception('Product does not exist on Amazon.com (US) site'); - + $result = LoggingService::handleException($exception); $this->assertEquals('Product does not exist on Amazon.com (US) site. Please check the URL and try again.', $result); - + Log::shouldHaveReceived('error')->once(); } public function test_handle_exception_data_type_error() { $exception = new \Exception('count(): Argument #1 ($value) must be of type Countable|array'); - + $result = LoggingService::handleException($exception); $this->assertEquals('Data processing error occurred. Please try again.', $result); - + Log::shouldHaveReceived('error')->once(); } public function test_handle_exception_fetching_failed() { $exception = new \Exception('Failed to fetch reviews from Amazon'); - + $result = LoggingService::handleException($exception); $this->assertEquals('Unable to fetch reviews at this time. Please try again later.', $result); - + Log::shouldHaveReceived('error')->once(); } public function test_handle_exception_openai_error() { $exception = new \Exception('OpenAI API request failed with status 500'); - + $result = LoggingService::handleException($exception); $this->assertEquals('Analysis service is temporarily unavailable. Please try again later.', $result); - + Log::shouldHaveReceived('error')->once(); } public function test_handle_exception_invalid_url() { $exception = new \Exception('Could not extract ASIN from URL: invalid-url'); - + $result = LoggingService::handleException($exception); $this->assertEquals('Please provide a valid Amazon product URL.', $result); - + Log::shouldHaveReceived('error')->once(); } public function test_handle_exception_redirect_failed() { $exception = new \Exception('Failed to follow redirect: connection timeout'); - + $result = LoggingService::handleException($exception); $this->assertEquals('Unable to resolve the shortened URL. Please try using the full Amazon product URL instead.', $result); - + Log::shouldHaveReceived('error')->once(); } public function test_handle_exception_generic_error() { $exception = new \Exception('Some unknown error occurred'); - + $result = LoggingService::handleException($exception); $this->assertEquals('An unexpected error occurred. Please try again later.', $result); - + Log::shouldHaveReceived('error')->once(); } public function test_handle_exception_logs_trace() { $exception = new \Exception('Test exception'); - + LoggingService::handleException($exception); Log::shouldHaveReceived('error') @@ -184,7 +184,7 @@ public function test_error_types_constants() public function test_error_types_array_structure() { $errorTypes = LoggingService::ERROR_TYPES; - + $this->assertIsArray($errorTypes); $this->assertArrayHasKey('TIMEOUT', $errorTypes); $this->assertArrayHasKey('PRODUCT_NOT_FOUND', $errorTypes); @@ -193,7 +193,7 @@ public function test_error_types_array_structure() $this->assertArrayHasKey('OPENAI_ERROR', $errorTypes); $this->assertArrayHasKey('INVALID_URL', $errorTypes); $this->assertArrayHasKey('REDIRECT_FAILED', $errorTypes); - + // Check structure of each error type foreach ($errorTypes as $type) { $this->assertArrayHasKey('patterns', $type); @@ -207,7 +207,7 @@ public function test_multiple_pattern_matching() { // Test that the first matching pattern is used $exception = new \Exception('Operation timed out and cURL error 28 occurred'); - + $result = LoggingService::handleException($exception); $this->assertEquals('The request took too long to complete. Please try again.', $result); @@ -217,10 +217,10 @@ public function test_case_sensitive_pattern_matching() { // Test that pattern matching is case sensitive $exception = new \Exception('CURL ERROR 28: timeout'); - + $result = LoggingService::handleException($exception); // Should not match the timeout pattern (case sensitive) $this->assertEquals('An unexpected error occurred. Please try again later.', $result); } -} \ No newline at end of file +} diff --git a/tests/Unit/OpenAIServiceTest.php b/tests/Unit/OpenAIServiceTest.php index e580e61..2095a4f 100644 --- a/tests/Unit/OpenAIServiceTest.php +++ b/tests/Unit/OpenAIServiceTest.php @@ -2,9 +2,9 @@ namespace Tests\Unit; -use Tests\TestCase; use App\Services\OpenAIService; use Illuminate\Support\Facades\Http; +use Tests\TestCase; class OpenAIServiceTest extends TestCase { @@ -13,14 +13,14 @@ class OpenAIServiceTest extends TestCase protected function setUp(): void { parent::setUp(); - + // Set up environment variables for testing config([ - 'services.openai.api_key' => 'test_api_key', - 'services.openai.model' => 'gpt-4', - 'services.openai.base_url' => 'https://api.openai.com/v1' + 'services.openai.api_key' => 'test_api_key', + 'services.openai.model' => 'gpt-4', + 'services.openai.base_url' => 'https://api.openai.com/v1', ]); - + // Create service instance $this->service = new OpenAIService(); } @@ -29,7 +29,7 @@ public function test_analyze_reviews_success() { $reviews = [ ['id' => 0, 'rating' => 5, 'review_title' => 'Great!', 'review_text' => 'Great product, highly recommend!', 'author' => 'John'], - ['id' => 1, 'rating' => 1, 'review_title' => 'Bad', 'review_text' => 'Terrible fake product, avoid at all costs!', 'author' => 'Jane'] + ['id' => 1, 'rating' => 1, 'review_title' => 'Bad', 'review_text' => 'Terrible fake product, avoid at all costs!', 'author' => 'Jane'], ]; // Mock OpenAI API response @@ -37,14 +37,14 @@ public function test_analyze_reviews_success() 'choices' => [ [ 'message' => [ - 'content' => '[{"id":"0","score":25},{"id":"1","score":85}]' - ] - ] - ] + 'content' => '[{"id":"0","score":25},{"id":"1","score":85}]', + ], + ], + ], ]; Http::fake([ - 'api.openai.com/*' => Http::response($openaiResponse, 200) + 'api.openai.com/*' => Http::response($openaiResponse, 200), ]); $result = $this->service->analyzeReviews($reviews); @@ -52,7 +52,7 @@ public function test_analyze_reviews_success() $this->assertIsArray($result); $this->assertArrayHasKey('detailed_scores', $result); $this->assertCount(2, $result['detailed_scores']); - + // Check specific scores $this->assertEquals(25, $result['detailed_scores'][0]); $this->assertEquals(85, $result['detailed_scores'][1]); @@ -70,16 +70,16 @@ public function test_analyze_reviews_empty_array() public function test_analyze_reviews_openai_api_error() { $reviews = [ - ['id' => 0, 'rating' => 5, 'review_title' => 'Great', 'review_text' => 'Great product', 'author' => 'John'] + ['id' => 0, 'rating' => 5, 'review_title' => 'Great', 'review_text' => 'Great product', 'author' => 'John'], ]; Http::fake([ 'api.openai.com/*' => Http::response([ 'error' => [ 'message' => 'Internal server error', - 'type' => 'server_error' - ] - ], 500) + 'type' => 'server_error', + ], + ], 500), ]); $this->expectException(\Exception::class); @@ -91,7 +91,7 @@ public function test_analyze_reviews_openai_api_error() public function test_analyze_reviews_invalid_json_response() { $reviews = [ - ['id' => 0, 'rating' => 5, 'review_title' => 'Great', 'review_text' => 'Great product', 'author' => 'John'] + ['id' => 0, 'rating' => 5, 'review_title' => 'Great', 'review_text' => 'Great product', 'author' => 'John'], ]; // Mock OpenAI API with invalid JSON in content @@ -99,14 +99,14 @@ public function test_analyze_reviews_invalid_json_response() 'choices' => [ [ 'message' => [ - 'content' => 'invalid json content' - ] - ] - ] + 'content' => 'invalid json content', + ], + ], + ], ]; Http::fake([ - 'api.openai.com/*' => Http::response($openaiResponse, 200) + 'api.openai.com/*' => Http::response($openaiResponse, 200), ]); $result = $this->service->analyzeReviews($reviews); @@ -120,18 +120,18 @@ public function test_analyze_reviews_invalid_json_response() public function test_analyze_reviews_missing_choices() { $reviews = [ - ['id' => 0, 'rating' => 5, 'review_title' => 'Great', 'review_text' => 'Great product', 'author' => 'John'] + ['id' => 0, 'rating' => 5, 'review_title' => 'Great', 'review_text' => 'Great product', 'author' => 'John'], ]; // Mock OpenAI API response without choices $openaiResponse = [ 'usage' => [ - 'total_tokens' => 100 - ] + 'total_tokens' => 100, + ], ]; Http::fake([ - 'api.openai.com/*' => Http::response($openaiResponse, 200) + 'api.openai.com/*' => Http::response($openaiResponse, 200), ]); $result = $this->service->analyzeReviews($reviews); @@ -145,16 +145,16 @@ public function test_analyze_reviews_missing_choices() public function test_analyze_reviews_rate_limit_error() { $reviews = [ - ['id' => 0, 'rating' => 5, 'review_title' => 'Great', 'review_text' => 'Great product', 'author' => 'John'] + ['id' => 0, 'rating' => 5, 'review_title' => 'Great', 'review_text' => 'Great product', 'author' => 'John'], ]; Http::fake([ 'api.openai.com/*' => Http::response([ 'error' => [ 'message' => 'Rate limit exceeded', - 'type' => 'rate_limit_error' - ] - ], 429) + 'type' => 'rate_limit_error', + ], + ], 429), ]); $this->expectException(\Exception::class); @@ -166,16 +166,16 @@ public function test_analyze_reviews_rate_limit_error() public function test_analyze_reviews_authentication_error() { $reviews = [ - ['id' => 0, 'rating' => 5, 'review_title' => 'Great', 'review_text' => 'Great product', 'author' => 'John'] + ['id' => 0, 'rating' => 5, 'review_title' => 'Great', 'review_text' => 'Great product', 'author' => 'John'], ]; Http::fake([ 'api.openai.com/*' => Http::response([ 'error' => [ 'message' => 'Invalid API key', - 'type' => 'authentication_error' - ] - ], 401) + 'type' => 'authentication_error', + ], + ], 401), ]); $this->expectException(\Exception::class); @@ -188,7 +188,7 @@ public function test_analyze_reviews_with_special_characters() { $reviews = [ ['id' => 0, 'rating' => 5, 'review_title' => 'Great!', 'review_text' => 'Great product! 😊 100% recommend', 'author' => 'John'], - ['id' => 1, 'rating' => 1, 'review_title' => 'Bad', 'review_text' => 'Terrible... "worst" purchase ever!!!', 'author' => 'Jane'] + ['id' => 1, 'rating' => 1, 'review_title' => 'Bad', 'review_text' => 'Terrible... "worst" purchase ever!!!', 'author' => 'Jane'], ]; // Mock OpenAI API response @@ -196,14 +196,14 @@ public function test_analyze_reviews_with_special_characters() 'choices' => [ [ 'message' => [ - 'content' => '[{"id":"0","score":20},{"id":"1","score":75}]' - ] - ] - ] + 'content' => '[{"id":"0","score":20},{"id":"1","score":75}]', + ], + ], + ], ]; Http::fake([ - 'api.openai.com/*' => Http::response($openaiResponse, 200) + 'api.openai.com/*' => Http::response($openaiResponse, 200), ]); $result = $this->service->analyzeReviews($reviews); @@ -229,4 +229,4 @@ protected function tearDown(): void Http::fake(); // Reset HTTP fakes parent::tearDown(); } -} \ No newline at end of file +} From 83781b93f5f64ef1938387eb7cc0dd63d27a0a89 Mon Sep 17 00:00:00 2001 From: SDH Date: Tue, 10 Jun 2025 09:33:21 -0400 Subject: [PATCH 02/24] Updated readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e29bb82..5b4bc4d 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ A Laravel application that analyzes Amazon product reviews to detect fake reviews using AI. The service fetches reviews via the Unwrangle API, analyzes them with OpenAI, and provides authenticity scores. -Visit [nullfake.com](https://nullfake.com) to try it out. +# Visit [nullfake.com](https://nullfake.com) to try it out. ## How It Works From 533faa730c9cad6423dc5a43c9176eae416a9818 Mon Sep 17 00:00:00 2001 From: SDH Date: Tue, 10 Jun 2025 09:41:38 -0400 Subject: [PATCH 03/24] Footer github link --- resources/views/home.blade.php | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/resources/views/home.blade.php b/resources/views/home.blade.php index 6a05109..40a1f9f 100644 --- a/resources/views/home.blade.php +++ b/resources/views/home.blade.php @@ -106,13 +106,24 @@ function gtag(){dataLayer.push(arguments);} @livewireScripts From bfd32e1e13105c6350bbcc09f1e1b3e22cb9f37e Mon Sep 17 00:00:00 2001 From: SDH Date: Tue, 10 Jun 2025 10:52:29 -0400 Subject: [PATCH 04/24] Added privacy policy, updated footer links --- resources/views/home.blade.php | 9 +- resources/views/privacy.blade.php | 132 ++++++++++++++++++++++++++++++ routes/web.php | 4 + 3 files changed, 143 insertions(+), 2 deletions(-) create mode 100644 resources/views/privacy.blade.php diff --git a/resources/views/home.blade.php b/resources/views/home.blade.php index 40a1f9f..a748e49 100644 --- a/resources/views/home.blade.php +++ b/resources/views/home.blade.php @@ -106,7 +106,7 @@ function gtag(){dataLayer.push(arguments);} @livewireScripts diff --git a/resources/views/privacy.blade.php b/resources/views/privacy.blade.php new file mode 100644 index 0000000..6eda961 --- /dev/null +++ b/resources/views/privacy.blade.php @@ -0,0 +1,132 @@ + + + + + + Privacy Policy - Null Fake + + + + + +
+ +
+ +
+ + +
+
+

Privacy Policy

+ +

Last updated: December 2024

+ +
+ +

Information We Collect

+

When you use Null Fake, we collect the following information:

+
    +
  • Product URLs: Amazon product URLs you submit for analysis
  • +
  • Analysis Results: Review data and authenticity scores generated by our AI analysis
  • +
  • Usage Data: Basic analytics about how our service is used (no personal identification)
  • +
  • IP Address: For security and abuse prevention through captcha verification
  • +
+ +

How We Use Your Information

+

We use the collected information to:

+
    +
  • Provide Amazon review authenticity analysis
  • +
  • Cache results for faster future lookups (30 days)
  • +
  • Prevent abuse and spam through captcha verification
  • +
  • Improve our AI analysis algorithms
  • +
  • Monitor service performance and reliability
  • +
+ +

Data Storage and Retention

+

+ Analysis results are stored in our database for 30 days to provide instant results for repeat requests. + After 30 days, cached analysis data may be refreshed with new data. We do not store personal + information that could identify individual users. +

+ +

Third-Party Services

+

Null Fake uses the following third-party services:

+
    +
  • OpenAI: For AI-powered review analysis (subject to OpenAI's privacy policy)
  • +
  • Unwrangle API: For fetching Amazon review data (subject to their privacy policy)
  • +
  • Captcha Services: reCAPTCHA or hCaptcha for spam prevention
  • +
+ +

Data Sharing

+

+ We do not sell, trade, or share your data with third parties except as necessary to provide + our service (e.g., sending review data to OpenAI for analysis). All data sharing is done + in accordance with the privacy policies of our service providers. +

+ +

Security

+

+ We implement appropriate security measures to protect your data. However, no internet + transmission is 100% secure. We cannot guarantee absolute security of data transmitted + to our service. +

+ +

Your Rights

+

You have the right to:

+
    +
  • Request information about data we have collected
  • +
  • Request deletion of your analysis data
  • +
  • Contact us with privacy concerns
  • +
+ +

Open Source

+

+ Null Fake is open source software released under the MIT License. You can review our + code and data handling practices on + GitHub. +

+ +

Changes to This Policy

+

+ We may update this privacy policy from time to time. Any changes will be posted on this page + with an updated "Last updated" date. +

+ +

Contact Us

+

+ If you have questions about this privacy policy, please contact us through our + GitHub repository. +

+ +
+
+
+ + + + +
+ + + \ No newline at end of file diff --git a/routes/web.php b/routes/web.php index 33e18b9..f00907b 100644 --- a/routes/web.php +++ b/routes/web.php @@ -16,3 +16,7 @@ Route::get('/', function () { return view('home'); })->name('home'); + +Route::get('/privacy', function () { + return view('privacy'); +}); From bfa596ca4f5af9e9d49b791af83f7f3157393613 Mon Sep 17 00:00:00 2001 From: SDH Date: Tue, 10 Jun 2025 14:38:09 -0400 Subject: [PATCH 05/24] Speed improvements, exception handling for unwrangle improvements --- app/Services/Amazon/AmazonFetchService.php | 130 +++++++---- app/Services/OpenAIService.php | 174 +++++++++++--- app/Services/ReviewAnalysisService.php | 56 ++--- config/services.php | 7 +- tests/Feature/PerformanceOptimizationTest.php | 218 ++++++++++++++++++ tests/Unit/AmazonFetchServiceTest.php | 22 +- 6 files changed, 485 insertions(+), 122 deletions(-) create mode 100644 tests/Feature/PerformanceOptimizationTest.php diff --git a/app/Services/Amazon/AmazonFetchService.php b/app/Services/Amazon/AmazonFetchService.php index 29f020b..c154b9c 100644 --- a/app/Services/Amazon/AmazonFetchService.php +++ b/app/Services/Amazon/AmazonFetchService.php @@ -20,8 +20,9 @@ class AmazonFetchService public function __construct() { $this->httpClient = new Client([ - 'timeout' => 30, + 'timeout' => 20, 'http_errors' => false, + 'connect_timeout' => 5, ]); } @@ -38,12 +39,17 @@ public function __construct() */ public function fetchReviewsAndSave(string $asin, string $country, string $productUrl): AsinData { - // Fetch reviews from Amazon - $reviewsData = $this->fetchReviews($asin, $country); + // Fetch reviews from Amazon with optimized performance + $reviewsData = $this->fetchReviewsOptimized($asin, $country); - // Check if fetching failed (empty reviews data means validation failed) + // Check if fetching failed and provide specific error message if (empty($reviewsData) || !isset($reviewsData['reviews'])) { - throw new \Exception('Product does not exist on Amazon.com (US) site. Please check the URL and try again.'); + // Check if the error was due to ASIN validation failure vs API issues + if (!$this->validateAsinExistsFast($asin)) { + throw new \Exception('Product does not exist on Amazon.com (US) site. Please check the URL and try again.'); + } else { + throw new \Exception('Unable to fetch product reviews at this time. This could be due to network issues or the review service being temporarily unavailable. Please try again in a few moments.'); + } } // Save to database - NO OpenAI analysis yet (will be done separately) @@ -57,17 +63,15 @@ public function fetchReviewsAndSave(string $asin, string $country, string $produ } /** - * Fetch reviews from Amazon using Unwrangle API. - * - * @param string $asin Amazon Standard Identification Number - * @param string $country Two-letter country code (defaults to 'us') - * - * @return array Array containing reviews, description, and total count + * Optimized version of fetchReviews with improved performance */ - public function fetchReviews(string $asin, string $country = 'us'): array + public function fetchReviewsOptimized(string $asin, string $country = 'us'): array { - // Check if the ASIN exists on Amazon US before calling Unwrangle API - if (!$this->validateAsinExists($asin)) { + // Skip validation for known working products to save 2 seconds + // Only validate if we haven't seen this ASIN recently + $shouldValidate = !$this->isRecentlyValidated($asin); + + if ($shouldValidate && !$this->validateAsinExistsFast($asin)) { LoggingService::log('ASIN validation failed - product does not exist on amazon.com', [ 'asin' => $asin, 'url_checked' => "https://www.amazon.com/dp/{$asin}", @@ -76,11 +80,15 @@ public function fetchReviews(string $asin, string $country = 'us'): array return []; } + if ($shouldValidate) { + $this->markAsValidated($asin); + } + $apiKey = env('UNWRANGLE_API_KEY'); $cookie = env('UNWRANGLE_AMAZON_COOKIE'); $baseUrl = 'https://data.unwrangle.com/api/getter/'; - $maxPages = 10; - $country = 'us'; // Always use US country for session cookie match + $maxPages = 10; // Keep original page count for comprehensive data + $country = 'us'; $query = [ 'platform' => 'amazon_reviews', @@ -92,19 +100,30 @@ public function fetchReviews(string $asin, string $country = 'us'): array ]; try { - $response = $this->httpClient->request('GET', $baseUrl, ['query' => $query]); + $startTime = microtime(true); + + $response = $this->httpClient->request('GET', $baseUrl, [ + 'query' => $query, + 'timeout' => 45, // Increased timeout for 10 pages of data + 'connect_timeout' => 5, + ]); + + $endTime = microtime(true); + $duration = round(($endTime - $startTime) * 1000, 2); + $status = $response->getStatusCode(); $body = $response->getBody()->getContents(); LoggingService::log('Unwrangle API response', [ 'status' => $status, 'has_data' => !empty($body), + 'duration_ms' => $duration, ]); if ($status !== 200) { LoggingService::log('Unwrangle API non-200 response', [ 'status' => $status, - 'body' => $body, + 'body' => substr($body, 0, 500), // Limit log size ]); return []; @@ -124,6 +143,9 @@ public function fetchReviews(string $asin, string $country = 'us'): array $description = $data['description'] ?? ''; $allReviews = $data['reviews'] ?? []; + // Keep all reviews for comprehensive analysis + LoggingService::log('Retrieved '.count($allReviews).' reviews for analysis'); + return [ 'reviews' => $allReviews, 'description' => $description, @@ -140,34 +162,31 @@ public function fetchReviews(string $asin, string $country = 'us'): array } /** - * Validate that an ASIN exists on Amazon US by checking the product page. + * Fetch reviews from Amazon using Unwrangle API. * - * @param string $asin Amazon Standard Identification Number + * @param string $asin Amazon Standard Identification Number + * @param string $country Two-letter country code (defaults to 'us') * - * @return bool True if product exists (returns 200), false otherwise + * @return array Array containing reviews, description, and total count + */ + public function fetchReviews(string $asin, string $country = 'us'): array + { + // Use optimized version + return $this->fetchReviewsOptimized($asin, $country); + } + + /** + * Fast ASIN validation with shorter timeout */ - private function validateAsinExists(string $asin): bool + private function validateAsinExistsFast(string $asin): bool { $url = "https://www.amazon.com/dp/{$asin}"; try { - $response = $this->httpClient->request('GET', $url, [ - 'timeout' => 10, - 'allow_redirects' => false, // Don't follow redirects to catch geo-redirects - 'headers' => [ - 'User-Agent' => 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36', - 'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8', - 'Accept-Language' => 'en-US,en;q=0.9', - 'Accept-Encoding' => 'gzip, deflate, br', - 'Cache-Control' => 'no-cache', - 'Pragma' => 'no-cache', - 'Upgrade-Insecure-Requests' => '1', - 'Sec-Fetch-Dest' => 'document', - 'Sec-Fetch-Mode' => 'navigate', - 'Sec-Fetch-Site' => 'none', - 'Sec-Fetch-User' => '?1', - ], - 'http_errors' => false, // Don't throw exceptions on 4xx/5xx + $response = $this->httpClient->request('HEAD', $url, [ + 'timeout' => 3, // Very short timeout for validation + 'connect_timeout' => 1, + 'allow_redirects' => false, // Don't follow redirects for speed ]); $statusCode = $response->getStatusCode(); @@ -178,9 +197,10 @@ private function validateAsinExists(string $asin): bool 'status_code' => $statusCode, ]); - return $statusCode === 200; + // Accept 200 (OK) and 3xx (redirect) as valid + return $statusCode >= 200 && $statusCode < 400; } catch (\Exception $e) { - LoggingService::log('ASIN validation failed with exception', [ + LoggingService::log('ASIN validation failed', [ 'asin' => $asin, 'error' => $e->getMessage(), ]); @@ -188,4 +208,32 @@ private function validateAsinExists(string $asin): bool return false; } } + + /** + * Check if ASIN has been validated recently (in-memory cache) + */ + private function isRecentlyValidated(string $asin): bool + { + static $validatedAsins = []; + static $lastClear = 0; + + $now = time(); + + // Clear cache every 5 minutes + if ($now - $lastClear > 300) { + $validatedAsins = []; + $lastClear = $now; + } + + return isset($validatedAsins[$asin]) && ($now - $validatedAsins[$asin]) < 300; + } + + /** + * Mark ASIN as validated (in-memory cache) + */ + private function markAsValidated(string $asin): void + { + static $validatedAsins = []; + $validatedAsins[$asin] = time(); + } } diff --git a/app/Services/OpenAIService.php b/app/Services/OpenAIService.php index 0d46692..09dda16 100644 --- a/app/Services/OpenAIService.php +++ b/app/Services/OpenAIService.php @@ -14,7 +14,7 @@ class OpenAIService public function __construct() { $this->apiKey = config('services.openai.api_key') ?? ''; - $this->model = config('services.openai.model', 'gpt-4'); + $this->model = config('services.openai.model', 'gpt-4o-mini'); $this->baseUrl = config('services.openai.base_url', 'https://api.openai.com/v1'); if (empty($this->apiKey)) { @@ -31,18 +31,18 @@ public function analyzeReviews(array $reviews): array // Log the number of reviews being sent LoggingService::log('Sending '.count($reviews).' reviews to OpenAI for analysis'); - $prompt = $this->buildPrompt($reviews); + // For better performance, process in parallel chunks if we have many reviews + $parallelThreshold = config('services.openai.parallel_threshold', 50); + if (count($reviews) > $parallelThreshold) { + LoggingService::log('Large dataset detected ('.count($reviews).' reviews), processing in parallel chunks'); + return $this->analyzeReviewsInParallelChunks($reviews); + } + + $prompt = $this->buildOptimizedPrompt($reviews); // Log prompt size for debugging $promptSize = strlen($prompt); - LoggingService::log("Prompt size: {$promptSize} characters"); - - // Only chunk if prompt is extremely large (>100k chars) or too many reviews (>100) - if ($promptSize > 100000 || count($reviews) > 100) { - LoggingService::log('Very large payload detected, processing in chunks to avoid API limits'); - - return $this->analyzeReviewsInChunks($reviews); - } + LoggingService::log("Optimized prompt size: {$promptSize} characters"); try { // Extract the endpoint from base_url if it includes the full path @@ -53,28 +53,30 @@ public function analyzeReviews(array $reviews): array LoggingService::log("Making OpenAI API request to: {$endpoint}"); - // Determine max_tokens based on model - $maxTokens = $this->getMaxTokensForModel($this->model); - LoggingService::log("Using max_tokens: {$maxTokens} for model: {$this->model}"); + // Use optimized parameters for faster processing + $maxTokens = $this->getOptimizedMaxTokens(count($reviews)); + $timeout = config('services.openai.timeout', 120); + LoggingService::log("Using optimized max_tokens: {$maxTokens} for {$this->model} with ".count($reviews)." reviews, timeout: {$timeout}s"); $response = Http::withHeaders([ 'Authorization' => 'Bearer '.$this->apiKey, 'Content-Type' => 'application/json', 'User-Agent' => 'ReviewAnalyzer/1.0', - ])->timeout(300)->connectTimeout(60)->retry(3, 2000)->post($endpoint, [ + ])->timeout($timeout)->connectTimeout(30)->retry(2, 1000)->post($endpoint, [ 'model' => $this->model, 'messages' => [ [ 'role' => 'system', - 'content' => 'You are an expert at detecting fake reviews. Analyze each review and provide a score from 0-100 where 0 is genuine and 100 is definitely fake. Return ONLY a JSON array with objects containing "id" and "score" fields.', + 'content' => 'You are a fast review authenticity detector. Analyze each review and return ONLY a JSON array with {"id":"X","score":Y} objects. Score 0-100 where 0=genuine, 100=fake.', ], [ 'role' => 'user', 'content' => $prompt, ], ], - 'temperature' => 0.1, // Lower for more consistent results + 'temperature' => 0.0, // Deterministic for consistency and speed 'max_tokens' => $maxTokens, + 'top_p' => 0.1, // More focused responses ]); if ($response->successful()) { @@ -102,9 +104,78 @@ public function analyzeReviews(array $reviews): array } } + /** + * Process reviews in parallel chunks for better performance with large datasets + */ + private function analyzeReviewsInParallelChunks(array $reviews): array + { + $chunkSize = config('services.openai.chunk_size', 25); + $chunks = array_chunk($reviews, $chunkSize); + $allDetailedScores = []; + + LoggingService::log('Processing '.count($chunks).' chunks in parallel for '.count($reviews).' reviews'); + + // Process chunks concurrently using Laravel's HTTP pool + $responses = Http::pool(function ($pool) use ($chunks) { + $endpoint = $this->baseUrl; + if (!str_ends_with($endpoint, '/chat/completions')) { + $endpoint = rtrim($endpoint, '/').'/chat/completions'; + } + + foreach ($chunks as $index => $chunk) { + $prompt = $this->buildOptimizedPrompt($chunk); + $maxTokens = $this->getOptimizedMaxTokens(count($chunk)); + + $pool->withHeaders([ + 'Authorization' => 'Bearer '.$this->apiKey, + 'Content-Type' => 'application/json', + 'User-Agent' => 'ReviewAnalyzer/1.0', + ])->timeout(60)->connectTimeout(20)->post($endpoint, [ + 'model' => $this->model, + 'messages' => [ + [ + 'role' => 'system', + 'content' => 'You are a fast review authenticity detector. Analyze each review and return ONLY a JSON array with {"id":"X","score":Y} objects. Score 0-100 where 0=genuine, 100=fake.', + ], + [ + 'role' => 'user', + 'content' => $prompt, + ], + ], + 'temperature' => 0.0, + 'max_tokens' => $maxTokens, + 'top_p' => 0.1, + ]); + } + }); + + // Process all responses + foreach ($responses as $chunkIndex => $response) { + $chunk = $chunks[$chunkIndex]; + + if ($response->successful()) { + $result = $response->json(); + $chunkResult = $this->parseOpenAIResponse($result, $chunk); + + if (isset($chunkResult['detailed_scores'])) { + $allDetailedScores = array_merge($allDetailedScores, $chunkResult['detailed_scores']); + } + + LoggingService::log('Successfully processed chunk '.($chunkIndex + 1).' ('.count($chunk).' reviews)'); + } else { + LoggingService::log('Error processing chunk '.($chunkIndex + 1).': HTTP '.$response->status()); + // Continue with other chunks even if one fails + } + } + + LoggingService::log('Parallel processing completed with '.count($allDetailedScores).' total scores'); + + return ['detailed_scores' => $allDetailedScores]; + } + private function analyzeReviewsInChunks(array $reviews): array { - $chunkSize = 25; // Process 25 reviews at a time + $chunkSize = config('services.openai.chunk_size', 25); $chunks = array_chunk($reviews, $chunkSize); $allDetailedScores = []; @@ -119,7 +190,7 @@ private function analyzeReviewsInChunks(array $reviews): array } // Small delay between chunks to avoid rate limiting - usleep(500000); // 0.5 seconds + usleep(200000); // Reduced to 0.2 seconds } catch (\Exception $e) { LoggingService::log('Error processing chunk '.($index + 1).': '.$e->getMessage()); // Continue with other chunks even if one fails @@ -129,25 +200,70 @@ private function analyzeReviewsInChunks(array $reviews): array return ['detailed_scores' => $allDetailedScores]; } - private function buildPrompt($reviews): string + /** + * Build an optimized prompt that uses fewer tokens while maintaining accuracy + */ + private function buildOptimizedPrompt($reviews): string { - $prompt = "Analyze these Amazon reviews for authenticity. Score each from 0-100 (0=genuine, 100=fake).\n\n"; - $prompt .= "FAKE INDICATORS: Generic language, excessive praise without specifics, promotional tone, very short 5-star reviews, missing product details, suspicious names.\n"; - $prompt .= "GENUINE INDICATORS: Specific details, balanced pros/cons, personal context, time references, ingredient mentions, detailed complaints.\n\n"; - $prompt .= "Return JSON: [{\"id\":\"X\",\"score\":Y},...]\n\n"; + $prompt = "Score each review 0-100 (0=real, 100=fake). Return JSON: [{\"id\":\"X\",\"score\":Y}]\n\n"; + $prompt .= "FAKE SIGNS: Generic text, excessive praise, no specifics, promotional tone, short 5-stars\n"; + $prompt .= "REAL SIGNS: Specific details, balanced feedback, personal context, complaints\n\n"; foreach ($reviews as $review) { - $verification = isset($review['meta_data']['verified_purchase']) && $review['meta_data']['verified_purchase'] ? 'Verified' : 'Unverified'; - $vine = isset($review['meta_data']['is_vine_voice']) && $review['meta_data']['is_vine_voice'] ? 'Vine' : 'Regular'; - - $prompt .= "ID:{$review['id']} {$review['rating']}/5 {$verification} {$vine}\n"; - $prompt .= "Title: {$review['review_title']}\n"; - $prompt .= "Text: {$review['review_text']}\n\n"; + $verified = isset($review['meta_data']['verified_purchase']) && $review['meta_data']['verified_purchase'] ? 'V' : 'U'; + $vine = isset($review['meta_data']['is_vine_voice']) && $review['meta_data']['is_vine_voice'] ? 'Vine' : ''; + + // Clean and truncate text to handle UTF-8 issues + $title = $this->cleanUtf8Text(substr($review['review_title'], 0, 100)); + $text = $this->cleanUtf8Text(substr($review['review_text'], 0, 400)); + + $prompt .= "ID:{$review['id']} {$review['rating']}/5 {$verified}{$vine}\n"; + $prompt .= "T: {$title}\n"; + $prompt .= "R: {$text}\n\n"; } return $prompt; } + /** + * Clean text to ensure valid UTF-8 encoding for JSON serialization + */ + private function cleanUtf8Text(string $text): string + { + // Remove or replace invalid UTF-8 sequences + $text = mb_convert_encoding($text, 'UTF-8', 'UTF-8'); + + // Remove null bytes and other problematic characters + $text = str_replace(["\0", "\x1A"], '', $text); + + // Ensure string is valid UTF-8 + if (!mb_check_encoding($text, 'UTF-8')) { + $text = mb_convert_encoding($text, 'UTF-8', 'auto'); + } + + return trim($text); + } + + /** + * Get optimized max_tokens based on number of reviews for faster processing + */ + private function getOptimizedMaxTokens(int $reviewCount): int + { + // Calculate based on expected output size: roughly 20 chars per review for JSON response + $baseTokens = $reviewCount * 8; // More conservative estimate + + // Add buffer but keep it minimal for speed + $buffer = min(500, $reviewCount * 2); + + return $baseTokens + $buffer; + } + + private function buildPrompt($reviews): string + { + // Fallback to old method if needed + return $this->buildOptimizedPrompt($reviews); + } + private function parseOpenAIResponse($response, $reviews): array { $content = $response['choices'][0]['message']['content'] ?? ''; diff --git a/app/Services/ReviewAnalysisService.php b/app/Services/ReviewAnalysisService.php index 38c898c..ee49b89 100644 --- a/app/Services/ReviewAnalysisService.php +++ b/app/Services/ReviewAnalysisService.php @@ -314,29 +314,19 @@ private function generateExplanation($totalReviews, $fakeCount, $fakePercentage) 'This product has very high fake review activity. We recommend avoiding this product.')))); } - private function calculateAdjustedRating($genuineReviews) + /** + * Calculate adjusted rating based on genuine reviews only. + */ + private function calculateAdjustedRating(array $genuineReviews): float { - LoggingService::log('calculateAdjustedRating called with '.count($genuineReviews).' genuine reviews'); - if (empty($genuineReviews)) { - LoggingService::log('No genuine reviews found, returning 0'); - return 0; } - $totalRating = 0; - foreach ($genuineReviews as $review) { - $totalRating += $review['rating']; - LoggingService::log('Adding rating: '.$review['rating'].', total so far: '.$totalRating); - } - + $totalRating = array_sum(array_column($genuineReviews, 'rating')); $adjustedRating = $totalRating / count($genuineReviews); - $roundedRating = round($adjustedRating, 2); - - LoggingService::log('Final calculation: '.$totalRating.' / '.count($genuineReviews).' = '.$adjustedRating); - LoggingService::log('Rounded result: '.$roundedRating); - return $roundedRating; + return round($adjustedRating, 2); } /** @@ -425,16 +415,13 @@ public function calculateFinalMetrics(AsinData $asinData): array $totalReviews = count($reviews); $fakeCount = 0; $amazonRatingSum = 0; - $genuineRatingSum = 0; - $genuineCount = 0; + $genuineReviews = []; LoggingService::log('=== STARTING CALCULATION DEBUG ==='); LoggingService::log("Total reviews found: {$totalReviews}"); LoggingService::log('Detailed scores count: '.count($detailedScores)); - $fakeReviews = []; - $genuineReviews = []; - + // Optimized single-pass calculation foreach ($reviews as $review) { $reviewId = $review['id']; $rating = $review['rating']; @@ -444,15 +431,10 @@ public function calculateFinalMetrics(AsinData $asinData): array if ($fakeScore >= 70) { $fakeCount++; - $fakeReviews[] = [ - 'id' => $reviewId, - 'rating' => $rating, - 'fake_score' => $fakeScore, - ]; - LoggingService::log("FAKE REVIEW: ID={$reviewId}, Rating={$rating}, Score={$fakeScore}"); + if ($fakeCount <= 8) { // Only log first 8 fake reviews to reduce log spam + LoggingService::log("FAKE REVIEW: ID={$reviewId}, Rating={$rating}, Score={$fakeScore}"); + } } else { - $genuineRatingSum += $rating; - $genuineCount++; $genuineReviews[] = [ 'id' => $reviewId, 'rating' => $rating, @@ -463,17 +445,14 @@ public function calculateFinalMetrics(AsinData $asinData): array LoggingService::log('=== FAKE REVIEWS SUMMARY ==='); LoggingService::log("Total fake reviews: {$fakeCount}"); - foreach ($fakeReviews as $fake) { - LoggingService::log("Fake: {$fake['id']} - Rating: {$fake['rating']} - Score: {$fake['fake_score']}"); - } LoggingService::log('=== GENUINE REVIEWS SUMMARY ==='); - LoggingService::log("Total genuine reviews: {$genuineCount}"); - $genuineRatingCounts = [1 => 0, 2 => 0, 3 => 0, 4 => 0, 5 => 0]; - foreach ($genuineReviews as $genuine) { - $genuineRatingCounts[$genuine['rating']]++; - } - foreach ($genuineRatingCounts as $rating => $count) { + LoggingService::log("Total genuine reviews: ".count($genuineReviews)); + + // Optimized rating count calculation + $genuineRatingCounts = array_count_values(array_column($genuineReviews, 'rating')); + for ($rating = 1; $rating <= 5; $rating++) { + $count = $genuineRatingCounts[$rating] ?? 0; LoggingService::log("Genuine {$rating}-star reviews: {$count}"); } @@ -484,7 +463,6 @@ public function calculateFinalMetrics(AsinData $asinData): array LoggingService::log('=== FINAL CALCULATIONS ==='); LoggingService::log("Amazon rating sum: {$amazonRatingSum}"); LoggingService::log("Amazon rating average: {$amazonRating}"); - LoggingService::log("Genuine rating sum: {$genuineRatingSum}"); LoggingService::log("Genuine rating average: {$adjustedRating}"); LoggingService::log("Fake percentage: {$fakePercentage}%"); diff --git a/config/services.php b/config/services.php index f04e972..1f24f91 100644 --- a/config/services.php +++ b/config/services.php @@ -37,9 +37,12 @@ ], 'openai' => [ - 'api_key' => env('OPENAI_API_KEY'), - 'model' => env('OPENAI_MODEL', 'gpt-4'), + 'api_key' => env('OPENAI_API_KEY'), + 'model' => env('OPENAI_MODEL', 'gpt-4o-mini'), 'base_url' => env('OPENAI_BASE_URL', 'https://api.openai.com/v1'), + 'timeout' => env('OPENAI_TIMEOUT', 120), + 'parallel_threshold' => env('OPENAI_PARALLEL_THRESHOLD', 50), + 'chunk_size' => env('OPENAI_CHUNK_SIZE', 25), ], ]; diff --git a/tests/Feature/PerformanceOptimizationTest.php b/tests/Feature/PerformanceOptimizationTest.php new file mode 100644 index 0000000..55b8089 --- /dev/null +++ b/tests/Feature/PerformanceOptimizationTest.php @@ -0,0 +1,218 @@ +mock(AmazonFetchService::class, function ($mock) { + $asinData = AsinData::create([ + 'asin' => 'B08TX7Q9JT', + 'country' => 'us', + 'reviews' => json_encode($this->generateMockReviews(100)), + 'openai_result' => json_encode(['detailed_scores' => []]), + 'status' => 'completed', + ]); + + $mock->shouldReceive('fetchReviewsAndSave') + ->andReturn($asinData); + }); + + // Mock OpenAI API responses for optimized processing + Http::fake([ + 'api.openai.com/*' => Http::response([ + 'choices' => [ + [ + 'message' => [ + 'content' => $this->generateMockOpenAIResponse(100), + ], + ], + ], + ], 200), + ]); + + $analysisService = app(ReviewAnalysisService::class); + + // Measure analysis time + $startTime = microtime(true); + + $result = $analysisService->analyzeProduct('https://www.amazon.com/dp/B08TX7Q9JT'); + + $endTime = microtime(true); + $duration = ($endTime - $startTime); + + // Assertions + $this->assertIsArray($result); + $this->assertArrayHasKey('fake_percentage', $result); + $this->assertArrayHasKey('amazon_rating', $result); + $this->assertArrayHasKey('adjusted_rating', $result); + $this->assertArrayHasKey('grade', $result); + $this->assertArrayHasKey('explanation', $result); + + // Performance assertion - should complete in under 10 seconds with mocked responses + $this->assertLessThan(10, $duration, 'Analysis should complete in under 10 seconds with optimizations'); + + // Verify the result makes sense + $this->assertGreaterThanOrEqual(0, $result['fake_percentage']); + $this->assertLessThanOrEqual(100, $result['fake_percentage']); + $this->assertGreaterThanOrEqual(0, $result['amazon_rating']); + $this->assertLessThanOrEqual(5, $result['amazon_rating']); + } + + public function test_parallel_processing_with_large_dataset() + { + // Test that parallel processing is triggered for large datasets + $reviews = $this->generateMockReviews(60); // Above the parallel threshold + + Http::fake([ + 'api.openai.com/*' => Http::response([ + 'choices' => [ + [ + 'message' => [ + 'content' => $this->generateMockOpenAIResponse(25), // Chunk size + ], + ], + ], + ], 200), + ]); + + $openAIService = app(OpenAIService::class); + + $startTime = microtime(true); + $result = $openAIService->analyzeReviews($reviews); + $endTime = microtime(true); + + $duration = ($endTime - $startTime); + + $this->assertIsArray($result); + $this->assertArrayHasKey('detailed_scores', $result); + + // With parallel processing, this should be faster than sequential processing + $this->assertLessThan(30, $duration, 'Parallel processing should complete faster'); + } + + public function test_optimized_prompt_reduces_token_usage() + { + $reviews = $this->generateMockReviews(10); + + $openAIService = app(OpenAIService::class); + + // Use reflection to test the optimized prompt method + $reflection = new \ReflectionClass($openAIService); + $method = $reflection->getMethod('buildOptimizedPrompt'); + $method->setAccessible(true); + + $optimizedPrompt = $method->invoke($openAIService, $reviews); + + // Verify prompt is concise but contains essential information + $this->assertLessThan(5000, strlen($optimizedPrompt), 'Optimized prompt should be under 5000 characters for 10 reviews'); + $this->assertStringContainsString('Score each review 0-100', $optimizedPrompt); + $this->assertStringContainsString('JSON:', $optimizedPrompt); + + // Verify all review IDs are present + foreach ($reviews as $review) { + $this->assertStringContainsString("ID:{$review['id']}", $optimizedPrompt); + } + } + + public function test_fast_asin_validation() + { + $fetchService = app(AmazonFetchService::class); + + // Use reflection to test the fast validation method + $reflection = new \ReflectionClass($fetchService); + $method = $reflection->getMethod('validateAsinExistsFast'); + $method->setAccessible(true); + + // Mock the HTTP client to return success quickly + Http::fake([ + 'https://www.amazon.com/dp/*' => Http::response('', 200), + ]); + + $startTime = microtime(true); + $result = $method->invoke($fetchService, 'B08TX7Q9JT'); + $endTime = microtime(true); + + $duration = ($endTime - $startTime); + + $this->assertTrue($result); + $this->assertLessThan(5, $duration, 'Fast ASIN validation should complete in under 5 seconds'); + } + + public function test_calculation_optimization() + { + // Create test data with many reviews + $reviews = $this->generateMockReviews(100); + $detailedScores = []; + + foreach ($reviews as $review) { + $detailedScores[$review['id']] = rand(0, 100); + } + + $asinData = AsinData::create([ + 'asin' => 'B08TX7Q9JT', + 'country' => 'us', + 'reviews' => $reviews, + 'openai_result' => ['detailed_scores' => $detailedScores], + ]); + + $analysisService = app(ReviewAnalysisService::class); + + $startTime = microtime(true); + $result = $analysisService->calculateFinalMetrics($asinData); + $endTime = microtime(true); + + $duration = ($endTime - $startTime); + + $this->assertIsArray($result); + $this->assertArrayHasKey('fake_percentage', $result); + $this->assertLessThan(2, $duration, 'Calculation should complete in under 2 seconds'); + } + + private function generateMockReviews(int $count): array + { + $reviews = []; + + for ($i = 0; $i < $count; $i++) { + $reviews[] = [ + 'id' => "R{$i}TESTID", + 'rating' => rand(1, 5), + 'review_title' => "Test Review Title {$i}", + 'review_text' => "This is a test review text for review {$i}. It contains some details about the product.", + 'author_name' => "Test Author {$i}", + 'meta_data' => [ + 'verified_purchase' => rand(0, 1) === 1, + 'is_vine_voice' => rand(0, 1) === 1, + ], + ]; + } + + return $reviews; + } + + private function generateMockOpenAIResponse(int $count): string + { + $scores = []; + + for ($i = 0; $i < $count; $i++) { + $scores[] = [ + 'id' => "R{$i}TESTID", + 'score' => rand(0, 100), + ]; + } + + return json_encode($scores); + } +} \ No newline at end of file diff --git a/tests/Unit/AmazonFetchServiceTest.php b/tests/Unit/AmazonFetchServiceTest.php index 737e7f4..f5adff0 100644 --- a/tests/Unit/AmazonFetchServiceTest.php +++ b/tests/Unit/AmazonFetchServiceTest.php @@ -168,12 +168,12 @@ public function test_fetch_reviews_unwrangle_api_exception() public function test_validate_asin_exists_success() { - // Mock Amazon validation (product exists) + // Mock successful response $this->mockHandler->append(new Response(200, [], 'Amazon product page')); // Use reflection to call private method $reflection = new \ReflectionClass($this->service); - $method = $reflection->getMethod('validateAsinExists'); + $method = $reflection->getMethod('validateAsinExistsFast'); $method->setAccessible(true); $result = $method->invoke($this->service, 'B08N5WRWNW'); @@ -183,12 +183,12 @@ public function test_validate_asin_exists_success() public function test_validate_asin_exists_not_found() { - // Mock Amazon validation (product doesn't exist) + // Mock 404 response $this->mockHandler->append(new Response(404, [], 'Not found')); // Use reflection to call private method $reflection = new \ReflectionClass($this->service); - $method = $reflection->getMethod('validateAsinExists'); + $method = $reflection->getMethod('validateAsinExistsFast'); $method->setAccessible(true); $result = $method->invoke($this->service, 'INVALID123'); @@ -198,30 +198,30 @@ public function test_validate_asin_exists_not_found() public function test_validate_asin_exists_redirect() { - // Mock Amazon validation with redirect (geo-redirect) + // Mock redirect response (should still be considered valid) $this->mockHandler->append(new Response(302, ['Location' => 'https://amazon.co.uk/dp/B08N5WRWNW'], '')); // Use reflection to call private method $reflection = new \ReflectionClass($this->service); - $method = $reflection->getMethod('validateAsinExists'); + $method = $reflection->getMethod('validateAsinExistsFast'); $method->setAccessible(true); $result = $method->invoke($this->service, 'B08N5WRWNW'); - $this->assertFalse($result); // Should return false for redirects + $this->assertTrue($result); } public function test_validate_asin_exists_exception() { - // Mock Amazon validation exception - $this->mockHandler->append(new RequestException( + // Mock exception + $this->mockHandler->append(new \GuzzleHttp\Exception\RequestException( 'Connection timeout', - new Request('GET', 'test') + new \GuzzleHttp\Psr7\Request('GET', 'test') )); // Use reflection to call private method $reflection = new \ReflectionClass($this->service); - $method = $reflection->getMethod('validateAsinExists'); + $method = $reflection->getMethod('validateAsinExistsFast'); $method->setAccessible(true); $result = $method->invoke($this->service, 'B08N5WRWNW'); From af951593224b7c170066d234a1e8c5ad694ccc44 Mon Sep 17 00:00:00 2001 From: SDH Date: Tue, 10 Jun 2025 15:46:28 -0400 Subject: [PATCH 06/24] Verbiage change on homepage blade template --- resources/views/home.blade.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/views/home.blade.php b/resources/views/home.blade.php index a748e49..a435d9e 100644 --- a/resources/views/home.blade.php +++ b/resources/views/home.blade.php @@ -99,7 +99,7 @@ function gtag(){dataLayer.push(arguments);}
-
AI-powered review analyzer. Instantly detect fake, AI-generated, or suspicious reviews and get a trust score for any product.
+
AI-powered review analyzer. Easily detect fake, AI-generated, or suspicious reviews and get a trust score for any product.
{{-- Livewire Review Analyzer replaces the form --}} From 1396001581c905e74bc67f6242bcebd4ebef62b2 Mon Sep 17 00:00:00 2001 From: SDH Date: Tue, 10 Jun 2025 15:47:11 -0400 Subject: [PATCH 07/24] Verbiage change on homepage blade template --- resources/views/home.blade.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/views/home.blade.php b/resources/views/home.blade.php index a435d9e..969b3a6 100644 --- a/resources/views/home.blade.php +++ b/resources/views/home.blade.php @@ -99,7 +99,7 @@ function gtag(){dataLayer.push(arguments);}
-
AI-powered review analyzer. Easily detect fake, AI-generated, or suspicious reviews and get a trust score for any product.
+
AI-powered review analyzer. Easily detect fake, AI-generated or suspicious reviews and get a trust score for any product.
{{-- Livewire Review Analyzer replaces the form --}} From 650b9710a5ae0186a728d1682d5496248f549448 Mon Sep 17 00:00:00 2001 From: SDH Date: Tue, 10 Jun 2025 15:47:50 -0400 Subject: [PATCH 08/24] Verbiage change on homepage blade template --- resources/views/livewire/review-analyzer.blade.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/views/livewire/review-analyzer.blade.php b/resources/views/livewire/review-analyzer.blade.php index 8cf57c9..1efc66c 100644 --- a/resources/views/livewire/review-analyzer.blade.php +++ b/resources/views/livewire/review-analyzer.blade.php @@ -3,7 +3,7 @@
- + @error('productUrl') From ca49fec90b920fc8c42fb224a07dcec2ceea121f Mon Sep 17 00:00:00 2001 From: SDH Date: Tue, 10 Jun 2025 15:48:32 -0400 Subject: [PATCH 09/24] Verbiage change on homepage blade template --- resources/views/home.blade.php | 2 +- resources/views/livewire/review-analyzer.blade.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/views/home.blade.php b/resources/views/home.blade.php index 969b3a6..38baae3 100644 --- a/resources/views/home.blade.php +++ b/resources/views/home.blade.php @@ -99,7 +99,7 @@ function gtag(){dataLayer.push(arguments);}
-
AI-powered review analyzer. Easily detect fake, AI-generated or suspicious reviews and get a trust score for any product.
+
AI-powered review analyzer. Easily detect fake, AI-generated or suspicious reviews and get a trust score for any product. Currently only US products are supported.
{{-- Livewire Review Analyzer replaces the form --}} diff --git a/resources/views/livewire/review-analyzer.blade.php b/resources/views/livewire/review-analyzer.blade.php index 1efc66c..8cf57c9 100644 --- a/resources/views/livewire/review-analyzer.blade.php +++ b/resources/views/livewire/review-analyzer.blade.php @@ -3,7 +3,7 @@
- + @error('productUrl') From b69c0b5c68bacb0e5ebf209c7a665bd8330d323d Mon Sep 17 00:00:00 2001 From: SDH Date: Tue, 10 Jun 2025 15:49:07 -0400 Subject: [PATCH 10/24] Verbiage change on homepage blade template --- resources/views/privacy.blade.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/views/privacy.blade.php b/resources/views/privacy.blade.php index 6eda961..5adb7dd 100644 --- a/resources/views/privacy.blade.php +++ b/resources/views/privacy.blade.php @@ -31,7 +31,7 @@

Privacy Policy

-

Last updated: December 2024

+

Last updated: June 2025

From a67b3d9bd06c4d7a2ac21de2c5645e7ab0842fa7 Mon Sep 17 00:00:00 2001 From: SDH Date: Tue, 10 Jun 2025 16:13:47 -0400 Subject: [PATCH 11/24] Fixed ASIN validation issue --- app/Services/Amazon/AmazonFetchService.php | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/app/Services/Amazon/AmazonFetchService.php b/app/Services/Amazon/AmazonFetchService.php index c154b9c..4057703 100644 --- a/app/Services/Amazon/AmazonFetchService.php +++ b/app/Services/Amazon/AmazonFetchService.php @@ -183,6 +183,7 @@ private function validateAsinExistsFast(string $asin): bool $url = "https://www.amazon.com/dp/{$asin}"; try { + // First try HEAD request for speed $response = $this->httpClient->request('HEAD', $url, [ 'timeout' => 3, // Very short timeout for validation 'connect_timeout' => 1, @@ -191,6 +192,22 @@ private function validateAsinExistsFast(string $asin): bool $statusCode = $response->getStatusCode(); + // If HEAD request returns 405 (Method Not Allowed), try GET request + if ($statusCode === 405) { + LoggingService::log('HEAD request returned 405, trying GET request', [ + 'asin' => $asin, + 'url' => $url, + ]); + + $response = $this->httpClient->request('GET', $url, [ + 'timeout' => 5, // Slightly longer timeout for GET + 'connect_timeout' => 2, + 'allow_redirects' => false, + ]); + + $statusCode = $response->getStatusCode(); + } + LoggingService::log('ASIN validation check', [ 'asin' => $asin, 'url' => $url, From e116598848374de2f92966fd0db2dd116a6a2567 Mon Sep 17 00:00:00 2001 From: SDH Date: Tue, 10 Jun 2025 16:20:03 -0400 Subject: [PATCH 12/24] Fixed Captcha validation --- app/Livewire/ReviewAnalyzer.php | 31 ++++++----- .../views/livewire/review-analyzer.blade.php | 54 ++++++++++--------- 2 files changed, 47 insertions(+), 38 deletions(-) diff --git a/app/Livewire/ReviewAnalyzer.php b/app/Livewire/ReviewAnalyzer.php index 9fc0ac3..bf1b81d 100644 --- a/app/Livewire/ReviewAnalyzer.php +++ b/app/Livewire/ReviewAnalyzer.php @@ -5,6 +5,7 @@ use App\Services\CaptchaService; use App\Services\LoggingService; use App\Services\ReviewAnalysisService; +use Illuminate\Http\Request; use Livewire\Component; /** @@ -105,20 +106,26 @@ public function analyze() // Captcha validation (if not local) if (!app()->environment('local')) { - $captchaService = app(CaptchaService::class); - $provider = $captchaService->getProvider(); - $clientIp = request()->ip(); - - if ($provider === 'recaptcha' && !empty($this->g_recaptcha_response)) { - if (!$captchaService->verify($this->g_recaptcha_response, $clientIp)) { - throw new \Exception('Captcha verification failed. Please try again.'); - } - } elseif ($provider === 'hcaptcha' && !empty($this->h_captcha_response)) { - if (!$captchaService->verify($this->h_captcha_response, $clientIp)) { + // Skip captcha validation if already passed in this session + if (!$this->captcha_passed) { + $captchaService = app(CaptchaService::class); + $provider = $captchaService->getProvider(); + + if ($provider === 'recaptcha' && !empty($this->g_recaptcha_response)) { + if (!$captchaService->verify($this->g_recaptcha_response)) { + throw new \Exception('Captcha verification failed. Please try again.'); + } + // Mark captcha as passed for this session + $this->captcha_passed = true; + } elseif ($provider === 'hcaptcha' && !empty($this->h_captcha_response)) { + if (!$captchaService->verify($this->h_captcha_response)) { + throw new \Exception('Captcha verification failed. Please try again.'); + } + // Mark captcha as passed for this session + $this->captcha_passed = true; + } else { throw new \Exception('Captcha verification failed. Please try again.'); } - } else { - throw new \Exception('Captcha verification failed. Please try again.'); } } diff --git a/resources/views/livewire/review-analyzer.blade.php b/resources/views/livewire/review-analyzer.blade.php index 8cf57c9..4831f3f 100644 --- a/resources/views/livewire/review-analyzer.blade.php +++ b/resources/views/livewire/review-analyzer.blade.php @@ -14,38 +14,40 @@ class="mt-1 w-full px-4 py-2 border rounded focus:outline-none focus:ring focus: @if(!app()->environment('local'))
@if($captcha->getProvider() === 'recaptcha') -
- - @error('g_recaptcha_response') -
{{ $message }}
- @enderror - - + + + @endif @elseif($captcha->getProvider() === 'hcaptcha') @if(!$captcha_passed)
Date: Tue, 10 Jun 2025 16:39:37 -0400 Subject: [PATCH 13/24] Fixed OpenAI parsing error --- app/Services/OpenAIService.php | 77 +++- test_openai_fix.php | 34 ++ .../Feature/CaptchaSessionPersistenceTest.php | 349 ++++++++++++++++++ 3 files changed, 451 insertions(+), 9 deletions(-) create mode 100644 test_openai_fix.php create mode 100644 tests/Feature/CaptchaSessionPersistenceTest.php diff --git a/app/Services/OpenAIService.php b/app/Services/OpenAIService.php index 09dda16..60e93aa 100644 --- a/app/Services/OpenAIService.php +++ b/app/Services/OpenAIService.php @@ -158,10 +158,12 @@ private function analyzeReviewsInParallelChunks(array $reviews): array $chunkResult = $this->parseOpenAIResponse($result, $chunk); if (isset($chunkResult['detailed_scores'])) { - $allDetailedScores = array_merge($allDetailedScores, $chunkResult['detailed_scores']); + $chunkScores = $chunkResult['detailed_scores']; + $allDetailedScores = array_merge($allDetailedScores, $chunkScores); + LoggingService::log('Successfully processed chunk '.($chunkIndex + 1).' ('.count($chunk).' reviews) with '.count($chunkScores).' scores'); + } else { + LoggingService::log('Chunk '.($chunkIndex + 1).' returned no scores'); } - - LoggingService::log('Successfully processed chunk '.($chunkIndex + 1).' ('.count($chunk).' reviews)'); } else { LoggingService::log('Error processing chunk '.($chunkIndex + 1).': HTTP '.$response->status()); // Continue with other chunks even if one fails @@ -249,11 +251,12 @@ private function cleanUtf8Text(string $text): string */ private function getOptimizedMaxTokens(int $reviewCount): int { - // Calculate based on expected output size: roughly 20 chars per review for JSON response - $baseTokens = $reviewCount * 8; // More conservative estimate + // GPT-4o-mini needs more tokens than GPT-4-turbo for the same output + // Calculate based on expected output size: roughly 25-30 chars per review for JSON response + $baseTokens = $reviewCount * 12; // Increased from 8 to 12 for GPT-4o-mini - // Add buffer but keep it minimal for speed - $buffer = min(500, $reviewCount * 2); + // Add larger buffer for GPT-4o-mini to prevent truncation + $buffer = min(1000, $reviewCount * 5); // Increased buffer return $baseTokens + $buffer; } @@ -268,7 +271,12 @@ private function parseOpenAIResponse($response, $reviews): array { $content = $response['choices'][0]['message']['content'] ?? ''; - LoggingService::log('Raw OpenAI response content: '.json_encode(['content' => $content])); + LoggingService::log('Raw OpenAI response content: '.json_encode(['content' => substr($content, 0, 200).'...'])); + + // Clean the content to handle markdown formatting + $content = preg_replace('/^```json\s*/', '', $content); + $content = preg_replace('/\s*```$/', '', $content); + $content = trim($content); // Try to extract JSON from the content if (preg_match('/\[[\s\S]*\]/', $content, $matches)) { @@ -278,6 +286,7 @@ private function parseOpenAIResponse($response, $reviews): array LoggingService::log('Extracted JSON string: '.json_encode(['json' => substr($jsonString, 0, 100).'...'])); try { + // Try to parse the complete JSON first $results = json_decode($jsonString, true); if (json_last_error() === JSON_ERROR_NONE && is_array($results)) { @@ -288,13 +297,18 @@ private function parseOpenAIResponse($response, $reviews): array } } + LoggingService::log('Successfully parsed complete JSON with '.count($detailedScores).' scores'); return [ 'detailed_scores' => $detailedScores, ]; } } catch (\Exception $e) { - LoggingService::log('Failed to parse OpenAI JSON response: '.$e->getMessage()); + LoggingService::log('Failed to parse complete JSON: '.$e->getMessage()); } + + // If complete JSON parsing fails, try to parse partial JSON + LoggingService::log('Attempting to parse partial JSON response'); + return $this->parsePartialJsonResponse($jsonString, $reviews); } } @@ -306,6 +320,51 @@ private function parseOpenAIResponse($response, $reviews): array ]; } + /** + * Parse partial JSON responses that may be truncated due to max_tokens limit + */ + private function parsePartialJsonResponse(string $jsonString, array $reviews): array + { + $detailedScores = []; + + // Fix common truncation issues + $jsonString = rtrim($jsonString, ','); + + // If the JSON doesn't end with ], try to close it + if (!str_ends_with(trim($jsonString), ']')) { + $jsonString = rtrim($jsonString, ',') . ']'; + } + + // Try parsing the fixed JSON + try { + $results = json_decode($jsonString, true); + + if (json_last_error() === JSON_ERROR_NONE && is_array($results)) { + foreach ($results as $result) { + if (isset($result['id']) && isset($result['score'])) { + $detailedScores[$result['id']] = (int) $result['score']; + } + } + + LoggingService::log('Successfully parsed partial JSON with '.count($detailedScores).' scores'); + return ['detailed_scores' => $detailedScores]; + } + } catch (\Exception $e) { + LoggingService::log('Failed to parse partial JSON: '.$e->getMessage()); + } + + // If still failing, try regex parsing individual entries + preg_match_all('/\{"id":"([^"]+)","score":(\d+)\}/', $jsonString, $matches, PREG_SET_ORDER); + + foreach ($matches as $match) { + $detailedScores[$match[1]] = (int) $match[2]; + } + + LoggingService::log('Regex parsing extracted '.count($detailedScores).' scores from partial response'); + + return ['detailed_scores' => $detailedScores]; + } + private function getMaxTokensForModel(string $model): int { // Map models to their max completion tokens diff --git a/test_openai_fix.php b/test_openai_fix.php new file mode 100644 index 0000000..37217e2 --- /dev/null +++ b/test_openai_fix.php @@ -0,0 +1,34 @@ +make(Illuminate\Contracts\Console\Kernel::class)->bootstrap(); + +try { + echo "Testing OpenAI service fix...\n"; + + $service = app(ReviewAnalysisService::class); + + // Test with a product that had the issue + $result = $service->analyzeProduct('B0C3QZ7SNF'); + + echo "Analysis completed successfully!\n"; + echo "Fake percentage: {$result['fake_percentage']}%\n"; + echo "Amazon rating: {$result['amazon_rating']}\n"; + echo "Adjusted rating: {$result['adjusted_rating']}\n"; + echo "Grade: {$result['grade']}\n"; + + if ($result['fake_percentage'] > 0) { + echo "✅ SUCCESS: Fix is working - detecting fake reviews!\n"; + } else { + echo "⚠️ WARNING: Still showing 0% fake - check logs for details\n"; + } + +} catch (Exception $e) { + echo "Error: " . $e->getMessage() . "\n"; + echo "Stack trace:\n" . $e->getTraceAsString() . "\n"; +} \ No newline at end of file diff --git a/tests/Feature/CaptchaSessionPersistenceTest.php b/tests/Feature/CaptchaSessionPersistenceTest.php new file mode 100644 index 0000000..d04fbc6 --- /dev/null +++ b/tests/Feature/CaptchaSessionPersistenceTest.php @@ -0,0 +1,349 @@ + 'recaptcha', + 'captcha.recaptcha.site_key' => 'test_site_key', + 'captcha.recaptcha.secret_key' => 'test_secret_key', + 'captcha.recaptcha.verify_url' => 'https://www.google.com/recaptcha/api/siteverify', + ]); + + // Create existing analysis data to avoid actual API calls + AsinData::create([ + 'asin' => 'B08N5WRWNW', + 'country' => 'us', + 'product_url' => 'https://www.amazon.com/dp/B08N5WRWNW', + 'reviews' => json_encode([ + ['id' => 0, 'rating' => 5, 'review_title' => 'Great!', 'review_text' => 'Great product', 'author' => 'John'], + ['id' => 1, 'rating' => 4, 'review_title' => 'Good', 'review_text' => 'Good product', 'author' => 'Jane'], + ]), + 'openai_result' => json_encode(['detailed_scores' => [0 => 25, 1 => 30]]), + 'fake_percentage' => 0.0, + 'amazon_rating' => 4.5, + 'adjusted_rating' => 4.5, + 'grade' => 'A', + 'explanation' => 'Test explanation', + 'status' => 'completed', + ]); + + AsinData::create([ + 'asin' => 'B081JLDJLB', + 'country' => 'us', + 'product_url' => 'https://www.amazon.com/dp/B081JLDJLB', + 'reviews' => json_encode([ + ['id' => 0, 'rating' => 3, 'review_title' => 'OK', 'review_text' => 'Average product', 'author' => 'Bob'], + ]), + 'openai_result' => json_encode(['detailed_scores' => [0 => 40]]), + 'fake_percentage' => 10.0, + 'amazon_rating' => 3.0, + 'adjusted_rating' => 3.2, + 'grade' => 'B', + 'explanation' => 'Second test explanation', + 'status' => 'completed', + ]); + } + + public function test_captcha_appears_on_first_submission_in_production() + { + // Mock CaptchaService for successful verification + $mockCaptchaService = $this->createMock(CaptchaService::class); + $mockCaptchaService->method('getProvider')->willReturn('recaptcha'); + $mockCaptchaService->method('getSiteKey')->willReturn('test_site_key'); + $mockCaptchaService->method('verify')->willReturn(true); + + App::instance(CaptchaService::class, $mockCaptchaService); + + // Set environment to production to enable captcha + App::shouldReceive('environment') + ->with('local') + ->andReturn(false); + + $component = Livewire::test(ReviewAnalyzer::class); + + // Initially, captcha_passed should be false + $component->assertSet('captcha_passed', false); + + // The component should render with captcha visible + $component->assertSee('g-recaptcha'); // This would be in the HTML if captcha is shown + } + + public function test_first_submission_requires_captcha_and_sets_passed_flag() + { + // Mock CaptchaService for successful verification + $mockCaptchaService = $this->createMock(CaptchaService::class); + $mockCaptchaService->method('getProvider')->willReturn('recaptcha'); + $mockCaptchaService->method('verify')->willReturn(true); + + App::instance(CaptchaService::class, $mockCaptchaService); + + // Mock ReviewAnalysisService to avoid API calls + $this->mockReviewAnalysisService(); + + // Set environment to production + App::shouldReceive('environment') + ->with('local') + ->andReturn(false); + + $component = Livewire::test(ReviewAnalyzer::class); + + // First submission: provide captcha response + $component->set('productUrl', 'https://www.amazon.com/dp/B08N5WRWNW') + ->set('g_recaptcha_response', 'valid_captcha_token') + ->call('analyze'); + + // After successful analysis with captcha, captcha_passed should be true + $component->assertSet('captcha_passed', true); + $component->assertSet('isAnalyzed', true); + $component->assertSet('error', null); + } + + public function test_second_submission_skips_captcha_validation() + { + // Mock CaptchaService - we'll verify it's NOT called for second submission + $mockCaptchaService = $this->createMock(CaptchaService::class); + $mockCaptchaService->method('getProvider')->willReturn('recaptcha'); + $mockCaptchaService->expects($this->once()) // Should only be called once + ->method('verify') + ->willReturn(true); + + App::instance(CaptchaService::class, $mockCaptchaService); + + // Mock ReviewAnalysisService + $this->mockReviewAnalysisService(); + + // Set environment to production + App::shouldReceive('environment') + ->with('local') + ->andReturn(false); + + $component = Livewire::test(ReviewAnalyzer::class); + + // First submission: complete captcha + $component->set('productUrl', 'https://www.amazon.com/dp/B08N5WRWNW') + ->set('g_recaptcha_response', 'valid_captcha_token') + ->call('analyze'); + + // Verify captcha is now passed + $component->assertSet('captcha_passed', true); + + // Second submission: different product, NO captcha response needed + $component->set('productUrl', 'https://www.amazon.com/dp/B081JLDJLB') + ->set('g_recaptcha_response', '') // Empty captcha response + ->call('analyze'); + + // Should succeed without requiring captcha + $component->assertSet('isAnalyzed', true); + $component->assertSet('error', null); + $component->assertSet('captcha_passed', true); // Should remain true + + // Verify analysis completed (don't check specific grade since mock routing may vary) + $this->assertNotNull($component->get('result')); + } + + public function test_captcha_failure_on_first_submission_keeps_captcha_required() + { + // Mock CaptchaService for failed verification + $mockCaptchaService = $this->createMock(CaptchaService::class); + $mockCaptchaService->method('getProvider')->willReturn('recaptcha'); + $mockCaptchaService->method('verify')->willReturn(false); // Captcha fails + + App::instance(CaptchaService::class, $mockCaptchaService); + + // Set environment to production + App::shouldReceive('environment') + ->with('local') + ->andReturn(false); + + $component = Livewire::test(ReviewAnalyzer::class); + + // First submission: provide invalid captcha response + $component->set('productUrl', 'https://www.amazon.com/dp/B08N5WRWNW') + ->set('g_recaptcha_response', 'invalid_captcha_token') + ->call('analyze'); + + // Should fail and captcha_passed should remain false + $component->assertSet('captcha_passed', false); + $component->assertSet('isAnalyzed', false); + $this->assertNotEmpty($component->get('error')); + // LoggingService converts the exception message, so check for generic error + $this->assertStringContainsString('try again', $component->get('error')); + } + + public function test_hcaptcha_session_persistence() + { + // Test the same behavior with hCaptcha + config(['captcha.provider' => 'hcaptcha']); + + $mockCaptchaService = $this->createMock(CaptchaService::class); + $mockCaptchaService->method('getProvider')->willReturn('hcaptcha'); + $mockCaptchaService->method('getSiteKey')->willReturn('test_hcaptcha_key'); + $mockCaptchaService->expects($this->once()) // Should only be called once + ->method('verify') + ->willReturn(true); + + App::instance(CaptchaService::class, $mockCaptchaService); + $this->mockReviewAnalysisService(); + + App::shouldReceive('environment') + ->with('local') + ->andReturn(false); + + $component = Livewire::test(ReviewAnalyzer::class); + + // First submission with hCaptcha + $component->set('productUrl', 'https://www.amazon.com/dp/B08N5WRWNW') + ->set('h_captcha_response', 'valid_hcaptcha_token') + ->call('analyze'); + + $component->assertSet('captcha_passed', true); + + // Second submission should skip captcha + $component->set('productUrl', 'https://www.amazon.com/dp/B081JLDJLB') + ->set('h_captcha_response', '') // No captcha needed + ->call('analyze'); + + $component->assertSet('isAnalyzed', true); + $component->assertSet('captcha_passed', true); + } + + public function test_local_environment_bypasses_captcha_completely() + { + // Mock services + $mockCaptchaService = $this->createMock(CaptchaService::class); + $mockCaptchaService->expects($this->never()) // Should never be called in local + ->method('verify'); + + App::instance(CaptchaService::class, $mockCaptchaService); + + // Set environment to local + App::shouldReceive('environment') + ->with('local') + ->andReturn(true); + + $component = Livewire::test(ReviewAnalyzer::class); + + // Test that the component allows the analyze method to proceed past captcha validation + // We'll use a partial test - just call validate to verify it doesn't require captcha + $component->set('productUrl', 'https://www.amazon.com/dp/B08N5WRWNW'); + + // The key test: in local environment, captcha should be bypassed + // This will fail if captcha is required, succeed if bypassed + try { + $component->call('analyze'); + // If we get here without a captcha error, the bypass worked + // The analysis may fail for other reasons (missing mocks), but captcha was bypassed + $captchaBypassed = true; + } catch (\Exception $e) { + // If the error is captcha-related, the bypass failed + $captchaBypassed = !str_contains($e->getMessage(), 'Captcha'); + } + + $this->assertTrue($captchaBypassed, 'Captcha should be bypassed in local environment'); + } + + public function test_captcha_state_persists_across_multiple_analyses() + { + $mockCaptchaService = $this->createMock(CaptchaService::class); + $mockCaptchaService->method('getProvider')->willReturn('recaptcha'); + $mockCaptchaService->expects($this->once()) // Only called on first submission + ->method('verify') + ->willReturn(true); + + App::instance(CaptchaService::class, $mockCaptchaService); + $this->mockReviewAnalysisService(); + + App::shouldReceive('environment') + ->with('local') + ->andReturn(false); + + $component = Livewire::test(ReviewAnalyzer::class); + + // First analysis + $component->set('productUrl', 'https://www.amazon.com/dp/B08N5WRWNW') + ->set('g_recaptcha_response', 'valid_token') + ->call('analyze'); + + $component->assertSet('captcha_passed', true); + $this->assertNotNull($component->get('result')); + + // Second analysis + $component->set('productUrl', 'https://www.amazon.com/dp/B081JLDJLB') + ->set('g_recaptcha_response', '') // No captcha needed + ->call('analyze'); + + $component->assertSet('captcha_passed', true); + $this->assertNotNull($component->get('result')); + + // Third analysis - same first product again + $component->set('productUrl', 'https://www.amazon.com/dp/B08N5WRWNW') + ->call('analyze'); + + $component->assertSet('captcha_passed', true); + $this->assertNotNull($component->get('result')); + } + + private function mockReviewAnalysisService() + { + $mockAnalysisService = $this->createMock(ReviewAnalysisService::class); + + $mockAnalysisService->method('checkProductExists') + ->willReturnCallback(function ($url) { + if (str_contains($url, 'B08N5WRWNW')) { + $asinData1 = AsinData::where('asin', 'B08N5WRWNW')->first(); + return [ + 'asin' => 'B08N5WRWNW', + 'country' => 'us', + 'product_url' => 'https://www.amazon.com/dp/B08N5WRWNW', + 'exists' => true, + 'asin_data' => $asinData1, + 'needs_fetching' => false, + 'needs_openai' => false, + ]; + } else { + $asinData2 = AsinData::where('asin', 'B081JLDJLB')->first(); + return [ + 'asin' => 'B081JLDJLB', + 'country' => 'us', + 'product_url' => 'https://www.amazon.com/dp/B081JLDJLB', + 'exists' => true, + 'asin_data' => $asinData2, + 'needs_fetching' => false, + 'needs_openai' => false, + ]; + } + }); + + $mockAnalysisService->method('calculateFinalMetrics') + ->willReturnCallback(function ($asinData) { + return [ + 'fake_percentage' => $asinData->fake_percentage, + 'amazon_rating' => $asinData->amazon_rating, + 'adjusted_rating' => $asinData->adjusted_rating, + 'grade' => $asinData->grade, + 'explanation' => $asinData->explanation, + 'asin_review' => $asinData, + ]; + }); + + App::instance(ReviewAnalysisService::class, $mockAnalysisService); + } +} \ No newline at end of file From 2220d9d8c6d64bbc2cf3e8b23dd2268451c6d575 Mon Sep 17 00:00:00 2001 From: SDH Date: Tue, 10 Jun 2025 16:41:28 -0400 Subject: [PATCH 14/24] Fixed OpenAI parsing error --- test_openai_fix.php | 34 ---------------------------------- 1 file changed, 34 deletions(-) delete mode 100644 test_openai_fix.php diff --git a/test_openai_fix.php b/test_openai_fix.php deleted file mode 100644 index 37217e2..0000000 --- a/test_openai_fix.php +++ /dev/null @@ -1,34 +0,0 @@ -make(Illuminate\Contracts\Console\Kernel::class)->bootstrap(); - -try { - echo "Testing OpenAI service fix...\n"; - - $service = app(ReviewAnalysisService::class); - - // Test with a product that had the issue - $result = $service->analyzeProduct('B0C3QZ7SNF'); - - echo "Analysis completed successfully!\n"; - echo "Fake percentage: {$result['fake_percentage']}%\n"; - echo "Amazon rating: {$result['amazon_rating']}\n"; - echo "Adjusted rating: {$result['adjusted_rating']}\n"; - echo "Grade: {$result['grade']}\n"; - - if ($result['fake_percentage'] > 0) { - echo "✅ SUCCESS: Fix is working - detecting fake reviews!\n"; - } else { - echo "⚠️ WARNING: Still showing 0% fake - check logs for details\n"; - } - -} catch (Exception $e) { - echo "Error: " . $e->getMessage() . "\n"; - echo "Stack trace:\n" . $e->getTraceAsString() . "\n"; -} \ No newline at end of file From 49fe98b87eff2df847624136b8cb2a48704d414f Mon Sep 17 00:00:00 2001 From: SDH Date: Wed, 11 Jun 2025 15:57:37 -0400 Subject: [PATCH 15/24] Issue 5 : Amazon URL validation was requiring a backend fetch request to amazon directly which was being throttled. This offloads that dependency to the browser end user session --- app/Livewire/ReviewAnalyzer.php | 21 ++ app/Services/Amazon/AmazonFetchService.php | 105 +----- app/Services/Amazon/AmazonScrapingService.php | 0 cloudflare-worker-proxy.js | 76 ++++ config/services.php | 3 + .../views/livewire/review-analyzer.blade.php | 350 +++++++++++++++++- 6 files changed, 462 insertions(+), 93 deletions(-) delete mode 100644 app/Services/Amazon/AmazonScrapingService.php create mode 100644 cloudflare-worker-proxy.js diff --git a/app/Livewire/ReviewAnalyzer.php b/app/Livewire/ReviewAnalyzer.php index bf1b81d..05099f9 100644 --- a/app/Livewire/ReviewAnalyzer.php +++ b/app/Livewire/ReviewAnalyzer.php @@ -80,6 +80,8 @@ public function mount() public function analyze() { LoggingService::log('=== LIVEWIRE ANALYZE METHOD STARTED ==='); + LoggingService::log('Product URL value: ' . ($this->productUrl ?? 'NULL')); + LoggingService::log('Product URL length: ' . strlen($this->productUrl ?? '')); try { // Loading state and progress are already initialized by initializeProgress() @@ -99,6 +101,15 @@ public function analyze() $this->adjusted_rating = 0.00; $this->isAnalyzed = false; + // Ensure productUrl is not empty before validation + if (empty($this->productUrl)) { + LoggingService::log('Product URL is empty before validation, attempting to get from input'); + // Try to get the value from the form if it exists + $this->productUrl = request()->input('productUrl', $this->productUrl); + } + + LoggingService::log('Final product URL before validation: ' . ($this->productUrl ?: 'EMPTY')); + // Validate input $this->validate([ 'productUrl' => 'required|url', @@ -272,7 +283,17 @@ public function startAnalysis() // Clear previous results $this->clearPreviousResults(); + // Force sync of input values (in case wire:model.live has timing issues) + $this->dispatch('syncInputs'); + // Run the analysis (JavaScript will handle progress simulation) $this->analyze(); } + + // Method to sync the URL from JavaScript if needed + public function setProductUrl($url) + { + $this->productUrl = $url; + LoggingService::log('Product URL set via JavaScript: ' . $url); + } } diff --git a/app/Services/Amazon/AmazonFetchService.php b/app/Services/Amazon/AmazonFetchService.php index 4057703..70c8fbd 100644 --- a/app/Services/Amazon/AmazonFetchService.php +++ b/app/Services/Amazon/AmazonFetchService.php @@ -44,12 +44,8 @@ public function fetchReviewsAndSave(string $asin, string $country, string $produ // Check if fetching failed and provide specific error message if (empty($reviewsData) || !isset($reviewsData['reviews'])) { - // Check if the error was due to ASIN validation failure vs API issues - if (!$this->validateAsinExistsFast($asin)) { - throw new \Exception('Product does not exist on Amazon.com (US) site. Please check the URL and try again.'); - } else { - throw new \Exception('Unable to fetch product reviews at this time. This could be due to network issues or the review service being temporarily unavailable. Please try again in a few moments.'); - } + // Since we're not doing server-side validation, assume it's an API issue + throw new \Exception('Unable to fetch product reviews at this time. This could be due to an invalid product URL, network issues, or the review service being temporarily unavailable. Please verify the Amazon URL and try again.'); } // Save to database - NO OpenAI analysis yet (will be done separately) @@ -67,22 +63,17 @@ public function fetchReviewsAndSave(string $asin, string $country, string $produ */ public function fetchReviewsOptimized(string $asin, string $country = 'us'): array { - // Skip validation for known working products to save 2 seconds - // Only validate if we haven't seen this ASIN recently - $shouldValidate = !$this->isRecentlyValidated($asin); - - if ($shouldValidate && !$this->validateAsinExistsFast($asin)) { - LoggingService::log('ASIN validation failed - product does not exist on amazon.com', [ - 'asin' => $asin, - 'url_checked' => "https://www.amazon.com/dp/{$asin}", + // Only do basic ASIN format validation - let client-side JS and Unwrangle API handle the rest + if (!$this->isValidAsinFormat($asin)) { + LoggingService::log('ASIN format validation failed', [ + 'asin' => $asin, ]); - return []; } - - if ($shouldValidate) { - $this->markAsValidated($asin); - } + + LoggingService::log('Server-side validation skipped - using client-side validation', [ + 'asin' => $asin, + ]); $apiKey = env('UNWRANGLE_API_KEY'); $cookie = env('UNWRANGLE_AMAZON_COOKIE'); @@ -175,82 +166,16 @@ public function fetchReviews(string $asin, string $country = 'us'): array return $this->fetchReviewsOptimized($asin, $country); } - /** - * Fast ASIN validation with shorter timeout - */ - private function validateAsinExistsFast(string $asin): bool - { - $url = "https://www.amazon.com/dp/{$asin}"; - - try { - // First try HEAD request for speed - $response = $this->httpClient->request('HEAD', $url, [ - 'timeout' => 3, // Very short timeout for validation - 'connect_timeout' => 1, - 'allow_redirects' => false, // Don't follow redirects for speed - ]); - - $statusCode = $response->getStatusCode(); - - // If HEAD request returns 405 (Method Not Allowed), try GET request - if ($statusCode === 405) { - LoggingService::log('HEAD request returned 405, trying GET request', [ - 'asin' => $asin, - 'url' => $url, - ]); - - $response = $this->httpClient->request('GET', $url, [ - 'timeout' => 5, // Slightly longer timeout for GET - 'connect_timeout' => 2, - 'allow_redirects' => false, - ]); - - $statusCode = $response->getStatusCode(); - } - - LoggingService::log('ASIN validation check', [ - 'asin' => $asin, - 'url' => $url, - 'status_code' => $statusCode, - ]); - - // Accept 200 (OK) and 3xx (redirect) as valid - return $statusCode >= 200 && $statusCode < 400; - } catch (\Exception $e) { - LoggingService::log('ASIN validation failed', [ - 'asin' => $asin, - 'error' => $e->getMessage(), - ]); - return false; - } - } /** - * Check if ASIN has been validated recently (in-memory cache) + * Validate ASIN format without hitting Amazon servers */ - private function isRecentlyValidated(string $asin): bool + private function isValidAsinFormat(string $asin): bool { - static $validatedAsins = []; - static $lastClear = 0; - - $now = time(); - - // Clear cache every 5 minutes - if ($now - $lastClear > 300) { - $validatedAsins = []; - $lastClear = $now; - } - - return isset($validatedAsins[$asin]) && ($now - $validatedAsins[$asin]) < 300; + // ASIN should be exactly 10 characters, alphanumeric + return preg_match('/^[A-Z0-9]{10}$/', $asin) === 1; } - /** - * Mark ASIN as validated (in-memory cache) - */ - private function markAsValidated(string $asin): void - { - static $validatedAsins = []; - $validatedAsins[$asin] = time(); - } + } diff --git a/app/Services/Amazon/AmazonScrapingService.php b/app/Services/Amazon/AmazonScrapingService.php deleted file mode 100644 index e69de29..0000000 diff --git a/cloudflare-worker-proxy.js b/cloudflare-worker-proxy.js new file mode 100644 index 0000000..771bc53 --- /dev/null +++ b/cloudflare-worker-proxy.js @@ -0,0 +1,76 @@ +export default { + async fetch(request, env, ctx) { + // Only allow requests from your domain + const allowedOrigins = ['https://yourdomain.com', 'https://nullfake.com']; + const origin = request.headers.get('Origin'); + + if (!allowedOrigins.includes(origin)) { + return new Response('Forbidden', { status: 403 }); + } + + // Extract ASIN from request + const url = new URL(request.url); + const asin = url.searchParams.get('asin'); + + if (!asin || !/^[A-Z0-9]{10}$/.test(asin)) { + return new Response('Invalid ASIN', { status: 400 }); + } + + // Add random delay to avoid patterns + const delay = Math.floor(Math.random() * 3000) + 1000; // 1-4 seconds + await new Promise(resolve => setTimeout(resolve, delay)); + + // Rotate user agents + const userAgents = [ + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15' + ]; + + const userAgent = userAgents[Math.floor(Math.random() * userAgents.length)]; + + try { + // Make request to Amazon + const amazonUrl = `https://www.amazon.com/dp/${asin}`; + const response = await fetch(amazonUrl, { + method: 'HEAD', + headers: { + 'User-Agent': userAgent, + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', + 'Accept-Language': 'en-US,en;q=0.5', + 'Accept-Encoding': 'gzip, deflate', + 'DNT': '1', + 'Connection': 'keep-alive', + }, + }); + + // Return just the status code + return new Response(JSON.stringify({ + asin: asin, + status_code: response.status, + timestamp: new Date().toISOString() + }), { + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': origin, + 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type', + } + }); + + } catch (error) { + return new Response(JSON.stringify({ + asin: asin, + error: error.message, + timestamp: new Date().toISOString() + }), { + status: 500, + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': origin, + } + }); + } + } +} \ No newline at end of file diff --git a/config/services.php b/config/services.php index 1f24f91..8693d93 100644 --- a/config/services.php +++ b/config/services.php @@ -45,4 +45,7 @@ 'chunk_size' => env('OPENAI_CHUNK_SIZE', 25), ], + // Amazon validation is now handled client-side only + // Server skips validation to avoid IP throttling + ]; diff --git a/resources/views/livewire/review-analyzer.blade.php b/resources/views/livewire/review-analyzer.blade.php index 4831f3f..67b8d9a 100644 --- a/resources/views/livewire/review-analyzer.blade.php +++ b/resources/views/livewire/review-analyzer.blade.php @@ -4,11 +4,13 @@
- + @error('productUrl')
{{ $message }}
@enderror +
@if(!app()->environment('local')) @@ -97,7 +99,8 @@ function renderHcaptcha() {
- @if(!app()->environment('local')) + @if(!app()->environment(['local', 'testing']))
@if($captcha->getProvider() === 'recaptcha') @if(!$captcha_passed) @@ -25,7 +25,7 @@ class="mt-1 w-full px-4 py-2 border rounded focus:outline-none focus:ring focus: