feat: send accepted teamer invoices to configurable email inbox
This commit is contained in:
@@ -0,0 +1,187 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user