diff --git a/VERSION.txt b/VERSION.txt index 3155d4f1aafc..f68965058b73 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -5.3.31 \ No newline at end of file +5.3.32 \ No newline at end of file diff --git a/app/Console/Commands/TranslationsExport.php b/app/Console/Commands/TranslationsExport.php new file mode 100644 index 000000000000..3d0e0c73d348 --- /dev/null +++ b/app/Console/Commands/TranslationsExport.php @@ -0,0 +1,106 @@ +langs as $lang) + { + Storage::makeDirectory(storage_path("lang/{$lang}")); + + $translations = Lang::getLoader()->load($lang,'texts'); + + Storage::put(storage_path("lang/{$lang}/{$lang}.json"), json_encode(Arr::dot($translations), JSON_UNESCAPED_UNICODE)); + + } + + } + +} diff --git a/app/Factory/ClientFactory.php b/app/Factory/ClientFactory.php index b2fb06686d18..35b600618c87 100644 --- a/app/Factory/ClientFactory.php +++ b/app/Factory/ClientFactory.php @@ -28,7 +28,7 @@ class ClientFactory $client->public_notes = ''; $client->balance = 0; $client->paid_to_date = 0; - $client->country_id = 4; + $client->country_id = 840; $client->is_deleted = 0; $client->client_hash = Str::random(40); $client->settings = ClientSettings::defaults(); diff --git a/app/Helpers/Mail/GmailTransport.php b/app/Helpers/Mail/GmailTransport.php index 8edfa1aab38a..3c929f00d9ba 100644 --- a/app/Helpers/Mail/GmailTransport.php +++ b/app/Helpers/Mail/GmailTransport.php @@ -70,7 +70,7 @@ class GmailTransport extends Transport if($child->getContentType() != 'text/plain') { - $this->gmail->attach(TempFile::filePath($child->getBody(), $child->getHeaders()->get('Content-Type')->getParameter('name') )); + $this->gmail->attach(TempFile::filePath($child->getBody(), $child->getHeaders()->get('Content-Type')->getParameter('name') )); } diff --git a/app/Http/Controllers/Auth/ContactForgotPasswordController.php b/app/Http/Controllers/Auth/ContactForgotPasswordController.php index b6338633c775..184d09475c2b 100644 --- a/app/Http/Controllers/Auth/ContactForgotPasswordController.php +++ b/app/Http/Controllers/Auth/ContactForgotPasswordController.php @@ -58,7 +58,7 @@ class ContactForgotPasswordController extends Controller */ public function showLinkRequestForm(Request $request) { - $account_id = $request->get('account_id'); + $account_id = $request->has('account_id') ? $request->get('account_id') : 1; $account = Account::find($account_id); $company = $account->companies->first(); diff --git a/app/Http/Controllers/Auth/ContactLoginController.php b/app/Http/Controllers/Auth/ContactLoginController.php index 3f9d0503f0ca..3cba611befb5 100644 --- a/app/Http/Controllers/Auth/ContactLoginController.php +++ b/app/Http/Controllers/Auth/ContactLoginController.php @@ -36,12 +36,9 @@ class ContactLoginController extends Controller public function showLoginForm(Request $request) { - //if we are on the root domain invoicing.co do not show any company logos - // if(Ninja::isHosted() && count(explode('.', request()->getHost())) == 2){ - // $company = null; - // }else $company = false; + $account = false; if($request->has('company_key')){ MultiDB::findAndSetDbByCompanyKey($request->input('company_key')); @@ -65,13 +62,16 @@ class ContactLoginController extends Controller } elseif (Ninja::isSelfHost()) { - $company = Account::first()->default_company; + $account = Account::first(); + $company = $account->default_company; } else { $company = null; } - $account_id = $request->get('account_id'); - $account = Account::find($account_id); + if(!$account){ + $account_id = $request->get('account_id'); + $account = Account::find($account_id); + } return $this->render('auth.login', ['account' => $account, 'company' => $company]); diff --git a/app/Http/Controllers/Auth/ContactResetPasswordController.php b/app/Http/Controllers/Auth/ContactResetPasswordController.php index 7c32d664a1f0..f5a981ae1859 100644 --- a/app/Http/Controllers/Auth/ContactResetPasswordController.php +++ b/app/Http/Controllers/Auth/ContactResetPasswordController.php @@ -68,7 +68,7 @@ class ContactResetPasswordController extends Controller */ public function showResetForm(Request $request, $token = null) { - $account_id = $request->get('account_id'); + $account_id = $request->has('account_id') ? $request->get('account_id') : 1; $account = Account::find($account_id); $db = $account->companies->first()->db; $company = $account->companies->first(); diff --git a/app/Http/Controllers/BaseController.php b/app/Http/Controllers/BaseController.php index 3c17614da0a8..065d6a1aa6f8 100644 --- a/app/Http/Controllers/BaseController.php +++ b/app/Http/Controllers/BaseController.php @@ -334,7 +334,7 @@ class BaseController extends Controller }, 'company.expense_categories'=> function ($query) use ($updated_at, $user) { - $query->where('updated_at', '>=', $updated_at); + $query->whereNotNull('updated_at'); }, 'company.task_statuses'=> function ($query) use ($updated_at, $user) { $query->whereNotNull('updated_at'); @@ -568,7 +568,7 @@ class BaseController extends Controller }, 'company.expense_categories'=> function ($query) use ($created_at, $user) { - $query->where('created_at', '>=', $created_at); + $query->whereNotNull('created_at'); }, 'company.task_statuses'=> function ($query) use ($created_at, $user) { diff --git a/app/Http/Controllers/ClientPortal/SubscriptionController.php b/app/Http/Controllers/ClientPortal/SubscriptionController.php index 6e637d8223eb..e668299ab74a 100644 --- a/app/Http/Controllers/ClientPortal/SubscriptionController.php +++ b/app/Http/Controllers/ClientPortal/SubscriptionController.php @@ -29,7 +29,9 @@ class SubscriptionController extends Controller ->where('client_id', auth('contact')->user()->client->id) ->where('company_id', auth('contact')->user()->client->company_id) ->where('status_id', RecurringInvoice::STATUS_ACTIVE) + ->where('is_deleted', 0) ->whereNotNull('subscription_id') + ->withTrashed() ->count(); if($count == 0) diff --git a/app/Http/Controllers/CreditController.php b/app/Http/Controllers/CreditController.php index 871034337f9f..88a48009878a 100644 --- a/app/Http/Controllers/CreditController.php +++ b/app/Http/Controllers/CreditController.php @@ -200,6 +200,7 @@ class CreditController extends BaseController $credit = $credit->service() ->fillDefaults() + ->triggeredActions($request) ->save(); event(new CreditWasCreated($credit, $credit->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null))); @@ -377,7 +378,9 @@ class CreditController extends BaseController $credit = $this->credit_repository->save($request->all(), $credit); - $credit->service()->deletePdf(); + $credit->service() + ->triggeredActions($request) + ->deletePdf(); event(new CreditWasUpdated($credit, $credit->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null))); diff --git a/app/Http/Controllers/PreviewController.php b/app/Http/Controllers/PreviewController.php index 153f76a656ae..052a1a3c9dcb 100644 --- a/app/Http/Controllers/PreviewController.php +++ b/app/Http/Controllers/PreviewController.php @@ -174,12 +174,7 @@ class PreviewController extends BaseController MultiDB::setDb($company->db); - if($request->input('entity') == 'invoice'){ - $repo = new InvoiceRepository(); - $entity_obj = InvoiceFactory::create($company->id, auth()->user()->id); - $class = Invoice::class; - } - elseif($request->input('entity') == 'quote'){ + if($request->input('entity') == 'quote'){ $repo = new QuoteRepository(); $entity_obj = QuoteFactory::create($company->id, auth()->user()->id); $class = Quote::class; @@ -195,7 +190,11 @@ class PreviewController extends BaseController $entity_obj = RecurringInvoiceFactory::create($company->id, auth()->user()->id); $class = RecurringInvoice::class; } - + else { //assume it is either an invoice or a null object + $repo = new InvoiceRepository(); + $entity_obj = InvoiceFactory::create($company->id, auth()->user()->id); + $class = Invoice::class; + } try { diff --git a/app/Http/Livewire/BillingPortalPurchase.php b/app/Http/Livewire/BillingPortalPurchase.php index 2ec8e48e70c1..3aa58383834c 100644 --- a/app/Http/Livewire/BillingPortalPurchase.php +++ b/app/Http/Livewire/BillingPortalPurchase.php @@ -189,6 +189,9 @@ class BillingPortalPurchase extends Component $this->coupon = request()->query('coupon'); $this->handleCoupon(); } + elseif(strlen($this->subscription->promo_code) == 0 && $this->subscription->promo_discount > 0){ + $this->price = $this->subscription->promo_price; + } } /** diff --git a/app/Http/Livewire/RequiredClientInfo.php b/app/Http/Livewire/RequiredClientInfo.php index 44f9eb84d4eb..35372358efc1 100644 --- a/app/Http/Livewire/RequiredClientInfo.php +++ b/app/Http/Livewire/RequiredClientInfo.php @@ -141,7 +141,7 @@ class RequiredClientInfo extends Component $_field = $this->mappings[$field['name']]; if (Str::startsWith($field['name'], 'client_')) { - if (empty($this->contact->client->{$_field}) || is_null($this->contact->client->{$_field})) { + if (empty($this->contact->client->{$_field}) || is_null($this->contact->client->{$_field}) || $this->contact->client->{$_field} = 840) { $this->show_form = true; } else { $this->fields[$index]['filled'] = true; @@ -149,7 +149,7 @@ class RequiredClientInfo extends Component } if (Str::startsWith($field['name'], 'contact_')) { - if ((empty($this->contact->{$_field}) || is_null($this->contact->{$_field}))) { + if ((empty($this->contact->{$_field}) || is_null($this->contact->{$_field})) || $this->contact->client->{$_field} = 840) { $this->show_form = true; } else { $this->fields[$index]['filled'] = true; diff --git a/app/Http/Livewire/TasksTable.php b/app/Http/Livewire/TasksTable.php index be084fecffbb..1e36c2eed654 100644 --- a/app/Http/Livewire/TasksTable.php +++ b/app/Http/Livewire/TasksTable.php @@ -36,6 +36,7 @@ class TasksTable extends Component { $query = Task::query() ->where('company_id', $this->company->id) + ->where('is_deleted', false) ->where('client_id', auth('contact')->user()->client->id); if ($this->company->getSetting('show_all_tasks_client_portal') === 'invoiced') { diff --git a/app/Http/Livewire/WepaySignup.php b/app/Http/Livewire/WepaySignup.php index 028de10cd853..0891230d7931 100644 --- a/app/Http/Livewire/WepaySignup.php +++ b/app/Http/Livewire/WepaySignup.php @@ -84,6 +84,8 @@ class WepaySignup extends Component public function submit() { + MultiDB::setDb($this->company->db); + $data = $this->validate($this->rules); //need to create or get a new WePay CompanyGateway diff --git a/app/Http/Requests/Account/CreateAccountRequest.php b/app/Http/Requests/Account/CreateAccountRequest.php index eb8f8724a2d0..75d3d05a026c 100644 --- a/app/Http/Requests/Account/CreateAccountRequest.php +++ b/app/Http/Requests/Account/CreateAccountRequest.php @@ -34,12 +34,12 @@ class CreateAccountRequest extends Request public function rules() { return [ - //'email' => 'required|string|email|max:100', 'first_name' => 'string|max:100', 'last_name' => 'string:max:100', 'password' => 'required|string|min:6', - 'email' => 'bail|required|email:rfc,dns', - 'email' => new NewUniqueUserRule(), + // 'email' => 'bail|required|email:rfc,dns', + // 'email' => new NewUniqueUserRule(), + 'email' => ['required', 'email:rfc,dns', new NewUniqueUserRule], 'privacy_policy' => 'required|boolean', 'terms_of_service' => 'required|boolean', ]; diff --git a/app/Http/Requests/ClientPortal/Documents/ShowDocumentRequest.php b/app/Http/Requests/ClientPortal/Documents/ShowDocumentRequest.php index 3f6b0cf38adf..a11f50fe6068 100644 --- a/app/Http/Requests/ClientPortal/Documents/ShowDocumentRequest.php +++ b/app/Http/Requests/ClientPortal/Documents/ShowDocumentRequest.php @@ -28,7 +28,8 @@ class ShowDocumentRequest extends FormRequest public function authorize() { return auth()->user('contact')->client->id == $this->document->documentable_id - || $this->document->documentable->client_id == auth()->user('contact')->client->id; + || $this->document->documentable->client_id == auth()->user('contact')->client->id + || $this->document->company_id == auth()->user('contact')->company->id; } /** diff --git a/app/Jobs/Credit/ApplyCreditPayment.php b/app/Jobs/Credit/ApplyCreditPayment.php index 2b342f03319e..05b9ca6566b8 100644 --- a/app/Jobs/Credit/ApplyCreditPayment.php +++ b/app/Jobs/Credit/ApplyCreditPayment.php @@ -69,6 +69,7 @@ class ApplyCreditPayment implements ShouldQueue $this->credit ->service() + ->markSent() ->setStatus(Credit::STATUS_APPLIED) ->adjustBalance($this->amount * -1) ->updatePaidToDate($this->amount) @@ -78,6 +79,7 @@ class ApplyCreditPayment implements ShouldQueue $this->credit ->service() + ->markSent() ->setStatus(Credit::STATUS_PARTIAL) ->adjustBalance($this->amount * -1) ->updatePaidToDate($this->amount) diff --git a/app/Listeners/Quote/QuoteApprovedNotification.php b/app/Listeners/Quote/QuoteApprovedNotification.php new file mode 100644 index 000000000000..59199d0c535d --- /dev/null +++ b/app/Listeners/Quote/QuoteApprovedNotification.php @@ -0,0 +1,86 @@ +company->db); + + $first_notification_sent = true; + + $quote = $event->quote; + + $nmo = new NinjaMailerObject; + $nmo->mailable = new NinjaMailer( (new QuoteApprovedObject($quote, $event->company))->build() ); + $nmo->company = $quote->company; + $nmo->settings = $quote->company->settings; + + /* We loop through each user and determine whether they need to be notified */ + foreach ($event->company->company_users as $company_user) { + + /* The User */ + $user = $company_user->user; + + if(!$user) + continue; + + /* This is only here to handle the alternate message channels - ie Slack */ + // $notification = new EntitySentNotification($event->invitation, 'quote'); + + /* Returns an array of notification methods */ + $methods = $this->findUserNotificationTypes($quote->invitations()->first(), $company_user, 'quote', ['all_notifications', 'quote_approved', 'quote_approved_all']); + + /* If one of the methods is email then we fire the EntitySentMailer */ + if (($key = array_search('mail', $methods)) !== false) { + unset($methods[$key]); + + + $nmo->to_user = $user; + + NinjaMailerJob::dispatch($nmo); + + /* This prevents more than one notification being sent */ + $first_notification_sent = false; + } + + /* Override the methods in the Notification Class */ + // $notification->method = $methods; + + // Notify on the alternate channels + // $user->notify($notification); + } + } +} diff --git a/app/Mail/Admin/ClientPaymentFailureObject.php b/app/Mail/Admin/ClientPaymentFailureObject.php index f7f3ffbbfea6..ad2629c3f27a 100644 --- a/app/Mail/Admin/ClientPaymentFailureObject.php +++ b/app/Mail/Admin/ClientPaymentFailureObject.php @@ -69,7 +69,7 @@ class ClientPaymentFailureObject /* Set customized translations _NOW_ */ $t->replace(Ninja::transformTranslations($this->company->settings)); - $this->invoices = Invoice::whereIn('id', $this->transformKeys(array_column($this->payment_hash->invoices(), 'invoice_id')))->get(); + $this->invoices = Invoice::withTrashed()->whereIn('id', $this->transformKeys(array_column($this->payment_hash->invoices(), 'invoice_id')))->get(); $mail_obj = new stdClass; $mail_obj->amount = $this->getAmount(); @@ -101,8 +101,13 @@ class ClientPaymentFailureObject private function getData() { + $invitation = $this->invoices->first()->invitations->first(); + + if(!$invitation) + throw new \Exception('Unable to find invitation for reference'); + $signature = $this->client->getSetting('email_signature'); - $html_variables = (new HtmlEngine($this->invoices->first()->invitations->first()))->makeValues(); + $html_variables = (new HtmlEngine($invitation))->makeValues(); $signature = str_replace(array_keys($html_variables), array_values($html_variables), $signature); $data = [ diff --git a/app/Mail/Admin/QuoteApprovedObject.php b/app/Mail/Admin/QuoteApprovedObject.php new file mode 100644 index 000000000000..a5c88e904de4 --- /dev/null +++ b/app/Mail/Admin/QuoteApprovedObject.php @@ -0,0 +1,103 @@ +quote = $quote; + $this->company = $company; + } + + public function build() + { + MultiDB::setDb($this->company->db); + + if(!$this->quote) + return; + + App::forgetInstance('translator'); + /* Init a new copy of the translator*/ + $t = app('translator'); + /* Set the locale*/ + App::setLocale($this->company->getLocale()); + /* Set customized translations _NOW_ */ + $t->replace(Ninja::transformTranslations($this->company->settings)); + + $mail_obj = new stdClass; + $mail_obj->amount = $this->getAmount(); + $mail_obj->subject = $this->getSubject(); + $mail_obj->data = $this->getData(); + $mail_obj->markdown = 'email.admin.generic'; + $mail_obj->tag = $this->company->company_key; + + return $mail_obj; + } + + private function getAmount() + { + return Number::formatMoney($this->quote->amount, $this->quote->client); + } + + private function getSubject() + { + return + ctrans( + "texts.notification_quote_approved_subject", + [ + 'client' => $this->quote->client->present()->name(), + 'invoice' => $this->quote->number, + ] + ); + } + + private function getData() + { + $settings = $this->quote->client->getMergedSettings(); + + $data = [ + 'title' => $this->getSubject(), + 'message' => ctrans( + "texts.notification_quote_approved", + [ + 'amount' => $this->getAmount(), + 'client' => $this->quote->client->present()->name(), + 'invoice' => $this->quote->number, + ] + ), + 'url' => $this->quote->invitations->first()->getAdminLink(), + 'button' => ctrans("texts.view_quote"), + 'signature' => $settings->email_signature, + 'logo' => $this->company->present()->logo(), + 'settings' => $settings, + 'whitelabel' => $this->company->account->isPaid() ? true : false, + ]; + + return $data; + } +} diff --git a/app/Mail/Engine/InvoiceEmailEngine.php b/app/Mail/Engine/InvoiceEmailEngine.php index 9ede43715dc8..afe907d348b6 100644 --- a/app/Mail/Engine/InvoiceEmailEngine.php +++ b/app/Mail/Engine/InvoiceEmailEngine.php @@ -136,4 +136,4 @@ class InvoiceEmailEngine extends BaseEmailEngine return $this; } -} +} \ No newline at end of file diff --git a/app/Mail/TemplateEmail.php b/app/Mail/TemplateEmail.php index 69557674a4f7..4f8e221b0634 100644 --- a/app/Mail/TemplateEmail.php +++ b/app/Mail/TemplateEmail.php @@ -18,6 +18,7 @@ use App\Models\ClientContact; use App\Models\User; use App\Services\PdfMaker\Designs\Utilities\DesignHelpers; use App\Utils\HtmlEngine; +use App\Utils\Ninja; use App\Utils\TemplateEngine; use Illuminate\Bus\Queueable; use Illuminate\Mail\Mailable; @@ -114,6 +115,10 @@ class TemplateEmail extends Mailable $message->invitation = $this->invitation; }); + /*In the hosted platform we need to slow things down a little for Storage to catch up.*/ + if(Ninja::isHosted()) + sleep(1); + foreach ($this->build_email->getAttachments() as $file) { if(is_string($file)) diff --git a/app/Models/BaseModel.php b/app/Models/BaseModel.php index 2cdf4974dec8..bddc4ad0e537 100644 --- a/app/Models/BaseModel.php +++ b/app/Models/BaseModel.php @@ -195,6 +195,8 @@ class BaseModel extends Model // Remove any runs of periods (thanks falstro!) $formatted_number = mb_ereg_replace("([\.]{2,})", '', $formatted_number); + $formatted_number = str_replace(" ", "_", $formatted_number); + return $formatted_number; } diff --git a/app/Models/RecurringInvoice.php b/app/Models/RecurringInvoice.php index 944a1038a197..bbb80d4b9369 100644 --- a/app/Models/RecurringInvoice.php +++ b/app/Models/RecurringInvoice.php @@ -222,6 +222,9 @@ class RecurringInvoice extends BaseModel return null; } + nlog("frequency = $this->frequency_id"); + nlog("frequency = $this->next_send_date"); + $offset = $this->client->timezone_offset(); /* diff --git a/app/PaymentDrivers/BaseDriver.php b/app/PaymentDrivers/BaseDriver.php index 0de59f60f565..55c472337398 100644 --- a/app/PaymentDrivers/BaseDriver.php +++ b/app/PaymentDrivers/BaseDriver.php @@ -404,7 +404,7 @@ class BaseDriver extends AbstractPaymentDriver throw new PaymentFailed($error, $e->getCode()); } - public function sendFailureMail(string $error) + public function sendFailureMail($error = '') { if (!is_null($this->payment_hash)) { diff --git a/app/PaymentDrivers/Mollie/Bancontact.php b/app/PaymentDrivers/Mollie/Bancontact.php index 45df88520e27..0a9e26e6be2e 100644 --- a/app/PaymentDrivers/Mollie/Bancontact.php +++ b/app/PaymentDrivers/Mollie/Bancontact.php @@ -86,6 +86,7 @@ class Bancontact implements MethodInterface 'webhookUrl' => $this->mollie->company_gateway->webhookUrl(), 'metadata' => [ 'client_id' => $this->mollie->client->hashed_id, + 'hash' => $this->mollie->payment_hash->hash ], ]); diff --git a/app/PaymentDrivers/Mollie/BankTransfer.php b/app/PaymentDrivers/Mollie/BankTransfer.php index 04e65996cbc2..41ccb4b5eb58 100644 --- a/app/PaymentDrivers/Mollie/BankTransfer.php +++ b/app/PaymentDrivers/Mollie/BankTransfer.php @@ -89,6 +89,7 @@ class BankTransfer implements MethodInterface 'webhookUrl' => $this->mollie->company_gateway->webhookUrl(), 'metadata' => [ 'client_id' => $this->mollie->client->hashed_id, + 'hash' => $this->mollie->payment_hash->hash ], ]); diff --git a/app/PaymentDrivers/Mollie/IDEAL.php b/app/PaymentDrivers/Mollie/IDEAL.php index 185cfe758e9c..b3a4ccfa8747 100644 --- a/app/PaymentDrivers/Mollie/IDEAL.php +++ b/app/PaymentDrivers/Mollie/IDEAL.php @@ -86,6 +86,7 @@ class IDEAL implements MethodInterface 'webhookUrl' => $this->mollie->company_gateway->webhookUrl(), 'metadata' => [ 'client_id' => $this->mollie->client->hashed_id, + 'hash' => $this->mollie->payment_hash->hash ], ]); diff --git a/app/PaymentDrivers/Mollie/KBC.php b/app/PaymentDrivers/Mollie/KBC.php index 1bb575d1bdd1..3dbc53772b29 100644 --- a/app/PaymentDrivers/Mollie/KBC.php +++ b/app/PaymentDrivers/Mollie/KBC.php @@ -86,6 +86,7 @@ class KBC implements MethodInterface 'webhookUrl' => $this->mollie->company_gateway->webhookUrl(), 'metadata' => [ 'client_id' => $this->mollie->client->hashed_id, + 'hash' => $this->mollie->payment_hash->hash ], ]); diff --git a/app/PaymentDrivers/PayPalExpressPaymentDriver.php b/app/PaymentDrivers/PayPalExpressPaymentDriver.php index 43b2a6a1ad4a..75a569c84c81 100644 --- a/app/PaymentDrivers/PayPalExpressPaymentDriver.php +++ b/app/PaymentDrivers/PayPalExpressPaymentDriver.php @@ -93,7 +93,7 @@ class PayPalExpressPaymentDriver extends BaseDriver return $response->redirect(); } - $this->sendFailureMail($response->getMessage()); + $this->sendFailureMail($response->getMessage() ?: ''); $message = [ 'server_response' => $response->getMessage(), @@ -151,7 +151,7 @@ class PayPalExpressPaymentDriver extends BaseDriver $data = $response->getData(); - $this->sendFailureMail($response->getMessage()); + $this->sendFailureMail($response->getMessage() ?: ''); $message = [ 'server_response' => $data['L_LONGMESSAGE0'], diff --git a/app/PaymentDrivers/StripePaymentDriver.php b/app/PaymentDrivers/StripePaymentDriver.php index aa37045de57f..78fd9019fee2 100644 --- a/app/PaymentDrivers/StripePaymentDriver.php +++ b/app/PaymentDrivers/StripePaymentDriver.php @@ -524,14 +524,25 @@ class StripePaymentDriver extends BaseDriver if ($request->type === 'charge.succeeded' || $request->type === 'payment_intent.succeeded') { foreach ($request->data as $transaction) { - $payment = Payment::query() - ->where('transaction_reference', $transaction['payment_intent']) + + if(array_key_exists('payment_intent', $transaction)) + { + $payment = Payment::query() ->where('company_id', $request->getCompany()->id) ->where(function ($query) use ($transaction) { $query->where('transaction_reference', $transaction['payment_intent']) ->orWhere('transaction_reference', $transaction['id']); }) ->first(); + } + else + { + $payment = Payment::query() + ->where('company_id', $request->getCompany()->id) + ->where('transaction_reference', $transaction['id']) + ->first(); + } + if ($payment) { $payment->status_id = Payment::STATUS_COMPLETED; $payment->save(); diff --git a/app/PaymentDrivers/WePayPaymentDriver.php b/app/PaymentDrivers/WePayPaymentDriver.php index 9624a329cf03..73b8d7573949 100644 --- a/app/PaymentDrivers/WePayPaymentDriver.php +++ b/app/PaymentDrivers/WePayPaymentDriver.php @@ -208,8 +208,8 @@ class WePayPaymentDriver extends BaseDriver return 'Processed successfully'; } elseif ($objectType == 'account') { - if ($accountId != $objectId) { - throw new \Exception('Unknown account'); + if ($accountId !== $objectId) { + throw new \Exception('Unknown account ' . $accountId . ' does not equal '.$objectId); } $wepayAccount = $this->wepay->request('account', array( diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index 52b15e64ba81..5acc2f234388 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -171,6 +171,7 @@ use App\Listeners\Payment\PaymentEmailedActivity; use App\Listeners\Payment\PaymentNotification; use App\Listeners\Payment\PaymentRestoredActivity; use App\Listeners\Quote\QuoteApprovedActivity; +use App\Listeners\Quote\QuoteApprovedNotification; use App\Listeners\Quote\QuoteApprovedWebhook; use App\Listeners\Quote\QuoteArchivedActivity; use App\Listeners\Quote\QuoteCreatedNotification; @@ -437,6 +438,7 @@ class EventServiceProvider extends ServiceProvider ReachWorkflowSettings::class, QuoteApprovedActivity::class, QuoteApprovedWebhook::class, + QuoteApprovedNotification::class, ], QuoteWasCreated::class => [ CreatedQuoteActivity::class, diff --git a/app/Repositories/ClientContactRepository.php b/app/Repositories/ClientContactRepository.php index 2bc644f8ba4d..6434665ff722 100644 --- a/app/Repositories/ClientContactRepository.php +++ b/app/Repositories/ClientContactRepository.php @@ -22,7 +22,9 @@ use Illuminate\Support\Str; */ class ClientContactRepository extends BaseRepository { - public $is_primary; + private bool $is_primary = true; + + private bool $set_send_email_on_contact = false; public function save(array $data, Client $client) : void { @@ -36,13 +38,20 @@ class ClientContactRepository extends BaseRepository ClientContact::destroy($contact); }); - $this->is_primary = true; + /* Ensure send_email always exists in at least one contact */ + if(!$contacts->contains('send_email', true)) + $this->set_send_email_on_contact = true; /* Set first record to primary - always */ $contacts = $contacts->sortByDesc('is_primary')->map(function ($contact) { $contact['is_primary'] = $this->is_primary; $this->is_primary = false; + if($this->set_send_email_on_contact){ + $contact['send_email'] = true; + $this->set_send_email_on_contact = false; + } + return $contact; }); diff --git a/app/Repositories/PaymentRepository.php b/app/Repositories/PaymentRepository.php index 3cfa82879142..c7ae018be8f0 100644 --- a/app/Repositories/PaymentRepository.php +++ b/app/Repositories/PaymentRepository.php @@ -129,7 +129,7 @@ class PaymentRepository extends BaseRepository { //todo optimize this into a single query foreach ($data['invoices'] as $paid_invoice) { - $invoice = Invoice::whereId($paid_invoice['invoice_id'])->first(); + $invoice = Invoice::withTrashed()->whereId($paid_invoice['invoice_id'])->first(); if ($invoice) { $invoice = $invoice->service() diff --git a/app/Services/Credit/CreditService.php b/app/Services/Credit/CreditService.php index acac1ff28ede..c6b41fb3eef3 100644 --- a/app/Services/Credit/CreditService.php +++ b/app/Services/Credit/CreditService.php @@ -14,6 +14,7 @@ namespace App\Services\Credit; use App\Jobs\Util\UnlinkFile; use App\Models\Credit; use App\Services\Credit\CreateInvitations; +use App\Services\Credit\TriggeredActions; use App\Utils\Traits\MakesHash; class CreditService @@ -150,6 +151,13 @@ class CreditService return $this; } + public function triggeredActions($request) + { + $this->invoice = (new TriggeredActions($this->credit, $request))->run(); + + return $this; + } + /** * Saves the credit. * @return Credit object diff --git a/app/Services/Credit/TriggeredActions.php b/app/Services/Credit/TriggeredActions.php new file mode 100644 index 000000000000..bf7ef3c05484 --- /dev/null +++ b/app/Services/Credit/TriggeredActions.php @@ -0,0 +1,76 @@ +request = $request; + + $this->credit = $credit; + } + + public function run() + { + // if ($this->request->has('auto_bill') && $this->request->input('auto_bill') == 'true') { + // $this->credit = $this->credit->service()->autoBill()->save(); + // } + + // if ($this->request->has('paid') && $this->request->input('paid') == 'true') { + // $this->credit = $this->credit->service()->markPaid()->save(); + // } + + // if ($this->request->has('amount_paid') && is_numeric($this->request->input('amount_paid')) ) { + // $this->credit = $this->credit->service()->applyPaymentAmount($this->request->input('amount_paid'))->save(); + // } + + if ($this->request->has('send_email') && $this->request->input('send_email') == 'true') { + $this->sendEmail(); + } + + if ($this->request->has('mark_sent') && $this->request->input('mark_sent') == 'true') { + $this->credit = $this->credit->service()->markSent()->save(); + } + + + return $this->credit; + } + + private function sendEmail() + { + + $reminder_template = $this->credit->calculateTemplate('credit'); + + $this->credit->invitations->load('contact.client.country', 'credit.client.country', 'credit.company')->each(function ($invitation) use ($reminder_template) { + EmailEntity::dispatch($invitation, $this->credit->company, $reminder_template); + }); + + if ($this->credit->invitations->count() > 0) { + event(new CreditWasEmailed($this->credit->invitations->first(), $this->credit->company, Ninja::eventVars(), 'credit')); + } + } +} diff --git a/app/Services/Invoice/ApplyPayment.php b/app/Services/Invoice/ApplyPayment.php index de82966abb09..b23e3389ff08 100644 --- a/app/Services/Invoice/ApplyPayment.php +++ b/app/Services/Invoice/ApplyPayment.php @@ -47,7 +47,7 @@ class ApplyPayment extends AbstractService $amount_paid = $this->payment_amount * -1; - $this->invoice->service()->clearPartial()->setDueDate()->setStatus(Invoice::STATUS_PARTIAL)->updateBalance($amount_paid); + $this->invoice->service()->clearPartial()->setDueDate()->setStatus(Invoice::STATUS_PARTIAL)->updateBalance($amount_paid)->save(); } elseif ($this->invoice->partial > 0 && $this->invoice->partial > $this->payment_amount) @@ -56,7 +56,7 @@ class ApplyPayment extends AbstractService $amount_paid = $this->payment_amount * -1; - $this->invoice->service()->updatePartial($amount_paid)->updateBalance($amount_paid); + $this->invoice->service()->updatePartial($amount_paid)->updateBalance($amount_paid)->save(); } elseif ($this->invoice->partial > 0 && $this->invoice->partial < $this->payment_amount) @@ -65,7 +65,7 @@ class ApplyPayment extends AbstractService $amount_paid = $this->payment_amount * -1; - $this->invoice->service()->clearPartial()->setDueDate()->setStatus(Invoice::STATUS_PARTIAL)->updateBalance($amount_paid); + $this->invoice->service()->clearPartial()->setDueDate()->setStatus(Invoice::STATUS_PARTIAL)->updateBalance($amount_paid)->save(); } @@ -76,7 +76,7 @@ class ApplyPayment extends AbstractService { $amount_paid = $this->payment_amount * -1; - $this->invoice->service()->clearPartial()->setStatus(Invoice::STATUS_PAID)->updateBalance($amount_paid); + $this->invoice->service()->clearPartial()->setStatus(Invoice::STATUS_PAID)->updateBalance($amount_paid)->save(); } elseif ($this->payment_amount < $this->invoice->balance) @@ -85,7 +85,7 @@ class ApplyPayment extends AbstractService $amount_paid = $this->payment_amount * -1; - $this->invoice->service()->clearPartial()->setStatus(Invoice::STATUS_PARTIAL)->updateBalance($amount_paid); + $this->invoice->service()->clearPartial()->setStatus(Invoice::STATUS_PARTIAL)->updateBalance($amount_paid)->save(); } @@ -95,7 +95,7 @@ class ApplyPayment extends AbstractService $amount_paid = $this->invoice->balance * -1; - $this->invoice->service()->clearPartial()->setStatus(Invoice::STATUS_PAID)->updateBalance($amount_paid); + $this->invoice->service()->clearPartial()->setStatus(Invoice::STATUS_PAID)->updateBalance($amount_paid)->save(); } } diff --git a/app/Services/Payment/UpdateInvoicePayment.php b/app/Services/Payment/UpdateInvoicePayment.php index a267d1f41b74..3fb5e91428ed 100644 --- a/app/Services/Payment/UpdateInvoicePayment.php +++ b/app/Services/Payment/UpdateInvoicePayment.php @@ -82,10 +82,15 @@ class UpdateInvoicePayment ->updateBalance($paid_amount * -1) ->updatePaidToDate($paid_amount) ->updateStatus() - ->deletePdf() - ->workFlow() ->save(); + $invoice->refresh(); + + $invoice->service() + ->deletePdf() + ->workFlow() + ->save(); + event(new InvoiceWasUpdated($invoice, $invoice->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null))); }); diff --git a/app/Services/Subscription/SubscriptionService.php b/app/Services/Subscription/SubscriptionService.php index 178bdd25a9b3..a1984fb7a2a0 100644 --- a/app/Services/Subscription/SubscriptionService.php +++ b/app/Services/Subscription/SubscriptionService.php @@ -75,9 +75,9 @@ class SubscriptionService $recurring_invoice = $this->convertInvoiceToRecurring($payment_hash->payment->client_id); $recurring_invoice_repo = new RecurringInvoiceRepository(); - $recurring_invoice->next_send_date = now(); $recurring_invoice = $recurring_invoice_repo->save([], $recurring_invoice); - $recurring_invoice->next_send_date = $recurring_invoice->nextSendDate(); + // $recurring_invoice->next_send_date = now()->format('Y-m-d'); + // $recurring_invoice->next_send_date = $recurring_invoice->nextSendDate(); $recurring_invoice->auto_bill = $this->subscription->auto_bill; /* Start the recurring service */ @@ -161,6 +161,11 @@ class SubscriptionService $recurring_invoice->discount = $this->subscription->promo_discount; $recurring_invoice->is_amount_discount = $this->subscription->is_amount_discount; } + elseif(strlen($this->subscription->promo_code) == 0 && $this->subscription->promo_discount > 0) { + $recurring_invoice->discount = $this->subscription->promo_discount; + $recurring_invoice->is_amount_discount = $this->subscription->is_amount_discount; + } + $recurring_invoice = $recurring_invoice_repo->save($data, $recurring_invoice); @@ -594,7 +599,7 @@ class SubscriptionService $recurring_invoice = $this->convertInvoiceToRecurring($old_recurring_invoice->client_id); $recurring_invoice = $recurring_invoice_repo->save([], $recurring_invoice); - $recurring_invoice->next_send_date = now(); + $recurring_invoice->next_send_date = now()->format('Y-m-d'); $recurring_invoice->next_send_date = $recurring_invoice->nextSendDate(); /* Start the recurring service */ @@ -693,6 +698,11 @@ class SubscriptionService $invoice->discount = $this->subscription->promo_discount; $invoice->is_amount_discount = $this->subscription->is_amount_discount; } + elseif(strlen($this->subscription->promo_code) == 0 && $this->subscription->promo_discount > 0) { + $invoice->discount = $this->subscription->promo_discount; + $invoice->is_amount_discount = $this->subscription->is_amount_discount; + } + return $invoice_repo->save($data, $invoice); @@ -722,7 +732,9 @@ class SubscriptionService $recurring_invoice->auto_bill = $client->getSetting('auto_bill'); $recurring_invoice->auto_bill_enabled = $this->setAutoBillFlag($recurring_invoice->auto_bill); $recurring_invoice->due_date_days = 'terms'; - + $recurring_invoice->next_send_date = now()->format('Y-m-d'); + $recurring_invoice->next_send_date = $recurring_invoice->nextSendDate(); + return $recurring_invoice; } @@ -1027,7 +1039,7 @@ class SubscriptionService 'subscription' => $this->subscription->hashed_id, 'recurring_invoice' => $recurring_invoice_hashed_id, 'client' => $invoice->client->hashed_id, - 'contact' => $invoice->client->primary_contact()->first() ? $invoice->client->primary_contact()->first(): $invoice->client->contacts->first(), + 'contact' => $invoice->client->primary_contact()->first() ? $invoice->client->primary_contact()->first()->hashed_id: $invoice->client->contacts->first()->hashed_id, 'invoice' => $invoice->hashed_id, ]; diff --git a/composer.json b/composer.json index 07c4fc75daa7..b67486d54c99 100644 --- a/composer.json +++ b/composer.json @@ -150,4 +150,4 @@ }, "minimum-stability": "dev", "prefer-stable": true -} +} \ No newline at end of file diff --git a/config/ninja.php b/config/ninja.php index 529cfada0da2..4dbd4f9de9d8 100644 --- a/config/ninja.php +++ b/config/ninja.php @@ -14,8 +14,8 @@ return [ 'require_https' => env('REQUIRE_HTTPS', true), 'app_url' => rtrim(env('APP_URL', ''), '/'), 'app_domain' => env('APP_DOMAIN', 'invoicing.co'), - 'app_version' => '5.3.31', - 'app_tag' => '5.3.31', + 'app_version' => '5.3.32', + 'app_tag' => '5.3.32', 'minimum_client_version' => '5.0.16', 'terms_version' => '1.0.1', 'api_secret' => env('API_SECRET', ''), diff --git a/resources/views/portal/ninja2020/components/general/sidebar/header.blade.php b/resources/views/portal/ninja2020/components/general/sidebar/header.blade.php index 95e5b31a6075..639f95cf4667 100644 --- a/resources/views/portal/ninja2020/components/general/sidebar/header.blade.php +++ b/resources/views/portal/ninja2020/components/general/sidebar/header.blade.php @@ -25,8 +25,8 @@ @foreach($multiple_contacts as $contact) {{ $contact->company->present()->name() }} - - {{ $contact->client->present()->name()}} + class="block px-4 py-2 text-sm leading-5 text-gray-700 hover:bg-gray-100 hover:text-gray-900 focus:outline-none focus:bg-gray-100 focus:text-gray-900">{{ $contact->client->present()->name()}} - {{ $contact->company->present()->name() }} + @endforeach diff --git a/routes/client.php b/routes/client.php index ebc44c1a3f5c..31478e3c6712 100644 --- a/routes/client.php +++ b/routes/client.php @@ -23,7 +23,7 @@ Route::get('tmp_pdf/{hash}', 'ClientPortal\TempRouteController@index')->name('tm Route::get('client/key_login/{contact_key}', 'ClientPortal\ContactHashLoginController@login')->name('client.contact_login')->middleware(['domain_db','contact_key_login']); Route::get('client/magic_link/{magic_link}', 'ClientPortal\ContactHashLoginController@magicLink')->name('client.contact_magic_link')->middleware(['domain_db','contact_key_login']); -Route::get('documents/{document_hash}', 'ClientPortal\DocumentController@publicDownload')->name('documents.public_download')->middleware(['document_db']); +Route::get('documents/{document_hash}', 'ClientPortal\DocumentController@publicDownload')->name('documents.public_download')->middleware(['domain_db']); Route::get('error', 'ClientPortal\ContactHashLoginController@errorPage')->name('client.error'); Route::get('client/payment/{contact_key}/{payment_id}', 'ClientPortal\InvitationController@paymentRouter')->middleware(['domain_db','contact_key_login']); Route::get('client/ninja/{contact_key}/{company_key}', 'ClientPortal\NinjaPlanController@index')->name('client.ninja_contact_login')->middleware(['domain_db']); diff --git a/tests/Unit/CollectionMergingTest.php b/tests/Unit/CollectionMergingTest.php index f223011a12c2..57ab951ad9d0 100644 --- a/tests/Unit/CollectionMergingTest.php +++ b/tests/Unit/CollectionMergingTest.php @@ -10,6 +10,7 @@ */ namespace Tests\Unit; +use App\Factory\ClientContactFactory; use App\Factory\InvoiceItemFactory; use App\Utils\Traits\UserSessionAttributes; use Illuminate\Support\Facades\Session; @@ -77,4 +78,41 @@ class CollectionMergingTest extends TestCase $this->assertTrue(collect($items)->contains('type_id', 3)); } + + public function testClientContactSendEmailExists() + { + $new_collection = collect(); + + $cc = ClientContactFactory::create(1,1); + $cc->send_email = true; + + $new_collection->push($cc); + + $cc_false = ClientContactFactory::create(1,1); + $cc_false->send_email = false; + + $new_collection->push($cc_false); + + $this->assertTrue($new_collection->contains('send_email', true)); + + } + + public function testClientContactSendEmailDoesNotExists() + { + $new_collection = collect(); + + $cc = ClientContactFactory::create(1,1); + $cc->send_email = false; + + $new_collection->push($cc); + + $cc_false = ClientContactFactory::create(1,1); + $cc_false->send_email = false; + + $new_collection->push($cc_false); + + $this->assertFalse($new_collection->contains('send_email', true)); + + } + }