Merge branch 'v5-develop' of https://github.com/turbo124/invoiceninja into v5-develop

This commit is contained in:
David Bomba 2023-08-16 15:01:04 +10:00
commit 2497e4296d
37 changed files with 887 additions and 482 deletions

View File

@ -471,7 +471,7 @@ class CheckData extends Command
$ii->saveQuietly(); $ii->saveQuietly();
}); });
collect([Invoice::class, Quote::class, Credit::class, PurchaseOrder::class])->each(function ($entity) { collect([Invoice::class, Quote::class, Credit::class, PurchaseOrder::class, RecurringInvoice::class])->each(function ($entity) {
if ($entity::doesntHave('invitations')->count() > 0) { if ($entity::doesntHave('invitations')->count() > 0) {
$entity::doesntHave('invitations')->cursor()->each(function ($entity) { $entity::doesntHave('invitations')->cursor()->each(function ($entity) {
$client_vendor_key = 'client_id'; $client_vendor_key = 'client_id';
@ -694,7 +694,7 @@ class CheckData extends Command
{ {
$this->wrong_balances = 0; $this->wrong_balances = 0;
Client::cursor()->where('is_deleted', 0)->where('clients.updated_at', '>', now()->subDays(2))->each(function ($client) { Client::query()->cursor()->where('is_deleted', 0)->where('clients.updated_at', '>', now()->subDays(2))->each(function ($client) {
$client->invoices->where('is_deleted', false)->whereIn('status_id', '!=', Invoice::STATUS_DRAFT)->each(function ($invoice) use ($client) { $client->invoices->where('is_deleted', false)->whereIn('status_id', '!=', Invoice::STATUS_DRAFT)->each(function ($invoice) use ($client) {
$total_paid = $invoice->payments() $total_paid = $invoice->payments()
->where('is_deleted', false)->whereIn('status_id', [Payment::STATUS_COMPLETED, Payment::STATUS_PENDING, Payment::STATUS_PARTIALLY_REFUNDED, Payment::STATUS_REFUNDED]) ->where('is_deleted', false)->whereIn('status_id', [Payment::STATUS_COMPLETED, Payment::STATUS_PENDING, Payment::STATUS_PARTIALLY_REFUNDED, Payment::STATUS_REFUNDED])
@ -876,7 +876,7 @@ class CheckData extends Command
$this->wrong_balances = 0; $this->wrong_balances = 0;
$this->wrong_paid_to_dates = 0; $this->wrong_paid_to_dates = 0;
foreach (Client::where('is_deleted', 0)->where('clients.updated_at', '>', now()->subDays(2))->cursor() as $client) { foreach (Client::query()->where('is_deleted', 0)->where('clients.updated_at', '>', now()->subDays(2))->cursor() as $client) {
$invoice_balance = $client->invoices()->where('is_deleted', false)->whereIn('status_id', [2,3])->sum('balance'); $invoice_balance = $client->invoices()->where('is_deleted', false)->whereIn('status_id', [2,3])->sum('balance');
$ledger = CompanyLedger::where('client_id', $client->id)->orderBy('id', 'DESC')->first(); $ledger = CompanyLedger::where('client_id', $client->id)->orderBy('id', 'DESC')->first();

View File

@ -0,0 +1,101 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\DataMapper\Analytics;
use Turbo124\Beacon\ExampleMetric\GenericMixedMetric;
class RevenueTrack extends GenericMixedMetric
{
/**
* The type of Sample.
*
* Monotonically incrementing counter
*
* - counter
*
* @var string
*/
public $type = 'mixed_metric';
/**
* The name of the counter.
* @var string
*/
public $name = 'app.revenue';
/**
* The datetime of the counter measurement.
*
* date("Y-m-d H:i:s")
*
*/
public $datetime;
/**
* The Client email
*
* @var string
*/
public $string_metric5 = 'email';
/**
* The AccountKey email
*
* @var string
*/
public $string_metric6 = 'key';
/**
* Product Type
*
* @var string
*/
public $string_metric7 = 'product';
/**
* Gateway Reference
*
* @var string
*/
public $string_metric8 = 'gateway_reference';
public $string_metric9 = 'entity_reference';
public $string_metric10 = 'gateway_type';
/**
* The counter
* set to 1.
*
* @var int
*/
public $int_metric1 = 1;
/**
* Amount Received
*
* @var double
*/
public $double_metric2 = 0;
public function __construct($string_metric5, $string_metric6, $int_metric1, $double_metric2, $string_metric7, $string_metric8, $string_metric9, $string_metric10)
{
$this->int_metric1 = $int_metric1;
$this->double_metric2 = $double_metric2;
$this->string_metric5 = $string_metric5;
$this->string_metric6 = $string_metric6;
$this->string_metric7 = $string_metric7;
$this->string_metric8 = $string_metric8;
$this->string_metric9 = $string_metric9;
$this->string_metric10 = $string_metric10;
}
}

View File

@ -0,0 +1,36 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Events\Account;
use App\Models\Company;
use Illuminate\Broadcasting\Channel;
use Illuminate\Queue\SerializesModels;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Broadcasting\InteractsWithSockets;
/**
* Class StripeConnectFailure.
*/
class StripeConnectFailure
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public function __construct(public Company $company, public string $db)
{
}
public function broadcastOn()
{
return [];
}
}

View File

@ -88,6 +88,11 @@ class CreditFilters extends QueryFilters
->orWhere('credits.custom_value4', 'like', '%'.$filter.'%') ->orWhere('credits.custom_value4', 'like', '%'.$filter.'%')
->orWhereHas('client', function ($q) use ($filter) { ->orWhereHas('client', function ($q) use ($filter) {
$q->where('name', 'like', '%'.$filter.'%'); $q->where('name', 'like', '%'.$filter.'%');
})
->orWhereHas('client.contacts', function ($q) use ($filter) {
$q->where('first_name', 'like', '%'.$filter.'%')
->orWhere('last_name', 'like', '%'.$filter.'%')
->orWhere('email', 'like', '%'.$filter.'%');
}); });
}); });
} }

View File

@ -116,6 +116,11 @@ class InvoiceFilters extends QueryFilters
->orWhere('custom_value4', 'like', '%'.$filter.'%') ->orWhere('custom_value4', 'like', '%'.$filter.'%')
->orWhereHas('client', function ($q) use ($filter) { ->orWhereHas('client', function ($q) use ($filter) {
$q->where('name', 'like', '%'.$filter.'%'); $q->where('name', 'like', '%'.$filter.'%');
})
->orWhereHas('client.contacts', function ($q) use ($filter) {
$q->where('first_name', 'like', '%'.$filter.'%')
->orWhere('last_name', 'like', '%'.$filter.'%')
->orWhere('email', 'like', '%'.$filter.'%');
}); });
}); });
} }

View File

@ -44,7 +44,12 @@ class PaymentFilters extends QueryFilters
->orWhere('custom_value4', 'like', '%'.$filter.'%') ->orWhere('custom_value4', 'like', '%'.$filter.'%')
->orWhereHas('client', function ($q) use ($filter) { ->orWhereHas('client', function ($q) use ($filter) {
$q->where('name', 'like', '%'.$filter.'%'); $q->where('name', 'like', '%'.$filter.'%');
}); })
->orWhereHas('client.contacts', function ($q) use ($filter) {
$q->where('first_name', 'like', '%'.$filter.'%')
->orWhere('last_name', 'like', '%'.$filter.'%')
->orWhere('email', 'like', '%'.$filter.'%');
});
}); });
} }

View File

@ -40,7 +40,12 @@ class QuoteFilters extends QueryFilters
->orWhere('custom_value4', 'like', '%'.$filter.'%') ->orWhere('custom_value4', 'like', '%'.$filter.'%')
->orWhereHas('client', function ($q) use ($filter) { ->orWhereHas('client', function ($q) use ($filter) {
$q->where('name', 'like', '%'.$filter.'%'); $q->where('name', 'like', '%'.$filter.'%');
}); })
->orWhereHas('client.contacts', function ($q) use ($filter) {
$q->where('first_name', 'like', '%'.$filter.'%')
->orWhere('last_name', 'like', '%'.$filter.'%')
->orWhere('email', 'like', '%'.$filter.'%');
});
}); });
} }

View File

@ -42,7 +42,12 @@ class RecurringInvoiceFilters extends QueryFilters
->orWhere('custom_value4', 'like', '%'.$filter.'%') ->orWhere('custom_value4', 'like', '%'.$filter.'%')
->orWhereHas('client', function ($q) use ($filter) { ->orWhereHas('client', function ($q) use ($filter) {
$q->where('name', 'like', '%'.$filter.'%'); $q->where('name', 'like', '%'.$filter.'%');
}); })
->orWhereHas('client.contacts', function ($q) use ($filter) {
$q->where('first_name', 'like', '%'.$filter.'%')
->orWhere('last_name', 'like', '%'.$filter.'%')
->orWhere('email', 'like', '%'.$filter.'%');
});
}); });
} }

View File

@ -45,7 +45,12 @@ class TaskFilters extends QueryFilters
}) })
->orWhereHas('client', function ($q) use ($filter) { ->orWhereHas('client', function ($q) use ($filter) {
$q->where('name', 'like', '%'.$filter.'%'); $q->where('name', 'like', '%'.$filter.'%');
}); })
->orWhereHas('client.contacts', function ($q) use ($filter) {
$q->where('first_name', 'like', '%'.$filter.'%')
->orWhere('last_name', 'like', '%'.$filter.'%')
->orWhere('email', 'like', '%'.$filter.'%');
});
}); });
} }

View File

@ -162,7 +162,9 @@ class InvoiceController extends BaseController
*/ */
public function create(CreateInvoiceRequest $request) public function create(CreateInvoiceRequest $request)
{ {
$invoice = InvoiceFactory::create(auth()->user()->company()->id, auth()->user()->id); /** @var \App\Models\User $user */
$user = auth()->user();
$invoice = InvoiceFactory::create($user->company()->id, $user->id);
return $this->itemResponse($invoice); return $this->itemResponse($invoice);
} }
@ -211,7 +213,11 @@ class InvoiceController extends BaseController
*/ */
public function store(StoreInvoiceRequest $request) public function store(StoreInvoiceRequest $request)
{ {
$invoice = $this->invoice_repo->save($request->all(), InvoiceFactory::create(auth()->user()->company()->id, auth()->user()->id));
/** @var \App\Models\User $user */
$user = auth()->user();
$invoice = $this->invoice_repo->save($request->all(), InvoiceFactory::create($user->company()->id, $user->id));
$invoice = $invoice->service() $invoice = $invoice->service()
->fillDefaults() ->fillDefaults()
@ -219,7 +225,7 @@ class InvoiceController extends BaseController
->adjustInventory() ->adjustInventory()
->save(); ->save();
event(new InvoiceWasCreated($invoice, $invoice->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null))); event(new InvoiceWasCreated($invoice, $invoice->company, Ninja::eventVars($user ? $user->id : null)));
$transaction = [ $transaction = [
'invoice' => $invoice->transaction_event(), 'invoice' => $invoice->transaction_event(),
@ -473,62 +479,17 @@ class InvoiceController extends BaseController
return $this->itemResponse($invoice->fresh()); return $this->itemResponse($invoice->fresh());
} }
/**
* Perform bulk actions on the list view.
*
* @return \Illuminate\Support\Collection
*
* @OA\Post(
* path="/api/v1/invoices/bulk",
* operationId="bulkInvoices",
* tags={"invoices"},
* summary="Performs bulk actions on an array of invoices",
* description="",
* @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="User credentials",
* required=true,
* @OA\MediaType(
* mediaType="application/json",
* @OA\Schema(
* type="array",
* @OA\Items(
* type="integer",
* description="Array of hashed IDs to be bulk 'actioned",
* example="[0,1,2,3]",
* ),
* )
* )
* ),
* @OA\Response(
* response=200,
* description="The Bulk Action response",
* @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"),
* @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"),
* @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"),
* ),
* @OA\Response(
* response=422,
* description="Validation error",
* @OA\JsonContent(ref="#/components/schemas/ValidationError"),
* ),
* @OA\Response(
* response="default",
* description="Unexpected Error",
* @OA\JsonContent(ref="#/components/schemas/Error"),
* ),
* )
*/
public function bulk(BulkInvoiceRequest $request) public function bulk(BulkInvoiceRequest $request)
{ {
/** @var \App\Models\User $user */
$user = auth()->user();
$action = $request->input('action'); $action = $request->input('action');
$ids = $request->input('ids'); $ids = $request->input('ids');
if (Ninja::isHosted() && (stripos($action, 'email') !== false) && !auth()->user()->company()->account->account_sms_verified) { if (Ninja::isHosted() && (stripos($action, 'email') !== false) && !$user->company()->account->account_sms_verified) {
return response(['message' => 'Please verify your account to send emails.'], 400); return response(['message' => 'Please verify your account to send emails.'], 400);
} }
@ -543,8 +504,8 @@ class InvoiceController extends BaseController
*/ */
if ($action == 'bulk_download' && $invoices->count() > 1) { if ($action == 'bulk_download' && $invoices->count() > 1) {
$invoices->each(function ($invoice) { $invoices->each(function ($invoice) use($user) {
if (auth()->user()->cannot('view', $invoice)) { if ($user->cannot('view', $invoice)) {
nlog('access denied'); nlog('access denied');
return response()->json(['message' => ctrans('text.access_denied')]); return response()->json(['message' => ctrans('text.access_denied')]);
@ -556,7 +517,7 @@ class InvoiceController extends BaseController
return response()->json(['message' => ctrans('texts.sent_message')], 200); return response()->json(['message' => ctrans('texts.sent_message')], 200);
} }
if ($action == 'download' && $invoices->count() >=1 && auth()->user()->can('view', $invoices->first())) { if ($action == 'download' && $invoices->count() >=1 && $user->can('view', $invoices->first())) {
$file = $invoices->first()->service()->getInvoicePdf(); $file = $invoices->first()->service()->getInvoicePdf();
return response()->streamDownload(function () use ($file) { return response()->streamDownload(function () use ($file) {
@ -564,7 +525,7 @@ class InvoiceController extends BaseController
}, basename($file), ['Content-Type' => 'application/pdf']); }, basename($file), ['Content-Type' => 'application/pdf']);
} }
if ($action == 'bulk_print' && auth()->user()->can('view', $invoices->first())) { if ($action == 'bulk_print' && $user->can('view', $invoices->first())) {
$paths = $invoices->map(function ($invoice) { $paths = $invoices->map(function ($invoice) {
return $invoice->service()->getInvoicePdf(); return $invoice->service()->getInvoicePdf();
}); });
@ -579,15 +540,15 @@ class InvoiceController extends BaseController
/* /*
* Send the other actions to the switch * Send the other actions to the switch
*/ */
$invoices->each(function ($invoice, $key) use ($action) { $invoices->each(function ($invoice, $key) use ($action, $user) {
if (auth()->user()->can('edit', $invoice)) { if ($user->can('edit', $invoice)) {
$this->performAction($invoice, $action, true); $this->performAction($invoice, $action, true);
} }
}); });
/* Need to understand which permission are required for the given bulk action ie. view / edit */ /* Need to understand which permission are required for the given bulk action ie. view / edit */
return $this->listResponse(Invoice::withTrashed()->whereIn('id', $this->transformKeys($ids))->company()); return $this->listResponse(Invoice::query()->withTrashed()->whereIn('id', $this->transformKeys($ids))->company());
} }
/** /**
@ -1005,16 +966,17 @@ class InvoiceController extends BaseController
*/ */
public function upload(UploadInvoiceRequest $request, Invoice $invoice) public function upload(UploadInvoiceRequest $request, Invoice $invoice)
{ {
if (! $this->checkFeature(Account::FEATURE_DOCUMENTS)) { if (! $this->checkFeature(Account::FEATURE_DOCUMENTS)) {
return $this->featureFailure(); return $this->featureFailure();
} }
if ($request->has('documents')) { if ($request->has('documents')) {
$this->saveDocuments($request->file('documents'), $invoice); $this->saveDocuments($request->file('documents'), $invoice, $request->input('is_public', true));
} }
if ($request->has('file')) { if ($request->has('file')) {
$this->saveDocuments($request->file('documents'), $invoice); $this->saveDocuments($request->file('documents'), $invoice, $request->input('is_public', true));
} }
return $this->itemResponse($invoice->fresh()); return $this->itemResponse($invoice->fresh());
@ -1022,7 +984,10 @@ class InvoiceController extends BaseController
public function update_reminders(UpdateReminderRequest $request) public function update_reminders(UpdateReminderRequest $request)
{ {
UpdateReminders::dispatch(auth()->user()->company()); /** @var \App\Models\User $user */
$user = auth()->user();
UpdateReminders::dispatch($user->company());
return response()->json(['message' => 'Updating reminders'], 200); return response()->json(['message' => 'Updating reminders'], 200);
} }

View File

@ -62,11 +62,11 @@ class BillingPortalPurchase extends Component
public $password; public $password;
/** /**
* Instance of subscription. * This arrives as an int and we resolve in the mount method
* *
* @var \App\Models\Subscription $subscription * @var int|Subscription
*/ */
public Subscription $subscription; public $subscription;
/** /**
* Instance of client contact. * Instance of client contact.

View File

@ -121,6 +121,7 @@ class PdfSlot extends Component
$this->show_cost = in_array('$product.unit_cost', $this->settings->pdf_variables->product_columns); $this->show_cost = in_array('$product.unit_cost', $this->settings->pdf_variables->product_columns);
$this->show_line_total = in_array('$product.line_total', $this->settings->pdf_variables->product_columns); $this->show_line_total = in_array('$product.line_total', $this->settings->pdf_variables->product_columns);
$this->show_quantity = in_array('$product.quantity', $this->settings->pdf_variables->product_columns);
if($this->entity_type == 'quote' && !$this->settings->sync_invoice_quote_columns ){ if($this->entity_type == 'quote' && !$this->settings->sync_invoice_quote_columns ){
$this->show_cost = in_array('$product.unit_cost', $this->settings->pdf_variables->product_quote_columns); $this->show_cost = in_array('$product.unit_cost', $this->settings->pdf_variables->product_quote_columns);

View File

@ -23,7 +23,9 @@ class UploadInvoiceRequest extends Request
*/ */
public function authorize() : bool public function authorize() : bool
{ {
return auth()->user()->can('edit', $this->invoice); /** @var \App\Models\User $user */
$user = auth()->user();
return $user->can('edit', $this->invoice);
} }
public function rules() public function rules()
@ -42,6 +44,8 @@ class UploadInvoiceRequest extends Request
$rules['file'] = $this->file_validation; $rules['file'] = $this->file_validation;
} }
$rules['is_public'] = 'sometimes|boolean';
return $rules; return $rules;
} }

View File

@ -20,7 +20,7 @@ class Request extends FormRequest
use MakesHash; use MakesHash;
use RuntimeFormRequest; use RuntimeFormRequest;
protected $file_validation = 'sometimes|file|mimes:png,ai,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx,webp|max:20000'; protected $file_validation = 'sometimes|file|mimes:png,ai,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx,webp,xml|max:20000';
/** /**
* Get the validation rules that apply to the request. * Get the validation rules that apply to the request.
* *

View File

@ -36,10 +36,10 @@ class UserEmailChanged implements ShouldQueue
* Create a new job instance. * Create a new job instance.
* *
* @param \App\Models\User $new_user * @param \App\Models\User $new_user
* @param \App\Models\User $old_user * @param \stdClass $old_user
* @param \App\Models\Company $company * @param \App\Models\Company $company
*/ */
public function __construct(protected User $new_user, protected User $old_user, protected Company $company) public function __construct(protected User $new_user, protected \stdClass $old_user, protected Company $company)
{ {
$this->settings = $this->company->settings; $this->settings = $this->company->settings;
} }

View File

@ -1821,7 +1821,7 @@ class Import implements ShouldQueue
private function processActivities(array $data): void private function processActivities(array $data): void
{ {
Activity::where('company_id', $this->company->id)->cursor()->each(function ($a){ Activity::query()->where('company_id', $this->company->id)->cursor()->each(function ($a){
$a->forceDelete(); $a->forceDelete();
nlog("deleting {$a->id}"); nlog("deleting {$a->id}");
}); });

View File

@ -53,7 +53,7 @@ class UploadFile implements ShouldQueue
public $disk; public $disk;
public function __construct($file, $type, $user, $company, $entity, $disk = null, $is_public = false) public function __construct($file, $type, $user, $company, $entity, $disk = null, $is_public = true)
{ {
$this->file = $file; $this->file = $file;
$this->type = $type; $this->type = $type;

View File

@ -0,0 +1,57 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Listeners\Account;
use App\Utils\Ninja;
use App\Libraries\MultiDB;
use App\Jobs\Mail\NinjaMailerJob;
use App\Jobs\Mail\NinjaMailerObject;
use Illuminate\Support\Facades\Cache;
use App\Mail\Ninja\StripeConnectFailed;
use Illuminate\Contracts\Queue\ShouldQueue;
class StripeConnectFailureListener implements ShouldQueue
{
/**
* Create the event listener.
*
*/
public function __construct()
{
}
/**
* Handle the event.
*
* @param object $event
* @return void
*/
public function handle($event)
{
MultiDB::setDb($event->db);
if (Ninja::isHosted() && is_null(Cache::get("stripe_connect_notification:{$event->company->company_key}")))
{
$nmo = new NinjaMailerObject();
$nmo->mailable = new StripeConnectFailed($event->company->owner(), $event->company);
$nmo->company = $event->company;
$nmo->settings = $event->company->settings;
$nmo->to_user = $event->company->owner();
NinjaMailerJob::dispatch($nmo, true);
Cache::put("stripe_connect_notification:{$event->company->company_key}", true, 60 * 60 * 24);
}
}
}

View File

@ -21,6 +21,7 @@ class PaymentCreatedActivity implements ShouldQueue
{ {
protected $activity_repo; protected $activity_repo;
public $delay = 5;
/** /**
* Create the event listener. * Create the event listener.
* *

View File

@ -21,6 +21,8 @@ class InvoicePaidActivity implements ShouldQueue
{ {
protected $activity_repo; protected $activity_repo;
public $delay = 10;
/** /**
* Create the event listener. * Create the event listener.
* *

View File

@ -11,14 +11,16 @@
namespace App\Listeners\Payment; namespace App\Listeners\Payment;
use App\Utils\Ninja;
use App\Libraries\MultiDB;
use App\Jobs\Mail\NinjaMailer; use App\Jobs\Mail\NinjaMailer;
use App\Jobs\Mail\NinjaMailerJob; use App\Jobs\Mail\NinjaMailerJob;
use App\Jobs\Mail\NinjaMailerObject; use App\Jobs\Mail\NinjaMailerObject;
use App\Libraries\MultiDB;
use App\Mail\Admin\EntityPaidObject; use App\Mail\Admin\EntityPaidObject;
use App\Utils\Ninja; use Turbo124\Beacon\Facades\LightLogs;
use App\Utils\Traits\Notifications\UserNotifies; use App\DataMapper\Analytics\RevenueTrack;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use App\Utils\Traits\Notifications\UserNotifies;
class PaymentNotification implements ShouldQueue class PaymentNotification implements ShouldQueue
{ {
@ -158,6 +160,15 @@ class PaymentNotification implements ShouldQueue
$url = $base."&t=item&in={$item}&ip={$amount}&iq=1"; $url = $base."&t=item&in={$item}&ip={$amount}&iq=1";
$this->sendAnalytics($url); $this->sendAnalytics($url);
$email = $client->present()->email();
$account_key = $client->custom_value2 ?? 'unknown';
$product = $item;
$gateway_reference = $client->gateway_tokens()->count() >= 1 ? ($client->gateway_tokens()->first()->gateway_customer_reference ?? '') : '';
// LightLogs::create(new RevenueTrack($email, $account_key, 1, $amount, $product, $gateway_reference, $entity_number))
// ->batch();
} }
/** /**

View File

@ -0,0 +1,114 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Mail\Ninja;
use App\Models\User;
use App\Models\Company;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Headers;
use Illuminate\Mail\Mailables\Envelope;
class StripeConnectFailed extends Mailable
{
/**
* Create a new message instance.
*
* @return void
*/
public function __construct(public User $user, public Company $company)
{
}
/**
* Get the message envelope.
*
* @return \Illuminate\Mail\Mailables\Envelope
*/
public function envelope()
{
return new Envelope(
subject: "Stripe Connect not configured, please login and connect.",
from: "maildelivery@invoicing.co",
to: $this->user->email,
);
}
/**
* Get the message content definition.
*
* @return \Illuminate\Mail\Mailables\Content
*/
public function content()
{
return new Content(
view: 'email.admin.stripe_connect_failed',
text: 'email.admin.stripe_connect_failed_text',
with: [
'text_body' => $this->textBody(), //@todo this is a bit hacky here.
'body' => $this->htmlBody(),
'title' => 'Connect your Stripe account',
'settings' => $this->company->settings,
'logo' => $this->company->present()->logo(),
'signature' => '',
'company' => $this->company,
'greeting' => '',
'links' => [],
'url' => 'https://www.loom.com/share/a3dc3131cc924e14a34634d5d48065c8?sid=b1971aa2-9deb-4339-8ebd-53f9947ef633',
'button' => "texts.view"
]
);
}
private function textBody()
{
return "
We note you are yet to connect your Stripe account to Invoice Ninja. Please log in to Invoice Ninja and connect your Stripe account.\n\n
Once logged in you can use the following resource to connect your Stripe account: \n\n
";
}
private function htmlBody()
{
return "
We note you are yet to connect your Stripe account to Invoice Ninja. Please log in to Invoice Ninja and connect your Stripe account.<br><br>
Once logged in you can use the following resource to connect your Stripe account: <br><br>
";
}
/**
* Get the attachments for the message.
*
* @return array
*/
public function attachments()
{
return [];
}
/**
* Get the message headers.
*
* @return \Illuminate\Mail\Mailables\Headers
*/
public function headers()
{
return new Headers(
messageId: null,
references: [],
text:['' => ''],
);
}
}

View File

@ -707,6 +707,7 @@ class Company extends BaseModel
public function getSetting($setting) public function getSetting($setting)
{ {
//todo $this->setting ?? false
if (property_exists($this->settings, $setting) != false) { if (property_exists($this->settings, $setting) != false) {
return $this->settings->{$setting}; return $this->settings->{$setting};
} }

View File

@ -20,7 +20,7 @@ use Illuminate\Database\Eloquent\Model;
* @property string $hash * @property string $hash
* @property float $fee_total * @property float $fee_total
* @property int|null $fee_invoice_id * @property int|null $fee_invoice_id
* @property mixed $data * @property \stdClass $data
* @property int|null $payment_id * @property int|null $payment_id
* @property \Illuminate\Support\Carbon|null $created_at * @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at * @property \Illuminate\Support\Carbon|null $updated_at
@ -38,9 +38,16 @@ class PaymentHash extends Model
'data' => 'object', 'data' => 'object',
]; ];
/**
* @class \App\Models\PaymentHash $this
* @property \App\Models\PaymentHash $data
* @class \stdClass $data
* @property string $raw_value
*/
/** /**
* @return array * @return mixed
*/ */
public function invoices() public function invoices()
{ {
@ -75,9 +82,8 @@ class PaymentHash extends Model
public function withData(string $property, $value): self public function withData(string $property, $value): self
{ {
$this->data = array_merge((array) $this->data, [$property => $value]); $this->data = array_merge((array) $this->data, [$property => $value]); // @phpstan-ignore-line
$this->save(); $this->save();// @phpstan-ignore-line
return $this; // @phpstan-ignore-line
return $this;
} }
} }

View File

@ -230,9 +230,9 @@ class Vendor extends BaseModel
* Returns a vendor settings proxying company setting * Returns a vendor settings proxying company setting
* *
* @param string $setting * @param string $setting
* @return string * @return mixed
*/ */
public function getSetting($setting): string public function getSetting($setting): mixed
{ {
if ((property_exists($this->company->settings, $setting) != false) && (isset($this->company->settings->{$setting}) !== false)) { if ((property_exists($this->company->settings, $setting) != false) && (isset($this->company->settings->{$setting}) !== false)) {
return $this->company->settings->{$setting}; return $this->company->settings->{$setting};

View File

@ -69,7 +69,7 @@ class CheckoutWebhook implements ShouldQueue
{ {
$payment_object = $this->webhook_array['data']; $payment_object = $this->webhook_array['data'];
$payment = Payment::withTrashed()->where('transaction_reference', $payment_object['id'])->first(); $payment = Payment::query()->withTrashed()->where('transaction_reference', $payment_object['id'])->first();
if($payment && $payment->status_id == Payment::STATUS_COMPLETED) if($payment && $payment->status_id == Payment::STATUS_COMPLETED)
return; return;
@ -84,18 +84,19 @@ class CheckoutWebhook implements ShouldQueue
$metadata = $this->webhook_array['metadata']; $metadata = $this->webhook_array['metadata'];
$payment_hash = PaymentHash::where('hash', $metadata['udf2'])->first(); $payment_hash = PaymentHash::query()->where('hash', $metadata['udf2'])->first();
$driver = $this->company_gateway->driver($payment_hash->fee_invoice->client)->init()->setPaymentMethod(); $driver = $this->company_gateway->driver($payment_hash->fee_invoice->client)->init()->setPaymentMethod();
$payment_hash->data = array_merge((array) $payment_hash->data, $this->webhook_array); $payment_hash->data = array_merge((array) $payment_hash->data, $this->webhook_array); // @phpstan-ignore-line
$payment_hash->save(); $payment_hash->save();
$driver->setPaymentHash($payment_hash); $driver->setPaymentHash($payment_hash);
// @phpstan-ignore-line
$data = [ $data = [
'payment_method' => isset($this->webhook_array['source']['id']) ? $this->webhook_array['source']['id'] : '', 'payment_method' => isset($this->webhook_array['source']['id']) ? $this->webhook_array['source']['id'] : '',
'payment_type' => PaymentType::CREDIT_CARD_OTHER, 'payment_type' => PaymentType::CREDIT_CARD_OTHER,
'amount' => $payment_hash->data->raw_value, 'amount' => $payment_hash->data->raw_value, // @phpstan-ignore-line
'transaction_reference' => $payment_object['id'], 'transaction_reference' => $payment_object['id'],
'gateway_type_id' => GatewayType::CREDIT_CARD, 'gateway_type_id' => GatewayType::CREDIT_CARD,
]; ];

View File

@ -66,8 +66,6 @@ class Webhook
/** /**
* Lists the workflows in Checkout * Lists the workflows in Checkout
*
* @return void
*/ */
public function getWorkFlows() public function getWorkFlows()
{ {

View File

@ -94,6 +94,7 @@ use App\Events\Misc\InvitationWasViewed;
use App\Events\Payment\PaymentWasVoided; use App\Events\Payment\PaymentWasVoided;
use App\Events\Vendor\VendorWasArchived; use App\Events\Vendor\VendorWasArchived;
use App\Events\Vendor\VendorWasRestored; use App\Events\Vendor\VendorWasRestored;
use App\Events\Account\StripeConnectFailure;
use App\Listeners\Mail\MailSentListener; use App\Listeners\Mail\MailSentListener;
use App\Observers\ClientContactObserver; use App\Observers\ClientContactObserver;
use App\Observers\PurchaseOrderObserver; use App\Observers\PurchaseOrderObserver;
@ -198,6 +199,8 @@ use App\Listeners\Invoice\InvoiceRestoredActivity;
use App\Listeners\Invoice\InvoiceReversedActivity; use App\Listeners\Invoice\InvoiceReversedActivity;
use App\Listeners\Payment\PaymentRestoredActivity; use App\Listeners\Payment\PaymentRestoredActivity;
use App\Listeners\Quote\QuoteApprovedNotification; use App\Listeners\Quote\QuoteApprovedNotification;
use SocialiteProviders\Apple\AppleExtendSocialite;
use SocialiteProviders\Manager\SocialiteWasCalled;
use App\Events\Subscription\SubscriptionWasCreated; use App\Events\Subscription\SubscriptionWasCreated;
use App\Events\Subscription\SubscriptionWasDeleted; use App\Events\Subscription\SubscriptionWasDeleted;
use App\Events\Subscription\SubscriptionWasUpdated; use App\Events\Subscription\SubscriptionWasUpdated;
@ -221,10 +224,12 @@ use App\Listeners\Invoice\InvoiceEmailFailedActivity;
use App\Events\PurchaseOrder\PurchaseOrderWasAccepted; use App\Events\PurchaseOrder\PurchaseOrderWasAccepted;
use App\Events\PurchaseOrder\PurchaseOrderWasArchived; use App\Events\PurchaseOrder\PurchaseOrderWasArchived;
use App\Events\PurchaseOrder\PurchaseOrderWasRestored; use App\Events\PurchaseOrder\PurchaseOrderWasRestored;
use App\Listeners\Payment\PaymentEmailFailureActivity;
use App\Listeners\Vendor\UpdateVendorContactLastLogin; use App\Listeners\Vendor\UpdateVendorContactLastLogin;
use App\Events\RecurringQuote\RecurringQuoteWasCreated; use App\Events\RecurringQuote\RecurringQuoteWasCreated;
use App\Events\RecurringQuote\RecurringQuoteWasDeleted; use App\Events\RecurringQuote\RecurringQuoteWasDeleted;
use App\Events\RecurringQuote\RecurringQuoteWasUpdated; use App\Events\RecurringQuote\RecurringQuoteWasUpdated;
use App\Listeners\Account\StripeConnectFailureListener;
use App\Listeners\Activity\CreatedSubscriptionActivity; use App\Listeners\Activity\CreatedSubscriptionActivity;
use App\Listeners\Activity\SubscriptionDeletedActivity; use App\Listeners\Activity\SubscriptionDeletedActivity;
use App\Listeners\Activity\SubscriptionUpdatedActivity; use App\Listeners\Activity\SubscriptionUpdatedActivity;
@ -234,6 +239,7 @@ use App\Events\RecurringQuote\RecurringQuoteWasRestored;
use App\Listeners\Activity\SubscriptionArchivedActivity; use App\Listeners\Activity\SubscriptionArchivedActivity;
use App\Listeners\Activity\SubscriptionRestoredActivity; use App\Listeners\Activity\SubscriptionRestoredActivity;
use App\Listeners\Invoice\InvoiceFailedEmailNotification; use App\Listeners\Invoice\InvoiceFailedEmailNotification;
use SocialiteProviders\Microsoft\MicrosoftExtendSocialite;
use App\Events\RecurringExpense\RecurringExpenseWasCreated; use App\Events\RecurringExpense\RecurringExpenseWasCreated;
use App\Events\RecurringExpense\RecurringExpenseWasDeleted; use App\Events\RecurringExpense\RecurringExpenseWasDeleted;
use App\Events\RecurringExpense\RecurringExpenseWasUpdated; use App\Events\RecurringExpense\RecurringExpenseWasUpdated;
@ -587,6 +593,9 @@ class EventServiceProvider extends ServiceProvider
TaskWasRestored::class => [ TaskWasRestored::class => [
TaskRestoredActivity::class, TaskRestoredActivity::class,
], ],
StripeConnectFailure::class => [
StripeConnectFailureListener::class,
],
SubscriptionWasCreated::class => [ SubscriptionWasCreated::class => [
CreatedSubscriptionActivity::class, CreatedSubscriptionActivity::class,
], ],

View File

@ -136,7 +136,11 @@ class InvoiceMigrationRepository extends BaseRepository
$state['finished_amount'] = $model->amount; $state['finished_amount'] = $model->amount;
$model = $model->service()->applyNumber()->setReminder()->save(); $model = $model->service()->applyNumber()->save();
if ($class->name == Invoice::class) {
$model->service()->setReminder()->save();
}
if ($class->name == Invoice::class || $class->name == RecurringInvoice::class) { if ($class->name == Invoice::class || $class->name == RecurringInvoice::class) {
if (($state['finished_amount'] != $state['starting_amount']) && ($model->status_id != Invoice::STATUS_DRAFT)) { if (($state['finished_amount'] != $state['starting_amount']) && ($model->status_id != Invoice::STATUS_DRAFT)) {

View File

@ -57,7 +57,7 @@ class PurchaseOrderExpense
$expense->number = empty($expense->number) ? $this->getNextExpenseNumber($expense) : $expense->number; $expense->number = empty($expense->number) ? $this->getNextExpenseNumber($expense) : $expense->number;
$expense->save(); $expense->saveQuietly();
event('eloquent.created: App\Models\Expense', $expense); event('eloquent.created: App\Models\Expense', $expense);
$this->purchase_order->expense_id = $expense->id; $this->purchase_order->expense_id = $expense->id;

View File

@ -20,11 +20,8 @@ class PurchaseOrderService
{ {
use MakesHash; use MakesHash;
public PurchaseOrder $purchase_order; public function __construct(public PurchaseOrder $purchase_order)
public function __construct(PurchaseOrder $purchase_order)
{ {
$this->purchase_order = $purchase_order;
} }
public function createInvitations() public function createInvitations()
@ -156,7 +153,7 @@ class PurchaseOrderService
/** /**
* Saves the purchase order. * Saves the purchase order.
* @return \App\Models\PurchaseOrder object * @return \App\Models\PurchaseOrder
*/ */
public function save(): ?PurchaseOrder public function save(): ?PurchaseOrder
{ {

View File

@ -52,8 +52,8 @@ class TemplateEngine
public $template; public $template;
/** @var \App\Models\Invoice | \App\Models\Quote | \App\Models\Credit | \App\Models\PurchaseOrder | \App\Models\RecurringInvoice $entity_obj **/ /** @var \App\Models\Invoice | \App\Models\Quote | \App\Models\Credit | \App\Models\PurchaseOrder | \App\Models\RecurringInvoice | \App\Models\Payment $entity_obj **/
private \App\Models\Invoice | \App\Models\Quote | \App\Models\Credit | \App\Models\PurchaseOrder | \App\Models\RecurringInvoice $entity_obj; private \App\Models\Invoice | \App\Models\Quote | \App\Models\Credit | \App\Models\PurchaseOrder | \App\Models\RecurringInvoice | \App\Models\Payment $entity_obj;
/** @var \App\Models\Company | \App\Models\Client | null $settings_entity **/ /** @var \App\Models\Company | \App\Models\Client | null $settings_entity **/
private $settings_entity; private $settings_entity;

743
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -22,8 +22,8 @@ return [
*/ */
'batch' => true, 'batch' => true,
'cache_connection' => 'sentinel-cache', 'cache_connection' => 'sentinel-cache',
// 'cache_connection' => 'cache',
/* /*
* The default key used to store * The default key used to store
* metrics for batching * metrics for batching

View File

@ -8,6 +8,7 @@ parameters:
excludePaths: excludePaths:
- 'vendor/' - 'vendor/'
- 'app/Jobs/Ninja/*' - 'app/Jobs/Ninja/*'
- 'app/Models/Presenters/*'
- 'app/Console/Commands/*' - 'app/Console/Commands/*'
- 'app/DataMapper/Analytics/*' - 'app/DataMapper/Analytics/*'
- 'app/PaymentDrivers/Authorize/*' - 'app/PaymentDrivers/Authorize/*'
@ -17,10 +18,7 @@ parameters:
- App\DataMapper\FeesAndLimits - App\DataMapper\FeesAndLimits
reportUnmatchedIgnoredErrors: false reportUnmatchedIgnoredErrors: false
ignoreErrors: ignoreErrors:
- '#Call to an undefined method [a-zA-Z0-9\\_]+::company\(\)#'
- '#Call to an undefined method [a-zA-Z0-9\\_]+::entityFilter\(\)#'
- '#Call to an undefined method [a-zA-Z0-9\\_]+::exclude\(\)#'
- '#Array has 2 duplicate keys with value#' - '#Array has 2 duplicate keys with value#'
- '#Undefined method#' - '#Call to an undefined method#'
- '#makeHidden#' - '#makeHidden#'
- '#Socialite#' - '#Socialite#'

View File

@ -0,0 +1,59 @@
@component('email.template.admin', ['design' => 'light', 'settings' => $settings, 'logo' => $logo])
<div class="center">
@isset($greeting)
<p>{{ $greeting }}</p>
@endisset
@isset($title)
<h1>{{ $title }}</h1>
@endisset
@isset($h2)
<h2>{{ $title }}</h2>
@endisset
<div style="margin-top: 10px; margin-bottom: 30px;">
@isset($body)
{!! $body !!}
@endisset
@isset($slot)
{{ $slot }}
@endisset
</div>
@isset($additional_info)
<p>{{ $additional_info }}</p>
@endisset
@isset($url)
<!--[if (gte mso 9)|(IE)]>
<table align="center" cellspacing="0" cellpadding="0" style="width: 600px;">
<tr>
<td align="center" valign="top">
<![endif]-->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" >
<tbody><tr>
<td align="center" class="new_button" style="border-radius: 2px; background-color: {{ $settings->primary_color }} ;">
<a href="{{ $url }}" target="_blank" class="new_button" style="text-decoration: none; border: 1px solid {{ $settings->primary_color }}; display: inline-block; border-radius: 2px; padding-top: 15px; padding-bottom: 15px; padding-left: 25px; padding-right: 25px; font-size: 20px; color: #fff">
<singleline label="cta button">{{ ctrans($button) }}</singleline>
</a>
</td>
</tr>
</tbody>
</table>
<!--[if (gte mso 9)|(IE)]>
</td>
</tr>
</table>
<![endif]-->
@endisset
@isset($signature)
<p>{!! nl2br($signature) !!}</p>
@endisset
</div>
@endcomponent

View File

@ -0,0 +1,5 @@
{!! $title !!}
{!! $text_body !!}
{!! $url !!}