Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:49:43 +08:00
commit 720714fdad
23 changed files with 4002 additions and 0 deletions

View File

@@ -0,0 +1,67 @@
---
name: backend-developer
description: Skill for PHP/Laravel backend development following project conventions. Use when creating or editing PHP code, models, services, controllers, tests, or any backend logic. Loads all backend rules from .claude/rules/backend/ and .claude/rules/dataclasses/.
---
# Backend Developer Skill
Use this skill when working with backend code to ensure project conventions are followed.
## Loading Conventions
**CRITICAL:** Before implementing any backend features, read ALL backend rules:
1. Use Glob to find all files: `.claude/rules/backend/*.md`
2. Read each file to load conventions
3. Also read: `.claude/rules/dataclasses/laravel-data.md`
These rules contain all patterns, conventions, and best practices for:
- Controller structure and responsibilities
- Data class creation and usage
- Database and model patterns
- PHP best practices
- Testing conventions
- And more...
## When to Use This Skill
Activate this skill when:
- Implementing backend features (models, services, controllers)
- Writing tests
- Refactoring backend code
- You need to verify backend patterns
- User asks to "follow backend conventions"
- You're in a different role but need backend context temporarily
## What This Skill Provides
After loading the rules, you have complete context for:
- When to create Data classes vs using arrays
- How to structure controllers and services
- Database and migration patterns
- Testing approaches and factory usage
- PHPDoc conventions
- Type safety patterns
## Integration with Other Skills
This skill works alongside project-specific skills:
- **`laravel-data-writer`**: For detailed Data class patterns
- **`data-objects`**: For DataObject CRUD operations
- **`object-definitions`**: For ObjectDefinition schema operations
- **`multi-tenancy`**: For tenant isolation patterns
- **`php-test-writer`**: For comprehensive test creation
## Key Principle
**Rules are the source of truth.** This skill simply loads them and provides context on when to apply them.
The rules define:
- WHAT the patterns are
- HOW to implement them
- WHAT to avoid
This skill provides:
- WHEN to use which patterns
- Context for applying rules in your current task

View File

@@ -0,0 +1,64 @@
---
name: documentation-writer
description: Skill for writing and updating codebase documentation. Use when creating or editing markdown documentation files in the docs/ directory, README files, or any documentation-related content. Also activates when maintaining the documentation index.
---
# Documentation Writer Skill
You are writing or updating project documentation. This skill ensures you follow project conventions and maintain consistency.
## When This Skill Activates
This skill activates when you are:
- Creating new documentation files in `docs/`
- Editing existing documentation in `docs/`
- Updating `docs/INDEX.md`
- Working on domain, feature, or layer documentation
- Updating documentation as part of code changes
## Load Documentation Conventions
**Before writing any documentation**, load the project's conventions:
```
Use Glob to find: .claude/rules/documentation/**/*.md
Read each file found
```
These files define:
- Documentation structure (domains, layers, features)
- File-to-doc mapping conventions
- Templates for different documentation types
- Writing style guidelines
- When to create documentation
- INDEX.md maintenance rules
## Follow the Conventions
All documentation practices are defined in `.claude/rules/documentation/`. Your job is to:
1. **Load the conventions first**
2. **Follow the structure** defined there (domains, layers, placement)
3. **Use the templates** provided for consistency
4. **Maintain INDEX.md** as specified in conventions
5. **Follow style guidelines** for clarity and completeness
## Critical Reminders
- **Always update `docs/INDEX.md`** when creating new documentation
- **Check INDEX.md first** before creating docs (might already exist)
- **Use lowercase-with-hyphens** for file names
- **Include code references** with line numbers: `path/to/file.php:123`
- **Link generously** between related documentation
## Integration with Other Skills
This skill works alongside:
- **backend-developer** - When backend code changes need doc updates
- **frontend-developer** - When frontend code changes need doc updates
- **research-agent** - Provides context for documentation gaps
- **process-documentation-reports** - Uses this skill when generating docs
## Quality Gate
This skill is the "quality gate" ensuring all documentation, whether created manually or from research reports, meets project standards defined in `.claude/rules/documentation/`.

View File

@@ -0,0 +1,55 @@
---
name: frontend-developer
description: Skill for implementing frontend features following project conventions. Use when writing Vue components, creating UI, or refactoring frontend code. Loads all frontend rules from .claude/rules/frontend/.
---
# Frontend Developer Skill
Use this skill when working with frontend code to ensure project conventions are followed.
## Loading Conventions
**CRITICAL:** Before implementing any frontend features, read ALL frontend rules:
1. Use Glob to find all files: `.claude/rules/frontend/*.md`
2. Read each file to load conventions
These rules contain all patterns, conventions, and best practices for:
- Vue component structure
- TypeScript usage
- Component composition patterns
- Styling conventions
- State management
- And more...
## When to Use This Skill
Activate this skill when:
- Implementing frontend features or UI components
- Refactoring frontend code
- You need to verify frontend patterns
- User asks to "follow frontend conventions"
- You're in a different role but need frontend context temporarily
## What This Skill Provides
After loading the rules, you have complete context for:
- How to structure Vue components
- Which patterns to use (and avoid)
- TypeScript conventions
- Inertia.js patterns (if used)
- Component communication patterns
- Styling approaches
## Key Principle
**Rules are the source of truth.** This skill simply loads them and provides context on when to apply them.
The rules define:
- WHAT the patterns are
- HOW to implement them
- WHAT to avoid
This skill provides:
- WHEN to use which patterns
- Context for applying rules in your current task

View File

@@ -0,0 +1,52 @@
---
name: linear-project-management
description: Skill for Linear project management tasks including creating projects, creating issues, and reviewing project structures. Use when working with Linear project setup, issue creation, project planning, or any Linear organizational tasks.
---
# Linear Project Management Skill
When this skill is activated, you MUST follow these steps:
1. **Load Linear Conventions**: Use the Read tool to read `.claude/rules/linear/README.md` from the user's project directory.
2. **Follow Loaded Rules**: The README.md file contains all necessary guidance for Linear project management tasks. Follow the instructions and conventions defined there.
## 🚨 MANDATORY CHECKPOINT - Linear Project Creation
**BEFORE creating any Linear project**, you MUST follow this workflow:
1. **Draft the project description** following all conventions (branch, purpose, scope, etc.)
2. **STOP** - Do not create the project yet
3. **Invoke the reviewer** using the Task tool:
```
subagent_type: "project-roles:linear-project-description-reviewer"
prompt: "Review this Linear project description: [paste description]"
```
4. **Address all feedback** from the reviewer
5. **ONLY THEN** create the project using `mcp__linear-server__create_project`
❌ WRONG: Draft description → Immediately create project
✅ RIGHT: Draft description → Get reviewed → Address feedback → Create project
**You do NOT have discretion to skip review.** This ensures every project provides comprehensive context for agents working on its issues.
## When This Skill Activates
This skill activates for:
- Creating new Linear projects
- Creating Linear issues
- Reviewing Linear project structures
- Planning or organizing work in Linear
- Any task involving Linear project management conventions
## Implementation Pattern
Always start by reading the conventions:
```
1. Read .claude/rules/linear/README.md
2. Follow the guidance provided in that file
3. Apply the conventions to your current task
```
The conventions file is the single source of truth for how Linear project management should be handled in this project.

View File

@@ -0,0 +1,771 @@
---
name: php-test-writer
description: Skill for creating and editing PHP tests following project conventions. Use when creating tests, updating test files, or refactoring tests. Applies proper structure, naming, factory usage, and Laravel/PHPUnit best practices.
---
# PHP Test Writer Skill
You are an expert at writing PHP tests for Laravel applications. Your role is to create well-structured, maintainable tests that follow the project's established conventions.
## Test Method Naming - CRITICAL Pattern
**ALWAYS use the `test_` prefix. DO NOT use the `#[Test]` attribute.**
```php
// ✅ CORRECT - Use ONLY test_ prefix
public function test_order_calculates_total_correctly()
{
// test implementation
}
// ❌ WRONG - Do not use #[Test] attribute
#[Test]
public function test_order_calculates_total_correctly()
{
// test implementation
}
// ❌ WRONG - Do not use #[Test] without prefix
#[Test]
public function order_calculates_total_correctly()
{
// test implementation
}
```
**Why:** The project uses the `test_` prefix pattern consistently. While `#[Test]` is valid in PHPUnit, it's unnecessary when using the prefix and adds visual noise to test files.
## Project Context
**Important System Details:**
- **Multitenancy**: Most models have `customer_id` - use `->recycle($customer)` to avoid N+1 customer creation
- **Database Schema**: Uses squashed schema (`database/schema/testing-schema.sql`)
- **Laravel Sail**: All commands must use `./vendor/bin/sail` prefix
- **TestCase Properties**: Feature tests have protected properties like `$customer`, `$user`, `$customerUser` - **DO NOT override these**
## Critical Guidelines
### 1. Always Read TestCase.php First
**MANDATORY**: Before writing any feature test, read `tests/TestCase.php` to understand:
- Protected properties that cannot be overridden
- Available helper methods (e.g., `getCustomer()`, `getAdminUser()`, `actingAsCustomerUser()`)
- Setup methods that run automatically (e.g., `setupGroups()`, `setupCurrencies()`)
```php
// ❌ BAD - Will cause errors
class MyTest extends TestCase
{
protected $customer; // ERROR: Property already exists in TestCase
}
// ✅ GOOD - Use TestCase helper methods
class MyTest extends TestCase
{
public function test_something()
{
$customer = $this->getCustomer(); // Use TestCase helper
}
}
```
### 2. File Structure & Naming
**Mirror the app/ directory structure:**
```
app/Services/DataObject/DataObjectService.php
→ tests/Feature/Services/DataObject/DataObjectService/DataObjectServiceTest.php
app/Enums/Filtering/RelativeDatePointEnum.php
→ tests/Unit/Enums/Filtering/RelativeDatePointEnum/RelativeDatePointEnumResolveTest.php
```
**Prefer split over flat structure:**
- When a class has many methods or complex edge cases, create a directory
- Use subdirectories to organize related tests
```
✅ Good (split structure):
tests/Feature/Services/DataObject/DataObjectService/
├── BaseDataObjectServiceTest.php # Base class
├── Create/
│ ├── BasicCreateTest.php
│ ├── UserColumnTest.php
│ └── FailedOperationTest.php
└── Update/
├── BasicUpdateTest.php
└── UserColumnTest.php
❌ Avoid (flat structure for complex classes):
tests/Feature/Services/DataObject/
└── DataObjectServiceTest.php # Too much in one file
```
### 3. Test Method Naming
## Test Method Naming - CRITICAL Pattern
**ALWAYS use the `test_` prefix. DO NOT use the `#[Test]` attribute.**
```php
// ✅ CORRECT - Use ONLY test_ prefix
public function test_order_calculates_total_correctly()
{
// test implementation
}
// ❌ WRONG - Do not use #[Test] attribute
#[Test]
public function test_order_calculates_total_correctly()
{
// test implementation
}
// ❌ WRONG - Do not use #[Test] without prefix
#[Test]
public function order_calculates_total_correctly()
{
// test implementation
}
```
**Why:** The project uses the `test_` prefix pattern consistently. While `#[Test]` is valid in PHPUnit, it's unnecessary when using the prefix and adds visual noise to test files.
**Formula**: `test_{methodUnderTest}__{conditions}__{expectedOutput}`
```php
// ✅ Excellent examples:
public function test_update_dispatches_data_object_received_event()
public function test_process_converts_non_string_values_to_strings()
public function test_last_month_with_year_transition()
public function test_attempt_to_create_dataobject_with_existing_extref__throws_error()
public function test_resolve_by_external_id_only_finds_users_for_correct_customer()
// ❌ Avoid:
public function test_update() // Too vague
public function testUpdateMethod() // Not descriptive enough
```
**When a whole file tests a single method:**
- Method name can be omitted from test name
- Example: `RelativeDatePointEnumResolveTest.php` tests only `resolve()`, so methods are named like `test_current_quarter_boundaries()`
**Always add PHPDoc:**
```php
/**
* Test that updating a DataObject dispatches DataObjectReceived event
*/
public function test_update_dispatches_data_object_received_event()
{
// Test implementation
}
```
### 4. Test Structure: Arrange-Act-Assert
Use the AAA pattern when it makes sense:
```php
public function test_update_object_fields()
{
// Arrange
$objectDefinition = $this->getObjectDefinition(
data_key: 'test_object_update'
);
$dataObject = $this->dataObjectService->create(
objectDefinition: $objectDefinition,
objectFields: [
'field1' => 'value1',
'field2' => 'value2',
]
);
// Act
$updatedDataObject = $this->dataObjectService->update(
dataObject: $dataObject,
objectFields: [
'field1' => 'updated_value1',
],
throwOnValidationErrors: true,
);
// Assert
$this->assertEquals('updated_value1', $updatedDataObject->object_fields['field1']);
$this->assertEquals('value2', $updatedDataObject->object_fields['field2']);
}
```
### 5. Factory Usage
**ALWAYS use factories - NEVER create models manually:**
```php
// ✅ GOOD - Use factories
$customer = Customer::factory()->create();
$user = User::factory()->create();
$customerUser = CustomerUser::factory()
->recycle($customer)
->recycle($user)
->create();
$objectDefinition = ObjectDefinition::factory()
->recycle($customer)
->create();
// ❌ BAD - Manual creation
$customer = Customer::create(['name' => 'Test Customer']);
$user = new User(['name' => 'Test', 'email' => 'test@test.com']);
$user->save();
```
**Use ->recycle() extensively for multitenancy:**
```php
// ✅ EXCELLENT - Recycle customer across all models
$customer = Customer::factory()->create();
$objectDefinition = ObjectDefinition::factory()
->recycle($customer) // Uses same customer
->create();
$dataObject = DataObject::factory()
->recycle($customer) // Same customer
->recycle($objectDefinition) // And its nested relations also use same customer
->createOneWithService();
// ❌ BAD - Creates multiple customers
$objectDefinition = ObjectDefinition::factory()->create(); // Creates new customer
$dataObject = DataObject::factory()
->recycle($objectDefinition)
->createOneWithService(); // objectDefinition and dataObject have different customers!
```
**Factory Tips:**
- Check if factories have custom states before manually setting attributes
- Use `->forCustomerUser()`, `->forUserGroup()`, etc. when available
- DataObject uses `->createOneWithService()` or `->createWithService()` instead of `->create()`
### 6. Named Arguments
**Always use named arguments for clarity:**
```php
// ✅ GOOD
$result = $this->processor->process(
inputValue: 'test',
processingContext: [],
objectDefinition: $objectDefinition,
columnData: $columnData
);
$dataObject = $this->dataObjectService->create(
objectDefinition: $objectDefinition,
objectFields: ['name' => 'Test'],
extRef: 'ext-123',
visibleRef: 'VIS-123'
);
// ❌ BAD
$result = $this->processor->process('test', [], $objectDefinition, $columnData);
$dataObject = $this->dataObjectService->create($objectDefinition, ['name' => 'Test'], 'ext-123');
```
### 7. Authentication & Session
**Use TestCase helpers:**
```php
// ✅ GOOD - Use TestCase helpers
$customer = $this->getCustomer();
$adminUser = $this->getAdminUser();
$adminCustomerUser = $this->getAdminCustomerUser();
// Acting as a customer user
$this->actingAsCustomerUser($adminCustomerUser);
// Or for session only
CustomerSession::store($customer);
// ❌ BAD - Manual session manipulation
session()->put('customer', CustomerSessionData::fromCustomer($customer)->toArray());
```
### 8. DataObject & ObjectDefinition Management
**CRITICAL: Use services and helpers for data management**
**DataObject Operations:**
- **ALL DataObject changes MUST go through DataObjectService**
- Never create or update DataObjects directly with Eloquent
- Resolve the service using `app()->make()` NOT `app()`
```php
// ✅ GOOD - Use DataObjectService
/** @var DataObjectService $dataObjectService */
$dataObjectService = app()->make(DataObjectService::class);
$dataObject = $dataObjectService->create(
objectDefinition: $objectDefinition,
extRef: 'test-ref',
visibleRef: 'TEST-001',
objectFields: ['field1' => 'value1']
);
$updated = $dataObjectService->update(
dataObject: $dataObject,
objectFields: ['field1' => 'updated_value']
);
// ❌ BAD - Direct model creation/update
$dataObject = DataObject::create([...]); // NEVER DO THIS
$dataObject->update([...]); // NEVER DO THIS
```
**ObjectDefinition Creation:**
- **ALWAYS use TestCase helper methods** for creating ObjectDefinitions
- Helper methods: `getObjectDefinition()` and `getManagedObjectDefinition()`
- These helpers use ObjectDefinitionService internally
```php
// ✅ GOOD - Use TestCase helper
$objectDefinition = $this->getObjectDefinition(
data_key: 'test_object',
columns: [
ObjectDefinitionColumnData::stringColumn(
column_key: 'name',
column_name: 'Name'
),
ObjectDefinitionColumnData::decimalColumn(
column_key: 'amount',
column_name: 'Amount'
),
],
);
// For managed object definitions (e.g., Integration)
$objectDefinition = $this->getManagedObjectDefinition(
data_key: 'deal',
manageable: $integration,
primaryTitleColumn: 'name',
columns: [
ObjectDefinitionColumnData::stringColumn(
column_name: 'name',
column_key: 'name'
),
],
);
// ❌ BAD - Manual creation with factories
$objectDefinition = ObjectDefinition::factory()
->recycle($this->customer)
->create(['data_key' => 'test']);
ObjectDefinitionColumn::factory()
->recycle($objectDefinition)
->create(['column_key' => 'test']);
```
**Service Resolution Pattern:**
```php
// ✅ GOOD - Use app()->make() for type-safe resolution
/** @var DataObjectService $dataObjectService */
$dataObjectService = app()->make(DataObjectService::class);
/** @var ObjectDefinitionService $objectDefinitionService */
$objectDefinitionService = app()->make(ObjectDefinitionService::class);
// ❌ BAD - Using app() directly (no type safety)
$dataObjectService = app(DataObjectService::class);
```
### 9. Base Test Classes
**Create base classes for shared setup:**
```php
// Example: BaseDataObjectServiceTest.php
abstract class BaseDataObjectServiceTest extends TestCase
{
protected ?DataObjectService $dataObjectService = null;
protected ?ObjectDefinitionService $objectDefinitionService = null;
protected function setUp(): void
{
parent::setUp();
$this->setupUserAndCustomer();
$this->dataObjectService = app()->make(DataObjectService::class);
$this->objectDefinitionService = app()->make(ObjectDefinitionService::class);
}
}
// Then extend in specific tests
class BasicCreateTest extends BaseDataObjectServiceTest
{
public function test_something()
{
// $this->dataObjectService is already available
}
}
```
**Create custom assertion helpers:**
```php
// Example: BaseProcessorTestCase.php
protected function assertProcessedSuccessfully(
ColumnProcessingResult $result,
mixed $expectedValue,
string $message = ''
): void {
$this->assertFalse($result->hasErrors(), $message ?: 'Expected processing to succeed');
$this->assertTrue($result->isSuccess(), $message ?: 'Expected success');
$this->assertEquals($expectedValue, $result->value, $message ?: 'Value mismatch');
}
// Usage in tests
$result = $this->processValue(inputValue: 'test', columnData: $columnData);
$this->assertProcessedSuccessfully(result: $result, expectedValue: 'test');
```
### 10. Common Patterns
**Testing events:**
```php
Event::fake();
// ... perform action ...
Event::assertDispatched(DataObjectReceived::class, function ($event) use ($dataObject) {
return $event->dataObject->id === $dataObject->id;
});
```
**Testing exceptions:**
```php
$this->expectException(DuplicateExtRefException::class);
$this->expectExceptionMessage('External reference already exists');
// ... code that should throw ...
```
**Using data providers:**
```php
/**
* @dataProvider nullAndEmptyValueProvider
*/
public function test_handles_null_and_empty($value)
{
// Test implementation
}
public static function nullAndEmptyValueProvider(): array
{
return [
'null' => [null],
'empty string' => [''],
];
}
```
### 11. Assertions
**Use specific assertions with meaningful messages:**
```php
// ✅ GOOD
$this->assertEquals('expected', $actual, 'Default value was not applied correctly');
$this->assertNotNull($result, 'Result should not be null');
$this->assertCount(3, $items, 'Expected 3 items in collection');
$this->assertInstanceOf(DataObject::class, $result);
$this->assertDatabaseHas('data_objects', ['ext_ref' => 'test-123']);
// ❌ AVOID
$this->assertTrue($actual == 'expected'); // Use assertEquals instead
$this->assertTrue(!is_null($result)); // Use assertNotNull instead
```
## Anti-Patterns to Avoid
### ❌ Hardcoded IDs
```php
// BAD
$dataObject = DataObject::create([
'object_definition_id' => 1,
'customer_id' => 1,
]);
// GOOD
$dataObject = DataObject::factory()
->recycle($objectDefinition)
->recycle($customer)
->createOneWithService();
```
### ❌ Manual Model Creation
```php
// BAD
$user = User::create([
'name' => 'Test',
'email' => 'test@example.com',
'password' => bcrypt('password'),
]);
// GOOD
$user = User::factory()->create([
'email' => 'test@example.com' // Only specify what matters for the test
]);
```
### ❌ Overriding TestCase Protected Properties
```php
// BAD - Will cause errors
class MyTest extends TestCase
{
protected $customer; // ERROR: Already defined in TestCase
protected $user; // ERROR: Already defined in TestCase
}
// GOOD - Use TestCase helpers
class MyTest extends TestCase
{
public function test_something()
{
$customer = $this->getCustomer();
$user = User::factory()->create();
}
}
```
### ❌ Using env() Directly
```php
// BAD
$apiKey = env('API_KEY');
// GOOD
$apiKey = config('services.api.key');
```
## Test Execution
**Running tests:**
```bash
# All tests
./vendor/bin/sail php artisan test
# Specific file
./vendor/bin/sail php artisan test tests/Feature/Services/DataObject/DataObjectService/Create/BasicCreateTest.php
# Specific test method
./vendor/bin/sail php artisan test --filter=test_update_dispatches_data_object_received_event
# With filter
./vendor/bin/sail php artisan test --filter=DataObjectService
```
**Schema regeneration (when migrations change):**
```bash
./vendor/bin/sail php artisan schema:regenerate-testing --env=testing
```
## Examples from Codebase
### Feature Test Example (Integration)
```php
<?php
namespace Tests\Feature\Services\DataObject\DataObjectService\Create;
use App\Data\ObjectDefinition\ObjectDefinitionColumnData;
use App\Exceptions\DataObject\DuplicateExtRefException;
use Tests\Feature\Services\DataObject\DataObjectService\BaseDataObjectServiceTest;
/**
* Test basic creation functionality of DataObjectService
*/
class BasicCreateTest extends BaseDataObjectServiceTest
{
public function test_attempt_to_create_dataobject_with_existing_extref__throws_error()
{
$objectDefinition = $this->getObjectDefinition(
columns: [
ObjectDefinitionColumnData::stringColumn(column_key: 'test_field'),
],
);
$this->dataObjectService->create(
objectDefinition: $objectDefinition,
objectFields: ['test_field' => 'Test Value'],
extRef: 'test-create-ref',
);
$this->expectException(DuplicateExtRefException::class);
// Should throw an exception because the extRef already exists
$this->dataObjectService->create(
objectDefinition: $objectDefinition,
objectFields: ['test_field' => 'Test Value'],
extRef: 'test-create-ref',
);
}
}
```
### Unit Test Example (Isolated)
```php
<?php
namespace Tests\Unit\Enums\Filtering\RelativeDatePointEnum;
use App\Enums\Filtering\RelativeDatePointEnum;
use Carbon\Carbon;
use Exception;
use Tests\Unit\BaseUnitTestCase;
class RelativeDatePointEnumResolveTest extends BaseUnitTestCase
{
/**
* Test that context period boundaries resolve correctly
*/
public function test_context_period_boundaries_resolve_correctly(): void
{
$periodStart = Carbon::parse('2025-01-01 00:00:00', 'UTC');
$periodEnd = Carbon::parse('2025-01-31 23:59:59', 'UTC');
$startResult = RelativeDatePointEnum::START_OF_CONTEXT_PERIOD->resolve(
contextPeriodStart: $periodStart,
contextPeriodEnd: $periodEnd
);
$endResult = RelativeDatePointEnum::END_OF_CONTEXT_PERIOD->resolve(
contextPeriodStart: $periodStart,
contextPeriodEnd: $periodEnd
);
$this->assertEquals('2025-01-01 00:00:00', $startResult->format('Y-m-d H:i:s'));
$this->assertEquals('2025-01-31 23:59:59', $endResult->format('Y-m-d H:i:s'));
}
/**
* Test that context period boundaries throw exception when context is missing
*/
public function test_context_period_boundaries_throw_exception_when_missing(): void
{
$this->expectException(Exception::class);
$this->expectExceptionMessage('Cannot resolve relative date point');
RelativeDatePointEnum::START_OF_CONTEXT_PERIOD->resolve();
}
}
```
### Base Test Class Example
```php
<?php
namespace Tests\Feature\Services\DataObject\ObjectFields\ColumnTypeProcessors;
use App\Data\ObjectDefinition\ObjectDefinitionColumnData;
use App\Enums\DataObject\Error\DataObjectErrorCode;
use App\Enums\ObjectDefinition\ObjectDefinitionColumn\ColumnTypeEnum;
use App\Models\ObjectDefinition;
use App\Services\DataObject\ObjectFields\ColumnProcessingResult;
use App\Services\DataObject\ObjectFields\ColumnTypeProcessors\AbstractColumnProcessor;
use Tests\TestCase;
/**
* Base test case for column processor tests with helpful assertion methods
*/
abstract class BaseProcessorTestCase extends TestCase
{
protected AbstractColumnProcessor $processor;
/**
* Create a simple column data object for testing
*/
protected function makeColumnData(
string $columnKey = 'test_field',
ColumnTypeEnum $columnType = ColumnTypeEnum::STRING,
bool $isRequired = false,
mixed $defaultValue = null,
?string $columnName = null
): ObjectDefinitionColumnData {
return ObjectDefinitionColumnData::from([
'column_key' => $columnKey,
'column_name' => $columnName ?? ucfirst(str_replace('_', ' ', $columnKey)),
'column_type' => $columnType,
'is_required' => $isRequired,
'default_value' => $defaultValue,
]);
}
/**
* Process a value using the processor with standard test parameters
*/
protected function processValue(
mixed $inputValue,
?ObjectDefinitionColumnData $columnData = null,
array $processingContext = []
): ColumnProcessingResult {
$columnData = $columnData ?? $this->makeColumnData();
$objectDefinition = \Mockery::mock(ObjectDefinition::class);
return $this->processor->process(
inputValue: $inputValue,
processingContext: $processingContext,
objectDefinition: $objectDefinition,
columnData: $columnData
);
}
/**
* Assert that processing was successful and returned the expected value
*/
protected function assertProcessedSuccessfully(
ColumnProcessingResult $result,
mixed $expectedValue,
string $message = ''
): void {
$this->assertFalse($result->hasErrors(), $message ?: 'Expected processing to succeed but it had errors');
$this->assertTrue($result->isSuccess(), $message ?: 'Expected processing to be marked as successful');
$this->assertEquals($expectedValue, $result->value, $message ?: 'Expected processed value did not match');
}
}
```
## Workflow
When writing tests:
1. **Read TestCase.php** to understand available helpers and protected properties
2. **Check for existing similar tests** to follow established patterns
3. **Read the PHPUnit guidelines** at `docs/development/guidelines/php/phpunit-guidelines.md`
4. **Determine test type**: Feature (integration) or Unit (isolated)
5. **Create proper directory structure** mirroring app/ directory
6. **Use factories exclusively** with `->recycle()` for multitenancy
7. **Write descriptive test names** following the convention
8. **Add PHPDoc** explaining what the test does
9. **Use named arguments** throughout
10. **Run the tests** to verify they pass
11. **Consider creating base test class** if you have multiple related test files
## Final Reminder
- **ALWAYS read TestCase.php first** for feature tests
- **NEVER override TestCase protected properties**
- **ALWAYS use factories** with `->recycle($customer)`
- **ALWAYS use named arguments** for clarity
- **ALL DataObject changes through DataObjectService** - Never create/update DataObjects directly
- **Use TestCase helpers for ObjectDefinitions** - `getObjectDefinition()` or `getManagedObjectDefinition()`
- **Resolve services with `app()->make()`** - NOT `app()` for type safety
- **Mirror app/ directory structure** in tests
- **Prefer split over flat** structure for complex classes
- **Run tests after writing** to ensure they pass
Your goal is to create maintainable, readable tests that future developers can easily understand and extend.