Files
myep/docs/contingent-api-plan.md
T

163 lines
9.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Contingent API with XML Caching (Crawler-Based) — Plan (Updated)
## Summary
Implement a cache-only contingent API backed by XML files in `var/xmlexportzimmer`, using Symfony cache (tag-aware) and Flysystem. Parsing uses DOM/Crawler and caches DTO arrays only. Add loader/parser/services/controllers and integrate cache invalidation into the existing XML sync flow. Provide endpoints for contingents, rooms, and calendar events.
## Current Status (feature/contingents-api)
- Implemented: parser, loader, data service, controller endpoints, XML sync for contingent dataset, and tests for parser/loader/service/controller.
- Implemented: hotel/date reference resolution (`hotelRef`/`dateRef`) with numeric passthrough and code mapping.
- Implemented: root-link one-hop behavior and BelKal control-room override semantics.
- Implemented: contingent caching via `bpn.cache` with `xml-sync` tagging strategy.
- In progress: enrich rooms payload with booking URL and additional travel metadata fields.
## Public API / Interface Changes
- **New endpoints (OAuth-protected)**:
1. `GET /api/contingents?hotelRef={hotelRef}&dateRef={dateRef}`
- Returns: object keyed by date with `{ date, minNights, total, capacity }`.
2. `GET /api/contingents/rooms?hotelRef={hotelRef}&dateRef={dateRef}&dateFrom={Y-m-d}&dateTo={Y-m-d}`
- Returns: **per date + room** rows with pricing and travel metadata.
3. `GET /api/contingents/calendar?hotelRef={hotelRef}&dateFrom={Y-m-d}&dateTo={Y-m-d}`
- Returns per date `{ date, status, pax, available }` (status is worst-case across rooms).
- **New config**:
- `.env`: `XML_EXPORT_CONTINGENTS_PATH="%kernel.project_dir%/var/xmlexportzimmer"`
- `config/packages/flysystem.yaml`: `xml_export_contingents.storage` and `xml_source_contingents.storage`.
- **New cache pool usage**:
- `bpn.cache` used for contingent file map + hotel cache entries, tagged with `xml-sync`.
## Key Decisions Locked
- **Parsing approach**: DOM/Crawler (SimpleXML/Crawler), not XMLReader.
- **Cache contents**: DTO arrays (scalars + `\DateTimeImmutable`) only. No SimpleXML/Crawler objects in cache.
- **Rooms response shape**: **per date + room** (not aggregated).
- **BelKal override**:
- BelKal is a control room that can override availability.
- If BelKal is A/S on a date, **all rooms** on that date are treated as on-request/blocked.
- **Root-level link**: one hop only; do not chain if linked file also has link attribute.
- **Room-level link**: link only affects which `zimmer[@idbuspro]` node is used for capacity.
## Detailed Implementation Plan
### 1) Configuration
1. Add `XML_EXPORT_CONTINGENTS_PATH` to `.env`.
2. Add Flysystem storage in `config/packages/flysystem.yaml`:
- `xml_export_contingents.storage` with `directory: '%env(resolve:XML_EXPORT_CONTINGENTS_PATH)%'`.
- `xml_source_contingents.storage` for SFTP contingent source.
3. Wire service injection in `config/services.yaml`:
- Bind `xml_export_contingents.storage` into `ContingentLoader`.
- Inject `@bpn.cache` explicitly into `ContingentLoader` and `BpnXmlSyncCommand`.
### 2) Parser (Crawler)
**New** `src/BusProNet/XmlParser/ContingentParser.php`
Responsibilities:
- Parse a single `HotelZimmer_*.xml` (string or Crawler root) and return:
- Room types: `{ idBusPro, code, pax, label, controlRoom, link }`
- Contingent rows (DTO arrays) with:
- `date` (`\DateTimeImmutable`)
- `roomCode`, `roomLabel`
- `pax`, `available`, `status` (`OK|ON_REQUEST|BLOCKED`)
- `minPrice`, `minNights`
- `additionalNightMinPrice`, `additionalNightMinNights`
- Logic:
- Room types from `unterbringungen/unterbringung`.
- Skip `code=PDGS`.
- Control room is `code=BelKal`.
- For each date node (`kapazitaeten/kapazitaet @termin d.m.Y`) and room type, resolve `zimmerliste/zimmer` by id:
- Use `roomType.link ?? roomType.idBusPro`.
- BelKal rows:
- status `A` => ON_REQUEST
- status `S` => BLOCKED
- Non-BelKal rows:
- `pax = kontingent * pax_max`
- `available = frei * pax_max`
- Return arrays (no XML objects) to be cache-safe.
### 3) Loader (Cache + Flysystem)
**New** `src/BusProNet/XmlLoader/ContingentLoader.php`
Responsibilities:
- Extend `AbstractLoader`.
- `generateFilesMap()`:
- List `HotelZimmer_*.xml` in `xml_export_zimmer.storage`.
- Build mapping `hotelId => filename`.
- Cache as `bpn_contingent_files` for 3h on `bpn.cache`.
- `loadByHotelId(int $hotelId): array`:
- Use file map to find filename; return `[]` if missing.
- Read XML content.
- **Root-level link**: if root has `idbuspro_kontingent_aus`, load that file **instead** (one hop only).
- Parse using `ContingentParser`.
- Cache result in `bpn.cache` as `contingent_hotel_{hotelId}` TTL 3600, tagged `xml-sync`.
### 4) Data Service (No DB)
**New** `src/Service/ContingentDataService.php`
Dependencies: `ContingentLoader`, `TravelLoader`, optional booking URL base.
Functions:
- `getAvailableContingents(int $hotelId, int $dateId): array`
- Load travel via `TravelLoader->loadById($dateId)` to get `dateFrom/dateTo`.
- Load contingents by hotel.
- Filter within range (inclusive).
- Aggregate per date: `minNights` (min), `total` (sum of available), `capacity` (sum pax).
- **BelKal override**: if BelKal is A/S on a date, mark all rooms on that date as on-request/blocked for outputs.
- `getAvailableRooms(string $dateFrom, string $dateTo, int $hotelId, int $dateId, string $myEpUrl = ''): array`
- Filter contingents by range; build **per date + room** rows.
- Compute `priceForSelection = minPrice + max(0, nights - minNights) * additionalNightMinPrice`.
- Add bookingUrl (see below).
- Add `summer/servicesIncluded/servicesOptional` if travel metadata available; otherwise empty.
- **BelKal override**: if BelKal is A/S on a date, force each rows `status` to ON_REQUEST/BLOCKED (and optionally available to 0 if desired by frontend).
- `getCalendarEvents(int $hotelId, string $dateFrom, string $dateTo): array`
- Aggregate per date:
- Exclude BelKal rows from `pax` and `available`.
- Status is worst-case across **all** rows (BelKal included).
- Return `{ date, status, pax, available }`.
### 5) Booking URL Utility
- Add `src/BusProNet/Utility/BookingUrlUtility.php`.
- URL format: `<base>/bookings/create?date_id={dateId}&hotel_id={hotelId}`.
- Base resolution: use optional `my_ep_url` when provided, otherwise `APP_BASE_URL` (default `https://my.ep-reisen.de`).
### 6) Controller + Routes
**New** `src/Controller/Api/ContingentController.php`
- Base: `#[Route('/api')]` and `#[IsGranted('ROLE_OAUTH2_API')]`.
- Canonical query routes:
1. `/api/contingents/calendar` with query `hotelRef`, `dateFrom`, `dateTo`
2. `/api/contingents` with query `hotelRef`, `dateRef`
3. `/api/contingents/rooms` with query `hotelRef`, `dateRef`, `dateFrom`, `dateTo`
- Reference resolution:
- Numeric `hotelRef` / `dateRef` values are treated as IDs.
- Non-numeric `hotelRef` is mapped as hotel code.
- Non-numeric `dateRef` is sanitized (`-` and `/` removed, uppercase) and mapped as date code.
### 7) Cache Invalidation on Sync
Update `src/Command/BpnXmlSyncCommand.php`:
- Inject `TagAwareCacheInterface $bpnCache` (bind to `@bpn.cache`).
- In `invalidateCaches()`:
- Invalidate `xml-sync` tag after successful sync.
- Sync now covers two datasets (`travel` and `contingents`) in one command run.
## Edge Cases & Failure Modes
- Missing `HotelZimmer_{hotelId}.xml`: loader returns empty array; API returns empty result or 404 (match existing pattern).
- Root-level link missing target file: return empty array (optionally log), no fallback to original.
- Travel not found for `dateId`: return 404.
- Hotel not in travel: optionally validate using TravelLoader mapping and return 404.
- Date parsing: `d.m.Y` -> `\DateTimeImmutable` with server timezone.
## Tests / Scenarios
1. **Parser happy path**: room types, capacities, prices parsed correctly.
2. **Parser room link**: `idbuspro_kontingent_aus` on room type uses linked id for `zimmerliste/zimmer`.
3. **Parser root link**: root `idbuspro_kontingent_aus` loads alternate file exactly once.
4. **Parser BelKal override**:
- BelKal A/S forces all rooms on that date to on-request/blocked.
- Calendar status uses BelKal and sums exclude BelKal.
5. **Service getAvailableContingents**: correct per-date aggregation.
6. **Service getAvailableRooms**: per date + room rows with correct pricing.
7. **Calendar**: worst-case status across rooms, with correct sums excluding BelKal.
8. **Cache**: entries tagged and invalidated after `app:bpn:xml-sync`.
## Performance Notes
- DOM/Crawler parsing should be acceptable for ~7MB XMLs, especially with cached DTOs.
- If XML size grows materially (>20-30MB) or cold-cache concurrency spikes, consider adding an XMLReader streaming parser as a future enhancement.
## Assumptions / Defaults
- `hotelRef` accepts either numeric `hotelId` (`idbuspro`) or hotel code.
- `dateRef` accepts either numeric `dateId` or date code.
- Rooms endpoint returns per date + room rows.
- Return structure follows existing API patterns (`TravelController`, `PickupController`) for HTTP status and JSON formatting.