diff --git a/app/Http/Controllers/PreviewController.php b/app/Http/Controllers/PreviewController.php index 8861ca321..ba81318fc 100644 --- a/app/Http/Controllers/PreviewController.php +++ b/app/Http/Controllers/PreviewController.php @@ -218,7 +218,7 @@ class PreviewController extends BaseController /* Catch all in case migration doesn't pass back a valid design */ if(!$design) - $design = Design::find(2); + $design = \App\Models\Design::find(2); if ($design->is_custom) { $options = [ diff --git a/app/Http/Requests/Payments/PaymentWebhookRequest.php b/app/Http/Requests/Payments/PaymentWebhookRequest.php index ea11d0af2..50bc276c1 100644 --- a/app/Http/Requests/Payments/PaymentWebhookRequest.php +++ b/app/Http/Requests/Payments/PaymentWebhookRequest.php @@ -74,9 +74,9 @@ class PaymentWebhookRequest extends Request { // For testing purposes we'll slow down the webhook processing by 2 seconds // to make sure webhook request doesn't came before our processing. - if (app()->environment() !== 'production') { + //if (app()->environment() !== 'production') { sleep(2); - } + //} // Some gateways, like Checkout, we can dynamically pass payment hash, // which we will resolve here and get payment information from it. diff --git a/app/Jobs/Entity/EmailEntity.php b/app/Jobs/Entity/EmailEntity.php index 32829c030..f8e4ae5af 100644 --- a/app/Jobs/Entity/EmailEntity.php +++ b/app/Jobs/Entity/EmailEntity.php @@ -119,7 +119,7 @@ class EmailEntity implements ShouldQueue $nmo->reminder_template = $this->reminder_template; $nmo->entity = $this->entity; - NinjaMailerJob::dispatch($nmo); + NinjaMailerJob::dispatchNow($nmo); /* Mark entity sent */ $this->entity->service()->markSent()->save(); diff --git a/app/Jobs/RecurringInvoice/SendRecurring.php b/app/Jobs/RecurringInvoice/SendRecurring.php index 70e249f58..96ed926e7 100644 --- a/app/Jobs/RecurringInvoice/SendRecurring.php +++ b/app/Jobs/RecurringInvoice/SendRecurring.php @@ -86,14 +86,14 @@ class SendRecurring implements ShouldQueue $this->recurring_invoice->save(); //Admin notification for recurring invoice sent. - if ($invoice->invitations->count() >= 1) { + if ($invoice->invitations->count() >= 1 ) { $invoice->entityEmailEvent($invoice->invitations->first(), 'invoice', 'email_template_invoice'); } nlog("Invoice {$invoice->number} created"); $invoice->invitations->each(function ($invitation) use ($invoice) { - if ($invitation->contact && strlen($invitation->contact->email) >=1) { + if ($invitation->contact && strlen($invitation->contact->email) >=1 && $invoice->client->getSetting('auto_email_invoice')) { try{ EmailEntity::dispatch($invitation, $invoice->company); diff --git a/app/Jobs/Util/Import.php b/app/Jobs/Util/Import.php index e256105de..7b01cc890 100644 --- a/app/Jobs/Util/Import.php +++ b/app/Jobs/Util/Import.php @@ -35,11 +35,14 @@ use App\Http\ValidationRules\ValidCompanyGatewayFeesAndLimitsRule; use App\Http\ValidationRules\ValidUserForCompany; use App\Jobs\Company\CreateCompanyTaskStatuses; use App\Jobs\Company\CreateCompanyToken; +use App\Jobs\Mail\NinjaMailerJob; +use App\Jobs\Mail\NinjaMailerObject; use App\Jobs\Ninja\CheckCompanyData; use App\Jobs\Ninja\CompanySizeCheck; use App\Jobs\Util\VersionCheck; use App\Libraries\MultiDB; use App\Mail\MigrationCompleted; +use App\Mail\Migration\StripeConnectMigration; use App\Models\Activity; use App\Models\Client; use App\Models\ClientContact; @@ -87,6 +90,7 @@ use Illuminate\Http\UploadedFile; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; use Illuminate\Support\Carbon; +use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Validator; use Illuminate\Support\Str; @@ -244,6 +248,10 @@ class Import implements ShouldQueue // $this->fixData(); try{ + App::forgetInstance('translator'); + $t = app('translator'); + $t->replace(Ninja::transformTranslations($this->company->settings)); + Mail::to($this->user->email, $this->user->name()) ->send(new MigrationCompleted($this->company, implode("
",$check_data))); } @@ -1381,9 +1389,21 @@ class Import implements ShouldQueue $modified['fees_and_limits'] = $this->cleanFeesAndLimits($modified['fees_and_limits']); } + /* On Hosted platform we need to advise Stripe users to connect with Stripe Connect */ if(Ninja::isHosted() && $modified['gateway_key'] == 'd14dd26a37cecc30fdd65700bfb55b23'){ + + $nmo = new NinjaMailerObject; + $nmo->mailable = new StripeConnectMigration($this->company); + $nmo->company = $this->company; + $nmo->settings = $this->company->settings; + $nmo->to_user = $this->user; + NinjaMailerJob::dispatch($nmo); + $modified['gateway_key'] = 'd14dd26a47cecc30fdd65700bfb67b34'; - $modified['fees_and_limits'] = []; + + //why do we set this to a blank array? + //$modified['fees_and_limits'] = []; + } $company_gateway = CompanyGateway::create($modified); diff --git a/app/Mail/Migration/StripeConnectMigration.php b/app/Mail/Migration/StripeConnectMigration.php new file mode 100644 index 000000000..93fda725a --- /dev/null +++ b/app/Mail/Migration/StripeConnectMigration.php @@ -0,0 +1,55 @@ +company = $company; + } + + /** + * Build the message. + * + * @return $this + */ + public function build() + { + $this->settings = $this->company->settings; + $this->logo = $this->company->present()->logo(); + $this->whitelabel = $this->company->account->isPaid(); + + return $this->from(config('mail.from.address'), config('mail.from.name')) + ->subject(ctrans('texts.stripe_connect_migration_title')) + ->view('email.migration.stripe_connect'); + } +} diff --git a/app/Models/Client.php b/app/Models/Client.php index 5bbfcc8af..894d928a0 100644 --- a/app/Models/Client.php +++ b/app/Models/Client.php @@ -15,7 +15,11 @@ use App\DataMapper\ClientSettings; use App\DataMapper\CompanySettings; use App\DataMapper\FeesAndLimits; use App\Models\CompanyGateway; +use App\Models\Expense; use App\Models\Presenters\ClientPresenter; +use App\Models\Project; +use App\Models\Quote; +use App\Models\Task; use App\Services\Client\ClientService; use App\Utils\Traits\AppSetup; use App\Utils\Traits\GeneratesCounter; @@ -153,6 +157,16 @@ class Client extends BaseModel implements HasLocalePreference return $this->hasMany(ClientGatewayToken::class); } + public function expenses() + { + return $this->hasMany(Expense::class)->withTrashed(); + } + + public function projects() + { + return $this->hasMany(Project::class)->withTrashed(); + } + /** * Retrieves the specific payment token per * gateway - per payment method. @@ -217,6 +231,16 @@ class Client extends BaseModel implements HasLocalePreference return $this->hasMany(Invoice::class)->withTrashed(); } + public function quotes() + { + return $this->hasMany(Quote::class)->withTrashed(); + } + + public function tasks() + { + return $this->hasMany(Task::class)->withTrashed(); + } + public function recurring_invoices() { return $this->hasMany(RecurringInvoice::class)->withTrashed(); @@ -774,7 +798,7 @@ class Client extends BaseModel implements HasLocalePreference public function payments() { - return $this->hasMany(Payment::class); + return $this->hasMany(Payment::class)->withTrashed(); } public function timezone_offset() diff --git a/app/Repositories/ActivityRepository.php b/app/Repositories/ActivityRepository.php index ce31e331c..ec0beef03 100644 --- a/app/Repositories/ActivityRepository.php +++ b/app/Repositories/ActivityRepository.php @@ -125,7 +125,7 @@ class ActivityRepository extends BaseRepository $design = Design::find($entity_design_id); - if(!$entity->invitations()->exists()){ + if(!$entity->invitations()->exists() || !$design){ nlog("No invitations for entity {$entity->id} - {$entity->number}"); return; } diff --git a/app/Services/Client/ClientService.php b/app/Services/Client/ClientService.php index 2355639f7..840a3b7a9 100644 --- a/app/Services/Client/ClientService.php +++ b/app/Services/Client/ClientService.php @@ -12,6 +12,7 @@ namespace App\Services\Client; use App\Models\Client; +use App\Services\Client\Merge; use App\Services\Client\PaymentMethod; use App\Utils\Number; use Illuminate\Database\Eloquent\Collection; @@ -77,6 +78,13 @@ class ClientService return (new PaymentMethod($this->client, $amount))->run(); } + public function merge(Client $mergable_client) + { + $this->client = (new Merge($this->client, $mergable_client))->run(); + + return $this; + } + public function save() :Client { $this->client->save(); diff --git a/app/Services/Client/Merge.php b/app/Services/Client/Merge.php new file mode 100644 index 000000000..e254000b2 --- /dev/null +++ b/app/Services/Client/Merge.php @@ -0,0 +1,102 @@ +client = $client; + $this->mergable_client = $mergable_client; + } + + public function run() + { + + $this->client->balance += $this->mergable_client->balance; + $this->client->paid_to_date += $this->mergable_client->paid_to_date; + $this->client->save(); + + $this->updateLedger($this->mergable_client->balance); + + $this->mergable_client->activities()->update(['client_id' => $this->client->id]); + $this->mergable_client->contacts()->update(['client_id' => $this->client->id]); + $this->mergable_client->gateway_tokens()->update(['client_id' => $this->client->id]); + $this->mergable_client->credits()->update(['client_id' => $this->client->id]); + $this->mergable_client->expenses()->update(['client_id' => $this->client->id]); + $this->mergable_client->invoices()->update(['client_id' => $this->client->id]); + $this->mergable_client->payments()->update(['client_id' => $this->client->id]); + $this->mergable_client->projects()->update(['client_id' => $this->client->id]); + $this->mergable_client->quotes()->update(['client_id' => $this->client->id]); + $this->mergable_client->recurring_invoices()->update(['client_id' => $this->client->id]); + $this->mergable_client->tasks()->update(['client_id' => $this->client->id]); + $this->mergable_client->documents()->update(['documentable_id' => $this->client->id]); + + /* Loop through contacts an only merge distinct contacts by email */ + $this->mergable_client->contacts->each(function ($contact){ + + $exist = $this->client->contacts->contains(function ($client_contact) use($contact){ + return $client_contact->email == $contact->email; + }); + + if($exist) + { + $contact->delete(); + $contact->save(); + } + + }); + + $this->mergable_client->forceDelete(); + + return $this->client; + } + + private function updateLedger($adjustment) + { + $balance = 0; + + $company_ledger = CompanyLedger::whereClientId($this->client->id) + ->orderBy('id', 'DESC') + ->first(); + + if ($company_ledger) { + $balance = $company_ledger->balance; + } + + $company_ledger = CompanyLedgerFactory::create($this->client->company_id, $this->client->user_id); + $company_ledger->client_id = $this->client->id; + $company_ledger->adjustment = $adjustment; + $company_ledger->notes = "Balance update after merging " . $this->mergable_client->present()->name(); + $company_ledger->balance = $balance + $adjustment; + $company_ledger->activity_id = Activity::UPDATE_CLIENT; + $company_ledger->save(); + + } + +} \ No newline at end of file diff --git a/app/Services/User/UserService.php b/app/Services/User/UserService.php index 5b4463242..bcecad59a 100644 --- a/app/Services/User/UserService.php +++ b/app/Services/User/UserService.php @@ -39,7 +39,7 @@ class UserService $nmo->to_user = $this->user; $nmo->settings = $company->settings; - NinjaMailerJob::dispatch($nmo); + NinjaMailerJob::dispatch($nmo, true); Ninja::registerNinjaUser($this->user); diff --git a/resources/lang/en/texts.php b/resources/lang/en/texts.php index 40038652e..eed12cf3f 100644 --- a/resources/lang/en/texts.php +++ b/resources/lang/en/texts.php @@ -4287,6 +4287,8 @@ $LANG = array( 'company_deleted' => 'Company deleted', 'company_deleted_body' => 'Company [ :company ] was deleted by :user', 'back_to' => 'Back to :url', + 'stripe_connect_migration_title' => 'Connect your Stripe Account', + 'stripe_connect_migration_desc' => 'Invoice Ninja v5 uses Stripe Connect to link your Stripe account to Invoice Ninja. This provides an additional layer of security for your account. Now that you data has migrated, you will need to Authorize Stripe to accept payments in v5.

To do this, navigate to Settings > Online Payments > Configure Gateways. Click on Stripe Connect and then under Settings click Setup Gateway. This will take you to Stripe to authorize Invoice Ninja and on your return your account will be successfully linked!', ); return $LANG; diff --git a/resources/views/email/migration/max_companies.blade.php b/resources/views/email/migration/max_companies.blade.php index 6dd1b45a9..11bb1484f 100644 --- a/resources/views/email/migration/max_companies.blade.php +++ b/resources/views/email/migration/max_companies.blade.php @@ -1,6 +1,7 @@ @component('email.template.admin', ['logo' => $logo, 'settings' => $settings])

{{ ctrans('texts.max_companies') }}

+

{{ ctrans('texts.max_companies_desc') }}

@endcomponent diff --git a/resources/views/email/migration/stripe_connect.blade.php b/resources/views/email/migration/stripe_connect.blade.php new file mode 100644 index 000000000..6e7be3c2c --- /dev/null +++ b/resources/views/email/migration/stripe_connect.blade.php @@ -0,0 +1,7 @@ +@component('email.template.admin', ['logo' => $logo, 'settings' => $settings]) +
+

{{ ctrans('texts.stripe_connect_migration_title') }}

+ +

{{ ctrans('texts.stripe_connect_migration_desc') }}

+
+@endcomponent diff --git a/tests/Feature/Client/ClientMergeTest.php b/tests/Feature/Client/ClientMergeTest.php new file mode 100644 index 000000000..c990b2013 --- /dev/null +++ b/tests/Feature/Client/ClientMergeTest.php @@ -0,0 +1,180 @@ +faker = Factory::create(); + $this->buildCache(true); + } + + public function testSearchingForContacts() + { + + $account = Account::factory()->create(); + + $this->user = User::factory()->create([ + 'account_id' => $account->id, + 'email' => $this->faker->safeEmail + ]); + + $this->company = Company::factory()->create([ + 'account_id' => $account->id + ]); + + $this->client = Client::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id + ]); + + $this->primary_contact = ClientContact::factory()->create([ + 'user_id' => $this->user->id, + 'client_id' => $this->client->id, + 'company_id' => $this->company->id, + 'is_primary' => 1, + ]); + + ClientContact::factory()->count(2)->create([ + 'user_id' => $this->user->id, + 'client_id' => $this->client->id, + 'company_id' => $this->company->id, + ]); + + ClientContact::factory()->create([ + 'user_id' => $this->user->id, + 'client_id' => $this->client->id, + 'company_id' => $this->company->id, + 'email' => 'search@gmail.com' + ]); + + + $this->assertEquals(4, $this->client->contacts->count()); + $this->assertTrue($this->client->contacts->contains(function ($contact) { + return $contact->email == 'search@gmail.com'; + })); + + $this->assertFalse($this->client->contacts->contains(function ($contact) { + return $contact->email == 'false@gmail.com'; + })); + + } + + public function testMergeClients() + { + + $account = Account::factory()->create(); + + $user = User::factory()->create([ + 'account_id' => $account->id, + 'email' => $this->faker->safeEmail + ]); + + $company = Company::factory()->create([ + 'account_id' => $account->id + ]); + + $client = Client::factory()->create([ + 'user_id' => $user->id, + 'company_id' => $company->id + ]); + + $primary_contact = ClientContact::factory()->create([ + 'user_id' => $user->id, + 'client_id' => $client->id, + 'company_id' => $company->id, + 'is_primary' => 1, + ]); + + ClientContact::factory()->count(2)->create([ + 'user_id' => $user->id, + 'client_id' => $client->id, + 'company_id' => $company->id, + ]); + + ClientContact::factory()->create([ + 'user_id' => $user->id, + 'client_id' => $client->id, + 'company_id' => $company->id, + 'email' => 'search@gmail.com' + ]); + //4contacts + + $mergable_client = Client::factory()->create([ + 'user_id' => $user->id, + 'company_id' => $company->id + ]); + + $primary_contact = ClientContact::factory()->create([ + 'user_id' => $user->id, + 'client_id' => $mergable_client->id, + 'company_id' => $company->id, + 'is_primary' => 1, + ]); + + ClientContact::factory()->count(2)->create([ + 'user_id' => $user->id, + 'client_id' => $mergable_client->id, + 'company_id' => $company->id, + ]); + + ClientContact::factory()->create([ + 'user_id' => $user->id, + 'client_id' => $mergable_client->id, + 'company_id' => $company->id, + 'email' => 'search@gmail.com' + ]); + //4 contacts + + $this->assertEquals(4, $client->contacts->count()); + $this->assertEquals(4, $mergable_client->contacts->count()); + + $client = $client->service()->merge($mergable_client)->save(); + + // nlog($client->contacts->fresh()->toArray()); + // $this->assertEquals(7, $client->fresh()->contacts->count()); + + } + +}