diff --git a/app/Console/Commands/CreateTestData.php b/app/Console/Commands/CreateTestData.php
index 8be45787b460..cfc80835f305 100644
--- a/app/Console/Commands/CreateTestData.php
+++ b/app/Console/Commands/CreateTestData.php
@@ -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,
diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php
index 1183ec91b99a..7587da2e5062 100644
--- a/app/Console/Kernel.php
+++ b/app/Console/Kernel.php
@@ -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();
diff --git a/app/DataMapper/CompanySettings.php b/app/DataMapper/CompanySettings.php
index 04abd19bc01a..6e65ccd3a8de 100644
--- a/app/DataMapper/CompanySettings.php
+++ b/app/DataMapper/CompanySettings.php
@@ -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',
diff --git a/app/DataMapper/EmailTemplateDefaults.php b/app/DataMapper/EmailTemplateDefaults.php
index 3ce6ab53ac96..60c40c819fb3 100644
--- a/app/DataMapper/EmailTemplateDefaults.php
+++ b/app/DataMapper/EmailTemplateDefaults.php
@@ -235,12 +235,17 @@ class EmailTemplateDefaults
public static function emailStatementSubject()
{
- return '';
+ return ctrans('texts.your_statement');
}
public static function emailStatementTemplate()
{
- return '';
+
+ $statement_message = '
$client
'.self::transformText('client_statement_body').'
';
+
+ return $statement_message;
+
+ // return ctrans('texts.client_statement_body', ['start_date' => '$start_date', 'end_date' => '$end_date']);
}
private static function transformText($string)
diff --git a/app/DataMapper/Schedule/ClientStatement.php b/app/DataMapper/Schedule/ClientStatement.php
new file mode 100644
index 000000000000..37ba76d081cb
--- /dev/null
+++ b/app/DataMapper/Schedule/ClientStatement.php
@@ -0,0 +1,96 @@
+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;
+ }
+}
diff --git a/app/Filters/QuoteFilters.php b/app/Filters/QuoteFilters.php
index 517479aa121d..2f342c4413f6 100644
--- a/app/Filters/QuoteFilters.php
+++ b/app/Filters/QuoteFilters.php
@@ -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');
});
diff --git a/app/Helpers/SwissQr/SwissQrGenerator.php b/app/Helpers/SwissQr/SwissQrGenerator.php
index 488c0d1a623c..e69db7d4734f 100644
--- a/app/Helpers/SwissQr/SwissQrGenerator.php
+++ b/app/Helpers/SwissQr/SwissQrGenerator.php
@@ -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'
));
diff --git a/app/Http/Controllers/BaseController.php b/app/Http/Controllers/BaseController.php
index b5b0f249f2ea..6eb9920a6bdd 100644
--- a/app/Http/Controllers/BaseController.php
+++ b/app/Http/Controllers/BaseController.php
@@ -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') : '';
diff --git a/app/Http/Controllers/ClientPortal/NinjaPlanController.php b/app/Http/Controllers/ClientPortal/NinjaPlanController.php
index 7d059d0d7b54..18ed6537e5e3 100644
--- a/app/Http/Controllers/ClientPortal/NinjaPlanController.php
+++ b/app/Http/Controllers/ClientPortal/NinjaPlanController.php
@@ -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();
}
diff --git a/app/Http/Controllers/PurchaseOrderController.php b/app/Http/Controllers/PurchaseOrderController.php
index 452c5895374d..821f0c88847a 100644
--- a/app/Http/Controllers/PurchaseOrderController.php
+++ b/app/Http/Controllers/PurchaseOrderController.php
@@ -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)) {
diff --git a/app/Http/Controllers/SchedulerController.php b/app/Http/Controllers/SchedulerController.php
index e1788ce4e85c..dcd7b35d1b52 100644
--- a/app/Http/Controllers/SchedulerController.php
+++ b/app/Http/Controllers/SchedulerController.php
@@ -1,4 +1,13 @@
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)));
+ }
+
}
diff --git a/app/Http/Livewire/BillingPortalPurchase.php b/app/Http/Livewire/BillingPortalPurchase.php
index 404194f58531..496c241b79aa 100644
--- a/app/Http/Livewire/BillingPortalPurchase.php
+++ b/app/Http/Livewire/BillingPortalPurchase.php
@@ -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());
+ }
+
}
/**
diff --git a/app/Http/Requests/Setup/CheckMailRequest.php b/app/Http/Requests/Setup/CheckMailRequest.php
index 7df6368212c9..dd6e7b1413e5 100644
--- a/app/Http/Requests/Setup/CheckMailRequest.php
+++ b/app/Http/Requests/Setup/CheckMailRequest.php
@@ -32,8 +32,6 @@ class CheckMailRequest extends Request
*/
public function rules()
{
- nlog($this->driver);
-
return [
'mail_driver' => 'required',
'encryption' => 'required_unless:mail_driver,log',
diff --git a/app/Http/Requests/TaskScheduler/CreateScheduledTaskRequest.php b/app/Http/Requests/TaskScheduler/CreateScheduledTaskRequest.php
deleted file mode 100644
index 80b4fc76bfd0..000000000000
--- a/app/Http/Requests/TaskScheduler/CreateScheduledTaskRequest.php
+++ /dev/null
@@ -1,39 +0,0 @@
-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);
- }
-}
diff --git a/app/Http/Requests/TaskScheduler/CreateSchedulerRequest.php b/app/Http/Requests/TaskScheduler/CreateSchedulerRequest.php
new file mode 100644
index 000000000000..71c0a8f3d9f2
--- /dev/null
+++ b/app/Http/Requests/TaskScheduler/CreateSchedulerRequest.php
@@ -0,0 +1,28 @@
+user()->isAdmin();
+ }
+
+}
diff --git a/app/Http/Requests/TaskScheduler/DestroySchedulerRequest.php b/app/Http/Requests/TaskScheduler/DestroySchedulerRequest.php
new file mode 100644
index 000000000000..93e15f06df95
--- /dev/null
+++ b/app/Http/Requests/TaskScheduler/DestroySchedulerRequest.php
@@ -0,0 +1,27 @@
+user()->isAdmin();
+ }
+}
diff --git a/app/Http/Requests/TaskScheduler/ShowSchedulerRequest.php b/app/Http/Requests/TaskScheduler/ShowSchedulerRequest.php
new file mode 100644
index 000000000000..a459edab3c99
--- /dev/null
+++ b/app/Http/Requests/TaskScheduler/ShowSchedulerRequest.php
@@ -0,0 +1,27 @@
+user()->can('view', $this->scheduler);
+ }
+}
diff --git a/app/Http/Requests/TaskScheduler/StoreSchedulerRequest.php b/app/Http/Requests/TaskScheduler/StoreSchedulerRequest.php
new file mode 100644
index 000000000000..0c63f95a9542
--- /dev/null
+++ b/app/Http/Requests/TaskScheduler/StoreSchedulerRequest.php
@@ -0,0 +1,44 @@
+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;
+
+ }
+}
diff --git a/app/Http/Requests/TaskScheduler/UpdateScheduledJobRequest.php b/app/Http/Requests/TaskScheduler/UpdateScheduledJobRequest.php
deleted file mode 100644
index 0db34ccc000f..000000000000
--- a/app/Http/Requests/TaskScheduler/UpdateScheduledJobRequest.php
+++ /dev/null
@@ -1,25 +0,0 @@
-user()->isAdmin();
- }
-
- public function rules(): array
- {
- return [
- 'action_name' => 'sometimes|string',
- ];
- }
-}
diff --git a/app/Http/Requests/TaskScheduler/UpdateScheduleRequest.php b/app/Http/Requests/TaskScheduler/UpdateSchedulerRequest.php
similarity index 51%
rename from app/Http/Requests/TaskScheduler/UpdateScheduleRequest.php
rename to app/Http/Requests/TaskScheduler/UpdateSchedulerRequest.php
index 69b819e84575..7e3ec3267152 100644
--- a/app/Http/Requests/TaskScheduler/UpdateScheduleRequest.php
+++ b/app/Http/Requests/TaskScheduler/UpdateSchedulerRequest.php
@@ -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;
+
}
}
diff --git a/app/Jobs/Ledger/ClientLedgerBalanceUpdate.php b/app/Jobs/Ledger/ClientLedgerBalanceUpdate.php
index 11f1ca358719..d0f5c86191bd 100644
--- a/app/Jobs/Ledger/ClientLedgerBalanceUpdate.php
+++ b/app/Jobs/Ledger/ClientLedgerBalanceUpdate.php
@@ -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);
}
}
diff --git a/app/Jobs/Mail/NinjaMailerJob.php b/app/Jobs/Mail/NinjaMailerJob.php
index b1c7ed2931ea..5302a41d6397 100644
--- a/app/Jobs/Mail/NinjaMailerJob.php
+++ b/app/Jobs/Mail/NinjaMailerJob.php
@@ -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();
diff --git a/app/Jobs/Ninja/TaskScheduler.php b/app/Jobs/Ninja/TaskScheduler.php
index 76ebad1883d1..69862f8877c2 100644
--- a/app/Jobs/Ninja/TaskScheduler.php
+++ b/app/Jobs/Ninja/TaskScheduler.php
@@ -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();
}
+
+
}
diff --git a/app/Jobs/RecurringInvoice/SendRecurring.php b/app/Jobs/RecurringInvoice/SendRecurring.php
index 63f6ecc2d645..81cf7b2f7a00 100644
--- a/app/Jobs/RecurringInvoice/SendRecurring.php
+++ b/app/Jobs/RecurringInvoice/SendRecurring.php
@@ -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! */
diff --git a/app/Mail/Client/ClientStatement.php b/app/Mail/Client/ClientStatement.php
new file mode 100644
index 000000000000..18ffb563d831
--- /dev/null
+++ b/app/Mail/Client/ClientStatement.php
@@ -0,0 +1,94 @@
+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;
+
+ }
+}
diff --git a/app/Models/BaseModel.php b/app/Models/BaseModel.php
index 22fc9ba12915..fdd51dff88d2 100644
--- a/app/Models/BaseModel.php
+++ b/app/Models/BaseModel.php
@@ -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");
}
diff --git a/app/Models/Company.php b/app/Models/Company.php
index 7864220ca00d..0d9d6793890b 100644
--- a/app/Models/Company.php
+++ b/app/Models/Company.php
@@ -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',
diff --git a/app/Models/Scheduler.php b/app/Models/Scheduler.php
index 287129638fb1..50c693cee6af 100644
--- a/app/Models/Scheduler.php
+++ b/app/Models/Scheduler.php
@@ -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;
+ // }
+ // }
}
diff --git a/app/PaymentDrivers/CheckoutCom/Utilities.php b/app/PaymentDrivers/CheckoutCom/Utilities.php
index 7ac9601d5d60..2407c1c395cb 100644
--- a/app/PaymentDrivers/CheckoutCom/Utilities.php
+++ b/app/PaymentDrivers/CheckoutCom/Utilities.php
@@ -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 {
diff --git a/app/Policies/SchedulerPolicy.php b/app/Policies/SchedulerPolicy.php
new file mode 100644
index 000000000000..b5eaba785f60
--- /dev/null
+++ b/app/Policies/SchedulerPolicy.php
@@ -0,0 +1,31 @@
+isAdmin();
+ }
+}
diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php
index eef723dcaab3..353e7d6caa3f 100644
--- a/app/Providers/AuthServiceProvider.php
+++ b/app/Providers/AuthServiceProvider.php
@@ -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,
diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php
index 2684f395df89..0a993436e417 100644
--- a/app/Providers/RouteServiceProvider.php
+++ b/app/Providers/RouteServiceProvider.php
@@ -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();
+
+ });
+
+
}
/**
diff --git a/app/Repositories/SchedulerRepository.php b/app/Repositories/SchedulerRepository.php
new file mode 100644
index 000000000000..5c9b9ad4e19c
--- /dev/null
+++ b/app/Repositories/SchedulerRepository.php
@@ -0,0 +1,38 @@
+fill($data);
+
+ $scheduler->save();
+
+ return $scheduler;
+
+ }
+
+}
diff --git a/app/Repositories/TaskSchedulerRepository.php b/app/Repositories/TaskSchedulerRepository.php
deleted file mode 100644
index 8eddc7637775..000000000000
--- a/app/Repositories/TaskSchedulerRepository.php
+++ /dev/null
@@ -1,16 +0,0 @@
-company_id = $company_id;
- $this->db = $db;
- }
+ public function __construct(public $company_id, public $db){}
public function handle() :void
{
diff --git a/app/Services/Email/EmailDefaults.php b/app/Services/Email/EmailDefaults.php
new file mode 100644
index 000000000000..3648a63dd329
--- /dev/null
+++ b/app/Services/Email/EmailDefaults.php
@@ -0,0 +1,313 @@
+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);
+ }
+
+}
\ No newline at end of file
diff --git a/app/Services/Email/EmailMailable.php b/app/Services/Email/EmailMailable.php
new file mode 100644
index 000000000000..a7036b7e6a15
--- /dev/null
+++ b/app/Services/Email/EmailMailable.php
@@ -0,0 +1,109 @@
+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,
+ );
+
+ }
+
+}
diff --git a/app/Services/Email/EmailMailer.php b/app/Services/Email/EmailMailer.php
new file mode 100644
index 000000000000..a5be905327c3
--- /dev/null
+++ b/app/Services/Email/EmailMailer.php
@@ -0,0 +1,515 @@
+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)
+ {
+
+ }
+
+}
diff --git a/app/Services/Email/EmailObject.php b/app/Services/Email/EmailObject.php
new file mode 100644
index 000000000000..27a1dd72bdfa
--- /dev/null
+++ b/app/Services/Email/EmailObject.php
@@ -0,0 +1,84 @@
+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;
+ }
+
+
+}
\ No newline at end of file
diff --git a/app/Services/Invoice/InvoiceService.php b/app/Services/Invoice/InvoiceService.php
index bdc770c7826b..0fcb71899d71 100644
--- a/app/Services/Invoice/InvoiceService.php
+++ b/app/Services/Invoice/InvoiceService.php
@@ -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();
}
diff --git a/app/Services/Schedule/ScheduleService.php b/app/Services/Schedule/ScheduleService.php
deleted file mode 100644
index b9638095b430..000000000000
--- a/app/Services/Schedule/ScheduleService.php
+++ /dev/null
@@ -1,57 +0,0 @@
-{$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;
+
+ }
+
+
+}
\ No newline at end of file
diff --git a/app/Services/TaskScheduler/TaskSchedulerService.php b/app/Services/TaskScheduler/TaskSchedulerService.php
index c0320a8b1c89..2b4302eaf62b 100644
--- a/app/Services/TaskScheduler/TaskSchedulerService.php
+++ b/app/Services/TaskScheduler/TaskSchedulerService.php
@@ -38,6 +38,8 @@ use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Validator;
use Symfony\Component\HttpFoundation\Request;
+
+//@deprecated - never used....
class TaskSchedulerService
{
diff --git a/app/Transformers/CompanyTransformer.php b/app/Transformers/CompanyTransformer.php
index 2f20c5300617..77bcae0fa469 100644
--- a/app/Transformers/CompanyTransformer.php
+++ b/app/Transformers/CompanyTransformer.php
@@ -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,
];
}
diff --git a/app/Transformers/TaskSchedulerTransformer.php b/app/Transformers/SchedulerTransformer.php
similarity index 66%
rename from app/Transformers/TaskSchedulerTransformer.php
rename to app/Transformers/SchedulerTransformer.php
index 88fbf9d29999..584ea4cbe9f9 100644
--- a/app/Transformers/TaskSchedulerTransformer.php
+++ b/app/Transformers/SchedulerTransformer.php
@@ -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,
];
}
}
diff --git a/database/factories/SchedulerFactory.php b/database/factories/SchedulerFactory.php
new file mode 100644
index 000000000000..92c86d8ad8ce
--- /dev/null
+++ b/database/factories/SchedulerFactory.php
@@ -0,0 +1,37 @@
+ $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',
+ ];
+ }
+}
diff --git a/database/migrations/2023_01_12_125540_set_auto_bill_on_regular_invoice_setting.php b/database/migrations/2023_01_12_125540_set_auto_bill_on_regular_invoice_setting.php
new file mode 100644
index 000000000000..fade7d7ad3f4
--- /dev/null
+++ b/database/migrations/2023_01_12_125540_set_auto_bill_on_regular_invoice_setting.php
@@ -0,0 +1,71 @@
+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()
+ {
+ //
+ }
+};
diff --git a/lang/en/texts.php b/lang/en/texts.php
index 388d200053c7..f983eaf0238a 100644
--- a/lang/en/texts.php
+++ b/lang/en/texts.php
@@ -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.'
);
diff --git a/resources/views/email/template/client.blade.php b/resources/views/email/template/client.blade.php
index 93e772abe888..5fcf2b9bdcd1 100644
--- a/resources/views/email/template/client.blade.php
+++ b/resources/views/email/template/client.blade.php
@@ -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 @@
-
-
+
+
{{ $slot ?? '' }}
@@ -163,8 +165,8 @@
| |
-
-
+ |
+
|
diff --git a/resources/views/index/index.blade.php b/resources/views/index/index.blade.php
index 2d95233ce2f3..b0dc8e5d5e34 100644
--- a/resources/views/index/index.blade.php
+++ b/resources/views/index/index.blade.php
@@ -1,29 +1,32 @@
-
+
- {{config('ninja.app_name')}}
+ {{ $white_label ? "" : config('ninja.app_name') }}
@if(\App\Utils\Ninja::isHosted())
+
+
+
-
+
-
+
@endif