diff --git a/VERSION.txt b/VERSION.txt index 4cc0e35cb30c..566ac6388b64 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -5.6.0 \ No newline at end of file +5.6.1 \ No newline at end of file diff --git a/app/Console/Commands/CheckData.php b/app/Console/Commands/CheckData.php index 5e6a18f1c9c4..9822ec1347f7 100644 --- a/app/Console/Commands/CheckData.php +++ b/app/Console/Commands/CheckData.php @@ -435,11 +435,38 @@ class CheckData extends Command private function checkEntityInvitations() { + RecurringInvoiceInvitation::where('deleted_at', "0000-00-00 00:00:00.000000")->withTrashed()->update(['deleted_at' => null]); InvoiceInvitation::where('deleted_at', "0000-00-00 00:00:00.000000")->withTrashed()->update(['deleted_at' => null]); QuoteInvitation::where('deleted_at', "0000-00-00 00:00:00.000000")->withTrashed()->update(['deleted_at' => null]); CreditInvitation::where('deleted_at', "0000-00-00 00:00:00.000000")->withTrashed()->update(['deleted_at' => null]); + InvoiceInvitation::where('sent_date', '0000-00-00 00:00:00')->cursor()->each(function ($ii){ + $ii->sent_date = null; + $ii->saveQuietly(); + }); + InvoiceInvitation::where('viewed_date', '0000-00-00 00:00:00')->cursor()->each(function ($ii) { + $ii->viewed_date = null; + $ii->saveQuietly(); + }); + + QuoteInvitation::where('sent_date', '0000-00-00 00:00:00')->cursor()->each(function ($ii) { + $ii->sent_date = null; + $ii->saveQuietly(); + }); + QuoteInvitation::where('viewed_date', '0000-00-00 00:00:00')->cursor()->each(function ($ii) { + $ii->viewed_date = null; + $ii->saveQuietly(); + }); + + CreditInvitation::where('sent_date', '0000-00-00 00:00:00')->cursor()->each(function ($ii) { + $ii->sent_date = null; + $ii->saveQuietly(); + }); + CreditInvitation::where('viewed_date', '0000-00-00 00:00:00')->cursor()->each(function ($ii) { + $ii->viewed_date = null; + $ii->saveQuietly(); + }); collect([Invoice::class, Quote::class, Credit::class, PurchaseOrder::class])->each(function ($entity) { if ($entity::doesntHave('invitations')->count() > 0) { diff --git a/app/Filters/PaymentFilters.php b/app/Filters/PaymentFilters.php index 58ab78f23687..236eeba519c0 100644 --- a/app/Filters/PaymentFilters.php +++ b/app/Filters/PaymentFilters.php @@ -102,8 +102,13 @@ class PaymentFilters extends QueryFilters if (count($payment_filters) >0) { $query->whereIn('status_id', $payment_filters); } + + if(in_array('partially_unapplied', $status_parameters)) { + $query->where('amount', '>', 'applied')->where('refunded', 0); + } }); + return $this->builder; } diff --git a/app/Jobs/Cron/UpdateCalculatedFields.php b/app/Jobs/Cron/UpdateCalculatedFields.php index 18e8c3f500f6..dcfdb6d48e90 100644 --- a/app/Jobs/Cron/UpdateCalculatedFields.php +++ b/app/Jobs/Cron/UpdateCalculatedFields.php @@ -11,6 +11,7 @@ namespace App\Jobs\Cron; +use App\Models\Payment; use App\Models\Project; use App\Libraries\MultiDB; use Illuminate\Support\Facades\Auth; @@ -65,7 +66,6 @@ class UpdateCalculatedFields $project->save(); }); - } } } diff --git a/app/Models/Account.php b/app/Models/Account.php index ab58b329738e..4e18792f8a62 100644 --- a/app/Models/Account.php +++ b/app/Models/Account.php @@ -621,7 +621,7 @@ class Account extends BaseModel public function getTrialDays() { - if ($this->payment_id) { + if ($this->payment_id || $this->is_migrated) { return 0; } diff --git a/app/Models/BaseModel.php b/app/Models/BaseModel.php index 8f1dfb3e19c6..c6234bf12728 100644 --- a/app/Models/BaseModel.php +++ b/app/Models/BaseModel.php @@ -32,6 +32,7 @@ use Illuminate\Support\Str; * @property \App\Models\Company $company * @method static \Illuminate\Database\Eloquent\Builder|BaseModel|Illuminate\Database\Eloquent\Relations\BelongsTo|\Awobaz\Compoships\Database\Eloquent\Relations\BelongsTo|\App\Models\Company company() * @method static \Illuminate\Database\Eloquent\Builder|BaseModel exclude($columns) + * @method static \Illuminate\Database\Eloquent\Builder|BaseModel with() * @method static \Illuminate\Database\Eloquent\Builder|BaseModel newModelQuery() * @method static \Illuminate\Database\Eloquent\Builder|BaseModel newQuery() * @method static \Illuminate\Database\Eloquent\Builder|BaseModel query() diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php index 9bcdc1f4682a..3a7364eda1ff 100644 --- a/app/Providers/RouteServiceProvider.php +++ b/app/Providers/RouteServiceProvider.php @@ -86,6 +86,10 @@ class RouteServiceProvider extends ServiceProvider } }); + RateLimiter::for('honeypot', function (Request $request) { + return Limit::perMinute(2)->by($request->ip()); + }); + } /** diff --git a/app/Services/Client/ClientService.php b/app/Services/Client/ClientService.php index d2fc9795099f..6492bdde5f89 100644 --- a/app/Services/Client/ClientService.php +++ b/app/Services/Client/ClientService.php @@ -80,7 +80,7 @@ class ClientService $amount = Payment::where('client_id', $this->client->id) ->where('is_deleted', 0) ->whereIn('status_id', [Payment::STATUS_COMPLETED, Payment::STATUS_PENDING, Payment::STATUS_PARTIALLY_REFUNDED, Payment::STATUS_REFUNDED]) - ->sum(DB::Raw('amount - refunded - applied')); + ->sum(DB::Raw('amount - applied')); DB::connection(config('database.default'))->transaction(function () use ($amount) { $this->client = Client::withTrashed()->where('id', $this->client->id)->lockForUpdate()->first(); diff --git a/app/Services/Client/Statement.php b/app/Services/Client/Statement.php index e315b5e7d464..63406638a343 100644 --- a/app/Services/Client/Statement.php +++ b/app/Services/Client/Statement.php @@ -371,28 +371,28 @@ class Statement return $ranges; case '60': - $ranges[0] = now()->startOfDay()->subDays(30); + $ranges[0] = now()->startOfDay()->subDays(31); $ranges[1] = now()->startOfDay()->subDays(60); return $ranges; case '90': - $ranges[0] = now()->startOfDay()->subDays(60); + $ranges[0] = now()->startOfDay()->subDays(61); $ranges[1] = now()->startOfDay()->subDays(90); return $ranges; case '120': - $ranges[0] = now()->startOfDay()->subDays(90); + $ranges[0] = now()->startOfDay()->subDays(91); $ranges[1] = now()->startOfDay()->subDays(120); return $ranges; case '120+': - $ranges[0] = now()->startOfDay()->subDays(120); + $ranges[0] = now()->startOfDay()->subDays(121); $ranges[1] = now()->startOfDay()->subYears(20); return $ranges; default: - $ranges[0] = now()->startOfDay()->subDays(0); - $ranges[1] = now()->subDays(30); + $ranges[0] = now()->startOfDay(); + $ranges[1] = now()->startOfDay()->subDays(30); return $ranges; } diff --git a/app/Services/Credit/CreditService.php b/app/Services/Credit/CreditService.php index d9051df17224..4adc71cd290c 100644 --- a/app/Services/Credit/CreditService.php +++ b/app/Services/Credit/CreditService.php @@ -130,13 +130,6 @@ class CreditService ->credits() ->attach($this->credit->id, ['amount' => $adjustment]); - //reduce client paid_to_date by $this->credit->balance amount - // $this->credit - // ->client - // ->service() - // ->updatePaidToDate($adjustment) - // ->save(); - $client = $this->credit->client->fresh(); $client->service() ->updatePaidToDate($adjustment) diff --git a/app/Services/Report/ARSummaryReport.php b/app/Services/Report/ARSummaryReport.php index b561d0fbf538..ebbb891cfee7 100644 --- a/app/Services/Report/ARSummaryReport.php +++ b/app/Services/Report/ARSummaryReport.php @@ -123,7 +123,7 @@ class ARSummaryReport extends BaseExport ->where('balance', '>', 0) ->where('is_deleted', 0) ->where(function ($query){ - $query->where('due_date', '<', now()->startOfDay()) + $query->where('due_date', '>', now()->startOfDay()) ->orWhereNull('due_date'); }) ->sum('balance'); @@ -173,22 +173,22 @@ class ARSummaryReport extends BaseExport return $ranges; case '60': - $ranges[0] = now()->startOfDay()->subDays(30); + $ranges[0] = now()->startOfDay()->subDays(31); $ranges[1] = now()->startOfDay()->subDays(60); return $ranges; case '90': - $ranges[0] = now()->startOfDay()->subDays(60); + $ranges[0] = now()->startOfDay()->subDays(61); $ranges[1] = now()->startOfDay()->subDays(90); return $ranges; case '120': - $ranges[0] = now()->startOfDay()->subDays(90); + $ranges[0] = now()->startOfDay()->subDays(91); $ranges[1] = now()->startOfDay()->subDays(120); return $ranges; case '120+': - $ranges[0] = now()->startOfDay()->subDays(120); + $ranges[0] = now()->startOfDay()->subDays(121); $ranges[1] = now()->startOfDay()->subYears(20); return $ranges; diff --git a/app/Transformers/TaskTransformer.php b/app/Transformers/TaskTransformer.php index 71b6d7c3436b..eb077d2e1d87 100644 --- a/app/Transformers/TaskTransformer.php +++ b/app/Transformers/TaskTransformer.php @@ -12,6 +12,7 @@ namespace App\Transformers; use App\Models\Document; +use App\Models\Project; use App\Models\Task; use App\Models\TaskStatus; use App\Utils\Traits\MakesHash; @@ -33,7 +34,8 @@ class TaskTransformer extends EntityTransformer */ protected $availableIncludes = [ 'client', - 'status' + 'status', + 'project', ]; public function includeDocuments(Task $task) @@ -65,6 +67,16 @@ class TaskTransformer extends EntityTransformer return $this->includeItem($task->status, $transformer, TaskStatus::class); } + public function includeProject(Task $task): ?Item + { + $transformer = new ProjectTransformer($this->serializer); + + if (!$task->project) { + return null; + } + + return $this->includeItem($task->project, $transformer, Project::class); + } public function transform(Task $task) { diff --git a/config/ninja.php b/config/ninja.php index 3fc09c390ee0..211655b46137 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.6.0', - 'app_tag' => '5.6.0', + 'app_version' => '5.6.1', + 'app_tag' => '5.6.1', 'minimum_client_version' => '5.0.16', 'terms_version' => '1.0.1', 'api_secret' => env('API_SECRET', ''), diff --git a/routes/api.php b/routes/api.php index a9999cd0be18..c1e24993bfcc 100644 --- a/routes/api.php +++ b/routes/api.php @@ -283,6 +283,7 @@ Route::group(['middleware' => ['throttle:api', 'api_db', 'token_auth', 'locale'] Route::post('reports/clients', ClientReportController::class); Route::post('reports/activities', ActivityReportController::class); + Route::post('reports/client_contacts', ClientContactReportController::class); Route::post('reports/contacts', ClientContactReportController::class); Route::post('reports/credits', CreditReportController::class); Route::post('reports/documents', DocumentReportController::class); diff --git a/routes/client.php b/routes/client.php index ecb1dd11226f..1be22a18ca7f 100644 --- a/routes/client.php +++ b/routes/client.php @@ -141,6 +141,9 @@ Route::group(['middleware' => ['invite_db'], 'prefix' => 'client', 'as' => 'clie Route::get('phantom/{entity}/{invitation_key}', [Phantom::class, 'displayInvitation'])->middleware(['invite_db', 'phantom_secret'])->name('phantom_view'); +Route::get('.env', function () { +})->middleware('throttle:honeypot'); + Route::fallback(function () { if (Ninja::isSelfHost() && Account::first()?->set_react_as_default_ap) { diff --git a/tests/Feature/TaskApiTest.php b/tests/Feature/TaskApiTest.php index 2f0d54a4c7da..236f2fa66291 100644 --- a/tests/Feature/TaskApiTest.php +++ b/tests/Feature/TaskApiTest.php @@ -11,14 +11,15 @@ namespace Tests\Feature; +use Tests\TestCase; use App\Models\Task; +use App\Models\Project; +use Tests\MockAccountData; use App\Utils\Traits\MakesHash; use Illuminate\Database\Eloquent\Model; -use Illuminate\Foundation\Testing\DatabaseTransactions; use Illuminate\Support\Facades\Session; use Illuminate\Validation\ValidationException; -use Tests\MockAccountData; -use Tests\TestCase; +use Illuminate\Foundation\Testing\DatabaseTransactions; /** * @test @@ -358,6 +359,34 @@ class TaskApiTest extends TestCase $this->assertTrue($this->checkTimeLog($log)); } + public function testTaskListWithProjects() + { + + $project = Project::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'client_id' => $this->client->id, + 'name' => 'proggy', + ]); + + $data = [ + 'project_id' => $this->encodePrimaryKey($project->id), + 'timelog' => [[1,2,'a'],[3,4,'d']], + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/tasks?include=project', $data); + + $response->assertStatus(200); + + $arr = $response->json(); + + $this->assertEquals('proggy', $arr['data']['project']['name']); + + } + public function testTaskListClientStatus() { $response = $this->withHeaders([