From c503d585050d09c35313777ed669ccb5be66be27 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Thu, 30 Apr 2020 21:45:47 +1000 Subject: [PATCH] Adjust email quotas - Hosted plan. (#3663) * Fixes for invitations not being created in RandomDataSeeder * Resend failed/quota exceeded emails * Queue email tests * Refund a client for a ninja account * Adjust email quotas - hosted plan --- app/Console/Kernel.php | 7 ++ app/DataMapper/EmailSpooledForSend.php | 37 ++++++++ app/Helpers/Email/EmailBuilder.php | 20 ++--- app/Http/Controllers/CompanyController.php | 17 ++-- .../ClientPortal/ShowInvoiceRequest.php | 4 +- app/Jobs/Credit/EmailCredit.php | 2 +- app/Jobs/Invoice/CreateUbl.php | 2 +- app/Jobs/Invoice/EmailInvoice.php | 2 +- app/Jobs/Ninja/AdjustEmailQuota.php | 80 +++++++++++++++++ app/Jobs/Ninja/RefundCancelledAccount.php | 78 +++++++++++++++++ app/Jobs/Util/Import.php | 9 ++ app/Jobs/Util/ProcessBulk.php | 9 ++ app/Jobs/Util/ReminderJob.php | 9 ++ app/Jobs/Util/SendFailedEmails.php | 85 +++++++++++++++++++ app/Jobs/Util/StartMigration.php | 9 ++ app/Jobs/Util/UnlinkFile.php | 9 ++ app/Jobs/Util/VersionCheck.php | 9 ++ app/Models/SystemLog.php | 4 + app/Services/Invoice/SendEmail.php | 2 +- app/Services/Quote/SendEmail.php | 2 +- database/factories/ClientContactFactory.php | 1 + database/seeds/RandomDataSeeder.php | 8 +- tests/Integration/SendFailedEmailsTest.php | 68 +++++++++++++++ tests/MockAccountData.php | 2 +- 24 files changed, 446 insertions(+), 29 deletions(-) create mode 100644 app/DataMapper/EmailSpooledForSend.php create mode 100644 app/Jobs/Ninja/AdjustEmailQuota.php create mode 100644 app/Jobs/Ninja/RefundCancelledAccount.php create mode 100644 app/Jobs/Util/SendFailedEmails.php create mode 100644 tests/Integration/SendFailedEmailsTest.php diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index f5da667a89e7..85b08871f769 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -12,7 +12,9 @@ namespace App\Console; use App\Jobs\Cron\RecurringInvoicesCron; +use App\Jobs\Util\AdjustEmailQuota; use App\Jobs\Util\ReminderJob; +use App\Jobs\Util\SendFailedEmails; use App\Jobs\Util\VersionCheck; use App\Utils\Ninja; use Illuminate\Console\Scheduling\Schedule; @@ -43,6 +45,11 @@ class Kernel extends ConsoleKernel $schedule->job(new ReminderJob)->daily(); + /* Run hosted specific jobs */ + if(Ninja::isHosted()) { + $schedule->json()->daily(new AdjustEmailQuota())->daily; + $schedule->job(new SendFailedEmails())->daily(); + } /* Run queue's in shared hosting with this*/ if (Ninja::isSelfHost()) { $schedule->command('queue:work')->everyMinute()->withoutOverlapping(); diff --git a/app/DataMapper/EmailSpooledForSend.php b/app/DataMapper/EmailSpooledForSend.php new file mode 100644 index 000000000000..06da9560523c --- /dev/null +++ b/app/DataMapper/EmailSpooledForSend.php @@ -0,0 +1,37 @@ +account->companies->count(); + $account = $company->account; if ($company_count == 1) { + $company->company_users->each(function ($company_user) { $company_user->user->forceDelete(); }); - $company->account->delete(); + if(Ninja::isHosted()) + RefundCancelledAccount::dispatchNow($account); + + $account->delete(); + } else { - $account = $company->account; + $company_id = $company->id; $company->delete(); @@ -479,11 +487,6 @@ class CompanyController extends BaseController $account->save(); } } - - //@todo delete documents also!! - - //@todo in the hosted version deleting the last - //account will trigger an account refund. return response()->json(['message' => 'success'], 200); } diff --git a/app/Http/Requests/ClientPortal/ShowInvoiceRequest.php b/app/Http/Requests/ClientPortal/ShowInvoiceRequest.php index e3371b40dc9d..277ebe16f4b6 100644 --- a/app/Http/Requests/ClientPortal/ShowInvoiceRequest.php +++ b/app/Http/Requests/ClientPortal/ShowInvoiceRequest.php @@ -23,8 +23,8 @@ class ShowInvoiceRequest extends Request */ public function authorize() : bool - {info(auth('contact')->user()->client->id); - info($this->invoice->client_id); + { + return auth('contact')->user()->client->id === $this->invoice->client_id; } } diff --git a/app/Jobs/Credit/EmailCredit.php b/app/Jobs/Credit/EmailCredit.php index 04aaf9f194ef..f4a66f2200b1 100644 --- a/app/Jobs/Credit/EmailCredit.php +++ b/app/Jobs/Credit/EmailCredit.php @@ -57,7 +57,7 @@ class EmailCredit implements ShouldQueue $template_style = $this->credit->client->getSetting('email_style'); $this->credit->invitations->each(function ($invitation) use ($template_style) { - if ($invitation->contact->send && $invitation->contact->email) { + if ($invitation->contact->send_email && $invitation->contact->email) { $message_array = $this->credit->getEmailData('', $invitation->contact); $message_array['title'] = &$message_array['subject']; $message_array['footer'] = "Sent to ".$invitation->contact->present()->name(); diff --git a/app/Jobs/Invoice/CreateUbl.php b/app/Jobs/Invoice/CreateUbl.php index 6a1476a270fd..b9f14203a65d 100644 --- a/app/Jobs/Invoice/CreateUbl.php +++ b/app/Jobs/Invoice/CreateUbl.php @@ -100,7 +100,7 @@ class CreateUbl implements ShouldQueue try { return Generator::invoice($ubl_invoice, $invoice->client->getCurrencyCode()); } catch (\Exception $exception) { - info(print_r($exception, 1)); + return false; } } diff --git a/app/Jobs/Invoice/EmailInvoice.php b/app/Jobs/Invoice/EmailInvoice.php index 0c8d8006d128..92444ffe5a7c 100644 --- a/app/Jobs/Invoice/EmailInvoice.php +++ b/app/Jobs/Invoice/EmailInvoice.php @@ -53,7 +53,7 @@ class EmailInvoice implements ShouldQueue */ public function handle() - { + { MultiDB::setDB($this->company->db); Mail::to($this->invoice_invitation->contact->email, $this->invoice_invitation->contact->present()->name()) diff --git a/app/Jobs/Ninja/AdjustEmailQuota.php b/app/Jobs/Ninja/AdjustEmailQuota.php new file mode 100644 index 000000000000..4720fc5a36ca --- /dev/null +++ b/app/Jobs/Ninja/AdjustEmailQuota.php @@ -0,0 +1,80 @@ +adjust(); + } else { + //multiDB environment, need to + foreach (MultiDB::$dbs as $db) { + + MultiDB::setDB($db); + + $this->adjust(); + } + } + + } + + public function adjust() + { + + foreach(Account::cursor() as $account){ + //@TODO once we add in the two columns daily_emails_quota daily_emails_sent_ + } + + } + +} diff --git a/app/Jobs/Ninja/RefundCancelledAccount.php b/app/Jobs/Ninja/RefundCancelledAccount.php new file mode 100644 index 000000000000..63cf508d0452 --- /dev/null +++ b/app/Jobs/Ninja/RefundCancelledAccount.php @@ -0,0 +1,78 @@ +account = $account; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + // if free plan, return + if(Ninja::isSelfHost() || $this->account->isFreeHostedClient()) + return; + + $plan_details = $account->getPlanDetails(); + + /* Trial user cancelling early.... */ + if($plan_details['trial_active']) + return; + + /* Is the plan Active? */ + if(!$plan_details['active']) + return; + + /* Refundable client! */ + + $plan_start = $plan_details['started']; + $plan_expires = $plan_details['expires']; + $paid = $plan_details['paid']; + $term = $plan_details['term']; + + $refund = $this->calculateRefundAmount($paid, $plan_expires); + + /* Are there any edge cases? */ + + //@TODO process refund by refunding directly to the payment_id; + } + + private function calculateRefundAmount($amount, $plan_expires) + { + $end_date = Carbon::parse($plan_expires); + $now = Carbon::now(); + + $days_left = $now->diffInDays($end_date); + + $pro_rata_ratio = $days_left / 365; + + $pro_rata_refund = $amount * $pro_rata_ratio; + + return $pro_rata_refund; + } + +} diff --git a/app/Jobs/Util/Import.php b/app/Jobs/Util/Import.php index 4f9f850989f6..6a84c10207c2 100644 --- a/app/Jobs/Util/Import.php +++ b/app/Jobs/Util/Import.php @@ -1,4 +1,13 @@ processEmails(); + } else { + //multiDB environment, need to + foreach (MultiDB::$dbs as $db) { + + MultiDB::setDB($db); + + $this->processEmails(); + } + } + + } + + private function processEmails() + { + + //info("process emails"); + //@todo check that the quota is available for the job + + $email_jobs = SystemLog::where('event_id', SystemLog::EVENT_MAIL_RETRY_QUEUE)->get(); + + $email_jobs->each(function($job){ + + $job_meta_array = $job->log; + + $invitation = $job_meta_array['entity_name']::where('key', $job_meta_array['invitation_key'])->with('contact')->first(); + + if($invitation->invoice){ + $email_builder = (new InvoiceEmail())->build($invitation, $job_meta_array['reminder_template']); + + if ($invitation->contact->send_email && $invitation->contact->email) { + EmailInvoice::dispatch($email_builder, $invitation, $invitation->company); + } + } + }); + + } + +} diff --git a/app/Jobs/Util/StartMigration.php b/app/Jobs/Util/StartMigration.php index 7025bd751f9c..e38343ec68ed 100644 --- a/app/Jobs/Util/StartMigration.php +++ b/app/Jobs/Util/StartMigration.php @@ -1,4 +1,13 @@ invoice->invitations->each(function ($invitation) { $email_builder = (new InvoiceEmail())->build($invitation, $this->reminder_template); - if ($invitation->contact->send && $invitation->contact->email) { + if ($invitation->contact->send_email && $invitation->contact->email) { EmailInvoice::dispatch($email_builder, $invitation, $invitation->company); } }); diff --git a/app/Services/Quote/SendEmail.php b/app/Services/Quote/SendEmail.php index e5ab7a9be414..666115839a8f 100644 --- a/app/Services/Quote/SendEmail.php +++ b/app/Services/Quote/SendEmail.php @@ -27,7 +27,7 @@ class SendEmail } $this->quote->invitations->each(function ($invitation) { - if ($invitation->contact->send && $invitation->contact->email) { + if ($invitation->contact->send_email && $invitation->contact->email) { $email_builder = (new QuoteEmail())->build($invitation, $reminder_template); EmailQuote::dispatchNow($email_builder, $invitation); diff --git a/database/factories/ClientContactFactory.php b/database/factories/ClientContactFactory.php index 1f0b6dea58c3..1a46709125be 100644 --- a/database/factories/ClientContactFactory.php +++ b/database/factories/ClientContactFactory.php @@ -20,6 +20,7 @@ $factory->define(App\Models\ClientContact::class, function (Faker $faker) { 'phone' => $faker->phoneNumber, 'email_verified_at' => now(), 'email' => $faker->unique()->safeEmail, + 'send_email' => true, 'password' => bcrypt('password'), 'remember_token' => \Illuminate\Support\Str::random(10), 'contact_key' => \Illuminate\Support\Str::random(40), diff --git a/database/seeds/RandomDataSeeder.php b/database/seeds/RandomDataSeeder.php index 1847b736cf59..871588befefb 100644 --- a/database/seeds/RandomDataSeeder.php +++ b/database/seeds/RandomDataSeeder.php @@ -144,7 +144,7 @@ class RandomDataSeeder extends Seeder /** Invoice Factory */ factory(\App\Models\Invoice::class, 20)->create(['user_id' => $user->id, 'company_id' => $company->id, 'client_id' => $client->id]); - $invoices = Invoice::cursor(); + $invoices = Invoice::all(); $invoice_repo = new InvoiceRepository(); $invoices->each(function ($invoice) use ($invoice_repo, $user, $company, $client) { @@ -160,12 +160,12 @@ class RandomDataSeeder extends Seeder $invoice->save(); - event(new CreateInvoiceInvitation($invoice)); + //event(new CreateInvoiceInvitation($invoice)); + $invoice->service()->createInvitations()->markSent()->save(); + $invoice->ledger()->updateInvoiceBalance($invoice->balance); - $invoice->service()->markSent()->save(); - event(new InvoiceWasMarkedSent($invoice, $company)); if (rand(0, 1)) { diff --git a/tests/Integration/SendFailedEmailsTest.php b/tests/Integration/SendFailedEmailsTest.php new file mode 100644 index 000000000000..917fc0a443e3 --- /dev/null +++ b/tests/Integration/SendFailedEmailsTest.php @@ -0,0 +1,68 @@ +makeTestData(); + } + + + public function testReminderFires() + { + $invitation = $this->invoice->invitations->first(); + $reminder_template = $this->invoice->calculateTemplate(); + + $sl = [ + 'entity_name' => 'App\Models\InvoiceInvitation', + 'invitation_key' => $invitation->key, + 'reminder_template' => $reminder_template, + 'subject' => '', + 'body' => '', + ]; + + $system_log = new SystemLog; + $system_log->company_id = $this->invoice->company_id; + $system_log->client_id = $this->invoice->client_id; + $system_log->category_id = SystemLog::CATEGORY_MAIL; + $system_log->event_id = SystemLog::EVENT_MAIL_RETRY_QUEUE; + $system_log->type_id = SystemLog::TYPE_QUOTA_EXCEEDED; + $system_log->log = $sl; + $system_log->save(); + + $sys_log = SystemLog::where('event_id', SystemLog::EVENT_MAIL_RETRY_QUEUE)->first(); + + $this->assertNotNull($sys_log); + + // Queue::fake(); + SendFailedEmails::dispatch(); + + //Queue::assertPushed(SendFailedEmails::class); + //Queue::assertPushed(EmailInvoice::class); + //$this->expectsJobs(EmailInvoice::class); + + } + +} diff --git a/tests/MockAccountData.php b/tests/MockAccountData.php index 19059b0a82f5..b73505faf8a4 100644 --- a/tests/MockAccountData.php +++ b/tests/MockAccountData.php @@ -200,7 +200,7 @@ trait MockAccountData $this->invoice->save(); - $this->invoice->service()->markSent(); + $this->invoice->service()->createInvitations()->markSent(); $this->quote = factory(\App\Models\Quote::class)->create([ 'user_id' => $this->user->id,