diff --git a/Modules/ReportBuilder/Database/Migrations/2025_10_26_create_report_templates_table.php b/Modules/ReportBuilder/Database/Migrations/2025_10_26_create_report_templates_table.php new file mode 100644 index 00000000..b259d8a6 --- /dev/null +++ b/Modules/ReportBuilder/Database/Migrations/2025_10_26_create_report_templates_table.php @@ -0,0 +1,30 @@ +id(); + $table->unsignedBigInteger('company_id'); + $table->string('name'); + $table->string('slug'); + $table->text('description')->nullable(); + $table->string('template_type'); + $table->boolean('is_system')->default(false); + $table->boolean('is_active')->default(true); + $table->timestamps(); + + $table->foreign('company_id')->references('id')->on('companies')->onDelete('cascade'); + $table->unique(['company_id', 'slug']); + }); + } + + public function down(): void + { + Schema::dropIfExists('report_templates'); + } +}; diff --git a/Modules/ReportBuilder/Models/ReportTemplate.php b/Modules/ReportBuilder/Models/ReportTemplate.php new file mode 100644 index 00000000..2ba5a5a9 --- /dev/null +++ b/Modules/ReportBuilder/Models/ReportTemplate.php @@ -0,0 +1,61 @@ + 'boolean', + 'is_active' => 'boolean', + 'template_type' => 'string', + ]; + + /** + * Check if the template can be cloned. + */ + public function isCloneable(): bool + { + return $this->is_active; + } + + /** + * Check if the template is a system template. + */ + public function isSystem(): bool + { + return $this->is_system; + } + + /** + * Get the file path for the template. + */ + public function getFilePath(): string + { + return "{$this->company_id}/{$this->slug}.json"; + } +} diff --git a/Modules/ReportBuilder/Tests/Unit/ReportTemplateTest.php b/Modules/ReportBuilder/Tests/Unit/ReportTemplateTest.php new file mode 100644 index 00000000..29075081 --- /dev/null +++ b/Modules/ReportBuilder/Tests/Unit/ReportTemplateTest.php @@ -0,0 +1,222 @@ +company = Company::factory()->create(['name' => 'Test Company']); + } + + #[Test] + #[Group('unit')] + public function it_can_create_a_report_template(): void + { + $template = ReportTemplate::create([ + 'company_id' => $this->company->id, + 'name' => 'Professional Invoice', + 'slug' => 'professional_invoice', + 'description' => 'A professional invoice template', + 'template_type' => 'invoice', + 'is_system' => false, + 'is_active' => true, + ]); + + $this->assertDatabaseHas('report_templates', [ + 'company_id' => $this->company->id, + 'name' => 'Professional Invoice', + 'slug' => 'professional_invoice', + 'template_type' => 'invoice', + ]); + + $this->assertEquals('Professional Invoice', $template->name); + $this->assertEquals('professional_invoice', $template->slug); + $this->assertFalse($template->is_system); + $this->assertTrue($template->is_active); + } + + #[Test] + #[Group('unit')] + public function it_casts_boolean_fields_correctly(): void + { + $template = ReportTemplate::create([ + 'company_id' => $this->company->id, + 'name' => 'System Template', + 'slug' => 'system_template', + 'template_type' => 'report', + 'is_system' => 1, + 'is_active' => 0, + ]); + + $this->assertTrue($template->is_system); + $this->assertFalse($template->is_active); + $this->assertIsBool($template->is_system); + $this->assertIsBool($template->is_active); + } + + #[Test] + #[Group('unit')] + public function it_belongs_to_a_company(): void + { + $template = ReportTemplate::create([ + 'company_id' => $this->company->id, + 'name' => 'Test Template', + 'slug' => 'test_template', + 'template_type' => 'invoice', + 'is_system' => false, + 'is_active' => true, + ]); + + $this->assertInstanceOf(Company::class, $template->company); + $this->assertEquals($this->company->id, $template->company->id); + } + + #[Test] + #[Group('unit')] + public function is_cloneable_returns_true_when_active(): void + { + $template = ReportTemplate::create([ + 'company_id' => $this->company->id, + 'name' => 'Active Template', + 'slug' => 'active_template', + 'template_type' => 'invoice', + 'is_system' => false, + 'is_active' => true, + ]); + + $this->assertTrue($template->isCloneable()); + } + + #[Test] + #[Group('unit')] + public function is_cloneable_returns_false_when_inactive(): void + { + $template = ReportTemplate::create([ + 'company_id' => $this->company->id, + 'name' => 'Inactive Template', + 'slug' => 'inactive_template', + 'template_type' => 'invoice', + 'is_system' => false, + 'is_active' => false, + ]); + + $this->assertFalse($template->isCloneable()); + } + + #[Test] + #[Group('unit')] + public function is_system_returns_true_for_system_templates(): void + { + $template = ReportTemplate::create([ + 'company_id' => $this->company->id, + 'name' => 'System Template', + 'slug' => 'system_template', + 'template_type' => 'invoice', + 'is_system' => true, + 'is_active' => true, + ]); + + $this->assertTrue($template->isSystem()); + } + + #[Test] + #[Group('unit')] + public function is_system_returns_false_for_user_templates(): void + { + $template = ReportTemplate::create([ + 'company_id' => $this->company->id, + 'name' => 'User Template', + 'slug' => 'user_template', + 'template_type' => 'invoice', + 'is_system' => false, + 'is_active' => true, + ]); + + $this->assertFalse($template->isSystem()); + } + + #[Test] + #[Group('unit')] + public function get_file_path_returns_correct_path(): void + { + $template = ReportTemplate::create([ + 'company_id' => $this->company->id, + 'name' => 'Test Template', + 'slug' => 'test_template', + 'template_type' => 'invoice', + 'is_system' => false, + 'is_active' => true, + ]); + + $expectedPath = "{$this->company->id}/test_template.json"; + $this->assertEquals($expectedPath, $template->getFilePath()); + } + + #[Test] + #[Group('unit')] + public function slug_must_be_unique_within_company(): void + { + ReportTemplate::create([ + 'company_id' => $this->company->id, + 'name' => 'Template 1', + 'slug' => 'unique_slug', + 'template_type' => 'invoice', + 'is_system' => false, + 'is_active' => true, + ]); + + $this->expectException(\Illuminate\Database\QueryException::class); + + ReportTemplate::create([ + 'company_id' => $this->company->id, + 'name' => 'Template 2', + 'slug' => 'unique_slug', + 'template_type' => 'invoice', + 'is_system' => false, + 'is_active' => true, + ]); + } + + #[Test] + #[Group('unit')] + public function same_slug_can_exist_in_different_companies(): void + { + $company2 = Company::factory()->create(['name' => 'Company 2']); + + $template1 = ReportTemplate::create([ + 'company_id' => $this->company->id, + 'name' => 'Template 1', + 'slug' => 'shared_slug', + 'template_type' => 'invoice', + 'is_system' => false, + 'is_active' => true, + ]); + + $template2 = ReportTemplate::create([ + 'company_id' => $company2->id, + 'name' => 'Template 2', + 'slug' => 'shared_slug', + 'template_type' => 'invoice', + 'is_system' => false, + 'is_active' => true, + ]); + + $this->assertEquals('shared_slug', $template1->slug); + $this->assertEquals('shared_slug', $template2->slug); + $this->assertNotEquals($template1->company_id, $template2->company_id); + } +}