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

9.0 KiB
Raw Blame History

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.