diff --git a/app/Http/Controllers/ClientPortal/NinjaPlanController.php b/app/Http/Controllers/ClientPortal/NinjaPlanController.php index 3ccdcc833bc8..2e109d39bf46 100644 --- a/app/Http/Controllers/ClientPortal/NinjaPlanController.php +++ b/app/Http/Controllers/ClientPortal/NinjaPlanController.php @@ -92,7 +92,10 @@ class NinjaPlanController extends Controller ->first(); //account status means user cannot perform upgrades until they pay their account. - $data['late_invoice'] = $late_invoice; + // $data['late_invoice'] = $late_invoice; + + //14-01-2022 remove late invoices from blocking upgrades + $data['late_invoice'] = false; } diff --git a/app/Http/Controllers/ClientPortal/SwitchCompanyController.php b/app/Http/Controllers/ClientPortal/SwitchCompanyController.php index c8e7162c4e70..bd9eaa81ef1b 100644 --- a/app/Http/Controllers/ClientPortal/SwitchCompanyController.php +++ b/app/Http/Controllers/ClientPortal/SwitchCompanyController.php @@ -29,6 +29,8 @@ class SwitchCompanyController extends Controller auth()->guard('contact')->loginUsingId($client_contact->id, true); + request()->session()->regenerate(); + return redirect('/client/dashboard'); } } diff --git a/app/Http/Controllers/PostMarkController.php b/app/Http/Controllers/PostMarkController.php index 6646f1724327..ac13bb17e6d1 100644 --- a/app/Http/Controllers/PostMarkController.php +++ b/app/Http/Controllers/PostMarkController.php @@ -13,6 +13,7 @@ namespace App\Http\Controllers; use App\DataMapper\Analytics\Mail\EmailBounce; use App\DataMapper\Analytics\Mail\EmailSpam; +use App\Jobs\PostMark\ProcessPostmarkWebhook; use App\Jobs\Util\SystemLogger; use App\Libraries\MultiDB; use App\Models\CreditInvitation; @@ -77,27 +78,7 @@ class PostMarkController extends BaseController if($request->header('X-API-SECURITY') && $request->header('X-API-SECURITY') == config('postmark.secret')) { - MultiDB::findAndSetDbByCompanyKey($request->input('Tag')); - - $this->invitation = $this->discoverInvitation($request->input('MessageID')); - - if($this->invitation) - $this->invitation->email_error = $request->input('Details'); - else - return response()->json(['message' => 'Message not found']); - - switch ($request->input('RecordType')) - { - case 'Delivery': - return $this->processDelivery($request); - case 'Bounce': - return $this->processBounce($request); - case 'SpamComplaint': - return $this->processSpamComplaint($request); - default: - # code... - break; - } + ProcessPostmarkWebhook::dispatch($request->all()); return response()->json(['message' => 'Success'], 200); @@ -107,139 +88,4 @@ class PostMarkController extends BaseController } -// { -// "RecordType": "Delivery", -// "ServerID": 23, -// "MessageStream": "outbound", -// "MessageID": "00000000-0000-0000-0000-000000000000", -// "Recipient": "john@example.com", -// "Tag": "welcome-email", -// "DeliveredAt": "2021-02-21T16:34:52Z", -// "Details": "Test delivery webhook details", -// "Metadata": { -// "example": "value", -// "example_2": "value" -// } -// } - private function processDelivery($request) - { - $this->invitation->email_status = 'delivered'; - $this->invitation->save(); - - SystemLogger::dispatch($request->all(), - SystemLog::CATEGORY_MAIL, - SystemLog::EVENT_MAIL_DELIVERY, - SystemLog::TYPE_WEBHOOK_RESPONSE, - $this->invitation->contact->client, - $this->invitation->company - ); - } - -// { -// "Metadata": { -// "example": "value", -// "example_2": "value" -// }, -// "RecordType": "Bounce", -// "ID": 42, -// "Type": "HardBounce", -// "TypeCode": 1, -// "Name": "Hard bounce", -// "Tag": "Test", -// "MessageID": "00000000-0000-0000-0000-000000000000", -// "ServerID": 1234, -// "MessageStream": "outbound", -// "Description": "The server was unable to deliver your message (ex: unknown user, mailbox not found).", -// "Details": "Test bounce details", -// "Email": "john@example.com", -// "From": "sender@example.com", -// "BouncedAt": "2021-02-21T16:34:52Z", -// "DumpAvailable": true, -// "Inactive": true, -// "CanActivate": true, -// "Subject": "Test subject", -// "Content": "Test content" -// } - - private function processBounce($request) - { - $this->invitation->email_status = 'bounced'; - $this->invitation->save(); - - $bounce = new EmailBounce( - $request->input('Tag'), - $request->input('From'), - $request->input('MessageID') - ); - - LightLogs::create($bounce)->queue(); - - SystemLogger::dispatch($request->all(), SystemLog::CATEGORY_MAIL, SystemLog::EVENT_MAIL_BOUNCED, SystemLog::TYPE_WEBHOOK_RESPONSE, $this->invitation->contact->client, $this->invitation->company); - - if(config('ninja.notification.slack')) - $this->invitation->company->notification(new EmailBounceNotification($this->invitation->company->account))->ninja(); - - } - -// { -// "Metadata": { -// "example": "value", -// "example_2": "value" -// }, -// "RecordType": "SpamComplaint", -// "ID": 42, -// "Type": "SpamComplaint", -// "TypeCode": 100001, -// "Name": "Spam complaint", -// "Tag": "Test", -// "MessageID": "00000000-0000-0000-0000-000000000000", -// "ServerID": 1234, -// "MessageStream": "outbound", -// "Description": "The subscriber explicitly marked this message as spam.", -// "Details": "Test spam complaint details", -// "Email": "john@example.com", -// "From": "sender@example.com", -// "BouncedAt": "2021-02-21T16:34:52Z", -// "DumpAvailable": true, -// "Inactive": true, -// "CanActivate": false, -// "Subject": "Test subject", -// "Content": "Test content" -// } - private function processSpamComplaint($request) - { - - $this->invitation->email_status = 'spam'; - $this->invitation->save(); - - $spam = new EmailSpam( - $request->input('Tag'), - $request->input('From'), - $request->input('MessageID') - ); - - LightLogs::create($spam)->queue(); - - SystemLogger::dispatch($request->all(), SystemLog::CATEGORY_MAIL, SystemLog::EVENT_MAIL_SPAM_COMPLAINT, SystemLog::TYPE_WEBHOOK_RESPONSE, $this->invitation->contact->client, $this->invitation->company); - - if(config('ninja.notification.slack')) - $this->invitation->company->notification(new EmailSpamNotification($this->invitation->company->account))->ninja(); - - } - - private function discoverInvitation($message_id) - { - $invitation = false; - - if($invitation = InvoiceInvitation::where('message_id', $message_id)->first()) - return $invitation; - elseif($invitation = QuoteInvitation::where('message_id', $message_id)->first()) - return $invitation; - elseif($invitation = RecurringInvoiceInvitation::where('message_id', $message_id)->first()) - return $invitation; - elseif($invitation = CreditInvitation::where('message_id', $message_id)->first()) - return $invitation; - else - return $invitation; - } } diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index 6b82749df99c..096026772db8 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -28,6 +28,7 @@ use App\Http\Middleware\PasswordProtection; use App\Http\Middleware\PhantomSecret; use App\Http\Middleware\QueryLogging; use App\Http\Middleware\RedirectIfAuthenticated; +use App\Http\Middleware\SessionDomains; use App\Http\Middleware\SetDb; use App\Http\Middleware\SetDbByCompanyKey; use App\Http\Middleware\SetDocumentDb; @@ -83,6 +84,7 @@ class Kernel extends HttpKernel */ protected $middlewareGroups = [ 'web' => [ + SessionDomains::class, EncryptCookies::class, AddQueuedCookiesToResponse::class, StartSession::class, @@ -103,6 +105,7 @@ class Kernel extends HttpKernel 'query_logging', ], 'client' => [ + SessionDomains::class, EncryptCookies::class, AddQueuedCookiesToResponse::class, StartSession::class, @@ -161,10 +164,12 @@ class Kernel extends HttpKernel 'check_client_existence' => CheckClientExistence::class, 'user_verified' => UserVerified::class, 'document_db' => SetDocumentDb::class, + 'session_domain' => SessionDomains::class, ]; protected $middlewarePriority = [ + SessionDomains::class, Cors::class, SetDomainNameDb::class, SetDb::class, diff --git a/app/Http/Livewire/BillingPortalPurchase.php b/app/Http/Livewire/BillingPortalPurchase.php index d93a639d43eb..e6a5de79c66e 100644 --- a/app/Http/Livewire/BillingPortalPurchase.php +++ b/app/Http/Livewire/BillingPortalPurchase.php @@ -409,7 +409,7 @@ class BillingPortalPurchase extends Component 'quantity' => $this->quantity, 'contact_id' => $this->contact->id, 'client_id' => $this->contact->client->id, - 'coupon' => '', + 'coupon' => $this->coupon, ]); } diff --git a/app/Http/Middleware/CheckClientExistence.php b/app/Http/Middleware/CheckClientExistence.php index f8238db1e6e2..554e74d8e6a1 100644 --- a/app/Http/Middleware/CheckClientExistence.php +++ b/app/Http/Middleware/CheckClientExistence.php @@ -42,7 +42,7 @@ class CheckClientExistence return $query->where('is_deleted', false); }) ->whereHas('company', function ($query){ - return $query->where('account_id', auth('contact')->user()->client->company->account->id); + return $query->where('id', auth('contact')->user()->client->company_id); }) ->get(); diff --git a/app/Http/Middleware/SessionDomains.php b/app/Http/Middleware/SessionDomains.php new file mode 100644 index 000000000000..d24ab7955796 --- /dev/null +++ b/app/Http/Middleware/SessionDomains.php @@ -0,0 +1,50 @@ +getHost(); + + if (strpos($domain_name, 'invoicing.co') !== false) + { + config(['session.domain' => '.invoicing.co']); + } + else{ + + Cookie::queue(Cookie::forget('invoice_ninja_session', '/')); + + config(['session.domain' => $domain_name]); + + } + + return $next($request); + } +} \ No newline at end of file diff --git a/app/Http/Middleware/VerifyCsrfToken.php b/app/Http/Middleware/VerifyCsrfToken.php index d0909c4b0bd3..90ed285e184e 100644 --- a/app/Http/Middleware/VerifyCsrfToken.php +++ b/app/Http/Middleware/VerifyCsrfToken.php @@ -12,6 +12,7 @@ namespace App\Http\Middleware; use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware; +use Illuminate\Session\TokenMismatchException; class VerifyCsrfToken extends Middleware { @@ -30,4 +31,17 @@ class VerifyCsrfToken extends Middleware protected $except = [ // 'livewire/message/*' ]; + + + // public function handle($request, \Closure $next) { + + // try { + // return parent::handle($request, $next); + // } catch (TokenMismatchException $ex) { + + // throw new TokenMismatchException('CSRF token mismatch.'); + + // } + // } + } diff --git a/app/Http/Requests/Client/StoreClientRequest.php b/app/Http/Requests/Client/StoreClientRequest.php index b2b8b6774a01..b1525656173a 100644 --- a/app/Http/Requests/Client/StoreClientRequest.php +++ b/app/Http/Requests/Client/StoreClientRequest.php @@ -89,8 +89,6 @@ class StoreClientRequest extends Request protected function prepareForValidation() { $input = $this->all(); - - //@todo implement feature permissions for > 50 clients $settings = ClientSettings::defaults(); @@ -133,6 +131,10 @@ class StoreClientRequest extends Request $input['shipping_country_id'] = $this->getCountryCode($input['shipping_country_code']); } + /* If there is no number, just unset it here. */ + if(array_key_exists('number', $input) && ( is_null($input['number']) || empty($input['number']))) + unset($input['number']); + $this->replace($input); } diff --git a/app/Http/Requests/Payment/UpdatePaymentRequest.php b/app/Http/Requests/Payment/UpdatePaymentRequest.php index e62826a2489e..2ac4983bc6a6 100644 --- a/app/Http/Requests/Payment/UpdatePaymentRequest.php +++ b/app/Http/Requests/Payment/UpdatePaymentRequest.php @@ -71,10 +71,6 @@ class UpdatePaymentRequest extends Request unset($input['amount']); } - // if (isset($input['number'])) { - // unset($input['number']); - // } - if (isset($input['invoices']) && is_array($input['invoices']) !== false) { foreach ($input['invoices'] as $key => $value) { $input['invoices'][$key]['invoice_id'] = $this->decodePrimaryKey($value['invoice_id']); diff --git a/app/Http/Requests/RecurringInvoice/StoreRecurringInvoiceRequest.php b/app/Http/Requests/RecurringInvoice/StoreRecurringInvoiceRequest.php index 7c527fd7f858..f292c7e4f230 100644 --- a/app/Http/Requests/RecurringInvoice/StoreRecurringInvoiceRequest.php +++ b/app/Http/Requests/RecurringInvoice/StoreRecurringInvoiceRequest.php @@ -111,6 +111,10 @@ class StoreRecurringInvoiceRequest extends Request } } + /* If there is no number, just unset it here. */ + if(array_key_exists('number', $input) && ( is_null($input['number']) || empty($input['number']))) + unset($input['number']); + $this->replace($input); } diff --git a/app/Http/ValidationRules/PaymentAppliedValidAmount.php b/app/Http/ValidationRules/PaymentAppliedValidAmount.php index 292712a1d738..357cde9f0ea0 100644 --- a/app/Http/ValidationRules/PaymentAppliedValidAmount.php +++ b/app/Http/ValidationRules/PaymentAppliedValidAmount.php @@ -42,7 +42,7 @@ class PaymentAppliedValidAmount implements Rule private function calculateAmounts() :bool { - $payment = Payment::whereId($this->decodePrimaryKey(request()->segment(4)))->company()->first(); + $payment = Payment::withTrashed()->whereId($this->decodePrimaryKey(request()->segment(4)))->company()->first(); if (! $payment) { return false; @@ -53,6 +53,15 @@ class PaymentAppliedValidAmount implements Rule $payment_amounts = $payment->amount - $payment->refunded - $payment->applied; + if(request()->has('credits') + && is_array(request()->input('credits')) + && count(request()->input('credits')) == 0 + && request()->has('invoices') + && is_array(request()->input('invoices')) + && count(request()->input('invoices')) == 0){ + return true; + } + if (request()->input('credits') && is_array(request()->input('credits'))) { foreach (request()->input('credits') as $credit) { $payment_amounts += $credit['amount']; diff --git a/app/Jobs/Mail/PaymentFailedMailer.php b/app/Jobs/Mail/PaymentFailedMailer.php index 9bdeafd63816..aebc9cbb330c 100644 --- a/app/Jobs/Mail/PaymentFailedMailer.php +++ b/app/Jobs/Mail/PaymentFailedMailer.php @@ -31,6 +31,7 @@ use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; use Illuminate\Support\Facades\Mail; +use Illuminate\Support\Facades\App; /*Multi Mailer implemented*/ @@ -74,8 +75,10 @@ class PaymentFailedMailer implements ShouldQueue //Set DB MultiDB::setDb($this->company->db); - + App::setLocale($this->client->locale()); + $settings = $this->client->getMergedSettings(); + $amount = 0; $invoice = false; diff --git a/app/Jobs/PostMark/ProcessPostmarkWebhook.php b/app/Jobs/PostMark/ProcessPostmarkWebhook.php new file mode 100644 index 000000000000..e3444172777c --- /dev/null +++ b/app/Jobs/PostMark/ProcessPostmarkWebhook.php @@ -0,0 +1,233 @@ +request = $request; + } + + /** + * Execute the job. + * + * + * @return void + */ + public function handle() + { + + MultiDB::findAndSetDbByCompanyKey($this->request['Tag']); + + $this->invitation = $this->discoverInvitation($this->request['MessageID']); + + if(!$this->invitation) + return; + + $this->invitation->email_error = $this->request['Details']; + + switch ($this->request['RecordType']) + { + case 'Delivery': + return $this->processDelivery(); + case 'Bounce': + return $this->processBounce(); + case 'SpamComplaint': + return $this->processSpamComplaint(); + default: + # code... + break; + } + + } + +// { +// "RecordType": "Delivery", +// "ServerID": 23, +// "MessageStream": "outbound", +// "MessageID": "00000000-0000-0000-0000-000000000000", +// "Recipient": "john@example.com", +// "Tag": "welcome-email", +// "DeliveredAt": "2021-02-21T16:34:52Z", +// "Details": "Test delivery webhook details", +// "Metadata": { +// "example": "value", +// "example_2": "value" +// } +// } + private function processDelivery() + { + $this->invitation->email_status = 'delivered'; + $this->invitation->save(); + + SystemLogger::dispatch($this->request, + SystemLog::CATEGORY_MAIL, + SystemLog::EVENT_MAIL_DELIVERY, + SystemLog::TYPE_WEBHOOK_RESPONSE, + $this->invitation->contact->client, + $this->invitation->company + ); + } + +// { +// "Metadata": { +// "example": "value", +// "example_2": "value" +// }, +// "RecordType": "Bounce", +// "ID": 42, +// "Type": "HardBounce", +// "TypeCode": 1, +// "Name": "Hard bounce", +// "Tag": "Test", +// "MessageID": "00000000-0000-0000-0000-000000000000", +// "ServerID": 1234, +// "MessageStream": "outbound", +// "Description": "The server was unable to deliver your message (ex: unknown user, mailbox not found).", +// "Details": "Test bounce details", +// "Email": "john@example.com", +// "From": "sender@example.com", +// "BouncedAt": "2021-02-21T16:34:52Z", +// "DumpAvailable": true, +// "Inactive": true, +// "CanActivate": true, +// "Subject": "Test subject", +// "Content": "Test content" +// } + + private function processBounce() + { + $this->invitation->email_status = 'bounced'; + $this->invitation->save(); + + $bounce = new EmailBounce( + $this->request['Tag'], + $this->request['From'], + $this->request['MessageID'] + ); + + LightLogs::create($bounce)->queue(); + + SystemLogger::dispatch($this->request, SystemLog::CATEGORY_MAIL, SystemLog::EVENT_MAIL_BOUNCED, SystemLog::TYPE_WEBHOOK_RESPONSE, $this->invitation->contact->client, $this->invitation->company); + + if(config('ninja.notification.slack')) + $this->invitation->company->notification(new EmailBounceNotification($this->invitation->company->account))->ninja(); + + } + +// { +// "Metadata": { +// "example": "value", +// "example_2": "value" +// }, +// "RecordType": "SpamComplaint", +// "ID": 42, +// "Type": "SpamComplaint", +// "TypeCode": 100001, +// "Name": "Spam complaint", +// "Tag": "Test", +// "MessageID": "00000000-0000-0000-0000-000000000000", +// "ServerID": 1234, +// "MessageStream": "outbound", +// "Description": "The subscriber explicitly marked this message as spam.", +// "Details": "Test spam complaint details", +// "Email": "john@example.com", +// "From": "sender@example.com", +// "BouncedAt": "2021-02-21T16:34:52Z", +// "DumpAvailable": true, +// "Inactive": true, +// "CanActivate": false, +// "Subject": "Test subject", +// "Content": "Test content" +// } + private function processSpamComplaint() + { + + $this->invitation->email_status = 'spam'; + $this->invitation->save(); + + $spam = new EmailSpam( + $this->request['Tag'], + $this->request['From'], + $this->request['MessageID'] + ); + + LightLogs::create($spam)->queue(); + + SystemLogger::dispatch($this->request, SystemLog::CATEGORY_MAIL, SystemLog::EVENT_MAIL_SPAM_COMPLAINT, SystemLog::TYPE_WEBHOOK_RESPONSE, $this->invitation->contact->client, $this->invitation->company); + + if(config('ninja.notification.slack')) + $this->invitation->company->notification(new EmailSpamNotification($this->invitation->company->account))->ninja(); + + } + + private function discoverInvitation($message_id) + { + $invitation = false; + + if($invitation = InvoiceInvitation::where('message_id', $message_id)->first()) + return $invitation; + elseif($invitation = QuoteInvitation::where('message_id', $message_id)->first()) + return $invitation; + elseif($invitation = RecurringInvoiceInvitation::where('message_id', $message_id)->first()) + return $invitation; + elseif($invitation = CreditInvitation::where('message_id', $message_id)->first()) + return $invitation; + else + return $invitation; + } +} \ No newline at end of file diff --git a/app/Jobs/Util/ReminderJob.php b/app/Jobs/Util/ReminderJob.php index 05c2e8f6bfbd..88a0aabf679c 100644 --- a/app/Jobs/Util/ReminderJob.php +++ b/app/Jobs/Util/ReminderJob.php @@ -83,7 +83,8 @@ class ReminderJob implements ShouldQueue $invoice->service()->touchPdf(); //check if this reminder needs to be emailed - if(in_array($reminder_template, ['reminder1','reminder2','reminder3','reminder_endless']) && $invoice->client->getSetting("enable_".$reminder_template)) + //15-01-2022 - insert addition if block if send_reminders is definitely set + if(in_array($reminder_template, ['reminder1','reminder2','reminder3','reminder_endless']) && $invoice->client->getSetting("enable_".$reminder_template) && $invoice->client->getSetting("send_reminders")) { $invoice->invitations->each(function ($invitation) use ($invoice, $reminder_template) { EmailEntity::dispatch($invitation, $invitation->company, $reminder_template); diff --git a/app/Mail/Admin/ClientPaymentFailureObject.php b/app/Mail/Admin/ClientPaymentFailureObject.php index ad2629c3f27a..265845280538 100644 --- a/app/Mail/Admin/ClientPaymentFailureObject.php +++ b/app/Mail/Admin/ClientPaymentFailureObject.php @@ -124,7 +124,8 @@ class ClientPaymentFailureObject 'settings' => $this->client->getMergedSettings(), 'whitelabel' => $this->company->account->isPaid() ? true : false, 'url' => $this->invoices->first()->invitations->first()->getPaymentLink(), - 'button' => 'texts.pay_now', + // 'button' => 'texts.pay_now', + 'button' => ctrans('texts.pay_now'), 'additional_info' => false, 'company' => $this->company, ]; diff --git a/app/Mail/BouncedEmail.php b/app/Mail/BouncedEmail.php index 030303a0b912..9227c39eb6d6 100644 --- a/app/Mail/BouncedEmail.php +++ b/app/Mail/BouncedEmail.php @@ -16,6 +16,7 @@ use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Mail\Mailable; use Illuminate\Queue\SerializesModels; +use Illuminate\Support\Facades\App; class BouncedEmail extends Mailable { @@ -34,9 +35,11 @@ class BouncedEmail extends Mailable */ public function build() { + App::setLocale($this->invitation->company->getLocale()); + $entity_type = class_basename(lcfirst($this->invitation->getEntityType())); - $subject = ctrans("texts.notification_{$entity_type}_bounced_subject", ['invoice' => $invoice->number]); + $subject = ctrans("texts.notification_{$entity_type}_bounced_subject", ['invoice' => $this->invitation->invoice->number]); return $this->from(config('mail.from.address'), config('mail.from.name')) diff --git a/app/Mail/ClientContact/ClientContactResetPasswordObject.php b/app/Mail/ClientContact/ClientContactResetPasswordObject.php index 77fdeba30029..a911045ff922 100644 --- a/app/Mail/ClientContact/ClientContactResetPasswordObject.php +++ b/app/Mail/ClientContact/ClientContactResetPasswordObject.php @@ -35,19 +35,19 @@ class ClientContactResetPasswordObject public function build() { + $settings = $this->client_contact->client->getMergedSettings(); App::forgetInstance('translator'); $t = app('translator'); App::setLocale($this->client_contact->preferredLocale()); - $t->replace(Ninja::transformTranslations($this->client_contact->client->getMergedSettings())); - + $t->replace(Ninja::transformTranslations($settings)); $data = [ 'title' => ctrans('texts.your_password_reset_link'), 'content' => ctrans('texts.reset_password'), 'url' => route('client.password.reset', ['token' => $this->token, 'email' => $this->client_contact->email]), 'button' => ctrans('texts.reset'), - 'signature' => $this->company->settings->email_signature, - 'settings' => $this->company->settings, + 'signature' => $settings->email_signature, + 'settings' => $settings, 'company' => $this->company, 'logo' => $this->company->present()->logo(), ]; diff --git a/app/Mail/Company/CompanyDeleted.php b/app/Mail/Company/CompanyDeleted.php index da021afeaf29..5f32f7d6071f 100644 --- a/app/Mail/Company/CompanyDeleted.php +++ b/app/Mail/Company/CompanyDeleted.php @@ -15,6 +15,7 @@ namespace App\Mail\Company; use Illuminate\Bus\Queueable; use Illuminate\Mail\Mailable; use Illuminate\Queue\SerializesModels; +use Illuminate\Support\Facades\App; class CompanyDeleted extends Mailable { @@ -47,6 +48,8 @@ class CompanyDeleted extends Mailable */ public function build() { + App::setLocale($this->company->getLocale()); + return $this->from(config('mail.from.address'), config('mail.from.name')) ->subject(ctrans('texts.company_deleted')) ->view('email.admin.company_deleted') diff --git a/app/Mail/ContactPasswordlessLogin.php b/app/Mail/ContactPasswordlessLogin.php index d301df400fc3..4370041767b6 100644 --- a/app/Mail/ContactPasswordlessLogin.php +++ b/app/Mail/ContactPasswordlessLogin.php @@ -18,6 +18,7 @@ use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Mail\Mailable; use Illuminate\Queue\SerializesModels; +use Illuminate\Support\Facades\App; class ContactPasswordlessLogin extends Mailable { @@ -52,6 +53,9 @@ class ContactPasswordlessLogin extends Mailable */ public function build() { + + App::setLocale($this->company->getLocale()); + return $this ->subject(ctrans('texts.account_passwordless_login')) ->view('email.billing.passwordless-login', [ diff --git a/app/Mail/DownloadInvoices.php b/app/Mail/DownloadInvoices.php index 5dc1b5808cd0..ba82d325a710 100644 --- a/app/Mail/DownloadInvoices.php +++ b/app/Mail/DownloadInvoices.php @@ -6,6 +6,7 @@ use App\Models\Company; use Illuminate\Bus\Queueable; use Illuminate\Mail\Mailable; use Illuminate\Queue\SerializesModels; +use Illuminate\Support\Facades\App; class DownloadInvoices extends Mailable { @@ -27,6 +28,9 @@ class DownloadInvoices extends Mailable */ public function build() { + + App::setLocale($this->company->getLocale()); + return $this->from(config('mail.from.address'), config('mail.from.name')) ->subject(ctrans('texts.download_files')) ->view('email.admin.download_invoices', [ diff --git a/app/Mail/Engine/CreditEmailEngine.php b/app/Mail/Engine/CreditEmailEngine.php index ad9645b6c8f7..d319bac91577 100644 --- a/app/Mail/Engine/CreditEmailEngine.php +++ b/app/Mail/Engine/CreditEmailEngine.php @@ -45,6 +45,7 @@ class CreditEmailEngine extends BaseEmailEngine public function build() { App::forgetInstance('translator'); + $t = app('translator'); $t->replace(Ninja::transformTranslations($this->client->getMergedSettings())); diff --git a/app/Mail/Gateways/ACHVerificationNotification.php b/app/Mail/Gateways/ACHVerificationNotification.php index 322e6b96e2c8..cd4840f54af6 100644 --- a/app/Mail/Gateways/ACHVerificationNotification.php +++ b/app/Mail/Gateways/ACHVerificationNotification.php @@ -15,6 +15,7 @@ namespace App\Mail\Gateways; use App\Models\Company; use Illuminate\Mail\Mailable; use Illuminate\Queue\SerializesModels; +use Illuminate\Support\Facades\App; class ACHVerificationNotification extends Mailable { @@ -49,6 +50,8 @@ class ACHVerificationNotification extends Mailable */ public function build() { + App::setLocale($this->company->getLocale()); + return $this ->subject(ctrans('texts.ach_verification_notification_label')) ->view('email.gateways.ach-verification-notification', [ diff --git a/app/Mail/Import/CompanyImportFailure.php b/app/Mail/Import/CompanyImportFailure.php index 053c78ca1bbe..761c89fdcc98 100644 --- a/app/Mail/Import/CompanyImportFailure.php +++ b/app/Mail/Import/CompanyImportFailure.php @@ -14,6 +14,7 @@ namespace App\Mail\Import; use Illuminate\Bus\Queueable; use Illuminate\Mail\Mailable; use Illuminate\Queue\SerializesModels; +use Illuminate\Support\Facades\App; class CompanyImportFailure extends Mailable { @@ -51,6 +52,8 @@ class CompanyImportFailure extends Mailable */ public function build() { + App::setLocale($this->company->getLocale()); + $this->settings = $this->company->settings; $this->logo = $this->company->present()->logo(); $this->title = ctrans('texts.company_import_failure_subject', ['company' => $this->company->present()->name()]); diff --git a/app/Mail/Import/ImportCompleted.php b/app/Mail/Import/ImportCompleted.php index 2a1e04da60f9..58a4420f7f01 100644 --- a/app/Mail/Import/ImportCompleted.php +++ b/app/Mail/Import/ImportCompleted.php @@ -49,6 +49,8 @@ class ImportCompleted extends Mailable { App::forgetInstance('translator'); + App::setLocale($this->company->getLocale()); + $t = app('translator'); $t->replace(Ninja::transformTranslations($this->company->settings)); diff --git a/app/Mail/Migration/MaxCompanies.php b/app/Mail/Migration/MaxCompanies.php index 905ad6692aa1..c879be10e0a2 100644 --- a/app/Mail/Migration/MaxCompanies.php +++ b/app/Mail/Migration/MaxCompanies.php @@ -14,6 +14,7 @@ namespace App\Mail\Migration; use Illuminate\Bus\Queueable; use Illuminate\Mail\Mailable; use Illuminate\Queue\SerializesModels; +use Illuminate\Support\Facades\App; class MaxCompanies extends Mailable { @@ -48,6 +49,8 @@ class MaxCompanies extends Mailable */ public function build() { + App::setLocale($this->company->getLocale()); + $this->settings = $this->company->settings; $this->logo = $this->company->present()->logo(); $this->title = ctrans('texts.max_companies'); diff --git a/app/Mail/Migration/StripeConnectMigration.php b/app/Mail/Migration/StripeConnectMigration.php index 93fda725afac..7290cb41b8fb 100644 --- a/app/Mail/Migration/StripeConnectMigration.php +++ b/app/Mail/Migration/StripeConnectMigration.php @@ -14,6 +14,7 @@ namespace App\Mail\Migration; use Illuminate\Bus\Queueable; use Illuminate\Mail\Mailable; use Illuminate\Queue\SerializesModels; +use Illuminate\Support\Facades\App; class StripeConnectMigration extends Mailable { @@ -44,6 +45,8 @@ class StripeConnectMigration extends Mailable */ public function build() { + App::setLocale($this->company->getLocale()); + $this->settings = $this->company->settings; $this->logo = $this->company->present()->logo(); $this->whitelabel = $this->company->account->isPaid(); diff --git a/app/Mail/Ninja/EmailQuotaExceeded.php b/app/Mail/Ninja/EmailQuotaExceeded.php index dc4507f73a16..8d6ba0bf938b 100644 --- a/app/Mail/Ninja/EmailQuotaExceeded.php +++ b/app/Mail/Ninja/EmailQuotaExceeded.php @@ -14,6 +14,7 @@ namespace App\Mail\Ninja; use Illuminate\Bus\Queueable; use Illuminate\Mail\Mailable; use Illuminate\Queue\SerializesModels; +use Illuminate\Support\Facades\App; class EmailQuotaExceeded extends Mailable { @@ -47,6 +48,8 @@ class EmailQuotaExceeded extends Mailable */ public function build() { + App::setLocale($this->company->getLocale()); + $this->settings = $this->company->settings; $this->logo = $this->company->present()->logo(); $this->title = ctrans('texts.email_quota_exceeded_subject'); diff --git a/app/Mail/RecurringInvoice/ClientContactRequestCancellationObject.php b/app/Mail/RecurringInvoice/ClientContactRequestCancellationObject.php index a21b1dd55ece..6032a552fa24 100644 --- a/app/Mail/RecurringInvoice/ClientContactRequestCancellationObject.php +++ b/app/Mail/RecurringInvoice/ClientContactRequestCancellationObject.php @@ -37,6 +37,8 @@ class ClientContactRequestCancellationObject { App::forgetInstance('translator'); + App::setLocale($this->company->getLocale()); + $t = app('translator'); $t->replace(Ninja::transformTranslations($this->company->settings)); diff --git a/app/PaymentDrivers/BaseDriver.php b/app/PaymentDrivers/BaseDriver.php index a1e3e279eb38..bd232b655d41 100644 --- a/app/PaymentDrivers/BaseDriver.php +++ b/app/PaymentDrivers/BaseDriver.php @@ -222,7 +222,9 @@ class BaseDriver extends AbstractPaymentDriver */ public function createPayment($data, $status = Payment::STATUS_COMPLETED): Payment { - $this->confirmGatewayFee(); + + if(in_array($status, [Payment::STATUS_COMPLETED, Payment::STATUS_PENDING]) ) + $this->confirmGatewayFee(); /*Never create a payment with a duplicate transaction reference*/ if(array_key_exists('transaction_reference', $data)){ @@ -253,6 +255,10 @@ class BaseDriver extends AbstractPaymentDriver $payment->client_contact_id = $client_contact_id; $payment->saveQuietly(); + /* Return early if the payment is no completed or pending*/ + if(!in_array($status, [Payment::STATUS_COMPLETED, Payment::STATUS_PENDING]) ) + return $payment; + $this->payment_hash->payment_id = $payment->id; $this->payment_hash->save(); diff --git a/app/PaymentDrivers/MolliePaymentDriver.php b/app/PaymentDrivers/MolliePaymentDriver.php index 78e6b495c295..4ddccd9fe5cb 100644 --- a/app/PaymentDrivers/MolliePaymentDriver.php +++ b/app/PaymentDrivers/MolliePaymentDriver.php @@ -312,11 +312,12 @@ class MolliePaymentDriver extends BaseDriver if($record){ $client = $record->client; + $this->client = $client; } else{ $client = Client::withTrashed()->find($this->decodePrimaryKey($payment->metadata->client_id)); - + $this->client = $client; // sometimes if the user is not returned to the site with a response from Mollie // we may not have a payment record - in these cases we need to re-construct the payment // record from the meta data in the payment hash. @@ -326,6 +327,9 @@ class MolliePaymentDriver extends BaseDriver /* Harvest Payment Hash*/ $payment_hash = PaymentHash::where('hash', $payment->metadata->hash)->first(); + /* If we are here, then we do not have access to the class payment hash, so lets set it here*/ + $this->payment_hash = $payment_hash; + $data = [ 'gateway_type_id' => $payment->metadata->gateway_type_id, 'amount' => $amount = array_sum(array_column($payment_hash->invoices(), 'amount')) + $payment_hash->fee_total, diff --git a/app/Transformers/ActivityTransformer.php b/app/Transformers/ActivityTransformer.php index 0959c8d9f4f5..e4c1442dd4cd 100644 --- a/app/Transformers/ActivityTransformer.php +++ b/app/Transformers/ActivityTransformer.php @@ -40,6 +40,7 @@ class ActivityTransformer extends EntityTransformer 'activity_type_id' => (string) $activity->activity_type_id, 'client_id' => $activity->client_id ? (string) $this->encodePrimaryKey($activity->client_id) : '', 'recurring_invoice_id' => $activity->recurring_invoice_id ? (string) $this->encodePrimaryKey($activity->recurring_invoice_id) : '', + 'recurring_expense_id' => $activity->recurring_expense_id ? (string) $this->encodePrimaryKey($activity->recurring_expense_id) : '', 'company_id' => $activity->company_id ? (string) $this->encodePrimaryKey($activity->company_id) : '', 'user_id' => (string) $this->encodePrimaryKey($activity->user_id), 'invoice_id' => $activity->invoice_id ? (string) $this->encodePrimaryKey($activity->invoice_id) : '', diff --git a/app/Utils/Traits/GeneratesCounter.php b/app/Utils/Traits/GeneratesCounter.php index c9494ca3b2b1..ecc16d9f6c8a 100644 --- a/app/Utils/Traits/GeneratesCounter.php +++ b/app/Utils/Traits/GeneratesCounter.php @@ -36,8 +36,6 @@ trait GeneratesCounter //todo in the form validation, we need to ensure that if a prefix and pattern is set we throw a validation error, //only one type is allow else this will cause confusion to the end user - - private function getNextEntityNumber($entity, Client $client, $is_recurring = false) { $prefix = ''; diff --git a/tests/Feature/ClientApiTest.php b/tests/Feature/ClientApiTest.php index a481c3775f79..0e67928252a4 100644 --- a/tests/Feature/ClientApiTest.php +++ b/tests/Feature/ClientApiTest.php @@ -68,6 +68,56 @@ class ClientApiTest extends TestCase } + public function testClientNoneValidation() + { + + $data = [ + 'name' => $this->faker->firstName, + 'number' => '', + ]; + + $response = false; + + try{ + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/clients/', $data); + } catch (ValidationException $e) { + $message = json_decode($e->validator->getMessageBag(), 1); + nlog($message); + } + + $response->assertStatus(200); + + } + + + public function testClientNullValidation() + { + + $data = [ + 'name' => $this->faker->firstName, + 'number' => null, + ]; + + $response = false; + + try{ + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/clients/', $data); + } catch (ValidationException $e) { + $message = json_decode($e->validator->getMessageBag(), 1); + nlog($message); + } + + $response->assertStatus(200); + + } + + public function testClientCountryCodeValidationTrueIso3() { diff --git a/tests/Feature/InvoiceAmountPaymentTest.php b/tests/Feature/InvoiceAmountPaymentTest.php index 0d1e429f4ed7..675c6615082b 100644 --- a/tests/Feature/InvoiceAmountPaymentTest.php +++ b/tests/Feature/InvoiceAmountPaymentTest.php @@ -111,4 +111,90 @@ class InvoiceAmountPaymentTest extends TestCase $this->assertEquals(10, $payment->amount); } + + public function testMarkPaidRemovesUnpaidGatewayFees() + { + + $data = [ + 'name' => 'A Nice Client', + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/clients', $data); + + $response->assertStatus(200); + + $arr = $response->json(); + + $client_hash_id = $arr['data']['id']; + $client = Client::find($this->decodePrimaryKey($client_hash_id)); + + $this->assertEquals($client->balance, 0); + $this->assertEquals($client->paid_to_date, 0); + //create new invoice. + + $line_items = []; + + $item = InvoiceItemFactory::create(); + $item->quantity = 1; + $item->cost = 10; + + $line_items[] = (array)$item; + + $item = InvoiceItemFactory::create(); + $item->quantity = 1; + $item->cost = 10; + + $line_items[] = (array)$item; + + $item = InvoiceItemFactory::create(); + $item->quantity = 1; + $item->cost = 5; + $item->type_id = "3"; + + $line_items[] = (array)$item; + + $invoice = [ + 'status_id' => 1, + 'number' => '', + 'discount' => 0, + 'is_amount_discount' => 1, + 'po_number' => '3434343', + 'public_notes' => 'notes', + 'is_deleted' => 0, + 'custom_value1' => 0, + 'custom_value2' => 0, + 'custom_value3' => 0, + 'custom_value4' => 0, + 'client_id' => $client_hash_id, + 'line_items' => (array)$line_items, + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/invoices?mark_sent=true', $invoice) + ->assertStatus(200); + + $arr = $response->json(); + + $invoice_one_hashed_id = $arr['data']['id']; + + $invoice = Invoice::find($this->decodePrimaryKey($invoice_one_hashed_id)); + + $this->assertEquals(25, $invoice->balance); + $this->assertEquals(25, $invoice->amount); + + $invoice->service()->markPaid()->save(); + + $invoice->fresh(); + + $this->assertEquals(20, $invoice->amount); + $this->assertEquals(0, $invoice->balance); + + } + + } \ No newline at end of file diff --git a/tests/Integration/PostmarkWebhookTest.php b/tests/Integration/PostmarkWebhookTest.php new file mode 100644 index 000000000000..0dbd89d6513c --- /dev/null +++ b/tests/Integration/PostmarkWebhookTest.php @@ -0,0 +1,141 @@ +markTestSkipped('Postmark Secret Set'); + + $this->makeTestData(); + } + + public function testDeliveryReport() + { + + $invitation = $this->invoice->invitations->first(); + $invitation->message_id = '00000000-0000-0000-0000-000000000000'; + $invitation->save(); + + $data = [ + 'RecordType' => "Delivery", + 'ServerID' => "23", + 'MessageStream' => "outbound", + 'MessageID' => "00000000-0000-0000-0000-000000000000", + 'Recipient' => "john@example.com", + 'Tag' => $this->company->company_key, + 'DeliveredAt' => "2021-02-21T16:34:52Z", + 'Details' => "Test delivery webhook details", + ]; + + $response = $this->post('/api/v1/postmark_webhook', $data); + + $response->assertStatus(403); + + $response = $this->withHeaders([ + 'X-API-SECURITY' => config('postmark.secret') + ])->post('/api/v1/postmark_webhook', $data); + + $response->assertStatus(200); + + } + + public function testDeliveryJob() + { + + $invitation = $this->invoice->invitations->first(); + $invitation->message_id = '00000000-0000-0000-0000-000000000000'; + $invitation->save(); + + ProcessPostmarkWebhook::dispatchNow([ + 'RecordType' => "Delivery", + 'ServerID' => "23", + 'MessageStream' => "outbound", + 'MessageID' => "00000000-0000-0000-0000-000000000000", + 'Recipient' => "john@example.com", + 'Tag' => $this->company->company_key, + 'DeliveredAt' => "2021-02-21T16:34:52Z", + 'Details' => "Test delivery webhook details", + ]); + + $this->assertEquals('delivered', $invitation->fresh()->email_status); + + } + + public function testSpamReport() + { + + $invitation = $this->invoice->invitations->first(); + $invitation->message_id = '00000000-0000-0000-0000-000000000001'; + $invitation->save(); + + $data = [ + 'RecordType' => "SpamComplaint", + 'ServerID' => "23", + 'MessageStream' => "outbound", + 'MessageID' => "00000000-0000-0000-0000-000000000001", + 'Recipient' => "john@example.com", + 'Tag' => $this->company->company_key, + 'DeliveredAt' => "2021-02-21T16:34:52Z", + 'Details' => "Test delivery webhook details", + ]; + + $response = $this->post('/api/v1/postmark_webhook', $data); + + $response->assertStatus(403); + + $response = $this->withHeaders([ + 'X-API-SECURITY' => config('postmark.secret') + ])->post('/api/v1/postmark_webhook', $data); + + $response->assertStatus(200); + + } + + public function testSpamJob() + { + + $invitation = $this->invoice->invitations->first(); + $invitation->message_id = '00000000-0000-0000-0000-000000000001'; + $invitation->save(); + + ProcessPostmarkWebhook::dispatchNow([ + 'RecordType' => "SpamComplaint", + 'ServerID' => "23", + 'MessageStream' => "outbound", + 'MessageID' => "00000000-0000-0000-0000-000000000001", + 'From' => "john@example.com", + 'Tag' => $this->company->company_key, + 'DeliveredAt' => "2021-02-21T16:34:52Z", + 'Details' => "Test delivery webhook details", + ]); + + $this->assertEquals('spam', $invitation->fresh()->email_status); + + } + +}