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

GEWIS / sudosos-backend / 25753937432

12 May 2026 09:17AM UTC coverage: 88.117% (-1.0%) from 89.089%
25753937432

push

github

web-flow
chore(deps): fix missing dependencies for running docs:dev (#911)

3925 of 4574 branches covered (85.81%)

Branch coverage included in aggregate %.

20093 of 22683 relevant lines covered (88.58%)

1125.83 hits per line

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

90.14
/src/controller/transfer-controller.ts
1
/**
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
 */
1✔
20

21
/**
1✔
22
 * This is the module page of the transfer-controller.
23
 *
24
 * @module transfers
25
 */
1✔
26

27
import { Response } from 'express';
28
import log4js, { Logger } from 'log4js';
29
import BaseController, { BaseControllerOptions } from './base-controller';
30
import Policy from './policy';
31
import { RequestWithToken } from '../middleware/token-middleware';
32
import TransferService, { parseGetTransferAggregateFilters, parseGetTransferFilters, parseGetTransferSummaryFilters } from '../service/transfer-service';
33
import TransferRequest from './request/transfer-request';
34
import Transfer from '../entity/transactions/transfer';
35
import { parseRequestPagination, toResponse } from '../helpers/pagination';
36
import userTokenInOrgan from '../helpers/token-helper';
37
import { PdfError } from '../errors';
38

39
export default class TransferController extends BaseController {
1✔
40
  private logger: Logger = log4js.getLogger('TransferController');
1✔
41

42
  /**
1✔
43
   * Creates a new transfer controller instance.
44
   * @param options - The options passed to the base controller.
45
   */
1✔
46
  public constructor(options: BaseControllerOptions) {
1✔
47
    super(options);
2✔
48
    this.configureLogger(this.logger);
2✔
49
  }
2✔
50

51
  getPolicy(): Policy {
1✔
52
    return {
2✔
53
      '/aggregate': {
2✔
54
        GET: {
2✔
55
          policy: async (req) => this.roleManager.can(req.token.roles, 'get', 'all', 'Transfer', ['*']),
2✔
56
          handler: this.returnTransferAggregate.bind(this),
2✔
57
        },
2✔
58
      },
2✔
59
      '/summary': {
2✔
60
        GET: {
2✔
61
          policy: async (req) => this.roleManager.can(req.token.roles, 'get', 'all', 'Transfer', ['*']),
2✔
62
          handler: this.returnTransferSummary.bind(this),
2✔
63
        },
2✔
64
      },
2✔
65
      '/': {
2✔
66
        GET: {
2✔
67
          policy: async (req) => this.roleManager.can(req.token.roles, 'get', 'all', 'Transfer', ['*']),
2✔
68
          handler: this.returnAllTransfers.bind(this),
2✔
69
        },
2✔
70
        POST: {
2✔
71
          body: { modelName: 'TransferRequest' },
2✔
72
          policy: async (req) => this.roleManager.can(req.token.roles, 'create', 'all', 'Transfer', ['*']),
2✔
73
          handler: this.postTransfer.bind(this),
2✔
74
        },
2✔
75
      },
2✔
76
      '/:id(\\d+)': {
2✔
77
        GET: {
2✔
78
          policy: async (req) => this.roleManager.can(req.token.roles, 'get', await TransferController.getRelation(req), 'Transfer', ['*']),
2✔
79
          handler: this.returnTransfer.bind(this),
2✔
80
        },
2✔
81
        DELETE: {
2✔
82
          policy: async (req) => this.roleManager.can(req.token.roles, 'delete', await TransferController.getRelation(req), 'Transfer', ['*']),
2✔
83
          handler: this.deleteTransfer.bind(this),
2✔
84
        },
2✔
85
      },
2✔
86
      '/:id(\\d+)/pdf': {
2✔
87
        GET: {
2✔
88
          policy: async (req) => this.roleManager.can(req.token.roles, 'get', await TransferController.getRelation(req), 'Transfer', ['*']),
2✔
89
          handler: this.getTransferPdf.bind(this),
2✔
90
        },
2✔
91
      },
2✔
92
    };
2✔
93
  }
2✔
94

95
  /**
1✔
96
   * Function to determine which credentials are needed to get transaction
97
   *        all if user is not connected to transaction
98
   *        own if user is connected to transaction
99
   *        organ if user is connected to transaction via organ
100
   * @param req
101
   * @return whether transaction is connected to used token
102
   */
1✔
103
  static async getRelation(req: RequestWithToken): Promise<string> {
1✔
104
    const transfer = await Transfer.findOne({ where: { id: parseInt(req.params.id, 10) }, relations: ['to', 'from'] });
21✔
105
    if (!transfer) return 'all';
21✔
106
    const fromId = transfer.from != null ? transfer.from.id : undefined;
21✔
107
    const toId = transfer.to != null ? transfer.to.id : undefined;
21✔
108
    if (userTokenInOrgan(req, fromId) || userTokenInOrgan(req, toId)) return 'organ';
21✔
109
    if (transfer
16✔
110
      && (fromId === req.token.user.id
16✔
111
      || toId === req.token.user.id)) {
21✔
112
      return 'own';
9✔
113
    }
9✔
114
    return 'all';
7✔
115
  }
7✔
116

117
  /**
1✔
118
   * GET /transfers/aggregate
119
   * @summary Returns the aggregate (sum and count) of transfers matching the given filters
120
   * @operationId getTransferAggregate
121
   * @tags transfers - Operations of transfer controller
122
   * @security JWT
123
   * @param {string} fromDate.query - Start date for selected transfers (inclusive)
124
   * @param {string} tillDate.query - End date for selected transfers (exclusive)
125
   * @param {integer} fromId.query - Filter transfers from this user ID
126
   * @param {integer} toId.query - Filter transfers to this user ID
127
   * @param {string} category.query - Restrict to a specific transfer category: deposit, payoutRequest, sellerPayout, invoice, creditInvoice, fine, waivedFines, writeOff, inactiveAdministrativeCost
128
   * @return {TransferAggregateResponse} 200 - Aggregate sum and count of matching transfers
129
   * @return {string} 400 - Validation error
130
   * @return {string} 500 - Internal server error
131
   */
1✔
132
  public async returnTransferAggregate(req: RequestWithToken, res: Response): Promise<void> {
1✔
133
    this.logger.trace('Get transfer aggregate by user', req.token.user);
11✔
134

135
    let filters;
11✔
136
    try {
11✔
137
      filters = parseGetTransferAggregateFilters(req);
11✔
138
    } catch (e) {
11✔
139
      res.status(400).send(e.message);
1✔
140
      return;
1✔
141
    }
1✔
142

143
    try {
10✔
144
      const { total, count } = await new TransferService().getTransferAggregate(filters);
10✔
145
      res.json({ total: total.toObject(), count });
10✔
146
    } catch (error) {
11!
147
      this.logger.error('Could not return transfer aggregate:', error);
×
148
      res.status(500).json('Internal server error.');
×
149
    }
×
150
  }
11✔
151

152
  /**
1✔
153
   * GET /transfers/summary
154
   * @summary Returns an aggregate breakdown of transfers for every category plus an overall total
155
   * @operationId getTransferSummary
156
   * @tags transfers - Operations of transfer controller
157
   * @security JWT
158
   * @param {string} fromDate.query - Start date for selected transfers (inclusive)
159
   * @param {string} tillDate.query - End date for selected transfers (exclusive)
160
   * @param {integer} fromId.query - Filter transfers from this user ID
161
   * @param {integer} toId.query - Filter transfers to this user ID
162
   * @return {TransferSummaryResponse} 200 - Per-category aggregate sums and counts
163
   * @return {string} 400 - Validation error
164
   * @return {string} 500 - Internal server error
165
   */
1✔
166
  public async returnTransferSummary(req: RequestWithToken, res: Response): Promise<void> {
1✔
167
    this.logger.trace('Get transfer summary by user', req.token.user);
4✔
168

169
    let filters;
4✔
170
    try {
4✔
171
      filters = parseGetTransferSummaryFilters(req);
4✔
172
    } catch (e) {
4!
173
      res.status(400).send(e.message);
×
174
      return;
×
175
    }
×
176

177
    try {
4✔
178
      const summary = await new TransferService().getTransferSummary(filters);
4✔
179
      res.json({
4✔
180
        total: { total: summary.total.total.toObject(), count: summary.total.count },
4✔
181
        deposits: { total: summary.deposits.total.toObject(), count: summary.deposits.count },
4✔
182
        payoutRequests: { total: summary.payoutRequests.total.toObject(), count: summary.payoutRequests.count },
4✔
183
        sellerPayouts: { total: summary.sellerPayouts.total.toObject(), count: summary.sellerPayouts.count },
4✔
184
        invoices: { total: summary.invoices.total.toObject(), count: summary.invoices.count },
4✔
185
        creditInvoices: { total: summary.creditInvoices.total.toObject(), count: summary.creditInvoices.count },
4✔
186
        fines: { total: summary.fines.total.toObject(), count: summary.fines.count },
4✔
187
        waivedFines: { total: summary.waivedFines.total.toObject(), count: summary.waivedFines.count },
4✔
188
        writeOffs: { total: summary.writeOffs.total.toObject(), count: summary.writeOffs.count },
4✔
189
        inactiveAdministrativeCosts: { total: summary.inactiveAdministrativeCosts.total.toObject(), count: summary.inactiveAdministrativeCosts.count },
4✔
190
        manualCreations: { total: summary.manualCreations.total.toObject(), count: summary.manualCreations.count },
4✔
191
        manualDeletions: { total: summary.manualDeletions.total.toObject(), count: summary.manualDeletions.count },
4✔
192
      });
4✔
193
    } catch (error) {
4!
194
      this.logger.error('Could not return transfer summary:', error);
×
195
      res.status(500).json('Internal server error.');
×
196
    }
×
197
  }
4✔
198

199
  /**
1✔
200
   * GET /transfers
201
   * @summary Returns all existing transfers
202
   * @operationId getAllTransfers
203
   * @tags transfers - Operations of transfer controller
204
   * @security JWT
205
   * @param {string} fromDate.query - Start date for selected transfers (inclusive)
206
   * @param {string} tillDate.query - End date for selected transfers (exclusive)
207
   * @param {integer} fromId.query - Filter transfers from this user ID
208
   * @param {integer} toId.query - Filter transfers to this user ID
209
   * @param {string} category.query - Restrict to a specific transfer category: deposit, payoutRequest, sellerPayout, invoice, creditInvoice, fine, waivedFines, writeOff, inactiveAdministrativeCost, manualCreation, manualDeletion
210
   * @param {integer} take.query - How many transfers the endpoint should return
211
   * @param {integer} skip.query - How many transfers should be skipped (for pagination)
212
   * @return {PaginatedTransferResponse} 200 - All existing transfers
213
   * @return {string} 400 - Validation error
214
   * @return {string} 500 - Internal server error
215
   */
1✔
216
  public async returnAllTransfers(req: RequestWithToken, res: Response): Promise<void> {
1✔
217
    const { body } = req;
14✔
218
    this.logger.trace('Get all transfers by user', body, 'by user', req.token.user);
14✔
219

220
    let filters;
14✔
221
    let take;
14✔
222
    let skip;
14✔
223
    try {
14✔
224
      filters = parseGetTransferFilters(req);
14✔
225
      const pagination = parseRequestPagination(req);
14✔
226
      take = pagination.take;
14✔
227
      skip = pagination.skip;
14✔
228
    } catch (e) {
14✔
229
      res.status(400).send(e.message);
3✔
230
      return;
3✔
231
    }
3✔
232

233
    try {
11✔
234
      const [transfers, count] = await new TransferService().getTransfers(filters, { take, skip });
11✔
235
      const records = transfers.map((t) => TransferService.asTransferResponse(t));
11✔
236
      res.json(toResponse(records, count, { take, skip }));
11✔
237
    } catch (error) {
14!
238
      this.logger.error('Could not return all transfers:', error);
×
239
      res.status(500).json('Internal server error.');
×
240
    }
×
241
  }
14✔
242

243
  /**
1✔
244
   * GET /transfers/{id}
245
   * @summary Returns the requested transfer
246
   * @operationId getSingleTransfer
247
   * @tags transfers - Operations of transfer controller
248
   * @param {integer} id.path.required - The id of the transfer which should be returned
249
   * @security JWT
250
   * @return {TransferResponse} 200 - The requested transfer entity
251
   * @return {string} 404 - Not found error
252
   * @return {string} 500 - Internal server error
253
   */
1✔
254
  public async returnTransfer(req: RequestWithToken, res: Response): Promise<void> {
1✔
255
    const { id } = req.params;
5✔
256
    this.logger.trace('Get single transfer', id, 'by user', req.token.user);
5✔
257
    try {
5✔
258
      const parsedId = parseInt(id, 10);
5✔
259
      const [transfers] = await new TransferService().getTransfers({ id: parsedId }, {});
5✔
260
      if (transfers.length > 0) {
5✔
261
        res.json(TransferService.asTransferResponse(transfers[0]));
4✔
262
      } else {
5✔
263
        res.status(404).json('Transfer not found.');
1✔
264
      }
1✔
265
    } catch (error) {
5!
266
      this.logger.error('Could not return transfer:', error);
×
267
      res.status(500).json('Internal server error.');
×
268
    }
×
269
  }
5✔
270

271
  /**
1✔
272
   * POST /transfers
273
   * @summary Post a new transfer.
274
   * @operationId createTransfer
275
   * @tags transfers - Operations of transfer controller
276
   * @param {TransferRequest} request.body.required
277
   * - The transfer which should be created
278
   * @security JWT
279
   * @return {TransferResponse} 200 - The created transfer entity
280
   * @return {string} 400 - Validation error
281
   * @return {string} 500 - Internal server error
282
   */
1✔
283
  public async postTransfer(req: RequestWithToken, res: Response) : Promise<void> {
1✔
284
    const request = req.body as TransferRequest;
2✔
285
    this.logger.trace('Post transfer', request, 'by user', req.token.user);
2✔
286

287
    const transferService = new TransferService();
2✔
288

289
    try {
2✔
290
      if (!(await transferService.verifyTransferRequest(request))) {
2✔
291
        res.status(400).json('Invalid transfer.');
1✔
292
        return;
1✔
293
      }
1✔
294

295
      const transfer = await transferService.postTransfer(request);
1✔
296
      res.json(TransferService.asTransferResponse(transfer));
1✔
297
    } catch (error) {
2!
298
      this.logger.error('Could not create transfer:', error);
×
299
      res.status(500).json('Internal server error.');
×
300
    }
×
301
  }
2✔
302

303
  /**
1✔
304
     * DELETE /transfers/{id}
305
     * @summary Deletes a transfer.
306
     * @operationId deleteTransfer
307
     * @tags transfers - Operations of transfer controller
308
     * @param {integer} id.path.required - The id of the transfer which should be deleted
309
     * @security JWT
310
     * @return 204 - Transfer successfully deleted
311
     * @return {string} 400 - Cannot delete transfer because it is referenced by another entityreturn
312
     * @return {string} 404 - Not found error
313
     */
1✔
314
  public async deleteTransfer(req: RequestWithToken, res: Response): Promise<void> {
1✔
315
    const { id } = req.params;
3✔
316
    this.logger.trace('Delete transfer', id, 'by user', req.token.user);
3✔
317

318
    try {
3✔
319
      await new TransferService().deleteTransfer(parseInt(id));
3✔
320
      res.status(204).send();
1✔
321
    } catch (error) {
3✔
322
      if (error.message === 'Transfer not found') {
2✔
323
        res.status(404).json('Transfer not found.');
1✔
324
      } else if (error.message === 'Cannot delete transfer because it is referenced by another entity') {
1✔
325
        res.status(400).json('Cannot delete transfer because it is referenced by another entity.');
1✔
326
      } else {
1!
327
        this.logger.error('Could not delete transfer:', error);
×
328
        res.status(500).json('Internal server error.');
×
329
      }
×
330
    }
2✔
331
  }
3✔
332

333
  /**
1✔
334
   * GET /transfers/{id}/pdf
335
   * @summary Get the PDF of the transfer
336
   * @operationId getTransferPdf
337
   * @tags transfers - Operations of the transfer controller
338
   * @param {integer} id.path.required - The transfer ID
339
   * @security JWT
340
   * @returns {string} 200 - The requested pdf of the transfer - application/pdf
341
   * @return {string} 400 - Transfer is decorated and has its own PDF service
342
   * @return {string} 404 - Transfer not found
343
   * @return {string} 500 - Internal server error
344
   */
1✔
345
  public async getTransferPdf(req: RequestWithToken, res: Response): Promise<void> {
1✔
346
    const { id } = req.params;
8✔
347
    const transferId = parseInt(id, 10);
8✔
348
    this.logger.trace('Get transfer PDF', id, 'by user', req.token.user);
8✔
349

350
    try {
8✔
351
      const transfer = await Transfer.findOne({
8✔
352
        where: { id: transferId },
8✔
353
      });
8✔
354
      if (!transfer) {
8✔
355
        res.status(404).json('Transfer not found.');
1✔
356
        return;
1✔
357
      }
1✔
358

359
      const pdf = await transfer.createPdf();
7✔
360
      const fileName = `transfer-${transfer.id}.pdf`;
1✔
361
      res.setHeader('Content-Type', 'application/pdf');
1✔
362
      res.setHeader('Content-Disposition', `attachment; filename="${fileName}"`);
1✔
363
      res.status(200).send(pdf);
1✔
364
    } catch (error: any) {
8✔
365
      if (error instanceof PdfError) {
6✔
366
        res.status(400).json(error.message);
5✔
367
        return;
5✔
368
      }
5✔
369
      this.logger.error('Could not return transfer PDF:', error);
1✔
370
      res.status(500).json('Internal server error.');
1✔
371
    }
1✔
372
  }
8✔
373
}
1✔
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