Collecting Product Reviews via Email
Automatic review requests after purchase are standard practice for e-commerce and SaaS. The email arrives N days after payment, contains a direct rating link, a text review form, and redirects to review platforms (Google, Trustpilot, Yandex).
Architecture
Payment → Job: ScheduleReviewRequest (delay: +14 days)
→ Email with rating buttons
→ Click → save rating → redirect to platform
Backend: Scheduling and Processing
// On successful payment
class HandlePaymentSucceeded
{
public function handle(PaymentSucceeded $event): void
{
// Delay email for 14 days — give time to use product
SendReviewRequestEmail::dispatch($event->order)->delay(now()->addDays(14));
}
}
// Job to send email
class SendReviewRequestEmail implements ShouldQueue
{
public function handle(): void
{
// Don't send if already reviewed
if ($this->order->review()->exists()) return;
// Don't send if issued refund
if ($this->order->refund()->exists()) return;
$token = ReviewToken::create([
'order_id' => $this->order->id,
'user_id' => $this->order->user_id,
'expires_at' => now()->addDays(30),
]);
Mail::to($this->order->user->email)->send(new ReviewRequestMail($this->order, $token));
}
}
Email Template with Direct Rating in Email
<!-- In email — rating buttons directly in body -->
<p>How was your purchase of "{{ $order->product->name }}"?</p>
<table>
<tr>
@foreach([1,2,3,4,5] as $star)
<td>
<a href="{{ route('reviews.rate', ['token' => $token->id, 'score' => $star]) }}"
style="display:inline-block; padding:8px 12px; background:#f3f4f6; border-radius:4px; text-decoration:none;">
{{ str_repeat('★', $star) }}{{ str_repeat('☆', 5 - $star) }}
</a>
</td>
@endforeach
</tr>
</table>
Processing Rating Click
// ReviewController
public function rate(ReviewToken $token, int $score): View|RedirectResponse
{
if ($token->isExpired() || $token->isUsed()) {
return redirect()->route('home')->with('error', 'Link expired');
}
// Save preliminary rating
session(['review_score' => $score, 'review_token' => $token->id]);
if ($score >= 4) {
// High rating → redirect to Google/Trustpilot for public review
$token->markPartiallyUsed($score);
return redirect(config('reviews.google_url') . '?hl=en');
}
// Low rating → show form for private review
return view('reviews.form', compact('token', 'score'));
}
public function submit(Request $request, ReviewToken $token): JsonResponse
{
$request->validate(['score' => 'required|integer|min:1|max:5', 'comment' => 'nullable|string|max:2000']);
Review::create([
'order_id' => $token->order_id,
'user_id' => $token->user_id,
'score' => $request->score,
'comment' => $request->comment,
'is_public' => $request->score >= 4,
]);
$token->markUsed();
// If low rating — notify support
if ($request->score <= 2) {
Notification::route('slack', '#reviews')->notify(new LowReviewNotification($token->order, $request->score));
}
return response()->json(['success' => true]);
}
External Review Platforms
| Platform | Redirect URL | Region |
|---|---|---|
| Google Business | maps.google.com/maps?cid=...&action=write-review |
Global |
| Trustpilot | trustpilot.com/evaluate/site.com |
Global |
| Yandex Maps | yandex.ru/maps/org/.../reviews/add/ |
Russia/CIS |
| 2GIS | 2gis.ru/... |
Russia/CIS |
Timeline
A review collection system with delayed sending, direct rating in email, and platform routing: 3–4 business days.







