Recurring Quotes

This commit is contained in:
David Bomba 2019-05-05 10:49:01 +10:00
parent c0d9708659
commit 10e0c8a05f
21 changed files with 1203 additions and 1 deletions

View File

@ -0,0 +1,51 @@
<?php
namespace App\Factory;
use App\DataMapper\ClientSettings;
use App\DataMapper\CompanySettings;
use App\Models\RecurringQuote;
class RecurringQuoteFactory
{
public static function create(int $company_id, int $user_id) :RecurringQuote
{
$quote = new RecurringQuote();
$quote->status_id = RecurringQuote::STATUS_DRAFT;
$quote->discount = 0;
$quote->is_amount_discount = true;
$quote->po_number = '';
$quote->footer = '';
$quote->terms = '';
$quote->public_notes = '';
$quote->private_notes = '';
$quote->quote_date = null;
$quote->valid_until = null;
$quote->partial_due_date = null;
$quote->is_deleted = false;
$quote->line_items = json_encode([]);
$quote->settings = ClientSettings::buildClientSettings(new CompanySettings(CompanySettings::defaults()), new ClientSettings(ClientSettings::defaults())); //todo need to embed the settings here
$quote->backup = json_encode([]);
$quote->tax_name1 = '';
$quote->tax_rate1 = 0;
$quote->tax_name2 = '';
$quote->tax_rate2 = 0;
$quote->custom_value1 = 0;
$quote->custom_value2 = 0;
$quote->custom_value3 = 0;
$quote->custom_value4 = 0;
$quote->amount = 0;
$quote->balance = 0;
$quote->partial = 0;
$quote->user_id = $user_id;
$quote->company_id = $company_id;
$quote->frequency_id = RecurringQuote::FREQUENCY_MONTHLY;
$quote->start_date = null;
$quote->last_sent_date = null;
$quote->next_send_date = null;
$quote->remaining_cycles = 0;
return $quote;
}
}

View File

@ -0,0 +1,111 @@
<?php
namespace App\Filters;
use App\Models\User;
use Illuminate\Database\Eloquent\Builder;
/**
* RecurringQuoteFilters
*/
class RecurringQuoteFilters extends QueryFilters
{
/**
* Filter based on search text
*
* @param string query filter
* @return Illuminate\Database\Query\Builder
* @deprecated
*
*/
public function filter(string $filter = '') : Builder
{
if(strlen($filter) == 0)
return $this->builder;
return $this->builder->where(function ($query) use ($filter) {
$query->where('recurring_quotes.custom_value1', 'like', '%'.$filter.'%')
->orWhere('recurring_quotes.custom_value2', 'like' , '%'.$filter.'%')
->orWhere('recurring_quotes.custom_value3', 'like' , '%'.$filter.'%')
->orWhere('recurring_quotes.custom_value4', 'like' , '%'.$filter.'%');
});
}
/**
* Filters the list based on the status
* archived, active, deleted
*
* @param string filter
* @return Illuminate\Database\Query\Builder
*/
public function status(string $filter = '') : Builder
{
if(strlen($filter) == 0)
return $this->builder;
$table = 'recurring_';
$filters = explode(',', $filter);
return $this->builder->where(function ($query) use ($filters, $table) {
$query->whereNull($table . '.id');
if (in_array(parent::STATUS_ACTIVE, $filters)) {
$query->orWhereNull($table . '.deleted_at');
}
if (in_array(parent::STATUS_ARCHIVED, $filters)) {
$query->orWhere(function ($query) use ($table) {
$query->whereNotNull($table . '.deleted_at');
if (! in_array($table, ['users'])) {
$query->where($table . '.is_deleted', '=', 0);
}
});
}
if (in_array(parent::STATUS_DELETED, $filters)) {
$query->orWhere($table . '.is_deleted', '=', 1);
}
});
}
/**
* Sorts the list based on $sort
*
* @param string sort formatted as column|asc
* @return Illuminate\Database\Query\Builder
*/
public function sort(string $sort) : Builder
{
$sort_col = explode("|", $sort);
return $this->builder->orderBy($sort_col[0], $sort_col[1]);
}
/**
* Returns the base query
*
* @param int company_id
* @return Illuminate\Database\Query\Builder
* @deprecated
*/
public function baseQuery(int $company_id, User $user) : Builder
{
}
/**
* Filters the query by the users company ID
*
* @param $company_id The company Id
* @return Illuminate\Database\Query\Builder
*/
public function entityFilter()
{
return $this->builder->whereCompanyId(auth()->user()->company()->id);
}
}

View File

@ -0,0 +1,236 @@
<?php
namespace App\Http\Controllers;
use App\Factory\CloneRecurringQuoteFactory;
use App\Factory\CloneRecurringQuoteToQuoteFactory;
use App\Factory\RecurringQuoteFactory;
use App\Filters\RecurringQuoteFilters;
use App\Http\Requests\RecurringQuote\ActionRecurringQuoteRequest;
use App\Http\Requests\RecurringQuote\CreateRecurringQuoteRequest;
use App\Http\Requests\RecurringQuote\DestroyRecurringQuoteRequest;
use App\Http\Requests\RecurringQuote\EditRecurringQuoteRequest;
use App\Http\Requests\RecurringQuote\ShowRecurringQuoteRequest;
use App\Http\Requests\RecurringQuote\StoreRecurringQuoteRequest;
use App\Http\Requests\RecurringQuote\UpdateRecurringQuoteRequest;
use App\Jobs\Entity\ActionEntity;
use App\Models\RecurringQuote;
use App\Repositories\BaseRepository;
use App\Repositories\RecurringQuoteRepository;
use App\Transformers\RecurringQuoteTransformer;
use App\Utils\Traits\MakesHash;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
/**
* Class RecurringQuoteController
* @package App\Http\Controllers\RecurringQuoteController
*/
class RecurringQuoteController extends BaseController
{
use MakesHash;
protected $entity_type = RecurringQuote::class;
protected $entity_transformer = RecurringQuoteTransformer::class;
/**
* @var RecurringQuoteRepository
*/
protected $recurring_quote_repo;
protected $base_repo;
/**
* RecurringQuoteController constructor.
*
* @param \App\Repositories\RecurringQuoteRepository $recurring_quote_repo The RecurringQuote repo
*/
public function __construct(RecurringQuoteRepository $recurring_quote_repo)
{
parent::__construct();
$this->recurring_quote_repo = $recurring_quote_repo;
}
/**
* Show the list of recurring_invoices
*
* @param \App\Filters\RecurringQuoteFilters $filters The filters
*
* @return \Illuminate\Http\Response
*/
public function index(RecurringQuoteFilters $filters)
{
$recurring_quotes = RecurringQuote::filter($filters);
return $this->listResponse($recurring_quotes);
}
/**
* Show the form for creating a new resource.
*
* @param \App\Http\Requests\RecurringQuote\CreateRecurringQuoteRequest $request The request
*
* @return \Illuminate\Http\Response
*/
public function create(CreateRecurringQuoteRequest $request)
{
$recurring_quote = RecurringQuoteFactory::create(auth()->user()->company()->id, auth()->user()->id);
return $this->itemResponse($recurring_quote);
}
/**
* Store a newly created resource in storage.
*
* @param \App\Http\Requests\RecurringQuote\StoreRecurringQuoteRequest $request The request
*
* @return \Illuminate\Http\Response
*/
public function store(StoreRecurringQuoteRequest $request)
{
$recurring_quote = $this->recurring_quote_repo->save($request, RecurringQuoteFactory::create(auth()->user()->company()->id, auth()->user()->id));
return $this->itemResponse($recurring_quote);
}
/**
* Display the specified resource.
*
* @param \App\Http\Requests\RecurringQuote\ShowRecurringQuoteRequest $request The request
* @param \App\Models\RecurringQuote $recurring_quote The RecurringQuote
*
* @return \Illuminate\Http\Response
*/
public function show(ShowRecurringQuoteRequest $request, RecurringQuote $recurring_quote)
{
return $this->itemResponse($recurring_quote);
}
/**
* Show the form for editing the specified resource.
*
* @param \App\Http\Requests\RecurringQuote\EditRecurringQuoteRequest $request The request
* @param \App\Models\RecurringQuote $recurring_quote The RecurringQuote
*
* @return \Illuminate\Http\Response
*/
public function edit(EditRecurringQuoteRequest $request, RecurringQuote $recurring_quote)
{
return $this->itemResponse($recurring_quote);
}
/**
* Update the specified resource in storage.
*
* @param \App\Http\Requests\RecurringQuote\UpdateRecurringQuoteRequest $request The request
* @param \App\Models\RecurringQuote $recurring_quote The RecurringQuote
*
* @return \Illuminate\Http\Response
*/
public function update(UpdateRecurringQuoteRequest $request, RecurringQuote $recurring_quote)
{
$recurring_quote = $this->recurring_quote_repo->save(request(), $recurring_quote);
return $this->itemResponse($recurring_quote);
}
/**
* Remove the specified resource from storage.
*
* @param \App\Http\Requests\RecurringQuote\DestroyRecurringQuoteRequest $request
* @param \App\Models\RecurringQuote $recurring_quote
*
* @return \Illuminate\Http\Response
*/
public function destroy(DestroyRecurringQuoteRequest $request, RecurringQuote $recurring_quote)
{
$recurring_quote->delete();
return response()->json([], 200);
}
/**
* Perform bulk actions on the list view
*
* @return Collection
*/
public function bulk()
{
$action = request()->input('action');
$ids = request()->input('ids');
$recurring_quotes = RecurringQuote::withTrashed()->find($ids);
$recurring_quotes->each(function ($recurring_quote, $key) use($action){
if(auth()->user()->can('edit', $recurring_quote))
$this->recurring_quote_repo->{$action}($recurring_quote);
});
//todo need to return the updated dataset
return $this->listResponse(RecurringQuote::withTrashed()->whereIn('id', $ids));
}
public function action(ActionRecurringQuoteRequest $request, RecurringQuote $recurring_quote, $action)
{
switch ($action) {
case 'clone_to_RecurringQuote':
// $recurring_invoice = CloneRecurringQuoteFactory::create($recurring_invoice, auth()->user()->id);
// return $this->itemResponse($recurring_invoice);
break;
case 'clone_to_quote':
// $quote = CloneRecurringQuoteToQuoteFactory::create($recurring_invoice, auth()->user()->id);
// todo build the quote transformer and return response here
break;
case 'history':
# code...
break;
case 'delivery_note':
# code...
break;
case 'mark_paid':
# code...
break;
case 'archive':
# code...
break;
case 'delete':
# code...
break;
case 'email':
//dispatch email to queue
break;
default:
# code...
break;
}
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace App\Http\Requests\RecurringQuote;
use App\Http\Requests\Request;
use App\Models\RecurringQuote;
class ActionRecurringQuoteRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize() : bool
{
return auth()->user()->can('edit', $this->recurring_quote);
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace App\Http\Requests\RecurringQuote;
use App\Http\Requests\Request;
use App\Models\RecurringQuote;
class CreateRecurringQuoteRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize() : bool
{
return auth()->user()->can('create', RecurringQuote::class);
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace App\Http\Requests\RecurringQuote;
use App\Http\Requests\Request;
use App\Models\RecurringQuote;
class DestroyRecurringQuoteRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize() : bool
{
return auth()->user()->can('edit', $this->recurring_quote);
}
}

View File

@ -0,0 +1,40 @@
<?php
namespace App\Http\Requests\RecurringQuote;
use App\Http\Requests\Request;
use App\Models\RecurringQuote;
class EditRecurringQuoteRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return auth()->user()->can('edit', $this->recurring_quote);
}
public function rules()
{
$rules = [];
return $rules;
}
public function sanitize()
{
$input = $this->all();
//$input['id'] = $this->encodePrimaryKey($input['id']);
//$this->replace($input);
return $this->all();
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace App\Http\Requests\RecurringQuote;
use App\Http\Requests\Request;
use App\Models\RecurringQuote;
class ShowRecurringQuoteRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize() : bool
{
return auth()->user()->can('view', $this->recurring_quote);
}
}

View File

@ -0,0 +1,41 @@
<?php
namespace App\Http\Requests\RecurringQuote;
use App\Http\Requests\Request;
use App\Models\RecurringQuote;
class StoreRecurringQuoteRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize() : bool
{
return auth()->user()->can('create', RecurringQuote::class);
}
public function rules()
{
return [
'documents' => 'mimes:png,ai,svg,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx',
];
}
public function sanitize()
{
//do post processing of RecurringQuote request here, ie. RecurringQuote_items
}
public function messages()
{
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace App\Http\Requests\RecurringQuote;
use App\Http\Requests\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Validation\Rule;
class UpdateRecurringQuoteRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize() : bool
{
return auth()->user()->can('edit', $this->recurring_quote);
}
public function rules()
{
return [
'documents' => 'mimes:png,ai,svg,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx',
];
}
}

View File

@ -40,7 +40,7 @@ class RecurringInvoice extends BaseModel
const FREQUENCY_ANNUALLY = 9;
const FREQUENCY_TWO_YEARS = 10;
const RECURS_INDEFINITELY = 1;
const RECURS_INDEFINITELY = -1;
protected $guarded = [
'id',

View File

@ -0,0 +1,77 @@
<?php
namespace App\Models;
use App\Models\Filterable;
use App\Utils\Traits\MakesHash;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
* Class for Recurring Invoices.
*/
class RecurringQuote extends BaseModel
{
use MakesHash;
use SoftDeletes;
use Filterable;
/**
* Invoice Statuses
*/
const STATUS_DRAFT = 2;
const STATUS_ACTIVE = 3;
const STATUS_PENDING = -1;
const STATUS_COMPLETED = -2;
const STATUS_CANCELLED = -3;
/**
* Recurring intervals
*/
const FREQUENCY_WEEKLY = 1;
const FREQUENCY_TWO_WEEKS = 2;
const FREQUENCY_FOUR_WEEKS = 3;
const FREQUENCY_MONTHLY = 4;
const FREQUENCY_TWO_MONTHS = 5;
const FREQUENCY_THREE_MONTHS = 6;
const FREQUENCY_FOUR_MONTHS = 7;
const FREQUENCY_SIX_MONTHS = 8;
const FREQUENCY_ANNUALLY = 9;
const FREQUENCY_TWO_YEARS = 10;
const RECURS_INDEFINITELY = -1;
protected $guarded = [
'id',
];
protected $casts = [
'settings' => 'object'
];
protected $with = [
// 'client',
// 'company',
];
public function company()
{
return $this->belongsTo(Company::class);
}
public function client()
{
return $this->belongsTo(Client::class);
}
public function user()
{
return $this->belongsTo(User::class);
}
public function invitations()
{
$this->morphMany(RecurringQuoteInvitation::class);
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace App\Policies;
use App\Models\RecurringQuote;
use App\Models\User;
/**
* Class RecurringQuotePolicy
* @package App\Policies
*/
class RecurringQuotePolicy extends EntityPolicy
{
/**
* Checks if the user has create permissions
*
* @param User $user
* @return bool
*/
public function create(User $user) : bool
{
return $user->isAdmin() || $user->hasPermission('create_recurring_quote');
}
}

View File

@ -8,6 +8,7 @@ use App\Models\Payment;
use App\Models\Product;
use App\Models\Quote;
use App\Models\RecurringInvoice;
use App\Models\RecurringQuote;
use App\Models\User;
use App\Policies\ClientPolicy;
use App\Policies\InvoicePolicy;
@ -15,6 +16,7 @@ use App\Policies\PaymentPolicy;
use App\Policies\ProductPolicy;
use App\Policies\QuotePolicy;
use App\Policies\RecurringInvoicePolicy;
use App\Policies\RecurringQuotePolicy;
use Auth;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Gate;
@ -32,6 +34,7 @@ class AuthServiceProvider extends ServiceProvider
Invoice::class => InvoicePolicy::class,
Payment::class => PaymentPolicy::class,
RecurringInvoice::class => RecurringInvoicePolicy::class,
RecurringQuote::class => RecurringQuotePolicy::class,
Quote::class => QuotePolicy::class,
User::class => UserPolicy::class,
];

View File

@ -46,6 +46,10 @@ class RouteServiceProvider extends ServiceProvider
return \App\Models\RecurringInvoice::withTrashed()->where('id', $this->decodePrimaryKey($value))->firstOrFail();
});
Route::bind('recurring_quote', function ($value) {
return \App\Models\RecurringQuote::withTrashed()->where('id', $this->decodePrimaryKey($value))->firstOrFail();
});
Route::bind('quote', function ($value) {
return \App\Models\Quote::withTrashed()->where('id', $this->decodePrimaryKey($value))->firstOrFail();
});

View File

@ -0,0 +1,38 @@
<?php
namespace App\Repositories;
use App\Helpers\Invoice\InvoiceCalc;
use App\Models\RecurringQuote;
use Illuminate\Http\Request;
/**
* RecurringQuoteRepository
*/
class RecurringQuoteRepository extends BaseRepository
{
public function getClassName()
{
return RecurringQuote::class;
}
public function save(Request $request, RecurringQuote $quote) : ?RecurringQuote
{
$quote->fill($request->input());
$quote->save();
$quote_calc = new InvoiceCalc($quote, $quote->settings);
$quote = $quote_calc->build()->getInvoice();
//fire events here that cascading from the saving of an Quote
//ie. client balance update...
return $quote;
}
}

View File

@ -0,0 +1,162 @@
<?php
namespace App\Transformers;
use App\Models\Quote;
use App\Models\RecurringQuote;
use App\Utils\Traits\MakesHash;
/**
* @SWG\Definition(definition="Invoice", required={"invoice_number"}, @SWG\Xml(name="Invoice"))
*/
class RecurringQuoteTransformer extends EntityTransformer
{
use MakesHash;
/**
* @SWG\Property(property="id", type="integer", example=1, readOnly=true)
* @SWG\Property(property="amount", type="number", format="float", example=10, readOnly=true)
* @SWG\Property(property="balance", type="number", format="float", example=10, readOnly=true)
* @SWG\Property(property="updated_at", type="integer", example=1451160233, readOnly=true)
* @SWG\Property(property="archived_at", type="integer", example=1451160233, readOnly=true)
* @SWG\Property(property="is_deleted", type="boolean", example=false, readOnly=true)
* @SWG\Property(property="client_id", type="integer", example=1)
* @SWG\Property(property="status_id", type="integer", example=1, readOnly=true)
* @SWG\Property(property="invoice_number", type="string", example="0001")
* @SWG\Property(property="discount", type="number", format="float", example=10)
* @SWG\Property(property="po_number", type="string", example="0001")
* @SWG\Property(property="invoice_date", type="string", format="date", example="2018-01-01")
* @SWG\Property(property="due_date", type="string", format="date", example="2018-01-01")
* @SWG\Property(property="terms", type="string", example="sample")
* @SWG\Property(property="private_notes", type="string", example="Notes")
* @SWG\Property(property="public_notes", type="string", example="Notes")
* @SWG\Property(property="invoice_type_id", type="integer", example=1, readOnly=true)
* @SWG\Property(property="is_recurring", type="boolean", example=false)
* @SWG\Property(property="frequency_id", type="integer", example=1)
* @SWG\Property(property="start_date", type="string", format="date", example="2018-01-01")
* @SWG\Property(property="end_date", type="string", format="date", example="2018-01-01")
* @SWG\Property(property="last_sent_date", type="string", format="date", example="2018-01-01", readOnly=true)
* @SWG\Property(property="recurring_invoice_id", type="integer", example=1, readOnly=true)
* @SWG\Property(property="tax_name1", type="string", example="VAT")
* @SWG\Property(property="tax_name2", type="string", example="Upkeep")
* @SWG\Property(property="tax_rate1", type="number", format="float", example="17.5")
* @SWG\Property(property="tax_rate2", type="number", format="float", example="30.0")
* @SWG\Property(property="is_amount_discount", type="boolean", example=false)
* @SWG\Property(property="invoice_footer", type="string", example="Footer")
* @SWG\Property(property="partial", type="number",format="float", example=10)
* @SWG\Property(property="partial_due_date", type="string", format="date", example="2018-01-01")
* @SWG\Property(property="has_tasks", type="boolean", example=false, readOnly=true)
* @SWG\Property(property="auto_bill", type="boolean", example=false)
* @SWG\Property(property="custom_value1", type="number",format="float", example=10)
* @SWG\Property(property="custom_value2", type="number",format="float", example=10)
* @SWG\Property(property="custom_taxes1", type="boolean", example=false)
* @SWG\Property(property="custom_taxes2", type="boolean", example=false)
* @SWG\Property(property="has_expenses", type="boolean", example=false, readOnly=true)
* @SWG\Property(property="quote_invoice_id", type="integer", example=1, readOnly=true)
* @SWG\Property(property="custom_text_value1", type="string", example="Custom Text Value")
* @SWG\Property(property="custom_text_value2", type="string", example="Custom Text Value")
* @SWG\Property(property="is_quote", type="boolean", example=false, readOnly=true)
* @SWG\Property(property="is_public", type="boolean", example=false)
* @SWG\Property(property="filename", type="string", example="Filename", readOnly=true)
*/
protected $defaultIncludes = [
// 'invoice_items',
];
protected $availableIncludes = [
// 'invitations',
// 'payments',
// 'client',
// 'documents',
];
/*
public function includeInvoiceItems(Invoice $quote)
{
$transformer = new InvoiceItemTransformer($this->serializer);
return $this->includeCollection($quote->invoice_items, $transformer, ENTITY_INVOICE_ITEM);
}
public function includeInvitations(Invoice $quote)
{
$transformer = new InvitationTransformer($this->account, $this->serializer);
return $this->includeCollection($quote->invitations, $transformer, ENTITY_INVITATION);
}
public function includePayments(Invoice $quote)
{
$transformer = new PaymentTransformer($this->account, $this->serializer, $quote);
return $this->includeCollection($quote->payments, $transformer, ENTITY_PAYMENT);
}
public function includeClient(Invoice $quote)
{
$transformer = new ClientTransformer($this->account, $this->serializer);
return $this->includeItem($quote->client, $transformer, ENTITY_CLIENT);
}
public function includeExpenses(Invoice $quote)
{
$transformer = new ExpenseTransformer($this->account, $this->serializer);
return $this->includeCollection($quote->expenses, $transformer, ENTITY_EXPENSE);
}
public function includeDocuments(Invoice $quote)
{
$transformer = new DocumentTransformer($this->account, $this->serializer);
$quote->documents->each(function ($document) use ($quote) {
$document->setRelation('invoice', $quote);
});
return $this->includeCollection($quote->documents, $transformer, ENTITY_DOCUMENT);
}
*/
public function transform(RecurringQuote $quote)
{
return [
'id' => $this->encodePrimaryKey($quote->id),
'amount' => (float) $quote->amount,
'balance' => (float) $quote->balance,
'client_id' => (int) $quote->client_id,
'status_id' => (int) ($quote->status_id ?: 1),
'updated_at' => $quote->updated_at,
'archived_at' => $quote->deleted_at,
'discount' => (float) $quote->discount,
'po_number' => $quote->po_number,
'quote_date' => $quote->quote_date ?: '',
'valid_until' => $quote->valid_until ?: '',
'terms' => $quote->terms ?: '',
'public_notes' => $quote->public_notes ?: '',
'private_notes' => $quote->private_notes ?: '',
'is_deleted' => (bool) $quote->is_deleted,
'tax_name1' => $quote->tax_name1 ? $quote->tax_name1 : '',
'tax_rate1' => (float) $quote->tax_rate1,
'tax_name2' => $quote->tax_name2 ? $quote->tax_name2 : '',
'tax_rate2' => (float) $quote->tax_rate2,
'is_amount_discount' => (bool) ($quote->is_amount_discount ?: false),
'quote_footer' => $quote->quote_footer ?: '',
'partial' => (float) ($quote->partial ?: 0.0),
'partial_due_date' => $quote->partial_due_date ?: '',
'custom_value1' => (float) $quote->custom_value1,
'custom_value2' => (float) $quote->custom_value2,
'custom_taxes1' => (bool) $quote->custom_taxes1,
'custom_taxes2' => (bool) $quote->custom_taxes2,
'has_tasks' => (bool) $quote->has_tasks,
'has_expenses' => (bool) $quote->has_expenses,
'custom_text_value1' => $quote->custom_text_value1 ?: '',
'custom_text_value2' => $quote->custom_text_value2 ?: '',
'backup' => $quote->backup ?: '',
'settings' => $quote->settings,
'frequency_id' => (int) $quote->frequency_id,
'start_date' => $quote->start_date,
'last_sent_date' => $quote->last_sent_date,
'next_send_date' => $quote->next_send_date,
'remaining_cycles' => (int) $quote->remaining_cycles,
];
}
}

View File

@ -0,0 +1,33 @@
<?php
use App\DataMapper\ClientSettings;
use App\DataMapper\CompanySettings;
use Faker\Generator as Faker;
$factory->define(App\Models\RecurringQuote::class, function (Faker $faker) {
return [
'status_id' => App\Models\RecurringQuote::STATUS_DRAFT,
'quote_number' => $faker->text(256),
'discount' => $faker->numberBetween(1,10),
'is_amount_discount' => $faker->boolean(),
'tax_name1' => 'GST',
'tax_rate1' => 10,
'tax_name2' => 'VAT',
'tax_rate2' => 17.5,
'custom_value1' => $faker->numberBetween(1,4),
'custom_value2' => $faker->numberBetween(1,4),
'custom_value3' => $faker->numberBetween(1,4),
'custom_value4' => $faker->numberBetween(1,4),
'is_deleted' => false,
'po_number' => $faker->text(10),
'quote_date' => $faker->date(),
'valid_until' => $faker->date(),
'line_items' => false,
'backup' => '',
'frequency_id' => App\Models\RecurringQuote::FREQUENCY_MONTHLY,
'start_date' => $faker->date(),
'last_sent_date' => $faker->date(),
'next_send_date' => $faker->date(),
'remaining_cycles' => $faker->numberBetween(1,10),
];
});

View File

@ -472,6 +472,64 @@ class CreateUsersTable extends Migration
});
Schema::create('recurring_quotes', function ($t) {
$t->increments('id');
$t->unsignedInteger('client_id')->index();
$t->unsignedInteger('user_id');
$t->unsignedInteger('company_id')->index();
$t->unsignedInteger('status_id')->index();
$t->float('discount');
$t->boolean('is_amount_discount');
$t->string('quote_number');
$t->string('po_number');
$t->date('quote_date')->nullable();
$t->date('valid_until')->nullable();
$t->boolean('is_deleted')->default(false);
$t->text('line_items')->nullable();
$t->text('settings')->nullable();
$t->text('backup')->nullable();
$t->text('footer')->nullable();
$t->text('public_notes')->nullable();
$t->text('private_notes')->nullable();
$t->text('terms')->nullable();
$t->string('tax_name1');
$t->decimal('tax_rate1', 13, 3);
$t->string('tax_name2');
$t->decimal('tax_rate2', 13, 3);
$t->string('custom_value1')->nullable();
$t->string('custom_value2')->nullable();
$t->string('custom_value3')->nullable();
$t->string('custom_value4')->nullable();
$t->decimal('amount', 13, 2);
$t->decimal('balance', 13, 2);
$t->datetime('last_viewed')->nullable();
$t->unsignedInteger('frequency_id');
$t->date('start_date')->nullable();
$t->date('last_sent_date')->nullable();
$t->date('next_send_date')->nullable();
$t->unsignedInteger('remaining_cycles')->nullable();
$t->foreign('client_id')->references('id')->on('clients')->onDelete('cascade');
$t->foreign('company_id')->references('id')->on('companies')->onDelete('cascade');
$t->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
$t->timestamps();
$t->softDeletes();
});
Schema::create('quotes', function ($t) {
$t->increments('id');
$t->unsignedInteger('client_id')->index();

View File

@ -50,6 +50,10 @@ Route::group(['middleware' => ['db','api_secret_check','token_auth'], 'prefix' =
Route::post('recurring_invoices/bulk', 'RecurringInvoiceController@bulk')->name('recurring_invoices.bulk');
Route::resource('recurring_quotes', 'RecurringQuoteController'); // name = (recurring_invoices. index / create / show / update / destroy / edit
Route::post('recurring_quotes/bulk', 'RecurringQuoteController@bulk')->name('recurring_quotes.bulk');
Route::resource('client_statement', 'ClientStatementController@statement'); // name = (client_statement. index / create / show / update / destroy / edit
Route::resource('payments', 'PaymentController'); // name = (payments. index / create / show / update / destroy / edit

View File

@ -0,0 +1,203 @@
<?php
namespace Tests\Feature;
use App\DataMapper\ClientSettings;
use App\DataMapper\CompanySettings;
use App\Models\Account;
use App\Models\Client;
use App\Models\RecurringQuote;
use App\Utils\Traits\MakesHash;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Session;
use Tests\TestCase;
/**
* @test
* @covers App\Http\Controllers\RecurringQuoteController
*/
class RecurringQuoteTest extends TestCase
{
use MakesHash;
use DatabaseTransactions;
public function setUp() :void
{
parent::setUp();
Session::start();
$this->faker = \Faker\Factory::create();
Model::reguard();
}
public function testRecurringQuoteList()
{
$data = [
'first_name' => $this->faker->firstName,
'last_name' => $this->faker->lastName,
'email' => $this->faker->unique()->safeEmail,
'password' => 'ALongAndBrilliantPassword123',
'_token' => csrf_token(),
'privacy_policy' => 1,
'terms_of_service' => 1
];
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
])->post('/api/v1/signup', $data);
$acc = $response->json();
$account = Account::find($this->decodePrimaryKey($acc['data']['id']));
$company_token = $account->default_company->tokens()->first();
$token = $company_token->token;
$company = $company_token->company;
$user = $company_token->user;
$this->assertNotNull($company_token);
$this->assertNotNull($token);
$this->assertNotNull($user);
$this->assertNotNull($company);
$this->assertNotNull($user->tokens->first()->company);
factory(\App\Models\Client::class, 1)->create(['user_id' => $user->id, 'company_id' => $company->id])->each(function ($c) use ($user, $company){
factory(\App\Models\ClientContact::class,1)->create([
'user_id' => $user->id,
'client_id' => $c->id,
'company_id' => $company->id,
'is_primary' => 1
]);
factory(\App\Models\ClientContact::class,1)->create([
'user_id' => $user->id,
'client_id' => $c->id,
'company_id' => $company->id
]);
});
$client = Client::all()->first();
factory(\App\Models\RecurringQuote::class, 1)->create(['user_id' => $user->id, 'company_id' => $company->id, 'client_id' => $client->id]);
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $token,
])->get('/api/v1/recurring_quotes');
$response->assertStatus(200);
}
public function testRecurringQuoteRESTEndPoints()
{
$data = [
'first_name' => $this->faker->firstName,
'last_name' => $this->faker->lastName,
'email' => $this->faker->unique()->safeEmail,
'password' => 'ALongAndBrilliantPassword123',
'_token' => csrf_token(),
'privacy_policy' => 1,
'terms_of_service' => 1
];
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
])->post('/api/v1/signup', $data);
$acc = $response->json();
$account = Account::find($this->decodePrimaryKey($acc['data']['id']));
$company_token = $account->default_company->tokens()->first();
$token = $company_token->token;
$company = $company_token->company;
$user = $company_token->user;
$this->assertNotNull($company_token);
$this->assertNotNull($token);
$this->assertNotNull($user);
$this->assertNotNull($company);
$this->assertNotNull($user->tokens->first()->company);
factory(\App\Models\Client::class, 1)->create(['user_id' => $user->id, 'company_id' => $company->id])->each(function ($c) use ($user, $company){
factory(\App\Models\ClientContact::class,1)->create([
'user_id' => $user->id,
'client_id' => $c->id,
'company_id' => $company->id,
'is_primary' => 1
]);
factory(\App\Models\ClientContact::class,1)->create([
'user_id' => $user->id,
'client_id' => $c->id,
'company_id' => $company->id
]);
});
$client = Client::all()->first();
factory(\App\Models\RecurringQuote::class, 1)->create(['user_id' => $user->id, 'company_id' => $company->id, 'client_id' => $client->id]);
$RecurringQuote = RecurringQuote::where('user_id',$user->id)->first();
$RecurringQuote->settings = $client->getMergedSettings();
$RecurringQuote->save();
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $token,
])->get('/api/v1/recurring_quotes/'.$this->encodePrimaryKey($RecurringQuote->id));
$response->assertStatus(200);
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $token,
])->get('/api/v1/recurring_quotes/'.$this->encodePrimaryKey($RecurringQuote->id).'/edit');
$response->assertStatus(200);
$RecurringQuote_update = [
'status_id' => RecurringQuote::STATUS_DRAFT
];
$this->assertNotNull($RecurringQuote);
$this->assertNotNull($RecurringQuote->settings);
$this->assertTrue(property_exists($RecurringQuote->settings, 'custom_taxes1'));
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $token,
])->put('/api/v1/recurring_quotes/'.$this->encodePrimaryKey($RecurringQuote->id), $RecurringQuote_update)
->assertStatus(200);
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $token,
])->delete('/api/v1/recurring_quotes/'.$this->encodePrimaryKey($RecurringQuote->id));
$response->assertStatus(200);
}
}