# Cursor Rules for bongo/estimate

## Project Overview

This is a Laravel package (`bongo/estimate`) providing an estimation/quoting system for service-based businesses. The package manages estimates with geographic service areas, tiered pricing plans, and automated calculations for recurring service costs.

**Key Features**:
- Multi-step frontend estimate workflow with Google Maps integration
- Database-driven service/plan/pricing hierarchy
- Geographic coverage validation with radius circles
- Automated price calculation with tax handling
- Admin backend for managing services, plans, locations
- Event-driven architecture with comprehensive domain events
- Session-based customer journey tracking

## Directory Structure

```
src/
├── Actions/              # Business logic operations (CalculateDistance, GenerateNumber, GetPrice)
├── Calculators/          # Complex calculation logic (EstimateItemPriceCalculator)
├── Commands/             # Artisan console commands (UpgradeToV4Command)
├── Components/           # Blade components (RadiusMap, ServiceDropdown)
├── Concerns/             # Reusable traits/mixins (CanBeExported)
├── Config/               # Package configuration (estimate.php)
├── Events/               # Domain events (Created/Updated/Deleted for all entities)
├── Exceptions/           # Custom exceptions
├── Http/
│   ├── Controllers/
│   │   ├── Backend/      # Admin CRUD controllers
│   │   └── Frontend/     # Public step controllers (Step1, Step2, Step3)
│   ├── Requests/         # Form validation
│   └── Resources/        # API transformers
├── Interfaces/           # Contracts (StatusInterface, PriceTypeInterface, GeoLookup)
├── Mailables/            # Email notifications
├── Maps/                 # Google Maps integration (StaticMap, RadiusMap, PolylineEncoder)
├── Migrations/           # Database schema
├── Models/               # Eloquent models
├── Routes/               # Route definitions (backend.php, frontend.php)
├── Services/             # External integrations (GoogleGeoCode)
├── Traits/               # Model composition traits
├── Translations/         # i18n files
├── Views/                # Blade templates
└── EstimateServiceProvider.php
```

## Architecture Patterns

### Model Hierarchy

```
EstimateService (e.g., "Lawn Care")
  └─ EstimatePlan (e.g., "Basic", "Enhanced", "Premier")
      └─ EstimatePlanItem (e.g., "50m² = £28", "100m² = £35")

Estimate (Customer Quote)
  └─ EstimateItem (Service/Plan selection for a property)
      └─ EstimateItemPrice (Calculated pricing breakdown)

EstimateLocation (Service coverage area with radius)
```

### Core Models

| Model | Primary Key | Soft Deletes | Key Traits | Purpose |
|-------|-------------|--------------|------------|---------|
| `EstimateService` | UUID | Yes | HasDefault, HasKey, HasStatus | Top-level service category |
| `EstimatePlan` | UUID | Yes | HasDefault, HasKey, HasPriceType, HasStatus | Pricing tier within service |
| `EstimatePlanItem` | UUID | Yes | - | Area-based pricing breakpoint |
| `EstimateLocation` | UUID | Yes | HasDefault, HasKey, HasStatus | Geographic coverage area |
| `Estimate` | UUID | Yes | HasAddress, HasContact, HasDate, HasNumber, HasStatus, CanBeExported | Customer quote |
| `EstimateItem` | UUID | Yes | HasAreas, HasItemNumber, HasMapMarker, HasSteps, CanBeExported | Service selection with drawn areas |
| `EstimateItemPrice` | UUID | Yes | HasTotals | Calculated pricing breakdown |

### Trait Composition Pattern

Models use traits instead of inheritance for cross-cutting concerns:

```php
class Estimate extends AbstractModel implements StatusInterface {
    use CanBeExported;
    use HasAddress;
    use HasContact;
    use HasDate;
    use HasNumber;
    use HasStatus;
    use HasVoucherCode;
    use HasUUID;
    use SoftDeletes;
}
```

**Common Traits**:
- `HasStatus` - Status field with scopes (pending, accepted, rejected)
- `HasAddress` - Address fields with geocoding (line_1, city, postcode, lat/lng)
- `HasContact` - Contact info (first_name, last_name, email, phone)
- `HasAreas` - JSON polygon storage for drawn areas
- `HasDate` - Auto-set date tracking
- `HasNumber` - Auto-generated reference numbers (e.g., "KLC-0001")
- `HasItemNumber` - Item numbering (estimate_number-count format)
- `HasSteps` - Multi-step workflow tracking (1, 2, 3)
- `HasDefault` - Ensure only one record marked as default
- `HasPriceType` - Price calculation type (per_treatment_month, per_treatment, per_month)
- `HasTotals` - Price fields (cost_per_m2, subtotal, tax_rate, tax, total, price_per_month)
- `CanBeExported` - Export tracking (exported_at, export_error)

### Action Classes

Static utility classes for discrete business logic:

```php
// Distance calculation
$miles = CalculateDistance::inMiles($lat1, $lng1, $lat2, $lng2);
$km = CalculateDistance::inKilometers($lat1, $lng1, $lat2, $lng2);

// Price retrieval
$price = GetPrice::forBasicPlan($totalAreaM2);
$price = GetPrice::forEnhancedPlan($totalAreaM2);

// Number generation
$number = GenerateNumber::forEstimate(); // KLC-0001, KLC-0002, etc.

// Entity retrieval
$estimate = FindEstimate::byUuid($uuid);
$item = FindEstimateItem::byUuid($uuid);
$service = GetDefaultService::execute();
```

### Calculator Pattern

Fluent builder pattern for complex calculations:

```php
(new EstimateItemPriceCalculator())
    ->setEstimateItemPrice($estimateItemPrice)
    ->calculate()
    ->save();
```

**Calculation Flow**:
1. Get tax rate from settings
2. Find EstimatePlanItem for area
3. Apply minimum price if needed
4. Calculate cost per m² (plan cost ÷ total area)
5. Calculate subtotal (same as plan cost)
6. Calculate tax (subtotal × tax rate / 100)
7. Calculate total (subtotal + tax)
8. Calculate monthly price (total × chargeable treatments ÷ 12)

### Event-Driven Architecture

Events fired on model lifecycle:
- `EstimateUpdated`
- `EstimateItemCreated`, `EstimateItemUpdated`, `EstimateItemDeleted`
- `ServiceCreated`, `ServiceUpdated`, `ServiceDeleted`
- `PlanCreated`, `PlanUpdated`, `PlanDeleted`
- `LocationCreated`, `LocationUpdated`, `LocationDeleted`

### Service Provider Integration

Extends `AbstractServiceProvider` from `bongo/framework`:

```php
class EstimateServiceProvider extends AbstractServiceProvider {
    protected string $module = 'estimate';
    public array $commands = [UpgradeToV4Command::class];
}
```

**Auto-loaded by AbstractServiceProvider**:
- Config: `src/Config/estimate.php`
- Routes: `src/Routes/backend.php`, `src/Routes/frontend.php`
- Views: `src/Views/estimate/`
- Migrations: `src/Migrations/`
- Translations: `src/Translations/`

**Route Middleware Patterns**:
- `backend.php` → `backend.*` named routes with `auth` + `employee` middleware
- `frontend.php` → `frontend.*` named routes with standard web middleware

## Coding Conventions

### Strict Types

All PHP files MUST include:
```php
declare(strict_types=1);
```

### Naming Conventions

**Models**: Singular PascalCase (Estimate, EstimateItem, EstimatePlan)
**Controllers**: PascalCase with suffix (EstimateController, Step1Controller)
**Actions**: Verb + Noun (CalculateDistance, GenerateNumber, GetPrice)
**Traits**: Has + Noun (HasStatus, HasAddress, HasContact)
**Events**: Noun + Past Tense (EstimateCreated, EstimateUpdated)

### Return Types

Always declare return types:

```php
public function hasItems(): bool
public function plans(): HasMany
public function findItemByAreaM2(int|float $areaM2): ?EstimatePlanItem
public static function inMiles(float $lat1, float $lng1, float $lat2, float $lng2): float
```

### Status Constants

Use interface constants for status values:

```php
class Estimate extends AbstractModel implements StatusInterface {
    // Use: self::PENDING, self::ACCEPTED, self::REJECTED
}
```

### Relationship Loading

Check relationships with load-on-demand pattern:

```php
public function hasItems(): bool {
    $this->loadMissing('items');
    return !is_null($this->items) && count($this->items);
}
```

### Model Fillable Fields

Use trait initialization to merge fillable fields:

```php
public function initializeHasStatus(): void {
    $this->mergeFillable(['status']);
}
```

### Casts

Use custom casts from bongo/framework:

```php
protected $casts = [
    'min_price' => Pence::class,           // Store pence as integer
    'area_m2' => 'decimal:2',              // Decimal with 2 places
    'treatments' => 'int',
];
```

## Common Tasks

### Adding a New Model

1. Create model in `src/Models/` extending `AbstractModel`
2. Add appropriate traits (HasUUID, HasStatus, SoftDeletes, etc.)
3. Define fillable fields via trait initialization
4. Add casts for data types
5. Define relationships
6. Create migration in `src/Migrations/`
7. Create factory in `database/factories/`
8. Create domain events if needed

### Adding a New Action

1. Create static class in `src/Actions/`
2. Implement public static methods with clear return types
3. Use descriptive method names (verb + context)
4. Handle edge cases with exceptions

### Adding a New Calculator

1. Create class in `src/Calculators/`
2. Use fluent builder pattern with `set*()` methods
3. Implement `calculate()` method that returns `$this`
4. Implement `save()` method for persistence
5. Keep calculation logic in separate private methods

### Adding a New Backend Controller

1. Create in `src/Http/Controllers/Backend/`
2. Add route in `src/Routes/backend.php` with `backend.*` name prefix
3. Use form requests for validation in `src/Http/Requests/`
4. Fire domain events after model changes
5. Return redirect with success/error flash messages

### Adding a New Frontend Step

1. Create controller in `src/Http/Controllers/Frontend/`
2. Add routes in `src/Routes/frontend.php` with `frontend.*` name prefix
3. Add honeypot to POST routes for spam protection
4. Validate step progression using `stepIs()`, `stepIsGt()`, etc.
5. Store data in session between steps
6. Create view in `src/Views/estimate/frontend/step_X/`

### Adding a New Trait

1. Create in `src/Traits/`
2. Implement `initializeHas*()` to merge fillable fields
3. Add computed properties via accessors if needed
4. Add scopes for common queries
5. Add helper methods for status checks

### Calculating Prices

```php
// For an EstimateItem:
$item = EstimateItem::find($id);
$plan = $item->plan;
$planItem = $plan->findItemByAreaM2($item->total_area_m2);

$estimateItemPrice = EstimateItemPrice::create([
    'estimate_item_id' => $item->id,
    'estimate_plan_id' => $plan->id,
    'estimate_plan_item_id' => $planItem->id,
]);

(new EstimateItemPriceCalculator())
    ->setEstimateItemPrice($estimateItemPrice)
    ->calculate()
    ->save();
```

### Working with Coverage Areas

```php
// Check if address is within coverage:
$estimate = Estimate::find($id);
$isInCoverage = $estimate->isWithinCoverageArea(
    $estimate->latitude,
    $estimate->longitude
);

// Get all active locations:
$locations = EstimateLocation::active()->get();
```

### Generating Static Maps

```php
$item = EstimateItem::find($id);

// Get URL:
$url = $item->getStaticMap(false);

// Generate and save image:
$imagePath = $item->getStaticMap(true); // Saved in storage/app/public/estimates/
```

## Testing

### Running Tests

```bash
composer test              # No coverage
composer test:coverage     # With Xdebug coverage
vendor/bin/phpunit --filter TestMethodName  # Single test
```

### Test Structure

Base test class: `tests/TestCase.php`
- Uses Orchestra Testbench
- In-memory SQLite database
- Manual table schema definition
- Pre-configured settings (tax_rate, chargeable_treatments, number_prefix)

### Factory Usage

```php
use Bongo\Estimate\Database\Factories\EstimateFactory;

$estimate = EstimateFactory::new()->create([
    'postcode' => 'NR1 1AA',
    'status' => Estimate::PENDING,
]);

$item = EstimateItemFactory::new()
    ->for($estimate)
    ->create();
```

### Example Test

```php
public function test_can_calculate_distance_in_miles(): void
{
    $distance = CalculateDistance::inMiles(52.6, 1.2, 52.7, 1.3);

    $this->assertIsFloat($distance);
    $this->assertGreaterThan(0, $distance);
}
```

## Commands

```bash
# Code quality
composer format            # Laravel Pint formatting
composer analyse           # PHPStan static analysis

# Testing
composer test              # Run tests without coverage
composer test:coverage     # Run tests with coverage

# Development
composer build             # Build testbench workbench
composer start             # Start test server
composer clear             # Clear testbench cache

# Upgrade
php artisan estimate:upgrade-to-v4  # Migrate v3 data to v4 structure
```

## Key Files Reference

| File | Purpose |
|------|---------|
| `src/EstimateServiceProvider.php` | Service provider registration |
| `src/Config/estimate.php` | Package configuration |
| `src/Models/Estimate.php` | Customer quote model |
| `src/Models/EstimateItem.php` | Quote item with areas |
| `src/Models/EstimateService.php` | Service category model |
| `src/Models/EstimatePlan.php` | Pricing tier model |
| `src/Models/EstimatePlanItem.php` | Area pricing breakpoint |
| `src/Calculators/EstimateItemPriceCalculator.php` | Price calculation logic |
| `src/Http/Controllers/Frontend/Step1Controller.php` | Contact/service selection |
| `src/Http/Controllers/Frontend/Step2Controller.php` | Area measurement |
| `src/Http/Controllers/Frontend/Step3Controller.php` | Quote display |
| `src/Services/GoogleGeoCode.php` | Address geocoding |
| `src/Maps/StaticMap.php` | Static map generation |
| `tests/TestCase.php` | Base test class |

## Configuration Keys

**Important settings in `config/estimate.php`**:

- `prefix` - Frontend route prefix (default: 'estimate')
- `backend_prefix` - Admin route prefix (default: 'estimates')
- `number_prefix` - Reference number prefix (default: 'KLC-')
- `map_center` - Default map center [lat, lng]
- `prices_include_tax` - Whether prices include tax (default: false)
- `treatments_per_year` - Total treatments per year (default: 11)
- `chargeable_treatments_per_year` - Billable treatments (default: 7)
- `cleanup_days` - Days before draft deletion (default: 7)
- `pricing_table_type` - Display variant: 'default', 'with_vat', 'with_features'
- `features` - Feature availability mapping by plan key

## Frontend Workflow

**Step 1: Contact & Service Selection**
- Route: `GET /estimate/step-1`
- Controller: `Step1Controller@show`
- Action: Collect postcode, contact details, select service
- Session: Stores estimate UUID and item UUID
- Validation: Postcode coverage check via GoogleGeoCode

**Step 2: Area Measurement**
- Route: `GET /estimate/step-2`
- Controller: `Step2Controller@show`
- Action: Draw property areas on Google Maps
- Data: Stores polygon coordinates as JSON in `areas` field
- Calculation: Total area computed in m²

**Step 3: Quote Display**
- Route: `GET /estimate/step-3`
- Controller: `Step3Controller@show`
- Action: Calculate pricing for all available plans
- Display: Pricing table based on `pricing_table_type` config

## Backend Workflow

**Service Configuration**:
1. Create EstimateService (e.g., "Lawn Care")
2. Create EstimatePlan tiers (Basic, Enhanced, Premier)
3. Add EstimatePlanItem breakpoints per plan (area → price mapping)
4. Configure features in config file

**Coverage Management**:
1. Create EstimateLocation with radius
2. Set latitude/longitude for center point
3. Define fill/stroke colours for map display

**Estimate Review**:
1. View estimates in datatable
2. Click to see detailed view with all items
3. Update status (draft → pending → accepted/rejected)
4. Track export status and errors

## Google Maps Integration

**Requirements**:
- Google Maps API key stored in settings: `setting('system::credentials.google_maps_api_key')`
- APIs enabled: Maps JavaScript API, Static Maps API, Geocoding API

**Static Map Usage**:
```php
$staticMap = (new StaticMap())
    ->setKey($apiKey)
    ->setCenter([$latitude, $longitude])
    ->setZoom(20);

$marker = (new Marker())->addLocation([$latitude, $longitude]);
$staticMap->addMarker($marker);

$path = (new Path())->setCoordinates($areaCoordinates);
$staticMap->addPath($path);

$url = $staticMap->generateUrl();
```

**Radius Map Component**:
```blade
<x-radius-map />
```

Displays all active EstimateLocations as circles on interactive map.

## Tips & Best Practices

1. **Always use UUID for primary keys** - All models use HasUUID trait
2. **Fire events after model changes** - Enable external integrations
3. **Use traits for composition** - Avoid deep inheritance hierarchies
4. **Validate step progression** - Use stepIs(), stepIsGt(), stepIsGte()
5. **Load relationships explicitly** - Use loadMissing() before checks
6. **Handle geocoding failures** - GoogleGeoCode can throw exceptions
7. **Apply minimum prices** - EstimateItemPriceCalculator handles this
8. **Test with factories** - All models have factories in database/factories/
9. **Use status constants** - Never hardcode status strings
10. **Check coverage areas** - Validate lat/lng within EstimateLocation radius

## Anti-Patterns to Avoid

- ❌ Don't hardcode status strings - use StatusInterface constants
- ❌ Don't bypass EstimateItemPriceCalculator - it handles tax logic
- ❌ Don't skip step validation - users can manipulate URLs
- ❌ Don't forget to fire events - external systems may depend on them
- ❌ Don't use direct database queries - use Eloquent models
- ❌ Don't skip honeypot on frontend forms - spam protection is critical
- ❌ Don't ignore soft deletes - use forceDelete() only when necessary
- ❌ Don't hardcode pricing - use EstimatePlan/EstimatePlanItem hierarchy
- ❌ Don't skip geocoding validation - coverage areas are business-critical
- ❌ Don't modify calculator state after calculate() - use new instance

## External Dependencies

- `bongo/framework` ^3.0 - Base package with AbstractModel, AbstractServiceProvider
- `spatie/laravel-honeypot` ^4.0 - Spam protection for forms
- `spatie/geocoder` ^3.12 - Address geocoding via Google Maps API

## Related Documentation

- See `ARCHITECTURE.md` for detailed architecture diagrams
- See `CLAUDE.md` for AI assistant guidance
- See `README.md` for installation and usage
- See `CHANGELOG.md` for version history
