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

GEWIS / sudosos-backend / 18949526732

28 Oct 2025 08:59AM UTC coverage: 89.883% (-0.004%) from 89.887%
18949526732

push

github

web-flow
refactor: remove old deprecated member-authenticator.ts and move to organ membership (#618)

1373 of 1631 branches covered (84.18%)

Branch coverage included in aggregate %.

28 of 29 new or added lines in 6 files covered. (96.55%)

84 existing lines in 11 files now uncovered.

7218 of 7927 relevant lines covered (91.06%)

1112.1 hits per line

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

83.54
/src/controller/debtor-controller.ts
1
/**
2
 *  SudoSOS back-end API service.
3
 *  Copyright (C) 2024  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 debtor-controller.
23
 *
24
 * @module debtors
25
 */
26

27
import BaseController, { BaseControllerOptions } from './base-controller';
2✔
28
import { Response } from 'express';
29
import log4js, { Logger } from 'log4js';
2✔
30
import Policy from './policy';
31
import { RequestWithToken } from '../middleware/token-middleware';
32
import { parseRequestPagination } from '../helpers/pagination';
2✔
33
import DebtorService from '../service/debtor-service';
2✔
34
import User from '../entity/user/user';
2✔
35
import { asArrayOfDates, asArrayOfUserTypes, asDate, asFromAndTillDate, asReturnFileType } from '../helpers/validators';
2✔
36
import { In } from 'typeorm';
2✔
37
import { HandoutFinesRequest } from './request/debtor-request';
38
import Fine from '../entity/fine/fine';
2✔
39
import { ReturnFileType } from 'pdf-generator-client';
40
import { PdfError } from '../errors';
2✔
41
import FineHandoutEvent from '../entity/fine/fineHandoutEvent';
2✔
42

43
export default class DebtorController extends BaseController {
2✔
44
  private logger: Logger = log4js.getLogger(' DebtorController');
2✔
45

46
  public constructor(options: BaseControllerOptions) {
47
    super(options);
2✔
48
    this.logger.level = process.env.LOG_LEVEL;
2✔
49
  }
50

51
  public getPolicy(): Policy {
52
    return {
2✔
53
      '/': {
54
        GET: {
55
          policy: async (req) => this.roleManager.can(req.token.roles, 'get', 'all', 'Fine', ['*']),
3✔
56
          handler: this.returnAllFineHandoutEvents.bind(this),
57
        },
58
      },
59
      '/:id(\\d+)': {
60
        GET: {
61
          policy: async (req) => this.roleManager.can(req.token.roles, 'get', 'all', 'Fine', ['*']),
3✔
62
          handler: this.returnSingleFineHandoutEvent.bind(this),
63
        },
64
      },
65
      '/single/:id(\\d+)': {
66
        DELETE: {
67
          policy: async (req) => this.roleManager.can(req.token.roles, 'delete', 'all', 'Fine', ['*']),
3✔
68
          handler: this.deleteFine.bind(this),
69
        },
70
      },
71
      '/eligible': {
72
        GET: {
73
          policy: async (req) => this.roleManager.can(req.token.roles, 'get', 'all', 'Fine', ['*']),
7✔
74
          handler: this.calculateFines.bind(this),
75
        },
76
      },
77
      '/handout': {
78
        POST: {
79
          policy: async (req) => this.roleManager.can(req.token.roles, 'create', 'all', 'Fine', ['*']),
5✔
80
          handler: this.handoutFines.bind(this),
81
          body: { modelName: 'HandoutFinesRequest' },
82
        },
83
      },
84
      '/handout/:id(\\d+)': {
85
        DELETE: {
86
          policy: async (req) => this.roleManager.can(req.token.roles, 'delete', 'all', 'Fine', ['*']),
3✔
87
          handler: this.deleteFineHandout.bind(this),
88
        },
89
      },
90
      '/notify': {
91
        POST: {
92
          policy: async (req) => this.roleManager.can(req.token.roles, 'notify', 'all', 'Fine', ['*']),
3✔
93
          handler: this.notifyAboutFutureFines.bind(this),
94
          body: { modelName: 'HandoutFinesRequest' },
95
        },
96
      },
97
      '/report': {
98
        GET: {
99
          policy: async (req) => this.roleManager.can(req.token.roles, 'get', 'all', 'Fine', ['*']),
6✔
100
          handler: this.getFineReport.bind(this),
101
        },
102
      },
103
      '/report/pdf': {
104
        GET: {
105
          policy: async (req) => this.roleManager.can(req.token.roles, 'get', 'all', 'Fine', ['*']),
6✔
106
          handler: this.getFineReportPdf.bind(this),
107
        },
108
      },
109
    };
110
  }
111

112
  /**
113
   * GET /fines
114
   * @summary Get all fine handout events
115
   * @tags debtors - Operations of the debtor controller
116
   * @operationId returnAllFineHandoutEvents
117
   * @security JWT
118
   * @param {integer} take.query - How many entries the endpoint should return
119
   * @param {integer} skip.query - How many entries should be skipped (for pagination)
120
   * @return {PaginatedFineHandoutEventResponse} 200 - All existing fine handout events
121
   * @return {string} 400 - Validation error
122
   * @return {string} 500 - Internal server error
123
   */
124
  public async returnAllFineHandoutEvents(req: RequestWithToken, res: Response): Promise<void> {
125
    this.logger.trace('Get all fine handout events by ', req.token.user);
2✔
126

127
    let take;
128
    let skip;
129
    try {
2✔
130
      const pagination = parseRequestPagination(req);
2✔
131
      take = pagination.take;
2✔
132
      skip = pagination.skip;
2✔
133
    } catch (e) {
134
      res.status(400).json(e.message);
×
135
      return;
×
136
    }
137

138
    try {
2✔
139
      res.json(await new DebtorService().getFineHandoutEvents({ take, skip }));
2✔
140
    } catch (error) {
UNCOV
141
      this.logger.error('Could not return all fine handout event:', error);
×
UNCOV
142
      res.status(500).json('Internal server error.');
×
143
    }
144
  }
145

146
  /**
147
   * GET /fines/{id}
148
   * @summary Get all fine handout events
149
   * @tags debtors - Operations of the debtor controller
150
   * @operationId returnSingleFineHandoutEvent
151
   * @security JWT
152
   * @param {integer} id.path.required - The id of the fine handout event which should be returned
153
   * @return {FineHandoutEventResponse} 200 - Requested fine handout event with corresponding fines
154
   * @return {string} 400 - Validation error
155
   * @return {string} 500 - Internal server error
156
   */
157
  public async returnSingleFineHandoutEvent(req: RequestWithToken, res: Response): Promise<void> {
158
    const { id } = req.params;
2✔
159
    this.logger.trace('Get fine handout event', id, 'by', req.token.user);
2✔
160

161
    try {
2✔
162
      res.json(await new DebtorService().getSingleFineHandoutEvent(Number.parseInt(id, 10)));
2✔
163
    } catch (error) {
UNCOV
164
      this.logger.error('Could not return fine handout event:', error);
×
UNCOV
165
      res.status(500).json('Internal server error.');
×
166
    }
167
  }
168

169
  /**
170
   * DELETE /fines/single/{id}
171
   * @summary Delete a fine
172
   * @tags debtors - Operations of the debtor controller
173
   * @operationId deleteFine
174
   * @security JWT
175
   * @param {integer} id.path.required - The id of the fine which should be deleted
176
   * @return 204 - Success
177
   * @return {string} 400 - Validation error
178
   * @return {string} 500 - Internal server error
179
   */
180
  public async deleteFine(req: RequestWithToken, res: Response): Promise<void> {
181
    const { id } = req.params;
2✔
182
    this.logger.trace('Delete fine', id, 'by', req.token.user);
2✔
183

184
    try {
2✔
185
      const parsedId = Number.parseInt(id, 10);
2✔
186
      const fine = await Fine.findOne({ where: { id: parsedId } });
2✔
187
      if (fine == null) {
2✔
188
        res.status(404).send();
1✔
189
        return;
1✔
190
      }
191

192
      await new DebtorService().deleteFine(parsedId);
1✔
193
      res.status(204).send();
1✔
194
    } catch (error) {
UNCOV
195
      this.logger.error('Could not return fine handout event:', error);
×
UNCOV
196
      res.status(500).json('Internal server error.');
×
197
    }
198
  }
199

200
  /**
201
   * GET /fines/eligible
202
   * @summary Return all users that had at most -5 euros balance both now and on the reference date.
203
   *    For all these users, also return their fine based on the reference date.
204
   * @tags debtors - Operations of the debtor controller
205
   * @operationId calculateFines
206
   * @security JWT
207
   * @param {Array<string>} userTypes.query - List of all user types fines should be calculated for (MEMBER, ORGAN, VOUCHER, LOCAL_USER, LOCAL_ADMIN, INVOICE, AUTOMATIC_INVOICE).
208
   * @param {Array<string>} referenceDates.query.required - Dates to base the fines on. Every returned user has at
209
   *    least five euros debt on every reference date. The height of the fine is based on the first date in the array.
210
   * @return {Array<UserToFineResponse>} 200 - List of eligible fines
211
   * @return {string} 400 - Validation error
212
   * @return {string} 500 - Internal server error
213
   */
214
  public async calculateFines(req: RequestWithToken, res: Response): Promise<void> {
215
    this.logger.trace('Get all possible fines by ', req.token.user);
6✔
216

217
    let params;
218
    try {
6✔
219
      if (req.query.referenceDates === undefined) throw new Error('referenceDates is required');
6✔
220
      const referenceDates = asArrayOfDates(req.query.referenceDates);
3✔
221
      if (referenceDates === undefined) throw new Error('referenceDates is not a valid array');
3!
222
      params = {
3✔
223
        userTypes: asArrayOfUserTypes(req.query.userTypes),
224
        referenceDates,
225
      };
226
      if (params.userTypes === undefined && req.query.userTypes !== undefined) throw new Error('userTypes is not a valid array of UserTypes');
3!
227
    } catch (e) {
228
      res.status(400).json(e.message);
3✔
229
      return;
3✔
230
    }
231

232
    try {
3✔
233
      res.json(await new DebtorService().calculateFinesOnDate(params));
3✔
234
    } catch (error) {
UNCOV
235
      this.logger.error('Could not calculate fines:', error);
×
UNCOV
236
      res.status(500).json('Internal server error.');
×
237
    }
238
  }
239

240
  /**
241
   * POST /fines/handout
242
   * @summary Handout fines to all given users. Fines will be handed out "now" to prevent rewriting history.
243
   * @tags debtors - Operations of the debtor controller
244
   * @operationId handoutFines
245
   * @security JWT
246
   * @param {HandoutFinesRequest} request.body.required
247
   * @return {FineHandoutEventResponse} 200 - Created fine handout event with corresponding fines
248
   * @return {string} 400 - Validation error
249
   * @return {string} 500 - Internal server error
250
   */
251
  public async handoutFines(req: RequestWithToken, res: Response): Promise<void> {
252
    const body = req.body as HandoutFinesRequest;
4✔
253
    this.logger.trace('Handout fines', body, 'by user', req.token.user);
4✔
254

255
    let referenceDate: Date;
256
    try {
4✔
257
      // Todo: write code-consistent validator (either /src/controller/request/validators or custom validator.js function)
258
      if (!Array.isArray(body.userIds)) throw new Error('userIds is not an array');
4!
259
      const users = await User.find({ where: { id: In(body.userIds) } });
4✔
260
      if (users.length !== body.userIds.length) throw new Error('userIds is not a valid array of user IDs');
4!
261

262
      if (body.referenceDate !== undefined) {
4✔
263
        referenceDate = asDate(body.referenceDate);
4✔
264
      }
265
    } catch (e) {
266
      res.status(400).json(e.message);
1✔
267
      return;
1✔
268
    }
269

270
    try {
3✔
271
      const result = await new DebtorService().handOutFines({ referenceDate, userIds: body.userIds }, req.token.user);
3✔
272
      res.json(result);
3✔
273
    } catch (error) {
UNCOV
274
      this.logger.error('Could not handout fines:', error);
×
UNCOV
275
      res.status(500).json('Internal server error.');
×
276
    }
277
  }
278

279
  /**
280
   * DELETE /fines/handout/{id}
281
   * @summary Delete a fine handout event
282
   * @tags debtors - Operations of the debtor controller
283
   * @operationId deleteFineHandout
284
   * @security JWT
285
   * @param {integer} id.path.required - The id of the fine handout event which should be deleted
286
   * @return 204 - Success
287
   * @return {string} 400 - Validation error
288
   * @return {string} 404 - Not found error
289
   * @return {string} 500 - Internal server error
290
   */
291
  public async deleteFineHandout(req: RequestWithToken, res: Response): Promise<void> {
292
    const { id } = req.params;
2✔
293
    this.logger.trace('Delete fine handout', id, 'by', req.token.user);
2✔
294

295
    try {
2✔
296
      const parsedId = Number.parseInt(id, 10);
2✔
297
      const event = await FineHandoutEvent.findOne({ where: { id: parsedId }, relations: ['fines'] });
2✔
298
      if (event == null) {
2✔
299
        res.status(404).send();
1✔
300
        return;
1✔
301
      }
302

303
      await new DebtorService().deleteFineHandout(event);
1✔
304
      res.status(204).send();
1✔
305
    } catch (error) {
306
      this.logger.error('Could not delete fine handout:', error);
×
307
      res.status(500).json('Internal server error.');
×
308
    }
309
  }
310

311
  /**
312
   * POST /fines/notify
313
   * @summary Send an email to all given users about their possible future fine.
314
   * @tags debtors - Operations of the debtor controller
315
   * @operationId notifyAboutFutureFines
316
   * @security JWT
317
   * @param {HandoutFinesRequest} request.body.required
318
   * @return 204 - Success
319
   * @return {string} 400 - Validation error
320
   * @return {string} 500 - Internal server error
321
   */
322
  public async notifyAboutFutureFines(req: RequestWithToken, res: Response): Promise<void> {
323
    const body = req.body as HandoutFinesRequest;
2✔
324
    this.logger.trace('Send future fine notification emails', body, 'by user', req.token.user);
2✔
325

326
    let referenceDate: Date;
327
    try {
2✔
328
      // Todo: write code-consistent validator (either /src/controller/request/validators or custom validator.js function)
329
      if (!Array.isArray(body.userIds)) throw new Error('userIds is not an array');
2!
330
      const users = await User.find({ where: { id: In(body.userIds) } });
2✔
331
      if (users.length !== body.userIds.length) throw new Error('userIds is not a valid array of user IDs');
2!
332

333
      if (body.referenceDate !== undefined) {
2✔
334
        referenceDate = asDate(body.referenceDate);
2✔
335
      }
336
    } catch (e) {
337
      res.status(400).json(e.message);
1✔
338
      return;
1✔
339
    }
340

341
    try {
1✔
342
      await new DebtorService().sendFineWarnings({ referenceDate, userIds: body.userIds });
1✔
343
      res.status(204).send();
1✔
344
    } catch (error) {
UNCOV
345
      this.logger.error('Could not send future fine notification emails:', error);
×
UNCOV
346
      res.status(500).json('Internal server error.');
×
347
    }
348
  }
349

350
  /**
351
   * GET /fines/report
352
   * @summary Get a report of all fines
353
   * @tags debtors - Operations of the debtor controller
354
   * @operationId getFineReport
355
   * @security JWT
356
   * @param {string} fromDate.query - The start date of the report, inclusive
357
   * @param {string} toDate.query - The end date of the report, exclusive
358
   * @return {FineReportResponse} 200 - The requested report
359
   * @return {string} 400 - Validation error
360
   * @return {string} 500 - Internal server error
361
   */
362
  public async getFineReport(req: RequestWithToken, res: Response): Promise<void> {
363
    this.logger.trace('Get fine report by ', req.token.user);
5✔
364

365
    let fromDate, toDate;
366
    try {
5✔
367
      const filters = asFromAndTillDate(req.query.fromDate, req.query.toDate);
5✔
368
      fromDate = filters.fromDate;
2✔
369
      toDate = filters.tillDate;
2✔
370
    } catch (e) {
371
      res.status(400).json(e.message);
3✔
372
      return;
3✔
373
    }
374

375
    try {
2✔
376
      const report = await new DebtorService().getFineReport(fromDate, toDate);
2✔
377
      res.json(report.toResponse());
2✔
378
    } catch (error) {
UNCOV
379
      this.logger.error('Could not get fine report:', error);
×
UNCOV
380
      res.status(500).json('Internal server error.');
×
381
    }
382
  }
383

384
  /**
385
   * GET /fines/report/pdf
386
   * @summary Get a report of all fines in pdf format
387
   * @tags debtors - Operations of the debtor controller
388
   * @operationId getFineReportPdf
389
   * @security JWT
390
   * @param {string} fromDate.query.required - The start date of the report, inclusive
391
   * @param {string} toDate.query.required - The end date of the report, exclusive
392
   * @param {string} fileType.query.required - enum:PDF,TEX - The file type of the report
393
   * @returns {string} 200 - The requested report - application/pdf
394
   * @return {string} 400 - Validation error
395
   * @return {string} 500 - Internal server error
396
   */
397
  public async getFineReportPdf(req: RequestWithToken, res: Response): Promise<void> {
398
    this.logger.trace('Get fine report by ', req.token.user);
5✔
399

400
    let fromDate, toDate;
401
    let fileType: ReturnFileType;
402
    try {
5✔
403
      const filters = asFromAndTillDate(req.query.fromDate, req.query.toDate);
5✔
404
      fromDate = filters.fromDate;
2✔
405
      toDate = filters.tillDate;
2✔
406
      fileType = asReturnFileType(req.query.fileType);
2✔
407
    } catch (e) {
408
      res.status(400).json(e.message);
3✔
409
      return;
3✔
410
    }
411

412
    try {
2✔
413
      const report = await new DebtorService().getFineReport(fromDate, toDate);
2✔
414

415
      const buffer = fileType === 'PDF' ? await report.createPdf() : await report.createTex();
2!
416
      const from = `${fromDate.getFullYear()}${fromDate.getMonth() + 1}${fromDate.getDate()}`;
1✔
417
      const to = `${toDate.getFullYear()}${toDate.getMonth() + 1}${toDate.getDate()}`;
1✔
418
      const fileName = `fine-report-${from}-${to}.${fileType}`;
1✔
419

420
      res.setHeader('Content-Type', 'application/pdf+tex');
1✔
421
      res.setHeader('Content-Disposition', `attachment; filename="${fileName}"`);
1✔
422
      res.send(buffer);
1✔
423
    } catch (error) {
424
      this.logger.error('Could not get fine report pdf:', error);
1✔
425
      if (error instanceof PdfError) {
1✔
426
        res.status(502).json('PDF Generator service failed.');
1✔
427
        return;
1✔
428
      }
UNCOV
429
      res.status(500).json('Internal server error.');
×
430
    }
431
  }
432

433
}
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