Merge pull request #7977 from turbo124/v5-develop

Bank Transaction Rules
This commit is contained in:
David Bomba 2022-11-25 17:01:56 +11:00 committed by GitHub
commit cec5bfafe0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
99 changed files with 5610 additions and 4021 deletions

View File

@ -22,6 +22,8 @@ use App\Jobs\Company\CreateCompanyTaskStatuses;
use App\Jobs\Ninja\CompanySizeCheck;
use App\Jobs\Util\VersionCheck;
use App\Models\Account;
use App\Models\BankIntegration;
use App\Models\BankTransaction;
use App\Models\Client;
use App\Models\ClientContact;
use App\Models\Company;
@ -223,6 +225,18 @@ class DemoMode extends Command
'company_id' => $company->id,
]);
$bi = BankIntegration::factory()->create([
'account_id' => $account->id,
'company_id' => $company->id,
'user_id' => $user->id,
]);
BankTransaction::factory()->count(50)->create([
'bank_integration_id' => $bi->id,
'user_id' => $user->id,
'company_id' => $company->id,
]);
$this->info('Creating '.$this->count.' clients');
for ($x = 0; $x < $this->count; $x++) {

View File

@ -42,7 +42,7 @@ class S3Cleanup extends Command
*/
public function handle()
{
if (! Ninja::isHosted()) {
if (!Ninja::isHosted()) {
return;
}

View File

@ -182,7 +182,7 @@ class Handler extends ExceptionHandler
} elseif ($exception instanceof FatalThrowableError && $request->expectsJson()) {
return response()->json(['message'=>'Fatal error'], 500);
} elseif ($exception instanceof AuthorizationException) {
return response()->json(['message'=>'You are not authorized to view or perform this action'], 401);
return response()->json(['message'=> $exception->getMessage()], 401);
} elseif ($exception instanceof TokenMismatchException) {
return redirect()
->back()

View File

@ -0,0 +1,30 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Factory;
use App\Models\BankTransactionRule;
use Illuminate\Support\Str;
class BankTransactionRuleFactory
{
public static function create(int $company_id, int $user_id) :BankTransactionRule
{
$bank_transaction_rule = new BankTransactionRule;
$bank_transaction_rule->user_id = $user_id;
$bank_transaction_rule->company_id = $company_id;
$bank_transaction_rule->name = '';
$bank_transaction_rule->rules = [];
return $bank_transaction_rule;
}
}

View File

@ -0,0 +1,133 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Filters;
use App\Models\BankTransactionRule;
use App\Models\User;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Gate;
/**
* BankTransactionRuleilters.
*/
class BankTransactionRuleFilters extends QueryFilters
{
/**
* Filter by name.
*
* @param string $name
* @return Builder
*/
public function name(string $name = ''): Builder
{
if(strlen($name) >=1)
return $this->builder->where('name', 'like', '%'.$name.'%');
return $this->builder;
}
/**
* Filter based on search text.
*
* @param string query filter
* @return 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('bank_transaction_rules.name', 'like', '%'.$filter.'%');
});
}
/**
* Filters the list based on the status
* archived, active, deleted.
*
* @param string filter
* @return Builder
*/
public function status(string $filter = '') : Builder
{
if (strlen($filter) == 0) {
return $this->builder;
}
$table = 'bank_transaction_rules';
$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 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
* @param User $user
* @return Builder
* @deprecated
*/
public function baseQuery(int $company_id, User $user) : Builder
{
}
/**
* Filters the query by the users company ID.
*
* @return Illuminate\Database\Query\Builder
*/
public function entityFilter()
{
//return $this->builder->whereCompanyId(auth()->user()->company()->id);
return $this->builder->company();
}
}

View File

@ -12,6 +12,7 @@
namespace App\Filters;
use App\Models\User;
use App\Utils\Traits\MakesHash;
use Illuminate\Database\Eloquent\Builder;
/**
@ -19,6 +20,8 @@ use Illuminate\Database\Eloquent\Builder;
*/
class TaskFilters extends QueryFilters
{
use MakesHash;
/**
* Filter based on search text.
*
@ -111,6 +114,16 @@ class TaskFilters extends QueryFilters
});
}
public function project_tasks($project)
{
if (strlen($project) == 0) {
return $this->builder;
}
return $this->builder->where('project_id', $this->decodePrimaryKey($project));
}
/**
* Sorts the list based on $sort.
*

View File

@ -76,7 +76,7 @@ class EpcQrGenerator
$this->formatMoney($this->amount),
$this->sepa['purpose'],
substr($this->invoice->number,0,34),
substr($this->invoice->public_notes,0,139),
'',
''
)), "\n");

View File

@ -110,6 +110,8 @@ class ActivityController extends BaseController
'vendor' => $activity->vendor ? $activity->vendor : '',
'vendor_contact' => $activity->vendor_contact ? $activity->vendor_contact : '',
'purchase_order' => $activity->purchase_order ? $activity->purchase_order : '',
'subscription' => $activity->subscription ? $activity->subscription : '',
'vendor_contact' => $activity->vendor_contact ? $activity->vendor_contact : '',
];
return array_merge($arr, $activity->toArray());

View File

@ -481,19 +481,32 @@ class BankTransactionController extends BaseController
{
$action = request()->input('action');
if(!in_array($action, ['archive', 'restore', 'delete']))
if(!in_array($action, ['archive', 'restore', 'delete', 'convert_matched']))
return response()->json(['message' => 'Unsupported action.'], 400);
$ids = request()->input('ids');
$bank_transactions = BankTransaction::withTrashed()->whereIn('id', $this->transformKeys($ids))->company()->get();
$bank_transactions->each(function ($bank_transaction, $key) use ($action) {
if (auth()->user()->can('edit', $bank_transaction)) {
$this->bank_transaction_repo->{$action}($bank_transaction);
if($action == 'convert_matched') //catch this action
{
if(auth()->user()->isAdmin())
{
$this->bank_transaction_repo->convert_matched($bank_transactions);
}
});
else
return;
}
else {
$bank_transactions->each(function ($bank_transaction, $key) use ($action) {
if (auth()->user()->can('edit', $bank_transaction)) {
$this->bank_transaction_repo->{$action}($bank_transaction);
}
});
}
/* Need to understand which permission are required for the given bulk action ie. view / edit */
return $this->listResponse(BankTransaction::withTrashed()->whereIn('id', $this->transformKeys($ids))->company());

View File

@ -0,0 +1,505 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\Controllers;
use App\Factory\BankTransactionFactory;
use App\Factory\BankTransactionRuleFactory;
use App\Filters\BankTransactionFilters;
use App\Filters\BankTransactionRuleFilters;
use App\Helpers\Bank\Yodlee\Yodlee;
use App\Http\Requests\BankTransactionRule\CreateBankTransactionRuleRequest;
use App\Http\Requests\BankTransactionRule\DestroyBankTransactionRuleRequest;
use App\Http\Requests\BankTransactionRule\EditBankTransactionRuleRequest;
use App\Http\Requests\BankTransactionRule\ShowBankTransactionRuleRequest;
use App\Http\Requests\BankTransactionRule\StoreBankTransactionRuleRequest;
use App\Http\Requests\BankTransactionRule\UpdateBankTransactionRuleRequest;
use App\Http\Requests\BankTransaction\AdminBankTransactionRuleRequest;
use App\Http\Requests\Import\PreImportRequest;
use App\Jobs\Bank\MatchBankTransactionRules;
use App\Models\BankTransaction;
use App\Models\BankTransactionRule;
use App\Repositories\BankTransactionRepository;
use App\Repositories\BankTransactionRuleRepository;
use App\Services\Bank\BankMatchingService;
use App\Transformers\BankTransactionRuleTransformer;
use App\Transformers\BankTransactionTransformer;
use App\Utils\Traits\MakesHash;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;
class BankTransactionRuleController extends BaseController
{
use MakesHash;
protected $entity_type = BankTransactionRule::class;
protected $entity_transformer = BankTransactionRuleTransformer::class;
protected $bank_transaction_repo;
public function __construct(BankTransactionRuleRepository $bank_transaction_repo)
{
parent::__construct();
$this->bank_transaction_repo = $bank_transaction_repo;
}
/**
* @OA\Get(
* path="/api/v1/bank_transaction_rules",
* operationId="getBankTransactionRules",
* tags={"bank_transaction_rules"},
* summary="Gets a list of bank_transaction_rules",
* description="Lists all bank transaction rules",
* @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\Parameter(
* name="rows",
* in="query",
* description="The number of bank integrations to return",
* example="50",
* required=false,
* @OA\Schema(
* type="number",
* format="integer",
* ),
* ),
* @OA\Response(
* response=200,
* description="A list of bank integrations",
* @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/BankTransactionRule"),
* ),
* @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"),
* ),
* )
* @param BankTransactionFilters $filter
* @return Response|mixed
*/
public function index(BankTransactionRuleFilters $filters)
{
$bank_transaction_rules = BankTransactionRule::filter($filters);
return $this->listResponse($bank_transaction_rules);
}
/**
* Display the specified resource.
*
* @param ShowBankTransactionRuleRequest $request
* @param BankTransactionRule $bank_transaction_rule
* @return Response
*
*
* @OA\Get(
* path="/api/v1/bank_transaction_rules/{id}",
* operationId="showBankTransactionRule",
* tags={"bank_transaction_rules"},
* summary="Shows a bank_transaction",
* description="Displays a bank_transaction 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 Bank Transaction RuleHashed ID",
* example="D2J234DFA",
* required=true,
* @OA\Schema(
* type="string",
* format="string",
* ),
* ),
* @OA\Response(
* response=200,
* description="Returns the bank_transaction rule 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/BankTransactionRule"),
* ),
* @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(ShowBankTransactionRuleRequest $request, BankTransactionRule $bank_transaction_rule)
{
return $this->itemResponse($bank_transaction_rule);
}
/**
* Show the form for editing the specified resource.
*
* @param EditBankTransactionRuleRequest $request
* @param BankTransactionRule $bank_transaction_rule
* @return Response
*
*
* @OA\Get(
* path="/api/v1/bank_transaction_rules/{id}/edit",
* operationId="editBankTransactionRule",
* tags={"bank_transaction_rules"},
* summary="Shows a bank_transaction for editing",
* description="Displays a bank_transaction 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 Bank Transaction Rule Hashed ID",
* example="D2J234DFA",
* required=true,
* @OA\Schema(
* type="string",
* format="string",
* ),
* ),
* @OA\Response(
* response=200,
* description="Returns the bank_transaction rule 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/BankTransactionRule"),
* ),
* @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(EditBankTransactionRuleRequest $request, BankTransactionRule $bank_transaction_rule)
{
return $this->itemResponse($bank_transaction_rule);
}
/**
* Update the specified resource in storage.
*
* @param UpdateBankTransactionRuleRequest $request
* @param BankTransactionRule $bank_transaction_rule
* @return Response
*
*
*
* @OA\Put(
* path="/api/v1/bank_transaction_rules/{id}",
* operationId="updateBankTransactionRule",
* tags={"bank_transaction_rules"},
* summary="Updates a bank_transaction Rule",
* description="Handles the updating of a bank_transaction rule 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 Bank Transaction Rule Hashed ID",
* example="D2J234DFA",
* required=true,
* @OA\Schema(
* type="string",
* format="string",
* ),
* ),
* @OA\Response(
* response=200,
* description="Returns the bank_transaction rule 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/BankTransactionRule"),
* ),
* @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(UpdateBankTransactionRuleRequest $request, BankTransactionRule $bank_transaction_rule)
{
//stubs for updating the model
$bank_transaction = $this->bank_transaction_repo->save($request->all(), $bank_transaction_rule);
return $this->itemResponse($bank_transaction_rule->fresh());
}
/**
* Show the form for creating a new resource.
*
* @param CreateBankTransactionRuleRequest $request
* @return Response
*
*
*
* @OA\Get(
* path="/api/v1/bank_transaction_rules/create",
* operationId="getBankTransactionRulesCreate",
* tags={"bank_transaction_rules"},
* summary="Gets a new blank bank_transaction rule 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 bank_transaction rule 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/BankTransactionRule"),
* ),
* @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(CreateBankTransactionRuleRequest $request)
{
$bank_transaction_rule = BankTransactionRuleFactory::create(auth()->user()->company()->id, auth()->user()->id, auth()->user()->account_id);
return $this->itemResponse($bank_transaction_rule);
}
/**
* Store a newly created resource in storage.
*
* @param StoreBankTransactionRuleRequest $request
* @return Response
*
*
*
* @OA\Post(
* path="/api/v1/bank_transaction_rules",
* operationId="storeBankTransaction",
* tags={"bank_transaction_rules"},
* summary="Adds a bank_transaction rule",
* description="Adds an bank_transaction 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 bank_transaction rule 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/BankTransactionRule"),
* ),
* @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(StoreBankTransactionRuleRequest $request)
{
//stub to store the model
$bank_transaction_rule = $this->bank_transaction_repo->save($request->all(), BankTransactionRuleFactory::create(auth()->user()->company()->id, auth()->user()->id, auth()->user()->account_id));
return $this->itemResponse($bank_transaction_rule);
}
/**
* Remove the specified resource from storage.
*
* @param DestroyBankTransactionRuleRequest $request
* @param BankTransactionRule $bank_transaction_rule
* @return Response
*
*
* @throws \Exception
* @OA\Delete(
* path="/api/v1/bank_transaction_rules/{id}",
* operationId="deleteBankTransactionRule",
* tags={"bank_transaction_rules"},
* summary="Deletes a bank_transaction rule",
* description="Handles the deletion of a bank_transaction rule 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 Bank Transaction Rule 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(DestroyBankTransactionRuleRequest $request, BankTransactionRule $bank_transaction_rule)
{
$this->bank_transaction_repo->delete($bank_transaction_rule);
return $this->itemResponse($bank_transaction_rule->fresh());
}
/**
* Perform bulk actions on the list view.
*
* @return Collection
*
* @OA\Post(
* path="/api/v1/bank_transation_rules/bulk",
* operationId="bulkBankTransactionRules",
* tags={"bank_transaction_rules"},
* summary="Performs bulk actions on an array of bank_transation rules",
* 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="Action paramters",
* 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 Bulk Action 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\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');
if(!in_array($action, ['archive', 'restore', 'delete']))
return response()->json(['message' => 'Unsupported action.'], 400);
$ids = request()->input('ids');
$bank_transaction_rules = BankTransactionRule::withTrashed()->whereIn('id', $this->transformKeys($ids))->company()->get();
$bank_transaction_rules->each(function ($bank_transaction_rule, $key) use ($action) {
if (auth()->user()->can('edit', $bank_transaction_rule)) {
$this->bank_transaction_repo->{$action}($bank_transaction_rule);
}
});
/* Need to understand which permission are required for the given bulk action ie. view / edit */
return $this->listResponse(BankTransactionRule::withTrashed()->whereIn('id', $this->transformKeys($ids))->company());
}
}

View File

@ -108,6 +108,7 @@ class BaseController extends Controller
'company.system_logs',
'company.bank_integrations',
'company.bank_transactions',
'company.bank_transaction_rules',
];
private $mini_load = [
@ -126,6 +127,7 @@ class BaseController extends Controller
'company.expense_categories',
'company.subscriptions',
'company.bank_integrations',
'company.bank_transaction_rules',
];
public function __construct()
@ -456,6 +458,13 @@ class BaseController extends Controller
$query->where('bank_transactions.user_id', $user->id);
}
},
'company.bank_transaction_rules'=> function ($query) use ($updated_at, $user) {
$query->where('updated_at', '>=', $updated_at);
if (! $user->isAdmin()) {
$query->where('bank_transaction_rules.user_id', $user->id);
}
},
]
);
@ -530,6 +539,12 @@ class BaseController extends Controller
$query->where('bank_integrations.user_id', $user->id);
}
},
'company.bank_transaction_rules'=> function ($query) use ($updated_at, $user) {
if (! $user->isAdmin()) {
$query->where('bank_transaction_rules.user_id', $user->id);
}
},
]
);

View File

@ -41,6 +41,25 @@ class SubscriptionPurchaseController extends Controller
]);
}
public function upgrade(Subscription $subscription, Request $request)
{
/* Make sure the contact is logged into the correct company for this subscription */
if (auth()->guard('contact')->user() && auth()->guard('contact')->user()->company_id != $subscription->company_id) {
auth()->guard('contact')->logout();
$request->session()->invalidate();
}
if ($request->has('locale')) {
$this->setLocale($request->query('locale'));
}
return view('billing-portal.purchasev2', [
'subscription' => $subscription,
'hash' => Str::uuid()->toString(),
'request_data' => $request->all(),
]);
}
/**
* Set locale for incoming request.
*
@ -56,4 +75,7 @@ class SubscriptionPurchaseController extends Controller
App::setLocale($record->locale);
}
}
}

View File

@ -28,6 +28,7 @@ use App\Http\Requests\Invoice\StoreInvoiceRequest;
use App\Http\Requests\Invoice\UpdateInvoiceRequest;
use App\Http\Requests\Invoice\UpdateReminderRequest;
use App\Http\Requests\Invoice\UploadInvoiceRequest;
use App\Jobs\Cron\AutoBill;
use App\Jobs\Entity\EmailEntity;
use App\Jobs\Invoice\BulkInvoiceJob;
use App\Jobs\Invoice\StoreInvoice;
@ -696,11 +697,14 @@ class InvoiceController extends BaseController
{
/*If we are using bulk actions, we don't want to return anything */
switch ($action) {
case 'auto_bill':
AutoBill::dispatch($invoice->id, $invoice->company->db);
return $this->itemResponse($invoice);
case 'clone_to_invoice':
$invoice = CloneInvoiceFactory::create($invoice, auth()->user()->id);
return $this->itemResponse($invoice);
break;
case 'clone_to_quote':
$quote = CloneInvoiceToQuoteFactory::create($invoice, auth()->user()->id);
@ -767,7 +771,7 @@ class InvoiceController extends BaseController
}
break;
case 'cancel':
$invoice = $invoice->service()->handleCancellation()->deletePdf()->touchPdf()->save();
$invoice = $invoice->service()->handleCancellation()->touchPdf()->save();
if (! $bulk) {
$this->itemResponse($invoice);
@ -777,7 +781,7 @@ class InvoiceController extends BaseController
case 'email':
//check query parameter for email_type and set the template else use calculateTemplate
if (request()->has('email_type') && property_exists($invoice->company->settings, request()->input('email_type'))) {
if (request()->has('email_type') && in_array(request()->input('email_type'), ['reminder1', 'reminder2', 'reminder3', 'reminder_endless', 'custom1', 'custom2', 'custom3'])) {
$this->reminder_template = $invoice->client->getSetting(request()->input('email_type'));
} else {
$this->reminder_template = $invoice->calculateTemplate('invoice');

View File

@ -181,7 +181,10 @@ class MigrationController extends BaseController
$company->tasks()->forceDelete();
$company->vendors()->forceDelete();
$company->expenses()->forceDelete();
$company->purchase_orders()->forceDelete();
$company->bank_transaction_rules()->forceDelete();
$company->bank_transactions()->forceDelete();
$company->bank_integrations()->forceDelete();
$company->all_activities()->forceDelete();
$settings = $company->settings;

View File

@ -0,0 +1,10 @@
<?php
/**
* @OA\Schema(
* schema="BTRules",
* type="object",
* @OA\Property(property="data_key", type="string", example="description,amount", description="The key to search"),
* @OA\Property(property="operator", type="string", example=">", description="The operator flag of the search"),
* @OA\Property(property="value", type="string" ,example="bob", description="The value to search for"),
* )
*/

View File

@ -6,7 +6,7 @@
* @OA\Property(property="id", type="string", example="AS3df3A", description="The bank integration hashed id"),
* @OA\Property(property="company_id", type="string", example="AS3df3A", description="The company hashed id"),
* @OA\Property(property="user_id", type="string", example="AS3df3A", description="The user hashed id"),
* @OA\Property(property="transaction_id", type="integer", example=343434, description="The id of the transaction"),
* @OA\Property(property="transaction_id", type="integer", example=343434, description="The id of the transaction rule"),
* @OA\Property(property="amount", type="number", example=10.00, description="The transaction amount"),
* @OA\Property(property="currency_id", type="string", example="1", description="The currency ID of the currency"),
* @OA\Property(property="account_type", type="string", example="creditCard", description="The account type"),

View File

@ -0,0 +1,25 @@
<?php
/**
* @OA\Schema(
* schema="BankTransactionRule",
* type="object",
* @OA\Property(property="id", type="string", example="AS3df3A", description="The bank transaction rules hashed id"),
* @OA\Property(property="company_id", type="string", example="AS3df3A", description="The company hashed id"),
* @OA\Property(property="user_id", type="string", example="AS3df3A", description="The user hashed id"),
* @OA\Property(property="name", type="string", example="Rule 1", description="The name of the transaction"),
* @OA\Property(
* property="rules",
* type="array",
* description="A mapped collection of the sub rules for the BankTransactionRule",
* @OA\Items(
* ref="#/components/schemas/BTRules",
* ),
* ),
* @OA\Property(property="auto_convert", type="boolean", example=true, description="Flags whether the rule converts the transaction automatically"),
* @OA\Property(property="matches_on_all", type="boolean", example=true, description="Flags whether all subrules are required for the match"),
* @OA\Property(property="applies_to", type="string", example="CREDIT", description="Flags whether the rule applies to a CREDIT or DEBIT"),
* @OA\Property(property="client_id", type="string", example="AS3df3A", description="The client hashed id"),
* @OA\Property(property="vendor_id", type="string", example="AS3df3A", description="The vendor hashed id"),
* @OA\Property(property="category_id", type="string", example="AS3df3A", description="The category hashed id"),
* )
*/

View File

@ -0,0 +1,528 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\Livewire;
use App\Factory\ClientFactory;
use App\Jobs\Mail\NinjaMailerJob;
use App\Jobs\Mail\NinjaMailerObject;
use App\Libraries\MultiDB;
use App\Mail\ContactPasswordlessLogin;
use App\Models\Client;
use App\Models\ClientContact;
use App\Models\Invoice;
use App\Models\Subscription;
use App\Repositories\ClientContactRepository;
use App\Repositories\ClientRepository;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use App\DataMapper\ClientSettings;
use Livewire\Component;
class BillingPortalPurchasev2 extends Component
{
/**
* Random hash generated by backend to handle the tracking of state.
*
* @var string
*/
public $hash;
/**
* Top level text on the left side of billing page.
*
* @var string
*/
public $heading_text;
/**
* E-mail address model for user input.
*
* @var string
*/
public $email;
/**
* Password model for user input.
*
* @var string
*/
public $password;
/**
* Instance of subscription.
*
* @var Subscription
*/
public $subscription;
/**
* Instance of client contact.
*
* @var null|ClientContact
*/
public $contact;
/**
* Rules for validating the form.
*
* @var \string[][]
*/
protected $rules = [
'email' => ['required', 'email'],
];
/**
* Id for CompanyGateway record.
*
* @var string|integer
*/
public $company_gateway_id;
/**
* Id for GatewayType.
*
* @var string|integer
*/
public $payment_method_id;
private $user_coupon;
/**
* List of steps that frontend form follows.
*
* @var array
*/
public $steps = [
'passed_email' => false,
'existing_user' => false,
'fetched_payment_methods' => false,
'fetched_client' => false,
'show_start_trial' => false,
'passwordless_login_sent' => false,
'started_payment' => false,
'discount_applied' => false,
'show_loading_bar' => false,
'not_eligible' => null,
'not_eligible_message' => null,
'payment_required' => true,
];
/**
* List of payment methods fetched from client.
*
* @var array
*/
public $methods = [];
/**
* Instance of \App\Models\Invoice
*
* @var Invoice
*/
public $invoice;
/**
* Coupon model for user input
*
* @var string
*/
public $coupon;
/**
* Quantity for seats
*
* @var int
*/
public $quantity;
/**
* First-hit request data (queries, locales...).
*
* @var array
*/
public $request_data;
/**
* @var string
*/
public $price;
/**
* Disabled state of passwordless login button.
*
* @var bool
*/
public $passwordless_login_btn = false;
/**
* Instance of company.
*
* @var Company
*/
public $company;
/**
* Campaign reference.
*
* @var string|null
*/
public $campaign;
public function mount()
{
MultiDB::setDb($this->company->db);
$this->quantity = 1;
$this->price = $this->subscription->price;
if (request()->query('coupon')) {
$this->coupon = request()->query('coupon');
$this->handleCoupon();
}
elseif(strlen($this->subscription->promo_code) == 0 && $this->subscription->promo_discount > 0){
$this->price = $this->subscription->promo_price;
}
}
/**
* Handle user authentication
*
* @return $this|bool|void
*/
public function authenticate()
{
$this->validate();
$contact = ClientContact::where('email', $this->email)
->where('company_id', $this->subscription->company_id)
->first();
if ($contact && $this->steps['existing_user'] === false) {
return $this->steps['existing_user'] = true;
}
if ($contact && $this->steps['existing_user']) {
$attempt = Auth::guard('contact')->attempt(['email' => $this->email, 'password' => $this->password, 'company_id' => $this->subscription->company_id]);
return $attempt
? $this->getPaymentMethods($contact)
: session()->flash('message', 'These credentials do not match our records.');
}
$this->steps['existing_user'] = false;
$contact = $this->createBlankClient();
if ($contact && $contact instanceof ClientContact) {
$this->getPaymentMethods($contact);
}
}
/**
* Create a blank client. Used for new customers purchasing.
*
* @return mixed
* @throws \Laracasts\Presenter\Exceptions\PresenterException
*/
protected function createBlankClient()
{
$company = $this->subscription->company;
$user = $this->subscription->user;
$user->setCompany($company);
$client_repo = new ClientRepository(new ClientContactRepository());
$data = [
'name' => '',
'contacts' => [
['email' => $this->email],
],
'client_hash' => Str::random(40),
'settings' => ClientSettings::defaults(),
];
foreach ($this->request_data as $field => $value) {
if (in_array($field, Client::$subscriptions_fillable)) {
$data[$field] = $value;
}
if (in_array($field, ClientContact::$subscription_fillable)) {
$data['contacts'][0][$field] = $value;
}
}
// nlog($this->subscription->group_settings->settings);
// nlog($this->subscription->group_settings->settings->currency_id);
if(array_key_exists('currency_id', $this->request_data)) {
$currency = Cache::get('currencies')->filter(function ($item){
return $item->id == $this->request_data['currency_id'];
})->first();
if($currency)
$data['settings']->currency_id = $currency->id;
}
elseif($this->subscription->group_settings && property_exists($this->subscription->group_settings->settings, 'currency_id')) {
$currency = Cache::get('currencies')->filter(function ($item){
return $item->id == $this->subscription->group_settings->settings->currency_id;
})->first();
if($currency)
$data['settings']->currency_id = $currency->id;
}
if (array_key_exists('locale', $this->request_data)) {
$request = $this->request_data;
$record = Cache::get('languages')->filter(function ($item) use ($request) {
return $item->locale == $request['locale'];
})->first();
if ($record) {
$data['settings']['language_id'] = (string)$record->id;
}
}
$client = $client_repo->save($data, ClientFactory::create($company->id, $user->id));
return $client->fresh()->contacts->first();
}
/**
* Fetching payment methods from the client.
*
* @param ClientContact $contact
* @return $this
*/
protected function getPaymentMethods(ClientContact $contact): self
{
Auth::guard('contact')->loginUsingId($contact->id, true);
$this->contact = $contact;
if ($this->subscription->trial_enabled) {
$this->heading_text = ctrans('texts.plan_trial');
$this->steps['show_start_trial'] = true;
return $this;
}
if ((int)$this->price == 0)
$this->steps['payment_required'] = false;
else
$this->steps['fetched_payment_methods'] = true;
$this->methods = $contact->client->service()->getPaymentMethods($this->price);
$this->heading_text = ctrans('texts.payment_methods');
return $this;
}
/**
* Middle method between selecting payment method &
* submitting the from to the backend.
*
* @param $company_gateway_id
* @param $gateway_type_id
*/
public function handleMethodSelectingEvent($company_gateway_id, $gateway_type_id)
{
$this->company_gateway_id = $company_gateway_id;
$this->payment_method_id = $gateway_type_id;
$this->handleBeforePaymentEvents();
}
/**
* Method to handle events before payments.
*
* @return void
*/
public function handleBeforePaymentEvents()
{
$this->steps['started_payment'] = true;
$this->steps['show_loading_bar'] = true;
$data = [
'client_id' => $this->contact->client->id,
'date' => now()->format('Y-m-d'),
'invitations' => [[
'key' => '',
'client_contact_id' => $this->contact->hashed_id,
]],
'user_input_promo_code' => $this->coupon,
'coupon' => empty($this->subscription->promo_code) ? '' : $this->coupon,
'quantity' => $this->quantity,
];
$is_eligible = $this->subscription->service()->isEligible($this->contact);
if (is_array($is_eligible) && $is_eligible['message'] != 'Success') {
$this->steps['not_eligible'] = true;
$this->steps['not_eligible_message'] = $is_eligible['message'];
$this->steps['show_loading_bar'] = false;
return;
}
$this->invoice = $this->subscription
->service()
->createInvoice($data, $this->quantity)
->service()
->markSent()
->fillDefaults()
->adjustInventory()
->save();
Cache::put($this->hash, [
'subscription_id' => $this->subscription->id,
'email' => $this->email ?? $this->contact->email,
'client_id' => $this->contact->client->id,
'invoice_id' => $this->invoice->id,
'context' => 'purchase',
'campaign' => $this->campaign,
], now()->addMinutes(60));
$this->emit('beforePaymentEventsCompleted');
}
/**
* Proxy method for starting the trial.
*
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
*/
public function handleTrial()
{
return $this->subscription->service()->startTrial([
'email' => $this->email ?? $this->contact->email,
'quantity' => $this->quantity,
'contact_id' => $this->contact->id,
'client_id' => $this->contact->client->id,
]);
}
public function handlePaymentNotRequired()
{
$is_eligible = $this->subscription->service()->isEligible($this->contact);
if ($is_eligible['status_code'] != 200) {
$this->steps['not_eligible'] = true;
$this->steps['not_eligible_message'] = $is_eligible['message'];
$this->steps['show_loading_bar'] = false;
return;
}
return $this->subscription->service()->handleNoPaymentRequired([
'email' => $this->email ?? $this->contact->email,
'quantity' => $this->quantity,
'contact_id' => $this->contact->id,
'client_id' => $this->contact->client->id,
'coupon' => $this->coupon,
]);
}
/**
* Update quantity property.
*
* @param string $option
* @return int
*/
public function updateQuantity(string $option): int
{
$this->handleCoupon();
if ($this->quantity == 1 && $option == 'decrement') {
$this->price = $this->price * 1;
return $this->quantity;
}
if ($this->quantity > $this->subscription->max_seats_limit && $option == 'increment') {
$this->price = $this->price * $this->subscription->max_seats_limit;
return $this->quantity;
}
if ($option == 'increment') {
$this->quantity++;
$this->price = $this->price * $this->quantity;
return $this->quantity;
}
$this->quantity--;
$this->price = $this->price * $this->quantity;
return $this->quantity;
}
public function handleCoupon()
{
if($this->steps['discount_applied']){
$this->price = $this->subscription->promo_price;
return;
}
if ($this->coupon == $this->subscription->promo_code) {
$this->price = $this->subscription->promo_price;
$this->quantity = 1;
$this->steps['discount_applied'] = true;
}
else
$this->price = $this->subscription->price;
}
public function passwordlessLogin()
{
$this->passwordless_login_btn = true;
$contact = ClientContact::query()
->where('email', $this->email)
->where('company_id', $this->subscription->company_id)
->first();
$mailer = new NinjaMailerObject();
$mailer->mailable = new ContactPasswordlessLogin($this->email, $this->subscription->company, (string)route('client.subscription.purchase', $this->subscription->hashed_id) . '?coupon=' . $this->coupon);
$mailer->company = $this->subscription->company;
$mailer->settings = $this->subscription->company->settings;
$mailer->to_user = $contact;
NinjaMailerJob::dispatch($mailer);
$this->steps['passwordless_login_sent'] = true;
$this->passwordless_login_btn = false;
}
public function render()
{
if (array_key_exists('email', $this->request_data)) {
$this->email = $this->request_data['email'];
}
if ($this->contact instanceof ClientContact) {
$this->getPaymentMethods($this->contact);
}
return render('components.livewire.billing-portal-purchasev2');
}
}

View File

@ -41,6 +41,7 @@ class ContactKeyLogin
$request->session()->invalidate();
}
//magic links survive for 1 hour
if ($request->segment(2) && $request->segment(2) == 'magic_link' && $request->segment(3)) {
$payload = Cache::get($request->segment(3));
@ -66,7 +67,11 @@ class ContactKeyLogin
}
} elseif ($request->segment(3) && config('ninja.db.multi_db_enabled')) {
if (MultiDB::findAndSetDbByContactKey($request->segment(3))) {
if ($client_contact = ClientContact::where('contact_key', $request->segment(3))->first()) {
if ($client_contact = ClientContact::with('company')->where('contact_key', $request->segment(3))->first()) {
if($client_contact->company->settings->enable_client_portal_password)
return redirect()->route('client.login', ['company_key' => $client_contact->company->company_key]);
if (empty($client_contact->email)) {
$client_contact->email = Str::random(6).'@example.com';
}
@ -82,7 +87,11 @@ class ContactKeyLogin
}
}
} elseif ($request->segment(2) && $request->segment(2) == 'key_login' && $request->segment(3)) {
if ($client_contact = ClientContact::where('contact_key', $request->segment(3))->first()) {
if ($client_contact = ClientContact::with('company')->where('contact_key', $request->segment(3))->first()) {
if($client_contact->company->settings->enable_client_portal_password)
return redirect()->route('client.login', ['company_key' => $client_contact->company->company_key]);
if (empty($client_contact->email)) {
$client_contact->email = Str::random(6).'@example.com';
$client_contact->save();
@ -125,7 +134,11 @@ class ContactKeyLogin
return redirect($this->setRedirectPath());
}
} elseif ($request->segment(3)) {
if ($client_contact = ClientContact::where('contact_key', $request->segment(3))->first()) {
if ($client_contact = ClientContact::with('company')->where('contact_key', $request->segment(3))->first()) {
if($client_contact->company->settings->enable_client_portal_password)
return redirect()->route('client.login', ['company_key' => $client_contact->company->company_key]);
if (empty($client_contact->email)) {
$client_contact->email = Str::random(6).'@example.com';
$client_contact->save();

View File

@ -0,0 +1,28 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\Requests\BankTransactionRule;
use App\Http\Requests\Request;
use App\Models\BankTransactionRule;
class CreateBankTransactionRuleRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize() : bool
{
return auth()->user()->can('create', BankTransactionRule::class);
}
}

View File

@ -0,0 +1,27 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\Requests\BankTransactionRule;
use App\Http\Requests\Request;
class DestroyBankTransactionRuleRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize() : bool
{
return auth()->user()->can('edit', $this->bank_transaction_rule);
}
}

View File

@ -0,0 +1,27 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\Requests\BankTransactionRule;
use App\Http\Requests\Request;
class EditBankTransactionRuleRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize() : bool
{
return auth()->user()->can('edit', $this->bank_transaction_rule);
}
}

View File

@ -0,0 +1,27 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\Requests\BankTransactionRule;
use App\Http\Requests\Request;
class ShowBankTransactionRuleRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize() : bool
{
return auth()->user()->can('view', $this->bank_transaction_rule);
}
}

View File

@ -0,0 +1,70 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\Requests\BankTransactionRule;
use App\Http\Requests\Request;
use App\Models\BankTransactionRule;
use App\Utils\Traits\MakesHash;
class StoreBankTransactionRuleRequest 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', BankTransactionRule::class);
}
public function rules()
{
/* Ensure we have a client name, and that all emails are unique*/
$rules = [
'name' => 'bail|required|string',
'rules' => 'bail|array',
'rules.*.operator' => 'bail|required|nullable',
'rules.*.search_key' => 'bail|required|nullable',
'rules.*.value' => 'bail|required|nullable',
'auto_convert' => 'bail|sometimes|bool',
'matches_on_all' => 'bail|sometimes|bool',
'applies_to' => 'bail|sometimes|string',
];
if(isset($this->category_id))
$rules['category_id'] = 'bail|sometimes|exists:expense_categories,id,company_id,'.auth()->user()->company()->id.',is_deleted,0';
if(isset($this->vendor_id))
$rules['vendor_id'] = 'bail|sometimes|exists:vendors,id,company_id,'.auth()->user()->company()->id.',is_deleted,0';
if(isset($this->client_id))
$rules['client_id'] = 'bail|sometimes|exists:clients,id,company_id,'.auth()->user()->company()->id.',is_deleted,0';
return $rules;
}
public function prepareForValidation()
{
$input = $this->all();
$input = $this->decodePrimaryKeys($input);
$this->replace($input);
}
}

View File

@ -0,0 +1,67 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\Requests\BankTransactionRule;
use App\Http\Requests\Request;
use App\Utils\Traits\MakesHash;
class UpdateBankTransactionRuleRequest extends Request
{
use MakesHash;
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize() : bool
{
return auth()->user()->can('edit', $this->bank_transaction_rule);
}
public function rules()
{
/* Ensure we have a client name, and that all emails are unique*/
$rules = [
'name' => 'bail|required|string',
'rules' => 'bail|array',
'rules.*.operator' => 'bail|required|nullable',
'rules.*.search_key' => 'bail|required|nullable',
'rules.*.value' => 'bail|required|nullable',
'auto_convert' => 'bail|sometimes|bool',
'matches_on_all' => 'bail|sometimes|bool',
'applies_to' => 'bail|sometimes|string',
];
if(isset($this->category_id))
$rules['category_id'] = 'bail|sometimes|exists:expense_categories,id,company_id,'.auth()->user()->company()->id.',is_deleted,0';
if(isset($this->vendor_id))
$rules['vendor_id'] = 'bail|sometimes|exists:vendors,id,company_id,'.auth()->user()->company()->id.',is_deleted,0';
if(isset($this->client_id))
$rules['client_id'] = 'bail|sometimes|exists:clients,id,company_id,'.auth()->user()->company()->id.',is_deleted,0';
return $rules;
}
public function prepareForValidation()
{
$input = $this->all();
$input = $this->decodePrimaryKeys($input);
$this->replace($input);
}
}

View File

@ -61,22 +61,15 @@ class StoreCompanyRequest extends Request
{
$input = $this->all();
if(!isset($input['name']))
$input['name'] = 'Untitled Company';
if (array_key_exists('google_analytics_url', $input)) {
$input['google_analytics_key'] = $input['google_analytics_url'];
}
// $company_settings = CompanySettings::defaults();
//@todo this code doesn't make sense as we never return $company_settings anywhere
//@deprecated???
// if (array_key_exists('settings', $input) && ! empty($input['settings'])) {
// foreach ($input['settings'] as $key => $value) {
// $company_settings->{$key} = $value;
// }
// }
if (array_key_exists('portal_domain', $input)) {
$input['portal_domain'] = strtolower($input['portal_domain']);
$input['portal_domain'] = rtrim(strtolower($input['portal_domain']), "/");
}
$this->replace($input);

View File

@ -74,9 +74,9 @@ class UpdateCompanyRequest extends Request
$input = $this->all();
if (Ninja::isHosted() && array_key_exists('portal_domain', $input) && strlen($input['portal_domain']) > 1) {
if (array_key_exists('portal_domain', $input) && strlen($input['portal_domain']) > 1) {
$input['portal_domain'] = $this->addScheme($input['portal_domain']);
$input['portal_domain'] = strtolower($input['portal_domain']);
$input['portal_domain'] = rtrim(strtolower($input['portal_domain']), "/");
}
if (array_key_exists('settings', $input)) {
@ -108,7 +108,7 @@ class UpdateCompanyRequest extends Request
}
}
$settings['email_style_custom'] = str_replace("{{", "", $settings['email_style_custom']);
$settings['email_style_custom'] = str_replace(['{{','}}'], ['',''], $settings['email_style_custom']);
if (! $account->isFreeHostedClient()) {
return $settings;
@ -127,9 +127,11 @@ class UpdateCompanyRequest extends Request
private function addScheme($url, $scheme = 'https://')
{
$url = str_replace('http://', '', $url);
$url = parse_url($url, PHP_URL_SCHEME) === null ? $scheme.$url : $url;
if(Ninja::isHosted())
{
$url = str_replace('http://', '', $url);
$url = parse_url($url, PHP_URL_SCHEME) === null ? $scheme.$url : $url;
}
return rtrim($url, '/');
}

View File

@ -125,6 +125,10 @@ class Request extends FormRequest
$input['company_gateway_id'] = $this->decodePrimaryKey($input['company_gateway_id']);
}
if (array_key_exists('category_id', $input) && is_string($input['category_id'])) {
$input['category_id'] = $this->decodePrimaryKey($input['category_id']);
}
if (isset($input['client_contacts'])) {
foreach ($input['client_contacts'] as $key => $contact) {
if (! array_key_exists('send_email', $contact) || ! array_key_exists('id', $contact)) {

View File

@ -35,26 +35,34 @@ class StoreSubscriptionRequest extends Request
public function rules()
{
$rules = [
'product_ids' => ['sometimes'],
'recurring_product_ids' => ['sometimes'],
'assigned_user_id' => ['sometimes'],
'is_recurring' => ['sometimes'],
'frequency_id' => ['required_with:recurring_product_ids'],
'auto_bill' => ['sometimes'],
'promo_code' => ['sometimes'],
'promo_discount' => ['sometimes'],
'is_amount_discount' => ['sometimes'],
'allow_cancellation' => ['sometimes'],
'per_set_enabled' => ['sometimes'],
'min_seats_limit' => ['sometimes'],
'max_seats_limit' => ['sometimes'],
'trial_enabled' => ['sometimes'],
'trial_duration' => ['sometimes'],
'allow_query_overrides' => ['sometimes'],
'allow_plan_changes' => ['sometimes'],
'refund_period' => ['sometimes'],
'webhook_configuration' => ['array'],
'name' => ['required', Rule::unique('subscriptions')->where('company_id', auth()->user()->company()->id)],
'group_id' => ['bail','sometimes', 'nullable', Rule::exists('group_settings','id')->where('company_id', auth()->user()->company()->id)],
'assigned_user_id' => ['bail','sometimes', 'nullable', Rule::exists('users','id')->where('account_id', auth()->user()->account_id)],
'product_ids' => 'bail|sometimes|nullable|string',
'recurring_product_ids' => 'bail|sometimes|nullable|string',
'is_recurring' => 'bail|sometimes|bool',
'frequency_id' => 'bail|required_with:recurring_product_ids',
'auto_bill' => 'bail|sometimes|nullable|string',
'promo_code' => 'bail|sometimes|nullable|string',
'promo_discount' => 'bail|sometimes|numeric',
'is_amount_discount' => 'bail|sometimes|bool',
'allow_cancellation' => 'bail|sometimes|bool',
'per_set_enabled' => 'bail|sometimes|bool',
'min_seats_limit' => 'bail|sometimes|numeric',
'max_seats_limit' => 'bail|sometimes|numeric',
'trial_enabled' => 'bail|sometimes|bool',
'trial_duration' => 'bail|sometimes|numeric',
'allow_query_overrides' => 'bail|sometimes|bool',
'allow_plan_changes' => 'bail|sometimes|bool',
'refund_period' => 'bail|sometimes|numeric',
'webhook_configuration' => 'bail|array',
'webhook_configuration.post_purchase_url' => 'bail|sometimes|nullable|string',
'webhook_configuration.post_purchase_rest_method' => 'bail|sometimes|nullable|string',
'webhook_configuration.post_purchase_headers' => 'bail|sometimes|array',
'registration_required' => 'bail|sometimes|bool',
'optional_recurring_product_ids' => 'bail|sometimes|nullable|string',
'optional_product_ids' => 'bail|sometimes|nullable|string',
'use_inventory_management' => 'bail|sometimes|bool'
];
return $this->globalRules($rules);

View File

@ -37,26 +37,34 @@ class UpdateSubscriptionRequest extends Request
public function rules()
{
$rules = [
'product_ids' => ['sometimes'],
'recurring_product_ids' => ['sometimes'],
'assigned_user_id' => ['sometimes'],
'is_recurring' => ['sometimes'],
'frequency_id' => ['required_with:recurring_product_ids'],
'auto_bill' => ['sometimes'],
'promo_code' => ['sometimes'],
'promo_discount' => ['sometimes'],
'is_amount_discount' => ['sometimes'],
'allow_cancellation' => ['sometimes'],
'per_set_enabled' => ['sometimes'],
'min_seats_limit' => ['sometimes'],
'max_seats_limit' => ['sometimes'],
'trial_enabled' => ['sometimes'],
'trial_duration' => ['sometimes'],
'allow_query_overrides' => ['sometimes'],
'allow_plan_changes' => ['sometimes'],
'refund_period' => ['sometimes'],
'webhook_configuration' => ['array'],
'name' => ['sometimes', Rule::unique('subscriptions')->where('company_id', auth()->user()->company()->id)->ignore($this->subscription->id)],
'name' => ['bail','sometimes', Rule::unique('subscriptions')->where('company_id', auth()->user()->company()->id)->ignore($this->subscription->id)],
'group_id' => ['bail','sometimes', 'nullable', Rule::exists('group_settings','id')->where('company_id', auth()->user()->company()->id)],
'assigned_user_id' => ['bail','sometimes', 'nullable', Rule::exists('users','id')->where('account_id', auth()->user()->account_id)],
'product_ids' => 'bail|sometimes|nullable|string',
'recurring_product_ids' => 'bail|sometimes|nullable|string',
'is_recurring' => 'bail|sometimes|bool',
'frequency_id' => 'bail|required_with:recurring_product_ids',
'auto_bill' => 'bail|sometimes|nullable|string',
'promo_code' => 'bail|sometimes|nullable|string',
'promo_discount' => 'bail|sometimes|numeric',
'is_amount_discount' => 'bail|sometimes|bool',
'allow_cancellation' => 'bail|sometimes|bool',
'per_set_enabled' => 'bail|sometimes|bool',
'min_seats_limit' => 'bail|sometimes|numeric',
'max_seats_limit' => 'bail|sometimes|numeric',
'trial_enabled' => 'bail|sometimes|bool',
'trial_duration' => 'bail|sometimes|numeric',
'allow_query_overrides' => 'bail|sometimes|bool',
'allow_plan_changes' => 'bail|sometimes|bool',
'refund_period' => 'bail|sometimes|numeric',
'webhook_configuration' => 'bail|array',
'webhook_configuration.post_purchase_url' => 'bail|sometimes|nullable|string',
'webhook_configuration.post_purchase_rest_method' => 'bail|sometimes|nullable|string',
'webhook_configuration.post_purchase_headers' => 'bail|sometimes|array',
'registration_required' => 'bail|sometimes|bool',
'optional_recurring_product_ids' => 'bail|sometimes|nullable|string',
'optional_product_ids' => 'bail|sometimes|nullable|string',
'use_inventory_management' => 'bail|sometimes|bool',
];
return $this->globalRules($rules);

View File

@ -15,6 +15,7 @@ use App\Http\Requests\Request;
use App\Models\Project;
use App\Utils\Traits\ChecksEntityStatus;
use App\Utils\Traits\MakesHash;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Validation\Rule;
class UpdateTaskRequest extends Request
@ -29,6 +30,10 @@ class UpdateTaskRequest extends Request
*/
public function authorize() : bool
{
//prevent locked tasks from updating
if($this->task->invoice_lock && $this->task->invoice_id)
return false;
return auth()->user()->can('edit', $this->task);
}
@ -87,4 +92,11 @@ class UpdateTaskRequest extends Request
$this->replace($input);
}
protected function failedAuthorization()
{
throw new AuthorizationException(ctrans('texts.task_update_authorization_error'));
}
}

View File

@ -24,7 +24,7 @@ class GenerateSmsRequest extends Request
*/
public function authorize() : bool
{
return auth()->user()->isAdmin();
return auth()->user();
}

View File

@ -26,6 +26,7 @@ class ValidCompanyQuantity implements Rule
*/
public function passes($attribute, $value)
{
if (Ninja::isSelfHost()) {
return auth()->user()->company()->account->companies->count() < 10;
}

View File

@ -46,6 +46,7 @@ use App\Repositories\PaymentRepository;
use App\Repositories\ProductRepository;
use App\Repositories\QuoteRepository;
use App\Repositories\VendorRepository;
use App\Services\Bank\BankMatchingService;
use App\Utils\Traits\MakesHash;
use Illuminate\Support\Facades\Validator;
use Symfony\Component\HttpFoundation\ParameterBag;
@ -107,6 +108,8 @@ class Csv extends BaseImport implements ImportInterface
$bank_transaction_count = $this->ingest($data, $entity_type);
$this->entity_count['bank_transactions'] = $bank_transaction_count;
BankMatchingService::dispatchSync($this->company->id, $this->company->db);
}
public function client()

View File

@ -196,13 +196,19 @@ class MatchBankTransactions implements ShouldQueue
$expense->payment_date = Carbon::parse($this->bt->date);
$expense->transaction_reference = $this->bt->description;
$expense->transaction_id = $this->bt->id;
$expense->vendor_id = array_key_exists('vendor_id', $input) ? $input['vendor_id'] : null;
if(array_key_exists('vendor_id', $input))
$expense->vendor_id = $input['vendor_id'];
$expense->invoice_documents = $this->company->invoice_expense_documents;
$expense->should_be_invoiced = $this->company->mark_expenses_invoiceable;
$expense->save();
$this->bt->expense_id = $expense->id;
$this->bt->vendor_id = array_key_exists('vendor_id', $input) ? $input['vendor_id'] : null;
if(array_key_exists('vendor_id', $input))
$this->bt->vendor_id = $input['vendor_id'];
$this->bt->status_id = BankTransaction::STATUS_CONVERTED;
$this->bt->save();
@ -254,6 +260,9 @@ class MatchBankTransactions implements ShouldQueue
}, 1);
if(!$this->invoice)
return;
/* Create Payment */
$payment = PaymentFactory::create($this->invoice->company_id, $this->invoice->user_id);

View File

@ -50,11 +50,20 @@ class ReminderJob implements ShouldQueue
$this->processReminders();
} else {
//multiDB environment, need to
/*
foreach (MultiDB::$dbs as $db) {
MultiDB::setDB($db);
nlog("set db {$db}");
$this->processReminders();
}
*/
//24-11-2022 fix for potential memory leak during a long running process, the second reminder may run twice
foreach (config('ninja.dbs') as $db) {
MultiDB::setDB($db);
nlog("set db {$db}");
$this->processReminders();
}
}
}
@ -62,57 +71,65 @@ class ReminderJob implements ShouldQueue
{
nlog('Sending invoice reminders '.now()->format('Y-m-d h:i:s'));
set_time_limit(0);
Invoice::query()
->where('is_deleted', 0)
->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL])
->whereNull('deleted_at')
->where('balance', '>', 0)
->where('next_send_date', '<=', now()->toDateTimeString())
->whereHas('client', function ($query) {
$query->where('is_deleted', 0)
->where('deleted_at', null);
})
->whereHas('company', function ($query) {
$query->where('is_disabled', 0);
})
->with('invitations')->cursor()->each(function ($invoice) {
if ($invoice->isPayable()) {
$reminder_template = $invoice->calculateTemplate('invoice');
nlog("reminder template = {$reminder_template}");
$invoice->service()->touchReminder($reminder_template)->save();
$invoice = $this->calcLateFee($invoice, $reminder_template);
->where('is_deleted', 0)
->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL])
->whereNull('deleted_at')
->where('balance', '>', 0)
->where('next_send_date', '<=', now()->toDateTimeString())
->whereHas('client', function ($query) {
$query->where('is_deleted', 0)
->where('deleted_at', null);
})
->whereHas('company', function ($query) {
$query->where('is_disabled', 0);
})
->with('invitations')->cursor()->each(function ($invoice) {
if ($invoice->isPayable()) {
$invoice->service()->touchPdf();
//Attempts to prevent duplicates from sending
if($invoice->reminder_last_sent && Carbon::parse($invoice->reminder_last_sent)->startOfDay()->eq(now()->startOfDay())){
nlog("caught a duplicate reminder for invoice {$invoice->number}");
return;
}
//20-04-2022 fixes for endless reminders - generic template naming was wrong
$enabled_reminder = 'enable_'.$reminder_template;
$reminder_template = $invoice->calculateTemplate('invoice');
nlog("reminder template = {$reminder_template}");
$invoice = $this->calcLateFee($invoice, $reminder_template);
$invoice->service()->touchReminder($reminder_template)->save();
$invoice->service()->touchPdf(true);
if ($reminder_template == 'endless_reminder') {
$enabled_reminder = 'enable_reminder_endless';
}
//check if this reminder needs to be emailed
//15-01-2022 - insert addition if block if send_reminders is definitely set
if (in_array($reminder_template, ['reminder1', 'reminder2', 'reminder3', 'reminder_endless', 'endless_reminder']) &&
$invoice->client->getSetting($enabled_reminder) &&
$invoice->client->getSetting('send_reminders') &&
(Ninja::isSelfHost() || $invoice->company->account->isPaidHostedClient())) {
$invoice->invitations->each(function ($invitation) use ($invoice, $reminder_template) {
EmailEntity::dispatchSync($invitation, $invitation->company, $reminder_template);
nlog("Firing reminder email for invoice {$invoice->number} - {$reminder_template}");
});
if ($invoice->invitations->count() > 0) {
event(new InvoiceWasEmailed($invoice->invitations->first(), $invoice->company, Ninja::eventVars(), $reminder_template));
}
}
$invoice->service()->setReminder()->save();
} else {
$invoice->next_send_date = null;
$invoice->save();
//20-04-2022 fixes for endless reminders - generic template naming was wrong
$enabled_reminder = 'enable_'.$reminder_template;
if ($reminder_template == 'endless_reminder') {
$enabled_reminder = 'enable_reminder_endless';
}
});
//check if this reminder needs to be emailed
//15-01-2022 - insert addition if block if send_reminders is definitely set
if (in_array($reminder_template, ['reminder1', 'reminder2', 'reminder3', 'reminder_endless', 'endless_reminder']) &&
$invoice->client->getSetting($enabled_reminder) &&
$invoice->client->getSetting('send_reminders') &&
(Ninja::isSelfHost() || $invoice->company->account->isPaidHostedClient())) {
$invoice->invitations->each(function ($invitation) use ($invoice, $reminder_template) {
EmailEntity::dispatch($invitation, $invitation->company, $reminder_template);
nlog("Firing reminder email for invoice {$invoice->number} - {$reminder_template}");
});
if ($invoice->invitations->count() > 0) {
event(new InvoiceWasEmailed($invoice->invitations->first(), $invoice->company, Ninja::eventVars(), $reminder_template));
}
}
$invoice->service()->setReminder()->save();
} else {
$invoice->next_send_date = null;
$invoice->save();
}
});
}
/**
@ -196,22 +213,17 @@ class ReminderJob implements ShouldQueue
$invoice->line_items = $invoice_items;
/**Refresh Invoice values*/
$invoice->calc()->getInvoice()->save();
$invoice->fresh();
$invoice->service()->deletePdf();
/* Refresh the client here to ensure the balance is fresh */
$client = $invoice->client;
$client = $client->fresh();
$invoice = $invoice->calc()->getInvoice();
// $invoice->service()->deletePdf(); 24-11-2022 no need to delete here because we regenerate later anyway
nlog('adjusting client balance and invoice balance by #'.$invoice->number.' '.($invoice->balance - $temp_invoice_balance));
$client->service()->updateBalance($invoice->balance - $temp_invoice_balance)->save();
$invoice->client->service()->updateBalance($invoice->balance - $temp_invoice_balance);
$invoice->ledger()->updateInvoiceBalance($invoice->balance - $temp_invoice_balance, "Late Fee Adjustment for invoice {$invoice->number}");
$transaction = [
'invoice' => $invoice->transaction_event(),
'payment' => [],
'client' => $client->transaction_event(),
'client' => $invoice->client->transaction_event(),
'credit' => [],
'metadata' => ['setLateFee'],
];

View File

@ -292,6 +292,14 @@ class Activity extends StaticModel
return $this->belongsTo(Quote::class)->withTrashed();
}
/**
* @return mixed
*/
public function subscription()
{
return $this->belongsTo(Subscription::class)->withTrashed();
}
/**
* @return mixed
*/

View File

@ -11,6 +11,7 @@
namespace App\Models;
use App\Models\BankTransactionRule;
use App\Models\Filterable;
use App\Models\Invoice;
use App\Services\Bank\BankService;

View File

@ -0,0 +1,171 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Models;
use App\Models\Filterable;
use App\Utils\Traits\MakesHash;
use Illuminate\Database\Eloquent\SoftDeletes;
class BankTransactionRule extends BaseModel
{
use SoftDeletes;
use MakesHash;
use Filterable;
protected $fillable = [
'name',
'rules',
'auto_convert',
'matches_on_all',
'applies_to',
'client_id',
'vendor_id',
'category_id',
];
protected $casts = [
'rules' => 'array',
'updated_at' => 'timestamp',
'created_at' => 'timestamp',
'deleted_at' => 'timestamp',
];
protected $dates = [
];
protected array $search_keys = [
'description' => 'string',
'amount' => 'number',
];
/* Amount */
protected array $number_operators = [
'=',
'>',
'>=',
'<',
'<='
];
/* Description, Client, Vendor, Reference Number */
protected array $string_operators = [
'is',
'contains',
'starts_with',
'is_empty',
];
private array $search_results = [];
// rule object looks like this:
//[
// {
// 'search_key': 'client_id',
// 'operator' : 'is',
// 'value' : 'Sparky'
// }
//]
// public function processRule(BankTransaction $bank_transaction)
// {
// foreach($this->rules as $key => $rule)
// {
// $this->search($rule, $key, $bank_transaction);
// }
// }
// private function search($rule, $key, $bank_transaction)
// {
// if($rule->search_key == 'amount')
// {
// //number search
// }
// else {
// //string search
// }
// }
// private function findAmount($amount, $bank_transaction)
// {
// if($bank_transaction->base_type == 'CREDIT'){
// //search invoices
// }
// else{
// //search expenses
// }
// }
// private function searchClient($rule, $bank_transaction)
// {
// if($bank_transaction->base_type == 'CREDIT'){
// //search invoices
// }
// else{
// //search expenses
// }
// }
// private function searchVendor($rule, $bank_transaction)
// {
// //search expenses
// }
// private function searchDescription($rule, $bank_transaction)
// {
// //search expenses public notes
// }
// private function searchReference($rule, $bank_transaction)
// {
// if($bank_transaction->base_type == 'CREDIT'){
// //search invoices
// }
// else{
// //search expenses
// }
// }
public function getEntityType()
{
return self::class;
}
public function company()
{
return $this->belongsTo(Company::class);
}
public function vendor()
{
return $this->belongsTo(Vendor::class);
}
public function client()
{
return $this->belongsTo(Client::class);
}
public function user()
{
return $this->belongsTo(User::class)->withTrashed();
}
public function expense_cateogry()
{
return $this->belongsTo(ExpenseCategory::class)->withTrashed();
}
}

View File

@ -13,6 +13,7 @@ namespace App\Models;
use App\DataMapper\CompanySettings;
use App\Models\BankTransaction;
use App\Models\BankTransactionRule;
use App\Models\Language;
use App\Models\Presenters\CompanyPresenter;
use App\Models\PurchaseOrder;
@ -123,6 +124,8 @@ class Company extends BaseModel
'enabled_expense_tax_rates',
'invoice_task_project',
'report_include_deleted',
'invoice_task_lock',
'use_vendor_currency',
];
protected $hidden = [
@ -188,6 +191,11 @@ class Company extends BaseModel
return $this->hasMany(BankTransaction::class);
}
public function bank_transaction_rules()
{
return $this->hasMany(BankTransactionRule::class);
}
public function getCompanyIdAttribute()
{
return $this->encodePrimaryKey($this->id);
@ -541,6 +549,23 @@ class Company extends BaseModel
return $this->company_users()->withTrashed()->where('is_owner', true)->first()?->user;
}
public function credit_rules()
{
return BankTransactionRule::query()
->where('company_id', $this->id)
->where('applies_to', 'CREDIT')
->get();
}
public function debit_rules()
{
return BankTransactionRule::query()
->where('company_id', $this->id)
->where('applies_to', 'DEBIT')
->get();
}
public function resolveRouteBinding($value, $field = null)
{
return $this->where('id', $this->decodePrimaryKey($value))->firstOrFail();

View File

@ -79,7 +79,8 @@ class PurchaseOrder extends BaseModel
'partial',
'paid_to_date',
'vendor_id',
'last_viewed'
'last_viewed',
'currency_id',
];
protected $casts = [

View File

@ -54,6 +54,10 @@ class Subscription extends BaseModel
'price',
'name',
'currency_id',
'registration_required',
'optional_product_ids',
'optional_recurring_product_ids',
'use_inventory_management',
];
protected $casts = [

View File

@ -40,6 +40,7 @@ class Task extends BaseModel
'number',
'is_date_based',
'status_order',
'invoice_lock'
];
protected $touches = [];

View File

@ -64,6 +64,7 @@ class VendorContact extends Authenticatable implements HasLocalePreference
'email',
'is_primary',
'vendor_id',
'send_email',
];
public function avatar()

View File

@ -0,0 +1,31 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Policies;
use App\Models\User;
/**
* Class BankTransactionPolicy.
*/
class BankTransactionRulePolicy extends EntityPolicy
{
/**
* Checks if the user has create permissions.
*
* @param User $user
* @return bool
*/
public function create(User $user) : bool
{
return $user->isAdmin();
}
}

View File

@ -15,6 +15,7 @@ use App\Models\Activity;
use App\Models\Bank;
use App\Models\BankIntegration;
use App\Models\BankTransaction;
use App\Models\BankTransactionRule;
use App\Models\Client;
use App\Models\Company;
use App\Models\CompanyGateway;
@ -45,6 +46,7 @@ use App\Models\Webhook;
use App\Policies\ActivityPolicy;
use App\Policies\BankIntegrationPolicy;
use App\Policies\BankTransactionPolicy;
use App\Policies\BankTransactionRulePolicy;
use App\Policies\ClientPolicy;
use App\Policies\CompanyGatewayPolicy;
use App\Policies\CompanyPolicy;
@ -86,6 +88,7 @@ class AuthServiceProvider extends ServiceProvider
Activity::class => ActivityPolicy::class,
BankIntegration::class => BankIntegrationPolicy::class,
BankTransaction::class => BankTransactionPolicy::class,
BankTransactionRule::class => BankTransactionRulePolicy::class,
Client::class => ClientPolicy::class,
Company::class => CompanyPolicy::class,
CompanyToken::class => CompanyTokenPolicy::class,

View File

@ -11,6 +11,7 @@
namespace App\Repositories;
use App\Jobs\Bank\MatchBankTransactions;
use App\Models\BankTransaction;
use App\Models\Task;
use App\Models\TaskStatus;
@ -28,17 +29,25 @@ class BankTransactionRepository extends BaseRepository
$bank_transaction->bank_integration_id = $data['bank_integration_id'];
$bank_transaction->fill($data);
$bank_transaction->save();
if($bank_transaction->base_type == 'CREDIT' && $invoice = $bank_transaction->service()->matchInvoiceNumber())
{
$bank_transaction->invoice_ids = $invoice->hashed_id;
$bank_transaction->status_id = BankTransaction::STATUS_MATCHED;
$bank_transaction->save();
}
$bank_transaction->service()->processRules();
return $bank_transaction;
return $bank_transaction->fresh();
}
public function convert_matched($bank_transactions)
{
$data['transactions'] = $bank_transactions->map(function ($bt){
return ['id' => $bt->id, 'invoice_ids' => $bt->invoice_ids];
})->toArray();
$bts = (new MatchBankTransactions(auth()->user()->company()->id, auth()->user()->company()->db, $data))->handle();
}
}

View File

@ -0,0 +1,35 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Repositories;
use App\Models\BankTransactionRule;
use App\Models\Task;
use App\Models\TaskStatus;
/**
* Class for bank transaction rule repository.
*/
class BankTransactionRuleRepository extends BaseRepository
{
public function save($data, BankTransactionRule $bank_transaction_rule)
{
$bank_transaction_rule->fill($data);
$bank_transaction_rule->save();
return $bank_transaction_rule;
}
}

View File

@ -11,35 +11,35 @@
namespace App\Services\Bank;
use App\Factory\ExpenseCategoryFactory;
use App\Factory\ExpenseFactory;
use App\Libraries\MultiDB;
use App\Models\BankTransaction;
use App\Models\Company;
use App\Models\ExpenseCategory;
use App\Models\Invoice;
use App\Services\Bank\BankService;
use App\Utils\Traits\GeneratesCounter;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Cache;
class BankMatchingService implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
private $company_id;
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, GeneratesCounter;
private Company $company;
private $db;
private $invoices;
public $deleteWhenMissingModels = true;
public function __construct($company_id, $db)
{
$this->company_id = $company_id;
$this->db = $db;
}
public function __construct(private int $company_id, private string $db){}
public function handle()
{
@ -48,37 +48,27 @@ class BankMatchingService implements ShouldQueue
$this->company = Company::find($this->company_id);
$this->invoices = Invoice::where('company_id', $this->company->id)
->whereIn('status_id', [1,2,3])
->where('is_deleted', 0)
->get();
$this->matchTransactions();
$this->match();
}
private function match()
private function matchTransactions()
{
BankTransaction::where('company_id', $this->company->id)
->where('status_id', BankTransaction::STATUS_UNMATCHED)
->cursor()
->each(function ($bt){
$invoice = $this->invoices->first(function ($value, $key) use ($bt){
->where('status_id', BankTransaction::STATUS_UNMATCHED)
->cursor()
->each(function ($bt){
(new BankService($bt))->processRules();
return str_contains($bt->description, $value->number);
});
});
if($invoice)
{
$bt->invoice_ids = $invoice->hashed_id;
$bt->status_id = BankTransaction::STATUS_MATCHED;
$bt->save();
}
});
}
public function middleware()
{
return [new WithoutOverlapping($this->company_id)];
}
}

View File

@ -13,7 +13,7 @@ namespace App\Services\Bank;
use App\Models\BankTransaction;
use App\Models\Invoice;
use App\Services\Bank\ProcessBankRule;
use App\Services\Bank\ProcessBankRules;
class BankService
{
@ -40,11 +40,9 @@ class BankService
}
public function processRule($rule)
public function processRules()
{
(new ProcessBankRule($this->bank_transaction, $rule))->run();
return $this;
(new ProcessBankRules($this->bank_transaction))->run();
}
}

View File

@ -1,27 +0,0 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Services\Bank;
use App\Models\BankTransaction;
use App\Services\AbstractService;
class ProcessBankRule extends AbstractService
{
public function __construct(private BankTransaction $bank_transaction, $rule){}
public function run() : void
{
}
}

View File

@ -0,0 +1,206 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Services\Bank;
use App\Factory\ExpenseCategoryFactory;
use App\Factory\ExpenseFactory;
use App\Models\BankTransaction;
use App\Models\ExpenseCategory;
use App\Models\Invoice;
use App\Services\AbstractService;
use App\Utils\Traits\GeneratesCounter;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Cache;
class ProcessBankRules extends AbstractService
{
use GeneratesCounter;
protected $credit_rules;
protected $debit_rules;
protected $categories;
public function __construct(public BankTransaction $bank_transaction){}
public function run()
{
if($this->bank_transaction->base_type == 'DEBIT')
$this->matchDebit();
else
$this->matchCredit();
}
private function matchCredit()
{
$this->credit_rules = $this->bank_transaction->company->credit_rules();
$this->invoices = Invoice::where('company_id', $this->bank_transaction->company_id)
->whereIn('status_id', [1,2,3])
->where('is_deleted', 0)
->get();
$invoice = $this->invoices->first(function ($value, $key){
return str_contains($this->bank_transaction->description, $value->number);
});
if($invoice)
{
$this->bank_transaction->invoice_ids = $invoice->hashed_id;
$this->bank_transaction->status_id = BankTransaction::STATUS_MATCHED;
$this->bank_transaction->save();
return;
}
//stub for credit rules
foreach($this->credit_rules as $rule)
{
// $this->bank_transaction->bank_transaction_rule_id = $bank_transaction_rule->id;
}
}
private function matchDebit()
{
$this->debit_rules = $this->bank_transaction->company->debit_rules();
$this->categories = collect(Cache::get('bank_categories'));
foreach($this->debit_rules as $bank_transaction_rule)
{
$matches = 0;
foreach($bank_transaction_rule['rules'] as $rule)
{
$rule_count = count($bank_transaction_rule['rules']);
if($rule['search_key'] == 'description')
{
if($this->matchStringOperator($this->bank_transaction->description, $rule['value'], $rule['operator'])){
$matches++;
}
}
if($rule['search_key'] == 'amount')
{
if($this->matchNumberOperator($this->bank_transaction->amount, $rule['value'] , $rule['operator'])){
$matches++;
}
}
if(($bank_transaction_rule['matches_on_all'] && ($matches == $rule_count)) || (!$bank_transaction_rule['matches_on_all'] && $matches > 0))
{
// $this->bank_transaction->client_id = empty($rule['client_id']) ? null : $rule['client_id'];
$this->bank_transaction->vendor_id = $bank_transaction_rule->vendor_id;
$this->bank_transaction->ninja_category_id = $bank_transaction_rule->category_id;
$this->bank_transaction->status_id = BankTransaction::STATUS_MATCHED;
$this->bank_transaction->bank_transaction_rule_id = $bank_transaction_rule->id;
$this->bank_transaction->save();
if($bank_transaction_rule['auto_convert'])
{
$expense = ExpenseFactory::create($this->bank_transaction->company_id, $this->bank_transaction->user_id);
$expense->category_id = $bank_transaction_rule->category_id ?: $this->resolveCategory();
$expense->amount = $this->bank_transaction->amount;
$expense->number = $this->getNextExpenseNumber($expense);
$expense->currency_id = $this->bank_transaction->currency_id;
$expense->date = Carbon::parse($this->bank_transaction->date);
$expense->payment_date = Carbon::parse($this->bank_transaction->date);
$expense->transaction_reference = $this->bank_transaction->description;
$expense->transaction_id = $this->bank_transaction->id;
$expense->vendor_id = $bank_transaction_rule->vendor_id;
$expense->invoice_documents = $this->bank_transaction->company->invoice_expense_documents;
$expense->should_be_invoiced = $this->bank_transaction->company->mark_expenses_invoiceable;
$expense->save();
$this->bank_transaction->expense_id = $expense->id;
$this->bank_transaction->status_id = BankTransaction::STATUS_CONVERTED;
$this->bank_transaction->save();
break;
}
}
}
}
}
private function resolveCategory()
{
$category = $this->categories->firstWhere('highLevelCategoryId', $this->bank_transaction->category_id);
$ec = ExpenseCategory::where('company_id', $this->bank_transaction->company_id)->where('bank_category_id', $this->bank_transaction->category_id)->first();
if($ec)
return $ec->id;
if($category)
{
$ec = ExpenseCategoryFactory::create($this->bank_transaction->company_id, $this->bank_transaction->user_id);
$ec->bank_category_id = $this->bank_transaction->category_id;
$ec->name = $category->highLevelCategoryName;
$ec->save();
return $ec->id;
}
}
private function matchNumberOperator($bt_value, $rule_value, $operator) :bool
{
return match ($operator) {
'>' => floatval($bt_value) > floatval($rule_value),
'>=' => floatval($bt_value) >= floatval($rule_value),
'=' => floatval($bt_value) == floatval($rule_value),
'<' => floatval($bt_value) < floatval($rule_value),
'<=' => floatval($bt_value) <= floatval($rule_value),
default => false,
};
}
private function matchStringOperator($bt_value, $rule_value, $operator) :bool
{
$bt_value = strtolower(str_replace(" ", "", $bt_value));
$rule_value = strtolower(str_replace(" ", "", $rule_value));
$rule_length = iconv_strlen($rule_value);
return match ($operator) {
'is' => $bt_value == $rule_value,
'contains' => stripos($bt_value, $rule_value) !== false,
'starts_with' => substr($bt_value, 0, $rule_length) == $rule_value,
'is_empty' => empty($bt_value),
default => false,
};
}
}

View File

@ -29,32 +29,44 @@ class ClientService
public function updateBalance(float $amount)
{
// $this->client->balance += $amount;
\DB::connection(config('database.default'))->transaction(function () use($amount) {
try {
\DB::connection(config('database.default'))->transaction(function () use($amount) {
$this->client = Client::withTrashed()->where('id', $this->client->id)->lockForUpdate()->first();
$this->client->balance += $amount;
$this->client->save();
nlog("inside transaction - updating balance by {$amount}");
}, 2);
$this->client = Client::withTrashed()->where('id', $this->client->id)->lockForUpdate()->first();
$this->client->balance += $amount;
$this->client->save();
}, 2);
}
catch (\Throwable $throwable) {
nlog("DB ERROR " . $throwable->getMessage());
}
return $this;
}
public function updateBalanceAndPaidToDate(float $balance, float $paid_to_date)
{
// $this->client->balance += $amount;
// $this->client->paid_to_date += $amount;
\DB::connection(config('database.default'))->transaction(function () use($balance, $paid_to_date) {
try {
\DB::connection(config('database.default'))->transaction(function () use($balance, $paid_to_date) {
$this->client = Client::withTrashed()->where('id', $this->client->id)->lockForUpdate()->first();
$this->client->balance += $balance;
$this->client->paid_to_date += $paid_to_date;
$this->client->save();
$this->client = Client::withTrashed()->where('id', $this->client->id)->lockForUpdate()->first();
$this->client->balance += $balance;
$this->client->paid_to_date += $paid_to_date;
$this->client->save();
}, 2);
}
catch (\Throwable $throwable) {
nlog("DB ERROR " . $throwable->getMessage());
}
}, 2);
return $this;
}

View File

@ -147,7 +147,7 @@ class ApplyPayment
event(new InvoiceWasUpdated($this->invoice, $this->invoice->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null)));
if ((int) $this->invoice->balance == 0) {
$this->invoice->service()->deletePdf();
$this->invoice->service()->touchPdf();
$this->invoice = $this->invoice->fresh();
event(new InvoiceWasPaid($this->invoice, $this->payment, $this->payment->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null)));
}

View File

@ -103,7 +103,7 @@ class ApplyPayment extends AbstractService
}
});
$this->invoice->service()->applyNumber()->workFlow()->save();
$this->invoice->service()->applyNumber()->workFlow()->touchPdf()->save();
$transaction = [
'invoice' => $this->invoice->transaction_event(),

View File

@ -83,7 +83,7 @@ class ApplyPaymentAmount extends AbstractService
->updatePaidToDate($payment->amount)
->setCalculatedStatus()
->applyNumber()
->deletePdf()
->touchPdf()
->save();
$this->invoice

View File

@ -112,10 +112,12 @@ class InvoiceService
* @param Payment $payment The Payment
* @param float $payment_amount The Payment amount
* @return InvoiceService Parent class object
* @deprecated 24-11-2022 - cannot find any references to this method anywhere
*/
public function applyPayment(Payment $payment, float $payment_amount)
{
$this->deletePdf();
// $this->deletePdf();
$this->invoice = $this->markSent()->save();
$this->invoice = (new ApplyPayment($this->invoice, $payment, $payment_amount))->run();
@ -218,7 +220,6 @@ class InvoiceService
public function markDeleted()
{
$this->removeUnpaidGatewayFees();
$this->deletePdf();
$this->invoice = (new MarkInvoiceDeleted($this->invoice))->run();
@ -378,6 +379,7 @@ class InvoiceService
})->toArray();
$this->invoice = $this->invoice->calc()->getInvoice();
$this->invoice->service()->touchPdf();
/* 24-03-2022 */
$new_balance = $this->invoice->balance;

View File

@ -0,0 +1,90 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Transformers;
use App\Models\Account;
use App\Models\BankTransaction;
use App\Models\BankTransactionRule;
use App\Models\Company;
use App\Models\Expense;
use App\Models\Invoice;
use App\Transformers\VendorTransformer;
use App\Utils\Traits\MakesHash;
/**
* Class BankTransactionRuleTransformer.
*/
class BankTransactionRuleTransformer extends EntityTransformer
{
use MakesHash;
/**
* @var array
*/
protected $defaultIncludes = [
];
/**
* @var array
*/
protected $availableIncludes = [
'company',
'vendor',
'client',
'expense_category',
];
/**
* @param BankTransaction $bank_integration
* @return array
*/
public function transform(BankTransactionRule $bank_transaction_rule)
{
return [
'id' => (string) $this->encodePrimaryKey($bank_transaction_rule->id),
'name' => (string) $bank_transaction_rule->name,
'rules' => $bank_transaction_rule->rules ?: (array) [],
'auto_convert' => (bool) $bank_transaction_rule->auto_convert,
'matches_on_all' => (bool) $bank_transaction_rule->matches_on_all,
'applies_to' => (string) $bank_transaction_rule->applies_to,
'client_id' => $this->encodePrimaryKey($bank_transaction_rule->client_id) ?: '',
'vendor_id' => $this->encodePrimaryKey($bank_transaction_rule->vendor_id) ?: '',
'category_id' => $this->encodePrimaryKey($bank_transaction_rule->category_id) ?: '',
'is_deleted' => (bool) $bank_transaction_rule->is_deleted,
'created_at' => (int) $bank_transaction_rule->created_at,
'updated_at' => (int) $bank_transaction_rule->updated_at,
'archived_at' => (int) $bank_transaction_rule->deleted_at,
];
}
public function includeCompany(BankTransactionRule $bank_transaction_rule)
{
$transformer = new CompanyTransformer($this->serializer);
return $this->includeItem($bank_transaction_rule->company, $transformer, Company::class);
}
public function includeClient(BankTransactionRule $bank_transaction_rule)
{
$transformer = new ClientTransformer($this->serializer);
return $this->includeItem($bank_transaction_rule->expense, $transformer, Client::class);
}
public function includeVendor(BankTransactionRule $bank_transaction_rule)
{
$transformer = new VendorTransformer($this->serializer);
return $this->includeItem($bank_transaction_rule->vendor, $transformer, Vendor::class);
}
}

View File

@ -67,6 +67,7 @@ class BankTransactionTransformer extends EntityTransformer
'invoice_ids' => (string) $bank_transaction->invoice_ids ?: '',
'expense_id'=> (string) $this->encodePrimaryKey($bank_transaction->expense_id) ?: '',
'vendor_id'=> (string) $this->encodePrimaryKey($bank_transaction->vendor_id) ?: '',
'bank_transaction_rule_id' => (string) $this->encodePrimaryKey($bank_transaction->bank_transaction_rule_id) ?: '',
'is_deleted' => (bool) $bank_transaction->is_deleted,
'created_at' => (int) $bank_transaction->created_at,
'updated_at' => (int) $bank_transaction->updated_at,

View File

@ -43,6 +43,7 @@ use App\Models\TaxRate;
use App\Models\User;
use App\Models\Webhook;
use App\Transformers\BankIntegrationTransformer;
use App\Transformers\BankTransactionRuleTransformer;
use App\Transformers\BankTransactionTransformer;
use App\Transformers\PurchaseOrderTransformer;
use App\Transformers\RecurringExpenseTransformer;
@ -104,6 +105,7 @@ class CompanyTransformer extends EntityTransformer
'purchase_orders',
'bank_integrations',
'bank_transactions',
'bank_transaction_rules',
];
/**
@ -186,6 +188,8 @@ class CompanyTransformer extends EntityTransformer
'enabled_expense_tax_rates' => (int) $company->enabled_expense_tax_rates,
'invoice_task_project' => (bool) $company->invoice_task_project,
'report_include_deleted' => (bool) $company->report_include_deleted,
'invoice_task_lock' => (bool) $company->invoice_task_lock,
'use_vendor_currency' => (bool) $company->use_vendor_currency,
];
}
@ -231,6 +235,14 @@ class CompanyTransformer extends EntityTransformer
return $this->includeCollection($company->bank_transactions, $transformer, BankTransaction::class);
}
public function includeBankTransactionRules(Company $company)
{
$transformer = new BankTransactionRuleTransformer($this->serializer);
return $this->includeCollection($company->bank_transaction_rules, $transformer, BankTransactionRule::class);
}
public function includeBankIntegrations(Company $company)
{
$transformer = new BankIntegrationTransformer($this->serializer);

View File

@ -132,6 +132,7 @@ class PurchaseOrderTransformer extends EntityTransformer
'paid_to_date' => (float)$purchase_order->paid_to_date,
'subscription_id' => $this->encodePrimaryKey($purchase_order->subscription_id),
'expense_id' => $this->encodePrimaryKey($purchase_order->expense_id),
'currency_id' => $purchase_order->currency_id ? (string) $purchase_order->currency_id : '',
];
}

View File

@ -68,6 +68,10 @@ class SubscriptionTransformer extends EntityTransformer
'updated_at' => (int) $subscription->updated_at,
'archived_at' => (int) $subscription->deleted_at,
'plan_map' => '', //@deprecated 03/04/2021
'use_inventory_management' => (bool) $subscription->use_inventory_management,
'optional_recurring_product_ids' =>(string)$subscription->optional_recurring_product_ids,
'optional_product_ids' => (string) $subscription->optional_product_ids,
'registration_required' => (bool) $subscription->registration_required,
];
}
}

View File

@ -72,7 +72,6 @@ class TaskTransformer extends EntityTransformer
'user_id' => (string) $this->encodePrimaryKey($task->user_id),
'assigned_user_id' => (string) $this->encodePrimaryKey($task->assigned_user_id),
'number' => (string) $task->number ?: '',
// 'start_time' => (int) $task->start_time,
'description' => (string) $task->description ?: '',
'duration' => (int) $task->duration ?: 0,
'rate' => (float) $task->rate ?: 0,
@ -93,6 +92,7 @@ class TaskTransformer extends EntityTransformer
'status_sort_order' => (int) $task->status_sort_order, //deprecated 5.0.34
'is_date_based' => (bool) $task->is_date_based,
'status_order' => is_null($task->status_order) ? null : (int) $task->status_order,
'invoice_lock' => (bool) $task->invoice_lock,
];
}
}

View File

@ -68,7 +68,7 @@ trait Inviteable
);
$writer = new Writer($renderer);
$qr = $writer->writeString($this->getPaymentLink());
$qr = $writer->writeString($this->getPaymentLink(), 'utf-8');
return "<svg viewBox='0 0 200 200' width='200' height='200' x='0' y='0' xmlns='http://www.w3.org/2000/svg'>
<rect x='0' y='0' width='100%'' height='100%' />{$qr}</svg>";
@ -80,7 +80,7 @@ trait Inviteable
if (Ninja::isHosted()) {
$domain = $this->company->domain();
} else {
$domain = config('ninja.app_url');
$domain = strlen($this->company->portal_domain) > 5 ? $this->company->portal_domain : config('ninja.app_url');
}
$entity_type = Str::snake(class_basename($this->entityType()));
@ -95,7 +95,7 @@ trait Inviteable
if (Ninja::isHosted()) {
$domain = $this->company->domain();
} else {
$domain = config('ninja.app_url');
$domain = strlen($this->company->portal_domain) > 5 ? $this->company->portal_domain : config('ninja.app_url');
}
switch ($this->company->portal_mode) {
@ -121,7 +121,7 @@ trait Inviteable
if (Ninja::isHosted()) {
$domain = $this->company->domain();
} else {
$domain = config('ninja.app_url');
$domain = strlen($this->company->portal_domain) > 5 ? $this->company->portal_domain : config('ninja.app_url');
}
switch ($this->company->portal_mode) {

View File

@ -211,4 +211,5 @@ return [
'dev_mode' => env("YODLEE_DEV_MODE", false),
'config_name' => env("YODLEE_CONFIG_NAME", false),
],
'dbs' => ['db-ninja-01','db-ninja-02']
];

View File

@ -0,0 +1,31 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace Database\Factories;
use App\Models\Account;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;
class BankTransactionRuleFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array
*/
public function definition()
{
return [
'name' =>$this->faker->name(),
];
}
}

View File

@ -0,0 +1,52 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('bank_transaction_rules', function (Blueprint $table) {
$table->id();
$table->unsignedInteger('company_id');
$table->unsignedInteger('user_id');
$table->string('name'); //name of rule
$table->mediumText('rules')->nullable(); //array of rule objects
$table->boolean('auto_convert')->default(false); //auto convert to match
$table->boolean('matches_on_all')->default(false); //match on all rules or just one
$table->string('applies_to')->default('CREDIT'); //CREDIT/DEBIT
$table->unsignedInteger('client_id')->nullable();
$table->unsignedInteger('vendor_id')->nullable();
$table->unsignedInteger('category_id')->nullable();
$table->boolean('is_deleted')->default(0);
$table->timestamps(6);
$table->softDeletes('deleted_at', 6);
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade')->onUpdate('cascade');
$table->foreign('company_id')->references('id')->on('companies')->onDelete('cascade')->onUpdate('cascade');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
//
}
};

View File

@ -0,0 +1,83 @@
<?php
use App\Models\Currency;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('tasks', function (Blueprint $table)
{
$table->boolean('invoice_lock')->default(false);
});
Schema::table('companies', function (Blueprint $table)
{
$table->boolean('invoice_task_lock')->default(false);
$table->boolean('use_vendor_currency')->default(false);
});
Schema::table('purchase_orders', function (Blueprint $table)
{
$table->unsignedInteger('currency_id')->nullable();
});
Schema::table('bank_transactions', function (Blueprint $table)
{
$table->bigInteger('bank_transaction_rule_id')->nullable();
});
Schema::table('subscriptions', function (Blueprint $table)
{
$table->boolean('registration_required')->default(false);
$table->boolean('use_inventory_management')->default(false);
$table->text('optional_product_ids')->nullable();
$table->text('optional_recurring_product_ids')->nullable();
});
$currencies = [
['id' => 113, 'name' => 'Swazi lilangeni', 'code' => 'SZL', 'symbol' => 'E', 'precision' => '2', 'thousand_separator' => ',', 'decimal_separator' => '.'],
];
foreach ($currencies as $currency) {
$record = Currency::whereCode($currency['code'])->first();
if ($record) {
$record->name = $currency['name'];
$record->symbol = $currency['symbol'];
$record->precision = $currency['precision'];
$record->thousand_separator = $currency['thousand_separator'];
$record->decimal_separator = $currency['decimal_separator'];
if (isset($currency['swap_currency_symbol'])) {
$record->swap_currency_symbol = $currency['swap_currency_symbol'];
}
$record->save();
} else {
Currency::create($currency);
}
}
\Illuminate\Support\Facades\Artisan::call('ninja:design-update');
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
//
}
};

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -127,6 +127,15 @@ class CurrenciesSeeder extends Seeder
['id' => 102, 'name' => 'Moldovan Leu', 'code' => 'MDL', 'symbol' => '', 'precision' => '2', 'thousand_separator' => ',', 'decimal_separator' => '.'],
['id' => 103, 'name' => 'Kazakhstani Tenge', 'code' => 'KZT', 'symbol' => '', 'precision' => '2', 'thousand_separator' => ',', 'decimal_separator' => '.'],
['id' => 104, 'name' => 'Ethiopian Birr', 'code' => 'ETB', 'symbol' => '', 'precision' => '2', 'thousand_separator' => ',', 'decimal_separator' => '.'],
['id' => 105, 'name' => 'Gambia Dalasi', 'code' => 'GMD', 'symbol' => 'D', 'precision' => '2', 'thousand_separator' => ',', 'decimal_separator' => '.'],
['id' => 106, 'name' => 'Paraguayan Guarani', 'code' => 'PYG', 'symbol' => '₲', 'precision' => '0', 'thousand_separator' => ',', 'decimal_separator' => '.'],
['id' => 107, 'name' => 'Malawi Kwacha', 'code' => 'MWK', 'symbol' => 'MK', 'precision' => '2', 'thousand_separator' => ',', 'decimal_separator' => '.'],
['id' => 108, 'name' => 'Zimbabwean Dollar', 'code' => 'ZWL', 'symbol' => 'Z$', 'precision' => '0', 'thousand_separator' => ',', 'decimal_separator' => '.'],
['id' => 109, 'name' => 'Cambodian Riel', 'code' => 'KHR', 'symbol' => '៛', 'precision' => '2', 'thousand_separator' => ',', 'decimal_separator' => '.'],
['id' => 110, 'name' => 'Vanuatu Vatu', 'code' => 'VUV', 'symbol' => 'VT', 'precision' => '0', 'thousand_separator' => ',', 'decimal_separator' => '.'],
['id' => 111, 'name' => 'Cuban Peso', 'code' => 'CUP', 'symbol' => '₱', 'precision' => '2', 'thousand_separator' => ',', 'decimal_separator' => '.'],
['id' => 112, 'name' => 'Cayman Island Dollar', 'code' => 'KYD', 'symbol' => '', 'precision' => '2', 'thousand_separator' => ',', 'decimal_separator' => '.'],
['id' => 113, 'name' => 'Swazi lilangeni', 'code' => 'SZL', 'symbol' => 'E', 'precision' => '2', 'thousand_separator' => ',', 'decimal_separator' => '.'],
];
foreach ($currencies as $currency) {

View File

@ -4843,6 +4843,11 @@ $LANG = array(
'refresh_accounts' => 'Refresh Accounts',
'upgrade_to_connect_bank_account' => 'Upgrade to Enterprise to connect your bank account',
'click_here_to_connect_bank_account' => 'Click here to connect your bank account',
'task_update_authorization_error' => 'Insufficient permissions, or task may be locked',
'cash_vs_accrual' => 'Accrual accounting',
'cash_vs_accrual_help' => 'Turn on for accrual reporting, turn off for cash basis reporting.',
'expense_paid_report' => 'Expensed reporting',
'expense_paid_report_help' => 'Turn on for reporting all expenses, turn off for reporting only paid expenses',
);
return $LANG;

65
package-lock.json generated
View File

@ -5,6 +5,8 @@
"packages": {
"": {
"dependencies": {
"@tailwindcss/forms": "^0.3.4",
"@tailwindcss/line-clamp": "^0.3.1",
"autoprefixer": "^10.3.7",
"axios": "^0.25",
"card-js": "^1.0.13",
@ -25,6 +27,7 @@
"devDependencies": {
"@babel/compat-data": "7.15.0",
"@babel/plugin-proposal-class-properties": "^7.14.5",
"@tailwindcss/aspect-ratio": "^0.4.2",
"laravel-mix-purgecss": "^6.0.0",
"vue-template-compiler": "^2.6.14"
}
@ -1682,6 +1685,34 @@
"node": ">= 8"
}
},
"node_modules/@tailwindcss/aspect-ratio": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/@tailwindcss/aspect-ratio/-/aspect-ratio-0.4.2.tgz",
"integrity": "sha512-8QPrypskfBa7QIMuKHg2TA7BqES6vhBrDLOv8Unb6FcFyd3TjKbc6lcmb9UPQHxfl24sXoJ41ux/H7qQQvfaSQ==",
"dev": true,
"peerDependencies": {
"tailwindcss": ">=2.0.0 || >=3.0.0 || >=3.0.0-alpha.1"
}
},
"node_modules/@tailwindcss/forms": {
"version": "0.3.4",
"resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.3.4.tgz",
"integrity": "sha512-vlAoBifNJUkagB+PAdW4aHMe4pKmSLroH398UPgIogBFc91D2VlHUxe4pjxQhiJl0Nfw53sHSJSQBSTQBZP3vA==",
"dependencies": {
"mini-svg-data-uri": "^1.2.3"
},
"peerDependencies": {
"tailwindcss": ">=2.0.0"
}
},
"node_modules/@tailwindcss/line-clamp": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/line-clamp/-/line-clamp-0.3.1.tgz",
"integrity": "sha512-pNr0T8LAc3TUx/gxCfQZRe9NB2dPEo/cedPHzUGIPxqDMhgjwNm6jYxww4W5l0zAsAddxr+XfZcqttGiFDgrGg==",
"peerDependencies": {
"tailwindcss": ">=2.0.0 || >=3.0.0 || >=3.0.0-alpha.1"
}
},
"node_modules/@trysound/sax": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz",
@ -5742,6 +5773,14 @@
"url": "https://opencollective.com/webpack"
}
},
"node_modules/mini-svg-data-uri": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz",
"integrity": "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==",
"bin": {
"mini-svg-data-uri": "cli.js"
}
},
"node_modules/minimalistic-assert": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
@ -10285,6 +10324,27 @@
"fastq": "^1.6.0"
}
},
"@tailwindcss/aspect-ratio": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/@tailwindcss/aspect-ratio/-/aspect-ratio-0.4.2.tgz",
"integrity": "sha512-8QPrypskfBa7QIMuKHg2TA7BqES6vhBrDLOv8Unb6FcFyd3TjKbc6lcmb9UPQHxfl24sXoJ41ux/H7qQQvfaSQ==",
"dev": true,
"requires": {}
},
"@tailwindcss/forms": {
"version": "0.3.4",
"resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.3.4.tgz",
"integrity": "sha512-vlAoBifNJUkagB+PAdW4aHMe4pKmSLroH398UPgIogBFc91D2VlHUxe4pjxQhiJl0Nfw53sHSJSQBSTQBZP3vA==",
"requires": {
"mini-svg-data-uri": "^1.2.3"
}
},
"@tailwindcss/line-clamp": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/line-clamp/-/line-clamp-0.3.1.tgz",
"integrity": "sha512-pNr0T8LAc3TUx/gxCfQZRe9NB2dPEo/cedPHzUGIPxqDMhgjwNm6jYxww4W5l0zAsAddxr+XfZcqttGiFDgrGg==",
"requires": {}
},
"@trysound/sax": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz",
@ -13399,6 +13459,11 @@
}
}
},
"mini-svg-data-uri": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz",
"integrity": "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg=="
},
"minimalistic-assert": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",

View File

@ -11,10 +11,13 @@
"devDependencies": {
"@babel/compat-data": "7.15.0",
"@babel/plugin-proposal-class-properties": "^7.14.5",
"@tailwindcss/aspect-ratio": "^0.4.2",
"laravel-mix-purgecss": "^6.0.0",
"vue-template-compiler": "^2.6.14"
},
"dependencies": {
"@tailwindcss/line-clamp": "^0.3.1",
"@tailwindcss/forms": "^0.3.4",
"autoprefixer": "^10.3.7",
"axios": "^0.25",
"card-js": "^1.0.13",

2
public/css/app.css vendored

File diff suppressed because one or more lines are too long

2
public/js/app.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,5 @@
{
"/js/app.js": "/js/app.js?id=384185bf9d293949134d09b890c81369",
"/js/app.js": "/js/app.js?id=19300612c6880925e8043b61e8d49632",
"/js/clients/payment_methods/authorize-authorize-card.js": "/js/clients/payment_methods/authorize-authorize-card.js?id=9fb77e87fe0f85a367050e08f79ec9df",
"/js/clients/payments/authorize-credit-card-payment.js": "/js/clients/payments/authorize-credit-card-payment.js?id=803182f668c39d631ca5c55437876da4",
"/js/clients/payments/forte-credit-card-payment.js": "/js/clients/payments/forte-credit-card-payment.js?id=6e9f466c5504d3753f9b4ffc6f947095",
@ -15,7 +15,7 @@
"/js/clients/quotes/action-selectors.js": "/js/clients/quotes/action-selectors.js?id=6fb63bae43d077b5061f4dadfe8dffc8",
"/js/clients/quotes/approve.js": "/js/clients/quotes/approve.js?id=cdc76607aaf0b47a5a4e554e4177713d",
"/js/clients/payments/stripe-credit-card.js": "/js/clients/payments/stripe-credit-card.js?id=809de47258a681f0ffebe787dd6a9a93",
"/js/setup/setup.js": "/js/setup/setup.js?id=87367cce4927b42a92defdbae7a64711",
"/js/setup/setup.js": "/js/setup/setup.js?id=27560b012f166f8b9417ced2188aab70",
"/js/clients/payments/card-js.min.js": "/js/clients/payments/card-js.min.js?id=8ce33c3deae058ad314fb8357e5be63b",
"/js/clients/shared/pdf.js": "/js/clients/shared/pdf.js?id=be5307abc990bb44f2f92628103b1d98",
"/js/clients/shared/multiple-downloads.js": "/js/clients/shared/multiple-downloads.js?id=c2caa29f753ad1f3a12ca45acddacd72",
@ -42,7 +42,7 @@
"/js/clients/payments/stripe-przelewy24.js": "/js/clients/payments/stripe-przelewy24.js?id=3d53d2f7d0291d9f92cf7414dd2d351c",
"/js/clients/payments/stripe-browserpay.js": "/js/clients/payments/stripe-browserpay.js?id=db71055862995fd6ae21becfc587a3de",
"/js/clients/payments/stripe-fpx.js": "/js/clients/payments/stripe-fpx.js?id=914a6846ad1e5584635e7430fef76875",
"/css/app.css": "/css/app.css?id=6bafb560444b3b12f8d1ce59bd7fd703",
"/css/app.css": "/css/app.css?id=2c1ff2517e9909ca83760beb295535be",
"/css/card-js.min.css": "/css/card-js.min.css?id=62afeb675235451543ada60afcedcb7c",
"/vendor/clipboard.min.js": "/vendor/clipboard.min.js?id=15f52a1ee547f2bdd46e56747332ca2d"
}

View File

@ -0,0 +1,17 @@
@extends('portal.ninja2020.layout.clean')
@section('meta_title', ctrans('texts.purchase'))
@section('body')
@livewire('billing-portal-purchasev2', ['subscription' => $subscription, 'company' => $subscription->company, 'contact' => auth()->guard('contact')->user(), 'hash' => $hash, 'request_data' => $request_data, 'campaign' => request()->query('campaign') ?? null])
@stop
@push('footer')
<script>
function updateGatewayFields(companyGatewayId, paymentMethodId) {
document.getElementById('company_gateway_id').value = companyGatewayId;
document.getElementById('payment_method_id').value = paymentMethodId;
}
Livewire.on('beforePaymentEventsCompleted', () => document.getElementById('payment-method-form').submit());
</script>
@endpush

View File

@ -321,6 +321,17 @@
position: fixed;
}
.project-header {
font-size: 1.2em;
margin-top: 0.1em;
margin-bottom: 0;
padding-bottom: 0;
margin-left: 0;
margin-right: 0;
font-weight: bold;
color: #505050;
}
/** Useful snippets, uncomment to enable. **/
/** Hide company logo **/

View File

@ -306,6 +306,16 @@
position: fixed;
}
.project-header {
font-size: 1.2em;
margin-top: 0.1em;
margin-bottom: 0;
padding-bottom: 0;
margin-left: 0;
margin-right: 0;
font-weight: bold;
color: #505050;
}
/** Useful snippets, uncomment to enable. **/
/** Hide company logo **/

View File

@ -293,6 +293,18 @@
z-index:200 !important;
position: fixed;
}
.project-header {
font-size: 1.2em;
margin-top: 0.1em;
margin-bottom: 0;
padding-bottom: 0;
margin-left: 0;
margin-right: 0;
font-weight: bold;
color: #505050;
}
/** Useful snippets, uncomment to enable. **/
/** Hide company logo **/

View File

@ -286,6 +286,17 @@
z-index:200 !important;
position: fixed;
}
.project-header {
font-size: 1.2em;
margin-top: 0.1em;
margin-bottom: 0;
padding-bottom: 0;
margin-left: 0;
margin-right: 0;
font-weight: bold;
color: #505050;
}
/** Useful snippets, uncomment to enable. **/

View File

@ -258,6 +258,17 @@
z-index:200 !important;
position: fixed;
}
.project-header {
font-size: 1.2em;
margin-top: 0.1em;
margin-bottom: 0;
padding-bottom: 0;
margin-left: 0;
margin-right: 0;
font-weight: bold;
color: #505050;
}
/** Useful snippets, uncomment to enable. **/
/** Hide company logo **/

View File

@ -262,6 +262,17 @@
opacity: 0.2;
z-index:200 !important;
position: fixed;
}
.project-header {
font-size: 1.2em;
margin-top: 0.1em;
margin-bottom: 0;
padding-bottom: 0;
margin-left: 0;
margin-right: 0;
font-weight: bold;
color: #505050;
}
/** Useful snippets, uncomment to enable. **/

View File

@ -280,6 +280,17 @@
z-index:200 !important;
position: fixed;
}
.project-header {
font-size: 1.2em;
margin-top: 0.1em;
margin-bottom: 0;
padding-bottom: 0;
margin-left: 0;
margin-right: 0;
font-weight: bold;
color: #505050;
}
/** Useful snippets, uncomment to enable. **/
/** Hide company logo **/

View File

@ -307,6 +307,17 @@
z-index:200 !important;
position: fixed;
}
.project-header {
font-size: 1.2em;
margin-top: 0.1em;
margin-bottom: 0;
padding-bottom: 0;
margin-left: 0;
margin-right: 0;
font-weight: bold;
color: #505050;
}
/** Useful snippets, uncomment to enable. **/
/** Hide company logo **/

View File

@ -249,6 +249,17 @@
opacity: 0.2;
z-index:200 !important;
position: fixed;
}
.project-header {
font-size: 1.2em;
margin-top: 0.1em;
margin-bottom: 0;
padding-bottom: 0;
margin-left: 0;
margin-right: 0;
font-weight: bold;
color: #505050;
}
/** Useful snippets, uncomment to enable. **/

View File

@ -323,6 +323,17 @@
z-index:200 !important;
position: fixed;
}
.project-header {
font-size: 1.2em;
margin-top: 0.1em;
margin-bottom: 0;
padding-bottom: 0;
margin-left: 0;
margin-right: 0;
font-weight: bold;
color: #505050;
}
/** Useful snippets, uncomment to enable. **/

View File

@ -287,6 +287,17 @@
z-index:200 !important;
position: fixed;
}
.project-header {
font-size: 1.2em;
margin-top: 0.1em;
margin-bottom: 0;
padding-bottom: 0;
margin-left: 0;
margin-right: 0;
font-weight: bold;
color: #505050;
}
/** Useful snippets, uncomment to enable. **/
/** Hide company logo **/

View File

@ -0,0 +1,152 @@
<style type="text/css">
</style>
<div class="grid grid-cols-12">
<div class="col-span-12 xl:col-span-8 bg-gray-50 flex flex-col max-h-100px items-center h-screen">
<div class="w-full p-8 md:max-w-3xl">
<img class="object-scale-down" style="max-height: 100px;"src="{{ $subscription->company->present()->logo }}" alt="{{ $subscription->company->present()->name }}">
<h1 id="billing-page-company-logo" class="text-3xl font-bold tracking-wide mt-6">
{{ $subscription->name }}
</h1>
</div>
<div class="w-full p-4 md:max-w-3xl">
@if(!empty($subscription->recurring_product_ids))
<p
class="mb-4 uppercase leading-4 tracking-wide inline-flex items-center rounded-full text-xs font-medium">
{{ ctrans('texts.recurring_purchases') }}
</p>
<ul role="list" class="divide-y divide-gray-200 bg-white">
@foreach($subscription->service()->recurring_products() as $product)
<li>
<a href="#" class="block hover:bg-gray-50">
<div class="px-4 py-4 sm:px-6">
<div class="flex items-center justify-between">
<div class="ml-2 flex flex-shrink-0">
<p class="inline-flex rounded-full bg-green-100 px-2 text-xs font-semibold leading-5 text-green-800"></p>
</div>
</div>
<div class="mt-0 sm:flex sm:justify-between">
<div class="sm:flex">
<p class="text-sm font-medium text-gray-900 mt-0">{!! nl2br($product->notes) !!}</p>
</div>
<div class="mt-2 flex items-center text-sm text-gray-500 sm:mt-0">
<span data-ref="price">{{ \App\Utils\Number::formatMoney($product->price, $subscription->company) }} / {{ App\Models\RecurringInvoice::frequencyForKey($subscription->frequency_id) }}</span>
</div>
</div>
</div>
</a>
</li>
@endforeach
</ul>
@endif
</div>
<div class="w-full p-4 md:max-w-3xl">
@if(!empty($subscription->product_ids))
<p
class="mb-4 uppercase leading-4 tracking-wide inline-flex items-center rounded-full text-xs font-medium">
{{ ctrans('texts.one_time_purchases') }}
</p>
<ul role="list" class="divide-y divide-gray-200 bg-white">
@foreach($subscription->service()->products() as $product)
<li>
<a href="#" class="block hover:bg-gray-50">
<div class="px-4 py-4 sm:px-6">
<div class="flex items-center justify-between">
<p class="truncate text-sm font-medium text-gray-600"></p>
<div class="ml-2 flex flex-shrink-0">
<p class="inline-flex rounded-full bg-green-100 px-2 text-xs font-semibold leading-5 text-green-800"></p>
</div>
</div>
<div class="mt-2 sm:flex sm:justify-between">
<div class="sm:flex">
<p class="text-sm font-medium text-gray-900 mt-2">{!! nl2br($product->notes) !!}</p>
</div>
<div class="mt-2 flex items-center text-sm text-gray-500 sm:mt-0">
<span data-ref="price">{{ \App\Utils\Number::formatMoney($product->price, $subscription->company) }}</span>
</div>
</div>
</div>
</a>
</li>
@endforeach
</ul>
@endif
</div>
<div class="relative mt-8">
<div class="absolute inset-0 flex items-center">
<div class="w-full border-t border-gray-300"></div>
</div>
<div class="relative flex justify-center text-sm leading-5">
<h1 class="text-2xl font-bold tracking-wide bg-gray-50 px-6 py-0">Optional products</h1>
</div>
</div>
<div class="w-full p-4 md:max-w-3xl">
@if(!empty($subscription->recurring_product_ids))
@foreach($subscription->service()->recurring_products() as $product)
<div class="flex items-center justify-between mb-4 bg-white rounded px-6 py-4 shadow-sm border">
<div class="text-sm">{!! nl2br($product->notes) !!}</div>
<div data-ref="price-and-quantity-container">
<span data-ref="price">{{ \App\Utils\Number::formatMoney($product->price, $subscription->company) }} / {{ App\Models\RecurringInvoice::frequencyForKey($subscription->frequency_id) }}</span>
{{-- <span data-ref="quantity" class="text-sm">(1x)</span>--}}
</div>
</div>
@endforeach
@endif
</div>
<div class="w-full p-4 md:max-w-3xl">
@if(!empty($subscription->product_ids))
@foreach($subscription->service()->products() as $product)
<div class="flex items-center justify-between mb-4 bg-white rounded px-6 py-4 shadow-sm border">
<div class="text-sm">{!! nl2br($product->notes) !!}</div>
<div data-ref="price-and-quantity-container">
<span
data-ref="price">{{ \App\Utils\Number::formatMoney($product->price, $subscription->company) }}</span>
{{-- <span data-ref="quantity" class="text-sm">(1x)</span>--}}
</div>
</div>
@endforeach
@endif
</div>
</div>
<div class="col-span-12 xl:col-span-4 bg-blue-500 flex flex-col item-center ">
<div class="w-full p-4 md:max-w-3xl">
<div id="summary" class="w-1/4 px-8 text-white">
<h1 class="font-semibold text-2xl border-b pb-8 text-white">Order Summary</h1>
<div class="flex justify-between mt-10 mb-5">
<span class="font-semibold text-sm uppercase">Items 3</span>
<span class="font-semibold text-sm">590$</span>
</div>
<div>
<label class="font-medium inline-block mb-3 text-sm uppercase">Shipping</label>
<select class="block p-2 text-white w-full text-sm">
<option>Standard shipping - $10.00</option>
</select>
</div>
<div class="py-10">
<label for="promo" class="font-semibold inline-block mb-3 text-sm uppercase">Promo Code</label>
<input type="text" id="promo" placeholder="Enter your code" class="p-2 text-sm w-full">
</div>
<button class="bg-white hover:bg-gray-600 px-5 py-2 text-sm text-blue-500 uppercase">Apply</button>
<div class="border-t mt-8">
<div class="flex font-semibold justify-between py-6 text-sm uppercase">
<span>Total cost</span>
<span>$600</span>
</div>
<button class="bg-white font-semibold hover:bg-gray-600 py-3 text-sm text-blue-500 uppercase w-full">Checkout</button>
</div>
</div>
</div>
</div>
</div>

View File

@ -14,9 +14,10 @@ use App\Http\Controllers\AccountController;
use App\Http\Controllers\ActivityController;
use App\Http\Controllers\Auth\ForgotPasswordController;
use App\Http\Controllers\Auth\LoginController;
use App\Http\Controllers\Bank\YodleeController;
use App\Http\Controllers\BankIntegrationController;
use App\Http\Controllers\BankTransactionController;
use App\Http\Controllers\BankTransactionRuleController;
use App\Http\Controllers\Bank\YodleeController;
use App\Http\Controllers\BaseController;
use App\Http\Controllers\ChartController;
use App\Http\Controllers\ClientController;
@ -119,6 +120,9 @@ Route::group(['middleware' => ['throttle:300,1', 'api_db', 'token_auth', 'locale
Route::post('bank_transactions/bulk', [BankTransactionController::class, 'bulk'])->name('bank_transactions.bulk');
Route::post('bank_transactions/match', [BankTransactionController::class, 'match'])->name('bank_transactions.match');
Route::resource('bank_transaction_rules', BankTransactionRuleController::class); // name = (clients. index / create / show / update / destroy / edit
Route::post('bank_transaction_rules/bulk', [BankTransactionRuleController::class, 'bulk'])->name('bank_transaction_rules.bulk');
Route::post('check_subdomain', [SubdomainController::class, 'index'])->name('check_subdomain');
Route::get('ping', [PingController::class, 'index'])->name('ping');
Route::get('health_check', [PingController::class, 'health'])->name('health_check');

View File

@ -115,6 +115,8 @@ Route::post('payments/process/response', [App\Http\Controllers\ClientPortal\Paym
Route::get('payments/process/response', [App\Http\Controllers\ClientPortal\PaymentController::class, 'response'])->name('client.payments.response.get')->middleware(['locale', 'domain_db', 'verify_hash']);
Route::get('client/subscriptions/{subscription}/purchase', [App\Http\Controllers\ClientPortal\SubscriptionPurchaseController::class, 'index'])->name('client.subscription.purchase')->middleware('domain_db');
Route::get('client/subscriptions/{subscription}/purchase/v2', [App\Http\Controllers\ClientPortal\SubscriptionPurchaseController::class, 'upgrade'])->name('client.subscription.upgrade')->middleware('domain_db');
Route::group(['middleware' => ['invite_db'], 'prefix' => 'client', 'as' => 'client.'], function () {
/*Invitation catches*/

6
tailwind.config.js vendored
View File

@ -17,5 +17,9 @@ module.exports = {
}
},
variants: {},
plugins: []
plugins: [
require('@tailwindcss/line-clamp'),
require('@tailwindcss/forms')
]
};

View File

@ -0,0 +1,766 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace Tests\Feature\Bank;
use App\Factory\BankIntegrationFactory;
use App\Factory\BankTransactionFactory;
use App\Models\BankIntegration;
use App\Models\BankTransaction;
use App\Models\BankTransactionRule;
use App\Models\Invoice;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Validation\ValidationException;
use Tests\MockAccountData;
use Tests\TestCase;
class BankTransactionRuleTest extends TestCase
{
use DatabaseTransactions;
use MockAccountData;
protected function setUp() :void
{
parent::setUp();
$this->makeTestData();
$this->withoutMiddleware(
ThrottleRequests::class
);
$this->withoutExceptionHandling();
}
public function testValidationContainsRule()
{
//[{"search_key":"description","operator":"contains","value":"hello"}]
$br = BankTransactionRule::factory()->create([
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'matches_on_all' => false,
'auto_convert' => true,
'applies_to' => 'DEBIT',
'client_id' => $this->client->id,
'vendor_id' => $this->vendor->id,
'category_id' =>$this->expense_category->id,
'rules' => [
[
'search_key' => 'description',
'operator' => 'contains',
'value' => 'hello',
]
]
]);
$bi = BankIntegration::factory()->create([
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'account_id' => $this->account->id,
]);
$bt = BankTransaction::factory()->create([
'bank_integration_id' => $bi->id,
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'description' => 'HellO ThErE CowBoY',
'base_type' => 'DEBIT',
'amount' => 100
]);
$bt->service()->processRules();
$bt = $bt->fresh();
$this->assertNotNull($bt->expense_id);
$this->assertNotNull($bt->expense->category_id);
$this->assertNotNull($bt->expense->vendor_id);
}
public function testUpdateValidationRules()
{
$br = BankTransactionRule::factory()->create([
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'matches_on_all' => false,
'auto_convert' => true,
'applies_to' => 'DEBIT',
'client_id' => $this->client->id,
'vendor_id' => $this->vendor->id,
'rules' => [
[
'search_key' => 'amount',
'operator' => '<=',
'value' => 100,
]
]
]);
$data = [
"applies_to" => "DEBIT",
"archived_at" => 0,
"auto_convert" => False,
"category_id" => $this->expense_category->hashed_id,
"is_deleted" => False,
"isChanged" => True,
"matches_on_all" => True,
"name" => "TEST 22",
"updated_at" => 1669060432,
"vendor_id" => $this->vendor->hashed_id
];
$response = null;
try {
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->putJson('/api/v1/bank_transaction_rules/'. $br->hashed_id, $data);
} catch (ValidationException $e) {
$message = json_decode($e->validator->getMessageBag(), 1);
nlog($message);
}
if($response){
$arr = $response->json();
$response->assertStatus(200);
}
}
public function testMatchingBankTransactionExpenseAmountLessThanEqualTo()
{
$br = BankTransactionRule::factory()->create([
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'matches_on_all' => false,
'auto_convert' => true,
'applies_to' => 'DEBIT',
'client_id' => $this->client->id,
'vendor_id' => $this->vendor->id,
'rules' => [
[
'search_key' => 'amount',
'operator' => '<=',
'value' => 100,
]
]
]);
$bi = BankIntegration::factory()->create([
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'account_id' => $this->account->id,
]);
$bt = BankTransaction::factory()->create([
'bank_integration_id' => $bi->id,
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'description' => '',
'base_type' => 'DEBIT',
'amount' => 100
]);
$bt->service()->processRules();
$bt = $bt->fresh();
$this->assertNotNull($bt->expense_id);
}
public function testMatchingBankTransactionExpenseAmountLessThan()
{
$br = BankTransactionRule::factory()->create([
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'matches_on_all' => false,
'auto_convert' => true,
'applies_to' => 'DEBIT',
'client_id' => $this->client->id,
'vendor_id' => $this->vendor->id,
'rules' => [
[
'search_key' => 'amount',
'operator' => '<',
'value' => 100,
]
]
]);
$bi = BankIntegration::factory()->create([
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'account_id' => $this->account->id,
]);
$bt = BankTransaction::factory()->create([
'bank_integration_id' => $bi->id,
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'description' => '',
'base_type' => 'DEBIT',
'amount' => 99
]);
$bt->service()->processRules();
$bt = $bt->fresh();
$this->assertNotNull($bt->expense_id);
}
public function testMatchingBankTransactionExpenseAmountGreaterThan()
{
$br = BankTransactionRule::factory()->create([
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'matches_on_all' => false,
'auto_convert' => true,
'applies_to' => 'DEBIT',
'client_id' => $this->client->id,
'vendor_id' => $this->vendor->id,
'rules' => [
[
'search_key' => 'amount',
'operator' => '>',
'value' => 100,
]
]
]);
$bi = BankIntegration::factory()->create([
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'account_id' => $this->account->id,
]);
$bt = BankTransaction::factory()->create([
'bank_integration_id' => $bi->id,
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'description' => '',
'base_type' => 'DEBIT',
'amount' => 101
]);
$bt->service()->processRules();
$bt = $bt->fresh();
$this->assertNotNull($bt->expense_id);
}
public function testMatchingBankTransactionExpenseAmountMiss()
{
$br = BankTransactionRule::factory()->create([
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'matches_on_all' => false,
'auto_convert' => true,
'applies_to' => 'DEBIT',
'client_id' => $this->client->id,
'vendor_id' => $this->vendor->id,
'rules' => [
[
'search_key' => 'amount',
'operator' => '=',
'value' => 100,
]
]
]);
$bi = BankIntegration::factory()->create([
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'account_id' => $this->account->id,
]);
$bt = BankTransaction::factory()->create([
'bank_integration_id' => $bi->id,
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'description' => '',
'base_type' => 'DEBIT',
'amount' => 101
]);
$bt->service()->processRules();
$bt = $bt->fresh();
$this->assertNull($bt->expense_id);
}
public function testMatchingBankTransactionExpenseAmount()
{
$br = BankTransactionRule::factory()->create([
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'matches_on_all' => false,
'auto_convert' => true,
'applies_to' => 'DEBIT',
'client_id' => $this->client->id,
'vendor_id' => $this->vendor->id,
'rules' => [
[
'search_key' => 'amount',
'operator' => '=',
'value' => 100,
]
]
]);
$bi = BankIntegration::factory()->create([
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'account_id' => $this->account->id,
]);
$bt = BankTransaction::factory()->create([
'bank_integration_id' => $bi->id,
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'description' => '',
'base_type' => 'DEBIT',
'amount' => 100
]);
$bt->service()->processRules();
$bt = $bt->fresh();
$this->assertNotNull($bt->expense_id);
}
public function testMatchingBankTransactionExpenseIsEmpty()
{
$br = BankTransactionRule::factory()->create([
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'matches_on_all' => false,
'auto_convert' => true,
'applies_to' => 'DEBIT',
'client_id' => $this->client->id,
'vendor_id' => $this->vendor->id,
'rules' => [
[
'search_key' => 'description',
'operator' => 'is_empty',
'value' => '',
]
]
]);
$bi = BankIntegration::factory()->create([
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'account_id' => $this->account->id,
]);
$bt = BankTransaction::factory()->create([
'bank_integration_id' => $bi->id,
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'description' => '',
'base_type' => 'DEBIT',
'amount' => 100
]);
$bt->service()->processRules();
$bt = $bt->fresh();
$this->assertNotNull($bt->expense_id);
}
public function testMatchingBankTransactionExpenseIsEmptyMiss()
{
$br = BankTransactionRule::factory()->create([
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'matches_on_all' => false,
'auto_convert' => true,
'applies_to' => 'DEBIT',
'client_id' => $this->client->id,
'vendor_id' => $this->vendor->id,
'rules' => [
[
'search_key' => 'description',
'operator' => 'is_empty',
'value' => '',
]
]
]);
$bi = BankIntegration::factory()->create([
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'account_id' => $this->account->id,
]);
$bt = BankTransaction::factory()->create([
'bank_integration_id' => $bi->id,
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'description' => 'asdadsa',
'base_type' => 'DEBIT',
'amount' => 100
]);
$bt->service()->processRules();
$bt = $bt->fresh();
$this->assertNull($bt->expense_id);
}
public function testMatchingBankTransactionExpenseStartsWithMiss()
{
$br = BankTransactionRule::factory()->create([
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'matches_on_all' => false,
'auto_convert' => true,
'applies_to' => 'DEBIT',
'client_id' => $this->client->id,
'vendor_id' => $this->vendor->id,
'rules' => [
[
'search_key' => 'description',
'operator' => 'starts_with',
'value' => 'chesst',
]
]
]);
$bi = BankIntegration::factory()->create([
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'account_id' => $this->account->id,
]);
$bt = BankTransaction::factory()->create([
'bank_integration_id' => $bi->id,
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'description' => 'ChESSSty coughs are terrible',
'base_type' => 'DEBIT',
'amount' => 100
]);
$bt->service()->processRules();
$bt = $bt->fresh();
$this->assertNull($bt->expense_id);
}
public function testMatchingBankTransactionExpenseStartsWith()
{
$br = BankTransactionRule::factory()->create([
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'matches_on_all' => false,
'auto_convert' => true,
'applies_to' => 'DEBIT',
'client_id' => $this->client->id,
'vendor_id' => $this->vendor->id,
'rules' => [
[
'search_key' => 'description',
'operator' => 'starts_with',
'value' => 'chess',
]
]
]);
$bi = BankIntegration::factory()->create([
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'account_id' => $this->account->id,
]);
$bt = BankTransaction::factory()->create([
'bank_integration_id' => $bi->id,
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'description' => 'ChESSSty coughs are terrible',
'base_type' => 'DEBIT',
'amount' => 100
]);
$bt->service()->processRules();
$bt = $bt->fresh();
$this->assertNotNull($bt->expense_id);
}
public function testMatchingBankTransactionExpenseContainsMiss()
{
$br = BankTransactionRule::factory()->create([
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'matches_on_all' => false,
'auto_convert' => true,
'applies_to' => 'DEBIT',
'client_id' => $this->client->id,
'vendor_id' => $this->vendor->id,
'rules' => [
[
'search_key' => 'description',
'operator' => 'contains',
'value' => 'asdddfd',
]
]
]);
$bi = BankIntegration::factory()->create([
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'account_id' => $this->account->id,
]);
$bt = BankTransaction::factory()->create([
'bank_integration_id' => $bi->id,
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'description' => 'Something asd bizarre',
'base_type' => 'DEBIT',
'amount' => 100
]);
$bt->service()->processRules();
$bt = $bt->fresh();
$this->assertNull($bt->expense_id);
}
public function testMatchingBankTransactionExpenseContains()
{
$br = BankTransactionRule::factory()->create([
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'matches_on_all' => false,
'auto_convert' => true,
'applies_to' => 'DEBIT',
'client_id' => $this->client->id,
'vendor_id' => $this->vendor->id,
'rules' => [
[
'search_key' => 'description',
'operator' => 'contains',
'value' => 'asd',
]
]
]);
$bi = BankIntegration::factory()->create([
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'account_id' => $this->account->id,
]);
$bt = BankTransaction::factory()->create([
'bank_integration_id' => $bi->id,
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'description' => 'Something asd bizarre',
'base_type' => 'DEBIT',
'amount' => 100
]);
$bt->service()->processRules();
$bt = $bt->fresh();
$this->assertNotNull($bt->expense_id);
}
public function testMatchingBankTransactionExpenseMiss()
{
$br = BankTransactionRule::factory()->create([
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'matches_on_all' => false,
'auto_convert' => true,
'applies_to' => 'DEBIT',
'client_id' => $this->client->id,
'vendor_id' => $this->vendor->id,
'rules' => [
[
'search_key' => 'description',
'operator' => 'is',
'value' => 'wallaby',
]
]
]);
$bi = BankIntegration::factory()->create([
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'account_id' => $this->account->id,
]);
$bt = BankTransaction::factory()->create([
'bank_integration_id' => $bi->id,
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'description' => 'Wall',
'base_type' => 'DEBIT',
]);
$bt->service()->processRules();
$bt = $bt->fresh();
$this->assertNull($bt->expense_id);
}
public function testMatchingBankTransactionExpense()
{
$br = BankTransactionRule::factory()->create([
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'matches_on_all' => false,
'auto_convert' => true,
'applies_to' => 'DEBIT',
'client_id' => $this->client->id,
'vendor_id' => $this->vendor->id,
'rules' => [
[
'search_key' => 'description',
'operator' => 'is',
'value' => 'wallaby',
]
]
]);
$bi = BankIntegration::factory()->create([
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'account_id' => $this->account->id,
]);
$bt = BankTransaction::factory()->create([
'bank_integration_id' => $bi->id,
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'description' => 'WallABy',
'base_type' => 'DEBIT',
]);
$bt->service()->processRules();
$bt = $bt->fresh();
$this->assertNotNull($bt->expense_id);
}
public function testMatchingBankTransactionInvoice()
{
$this->invoice->number = "MUHMUH";
$this->invoice->save();
$br = BankTransactionRule::factory()->create([
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'matches_on_all' => false,
'auto_convert' => true,
'applies_to' => 'CREDIT',
'client_id' => $this->client->id,
'vendor_id' => $this->vendor->id,
'rules' => [
[
'search_key' => 'description',
'operator' => 'is',
'value' => 'MUHMUH',
]
]
]);
$bi = BankIntegration::factory()->create([
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'account_id' => $this->account->id,
]);
$bt = BankTransaction::factory()->create([
'bank_integration_id' => $bi->id,
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'description' => 'MUHMUH',
'base_type' => 'CREDIT',
'amount' => 100
]);
$bt->service()->processRules();
$bt = $bt->fresh();
$this->assertEquals(BankTransaction::STATUS_MATCHED, $bt->status_id);
}
}

View File

@ -0,0 +1,230 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace Tests\Feature;
use App\Utils\Traits\MakesHash;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Session;
use Illuminate\Validation\ValidationException;
use Tests\MockAccountData;
use Tests\TestCase;
/**
* @test
* @covers App\Http\Controllers\BankTransactionRuleController
*/
class BankTransactionRuleApiTest extends TestCase
{
use MakesHash;
use DatabaseTransactions;
use MockAccountData;
protected function setUp() :void
{
parent::setUp();
$this->makeTestData();
Session::start();
$this->faker = \Faker\Factory::create();
Model::reguard();
}
/*
$rules = [
'name' => 'bail|required|string',
'rules' => 'bail|array',
'auto_convert' => 'bail|sometimes|bool',
'matches_on_all' => 'bail|sometimes|bool',
'applies_to' => 'bail|sometimes|bool',
];
if(isset($this->category_id))
$rules['category_id'] = 'bail|sometimes|exists:expense_categories,id,'.auth()->user()->company()->id.',is_deleted,0';
if(isset($this->vendor_id))
$rules['vendor_id'] = 'bail|sometimes|exists:vendors,id,company_id,'.auth()->user()->company()->id.',is_deleted,0';
if(isset($this->client_id))
$rules['client_id'] = 'bail|sometimes|exists:clients,id,company_id,'.auth()->user()->company()->id.',is_deleted,0';
*/
public function testBankRuleCategoryIdValidation()
{
$data = [
'name' => 'The First Rule',
'rules' => [
[
"operator" => "contains",
"search_key" => "description",
"value" => "mobile"
],
],
'assigned_user_id' => null,
'auto_convert' => false,
'matches_on_all' => true,
'applies_to' => 'DEBIT',
'category_id' => $this->expense_category->hashed_id,
'vendor_id' => $this->vendor->hashed_id
];
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->postJson('/api/v1/bank_transaction_rules/', $data);
$arr = $response->json();
$response->assertStatus(200);
$this->assertEquals('DEBIT', $arr['data']['applies_to']);
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->putJson('/api/v1/bank_transaction_rules/'. $arr['data']['id'], $data);
$arr = $response->json();
$response->assertStatus(200);
$this->assertEquals('DEBIT', $arr['data']['applies_to']);
}
public function testBankRulePost()
{
$data = [
'name' => 'The First Rule',
'rules' => [],
'auto_convert' => false,
'matches_on_all' => false,
'applies_to' => 'CREDIT',
];
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->postJson('/api/v1/bank_transaction_rules/', $data);
$arr = $response->json();
$response->assertStatus(200);
$this->assertEquals('The First Rule', $arr['data']['name']);
}
public function testBankRulePut()
{
$data = [
'name' => 'The First Rule',
'rules' => [],
'auto_convert' => false,
'matches_on_all' => false,
'applies_to' => 'CREDIT',
];
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->postJson('/api/v1/bank_transaction_rules/', $data);
$arr = $response->json();
$response->assertStatus(200);
$this->assertEquals('The First Rule', $arr['data']['name']);
$data = [
'name' => 'A New Name For The First Rule',
'rules' => [],
'auto_convert' => false,
'matches_on_all' => false,
'applies_to' => 'CREDIT',
];
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->putJson('/api/v1/bank_transaction_rules/'. $arr['data']['id'], $data);
$arr = $response->json();
$response->assertStatus(200);
$this->assertEquals('A New Name For The First Rule', $arr['data']['name']);
}
public function testBankTransactionRuleGet()
{
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->get('/api/v1/bank_transaction_rules/'.$this->encodePrimaryKey($this->bank_transaction_rule->id));
$response->assertStatus(200);
}
public function testBankTransactionRuleArchived()
{
$data = [
'ids' => [$this->encodePrimaryKey($this->bank_transaction_rule->id)],
];
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->post('/api/v1/bank_transaction_rules/bulk?action=archive', $data);
$arr = $response->json();
$this->assertNotNull($arr['data'][0]['archived_at']);
}
public function testBankTransactionRuleRestored()
{
$data = [
'ids' => [$this->encodePrimaryKey($this->bank_transaction_rule->id)],
];
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->post('/api/v1/bank_transaction_rules/bulk?action=restore', $data);
$arr = $response->json();
$this->assertEquals(0, $arr['data'][0]['archived_at']);
}
public function testBankTransactionRuleDeleted()
{
$data = [
'ids' => [$this->encodePrimaryKey($this->bank_transaction_rule->id)],
];
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->post('/api/v1/bank_transaction_rules/bulk?action=delete', $data);
$arr = $response->json();
$this->assertTrue($arr['data'][0]['is_deleted']);
}
}

View File

@ -83,7 +83,7 @@ class SubscriptionApiTest extends TestCase
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->post('/api/v1/subscriptions', ['product_ids' => $product->id, 'allow_cancellation' => true, 'name' => Str::random(5)]);
])->post('/api/v1/subscriptions', ['product_ids' => $product->hashed_id, 'allow_cancellation' => true, 'name' => Str::random(5)]);
// nlog($response);
$response->assertStatus(200);
@ -98,7 +98,7 @@ class SubscriptionApiTest extends TestCase
$response1 = $this
->withHeaders(['X-API-SECRET' => config('ninja.api_secret'), 'X-API-TOKEN' => $this->token])
->post('/api/v1/subscriptions', ['product_ids' => $product->id, 'name' => Str::random(5)])
->post('/api/v1/subscriptions', ['product_ids' => $product->hashed_id, 'name' => Str::random(5)])
->assertStatus(200)
->json();

View File

@ -11,6 +11,7 @@
namespace Tests\Feature;
use App\Models\Task;
use App\Utils\Traits\MakesHash;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Testing\DatabaseTransactions;
@ -42,6 +43,90 @@ class TaskApiTest extends TestCase
Model::reguard();
}
public function testTaskLockingGate()
{
$data = [
'timelog' => [[1,2],[3,4]],
];
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->post('/api/v1/tasks', $data);
$arr = $response->json();
$response->assertStatus(200);
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->putJson('/api/v1/tasks/' . $arr['data']['id'], $data);
$arr = $response->json();
$response->assertStatus(200);
$task = Task::find($this->decodePrimaryKey($arr['data']['id']));
$task->invoice_id = $this->invoice->id;
$task->save();
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->putJson('/api/v1/tasks/' . $arr['data']['id'], $data);
$arr = $response->json();
$response->assertStatus(200);
$task = Task::find($this->decodePrimaryKey($arr['data']['id']));
$task->invoice_lock =true;
$task->invoice_id = $this->invoice->id;
$task->save();
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->putJson('/api/v1/tasks/' . $arr['data']['id'], $data);
$arr = $response->json();
$response->assertStatus(401);
}
public function testTaskLocking()
{
$data = [
'timelog' => [[1,2],[3,4]],
'invoice_lock' => true
];
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->post('/api/v1/tasks', $data);
$arr = $response->json();
$response->assertStatus(200);
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->putJson('/api/v1/tasks/' . $arr['data']['id'], $data);
$arr = $response->json();
$response->assertStatus(200);
}
public function testTimeLogValidation()
{
$data = [
@ -75,9 +160,10 @@ class TaskApiTest extends TestCase
$arr = $response->json();
$response->assertStatus(200);
}
public function testTimeLogValidation2()
{
$data = [

View File

@ -26,6 +26,7 @@ use App\Jobs\Company\CreateCompanyTaskStatuses;
use App\Models\Account;
use App\Models\BankIntegration;
use App\Models\BankTransaction;
use App\Models\BankTransactionRule;
use App\Models\Client;
use App\Models\ClientContact;
use App\Models\Company;
@ -153,6 +154,11 @@ trait MockAccountData
*/
public $bank_transaction;
/**
* @var
*/
public $bank_transaction_rule;
/**
* @var
*/
@ -572,6 +578,11 @@ trait MockAccountData
'bank_integration_id' => $this->bank_integration->id,
]);
$this->bank_transaction_rule = BankTransactionRule::factory()->create([
'user_id' => $user_id,
'company_id' => $this->company->id,
]);
$invitations = CreditInvitation::whereCompanyId($this->credit->company_id)
->whereCreditId($this->credit->id);