Invoice system working
This commit is contained in:
@@ -9,8 +9,7 @@ use App\Models\Product;
|
||||
use App\Models\PrintStock;
|
||||
use App\Services\ShippingService;
|
||||
use App\Services\InvoiceService;
|
||||
use App\Mail\InvoiceEmail;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use App\Services\MailjetService;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class OrderController extends Controller
|
||||
@@ -574,19 +573,35 @@ class OrderController extends Controller
|
||||
$product->save();
|
||||
}
|
||||
|
||||
// Generate invoice PDF
|
||||
// Generate invoice PDF and send email via Mailjet
|
||||
try {
|
||||
$invoicePath = InvoiceService::generateInvoice($order);
|
||||
$fullPath = storage_path('app/public/' . $invoicePath);
|
||||
|
||||
// Send invoice email via Mailjet
|
||||
Mail::to($order->customer_email)
|
||||
->send(new InvoiceEmail($order, $invoicePath));
|
||||
// Send invoice email directly via Mailjet API (no queue needed)
|
||||
$mailjetService = new MailjetService();
|
||||
$customerName = $order->customer_name ?? explode('@', $order->customer_email)[0];
|
||||
|
||||
\Log::info('Yoco Webhook: Invoice generated and email sent', [
|
||||
'order_uuid' => $orderUuid,
|
||||
'order_id' => $order->id,
|
||||
'invoice_path' => $invoicePath,
|
||||
]);
|
||||
$success = $mailjetService->send(
|
||||
toEmail: $order->customer_email,
|
||||
toName: $customerName,
|
||||
subject: 'Invoice #' . $order->order_number . ' - ADDITIONAL DESIGN',
|
||||
htmlContent: $this->getBasicInvoiceHtml($order),
|
||||
attachments: [$fullPath]
|
||||
);
|
||||
|
||||
if ($success) {
|
||||
\Log::info('Yoco Webhook: Invoice generated and email sent via Mailjet', [
|
||||
'order_uuid' => $orderUuid,
|
||||
'order_id' => $order->id,
|
||||
'invoice_path' => $invoicePath,
|
||||
]);
|
||||
} else {
|
||||
\Log::warning('Yoco Webhook: Mailjet email send returned false', [
|
||||
'order_uuid' => $orderUuid,
|
||||
'order_id' => $order->id,
|
||||
]);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
\Log::error('Yoco Webhook: Invoice generation or email failed', [
|
||||
'order_uuid' => $orderUuid,
|
||||
@@ -716,4 +731,55 @@ class OrderController extends Controller
|
||||
// Successful search - no rate limit increment
|
||||
return view('track-order-result', ['order' => $order]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate basic HTML template for invoice email
|
||||
*/
|
||||
private function getBasicInvoiceHtml(Order $order): string
|
||||
{
|
||||
return <<<HTML
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; color: #333; }
|
||||
.header { background-color: #f5f5f5; padding: 20px; text-align: center; }
|
||||
.content { padding: 20px; }
|
||||
.footer { background-color: #f5f5f5; padding: 20px; text-align: center; font-size: 12px; }
|
||||
table { width: 100%; border-collapse: collapse; margin: 20px 0; }
|
||||
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
|
||||
th { background-color: #f5f5f5; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1>Invoice #{$order->order_number}</h1>
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>Dear {$order->customer_name},</p>
|
||||
<p>Please find your invoice attached to this email.</p>
|
||||
<h3>Order Details</h3>
|
||||
<table>
|
||||
<tr>
|
||||
<th>Order Number</th>
|
||||
<td>{$order->order_number}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Order Date</th>
|
||||
<td>{$order->created_at->format('d M Y')}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Total Amount</th>
|
||||
<td>R {$order->total}</td>
|
||||
</tr>
|
||||
</table>
|
||||
<p>Thank you for your order!</p>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<p>© 2025 ADDITIONAL DESIGN. All rights reserved.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
HTML;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Models\Order;
|
||||
use App\Services\MailjetService;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\View;
|
||||
|
||||
class SendInvoiceEmail implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public int $tries = 3;
|
||||
public int $backoff = 60;
|
||||
|
||||
public function __construct(
|
||||
public Order $order,
|
||||
public string $invoicePath
|
||||
) {}
|
||||
|
||||
public function handle(MailjetService $mailjetService): void
|
||||
{
|
||||
try {
|
||||
// Render the email view with proper view isolation
|
||||
// Use ViewNotFoundException handling for nested views
|
||||
try {
|
||||
$htmlContent = View::make('emails.invoice', [
|
||||
'order' => $this->order,
|
||||
'invoiceNumber' => $this->order->order_number,
|
||||
])->render();
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
// If the mail::message namespace isn't available, render without it
|
||||
// This is a fallback for when the mail views aren't published
|
||||
Log::warning('Mail views not published, using basic HTML template', [
|
||||
'order_id' => $this->order->id,
|
||||
]);
|
||||
$htmlContent = $this->getBasicInvoiceHtml();
|
||||
}
|
||||
|
||||
// Get customer name (fallback to email if name not available)
|
||||
$customerName = $this->order->customer_name ?? explode('@', $this->order->customer_email)[0];
|
||||
|
||||
// Send via Mailjet
|
||||
$success = $mailjetService->send(
|
||||
toEmail: $this->order->customer_email,
|
||||
toName: $customerName,
|
||||
subject: 'Invoice #' . $this->order->order_number . ' - ADDITIONAL DESIGN',
|
||||
htmlContent: $htmlContent,
|
||||
attachments: [$this->invoicePath]
|
||||
);
|
||||
|
||||
if (!$success) {
|
||||
throw new \Exception('Mailjet send returned false');
|
||||
}
|
||||
|
||||
Log::info('Invoice email sent successfully via Mailjet', [
|
||||
'order_id' => $this->order->id,
|
||||
'order_number' => $this->order->order_number,
|
||||
'customer_email' => $this->order->customer_email,
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Failed to send invoice email via Mailjet', [
|
||||
'order_id' => $this->order->id,
|
||||
'order_number' => $this->order->order_number,
|
||||
'customer_email' => $this->order->customer_email,
|
||||
'error' => $e->getMessage(),
|
||||
'attempt' => $this->attempts(),
|
||||
]);
|
||||
|
||||
// Re-throw to trigger queue retry mechanism
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fallback basic HTML template for invoice email
|
||||
*/
|
||||
private function getBasicInvoiceHtml(): string
|
||||
{
|
||||
return <<<HTML
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; color: #333; }
|
||||
.header { background-color: #f5f5f5; padding: 20px; text-align: center; }
|
||||
.content { padding: 20px; }
|
||||
.footer { background-color: #f5f5f5; padding: 20px; text-align: center; font-size: 12px; }
|
||||
table { width: 100%; border-collapse: collapse; margin: 20px 0; }
|
||||
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
|
||||
th { background-color: #f5f5f5; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1>Invoice #{$this->order->order_number}</h1>
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>Dear {$this->order->customer_name ?: 'Customer'},</p>
|
||||
<p>Please find your invoice attached to this email.</p>
|
||||
<h3>Order Details</h3>
|
||||
<table>
|
||||
<tr>
|
||||
<th>Order Number</th>
|
||||
<td>{$this->order->order_number}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Order Date</th>
|
||||
<td>{$this->order->created_at->format('d M Y')}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Total Amount</th>
|
||||
<td>R {$this->order->total}</td>
|
||||
</tr>
|
||||
</table>
|
||||
<p>Thank you for your order!</p>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<p>© 2025 ADDITIONAL DESIGN. All rights reserved.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
HTML;
|
||||
}
|
||||
|
||||
public function failed(\Throwable $exception): void
|
||||
{
|
||||
Log::critical('Invoice email job failed permanently after all retries', [
|
||||
'order_id' => $this->order->id,
|
||||
'order_number' => $this->order->order_number,
|
||||
'customer_email' => $this->order->customer_email,
|
||||
'error' => $exception->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -36,8 +36,8 @@ class InvoiceService
|
||||
throw new \Exception('Failed to create directory: ' . $directory);
|
||||
}
|
||||
|
||||
// Create filename: INV-{order_number}-{uuid}.pdf
|
||||
$filename = 'INV-' . $order->order_number . '-' . $order->uuid . '.pdf';
|
||||
// Create filename: INV-{order_number}.pdf
|
||||
$filename = 'INV-' . $order->order_number . '.pdf';
|
||||
$path = "{$directory}/{$filename}";
|
||||
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Mailjet\Client;
|
||||
use Mailjet\Resources;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class MailjetService
|
||||
{
|
||||
protected Client $client;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->client = new Client(
|
||||
getenv('MAILJET_APIKEY_PUBLIC'),
|
||||
getenv('MAILJET_SECRETKEY'),
|
||||
true,
|
||||
['version' => 'v3.1']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an email via Mailjet API
|
||||
*
|
||||
* @param string $toEmail Recipient email address
|
||||
* @param string $toName Recipient name
|
||||
* @param string $subject Email subject
|
||||
* @param string $htmlContent HTML email content
|
||||
* @param string|null $textContent Plain text email content
|
||||
* @param array|null $attachments Array of file paths to attach
|
||||
* @param array|null $attachmentNames Optional custom filenames for attachments (by file path)
|
||||
* @return bool Success status
|
||||
*/
|
||||
public function send(
|
||||
string $toEmail,
|
||||
string $toName,
|
||||
string $subject,
|
||||
string $htmlContent,
|
||||
?string $textContent = null,
|
||||
?array $attachments = null,
|
||||
?array $attachmentNames = null
|
||||
): bool {
|
||||
try {
|
||||
$body = [
|
||||
'Messages' => [
|
||||
[
|
||||
'From' => [
|
||||
'Email' => config('mail.from.address'),
|
||||
'Name' => config('mail.from.name'),
|
||||
],
|
||||
'To' => [
|
||||
[
|
||||
'Email' => $toEmail,
|
||||
'Name' => $toName,
|
||||
]
|
||||
],
|
||||
'Subject' => $subject,
|
||||
'HTMLPart' => $htmlContent,
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
// Add text part if provided
|
||||
if ($textContent) {
|
||||
$body['Messages'][0]['TextPart'] = $textContent;
|
||||
}
|
||||
|
||||
// Add attachments if provided
|
||||
if ($attachments && is_array($attachments)) {
|
||||
$body['Messages'][0]['Attachments'] = [];
|
||||
foreach ($attachments as $filePath) {
|
||||
if (file_exists($filePath)) {
|
||||
$fileContent = file_get_contents($filePath);
|
||||
// Use custom filename if provided, otherwise use basename
|
||||
$filename = $attachmentNames[$filePath] ?? basename($filePath);
|
||||
$body['Messages'][0]['Attachments'][] = [
|
||||
'ContentType' => mime_content_type($filePath) ?: 'application/octet-stream',
|
||||
'Filename' => $filename,
|
||||
'Base64Content' => base64_encode($fileContent),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$response = $this->client->post(Resources::$Email, ['body' => $body]);
|
||||
|
||||
if ($response->success()) {
|
||||
Log::info('Mailjet email sent successfully', [
|
||||
'to' => $toEmail,
|
||||
'subject' => $subject,
|
||||
'response' => $response->getData(),
|
||||
]);
|
||||
return true;
|
||||
} else {
|
||||
Log::error('Mailjet email failed', [
|
||||
'to' => $toEmail,
|
||||
'subject' => $subject,
|
||||
'error' => $response->getStatus(),
|
||||
'data' => $response->getData(),
|
||||
]);
|
||||
return false;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Mailjet email exception', [
|
||||
'to' => $toEmail,
|
||||
'subject' => $subject,
|
||||
'exception' => $e->getMessage(),
|
||||
]);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user