22
33declare (strict_types=1 );
44
5+ // SPDX-FileCopyrightText: Chen Asraf <contact@casraf.dev>
6+ // SPDX-License-Identifier: AGPL-3.0-or-later
7+
58namespace OCA \AutoCurrency \Controller ;
69
10+ use DateTimeImmutable ;
711use OCA \AutoCurrency \AppInfo ;
12+ use OCA \AutoCurrency \Db \AutocurrencyRateHistoryMapper ;
13+ use OCA \AutoCurrency \Db \CospendProjectMapper ;
14+ use OCA \AutoCurrency \Db \CurrencyMapper ;
815use OCA \AutoCurrency \Service \FetchCurrenciesService ;
916use OCP \AppFramework \Http ;
1017use OCP \AppFramework \Http \Attribute \ApiRoute ;
1926 * @psalm-suppress UnusedClass
2027 */
2128class ApiController extends OCSController {
22- /** @var IAppConfig */
23- private $ config ;
24-
25- /** @var IL10N */
26- private $ l ;
27-
28- /** @var FetchCurrenciesService */
29- private $ service ;
30-
3129 /**
3230 * Admin constructor.
3331 *
@@ -40,9 +38,12 @@ class ApiController extends OCSController {
4038 public function __construct (
4139 string $ appName ,
4240 IRequest $ request ,
43- IAppConfig $ config ,
44- IL10N $ l ,
45- FetchCurrenciesService $ service ,
41+ private IAppConfig $ config ,
42+ private IL10N $ l ,
43+ private FetchCurrenciesService $ service ,
44+ private CurrencyMapper $ currencyMapper ,
45+ private CospendProjectMapper $ projectMapper ,
46+ private AutocurrencyRateHistoryMapper $ historyMapper ,
4647 ) {
4748 parent ::__construct ($ appName , $ request );
4849 $ this ->config = $ config ;
@@ -118,4 +119,125 @@ public function updateSettings(mixed $data): DataResponse {
118119 ['status ' => 'OK ' ]
119120 );
120121 }
122+
123+ /**
124+ * List Cospend projects
125+ *
126+ * @return DataResponse<Http::STATUS_OK, array{
127+ * projects: list<array{id:string,name:string,currencyName:string|null}>
128+ * }, array{}>
129+ *
130+ * 200: Data returned
131+ */
132+ #[ApiRoute(verb: 'GET ' , url: '/api/projects ' )]
133+ public function getProjects (): DataResponse {
134+ $ projects = $ this ->projectMapper ->findAll ();
135+
136+ $ list = [];
137+ foreach ($ projects as $ p ) {
138+ $ name = (string )$ p ->getName ();
139+ $ id = (string )$ p ->getId ();
140+ $ currencyName = (string )$ p ->getCurrencyName ();
141+ $ currencies = $ this ->currencyMapper ->findAll ($ id );
142+ $ currencyNames = array_map (fn ($ c ) => strtolower ((string )$ c ->getName ()), $ currencies );
143+
144+ $ list [] = [
145+ 'id ' => $ id ,
146+ 'name ' => $ name !== '' ? $ name : $ id ,
147+ 'baseCurrency ' => $ currencyName ,
148+ 'currencies ' => $ currencyNames ,
149+ ];
150+ }
151+
152+ return new DataResponse (['projects ' => $ list ]);
153+ }
154+
155+ /**
156+ * Get rate history for a project (uses the project's base currency)
157+ *
158+ * @param string $projectId Project ID (required)
159+ * @param string|null $currency Quoted currency code to filter (e.g. "eur")
160+ * @param string|null $from ISO-8601 datetime (inclusive)
161+ * @param string|null $to ISO-8601 datetime (inclusive)
162+ * @param int|null $limit Max rows to return (optional)
163+ * @param int|null $offset Offset for pagination (optional)
164+ *
165+ * @return DataResponse<Http::STATUS_OK, array{
166+ * projectId: string,
167+ * baseCurrency: string,
168+ * points: list<array{
169+ * fetchedAt: string,
170+ * rate: string,
171+ * currencyName: string,
172+ * source: string|null
173+ * }>
174+ * }, array{}>
175+ *
176+ * 200: Data returned
177+ */
178+ #[ApiRoute(verb: 'GET ' , url: '/api/history ' )]
179+ public function getHistory (
180+ string $ projectId ,
181+ ?string $ currency = null ,
182+ ?string $ from = null ,
183+ ?string $ to = null ,
184+ ?int $ limit = null ,
185+ ?int $ offset = null ,
186+ ): DataResponse {
187+ if ($ projectId === '' ) {
188+ return new DataResponse (['error ' => 'projectId is required ' ], Http::STATUS_BAD_REQUEST );
189+ }
190+
191+ // Parse dates if provided (ISO-8601). If invalid, treat as null.
192+ // If "to" is a DATE ONLY (no time), shift it to end-of-day 23:59:59.
193+ $ fromDt = null ;
194+ $ toDt = null ;
195+ try {
196+ if (is_string ($ from ) && $ from !== '' ) {
197+ $ fromDt = new DateTimeImmutable ($ from );
198+ }
199+ if (is_string ($ to ) && $ to !== '' ) {
200+ // Date-only? e.g. "2025-09-25"
201+ if (preg_match ('/^\d{4}-\d{2}-\d{2}$/ ' , $ to ) === 1 ) {
202+ $ toDt = new DateTimeImmutable ($ to . ' 23:59:59 ' );
203+ } else {
204+ $ toDt = new DateTimeImmutable ($ to );
205+ }
206+ }
207+ } catch (\Throwable $ e ) {
208+ // ignore parsing errors; nulls mean "no bound"
209+ }
210+
211+ // Resolve project and its base currency
212+ $ project = $ this ->projectMapper ->find ($ projectId );
213+ $ projectBase = $ project ->getCurrencyName ();
214+ $ lbase = strtolower ((string )$ projectBase );
215+
216+ $ rows = $ this ->historyMapper ->findByProjectAndBase (
217+ projectId: $ projectId ,
218+ baseCurrency: $ lbase ,
219+ currencyName: is_string ($ currency ) && $ currency !== '' ? strtolower ($ currency ) : null ,
220+ from: $ fromDt ,
221+ to: $ toDt ,
222+ limit: (int )($ limit ?? 0 ),
223+ offset: (int )($ offset ?? 0 ),
224+ order: 'ASC '
225+ );
226+
227+ $ points = array_map (static function ($ row ) {
228+ /** @var \OCA\AutoCurrency\Db\AutocurrencyRateHistory $row */
229+ return [
230+ 'fetchedAt ' => $ row ->getFetchedAt () ? $ row ->getFetchedAt ()->format (DATE_ATOM ) : null ,
231+ 'rate ' => $ row ->getRate (),
232+ 'currencyName ' => $ row ->getCurrencyName (),
233+ 'source ' => $ row ->getSource (),
234+ ];
235+ }, $ rows );
236+
237+ return new DataResponse ([
238+ 'projectId ' => $ projectId ,
239+ 'baseCurrency ' => $ lbase ,
240+ 'points ' => $ points ,
241+ ]);
242+ }
121243}
0 commit comments