Files
myep-team/docs/PLAN-cancellation-statistics.md
T

187 lines
5.5 KiB
Markdown

# Plan: Cancellation Statistics Feature
## Objective
Enable stakeholders to see how often dispositions and applications have been canceled by either the teamer or the office, per hotel and for a given date range.
---
## Current State Analysis
### Dispositions - Already Tracked
The `Disposition` entity has comprehensive cancellation tracking:
| Field | Type | Purpose |
|-------|------|---------|
| `status` | string | `'called_off'` when cancelled |
| `calledOffBy` | string (nullable) | `'teamer'` or `'office'` |
| `calledOffReason` | text (nullable) | Reason for cancellation |
| `updatedAt` | datetime | Timestamp of last change |
**Constants:**
```php
public const STATUS_CALLED_OFF = 'called_off';
public const CALLED_OFF_BY_TEAMER = 'teamer';
public const CALLED_OFF_BY_OFFICE = 'office';
```
**Cancellation entry points:**
1. Individual disposition cancellation via `Administrative/Disposition/CallOffController`
- Form requires `calledOffBy` and `calledOffReason`
- Dispatches `DispositionCalledOffEvent`
2. Full assignment cancellation via `Administrative/Assignment/CallOffController`
- Sets all dispositions to `calledOffBy = 'office'`
- Dispatches `AssignmentCalledOffEvent`
### Applications - Missing Tracking
The `Application` entity lacks cancellation tracking:
| Current Field | Issue |
|---------------|-------|
| `status` | Only `'rejected'` - no distinction between teamer withdrawal and office rejection |
| - | No `cancelledBy` field |
| - | No `cancellationReason` field |
**Current rejection mechanisms:**
1. Manual rejection by office (no tracking of who)
2. Automatic rejection by `RejectApplicationListener` when disposition slots fill
3. Automatic removal by `InvalidateApplicationsListener` when teamer has overlapping confirmed dispositions
---
## Pending Decision
**Question for stakeholders:** Should automatic rejections be included in statistics?
| Option | Description | Impact |
|--------|-------------|--------|
| **Only user-initiated** | Count only manual cancellations by teamer or office | Simpler model, clearer accountability |
| **Include automatic** | Also track system-initiated rejections | Full picture of lost applications, requires `'system'` category |
**Status:** Awaiting stakeholder response
---
## Proposed Changes
### Application Entity
**Option A: If only user-initiated cancellations count**
Add new status and fields:
```php
public const STATUS_WITHDRAWN = 'withdrawn'; // teamer-initiated
public const STATUS_REJECTED = 'rejected'; // office-initiated (existing)
private ?string $cancellationReason = null;
```
**Option B: If automatic rejections should be tracked**
Add new status, fields, and constant:
```php
public const STATUS_WITHDRAWN = 'withdrawn'; // teamer-initiated
public const STATUS_REJECTED = 'rejected'; // office or system initiated
public const CANCELLED_BY_TEAMER = 'teamer';
public const CANCELLED_BY_OFFICE = 'office';
public const CANCELLED_BY_SYSTEM = 'system';
private ?string $cancelledBy = null;
private ?string $cancellationReason = null;
```
### Database Migration
Add columns to `application` table:
- `cancellation_reason` (LONGTEXT, nullable)
- Possibly `cancelled_by` (VARCHAR(64), nullable) if Option B
### Repository Methods
Add to `ApplicationRepository`:
```php
/**
* @return array<string, array{teamer: int, office: int}>
*/
public function getCancellationStatsByHotel(
\DateTimeImmutable $startDate,
\DateTimeImmutable $endDate
): array;
```
Add to `DispositionRepository`:
```php
/**
* @return array<string, array{teamer: int, office: int}>
*/
public function getCancellationStatsByHotel(
\DateTimeImmutable $startDate,
\DateTimeImmutable $endDate
): array;
```
**Date filtering:** Based on assignment travel dates (confirmed in discussion).
### Update Listeners (if Option B)
Modify `RejectApplicationListener` and `InvalidateApplicationsListener` to set `cancelledBy = 'system'`.
### UI Changes
Add withdrawal functionality for teamers (if not already present) that sets `STATUS_WITHDRAWN`.
---
## Implementation Steps
1. [ ] Finalize decision on automatic rejection tracking
2. [ ] Add new constants and fields to `Application` entity
3. [ ] Create database migration
4. [ ] Update `ApplicationRepository` with statistics methods
5. [ ] Update `DispositionRepository` with statistics methods
6. [ ] Update listeners if tracking automatic rejections
7. [ ] Add/update controllers for teamer withdrawal flow
8. [ ] Write tests for new repository methods
9. [ ] Run php-cs-fixer
---
## Related Files
### Entities
- `src/Entity/Application.php`
- `src/Entity/Disposition.php`
- `src/Entity/Assignment.php`
### Repositories
- `src/Repository/ApplicationRepository.php`
- `src/Repository/DispositionRepository.php`
### Controllers
- `src/Controller/Administrative/Disposition/CallOffController.php`
- `src/Controller/Administrative/Assignment/CallOffController.php`
- `src/Controller/Teamer/Disposition/DetailController.php`
### Listeners
- `src/EventListener/InvalidateApplicationsListener.php`
- `src/EventListener/RejectApplicationListener.php`
### Events
- `src/Event/DispositionCalledOffEvent.php`
- `src/Event/AssignmentCalledOffEvent.php`
### Forms
- `src/Form/DispositionCallOffType.php`
---
## Notes
- Filter statistics by **assignment travel dates**, not cancellation timestamp
- Disposition tracking already works - only need repository query methods
- Application tracking requires entity changes and migration
- Consider whether teamer self-service withdrawal exists or needs to be added