9.0 KiB
9.0 KiB
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.cachewithxml-synctagging strategy. - In progress: enrich rooms payload with booking URL and additional travel metadata fields.
Public API / Interface Changes
- New endpoints (OAuth-protected):
GET /api/contingents?hotelRef={hotelRef}&dateRef={dateRef}- Returns: object keyed by date with
{ date, minNights, total, capacity }.
- Returns: object keyed by date with
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.
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).
- Returns per date
- New config:
.env:XML_EXPORT_CONTINGENTS_PATH="%kernel.project_dir%/var/xmlexportzimmer"config/packages/flysystem.yaml:xml_export_contingents.storageandxml_source_contingents.storage.
- New cache pool usage:
bpn.cacheused for contingent file map + hotel cache entries, tagged withxml-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
- Add
XML_EXPORT_CONTINGENTS_PATHto.env. - Add Flysystem storage in
config/packages/flysystem.yaml:xml_export_contingents.storagewithdirectory: '%env(resolve:XML_EXPORT_CONTINGENTS_PATH)%'.xml_source_contingents.storagefor SFTP contingent source.
- Wire service injection in
config/services.yaml:- Bind
xml_export_contingents.storageintoContingentLoader. - Inject
@bpn.cacheexplicitly intoContingentLoaderandBpnXmlSyncCommand.
- Bind
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,roomLabelpax,available,status(OK|ON_REQUEST|BLOCKED)minPrice,minNightsadditionalNightMinPrice,additionalNightMinNights
- Room types:
- 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, resolvezimmerliste/zimmerby id:- Use
roomType.link ?? roomType.idBusPro.
- Use
- BelKal rows:
- status
A=> ON_REQUEST - status
S=> BLOCKED
- status
- Non-BelKal rows:
pax = kontingent * pax_maxavailable = frei * pax_max
- Room types from
- 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_*.xmlinxml_export_zimmer.storage. - Build mapping
hotelId => filename. - Cache as
bpn_contingent_filesfor 3h onbpn.cache.
- List
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.cacheascontingent_hotel_{hotelId}TTL 3600, taggedxml-sync.
- Use file map to find filename; return
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 getdateFrom/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.
- Load travel via
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/servicesOptionalif travel metadata available; otherwise empty. - BelKal override: if BelKal is A/S on a date, force each row’s
statusto 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
paxandavailable. - Status is worst-case across all rows (BelKal included).
- Exclude BelKal rows from
- Return
{ date, status, pax, available }.
- Aggregate per date:
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_urlwhen provided, otherwiseAPP_BASE_URL(defaulthttps://my.ep-reisen.de).
6) Controller + Routes
New src/Controller/Api/ContingentController.php
- Base:
#[Route('/api')]and#[IsGranted('ROLE_OAUTH2_API')]. - Canonical query routes:
/api/contingents/calendarwith queryhotelRef,dateFrom,dateTo/api/contingentswith queryhotelRef,dateRef/api/contingents/roomswith queryhotelRef,dateRef,dateFrom,dateTo
- Reference resolution:
- Numeric
hotelRef/dateRefvalues are treated as IDs. - Non-numeric
hotelRefis mapped as hotel code. - Non-numeric
dateRefis sanitized (-and/removed, uppercase) and mapped as date code.
- Numeric
7) Cache Invalidation on Sync
Update src/Command/BpnXmlSyncCommand.php:
- Inject
TagAwareCacheInterface $bpnCache(bind to@bpn.cache). - In
invalidateCaches():- Invalidate
xml-synctag after successful sync. - Sync now covers two datasets (
travelandcontingents) in one command run.
- Invalidate
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->\DateTimeImmutablewith server timezone.
Tests / Scenarios
- Parser – happy path: room types, capacities, prices parsed correctly.
- Parser – room link:
idbuspro_kontingent_auson room type uses linked id forzimmerliste/zimmer. - Parser – root link: root
idbuspro_kontingent_ausloads alternate file exactly once. - Parser – BelKal override:
- BelKal A/S forces all rooms on that date to on-request/blocked.
- Calendar status uses BelKal and sums exclude BelKal.
- Service – getAvailableContingents: correct per-date aggregation.
- Service – getAvailableRooms: per date + room rows with correct pricing.
- Calendar: worst-case status across rooms, with correct sums excluding BelKal.
- 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
hotelRefaccepts either numerichotelId(idbuspro) or hotel code.dateRefaccepts either numericdateIdor date code.- Rooms endpoint returns per date + room rows.
- Return structure follows existing API patterns (
TravelController,PickupController) for HTTP status and JSON formatting.