feat: add invoice
This commit is contained in:
parent
a01387bb09
commit
1216c8a073
|
|
@ -44,6 +44,10 @@ class PaymentTransactionInfolist
|
|||
->label(__('admin.payment_transactions.fields.processed_at'))
|
||||
->dateTime()
|
||||
->placeholder(__('admin.payment_transactions.placeholders.empty')),
|
||||
TextEntry::make('invoice_email_sent_at')
|
||||
->label(__('admin.payment_transactions.fields.invoice_email_sent_at'))
|
||||
->dateTime()
|
||||
->placeholder(__('admin.payment_transactions.placeholders.empty')),
|
||||
]),
|
||||
Section::make(__('admin.payment_transactions.sections.stripe'))
|
||||
->columns(2)
|
||||
|
|
@ -79,6 +83,18 @@ class PaymentTransactionInfolist
|
|||
->label(__('admin.payment_transactions.fields.stripe_price_id'))
|
||||
->copyable()
|
||||
->placeholder(__('admin.payment_transactions.placeholders.empty')),
|
||||
TextEntry::make('invoice_url')
|
||||
->label(__('admin.payment_transactions.fields.invoice_url'))
|
||||
->copyable()
|
||||
->url(fn ($state): ?string => $state)
|
||||
->openUrlInNewTab()
|
||||
->placeholder(__('admin.payment_transactions.placeholders.empty')),
|
||||
TextEntry::make('invoice_pdf_url')
|
||||
->label(__('admin.payment_transactions.fields.invoice_pdf_url'))
|
||||
->copyable()
|
||||
->url(fn ($state): ?string => $state)
|
||||
->openUrlInNewTab()
|
||||
->placeholder(__('admin.payment_transactions.placeholders.empty')),
|
||||
]),
|
||||
Section::make(__('admin.payment_transactions.sections.error'))
|
||||
->schema([
|
||||
|
|
|
|||
|
|
@ -72,6 +72,11 @@ class PaymentTransactionsTable
|
|||
->label(__('admin.payment_transactions.fields.error_message'))
|
||||
->limit(60)
|
||||
->toggleable(),
|
||||
TextColumn::make('invoice_email_sent_at')
|
||||
->label(__('admin.payment_transactions.fields.invoice_email_sent_at'))
|
||||
->dateTime()
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
])
|
||||
->filters([
|
||||
SelectFilter::make('status')
|
||||
|
|
|
|||
|
|
@ -51,11 +51,24 @@ class BillingController extends Controller
|
|||
];
|
||||
|
||||
if ($creditProduct->type === CreditProductType::MONTHLY) {
|
||||
if ($user->subscribed('default')) {
|
||||
return response()->json([
|
||||
'message' => __('api.billing.active_subscription_exists'),
|
||||
], 409);
|
||||
}
|
||||
|
||||
$checkout = $user
|
||||
->newSubscription('default', $creditProduct->stripe_price_id)
|
||||
->withMetadata($metadata)
|
||||
->checkout($sessionOptions);
|
||||
} else {
|
||||
$sessionOptions['invoice_creation'] = [
|
||||
'enabled' => true,
|
||||
'invoice_data' => [
|
||||
'metadata' => $metadata,
|
||||
],
|
||||
];
|
||||
|
||||
$checkout = $user->checkout([
|
||||
$creditProduct->stripe_price_id => 1,
|
||||
], $sessionOptions);
|
||||
|
|
@ -83,6 +96,12 @@ class BillingController extends Controller
|
|||
$user = $request->user();
|
||||
$this->ensureStripeCustomer($user);
|
||||
|
||||
if (! $user->subscribed('default')) {
|
||||
return response()->json([
|
||||
'message' => __('api.billing.no_active_subscription'),
|
||||
], 409);
|
||||
}
|
||||
|
||||
$portalUrl = $user->billingPortalUrl($this->redirectUrl('portal'));
|
||||
|
||||
return response()->json([
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ class UserResource extends JsonResource
|
|||
'role' => $this->role,
|
||||
'accountVerified' => $this->account_verified_at !== null,
|
||||
'aiCreditsBalance' => (int) $this->ai_credits_balance,
|
||||
'hasActiveSubscription' => $this->subscribed('default'),
|
||||
'suspendedAt' => $this->suspended_at?->toISOString(),
|
||||
'physicalActivityLevel' => $this->physical_activity_level,
|
||||
'physicalActivityLevelLabel' => $this->physical_activity_level?->getLabel(),
|
||||
|
|
|
|||
|
|
@ -8,12 +8,15 @@ use App\Models\CreditProduct;
|
|||
use App\Models\CreditLedgerEntry;
|
||||
use App\Models\User;
|
||||
use App\Services\AiCreditService;
|
||||
use App\Services\PaymentInvoiceEmailer;
|
||||
use App\Services\PaymentTransactionRecorder;
|
||||
use Laravel\Cashier\Cashier;
|
||||
|
||||
class GrantCreditsFromStripeWebhook
|
||||
{
|
||||
public function __construct(
|
||||
private AiCreditService $credits,
|
||||
private PaymentInvoiceEmailer $invoiceEmails,
|
||||
private PaymentTransactionRecorder $transactions,
|
||||
) {}
|
||||
|
||||
|
|
@ -106,8 +109,9 @@ class GrantCreditsFromStripeWebhook
|
|||
);
|
||||
|
||||
$entry ??= CreditLedgerEntry::query()->where('source', $source)->first();
|
||||
$invoice = $this->retrieveInvoice($this->stringValue($session['invoice'] ?? null));
|
||||
|
||||
$this->transactions->recordCheckoutSession(
|
||||
$transaction = $this->transactions->recordCheckoutSession(
|
||||
user: $user,
|
||||
product: $product,
|
||||
stripeCheckoutSessionId: $sessionId,
|
||||
|
|
@ -120,8 +124,12 @@ class GrantCreditsFromStripeWebhook
|
|||
currency: $this->stringValue($session['currency'] ?? null),
|
||||
creditsGranted: $entry ? $product->credits : 0,
|
||||
ledgerEntry: $entry,
|
||||
invoiceUrl: $invoice ? $this->stringValue($invoice['hosted_invoice_url'] ?? null) : null,
|
||||
invoicePdfUrl: $invoice ? $this->stringValue($invoice['invoice_pdf'] ?? null) : null,
|
||||
payload: $session,
|
||||
);
|
||||
|
||||
$this->invoiceEmails->sendIfAvailable($transaction);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -243,7 +251,7 @@ class GrantCreditsFromStripeWebhook
|
|||
|
||||
$entry ??= CreditLedgerEntry::query()->where('source', $source)->first();
|
||||
|
||||
$this->transactions->recordInvoice(
|
||||
$transaction = $this->transactions->recordInvoice(
|
||||
user: $user,
|
||||
product: $product,
|
||||
stripeInvoiceId: $invoiceId,
|
||||
|
|
@ -258,8 +266,12 @@ class GrantCreditsFromStripeWebhook
|
|||
currency: $this->stringValue($invoice['currency'] ?? null),
|
||||
creditsGranted: $entry ? $product->credits : 0,
|
||||
ledgerEntry: $entry,
|
||||
invoiceUrl: $this->stringValue($invoice['hosted_invoice_url'] ?? null),
|
||||
invoicePdfUrl: $this->stringValue($invoice['invoice_pdf'] ?? null),
|
||||
payload: $invoice,
|
||||
);
|
||||
|
||||
$this->invoiceEmails->sendIfAvailable($transaction);
|
||||
}
|
||||
|
||||
if (! $matchedLine) {
|
||||
|
|
@ -352,4 +364,23 @@ class GrantCreditsFromStripeWebhook
|
|||
{
|
||||
return is_numeric($value) ? (int) $value : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
private function retrieveInvoice(?string $invoiceId): ?array
|
||||
{
|
||||
if (! $invoiceId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return Cashier::stripe()
|
||||
->invoices
|
||||
->retrieve($invoiceId)
|
||||
->toArray();
|
||||
} catch (\Throwable) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,65 @@
|
|||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use App\Models\PaymentTransaction;
|
||||
use App\Models\User;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Mail\Mailables\Content;
|
||||
use Illuminate\Mail\Mailables\Envelope;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class PaymentInvoiceMail extends Mailable
|
||||
{
|
||||
use Queueable, SerializesModels;
|
||||
|
||||
public function __construct(
|
||||
public User $user,
|
||||
public PaymentTransaction $transaction,
|
||||
) {}
|
||||
|
||||
public function envelope(): Envelope
|
||||
{
|
||||
return new Envelope(
|
||||
subject: trans('mail.payment_invoice.subject', [], $this->mailLocale()),
|
||||
);
|
||||
}
|
||||
|
||||
public function content(): Content
|
||||
{
|
||||
return new Content(
|
||||
view: 'emails.payment-invoice',
|
||||
with: [
|
||||
'amount' => $this->formattedAmount(),
|
||||
'credits' => $this->transaction->credits_granted ?: $this->transaction->credits_expected,
|
||||
'invoicePdfUrl' => $this->transaction->invoice_pdf_url,
|
||||
'invoiceUrl' => $this->transaction->invoice_url,
|
||||
'locale' => $this->mailLocale(),
|
||||
'productName' => $this->transaction->creditProduct?->name,
|
||||
'userName' => $this->user->name,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
public function attachments(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
private function formattedAmount(): string
|
||||
{
|
||||
if ($this->transaction->amount === null) {
|
||||
return trans('mail.payment_invoice.unknown_amount', [], $this->mailLocale());
|
||||
}
|
||||
|
||||
return number_format($this->transaction->amount / 100, 2, ',', ' ')
|
||||
.' '
|
||||
.strtoupper($this->transaction->currency ?: 'eur');
|
||||
}
|
||||
|
||||
private function mailLocale(): string
|
||||
{
|
||||
return $this->user->preferredLocale() ?? 'en';
|
||||
}
|
||||
}
|
||||
|
|
@ -32,6 +32,9 @@ class PaymentTransaction extends Model
|
|||
'currency',
|
||||
'credits_expected',
|
||||
'credits_granted',
|
||||
'invoice_url',
|
||||
'invoice_pdf_url',
|
||||
'invoice_email_sent_at',
|
||||
'error_message',
|
||||
'payload',
|
||||
'processed_at',
|
||||
|
|
@ -60,6 +63,7 @@ class PaymentTransaction extends Model
|
|||
'amount' => 'integer',
|
||||
'credits_expected' => 'integer',
|
||||
'credits_granted' => 'integer',
|
||||
'invoice_email_sent_at' => 'datetime',
|
||||
'payload' => 'array',
|
||||
'processed_at' => 'datetime',
|
||||
];
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Mail\PaymentInvoiceMail;
|
||||
use App\Models\PaymentTransaction;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Throwable;
|
||||
|
||||
class PaymentInvoiceEmailer
|
||||
{
|
||||
public function sendIfAvailable(PaymentTransaction $transaction): void
|
||||
{
|
||||
if ($transaction->invoice_email_sent_at !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (! $transaction->user || ! $transaction->user->email) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (! $transaction->invoice_url && ! $transaction->invoice_pdf_url) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Mail::to($transaction->user->email)->send(
|
||||
new PaymentInvoiceMail($transaction->user, $transaction->loadMissing('creditProduct'))
|
||||
);
|
||||
} catch (Throwable $exception) {
|
||||
$transaction->forceFill([
|
||||
'error_message' => 'Invoice email failed: '.$exception->getMessage(),
|
||||
])->save();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$transaction->forceFill([
|
||||
'invoice_email_sent_at' => now(),
|
||||
])->save();
|
||||
}
|
||||
}
|
||||
|
|
@ -28,6 +28,8 @@ class PaymentTransactionRecorder
|
|||
?int $creditsExpected = null,
|
||||
?int $creditsGranted = null,
|
||||
?CreditLedgerEntry $ledgerEntry = null,
|
||||
?string $invoiceUrl = null,
|
||||
?string $invoicePdfUrl = null,
|
||||
?string $errorMessage = null,
|
||||
?array $payload = null,
|
||||
): PaymentTransaction {
|
||||
|
|
@ -47,6 +49,8 @@ class PaymentTransactionRecorder
|
|||
'currency' => $currency,
|
||||
'credits_expected' => $creditsExpected ?? $product?->credits,
|
||||
'credits_granted' => $creditsGranted ?? 0,
|
||||
'invoice_url' => $invoiceUrl,
|
||||
'invoice_pdf_url' => $invoicePdfUrl,
|
||||
'error_message' => $errorMessage,
|
||||
'payload' => $payload,
|
||||
'processed_at' => $status === PaymentTransactionStatus::PENDING ? null : now(),
|
||||
|
|
@ -72,6 +76,8 @@ class PaymentTransactionRecorder
|
|||
?int $creditsExpected = null,
|
||||
?int $creditsGranted = null,
|
||||
?CreditLedgerEntry $ledgerEntry = null,
|
||||
?string $invoiceUrl = null,
|
||||
?string $invoicePdfUrl = null,
|
||||
?string $errorMessage = null,
|
||||
?array $payload = null,
|
||||
): PaymentTransaction {
|
||||
|
|
@ -93,6 +99,8 @@ class PaymentTransactionRecorder
|
|||
'currency' => $currency,
|
||||
'credits_expected' => $creditsExpected ?? $product?->credits,
|
||||
'credits_granted' => $creditsGranted ?? 0,
|
||||
'invoice_url' => $invoiceUrl,
|
||||
'invoice_pdf_url' => $invoicePdfUrl,
|
||||
'error_message' => $errorMessage,
|
||||
'payload' => $payload,
|
||||
'processed_at' => now(),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('payment_transactions', function (Blueprint $table): void {
|
||||
$table->string('invoice_url', 2048)->nullable();
|
||||
$table->string('invoice_pdf_url', 2048)->nullable();
|
||||
$table->timestamp('invoice_email_sent_at')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('payment_transactions', function (Blueprint $table): void {
|
||||
$table->dropColumn([
|
||||
'invoice_url',
|
||||
'invoice_pdf_url',
|
||||
'invoice_email_sent_at',
|
||||
]);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -119,6 +119,9 @@ return [
|
|||
'amount' => 'Amount',
|
||||
'credits_expected' => 'Expected credits',
|
||||
'credits_granted' => 'Granted credits',
|
||||
'invoice_url' => 'Invoice URL',
|
||||
'invoice_pdf_url' => 'Invoice PDF',
|
||||
'invoice_email_sent_at' => 'Invoice email sent at',
|
||||
'stripe_event_id' => 'Stripe event ID',
|
||||
'stripe_event_type' => 'Event',
|
||||
'stripe_customer_id' => 'Stripe customer ID',
|
||||
|
|
|
|||
|
|
@ -31,6 +31,10 @@ return [
|
|||
'legal_documents' => [
|
||||
'not_found' => 'Legal document not found.',
|
||||
],
|
||||
'billing' => [
|
||||
'active_subscription_exists' => 'You already have an active subscription.',
|
||||
'no_active_subscription' => 'There is no active subscription to manage.',
|
||||
],
|
||||
'notifications' => [
|
||||
'test_title' => 'Test',
|
||||
'test_body' => 'Test notification from the Laravel API.',
|
||||
|
|
|
|||
|
|
@ -33,4 +33,19 @@ return [
|
|||
'password_confirmation' => 'Confirm password',
|
||||
'submit' => 'Update password',
|
||||
],
|
||||
'payment_invoice' => [
|
||||
'subject' => 'Your Bowli invoice',
|
||||
'title' => 'Your invoice is available',
|
||||
'greeting' => 'Hi :name,',
|
||||
'default_name' => 'there',
|
||||
'intro' => 'Thanks for your payment. Your summary and invoice link are below.',
|
||||
'product' => 'Product',
|
||||
'default_product' => 'Bowli credits',
|
||||
'credits' => 'Credits',
|
||||
'amount' => 'Amount',
|
||||
'unknown_amount' => 'Amount unavailable',
|
||||
'action' => 'View my invoice',
|
||||
'pdf_link' => 'PDF link:',
|
||||
'footer' => 'If you have any question, simply reply to this email.',
|
||||
],
|
||||
];
|
||||
|
|
|
|||
|
|
@ -119,6 +119,9 @@ return [
|
|||
'amount' => 'Montant',
|
||||
'credits_expected' => 'Crédits attendus',
|
||||
'credits_granted' => 'Crédits crédités',
|
||||
'invoice_url' => 'URL facture',
|
||||
'invoice_pdf_url' => 'PDF facture',
|
||||
'invoice_email_sent_at' => 'Email facture envoyé le',
|
||||
'stripe_event_id' => 'Stripe event ID',
|
||||
'stripe_event_type' => 'Événement',
|
||||
'stripe_customer_id' => 'Stripe customer ID',
|
||||
|
|
|
|||
|
|
@ -31,6 +31,10 @@ return [
|
|||
'legal_documents' => [
|
||||
'not_found' => 'Document légal introuvable.',
|
||||
],
|
||||
'billing' => [
|
||||
'active_subscription_exists' => 'Tu as déjà un abonnement en cours.',
|
||||
'no_active_subscription' => 'Aucun abonnement actif à gérer.',
|
||||
],
|
||||
'notifications' => [
|
||||
'test_title' => 'Test',
|
||||
'test_body' => 'Notification test depuis Laravel API.',
|
||||
|
|
|
|||
|
|
@ -33,4 +33,19 @@ return [
|
|||
'password_confirmation' => 'Confirmer le mot de passe',
|
||||
'submit' => 'Modifier le mot de passe',
|
||||
],
|
||||
'payment_invoice' => [
|
||||
'subject' => 'Ta facture Bowli',
|
||||
'title' => 'Ta facture est disponible',
|
||||
'greeting' => 'Bonjour :name,',
|
||||
'default_name' => 'à toi',
|
||||
'intro' => 'Merci pour ton paiement. Tu trouveras ci-dessous le récapitulatif et le lien vers ta facture.',
|
||||
'product' => 'Produit',
|
||||
'default_product' => 'Crédits Bowli',
|
||||
'credits' => 'Crédits',
|
||||
'amount' => 'Montant',
|
||||
'unknown_amount' => 'Montant indisponible',
|
||||
'action' => 'Voir ma facture',
|
||||
'pdf_link' => 'Lien PDF :',
|
||||
'footer' => 'Si tu as une question, réponds simplement à cet email.',
|
||||
],
|
||||
];
|
||||
|
|
|
|||
|
|
@ -0,0 +1,87 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="{{ $locale }}">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{{ trans('mail.payment_invoice.subject', [], $locale) }}</title>
|
||||
</head>
|
||||
<body style="margin: 0; padding: 0; background: #f5efe6; color: #232323; font-family: Arial, sans-serif;">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background: #f5efe6; padding: 32px 16px;">
|
||||
<tr>
|
||||
<td align="center">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="max-width: 560px; background: #ffffff; border-radius: 8px; overflow: hidden;">
|
||||
<tr>
|
||||
<td style="padding: 32px;">
|
||||
<p style="margin: 0 0 8px; color: #426fb3; font-size: 14px; font-weight: 700; text-transform: uppercase;">
|
||||
Bowli
|
||||
</p>
|
||||
|
||||
<h1 style="margin: 0 0 16px; color: #000000; font-size: 28px; line-height: 34px;">
|
||||
{{ trans('mail.payment_invoice.title', [], $locale) }}
|
||||
</h1>
|
||||
|
||||
<p style="margin: 0 0 16px; color: #232323; font-size: 16px; line-height: 24px;">
|
||||
{{ trans('mail.payment_invoice.greeting', ['name' => $userName ?: trans('mail.payment_invoice.default_name', [], $locale)], $locale) }}
|
||||
</p>
|
||||
|
||||
<p style="margin: 0 0 20px; color: #232323; font-size: 16px; line-height: 24px;">
|
||||
{{ trans('mail.payment_invoice.intro', [], $locale) }}
|
||||
</p>
|
||||
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="margin: 0 0 24px; border-collapse: collapse;">
|
||||
<tr>
|
||||
<td style="padding: 10px 0; color: #666666; font-size: 14px;">
|
||||
{{ trans('mail.payment_invoice.product', [], $locale) }}
|
||||
</td>
|
||||
<td align="right" style="padding: 10px 0; color: #232323; font-size: 14px; font-weight: 700;">
|
||||
{{ $productName ?: trans('mail.payment_invoice.default_product', [], $locale) }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 10px 0; color: #666666; font-size: 14px; border-top: 1px solid #eeeeee;">
|
||||
{{ trans('mail.payment_invoice.credits', [], $locale) }}
|
||||
</td>
|
||||
<td align="right" style="padding: 10px 0; color: #232323; font-size: 14px; font-weight: 700; border-top: 1px solid #eeeeee;">
|
||||
{{ $credits }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 10px 0; color: #666666; font-size: 14px; border-top: 1px solid #eeeeee;">
|
||||
{{ trans('mail.payment_invoice.amount', [], $locale) }}
|
||||
</td>
|
||||
<td align="right" style="padding: 10px 0; color: #232323; font-size: 14px; font-weight: 700; border-top: 1px solid #eeeeee;">
|
||||
{{ $amount }}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
@if ($invoiceUrl)
|
||||
<table role="presentation" cellpadding="0" cellspacing="0" style="margin: 0 0 24px;">
|
||||
<tr>
|
||||
<td style="background: #000000; border-radius: 999px;">
|
||||
<a href="{{ $invoiceUrl }}" style="display: inline-block; padding: 14px 24px; color: #ffffff; font-size: 16px; font-weight: 700; text-decoration: none;">
|
||||
{{ trans('mail.payment_invoice.action', [], $locale) }}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@endif
|
||||
|
||||
@if ($invoicePdfUrl)
|
||||
<p style="margin: 0 0 16px; color: #666666; font-size: 14px; line-height: 22px;">
|
||||
{{ trans('mail.payment_invoice.pdf_link', [], $locale) }}
|
||||
<a href="{{ $invoicePdfUrl }}" style="color: #426fb3;">{{ $invoicePdfUrl }}</a>
|
||||
</p>
|
||||
@endif
|
||||
|
||||
<p style="margin: 0; color: #666666; font-size: 14px; line-height: 22px;">
|
||||
{{ trans('mail.payment_invoice.footer', [], $locale) }}
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Reference in New Issue