diff --git a/CHANGELOG.md b/CHANGELOG.md index 91283f1560..c15329c62e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ ## [Unreleased (daily channel)](https://github.com/invoiceninja/invoiceninja/tree/v5-develop) ## Fixed: +- Fix User created/updated/deleted Actvity display format +- Fix for Stripe autobill / token regression + +## Added: +- Invoice / Quote / Credit created notifications +- Logout route - deletes all auth tokens + +## [v5.1.54-release](https://github.com/invoiceninja/invoiceninja/releases/tag/v5.1.50-release) +## Fixed: - Fixes for e-mails, encoding & parsing invalid HTML ## [v5.1.50-release](https://github.com/invoiceninja/invoiceninja/releases/tag/v5.1.50-release) diff --git a/VERSION.txt b/VERSION.txt index ba7bd4ce92..fe757942e6 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -5.1.54 \ No newline at end of file +5.1.55 \ No newline at end of file diff --git a/app/Helpers/Invoice/InvoiceSum.php b/app/Helpers/Invoice/InvoiceSum.php index c6e3fcfd36..3252f7cfe0 100644 --- a/app/Helpers/Invoice/InvoiceSum.php +++ b/app/Helpers/Invoice/InvoiceSum.php @@ -183,6 +183,16 @@ class InvoiceSum return $this; } + /** + * Allow us to get the entity without persisting it + * @return Invoice the temp + */ + public function getTempEntity() + { + $this->setCalculatedAttributes(); + return $this->invoice; + } + public function getInvoice() { //Build invoice values here and return Invoice diff --git a/app/Helpers/Invoice/InvoiceSumInclusive.php b/app/Helpers/Invoice/InvoiceSumInclusive.php index c0505387b7..80e95153cd 100644 --- a/app/Helpers/Invoice/InvoiceSumInclusive.php +++ b/app/Helpers/Invoice/InvoiceSumInclusive.php @@ -196,6 +196,12 @@ class InvoiceSumInclusive return $this->invoice; } + public function getTempEntity() + { + $this->setCalculatedAttributes(); + return $this->invoice; + } + public function getInvoice() { //Build invoice values here and return Invoice diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php index 12cdb5ef62..81dd5fffad 100644 --- a/app/Http/Controllers/Auth/LoginController.php +++ b/app/Http/Controllers/Auth/LoginController.php @@ -16,6 +16,7 @@ use App\DataMapper\Analytics\LoginSuccess; use App\Http\Controllers\BaseController; use App\Http\Controllers\Controller; use App\Jobs\Account\CreateAccount; +use App\Jobs\Company\CreateCompanyToken; use App\Libraries\MultiDB; use App\Libraries\OAuth\OAuth; use App\Libraries\OAuth\Providers\Google; @@ -199,6 +200,15 @@ class LoginController extends BaseController $cu = CompanyUser::query() ->where('user_id', auth()->user()->id); + $cu->first()->account->companies->each(function ($company) use($cu, $request){ + + if($company->tokens()->where('is_system', true)->count() == 0) + { + CreateCompanyToken::dispatchNow($company, $cu->first()->user, $request->server('HTTP_USER_AGENT')); + } + + }); + return $this->listResponse($cu); } else { @@ -262,9 +272,16 @@ class LoginController extends BaseController $cu = CompanyUser::query() ->where('user_id', $company_token->user_id); - //->where('company_id', $company_token->company_id); - //$ct = CompanyUser::whereUserId(auth()->user()->id); + + $cu->first()->account->companies->each(function ($company) use($cu, $request){ + + if($company->tokens()->where('is_system', true)->count() == 0) + { + CreateCompanyToken::dispatchNow($company, $cu->first()->user, $request->server('HTTP_USER_AGENT')); + } + }); + return $this->refreshResponse($cu); } @@ -317,6 +334,14 @@ class LoginController extends BaseController $cu = CompanyUser::query() ->where('user_id', auth()->user()->id); + $cu->first()->account->companies->each(function ($company) use($cu){ + + if($company->tokens()->where('is_system', true)->count() == 0) + { + CreateCompanyToken::dispatchNow($company, $cu->first()->user, request()->server('HTTP_USER_AGENT')); + } + }); + return $this->listResponse($cu); } @@ -348,9 +373,17 @@ class LoginController extends BaseController $timeout = auth()->user()->company()->default_password_timeout / 60000; Cache::put(auth()->user()->hashed_id.'_logged_in', Str::random(64), $timeout); - $ct = CompanyUser::whereUserId(auth()->user()->id); + $cu = CompanyUser::whereUserId(auth()->user()->id); - return $this->listResponse($ct); + $cu->first()->account->companies->each(function ($company) use($cu){ + + if($company->tokens()->where('is_system', true)->count() == 0) + { + CreateCompanyToken::dispatchNow($company, $cu->first()->user, request()->server('HTTP_USER_AGENT')); + } + }); + + return $this->listResponse($cu); } return response() diff --git a/app/Http/Requests/Invoice/UpdateInvoiceRequest.php b/app/Http/Requests/Invoice/UpdateInvoiceRequest.php index ec08c200bb..663fc0e5fa 100644 --- a/app/Http/Requests/Invoice/UpdateInvoiceRequest.php +++ b/app/Http/Requests/Invoice/UpdateInvoiceRequest.php @@ -12,7 +12,9 @@ namespace App\Http\Requests\Invoice; use App\Http\Requests\Request; +use App\Http\ValidationRules\Invoice\InvoiceBalanceSanity; use App\Http\ValidationRules\Invoice\LockedInvoiceRule; +use App\Models\Invoice; use App\Utils\Traits\ChecksEntityStatus; use App\Utils\Traits\CleanLineItems; use App\Utils\Traits\MakesHash; @@ -55,6 +57,9 @@ class UpdateInvoiceRequest extends Request $rules['line_items'] = 'array'; + if($this->input('status_id') != Invoice::STATUS_DRAFT) + $rules['balance'] = new InvoiceBalanceSanity($this->invoice, $this->all()); + return $rules; } diff --git a/app/Http/ValidationRules/Invoice/InvoiceBalanceSanity.php b/app/Http/ValidationRules/Invoice/InvoiceBalanceSanity.php new file mode 100644 index 0000000000..e171667b44 --- /dev/null +++ b/app/Http/ValidationRules/Invoice/InvoiceBalanceSanity.php @@ -0,0 +1,77 @@ +invoice = $invoice; + $this->input = $input; + } + + /** + * @param string $attribute + * @param mixed $value + * @return bool + */ + public function passes($attribute, $value) + { + return $this->checkIfInvoiceBalanceIsSane(); //if it exists, return false! + } + + /** + * @return string + */ + public function message() + { + return $this->message; + } + + /** + * @return bool + */ + private function checkIfInvoiceBalanceIsSane() : bool + { + + $this->invoice->line_items = $this->input['line_items']; + + DB::beginTransaction(); + + $temp_invoice = $this->invoice->calc()->getTempEntity(); + + DB::rollBack(); + + if($temp_invoice->balance < 0){ + $this->message = 'Invoice balance cannot go negative'; + return false; + } + + + return true; + + } +} diff --git a/app/Listeners/Credit/CreditCreatedNotification.php b/app/Listeners/Credit/CreditCreatedNotification.php new file mode 100644 index 0000000000..123b325507 --- /dev/null +++ b/app/Listeners/Credit/CreditCreatedNotification.php @@ -0,0 +1,82 @@ +company->db); + + $first_notification_sent = true; + + $credit = $event->credit; + + $nmo = new NinjaMailerObject; + $nmo->mailable = new NinjaMailer( (new EntityCreatedObject($credit, 'credit'))->build() ); + $nmo->company = $credit->company; + $nmo->settings = $credit->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; + + /* This is only here to handle the alternate message channels - ie Slack */ + // $notification = new EntitySentNotification($event->invitation, 'credit'); + + /* Returns an array of notification methods */ + $methods = $this->findUserNotificationTypes($credit->invitations()->first(), $company_user, 'credit', ['all_notifications', 'credit_created', 'credit_created_all']); + + /* If one of the methods is email then we fire the EntitySentMailer */ + if (($key = array_search('mail', $methods)) !== false && $first_notification_sent === true) { + 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/Listeners/Invoice/InvoiceCreatedNotification.php b/app/Listeners/Invoice/InvoiceCreatedNotification.php new file mode 100644 index 0000000000..7008f83c91 --- /dev/null +++ b/app/Listeners/Invoice/InvoiceCreatedNotification.php @@ -0,0 +1,82 @@ +company->db); + + $first_notification_sent = true; + + $invoice = $event->invoice; + + $nmo = new NinjaMailerObject; + $nmo->mailable = new NinjaMailer( (new EntityCreatedObject($invoice, 'invoice'))->build() ); + $nmo->company = $invoice->company; + $nmo->settings = $invoice->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; + + /* This is only here to handle the alternate message channels - ie Slack */ + // $notification = new EntitySentNotification($event->invitation, 'invoice'); + + /* Returns an array of notification methods */ + $methods = $this->findUserNotificationTypes($invoice->invitations()->first(), $company_user, 'invoice', ['all_notifications', 'invoice_created', 'invoice_created_all']); + + /* If one of the methods is email then we fire the EntitySentMailer */ + if (($key = array_search('mail', $methods)) !== false && $first_notification_sent === true) { + 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/Listeners/Quote/QuoteCreatedNotification.php b/app/Listeners/Quote/QuoteCreatedNotification.php new file mode 100644 index 0000000000..867a1daa9c --- /dev/null +++ b/app/Listeners/Quote/QuoteCreatedNotification.php @@ -0,0 +1,82 @@ +company->db); + + $first_notification_sent = true; + + $quote = $event->quote; + + $nmo = new NinjaMailerObject; + $nmo->mailable = new NinjaMailer( (new EntityCreatedObject($quote, 'quote'))->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; + + /* 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_created', 'quote_created_all']); + + /* If one of the methods is email then we fire the EntitySentMailer */ + if (($key = array_search('mail', $methods)) !== false && $first_notification_sent === true) { + 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/Listeners/User/ArchivedUserActivity.php b/app/Listeners/User/ArchivedUserActivity.php index baad1fcf54..8df5f98420 100644 --- a/app/Listeners/User/ArchivedUserActivity.php +++ b/app/Listeners/User/ArchivedUserActivity.php @@ -43,8 +43,8 @@ class ArchivedUserActivity implements ShouldQueue $fields = new stdClass; - $fields->user_id = $event->user->id; - $fields->notes = $event->creating_user->present()->name . " Archived User"; + $fields->user_id = $event->creating_user->id; + $fields->notes = $event->creating_user->present()->name . " Archived User " . $event->user->present()->name(); $fields->company_id = $event->company->id; $fields->activity_type_id = Activity::ARCHIVE_USER; diff --git a/app/Listeners/User/CreatedUserActivity.php b/app/Listeners/User/CreatedUserActivity.php index 757253f0e7..73266845a5 100644 --- a/app/Listeners/User/CreatedUserActivity.php +++ b/app/Listeners/User/CreatedUserActivity.php @@ -43,8 +43,8 @@ class CreatedUserActivity implements ShouldQueue $fields = new stdClass; - $fields->user_id = $event->user->id; - $fields->notes = $event->creating_user->present()->name() . " Created the user"; + $fields->user_id = $event->creating_user->id; + $fields->notes = $event->creating_user->present()->name() . " Created the user " . $event->user->present()->name(); $fields->company_id = $event->company->id; $fields->activity_type_id = Activity::CREATE_USER; diff --git a/app/Listeners/User/DeletedUserActivity.php b/app/Listeners/User/DeletedUserActivity.php index c530fa2c20..9e961d4dac 100644 --- a/app/Listeners/User/DeletedUserActivity.php +++ b/app/Listeners/User/DeletedUserActivity.php @@ -48,8 +48,8 @@ class DeletedUserActivity implements ShouldQueue $fields = new stdClass; - $fields->user_id = $event->user->id; - $fields->notes = $event->creating_user->present()->name . " Deleted User"; + $fields->user_id = $event->creating_user->id; + $fields->notes = $event->creating_user->present()->name() . " Deleted the user " . $event->user->present()->name(); $fields->company_id = $event->company->id; $fields->activity_type_id = Activity::DELETE_USER; diff --git a/app/Listeners/User/RestoredUserActivity.php b/app/Listeners/User/RestoredUserActivity.php index 77fbc99870..963ed7ef58 100644 --- a/app/Listeners/User/RestoredUserActivity.php +++ b/app/Listeners/User/RestoredUserActivity.php @@ -43,8 +43,8 @@ class RestoredUserActivity implements ShouldQueue $fields = new stdClass; - $fields->user_id = $event->user->id; - $fields->notes = $event->creating_user->present()->name() . " Restored user"; + $fields->user_id = $creating_user->user->id; + $fields->notes = $event->creating_user->present()->name() . " Restored user " . $event->user->present()->name(); $fields->company_id = $event->company->id; $fields->activity_type_id = Activity::RESTORE_USER; diff --git a/app/Listeners/User/UpdatedUserActivity.php b/app/Listeners/User/UpdatedUserActivity.php index a94121dcd6..a04ab4f6e5 100644 --- a/app/Listeners/User/UpdatedUserActivity.php +++ b/app/Listeners/User/UpdatedUserActivity.php @@ -42,8 +42,9 @@ class UpdatedUserActivity implements ShouldQueue MultiDB::setDb($event->company->db); $fields = new stdClass; - $fields->user_id = $event->user->id; - $fields->notes = $event->creating_user->present()->name . " Updated user"; + $fields->user_id = $event->creating_user->id; + $fields->notes = $event->creating_user->present()->name() . " Updated user " . $event->user->present()->name(); + $fields->company_id = $event->company->id; $fields->activity_type_id = Activity::UPDATE_USER; diff --git a/app/Mail/Admin/EntityCreatedObject.php b/app/Mail/Admin/EntityCreatedObject.php new file mode 100644 index 0000000000..f33c7cd540 --- /dev/null +++ b/app/Mail/Admin/EntityCreatedObject.php @@ -0,0 +1,127 @@ +entity_type = $entity_type; + $this->entity = $entity; + } + + public function build() + { + + $this->contact = $this->entity->invitations()->first()->contact; + $this->company = $this->entity->company; + + $this->setTemplate(); + + $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 setTemplate() + { + // nlog($this->template); + + switch ($this->entity_type) { + case 'invoice': + $this->template_subject = "texts.notification_invoice_created_subject"; + $this->template_body = "texts.notification_invoice_sent"; + break; + case 'quote': + $this->template_subject = "texts.notification_quote_created_subject"; + $this->template_body = "texts.notification_quote_sent"; + break; + case 'credit': + $this->template_subject = "texts.notification_credit_created_subject"; + $this->template_body = "texts.notification_credit_sent"; + break; + + default: + $this->template_subject = "texts.notification_invoice_created_subject"; + $this->template_body = "texts.notification_invoice_sent"; + break; + } + } + + private function getAmount() + { + return Number::formatMoney($this->entity->amount, $this->entity->client); + } + + private function getSubject() + { + return + ctrans( + $this->template_subject, + [ + 'client' => $this->contact->present()->name(), + 'invoice' => $this->entity->number, + ] + ); + } + + private function getMessage() + { + return ctrans( + $this->template_body, + [ + 'amount' => $this->getAmount(), + 'client' => $this->contact->present()->name(), + 'invoice' => $this->entity->number, + ] + ); + } + + private function getData() + { + $settings = $this->entity->client->getMergedSettings(); + + return [ + 'title' => $this->getSubject(), + 'message' => $this->getMessage(), + 'url' => $this->entity->invitations()->first()->getAdminLink(), + 'button' => ctrans("texts.view_{$this->entity_type}"), + 'signature' => $settings->email_signature, + 'logo' => $this->company->present()->logo(), + 'settings' => $settings, + 'whitelabel' => $this->company->account->isPaid() ? true : false, + ]; + } +} diff --git a/app/PaymentDrivers/Stripe/Charge.php b/app/PaymentDrivers/Stripe/Charge.php index 917aeeaab2..cdf9857fc5 100644 --- a/app/PaymentDrivers/Stripe/Charge.php +++ b/app/PaymentDrivers/Stripe/Charge.php @@ -69,14 +69,18 @@ class Charge $response = null; try { - $response = $local_stripe->paymentIntents->create([ + + $data = [ 'amount' => $this->stripe->convertToStripeAmount($amount, $this->stripe->client->currency()->precision), 'currency' => $this->stripe->client->getCurrencyCode(), 'payment_method' => $cgt->token, 'customer' => $cgt->gateway_customer_reference, 'confirm' => true, 'description' => $description, - ]); + ]; + + $response = $this->stripe->createPaymentIntent($data); + // $response = $local_stripe->paymentIntents->create($data); SystemLogger::dispatch($response, SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_SUCCESS, SystemLog::TYPE_STRIPE, $this->stripe->client); } catch (\Exception $e) { diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index ca4706fc59..a5f0e5c40d 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -127,6 +127,7 @@ use App\Listeners\Activity\VendorDeletedActivity; use App\Listeners\Activity\VendorRestoredActivity; use App\Listeners\Activity\VendorUpdatedActivity; use App\Listeners\Contact\UpdateContactLastLogin; +use App\Listeners\Credit\CreditCreatedNotification; use App\Listeners\Credit\CreditEmailedNotification; use App\Listeners\Credit\CreditRestoredActivity; use App\Listeners\Credit\CreditViewedActivity; @@ -136,6 +137,7 @@ use App\Listeners\Invoice\CreateInvoiceHtmlBackup; use App\Listeners\Invoice\CreateInvoicePdf; use App\Listeners\Invoice\InvoiceArchivedActivity; use App\Listeners\Invoice\InvoiceCancelledActivity; +use App\Listeners\Invoice\InvoiceCreatedNotification; use App\Listeners\Invoice\InvoiceDeletedActivity; use App\Listeners\Invoice\InvoiceEmailActivity; use App\Listeners\Invoice\InvoiceEmailFailedActivity; @@ -155,6 +157,7 @@ use App\Listeners\Payment\PaymentNotification; use App\Listeners\Payment\PaymentRestoredActivity; use App\Listeners\Quote\QuoteApprovedActivity; use App\Listeners\Quote\QuoteArchivedActivity; +use App\Listeners\Quote\QuoteCreatedNotification; use App\Listeners\Quote\QuoteDeletedActivity; use App\Listeners\Quote\QuoteEmailActivity; use App\Listeners\Quote\QuoteEmailedNotification; @@ -260,6 +263,7 @@ class EventServiceProvider extends ServiceProvider ], CreditWasCreated::class => [ CreatedCreditActivity::class, + CreditCreatedNotification::class, ], CreditWasDeleted::class => [ DeleteCreditActivity::class, @@ -318,6 +322,7 @@ class EventServiceProvider extends ServiceProvider ], InvoiceWasCreated::class => [ CreateInvoiceActivity::class, + InvoiceCreatedNotification::class, // CreateInvoicePdf::class, ], InvoiceWasPaid::class => [ @@ -373,6 +378,7 @@ class EventServiceProvider extends ServiceProvider ], QuoteWasCreated::class => [ CreatedQuoteActivity::class, + QuoteCreatedNotification::class, ], QuoteWasUpdated::class => [ QuoteUpdatedActivity::class, diff --git a/app/Repositories/BaseRepository.php b/app/Repositories/BaseRepository.php index 9942698ef7..a1084a2d43 100644 --- a/app/Repositories/BaseRepository.php +++ b/app/Repositories/BaseRepository.php @@ -302,8 +302,14 @@ class BaseRepository /* Perform model specific tasks */ if ($model instanceof Invoice) { +nlog("in base"); +nlog($state['finished_amount']); +nlog($state['starting_amount']); +nlog($model->status_id); + if (($state['finished_amount'] != $state['starting_amount']) && ($model->status_id != Invoice::STATUS_DRAFT)) { + $model->service()->updateStatus()->save(); $model->ledger()->updateInvoiceBalance(($state['finished_amount'] - $state['starting_amount']), "Update adjustment for invoice {$model->number}"); $model->client->service()->updateBalance(($state['finished_amount'] - $state['starting_amount']))->save(); diff --git a/config/ninja.php b/config/ninja.php index 68f0ac4209..222f7f1348 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', ''), - 'app_version' => '5.1.54', - 'app_tag' => '5.1.54-release', + 'app_version' => '5.1.55', + 'app_tag' => '5.1.55-release', 'minimum_client_version' => '5.0.16', 'terms_version' => '1.0.1', 'api_secret' => env('API_SECRET', false), diff --git a/resources/lang/en/texts.php b/resources/lang/en/texts.php index 6260c7b01f..2619e20330 100644 --- a/resources/lang/en/texts.php +++ b/resources/lang/en/texts.php @@ -4204,6 +4204,22 @@ $LANG = array( 'promo_code' => 'Promo code', 'recurring_invoice_issued_to' => 'Recurring invoice issued to', 'subscription' => 'Subscription', + 'new_subscription' => 'New Subscription', + 'deleted_subscription' => 'Successfully deleted subscription', + 'removed_subscription' => 'Successfully removed subscription', + 'restored_subscription' => 'Successfully restored subscription', + 'search_subscription' => 'Search 1 Subscription', + 'search_subscriptions' => 'Search :count Subscriptions', + 'subdomain_is_not_available' => 'Subdomain is not available', + 'connect_gmail' => 'Connect Gmail', + 'disconnect_gmail' => 'Disconnect Gmail', + 'connected_gmail' => 'Successfully connected Gmail', + 'disconnected_gmail' => 'Successfully disconnected Gmail', + 'update_fail_help' => 'Changes to the codebase may be blocking the update, you can run this command to discard the changes:', + 'client_id_number' => 'Client ID Number', + 'count_minutes' => ':count Minutes', + 'password_timeout' => 'Password Timeout', + 'shared_invoice_credit_counter' => 'Shared Invoice/Credit Counter', 'activity_80' => ':user created subscription :subscription', 'activity_81' => ':user updated subscription :subscription', @@ -4212,6 +4228,13 @@ $LANG = array( 'activity_84' => ':user restored subscription :subscription', 'amount_greater_than_balance_v5' => 'The amount is greater than the invoice balance. You cannot overpay an invoice.', 'click_to_continue' => 'Click to continue', + + 'notification_invoice_created_subject' => 'Invoice :invoice was created to :client', + 'notification_invoice_created_subject' => 'Invoice :invoice was created for :client', + 'notification_quote_created_subject' => 'Quote :invoice was created to :client', + 'notification_quote_created_subject' => 'Quote :invoice was created for :client', + 'notification_credit_created_subject' => 'Credit :invoice was created to :client', + 'notification_credit_created_subject' => 'Credit :invoice was created for :client', ); return $LANG;