mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2024-11-14 07:02:34 +01:00
Minor fixes for client ledger balance update
This commit is contained in:
commit
3ce3187ec2
@ -71,7 +71,7 @@ class Kernel extends ConsoleKernel
|
|||||||
$schedule->job(new RecurringInvoicesCron)->hourly()->withoutOverlapping()->name('recurring-invoice-job')->onOneServer();
|
$schedule->job(new RecurringInvoicesCron)->hourly()->withoutOverlapping()->name('recurring-invoice-job')->onOneServer();
|
||||||
|
|
||||||
/* Stale Invoice Cleanup*/
|
/* Stale Invoice Cleanup*/
|
||||||
$schedule->job(new CleanStaleInvoiceOrder)->hourly()->withoutOverlapping()->name('stale-invoice-job')->onOneServer();
|
$schedule->job(new CleanStaleInvoiceOrder)->hourlyAt(30)->withoutOverlapping()->name('stale-invoice-job')->onOneServer();
|
||||||
|
|
||||||
/* Sends recurring invoices*/
|
/* Sends recurring invoices*/
|
||||||
$schedule->job(new RecurringExpensesCron)->dailyAt('00:10')->withoutOverlapping()->name('recurring-expense-job')->onOneServer();
|
$schedule->job(new RecurringExpensesCron)->dailyAt('00:10')->withoutOverlapping()->name('recurring-expense-job')->onOneServer();
|
||||||
@ -89,7 +89,7 @@ class Kernel extends ConsoleKernel
|
|||||||
$schedule->job(new SchedulerCheck)->dailyAt('01:10')->withoutOverlapping();
|
$schedule->job(new SchedulerCheck)->dailyAt('01:10')->withoutOverlapping();
|
||||||
|
|
||||||
/* Checks for scheduled tasks */
|
/* Checks for scheduled tasks */
|
||||||
$schedule->job(new TaskScheduler())->dailyAt('06:50')->withoutOverlapping()->name('task-scheduler-job')->onOneServer();
|
$schedule->job(new TaskScheduler())->hourlyAt(10)->withoutOverlapping()->name('task-scheduler-job')->onOneServer();
|
||||||
|
|
||||||
/* Performs system maintenance such as pruning the backup table */
|
/* Performs system maintenance such as pruning the backup table */
|
||||||
$schedule->job(new SystemMaintenance)->sundays()->at('02:30')->withoutOverlapping()->name('system-maintenance-job')->onOneServer();
|
$schedule->job(new SystemMaintenance)->sundays()->at('02:30')->withoutOverlapping()->name('system-maintenance-job')->onOneServer();
|
||||||
|
@ -447,7 +447,13 @@ class CompanySettings extends BaseSettings
|
|||||||
|
|
||||||
public $mailgun_domain = '';
|
public $mailgun_domain = '';
|
||||||
|
|
||||||
|
public $auto_bill_standard_invoices = false;
|
||||||
|
|
||||||
|
public $email_alignment = 'center'; // center , left, right
|
||||||
|
|
||||||
public static $casts = [
|
public static $casts = [
|
||||||
|
'email_alignment' => 'string',
|
||||||
|
'auto_bill_standard_invoices' => 'bool',
|
||||||
'postmark_secret' => 'string',
|
'postmark_secret' => 'string',
|
||||||
'mailgun_secret' => 'string',
|
'mailgun_secret' => 'string',
|
||||||
'mailgun_domain' => 'string',
|
'mailgun_domain' => 'string',
|
||||||
|
@ -235,12 +235,17 @@ class EmailTemplateDefaults
|
|||||||
|
|
||||||
public static function emailStatementSubject()
|
public static function emailStatementSubject()
|
||||||
{
|
{
|
||||||
return '';
|
return ctrans('texts.your_statement');
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function emailStatementTemplate()
|
public static function emailStatementTemplate()
|
||||||
{
|
{
|
||||||
return '';
|
|
||||||
|
$statement_message = '<p>$client<br><br>'.self::transformText('client_statement_body').'<br></p>';
|
||||||
|
|
||||||
|
return $statement_message;
|
||||||
|
|
||||||
|
// return ctrans('texts.client_statement_body', ['start_date' => '$start_date', 'end_date' => '$end_date']);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function transformText($string)
|
private static function transformText($string)
|
||||||
|
96
app/DataMapper/Schedule/ClientStatement.php
Normal file
96
app/DataMapper/Schedule/ClientStatement.php
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
<?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\DataMapper;
|
||||||
|
|
||||||
|
use App\Models\Client;
|
||||||
|
use stdClass;
|
||||||
|
|
||||||
|
class ClientStatement
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the template name
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public string $template = 'client_statement';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An array of clients hashed_ids
|
||||||
|
*
|
||||||
|
* Leave blank if this action should apply to all clients
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
public array $clients = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The consts to be used to define the date_range variable of the statement
|
||||||
|
*/
|
||||||
|
public const THIS_MONTH = 'this_month';
|
||||||
|
public const THIS_QUARTER = 'this_quarter';
|
||||||
|
public const THIS_YEAR = 'this_year';
|
||||||
|
public const PREVIOUS_MONTH = 'previous_month';
|
||||||
|
public const PREVIOUS_QUARTER = 'previous_quarter';
|
||||||
|
public const PREVIOUS_YEAR = 'previous_year';
|
||||||
|
public const CUSTOM_RANGE = "custom_range";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The date range the statement should include
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public string $date_range = 'this_month';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If a custom range is select for the date range then
|
||||||
|
* the start_date should be supplied in Y-m-d format
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public string $start_date = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If a custom range is select for the date range then
|
||||||
|
* the end_date should be supplied in Y-m-d format
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public string $end_date = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flag which allows the payment table
|
||||||
|
* to be shown
|
||||||
|
*
|
||||||
|
* @var boolean
|
||||||
|
*/
|
||||||
|
public bool $show_payments_table = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flag which allows the aging table
|
||||||
|
* to be shown
|
||||||
|
*
|
||||||
|
* @var boolean
|
||||||
|
*/
|
||||||
|
public bool $show_aging_table = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* String const which defines whether
|
||||||
|
* the invoices to be shown are either
|
||||||
|
* paid or unpaid
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public string $status = 'paid'; // paid | unpaid
|
||||||
|
|
||||||
|
|
||||||
|
}
|
32
app/Factory/SchedulerFactory.php
Normal file
32
app/Factory/SchedulerFactory.php
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<?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\Scheduler;
|
||||||
|
|
||||||
|
class SchedulerFactory
|
||||||
|
{
|
||||||
|
public static function create($company_id, $user_id) :Scheduler
|
||||||
|
{
|
||||||
|
$scheduler = new Scheduler;
|
||||||
|
|
||||||
|
$scheduler->name = '';
|
||||||
|
$scheduler->company_id = $company_id;
|
||||||
|
$scheduler->user_id = $user_id;
|
||||||
|
$scheduler->parameters = [];
|
||||||
|
$scheduler->is_paused = false;
|
||||||
|
$scheduler->is_deleted = false;
|
||||||
|
$scheduler->template = '';
|
||||||
|
|
||||||
|
return $scheduler;
|
||||||
|
}
|
||||||
|
}
|
@ -87,10 +87,10 @@ class SwissQrGenerator
|
|||||||
$qrBill->setUltimateDebtor(
|
$qrBill->setUltimateDebtor(
|
||||||
QrBill\DataGroup\Element\StructuredAddress::createWithStreet(
|
QrBill\DataGroup\Element\StructuredAddress::createWithStreet(
|
||||||
substr($this->client->present()->name(), 0 , 70),
|
substr($this->client->present()->name(), 0 , 70),
|
||||||
$this->client->address1 ? substr($this->client->address1, 0 , 70) : '_',
|
$this->client->address1 ? substr($this->client->address1, 0 , 70) : ' ',
|
||||||
$this->client->address2 ? substr($this->client->address2, 0 , 16) : '_',
|
$this->client->address2 ? substr($this->client->address2, 0 , 16) : ' ',
|
||||||
$this->client->postal_code ? substr($this->client->postal_code, 0, 16) : '_',
|
$this->client->postal_code ? substr($this->client->postal_code, 0, 16) : ' ',
|
||||||
$this->client->city ? substr($this->client->city, 0, 35) : '_',
|
$this->client->city ? substr($this->client->city, 0, 35) : ' ',
|
||||||
'CH'
|
'CH'
|
||||||
));
|
));
|
||||||
|
|
||||||
|
@ -984,6 +984,9 @@ class BaseController extends Controller
|
|||||||
//pass report errors bool to front end
|
//pass report errors bool to front end
|
||||||
$data['report_errors'] = Ninja::isSelfHost() ? $account->report_errors : true;
|
$data['report_errors'] = Ninja::isSelfHost() ? $account->report_errors : true;
|
||||||
|
|
||||||
|
//pass whitelabel bool to front end
|
||||||
|
$data['white_label'] = Ninja::isSelfHost() ? $account->isPaid() : false;
|
||||||
|
|
||||||
//pass referral code to front end
|
//pass referral code to front end
|
||||||
$data['rc'] = request()->has('rc') ? request()->input('rc') : '';
|
$data['rc'] = request()->has('rc') ? request()->input('rc') : '';
|
||||||
$data['build'] = request()->has('build') ? request()->input('build') : '';
|
$data['build'] = request()->has('build') ? request()->input('build') : '';
|
||||||
|
@ -502,7 +502,6 @@ class PurchaseOrderController extends BaseController
|
|||||||
/*
|
/*
|
||||||
* Download Purchase Order/s
|
* Download Purchase Order/s
|
||||||
*/
|
*/
|
||||||
|
|
||||||
if ($action == 'bulk_download' && $purchase_orders->count() >= 1) {
|
if ($action == 'bulk_download' && $purchase_orders->count() >= 1) {
|
||||||
$purchase_orders->each(function ($purchase_order) {
|
$purchase_orders->each(function ($purchase_order) {
|
||||||
if (auth()->user()->cannot('view', $purchase_order)) {
|
if (auth()->user()->cannot('view', $purchase_order)) {
|
||||||
|
@ -1,4 +1,13 @@
|
|||||||
<?php
|
<?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;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
@ -11,37 +11,40 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
use App\Http\Requests\TaskScheduler\CreateScheduledTaskRequest;
|
use App\Factory\SchedulerFactory;
|
||||||
use App\Http\Requests\TaskScheduler\UpdateScheduleRequest;
|
use App\Http\Requests\TaskScheduler\CreateSchedulerRequest;
|
||||||
|
use App\Http\Requests\TaskScheduler\ShowSchedulerRequest;
|
||||||
|
use App\Http\Requests\TaskScheduler\StoreSchedulerRequest;
|
||||||
|
use App\Http\Requests\TaskScheduler\UpdateSchedulerRequest;
|
||||||
|
use App\Http\Requests\Task\DestroySchedulerRequest;
|
||||||
use App\Jobs\Ninja\TaskScheduler;
|
use App\Jobs\Ninja\TaskScheduler;
|
||||||
use App\Jobs\Report\ProfitAndLoss;
|
use App\Jobs\Report\ProfitAndLoss;
|
||||||
use App\Models\Scheduler;
|
use App\Models\Scheduler;
|
||||||
use App\Repositories\TaskSchedulerRepository;
|
use App\Repositories\SchedulerRepository;
|
||||||
use App\Transformers\TaskSchedulerTransformer;
|
use App\Transformers\SchedulerTransformer;
|
||||||
|
use App\Utils\Traits\MakesHash;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
|
||||||
class TaskSchedulerController extends BaseController
|
class TaskSchedulerController extends BaseController
|
||||||
{
|
{
|
||||||
|
use MakesHash;
|
||||||
|
|
||||||
protected $entity_type = Scheduler::class;
|
protected $entity_type = Scheduler::class;
|
||||||
|
|
||||||
protected $entity_transformer = TaskSchedulerTransformer::class;
|
protected $entity_transformer = SchedulerTransformer::class;
|
||||||
|
|
||||||
protected TaskSchedulerRepository $scheduler_repository;
|
public function __construct(protected SchedulerRepository $scheduler_repository)
|
||||||
|
|
||||||
public function __construct(TaskSchedulerRepository $scheduler_repository)
|
|
||||||
{
|
{
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
|
|
||||||
$this->scheduler_repository = $scheduler_repository;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @OA\GET(
|
* @OA\GET(
|
||||||
* path="/api/v1/task_scheduler/",
|
* path="/api/v1/task_schedulers/",
|
||||||
* operationId="getTaskSchedulers",
|
* operationId="getTaskSchedulers",
|
||||||
* tags={"task_scheduler"},
|
* tags={"task_schedulers"},
|
||||||
* summary="Task Scheduler Index",
|
* summary="Task Scheduler Index",
|
||||||
* description="Get all schedulers with associated jobs",
|
* description="Get all schedulers with associated jobs",
|
||||||
* @OA\Parameter(ref="#/components/parameters/X-Api-Secret"),
|
* @OA\Parameter(ref="#/components/parameters/X-Api-Secret"),
|
||||||
@ -67,11 +70,57 @@ class TaskSchedulerController extends BaseController
|
|||||||
return $this->listResponse($schedulers);
|
return $this->listResponse($schedulers);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the form for creating a new resource.
|
||||||
|
*
|
||||||
|
* @param CreateSchedulerRequest $request The request
|
||||||
|
*
|
||||||
|
* @return Response
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @OA\Get(
|
||||||
|
* path="/api/v1/invoices/task_schedulers",
|
||||||
|
* operationId="getTaskScheduler",
|
||||||
|
* tags={"task_schedulers"},
|
||||||
|
* summary="Gets a new blank scheduler 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 scheduler 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/TaskSchedulerSchema"),
|
||||||
|
* ),
|
||||||
|
* @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(CreateSchedulerRequest $request)
|
||||||
|
{
|
||||||
|
$scheduler = SchedulerFactory::create(auth()->user()->company()->id, auth()->user()->id);
|
||||||
|
|
||||||
|
return $this->itemResponse($scheduler);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @OA\Post(
|
* @OA\Post(
|
||||||
* path="/api/v1/task_scheduler/",
|
* path="/api/v1/task_schedulers/",
|
||||||
* operationId="createTaskScheduler",
|
* operationId="createTaskScheduler",
|
||||||
* tags={"task_scheduler"},
|
* tags={"task_schedulers"},
|
||||||
* summary="Create task scheduler with job ",
|
* summary="Create task scheduler with job ",
|
||||||
* description="Create task scheduler with a job (action(job) request should be sent via request also. Example: We want client report to be job which will be run
|
* description="Create task scheduler with a job (action(job) request should be sent via request also. Example: We want client report to be job which will be run
|
||||||
* multiple times, we should send the same parameters in the request as we would send if we wanted to get report, see example",
|
* multiple times, we should send the same parameters in the request as we would send if we wanted to get report, see example",
|
||||||
@ -100,19 +149,18 @@ class TaskSchedulerController extends BaseController
|
|||||||
* ),
|
* ),
|
||||||
* )
|
* )
|
||||||
*/
|
*/
|
||||||
public function store(CreateScheduledTaskRequest $request)
|
public function store(StoreSchedulerRequest $request)
|
||||||
{
|
{
|
||||||
$scheduler = new Scheduler();
|
$scheduler = $this->scheduler_repository->save($request->all(), SchedulerFactory::create(auth()->user()->company()->id, auth()->user()->id));
|
||||||
$scheduler->service()->store($scheduler, $request);
|
|
||||||
|
|
||||||
return $this->itemResponse($scheduler);
|
return $this->itemResponse($scheduler);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @OA\GET(
|
* @OA\GET(
|
||||||
* path="/api/v1/task_scheduler/{id}",
|
* path="/api/v1/task_schedulers/{id}",
|
||||||
* operationId="showTaskScheduler",
|
* operationId="showTaskScheduler",
|
||||||
* tags={"task_scheduler"},
|
* tags={"task_schedulers"},
|
||||||
* summary="Show given scheduler",
|
* summary="Show given scheduler",
|
||||||
* description="Get scheduler with associated job",
|
* description="Get scheduler with associated job",
|
||||||
* @OA\Parameter(ref="#/components/parameters/X-Api-Secret"),
|
* @OA\Parameter(ref="#/components/parameters/X-Api-Secret"),
|
||||||
@ -142,16 +190,16 @@ class TaskSchedulerController extends BaseController
|
|||||||
* ),
|
* ),
|
||||||
* )
|
* )
|
||||||
*/
|
*/
|
||||||
public function show(Scheduler $scheduler)
|
public function show(ShowSchedulerRequest $request, Scheduler $scheduler)
|
||||||
{
|
{
|
||||||
return $this->itemResponse($scheduler);
|
return $this->itemResponse($scheduler);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @OA\PUT(
|
* @OA\PUT(
|
||||||
* path="/api/v1/task_scheduler/{id}",
|
* path="/api/v1/task_schedulers/{id}",
|
||||||
* operationId="updateTaskScheduler",
|
* operationId="updateTaskScheduler",
|
||||||
* tags={"task_scheduler"},
|
* tags={"task_schedulers"},
|
||||||
* summary="Update task scheduler ",
|
* summary="Update task scheduler ",
|
||||||
* description="Update task scheduler",
|
* description="Update task scheduler",
|
||||||
* @OA\Parameter(ref="#/components/parameters/X-Api-Secret"),
|
* @OA\Parameter(ref="#/components/parameters/X-Api-Secret"),
|
||||||
@ -168,7 +216,7 @@ class TaskSchedulerController extends BaseController
|
|||||||
* ),
|
* ),
|
||||||
* ), * @OA\RequestBody(
|
* ), * @OA\RequestBody(
|
||||||
* required=true,
|
* required=true,
|
||||||
* @OA\JsonContent(ref="#/components/schemas/UpdateTaskSchedulerSchema")
|
* @OA\JsonContent(ref="#/components/schemas/TaskSchedulerSchema")
|
||||||
* ),
|
* ),
|
||||||
* @OA\Response(
|
* @OA\Response(
|
||||||
* response=200,
|
* response=200,
|
||||||
@ -189,18 +237,18 @@ class TaskSchedulerController extends BaseController
|
|||||||
* ),
|
* ),
|
||||||
* )
|
* )
|
||||||
*/
|
*/
|
||||||
public function update(Scheduler $scheduler, UpdateScheduleRequest $request)
|
public function update(UpdateSchedulerRequest $request, Scheduler $scheduler)
|
||||||
{
|
{
|
||||||
$scheduler->service()->update($scheduler, $request);
|
$this->scheduler_repository->save($request->all(), $scheduler);
|
||||||
|
|
||||||
return $this->itemResponse($scheduler);
|
return $this->itemResponse($scheduler);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @OA\DELETE(
|
* @OA\DELETE(
|
||||||
* path="/api/v1/task_scheduler/{id}",
|
* path="/api/v1/task_schedulers/{id}",
|
||||||
* operationId="destroyTaskScheduler",
|
* operationId="destroyTaskScheduler",
|
||||||
* tags={"task_scheduler"},
|
* tags={"task_schedulers"},
|
||||||
* summary="Destroy Task Scheduler",
|
* summary="Destroy Task Scheduler",
|
||||||
* description="Destroy task scheduler and its associated job",
|
* description="Destroy task scheduler and its associated job",
|
||||||
* @OA\Parameter(ref="#/components/parameters/X-Api-Secret"),
|
* @OA\Parameter(ref="#/components/parameters/X-Api-Secret"),
|
||||||
@ -230,10 +278,83 @@ class TaskSchedulerController extends BaseController
|
|||||||
* ),
|
* ),
|
||||||
* )
|
* )
|
||||||
*/
|
*/
|
||||||
public function destroy(Scheduler $scheduler)
|
public function destroy(DestroySchedulerRequest $request, Scheduler $scheduler)
|
||||||
{
|
{
|
||||||
$this->scheduler_repository->delete($scheduler);
|
$this->scheduler_repository->delete($scheduler);
|
||||||
|
|
||||||
return $this->itemResponse($scheduler->fresh());
|
return $this->itemResponse($scheduler->fresh());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform bulk actions on the list view.
|
||||||
|
*
|
||||||
|
* @return Response
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @OA\Post(
|
||||||
|
* path="/api/v1/task_schedulers/bulk",
|
||||||
|
* operationId="bulkTaskSchedulerActions",
|
||||||
|
* tags={"task_schedulers"},
|
||||||
|
* summary="Performs bulk actions on an array of task_schedulers",
|
||||||
|
* 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="array of ids",
|
||||||
|
* 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 TaskSchedule response",
|
||||||
|
* @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"),
|
||||||
|
* @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"),
|
||||||
|
* @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"),
|
||||||
|
* @OA\JsonContent(ref="#/components/schemas/TaskScheduleSchema"),
|
||||||
|
* ),
|
||||||
|
* @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' => 'Bulk action does not exist'], 400);
|
||||||
|
|
||||||
|
$ids = request()->input('ids');
|
||||||
|
|
||||||
|
$task_schedulers = Scheduler::withTrashed()->find($this->transformKeys($ids));
|
||||||
|
|
||||||
|
$task_schedulers->each(function ($task_scheduler, $key) use ($action) {
|
||||||
|
if (auth()->user()->can('edit', $task_scheduler)) {
|
||||||
|
$this->scheduler_repository->{$action}($task_scheduler);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return $this->listResponse(Scheduler::withTrashed()->whereIn('id', $this->transformKeys($ids)));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,39 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Requests\TaskScheduler;
|
|
||||||
|
|
||||||
use App\Http\Requests\Request;
|
|
||||||
|
|
||||||
class CreateScheduledTaskRequest extends Request
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Determine if the user is authorized to make this request.
|
|
||||||
*
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public function authorize(): bool
|
|
||||||
{
|
|
||||||
return auth()->user()->isAdmin();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function rules()
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'paused' => 'sometimes|bool',
|
|
||||||
'repeat_every' => 'required|string|in:DAY,WEEK,MONTH,3MONTHS,YEAR',
|
|
||||||
'start_from' => 'sometimes|string',
|
|
||||||
'job' => 'required',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function prepareForValidation()
|
|
||||||
{
|
|
||||||
$input = $this->all();
|
|
||||||
|
|
||||||
if (! array_key_exists('start_from', $input)) {
|
|
||||||
$input['start_from'] = now();
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->replace($input);
|
|
||||||
}
|
|
||||||
}
|
|
28
app/Http/Requests/TaskScheduler/CreateSchedulerRequest.php
Normal file
28
app/Http/Requests/TaskScheduler/CreateSchedulerRequest.php
Normal 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\TaskScheduler;
|
||||||
|
|
||||||
|
use App\Http\Requests\Request;
|
||||||
|
|
||||||
|
class CreateSchedulerRequest extends Request
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine if the user is authorized to make this request.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return auth()->user()->isAdmin();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
27
app/Http/Requests/TaskScheduler/DestroySchedulerRequest.php
Normal file
27
app/Http/Requests/TaskScheduler/DestroySchedulerRequest.php
Normal 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\Task;
|
||||||
|
|
||||||
|
use App\Http\Requests\Request;
|
||||||
|
|
||||||
|
class DestroySchedulerRequest extends Request
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine if the user is authorized to make this request.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function authorize() : bool
|
||||||
|
{
|
||||||
|
return auth()->user()->isAdmin();
|
||||||
|
}
|
||||||
|
}
|
27
app/Http/Requests/TaskScheduler/ShowSchedulerRequest.php
Normal file
27
app/Http/Requests/TaskScheduler/ShowSchedulerRequest.php
Normal 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\TaskScheduler;
|
||||||
|
|
||||||
|
use App\Http\Requests\Request;
|
||||||
|
|
||||||
|
class ShowSchedulerRequest extends Request
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine if the user is authorized to make this request.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function authorize() : bool
|
||||||
|
{
|
||||||
|
return auth()->user()->can('view', $this->scheduler);
|
||||||
|
}
|
||||||
|
}
|
44
app/Http/Requests/TaskScheduler/StoreSchedulerRequest.php
Normal file
44
app/Http/Requests/TaskScheduler/StoreSchedulerRequest.php
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
<?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\TaskScheduler;
|
||||||
|
|
||||||
|
use App\Http\Requests\Request;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
|
class StoreSchedulerRequest extends Request
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine if the user is authorized to make this request.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return auth()->user()->isAdmin();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules()
|
||||||
|
{
|
||||||
|
|
||||||
|
$rules = [
|
||||||
|
'name' => ['bail', 'required', Rule::unique('schedulers')->where('company_id', auth()->user()->company()->id)],
|
||||||
|
'is_paused' => 'bail|sometimes|boolean',
|
||||||
|
'frequency_id' => 'bail|required|integer|digits_between:1,12',
|
||||||
|
'next_run' => 'bail|required|date:Y-m-d',
|
||||||
|
'template' => 'bail|required|string',
|
||||||
|
'parameters' => 'bail|array',
|
||||||
|
];
|
||||||
|
|
||||||
|
return $rules;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -1,25 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Requests\TaskScheduler;
|
|
||||||
|
|
||||||
use App\Http\Requests\Request;
|
|
||||||
|
|
||||||
class UpdateScheduledJobRequest extends Request
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Determine if the user is authorized to make this request.
|
|
||||||
*
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public function authorize(): bool
|
|
||||||
{
|
|
||||||
return auth()->user()->isAdmin();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function rules(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'action_name' => 'sometimes|string',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
@ -8,14 +8,12 @@
|
|||||||
*
|
*
|
||||||
* @license https://www.elastic.co/licensing/elastic-license
|
* @license https://www.elastic.co/licensing/elastic-license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
namespace App\Http\Requests\TaskScheduler;
|
namespace App\Http\Requests\TaskScheduler;
|
||||||
|
|
||||||
use App\Http\Requests\Request;
|
use App\Http\Requests\Request;
|
||||||
use Carbon\Carbon;
|
|
||||||
use Illuminate\Validation\Rule;
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
class UpdateScheduleRequest extends Request
|
class UpdateSchedulerRequest extends Request
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Determine if the user is authorized to make this request.
|
* Determine if the user is authorized to make this request.
|
||||||
@ -29,23 +27,17 @@ class UpdateScheduleRequest extends Request
|
|||||||
|
|
||||||
public function rules(): array
|
public function rules(): array
|
||||||
{
|
{
|
||||||
return [
|
|
||||||
'paused' => 'sometimes|bool',
|
$rules = [
|
||||||
'repeat_every' => 'sometimes|string|in:DAY,WEEK,BIWEEKLY,MONTH,3MONTHS,YEAR',
|
'name' => ['bail', 'sometimes', Rule::unique('schedulers')->where('company_id', auth()->user()->company()->id)->ignore($this->task_scheduler->id)],
|
||||||
'start_from' => 'sometimes',
|
'is_paused' => 'bail|sometimes|boolean',
|
||||||
'scheduled_run'=>'sometimes',
|
'frequency_id' => 'bail|required|integer|digits_between:1,12',
|
||||||
|
'next_run' => 'bail|required|date:Y-m-d',
|
||||||
|
'template' => 'bail|required|string',
|
||||||
|
'parameters' => 'bail|array',
|
||||||
];
|
];
|
||||||
}
|
|
||||||
|
|
||||||
public function prepareForValidation()
|
return $rules;
|
||||||
{
|
|
||||||
$input = $this->all();
|
|
||||||
|
|
||||||
if (isset($input['start_from'])) {
|
|
||||||
$input['scheduled_run'] = Carbon::parse((int) $input['start_from']);
|
|
||||||
$input['start_from'] = Carbon::parse((int) $input['start_from']);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->replace($input);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -56,6 +56,7 @@ class ClientLedgerBalanceUpdate implements ShouldQueue
|
|||||||
if ($company_ledger->balance == 0)
|
if ($company_ledger->balance == 0)
|
||||||
{
|
{
|
||||||
|
|
||||||
|
|
||||||
$last_record = CompanyLedger::where('client_id', $company_ledger->client_id)
|
$last_record = CompanyLedger::where('client_id', $company_ledger->client_id)
|
||||||
->where('company_id', $company_ledger->company_id)
|
->where('company_id', $company_ledger->company_id)
|
||||||
->where('balance', '!=', 0)
|
->where('balance', '!=', 0)
|
||||||
@ -69,15 +70,12 @@ class ClientLedgerBalanceUpdate implements ShouldQueue
|
|||||||
->first();
|
->first();
|
||||||
}
|
}
|
||||||
|
|
||||||
// nlog("Updating Balance NOW");
|
}
|
||||||
|
|
||||||
$company_ledger->balance = $last_record->balance + $company_ledger->adjustment;
|
$company_ledger->balance = $last_record->balance + $company_ledger->adjustment;
|
||||||
$company_ledger->save();
|
$company_ledger->save();
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// nlog("Updating company ledger for client ". $this->client->id);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -450,16 +450,6 @@ class NinjaMailerJob implements ShouldQueue
|
|||||||
|
|
||||||
$this->checkValidSendingUser($user);
|
$this->checkValidSendingUser($user);
|
||||||
|
|
||||||
/* Always ensure the user is set on the correct account */
|
|
||||||
// if($user->account_id != $this->company->account_id){
|
|
||||||
|
|
||||||
// $this->nmo->settings->email_sending_method = 'default';
|
|
||||||
// return $this->setMailDriver();
|
|
||||||
|
|
||||||
// }
|
|
||||||
|
|
||||||
$this->checkValidSendingUser($user);
|
|
||||||
|
|
||||||
nlog("Sending via {$user->name()}");
|
nlog("Sending via {$user->name()}");
|
||||||
|
|
||||||
$google = (new Google())->init();
|
$google = (new Google())->init();
|
||||||
|
@ -21,6 +21,7 @@ use Illuminate\Foundation\Bus\Dispatchable;
|
|||||||
use Illuminate\Queue\InteractsWithQueue;
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
use Illuminate\Queue\SerializesModels;
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
|
||||||
|
//@rebuild it
|
||||||
class TaskScheduler implements ShouldQueue
|
class TaskScheduler implements ShouldQueue
|
||||||
{
|
{
|
||||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||||
@ -45,9 +46,10 @@ class TaskScheduler implements ShouldQueue
|
|||||||
MultiDB::setDB($db);
|
MultiDB::setDB($db);
|
||||||
|
|
||||||
Scheduler::with('company')
|
Scheduler::with('company')
|
||||||
->where('paused', false)
|
->where('is_paused', false)
|
||||||
->where('is_deleted', false)
|
->where('is_deleted', false)
|
||||||
->where('scheduled_run', '<', now())
|
->whereNotNull('next_run')
|
||||||
|
->where('next_run', '<=', now())
|
||||||
->cursor()
|
->cursor()
|
||||||
->each(function ($scheduler) {
|
->each(function ($scheduler) {
|
||||||
$this->doJob($scheduler);
|
$this->doJob($scheduler);
|
||||||
@ -57,59 +59,16 @@ class TaskScheduler implements ShouldQueue
|
|||||||
|
|
||||||
private function doJob(Scheduler $scheduler)
|
private function doJob(Scheduler $scheduler)
|
||||||
{
|
{
|
||||||
nlog("Doing job {$scheduler->action_name}");
|
nlog("Doing job {$scheduler->name}");
|
||||||
|
|
||||||
$company = $scheduler->company;
|
try {
|
||||||
|
$scheduler->service()->runTask();
|
||||||
$parameters = $scheduler->parameters;
|
}
|
||||||
|
catch(\Exception $e){
|
||||||
switch ($scheduler->action_name) {
|
nlog($e->getMessage());
|
||||||
case Scheduler::CREATE_CLIENT_REPORT:
|
|
||||||
SendToAdmin::dispatch($company, $parameters, $scheduler->action_class, 'contacts.csv');
|
|
||||||
break;
|
|
||||||
case Scheduler::CREATE_CLIENT_CONTACT_REPORT:
|
|
||||||
SendToAdmin::dispatch($company, $parameters, $scheduler->action_class, 'clients.csv');
|
|
||||||
break;
|
|
||||||
case Scheduler::CREATE_CREDIT_REPORT:
|
|
||||||
SendToAdmin::dispatch($company, $parameters, $scheduler->action_class, 'credits.csv');
|
|
||||||
break;
|
|
||||||
case Scheduler::CREATE_DOCUMENT_REPORT:
|
|
||||||
SendToAdmin::dispatch($company, $parameters, $scheduler->action_class, 'documents.csv');
|
|
||||||
break;
|
|
||||||
case Scheduler::CREATE_EXPENSE_REPORT:
|
|
||||||
SendToAdmin::dispatch($company, $parameters, $scheduler->action_class, 'expense.csv');
|
|
||||||
break;
|
|
||||||
case Scheduler::CREATE_INVOICE_ITEM_REPORT:
|
|
||||||
SendToAdmin::dispatch($company, $parameters, $scheduler->action_class, 'invoice_items.csv');
|
|
||||||
break;
|
|
||||||
case Scheduler::CREATE_INVOICE_REPORT:
|
|
||||||
SendToAdmin::dispatch($company, $parameters, $scheduler->action_class, 'invoices.csv');
|
|
||||||
break;
|
|
||||||
case Scheduler::CREATE_PAYMENT_REPORT:
|
|
||||||
SendToAdmin::dispatch($company, $parameters, $scheduler->action_class, 'payments.csv');
|
|
||||||
break;
|
|
||||||
case Scheduler::CREATE_PRODUCT_REPORT:
|
|
||||||
SendToAdmin::dispatch($company, $parameters, $scheduler->action_class, 'products.csv');
|
|
||||||
break;
|
|
||||||
case Scheduler::CREATE_PROFIT_AND_LOSS_REPORT:
|
|
||||||
SendToAdmin::dispatch($company, $parameters, $scheduler->action_class, 'profit_and_loss.csv');
|
|
||||||
break;
|
|
||||||
case Scheduler::CREATE_QUOTE_ITEM_REPORT:
|
|
||||||
SendToAdmin::dispatch($company, $parameters, $scheduler->action_class, 'quote_items.csv');
|
|
||||||
break;
|
|
||||||
case Scheduler::CREATE_QUOTE_REPORT:
|
|
||||||
SendToAdmin::dispatch($company, $parameters, $scheduler->action_class, 'quotes.csv');
|
|
||||||
break;
|
|
||||||
case Scheduler::CREATE_RECURRING_INVOICE_REPORT:
|
|
||||||
SendToAdmin::dispatch($company, $parameters, $scheduler->action_class, 'recurring_invoices.csv');
|
|
||||||
break;
|
|
||||||
case Scheduler::CREATE_TASK_REPORT:
|
|
||||||
SendToAdmin::dispatch($company, $parameters, $scheduler->action_class, 'tasks.csv');
|
|
||||||
break;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$scheduler->scheduled_run = $scheduler->nextScheduledDate();
|
|
||||||
$scheduler->save();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -66,13 +66,6 @@ class SendRecurring implements ShouldQueue
|
|||||||
// Generate Standard Invoice
|
// Generate Standard Invoice
|
||||||
$invoice = RecurringInvoiceToInvoiceFactory::create($this->recurring_invoice, $this->recurring_invoice->client);
|
$invoice = RecurringInvoiceToInvoiceFactory::create($this->recurring_invoice, $this->recurring_invoice->client);
|
||||||
|
|
||||||
if ($this->recurring_invoice->auto_bill === 'always') {
|
|
||||||
$invoice->auto_bill_enabled = true;
|
|
||||||
} elseif ($this->recurring_invoice->auto_bill === 'optout' || $this->recurring_invoice->auto_bill === 'optin') {
|
|
||||||
} elseif ($this->recurring_invoice->auto_bill === 'off') {
|
|
||||||
$invoice->auto_bill_enabled = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$invoice->date = date('Y-m-d');
|
$invoice->date = date('Y-m-d');
|
||||||
|
|
||||||
nlog("Recurring Invoice Date Set on Invoice = {$invoice->date} - ". now()->format('Y-m-d'));
|
nlog("Recurring Invoice Date Set on Invoice = {$invoice->date} - ". now()->format('Y-m-d'));
|
||||||
@ -94,6 +87,14 @@ class SendRecurring implements ShouldQueue
|
|||||||
->save();
|
->save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//12-01-2023 i moved this block after fillDefaults to handle if standard invoice auto bill config has been enabled, recurring invoice should override.
|
||||||
|
if ($this->recurring_invoice->auto_bill === 'always') {
|
||||||
|
$invoice->auto_bill_enabled = true;
|
||||||
|
} elseif ($this->recurring_invoice->auto_bill === 'optout' || $this->recurring_invoice->auto_bill === 'optin') {
|
||||||
|
} elseif ($this->recurring_invoice->auto_bill === 'off') {
|
||||||
|
$invoice->auto_bill_enabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
$invoice = $this->createRecurringInvitations($invoice);
|
$invoice = $this->createRecurringInvitations($invoice);
|
||||||
|
|
||||||
/* 09-01-2022 ensure we create the PDFs at this point in time! */
|
/* 09-01-2022 ensure we create the PDFs at this point in time! */
|
||||||
|
94
app/Mail/Client/ClientStatement.php
Normal file
94
app/Mail/Client/ClientStatement.php
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
<?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\Mail\Client;
|
||||||
|
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Mail\Attachment;
|
||||||
|
use Illuminate\Mail\Mailable;
|
||||||
|
use Illuminate\Mail\Mailables\Content;
|
||||||
|
use Illuminate\Mail\Mailables\Envelope;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
|
||||||
|
class ClientStatement extends Mailable
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new message instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct(public array $data){}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the message envelope.
|
||||||
|
*
|
||||||
|
* @return \Illuminate\Mail\Mailables\Envelope
|
||||||
|
*/
|
||||||
|
public function envelope()
|
||||||
|
{
|
||||||
|
return new Envelope(
|
||||||
|
subject: $this->data['subject'],
|
||||||
|
tags: [$this->data['company_key']],
|
||||||
|
replyTo: $this->data['reply_to'],
|
||||||
|
from: $this->data['from'],
|
||||||
|
to: $this->data['to'],
|
||||||
|
bcc: $this->data['bcc']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the message content definition.
|
||||||
|
*
|
||||||
|
* @return \Illuminate\Mail\Mailables\Content
|
||||||
|
*/
|
||||||
|
public function content()
|
||||||
|
{
|
||||||
|
return new Content(
|
||||||
|
view: 'email.template.client',
|
||||||
|
text: 'email.template.text',
|
||||||
|
with: [
|
||||||
|
'text_body' => $this->data['body'],
|
||||||
|
'body' => $this->data['body'],
|
||||||
|
'whitelabel' => $this->data['whitelabel'],
|
||||||
|
'settings' => $this->data['settings'],
|
||||||
|
'whitelabel' => $this->data['whitelabel'],
|
||||||
|
'logo' => $this->data['logo'],
|
||||||
|
'signature' => $this->data['signature'],
|
||||||
|
'company' => $this->data['company'],
|
||||||
|
'greeting' => $this->data['greeting'],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the attachments for the message.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function attachments()
|
||||||
|
{
|
||||||
|
$array_of_attachments = [];
|
||||||
|
|
||||||
|
foreach($this->data['attachments'] as $attachment)
|
||||||
|
{
|
||||||
|
|
||||||
|
$array_of_attachments[] =
|
||||||
|
Attachment::fromData(fn () => base64_decode($attachment['file']), $attachment['name'])
|
||||||
|
->withMime('application/pdf');
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return $array_of_attachments;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -170,6 +170,7 @@ class BaseModel extends Model
|
|||||||
*/
|
*/
|
||||||
public function resolveRouteBinding($value, $field = null)
|
public function resolveRouteBinding($value, $field = null)
|
||||||
{
|
{
|
||||||
|
|
||||||
if (is_numeric($value)) {
|
if (is_numeric($value)) {
|
||||||
throw new ModelNotFoundException("Record with value {$value} not found");
|
throw new ModelNotFoundException("Record with value {$value} not found");
|
||||||
}
|
}
|
||||||
|
@ -66,6 +66,7 @@ class Company extends BaseModel
|
|||||||
protected $presenter = CompanyPresenter::class;
|
protected $presenter = CompanyPresenter::class;
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
|
'invoice_task_hours',
|
||||||
'markdown_enabled',
|
'markdown_enabled',
|
||||||
'calculate_expense_tax_by_amount',
|
'calculate_expense_tax_by_amount',
|
||||||
'invoice_expense_documents',
|
'invoice_expense_documents',
|
||||||
|
@ -11,8 +11,7 @@
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use App\Services\TaskScheduler\TaskSchedulerService;
|
use App\Services\Scheduler\SchedulerService;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
||||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
|
|
||||||
@ -20,8 +19,8 @@ use Illuminate\Support\Carbon;
|
|||||||
* @property bool paused
|
* @property bool paused
|
||||||
* @property bool is_deleted
|
* @property bool is_deleted
|
||||||
* @property \Carbon\Carbon|mixed start_from
|
* @property \Carbon\Carbon|mixed start_from
|
||||||
* @property string repeat_every
|
* @property int frequency_id
|
||||||
* @property \Carbon\Carbon|mixed scheduled_run
|
* @property \Carbon\Carbon|mixed next_run
|
||||||
* @property int company_id
|
* @property int company_id
|
||||||
* @property int updated_at
|
* @property int updated_at
|
||||||
* @property int created_at
|
* @property int created_at
|
||||||
@ -33,76 +32,38 @@ use Illuminate\Support\Carbon;
|
|||||||
*/
|
*/
|
||||||
class Scheduler extends BaseModel
|
class Scheduler extends BaseModel
|
||||||
{
|
{
|
||||||
use HasFactory, SoftDeletes;
|
use SoftDeletes;
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'start_from',
|
'name',
|
||||||
'paused',
|
'frequency_id',
|
||||||
'repeat_every',
|
'next_run',
|
||||||
'scheduled_run',
|
'scheduled_run',
|
||||||
'action_class',
|
'template',
|
||||||
'action_name',
|
'is_paused',
|
||||||
'parameters',
|
'parameters',
|
||||||
'company_id',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
'start_from' => 'timestamp',
|
'next_run' => 'datetime',
|
||||||
'scheduled_run' => 'timestamp',
|
|
||||||
'created_at' => 'timestamp',
|
'created_at' => 'timestamp',
|
||||||
'updated_at' => 'timestamp',
|
'updated_at' => 'timestamp',
|
||||||
'deleted_at' => 'timestamp',
|
'deleted_at' => 'timestamp',
|
||||||
'paused' => 'boolean',
|
'is_paused' => 'boolean',
|
||||||
'is_deleted' => 'boolean',
|
'is_deleted' => 'boolean',
|
||||||
'parameters' => 'array',
|
'parameters' => 'array',
|
||||||
];
|
];
|
||||||
|
|
||||||
const DAILY = 'DAY';
|
protected $appends = [
|
||||||
|
'hashed_id',
|
||||||
const WEEKLY = 'WEEK';
|
];
|
||||||
|
|
||||||
const BIWEEKLY = 'BIWEEKLY';
|
|
||||||
|
|
||||||
const MONTHLY = 'MONTH';
|
|
||||||
|
|
||||||
const QUARTERLY = '3MONTHS';
|
|
||||||
|
|
||||||
const ANNUALLY = 'YEAR';
|
|
||||||
|
|
||||||
const CREATE_CLIENT_REPORT = 'create_client_report';
|
|
||||||
|
|
||||||
const CREATE_CLIENT_CONTACT_REPORT = 'create_client_contact_report';
|
|
||||||
|
|
||||||
const CREATE_CREDIT_REPORT = 'create_credit_report';
|
|
||||||
|
|
||||||
const CREATE_DOCUMENT_REPORT = 'create_document_report';
|
|
||||||
|
|
||||||
const CREATE_EXPENSE_REPORT = 'create_expense_report';
|
|
||||||
|
|
||||||
const CREATE_INVOICE_ITEM_REPORT = 'create_invoice_item_report';
|
|
||||||
|
|
||||||
const CREATE_INVOICE_REPORT = 'create_invoice_report';
|
|
||||||
|
|
||||||
const CREATE_PAYMENT_REPORT = 'create_payment_report';
|
|
||||||
|
|
||||||
const CREATE_PRODUCT_REPORT = 'create_product_report';
|
|
||||||
|
|
||||||
const CREATE_PROFIT_AND_LOSS_REPORT = 'create_profit_and_loss_report';
|
|
||||||
|
|
||||||
const CREATE_QUOTE_ITEM_REPORT = 'create_quote_item_report';
|
|
||||||
|
|
||||||
const CREATE_QUOTE_REPORT = 'create_quote_report';
|
|
||||||
|
|
||||||
const CREATE_RECURRING_INVOICE_REPORT = 'create_recurring_invoice_report';
|
|
||||||
|
|
||||||
const CREATE_TASK_REPORT = 'create_task_report';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service entry points.
|
* Service entry points.
|
||||||
*/
|
*/
|
||||||
public function service(): TaskSchedulerService
|
public function service(): SchedulerService
|
||||||
{
|
{
|
||||||
return new TaskSchedulerService($this);
|
return new SchedulerService($this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function company(): \Illuminate\Database\Eloquent\Relations\BelongsTo
|
public function company(): \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||||
@ -110,43 +71,43 @@ class Scheduler extends BaseModel
|
|||||||
return $this->belongsTo(Company::class);
|
return $this->belongsTo(Company::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function nextScheduledDate(): ?Carbon
|
// public function nextScheduledDate(): ?Carbon
|
||||||
{
|
// {
|
||||||
$offset = 0;
|
// $offset = 0;
|
||||||
|
|
||||||
$entity_send_time = $this->company->settings->entity_send_time;
|
// $entity_send_time = $this->company->settings->entity_send_time;
|
||||||
|
|
||||||
if ($entity_send_time != 0) {
|
// if ($entity_send_time != 0) {
|
||||||
$timezone = $this->company->timezone();
|
// $timezone = $this->company->timezone();
|
||||||
|
|
||||||
$offset -= $timezone->utc_offset;
|
// $offset -= $timezone->utc_offset;
|
||||||
$offset += ($entity_send_time * 3600);
|
// $offset += ($entity_send_time * 3600);
|
||||||
}
|
// }
|
||||||
|
|
||||||
/*
|
// /*
|
||||||
As we are firing at UTC+0 if our offset is negative it is technically firing the day before so we always need
|
// As we are firing at UTC+0 if our offset is negative it is technically firing the day before so we always need
|
||||||
to add ON a day - a day = 86400 seconds
|
// to add ON a day - a day = 86400 seconds
|
||||||
*/
|
// */
|
||||||
|
|
||||||
if ($offset < 0) {
|
// if ($offset < 0) {
|
||||||
$offset += 86400;
|
// $offset += 86400;
|
||||||
}
|
// }
|
||||||
|
|
||||||
switch ($this->repeat_every) {
|
// switch ($this->repeat_every) {
|
||||||
case self::DAILY:
|
// case self::DAILY:
|
||||||
return Carbon::parse($this->scheduled_run)->startOfDay()->addDay()->addSeconds($offset);
|
// return Carbon::parse($this->scheduled_run)->startOfDay()->addDay()->addSeconds($offset);
|
||||||
case self::WEEKLY:
|
// case self::WEEKLY:
|
||||||
return Carbon::parse($this->scheduled_run)->startOfDay()->addWeek()->addSeconds($offset);
|
// return Carbon::parse($this->scheduled_run)->startOfDay()->addWeek()->addSeconds($offset);
|
||||||
case self::BIWEEKLY:
|
// case self::BIWEEKLY:
|
||||||
return Carbon::parse($this->scheduled_run)->startOfDay()->addWeeks(2)->addSeconds($offset);
|
// return Carbon::parse($this->scheduled_run)->startOfDay()->addWeeks(2)->addSeconds($offset);
|
||||||
case self::MONTHLY:
|
// case self::MONTHLY:
|
||||||
return Carbon::parse($this->scheduled_run)->startOfDay()->addMonthNoOverflow()->addSeconds($offset);
|
// return Carbon::parse($this->scheduled_run)->startOfDay()->addMonthNoOverflow()->addSeconds($offset);
|
||||||
case self::QUARTERLY:
|
// case self::QUARTERLY:
|
||||||
return Carbon::parse($this->scheduled_run)->startOfDay()->addMonthsNoOverflow(3)->addSeconds($offset);
|
// return Carbon::parse($this->scheduled_run)->startOfDay()->addMonthsNoOverflow(3)->addSeconds($offset);
|
||||||
case self::ANNUALLY:
|
// case self::ANNUALLY:
|
||||||
return Carbon::parse($this->scheduled_run)->startOfDay()->addYearNoOverflow()->addSeconds($offset);
|
// return Carbon::parse($this->scheduled_run)->startOfDay()->addYearNoOverflow()->addSeconds($offset);
|
||||||
default:
|
// default:
|
||||||
return null;
|
// return null;
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
31
app/Policies/SchedulerPolicy.php
Normal file
31
app/Policies/SchedulerPolicy.php
Normal 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 SchedulerPolicy.
|
||||||
|
*/
|
||||||
|
class SchedulerPolicy extends EntityPolicy
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Checks if the user has create permissions.
|
||||||
|
*
|
||||||
|
* @param User $user
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function create(User $user) : bool
|
||||||
|
{
|
||||||
|
return $user->isAdmin();
|
||||||
|
}
|
||||||
|
}
|
@ -36,6 +36,7 @@ use App\Models\Quote;
|
|||||||
use App\Models\RecurringExpense;
|
use App\Models\RecurringExpense;
|
||||||
use App\Models\RecurringInvoice;
|
use App\Models\RecurringInvoice;
|
||||||
use App\Models\RecurringQuote;
|
use App\Models\RecurringQuote;
|
||||||
|
use App\Models\Scheduler;
|
||||||
use App\Models\Subscription;
|
use App\Models\Subscription;
|
||||||
use App\Models\Task;
|
use App\Models\Task;
|
||||||
use App\Models\TaskStatus;
|
use App\Models\TaskStatus;
|
||||||
@ -67,6 +68,7 @@ use App\Policies\QuotePolicy;
|
|||||||
use App\Policies\RecurringExpensePolicy;
|
use App\Policies\RecurringExpensePolicy;
|
||||||
use App\Policies\RecurringInvoicePolicy;
|
use App\Policies\RecurringInvoicePolicy;
|
||||||
use App\Policies\RecurringQuotePolicy;
|
use App\Policies\RecurringQuotePolicy;
|
||||||
|
use App\Policies\SchedulerPolicy;
|
||||||
use App\Policies\SubscriptionPolicy;
|
use App\Policies\SubscriptionPolicy;
|
||||||
use App\Policies\TaskPolicy;
|
use App\Policies\TaskPolicy;
|
||||||
use App\Policies\TaskStatusPolicy;
|
use App\Policies\TaskStatusPolicy;
|
||||||
@ -109,6 +111,7 @@ class AuthServiceProvider extends ServiceProvider
|
|||||||
RecurringExpense::class => RecurringExpensePolicy::class,
|
RecurringExpense::class => RecurringExpensePolicy::class,
|
||||||
RecurringInvoice::class => RecurringInvoicePolicy::class,
|
RecurringInvoice::class => RecurringInvoicePolicy::class,
|
||||||
RecurringQuote::class => RecurringQuotePolicy::class,
|
RecurringQuote::class => RecurringQuotePolicy::class,
|
||||||
|
Scheduler::class => SchedulerPolicy::class,
|
||||||
Subscription::class => SubscriptionPolicy::class,
|
Subscription::class => SubscriptionPolicy::class,
|
||||||
Task::class => TaskPolicy::class,
|
Task::class => TaskPolicy::class,
|
||||||
TaskStatus::class => TaskStatusPolicy::class,
|
TaskStatus::class => TaskStatusPolicy::class,
|
||||||
|
@ -11,7 +11,9 @@
|
|||||||
|
|
||||||
namespace App\Providers;
|
namespace App\Providers;
|
||||||
|
|
||||||
|
use App\Models\Scheduler;
|
||||||
use App\Utils\Traits\MakesHash;
|
use App\Utils\Traits\MakesHash;
|
||||||
|
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||||
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
|
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
@ -27,6 +29,21 @@ class RouteServiceProvider extends ServiceProvider
|
|||||||
public function boot()
|
public function boot()
|
||||||
{
|
{
|
||||||
parent::boot();
|
parent::boot();
|
||||||
|
|
||||||
|
|
||||||
|
Route::bind('task_scheduler', function ($value) {
|
||||||
|
|
||||||
|
if (is_numeric($value)) {
|
||||||
|
throw new ModelNotFoundException("Record with value {$value} not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Scheduler::query()
|
||||||
|
->withTrashed()
|
||||||
|
->where('id', $this->decodePrimaryKey($value))->firstOrFail();
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
38
app/Repositories/SchedulerRepository.php
Normal file
38
app/Repositories/SchedulerRepository.php
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
<?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\Scheduler;
|
||||||
|
|
||||||
|
class SchedulerRepository extends BaseRepository
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves the scheduler.
|
||||||
|
*
|
||||||
|
* @param array $data The data
|
||||||
|
* @param \App\Models\Scheduler $scheduler The scheduler
|
||||||
|
*
|
||||||
|
* @return \App\Models\Scheduler
|
||||||
|
*/
|
||||||
|
public function save(array $data, Scheduler $scheduler): Scheduler
|
||||||
|
{
|
||||||
|
|
||||||
|
$scheduler->fill($data);
|
||||||
|
|
||||||
|
$scheduler->save();
|
||||||
|
|
||||||
|
return $scheduler;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,16 +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\Repositories;
|
|
||||||
|
|
||||||
class TaskSchedulerRepository extends BaseRepository
|
|
||||||
{
|
|
||||||
}
|
|
244
app/Services/Email/EmailDefaults.php
Normal file
244
app/Services/Email/EmailDefaults.php
Normal file
@ -0,0 +1,244 @@
|
|||||||
|
<?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\Email;
|
||||||
|
|
||||||
|
use App\DataMapper\EmailTemplateDefaults;
|
||||||
|
use App\Models\Account;
|
||||||
|
use App\Models\Company;
|
||||||
|
use App\Services\Email\EmailObject;
|
||||||
|
use App\Utils\Ninja;
|
||||||
|
use Illuminate\Mail\Mailables\Address;
|
||||||
|
use Illuminate\Support\Facades\App;
|
||||||
|
use Illuminate\Support\Facades\Mail;
|
||||||
|
use League\CommonMark\CommonMarkConverter;
|
||||||
|
use Illuminate\Mail\Attachment;
|
||||||
|
|
||||||
|
class EmailDefaults
|
||||||
|
{
|
||||||
|
protected $settings;
|
||||||
|
|
||||||
|
private string $template;
|
||||||
|
|
||||||
|
private string $locale;
|
||||||
|
|
||||||
|
public function __construct(protected EmailService $email_service, public EmailObject $email_object){}
|
||||||
|
|
||||||
|
public function run()
|
||||||
|
{
|
||||||
|
$this->settings = $this->email_object->settings;
|
||||||
|
|
||||||
|
$this->setLocale()
|
||||||
|
->setFrom()
|
||||||
|
->setTemplate()
|
||||||
|
->setBody()
|
||||||
|
->setSubject()
|
||||||
|
->setReplyTo()
|
||||||
|
->setBcc()
|
||||||
|
->setAttachments()
|
||||||
|
->setMetaData()
|
||||||
|
->setVariables();
|
||||||
|
|
||||||
|
return $this->email_object;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private function setMetaData(): self
|
||||||
|
{
|
||||||
|
|
||||||
|
$this->email_object->company_key = $this->email_service->company->company_key;
|
||||||
|
|
||||||
|
$this->email_object->logo = $this->email_service->company->present()->logo();
|
||||||
|
|
||||||
|
$this->email_object->signature = $this->email_object->signature ?: $this->settings->email_signature;
|
||||||
|
|
||||||
|
$this->email_object->whitelabel = $this->email_object->company->account->isPaid() ? true : false;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private function setLocale(): self
|
||||||
|
{
|
||||||
|
|
||||||
|
if($this->email_object->client)
|
||||||
|
$this->locale = $this->email_object->client->locale();
|
||||||
|
elseif($this->email_object->vendor)
|
||||||
|
$this->locale = $this->email_object->vendor->locale();
|
||||||
|
else
|
||||||
|
$this->locale = $this->email_service->company->locale();
|
||||||
|
|
||||||
|
App::setLocale($this->locale);
|
||||||
|
App::forgetInstance('translator');
|
||||||
|
$t = app('translator');
|
||||||
|
$t->replace(Ninja::transformTranslations($this->settings));
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function setTemplate(): self
|
||||||
|
{
|
||||||
|
$this->template = $this->email_object->settings->email_style;
|
||||||
|
|
||||||
|
match($this->email_object->settings->email_style){
|
||||||
|
'light' => $this->template = 'email.template.client',
|
||||||
|
'dark' => $this->template = 'email.template.client',
|
||||||
|
'custom' => $this->template = 'email.template.custom',
|
||||||
|
default => $this->template = 'email.template.client',
|
||||||
|
};
|
||||||
|
|
||||||
|
$this->email_object->html_template = $this->template;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function setFrom(): self
|
||||||
|
{
|
||||||
|
if($this->email_object->from)
|
||||||
|
return $this;
|
||||||
|
|
||||||
|
$this->email_object->from = new Address($this->email_service->company->owner()->email, $this->email_service->company->owner()->name());
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
//think about where we do the string replace for variables....
|
||||||
|
private function setBody(): self
|
||||||
|
{
|
||||||
|
|
||||||
|
if($this->email_object->body){
|
||||||
|
$this->email_object->body = $this->email_object->body;
|
||||||
|
}
|
||||||
|
elseif(strlen($this->email_object->settings->{$this->email_object->email_template_body}) > 3){
|
||||||
|
$this->email_object->body = $this->email_object->settings->{$this->email_object->email_template_body};
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
$this->email_object->body = EmailTemplateDefaults::getDefaultTemplate($this->email_object->email_template_body, $this->locale);
|
||||||
|
}
|
||||||
|
|
||||||
|
if($this->template == 'email.template.custom'){
|
||||||
|
$this->email_object->body = (str_replace('$body', $this->email_object->body, $this->email_object->settings->email_style_custom));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
//think about where we do the string replace for variables....
|
||||||
|
private function setSubject(): self
|
||||||
|
{
|
||||||
|
|
||||||
|
if ($this->email_object->subject) //where the user updates the subject from the UI
|
||||||
|
return $this;
|
||||||
|
elseif(strlen($this->email_object->settings->{$this->email_object->email_template_subject}) > 3)
|
||||||
|
$this->email_object->subject = $this->email_object->settings->{$this->email_object->email_template_subject};
|
||||||
|
else
|
||||||
|
$this->email_object->subject = EmailTemplateDefaults::getDefaultTemplate($this->email_object->email_template_subject, $this->locale);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setVariables(): self
|
||||||
|
{
|
||||||
|
|
||||||
|
$this->email_object->body = strtr($this->email_object->body, $this->email_object->variables);
|
||||||
|
|
||||||
|
$this->email_object->subject = strtr($this->email_object->subject, $this->email_object->variables);
|
||||||
|
|
||||||
|
if($this->template != 'custom')
|
||||||
|
$this->email_object->body = $this->parseMarkdownToHtml($this->email_object->body);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function setReplyTo(): self
|
||||||
|
{
|
||||||
|
|
||||||
|
$reply_to_email = str_contains($this->email_object->settings->reply_to_email, "@") ? $this->email_object->settings->reply_to_email : $this->email_service->company->owner()->email;
|
||||||
|
|
||||||
|
$reply_to_name = strlen($this->email_object->settings->reply_to_name) > 3 ? $this->email_object->settings->reply_to_name : $this->email_service->company->owner()->present()->name();
|
||||||
|
|
||||||
|
$this->email_object->reply_to = array_merge($this->email_object->reply_to, [new Address($reply_to_email, $reply_to_name)]);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function setBcc(): self
|
||||||
|
{
|
||||||
|
$bccs = [];
|
||||||
|
$bcc_array = [];
|
||||||
|
|
||||||
|
if (strlen($this->email_object->settings->bcc_email) > 1) {
|
||||||
|
|
||||||
|
if (Ninja::isHosted() && $this->email_service->company->account->isPaid()) {
|
||||||
|
$bccs = array_slice(explode(',', str_replace(' ', '', $this->email_object->settings->bcc_email)), 0, 2);
|
||||||
|
} else {
|
||||||
|
$bccs(explode(',', str_replace(' ', '', $this->email_object->settings->bcc_email)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach($bccs as $bcc)
|
||||||
|
{
|
||||||
|
$bcc_array[] = new Address($bcc);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->email_object->bcc = array_merge($this->email_object->bcc, $bcc_array);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildCc()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function setAttachments(): self
|
||||||
|
{
|
||||||
|
$attachments = [];
|
||||||
|
|
||||||
|
if ($this->email_object->settings->document_email_attachment && $this->email_service->company->account->hasFeature(Account::FEATURE_DOCUMENTS)) {
|
||||||
|
|
||||||
|
foreach ($this->email_service->company->documents as $document) {
|
||||||
|
|
||||||
|
$attachments[] = ['file' => base64_encode($document->getFile()), 'name' => $document->name];
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->email_object->attachments = array_merge($this->email_object->attachments, $attachments);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private function setHeaders(): self
|
||||||
|
{
|
||||||
|
if($this->email_object->invitation_key)
|
||||||
|
$this->email_object->headers = array_merge($this->email_object->headers, ['x-invitation-key' => $this->email_object->invitation_key]);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function parseMarkdownToHtml(string $markdown): ?string
|
||||||
|
{
|
||||||
|
$converter = new CommonMarkConverter([
|
||||||
|
'allow_unsafe_links' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $converter->convert($markdown);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
109
app/Services/Email/EmailMailable.php
Normal file
109
app/Services/Email/EmailMailable.php
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
<?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\Email;
|
||||||
|
|
||||||
|
use App\Services\Email\EmailObject;
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Mail\Attachment;
|
||||||
|
use Illuminate\Mail\Mailable;
|
||||||
|
use Illuminate\Mail\Mailables\Content;
|
||||||
|
use Illuminate\Mail\Mailables\Envelope;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
use Illuminate\Mail\Mailables\Headers;
|
||||||
|
|
||||||
|
class EmailMailable extends Mailable
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new message instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct(public EmailObject $email_object){}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the message envelope.
|
||||||
|
*
|
||||||
|
* @return \Illuminate\Mail\Mailables\Envelope
|
||||||
|
*/
|
||||||
|
public function envelope()
|
||||||
|
{
|
||||||
|
return new Envelope(
|
||||||
|
subject: $this->email_object->subject,
|
||||||
|
tags: [$this->email_object->company_key],
|
||||||
|
replyTo: $this->email_object->reply_to,
|
||||||
|
from: $this->email_object->from,
|
||||||
|
to: $this->email_object->to,
|
||||||
|
bcc: $this->email_object->bcc
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the message content definition.
|
||||||
|
*
|
||||||
|
* @return \Illuminate\Mail\Mailables\Content
|
||||||
|
*/
|
||||||
|
public function content()
|
||||||
|
{
|
||||||
|
return new Content(
|
||||||
|
view: $this->email_object->template,
|
||||||
|
text: $this->email_object->text_template,
|
||||||
|
with: [
|
||||||
|
'text_body' => strip_tags($this->email_object->body),
|
||||||
|
'body' => $this->email_object->body,
|
||||||
|
'settings' => $this->email_object->settings,
|
||||||
|
'whitelabel' => $this->email_object->whitelabel,
|
||||||
|
'logo' => $this->email_object->logo,
|
||||||
|
'signature' => $this->email_object->signature,
|
||||||
|
'company' => $this->email_object->company,
|
||||||
|
'greeting' => ''
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the attachments for the message.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function attachments()
|
||||||
|
{
|
||||||
|
|
||||||
|
$attachments = [];
|
||||||
|
|
||||||
|
foreach($this->email_object->attachments as $file)
|
||||||
|
{
|
||||||
|
$attachments[] = Attachment::fromData(fn () => base64_decode($file['file']), $file['name']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $attachments;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the message headers.
|
||||||
|
*
|
||||||
|
* @return \Illuminate\Mail\Mailables\Headers
|
||||||
|
*/
|
||||||
|
public function headers()
|
||||||
|
{
|
||||||
|
|
||||||
|
return new Headers(
|
||||||
|
messageId: null,
|
||||||
|
references: [],
|
||||||
|
text: $this->email_object->headers,
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
508
app/Services/Email/EmailMailer.php
Normal file
508
app/Services/Email/EmailMailer.php
Normal file
@ -0,0 +1,508 @@
|
|||||||
|
<?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\Email;
|
||||||
|
|
||||||
|
use App\DataMapper\Analytics\EmailFailure;
|
||||||
|
use App\DataMapper\Analytics\EmailSuccess;
|
||||||
|
use App\DataMapper\EmailTemplateDefaults;
|
||||||
|
use App\Events\Invoice\InvoiceWasEmailedAndFailed;
|
||||||
|
use App\Events\Payment\PaymentWasEmailedAndFailed;
|
||||||
|
use App\Jobs\Util\SystemLogger;
|
||||||
|
use App\Libraries\Google\Google;
|
||||||
|
use App\Libraries\MultiDB;
|
||||||
|
use App\Models\Account;
|
||||||
|
use App\Models\ClientContact;
|
||||||
|
use App\Models\Company;
|
||||||
|
use App\Models\InvoiceInvitation;
|
||||||
|
use App\Models\Payment;
|
||||||
|
use App\Models\SystemLog;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Services\Email\EmailObject;
|
||||||
|
use App\Utils\Ninja;
|
||||||
|
use App\Utils\Traits\MakesHash;
|
||||||
|
use GuzzleHttp\Exception\ClientException;
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
|
use Illuminate\Mail\Attachment;
|
||||||
|
use Illuminate\Mail\Mailable;
|
||||||
|
use Illuminate\Mail\Mailables\Address;
|
||||||
|
use Illuminate\Mail\Mailer;
|
||||||
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
use Illuminate\Support\Facades\App;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
use Illuminate\Support\Facades\Mail;
|
||||||
|
use League\CommonMark\CommonMarkConverter;
|
||||||
|
use Turbo124\Beacon\Facades\LightLogs;
|
||||||
|
|
||||||
|
class EmailMailer implements ShouldQueue
|
||||||
|
{
|
||||||
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, MakesHash;
|
||||||
|
|
||||||
|
public $tries = 3; //number of retries
|
||||||
|
|
||||||
|
public $backoff = 30; //seconds to wait until retry
|
||||||
|
|
||||||
|
public $deleteWhenMissingModels = true;
|
||||||
|
|
||||||
|
public $override;
|
||||||
|
|
||||||
|
private $mailer;
|
||||||
|
|
||||||
|
protected $client_postmark_secret = false;
|
||||||
|
|
||||||
|
protected $client_mailgun_secret = false;
|
||||||
|
|
||||||
|
protected $client_mailgun_domain = false;
|
||||||
|
|
||||||
|
public function __construct(public EmailService $email_service, public Mailable $email_mailable){}
|
||||||
|
|
||||||
|
public function handle(): void
|
||||||
|
{
|
||||||
|
MultiDB::setDb($this->email_service->company->db);
|
||||||
|
|
||||||
|
//decode all attachments
|
||||||
|
$this->setMailDriver();
|
||||||
|
|
||||||
|
$mailer = Mail::mailer($this->mailer);
|
||||||
|
|
||||||
|
if($this->client_postmark_secret){
|
||||||
|
nlog("inside postmark config");
|
||||||
|
nlog($this->client_postmark_secret);
|
||||||
|
$mailer->postmark_config($this->client_postmark_secret);
|
||||||
|
}
|
||||||
|
|
||||||
|
if($this->client_mailgun_secret){
|
||||||
|
$mailer->mailgun_config($this->client_mailgun_secret, $this->client_mailgun_domain);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//send email
|
||||||
|
try {
|
||||||
|
|
||||||
|
nlog("Using mailer => ". $this->mailer. " ". now()->toDateTimeString());
|
||||||
|
$mailer->send($this->email_mailable);
|
||||||
|
|
||||||
|
Cache::increment($this->email_service->company->account->key);
|
||||||
|
|
||||||
|
LightLogs::create(new EmailSuccess($this->email_service->company->company_key))
|
||||||
|
->send();
|
||||||
|
|
||||||
|
} catch (\Exception | \RuntimeException | \Google\Service\Exception $e) {
|
||||||
|
|
||||||
|
nlog("error failed with {$e->getMessage()}");
|
||||||
|
|
||||||
|
$this->cleanUpMailers();
|
||||||
|
|
||||||
|
$message = $e->getMessage();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Post mark buries the proper message in a a guzzle response
|
||||||
|
* this merges a text string with a json object
|
||||||
|
* need to harvest the ->Message property using the following
|
||||||
|
*/
|
||||||
|
if($e instanceof ClientException) { //postmark specific failure
|
||||||
|
|
||||||
|
$response = $e->getResponse();
|
||||||
|
$message_body = json_decode($response->getBody()->getContents());
|
||||||
|
|
||||||
|
if($message_body && property_exists($message_body, 'Message')){
|
||||||
|
$message = $message_body->Message;
|
||||||
|
nlog($message);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/* If the is an entity attached to the message send a failure mailer */
|
||||||
|
$this->entityEmailFailed($message);
|
||||||
|
|
||||||
|
/* Don't send postmark failures to Sentry */
|
||||||
|
if(Ninja::isHosted() && (!$e instanceof ClientException))
|
||||||
|
app('sentry')->captureException($e);
|
||||||
|
|
||||||
|
$message = null;
|
||||||
|
// $this->email_service = null;
|
||||||
|
// $this->email_mailable = null;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entity notification when an email fails to send
|
||||||
|
*
|
||||||
|
* @param string $message
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function entityEmailFailed($message)
|
||||||
|
{
|
||||||
|
|
||||||
|
if(!$this->email_service->email_object->entity_id)
|
||||||
|
return;
|
||||||
|
|
||||||
|
switch ($this->email_service->email_object->entity_class) {
|
||||||
|
case Invoice::class:
|
||||||
|
$invitation = InvoiceInvitation::withTrashed()->find($this->email_service->email_object->entity_id);
|
||||||
|
if($invitation)
|
||||||
|
event(new InvoiceWasEmailedAndFailed($invitation, $this->email_service->company, $message, $this->email_service->email_object->reminder_template, Ninja::eventVars(auth()->user() ? auth()->user()->id : null)));
|
||||||
|
break;
|
||||||
|
case Payment::class:
|
||||||
|
$payment = Payment::withTrashed()->find($this->email_service->email_object->entity_id);
|
||||||
|
if($payment)
|
||||||
|
event(new PaymentWasEmailedAndFailed($payment, $this->email_service->company, $message, Ninja::eventVars(auth()->user() ? auth()->user()->id : null)));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
# code...
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->email_service->email_object->client_contact instanceof ClientContact)
|
||||||
|
$this->logMailError($message, $this->email_service->email_object->client_contact);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private function setMailDriver(): self
|
||||||
|
{
|
||||||
|
|
||||||
|
switch ($this->email_service->email_object->settings->email_sending_method) {
|
||||||
|
case 'default':
|
||||||
|
$this->mailer = config('mail.default');
|
||||||
|
break;
|
||||||
|
case 'gmail':
|
||||||
|
$this->mailer = 'gmail';
|
||||||
|
$this->setGmailMailer();
|
||||||
|
return $this;
|
||||||
|
case 'office365':
|
||||||
|
$this->mailer = 'office365';
|
||||||
|
$this->setOfficeMailer();
|
||||||
|
return $this;
|
||||||
|
case 'client_postmark':
|
||||||
|
$this->mailer = 'postmark';
|
||||||
|
$this->setPostmarkMailer();
|
||||||
|
return $this;
|
||||||
|
case 'client_mailgun':
|
||||||
|
$this->mailer = 'mailgun';
|
||||||
|
$this->setMailgunMailer();
|
||||||
|
return $this;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(Ninja::isSelfHost())
|
||||||
|
$this->setSelfHostMultiMailer();
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows configuration of multiple mailers
|
||||||
|
* per company for use by self hosted users
|
||||||
|
*/
|
||||||
|
private function setSelfHostMultiMailer(): void
|
||||||
|
{
|
||||||
|
|
||||||
|
if (env($this->email_service->company->id . '_MAIL_HOST'))
|
||||||
|
{
|
||||||
|
|
||||||
|
config([
|
||||||
|
'mail.mailers.smtp' => [
|
||||||
|
'transport' => 'smtp',
|
||||||
|
'host' => env($this->email_service->company->id . '_MAIL_HOST'),
|
||||||
|
'port' => env($this->email_service->company->id . '_MAIL_PORT'),
|
||||||
|
'username' => env($this->email_service->company->id . '_MAIL_USERNAME'),
|
||||||
|
'password' => env($this->email_service->company->id . '_MAIL_PASSWORD'),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
if(env($this->email_service->company->id . '_MAIL_FROM_ADDRESS'))
|
||||||
|
{
|
||||||
|
$this->email_mailable
|
||||||
|
->from(env($this->email_service->company->id . '_MAIL_FROM_ADDRESS', env('MAIL_FROM_ADDRESS')), env($this->email_service->company->id . '_MAIL_FROM_NAME', env('MAIL_FROM_NAME')));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure we discard any data that is not required
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function cleanUpMailers(): void
|
||||||
|
{
|
||||||
|
$this->client_postmark_secret = false;
|
||||||
|
|
||||||
|
$this->client_mailgun_secret = false;
|
||||||
|
|
||||||
|
$this->client_mailgun_domain = false;
|
||||||
|
|
||||||
|
//always dump the drivers to prevent reuse
|
||||||
|
app('mail.manager')->forgetMailers();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check to ensure no cross account
|
||||||
|
* emails can be sent.
|
||||||
|
*
|
||||||
|
* @param User $user
|
||||||
|
*/
|
||||||
|
private function checkValidSendingUser($user)
|
||||||
|
{
|
||||||
|
/* Always ensure the user is set on the correct account */
|
||||||
|
if($user->account_id != $this->email_service->company->account_id){
|
||||||
|
|
||||||
|
$this->email_service->email_object->settings->email_sending_method = 'default';
|
||||||
|
return $this->setMailDriver();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the sending user
|
||||||
|
* when configuring the Mailer
|
||||||
|
* on behalf of the client
|
||||||
|
*
|
||||||
|
* @return User $user
|
||||||
|
*/
|
||||||
|
private function resolveSendingUser(): ?User
|
||||||
|
{
|
||||||
|
$sending_user = $this->email_service->email_object->settings->gmail_sending_user_id;
|
||||||
|
|
||||||
|
$user = User::find($this->decodePrimaryKey($sending_user));
|
||||||
|
|
||||||
|
return $user;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configures Mailgun using client supplied secret
|
||||||
|
* as the Mailer
|
||||||
|
*/
|
||||||
|
private function setMailgunMailer()
|
||||||
|
{
|
||||||
|
if(strlen($this->email_service->email_object->settings->mailgun_secret) > 2 && strlen($this->email_service->email_object->settings->mailgun_domain) > 2){
|
||||||
|
$this->client_mailgun_secret = $this->email_service->email_object->settings->mailgun_secret;
|
||||||
|
$this->client_mailgun_domain = $this->email_service->email_object->settings->mailgun_domain;
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
$this->email_service->email_object->settings->email_sending_method = 'default';
|
||||||
|
return $this->setMailDriver();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$user = $this->resolveSendingUser();
|
||||||
|
|
||||||
|
$this->mailable
|
||||||
|
->from($user->email, $user->name());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configures Postmark using client supplied secret
|
||||||
|
* as the Mailer
|
||||||
|
*/
|
||||||
|
private function setPostmarkMailer()
|
||||||
|
{
|
||||||
|
if(strlen($this->email_service->email_object->settings->postmark_secret) > 2){
|
||||||
|
$this->client_postmark_secret = $this->email_service->email_object->settings->postmark_secret;
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
$this->email_service->email_object->settings->email_sending_method = 'default';
|
||||||
|
return $this->setMailDriver();
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = $this->resolveSendingUser();
|
||||||
|
|
||||||
|
$this->mailable
|
||||||
|
->from($user->email, $user->name());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configures Microsoft via Oauth
|
||||||
|
* as the Mailer
|
||||||
|
*/
|
||||||
|
private function setOfficeMailer()
|
||||||
|
{
|
||||||
|
$user = $this->resolveSendingUser();
|
||||||
|
|
||||||
|
$this->checkValidSendingUser($user);
|
||||||
|
|
||||||
|
nlog("Sending via {$user->name()}");
|
||||||
|
|
||||||
|
$token = $this->refreshOfficeToken($user);
|
||||||
|
|
||||||
|
if($token)
|
||||||
|
{
|
||||||
|
$user->oauth_user_token = $token;
|
||||||
|
$user->save();
|
||||||
|
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
|
||||||
|
$this->email_service->email_object->settings->email_sending_method = 'default';
|
||||||
|
return $this->setMailDriver();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->mailable
|
||||||
|
->from($user->email, $user->name())
|
||||||
|
->withSymfonyMessage(function ($message) use($token) {
|
||||||
|
$message->getHeaders()->addTextHeader('gmailtoken', $token);
|
||||||
|
});
|
||||||
|
|
||||||
|
sleep(rand(1,3));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configures GMail via Oauth
|
||||||
|
* as the Mailer
|
||||||
|
*/
|
||||||
|
private function setGmailMailer()
|
||||||
|
{
|
||||||
|
|
||||||
|
$user = $this->resolveSendingUser();
|
||||||
|
|
||||||
|
$this->checkValidSendingUser($user);
|
||||||
|
|
||||||
|
nlog("Sending via {$user->name()}");
|
||||||
|
|
||||||
|
$google = (new Google())->init();
|
||||||
|
|
||||||
|
try{
|
||||||
|
|
||||||
|
if ($google->getClient()->isAccessTokenExpired()) {
|
||||||
|
$google->refreshToken($user);
|
||||||
|
$user = $user->fresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
$google->getClient()->setAccessToken(json_encode($user->oauth_user_token));
|
||||||
|
|
||||||
|
sleep(rand(2,4));
|
||||||
|
}
|
||||||
|
catch(\Exception $e) {
|
||||||
|
$this->logMailError('Gmail Token Invalid', $this->email_service->company->clients()->first());
|
||||||
|
$this->email_service->email_object->settings->email_sending_method = 'default';
|
||||||
|
return $this->setMailDriver();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the user doesn't have a valid token, notify them
|
||||||
|
*/
|
||||||
|
|
||||||
|
if(!$user->oauth_user_token) {
|
||||||
|
$this->email_service->company->account->gmailCredentialNotification();
|
||||||
|
$this->email_service->email_object->settings->email_sending_method = 'default';
|
||||||
|
return $this->setMailDriver();
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Now that our token is refreshed and valid we can boot the
|
||||||
|
* mail driver at runtime and also set the token which will persist
|
||||||
|
* just for this request.
|
||||||
|
*/
|
||||||
|
|
||||||
|
$token = $user->oauth_user_token->access_token;
|
||||||
|
|
||||||
|
if(!$token) {
|
||||||
|
$this->email_service->company->account->gmailCredentialNotification();
|
||||||
|
$this->email_service->email_object->settings->email_sending_method = 'default';
|
||||||
|
return $this->setMailDriver();
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->mailable
|
||||||
|
->from($user->email, $user->name())
|
||||||
|
->withSymfonyMessage(function ($message) use($token) {
|
||||||
|
$message->getHeaders()->addTextHeader('gmailtoken', $token);
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logs any errors to the SystemLog
|
||||||
|
*
|
||||||
|
* @param string $errors
|
||||||
|
* @param App\Models\User | App\Models\Client $recipient_object
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function logMailError($errors, $recipient_object) :void
|
||||||
|
{
|
||||||
|
|
||||||
|
(new SystemLogger(
|
||||||
|
$errors,
|
||||||
|
SystemLog::CATEGORY_MAIL,
|
||||||
|
SystemLog::EVENT_MAIL_SEND,
|
||||||
|
SystemLog::TYPE_FAILURE,
|
||||||
|
$recipient_object,
|
||||||
|
$this->email_service->company
|
||||||
|
))->handle();
|
||||||
|
|
||||||
|
$job_failure = new EmailFailure($this->email_service->company->company_key);
|
||||||
|
$job_failure->string_metric5 = 'failed_email';
|
||||||
|
$job_failure->string_metric6 = substr($errors, 0, 150);
|
||||||
|
|
||||||
|
LightLogs::create($job_failure)
|
||||||
|
->send();
|
||||||
|
|
||||||
|
$job_failure = null;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to refresh the Microsoft refreshToken
|
||||||
|
*
|
||||||
|
* @param App\Models\User
|
||||||
|
* @return string | bool
|
||||||
|
*/
|
||||||
|
private function refreshOfficeToken($user)
|
||||||
|
{
|
||||||
|
$expiry = $user->oauth_user_token_expiry ?: now()->subDay();
|
||||||
|
|
||||||
|
if($expiry->lt(now()))
|
||||||
|
{
|
||||||
|
$guzzle = new \GuzzleHttp\Client();
|
||||||
|
$url = 'https://login.microsoftonline.com/common/oauth2/v2.0/token';
|
||||||
|
|
||||||
|
$token = json_decode($guzzle->post($url, [
|
||||||
|
'form_params' => [
|
||||||
|
'client_id' => config('ninja.o365.client_id') ,
|
||||||
|
'client_secret' => config('ninja.o365.client_secret') ,
|
||||||
|
'scope' => 'email Mail.Send offline_access profile User.Read openid',
|
||||||
|
'grant_type' => 'refresh_token',
|
||||||
|
'refresh_token' => $user->oauth_user_refresh_token
|
||||||
|
],
|
||||||
|
])->getBody()->getContents());
|
||||||
|
|
||||||
|
if($token){
|
||||||
|
|
||||||
|
$user->oauth_user_refresh_token = property_exists($token, 'refresh_token') ? $token->refresh_token : $user->oauth_user_refresh_token;
|
||||||
|
$user->oauth_user_token = $token->access_token;
|
||||||
|
$user->oauth_user_token_expiry = now()->addSeconds($token->expires_in);
|
||||||
|
$user->save();
|
||||||
|
|
||||||
|
return $token->access_token;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $user->oauth_user_token;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public function failed($exception = null)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
84
app/Services/Email/EmailObject.php
Normal file
84
app/Services/Email/EmailObject.php
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
<?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\Email;
|
||||||
|
|
||||||
|
use App\Models\Client;
|
||||||
|
use App\Models\ClientContact;
|
||||||
|
use App\Models\Company;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Models\Vendor;
|
||||||
|
use App\Models\VendorContact;
|
||||||
|
use Illuminate\Mail\Mailables\Address;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* EmailObject.
|
||||||
|
*/
|
||||||
|
class EmailObject
|
||||||
|
{
|
||||||
|
|
||||||
|
public array $to = [];
|
||||||
|
|
||||||
|
public ?Address $from = null;
|
||||||
|
|
||||||
|
public array $reply_to = [];
|
||||||
|
|
||||||
|
public array $cc = [];
|
||||||
|
|
||||||
|
public array $bcc = [];
|
||||||
|
|
||||||
|
public ?string $subject = null;
|
||||||
|
|
||||||
|
public ?string $body = null;
|
||||||
|
|
||||||
|
public array $attachments = [];
|
||||||
|
|
||||||
|
public string $company_key;
|
||||||
|
|
||||||
|
public ?object $settings = null;
|
||||||
|
|
||||||
|
public bool $whitelabel = false;
|
||||||
|
|
||||||
|
public ?string $logo = null;
|
||||||
|
|
||||||
|
public ?string $signature = null;
|
||||||
|
|
||||||
|
public ?string $greeting = null;
|
||||||
|
|
||||||
|
public ?Client $client = null;
|
||||||
|
|
||||||
|
public ?Vendor $vendor = null;
|
||||||
|
|
||||||
|
public ?User $user = null;
|
||||||
|
|
||||||
|
public ?ClientContact $client_contact = null;
|
||||||
|
|
||||||
|
public ?VendorContact $vendor_contact = null;
|
||||||
|
|
||||||
|
public ?string $email_template_body = null;
|
||||||
|
|
||||||
|
public ?string $email_template_subject = null;
|
||||||
|
|
||||||
|
public ?string $html_template = null;
|
||||||
|
|
||||||
|
public ?string $text_template = 'email.template.text';
|
||||||
|
|
||||||
|
public array $headers = [];
|
||||||
|
|
||||||
|
public ?string $invitation_key = null;
|
||||||
|
|
||||||
|
public ?int $entity_id = null;
|
||||||
|
|
||||||
|
public ?string $entity_class = null;
|
||||||
|
|
||||||
|
public array $variables = [];
|
||||||
|
|
||||||
|
}
|
73
app/Services/Email/EmailService.php
Normal file
73
app/Services/Email/EmailService.php
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
<?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\Email;
|
||||||
|
|
||||||
|
use App\Models\Company;
|
||||||
|
use App\Services\Email\EmailObject;
|
||||||
|
use Illuminate\Mail\Mailable;
|
||||||
|
|
||||||
|
class EmailService
|
||||||
|
{
|
||||||
|
protected string $mailer;
|
||||||
|
|
||||||
|
protected bool $override;
|
||||||
|
|
||||||
|
public Mailable $mailable;
|
||||||
|
|
||||||
|
public function __construct(public EmailObject $email_object, public Company $company){}
|
||||||
|
|
||||||
|
public function send($override = false) :void
|
||||||
|
{
|
||||||
|
$this->override = $override;
|
||||||
|
|
||||||
|
$this->setDefaults()
|
||||||
|
->updateMailable()
|
||||||
|
->email();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function sendNow($override = false) :void
|
||||||
|
{
|
||||||
|
$this->setDefaults()
|
||||||
|
->updateMailable()
|
||||||
|
->email(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function email($force = false): void
|
||||||
|
{
|
||||||
|
if($force)
|
||||||
|
(new EmailMailer($this, $this->mailable))->handle();
|
||||||
|
else
|
||||||
|
EmailMailer::dispatch($this, $this->mailable)->delay(2);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private function setDefaults(): self
|
||||||
|
{
|
||||||
|
$defaults = new EmailDefaults($this, $this->email_object);
|
||||||
|
$defaults->run();
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function updateMailable()
|
||||||
|
{
|
||||||
|
$this->mailable = new EmailMailable($this->email_object);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function emailQualityCheck()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -35,12 +35,7 @@ class InvoiceService
|
|||||||
{
|
{
|
||||||
use MakesHash;
|
use MakesHash;
|
||||||
|
|
||||||
public $invoice;
|
public function __construct(public Invoice $invoice){}
|
||||||
|
|
||||||
public function __construct($invoice)
|
|
||||||
{
|
|
||||||
$this->invoice = $invoice;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Marks as invoice as paid
|
* Marks as invoice as paid
|
||||||
@ -531,6 +526,10 @@ class InvoiceService
|
|||||||
$this->invoice->exchange_rate = $this->invoice->client->currency()->exchange_rate;
|
$this->invoice->exchange_rate = $this->invoice->client->currency()->exchange_rate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($settings->auto_bill_standard_invoices) {
|
||||||
|
$this->invoice->auto_bill_enabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
if ($settings->counter_number_applied == 'when_saved') {
|
if ($settings->counter_number_applied == 'when_saved') {
|
||||||
$this->invoice->service()->applyNumber()->save();
|
$this->invoice->service()->applyNumber()->save();
|
||||||
}
|
}
|
||||||
|
@ -1,57 +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\Schedule;
|
|
||||||
|
|
||||||
class ScheduleService
|
|
||||||
{
|
|
||||||
|
|
||||||
public function __construct(public Scheduler $scheduler) {}
|
|
||||||
|
|
||||||
public function scheduleStatement()
|
|
||||||
{
|
|
||||||
|
|
||||||
//Is it for one client
|
|
||||||
//Is it for all clients
|
|
||||||
//Is it for all clients excluding these clients
|
|
||||||
|
|
||||||
//Frequency
|
|
||||||
|
|
||||||
//show aging
|
|
||||||
//show payments
|
|
||||||
//paid/unpaid
|
|
||||||
|
|
||||||
//When to send? 1st of month
|
|
||||||
//End of month
|
|
||||||
//This date
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public function scheduleReport()
|
|
||||||
{
|
|
||||||
//Report type
|
|
||||||
//same schema as ScheduleStatement
|
|
||||||
}
|
|
||||||
|
|
||||||
public function scheduleEntitySend()
|
|
||||||
{
|
|
||||||
//Entity
|
|
||||||
//Entity Id
|
|
||||||
//When
|
|
||||||
}
|
|
||||||
|
|
||||||
public function projectStatus()
|
|
||||||
{
|
|
||||||
//Project ID
|
|
||||||
//Tasks - task statuses
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
129
app/Services/Scheduler/SchedulerService.php
Normal file
129
app/Services/Scheduler/SchedulerService.php
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
<?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\Scheduler;
|
||||||
|
|
||||||
|
use App\DataMapper\EmailTemplateDefaults;
|
||||||
|
use App\Mail\Client\ClientStatement;
|
||||||
|
use App\Models\Client;
|
||||||
|
use App\Models\Scheduler;
|
||||||
|
use App\Services\Email\EmailMailable;
|
||||||
|
use App\Services\Email\EmailObject;
|
||||||
|
use App\Services\Email\EmailService;
|
||||||
|
use App\Utils\Ninja;
|
||||||
|
use App\Utils\Traits\MakesDates;
|
||||||
|
use App\Utils\Traits\MakesHash;
|
||||||
|
use Illuminate\Mail\Mailables\Address;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
class SchedulerService
|
||||||
|
{
|
||||||
|
use MakesHash;
|
||||||
|
use MakesDates;
|
||||||
|
|
||||||
|
private string $method;
|
||||||
|
|
||||||
|
private Client $client;
|
||||||
|
|
||||||
|
public function __construct(public Scheduler $scheduler) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called from the TaskScheduler Cron
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function runTask(): void
|
||||||
|
{
|
||||||
|
$this->{$this->scheduler->template}();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function client_statement()
|
||||||
|
{
|
||||||
|
$query = Client::query()
|
||||||
|
->where('company_id', $this->scheduler->company_id);
|
||||||
|
|
||||||
|
//Email only the selected clients
|
||||||
|
if(count($this->scheduler->parameters['clients']) >= 1)
|
||||||
|
$query->where('id', $this->transformKeys($this->scheduler->parameters['clients']));
|
||||||
|
|
||||||
|
|
||||||
|
$query->cursor()
|
||||||
|
->each(function ($_client){
|
||||||
|
|
||||||
|
$this->client = $_client;
|
||||||
|
$statement_properties = $this->calculateStatementProperties();
|
||||||
|
|
||||||
|
//work out the date range
|
||||||
|
$pdf = $_client->service()->statement($statement_properties);
|
||||||
|
|
||||||
|
$email_service = new EmailService($this->buildMailableData($pdf), $_client->company);
|
||||||
|
$email_service->send();
|
||||||
|
|
||||||
|
//calculate next run dates;
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private function calculateStatementProperties()
|
||||||
|
{
|
||||||
|
$start_end = $this->calculateStartAndEndDates();
|
||||||
|
|
||||||
|
$this->client_start_date = $this->translateDate($start_end[0], $this->client->date_format(), $this->client->locale());
|
||||||
|
$this->client_end_date = $this->translateDate($start_end[1], $this->client->date_format(), $this->client->locale());
|
||||||
|
|
||||||
|
return [
|
||||||
|
'start_date' =>$start_end[0],
|
||||||
|
'end_date' =>$start_end[1],
|
||||||
|
'show_payments_table' => $this->scheduler->parameters['show_payments_table'],
|
||||||
|
'show_aging_table' => $this->scheduler->parameters['show_aging_table'],
|
||||||
|
'status' => $this->scheduler->parameters['status']
|
||||||
|
];
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private function calculateStartAndEndDates()
|
||||||
|
{
|
||||||
|
return match ($this->scheduler->parameters['date_range']) {
|
||||||
|
'this_month' => [now()->firstOfMonth()->format('Y-m-d'), now()->lastOfMonth()->format('Y-m-d')],
|
||||||
|
'this_quarter' => [now()->firstOfQuarter()->format('Y-m-d'), now()->lastOfQuarter()->format('Y-m-d')],
|
||||||
|
'this_year' => [now()->firstOfYear()->format('Y-m-d'), now()->lastOfYear()->format('Y-m-d')],
|
||||||
|
'previous_month' => [now()->subMonth()->firstOfMonth()->format('Y-m-d'), now()->subMonth()->lastOfMonth()->format('Y-m-d')],
|
||||||
|
'previous_quarter' => [now()->subQuarter()->firstOfQuarter()->format('Y-m-d'), now()->subQuarter()->lastOfQuarter()->format('Y-m-d')],
|
||||||
|
'previous_year' => [now()->subYear()->firstOfYear()->format('Y-m-d'), now()->subYear()->lastOfYear()->format('Y-m-d')],
|
||||||
|
'custom_range' => [$this->scheduler->parameters['start_date'], $this->scheduler->parameters['end_date']],
|
||||||
|
default => [now()->firstOfMonth()->format('Y-m-d'), now()->lastOfMonth()->format('Y-m-d')],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildMailableData($pdf)
|
||||||
|
{
|
||||||
|
|
||||||
|
$email_object = new EmailObject;
|
||||||
|
$email_object->to = [new Address($this->client->present()->email(), $this->client->present()->name())];
|
||||||
|
$email_object->attachments = [['file' => base64_encode($pdf), 'name' => ctrans('texts.statement') . ".pdf"]];
|
||||||
|
$email_object->settings = $this->client->getMergedSettings();
|
||||||
|
$email_object->company = $this->client->company;
|
||||||
|
$email_object->client = $this->client;
|
||||||
|
$email_object->email_template_subject = 'email_subject_statement';
|
||||||
|
$email_object->email_template_body = 'email_template_statement';
|
||||||
|
$email_object->variables = [
|
||||||
|
'$client' => $this->client->present()->name(),
|
||||||
|
'$start_date' => $this->client_start_date,
|
||||||
|
'$end_date' => $this->client_end_date,
|
||||||
|
];
|
||||||
|
|
||||||
|
return $email_object;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -38,6 +38,8 @@ use Illuminate\Database\Eloquent\Model;
|
|||||||
use Illuminate\Support\Facades\Validator;
|
use Illuminate\Support\Facades\Validator;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
|
||||||
|
|
||||||
|
//@deprecated - never used....
|
||||||
class TaskSchedulerService
|
class TaskSchedulerService
|
||||||
{
|
{
|
||||||
|
|
||||||
|
@ -194,6 +194,7 @@ class CompanyTransformer extends EntityTransformer
|
|||||||
'convert_payment_currency' => (bool) $company->convert_payment_currency,
|
'convert_payment_currency' => (bool) $company->convert_payment_currency,
|
||||||
'convert_expense_currency' => (bool) $company->convert_expense_currency,
|
'convert_expense_currency' => (bool) $company->convert_expense_currency,
|
||||||
'notify_vendor_when_paid' => (bool) $company->notify_vendor_when_paid,
|
'notify_vendor_when_paid' => (bool) $company->notify_vendor_when_paid,
|
||||||
|
'invoice_task_hours' => (bool) $company->invoice_task_hours,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@ namespace App\Transformers;
|
|||||||
use App\Models\Scheduler;
|
use App\Models\Scheduler;
|
||||||
use App\Utils\Traits\MakesHash;
|
use App\Utils\Traits\MakesHash;
|
||||||
|
|
||||||
class TaskSchedulerTransformer extends EntityTransformer
|
class SchedulerTransformer extends EntityTransformer
|
||||||
{
|
{
|
||||||
use MakesHash;
|
use MakesHash;
|
||||||
|
|
||||||
@ -22,17 +22,17 @@ class TaskSchedulerTransformer extends EntityTransformer
|
|||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'id' => $this->encodePrimaryKey($scheduler->id),
|
'id' => $this->encodePrimaryKey($scheduler->id),
|
||||||
|
'name' => (string) $scheduler->name,
|
||||||
|
'frequency_id' => (string) $scheduler->frequency_id,
|
||||||
|
'next_run' => $scheduler->next_run,
|
||||||
|
'template' => (string) $scheduler->template,
|
||||||
|
'is_paused' => (bool) $scheduler->is_paused,
|
||||||
|
'is_deleted' => (bool) $scheduler->is_deleted,
|
||||||
|
'parameters'=> (array) $scheduler->parameters,
|
||||||
'is_deleted' => (bool) $scheduler->is_deleted,
|
'is_deleted' => (bool) $scheduler->is_deleted,
|
||||||
'paused' => (bool) $scheduler->paused,
|
|
||||||
'repeat_every' => (string) $scheduler->repeat_every,
|
|
||||||
'start_from' => (int) $scheduler->start_from,
|
|
||||||
'scheduled_run' => (int) $scheduler->scheduled_run,
|
|
||||||
'updated_at' => (int) $scheduler->updated_at,
|
'updated_at' => (int) $scheduler->updated_at,
|
||||||
'created_at' => (int) $scheduler->created_at,
|
'created_at' => (int) $scheduler->created_at,
|
||||||
'archived_at' => (int) $scheduler->deleted_at,
|
'archived_at' => (int) $scheduler->deleted_at,
|
||||||
'action_name' => (string) $scheduler->action_name,
|
|
||||||
'action_class' => (string) $scheduler->action_class,
|
|
||||||
'parameters'=> (array) $scheduler->parameters,
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
37
database/factories/SchedulerFactory.php
Normal file
37
database/factories/SchedulerFactory.php
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
<?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\RecurringInvoice;
|
||||||
|
use App\Models\Scheduler;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
|
||||||
|
class SchedulerFactory extends Factory
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Define the model's default state.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function definition()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'name' => $this->faker->name(),
|
||||||
|
'is_paused' => rand(0,1),
|
||||||
|
'is_deleted' => rand(0,1),
|
||||||
|
'parameters' => [],
|
||||||
|
'frequency_id' => RecurringInvoice::FREQUENCY_MONTHLY,
|
||||||
|
'next_run' => now()->addSeconds(rand(86400,8640000)),
|
||||||
|
'template' => 'statement_task',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,71 @@
|
|||||||
|
<?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::table('accounts', function (Blueprint $table)
|
||||||
|
{
|
||||||
|
$table->boolean('is_trial')->default(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::table('companies', function (Blueprint $table)
|
||||||
|
{
|
||||||
|
$table->boolean('invoice_task_hours')->default(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::table('schedulers', function (Blueprint $table)
|
||||||
|
{
|
||||||
|
|
||||||
|
$table->dropColumn('repeat_every');
|
||||||
|
$table->dropColumn('start_from');
|
||||||
|
$table->dropColumn('scheduled_run');
|
||||||
|
$table->dropColumn('action_name');
|
||||||
|
$table->dropColumn('action_class');
|
||||||
|
$table->dropColumn('paused');
|
||||||
|
$table->dropColumn('company_id');
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
Schema::table('schedulers', function (Blueprint $table)
|
||||||
|
{
|
||||||
|
|
||||||
|
$table->unsignedInteger('company_id');
|
||||||
|
$table->boolean('is_paused')->default(false);
|
||||||
|
$table->unsignedInteger('frequency_id')->nullable();
|
||||||
|
$table->datetime('next_run')->nullable();
|
||||||
|
$table->datetime('next_run_client')->nullable();
|
||||||
|
$table->unsignedInteger('user_id');
|
||||||
|
$table->string('name', 191);
|
||||||
|
$table->string('template', 191);
|
||||||
|
|
||||||
|
$table->foreign('company_id')->references('id')->on('companies')->onDelete('cascade')->onUpdate('cascade');
|
||||||
|
|
||||||
|
$table->unique(['company_id', 'name']);
|
||||||
|
$table->index(['company_id', 'deleted_at']);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
};
|
@ -4924,6 +4924,7 @@ $LANG = array(
|
|||||||
'action_add_to_invoice' => 'Add To Invoice',
|
'action_add_to_invoice' => 'Add To Invoice',
|
||||||
'danger_zone' => 'Danger Zone',
|
'danger_zone' => 'Danger Zone',
|
||||||
'import_completed' => 'Import completed',
|
'import_completed' => 'Import completed',
|
||||||
|
'client_statement_body' => 'Your statement from :start_date to :end_date is attached.'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
@php
|
@php
|
||||||
$primary_color = isset($settings) ? $settings->primary_color : '#4caf50';
|
$primary_color = isset($settings) ? $settings->primary_color : '#4caf50';
|
||||||
|
$email_alignment = isset($settings) ? $settings->email_alignment : 'center';
|
||||||
@endphp
|
@endphp
|
||||||
|
|
||||||
|
|
||||||
@ -60,7 +61,8 @@
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
padding: 15px 50px;
|
padding: 15px 50px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin-bottom: 30px;
|
margin-bottom: 5px;
|
||||||
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
#content h1 {
|
#content h1 {
|
||||||
font-family: 'canada-type-gibson', 'roboto', Arial, Helvetica, sans-serif;
|
font-family: 'canada-type-gibson', 'roboto', Arial, Helvetica, sans-serif;
|
||||||
@ -146,8 +148,8 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td cellpadding="20">
|
<td cellpadding="5">
|
||||||
<div style="border: 1px solid #c2c2c2; border-top: none; border-bottom: none; padding: 20px; text-align: center" id="content">
|
<div style="border: 1px solid #c2c2c2; border-top: none; border-bottom: none; padding: 20px; text-align: {{ $email_alignment }}" id="content">
|
||||||
<div style="padding-top: 10px;"></div>
|
<div style="padding-top: 10px;"></div>
|
||||||
|
|
||||||
{{ $slot ?? '' }}
|
{{ $slot ?? '' }}
|
||||||
@ -163,8 +165,8 @@
|
|||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td height="20">
|
<td height="0">
|
||||||
<div style="border: 1px solid #c2c2c2; border-top: none; border-bottom: none; padding: 20px; text-align: center" id="content"> </div>
|
<div style="border: 1px solid #c2c2c2; border-top: none; border-bottom: none; padding: 5px; text-align: center" id="content"> </div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
|
@ -1,29 +1,32 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html data-report-errors="{{ $report_errors }}" data-rc="{{ $rc }}" data-user-agent="{{ $user_agent }}" data-login="{{ $login }}" data-signup="{{ $signup }}">
|
<html data-report-errors="{{ $report_errors }}" data-rc="{{ $rc }}" data-user-agent="{{ $user_agent }}" data-login="{{ $login }}" data-signup="{{ $signup }}" data-white-label="{{ $white_label }}">
|
||||||
<head>
|
<head>
|
||||||
<!-- Source: https://github.com/invoiceninja/invoiceninja -->
|
<!-- Source: https://github.com/invoiceninja/invoiceninja -->
|
||||||
<!-- Version: {{ config('ninja.app_version') }} -->
|
<!-- Version: {{ config('ninja.app_version') }} -->
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>{{config('ninja.app_name')}}</title>
|
<title>{{ $white_label ? "" : config('ninja.app_name') }}</title>
|
||||||
<meta name="google-signin-client_id" content="{{ config('services.google.client_id') }}">
|
<meta name="google-signin-client_id" content="{{ config('services.google.client_id') }}">
|
||||||
<link rel="manifest" href="manifest.json?v={{ config('ninja.app_version') }}">
|
<link rel="manifest" href="manifest.json?v={{ config('ninja.app_version') }}">
|
||||||
<script src="{{ asset('js/pdf.min.js') }}"></script>
|
<script src="{{ asset('js/pdf.min.js') }}"></script>
|
||||||
@if(\App\Utils\Ninja::isHosted())
|
@if(\App\Utils\Ninja::isHosted())
|
||||||
|
|
||||||
|
<!-- Apple OAuth Library -->
|
||||||
<script type="text/javascript" src="https://appleid.cdn-apple.com/appleauth/static/jsapi/appleid/1/en_US/appleid.auth.js"></script>
|
<script type="text/javascript" src="https://appleid.cdn-apple.com/appleauth/static/jsapi/appleid/1/en_US/appleid.auth.js"></script>
|
||||||
|
|
||||||
|
<!-- Microsoft OAuth library -->
|
||||||
<script type="text/javascript"
|
<script type="text/javascript"
|
||||||
src="https://alcdn.msauth.net/browser/2.14.2/js/msal-browser.min.js"
|
src="https://alcdn.msauth.net/browser/2.14.2/js/msal-browser.min.js"
|
||||||
integrity="sha384-ggh+EF1aSqm+Y4yvv2n17KpurNcZTeYtUZUvhPziElsstmIEubyEB6AIVpKLuZgr"
|
integrity="sha384-ggh+EF1aSqm+Y4yvv2n17KpurNcZTeYtUZUvhPziElsstmIEubyEB6AIVpKLuZgr"
|
||||||
crossorigin="anonymous">
|
crossorigin="anonymous">
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Google Tag Manager -->
|
<!-- G Tag Manager -->
|
||||||
<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
|
<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
|
||||||
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
|
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
|
||||||
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
|
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
|
||||||
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
|
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
|
||||||
})(window,document,'script','dataLayer','GTM-WMJ5W23');</script>
|
})(window,document,'script','dataLayer','GTM-WMJ5W23');</script>
|
||||||
<!-- End Google Tag Manager -->
|
<!-- End G Tag Manager -->
|
||||||
|
|
||||||
@endif
|
@endif
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
|
@ -275,7 +275,8 @@ Route::group(['middleware' => ['throttle:300,1', 'api_db', 'token_auth', 'locale
|
|||||||
Route::post('reports/tasks', TaskReportController::class);
|
Route::post('reports/tasks', TaskReportController::class);
|
||||||
Route::post('reports/profitloss', ProfitAndLossController::class);
|
Route::post('reports/profitloss', ProfitAndLossController::class);
|
||||||
|
|
||||||
Route::resource('task_scheduler', TaskSchedulerController::class)->except('edit')->parameters(['task_scheduler' => 'scheduler']);
|
Route::resource('task_schedulers', TaskSchedulerController::class);
|
||||||
|
Route::post('task_schedulers/bulk', [TaskSchedulerController::class, 'bulk'])->name('task_schedulers.bulk');
|
||||||
|
|
||||||
Route::get('scheduler', [SchedulerController::class, 'index']);
|
Route::get('scheduler', [SchedulerController::class, 'index']);
|
||||||
Route::post('support/messages/send', SendingController::class);
|
Route::post('support/messages/send', SendingController::class);
|
||||||
|
365
tests/Feature/Scheduler/SchedulerTest.php
Normal file
365
tests/Feature/Scheduler/SchedulerTest.php
Normal file
@ -0,0 +1,365 @@
|
|||||||
|
<?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 Tests\Feature\Scheduler;
|
||||||
|
|
||||||
|
use App\Export\CSV\ClientExport;
|
||||||
|
use App\Models\RecurringInvoice;
|
||||||
|
use App\Models\Scheduler;
|
||||||
|
use App\Utils\Traits\MakesHash;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Illuminate\Foundation\Testing\WithoutEvents;
|
||||||
|
use Illuminate\Routing\Middleware\ThrottleRequests;
|
||||||
|
use Illuminate\Support\Facades\Session;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
use Tests\MockAccountData;
|
||||||
|
use Tests\MockUnitData;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class SchedulerTest extends TestCase
|
||||||
|
{
|
||||||
|
use MakesHash;
|
||||||
|
use MockAccountData;
|
||||||
|
use WithoutEvents;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
Session::start();
|
||||||
|
|
||||||
|
$this->faker = \Faker\Factory::create();
|
||||||
|
|
||||||
|
Model::reguard();
|
||||||
|
|
||||||
|
$this->makeTestData();
|
||||||
|
|
||||||
|
$this->withoutMiddleware(
|
||||||
|
ThrottleRequests::class
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->withoutExceptionHandling();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function testGetThisMonthRange()
|
||||||
|
{
|
||||||
|
|
||||||
|
$this->travelTo(Carbon::parse('2023-01-14'));
|
||||||
|
|
||||||
|
$this->assertEqualsCanonicalizing(['2023-01-01','2023-01-31'], $this->getDateRange('this_month'));
|
||||||
|
$this->assertEqualsCanonicalizing(['2023-01-01','2023-03-31'], $this->getDateRange('this_quarter'));
|
||||||
|
$this->assertEqualsCanonicalizing(['2023-01-01','2023-12-31'], $this->getDateRange('this_year'));
|
||||||
|
|
||||||
|
$this->assertEqualsCanonicalizing(['2022-12-01','2022-12-31'], $this->getDateRange('previous_month'));
|
||||||
|
$this->assertEqualsCanonicalizing(['2022-10-01','2022-12-31'], $this->getDateRange('previous_quarter'));
|
||||||
|
$this->assertEqualsCanonicalizing(['2022-01-01','2022-12-31'], $this->getDateRange('previous_year'));
|
||||||
|
|
||||||
|
$this->travelBack();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getDateRange($range)
|
||||||
|
{
|
||||||
|
return match ($range) {
|
||||||
|
'this_month' => [now()->firstOfMonth()->format('Y-m-d'), now()->lastOfMonth()->format('Y-m-d')],
|
||||||
|
'this_quarter' => [now()->firstOfQuarter()->format('Y-m-d'), now()->lastOfQuarter()->format('Y-m-d')],
|
||||||
|
'this_year' => [now()->firstOfYear()->format('Y-m-d'), now()->lastOfYear()->format('Y-m-d')],
|
||||||
|
'previous_month' => [now()->subMonth()->firstOfMonth()->format('Y-m-d'), now()->subMonth()->lastOfMonth()->format('Y-m-d')],
|
||||||
|
'previous_quarter' => [now()->subQuarter()->firstOfQuarter()->format('Y-m-d'), now()->subQuarter()->lastOfQuarter()->format('Y-m-d')],
|
||||||
|
'previous_year' => [now()->subYear()->firstOfYear()->format('Y-m-d'), now()->subYear()->lastOfYear()->format('Y-m-d')],
|
||||||
|
'custom_range' => [$this->scheduler->parameters['start_date'], $this->scheduler->parameters['end_date']]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 'name' => ['bail', 'required', Rule::unique('schedulers')->where('company_id', auth()->user()->company()->id)],
|
||||||
|
'is_paused' => 'bail|sometimes|boolean',
|
||||||
|
'frequency_id' => 'bail|required|integer|digits_between:1,12',
|
||||||
|
'next_run' => 'bail|required|date:Y-m-d',
|
||||||
|
'template' => 'bail|required|string',
|
||||||
|
'parameters' => 'bail|array',
|
||||||
|
*/
|
||||||
|
|
||||||
|
public function testClientStatementGeneration()
|
||||||
|
{
|
||||||
|
$data = [
|
||||||
|
'name' => 'A test statement scheduler',
|
||||||
|
'frequency_id' => RecurringInvoice::FREQUENCY_MONTHLY,
|
||||||
|
'next_run' => '2023-01-14',
|
||||||
|
'template' => 'client_statement',
|
||||||
|
'parameters' => [
|
||||||
|
'date_range' => 'last_month',
|
||||||
|
'show_payments_table' => true,
|
||||||
|
'show_aging_table' => true,
|
||||||
|
'status' => 'paid',
|
||||||
|
'clients' => [],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$response = $this->withHeaders([
|
||||||
|
'X-API-SECRET' => config('ninja.api_secret'),
|
||||||
|
'X-API-TOKEN' => $this->token,
|
||||||
|
])->postJson('/api/v1/task_schedulers', $data);
|
||||||
|
|
||||||
|
$response->assertStatus(200);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDeleteSchedule()
|
||||||
|
{
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'ids' => [$this->scheduler->hashed_id],
|
||||||
|
];
|
||||||
|
|
||||||
|
$response = $this->withHeaders([
|
||||||
|
'X-API-SECRET' => config('ninja.api_secret'),
|
||||||
|
'X-API-TOKEN' => $this->token,
|
||||||
|
])->postJson('/api/v1/task_schedulers/bulk?action=delete', $data)
|
||||||
|
->assertStatus(200);
|
||||||
|
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'ids' => [$this->scheduler->hashed_id],
|
||||||
|
];
|
||||||
|
|
||||||
|
$response = $this->withHeaders([
|
||||||
|
'X-API-SECRET' => config('ninja.api_secret'),
|
||||||
|
'X-API-TOKEN' => $this->token,
|
||||||
|
])->postJson('/api/v1/task_schedulers/bulk?action=restore', $data)
|
||||||
|
->assertStatus(200);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testRestoreSchedule()
|
||||||
|
{
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'ids' => [$this->scheduler->hashed_id],
|
||||||
|
];
|
||||||
|
|
||||||
|
$response = $this->withHeaders([
|
||||||
|
'X-API-SECRET' => config('ninja.api_secret'),
|
||||||
|
'X-API-TOKEN' => $this->token,
|
||||||
|
])->postJson('/api/v1/task_schedulers/bulk?action=archive', $data)
|
||||||
|
->assertStatus(200);
|
||||||
|
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'ids' => [$this->scheduler->hashed_id],
|
||||||
|
];
|
||||||
|
|
||||||
|
$response = $this->withHeaders([
|
||||||
|
'X-API-SECRET' => config('ninja.api_secret'),
|
||||||
|
'X-API-TOKEN' => $this->token,
|
||||||
|
])->postJson('/api/v1/task_schedulers/bulk?action=restore', $data)
|
||||||
|
->assertStatus(200);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testArchiveSchedule()
|
||||||
|
{
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'ids' => [$this->scheduler->hashed_id],
|
||||||
|
];
|
||||||
|
|
||||||
|
$response = $this->withHeaders([
|
||||||
|
'X-API-SECRET' => config('ninja.api_secret'),
|
||||||
|
'X-API-TOKEN' => $this->token,
|
||||||
|
])->postJson('/api/v1/task_schedulers/bulk?action=archive', $data)
|
||||||
|
->assertStatus(200);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSchedulerPost()
|
||||||
|
{
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'name' => 'A different Name',
|
||||||
|
'frequency_id' => 5,
|
||||||
|
'next_run' => now()->addDays(2)->format('Y-m-d'),
|
||||||
|
'template' =>'statement',
|
||||||
|
'parameters' => [],
|
||||||
|
];
|
||||||
|
|
||||||
|
$response = $this->withHeaders([
|
||||||
|
'X-API-SECRET' => config('ninja.api_secret'),
|
||||||
|
'X-API-TOKEN' => $this->token,
|
||||||
|
])->postJson('/api/v1/task_schedulers', $data);
|
||||||
|
|
||||||
|
$response->assertStatus(200);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSchedulerPut()
|
||||||
|
{
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'name' => 'A different Name',
|
||||||
|
'frequency_id' => 5,
|
||||||
|
'next_run' => now()->addDays(2)->format('Y-m-d'),
|
||||||
|
'template' =>'statement',
|
||||||
|
'parameters' => [],
|
||||||
|
];
|
||||||
|
|
||||||
|
$response = $this->withHeaders([
|
||||||
|
'X-API-SECRET' => config('ninja.api_secret'),
|
||||||
|
'X-API-TOKEN' => $this->token,
|
||||||
|
])->putJson('/api/v1/task_schedulers/'.$this->scheduler->hashed_id, $data);
|
||||||
|
|
||||||
|
$response->assertStatus(200);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSchedulerGet()
|
||||||
|
{
|
||||||
|
$response = $this->withHeaders([
|
||||||
|
'X-API-SECRET' => config('ninja.api_secret'),
|
||||||
|
'X-API-TOKEN' => $this->token,
|
||||||
|
])->get('/api/v1/task_schedulers');
|
||||||
|
|
||||||
|
$response->assertStatus(200);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSchedulerCreate()
|
||||||
|
{
|
||||||
|
$response = $this->withHeaders([
|
||||||
|
'X-API-SECRET' => config('ninja.api_secret'),
|
||||||
|
'X-API-TOKEN' => $this->token,
|
||||||
|
])->get('/api/v1/task_schedulers/create');
|
||||||
|
|
||||||
|
$response->assertStatus(200);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// public function testSchedulerPut()
|
||||||
|
// {
|
||||||
|
// $data = [
|
||||||
|
// 'description' => $this->faker->firstName(),
|
||||||
|
// ];
|
||||||
|
|
||||||
|
// $response = $this->withHeaders([
|
||||||
|
// 'X-API-SECRET' => config('ninja.api_secret'),
|
||||||
|
// 'X-API-TOKEN' => $this->token,
|
||||||
|
// ])->put('/api/v1/task_schedulers/'.$this->encodePrimaryKey($this->task->id), $data);
|
||||||
|
|
||||||
|
// $response->assertStatus(200);
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// public function testSchedulerCantBeCreatedWithWrongData()
|
||||||
|
// {
|
||||||
|
// $data = [
|
||||||
|
// 'repeat_every' => Scheduler::DAILY,
|
||||||
|
// 'job' => Scheduler::CREATE_CLIENT_REPORT,
|
||||||
|
// 'date_key' => '123',
|
||||||
|
// 'report_keys' => ['test'],
|
||||||
|
// 'date_range' => 'all',
|
||||||
|
// // 'start_from' => '2022-01-01'
|
||||||
|
// ];
|
||||||
|
|
||||||
|
// $response = false;
|
||||||
|
|
||||||
|
// $response = $this->withHeaders([
|
||||||
|
// 'X-API-SECRET' => config('ninja.api_secret'),
|
||||||
|
// 'X-API-TOKEN' => $this->token,
|
||||||
|
// ])->post('/api/v1/task_scheduler/', $data);
|
||||||
|
|
||||||
|
// $response->assertSessionHasErrors();
|
||||||
|
// }
|
||||||
|
|
||||||
|
// public function testSchedulerCanBeUpdated()
|
||||||
|
// {
|
||||||
|
// $response = $this->createScheduler();
|
||||||
|
|
||||||
|
// $arr = $response->json();
|
||||||
|
// $id = $arr['data']['id'];
|
||||||
|
|
||||||
|
// $scheduler = Scheduler::find($this->decodePrimaryKey($id));
|
||||||
|
|
||||||
|
// $updateData = [
|
||||||
|
// 'start_from' => 1655934741,
|
||||||
|
// ];
|
||||||
|
// $response = $this->withHeaders([
|
||||||
|
// 'X-API-SECRET' => config('ninja.api_secret'),
|
||||||
|
// 'X-API-TOKEN' => $this->token,
|
||||||
|
// ])->put('/api/v1/task_scheduler/'.$this->encodePrimaryKey($scheduler->id), $updateData);
|
||||||
|
|
||||||
|
// $responseData = $response->json();
|
||||||
|
// $this->assertEquals($updateData['start_from'], $responseData['data']['start_from']);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// public function testSchedulerCanBeSeen()
|
||||||
|
// {
|
||||||
|
// $response = $this->createScheduler();
|
||||||
|
|
||||||
|
// $arr = $response->json();
|
||||||
|
// $id = $arr['data']['id'];
|
||||||
|
|
||||||
|
// $scheduler = Scheduler::find($this->decodePrimaryKey($id));
|
||||||
|
|
||||||
|
// $response = $this->withHeaders([
|
||||||
|
// 'X-API-SECRET' => config('ninja.api_secret'),
|
||||||
|
// 'X-API-TOKEN' => $this->token,
|
||||||
|
// ])->get('/api/v1/task_scheduler/'.$this->encodePrimaryKey($scheduler->id));
|
||||||
|
|
||||||
|
// $arr = $response->json();
|
||||||
|
// $this->assertEquals('create_client_report', $arr['data']['action_name']);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// public function testSchedulerJobCanBeUpdated()
|
||||||
|
// {
|
||||||
|
// $response = $this->createScheduler();
|
||||||
|
|
||||||
|
// $arr = $response->json();
|
||||||
|
// $id = $arr['data']['id'];
|
||||||
|
|
||||||
|
// $scheduler = Scheduler::find($this->decodePrimaryKey($id));
|
||||||
|
|
||||||
|
// $this->assertSame('create_client_report', $scheduler->action_name);
|
||||||
|
|
||||||
|
// $updateData = [
|
||||||
|
// 'job' => Scheduler::CREATE_CREDIT_REPORT,
|
||||||
|
// 'date_range' => 'all',
|
||||||
|
// 'report_keys' => ['test1'],
|
||||||
|
// ];
|
||||||
|
|
||||||
|
// $response = $this->withHeaders([
|
||||||
|
// 'X-API-SECRET' => config('ninja.api_secret'),
|
||||||
|
// 'X-API-TOKEN' => $this->token,
|
||||||
|
// ])->put('/api/v1/task_scheduler/'.$this->encodePrimaryKey($scheduler->id), $updateData);
|
||||||
|
|
||||||
|
// $updatedSchedulerJob = Scheduler::first()->action_name;
|
||||||
|
// $arr = $response->json();
|
||||||
|
|
||||||
|
// $this->assertSame('create_credit_report', $arr['data']['action_name']);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// public function createScheduler()
|
||||||
|
// {
|
||||||
|
// $data = [
|
||||||
|
// 'repeat_every' => Scheduler::DAILY,
|
||||||
|
// 'job' => Scheduler::CREATE_CLIENT_REPORT,
|
||||||
|
// 'date_key' => '123',
|
||||||
|
// 'report_keys' => ['test'],
|
||||||
|
// 'date_range' => 'all',
|
||||||
|
// 'start_from' => '2022-01-01',
|
||||||
|
// ];
|
||||||
|
|
||||||
|
// return $response = $this->withHeaders([
|
||||||
|
// 'X-API-SECRET' => config('ninja.api_secret'),
|
||||||
|
// 'X-API-TOKEN' => $this->token,
|
||||||
|
// ])->post('/api/v1/task_scheduler/', $data);
|
||||||
|
// }
|
||||||
|
}
|
@ -48,6 +48,7 @@ use App\Models\QuoteInvitation;
|
|||||||
use App\Models\RecurringExpense;
|
use App\Models\RecurringExpense;
|
||||||
use App\Models\RecurringInvoice;
|
use App\Models\RecurringInvoice;
|
||||||
use App\Models\RecurringQuote;
|
use App\Models\RecurringQuote;
|
||||||
|
use App\Models\Scheduler;
|
||||||
use App\Models\Task;
|
use App\Models\Task;
|
||||||
use App\Models\TaskStatus;
|
use App\Models\TaskStatus;
|
||||||
use App\Models\TaxRate;
|
use App\Models\TaxRate;
|
||||||
@ -177,6 +178,11 @@ trait MockAccountData
|
|||||||
*/
|
*/
|
||||||
public $tax_rate;
|
public $tax_rate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var
|
||||||
|
*/
|
||||||
|
public $scheduler;
|
||||||
|
|
||||||
public function makeTestData()
|
public function makeTestData()
|
||||||
{
|
{
|
||||||
config(['database.default' => config('ninja.db.default')]);
|
config(['database.default' => config('ninja.db.default')]);
|
||||||
@ -804,6 +810,14 @@ trait MockAccountData
|
|||||||
|
|
||||||
$this->client = $this->client->fresh();
|
$this->client = $this->client->fresh();
|
||||||
$this->invoice = $this->invoice->fresh();
|
$this->invoice = $this->invoice->fresh();
|
||||||
|
|
||||||
|
$this->scheduler = Scheduler::factory()->create([
|
||||||
|
'user_id' => $user_id,
|
||||||
|
'company_id' => $this->company->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->scheduler->save();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -42,4 +42,5 @@ class RecurringDateTest extends TestCase
|
|||||||
|
|
||||||
$this->assertequals($trial_ends->format('Y-m-d'), '2021-12-03');
|
$this->assertequals($trial_ends->format('Y-m-d'), '2021-12-03');
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user