diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6b5489f6e390..b1be04124823 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,7 +23,7 @@ jobs: - name: Copy .env file run: | cp .env.example .env - + - name: Install composer dependencies run: | composer config -g github-oauth.github.com ${{ secrets.GITHUB_TOKEN }} @@ -65,8 +65,9 @@ jobs: - name: Build project run: | - zip -r ./invoiceninja.zip .* -x "../*" - + zip -r /home/runner/work/invoiceninja/invoiceninja.zip .* -x "../*" + shopt -s dotglob + tar --exclude='public/storage' --exclude='.htaccess' --exclude='invoiceninja.zip' -zcvf /home/runner/work/invoiceninja/invoiceninja.tar * - name: Release uses: softprops/action-gh-release@v1 if: startsWith(github.ref, 'refs/tags/') @@ -74,4 +75,5 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: files: | - invoiceninja.zip + /home/runner/work/invoiceninja/invoiceninja.tar + /home/runner/work/invoiceninja/invoiceninja.zip \ No newline at end of file diff --git a/VERSION.txt b/VERSION.txt index cd6647b66e59..f74778e9a38a 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -5.5.113 \ No newline at end of file +5.5.114 \ No newline at end of file diff --git a/app/Filters/InvoiceFilters.php b/app/Filters/InvoiceFilters.php index 2cc72fb4e846..9460026c63ef 100644 --- a/app/Filters/InvoiceFilters.php +++ b/app/Filters/InvoiceFilters.php @@ -114,6 +114,23 @@ class InvoiceFilters extends QueryFilters }); } + /** + * @return Builder + * @throws RuntimeException + */ + public function status_id(string $status = ''): Builder + { + + if (strlen($status) == 0) { + return $this->builder; + } + + return $this->builder->whereIn('status_id', explode(",", $status)); + + } + + + /** * @return Builder * @throws RuntimeException diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php index fbad5f88b741..b3a4a96e177d 100644 --- a/app/Http/Controllers/Auth/LoginController.php +++ b/app/Http/Controllers/Auth/LoginController.php @@ -24,6 +24,7 @@ use App\Models\CompanyToken; use Illuminate\Http\Request; use App\Libraries\OAuth\OAuth; use App\Events\User\UserLoggedIn; +use Illuminate\Http\JsonResponse; use PragmaRX\Google2FA\Google2FA; use App\Jobs\Account\CreateAccount; use Illuminate\Support\Facades\Auth; @@ -32,6 +33,7 @@ use Illuminate\Support\Facades\Cache; use Turbo124\Beacon\Facades\LightLogs; use App\Http\Controllers\BaseController; use App\Jobs\Company\CreateCompanyToken; +use Illuminate\Support\Facades\Response; use Laravel\Socialite\Facades\Socialite; use App\Http\Requests\Login\LoginRequest; use App\Libraries\OAuth\Providers\Google; @@ -109,6 +111,7 @@ class LoginController extends BaseController ->increment() ->batch(); + /** @var \App\Models\User $user */ $user = $this->guard()->user(); //2FA @@ -135,7 +138,8 @@ class LoginController extends BaseController $account->save(); $user = $user->fresh(); } - + + /** @var \App\Models\CompanyUser $cu */ $cu = $this->hydrateCompanyUser(); if ($cu->count() == 0) { @@ -168,7 +172,7 @@ class LoginController extends BaseController * Refreshes the data feed with the current Company User. * * @param Request $request - * @return CompanyUser Refresh Feed. + * @return Response|JsonResponse */ public function refresh(Request $request) { @@ -271,6 +275,7 @@ class LoginController extends BaseController Auth::login($existing_user, true); + /** @var \App\Models\CompanyUser $cu */ $cu = $this->hydrateCompanyUser(); if ($cu->count() == 0) { @@ -290,12 +295,16 @@ class LoginController extends BaseController } Auth::login($existing_login_user, true); + /** @var \App\Models\User $user */ - auth()->user()->update([ + $user = auth()->user(); + + $user->update([ 'oauth_user_id' => $user->id, 'oauth_provider_id' => $provider, ]); + /** @var \App\Models\CompanyUser $cu */ $cu = $this->hydrateCompanyUser(); if ($cu->count() == 0) { @@ -333,9 +342,14 @@ class LoginController extends BaseController $account = (new CreateAccount($new_account, request()->getClientIp()))->handle(); Auth::login($account->default_company->owner(), true); - auth()->user()->email_verified_at = now(); - auth()->user()->save(); + /** @var \App\Models\User $user */ + $user = auth()->user(); + + $user->email_verified_at = now(); + $user->save(); + + /** @var \App\Models\CompanyUser $cu */ $cu = $this->hydrateCompanyUser(); if ($cu->count() == 0) { @@ -363,26 +377,23 @@ class LoginController extends BaseController $set_company = $cu->first()->company; } - auth()->user()->setCompany($set_company); + /** @var \App\Models\User $user */ + $user = auth()->user(); - $this->setLoginCache(auth()->user()); + $user->setCompany($set_company); + + $this->setLoginCache($user); $truth = app()->make(TruthSource::class); $truth->setCompanyUser($cu->first()); - $truth->setUser(auth()->user()); + $truth->setUser($user); $truth->setCompany($set_company); - if ($cu->count() == 0) { - return $cu; - } - - if (auth()->user()->company_users()->count() != auth()->user()->tokens()->distinct('company_id')->count()) { - auth()->user()->companies->each(function ($company) { - if (!CompanyToken::where('user_id', auth()->user()->id)->where('company_id', $company->id)->where('is_system', true)->exists()) { - (new CreateCompanyToken($company, auth()->user(), 'Google_O_Auth'))->handle(); - } - }); - } + $cu->first()->account->companies->each(function ($company) use ($cu) { + if ($company->tokens()->where('is_system', true)->count() == 0) { + (new CreateCompanyToken($company, $cu->first()->user, request()->server('HTTP_USER_AGENT')))->handle(); + } + }); $truth->setCompanyToken(CompanyToken::where('user_id', auth()->user()->id)->where('company_id', $set_company->id)->first()); @@ -457,10 +468,17 @@ class LoginController extends BaseController return response()->json(['message' => 'Unable to authenticate this user'], 400); } + /** + * send login response to oauthed users + * + * @param \App\Models\User $existing_user + * @return Response | JsonResponse + */ private function existingOauthUser($existing_user) { Auth::login($existing_user, true); + /** @var \App\Models\CompanyUser $cu */ $cu = $this->hydrateCompanyUser(); if ($cu->count() == 0) { @@ -476,11 +494,16 @@ class LoginController extends BaseController private function existingLoginUser($oauth_user_id, $provider) { - auth()->user()->update([ + + /** @var \App\Models\User $user */ + $user = auth()->user(); + + $user->update([ 'oauth_user_id' => $oauth_user_id, 'oauth_provider_id' => $provider, ]); + /** @var \App\Models\CompanyUser $cu */ $cu = $this->hydrateCompanyUser(); if ($cu->count() == 0) { @@ -579,9 +602,14 @@ class LoginController extends BaseController } Auth::login($account->default_company->owner(), true); - auth()->user()->email_verified_at = now(); - auth()->user()->save(); + /** @var \App\Models\User $user */ + $user = auth()->user(); + + $user->email_verified_at = now(); + $user->save(); + + /** @var \App\Models\CompanyUser $cu */ $cu = $this->hydrateCompanyUser(); if ($cu->count() == 0) { @@ -694,8 +722,6 @@ class LoginController extends BaseController 'email' => $socialite_user->getEmail(), 'oauth_user_id' => $socialite_user->getId(), 'oauth_provider_id' => $provider, - // 'oauth_user_token' => $oauth_user_token, - // 'oauth_user_refresh_token' => $socialite_user->accessTokenResponseBody['refresh_token'], 'oauth_user_token_expiry' => $oauth_expiry, ]; diff --git a/app/Http/Controllers/ClientPortal/InvoiceController.php b/app/Http/Controllers/ClientPortal/InvoiceController.php index 351d23f36fb4..838bf1dd1b09 100644 --- a/app/Http/Controllers/ClientPortal/InvoiceController.php +++ b/app/Http/Controllers/ClientPortal/InvoiceController.php @@ -60,7 +60,7 @@ class InvoiceController extends Controller $invitation->markViewed(); event(new InvitationWasViewed($invoice, $invitation, $invoice->company, Ninja::eventVars())); - event(new InvoiceWasViewed($invitation, $invitation->company, Ninja::eventVars())); + event(new InvoiceWasViewed($invitation, $invoice->company, Ninja::eventVars())); } $data = [ diff --git a/app/Http/Controllers/ClientPortal/NinjaPlanController.php b/app/Http/Controllers/ClientPortal/NinjaPlanController.php index d3d275b92a9f..bf61b7d34517 100644 --- a/app/Http/Controllers/ClientPortal/NinjaPlanController.php +++ b/app/Http/Controllers/ClientPortal/NinjaPlanController.php @@ -177,8 +177,10 @@ class NinjaPlanController extends Controller ->increment() ->queue(); - - $old_recurring = RecurringInvoice::where('company_id', config('ninja.ninja_default_company_id'))->where('client_id', $client->id)->first(); + $old_recurring = RecurringInvoice::where('company_id', config('ninja.ninja_default_company_id')) + ->where('client_id', $client->id) + ->where('id', '!=', $recurring_invoice->id) + ->first(); if ($old_recurring) { $old_recurring->service()->stop()->save(); diff --git a/app/Http/Controllers/SelfUpdateController.php b/app/Http/Controllers/SelfUpdateController.php index fcd642c284ae..aeb28723c143 100644 --- a/app/Http/Controllers/SelfUpdateController.php +++ b/app/Http/Controllers/SelfUpdateController.php @@ -12,7 +12,6 @@ namespace App\Http\Controllers; use App\Exceptions\FilePermissionsFailure; -use App\Models\Client; use App\Utils\Ninja; use App\Utils\Traits\AppSetup; use App\Utils\Traits\ClientGroupSettingsSaver; @@ -26,6 +25,10 @@ class SelfUpdateController extends BaseController use ClientGroupSettingsSaver; use AppSetup; + // private bool $use_zip = false; + + private string $filename = 'invoiceninja.tar'; + private array $purge_file_list = [ 'bootstrap/cache/compiled.php', 'bootstrap/cache/config.php', @@ -39,35 +42,6 @@ class SelfUpdateController extends BaseController { } - /** - * @OA\Post( - * path="/api/v1/self-update", - * operationId="selfUpdate", - * tags={"update"}, - * summary="Performs a system update", - * description="Performs a system update", - * @OA\Parameter(ref="#/components/parameters/X-API-TOKEN"), - * @OA\Parameter(ref="#/components/parameters/X-API-PASSWORD"), - * @OA\Parameter(ref="#/components/parameters/X-Requested-With"), - * @OA\Parameter(ref="#/components/parameters/include"), - * @OA\Response( - * response=200, - * description="Success/failure response" - * ), - * @OA\Response( - * response=422, - * description="Validation error", - * @OA\JsonContent(ref="#/components/schemas/ValidationError"), - * - * ), - * @OA\Response( - * response="default", - * description="Unexpected Error", - * @OA\JsonContent(ref="#/components/schemas/Error"), - * ), - * ) - */ - public function update() { set_time_limit(0); @@ -87,7 +61,8 @@ class SelfUpdateController extends BaseController nlog('copying release file'); - if (copy($this->getDownloadUrl(), storage_path('app/invoiceninja.zip'))) { + // if (copy($this->getDownloadUrl(), storage_path('app/invoiceninja.zip'))) { + if (copy($this->getDownloadUrl(), storage_path("app/{$this->filename}"))) { nlog('Copied file from URL'); } else { return response()->json(['message' => 'Download not yet available. Please try again shortly.'], 410); @@ -95,27 +70,21 @@ class SelfUpdateController extends BaseController nlog('Finished copying'); - $file = Storage::disk('local')->path('invoiceninja.zip'); + // if($this->use_zip) { + $file = Storage::disk('local')->path($this->filename); - nlog('Extracting zip'); + nlog('Extracting tar'); - $zipFile = new \PhpZip\ZipFile(); + $phar = new \PharData($file); + $phar->extractTo(base_path(), null, true); - $zipFile->openFile($file); + nlog('Finished extracting files'); - $zipFile->deleteFromName(".htaccess"); - - $zipFile->rewrite(); - - $zipFile->extractTo(base_path()); - - $zipFile->close(); - - $zipFile = null; - - nlog('Finished extracting files'); - - unlink($file); + unlink($file); + // } + // else { + // $this->extractUsingZip(); + // } nlog('Deleted release zip file'); @@ -141,40 +110,24 @@ class SelfUpdateController extends BaseController return response()->json(['message' => 'Update completed'], 200); } - private function deleteDirectory($dir) - { - if (! file_exists($dir)) { - return true; - } + // private function extractUsingZip() + // { - if (! is_dir($dir) || is_link($dir)) { - return unlink($dir); - } - foreach (scandir($dir) as $item) { - if ($item == '.' || $item == '..') { - continue; - } - if (! $this->deleteDirectory($dir.'/'.$item)) { - if (! $this->deleteDirectory($dir.'/'.$item)) { - return false; - } - } - } + // $file = Storage::disk('local')->path($this->filename); - return rmdir($dir); - } + // nlog('Extracting zip'); - private function postHookUpdate() - { - if (config('ninja.app_version') == '5.3.82') { - Client::withTrashed()->cursor()->each(function ($client) { - $entity_settings = $this->checkSettingType($client->settings); - $entity_settings->md5 = md5(time()); - $client->settings = $entity_settings; - $client->save(); - }); - } - } + // $zipFile = new \PhpZip\ZipFile(); + // $zipFile->openFile($file); + // $zipFile->deleteFromName(".htaccess"); + // $zipFile->rewrite(); + // $zipFile->extractTo(base_path()); + // $zipFile->close(); + // $zipFile = null; + + // unlink($file); + + // } private function clearCacheDir() { @@ -197,10 +150,10 @@ class SelfUpdateController extends BaseController } if ($file->isFile() && ! $file->isWritable()) { + nlog("Cannot update system because {$file->getFileName()} is not writable"); throw new FilePermissionsFailure("Cannot update system because {$file->getFileName()} is not writable"); - return false; } } @@ -218,6 +171,10 @@ class SelfUpdateController extends BaseController { $version = $this->checkVersion(); - return "https://github.com/invoiceninja/invoiceninja/releases/download/v{$version}/invoiceninja.zip"; + // if(request()->has('zip')) + // return "https://github.com/invoiceninja/invoiceninja/releases/download/v{$version}/invoiceninja.zip"; + + return "https://github.com/invoiceninja/invoiceninja/releases/download/v{$version}/invoiceninja.tar"; + } } diff --git a/app/Import/Definitions/BankTransactionMap.php b/app/Import/Definitions/BankTransactionMap.php index b6482926ee8c..fb30efd891f0 100644 --- a/app/Import/Definitions/BankTransactionMap.php +++ b/app/Import/Definitions/BankTransactionMap.php @@ -1,10 +1,10 @@ transform($raw_invoice); - + $invoice_data['user_id'] = $this->company->owner()->id; + $invoice_data['line_items'] = $this->cleanItems( $invoice_data['line_items'] ?? [] ); diff --git a/app/Import/Transformer/Bank/BankTransformer.php b/app/Import/Transformer/Bank/BankTransformer.php index bff66a30e895..d9377edc09de 100644 --- a/app/Import/Transformer/Bank/BankTransformer.php +++ b/app/Import/Transformer/Bank/BankTransformer.php @@ -1,10 +1,10 @@ account_sms_verified = false; } - // $sp794f3f->trial_started = now(); - // $sp794f3f->trial_plan = 'pro'; } $sp794f3f->save(); diff --git a/app/Jobs/Cron/AutoBillCron.php b/app/Jobs/Cron/AutoBillCron.php index 8879c6930776..f0e4343964b1 100644 --- a/app/Jobs/Cron/AutoBillCron.php +++ b/app/Jobs/Cron/AutoBillCron.php @@ -11,10 +11,11 @@ namespace App\Jobs\Cron; -use App\Libraries\MultiDB; use App\Models\Invoice; -use Illuminate\Foundation\Bus\Dispatchable; +use App\Libraries\MultiDB; use Illuminate\Support\Carbon; +use Illuminate\Support\Facades\Auth; +use Illuminate\Foundation\Bus\Dispatchable; class AutoBillCron { @@ -45,6 +46,8 @@ class AutoBillCron /* Get all invoices where the send date is less than NOW + 30 minutes() */ info('Performing Autobilling '.Carbon::now()->format('Y-m-d h:i:s')); + Auth::logout(); + if (! config('ninja.db.multi_db_enabled')) { $auto_bill_partial_invoices = Invoice::whereDate('partial_due_date', '<=', now()) ->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL]) diff --git a/app/Jobs/Cron/RecurringExpensesCron.php b/app/Jobs/Cron/RecurringExpensesCron.php index 5b7fb2c33cae..476f57e7ba81 100644 --- a/app/Jobs/Cron/RecurringExpensesCron.php +++ b/app/Jobs/Cron/RecurringExpensesCron.php @@ -11,13 +11,14 @@ namespace App\Jobs\Cron; -use App\Factory\RecurringExpenseToExpenseFactory; use App\Libraries\MultiDB; +use Illuminate\Support\Carbon; use App\Models\RecurringExpense; use App\Models\RecurringInvoice; +use Illuminate\Support\Facades\Auth; use App\Utils\Traits\GeneratesCounter; use Illuminate\Foundation\Bus\Dispatchable; -use Illuminate\Support\Carbon; +use App\Factory\RecurringExpenseToExpenseFactory; class RecurringExpensesCron { @@ -45,6 +46,8 @@ class RecurringExpensesCron /* Get all expenses where the send date is less than NOW + 30 minutes() */ nlog('Sending recurring expenses '.Carbon::now()->format('Y-m-d h:i:s')); + Auth::logout(); + if (! config('ninja.db.multi_db_enabled')) { $recurring_expenses = RecurringExpense::where('next_send_date', '<=', now()->toDateTimeString()) ->whereNotNull('next_send_date') diff --git a/app/Jobs/Cron/RecurringInvoicesCron.php b/app/Jobs/Cron/RecurringInvoicesCron.php index 69bf17a41fb4..2e1e53ee218f 100644 --- a/app/Jobs/Cron/RecurringInvoicesCron.php +++ b/app/Jobs/Cron/RecurringInvoicesCron.php @@ -11,12 +11,13 @@ namespace App\Jobs\Cron; -use App\Jobs\RecurringInvoice\SendRecurring; -use App\Libraries\MultiDB; use App\Models\Invoice; -use App\Models\RecurringInvoice; -use Illuminate\Foundation\Bus\Dispatchable; +use App\Libraries\MultiDB; use Illuminate\Support\Carbon; +use App\Models\RecurringInvoice; +use Illuminate\Support\Facades\Auth; +use Illuminate\Foundation\Bus\Dispatchable; +use App\Jobs\RecurringInvoice\SendRecurring; class RecurringInvoicesCron { @@ -43,6 +44,8 @@ class RecurringInvoicesCron /* Get all invoices where the send date is less than NOW + 30 minutes() */ $start = Carbon::now()->format('Y-m-d h:i:s'); nlog('Sending recurring invoices '.$start); + + Auth::logout(); if (! config('ninja.db.multi_db_enabled')) { $recurring_invoices = RecurringInvoice::where('status_id', RecurringInvoice::STATUS_ACTIVE) diff --git a/app/Jobs/Cron/SubscriptionCron.php b/app/Jobs/Cron/SubscriptionCron.php index 8fc2412bf630..c77a62769521 100644 --- a/app/Jobs/Cron/SubscriptionCron.php +++ b/app/Jobs/Cron/SubscriptionCron.php @@ -11,8 +11,9 @@ namespace App\Jobs\Cron; -use App\Libraries\MultiDB; use App\Models\Invoice; +use App\Libraries\MultiDB; +use Illuminate\Support\Facades\Auth; use App\Utils\Traits\SubscriptionHooker; use Illuminate\Foundation\Bus\Dispatchable; @@ -39,6 +40,8 @@ class SubscriptionCron { nlog('Subscription Cron'); + Auth::logout(); + if (! config('ninja.db.multi_db_enabled')) { $invoices = Invoice::where('is_deleted', 0) ->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL]) diff --git a/app/Jobs/Cron/UpdateCalculatedFields.php b/app/Jobs/Cron/UpdateCalculatedFields.php index ba2ab9717c57..18e8c3f500f6 100644 --- a/app/Jobs/Cron/UpdateCalculatedFields.php +++ b/app/Jobs/Cron/UpdateCalculatedFields.php @@ -13,6 +13,7 @@ namespace App\Jobs\Cron; use App\Models\Project; use App\Libraries\MultiDB; +use Illuminate\Support\Facades\Auth; use Illuminate\Foundation\Bus\Dispatchable; class UpdateCalculatedFields @@ -37,6 +38,8 @@ class UpdateCalculatedFields { nlog("Updating calculated fields"); + Auth::logout(); + if (! config('ninja.db.multi_db_enabled')) { Project::with('tasks')->where('updated_at', '>', now()->subHours(2)) diff --git a/app/Jobs/Ninja/TaskScheduler.php b/app/Jobs/Ninja/TaskScheduler.php index 46a3fb27b04b..307ffb07bf18 100644 --- a/app/Jobs/Ninja/TaskScheduler.php +++ b/app/Jobs/Ninja/TaskScheduler.php @@ -11,13 +11,14 @@ namespace App\Jobs\Ninja; -use App\Libraries\MultiDB; use App\Models\Scheduler; +use App\Libraries\MultiDB; use Illuminate\Bus\Queueable; +use Illuminate\Support\Facades\Auth; +use Illuminate\Queue\SerializesModels; +use Illuminate\Queue\InteractsWithQueue; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; -use Illuminate\Queue\InteractsWithQueue; -use Illuminate\Queue\SerializesModels; //@rebuild it class TaskScheduler implements ShouldQueue @@ -42,6 +43,8 @@ class TaskScheduler implements ShouldQueue */ public function handle() { + Auth::logout(); + if (! config('ninja.db.multi_db_enabled')) { Scheduler::with('company') ->where('is_paused', false) diff --git a/app/Jobs/Subscription/CleanStaleInvoiceOrder.php b/app/Jobs/Subscription/CleanStaleInvoiceOrder.php index 7d8e6c517353..f872b467f636 100644 --- a/app/Jobs/Subscription/CleanStaleInvoiceOrder.php +++ b/app/Jobs/Subscription/CleanStaleInvoiceOrder.php @@ -19,6 +19,7 @@ use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; +use Illuminate\Support\Facades\Auth; class CleanStaleInvoiceOrder implements ShouldQueue { @@ -39,6 +40,8 @@ class CleanStaleInvoiceOrder implements ShouldQueue { nlog("Cleaning Stale Invoices:"); + Auth::logout(); + if (! config('ninja.db.multi_db_enabled')) { Invoice::query() ->withTrashed() diff --git a/app/Jobs/Util/ReminderJob.php b/app/Jobs/Util/ReminderJob.php index 08053467d8e7..76a048675b9d 100644 --- a/app/Jobs/Util/ReminderJob.php +++ b/app/Jobs/Util/ReminderJob.php @@ -11,23 +11,22 @@ namespace App\Jobs\Util; +use App\Utils\Ninja; +use App\Models\Invoice; +use App\Libraries\MultiDB; +use Illuminate\Bus\Queueable; +use Illuminate\Support\Carbon; use App\DataMapper\InvoiceItem; use App\Factory\InvoiceFactory; use App\Jobs\Entity\EmailEntity; -use App\Jobs\Ninja\TransactionLog; -use App\Libraries\MultiDB; -use App\Models\Invoice; -use App\Models\TransactionEvent; -use App\Utils\Ninja; use App\Utils\Traits\MakesDates; +use Illuminate\Support\Facades\App; use App\Utils\Traits\MakesReminders; -use Illuminate\Bus\Queueable; +use Illuminate\Support\Facades\Auth; +use Illuminate\Queue\SerializesModels; +use Illuminate\Queue\InteractsWithQueue; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; -use Illuminate\Queue\InteractsWithQueue; -use Illuminate\Queue\SerializesModels; -use Illuminate\Support\Carbon; -use Illuminate\Support\Facades\App; class ReminderJob implements ShouldQueue { @@ -53,6 +52,8 @@ class ReminderJob implements ShouldQueue { set_time_limit(0); + Auth::logout(); + if (! config('ninja.db.multi_db_enabled')) { nlog("Sending invoice reminders on ".now()->format('Y-m-d h:i:s')); diff --git a/app/Libraries/MultiDB.php b/app/Libraries/MultiDB.php index 8b9f2a4d5764..2b6274345d3e 100644 --- a/app/Libraries/MultiDB.php +++ b/app/Libraries/MultiDB.php @@ -310,7 +310,24 @@ class MultiDB self::setDB($current_db); - return false; + return null; + } + + public static function findAndSetDbByShopifyName($shopify_name) :?Company + { + $current_db = config('database.default'); + + foreach (self::$dbs as $db) { + if ($company = Company::on($db)->with('tokens')->where('shopify_name', $shopify_name)->first()) { + self::setDb($db); + + return $company; + } + } + + self::setDB($current_db); + + return null; } public static function findAndSetDbByAccountKey($account_key) :bool @@ -413,7 +430,7 @@ class MultiDB self::setDB($current_db); - return false; + return null; } public static function findAndSetDbByDomain($query_array) diff --git a/app/Mail/Client/ClientStatement.php b/app/Mail/Client/ClientStatement.php index ec34f05d77c0..b715840083ed 100644 --- a/app/Mail/Client/ClientStatement.php +++ b/app/Mail/Client/ClientStatement.php @@ -57,7 +57,6 @@ class ClientStatement extends Mailable with: [ 'text_body' => $this->data['body'], 'body' => $this->data['body'], - 'whitelabel' => $this->data['whitelabel'], 'settings' => $this->data['settings'], 'whitelabel' => $this->data['whitelabel'], 'logo' => $this->data['logo'], diff --git a/app/Models/User.php b/app/Models/User.php index 9fd05311598c..aa578900d31d 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -203,6 +203,7 @@ class User extends Authenticatable implements MustVerifyEmail 'custom_value3', 'custom_value4', 'is_deleted', + 'shopify_user_id', // 'oauth_user_token', // 'oauth_user_refresh_token', ]; diff --git a/app/PaymentDrivers/Braintree/ACH.php b/app/PaymentDrivers/Braintree/ACH.php index 518698f02df6..da8e220d3a65 100644 --- a/app/PaymentDrivers/Braintree/ACH.php +++ b/app/PaymentDrivers/Braintree/ACH.php @@ -39,8 +39,15 @@ class ACH implements MethodInterface public function authorizeView(array $data) { - $data['gateway'] = $this->braintree; - $data['client_token'] = $this->braintree->gateway->clientToken()->generate(); + try { + $data['gateway'] = $this->braintree; + $data['client_token'] = $this->braintree->gateway->clientToken()->generate(); + } + catch(\Exception $e){ + + throw new PaymentFailed("Unable to generate client token, check your Braintree credentials. Error: " . $e->getMessage(), 500); + + } return render('gateways.braintree.ach.authorize', $data); } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 6271eed87fe0..b28db7336dc4 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -119,6 +119,7 @@ class AppServiceProvider extends ServiceProvider ParallelTesting::setUpTestDatabase(function ($database, $token) { Artisan::call('db:seed'); }); + } public function register(): void diff --git a/app/Services/Client/ClientService.php b/app/Services/Client/ClientService.php index b8a5a8a6279c..5167c4af0938 100644 --- a/app/Services/Client/ClientService.php +++ b/app/Services/Client/ClientService.php @@ -16,7 +16,6 @@ use App\Models\Credit; use App\Models\Payment; use App\Services\Email\Email; use App\Services\Email\EmailObject; -use App\Services\Email\EmailService; use App\Utils\Number; use App\Utils\Traits\MakesDates; use Illuminate\Mail\Mailables\Address; @@ -167,9 +166,6 @@ class ClientService { $this->client_start_date = $this->translateDate($options['start_date'], $this->client->date_format(), $this->client->locale()); $this->client_end_date = $this->translateDate($options['end_date'], $this->client->date_format(), $this->client->locale()); - - // $email_service = new EmailService($this->buildStatementMailableData($pdf), $this->client->company); - // $email_service->send(); $email_object = $this->buildStatementMailableData($pdf); Email::dispatch($email_object, $this->client->company); diff --git a/app/Services/Client/Statement.php b/app/Services/Client/Statement.php index db1fcb0cd899..e315b5e7d464 100644 --- a/app/Services/Client/Statement.php +++ b/app/Services/Client/Statement.php @@ -245,17 +245,17 @@ class Statement switch ($status) { case 'all': return [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL, Invoice::STATUS_PAID]; - break; + case 'paid': return [Invoice::STATUS_PAID]; - break; + case 'unpaid': return [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL]; - break; + default: return [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL, Invoice::STATUS_PAID]; - break; + } } diff --git a/app/Services/Email/EmailDefaults.php b/app/Services/Email/EmailDefaults.php index a1b8816b8066..79f1612915bf 100644 --- a/app/Services/Email/EmailDefaults.php +++ b/app/Services/Email/EmailDefaults.php @@ -23,13 +23,11 @@ use App\Jobs\Invoice\CreateUbl; use App\Utils\Traits\MakesHash; use App\Jobs\Entity\CreateRawPdf; use Illuminate\Support\Facades\App; -use App\Jobs\Invoice\CreateEInvoice; use Illuminate\Mail\Mailables\Address; use Illuminate\Support\Facades\Storage; use App\DataMapper\EmailTemplateDefaults; use League\CommonMark\CommonMarkConverter; use App\Jobs\Vendor\CreatePurchaseOrderPdf; -use App\Services\Invoice\GetInvoiceXInvoice; class EmailDefaults { diff --git a/app/Services/Email/EmailMailer.php b/app/Services/Email/EmailMailer.php deleted file mode 100644 index a3e52bc49aed..000000000000 --- a/app/Services/Email/EmailMailer.php +++ /dev/null @@ -1,502 +0,0 @@ -email_service->company->db); - - /* Perform final checks */ - if ($this->email_service->preFlightChecksFail()) { - return; - } - - /* Boot the required driver*/ - $this->setMailDriver(); - - /* Init the mailer*/ - $mailer = Mail::mailer($this->mailer); - - /* Additional configuration if using a client third party mailer */ - if ($this->client_postmark_secret) { - $mailer->postmark_config($this->client_postmark_secret); - } - - if ($this->client_mailgun_secret) { - $mailer->mailgun_config($this->client_mailgun_secret, $this->client_mailgun_domain); - } - - /* Attempt the send! */ - try { - nlog("Using mailer => ". $this->mailer. " ". now()->toDateTimeString()); - - $mailer->send($this->email_mailable); - - Cache::increment("email_quota".$this->email_service->company->account->key); - - LightLogs::create(new EmailSuccess($this->email_service->company->company_key)) - ->send(); - } catch (\Exception | \RuntimeException | \Google\Service\Exception $e) { - nlog("Mailer failed with {$e->getMessage()}"); - - $message = $e->getMessage(); - - /** - * Post mark buries the proper message in a a guzzle response - * this merges a text string with a json object - * need to harvest the ->Message property using the following - */ - if ($e instanceof ClientException) { //postmark specific failure - $response = $e->getResponse(); - $message_body = json_decode($response->getBody()->getContents()); - - if ($message_body && property_exists($message_body, 'Message')) { - $message = $message_body->Message; - nlog($message); - } - - $this->fail(); - $this->cleanUpMailers(); - return; - } - - //only report once, not on all tries - if ($this->attempts() == $this->tries) { - /* If the is an entity attached to the message send a failure mailer */ - $this->entityEmailFailed($message); - - /* Don't send postmark failures to Sentry */ - if (Ninja::isHosted() && (!$e instanceof ClientException)) { - app('sentry')->captureException($e); - } - } - - - /* Releasing immediately does not add in the backoff */ - sleep(rand(0, 3)); - - $this->release($this->backoff()[$this->attempts()-1]); - - $message = null; - } - - $this->cleanUpMailers(); - } - - /** - * Entity notification when an email fails to send - * - * @todo - rewrite this - * @param string $message - * @return void - */ - private function entityEmailFailed($message) - { - if (!$this->email_service->email_object->entity_id) { - return; - } - - switch ($this->email_service->email_object->entity_class) { - case Invoice::class: - $invitation = InvoiceInvitation::withTrashed()->find($this->email_service->email_object->entity_id); - if ($invitation) { - event(new InvoiceWasEmailedAndFailed($invitation, $this->email_service->company, $message, $this->email_service->email_object->reminder_template, Ninja::eventVars(auth()->user() ? auth()->user()->id : null))); - } - break; - case Payment::class: - $payment = Payment::withTrashed()->find($this->email_service->email_object->entity_id); - if ($payment) { - event(new PaymentWasEmailedAndFailed($payment, $this->email_service->company, $message, Ninja::eventVars(auth()->user() ? auth()->user()->id : null))); - } - break; - default: - # code... - break; - } - - if ($this->email_service->email_object->client_contact instanceof ClientContact) { - $this->logMailError($message, $this->email_service->email_object->client_contact); - } - } - - /** - * Sets the mail driver to use and applies any specific configuration - * the the mailable - */ - private function setMailDriver(): self - { - switch ($this->email_service->email_object->settings->email_sending_method) { - case 'default': - $this->mailer = config('mail.default'); - break; - case 'gmail': - $this->mailer = 'gmail'; - $this->setGmailMailer(); - return $this; - case 'office365': - $this->mailer = 'office365'; - $this->setOfficeMailer(); - return $this; - case 'client_postmark': - $this->mailer = 'postmark'; - $this->setPostmarkMailer(); - return $this; - case 'client_mailgun': - $this->mailer = 'mailgun'; - $this->setMailgunMailer(); - return $this; - - default: - break; - } - - if (Ninja::isSelfHost()) { - $this->setSelfHostMultiMailer(); - } - - return $this; - } - - /** - * Allows configuration of multiple mailers - * per company for use by self hosted users - */ - private function setSelfHostMultiMailer(): void - { - if (env($this->email_service->company->id . '_MAIL_HOST')) { - config([ - 'mail.mailers.smtp' => [ - 'transport' => 'smtp', - 'host' => env($this->email_service->company->id . '_MAIL_HOST'), - 'port' => env($this->email_service->company->id . '_MAIL_PORT'), - 'username' => env($this->email_service->company->id . '_MAIL_USERNAME'), - 'password' => env($this->email_service->company->id . '_MAIL_PASSWORD'), - ], - ]); - - if (env($this->email_service->company->id . '_MAIL_FROM_ADDRESS')) { - $this->email_mailable - ->from(env($this->email_service->company->id . '_MAIL_FROM_ADDRESS', env('MAIL_FROM_ADDRESS')), env($this->email_service->company->id . '_MAIL_FROM_NAME', env('MAIL_FROM_NAME'))); - } - } - } - - - /** - * Ensure we discard any data that is not required - * - * @return void - */ - private function cleanUpMailers(): void - { - $this->client_postmark_secret = false; - - $this->client_mailgun_secret = false; - - $this->client_mailgun_domain = false; - - //always dump the drivers to prevent reuse - app('mail.manager')->forgetMailers(); - } - - /** - * Check to ensure no cross account - * emails can be sent. - * - * @param User $user - */ - private function checkValidSendingUser($user) - { - /* Always ensure the user is set on the correct account */ - if ($user->account_id != $this->email_service->company->account_id) { - $this->email_service->email_object->settings->email_sending_method = 'default'; - - return $this->setMailDriver(); - } - } - - /** - * Resolves the sending user - * when configuring the Mailer - * on behalf of the client - * - * @return User $user - */ - private function resolveSendingUser(): ?User - { - $sending_user = $this->email_service->email_object->settings->gmail_sending_user_id; - - if ($sending_user == "0") { - $user = $this->email_service->company->owner(); - } else { - $user = User::find($this->decodePrimaryKey($sending_user)); - } - - return $user; - } - /** - * Configures Mailgun using client supplied secret - * as the Mailer - */ - private function setMailgunMailer() - { - if (strlen($this->email_service->email_object->settings->mailgun_secret) > 2 && strlen($this->email_service->email_object->settings->mailgun_domain) > 2) { - $this->client_mailgun_secret = $this->email_service->email_object->settings->mailgun_secret; - $this->client_mailgun_domain = $this->email_service->email_object->settings->mailgun_domain; - } else { - $this->email_service->email_object->settings->email_sending_method = 'default'; - return $this->setMailDriver(); - } - - $user = $this->resolveSendingUser(); - - $sending_email = (isset($this->email_service->email_object->settings->custom_sending_email) && stripos($this->email_service->email_object->settings->custom_sending_email, "@")) ? $this->email_service->email_object->settings->custom_sending_email : $user->email; - $sending_user = (isset($this->email_service->email_object->settings->email_from_name) && strlen($this->email_service->email_object->settings->email_from_name) > 2) ? $this->email_service->email_object->settings->email_from_name : $user->name(); - - $this->email_mailable - ->from($sending_email, $sending_user); - } - - /** - * Configures Postmark using client supplied secret - * as the Mailer - */ - private function setPostmarkMailer() - { - if (strlen($this->email_service->email_object->settings->postmark_secret) > 2) { - $this->client_postmark_secret = $this->email_service->email_object->settings->postmark_secret; - } else { - $this->email_service->email_object->settings->email_sending_method = 'default'; - return $this->setMailDriver(); - } - - $user = $this->resolveSendingUser(); - - $sending_email = (isset($this->email_service->email_object->settings->custom_sending_email) && stripos($this->email_service->email_object->settings->custom_sending_email, "@")) ? $this->email_service->email_object->settings->custom_sending_email : $user->email; - $sending_user = (isset($this->email_service->email_object->settings->email_from_name) && strlen($this->email_service->email_object->settings->email_from_name) > 2) ? $this->email_service->email_object->settings->email_from_name : $user->name(); - - $this->email_mailable - ->from($sending_email, $sending_user); - } - - /** - * Configures Microsoft via Oauth - * as the Mailer - */ - private function setOfficeMailer() - { - $user = $this->resolveSendingUser(); - - $this->checkValidSendingUser($user); - - nlog("Sending via {$user->name()}"); - - $token = $this->refreshOfficeToken($user); - - if ($token) { - $user->oauth_user_token = $token; - $user->save(); - } else { - $this->email_service->email_object->settings->email_sending_method = 'default'; - return $this->setMailDriver(); - } - - $this->email_mailable - ->from($user->email, $user->name()) - ->withSymfonyMessage(function ($message) use ($token) { - $message->getHeaders()->addTextHeader('gmailtoken', $token); - }); - } - - /** - * Configures GMail via Oauth - * as the Mailer - */ - private function setGmailMailer() - { - $user = $this->resolveSendingUser(); - - $this->checkValidSendingUser($user); - - nlog("Sending via {$user->name()}"); - - $google = (new Google())->init(); - - try { - if ($google->getClient()->isAccessTokenExpired()) { - $google->refreshToken($user); - $user = $user->fresh(); - } - - $google->getClient()->setAccessToken(json_encode($user->oauth_user_token)); - } catch(\Exception $e) { - $this->logMailError('Gmail Token Invalid', $this->email_service->company->clients()->first()); - $this->email_service->email_object->settings->email_sending_method = 'default'; - return $this->setMailDriver(); - } - - /** - * If the user doesn't have a valid token, notify them - */ - - if (!$user->oauth_user_token) { - $this->email_service->company->account->gmailCredentialNotification(); - $this->email_service->email_object->settings->email_sending_method = 'default'; - return $this->setMailDriver(); - } - - /* - * Now that our token is refreshed and valid we can boot the - * mail driver at runtime and also set the token which will persist - * just for this request. - */ - - $token = $user->oauth_user_token->access_token; - - if (!$token) { - $this->email_service->company->account->gmailCredentialNotification(); - $this->email_service->email_object->settings->email_sending_method = 'default'; - return $this->setMailDriver(); - } - - $this->email_mailable - ->from($user->email, $user->name()) - ->withSymfonyMessage(function ($message) use ($token) { - $message->getHeaders()->addTextHeader('gmailtoken', $token); - }); - } - - /** - * Logs any errors to the SystemLog - * - * @param string $errors - * @param App\Models\User | App\Models\Client $recipient_object - * @return void - */ - private function logMailError($errors, $recipient_object) :void - { - (new SystemLogger( - $errors, - SystemLog::CATEGORY_MAIL, - SystemLog::EVENT_MAIL_SEND, - SystemLog::TYPE_FAILURE, - $recipient_object, - $this->email_service->company - ))->handle(); - - $job_failure = new EmailFailure($this->email_service->company->company_key); - $job_failure->string_metric5 = 'failed_email'; - $job_failure->string_metric6 = substr($errors, 0, 150); - - LightLogs::create($job_failure) - ->send(); - - $job_failure = null; - } - - /** - * Attempts to refresh the Microsoft refreshToken - * - * @param App\Models\User - * @return mixed - */ - private function refreshOfficeToken(User $user): mixed - { - $expiry = $user->oauth_user_token_expiry ?: now()->subDay(); - - if ($expiry->lt(now())) { - $guzzle = new \GuzzleHttp\Client(); - $url = 'https://login.microsoftonline.com/common/oauth2/v2.0/token'; - - $token = json_decode($guzzle->post($url, [ - 'form_params' => [ - 'client_id' => config('ninja.o365.client_id') , - 'client_secret' => config('ninja.o365.client_secret') , - 'scope' => 'email Mail.Send offline_access profile User.Read openid', - 'grant_type' => 'refresh_token', - 'refresh_token' => $user->oauth_user_refresh_token - ], - ])->getBody()->getContents()); - - if ($token) { - $user->oauth_user_refresh_token = property_exists($token, 'refresh_token') ? $token->refresh_token : $user->oauth_user_refresh_token; - $user->oauth_user_token = $token->access_token; - $user->oauth_user_token_expiry = now()->addSeconds($token->expires_in); - $user->save(); - - return $token->access_token; - } - - return false; - } - - return $user->oauth_user_token; - } - - public function failed($exception = null) - { - } -} diff --git a/app/Services/Email/EmailService.php b/app/Services/Email/EmailService.php deleted file mode 100644 index 731815deaefc..000000000000 --- a/app/Services/Email/EmailService.php +++ /dev/null @@ -1,163 +0,0 @@ -override = $override; - - $this->setDefaults() - ->updateMailable() - ->email(); - } - - public function sendNow($force = false) :void - { - $this->setDefaults() - ->updateMailable() - ->email($force); - } - - private function email($force = false): void - { - if ($force) { - (new EmailMailer($this, $this->mailable))->handle(); - } else { - EmailMailer::dispatch($this, $this->mailable)->delay(2); - } - } - - private function setDefaults(): self - { - $defaults = new EmailDefaults($this, $this->email_object); - $defaults->run(); - - return $this; - } - - private function updateMailable() - { - $this->mailable = new EmailMailable($this->email_object); - - return $this; - } - - /** - * On the hosted platform we scan all outbound email for - * spam. This sequence processes the filters we use on all - * emails. - * - * @return bool - */ - public function preFlightChecksFail(): bool - { - /* If we are migrating data we don't want to fire any emails */ - if ($this->company->is_disabled && !$this->override) { - return true; - } - - if (Ninja::isSelfHost()) { - return false; - } - - /* To handle spam users we drop all emails from flagged accounts */ - if ($this->company->account && $this->company->account->is_flagged) { - return true; - } - - /* On the hosted platform we set default contacts a @example.com email address - we shouldn't send emails to these types of addresses */ - if ($this->hasInValidEmails()) { - return true; - } - - /* GMail users are uncapped */ - if (in_array($this->email_object->settings->email_sending_method, ['gmail', 'office365', 'client_postmark', 'client_mailgun'])) { - return false; - } - - /* On the hosted platform, if the user is over the email quotas, we do not send the email. */ - if ($this->company->account && $this->company->account->emailQuotaExceeded()) { - return true; - } - - /* If the account is verified, we allow emails to flow */ - if ($this->company->account && $this->company->account->is_verified_account) { - //11-01-2022 - - /* Continue to analyse verified accounts in case they later start sending poor quality emails*/ - // if(class_exists(\Modules\Admin\Jobs\Account\EmailQuality::class)) - // (new \Modules\Admin\Jobs\Account\EmailQuality($this->nmo, $this->company))->run(); - - return false; - } - - /* On the hosted platform if the user has not verified their account we fail here - but still check what they are trying to send! */ - if ($this->company->account && !$this->company->account->account_sms_verified) { - if (class_exists(\Modules\Admin\Jobs\Account\EmailFilter::class)) { - return (new \Modules\Admin\Jobs\Account\EmailFilter($this->email_object, $this->company))->run(); - } - - return true; - } - - /* On the hosted platform we actively scan all outbound emails to ensure outbound email quality remains high */ - if (class_exists(\Modules\Admin\Jobs\Account\EmailFilter::class)) { - return (new \Modules\Admin\Jobs\Account\EmailFilter($this->email_object, $this->company))->run(); - } - - return false; - } - - private function hasInValidEmails(): bool - { - foreach ($this->email_object->to as $address_object) { - if (strpos($address_object->address, '@example.com') !== false) { - return true; - } - - if (!str_contains($address_object->address, "@")) { - return true; - } - - if ($address_object->address == " ") { - return true; - } - } - - - return false; - } -} diff --git a/composer.json b/composer.json index df88a3d8e6e9..adf2e9cbe051 100644 --- a/composer.json +++ b/composer.json @@ -81,6 +81,7 @@ "sentry/sentry-laravel": "^3", "setasign/fpdf": "^1.8", "setasign/fpdi": "^2.3", + "shopify/shopify-api": "^4.3", "socialiteproviders/apple": "^5.2", "socialiteproviders/microsoft": "^4.1", "sprain/swiss-qr-bill": "^3.2", diff --git a/composer.lock b/composer.lock index 442dcfbcd1b6..71488b238373 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "8c21eb3ea2c2baeecb223d5fdbc8423c", + "content-hash": "c5abc776f4fd59ba436de99f6c401429", "packages": [ { "name": "adrienrn/php-mimetyper", @@ -9342,6 +9342,69 @@ ], "time": "2023-02-09T10:38:43+00:00" }, + { + "name": "shopify/shopify-api", + "version": "v4.3.0", + "source": { + "type": "git", + "url": "https://github.com/Shopify/shopify-api-php.git", + "reference": "80cde593a69acb9b9095235fa8f7748e9389294c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Shopify/shopify-api-php/zipball/80cde593a69acb9b9095235fa8f7748e9389294c", + "reference": "80cde593a69acb9b9095235fa8f7748e9389294c", + "shasum": "" + }, + "require": { + "doctrine/inflector": "^2.0", + "ext-json": "*", + "firebase/php-jwt": "^5.2 || ^6.2", + "guzzlehttp/guzzle": "^7.0", + "php": "^7.4 || ^8.0 || ^8.1", + "psr/http-client": "^1.0", + "psr/log": "^1.1 || ^2.0 || ^3.0", + "ramsey/uuid": "^4.1" + }, + "require-dev": { + "mikey179/vfsstream": "^1.6", + "phpunit/phpunit": "^9", + "squizlabs/php_codesniffer": "^3.6" + }, + "type": "library", + "autoload": { + "psr-4": { + "Shopify\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Shopify Inc.", + "email": "dev-tools-education@shopify.com" + } + ], + "description": "Shopify API Library for PHP", + "keywords": [ + "Storefront API", + "admin api", + "app", + "graphql", + "jwt", + "node", + "rest", + "shopify", + "webhook" + ], + "support": { + "issues": "https://github.com/Shopify/shopify-api-php/issues", + "source": "https://github.com/Shopify/shopify-api-php/tree/v4.3.0" + }, + "time": "2023-04-12T15:42:26+00:00" + }, { "name": "smalot/pdfparser", "version": "v0.19.0", diff --git a/config/ninja.php b/config/ninja.php index 3ca23f4d4e78..ffb043ad8f62 100644 --- a/config/ninja.php +++ b/config/ninja.php @@ -15,8 +15,8 @@ return [ 'require_https' => env('REQUIRE_HTTPS', true), 'app_url' => rtrim(env('APP_URL', ''), '/'), 'app_domain' => env('APP_DOMAIN', 'invoicing.co'), - 'app_version' => '5.5.113', - 'app_tag' => '5.5.113', + 'app_version' => '5.5.114', + 'app_tag' => '5.5.114', 'minimum_client_version' => '5.0.16', 'terms_version' => '1.0.1', 'api_secret' => env('API_SECRET', ''), @@ -215,4 +215,8 @@ return [ ], 'licenses' => env('LICENSES',false), 'google_application_credentials' => env("GOOGLE_APPLICATION_CREDENTIALS", false), + 'shopify' => [ + 'client_id' => env('SHOPIFY_CLIENT_ID', null), + 'client_secret' => env('SHOPIFY_CLIENT_SECRET', null), + ], ]; diff --git a/database/migrations/2023_05_03_023956_add_shopify_user_id.php b/database/migrations/2023_05_03_023956_add_shopify_user_id.php new file mode 100644 index 000000000000..77ef774d233f --- /dev/null +++ b/database/migrations/2023_05_03_023956_add_shopify_user_id.php @@ -0,0 +1,50 @@ +cursor()->each(function ($company) { + + $settings = $company->settings; + + if(!property_exists($settings, 'enable_e_invoice')) { + + $company->saveSettings((array)$company->settings, $company); + + } + + }); + + Schema::table('users', function (Illuminate\Database\Schema\Blueprint $table) { + $table->unsignedBigInteger('shopify_user_id')->index()->nullable(); + }); + + Schema::table('companies', function(Illuminate\Database\Schema\Blueprint $table){ + $table->string('shopify_name')->index()->nullable(); + $table->string('shopify_access_token')->index()->nullable(); + }); + + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + // + } +}; diff --git a/lang/en/texts.php b/lang/en/texts.php index 7499b0386f21..03f5170af4d7 100644 --- a/lang/en/texts.php +++ b/lang/en/texts.php @@ -5068,6 +5068,18 @@ $LANG = array( 'tax_exempt' => 'Tax Exempt', 'late_fee_added_locked_invoice' => 'Late fee for invoice :invoice added on :date', 'lang_Khmer' => 'Khmer', + 'routing_id' => 'Routing ID', + 'enable_e_invoice' => 'Enable E-Invoice', + 'e_invoice_type' => 'E-Invoice Type', + 'reduced_tax' => 'Reduced Tax', + 'override_tax' => 'Override Tax', + 'zero_rated' => 'Zero Rated', + 'reverse_tax' => 'Reverse Tax', + 'updated_tax_category' => 'Successfully updated the tax category', + 'updated_tax_categories' => 'Successfully updated the tax categories', + 'set_tax_category' => 'Set Tax Category', + 'payment_manual' => 'Payment Manual', + 'expense_payment_type' => 'Expense Payment Type', ); diff --git a/resources/views/portal/ninja2020/payments/show.blade.php b/resources/views/portal/ninja2020/payments/show.blade.php index 91f68b79dbbc..a0604b92efe8 100644 --- a/resources/views/portal/ninja2020/payments/show.blade.php +++ b/resources/views/portal/ninja2020/payments/show.blade.php @@ -123,7 +123,7 @@
@foreach($payment->invoices as $invoice) - @if(!$invoice->is_proforma) + @if(!$invoice->is_proforma && !$invoice->is_deleted)
{{ ctrans('texts.invoice_number') }} diff --git a/tests/Feature/Email/EmailServiceTest.php b/tests/Feature/Email/EmailServiceTest.php deleted file mode 100644 index 17efec114d43..000000000000 --- a/tests/Feature/Email/EmailServiceTest.php +++ /dev/null @@ -1,164 +0,0 @@ -markTestSkipped('Skipped :: test not needed in this environment'); - } - - $this->makeTestData(); - - $this->email_object = new EmailObject(); - $this->email_object->to = [new Address("testing@gmail.com", "Cool Name")]; - $this->email_object->attachments = []; - $this->email_object->settings = $this->client->getMergedSettings(); - $this->email_object->company = $this->client->company; - $this->email_object->client = $this->client; - $this->email_object->email_template_subject = 'email_subject_statement'; - $this->email_object->email_template_body = 'email_template_statement'; - $this->email_object->variables = [ - '$client' => $this->client->present()->name(), - '$start_date' => '2022-01-01', - '$end_date' => '2023-01-01', - ]; - - $this->email_service = new EmailService($this->email_object, $this->company); - } - - public function testScanEmailsAttemptedFromVerifiedAccounts() - { - $email_filter = new \Modules\Admin\Jobs\Account\EmailFilter($this->email_object, $this->client->company); - - Cache::put($this->account->key, 1); - - config(['ninja.environment' => 'hosted']); - - $this->account->account_sms_verified = true; - $this->account->is_verified_account = false; - $this->account->save(); - - $this->assertFalse($this->email_service->preFlightChecksFail()); - - collect($email_filter->getSpamKeywords())->each(function ($spam_subject) { - $this->email_object->subject = $spam_subject; - - $this->assertTrue($this->email_service->preFlightChecksFail()); - }); - } - - - - public function scanEmailsAttemptedFromUnverifiedAccounts() - { - config(['ninja.environment' => 'hosted']); - - Cache::put($this->account->key, 1); - - $this->account->account_sms_verified = false; - $this->account->save(); - - $this->assertTrue($this->email_service->preFlightChecksFail()); - } - - - public function testVerifiedAccountsSkipFilters() - { - config(['ninja.environment' => 'hosted']); - - Cache::put($this->account->key, 1); - - $this->account->is_verified_account = true; - $this->account->save(); - - $this->assertFalse($this->email_service->preFlightChecksFail()); - } - - public function testFlaggedInvalidEmailsPrevented() - { - config(['ninja.environment' => 'hosted']); - - Cache::put($this->account->key, 1); - - $this->email_object->to = [new Address("user@example.com", "Cool Name")]; - - $this->assertTrue($this->email_service->preFlightChecksFail()); - - - collect([ - 'user@example.com', - '', - 'bademail', - 'domain.com', - ])->each(function ($email) { - $this->email_object->to = [new Address($email, "Cool Name")]; - - $this->assertTrue($this->email_service->preFlightChecksFail()); - }); - } - - public function testFlaggedAccountsPrevented() - { - Cache::put($this->account->key, 1); - - config(['ninja.environment' => 'hosted']); - - $this->account->is_flagged = true; - $this->account->save(); - - $this->assertTrue($this->email_service->preFlightChecksFail()); - } - - public function testPreFlightChecksHosted() - { - Cache::put($this->account->key, 1); - - config(['ninja.environment' => 'hosted']); - - $this->assertFalse($this->email_service->preFlightChecksFail()); - } - - public function testPreFlightChecksSelfHost() - { - Cache::put($this->account->key, 1); - - config(['ninja.environment' => 'selfhost']); - - $this->assertFalse($this->email_service->preFlightChecksFail()); - } -} diff --git a/tests/Feature/Export/ProductSalesReportTest.php b/tests/Feature/Export/ProductSalesReportTest.php index 8684533f7a35..f530acb3f3ec 100644 --- a/tests/Feature/Export/ProductSalesReportTest.php +++ b/tests/Feature/Export/ProductSalesReportTest.php @@ -20,6 +20,7 @@ use App\Models\Company; use App\Models\Expense; use App\Models\Invoice; use App\Models\User; +use App\Utils\Traits\AppSetup; use App\Utils\Traits\MakesHash; use Illuminate\Routing\Middleware\ThrottleRequests; use Tests\TestCase; @@ -31,6 +32,7 @@ use Tests\TestCase; class ProductSalesReportTest extends TestCase { use MakesHash; + use AppSetup; public $faker; @@ -45,6 +47,8 @@ class ProductSalesReportTest extends TestCase ); $this->withoutExceptionHandling(); + + $this->buildCache(true); } public $company; @@ -132,7 +136,6 @@ class ProductSalesReportTest extends TestCase { $this->buildData(); - $this->payload = [ 'start_date' => '2000-01-01', 'end_date' => '2030-01-11', diff --git a/tests/Feature/InvoiceTest.php b/tests/Feature/InvoiceTest.php index 60f5db3ae254..8730566b12b0 100644 --- a/tests/Feature/InvoiceTest.php +++ b/tests/Feature/InvoiceTest.php @@ -33,6 +33,8 @@ class InvoiceTest extends TestCase use DatabaseTransactions; use MockAccountData; + public $faker; + protected function setUp() :void { parent::setUp(); @@ -47,6 +49,24 @@ class InvoiceTest extends TestCase } + public function testInvoiceGetPaidReversedInvoice() + { + $this->invoice->service()->handleReversal()->save(); + + $this->assertEquals(6, $this->invoice->fresh()->status_id); + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->get('/api/v1/invoices?status_id=6', ) + ->assertStatus(200); + + $arr = $response->json(); + + $this->assertCount(1, $arr['data']); + } + + public function testInvoiceGetPaidInvoices() { $response = $this->withHeaders([ diff --git a/tests/Feature/PdfCreatorTest.php b/tests/Feature/PdfCreatorTest.php index 0dd2927ada35..0eba272e660a 100644 --- a/tests/Feature/PdfCreatorTest.php +++ b/tests/Feature/PdfCreatorTest.php @@ -35,6 +35,10 @@ class PdfCreatorTest extends TestCase $this->withoutMiddleware( ThrottleRequests::class ); + + if(config('ninja.testvars.travis')) + $this->markTestSkipped(); + } // public function testCreditPdfCreated() diff --git a/tests/Pdf/PdfServiceTest.php b/tests/Pdf/PdfServiceTest.php index 4bba1ff7b01d..625783cef598 100644 --- a/tests/Pdf/PdfServiceTest.php +++ b/tests/Pdf/PdfServiceTest.php @@ -36,6 +36,9 @@ class PdfServiceTest extends TestCase public function testPdfGeneration() { + if(config('ninja.testvars.travis')) + $this->markTestSkipped(); + $invitation = $this->invoice->invitations->first(); $service = (new PdfService($invitation))->boot();