tenant = Tenant::factory()->create(); $this->user = User::factory()->create(['tenant_id' => $this->tenant->id]); $this->actingAs($this->user); }); function makeReminder(int $tenantId, int $userId, array $overrides = []): Reminder { return Reminder::create(array_merge([ 'tenant_id' => $tenantId, 'deal_id' => 42, 'text' => 'Перезвонить клиенту', 'remind_at' => Carbon::now()->addHour(), 'created_by' => $userId, 'is_sent' => false, ], $overrides)); } test('GET /api/reminders: 401 без auth', function () { auth()->logout(); $this->getJson('/api/reminders')->assertStatus(401); }); test('GET /api/reminders: пустой', function () { $response = $this->getJson('/api/reminders'); $response->assertOk(); expect($response->json('items'))->toBe([]); expect($response->json('counts.active'))->toBe(0); }); test('GET /api/reminders: возвращает только свои', function () { $otherTenant = Tenant::factory()->create(); $otherUser = User::factory()->create(['tenant_id' => $otherTenant->id]); makeReminder($this->tenant->id, $this->user->id); makeReminder($otherTenant->id, $otherUser->id); $response = $this->getJson('/api/reminders'); $response->assertOk(); expect($response->json('items'))->toHaveCount(1); }); test('GET /api/reminders?filter=overdue: возвращает только просроченные', function () { makeReminder($this->tenant->id, $this->user->id, ['remind_at' => Carbon::now()->subDays(2)]); // overdue makeReminder($this->tenant->id, $this->user->id, ['remind_at' => Carbon::now()->addHour()]); // today makeReminder($this->tenant->id, $this->user->id, ['remind_at' => Carbon::now()->addDays(5)]); // upcoming $response = $this->getJson('/api/reminders?filter=overdue'); expect($response->json('items'))->toHaveCount(1); }); test('GET /api/reminders?filter=today: возвращает в пределах ±1д', function () { makeReminder($this->tenant->id, $this->user->id, ['remind_at' => Carbon::now()->addHours(3)]); makeReminder($this->tenant->id, $this->user->id, ['remind_at' => Carbon::now()->addDays(5)]); // upcoming $response = $this->getJson('/api/reminders?filter=today'); expect($response->json('items'))->toHaveCount(1); }); test('GET /api/reminders?filter=completed: возвращает выполненные', function () { makeReminder($this->tenant->id, $this->user->id, ['completed_at' => Carbon::now()]); makeReminder($this->tenant->id, $this->user->id, ['completed_at' => null]); $response = $this->getJson('/api/reminders?filter=completed'); expect($response->json('items'))->toHaveCount(1); }); test('GET /api/reminders: counts отдельно для каждого фильтра', function () { makeReminder($this->tenant->id, $this->user->id, ['remind_at' => Carbon::now()->subDays(2)]); makeReminder($this->tenant->id, $this->user->id, ['remind_at' => Carbon::now()->addHour()]); makeReminder($this->tenant->id, $this->user->id, ['remind_at' => Carbon::now()->addDays(3)]); $response = $this->getJson('/api/reminders'); expect($response->json('counts.active'))->toBe(3); expect($response->json('counts.overdue'))->toBe(1); expect($response->json('counts.today'))->toBe(1); expect($response->json('counts.upcoming'))->toBe(1); }); test('GET /api/reminders?deal_id=42: фильтр по сделке', function () { makeReminder($this->tenant->id, $this->user->id, ['deal_id' => 42]); makeReminder($this->tenant->id, $this->user->id, ['deal_id' => 43]); $response = $this->getJson('/api/reminders?deal_id=42'); expect($response->json('items'))->toHaveCount(1); expect($response->json('items.0.deal_id'))->toBe(42); }); test('POST /api/reminders: создаёт', function () { $response = $this->postJson('/api/reminders', [ 'deal_id' => 100, 'text' => 'Перезвонить через час', 'remind_at' => Carbon::now()->addHour()->toIso8601String(), ]); $response->assertStatus(201); expect($response->json('reminder.deal_id'))->toBe(100); expect($response->json('reminder.text'))->toBe('Перезвонить через час'); expect($response->json('reminder.is_sent'))->toBeFalse(); expect($response->json('reminder.created_by'))->toBe($this->user->id); expect(Reminder::query()->count())->toBe(1); }); test('POST /api/reminders: 422 без deal_id', function () { $this->postJson('/api/reminders', [ 'remind_at' => Carbon::now()->addHour()->toIso8601String(), ])->assertStatus(422); }); test('POST /api/reminders: 422 без remind_at', function () { $this->postJson('/api/reminders', [ 'deal_id' => 100, ])->assertStatus(422); }); test('POST /api/reminders: assignee_id чужого тенанта → 422', function () { $other = User::factory()->create(); // другой тенант $this->postJson('/api/reminders', [ 'deal_id' => 100, 'remind_at' => Carbon::now()->addHour()->toIso8601String(), 'assignee_id' => $other->id, ])->assertStatus(422); }); test('POST /api/reminders: assignee_id своего тенанта — ok', function () { $colleague = User::factory()->create(['tenant_id' => $this->tenant->id]); $response = $this->postJson('/api/reminders', [ 'deal_id' => 100, 'remind_at' => Carbon::now()->addHour()->toIso8601String(), 'assignee_id' => $colleague->id, ]); $response->assertStatus(201); expect($response->json('reminder.assignee_id'))->toBe($colleague->id); }); test('PATCH /api/reminders/{id}: обновляет text', function () { $reminder = makeReminder($this->tenant->id, $this->user->id); $response = $this->patchJson("/api/reminders/{$reminder->id}", ['text' => 'Новый текст']); $response->assertOk(); expect($response->json('reminder.text'))->toBe('Новый текст'); }); test('PATCH /api/reminders/{id}: смена remind_at сбрасывает is_sent', function () { $reminder = makeReminder($this->tenant->id, $this->user->id, [ 'is_sent' => true, 'sent_at' => Carbon::now()->subMinute(), ]); $response = $this->patchJson("/api/reminders/{$reminder->id}", [ 'remind_at' => Carbon::now()->addHours(3)->toIso8601String(), ]); $response->assertOk(); expect($response->json('reminder.is_sent'))->toBeFalse(); expect($response->json('reminder.sent_at'))->toBeNull(); }); test('PATCH /api/reminders/{id}: 404 для чужого', function () { $other = User::factory()->create(); $reminder = makeReminder($other->tenant_id, $other->id); $this->patchJson("/api/reminders/{$reminder->id}", ['text' => 'Hack'])->assertStatus(404); }); test('PATCH /api/reminders/{id}: 422 без полей', function () { $reminder = makeReminder($this->tenant->id, $this->user->id); $this->patchJson("/api/reminders/{$reminder->id}", [])->assertStatus(422); }); test('POST /api/reminders/{id}/complete: ставит completed_at', function () { $reminder = makeReminder($this->tenant->id, $this->user->id); $response = $this->postJson("/api/reminders/{$reminder->id}/complete"); $response->assertOk(); expect($response->json('reminder.completed_at'))->not->toBeNull(); }); test('POST /api/reminders/{id}/complete: idempotent (повторный NO-OP)', function () { $reminder = makeReminder($this->tenant->id, $this->user->id); $first = $this->postJson("/api/reminders/{$reminder->id}/complete"); $firstCompletedAt = $first->json('reminder.completed_at'); sleep(1); $second = $this->postJson("/api/reminders/{$reminder->id}/complete"); expect($second->json('reminder.completed_at'))->toBe($firstCompletedAt); // не изменилось }); test('DELETE /api/reminders/{id}: удаляет', function () { $reminder = makeReminder($this->tenant->id, $this->user->id); $this->deleteJson("/api/reminders/{$reminder->id}")->assertOk(); expect(Reminder::query()->find($reminder->id))->toBeNull(); }); test('DELETE /api/reminders/{id}: 404 для чужого', function () { $other = User::factory()->create(); $reminder = makeReminder($other->tenant_id, $other->id); $this->deleteJson("/api/reminders/{$reminder->id}")->assertStatus(404); expect(Reminder::query()->find($reminder->id))->not->toBeNull(); });