• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

GEWIS / sudosos-backend / 21110430966

18 Jan 2026 10:44AM UTC coverage: 89.59% (-0.003%) from 89.593%
21110430966

push

github

web-flow
feat: add user settings store (#699)

1671 of 2017 branches covered (82.85%)

Branch coverage included in aggregate %.

71 of 79 new or added lines in 5 files covered. (89.87%)

26 existing lines in 2 files now uncovered.

8682 of 9539 relevant lines covered (91.02%)

1013.72 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

85.81
/src/controller/inactive-administrative-cost-controller.ts
1
/**
2
 *  SudoSOS back-end API service.
3
 *  Copyright (C) 2026 Study association GEWIS
4
 *
5
 *  This program is free software: you can redistribute it and/or modify
6
 *  it under the terms of the GNU Affero General Public License as published
7
 *  by the Free Software Foundation, either version 3 of the License, or
8
 *  (at your option) any later version.
9
 *
10
 *  This program is distributed in the hope that it will be useful,
11
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13
 *  GNU Affero General Public License for more details.
14
 *
15
 *  You should have received a copy of the GNU Affero General Public License
16
 *  along with this program.  If not, see <https://www.gnu.org/licenses/>.
17
 *
18
 *  @license
19
 */
20

21
/**
22
 * This is the module page of the inactive-administrative-cost-controller
23
 * @module internal/controllers
24
 */
25

26
import BaseController, { BaseControllerOptions } from './base-controller';
2✔
27
import log4js, { Logger } from 'log4js';
2✔
28
import { Response } from 'express';
29
import Policy from './policy';
30
import { RequestWithToken } from '../middleware/token-middleware';
31
import { parseRequestPagination } from '../helpers/pagination';
2✔
32
import InactiveAdministrativeCostService, { parseInactiveAdministrativeCostFilterParameters, InactiveAdministrativeCostFilterParameters } from '../service/inactive-administrative-cost-service';
2✔
33
import { PaginatedInactiveAdministrativeCostResponse } from './response/inactive-administrative-cost-response';
34
import { isFail } from '../helpers/specification-validation';
2✔
35
import {
36
  CreateInactiveAdministrativeCostRequest,
37
  HandoutInactiveAdministrativeCostsRequest,
38
} from './request/inactive-administrative-cost-request';
39
import { NotImplementedError } from '../errors';
2✔
40
import verifyValidUserId from './request/validators/inactive-administrative-cost-request-spec';
2✔
41
import InactiveAdministrativeCost from '../entity/transactions/inactive-administrative-cost';
42
import User from '../entity/user/user';
2✔
43
import { In } from 'typeorm';
2✔
44
import { asBoolean, asFromAndTillDate } from '../helpers/validators';
2✔
45
import { PdfError } from '../errors';
2✔
46
import { formatTitleDate } from '../helpers/pdf';
2✔
47

48

49
export default class InactiveAdministrativeCostController extends BaseController {
2✔
50
  /**
51
   * Reference to the logger instance.
52
   */
53
  private logger: Logger = log4js.getLogger('InactiveAdministrativeCostController');
2✔
54

55
  /**
56
   * Creates a new InactiveAdministrativeCost controller instance
57
   * @param options - The options passed to the base controller.
58
   */
59
  public constructor(options: BaseControllerOptions) {
60
    super(options);
2✔
61
    this.logger.level = process.env.LOG_LEVEL;
2✔
62
  }
63

64
  /**
65
     * @inheritDoc
66
     */
67
  getPolicy(): Policy {
68
    return {
2✔
69
      '/': {
70
        GET: {
71
          policy: async (req) => this.roleManager.can(req.token.roles, 'get', 'all', 'InactiveAdministrativeCost', ['*']),
6✔
72
          handler: this.getAllInactiveAdministrativeCosts.bind(this),
73
        },
74
        POST: {
75
          body: { modelName: 'CreateInactiveAdministrativeCostRequest' },
76
          policy: async (req) => this.roleManager.can(req.token.roles, 'create', 'all', 'InactiveAdministrativeCost', ['*']),
4✔
77
          handler: this.createInactiveAdministrativeCost.bind(this),
78
        },
79
      },
80
      '/:id(\\d+)': {
81
        GET: {
82
          policy: async (req) => this.roleManager.can(req.token.roles, 'get', 'all', 'InactiveAdministrativeCost', ['*']),
4✔
83
          handler: this.getSingleInactiveAdministrativeCost.bind(this),
84
        },
85
        DELETE: {
86
          policy: async (req) => this.roleManager.can(req.token.roles, 'delete', 'all', 'InactiveAdministrativeCost', ['*']),
3✔
87
          handler: this.deleteInactiveAdministrativeCost.bind(this),
88
        },
89
      },
90
      '/eligible-users': {
91
        GET: {
92
          policy: async (req) => this.roleManager.can(req.token.roles, 'get', 'all', 'InactiveAdministrativeCost', ['*']),
2✔
93
          handler: this.checkInactiveUsers.bind(this),
94
        },
95
      },
96
      '/notify': {
97
        POST: {
98
          policy: async (req) => this.roleManager.can(req.token.roles, 'notify', 'all', 'InactiveAdministrativeCost', ['*']),
3✔
99
          handler: this.notifyInactiveUsers.bind(this),
100
          body: { modelName: 'HandoutInactiveAdministrativeCostsRequest' },
101
        },
102
      },
103
      '/handout': {
104
        POST: {
105
          policy: async (req) => this.roleManager.can(req.token.roles, 'create', 'all', 'InactiveAdministrativeCost', ['*']),
3✔
106
          handler: this.handoutInactiveAdministrativeCost.bind(this),
107
          body: { modelName: 'HandoutInactiveAdministrativeCostsRequest' },
108
        },
109
      },
110
      '/report': {
111
        GET: {
112
          policy: async (req) => this.roleManager.can(req.token.roles, 'get', 'all', 'InactiveAdministrativeCost', ['*']),
6✔
113
          handler: this.getInactiveAdministrativeCostReport.bind(this),
114
        },
115
      },
116
      '/report/pdf': {
117
        GET: {
118
          policy: async (req) => this.roleManager.can(req.token.roles, 'get', 'all', 'InactiveAdministrativeCost', ['*']),
7✔
119
          handler: this.getInactiveAdministrativeCostReportPdf.bind(this),
120
        },
121
      },
122
    };
123
  }
124

125
  /**
126
   * GET /inactive-administrative-costs
127
   * @summary Returns all inactive administrative costs in the system.
128
   * @operationId getAllInactiveAdministrativeCosts
129
   * @tags inactiveAdministrativeCosts - Operations of the invoices controller
130
   * @security JWT
131
   * @param {integer} fromId.query - Filter on the id of the user
132
   * @param {integer} inactiveAdministrativeCostId.query - Filter on the id of entity
133
   * @return {PaginatedInactiveAdministrativeCostResponse} 200 - All existing inactive administrative costs
134
   * @return {string} 400 - Validation Error
135
   * @return {string} 500 - Internal server error
136
   */
137
  public async getAllInactiveAdministrativeCosts(req: RequestWithToken, res: Response): Promise<void> {
138
    const { body } = req;
5✔
139
    this.logger.trace('Get all inactive administrative costs', body, 'by user', req.token.user);
5✔
140

141
    let take;
142
    let skip;
143
    let filter: InactiveAdministrativeCostFilterParameters;
144

145
    try {
5✔
146
      const pagination = parseRequestPagination(req);
5✔
147
      filter = parseInactiveAdministrativeCostFilterParameters(req);
5✔
148
      take = pagination.take;
4✔
149
      skip = pagination.skip;
4✔
150
    } catch (e) {
151
      res.status(400).send(e.message);
1✔
152
      return;
1✔
153
    }
154

155
    // Handle request
156
    try {
4✔
157
      const inactiveAdministrativeCosts: PaginatedInactiveAdministrativeCostResponse = await new InactiveAdministrativeCostService().getPaginatedInactiveAdministrativeCosts(
4✔
158
        filter, { take, skip },
159
      );
160
      res.json(inactiveAdministrativeCosts);
4✔
161
    } catch (error) {
UNCOV
162
      this.logger.error('Could not return all inactive administrative costs', error);
×
UNCOV
163
      res.status(500).json('Internal server error.');
×
164
    }
165
  }
166

167
  /**
168
   * GET /inactive-administrative-costs/{id}
169
   * @summary Returns a single inactive administrative cost entity
170
   * @operationId getInactiveAdministrativeCosts
171
   * @param {integer} id.path.required - The id of the requested inactive administrative cost
172
   * @tags inactiveAdministrativeCosts - Operations of the invoices controller
173
   * @security JWT
174
   * @return {BaseInactiveAdministrativeCostResponse} 200 - All existing inactive administrative cost
175
   * @return {string} 404 - InactiveAdministrativeCost not found
176
   * @return {string} 500 - Internal server error
177
   */
178
  public async getSingleInactiveAdministrativeCost(req: RequestWithToken, res: Response): Promise<void> {
179
    const { id } = req.params;
3✔
180
    const inactiveAdministrativeCostId = parseInt(id, 10);
3✔
181
    this.logger.trace('Get inactive administrative costs', inactiveAdministrativeCostId, 'by user', req.token.user);
3✔
182

183
    try {
3✔
184
      const inactiveAdministrativeCosts: InactiveAdministrativeCost[] = await new InactiveAdministrativeCostService().getInactiveAdministrativeCosts(
3✔
185
        { inactiveAdministrativeCostId },
186
      );
187

188
      const inactiveAdministrativeCost = inactiveAdministrativeCosts[0];
3✔
189
      if (!inactiveAdministrativeCost) {
3✔
190
        res.status(404).json('Unknown inactive administrative cost ID.');
1✔
191
        return;
1✔
192
      }
193
      const response = InactiveAdministrativeCostService.asInactiveAdministrativeCostResponse(inactiveAdministrativeCost);
2✔
194

195
      res.json(response);
2✔
196
    } catch (error) {
UNCOV
197
      this.logger.error('Could not return inactive administrative cost', error);
×
UNCOV
198
      res.status(500).json('Internal server error.');
×
199
    }
200

201
  }
202

203
  /**
204
   * POST /inactive-administrative-costs
205
   * @summary Adds and inactive administrative cost to the system.
206
   * @operationId createInactiveAdministrativeCosts
207
   * @tags inactiveAdministrativeCosts - Operations of the inactive administrative cost controller
208
   * @security JWT
209
   * @param {CreateInactiveAdministrativeCostRequest} request.body.required -
210
   * The inactive administrative cost which should be created
211
   * @return {BaseInactiveAdministrativeCostResponse} 200 - The created inactive administrative cost entity
212
   * @return {string} 400 - Validation error
213
   * @return {string} 500 - Internal server error
214
   */
215
  public async createInactiveAdministrativeCost(req: RequestWithToken, res: Response): Promise<void> {
216
    const body   = req.body as CreateInactiveAdministrativeCostRequest;
3✔
217
    this.logger.trace('Create InactiveAdministrativeCosts', body, 'by user', req.token.user);
3✔
218

219
    // handle request
220
    try {
3✔
221
      const validation = await verifyValidUserId(body);
3✔
222
      if (isFail(validation)) {
3✔
223
        res.status(400).json(validation.fail.value);
2✔
224
        return;
2✔
225
      }
226

227
      const inactiveAdministrativeCost = await new InactiveAdministrativeCostService().createInactiveAdministrativeCost(body);
1✔
228
      res.json(InactiveAdministrativeCostService.asInactiveAdministrativeCostResponse(inactiveAdministrativeCost));
1✔
229
    } catch (error) {
UNCOV
230
      if (error instanceof NotImplementedError) {
×
UNCOV
231
        res.status(501).json(error.message);
×
UNCOV
232
        return;
×
233
      }
UNCOV
234
      this.logger.error('Could not create inactive administrative cost:', error);
×
UNCOV
235
      res.status(500).json('Internal server error.');
×
236
    }
237
  }
238

239
  /**
240
   * DELETE /inactive-administrative-costs/{id}
241
   * @summary Deletes an inactive administrative cost.
242
   * @operationId deleteInactiveAdministrativeCost
243
   * @tags inactiveAdministrativeCosts - Operations of the inactive administrative cost controller
244
   * @security JWT
245
   * @param {integer} id.path.required - The id of the inactive administrative cost which should be deleted.
246
   * @return {string} 404 - Invoice not found
247
   * @return 204 - Deletion success
248
   * @return {string} 500 - Internal server error
249
   */
250
  public async deleteInactiveAdministrativeCost(req: RequestWithToken, res: Response): Promise<void> {
251
    const { id } = req.params;
2✔
252
    const inactiveAdministrativeCostId = parseInt(id, 10);
2✔
253
    this.logger.trace('Delete inactive administrative costs', inactiveAdministrativeCostId, 'by user', req.token.user);
2✔
254

255
    try {
2✔
256
      const inactiveAdministrativeCost = await new InactiveAdministrativeCostService().deleteInactiveAdministrativeCost(inactiveAdministrativeCostId);
2✔
257
      if (!inactiveAdministrativeCost) {
2✔
258
        res.status(404).json('InactiveAdministrativeCost not found.');
1✔
259
        return;
1✔
260
      }
261
      res.status(204).send();
1✔
262
    } catch (error) {
UNCOV
263
      this.logger.error('Could not delete InactiveAdministrativeCost:', error);
×
UNCOV
264
      res.status(500).json('Internal server error.');
×
265
    }
266
  }
267

268
  /**
269
   * GET /inactive-administrative-costs/eligible-users
270
   * @summary Find all users who are eligible for notification or creation of inactive administrative cost
271
   * @operationId getInactiveAdministrativeCostsEligibleUsers
272
   * @tags inactiveAdministrativeCosts - Operations of the inactive administrative cost controller
273
   * @security JWT
274
   * @param {boolean} notification.query - Whether to check for notification or for fine.
275
   * @return {Array<UserToInactiveAdministrativeCostResponse>} 200 - List of eligible users
276
   * @return {string} 500 - Internal server error
277
   */
278
  public async checkInactiveUsers(req: RequestWithToken, res: Response): Promise<void> {
279
    const { body } = req;
1✔
280
    this.logger.trace('Check Inactive Users', body, 'by user', req.token.user);
1✔
281

282
    try {
1✔
283
      const notification = asBoolean(req.query.notification);
1✔
284

285
      const usersResponses = await new InactiveAdministrativeCostService().checkInactiveUsers({ notification });
1✔
286

287
      res.json(usersResponses);
1✔
288
    } catch (error) {
UNCOV
289
      this.logger.error('Could not check inactive users:', error);
×
UNCOV
290
      res.status(500).json('Internal server error.');
×
291
    }
292
  }
293

294
  /**
295
   * POST /inactive-administrative-costs/notify
296
   * @summary Notify all users which will pay administrative costs within a year
297
   * @operationId notifyInactiveAdministrativeCostsUsers
298
   * @tags inactiveAdministrativeCosts - Operations of the inactive administrative cost controller
299
   * @security JWT
300
   * @param {HandoutInactiveAdministrativeCostsRequest} request.body.required -
301
   * The users that should be notified
302
   * @return 204 - Success
303
   * @return {string} 400 - Validation error
304
   * @return {string} 500 - Internal server error
305
   */
306
  public async notifyInactiveUsers(req: RequestWithToken, res: Response): Promise<void> {
307
    const body = req.body as HandoutInactiveAdministrativeCostsRequest;
2✔
308
    this.logger.trace('Notify Inactive Users', body, 'by user', req.token.user);
2✔
309

310
    try {
2✔
311
      if (!Array.isArray(body.userIds)) throw new Error('userIds is not an Array.');
2!
312
      
313
      const users = await User.find({ where: { id: In(body.userIds) } });
2✔
314
      if (users.length !== body.userIds.length) throw new Error('userIds is not a valid array of user IDs');
2✔
315
    } catch (error) {
316
      res.status(400).json(error.message);
1✔
317
      return ;
1✔
318
    }
319

320
    try {
1✔
321
      await new InactiveAdministrativeCostService().sendInactiveNotification(body);
1✔
322
      res.status(204).send();
1✔
323
    } catch (error) {
UNCOV
324
      this.logger.error('Could not check inactive users:', error);
×
UNCOV
325
      res.status(500).json('Internal server error.');
×
326
    }
327
  }
328

329
  /**
330
   * POST /inactive-administrative-costs/handout
331
   * @summary Handout inactive administrative costs to all users who are eligible.
332
   * @operationId handoutInactiveAdministrativeCostsUsers
333
   * @tags inactiveAdministrativeCosts - Operations of the inactive administrative cost controller
334
   * @security JWT
335
   * @param {HandoutInactiveAdministrativeCostsRequest} request.body.required -
336
   * The users that should be fined
337
   * @return 204 - Success
338
   * @return {string} 400 - Validation error
339
   * @return {string} 500 - Internal server error
340
   */
341
  public async handoutInactiveAdministrativeCost(req: RequestWithToken, res: Response): Promise<void> {
342
    const body = req.body as HandoutInactiveAdministrativeCostsRequest;
2✔
343
    this.logger.trace('Handout InactiveAdministrativeCosts', body, 'by user', req.token.user);
2✔
344

345
    try {
2✔
346
      if (!Array.isArray(body.userIds)) throw new Error('userIds is not an Array.');
2!
347

348
      const users = await User.find({ where: { id: In(body.userIds) } });
2✔
349
      if (users.length !== body.userIds.length) throw new Error('userIds is not a valid array of user IDs');
2✔
350
    } catch (error) {
351
      res.status(400).json(error.message);
1✔
352
      return ;
1✔
353
    }
354

355
    try {
1✔
356
      const inactiveAdministrativeCosts = await new InactiveAdministrativeCostService().handOutInactiveAdministrativeCost(body);
1✔
357
      const response = InactiveAdministrativeCostService.toArrayResponse(inactiveAdministrativeCosts);
1✔
358

359
      res.status(200).send(response);
1✔
360
    } catch (error) {
UNCOV
361
      this.logger.error('Could not check inactive users:', error);
×
UNCOV
362
      res.status(500).json('Internal server error.');
×
363
    }
364
  }
365

366
  /**
367
   * GET /inactive-administrative-costs/report
368
   * @summary Get a report of all inactive administrative costs
369
   * @operationId getInactiveAdministrativeCostReport
370
   * @tags inactiveAdministrativeCosts - Operations of the inactive administrative cost controller
371
   * @security JWT
372
   * @param {string} fromDate.query - The start date of the report, inclusive
373
   * @param {string} toDate.query - The end date of the report, exclusive
374
   * @return {InactiveAdministrativeCostReportResponse} 200 - The requested report
375
   * @return {string} 400 - Validation error
376
   * @return {string} 500 - Internal server error
377
   */
378
  public async getInactiveAdministrativeCostReport(req: RequestWithToken, res: Response): Promise<void> {
379
    this.logger.trace('Get inactive administrative cost report by user', req.token.user);
5✔
380

381
    let fromDate, toDate;
382
    try {
5✔
383
      const filters = asFromAndTillDate(req.query.fromDate, req.query.toDate);
5✔
384
      fromDate = filters.fromDate;
2✔
385
      toDate = filters.tillDate;
2✔
386
    } catch (e) {
387
      res.status(400).json(e.message);
3✔
388
      return;
3✔
389
    }
390

391
    try {
2✔
392
      const report = await new InactiveAdministrativeCostService().getInactiveAdministrativeCostReport(fromDate, toDate);
2✔
393
      res.json(report.toResponse());
2✔
394
    } catch (error) {
UNCOV
395
      this.logger.error('Could not get inactive administrative cost report:', error);
×
UNCOV
396
      res.status(500).json('Internal server error.');
×
397
    }
398
  }
399

400
  /**
401
   * GET /inactive-administrative-costs/report/pdf
402
   * @summary Get a report of all inactive administrative costs in pdf format
403
   * @operationId getInactiveAdministrativeCostReportPdf
404
   * @tags inactiveAdministrativeCosts - Operations of the inactive administrative cost controller
405
   * @security JWT
406
   * @param {string} fromDate.query.required - The start date of the report, inclusive
407
   * @param {string} toDate.query.required - The end date of the report, exclusive
408
   * @returns {string} 200 - The requested report - application/pdf
409
   * @return {string} 400 - Validation error
410
   * @return {string} 502 - PDF Generator service failed
411
   * @return {string} 500 - Internal server error
412
   */
413
  public async getInactiveAdministrativeCostReportPdf(req: RequestWithToken, res: Response): Promise<void> {
414
    this.logger.trace('Get inactive administrative cost report PDF by user', req.token.user);
6✔
415

416
    let fromDate, toDate;
417
    try {
6✔
418
      const filters = asFromAndTillDate(req.query.fromDate, req.query.toDate);
6✔
419
      fromDate = filters.fromDate;
3✔
420
      toDate = filters.tillDate;
3✔
421
    } catch (e) {
422
      res.status(400).json(e.message);
3✔
423
      return;
3✔
424
    }
425

426
    try {
3✔
427
      const report = await new InactiveAdministrativeCostService().getInactiveAdministrativeCostReport(fromDate, toDate);
3✔
428

429
      const pdf = await report.createPdf();
3✔
430
      const from = formatTitleDate(fromDate);
1✔
431
      const to = formatTitleDate(toDate);
1✔
432
      const fileName = `inactive-cost-report-${from}-${to}.pdf`;
1✔
433

434
      res.setHeader('Content-Type', 'application/pdf');
1✔
435
      res.setHeader('Content-Disposition', `attachment; filename="${fileName}"`);
1✔
436
      res.status(200).send(pdf);
1✔
437
    } catch (error) {
438
      this.logger.error('Could not get inactive administrative cost report PDF:', error);
2✔
439
      if (error instanceof PdfError) {
2✔
440
        res.status(502).json('PDF Generator service failed.');
1✔
441
        return;
1✔
442
      }
443
      res.status(500).json('Internal server error.');
1✔
444
    }
445
  }
446

447

448
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc