Merge pull request #4138 from turbo124/v2

Projects API
This commit is contained in:
David Bomba 2020-10-08 09:31:37 +11:00 committed by GitHub
commit cd0dce3816
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
49 changed files with 1714 additions and 159 deletions

View File

@ -0,0 +1,49 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\DataMapper\Analytics;
class AccountCreated
{
/**
* The type of Sample.
*
* Monotonically incrementing counter
*
* - counter
*
* @var string
*/
public $type = 'counter';
/**
* The name of the counter.
* @var string
*/
public $name = 'account.created';
/**
* The datetime of the counter measurement.
*
* date("Y-m-d H:i:s")
*
* @var DateTime
*/
public $datetime;
/**
* The increment amount... should always be
* set to 0.
*
* @var int
*/
public $metric = 0;
}

View File

@ -0,0 +1,49 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\DataMapper\Analytics;
class AccountDeleted
{
/**
* The type of Sample.
*
* Monotonically incrementing counter
*
* - counter
*
* @var string
*/
public $type = 'counter';
/**
* The name of the counter.
* @var string
*/
public $name = 'account.deleted';
/**
* The datetime of the counter measurement.
*
* date("Y-m-d H:i:s")
*
* @var DateTime
*/
public $datetime;
/**
* The increment amount... should always be
* set to 0.
*
* @var int
*/
public $metric = 0;
}

View File

@ -0,0 +1,57 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\DataMapper\Analytics;
class EmailInvoiceFailure
{
/**
* The type of Sample.
*
* Monotonically incrementing counter
*
* - counter
*
* @var string
*/
public $type = 'mixed_metric';
/**
* The name of the counter.
* @var string
*/
public $name = 'job.failure.email_invoice';
/**
* The datetime of the counter measurement.
*
* date("Y-m-d H:i:s")
*
* @var DateTime
*/
public $datetime;
/**
* The Class failure name
* set to 0.
*
* @var string
*/
public $string_metric5 = '';
/**
* The exception string
* set to 0.
*
* @var string
*/
public $string_metric6 = '';
}

View File

@ -0,0 +1,57 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\DataMapper\Analytics;
class MigrationFailure
{
/**
* The type of Sample.
*
* Monotonically incrementing counter
*
* - counter
*
* @var string
*/
public $type = 'mixed_metric';
/**
* The name of the counter.
* @var string
*/
public $name = 'job.failure.migration';
/**
* The datetime of the counter measurement.
*
* date("Y-m-d H:i:s")
*
* @var DateTime
*/
public $datetime;
/**
* The Class failure name
* set to 0.
*
* @var string
*/
public $string_metric5 = '';
/**
* The exception string
* set to 0.
*
* @var string
*/
public $string_metric6 = '';
}

View File

@ -0,0 +1,57 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\DataMapper\Analytics;
class SendRecurringFailure
{
/**
* The type of Sample.
*
* Monotonically incrementing counter
*
* - counter
*
* @var string
*/
public $type = 'mixed_metric';
/**
* The name of the counter.
* @var string
*/
public $name = 'job.failure.send_recurring';
/**
* The datetime of the counter measurement.
*
* date("Y-m-d H:i:s")
*
* @var DateTime
*/
public $datetime;
/**
* The Class failure name
* set to 0.
*
* @var string
*/
public $string_metric5 = '';
/**
* The exception string
* set to 0.
*
* @var string
*/
public $string_metric6 = '';
}

View File

@ -0,0 +1,37 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Factory;
use App\Models\Project;
class ProjectFactory
{
public static function create(int $company_id, int $user_id) :Project
{
$project = new Project;
$project->company_id = $company_id;
$project->user_id = $user_id;
$project->public_notes = '';
$project->private_notes = '';
$project->budgeted_hours = 0;
$project->task_rate = 0;
$project->name = '';
$project->custom_value1 = '';
$project->custom_value2 = '';
$project->custom_value3 = '';
$project->custom_value4 = '';
$project->is_deleted = 0;
return $project;
}
}

View File

@ -30,7 +30,7 @@ class RecurringInvoiceToInvoiceFactory
$invoice->terms = $recurring_invoice->terms; $invoice->terms = $recurring_invoice->terms;
$invoice->public_notes = $recurring_invoice->public_notes; $invoice->public_notes = $recurring_invoice->public_notes;
$invoice->private_notes = $recurring_invoice->private_notes; $invoice->private_notes = $recurring_invoice->private_notes;
$invoice->date = date_create()->format($client->date_format()); //$invoice->date = now()->format($client->date_format());
$invoice->due_date = $recurring_invoice->calculateDueDate($recurring_invoice->next_send_date); $invoice->due_date = $recurring_invoice->calculateDueDate($recurring_invoice->next_send_date);
$invoice->is_deleted = $recurring_invoice->is_deleted; $invoice->is_deleted = $recurring_invoice->is_deleted;
$invoice->line_items = $recurring_invoice->line_items; $invoice->line_items = $recurring_invoice->line_items;

View File

@ -0,0 +1,145 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Filters;
use App\Models\Project;
use App\Models\User;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Gate;
/**
* ProjectFilters.
*/
class ProjectFilters 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('projects.name', 'like', '%'.$filter.'%')
->orWhere('projects.public_notes', 'like', '%'.$filter.'%')
->orWhere('projects.private_notes', '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 = 'projects';
$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
{
$query = DB::table('projects')
->join('companies', 'companies.id', '=', 'projects.company_id')
->where('projects.company_id', '=', $company_id)
//->whereRaw('(projects.name != "" or contacts.first_name != "" or contacts.last_name != "" or contacts.email != "")') // filter out buy now invoices
->select(
'projects.id',
'projects.name',
'projects.public_notes',
'projects.private_notes',
'projects.created_at',
'projects.created_at as project_created_at',
'projects.deleted_at',
'projects.is_deleted',
'projects.user_id',
'projects.assigned_user_id',
);
/*
* If the user does not have permissions to view all invoices
* limit the user to only the invoices they have created
*/
if (Gate::denies('view-list', Project::class)) {
$query->where('projects.user_id', '=', $user->id);
}
return $query;
}
/**
* 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);
return $this->builder->whereCompanyId(auth()->user()->company()->id)->orWhere('company_id', null);
}
}

View File

@ -230,7 +230,10 @@ class InvoiceItemSum
continue; continue;
} }
$amount = $this->item->line_total - ($this->item->line_total * ($this->invoice->discount / $this->sub_total)); //$amount = $this->item->line_total - ($this->item->line_total * ($this->invoice->discount / $this->sub_total));
$amount = ( $this->sub_total > 0 ) ? $this->item->line_total - ($this->item->line_total * ($this->invoice->discount / $this->sub_total)) : 0;
$item_tax_rate1_total = $this->calcAmountLineTax($this->item->tax_rate1, $amount); $item_tax_rate1_total = $this->calcAmountLineTax($this->item->tax_rate1, $amount);
$item_tax += $item_tax_rate1_total; $item_tax += $item_tax_rate1_total;
@ -260,7 +263,8 @@ class InvoiceItemSum
} }
/** /**
* Sets default values for the line_items. * Sets default casts for the values in the line_items.
*
* @return $this * @return $this
*/ */
private function cleanLineItem() private function cleanLineItem()

View File

@ -11,6 +11,7 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\DataMapper\Analytics\AccountDeleted;
use App\DataMapper\CompanySettings; use App\DataMapper\CompanySettings;
use App\DataMapper\DefaultSettings; use App\DataMapper\DefaultSettings;
use App\Http\Requests\Company\CreateCompanyRequest; use App\Http\Requests\Company\CreateCompanyRequest;
@ -40,6 +41,7 @@ use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Hash;
use Turbo124\Beacon\Facades\LightLogs;
/** /**
* Class CompanyController. * Class CompanyController.
@ -471,6 +473,11 @@ class CompanyController extends BaseController
} }
$account->delete(); $account->delete();
LightLogs::create(new AccountDeleted())
->increment()
->batch();
} else { } else {
$company_id = $company->id; $company_id = $company->id;
$company->delete(); $company->delete();

View File

@ -0,0 +1,485 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Http\Controllers;
use App\Factory\ProjectFactory;
use App\Filters\ProjectFilters;
use App\Http\Requests\Project\CreateProjectRequest;
use App\Http\Requests\Project\DestroyProjectRequest;
use App\Http\Requests\Project\EditProjectRequest;
use App\Http\Requests\Project\ShowProjectRequest;
use App\Http\Requests\Project\StoreProjectRequest;
use App\Http\Requests\Project\UpdateProjectRequest;
use App\Jobs\Entity\ActionEntity;
use App\Models\Project;
use App\Repositories\ProjectRepository;
use App\Transformers\ProjectTransformer;
use App\Utils\Traits\BulkOptions;
use App\Utils\Traits\MakesHash;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
/**
* Class ProjectController.
*/
class ProjectController extends BaseController
{
use MakesHash;
protected $entity_type = Project::class;
protected $entity_transformer = ProjectTransformer::class;
protected $project_repo;
/**
* ProjectController constructor.
* @param ProjectRepository $projectRepo
*/
public function __construct(ProjectRepository $project_repo)
{
parent::__construct();
$this->project_repo = $project_repo;
}
/**
* @OA\Get(
* path="/api/v1/projects",
* operationId="getProjects",
* tags={"projects"},
* summary="Gets a list of projects",
* description="Lists projects",
* @OA\Parameter(ref="#/components/parameters/X-Api-Secret"),
* @OA\Parameter(ref="#/components/parameters/X-Api-Token"),
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
* @OA\Parameter(ref="#/components/parameters/include"),
* @OA\Parameter(ref="#/components/parameters/index"),
* @OA\Response(
* response=200,
* description="A list of projects",
* @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"),
* @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"),
* @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"),
* @OA\JsonContent(ref="#/components/schemas/Project"),
* ),
* @OA\Response(
* response=422,
* description="Validation error",
* @OA\JsonContent(ref="#/components/schemas/ValidationError"),
* ),
* @OA\Response(
* response="default",
* description="Unexpected Error",
* @OA\JsonContent(ref="#/components/schemas/Error"),
* ),
* )
*/
public function index(ProjectFilters $filters)
{
$projects = Project::filter($filters);
return $this->listResponse($projects);
}
/**
* Display the specified resource.
*
* @param int $id
* @return \Illuminate\Http\Response
*
*
* @OA\Get(
* path="/api/v1/projects/{id}",
* operationId="showProject",
* tags={"projects"},
* summary="Shows a project",
* description="Displays a project by id",
* @OA\Parameter(ref="#/components/parameters/X-Api-Secret"),
* @OA\Parameter(ref="#/components/parameters/X-Api-Token"),
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
* @OA\Parameter(ref="#/components/parameters/include"),
* @OA\Parameter(
* name="id",
* in="path",
* description="The Project Hashed ID",
* example="D2J234DFA",
* required=true,
* @OA\Schema(
* type="string",
* format="string",
* ),
* ),
* @OA\Response(
* response=200,
* description="Returns the expense object",
* @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"),
* @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"),
* @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"),
* @OA\JsonContent(ref="#/components/schemas/Project"),
* ),
* @OA\Response(
* response=422,
* description="Validation error",
* @OA\JsonContent(ref="#/components/schemas/ValidationError"),
*
* ),
* @OA\Response(
* response="default",
* description="Unexpected Error",
* @OA\JsonContent(ref="#/components/schemas/Error"),
* ),
* )
*/
public function show(ShowProjectRequest $request, Project $project)
{
return $this->itemResponse($project);
}
/**
* Show the form for editing the specified resource.
*
* @param int $id
* @return \Illuminate\Http\Response
*
*
* @OA\Get(
* path="/api/v1/projects/{id}/edit",
* operationId="editProject",
* tags={"projects"},
* summary="Shows a project for editting",
* description="Displays a project by id",
* @OA\Parameter(ref="#/components/parameters/X-Api-Secret"),
* @OA\Parameter(ref="#/components/parameters/X-Api-Token"),
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
* @OA\Parameter(ref="#/components/parameters/include"),
* @OA\Parameter(
* name="id",
* in="path",
* description="The Project Hashed ID",
* example="D2J234DFA",
* required=true,
* @OA\Schema(
* type="string",
* format="string",
* ),
* ),
* @OA\Response(
* response=200,
* description="Returns the project object",
* @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"),
* @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"),
* @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"),
* @OA\JsonContent(ref="#/components/schemas/Project"),
* ),
* @OA\Response(
* response=422,
* description="Validation error",
* @OA\JsonContent(ref="#/components/schemas/ValidationError"),
*
* ),
* @OA\Response(
* response="default",
* description="Unexpected Error",
* @OA\JsonContent(ref="#/components/schemas/Error"),
* ),
* )
*/
public function edit(EditProjectRequest $request, Project $project)
{
return $this->itemResponse($project);
}
/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param App\Models\Project $project
* @return \Illuminate\Http\Response
*
*
*
* @OA\Put(
* path="/api/v1/projects/{id}",
* operationId="updateProject",
* tags={"projects"},
* summary="Updates a project",
* description="Handles the updating of a project by id",
* @OA\Parameter(ref="#/components/parameters/X-Api-Secret"),
* @OA\Parameter(ref="#/components/parameters/X-Api-Token"),
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
* @OA\Parameter(ref="#/components/parameters/include"),
* @OA\Parameter(
* name="id",
* in="path",
* description="The Project Hashed ID",
* example="D2J234DFA",
* required=true,
* @OA\Schema(
* type="string",
* format="string",
* ),
* ),
* @OA\Response(
* response=200,
* description="Returns the project object",
* @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"),
* @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"),
* @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"),
* @OA\JsonContent(ref="#/components/schemas/Project"),
* ),
* @OA\Response(
* response=422,
* description="Validation error",
* @OA\JsonContent(ref="#/components/schemas/ValidationError"),
*
* ),
* @OA\Response(
* response="default",
* description="Unexpected Error",
* @OA\JsonContent(ref="#/components/schemas/Error"),
* ),
* )
*/
public function update(UpdateProjectRequest $request, Project $project)
{
if ($request->entityIsDeleted($project)) {
return $request->disallowUpdate();
}
$project->fill($request->all());
$project->save();
return $this->itemResponse($project->fresh());
}
/**
* Show the form for creating a new resource.
*
* @return \Illuminate\Http\Response
*
*
*
* @OA\Get(
* path="/api/v1/projects/create",
* operationId="getProjectsCreate",
* tags={"projects"},
* summary="Gets a new blank project object",
* description="Returns a blank object with default values",
* @OA\Parameter(ref="#/components/parameters/X-Api-Secret"),
* @OA\Parameter(ref="#/components/parameters/X-Api-Token"),
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
* @OA\Parameter(ref="#/components/parameters/include"),
* @OA\Response(
* response=200,
* description="A blank project object",
* @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"),
* @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"),
* @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"),
* @OA\JsonContent(ref="#/components/schemas/Project"),
* ),
* @OA\Response(
* response=422,
* description="Validation error",
* @OA\JsonContent(ref="#/components/schemas/ValidationError"),
*
* ),
* @OA\Response(
* response="default",
* description="Unexpected Error",
* @OA\JsonContent(ref="#/components/schemas/Error"),
* ),
* )
*/
public function create(CreateProjectRequest $request)
{
$project = ProjectFactory::create(auth()->user()->company()->id, auth()->user()->id);
return $this->itemResponse($project);
}
/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*
*
*
* @OA\Post(
* path="/api/v1/projects",
* operationId="storeProject",
* tags={"projects"},
* summary="Adds a project",
* description="Adds an project to a company",
* @OA\Parameter(ref="#/components/parameters/X-Api-Secret"),
* @OA\Parameter(ref="#/components/parameters/X-Api-Token"),
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
* @OA\Parameter(ref="#/components/parameters/include"),
* @OA\Response(
* response=200,
* description="Returns the saved project object",
* @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"),
* @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"),
* @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"),
* @OA\JsonContent(ref="#/components/schemas/Project"),
* ),
* @OA\Response(
* response=422,
* description="Validation error",
* @OA\JsonContent(ref="#/components/schemas/ValidationError"),
*
* ),
* @OA\Response(
* response="default",
* description="Unexpected Error",
* @OA\JsonContent(ref="#/components/schemas/Error"),
* ),
* )
*/
public function store(StoreProjectRequest $request)
{
$project = ProjectFactory::create(auth()->user()->company()->id, auth()->user()->id);
$project->fill($request->all());
$project->save();
return $this->itemResponse($project->fresh());
}
/**
* Remove the specified resource from storage.
*
* @param int $id
* @return \Illuminate\Http\Response
*
*
* @OA\Delete(
* path="/api/v1/projects/{id}",
* operationId="deleteProject",
* tags={"projects"},
* summary="Deletes a project",
* description="Handles the deletion of a project by id",
* @OA\Parameter(ref="#/components/parameters/X-Api-Secret"),
* @OA\Parameter(ref="#/components/parameters/X-Api-Token"),
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
* @OA\Parameter(ref="#/components/parameters/include"),
* @OA\Parameter(
* name="id",
* in="path",
* description="The Project Hashed ID",
* example="D2J234DFA",
* required=true,
* @OA\Schema(
* type="string",
* format="string",
* ),
* ),
* @OA\Response(
* response=200,
* description="Returns a HTTP status",
* @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"),
* @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"),
* @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"),
* ),
* @OA\Response(
* response=422,
* description="Validation error",
* @OA\JsonContent(ref="#/components/schemas/ValidationError"),
*
* ),
* @OA\Response(
* response="default",
* description="Unexpected Error",
* @OA\JsonContent(ref="#/components/schemas/Error"),
* ),
* )
*/
public function destroy(DestroyProjectRequest $request, Project $project)
{
//may not need these destroy routes as we are using actions to 'archive/delete'
$project->is_deleted = true;
$project->delete();
$project->save();
return response()->json([], 200);
}
/**
* Perform bulk actions on the list view.
*
* @param BulkProjectRequest $request
* @return \Illuminate\Http\Response
*
*
* @OA\Post(
* path="/api/v1/projects/bulk",
* operationId="bulkProjects",
* tags={"projects"},
* summary="Performs bulk actions on an array of projects",
* description="",
* @OA\Parameter(ref="#/components/parameters/X-Api-Secret"),
* @OA\Parameter(ref="#/components/parameters/X-Api-Token"),
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
* @OA\Parameter(ref="#/components/parameters/index"),
* @OA\RequestBody(
* description="User credentials",
* required=true,
* @OA\MediaType(
* mediaType="application/json",
* @OA\Schema(
* type="array",
* @OA\Items(
* type="integer",
* description="Array of hashed IDs to be bulk 'actioned",
* example="[0,1,2,3]",
* ),
* )
* )
* ),
* @OA\Response(
* response=200,
* description="The Project User response",
* @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"),
* @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"),
* @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"),
* @OA\JsonContent(ref="#/components/schemas/Project"),
* ),
* @OA\Response(
* response=422,
* description="Validation error",
* @OA\JsonContent(ref="#/components/schemas/ValidationError"),
* ),
* @OA\Response(
* response="default",
* description="Unexpected Error",
* @OA\JsonContent(ref="#/components/schemas/Error"),
* ),
* )
*/
public function bulk()
{
$action = request()->input('action');
$ids = request()->input('ids');
$projects = Project::withTrashed()->find($this->transformKeys($ids));
$projects->each(function ($project, $key) use ($action) {
if (auth()->user()->can('edit', $project)) {
$this->project_repo->{$action}($project);
}
});
return $this->listResponse(Project::withTrashed()->whereIn('id', $this->transformKeys($ids)));
}
}

View File

@ -1,47 +0,0 @@
<?php
namespace App\Http\Requests\Client;
use App\Models\Client;
use App\Utils\Traits\BulkOptions;
use Illuminate\Foundation\Http\FormRequest;
class BulkClientRequest extends FormRequest
{
use BulkOptions;
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
if (! $this->has('action')) {
return false;
}
if (! in_array($this->action, $this->getBulkOptions(), true)) {
return false;
}
return auth()->user()->can(auth()->user()->isAdmin(), Client::class);
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
$rules = $this->getGlobalRules();
/* We don't require IDs on bulk storing. */
if ($this->action !== self::$STORE_METHOD) {
$rules['ids'] = ['required'];
}
return $rules;
}
}

View File

@ -0,0 +1,28 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Http\Requests\Project;
use App\Http\Requests\Request;
use App\Models\Project;
class CreateProjectRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize() : bool
{
return auth()->user()->isAdmin();
}
}

View File

@ -0,0 +1,27 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Http\Requests\Project;
use App\Http\Requests\Request;
class DestroyProjectRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize() : bool
{
return auth()->user()->isAdmin();
}
}

View File

@ -0,0 +1,39 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Http\Requests\Project;
use App\Http\Requests\Request;
class EditProjectRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize() : bool
{
return auth()->user()->isAdmin();
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
//
];
}
}

View File

@ -0,0 +1,39 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Http\Requests\Project;
use App\Http\Requests\Request;
class ShowProjectRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize() : bool
{
return auth()->user()->isAdmin();
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
//
];
}
}

View File

@ -0,0 +1,53 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Http\Requests\Project;
use App\Http\Requests\Request;
use App\Models\Project;
use App\Utils\Traits\MakesHash;
class StoreProjectRequest extends Request
{
use MakesHash;
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize() : bool
{
return auth()->user()->can('create', Project::class);
}
public function rules()
{
$rules = [];
$rules['name'] ='required|unique:projects,name,null,null,company_id,'.auth()->user()->companyId();
$rules['client_id'] = 'required|exists:clients,id,company_id,'.auth()->user()->company()->id;
return $rules;
}
protected function prepareForValidation()
{
$input = $this->all();
if (array_key_exists('client_id', $input) && is_string($input['client_id'])) {
$input['client_id'] = $this->decodePrimaryKey($input['client_id']);
}
$this->replace($input);
}
}

View File

@ -0,0 +1,43 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Http\Requests\Project;
use App\Http\Requests\Request;
use App\Models\Project;
use App\Utils\Traits\ChecksEntityStatus;
class UpdateProjectRequest extends Request
{
use ChecksEntityStatus;
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize() : bool
{
return auth()->user()->can('edit', $this->project);
}
public function rules()
{
return [];
}
protected function prepareForValidation()
{
$input = $this->all();
$this->replace($input);
}
}

View File

@ -54,7 +54,7 @@ class UpdateRecurringInvoiceRequest extends Request
protected function prepareForValidation() protected function prepareForValidation()
{ {
$input = $this->all(); $input = $this->all();
info($input);
if (array_key_exists('design_id', $input) && is_string($input['design_id'])) { if (array_key_exists('design_id', $input) && is_string($input['design_id'])) {
$input['design_id'] = $this->decodePrimaryKey($input['design_id']); $input['design_id'] = $this->decodePrimaryKey($input['design_id']);
} }
@ -91,16 +91,27 @@ class UpdateRecurringInvoiceRequest extends Request
$this->replace($input); $this->replace($input);
} }
private function setAutoBillFlag($auto_bill) /**
* if($auto_bill == '')
* off / optin / optout will reset the status of this field to off to allow
* the client to choose whether to auto_bill or not.
*
* @param enum $auto_bill off/always/optin/optout
*
* @return bool
*/
private function setAutoBillFlag($auto_bill) :bool
{ {
if($auto_bill == 'always') if($auto_bill == 'always')
return true; return true;
if($auto_bill == 'off') // if($auto_bill == '')
return false; // off / optin / optout will reset the status of this field to off to allow
// the client to choose whether to auto_bill or not.
//todo do we need to handle optin / optout here?
return false;
} }
} }

View File

@ -55,6 +55,9 @@ class UniqueInvoiceNumberRule implements Rule
*/ */
private function checkIfInvoiceNumberUnique() : bool private function checkIfInvoiceNumberUnique() : bool
{ {
if(empty($this->input['number']))
return true;
$invoice = Invoice::where('client_id', $this->input['client_id']) $invoice = Invoice::where('client_id', $this->input['client_id'])
->where('number', $this->input['number']) ->where('number', $this->input['number'])
->withTrashed() ->withTrashed()

View File

@ -1,7 +1,17 @@
<?php <?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Jobs\Account; namespace App\Jobs\Account;
use App\DataMapper\Analytics\AccountCreated as AnalyticsAccountCreated;
use App\Events\Account\AccountCreated; use App\Events\Account\AccountCreated;
use App\Jobs\Company\CreateCompany; use App\Jobs\Company\CreateCompany;
use App\Jobs\Company\CreateCompanyPaymentTerms; use App\Jobs\Company\CreateCompanyPaymentTerms;
@ -19,6 +29,7 @@ use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Notification; use Illuminate\Support\Facades\Notification;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Turbo124\Beacon\Facades\LightLogs;
class CreateAccount class CreateAccount
{ {
@ -80,6 +91,10 @@ class CreateAccount
$sp035a66->notification(new NewAccountCreated($spaa9f78, $sp035a66))->ninja(); $sp035a66->notification(new NewAccountCreated($spaa9f78, $sp035a66))->ninja();
LightLogs::create(new AnalyticsAccountCreated())
->increment()
->batch();
return $sp794f3f; return $sp794f3f;
} }
} }

View File

@ -11,6 +11,7 @@
namespace App\Jobs\Invoice; namespace App\Jobs\Invoice;
use App\DataMapper\Analytics\EmailInvoiceFailure;
use App\Events\Invoice\InvoiceWasEmailed; use App\Events\Invoice\InvoiceWasEmailed;
use App\Events\Invoice\InvoiceWasEmailedAndFailed; use App\Events\Invoice\InvoiceWasEmailedAndFailed;
use App\Helpers\Email\InvoiceEmail; use App\Helpers\Email\InvoiceEmail;
@ -30,6 +31,7 @@ use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Mail;
use Symfony\Component\Mime\Test\Constraint\EmailTextBodyContains; use Symfony\Component\Mime\Test\Constraint\EmailTextBodyContains;
use Turbo124\Beacon\Facades\LightLogs;
/*Multi Mailer implemented*/ /*Multi Mailer implemented*/
@ -95,4 +97,17 @@ class EmailInvoice extends BaseMailerJob implements ShouldQueue
/* Mark invoice sent */ /* Mark invoice sent */
$this->invoice_invitation->invoice->service()->markSent()->save(); $this->invoice_invitation->invoice->service()->markSent()->save();
} }
public function failed($exception = null)
{
info('the job failed');
$job_failure = new EmailInvoiceFailure();
$job_failure->string_metric5 = get_class($this);
$job_failure->string_metric6 = $exception->getMessage();
LightLogs::create($job_failure)
->batch();
}
} }

View File

@ -11,6 +11,7 @@
namespace App\Jobs\RecurringInvoice; namespace App\Jobs\RecurringInvoice;
use App\DataMapper\Analytics\SendRecurringFailure;
use App\Events\Invoice\InvoiceWasEmailed; use App\Events\Invoice\InvoiceWasEmailed;
use App\Factory\RecurringInvoiceToInvoiceFactory; use App\Factory\RecurringInvoiceToInvoiceFactory;
use App\Helpers\Email\InvoiceEmail; use App\Helpers\Email\InvoiceEmail;
@ -26,6 +27,7 @@ use Illuminate\Http\Request;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use Turbo124\Beacon\Facades\LightLogs;
class SendRecurring implements ShouldQueue class SendRecurring implements ShouldQueue
{ {
@ -58,7 +60,9 @@ class SendRecurring implements ShouldQueue
// Generate Standard Invoice // Generate Standard Invoice
$invoice = RecurringInvoiceToInvoiceFactory::create($this->recurring_invoice, $this->recurring_invoice->client); $invoice = RecurringInvoiceToInvoiceFactory::create($this->recurring_invoice, $this->recurring_invoice->client);
$invoice->date = now()->format('Y-m-d');
$invoice = $invoice->service() $invoice = $invoice->service()
->markSent() ->markSent()
->applyNumber() ->applyNumber()
@ -71,9 +75,10 @@ class SendRecurring implements ShouldQueue
$email_builder = (new InvoiceEmail())->build($invitation); $email_builder = (new InvoiceEmail())->build($invitation);
EmailInvoice::dispatch($email_builder, $invitation, $invoice->company); if($invitation->contact && strlen($invitation->contact->email) >=1){
EmailInvoice::dispatch($email_builder, $invitation, $invoice->company);
info("Firing email for invoice {$invoice->number}"); info("Firing email for invoice {$invoice->number}");
}
}); });
@ -101,4 +106,18 @@ class SendRecurring implements ShouldQueue
} }
public function failed($exception = null)
{
info('the job failed');
$job_failure = new SendRecurringFailure();
$job_failure->string_metric5 = get_class($this);
$job_failure->string_metric6 = $exception->getMessage();
LightLogs::create($job_failure)
->batch();
info(print_r($exception->getMessage(), 1));
}
} }

View File

@ -11,6 +11,7 @@
namespace App\Jobs\Util; namespace App\Jobs\Util;
use App\DataMapper\Analytics\MigrationFailure;
use App\DataMapper\CompanySettings; use App\DataMapper\CompanySettings;
use App\Exceptions\MigrationValidatorFailed; use App\Exceptions\MigrationValidatorFailed;
use App\Exceptions\ResourceDependencyMissing; use App\Exceptions\ResourceDependencyMissing;
@ -72,6 +73,7 @@ use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Validator; use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Turbo124\Beacon\Facades\LightLogs;
class Import implements ShouldQueue class Import implements ShouldQueue
{ {
@ -966,6 +968,15 @@ class Import implements ShouldQueue
public function failed($exception = null) public function failed($exception = null)
{ {
info('the job failed'); info('the job failed');
$job_failure = new MigrationFailure();
$job_failure->string_metric5 = get_class($this);
$job_failure->string_metric6 = $exception->getMessage();
LightLogs::create($job_failure)
->batch();
info(print_r($exception->getMessage(), 1)); info(print_r($exception->getMessage(), 1));
} }
} }

View File

@ -156,6 +156,11 @@ class Client extends BaseModel implements HasLocalePreference
->first(); ->first();
} }
public function credits()
{
return $this->hasMany(Credit::class)->withTrashed();
}
public function activities() public function activities()
{ {
return $this->hasMany(Activity::class)->orderBy('id', 'desc'); return $this->hasMany(Activity::class)->orderBy('id', 'desc');

View File

@ -37,6 +37,7 @@ class Gateway extends StaticModel
/** /**
* @return mixed * @return mixed
* @deprecated 5.0.17 No longer needs as we are removing omnipay dependence
*/ */
public function getFields() public function getFields()
{ {
@ -116,7 +117,7 @@ class Gateway extends StaticModel
return ['methods' => [GatewayType::CREDIT_CARD], 'refund' => true, 'token_billing' => true ]; //Checkout return ['methods' => [GatewayType::CREDIT_CARD], 'refund' => true, 'token_billing' => true ]; //Checkout
break; break;
default: default:
return []; return ['methods' => [], 'refund' => false, 'token_billing' => false];
break; break;
} }
} }

View File

@ -2,6 +2,7 @@
namespace App\Models; namespace App\Models;
use App\Models\Filterable;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
use Laracasts\Presenter\PresentableTrait; use Laracasts\Presenter\PresentableTrait;
@ -13,7 +14,8 @@ class Project extends BaseModel
// Expense Categories // Expense Categories
use SoftDeletes; use SoftDeletes;
use PresentableTrait; use PresentableTrait;
use Filterable;
/** /**
* @var array * @var array
*/ */
@ -24,17 +26,16 @@ class Project extends BaseModel
*/ */
protected $fillable = [ protected $fillable = [
'name', 'name',
'client_id',
'task_rate', 'task_rate',
'private_notes', 'private_notes',
'public_notes',
'due_date', 'due_date',
'budgeted_hours', 'budgeted_hours',
'custom_value1', 'custom_value1',
'custom_value2', 'custom_value2',
]; 'custom_value3',
'custom_value4',
protected $casts = [
'updated_at' => 'timestamp',
'created_at' => 'timestamp',
]; ];
public function getEntityType() public function getEntityType()

View File

@ -44,20 +44,8 @@ class RecurringInvoice extends BaseModel
const STATUS_PENDING = -1; const STATUS_PENDING = -1;
/** /**
* Recurring intervals //todo MAP WHEN WE MIGRATE. * Invoice Frequencies.
*/ */
/* Make sure we support overflow!!!!!!!!!!
$start = Carbon::today();
$subscription = Carbon::parse('2017-12-31');
foreach (range(1, 12) as $month) {
$day = $start->addMonthNoOverflow()->thisDayOrLast($subscription->day);
echo "You will be billed on {$day} in month {$month}\n";
}
*/
const FREQUENCY_DAILY = 1; const FREQUENCY_DAILY = 1;
const FREQUENCY_WEEKLY = 2; const FREQUENCY_WEEKLY = 2;
const FREQUENCY_TWO_WEEKS = 3; const FREQUENCY_TWO_WEEKS = 3;

View File

@ -0,0 +1,41 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Policies;
use App\Models\User;
use Illuminate\Auth\Access\HandlesAuthorization;
class ProjectPolicy extends EntityPolicy
{
use HandlesAuthorization;
/**
* Create a new policy instance.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Checks if the user has create permissions.
*
* @param User $user
* @return bool
*/
public function create(User $user) : bool
{
return $user->isAdmin() || $user->hasPermission('create_project') || $user->hasPermission('create_all');
}
}

View File

@ -25,6 +25,7 @@ use App\Models\Invoice;
use App\Models\Payment; use App\Models\Payment;
use App\Models\PaymentTerm; use App\Models\PaymentTerm;
use App\Models\Product; use App\Models\Product;
use App\Models\Project;
use App\Models\Quote; use App\Models\Quote;
use App\Models\RecurringInvoice; use App\Models\RecurringInvoice;
use App\Models\RecurringQuote; use App\Models\RecurringQuote;
@ -46,6 +47,7 @@ use App\Policies\InvoicePolicy;
use App\Policies\PaymentPolicy; use App\Policies\PaymentPolicy;
use App\Policies\PaymentTermPolicy; use App\Policies\PaymentTermPolicy;
use App\Policies\ProductPolicy; use App\Policies\ProductPolicy;
use App\Policies\ProjectPolicy;
use App\Policies\QuotePolicy; use App\Policies\QuotePolicy;
use App\Policies\RecurringInvoicePolicy; use App\Policies\RecurringInvoicePolicy;
use App\Policies\RecurringQuotePolicy; use App\Policies\RecurringQuotePolicy;
@ -79,6 +81,7 @@ class AuthServiceProvider extends ServiceProvider
Payment::class => PaymentPolicy::class, Payment::class => PaymentPolicy::class,
PaymentTerm::class => PaymentTermPolicy::class, PaymentTerm::class => PaymentTermPolicy::class,
Product::class => ProductPolicy::class, Product::class => ProductPolicy::class,
Project::class => ProjectPolicy::class,
Quote::class => QuotePolicy::class, Quote::class => QuotePolicy::class,
RecurringInvoice::class => RecurringInvoicePolicy::class, RecurringInvoice::class => RecurringInvoicePolicy::class,
RecurringQuote::class => RecurringQuotePolicy::class, RecurringQuote::class => RecurringQuotePolicy::class,

View File

@ -0,0 +1,31 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Repositories;
use App\Models\Project;
/**
* Class for project repository.
*/
class ProjectRepository extends BaseRepository
{
/**
* Gets the class name.
*
* @return string The class name.
*/
public function getClassName()
{
return Project::class;
}
}

View File

@ -44,11 +44,11 @@ class ApplyNumber extends AbstractService
switch ($this->client->getSetting('counter_number_applied')) { switch ($this->client->getSetting('counter_number_applied')) {
case 'when_saved': case 'when_saved':
$this->invoice->number = $this->getNextInvoiceNumber($this->client); $this->invoice->number = $this->getNextInvoiceNumber($this->client, $this->invoice);
break; break;
case 'when_sent': case 'when_sent':
if ($this->invoice->status_id == Invoice::STATUS_SENT) { if ($this->invoice->status_id == Invoice::STATUS_SENT) {
$this->invoice->number = $this->getNextInvoiceNumber($this->client); $this->invoice->number = $this->getNextInvoiceNumber($this->client, $this->invoice);
} }
break; break;

View File

@ -30,6 +30,8 @@ class AutoBillInvoice extends AbstractService
private $client; private $client;
private $payment;
public function __construct(Invoice $invoice) public function __construct(Invoice $invoice)
{ {
$this->invoice = $invoice; $this->invoice = $invoice;
@ -39,32 +41,37 @@ class AutoBillInvoice extends AbstractService
public function run() public function run()
{ {
if (! $this->invoice->isPayable()) { /* Is the invoice payable? */
if (! $this->invoice->isPayable())
return $this->invoice; return $this->invoice;
}
/* Mark the invoice as sent */
$this->invoice = $this->invoice->service()->markSent()->save(); $this->invoice = $this->invoice->service()->markSent()->save();
if ($this->invoice->balance > 0) { /* Mark the invoice as paid if there is no balance */
$gateway_token = $this->getGateway($this->invoice->balance); //todo what if it is only a partial amount? if ((int)$this->invoice->balance == 0)
} else {
return $this->invoice->service()->markPaid()->save(); return $this->invoice->service()->markPaid()->save();
}
if (! $gateway_token || ! $gateway_token->gateway->driver($this->client)->token_billing) { $this->applyCreditPayment();
return $this->invoice;
}
if ($this->invoice->partial > 0) { /* Determine $amount */
$fee = $gateway_token->gateway->calcGatewayFee($this->invoice->partial); if ($this->invoice->partial > 0)
// $amount = $this->invoice->partial + $fee;
$amount = $this->invoice->partial; $amount = $this->invoice->partial;
} else { elseif($this->invoice->balance >0)
$fee = $gateway_token->gateway->calcGatewayFee($this->invoice->balance);
// $amount = $this->invoice->balance + $fee;
$amount = $this->invoice->balance; $amount = $this->invoice->balance;
} else
return $this->invoice;
$gateway_token = $this->getGateway($amount);
/* Bail out if no payment methods available */
if (! $gateway_token || ! $gateway_token->gateway->driver($this->client)->token_billing)
return $this->invoice;
/* $gateway fee */
$fee = $gateway_token->gateway->calcGatewayFee($this->invoice->partial);
/* Build payment hash */
$payment_hash = PaymentHash::create([ $payment_hash = PaymentHash::create([
'hash' => Str::random(128), 'hash' => Str::random(128),
'data' => ['invoice_id' => $this->invoice->hashed_id, 'amount' => $amount], 'data' => ['invoice_id' => $this->invoice->hashed_id, 'amount' => $amount],
@ -72,11 +79,103 @@ class AutoBillInvoice extends AbstractService
'fee_invoice_id' => $this->invoice->id, 'fee_invoice_id' => $this->invoice->id,
]); ]);
$payment = $gateway_token->gateway->driver($this->client)->tokenBilling($gateway_token, $payment_hash); $payment = $gateway_token->gateway
->driver($this->client)
->tokenBilling($gateway_token, $payment_hash);
return $this->invoice; return $this->invoice;
} }
/**
* Applies credits to a payment prior to push
* to the payment gateway
*
* @return $this
*/
private function applyCreditPayment()
{
$available_credits = $this->client
->credits
->where('is_deleted', false)
->where('balance', '>', 0)
->sortBy('created_at');
$available_credit_balance = $available_credits->sum('balance');
if((int)$available_credit_balance == 0)
return;
$is_partial_amount = false;
if ($this->invoice->partial > 0) {
$is_partial_amount = true;
}
$this->payment = PaymentFactory::create($this->client->company_id, $this->client->user_id);
$this->payment->save();
$available_credits->each(function($credit) use($is_partial_amount){
//todo need to iterate until the partial or balance is completely consumed
//by the credit, any remaining balance is then dealt with by
//the gateway
//each time a credit is applied SAVE the invoice
// if($credit->balance >= $amount){
// //current credit covers the total amount
// }
//return false to exit each loop
});
return $this;
}
private function buildPayment($credit, $is_partial_amount)
{
if($is_partial_amount) {
if($this->invoice->partial >= $credit->balance) {
$amount = $this->invoice->partial - $credit->balance;
$this->invoice->partial -= $amount;
$this->payment->credits()->attach([
$credit->id => ['amount' => $amount]
]);
$this->payment->invoice()->attach([
$this->invoice->id => ['amount' => $amount]
]);
$this->applyPaymentToCredit($credit, $amount);
}
}
}
private function applyPaymentToCredit($credit, $amount)
{
$credit_item = new InvoiceItem;
$credit_item->type_id = '1';
$credit_item->product_key = ctrans('texts.credit');
$credit_item->notes = ctrans('texts.credit_payment', ['invoice_number' => $this->invoice->number]);
$credit_item->quantity = 1;
$credit_item->cost = $amount * -1;
$credit_items = $credit->line_items;
$credit_items[] = $credit_item;
$credit->line_items = $credit_items;
$credit = $credit->calc()->getCredit();
}
/** /**
* Harvests a client gateway token which passes the * Harvests a client gateway token which passes the
* necessary filters for an $amount. * necessary filters for an $amount.

View File

@ -49,8 +49,11 @@ class RecurringService
public function start() public function start()
{ {
//make sure next_send_date is either now or in the future else return. //make sure next_send_date is either now or in the future else return.
if(Carbon::parse($this->recurring_entity->next_send_date)->lt(now())) // if(Carbon::parse($this->recurring_entity->next_send_date)->lt(now()))
return $this; // return $this;
if($this->recurring_entity->remaining_cycles == 0)
return $this;
$this->recurring_entity->status_id = RecurringInvoice::STATUS_ACTIVE; $this->recurring_entity->status_id = RecurringInvoice::STATUS_ACTIVE;

View File

@ -51,6 +51,7 @@ class GatewayTransformer extends EntityTransformer
'fields' => (string) $gateway->fields ?: '', 'fields' => (string) $gateway->fields ?: '',
'updated_at' => (int) $gateway->updated_at, 'updated_at' => (int) $gateway->updated_at,
'created_at' => (int) $gateway->created_at, 'created_at' => (int) $gateway->created_at,
'options' => $gateway->getMethods(),
]; ];
} }
} }

View File

@ -34,20 +34,23 @@ class ProjectTransformer extends EntityTransformer
{ {
return [ return [
'id' => (string) $this->encodePrimaryKey($project->id), 'id' => (string) $this->encodePrimaryKey($project->id),
'name' => $project->name ?: '', 'user_id' => (string) $this->encodePrimaryKey($project->user_id),
'assigned_user_id' => (string) $this->encodePrimaryKey($project->assigned_user_id),
'client_id' => (string) $this->encodePrimaryKey($project->client_id), 'client_id' => (string) $this->encodePrimaryKey($project->client_id),
'name' => $project->name ?: '',
'created_at' => (int) $project->created_at, 'created_at' => (int) $project->created_at,
'updated_at' => (int) $project->updated_at, 'updated_at' => (int) $project->updated_at,
'archived_at' => (int) $project->deleted_at, 'archived_at' => (int) $project->deleted_at,
'is_deleted' => (bool) $project->is_deleted, 'is_deleted' => (bool) $project->is_deleted,
'task_rate' => (float) $project->task_rate, 'task_rate' => (float) $project->task_rate,
'due_date' => $project->due_date ?: '', 'due_date' => $project->due_date ?: '',
'private_notes' => $project->private_notes ?: '', 'private_notes' => (string) $project->private_notes ?: '',
'public_notes' => (string) $project->public_notes ?: '',
'budgeted_hours' => (float) $project->budgeted_hours, 'budgeted_hours' => (float) $project->budgeted_hours,
'custom_value1' => $project->custom_value1 ?: '', 'custom_value1' => (string) $project->custom_value1 ?: '',
'custom_value2' => $project->custom_value2 ?: '', 'custom_value2' => (string) $project->custom_value2 ?: '',
'custom_value3' => $project->custom_value3 ?: '', 'custom_value3' => (string) $project->custom_value3 ?: '',
'custom_value4' => $project->custom_value4 ?: '', 'custom_value4' => (string) $project->custom_value4 ?: '',
]; ];
} }
} }

View File

@ -37,7 +37,7 @@ trait GeneratesCounter
* *
* @return string The next invoice number. * @return string The next invoice number.
*/ */
public function getNextInvoiceNumber(Client $client) :string public function getNextInvoiceNumber(Client $client, ?Invoice $invoice) :string
{ {
//Reset counters if enabled //Reset counters if enabled
$this->resetCounters($client); $this->resetCounters($client);
@ -64,8 +64,12 @@ trait GeneratesCounter
//Return a valid counter //Return a valid counter
$pattern = $client->getSetting('invoice_number_pattern'); $pattern = $client->getSetting('invoice_number_pattern');
$padding = $client->getSetting('counter_padding'); $padding = $client->getSetting('counter_padding');
$prefix = '';
$invoice_number = $this->checkEntityNumber(Invoice::class, $client, $counter, $padding, $pattern); if($invoice && $invoice->recurring_id)
$prefix = $client->getSetting('recurring_number_prefix');
$invoice_number = $this->checkEntityNumber(Invoice::class, $client, $counter, $padding, $pattern, $prefix);
$this->incrementCounter($counter_entity, 'invoice_number_counter'); $this->incrementCounter($counter_entity, 'invoice_number_counter');
@ -140,6 +144,9 @@ trait GeneratesCounter
$quote_number = $this->checkEntityNumber(Quote::class, $client, $counter, $padding, $pattern); $quote_number = $this->checkEntityNumber(Quote::class, $client, $counter, $padding, $pattern);
// if($this->recurring_id)
// $quote_number = $this->prefixCounter($quote_number, $client->getSetting('recurring_number_prefix'));
$this->incrementCounter($counter_entity, $used_counter); $this->incrementCounter($counter_entity, $used_counter);
return $quote_number; return $quote_number;
@ -168,7 +175,7 @@ trait GeneratesCounter
$pattern = ''; $pattern = '';
$padding = $client->getSetting('counter_padding'); $padding = $client->getSetting('counter_padding');
$invoice_number = $this->checkEntityNumber(RecurringInvoice::class, $client, $counter, $padding, $pattern); $invoice_number = $this->checkEntityNumber(RecurringInvoice::class, $client, $counter, $padding, $pattern);
$invoice_number = $this->prefixCounter($invoice_number, $client->getSetting('recurring_number_prefix')); //$invoice_number = $this->prefixCounter($invoice_number, $client->getSetting('recurring_number_prefix'));
//increment the correct invoice_number Counter (company vs client) //increment the correct invoice_number Counter (company vs client)
if ($is_client_counter) { if ($is_client_counter) {
@ -283,7 +290,7 @@ trait GeneratesCounter
* *
* @return string The padded and prefixed entity number * @return string The padded and prefixed entity number
*/ */
private function checkEntityNumber($class, $entity, $counter, $padding, $pattern) private function checkEntityNumber($class, $entity, $counter, $padding, $pattern, $prefix = '')
{ {
$check = false; $check = false;
@ -292,6 +299,8 @@ trait GeneratesCounter
$number = $this->applyNumberPattern($entity, $number, $pattern); $number = $this->applyNumberPattern($entity, $number, $pattern);
$number = $this->prefixCounter($number, $prefix);
if ($class == Invoice::class || $class == RecurringInvoice::class) if ($class == Invoice::class || $class == RecurringInvoice::class)
$check = $class::whereCompanyId($entity->company_id)->whereNumber($number)->withTrashed()->first(); $check = $class::whereCompanyId($entity->company_id)->whereNumber($number)->withTrashed()->first();
elseif ($class == Client::class || $class == Vendor::class) elseif ($class == Client::class || $class == Vendor::class)

38
composer.lock generated
View File

@ -108,16 +108,16 @@
}, },
{ {
"name": "aws/aws-sdk-php", "name": "aws/aws-sdk-php",
"version": "3.158.2", "version": "3.158.4",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/aws/aws-sdk-php.git", "url": "https://github.com/aws/aws-sdk-php.git",
"reference": "b80957465d94c127254e36061dd3d0c3ccc94cc1" "reference": "c0c0df79edc0a646a7ccd6b2e8d1723ff4ba88e2"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/b80957465d94c127254e36061dd3d0c3ccc94cc1", "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/c0c0df79edc0a646a7ccd6b2e8d1723ff4ba88e2",
"reference": "b80957465d94c127254e36061dd3d0c3ccc94cc1", "reference": "c0c0df79edc0a646a7ccd6b2e8d1723ff4ba88e2",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -189,7 +189,7 @@
"s3", "s3",
"sdk" "sdk"
], ],
"time": "2020-10-05T18:13:27+00:00" "time": "2020-10-07T18:12:22+00:00"
}, },
{ {
"name": "brick/math", "name": "brick/math",
@ -1814,16 +1814,16 @@
}, },
{ {
"name": "google/auth", "name": "google/auth",
"version": "v1.14.0", "version": "v1.14.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/googleapis/google-auth-library-php.git", "url": "https://github.com/googleapis/google-auth-library-php.git",
"reference": "95c23ebd89a0a4d1b511aed81426f57388ab7268" "reference": "2df57c61c2fd739a15a81f792b1ccedc3e06d2b6"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/googleapis/google-auth-library-php/zipball/95c23ebd89a0a4d1b511aed81426f57388ab7268", "url": "https://api.github.com/repos/googleapis/google-auth-library-php/zipball/2df57c61c2fd739a15a81f792b1ccedc3e06d2b6",
"reference": "95c23ebd89a0a4d1b511aed81426f57388ab7268", "reference": "2df57c61c2fd739a15a81f792b1ccedc3e06d2b6",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -1862,7 +1862,7 @@
"google", "google",
"oauth2" "oauth2"
], ],
"time": "2020-10-02T22:20:36+00:00" "time": "2020-10-06T18:10:43+00:00"
}, },
{ {
"name": "graham-campbell/result-type", "name": "graham-campbell/result-type",
@ -2497,16 +2497,16 @@
}, },
{ {
"name": "laravel/framework", "name": "laravel/framework",
"version": "v8.8.0", "version": "v8.9.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/laravel/framework.git", "url": "https://github.com/laravel/framework.git",
"reference": "0bdd5c6f12cb7cb6644e484169656245af417735" "reference": "8a6bf870bcfa1597e514a9c7ee6df44db98abb54"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/laravel/framework/zipball/0bdd5c6f12cb7cb6644e484169656245af417735", "url": "https://api.github.com/repos/laravel/framework/zipball/8a6bf870bcfa1597e514a9c7ee6df44db98abb54",
"reference": "0bdd5c6f12cb7cb6644e484169656245af417735", "reference": "8a6bf870bcfa1597e514a9c7ee6df44db98abb54",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -2656,7 +2656,7 @@
"framework", "framework",
"laravel" "laravel"
], ],
"time": "2020-10-02T14:33:08+00:00" "time": "2020-10-06T14:22:36+00:00"
}, },
{ {
"name": "laravel/slack-notification-channel", "name": "laravel/slack-notification-channel",
@ -6504,12 +6504,12 @@
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/console.git", "url": "https://github.com/symfony/console.git",
"reference": "04c3a31fe8ea94b42c9e2d1acc93d19782133b00" "reference": "ae789a8a2ad189ce7e8216942cdb9b77319f5eb8"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/04c3a31fe8ea94b42c9e2d1acc93d19782133b00", "url": "https://api.github.com/repos/symfony/console/zipball/ae789a8a2ad189ce7e8216942cdb9b77319f5eb8",
"reference": "04c3a31fe8ea94b42c9e2d1acc93d19782133b00", "reference": "ae789a8a2ad189ce7e8216942cdb9b77319f5eb8",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -6589,7 +6589,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2020-09-18T14:27:32+00:00" "time": "2020-10-07T15:23:00+00:00"
}, },
{ {
"name": "symfony/css-selector", "name": "symfony/css-selector",

View File

@ -39,6 +39,7 @@ class CompanyFactory extends Factory
'db' => config('database.default'), 'db' => config('database.default'),
'settings' => CompanySettings::defaults(), 'settings' => CompanySettings::defaults(),
'is_large' => false, 'is_large' => false,
'enabled_modules' => config('ninja.enabled_modules'),
'custom_fields' => (object) [ 'custom_fields' => (object) [
//'invoice1' => 'Custom Date|date', //'invoice1' => 'Custom Date|date',
// 'invoice2' => '2|switch', // 'invoice2' => '2|switch',

View File

@ -40,6 +40,7 @@ class UpdateGatewayTableVisibleColumn extends Migration
$t->text('public_notes')->nullable(); $t->text('public_notes')->nullable();
$t->dropColumn('description'); $t->dropColumn('description');
$t->decimal('budgeted_hours', 12,2)->change(); $t->decimal('budgeted_hours', 12,2)->change();
$t->boolean('is_deleted')->default(0);
}); });
} }

View File

@ -214,7 +214,7 @@ class RandomDataSeeder extends Seeder
$invoice->ledger()->updateInvoiceBalance($invoice->balance); $invoice->ledger()->updateInvoiceBalance($invoice->balance);
if (rand(0, 1)) { if (rand(0, 1)) {
$payment = App\Models\Payment::create([ $payment = Payment::create([
'date' => now(), 'date' => now(),
'user_id' => $user->id, 'user_id' => $user->id,
'company_id' => $company->id, 'company_id' => $company->id,

View File

@ -3281,4 +3281,7 @@ return [
'paused' => 'Paused', 'paused' => 'Paused',
'saved_at' => 'Saved at :time', 'saved_at' => 'Saved at :time',
'credit_payment' => 'Credit applied to Invoice :invoice_number',
]; ];

View File

@ -13,7 +13,11 @@ if(!isset($design)) $design = 'light';
<style> <style>
:root { :root {
@if($settings)
--primary-color: {{ $settings->primary_color }}; --primary-color: {{ $settings->primary_color }};
@else
--primary-color: #4caf50;
@endif
} }
.border-primary { .border-primary {

View File

@ -69,6 +69,9 @@ Route::group(['middleware' => ['api_db', 'token_auth', 'locale'], 'prefix' => 'a
Route::post('expenses/bulk', 'ExpenseController@bulk')->name('expenses.bulk'); Route::post('expenses/bulk', 'ExpenseController@bulk')->name('expenses.bulk');
Route::resource('projects', 'ProjectController'); // name = (projects. index / create / show / update / destroy / edit
Route::post('projects/bulk', 'ProjectController@bulk')->name('projects.bulk');
Route::resource('vendors', 'VendorController'); // name = (vendors. index / create / show / update / destroy / edit Route::resource('vendors', 'VendorController'); // name = (vendors. index / create / show / update / destroy / edit
Route::post('vendors/bulk', 'VendorController@bulk')->name('vendors.bulk'); Route::post('vendors/bulk', 'VendorController@bulk')->name('vendors.bulk');
@ -157,13 +160,6 @@ Route::group(['middleware' => ['api_db', 'token_auth', 'locale'], 'prefix' => 'a
Route::post('tasks/bulk', 'TaskController@bulk')->name('tasks.bulk'); Route::post('tasks/bulk', 'TaskController@bulk')->name('tasks.bulk');
Route::resource('credits', 'CreditController'); // name = (credits. index / create / show / update / destroy / edit
Route::post('credits/bulk', 'CreditController@bulk')->name('credits.bulk');
Route::get('settings', 'SettingsController@index')->name('user.settings'); Route::get('settings', 'SettingsController@index')->name('user.settings');
*/ */
Route::get('scheduler', 'SchedulerController@index'); Route::get('scheduler', 'SchedulerController@index');

View File

@ -54,7 +54,7 @@ class InvoiceEmailTest extends TestCase
{ {
$this->invoice->date = now(); $this->invoice->date = now();
$this->invoice->due_date = now()->addDays(7); $this->invoice->due_date = now()->addDays(7);
$this->invoice->number = $this->getNextInvoiceNumber($this->client); $this->invoice->number = $this->getNextInvoiceNumber($this->client, $this->invoice);
$this->invoice->client_id = $this->client->id; $this->invoice->client_id = $this->client->id;
@ -90,7 +90,7 @@ class InvoiceEmailTest extends TestCase
$this->invoice->date = now(); $this->invoice->date = now();
$this->invoice->due_date = now()->addDays(7); $this->invoice->due_date = now()->addDays(7);
$this->invoice->number = $this->getNextInvoiceNumber($this->client); $this->invoice->number = $this->getNextInvoiceNumber($this->client, $this->invoice);
$this->invoice->client_id = $this->client->id; $this->invoice->client_id = $this->client->id;
$this->invoice->setRelation('client', $this->client); $this->invoice->setRelation('client', $this->client);
@ -115,7 +115,7 @@ class InvoiceEmailTest extends TestCase
$this->invoice->date = now(); $this->invoice->date = now();
$this->invoice->due_date = now()->addDays(7); $this->invoice->due_date = now()->addDays(7);
$this->invoice->number = $this->getNextInvoiceNumber($this->client); $this->invoice->number = $this->getNextInvoiceNumber($this->client, $this->invoice);
$this->invoice->client_id = $this->client->id; $this->invoice->client_id = $this->client->id;
@ -145,7 +145,7 @@ class InvoiceEmailTest extends TestCase
$this->invoice->date = now(); $this->invoice->date = now();
$this->invoice->due_date = now()->addDays(7); $this->invoice->due_date = now()->addDays(7);
$this->invoice->number = $this->getNextInvoiceNumber($this->client); $this->invoice->number = $this->getNextInvoiceNumber($this->client, $this->invoice);
$this->invoice->client_id = $this->client->id; $this->invoice->client_id = $this->client->id;
$this->invoice->setRelation('client', $this->client); $this->invoice->setRelation('client', $this->client);

View File

@ -86,7 +86,10 @@ class ProductTest extends TestCase
) )
->assertStatus(200); ->assertStatus(200);
$product = Product::all()->first();
$arr = $response->json();
$product = Product::find($this->decodePrimaryKey($arr['data']['id']));
$product_update = [ $product_update = [
'notes' => 'CHANGE', 'notes' => 'CHANGE',

View File

@ -0,0 +1,155 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace Tests\Feature;
use App\DataMapper\DefaultSettings;
use App\Models\Account;
use App\Models\Project;
use App\Models\ProjectContact;
use App\Models\Company;
use App\Models\User;
use App\Utils\Traits\MakesHash;
use Faker\Factory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Session;
use Tests\MockAccountData;
use Tests\TestCase;
/**
* @test
* @covers App\Http\Controllers\ProjectController
*/
class ProjectApiTest extends TestCase
{
use MakesHash;
use DatabaseTransactions;
use MockAccountData;
public function setUp() :void
{
parent::setUp();
$this->makeTestData();
Session::start();
$this->faker = \Faker\Factory::create();
Model::reguard();
}
public function testProjectGet()
{
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->get('/api/v1/projects/'.$this->encodePrimaryKey($this->project->id));
$response->assertStatus(200);
}
public function testProjectPost()
{
$data = [
'name' => $this->faker->firstName,
'client_id' => $this->client->hashed_id,
];
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->post('/api/v1/projects', $data);
$response->assertStatus(200);
}
public function testProjectPut()
{
$data = [
'name' => $this->faker->firstName,
'public_notes' => 'Coolio',
];
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->put('/api/v1/projects/'.$this->encodePrimaryKey($this->project->id), $data);
$response->assertStatus(200);
}
public function testProjectNotArchived()
{
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->get('/api/v1/projects/'.$this->encodePrimaryKey($this->project->id));
$arr = $response->json();
$this->assertEquals(0, $arr['data']['archived_at']);
}
public function testProjectArchived()
{
$data = [
'ids' => [$this->encodePrimaryKey($this->project->id)],
];
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->post('/api/v1/projects/bulk?action=archive', $data);
$arr = $response->json();
$this->assertNotNull($arr['data'][0]['archived_at']);
}
public function testProjectRestored()
{
$data = [
'ids' => [$this->encodePrimaryKey($this->project->id)],
];
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->post('/api/v1/projects/bulk?action=restore', $data);
$arr = $response->json();
$this->assertEquals(0, $arr['data'][0]['archived_at']);
}
public function testProjectDeleted()
{
$data = [
'ids' => [$this->encodePrimaryKey($this->project->id)],
];
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->post('/api/v1/projects/bulk?action=delete', $data);
$arr = $response->json();
$this->assertTrue($arr['data'][0]['is_deleted']);
}
}

View File

@ -35,6 +35,7 @@ use App\Models\Expense;
use App\Models\GroupSetting; use App\Models\GroupSetting;
use App\Models\Invoice; use App\Models\Invoice;
use App\Models\InvoiceInvitation; use App\Models\InvoiceInvitation;
use App\Models\Project;
use App\Models\Quote; use App\Models\Quote;
use App\Models\QuoteInvitation; use App\Models\QuoteInvitation;
use App\Models\RecurringInvoice; use App\Models\RecurringInvoice;
@ -200,7 +201,10 @@ trait MockAccountData
'send_email' => true, 'send_email' => true,
]); ]);
$this->project = Project::factory()->create([
'user_id' => $this->user->id,
'company_id' => $this->company->id,
]);
$this->expense = Expense::factory()->create([ $this->expense = Expense::factory()->create([
'user_id' => $this->user->id, 'user_id' => $this->user->id,
@ -356,7 +360,7 @@ trait MockAccountData
$recurring_invoice->next_send_date = Carbon::now(); $recurring_invoice->next_send_date = Carbon::now();
$recurring_invoice->save(); $recurring_invoice->save();
$recurring_invoice->number = $this->getNextInvoiceNumber($this->invoice->client); $recurring_invoice->number = $this->getNextInvoiceNumber($this->invoice->client, $this->invoice);
$recurring_invoice->save(); $recurring_invoice->save();
$recurring_invoice = InvoiceToRecurringInvoiceFactory::create($this->invoice); $recurring_invoice = InvoiceToRecurringInvoiceFactory::create($this->invoice);
@ -366,7 +370,7 @@ trait MockAccountData
$recurring_invoice->next_send_date = Carbon::now(); $recurring_invoice->next_send_date = Carbon::now();
$recurring_invoice->save(); $recurring_invoice->save();
$recurring_invoice->number = $this->getNextInvoiceNumber($this->invoice->client); $recurring_invoice->number = $this->getNextInvoiceNumber($this->invoice->client, $this->invoice);
$recurring_invoice->save(); $recurring_invoice->save();
$recurring_invoice = InvoiceToRecurringInvoiceFactory::create($this->invoice); $recurring_invoice = InvoiceToRecurringInvoiceFactory::create($this->invoice);
@ -376,7 +380,7 @@ trait MockAccountData
$recurring_invoice->next_send_date = Carbon::now(); $recurring_invoice->next_send_date = Carbon::now();
$recurring_invoice->save(); $recurring_invoice->save();
$recurring_invoice->number = $this->getNextInvoiceNumber($this->invoice->client); $recurring_invoice->number = $this->getNextInvoiceNumber($this->invoice->client, $this->invoice);
$recurring_invoice->save(); $recurring_invoice->save();
$recurring_invoice = InvoiceToRecurringInvoiceFactory::create($this->invoice); $recurring_invoice = InvoiceToRecurringInvoiceFactory::create($this->invoice);
@ -386,7 +390,7 @@ trait MockAccountData
$recurring_invoice->next_send_date = Carbon::now(); $recurring_invoice->next_send_date = Carbon::now();
$recurring_invoice->save(); $recurring_invoice->save();
$recurring_invoice->number = $this->getNextInvoiceNumber($this->invoice->client); $recurring_invoice->number = $this->getNextInvoiceNumber($this->invoice->client, $this->invoice);
$recurring_invoice->save(); $recurring_invoice->save();
$recurring_invoice = InvoiceToRecurringInvoiceFactory::create($this->invoice); $recurring_invoice = InvoiceToRecurringInvoiceFactory::create($this->invoice);
@ -396,7 +400,7 @@ trait MockAccountData
$recurring_invoice->next_send_date = Carbon::now(); $recurring_invoice->next_send_date = Carbon::now();
$recurring_invoice->save(); $recurring_invoice->save();
$recurring_invoice->number = $this->getNextInvoiceNumber($this->invoice->client); $recurring_invoice->number = $this->getNextInvoiceNumber($this->invoice->client, $this->invoice);
$recurring_invoice->save(); $recurring_invoice->save();
$recurring_invoice = InvoiceToRecurringInvoiceFactory::create($this->invoice); $recurring_invoice = InvoiceToRecurringInvoiceFactory::create($this->invoice);
@ -406,7 +410,7 @@ trait MockAccountData
$recurring_invoice->next_send_date = Carbon::now()->addDays(10); $recurring_invoice->next_send_date = Carbon::now()->addDays(10);
$recurring_invoice->save(); $recurring_invoice->save();
$recurring_invoice->number = $this->getNextInvoiceNumber($this->invoice->client); $recurring_invoice->number = $this->getNextInvoiceNumber($this->invoice->client, $this->invoice);
$recurring_invoice->save(); $recurring_invoice->save();
$gs = new GroupSetting; $gs = new GroupSetting;

View File

@ -79,11 +79,11 @@ class GeneratesCounterTest extends TestCase
public function testInvoiceNumberValue() public function testInvoiceNumberValue()
{ {
$invoice_number = $this->getNextInvoiceNumber($this->client); $invoice_number = $this->getNextInvoiceNumber($this->client, $this->invoice);
$this->assertEquals($invoice_number, '0008'); $this->assertEquals($invoice_number, '0008');
$invoice_number = $this->getNextInvoiceNumber($this->client); $invoice_number = $this->getNextInvoiceNumber($this->client, $this->invoice);
$this->assertEquals($invoice_number, '0009'); $this->assertEquals($invoice_number, '0009');
} }
@ -112,8 +112,8 @@ class GeneratesCounterTest extends TestCase
$this->client->save(); $this->client->save();
$this->client->fresh(); $this->client->fresh();
$invoice_number = $this->getNextInvoiceNumber($this->client); $invoice_number = $this->getNextInvoiceNumber($this->client, $this->invoice);
$invoice_number2 = $this->getNextInvoiceNumber($this->client); $invoice_number2 = $this->getNextInvoiceNumber($this->client, $this->invoice);
$this->assertEquals($invoice_number, date('Y').'-0001'); $this->assertEquals($invoice_number, date('Y').'-0001');
$this->assertEquals($invoice_number2, date('Y').'-0002'); $this->assertEquals($invoice_number2, date('Y').'-0002');
@ -203,7 +203,7 @@ class GeneratesCounterTest extends TestCase
$cliz = ClientFactory::create($this->company->id, $this->user->id); $cliz = ClientFactory::create($this->company->id, $this->user->id);
$cliz->settings = ClientSettings::defaults(); $cliz->settings = ClientSettings::defaults();
$cliz->save(); $cliz->save();
$invoice_number = $this->getNextInvoiceNumber($cliz); $invoice_number = $this->getNextInvoiceNumber($cliz, $this->invoice);
$this->assertEquals($cliz->getSetting('counter_padding'), 5); $this->assertEquals($cliz->getSetting('counter_padding'), 5);
$this->assertEquals($invoice_number, '00007'); $this->assertEquals($invoice_number, '00007');
@ -218,7 +218,7 @@ class GeneratesCounterTest extends TestCase
$cliz->settings = ClientSettings::defaults(); $cliz->settings = ClientSettings::defaults();
$cliz->save(); $cliz->save();
$invoice_number = $this->getNextInvoiceNumber($cliz); $invoice_number = $this->getNextInvoiceNumber($cliz, $this->invoice);
$this->assertEquals($cliz->getSetting('counter_padding'), 10); $this->assertEquals($cliz->getSetting('counter_padding'), 10);
$this->assertEquals(strlen($invoice_number), 10); $this->assertEquals(strlen($invoice_number), 10);
@ -235,11 +235,11 @@ class GeneratesCounterTest extends TestCase
$cliz->settings = ClientSettings::defaults(); $cliz->settings = ClientSettings::defaults();
$cliz->save(); $cliz->save();
$invoice_number = $this->getNextInvoiceNumber($cliz); $invoice_number = $this->getNextInvoiceNumber($cliz, $this->invoice);
$this->assertEquals($invoice_number, '0008'); $this->assertEquals($invoice_number, '0008');
$invoice_number = $this->getNextInvoiceNumber($cliz); $invoice_number = $this->getNextInvoiceNumber($cliz, $this->invoice);
$this->assertEquals($invoice_number, '0009'); $this->assertEquals($invoice_number, '0009');
} }