Skip to content

Commit 2ebcb62

Browse files
authored
Merge pull request #313 from os2display/feature/brnd-feed
BRND Booking
2 parents 05897a5 + e1509fa commit 2ebcb62

File tree

6 files changed

+491
-0
lines changed

6 files changed

+491
-0
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ All notable changes to this project will be documented in this file.
44

55
## [Unreleased]
66

7+
- [#313](https://github.com/os2display/display-api-service/pull/313)
8+
- Add BRND booking feed type
9+
710
## [2.5.2] - 2025-09-25
811

912
- [#260](https://github.com/os2display/display-api-service/pull/260)

src/Feed/BrndFeedType.php

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Feed;
6+
7+
use App\Entity\Tenant\Feed;
8+
use App\Entity\Tenant\FeedSource;
9+
use App\Feed\SourceType\Brnd\ApiClient;
10+
use App\Feed\SourceType\Brnd\SecretsDTO;
11+
use App\Service\FeedService;
12+
use Psr\Cache\CacheItemPoolInterface;
13+
use Psr\Log\LoggerInterface;
14+
use Symfony\Component\HttpFoundation\Request;
15+
16+
/**
17+
* Brnd Bookingsystem Feed.
18+
*
19+
* @see https://brndapi.brnd.com/swagger/index.html
20+
*/
21+
class BrndFeedType implements FeedTypeInterface
22+
{
23+
public const int CACHE_TTL = 3600;
24+
25+
final public const string SUPPORTED_FEED_TYPE = FeedOutputModels::BRND_BOOKING_OUTPUT;
26+
27+
public function __construct(
28+
private readonly FeedService $feedService,
29+
private readonly ApiClient $apiClient,
30+
private readonly CacheItemPoolInterface $feedsCache,
31+
private readonly LoggerInterface $logger,
32+
) {}
33+
34+
public function getAdminFormOptions(FeedSource $feedSource): array
35+
{
36+
$feedEntryRecipients = $this->feedService->getFeedSourceConfigUrl($feedSource, 'sport-center');
37+
38+
return [
39+
[
40+
'key' => 'brnd-sport-center-id',
41+
'input' => 'input',
42+
'type' => 'text',
43+
'name' => 'sport_center_id',
44+
'label' => 'Sportcenter ID',
45+
'formGroupClasses' => 'mb-3',
46+
],
47+
];
48+
}
49+
50+
public function getData(Feed $feed): array
51+
{
52+
$result = [
53+
'title' => 'BRND Booking',
54+
'bookings' => [],
55+
];
56+
57+
try {
58+
$configuration = $feed->getConfiguration();
59+
$feedSource = $feed->getFeedSource();
60+
61+
if (null == $feedSource) {
62+
return $result;
63+
}
64+
65+
$secrets = new SecretsDTO($feedSource);
66+
67+
$baseUri = $secrets->apiBaseUri;
68+
$sportCenterId = $configuration['sport_center_id'] ?? null;
69+
70+
if ('' === $baseUri || null === $sportCenterId || '' === $sportCenterId) {
71+
return $result;
72+
}
73+
74+
$bookings = $this->apiClient->getInfomonitorBookingsDetails($feedSource, $sportCenterId);
75+
76+
$result['bookings'] = array_reduce($bookings, function (array $carry, array $booking): array {
77+
$parsedBooking = $this->parseBrndBooking($booking);
78+
79+
// Validate that booking has required fields
80+
if (!empty($parsedBooking['bookingcode']) && !empty($parsedBooking['bookingBy'])) {
81+
$carry[] = $parsedBooking;
82+
}
83+
84+
return $carry;
85+
}, []);
86+
} catch (\Throwable $throwable) {
87+
$this->logger->error($throwable->getMessage());
88+
// Silently catch all exceptions and return empty result
89+
// $result is already initialized with empty bookings array
90+
}
91+
92+
return $result;
93+
}
94+
95+
private function parseBrndBooking(array $booking): array
96+
{
97+
// Parse start time
98+
$startDateTime = null;
99+
if (!empty($booking['dato']) && isset($booking['starttid']) && is_string($booking['starttid'])) {
100+
try {
101+
// Trim starttime to 6 digits after dot for microseconds
102+
$startTimeString = preg_replace('/\.(\d{6})\d+$/', '.$1', $booking['starttid']);
103+
$dateOnly = substr($booking['dato'], 0, 10);
104+
$dateTimeString = $dateOnly.' '.$startTimeString;
105+
$startDateTime = \DateTimeImmutable::createFromFormat('m/d/Y H:i:s.u', $dateTimeString);
106+
if (false === $startDateTime) {
107+
$startDateTime = null;
108+
}
109+
} catch (\ValueError) {
110+
$startDateTime = null;
111+
}
112+
}
113+
114+
// Parse end time
115+
$endDateTime = null;
116+
if (!empty($booking['dato']) && isset($booking['sluttid']) && is_string($booking['sluttid'])) {
117+
try {
118+
$endTimeString = preg_replace('/\.(\d{6})\d+$/', '.$1', $booking['sluttid']);
119+
$dateOnly = substr($booking['dato'], 0, 10);
120+
$dateTimeString = $dateOnly.' '.$endTimeString;
121+
$endDateTime = \DateTimeImmutable::createFromFormat('m/d/Y H:i:s.u', $dateTimeString);
122+
if (false === $endDateTime) {
123+
$endDateTime = null;
124+
}
125+
} catch (\ValueError) {
126+
$endDateTime = null;
127+
}
128+
}
129+
130+
return [
131+
'bookingcode' => $booking['ansøgning'] ?? '',
132+
'remarks' => $booking['bemærkninger'] ?? '',
133+
'startTime' => $startDateTime ? $startDateTime->getTimestamp() : null,
134+
'endTime' => $endDateTime ? $endDateTime->getTimestamp() : null,
135+
'complex' => $booking['anlæg'] ?? '',
136+
'area' => $booking['område'] ?? '',
137+
'facility' => $booking['facilitet'] ?? '',
138+
'activity' => $booking['aktivitet'] ?? '',
139+
'team' => $booking['hold'] ?? '',
140+
'status' => $booking['status'] ?? '',
141+
'checkIn' => $booking['checK_IN'] ?? '',
142+
'bookingBy' => $booking['ansøgt_af'] ?? '',
143+
'changingRooms' => $booking['omklædningsrum'] ?? '',
144+
];
145+
}
146+
147+
public function getConfigOptions(Request $request, FeedSource $feedSource, string $name): ?array
148+
{
149+
return null;
150+
}
151+
152+
public function getRequiredSecrets(): array
153+
{
154+
return [
155+
'api_base_uri' => [
156+
'type' => 'string',
157+
'exposeValue' => true,
158+
],
159+
'company_id' => [
160+
'type' => 'string',
161+
'exposeValue' => true,
162+
],
163+
'api_auth_key' => [
164+
'type' => 'string',
165+
'exposeValue' => false,
166+
],
167+
];
168+
}
169+
170+
public function getRequiredConfiguration(): array
171+
{
172+
return ['sport_center_id'];
173+
}
174+
175+
public function getSupportedFeedOutputType(): string
176+
{
177+
return self::SUPPORTED_FEED_TYPE;
178+
}
179+
180+
public function getSchema(): array
181+
{
182+
return [
183+
'$schema' => 'http://json-schema.org/draft-04/schema#',
184+
'type' => 'object',
185+
'properties' => [
186+
'api_base_uri' => [
187+
'type' => 'string',
188+
'format' => 'uri',
189+
],
190+
'company_id' => [
191+
'type' => 'string',
192+
],
193+
'api_auth_key' => [
194+
'type' => 'string',
195+
],
196+
],
197+
'required' => ['api_base_uri', 'company_id', 'api_auth_key'],
198+
];
199+
}
200+
201+
public static function getIdKey(FeedSource $feedSource): string
202+
{
203+
$ulid = $feedSource->getId();
204+
assert(null !== $ulid);
205+
206+
return $ulid->toBase32();
207+
}
208+
}

src/Feed/FeedOutputModels.php

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,4 +61,43 @@ class FeedOutputModels
6161
* ]
6262
*/
6363
final public const string RSS_OUTPUT = 'rss';
64+
65+
/**
66+
* Data example:
67+
* [
68+
* {
69+
* activity: "Svømning",
70+
* area: "Svømmehal",
71+
* bookingBy: "Offentlig svømning",
72+
* bookingcode: "BKN-363973",
73+
* changingRooms: "",
74+
* checkIn: "0",
75+
* complex: "Humlehøj Hallen",
76+
* endTime: 1751615100,
77+
* facility: "Svømmehal",
78+
* remarks: "",
79+
* startTime: 1751608800,
80+
* status: "Tildelt tid",
81+
* team: ""
82+
* },
83+
* {
84+
* activity: "Undervisning",
85+
* area: "Mødelokaler",
86+
* bookingBy: "Svømmeklubben Sønderborg",
87+
* bookingcode: "BKN-388946",
88+
* changingRooms: "",
89+
* checkIn: "0",
90+
* complex: "Humlehøj Hallen",
91+
* endTime: 1751641200,
92+
* facility: "Mødelokale 1+2",
93+
* remarks: "",
94+
* startTime: 1751630400,
95+
* status: "Tildelt tid",
96+
* team: ""
97+
* }
98+
* ]
99+
*
100+
* Start/end time are unix timestamps.
101+
*/
102+
final public const string BRND_BOOKING_OUTPUT = 'brnd-booking';
64103
}

0 commit comments

Comments
 (0)