Enabled console invoice generation

This commit is contained in:
Deon George 2020-04-01 23:35:06 +11:00
parent fb9ccd927d
commit 23dc668c65
11 changed files with 273 additions and 166 deletions

View File

@ -1,106 +0,0 @@
<?php
namespace App\Classes;
use Illuminate\Support\Arr;
class SSL
{
// Our CSR
private $csr_pem = NULL;
// Our Certificate
private $crt = [];
private $crt_pem = NULL;
// Our Key
private $key_pem = NULL;
/**
* @param $key
* @return null
* @throws \Exception
*/
public function __get($key)
{
switch($key)
{
case 'cn': return $this->cn();
case 'dn': return $this->dn();
default:
throw new \App\Exceptions\SSLUnknownAttribute($key);
}
}
private function cn()
{
$subject = Arr::get($this->crt,'subject');
if (! $subject AND $this->csr_pem) {
$subject = openssl_csr_get_subject($this->csr_pem);
}
return isset($subject['CN']) ? $subject['CN'] : NULL;
}
/**
* Add CSR
*
* @param $value
* @return mixed
*/
public function csr($value)
{
$this->csr_pem = $value;
return $this;
}
/**
* Add certificate
*
* @param $value
* @return mixed
*/
public function crt($value)
{
$this->crt_pem = $value;
$this->crt = openssl_x509_parse($this->crt);
return $this;
}
public function dn()
{
$dn = '';
if ($this->crt_pem) {
$this->crt = openssl_x509_parse($this->crt_pem);
$dn = Arr::get($this->crt,'name');
}
if (! $dn AND $this->csr_pem) {
$dna = openssl_csr_get_subject($this->csr_pem);
foreach ($dna as $k=>$v) {
if ($dn)
$dn .= ',';
$dn .= sprintf('%s=%s',$k,$v);
}
}
return $dn;
}
/**
* Add the Key
*
* @param $value
* @return mixed
*/
public function key($value)
{
$this->key_pem = $value;
return $this;
}
}

View File

@ -0,0 +1,74 @@
<?php
namespace App\Console\Commands;
use App\Models\Account;
use App\Models\Invoice;
use Carbon\Carbon;
use Illuminate\Console\Command;
class InvoiceGenerate extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'invoice:generate {account?} {--p|preview : Preview}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Generate Invoices to be Sent';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
if ($this->argument('account'))
$accounts = collect()->push(Account::find($this->argument('account')));
else
$accounts = Account::active()->get();
foreach ($accounts as $o) {
$io = new Invoice;
foreach ($o->services(TRUE)->get() as $so) {
foreach ($so->next_invoice_items(FALSE) as $ooo)
$io->items->push($ooo);
}
// If there are no items, no reason to do anything
if (! $io->items->count())
continue;
$io->account_id = $o->id;
if ($this->option('preview')) {
$this->info(sprintf('Invoice for Account [%d] - [%d] items totalling [%3.2f]',$o->id,$io->items->count(),$io->total));
continue;
}
// Save the invoice
$io->site_id = 1; // @todo
$io->active = 1;
$io->pushNew();
}
}
}

View File

@ -9,14 +9,14 @@ interface ServiceItem
* *
* @return string * @return string
*/ */
public function getServiceDescriptionAttribute():string; public function getServiceDescriptionAttribute(): string;
/** /**
* Return the Service Name. * Return the Service Name.
* *
* @return string * @return string
*/ */
public function getServiceNameAttribute():string; public function getServiceNameAttribute(): string;
/** /**
* Is this service in a contract * Is this service in a contract

View File

@ -2,6 +2,7 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use App\Traits\NextKey; use App\Traits\NextKey;
@ -61,7 +62,7 @@ class Account extends Model
{ {
$query = $this->hasMany(Service::class); $query = $this->hasMany(Service::class);
return $active ? $query->where('active','=',TRUE) : $query; return $active ? $query->active() : $query;
} }
public function user() public function user()
@ -71,6 +72,14 @@ class Account extends Model
/** SCOPES */ /** SCOPES */
/**
* Only query active categories
*/
public function scopeActive($query)
{
return $query->where('active',TRUE);
}
/** /**
* Search for a record * Search for a record
* *

View File

@ -2,12 +2,24 @@
namespace App\Models; namespace App\Models;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use App\Traits\NextKey;
use App\Traits\PushNew;
class Invoice extends Model class Invoice extends Model
{ {
use NextKey,PushNew;
const RECORD_ID = 'invoice';
public $incrementing = FALSE;
protected $table = 'ab_invoice'; protected $table = 'ab_invoice';
const CREATED_AT = 'date_orig';
const UPDATED_AT = 'date_last';
protected $dates = ['date_orig','due_date']; protected $dates = ['date_orig','due_date'];
public $dateFormat = 'U';
protected $appends = [ protected $appends = [
'date_due', 'date_due',
@ -183,4 +195,23 @@ class Invoice extends Model
return $item->product_id == $po->id AND $item->service_id == $so->id; return $item->product_id == $po->id AND $item->service_id == $so->id;
})->filter()->sortBy('item_type'); })->filter()->sortBy('item_type');
} }
/**
* Automatically set our due_date at save time.
*
* @param array $options
* @return bool
*/
public function save(array $options = []) {
// Automatically set the date_due attribute for new records.
if (! $this->exists AND ! $this->due_date) {
$this->due_date = $this->items->min('date_start');
// @todo This 7 days should be sysetm configurable
if (($x=Carbon::now()->addDay(7)) > $this->due_date)
$this->due_date = $x;
}
return parent::save($options);
}
} }

View File

@ -6,14 +6,22 @@ use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use App\Traits\NextKey;
use App\Traits\PushNew;
use Leenooks\Carbon; use Leenooks\Carbon;
class InvoiceItem extends Model class InvoiceItem extends Model
{ {
protected $dates = ['date_start','date_stop']; use NextKey,PushNew;
public $dateFormat = 'U'; const RECORD_ID = 'invoice_item';
public $incrementing = FALSE;
protected $table = 'ab_invoice_item'; protected $table = 'ab_invoice_item';
const CREATED_AT = 'date_orig';
const UPDATED_AT = 'date_last';
protected $dates = ['date_start','date_stop'];
public $dateFormat = 'U';
private $_tax = 0; private $_tax = 0;
@ -115,6 +123,7 @@ class InvoiceItem extends Model
{ {
return $this->quantity * $this->price_base; return $this->quantity * $this->price_base;
} }
public function getTaxAttribute() public function getTaxAttribute()
{ {
if (! $this->_tax) if (! $this->_tax)
@ -149,6 +158,7 @@ class InvoiceItem extends Model
$iit = new InvoiceItemTax; $iit = new InvoiceItemTax;
$iit->tax_id = $to->id; $iit->tax_id = $to->id;
$iit->amount = round($this->quantity*$this->price_base*$to->rate,3); $iit->amount = round($this->quantity*$this->price_base*$to->rate,3);
$iit->site_id = 1;
$this->taxes->push($iit); $this->taxes->push($iit);
} }
} }

View File

@ -4,9 +4,20 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use App\Traits\NextKey;
use App\Traits\PushNew;
class InvoiceItemTax extends Model class InvoiceItemTax extends Model
{ {
use NextKey,PushNew;
const RECORD_ID = 'invoice_item_tax';
public $incrementing = FALSE;
protected $table = 'ab_invoice_item_tax'; protected $table = 'ab_invoice_item_tax';
const CREATED_AT = 'date_orig';
const UPDATED_AT = NULL;
public $dateFormat = 'U';
public function invoice_item() public function invoice_item()
{ {

View File

@ -468,7 +468,15 @@ class Service extends Model
*/ */
public function getInvoiceToAttribute() public function getInvoiceToAttribute()
{ {
return ($x=$this->invoice_items->filter(function($item) { return $item->item_type === 0;}))->count() ? $x->last()->date_stop : NULL; $result = ($x=$this->invoice_items->filter(function($item) { return $item->item_type === 0;}))->count()
? $x->last()->date_stop
: NULL;
// For SSL Certificates, the invoice_to date is the expiry date of the Cert
if (is_null($result) AND $this->type->type == 'ssl' AND $this->type->valid_to)
return $this->type->valid_to;
return $result;
} }
public function getNameAttribute(): string public function getNameAttribute(): string
@ -480,13 +488,12 @@ class Service extends Model
* Return the short name for the service. * Return the short name for the service.
* *
* EG: * EG:
* For ADSL, this would be the phone number, * + For ADSL, this would be the phone number,
* For Hosting, this would be the domain name, etc * + For Hosting, this would be the domain name, etc
* @deprecated
*/ */
public function getNameShortAttribute() public function getNameShortAttribute()
{ {
return $this->model ? $this->type->name : 'NAME UNKNOWN'; return $this->type->service_name;
} }
/** /**
@ -599,10 +606,10 @@ class Service extends Model
/** /**
* Return the service description. * Return the service description.
* For: * For:
* + Broadband, this is the service address * + Broadband, this is the service address
* + Domains, blank * + Domains, blank
* + Hosting, blank * + Hosting, blank
* + SSL, blank * + SSL, blank
* *
* @return string * @return string
*/ */
@ -616,10 +623,10 @@ class Service extends Model
/** /**
* Return the service name. * Return the service name.
* For: * For:
* + Broadband, this is the service number * + Broadband, this is the service number
* + Domains, this is the full domain name * + Domains, this is the full domain name
* + Hosting, this is the full domain name * + Hosting, this is the full domain name
* + SSL, this is the DN * + SSL, this is the DN
* *
* @return string * @return string
*/ */
@ -789,30 +796,33 @@ class Service extends Model
/** /**
* Generate a collection of invoice_item objects that will be billed for the next invoice * Generate a collection of invoice_item objects that will be billed for the next invoice
* *
* @param bool $future Next item to be billed (not in the next x days)
* @return Collection * @return Collection
* @throws Exception * @throws Exception
*/ */
public function next_invoice_items(bool $future): Collection public function next_invoice_items(bool $future): Collection
{ {
if ($this->wasCancelled()) if ($this->wasCancelled() OR $this->suspend_billing OR ! $this->active)
return collect(); return collect();
// If pending, add any connection charges // If pending, add any connection charges
// Connection charges are only charged once // Connection charges are only charged once
if ((! $this->invoice_items->filter(function($item) { return $item->item_type==4; })->sum('total')) if ((! $this->invoice_items->filter(function($item) { return $item->item_type==4; })->sum('total'))
AND ($this->isPending() OR is_null($this->invoice_to))) AND ($this->isPending() OR is_null($this->invoice_to))
AND $this->product->price($this->recur_schedule,'price_setup'))
{ {
$o = new InvoiceItem; $o = new InvoiceItem;
$o->active = TRUE; $o->active = TRUE;
$o->service_id = $this->id; $o->service_id = $this->id;
$o->product_id = $this->product_id; $o->product_id = $this->product_id;
$o->item_type = 4; // @todo change to const or something $o->item_type = 4; // @todo change to const or something
$o->price_base = $this->price ?: $this->product->price($this->recur_schedule,'price_setup'); // @todo change to a method in this class $o->price_base = $this->product->price($this->recur_schedule,'price_setup'); // @todo change to a method in this class
//$o->recurring_schedule = $this->recur_schedule; //$o->recurring_schedule = $this->recur_schedule;
$o->date_start = $this->invoice_next; $o->date_start = $this->invoice_next;
$o->date_stop = $this->invoice_next; $o->date_stop = $this->invoice_next;
$o->quantity = 1; $o->quantity = 1;
$o->site_id = 1; // @todo
$o->addTaxes($this->account->country->taxes); $o->addTaxes($this->account->country->taxes);
$this->invoice_items->push($o); $this->invoice_items->push($o);
@ -820,7 +830,8 @@ class Service extends Model
// If the service is active, there will be service charges // If the service is active, there will be service charges
if ((! $this->invoice_items->filter(function($item) { return $item->item_type==0 AND ! $item->exists; })->count()) if ((! $this->invoice_items->filter(function($item) { return $item->item_type==0 AND ! $item->exists; })->count())
AND ($this->active OR $this->isPending())) AND ($this->active OR $this->isPending())
AND ($future == FALSE AND ($this->invoice_to < Carbon::now()->addDays(30))))
{ {
do { do {
$o = new InvoiceItem; $o = new InvoiceItem;
@ -828,35 +839,39 @@ class Service extends Model
$o->service_id = $this->id; $o->service_id = $this->id;
$o->product_id = $this->product_id; $o->product_id = $this->product_id;
$o->item_type = 0; $o->item_type = 0;
$o->price_base = $this->price ?: $this->product->price($this->recur_schedule); // @todo change to a method in this class $o->price_base = is_null($this->price)
? (is_null($this->price_override) ? $this->product->price($this->recur_schedule) : $this->price_override)
: $this->price; // @todo change to a method in this class
$o->recurring_schedule = $this->recur_schedule; $o->recurring_schedule = $this->recur_schedule;
$o->date_start = $this->invoice_next; $o->date_start = $this->invoice_next;
$o->date_stop = $this->invoice_next_end; $o->date_stop = $this->invoice_next_end;
$o->quantity = $this->invoice_next_quantity; $o->quantity = $this->invoice_next_quantity;
$o->site_id = 1; // @todo
$o->addTaxes($this->account->country->taxes); $o->addTaxes($this->account->country->taxes);
$this->invoice_items->push($o); $this->invoice_items->push($o);
} while ($future == FALSE AND ($this->invoice_to < Carbon::now()->addDays(30))); } while ($future == FALSE AND ($this->invoice_to < Carbon::now()->addDays(30)));
} }
// Add additional charges // Add additional charges
if (! $this->invoice_items->filter(function($item) { return $item->module_id == 30 AND ! $item->exists; })->count()) if (! $this->invoice_items->filter(function($item) { return $item->module_id == 30 AND ! $item->exists; })->count())
foreach ($this->charges->filter(function($item) { return ! $item->processed; }) as $oo) { foreach ($this->charges->filter(function($item) { return ! $item->processed; }) as $oo) {
$o = new InvoiceItem; $o = new InvoiceItem;
$o->active = TRUE; $o->active = TRUE;
$o->service_id = $oo->service_id; $o->service_id = $oo->service_id;
$o->product_id = $this->product_id; $o->product_id = $this->product_id;
$o->quantity = $oo->quantity; $o->quantity = $oo->quantity;
$o->item_type = $oo->type; $o->item_type = $oo->type;
$o->price_base = $oo->amount; $o->price_base = $oo->amount;
$o->date_start = $oo->date_charge; $o->date_start = $oo->date_charge;
$o->date_stop = $oo->date_charge; $o->date_stop = $oo->date_charge;
$o->module_id = 30; // @todo This shouldnt be hard coded $o->module_id = 30; // @todo This shouldnt be hard coded
$o->module_ref = $oo->id; $o->module_ref = $oo->id;
$o->site_id = 1; // @todo
$o->addTaxes($this->account->country->taxes); $o->addTaxes($this->account->country->taxes);
$this->invoice_items->push($o); $this->invoice_items->push($o);
} }
return $this->invoice_items->filter(function($item) { return ! $item->exists; }); return $this->invoice_items->filter(function($item) { return ! $item->exists; });
} }

View File

@ -2,6 +2,9 @@
namespace App\Models\Service; namespace App\Models\Service;
use Illuminate\Support\Arr;
use Carbon\Carbon;
use App\Interfaces\ServiceItem; use App\Interfaces\ServiceItem;
use App\Models\Base\ServiceType; use App\Models\Base\ServiceType;
use App\Traits\NextKey; use App\Traits\NextKey;
@ -13,32 +16,53 @@ class SSL extends ServiceType implements ServiceItem
protected $table = 'ab_service__ssl'; protected $table = 'ab_service__ssl';
protected $_o = NULL; protected $crt_parse = NULL;
protected $public_key = NULL;
public function getSSLAttribute() public static function boot()
{ {
if (is_null($this->_o)) { parent::boot();
$this->_o = new \App\Classes\SSL;
if ($this->cert) static::retrieved(function($model) {
$this->_o->crt($this->cert); if ($model->cert);
if ($this->csr) $model->crt_parse = collect(openssl_x509_parse($model->cert));
$this->_o->csr($this->csr);
if ($this->pk)
$this->_o->key($this->pk);
}
return $this->_o; if ($model->csr) {
$model->public_key = collect(openssl_pkey_get_details(openssl_csr_get_public_key($model->csr)));
}
});
}
public function getValidToAttribute()
{
return $this->cert ? Carbon::createFromTimestamp($this->crt_parse->get('validTo_time_t')) : NULL;
} }
public function getServiceDescriptionAttribute(): string public function getServiceDescriptionAttribute(): string
{ {
return $this->ssl->dn; if ($this->cert)
return Arr::get($this->crt_parse,'name');
else {
$dn = '';
$dna = openssl_csr_get_subject($this->csr);
foreach ($dna as $k=>$v) {
if ($dn)
$dn .= ',';
$dn .= sprintf('%s=%s',$k,$v);
}
return $dn;
}
} }
public function getServiceNameAttribute(): string public function getServiceNameAttribute(): string
{ {
return $this->ssl->cn; return $this->cert
? Arr::get($this->crt_parse,'subject.CN')
: Arr::get(openssl_csr_get_subject($this->csr),'CN');
} }
public function inContract(): bool public function inContract(): bool

37
app/Traits/PushNew.php Normal file
View File

@ -0,0 +1,37 @@
<?php
/**
* Enables a pushnew() method, similar to push() but when all relationships are new records
*
* Base on Model::push()
*/
namespace App\Traits;
use Illuminate\Database\Eloquent\Collection;
trait PushNew
{
public function pushNew() {
if (! $this->save()) {
return false;
}
// To sync all of the relationships to the database, we will simply spin through
// the relationships and save each model via this "pushNew" method, which allows
// us to recurse into all of these nested relations for the model instance.
foreach ($this->relations as $models) {
$models = $models instanceof Collection
? $models->all() : [$models];
foreach (array_filter($models) as $model) {
$model->setAttribute($this->getForeignKey(),$this->{$this->getKeyName()});
if (! $model->pushNew()) {
return false;
}
}
}
return true;
}
}

View File

@ -23,14 +23,16 @@
<th>Invoiced To</th> <th>Invoiced To</th>
<td>{{ $o->invoice_to->format('Y-m-d') }}</td> <td>{{ $o->invoice_to->format('Y-m-d') }}</td>
</tr> </tr>
<tr> @if($o->paid_to)
<th>Paid Until</th> <tr>
<td>{{ $o->paid_to->format('Y-m-d') }}</td> <th>Paid Until</th>
</tr> <td>{{ $o->paid_to->format('Y-m-d') }}</td>
</tr>
@endif
@endif @endif
<tr> <tr>
<th>Next Invoice</th> <th>Next Invoice</th>
<td>{{ $o->invoice_next->format('Y-m-d') }}</td> <td>@if ($o->suspend_billing)<strike>@endif{{ $o->invoice_next->format('Y-m-d') }}@if ($o->suspend_billing)</strike> <strong>SUSPENDED</strong>@endif</td>
</tr> </tr>
<tr> <tr>
<th>Next Estimated Invoice</th> <th>Next Estimated Invoice</th>