diff --git a/app/Console/Commands/InvoiceEmail.php b/app/Console/Commands/InvoiceEmail.php new file mode 100644 index 0000000..10ad69a --- /dev/null +++ b/app/Console/Commands/InvoiceEmail.php @@ -0,0 +1,58 @@ +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(); + } + } +} \ No newline at end of file diff --git a/app/Http/Controllers/UserHomeController.php b/app/Http/Controllers/UserHomeController.php index 2e7278b..3600915 100644 --- a/app/Http/Controllers/UserHomeController.php +++ b/app/Http/Controllers/UserHomeController.php @@ -2,8 +2,11 @@ namespace App\Http\Controllers; +use Clarkeash\Doorman\Exceptions\{DoormanException,ExpiredInviteCode}; +use Clarkeash\Doorman\Facades\Doorman; use Illuminate\Contracts\View\Factory; use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Log; use Illuminate\View\View; use Barryvdh\Snappy\Facades\SnappyPdf as PDF; @@ -14,7 +17,8 @@ class UserHomeController extends Controller { 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)); } + /** + * 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. * diff --git a/app/Mail/InvoiceEmail.php b/app/Mail/InvoiceEmail.php new file mode 100644 index 0000000..0c8db67 --- /dev/null +++ b/app/Mail/InvoiceEmail.php @@ -0,0 +1,47 @@ +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, + ]); + } +} \ No newline at end of file diff --git a/app/Models/Invoice.php b/app/Models/Invoice.php index e28a808..a74117b 100644 --- a/app/Models/Invoice.php +++ b/app/Models/Invoice.php @@ -3,10 +3,13 @@ namespace App\Models; use Carbon\Carbon; +use Clarkeash\Doorman\Facades\Doorman; +use Clarkeash\Doorman\Models\Invite; use Illuminate\Database\Eloquent\Model; use App\Traits\NextKey; use App\Traits\PushNew; +use Illuminate\Support\Arr; class Invoice extends Model { @@ -52,7 +55,7 @@ class Invoice extends Model public function items() { - return $this->hasMany(InvoiceItem::class); + return $this->hasMany(InvoiceItem::class)->where('active',1); } public function paymentitems() @@ -154,6 +157,29 @@ class Invoice extends Model 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() { $return = collect(); @@ -196,6 +222,20 @@ class Invoice extends Model })->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. * diff --git a/config/doorman.php b/config/doorman.php new file mode 100644 index 0000000..595b016 --- /dev/null +++ b/config/doorman.php @@ -0,0 +1,65 @@ + '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, + ], + +]; diff --git a/resources/views/email/user/invoice.blade.php b/resources/views/email/user/invoice.blade.php new file mode 100644 index 0000000..7c3f481 --- /dev/null +++ b/resources/views/email/user/invoice.blade.php @@ -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,
+{{ config('mail.from.name') }} +@endcomponent \ No newline at end of file diff --git a/resources/views/vendor/mail/html/themes/default.css b/resources/views/vendor/mail/html/themes/default.css index 56e1dd2..2fb1fdb 100644 --- a/resources/views/vendor/mail/html/themes/default.css +++ b/resources/views/vendor/mail/html/themes/default.css @@ -1,117 +1,118 @@ body{ - margin: 0 auto; - font-family: 'Bree Serif'; - background: #f4f4f4; - color: #6c7584; - font-size: 1.2em; + margin: 0 auto; + font-family: 'Bree Serif'; + background: #f4f4f4; + color: #6c7584; + font-size: 1.2em; } .header{ - background: #232323; - border-bottom: 5px solid #454d59; - padding: 20px 0px 10px 0px; - margin-bottom: 30px; - color: #f4f4f4; - font-weight: 300; + background: #232323; + border-bottom: 5px solid #454d59; + padding: 20px 0px 10px 0px; + margin-bottom: 30px; + color: #f4f4f4; + font-weight: 300; } .footer{ - background: #232323; - border-top: 5px solid #454d59; - padding: 10px 0px 20px 0px; - margin-top: 30px; - color: #f4f4f4; - font-weight: 100; + background: #232323; + border-top: 5px solid #454d59; + padding: 10px 0px 20px 0px; + margin-top: 30px; + color: #f4f4f4; + font-weight: 100; } .subject{ - font-weight: 300; - font-family: 'Bree Serif',serif; - font-size: 1.5em; - /* text-align: right; */ + font-weight: 300; + font-family: 'Bree Serif',serif; + font-size: 1.5em; + /* text-align: right; */ } .panel{ - background:#454d59; - border-radius: 10px; - padding: 20px; - font-weight: 300; - color: #f4f4f4; - font-size: 1.4em; - display: inline-block + background:#454d59; + border-radius: 10px; + margin-top: 20px; + padding: 20px; + font-weight: 300; + color: #f4f4f4; + font-size: 1.4em; + display: inline-block } .light-box{ - background: #f9f9f9; - border-radius: 10px; - padding: 10px; - font-weight: 300; - margin-top: 10px; - font-size: 0.8em; - margin-bottom: 10px; + background: #f9f9f9; + border-radius: 10px; + padding: 10px; + font-weight: 300; + margin-top: 10px; + font-size: 0.8em; + margin-bottom: 10px; } .main-body{ - background: #ffffff; - border-radius: 10px; - color:#6c7584; - font-weight: 400; - padding:10px 20px; - border-top:1px solid #dbdbdb; - border-left:1px solid #dbdbdb; - border-right:1px solid #dbdbdb; - border-bottom:3px solid #dbdbdb; + background: #ffffff; + border-radius: 10px; + color:#6c7584; + font-weight: 400; + padding:10px 20px; + border-top:1px solid #dbdbdb; + border-left:1px solid #dbdbdb; + border-right:1px solid #dbdbdb; + border-bottom:3px solid #dbdbdb; } .main-body table thead td{ - font-weight: 300; - border-bottom: 1px solid #dbdbdb; - color: #ccc + font-weight: 300; + border-bottom: 1px solid #dbdbdb; + color: #ccc } .main-body table td.title{ - font-size: 1.1em; - line-height: 20px; + font-size: 1.1em; + line-height: 20px; } .main-body table td.title small{ - font-weight: 300; - font-size: 0.9em; - color: #6c7584 + font-weight: 300; + font-size: 0.9em; + color: #6c7584 } .main-body .note{ - font-size: 0.8em; - font-weight: 300; - font-style: normal; + font-size: 0.8em; + font-weight: 300; + font-style: normal; } .panel a{ - text-decoration: underline; - color: #f4f4f4; + text-decoration: underline; + color: #f4f4f4; } .panel a:hover{ - text-decoration: none; - color: #ffffff; + text-decoration: none; + color: #ffffff; } /* -- TO VALIDATE -- */ h1{ - font-weight: 300; - color: #121212; - font-family: 'Bree Serif',serif; - margin:0 auto; - font-size: 32px; + font-weight: 300; + color: #121212; + font-family: 'Bree Serif',serif; + margin:0 auto; + font-size: 32px; } h1 small{font-weight: 200; font-size: 24px;} h3{ - font-weight: 300; - color: #121212; - font-family: 'Bree Serif',serif; - margin: 25px auto; - font-size: 24px; + font-weight: 300; + color: #121212; + font-family: 'Bree Serif',serif; + margin: 25px auto; + font-size: 24px; } .free{color:#1cbbb4;} .paid{color:#0f80bb;} .links{ - margin: 30px auto; - width: 600px; + margin: 30px auto; + width: 600px; } .links table td{ - padding: 20px; - text-align: center; - font-weight: 300; - color: #333; - font-size:18px; + padding: 20px; + text-align: center; + font-weight: 300; + color: #333; + font-size:18px; } .links table td span, .links table td a{font-weight: 400} .border-l{border-left:1px solid #ccc} diff --git a/routes/web.php b/routes/web.php index 93d04ba..218ad83 100644 --- a/routes/web.php +++ b/routes/web.php @@ -74,6 +74,13 @@ Route::group(['middleware'=>['theme:adminlte-be','auth'],'prefix'=>'u'],function ->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) Route::group(['middleware'=>['theme:metronic-fe']],function() { Route::get('/','WelcomeController@index');