mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2024-11-10 21:22:58 +01:00
commit
2a53be6584
@ -437,7 +437,7 @@ class CreateTestData extends Command
|
||||
'company_id' => $client->company->id,
|
||||
]);
|
||||
|
||||
Document::factory()->count(5)->create([
|
||||
Document::factory()->count(1)->create([
|
||||
'user_id' => $client->user->id,
|
||||
'company_id' => $client->company_id,
|
||||
'documentable_type' => Vendor::class,
|
||||
|
@ -71,7 +71,7 @@ class Kernel extends ConsoleKernel
|
||||
$schedule->job(new RecurringInvoicesCron)->hourly()->withoutOverlapping()->name('recurring-invoice-job')->onOneServer();
|
||||
|
||||
/* 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*/
|
||||
$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();
|
||||
|
||||
/* 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 */
|
||||
$schedule->job(new SystemMaintenance)->sundays()->at('02:30')->withoutOverlapping()->name('system-maintenance-job')->onOneServer();
|
||||
|
@ -447,7 +447,13 @@ class CompanySettings extends BaseSettings
|
||||
|
||||
public $mailgun_domain = '';
|
||||
|
||||
public $auto_bill_standard_invoices = false;
|
||||
|
||||
public $email_alignment = 'center'; // center , left, right
|
||||
|
||||
public static $casts = [
|
||||
'email_alignment' => 'string',
|
||||
'auto_bill_standard_invoices' => 'bool',
|
||||
'postmark_secret' => 'string',
|
||||
'mailgun_secret' => 'string',
|
||||
'mailgun_domain' => 'string',
|
||||
|
@ -235,12 +235,17 @@ class EmailTemplateDefaults
|
||||
|
||||
public static function emailStatementSubject()
|
||||
{
|
||||
return '';
|
||||
return ctrans('texts.your_statement');
|
||||
}
|
||||
|
||||
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)
|
||||
|
96
app/DataMapper/Schedule/ClientStatement.php
Normal file
96
app/DataMapper/Schedule/ClientStatement.php
Normal file
@ -0,0 +1,96 @@
|
||||
<?php
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\DataMapper;
|
||||
|
||||
use App\Models\Client;
|
||||
use stdClass;
|
||||
|
||||
class ClientStatement
|
||||
{
|
||||
|
||||
/**
|
||||
* Defines the template name
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $template = 'client_statement';
|
||||
|
||||
/**
|
||||
* An array of clients hashed_ids
|
||||
*
|
||||
* Leave blank if this action should apply to all clients
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public array $clients = [];
|
||||
|
||||
/**
|
||||
* The consts to be used to define the date_range variable of the statement
|
||||
*/
|
||||
public const THIS_MONTH = 'this_month';
|
||||
public const THIS_QUARTER = 'this_quarter';
|
||||
public const THIS_YEAR = 'this_year';
|
||||
public const PREVIOUS_MONTH = 'previous_month';
|
||||
public const PREVIOUS_QUARTER = 'previous_quarter';
|
||||
public const PREVIOUS_YEAR = 'previous_year';
|
||||
public const CUSTOM_RANGE = "custom_range";
|
||||
|
||||
/**
|
||||
* The date range the statement should include
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $date_range = 'this_month';
|
||||
|
||||
/**
|
||||
* If a custom range is select for the date range then
|
||||
* the start_date should be supplied in Y-m-d format
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $start_date = '';
|
||||
|
||||
/**
|
||||
* If a custom range is select for the date range then
|
||||
* the end_date should be supplied in Y-m-d format
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $end_date = '';
|
||||
|
||||
/**
|
||||
* Flag which allows the payment table
|
||||
* to be shown
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
public bool $show_payments_table = true;
|
||||
|
||||
/**
|
||||
* Flag which allows the aging table
|
||||
* to be shown
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
public bool $show_aging_table = true;
|
||||
|
||||
/**
|
||||
* String const which defines whether
|
||||
* the invoices to be shown are either
|
||||
* paid or unpaid
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $status = 'paid'; // paid | unpaid
|
||||
|
||||
|
||||
}
|
32
app/Factory/SchedulerFactory.php
Normal file
32
app/Factory/SchedulerFactory.php
Normal file
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\Factory;
|
||||
|
||||
use App\Models\Scheduler;
|
||||
|
||||
class SchedulerFactory
|
||||
{
|
||||
public static function create($company_id, $user_id) :Scheduler
|
||||
{
|
||||
$scheduler = new Scheduler;
|
||||
|
||||
$scheduler->name = '';
|
||||
$scheduler->company_id = $company_id;
|
||||
$scheduler->user_id = $user_id;
|
||||
$scheduler->parameters = [];
|
||||
$scheduler->is_paused = false;
|
||||
$scheduler->is_deleted = false;
|
||||
$scheduler->template = '';
|
||||
|
||||
return $scheduler;
|
||||
}
|
||||
}
|
@ -87,6 +87,7 @@ class QuoteFilters extends QueryFilters
|
||||
if (in_array('expired', $status_parameters)) {
|
||||
$this->builder->orWhere(function ($query){
|
||||
$query->where('status_id', Quote::STATUS_SENT)
|
||||
->company()
|
||||
->whereNotNull('due_date')
|
||||
->where('due_date', '<=', now()->toDateString());
|
||||
});
|
||||
@ -95,6 +96,7 @@ class QuoteFilters extends QueryFilters
|
||||
if (in_array('upcoming', $status_parameters)) {
|
||||
$this->builder->orWhere(function ($query){
|
||||
$query->where('status_id', Quote::STATUS_SENT)
|
||||
->company()
|
||||
->where('due_date', '>=', now()->toDateString())
|
||||
->orderBy('due_date', 'DESC');
|
||||
});
|
||||
|
@ -87,10 +87,10 @@ class SwissQrGenerator
|
||||
$qrBill->setUltimateDebtor(
|
||||
QrBill\DataGroup\Element\StructuredAddress::createWithStreet(
|
||||
substr($this->client->present()->name(), 0 , 70),
|
||||
$this->client->address1 ? substr($this->client->address1, 0 , 70) : '_',
|
||||
$this->client->address2 ? substr($this->client->address2, 0 , 16) : '_',
|
||||
$this->client->postal_code ? substr($this->client->postal_code, 0, 16) : '_',
|
||||
$this->client->city ? substr($this->client->city, 0, 35) : '_',
|
||||
$this->client->address1 ? substr($this->client->address1, 0 , 70) : ' ',
|
||||
$this->client->address2 ? substr($this->client->address2, 0 , 16) : ' ',
|
||||
$this->client->postal_code ? substr($this->client->postal_code, 0, 16) : ' ',
|
||||
$this->client->city ? substr($this->client->city, 0, 35) : ' ',
|
||||
'CH'
|
||||
));
|
||||
|
||||
|
@ -984,6 +984,9 @@ class BaseController extends Controller
|
||||
//pass report errors bool to front end
|
||||
$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
|
||||
$data['rc'] = request()->has('rc') ? request()->input('rc') : '';
|
||||
$data['build'] = request()->has('build') ? request()->input('build') : '';
|
||||
|
@ -148,6 +148,7 @@ class NinjaPlanController extends Controller
|
||||
$account->plan_term = 'month';
|
||||
$account->plan_started = now();
|
||||
$account->plan_expires = now()->addDays(14);
|
||||
$account->is_trial=true;
|
||||
$account->save();
|
||||
}
|
||||
|
||||
@ -216,7 +217,7 @@ class NinjaPlanController extends Controller
|
||||
|
||||
if ($account) {
|
||||
//offer the option to have a free trial
|
||||
if (! $account->trial_started && ! $account->plan) {
|
||||
if (!$account->is_trial) {
|
||||
return $this->trial();
|
||||
}
|
||||
|
||||
|
@ -502,7 +502,6 @@ class PurchaseOrderController extends BaseController
|
||||
/*
|
||||
* Download Purchase Order/s
|
||||
*/
|
||||
|
||||
if ($action == 'bulk_download' && $purchase_orders->count() >= 1) {
|
||||
$purchase_orders->each(function ($purchase_order) {
|
||||
if (auth()->user()->cannot('view', $purchase_order)) {
|
||||
|
@ -1,4 +1,13 @@
|
||||
<?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;
|
||||
|
||||
|
@ -11,37 +11,40 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Requests\TaskScheduler\CreateScheduledTaskRequest;
|
||||
use App\Http\Requests\TaskScheduler\UpdateScheduleRequest;
|
||||
use App\Factory\SchedulerFactory;
|
||||
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\Report\ProfitAndLoss;
|
||||
use App\Models\Scheduler;
|
||||
use App\Repositories\TaskSchedulerRepository;
|
||||
use App\Transformers\TaskSchedulerTransformer;
|
||||
use App\Repositories\SchedulerRepository;
|
||||
use App\Transformers\SchedulerTransformer;
|
||||
use App\Utils\Traits\MakesHash;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
class TaskSchedulerController extends BaseController
|
||||
{
|
||||
use MakesHash;
|
||||
|
||||
protected $entity_type = Scheduler::class;
|
||||
|
||||
protected $entity_transformer = TaskSchedulerTransformer::class;
|
||||
protected $entity_transformer = SchedulerTransformer::class;
|
||||
|
||||
protected TaskSchedulerRepository $scheduler_repository;
|
||||
|
||||
public function __construct(TaskSchedulerRepository $scheduler_repository)
|
||||
public function __construct(protected SchedulerRepository $scheduler_repository)
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
$this->scheduler_repository = $scheduler_repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* @OA\GET(
|
||||
* path="/api/v1/task_scheduler/",
|
||||
* path="/api/v1/task_schedulers/",
|
||||
* operationId="getTaskSchedulers",
|
||||
* tags={"task_scheduler"},
|
||||
* tags={"task_schedulers"},
|
||||
* summary="Task Scheduler Index",
|
||||
* description="Get all schedulers with associated jobs",
|
||||
* @OA\Parameter(ref="#/components/parameters/X-Api-Secret"),
|
||||
@ -67,11 +70,57 @@ class TaskSchedulerController extends BaseController
|
||||
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(
|
||||
* path="/api/v1/task_scheduler/",
|
||||
* path="/api/v1/task_schedulers/",
|
||||
* operationId="createTaskScheduler",
|
||||
* tags={"task_scheduler"},
|
||||
* tags={"task_schedulers"},
|
||||
* 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
|
||||
* 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->service()->store($scheduler, $request);
|
||||
$scheduler = $this->scheduler_repository->save($request->all(), SchedulerFactory::create(auth()->user()->company()->id, auth()->user()->id));
|
||||
|
||||
return $this->itemResponse($scheduler);
|
||||
}
|
||||
|
||||
/**
|
||||
* @OA\GET(
|
||||
* path="/api/v1/task_scheduler/{id}",
|
||||
* path="/api/v1/task_schedulers/{id}",
|
||||
* operationId="showTaskScheduler",
|
||||
* tags={"task_scheduler"},
|
||||
* tags={"task_schedulers"},
|
||||
* summary="Show given scheduler",
|
||||
* description="Get scheduler with associated job",
|
||||
* @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);
|
||||
}
|
||||
|
||||
/**
|
||||
* @OA\PUT(
|
||||
* path="/api/v1/task_scheduler/{id}",
|
||||
* path="/api/v1/task_schedulers/{id}",
|
||||
* operationId="updateTaskScheduler",
|
||||
* tags={"task_scheduler"},
|
||||
* tags={"task_schedulers"},
|
||||
* summary="Update task scheduler ",
|
||||
* description="Update task scheduler",
|
||||
* @OA\Parameter(ref="#/components/parameters/X-Api-Secret"),
|
||||
@ -168,7 +216,7 @@ class TaskSchedulerController extends BaseController
|
||||
* ),
|
||||
* ), * @OA\RequestBody(
|
||||
* required=true,
|
||||
* @OA\JsonContent(ref="#/components/schemas/UpdateTaskSchedulerSchema")
|
||||
* @OA\JsonContent(ref="#/components/schemas/TaskSchedulerSchema")
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* @OA\DELETE(
|
||||
* path="/api/v1/task_scheduler/{id}",
|
||||
* path="/api/v1/task_schedulers/{id}",
|
||||
* operationId="destroyTaskScheduler",
|
||||
* tags={"task_scheduler"},
|
||||
* tags={"task_schedulers"},
|
||||
* summary="Destroy Task Scheduler",
|
||||
* description="Destroy task scheduler and its associated job",
|
||||
* @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);
|
||||
|
||||
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)));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -202,6 +202,12 @@ class BillingPortalPurchase extends Component
|
||||
elseif(strlen($this->subscription->promo_code) == 0 && $this->subscription->promo_discount > 0){
|
||||
$this->price = $this->subscription->promo_price;
|
||||
}
|
||||
|
||||
/* Leave this here, otherwise a logged in user will need to reauth... painfully */
|
||||
if(Auth::guard('contact')->check()){
|
||||
return $this->getPaymentMethods(auth()->guard('contact')->user());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -32,8 +32,6 @@ class CheckMailRequest extends Request
|
||||
*/
|
||||
public function rules()
|
||||
{
|
||||
nlog($this->driver);
|
||||
|
||||
return [
|
||||
'mail_driver' => 'required',
|
||||
'encryption' => 'required_unless:mail_driver,log',
|
||||
|
@ -1,39 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\TaskScheduler;
|
||||
|
||||
use App\Http\Requests\Request;
|
||||
|
||||
class CreateScheduledTaskRequest extends Request
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return auth()->user()->isAdmin();
|
||||
}
|
||||
|
||||
public function rules()
|
||||
{
|
||||
return [
|
||||
'paused' => 'sometimes|bool',
|
||||
'repeat_every' => 'required|string|in:DAY,WEEK,MONTH,3MONTHS,YEAR',
|
||||
'start_from' => 'sometimes|string',
|
||||
'job' => 'required',
|
||||
];
|
||||
}
|
||||
|
||||
public function prepareForValidation()
|
||||
{
|
||||
$input = $this->all();
|
||||
|
||||
if (! array_key_exists('start_from', $input)) {
|
||||
$input['start_from'] = now();
|
||||
}
|
||||
|
||||
$this->replace($input);
|
||||
}
|
||||
}
|
28
app/Http/Requests/TaskScheduler/CreateSchedulerRequest.php
Normal file
28
app/Http/Requests/TaskScheduler/CreateSchedulerRequest.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\Http\Requests\TaskScheduler;
|
||||
|
||||
use App\Http\Requests\Request;
|
||||
|
||||
class CreateSchedulerRequest extends Request
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return auth()->user()->isAdmin();
|
||||
}
|
||||
|
||||
}
|
27
app/Http/Requests/TaskScheduler/DestroySchedulerRequest.php
Normal file
27
app/Http/Requests/TaskScheduler/DestroySchedulerRequest.php
Normal file
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\Http\Requests\Task;
|
||||
|
||||
use App\Http\Requests\Request;
|
||||
|
||||
class DestroySchedulerRequest extends Request
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function authorize() : bool
|
||||
{
|
||||
return auth()->user()->isAdmin();
|
||||
}
|
||||
}
|
27
app/Http/Requests/TaskScheduler/ShowSchedulerRequest.php
Normal file
27
app/Http/Requests/TaskScheduler/ShowSchedulerRequest.php
Normal file
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\Http\Requests\TaskScheduler;
|
||||
|
||||
use App\Http\Requests\Request;
|
||||
|
||||
class ShowSchedulerRequest extends Request
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function authorize() : bool
|
||||
{
|
||||
return auth()->user()->can('view', $this->scheduler);
|
||||
}
|
||||
}
|
44
app/Http/Requests/TaskScheduler/StoreSchedulerRequest.php
Normal file
44
app/Http/Requests/TaskScheduler/StoreSchedulerRequest.php
Normal file
@ -0,0 +1,44 @@
|
||||
<?php
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\Http\Requests\TaskScheduler;
|
||||
|
||||
use App\Http\Requests\Request;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class StoreSchedulerRequest extends Request
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return auth()->user()->isAdmin();
|
||||
}
|
||||
|
||||
public function rules()
|
||||
{
|
||||
|
||||
$rules = [
|
||||
'name' => ['bail', 'required', Rule::unique('schedulers')->where('company_id', auth()->user()->company()->id)],
|
||||
'is_paused' => 'bail|sometimes|boolean',
|
||||
'frequency_id' => 'bail|required|integer|digits_between:1,12',
|
||||
'next_run' => 'bail|required|date:Y-m-d',
|
||||
'template' => 'bail|required|string',
|
||||
'parameters' => 'bail|array',
|
||||
];
|
||||
|
||||
return $rules;
|
||||
|
||||
}
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\TaskScheduler;
|
||||
|
||||
use App\Http\Requests\Request;
|
||||
|
||||
class UpdateScheduledJobRequest extends Request
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return auth()->user()->isAdmin();
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'action_name' => 'sometimes|string',
|
||||
];
|
||||
}
|
||||
}
|
@ -8,14 +8,12 @@
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\Http\Requests\TaskScheduler;
|
||||
|
||||
use App\Http\Requests\Request;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class UpdateScheduleRequest extends Request
|
||||
class UpdateSchedulerRequest extends Request
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
@ -29,23 +27,17 @@ class UpdateScheduleRequest extends Request
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'paused' => 'sometimes|bool',
|
||||
'repeat_every' => 'sometimes|string|in:DAY,WEEK,BIWEEKLY,MONTH,3MONTHS,YEAR',
|
||||
'start_from' => 'sometimes',
|
||||
'scheduled_run'=>'sometimes',
|
||||
|
||||
$rules = [
|
||||
'name' => ['bail', 'sometimes', Rule::unique('schedulers')->where('company_id', auth()->user()->company()->id)->ignore($this->task_scheduler->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 prepareForValidation()
|
||||
{
|
||||
$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);
|
||||
return $rules;
|
||||
|
||||
}
|
||||
}
|
@ -56,6 +56,7 @@ class ClientLedgerBalanceUpdate implements ShouldQueue
|
||||
if ($company_ledger->balance == 0)
|
||||
{
|
||||
|
||||
|
||||
$last_record = CompanyLedger::where('client_id', $company_ledger->client_id)
|
||||
->where('company_id', $company_ledger->company_id)
|
||||
->where('balance', '!=', 0)
|
||||
@ -69,15 +70,12 @@ class ClientLedgerBalanceUpdate implements ShouldQueue
|
||||
->first();
|
||||
}
|
||||
|
||||
// nlog("Updating Balance NOW");
|
||||
}
|
||||
|
||||
$company_ledger->balance = $last_record->balance + $company_ledger->adjustment;
|
||||
$company_ledger->save();
|
||||
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
|
||||
// nlog("Updating company ledger for client ". $this->client->id);
|
||||
}
|
||||
}
|
||||
|
@ -450,16 +450,6 @@ class NinjaMailerJob implements ShouldQueue
|
||||
|
||||
$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()}");
|
||||
|
||||
$google = (new Google())->init();
|
||||
|
@ -21,6 +21,7 @@ use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
//@rebuild it
|
||||
class TaskScheduler implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
@ -45,9 +46,10 @@ class TaskScheduler implements ShouldQueue
|
||||
MultiDB::setDB($db);
|
||||
|
||||
Scheduler::with('company')
|
||||
->where('paused', false)
|
||||
->where('is_paused', false)
|
||||
->where('is_deleted', false)
|
||||
->where('scheduled_run', '<', now())
|
||||
->whereNotNull('next_run')
|
||||
->where('next_run', '<=', now())
|
||||
->cursor()
|
||||
->each(function ($scheduler) {
|
||||
$this->doJob($scheduler);
|
||||
@ -57,59 +59,16 @@ class TaskScheduler implements ShouldQueue
|
||||
|
||||
private function doJob(Scheduler $scheduler)
|
||||
{
|
||||
nlog("Doing job {$scheduler->action_name}");
|
||||
|
||||
$company = $scheduler->company;
|
||||
|
||||
$parameters = $scheduler->parameters;
|
||||
|
||||
switch ($scheduler->action_name) {
|
||||
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;
|
||||
|
||||
nlog("Doing job {$scheduler->name}");
|
||||
|
||||
try {
|
||||
$scheduler->service()->runTask();
|
||||
}
|
||||
catch(\Exception $e){
|
||||
nlog($e->getMessage());
|
||||
|
||||
}
|
||||
|
||||
$scheduler->scheduled_run = $scheduler->nextScheduledDate();
|
||||
$scheduler->save();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -66,13 +66,6 @@ class SendRecurring implements ShouldQueue
|
||||
// Generate Standard Invoice
|
||||
$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');
|
||||
|
||||
nlog("Recurring Invoice Date Set on Invoice = {$invoice->date} - ". now()->format('Y-m-d'));
|
||||
@ -94,6 +87,14 @@ class SendRecurring implements ShouldQueue
|
||||
->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);
|
||||
|
||||
/* 09-01-2022 ensure we create the PDFs at this point in time! */
|
||||
|
94
app/Mail/Client/ClientStatement.php
Normal file
94
app/Mail/Client/ClientStatement.php
Normal file
@ -0,0 +1,94 @@
|
||||
<?php
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\Mail\Client;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Mail\Attachment;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Mail\Mailables\Content;
|
||||
use Illuminate\Mail\Mailables\Envelope;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class ClientStatement extends Mailable
|
||||
{
|
||||
|
||||
/**
|
||||
* Create a new message instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(public array $data){}
|
||||
|
||||
/**
|
||||
* Get the message envelope.
|
||||
*
|
||||
* @return \Illuminate\Mail\Mailables\Envelope
|
||||
*/
|
||||
public function envelope()
|
||||
{
|
||||
return new Envelope(
|
||||
subject: $this->data['subject'],
|
||||
tags: [$this->data['company_key']],
|
||||
replyTo: $this->data['reply_to'],
|
||||
from: $this->data['from'],
|
||||
to: $this->data['to'],
|
||||
bcc: $this->data['bcc']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message content definition.
|
||||
*
|
||||
* @return \Illuminate\Mail\Mailables\Content
|
||||
*/
|
||||
public function content()
|
||||
{
|
||||
return new Content(
|
||||
view: 'email.template.client',
|
||||
text: 'email.template.text',
|
||||
with: [
|
||||
'text_body' => $this->data['body'],
|
||||
'body' => $this->data['body'],
|
||||
'whitelabel' => $this->data['whitelabel'],
|
||||
'settings' => $this->data['settings'],
|
||||
'whitelabel' => $this->data['whitelabel'],
|
||||
'logo' => $this->data['logo'],
|
||||
'signature' => $this->data['signature'],
|
||||
'company' => $this->data['company'],
|
||||
'greeting' => $this->data['greeting'],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the attachments for the message.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function attachments()
|
||||
{
|
||||
$array_of_attachments = [];
|
||||
|
||||
foreach($this->data['attachments'] as $attachment)
|
||||
{
|
||||
|
||||
$array_of_attachments[] =
|
||||
Attachment::fromData(fn () => base64_decode($attachment['file']), $attachment['name'])
|
||||
->withMime('application/pdf');
|
||||
|
||||
}
|
||||
|
||||
return $array_of_attachments;
|
||||
|
||||
}
|
||||
}
|
@ -170,6 +170,7 @@ class BaseModel extends Model
|
||||
*/
|
||||
public function resolveRouteBinding($value, $field = null)
|
||||
{
|
||||
|
||||
if (is_numeric($value)) {
|
||||
throw new ModelNotFoundException("Record with value {$value} not found");
|
||||
}
|
||||
|
@ -66,6 +66,7 @@ class Company extends BaseModel
|
||||
protected $presenter = CompanyPresenter::class;
|
||||
|
||||
protected $fillable = [
|
||||
'invoice_task_hours',
|
||||
'markdown_enabled',
|
||||
'calculate_expense_tax_by_amount',
|
||||
'invoice_expense_documents',
|
||||
|
@ -11,8 +11,7 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Services\TaskScheduler\TaskSchedulerService;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use App\Services\Scheduler\SchedulerService;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
@ -20,8 +19,8 @@ use Illuminate\Support\Carbon;
|
||||
* @property bool paused
|
||||
* @property bool is_deleted
|
||||
* @property \Carbon\Carbon|mixed start_from
|
||||
* @property string repeat_every
|
||||
* @property \Carbon\Carbon|mixed scheduled_run
|
||||
* @property int frequency_id
|
||||
* @property \Carbon\Carbon|mixed next_run
|
||||
* @property int company_id
|
||||
* @property int updated_at
|
||||
* @property int created_at
|
||||
@ -33,76 +32,38 @@ use Illuminate\Support\Carbon;
|
||||
*/
|
||||
class Scheduler extends BaseModel
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
use SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'start_from',
|
||||
'paused',
|
||||
'repeat_every',
|
||||
'name',
|
||||
'frequency_id',
|
||||
'next_run',
|
||||
'scheduled_run',
|
||||
'action_class',
|
||||
'action_name',
|
||||
'template',
|
||||
'is_paused',
|
||||
'parameters',
|
||||
'company_id',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'start_from' => 'timestamp',
|
||||
'scheduled_run' => 'timestamp',
|
||||
'next_run' => 'datetime',
|
||||
'created_at' => 'timestamp',
|
||||
'updated_at' => 'timestamp',
|
||||
'deleted_at' => 'timestamp',
|
||||
'paused' => 'boolean',
|
||||
'is_paused' => 'boolean',
|
||||
'is_deleted' => 'boolean',
|
||||
'parameters' => 'array',
|
||||
];
|
||||
|
||||
const DAILY = 'DAY';
|
||||
|
||||
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';
|
||||
protected $appends = [
|
||||
'hashed_id',
|
||||
];
|
||||
|
||||
/**
|
||||
* 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
|
||||
@ -110,43 +71,43 @@ class Scheduler extends BaseModel
|
||||
return $this->belongsTo(Company::class);
|
||||
}
|
||||
|
||||
public function nextScheduledDate(): ?Carbon
|
||||
{
|
||||
$offset = 0;
|
||||
// public function nextScheduledDate(): ?Carbon
|
||||
// {
|
||||
// $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) {
|
||||
$timezone = $this->company->timezone();
|
||||
// if ($entity_send_time != 0) {
|
||||
// $timezone = $this->company->timezone();
|
||||
|
||||
$offset -= $timezone->utc_offset;
|
||||
$offset += ($entity_send_time * 3600);
|
||||
}
|
||||
// $offset -= $timezone->utc_offset;
|
||||
// $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
|
||||
to add ON a day - a day = 86400 seconds
|
||||
*/
|
||||
// /*
|
||||
// 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
|
||||
// */
|
||||
|
||||
if ($offset < 0) {
|
||||
$offset += 86400;
|
||||
}
|
||||
// if ($offset < 0) {
|
||||
// $offset += 86400;
|
||||
// }
|
||||
|
||||
switch ($this->repeat_every) {
|
||||
case self::DAILY:
|
||||
return Carbon::parse($this->scheduled_run)->startOfDay()->addDay()->addSeconds($offset);
|
||||
case self::WEEKLY:
|
||||
return Carbon::parse($this->scheduled_run)->startOfDay()->addWeek()->addSeconds($offset);
|
||||
case self::BIWEEKLY:
|
||||
return Carbon::parse($this->scheduled_run)->startOfDay()->addWeeks(2)->addSeconds($offset);
|
||||
case self::MONTHLY:
|
||||
return Carbon::parse($this->scheduled_run)->startOfDay()->addMonthNoOverflow()->addSeconds($offset);
|
||||
case self::QUARTERLY:
|
||||
return Carbon::parse($this->scheduled_run)->startOfDay()->addMonthsNoOverflow(3)->addSeconds($offset);
|
||||
case self::ANNUALLY:
|
||||
return Carbon::parse($this->scheduled_run)->startOfDay()->addYearNoOverflow()->addSeconds($offset);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
// switch ($this->repeat_every) {
|
||||
// case self::DAILY:
|
||||
// return Carbon::parse($this->scheduled_run)->startOfDay()->addDay()->addSeconds($offset);
|
||||
// case self::WEEKLY:
|
||||
// return Carbon::parse($this->scheduled_run)->startOfDay()->addWeek()->addSeconds($offset);
|
||||
// case self::BIWEEKLY:
|
||||
// return Carbon::parse($this->scheduled_run)->startOfDay()->addWeeks(2)->addSeconds($offset);
|
||||
// case self::MONTHLY:
|
||||
// return Carbon::parse($this->scheduled_run)->startOfDay()->addMonthNoOverflow()->addSeconds($offset);
|
||||
// case self::QUARTERLY:
|
||||
// return Carbon::parse($this->scheduled_run)->startOfDay()->addMonthsNoOverflow(3)->addSeconds($offset);
|
||||
// case self::ANNUALLY:
|
||||
// return Carbon::parse($this->scheduled_run)->startOfDay()->addYearNoOverflow()->addSeconds($offset);
|
||||
// default:
|
||||
// return null;
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
@ -90,9 +90,7 @@ trait Utilities
|
||||
nlog("checkout failure");
|
||||
nlog($_payment);
|
||||
|
||||
if (is_array($_payment) && array_key_exists('actions', $_payment) && array_key_exists('response_summary', end($_payment['actions']))) {
|
||||
$error_message = end($_payment['actions'])['response_summary'];
|
||||
} elseif (is_array($_payment) && array_key_exists('status', $_payment)) {
|
||||
if (is_array($_payment) && array_key_exists('status', $_payment)) {
|
||||
$error_message = $_payment['status'];
|
||||
}
|
||||
else {
|
||||
|
31
app/Policies/SchedulerPolicy.php
Normal file
31
app/Policies/SchedulerPolicy.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\User;
|
||||
|
||||
/**
|
||||
* Class SchedulerPolicy.
|
||||
*/
|
||||
class SchedulerPolicy extends EntityPolicy
|
||||
{
|
||||
/**
|
||||
* Checks if the user has create permissions.
|
||||
*
|
||||
* @param User $user
|
||||
* @return bool
|
||||
*/
|
||||
public function create(User $user) : bool
|
||||
{
|
||||
return $user->isAdmin();
|
||||
}
|
||||
}
|
@ -36,6 +36,7 @@ use App\Models\Quote;
|
||||
use App\Models\RecurringExpense;
|
||||
use App\Models\RecurringInvoice;
|
||||
use App\Models\RecurringQuote;
|
||||
use App\Models\Scheduler;
|
||||
use App\Models\Subscription;
|
||||
use App\Models\Task;
|
||||
use App\Models\TaskStatus;
|
||||
@ -67,6 +68,7 @@ use App\Policies\QuotePolicy;
|
||||
use App\Policies\RecurringExpensePolicy;
|
||||
use App\Policies\RecurringInvoicePolicy;
|
||||
use App\Policies\RecurringQuotePolicy;
|
||||
use App\Policies\SchedulerPolicy;
|
||||
use App\Policies\SubscriptionPolicy;
|
||||
use App\Policies\TaskPolicy;
|
||||
use App\Policies\TaskStatusPolicy;
|
||||
@ -109,6 +111,7 @@ class AuthServiceProvider extends ServiceProvider
|
||||
RecurringExpense::class => RecurringExpensePolicy::class,
|
||||
RecurringInvoice::class => RecurringInvoicePolicy::class,
|
||||
RecurringQuote::class => RecurringQuotePolicy::class,
|
||||
Scheduler::class => SchedulerPolicy::class,
|
||||
Subscription::class => SubscriptionPolicy::class,
|
||||
Task::class => TaskPolicy::class,
|
||||
TaskStatus::class => TaskStatusPolicy::class,
|
||||
|
@ -11,7 +11,9 @@
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Models\Scheduler;
|
||||
use App\Utils\Traits\MakesHash;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
@ -27,6 +29,21 @@ class RouteServiceProvider extends ServiceProvider
|
||||
public function boot()
|
||||
{
|
||||
parent::boot();
|
||||
|
||||
|
||||
Route::bind('task_scheduler', function ($value) {
|
||||
|
||||
if (is_numeric($value)) {
|
||||
throw new ModelNotFoundException("Record with value {$value} not found");
|
||||
}
|
||||
|
||||
return Scheduler::query()
|
||||
->withTrashed()
|
||||
->where('id', $this->decodePrimaryKey($value))->firstOrFail();
|
||||
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
38
app/Repositories/SchedulerRepository.php
Normal file
38
app/Repositories/SchedulerRepository.php
Normal file
@ -0,0 +1,38 @@
|
||||
<?php
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\Repositories;
|
||||
|
||||
use App\Models\Scheduler;
|
||||
|
||||
class SchedulerRepository extends BaseRepository
|
||||
{
|
||||
|
||||
/**
|
||||
* Saves the scheduler.
|
||||
*
|
||||
* @param array $data The data
|
||||
* @param \App\Models\Scheduler $scheduler The scheduler
|
||||
*
|
||||
* @return \App\Models\Scheduler
|
||||
*/
|
||||
public function save(array $data, Scheduler $scheduler): Scheduler
|
||||
{
|
||||
|
||||
$scheduler->fill($data);
|
||||
|
||||
$scheduler->save();
|
||||
|
||||
return $scheduler;
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\Repositories;
|
||||
|
||||
class TaskSchedulerRepository extends BaseRepository
|
||||
{
|
||||
}
|
@ -34,15 +34,7 @@ class BankMatchingService implements ShouldQueue, ShouldBeUnique
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $company_id;
|
||||
|
||||
protected $db;
|
||||
|
||||
public function __construct($company_id, $db)
|
||||
{
|
||||
$this->company_id = $company_id;
|
||||
$this->db = $db;
|
||||
}
|
||||
public function __construct(public $company_id, public $db){}
|
||||
|
||||
public function handle() :void
|
||||
{
|
||||
|
313
app/Services/Email/EmailDefaults.php
Normal file
313
app/Services/Email/EmailDefaults.php
Normal file
@ -0,0 +1,313 @@
|
||||
<?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
|
||||
{
|
||||
/**
|
||||
* The settings object for this email
|
||||
* @var CompanySettings $settings
|
||||
*/
|
||||
protected $settings;
|
||||
|
||||
/**
|
||||
* The HTML / Template to use for this email
|
||||
* @var string $template
|
||||
*/
|
||||
private string $template;
|
||||
|
||||
/**
|
||||
* The locale to use for
|
||||
* translations for this email
|
||||
*/
|
||||
private string $locale;
|
||||
|
||||
/**
|
||||
* @param EmailService $email_service The email service class
|
||||
* @param EmailObject $email_object the email object class
|
||||
*/
|
||||
public function __construct(protected EmailService $email_service, public EmailObject $email_object){}
|
||||
|
||||
/**
|
||||
* Entry point for generating
|
||||
* the defaults for the email object
|
||||
*
|
||||
* @return EmailObject $email_object The 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;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the meta data for the 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;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the locale
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the template
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the FROM address
|
||||
*/
|
||||
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;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the body of the email
|
||||
*/
|
||||
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;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the subject of the email
|
||||
*/
|
||||
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;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the reply to of the email
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the template placeholders
|
||||
* with variable values.
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the BCC of the email
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the CC of the email
|
||||
* @todo at some point....
|
||||
*/
|
||||
private function buildCc()
|
||||
{
|
||||
return [
|
||||
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the attachments for the email
|
||||
*
|
||||
* Note that we base64 encode these, as they
|
||||
* sometimes may not survive serialization.
|
||||
*
|
||||
* We decode these in the Mailable later
|
||||
*/
|
||||
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;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the headers for the email
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts any markdown to HTML in the email
|
||||
*
|
||||
* @param string $markdown The body to convert
|
||||
* @return string The parsed markdown response
|
||||
*/
|
||||
private function parseMarkdownToHtml(string $markdown): ?string
|
||||
{
|
||||
$converter = new CommonMarkConverter([
|
||||
'allow_unsafe_links' => false,
|
||||
]);
|
||||
|
||||
return $converter->convert($markdown);
|
||||
}
|
||||
|
||||
}
|
109
app/Services/Email/EmailMailable.php
Normal file
109
app/Services/Email/EmailMailable.php
Normal file
@ -0,0 +1,109 @@
|
||||
<?php
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\Services\Email;
|
||||
|
||||
use App\Services\Email\EmailObject;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Mail\Attachment;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Mail\Mailables\Content;
|
||||
use Illuminate\Mail\Mailables\Envelope;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Mail\Mailables\Headers;
|
||||
|
||||
class EmailMailable extends Mailable
|
||||
{
|
||||
|
||||
/**
|
||||
* Create a new message instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(public EmailObject $email_object){}
|
||||
|
||||
/**
|
||||
* Get the message envelope.
|
||||
*
|
||||
* @return \Illuminate\Mail\Mailables\Envelope
|
||||
*/
|
||||
public function envelope()
|
||||
{
|
||||
return new Envelope(
|
||||
subject: $this->email_object->subject,
|
||||
tags: [$this->email_object->company_key],
|
||||
replyTo: $this->email_object->reply_to,
|
||||
from: $this->email_object->from,
|
||||
to: $this->email_object->to,
|
||||
bcc: $this->email_object->bcc
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message content definition.
|
||||
*
|
||||
* @return \Illuminate\Mail\Mailables\Content
|
||||
*/
|
||||
public function content()
|
||||
{
|
||||
return new Content(
|
||||
view: $this->email_object->html_template,
|
||||
text: $this->email_object->text_template,
|
||||
with: [
|
||||
'text_body' => strip_tags($this->email_object->body), //@todo this is a bit hacky here.
|
||||
'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,
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
}
|
515
app/Services/Email/EmailMailer.php
Normal file
515
app/Services/Email/EmailMailer.php
Normal file
@ -0,0 +1,515 @@
|
||||
<?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);
|
||||
|
||||
/* Perform final checks */
|
||||
if($this->email_service->preFlightChecksFail())
|
||||
return;
|
||||
|
||||
/* Boot the required driver*/
|
||||
$this->setMailDriver();
|
||||
|
||||
/* Init the mailer*/
|
||||
$mailer = Mail::mailer($this->mailer);
|
||||
|
||||
/* Additional configuration if using a client third party mailer */
|
||||
if($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);
|
||||
|
||||
/* Attempt the send! */
|
||||
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;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Entity notification when an email fails to send
|
||||
*
|
||||
* @todo - rewrite this
|
||||
* @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);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the mail driver to use and applies any specific configuration
|
||||
* the the mailable
|
||||
*/
|
||||
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;
|
||||
|
||||
if($sending_user == "0")
|
||||
$user = $this->email_service->company->owner();
|
||||
else
|
||||
$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 mixed
|
||||
*/
|
||||
private function refreshOfficeToken(User $user): mixed
|
||||
{
|
||||
$expiry = $user->oauth_user_token_expiry ?: now()->subDay();
|
||||
|
||||
if($expiry->lt(now()))
|
||||
{
|
||||
$guzzle = new \GuzzleHttp\Client();
|
||||
$url = 'https://login.microsoftonline.com/common/oauth2/v2.0/token';
|
||||
|
||||
$token = json_decode($guzzle->post($url, [
|
||||
'form_params' => [
|
||||
'client_id' => config('ninja.o365.client_id') ,
|
||||
'client_secret' => config('ninja.o365.client_secret') ,
|
||||
'scope' => 'email Mail.Send offline_access profile User.Read openid',
|
||||
'grant_type' => 'refresh_token',
|
||||
'refresh_token' => $user->oauth_user_refresh_token
|
||||
],
|
||||
])->getBody()->getContents());
|
||||
|
||||
if($token){
|
||||
|
||||
$user->oauth_user_refresh_token = property_exists($token, 'refresh_token') ? $token->refresh_token : $user->oauth_user_refresh_token;
|
||||
$user->oauth_user_token = $token->access_token;
|
||||
$user->oauth_user_token_expiry = now()->addSeconds($token->expires_in);
|
||||
$user->save();
|
||||
|
||||
return $token->access_token;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return $user->oauth_user_token;
|
||||
|
||||
}
|
||||
|
||||
public function failed($exception = null)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
}
|
84
app/Services/Email/EmailObject.php
Normal file
84
app/Services/Email/EmailObject.php
Normal file
@ -0,0 +1,84 @@
|
||||
<?php
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\Services\Email;
|
||||
|
||||
use App\Models\Client;
|
||||
use App\Models\ClientContact;
|
||||
use App\Models\Company;
|
||||
use App\Models\User;
|
||||
use App\Models\Vendor;
|
||||
use App\Models\VendorContact;
|
||||
use Illuminate\Mail\Mailables\Address;
|
||||
|
||||
/**
|
||||
* EmailObject.
|
||||
*/
|
||||
class EmailObject
|
||||
{
|
||||
|
||||
public array $to = [];
|
||||
|
||||
public ?Address $from = null;
|
||||
|
||||
public array $reply_to = [];
|
||||
|
||||
public array $cc = [];
|
||||
|
||||
public array $bcc = [];
|
||||
|
||||
public ?string $subject = null;
|
||||
|
||||
public ?string $body = null;
|
||||
|
||||
public array $attachments = [];
|
||||
|
||||
public string $company_key;
|
||||
|
||||
public ?object $settings = null;
|
||||
|
||||
public bool $whitelabel = false;
|
||||
|
||||
public ?string $logo = null;
|
||||
|
||||
public ?string $signature = null;
|
||||
|
||||
public ?string $greeting = null;
|
||||
|
||||
public ?Client $client = null;
|
||||
|
||||
public ?Vendor $vendor = null;
|
||||
|
||||
public ?User $user = null;
|
||||
|
||||
public ?ClientContact $client_contact = null;
|
||||
|
||||
public ?VendorContact $vendor_contact = null;
|
||||
|
||||
public ?string $email_template_body = null;
|
||||
|
||||
public ?string $email_template_subject = null;
|
||||
|
||||
public ?string $html_template = null;
|
||||
|
||||
public ?string $text_template = 'email.template.text';
|
||||
|
||||
public array $headers = [];
|
||||
|
||||
public ?string $invitation_key = null;
|
||||
|
||||
public ?int $entity_id = null;
|
||||
|
||||
public ?string $entity_class = null;
|
||||
|
||||
public array $variables = [];
|
||||
|
||||
}
|
155
app/Services/Email/EmailService.php
Normal file
155
app/Services/Email/EmailService.php
Normal file
@ -0,0 +1,155 @@
|
||||
<?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 App\Utils\Ninja;
|
||||
use Illuminate\Mail\Mailable;
|
||||
|
||||
class EmailService
|
||||
{
|
||||
|
||||
/**
|
||||
* Used to flag whether we force send the email regardless
|
||||
*
|
||||
* @var bool $override;
|
||||
*/
|
||||
protected bool $override;
|
||||
|
||||
public Mailable $mailable;
|
||||
|
||||
public function __construct(public EmailObject $email_object, public Company $company){}
|
||||
|
||||
/**
|
||||
* Sends the email via a dispatched job
|
||||
* @param boolean $override Whether the email should send regardless
|
||||
* @return void
|
||||
*/
|
||||
public function send($override = false) :void
|
||||
{
|
||||
$this->override = $override;
|
||||
|
||||
$this->setDefaults()
|
||||
->updateMailable()
|
||||
->email();
|
||||
}
|
||||
|
||||
public function sendNow($force = false) :void
|
||||
{
|
||||
$this->setDefaults()
|
||||
->updateMailable()
|
||||
->email($force);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* On the hosted platform we scan all outbound email for
|
||||
* spam. This sequence processes the filters we use on all
|
||||
* emails.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function preFlightChecksFail(): bool
|
||||
{
|
||||
|
||||
/* If we are migrating data we don't want to fire any emails */
|
||||
if($this->company->is_disabled && !$this->override)
|
||||
return true;
|
||||
|
||||
/* To handle spam users we drop all emails from flagged accounts */
|
||||
if(Ninja::isHosted() && $this->company->account && $this->company->account->is_flagged)
|
||||
return true;
|
||||
|
||||
/* On the hosted platform we set default contacts a @example.com email address - we shouldn't send emails to these types of addresses */
|
||||
if(Ninja::isHosted() && $this->hasValidEmails())
|
||||
return true;
|
||||
|
||||
/* GMail users are uncapped */
|
||||
if(Ninja::isHosted() && in_array($this->email_object->settings->email_sending_method, ['gmail', 'office365', 'client_postmark', 'client_mailgun']))
|
||||
return false;
|
||||
|
||||
/* On the hosted platform, if the user is over the email quotas, we do not send the email. */
|
||||
if(Ninja::isHosted() && $this->company->account && $this->company->account->emailQuotaExceeded())
|
||||
return true;
|
||||
|
||||
/* If the account is verified, we allow emails to flow */
|
||||
if(Ninja::isHosted() && $this->company->account && $this->company->account->is_verified_account) {
|
||||
|
||||
//11-01-2022
|
||||
|
||||
/* Continue to analyse verified accounts in case they later start sending poor quality emails*/
|
||||
// if(class_exists(\Modules\Admin\Jobs\Account\EmailQuality::class))
|
||||
// (new \Modules\Admin\Jobs\Account\EmailQuality($this->nmo, $this->company))->run();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/* On the hosted platform if the user has not verified their account we fail here - but still check what they are trying to send! */
|
||||
if(Ninja::isHosted() && $this->company->account && !$this->company->account->account_sms_verified){
|
||||
|
||||
if(class_exists(\Modules\Admin\Jobs\Account\EmailFilter::class))
|
||||
return (new \Modules\Admin\Jobs\Account\EmailFilter($this->email_object, $this->company))->run();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/* On the hosted platform we actively scan all outbound emails to ensure outbound email quality remains high */
|
||||
if(class_exists(\Modules\Admin\Jobs\Account\EmailFilter::class))
|
||||
return (new \Modules\Admin\Jobs\Account\EmailFilter($this->email_object, $this->company))->run();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function hasValidEmails(): bool
|
||||
{
|
||||
|
||||
foreach($this->email_object->to as $address_object)
|
||||
{
|
||||
|
||||
if(strpos($address_object->address, '@example.com') !== false)
|
||||
return true;
|
||||
|
||||
if(!str_contains($address_object->address, "@"))
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -35,12 +35,7 @@ class InvoiceService
|
||||
{
|
||||
use MakesHash;
|
||||
|
||||
public $invoice;
|
||||
|
||||
public function __construct($invoice)
|
||||
{
|
||||
$this->invoice = $invoice;
|
||||
}
|
||||
public function __construct(public Invoice $invoice){}
|
||||
|
||||
/**
|
||||
* Marks as invoice as paid
|
||||
@ -531,6 +526,10 @@ class InvoiceService
|
||||
$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') {
|
||||
$this->invoice->service()->applyNumber()->save();
|
||||
}
|
||||
|
@ -1,57 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\Services\Schedule;
|
||||
|
||||
class ScheduleService
|
||||
{
|
||||
|
||||
public function __construct(public Scheduler $scheduler) {}
|
||||
|
||||
public function scheduleStatement()
|
||||
{
|
||||
|
||||
//Is it for one client
|
||||
//Is it for all clients
|
||||
//Is it for all clients excluding these clients
|
||||
|
||||
//Frequency
|
||||
|
||||
//show aging
|
||||
//show payments
|
||||
//paid/unpaid
|
||||
|
||||
//When to send? 1st of month
|
||||
//End of month
|
||||
//This date
|
||||
|
||||
}
|
||||
|
||||
public function scheduleReport()
|
||||
{
|
||||
//Report type
|
||||
//same schema as ScheduleStatement
|
||||
}
|
||||
|
||||
public function scheduleEntitySend()
|
||||
{
|
||||
//Entity
|
||||
//Entity Id
|
||||
//When
|
||||
}
|
||||
|
||||
public function projectStatus()
|
||||
{
|
||||
//Project ID
|
||||
//Tasks - task statuses
|
||||
}
|
||||
|
||||
}
|
129
app/Services/Scheduler/SchedulerService.php
Normal file
129
app/Services/Scheduler/SchedulerService.php
Normal file
@ -0,0 +1,129 @@
|
||||
<?php
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\Services\Scheduler;
|
||||
|
||||
use App\DataMapper\EmailTemplateDefaults;
|
||||
use App\Mail\Client\ClientStatement;
|
||||
use App\Models\Client;
|
||||
use App\Models\Scheduler;
|
||||
use App\Services\Email\EmailMailable;
|
||||
use App\Services\Email\EmailObject;
|
||||
use App\Services\Email\EmailService;
|
||||
use App\Utils\Ninja;
|
||||
use App\Utils\Traits\MakesDates;
|
||||
use App\Utils\Traits\MakesHash;
|
||||
use Illuminate\Mail\Mailables\Address;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class SchedulerService
|
||||
{
|
||||
use MakesHash;
|
||||
use MakesDates;
|
||||
|
||||
private string $method;
|
||||
|
||||
private Client $client;
|
||||
|
||||
public function __construct(public Scheduler $scheduler) {}
|
||||
|
||||
/**
|
||||
* Called from the TaskScheduler Cron
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function runTask(): void
|
||||
{
|
||||
$this->{$this->scheduler->template}();
|
||||
}
|
||||
|
||||
private function client_statement()
|
||||
{
|
||||
$query = Client::query()
|
||||
->where('company_id', $this->scheduler->company_id);
|
||||
|
||||
//Email only the selected clients
|
||||
if(count($this->scheduler->parameters['clients']) >= 1)
|
||||
$query->where('id', $this->transformKeys($this->scheduler->parameters['clients']));
|
||||
|
||||
|
||||
$query->cursor()
|
||||
->each(function ($_client){
|
||||
|
||||
$this->client = $_client;
|
||||
$statement_properties = $this->calculateStatementProperties();
|
||||
|
||||
//work out the date range
|
||||
$pdf = $_client->service()->statement($statement_properties);
|
||||
|
||||
$email_service = new EmailService($this->buildMailableData($pdf), $_client->company);
|
||||
$email_service->send();
|
||||
|
||||
//calculate next run dates;
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
private function calculateStatementProperties()
|
||||
{
|
||||
$start_end = $this->calculateStartAndEndDates();
|
||||
|
||||
$this->client_start_date = $this->translateDate($start_end[0], $this->client->date_format(), $this->client->locale());
|
||||
$this->client_end_date = $this->translateDate($start_end[1], $this->client->date_format(), $this->client->locale());
|
||||
|
||||
return [
|
||||
'start_date' =>$start_end[0],
|
||||
'end_date' =>$start_end[1],
|
||||
'show_payments_table' => $this->scheduler->parameters['show_payments_table'],
|
||||
'show_aging_table' => $this->scheduler->parameters['show_aging_table'],
|
||||
'status' => $this->scheduler->parameters['status']
|
||||
];
|
||||
|
||||
}
|
||||
|
||||
private function calculateStartAndEndDates()
|
||||
{
|
||||
return match ($this->scheduler->parameters['date_range']) {
|
||||
'this_month' => [now()->firstOfMonth()->format('Y-m-d'), now()->lastOfMonth()->format('Y-m-d')],
|
||||
'this_quarter' => [now()->firstOfQuarter()->format('Y-m-d'), now()->lastOfQuarter()->format('Y-m-d')],
|
||||
'this_year' => [now()->firstOfYear()->format('Y-m-d'), now()->lastOfYear()->format('Y-m-d')],
|
||||
'previous_month' => [now()->subMonth()->firstOfMonth()->format('Y-m-d'), now()->subMonth()->lastOfMonth()->format('Y-m-d')],
|
||||
'previous_quarter' => [now()->subQuarter()->firstOfQuarter()->format('Y-m-d'), now()->subQuarter()->lastOfQuarter()->format('Y-m-d')],
|
||||
'previous_year' => [now()->subYear()->firstOfYear()->format('Y-m-d'), now()->subYear()->lastOfYear()->format('Y-m-d')],
|
||||
'custom_range' => [$this->scheduler->parameters['start_date'], $this->scheduler->parameters['end_date']],
|
||||
default => [now()->firstOfMonth()->format('Y-m-d'), now()->lastOfMonth()->format('Y-m-d')],
|
||||
};
|
||||
}
|
||||
|
||||
private function buildMailableData($pdf)
|
||||
{
|
||||
|
||||
$email_object = new EmailObject;
|
||||
$email_object->to = [new Address($this->client->present()->email(), $this->client->present()->name())];
|
||||
$email_object->attachments = [['file' => base64_encode($pdf), 'name' => ctrans('texts.statement') . ".pdf"]];
|
||||
$email_object->settings = $this->client->getMergedSettings();
|
||||
$email_object->company = $this->client->company;
|
||||
$email_object->client = $this->client;
|
||||
$email_object->email_template_subject = 'email_subject_statement';
|
||||
$email_object->email_template_body = 'email_template_statement';
|
||||
$email_object->variables = [
|
||||
'$client' => $this->client->present()->name(),
|
||||
'$start_date' => $this->client_start_date,
|
||||
'$end_date' => $this->client_end_date,
|
||||
];
|
||||
|
||||
return $email_object;
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -38,6 +38,8 @@ use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
|
||||
//@deprecated - never used....
|
||||
class TaskSchedulerService
|
||||
{
|
||||
|
||||
|
@ -194,6 +194,7 @@ class CompanyTransformer extends EntityTransformer
|
||||
'convert_payment_currency' => (bool) $company->convert_payment_currency,
|
||||
'convert_expense_currency' => (bool) $company->convert_expense_currency,
|
||||
'notify_vendor_when_paid' => (bool) $company->notify_vendor_when_paid,
|
||||
'invoice_task_hours' => (bool) $company->invoice_task_hours,
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -14,7 +14,7 @@ namespace App\Transformers;
|
||||
use App\Models\Scheduler;
|
||||
use App\Utils\Traits\MakesHash;
|
||||
|
||||
class TaskSchedulerTransformer extends EntityTransformer
|
||||
class SchedulerTransformer extends EntityTransformer
|
||||
{
|
||||
use MakesHash;
|
||||
|
||||
@ -22,17 +22,17 @@ class TaskSchedulerTransformer extends EntityTransformer
|
||||
{
|
||||
return [
|
||||
'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,
|
||||
'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,
|
||||
'created_at' => (int) $scheduler->created_at,
|
||||
'archived_at' => (int) $scheduler->deleted_at,
|
||||
'action_name' => (string) $scheduler->action_name,
|
||||
'action_class' => (string) $scheduler->action_class,
|
||||
'parameters'=> (array) $scheduler->parameters,
|
||||
];
|
||||
}
|
||||
}
|
37
database/factories/SchedulerFactory.php
Normal file
37
database/factories/SchedulerFactory.php
Normal file
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\RecurringInvoice;
|
||||
use App\Models\Scheduler;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
class SchedulerFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function definition()
|
||||
{
|
||||
return [
|
||||
'name' => $this->faker->name(),
|
||||
'is_paused' => rand(0,1),
|
||||
'is_deleted' => rand(0,1),
|
||||
'parameters' => [],
|
||||
'frequency_id' => RecurringInvoice::FREQUENCY_MONTHLY,
|
||||
'next_run' => now()->addSeconds(rand(86400,8640000)),
|
||||
'template' => 'statement_task',
|
||||
];
|
||||
}
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
|
||||
Schema::table('accounts', function (Blueprint $table)
|
||||
{
|
||||
$table->boolean('is_trial')->default(false);
|
||||
});
|
||||
|
||||
Schema::table('companies', function (Blueprint $table)
|
||||
{
|
||||
$table->boolean('invoice_task_hours')->default(false);
|
||||
});
|
||||
|
||||
Schema::table('schedulers', function (Blueprint $table)
|
||||
{
|
||||
|
||||
$table->dropColumn('repeat_every');
|
||||
$table->dropColumn('start_from');
|
||||
$table->dropColumn('scheduled_run');
|
||||
$table->dropColumn('action_name');
|
||||
$table->dropColumn('action_class');
|
||||
$table->dropColumn('paused');
|
||||
$table->dropColumn('company_id');
|
||||
|
||||
});
|
||||
|
||||
|
||||
Schema::table('schedulers', function (Blueprint $table)
|
||||
{
|
||||
|
||||
$table->unsignedInteger('company_id');
|
||||
$table->boolean('is_paused')->default(false);
|
||||
$table->unsignedInteger('frequency_id')->nullable();
|
||||
$table->datetime('next_run')->nullable();
|
||||
$table->datetime('next_run_client')->nullable();
|
||||
$table->unsignedInteger('user_id');
|
||||
$table->string('name', 191);
|
||||
$table->string('template', 191);
|
||||
|
||||
$table->foreign('company_id')->references('id')->on('companies')->onDelete('cascade')->onUpdate('cascade');
|
||||
|
||||
$table->unique(['company_id', 'name']);
|
||||
$table->index(['company_id', 'deleted_at']);
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
//
|
||||
}
|
||||
};
|
@ -4924,6 +4924,7 @@ $LANG = array(
|
||||
'action_add_to_invoice' => 'Add To Invoice',
|
||||
'danger_zone' => 'Danger Zone',
|
||||
'import_completed' => 'Import completed',
|
||||
'client_statement_body' => 'Your statement from :start_date to :end_date is attached.'
|
||||
);
|
||||
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
@php
|
||||
$primary_color = isset($settings) ? $settings->primary_color : '#4caf50';
|
||||
$email_alignment = isset($settings) ? $settings->email_alignment : 'center';
|
||||
@endphp
|
||||
|
||||
|
||||
@ -60,7 +61,8 @@
|
||||
font-size: 13px;
|
||||
padding: 15px 50px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 30px;
|
||||
margin-bottom: 5px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
#content h1 {
|
||||
font-family: 'canada-type-gibson', 'roboto', Arial, Helvetica, sans-serif;
|
||||
@ -146,8 +148,8 @@
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td cellpadding="20">
|
||||
<div style="border: 1px solid #c2c2c2; border-top: none; border-bottom: none; padding: 20px; text-align: center" id="content">
|
||||
<td cellpadding="5">
|
||||
<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>
|
||||
|
||||
{{ $slot ?? '' }}
|
||||
@ -163,8 +165,8 @@
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td height="20">
|
||||
<div style="border: 1px solid #c2c2c2; border-top: none; border-bottom: none; padding: 20px; text-align: center" id="content"> </div>
|
||||
<td height="0">
|
||||
<div style="border: 1px solid #c2c2c2; border-top: none; border-bottom: none; padding: 5px; text-align: center" id="content"> </div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
@ -1,29 +1,32 @@
|
||||
<!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>
|
||||
<!-- Source: https://github.com/invoiceninja/invoiceninja -->
|
||||
<!-- Version: {{ config('ninja.app_version') }} -->
|
||||
<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') }}">
|
||||
<link rel="manifest" href="manifest.json?v={{ config('ninja.app_version') }}">
|
||||
<script src="{{ asset('js/pdf.min.js') }}"></script>
|
||||
@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>
|
||||
|
||||
<!-- Microsoft OAuth library -->
|
||||
<script type="text/javascript"
|
||||
src="https://alcdn.msauth.net/browser/2.14.2/js/msal-browser.min.js"
|
||||
integrity="sha384-ggh+EF1aSqm+Y4yvv2n17KpurNcZTeYtUZUvhPziElsstmIEubyEB6AIVpKLuZgr"
|
||||
crossorigin="anonymous">
|
||||
</script>
|
||||
|
||||
<!-- Google Tag Manager -->
|
||||
<!-- G Tag Manager -->
|
||||
<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],
|
||||
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);
|
||||
})(window,document,'script','dataLayer','GTM-WMJ5W23');</script>
|
||||
<!-- End Google Tag Manager -->
|
||||
<!-- End G Tag Manager -->
|
||||
|
||||
@endif
|
||||
<script type="text/javascript">
|
||||
|
@ -275,7 +275,8 @@ Route::group(['middleware' => ['throttle:300,1', 'api_db', 'token_auth', 'locale
|
||||
Route::post('reports/tasks', TaskReportController::class);
|
||||
Route::post('reports/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::post('support/messages/send', SendingController::class);
|
||||
|
365
tests/Feature/Scheduler/SchedulerTest.php
Normal file
365
tests/Feature/Scheduler/SchedulerTest.php
Normal file
@ -0,0 +1,365 @@
|
||||
<?php
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace Tests\Feature\Scheduler;
|
||||
|
||||
use App\Export\CSV\ClientExport;
|
||||
use App\Models\RecurringInvoice;
|
||||
use App\Models\Scheduler;
|
||||
use App\Utils\Traits\MakesHash;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Foundation\Testing\WithoutEvents;
|
||||
use Illuminate\Routing\Middleware\ThrottleRequests;
|
||||
use Illuminate\Support\Facades\Session;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Tests\MockAccountData;
|
||||
use Tests\MockUnitData;
|
||||
use Tests\TestCase;
|
||||
|
||||
class SchedulerTest extends TestCase
|
||||
{
|
||||
use MakesHash;
|
||||
use MockAccountData;
|
||||
use WithoutEvents;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
Session::start();
|
||||
|
||||
$this->faker = \Faker\Factory::create();
|
||||
|
||||
Model::reguard();
|
||||
|
||||
$this->makeTestData();
|
||||
|
||||
$this->withoutMiddleware(
|
||||
ThrottleRequests::class
|
||||
);
|
||||
|
||||
$this->withoutExceptionHandling();
|
||||
}
|
||||
|
||||
|
||||
public function testGetThisMonthRange()
|
||||
{
|
||||
|
||||
$this->travelTo(Carbon::parse('2023-01-14'));
|
||||
|
||||
$this->assertEqualsCanonicalizing(['2023-01-01','2023-01-31'], $this->getDateRange('this_month'));
|
||||
$this->assertEqualsCanonicalizing(['2023-01-01','2023-03-31'], $this->getDateRange('this_quarter'));
|
||||
$this->assertEqualsCanonicalizing(['2023-01-01','2023-12-31'], $this->getDateRange('this_year'));
|
||||
|
||||
$this->assertEqualsCanonicalizing(['2022-12-01','2022-12-31'], $this->getDateRange('previous_month'));
|
||||
$this->assertEqualsCanonicalizing(['2022-10-01','2022-12-31'], $this->getDateRange('previous_quarter'));
|
||||
$this->assertEqualsCanonicalizing(['2022-01-01','2022-12-31'], $this->getDateRange('previous_year'));
|
||||
|
||||
$this->travelBack();
|
||||
|
||||
}
|
||||
|
||||
private function getDateRange($range)
|
||||
{
|
||||
return match ($range) {
|
||||
'this_month' => [now()->firstOfMonth()->format('Y-m-d'), now()->lastOfMonth()->format('Y-m-d')],
|
||||
'this_quarter' => [now()->firstOfQuarter()->format('Y-m-d'), now()->lastOfQuarter()->format('Y-m-d')],
|
||||
'this_year' => [now()->firstOfYear()->format('Y-m-d'), now()->lastOfYear()->format('Y-m-d')],
|
||||
'previous_month' => [now()->subMonth()->firstOfMonth()->format('Y-m-d'), now()->subMonth()->lastOfMonth()->format('Y-m-d')],
|
||||
'previous_quarter' => [now()->subQuarter()->firstOfQuarter()->format('Y-m-d'), now()->subQuarter()->lastOfQuarter()->format('Y-m-d')],
|
||||
'previous_year' => [now()->subYear()->firstOfYear()->format('Y-m-d'), now()->subYear()->lastOfYear()->format('Y-m-d')],
|
||||
'custom_range' => [$this->scheduler->parameters['start_date'], $this->scheduler->parameters['end_date']]
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 'name' => ['bail', 'required', Rule::unique('schedulers')->where('company_id', auth()->user()->company()->id)],
|
||||
'is_paused' => 'bail|sometimes|boolean',
|
||||
'frequency_id' => 'bail|required|integer|digits_between:1,12',
|
||||
'next_run' => 'bail|required|date:Y-m-d',
|
||||
'template' => 'bail|required|string',
|
||||
'parameters' => 'bail|array',
|
||||
*/
|
||||
|
||||
public function testClientStatementGeneration()
|
||||
{
|
||||
$data = [
|
||||
'name' => 'A test statement scheduler',
|
||||
'frequency_id' => RecurringInvoice::FREQUENCY_MONTHLY,
|
||||
'next_run' => '2023-01-14',
|
||||
'template' => 'client_statement',
|
||||
'parameters' => [
|
||||
'date_range' => 'last_month',
|
||||
'show_payments_table' => true,
|
||||
'show_aging_table' => true,
|
||||
'status' => 'paid',
|
||||
'clients' => [],
|
||||
],
|
||||
];
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'X-API-SECRET' => config('ninja.api_secret'),
|
||||
'X-API-TOKEN' => $this->token,
|
||||
])->postJson('/api/v1/task_schedulers', $data);
|
||||
|
||||
$response->assertStatus(200);
|
||||
|
||||
}
|
||||
|
||||
public function testDeleteSchedule()
|
||||
{
|
||||
|
||||
$data = [
|
||||
'ids' => [$this->scheduler->hashed_id],
|
||||
];
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'X-API-SECRET' => config('ninja.api_secret'),
|
||||
'X-API-TOKEN' => $this->token,
|
||||
])->postJson('/api/v1/task_schedulers/bulk?action=delete', $data)
|
||||
->assertStatus(200);
|
||||
|
||||
|
||||
$data = [
|
||||
'ids' => [$this->scheduler->hashed_id],
|
||||
];
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'X-API-SECRET' => config('ninja.api_secret'),
|
||||
'X-API-TOKEN' => $this->token,
|
||||
])->postJson('/api/v1/task_schedulers/bulk?action=restore', $data)
|
||||
->assertStatus(200);
|
||||
|
||||
}
|
||||
|
||||
public function testRestoreSchedule()
|
||||
{
|
||||
|
||||
$data = [
|
||||
'ids' => [$this->scheduler->hashed_id],
|
||||
];
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'X-API-SECRET' => config('ninja.api_secret'),
|
||||
'X-API-TOKEN' => $this->token,
|
||||
])->postJson('/api/v1/task_schedulers/bulk?action=archive', $data)
|
||||
->assertStatus(200);
|
||||
|
||||
|
||||
$data = [
|
||||
'ids' => [$this->scheduler->hashed_id],
|
||||
];
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'X-API-SECRET' => config('ninja.api_secret'),
|
||||
'X-API-TOKEN' => $this->token,
|
||||
])->postJson('/api/v1/task_schedulers/bulk?action=restore', $data)
|
||||
->assertStatus(200);
|
||||
|
||||
}
|
||||
|
||||
public function testArchiveSchedule()
|
||||
{
|
||||
|
||||
$data = [
|
||||
'ids' => [$this->scheduler->hashed_id],
|
||||
];
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'X-API-SECRET' => config('ninja.api_secret'),
|
||||
'X-API-TOKEN' => $this->token,
|
||||
])->postJson('/api/v1/task_schedulers/bulk?action=archive', $data)
|
||||
->assertStatus(200);
|
||||
|
||||
}
|
||||
|
||||
public function testSchedulerPost()
|
||||
{
|
||||
|
||||
$data = [
|
||||
'name' => 'A different Name',
|
||||
'frequency_id' => 5,
|
||||
'next_run' => now()->addDays(2)->format('Y-m-d'),
|
||||
'template' =>'statement',
|
||||
'parameters' => [],
|
||||
];
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'X-API-SECRET' => config('ninja.api_secret'),
|
||||
'X-API-TOKEN' => $this->token,
|
||||
])->postJson('/api/v1/task_schedulers', $data);
|
||||
|
||||
$response->assertStatus(200);
|
||||
}
|
||||
|
||||
public function testSchedulerPut()
|
||||
{
|
||||
|
||||
$data = [
|
||||
'name' => 'A different Name',
|
||||
'frequency_id' => 5,
|
||||
'next_run' => now()->addDays(2)->format('Y-m-d'),
|
||||
'template' =>'statement',
|
||||
'parameters' => [],
|
||||
];
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'X-API-SECRET' => config('ninja.api_secret'),
|
||||
'X-API-TOKEN' => $this->token,
|
||||
])->putJson('/api/v1/task_schedulers/'.$this->scheduler->hashed_id, $data);
|
||||
|
||||
$response->assertStatus(200);
|
||||
}
|
||||
|
||||
public function testSchedulerGet()
|
||||
{
|
||||
$response = $this->withHeaders([
|
||||
'X-API-SECRET' => config('ninja.api_secret'),
|
||||
'X-API-TOKEN' => $this->token,
|
||||
])->get('/api/v1/task_schedulers');
|
||||
|
||||
$response->assertStatus(200);
|
||||
}
|
||||
|
||||
public function testSchedulerCreate()
|
||||
{
|
||||
$response = $this->withHeaders([
|
||||
'X-API-SECRET' => config('ninja.api_secret'),
|
||||
'X-API-TOKEN' => $this->token,
|
||||
])->get('/api/v1/task_schedulers/create');
|
||||
|
||||
$response->assertStatus(200);
|
||||
}
|
||||
|
||||
|
||||
// public function testSchedulerPut()
|
||||
// {
|
||||
// $data = [
|
||||
// 'description' => $this->faker->firstName(),
|
||||
// ];
|
||||
|
||||
// $response = $this->withHeaders([
|
||||
// 'X-API-SECRET' => config('ninja.api_secret'),
|
||||
// 'X-API-TOKEN' => $this->token,
|
||||
// ])->put('/api/v1/task_schedulers/'.$this->encodePrimaryKey($this->task->id), $data);
|
||||
|
||||
// $response->assertStatus(200);
|
||||
// }
|
||||
|
||||
|
||||
|
||||
// public function testSchedulerCantBeCreatedWithWrongData()
|
||||
// {
|
||||
// $data = [
|
||||
// 'repeat_every' => Scheduler::DAILY,
|
||||
// 'job' => Scheduler::CREATE_CLIENT_REPORT,
|
||||
// 'date_key' => '123',
|
||||
// 'report_keys' => ['test'],
|
||||
// 'date_range' => 'all',
|
||||
// // 'start_from' => '2022-01-01'
|
||||
// ];
|
||||
|
||||
// $response = false;
|
||||
|
||||
// $response = $this->withHeaders([
|
||||
// 'X-API-SECRET' => config('ninja.api_secret'),
|
||||
// 'X-API-TOKEN' => $this->token,
|
||||
// ])->post('/api/v1/task_scheduler/', $data);
|
||||
|
||||
// $response->assertSessionHasErrors();
|
||||
// }
|
||||
|
||||
// public function testSchedulerCanBeUpdated()
|
||||
// {
|
||||
// $response = $this->createScheduler();
|
||||
|
||||
// $arr = $response->json();
|
||||
// $id = $arr['data']['id'];
|
||||
|
||||
// $scheduler = Scheduler::find($this->decodePrimaryKey($id));
|
||||
|
||||
// $updateData = [
|
||||
// 'start_from' => 1655934741,
|
||||
// ];
|
||||
// $response = $this->withHeaders([
|
||||
// 'X-API-SECRET' => config('ninja.api_secret'),
|
||||
// 'X-API-TOKEN' => $this->token,
|
||||
// ])->put('/api/v1/task_scheduler/'.$this->encodePrimaryKey($scheduler->id), $updateData);
|
||||
|
||||
// $responseData = $response->json();
|
||||
// $this->assertEquals($updateData['start_from'], $responseData['data']['start_from']);
|
||||
// }
|
||||
|
||||
// public function testSchedulerCanBeSeen()
|
||||
// {
|
||||
// $response = $this->createScheduler();
|
||||
|
||||
// $arr = $response->json();
|
||||
// $id = $arr['data']['id'];
|
||||
|
||||
// $scheduler = Scheduler::find($this->decodePrimaryKey($id));
|
||||
|
||||
// $response = $this->withHeaders([
|
||||
// 'X-API-SECRET' => config('ninja.api_secret'),
|
||||
// 'X-API-TOKEN' => $this->token,
|
||||
// ])->get('/api/v1/task_scheduler/'.$this->encodePrimaryKey($scheduler->id));
|
||||
|
||||
// $arr = $response->json();
|
||||
// $this->assertEquals('create_client_report', $arr['data']['action_name']);
|
||||
// }
|
||||
|
||||
// public function testSchedulerJobCanBeUpdated()
|
||||
// {
|
||||
// $response = $this->createScheduler();
|
||||
|
||||
// $arr = $response->json();
|
||||
// $id = $arr['data']['id'];
|
||||
|
||||
// $scheduler = Scheduler::find($this->decodePrimaryKey($id));
|
||||
|
||||
// $this->assertSame('create_client_report', $scheduler->action_name);
|
||||
|
||||
// $updateData = [
|
||||
// 'job' => Scheduler::CREATE_CREDIT_REPORT,
|
||||
// 'date_range' => 'all',
|
||||
// 'report_keys' => ['test1'],
|
||||
// ];
|
||||
|
||||
// $response = $this->withHeaders([
|
||||
// 'X-API-SECRET' => config('ninja.api_secret'),
|
||||
// 'X-API-TOKEN' => $this->token,
|
||||
// ])->put('/api/v1/task_scheduler/'.$this->encodePrimaryKey($scheduler->id), $updateData);
|
||||
|
||||
// $updatedSchedulerJob = Scheduler::first()->action_name;
|
||||
// $arr = $response->json();
|
||||
|
||||
// $this->assertSame('create_credit_report', $arr['data']['action_name']);
|
||||
// }
|
||||
|
||||
// public function createScheduler()
|
||||
// {
|
||||
// $data = [
|
||||
// 'repeat_every' => Scheduler::DAILY,
|
||||
// 'job' => Scheduler::CREATE_CLIENT_REPORT,
|
||||
// 'date_key' => '123',
|
||||
// 'report_keys' => ['test'],
|
||||
// 'date_range' => 'all',
|
||||
// 'start_from' => '2022-01-01',
|
||||
// ];
|
||||
|
||||
// return $response = $this->withHeaders([
|
||||
// 'X-API-SECRET' => config('ninja.api_secret'),
|
||||
// 'X-API-TOKEN' => $this->token,
|
||||
// ])->post('/api/v1/task_scheduler/', $data);
|
||||
// }
|
||||
}
|
@ -48,6 +48,7 @@ use App\Models\QuoteInvitation;
|
||||
use App\Models\RecurringExpense;
|
||||
use App\Models\RecurringInvoice;
|
||||
use App\Models\RecurringQuote;
|
||||
use App\Models\Scheduler;
|
||||
use App\Models\Task;
|
||||
use App\Models\TaskStatus;
|
||||
use App\Models\TaxRate;
|
||||
@ -177,6 +178,11 @@ trait MockAccountData
|
||||
*/
|
||||
public $tax_rate;
|
||||
|
||||
/**
|
||||
* @var
|
||||
*/
|
||||
public $scheduler;
|
||||
|
||||
public function makeTestData()
|
||||
{
|
||||
config(['database.default' => config('ninja.db.default')]);
|
||||
@ -804,6 +810,14 @@ trait MockAccountData
|
||||
|
||||
$this->client = $this->client->fresh();
|
||||
$this->invoice = $this->invoice->fresh();
|
||||
|
||||
$this->scheduler = Scheduler::factory()->create([
|
||||
'user_id' => $user_id,
|
||||
'company_id' => $this->company->id,
|
||||
]);
|
||||
|
||||
$this->scheduler->save();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -42,4 +42,5 @@ class RecurringDateTest extends TestCase
|
||||
|
||||
$this->assertequals($trial_ends->format('Y-m-d'), '2021-12-03');
|
||||
}
|
||||
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user