createMock(HandoverRepository::class); $productService = $productService ?? $this->createMock(SoftwareProductService::class); return new HandoverService($repository, $productService, $revisionService); } private function mockProductService(bool $exists = true, bool $hasSchema = true): SoftwareProductService { $mock = $this->createMock(SoftwareProductService::class); if (!$exists) { $mock->method('findByCode')->willReturn(null); } else { $mock->method('findByCode')->willReturn([ 'id' => 1, 'code' => 'PROD-A', 'name' => 'Product A', 'active' => 1, 'handover_protocol_schema' => $hasSchema ? self::VALID_SCHEMA_JSON : null, ]); } return $mock; } // ── Create tests ───────────────────────────────────────────────── public function testCreateHappyPath(): void { $repo = $this->createMock(HandoverRepository::class); $repo->expects($this->once())->method('insert')->willReturn(99); $service = $this->createService($repo, $this->mockProductService()); $result = $service->create(self::TENANT_ID, 'D10001', 'Acme Corp', 'PROD-A', self::USER_ID); $this->assertTrue($result['ok']); $this->assertSame(99, $result['id']); $this->assertEmpty($result['errors']); } public function testCreateWithInvalidProductCode(): void { $service = $this->createService(productService: $this->mockProductService(exists: false)); $result = $service->create(self::TENANT_ID, 'D10001', 'Acme Corp', 'INVALID', self::USER_ID); $this->assertFalse($result['ok']); $this->assertNull($result['id']); $this->assertArrayHasKey('product_code', $result['errors']); } public function testCreateWithProductWithoutSchema(): void { $service = $this->createService(productService: $this->mockProductService(hasSchema: false)); $result = $service->create(self::TENANT_ID, 'D10001', 'Acme Corp', 'PROD-A', self::USER_ID); $this->assertFalse($result['ok']); $this->assertNull($result['id']); $this->assertArrayHasKey('product_code', $result['errors']); } public function testCreateWithEmptyDebitorNo(): void { $service = $this->createService(); $result = $service->create(self::TENANT_ID, '', 'Acme Corp', 'PROD-A', self::USER_ID); $this->assertFalse($result['ok']); $this->assertArrayHasKey('debitor_no', $result['errors']); } // ── Update fields tests ────────────────────────────────────────── public function testUpdateFieldsHappyPath(): void { $repo = $this->createMock(HandoverRepository::class); $repo->method('findById')->willReturn([ 'id' => 1, 'status' => 'draft', 'schema_snapshot' => self::VALID_SCHEMA_JSON, ]); $repo->expects($this->once())->method('updateFieldValues')->willReturn(true); $service = $this->createService($repo); $result = $service->updateFields(self::TENANT_ID, 1, ['name' => 'Test', 'priority' => 'high'], self::USER_ID, HandoverService::PERMISSION_CREATE); $this->assertTrue($result['ok']); } public function testUpdateFieldsRejectsUnknownKeys(): void { $repo = $this->createMock(HandoverRepository::class); $repo->method('findById')->willReturn([ 'id' => 1, 'status' => 'draft', 'schema_snapshot' => self::VALID_SCHEMA_JSON, ]); $service = $this->createService($repo); $result = $service->updateFields(self::TENANT_ID, 1, ['unknown_field' => 'value'], self::USER_ID, HandoverService::PERMISSION_CREATE); $this->assertFalse($result['ok']); $this->assertArrayHasKey('unknown_field', $result['errors']); } // ── Status transition tests ────────────────────────────────────── public function testChangeStatusByCreateUser(): void { $repo = $this->createMock(HandoverRepository::class); $repo->method('findById')->willReturn(['id' => 1, 'status' => 'draft']); $repo->method('updateStatus')->willReturn(true); $service = $this->createService($repo); // Can set in_progress $result = $service->changeStatus(self::TENANT_ID, 1, 'in_progress', self::USER_ID, HandoverService::PERMISSION_CREATE); $this->assertTrue($result['ok']); } public function testChangeStatusCreateUserCannotArchive(): void { $repo = $this->createMock(HandoverRepository::class); $repo->method('findById')->willReturn(['id' => 1, 'status' => 'completed']); $service = $this->createService($repo); $result = $service->changeStatus(self::TENANT_ID, 1, 'archived', self::USER_ID, HandoverService::PERMISSION_CREATE); $this->assertFalse($result['ok']); $this->assertArrayHasKey('status', $result['errors']); } public function testChangeStatusManageUserCanArchive(): void { $repo = $this->createMock(HandoverRepository::class); $repo->method('findById')->willReturn(['id' => 1, 'status' => 'completed']); $repo->method('updateStatus')->willReturn(true); $service = $this->createService($repo); $result = $service->changeStatus(self::TENANT_ID, 1, 'archived', self::USER_ID, HandoverService::PERMISSION_MANAGE); $this->assertTrue($result['ok']); } // ── Edit permission tests ──────────────────────────────────────── public function testEditDeniedForCreateUserOnCompletedHandover(): void { $repo = $this->createMock(HandoverRepository::class); $repo->method('findById')->willReturn([ 'id' => 1, 'status' => 'completed', 'schema_snapshot' => self::VALID_SCHEMA_JSON, ]); $service = $this->createService($repo); $result = $service->updateFields(self::TENANT_ID, 1, ['name' => 'Test'], self::USER_ID, HandoverService::PERMISSION_CREATE); $this->assertFalse($result['ok']); $this->assertArrayHasKey('general', $result['errors']); } public function testEditAllowedForManageUserOnCompletedHandover(): void { $repo = $this->createMock(HandoverRepository::class); $repo->method('findById')->willReturn([ 'id' => 1, 'status' => 'completed', 'schema_snapshot' => self::VALID_SCHEMA_JSON, ]); $repo->expects($this->once())->method('updateFieldValues')->willReturn(true); $service = $this->createService($repo); $result = $service->updateFields(self::TENANT_ID, 1, ['name' => 'Test'], self::USER_ID, HandoverService::PERMISSION_MANAGE); $this->assertTrue($result['ok']); } // ── Revision integration tests ────────────────────────────────── public function testCreateCreatesInitialRevision(): void { $repo = $this->createMock(HandoverRepository::class); $repo->expects($this->once())->method('insert')->willReturn(99); $revisionService = $this->createMock(HandoverRevisionService::class); $revisionService->expects($this->once()) ->method('createRevision') ->with( self::TENANT_ID, 99, [], HandoverService::STATUS_DRAFT, 1, self::USER_ID, HandoverRevisionService::CHANGE_TYPE_INITIAL ); $service = $this->createService($repo, $this->mockProductService(), $revisionService); $result = $service->create(self::TENANT_ID, 'D10001', 'Acme Corp', 'PROD-A', self::USER_ID); $this->assertTrue($result['ok']); } public function testCreateRevisionAfterSaveDeterminesChangeType(): void { $repo = $this->createMock(HandoverRepository::class); $revisionService = $this->createMock(HandoverRevisionService::class); $revisionService->expects($this->once()) ->method('createRevision') ->with( self::TENANT_ID, 1, ['name' => 'Test'], 'in_progress', 1, self::USER_ID, HandoverRevisionService::CHANGE_TYPE_BOTH ); $service = $this->createService($repo, null, $revisionService); $service->createRevisionAfterSave(self::TENANT_ID, 1, ['name' => 'Test'], 'in_progress', 'draft', 1, self::USER_ID); } public function testCreateRevisionAfterSaveFieldsOnlyChangeType(): void { $repo = $this->createMock(HandoverRepository::class); $revisionService = $this->createMock(HandoverRevisionService::class); $revisionService->expects($this->once()) ->method('createRevision') ->with( $this->anything(), $this->anything(), $this->anything(), $this->anything(), $this->anything(), $this->anything(), HandoverRevisionService::CHANGE_TYPE_FIELDS ); $service = $this->createService($repo, null, $revisionService); $service->createRevisionAfterSave(self::TENANT_ID, 1, ['name' => 'Test'], 'draft', 'draft', 1, self::USER_ID); } public function testCreateRevisionForStatusChange(): void { $repo = $this->createMock(HandoverRepository::class); $repo->method('findById')->willReturn([ 'id' => 1, 'status' => 'in_progress', 'field_values' => '{"name":"Test"}', 'schema_snapshot' => self::VALID_SCHEMA_JSON, ]); $revisionService = $this->createMock(HandoverRevisionService::class); $revisionService->expects($this->once()) ->method('createRevision') ->with( self::TENANT_ID, 1, ['name' => 'Test'], 'completed', 1, self::USER_ID, HandoverRevisionService::CHANGE_TYPE_STATUS ); $service = $this->createService($repo, null, $revisionService); $service->createRevisionForStatusChange(self::TENANT_ID, 1, 'completed', 1, self::USER_ID); } }