Enable invoice emailing
This commit is contained in:
parent
cbffda959d
commit
32c562cf30
58
app/Console/Commands/InvoiceEmail.php
Normal file
58
app/Console/Commands/InvoiceEmail.php
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Mail;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
|
use App\Models\Invoice;
|
||||||
|
|
||||||
|
class InvoiceEmail extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'invoice:email {id}';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Email Invoices to be client';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new command instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
$o = Invoice::findOrFail($this->argument('id'));
|
||||||
|
|
||||||
|
Mail::to($o->account->user->email)->send(new \App\Mail\InvoiceEmail($o));
|
||||||
|
|
||||||
|
if (Mail::failures()) {
|
||||||
|
dump('Failure?');
|
||||||
|
|
||||||
|
dump(Mail::failures());
|
||||||
|
|
||||||
|
} else {
|
||||||
|
$o->print_status = TRUE;
|
||||||
|
$o->reminders = $o->reminders('send');
|
||||||
|
$o->save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -2,8 +2,11 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use Clarkeash\Doorman\Exceptions\{DoormanException,ExpiredInviteCode};
|
||||||
|
use Clarkeash\Doorman\Facades\Doorman;
|
||||||
use Illuminate\Contracts\View\Factory;
|
use Illuminate\Contracts\View\Factory;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
use Illuminate\View\View;
|
use Illuminate\View\View;
|
||||||
use Barryvdh\Snappy\Facades\SnappyPdf as PDF;
|
use Barryvdh\Snappy\Facades\SnappyPdf as PDF;
|
||||||
|
|
||||||
@ -14,7 +17,8 @@ class UserHomeController extends Controller
|
|||||||
{
|
{
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$this->middleware('auth');
|
// Route protection is in routes.web
|
||||||
|
// $this->middleware('auth');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -65,6 +69,32 @@ class UserHomeController extends Controller
|
|||||||
return PDF::loadView('u.invoice', ['o'=>$o])->stream(sprintf('%s.pdf',$o->invoice_account_id));
|
return PDF::loadView('u.invoice', ['o'=>$o])->stream(sprintf('%s.pdf',$o->invoice_account_id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable the user to down an invoice by providing a link in email
|
||||||
|
*
|
||||||
|
* @param Invoice $o
|
||||||
|
* @param string $code
|
||||||
|
* @return \Illuminate\Http\RedirectResponse|mixed
|
||||||
|
*/
|
||||||
|
public function invoice_pdf_email(Invoice $o,string $code)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
Doorman::redeem($code,$o->account->user->email);
|
||||||
|
|
||||||
|
} catch (ExpiredInviteCode $e) {
|
||||||
|
Log::alert(sprintf('User is using an expired token for invoice [%s] using [%s]',$o->id,$code));
|
||||||
|
|
||||||
|
return redirect()->to('/login');
|
||||||
|
|
||||||
|
} catch (DoormanException $e) {
|
||||||
|
Log::alert(sprintf('An attempt to read invoice id [%s] using [%s]',$o->id,$code));
|
||||||
|
|
||||||
|
abort(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->invoice_pdf($o);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper to redirect to the old site, when functions are not available in this one.
|
* Helper to redirect to the old site, when functions are not available in this one.
|
||||||
*
|
*
|
||||||
|
47
app/Mail/InvoiceEmail.php
Normal file
47
app/Mail/InvoiceEmail.php
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Mail;
|
||||||
|
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Mail\Mailable;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
|
||||||
|
use App\Models\Invoice;
|
||||||
|
|
||||||
|
class InvoiceEmail extends Mailable
|
||||||
|
{
|
||||||
|
use Queueable, SerializesModels;
|
||||||
|
|
||||||
|
public $invoice;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new message instance.
|
||||||
|
*
|
||||||
|
* @param Invoice $o
|
||||||
|
* @param string $notes
|
||||||
|
*/
|
||||||
|
public function __construct(Invoice $o)
|
||||||
|
{
|
||||||
|
$this->invoice = $o;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the message.
|
||||||
|
*
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function build()
|
||||||
|
{
|
||||||
|
return $this
|
||||||
|
->markdown('email.user.invoice')
|
||||||
|
->subject(sprintf( 'Invoice: %s - Total: $%s - Due: %s',
|
||||||
|
$this->invoice->id,
|
||||||
|
number_format($this->invoice->total,2),
|
||||||
|
$this->invoice->date_due))
|
||||||
|
->with([
|
||||||
|
'user'=>$this->invoice->account->user,
|
||||||
|
'site'=>$this->invoice->account->user->site,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
@ -3,10 +3,13 @@
|
|||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
|
use Clarkeash\Doorman\Facades\Doorman;
|
||||||
|
use Clarkeash\Doorman\Models\Invite;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
use App\Traits\NextKey;
|
use App\Traits\NextKey;
|
||||||
use App\Traits\PushNew;
|
use App\Traits\PushNew;
|
||||||
|
use Illuminate\Support\Arr;
|
||||||
|
|
||||||
class Invoice extends Model
|
class Invoice extends Model
|
||||||
{
|
{
|
||||||
@ -52,7 +55,7 @@ class Invoice extends Model
|
|||||||
|
|
||||||
public function items()
|
public function items()
|
||||||
{
|
{
|
||||||
return $this->hasMany(InvoiceItem::class);
|
return $this->hasMany(InvoiceItem::class)->where('active',1);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function paymentitems()
|
public function paymentitems()
|
||||||
@ -154,6 +157,29 @@ class Invoice extends Model
|
|||||||
return $this->account->country->currency;
|
return $this->account->country->currency;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a download link for non-auth downloads
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function download_link(): string
|
||||||
|
{
|
||||||
|
// Re-use an existing code
|
||||||
|
$io = Invite::where('for',$this->account->user->email)->first();
|
||||||
|
|
||||||
|
$tokendate = ($x=Carbon::now()->addDays(21)) > ($y=$this->due_date->addDays(21)) ? $x : $y;
|
||||||
|
|
||||||
|
// Extend the expire date
|
||||||
|
if ($io AND ($tokendate > $io->valid_until)) {
|
||||||
|
$io->valid_until = $tokendate;
|
||||||
|
$io->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
$code = (! $io) ? Doorman::generate()->for($this->account->user->email)->uses(0)->expiresOn($tokendate)->make() : $io->code;
|
||||||
|
|
||||||
|
return url('u/invoice',[$this->id,'email',$code]);
|
||||||
|
}
|
||||||
|
|
||||||
public function products()
|
public function products()
|
||||||
{
|
{
|
||||||
$return = collect();
|
$return = collect();
|
||||||
@ -196,6 +222,20 @@ class Invoice extends Model
|
|||||||
})->filter()->sortBy('item_type');
|
})->filter()->sortBy('item_type');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $key
|
||||||
|
* @return string
|
||||||
|
* @todo Ugly hack to update reminders
|
||||||
|
*/
|
||||||
|
public function reminders(string $key) {
|
||||||
|
$r = unserialize($this->reminders);
|
||||||
|
|
||||||
|
if (! Arr::get($r,$key)) {
|
||||||
|
$r[$key] = time();
|
||||||
|
return serialize($r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Automatically set our due_date at save time.
|
* Automatically set our due_date at save time.
|
||||||
*
|
*
|
||||||
|
65
config/doorman.php
Normal file
65
config/doorman.php
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Invite Table Name
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
'invite_table_name' => 'invites',
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Invite Model Class
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option allows you to override the default model.
|
||||||
|
| Your model MUST extend the base Invite model.
|
||||||
|
|
|
||||||
|
| Default: Clarkeash\Doorman\Models\Invite::class
|
||||||
|
*/
|
||||||
|
'invite_model' => Clarkeash\Doorman\Models\Invite::class,
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default Code Generator
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option controls how the invite codes are generated.
|
||||||
|
| You should adjust this based on your needs.
|
||||||
|
|
|
||||||
|
| Supported: "basic", "uuid"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
'driver' => env('DOORMAN_DRIVER', 'basic'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Driver Configurations
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here are each of the driver configurations for your application.
|
||||||
|
| You can customize should your application require it.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
'basic' => [
|
||||||
|
'length' => 6,
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| UUID
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| supported versions: 1,3,4,5
|
||||||
|
|
|
||||||
|
| Versions 3 & 5, require 'namespace' and 'name' to be set
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
'uuid' => [
|
||||||
|
'version' => 4,
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
25
resources/views/email/user/invoice.blade.php
Normal file
25
resources/views/email/user/invoice.blade.php
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
@component('mail::message',['site'=>$site,'heading'=>'Invoice: '.$invoice->id])
|
||||||
|
Hi {{ isset($user) ? $user->name.',' : '' }}
|
||||||
|
|
||||||
|
A new invoice has been generated on your account. A summary of that invoice is below.
|
||||||
|
|
||||||
|
@component('mail::table')
|
||||||
|
| # | ID | Name | Amount |
|
||||||
|
| -: | - |:-----| ------:|
|
||||||
|
@foreach ($invoice->products() as $po)
|
||||||
|
| {{ $po->count }} | {{ $po->product_id }} | {{ $po->name($invoice->account->user->language) }} | ${{ number_format($invoice->items->filter(function($item) use ($po) {return $item->product_id == $po->id; })->sum('total'),$invoice->currency()->rounding) }} |
|
||||||
|
@endforeach
|
||||||
|
||| Sub Total | ${{ $invoice->sub_total }} |
|
||||||
|
||| Tax | ${{ $invoice->tax_total }} |
|
||||||
|
||| Total | ${{ $invoice->total }} |
|
||||||
|
@endcomponent
|
||||||
|
|
||||||
|
If you would like a PDF copy of that invoice, please click on the link below:
|
||||||
|
|
||||||
|
@component('mail::panel',['url'=>$invoice->download_link()])
|
||||||
|
Download PDF
|
||||||
|
@endcomponent
|
||||||
|
|
||||||
|
Thanks,<br>
|
||||||
|
{{ config('mail.from.name') }}
|
||||||
|
@endcomponent
|
153
resources/views/vendor/mail/html/themes/default.css
vendored
153
resources/views/vendor/mail/html/themes/default.css
vendored
@ -1,117 +1,118 @@
|
|||||||
body{
|
body{
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
font-family: 'Bree Serif';
|
font-family: 'Bree Serif';
|
||||||
background: #f4f4f4;
|
background: #f4f4f4;
|
||||||
color: #6c7584;
|
color: #6c7584;
|
||||||
font-size: 1.2em;
|
font-size: 1.2em;
|
||||||
}
|
}
|
||||||
.header{
|
.header{
|
||||||
background: #232323;
|
background: #232323;
|
||||||
border-bottom: 5px solid #454d59;
|
border-bottom: 5px solid #454d59;
|
||||||
padding: 20px 0px 10px 0px;
|
padding: 20px 0px 10px 0px;
|
||||||
margin-bottom: 30px;
|
margin-bottom: 30px;
|
||||||
color: #f4f4f4;
|
color: #f4f4f4;
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
}
|
}
|
||||||
.footer{
|
.footer{
|
||||||
background: #232323;
|
background: #232323;
|
||||||
border-top: 5px solid #454d59;
|
border-top: 5px solid #454d59;
|
||||||
padding: 10px 0px 20px 0px;
|
padding: 10px 0px 20px 0px;
|
||||||
margin-top: 30px;
|
margin-top: 30px;
|
||||||
color: #f4f4f4;
|
color: #f4f4f4;
|
||||||
font-weight: 100;
|
font-weight: 100;
|
||||||
}
|
}
|
||||||
.subject{
|
.subject{
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
font-family: 'Bree Serif',serif;
|
font-family: 'Bree Serif',serif;
|
||||||
font-size: 1.5em;
|
font-size: 1.5em;
|
||||||
/* text-align: right; */
|
/* text-align: right; */
|
||||||
}
|
}
|
||||||
.panel{
|
.panel{
|
||||||
background:#454d59;
|
background:#454d59;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
padding: 20px;
|
margin-top: 20px;
|
||||||
font-weight: 300;
|
padding: 20px;
|
||||||
color: #f4f4f4;
|
font-weight: 300;
|
||||||
font-size: 1.4em;
|
color: #f4f4f4;
|
||||||
display: inline-block
|
font-size: 1.4em;
|
||||||
|
display: inline-block
|
||||||
}
|
}
|
||||||
.light-box{
|
.light-box{
|
||||||
background: #f9f9f9;
|
background: #f9f9f9;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
.main-body{
|
.main-body{
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
color:#6c7584;
|
color:#6c7584;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
padding:10px 20px;
|
padding:10px 20px;
|
||||||
border-top:1px solid #dbdbdb;
|
border-top:1px solid #dbdbdb;
|
||||||
border-left:1px solid #dbdbdb;
|
border-left:1px solid #dbdbdb;
|
||||||
border-right:1px solid #dbdbdb;
|
border-right:1px solid #dbdbdb;
|
||||||
border-bottom:3px solid #dbdbdb;
|
border-bottom:3px solid #dbdbdb;
|
||||||
}
|
}
|
||||||
.main-body table thead td{
|
.main-body table thead td{
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
border-bottom: 1px solid #dbdbdb;
|
border-bottom: 1px solid #dbdbdb;
|
||||||
color: #ccc
|
color: #ccc
|
||||||
}
|
}
|
||||||
.main-body table td.title{
|
.main-body table td.title{
|
||||||
font-size: 1.1em;
|
font-size: 1.1em;
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
}
|
}
|
||||||
.main-body table td.title small{
|
.main-body table td.title small{
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
color: #6c7584
|
color: #6c7584
|
||||||
}
|
}
|
||||||
.main-body .note{
|
.main-body .note{
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
}
|
}
|
||||||
.panel a{
|
.panel a{
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
color: #f4f4f4;
|
color: #f4f4f4;
|
||||||
}
|
}
|
||||||
.panel a:hover{
|
.panel a:hover{
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -- TO VALIDATE -- */
|
/* -- TO VALIDATE -- */
|
||||||
h1{
|
h1{
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
color: #121212;
|
color: #121212;
|
||||||
font-family: 'Bree Serif',serif;
|
font-family: 'Bree Serif',serif;
|
||||||
margin:0 auto;
|
margin:0 auto;
|
||||||
font-size: 32px;
|
font-size: 32px;
|
||||||
}
|
}
|
||||||
h1 small{font-weight: 200; font-size: 24px;}
|
h1 small{font-weight: 200; font-size: 24px;}
|
||||||
h3{
|
h3{
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
color: #121212;
|
color: #121212;
|
||||||
font-family: 'Bree Serif',serif;
|
font-family: 'Bree Serif',serif;
|
||||||
margin: 25px auto;
|
margin: 25px auto;
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
}
|
}
|
||||||
.free{color:#1cbbb4;}
|
.free{color:#1cbbb4;}
|
||||||
.paid{color:#0f80bb;}
|
.paid{color:#0f80bb;}
|
||||||
.links{
|
.links{
|
||||||
margin: 30px auto;
|
margin: 30px auto;
|
||||||
width: 600px;
|
width: 600px;
|
||||||
}
|
}
|
||||||
.links table td{
|
.links table td{
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
color: #333;
|
color: #333;
|
||||||
font-size:18px;
|
font-size:18px;
|
||||||
}
|
}
|
||||||
.links table td span, .links table td a{font-weight: 400}
|
.links table td span, .links table td a{font-weight: 400}
|
||||||
.border-l{border-left:1px solid #ccc}
|
.border-l{border-left:1px solid #ccc}
|
||||||
|
@ -74,6 +74,13 @@ Route::group(['middleware'=>['theme:adminlte-be','auth'],'prefix'=>'u'],function
|
|||||||
->middleware('can:progress,o,status');
|
->middleware('can:progress,o,status');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Doorman Code Routes
|
||||||
|
Route::group(['middleware'=>['theme:adminlte-be'],'prefix'=>'u'],function() {
|
||||||
|
Route::get('invoice/{o}/email/{code}','UserHomeController@invoice_pdf_email')
|
||||||
|
->where('o','[0-9]+')
|
||||||
|
->where('code','[0-9A-Z]{6}');
|
||||||
|
});
|
||||||
|
|
||||||
// Frontend Routes (Non-Authed Users)
|
// Frontend Routes (Non-Authed Users)
|
||||||
Route::group(['middleware'=>['theme:metronic-fe']],function() {
|
Route::group(['middleware'=>['theme:metronic-fe']],function() {
|
||||||
Route::get('/','WelcomeController@index');
|
Route::get('/','WelcomeController@index');
|
||||||
|
Loading…
Reference in New Issue
Block a user