1
0
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:
David Bomba 2023-01-15 17:42:13 +11:00
commit 3ce3187ec2
50 changed files with 2372 additions and 382 deletions

View File

@ -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();

View File

@ -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',

View File

@ -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)

View 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
}

View 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;
}
}

View File

@ -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'
)); ));

View File

@ -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') : '';

View File

@ -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)) {

View File

@ -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;

View File

@ -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)));
}
} }

View File

@ -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);
}
}

View File

@ -0,0 +1,28 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\Requests\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();
}
}

View File

@ -0,0 +1,27 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\Requests\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();
}
}

View File

@ -0,0 +1,27 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\Requests\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);
}
}

View 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;
}
}

View File

@ -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',
];
}
}

View File

@ -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);
} }
} }

View File

@ -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);
} }
} }

View File

@ -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();

View File

@ -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();
} }
} }

View File

@ -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! */

View 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;
}
}

View File

@ -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");
} }

View File

@ -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',

View File

@ -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;
} // }
} // }
} }

View File

@ -0,0 +1,31 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Policies;
use App\Models\User;
/**
* Class 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();
}
}

View File

@ -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,

View File

@ -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();
});
} }
/** /**

View 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;
}
}

View File

@ -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
{
}

View 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);
}
}

View 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,
);
}
}

View 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)
{
}
}

View 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 = [];
}

View 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()
{
}
}

View File

@ -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();
} }

View File

@ -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
}
}

View 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;
}
}

View File

@ -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
{ {

View File

@ -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,
]; ];
} }

View File

@ -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,
]; ];
} }
} }

View 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',
];
}
}

View File

@ -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()
{
//
}
};

View File

@ -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.'
); );

View File

@ -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>

View File

@ -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">

View File

@ -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);

View 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);
// }
}

View File

@ -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();
} }
/** /**

View File

@ -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');
} }
} }