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

GEWIS / sudosos-backend / 27283082186

10 Jun 2026 02:13PM UTC coverage: 91.996% (+0.04%) from 91.956%
27283082186

push

github

web-flow
chore(deps): bump bullmq from 5.77.6 to 5.78.0 (#949)

Bumps [bullmq](https://github.com/taskforcesh/bullmq) from 5.77.6 to 5.78.0.
- [Release notes](https://github.com/taskforcesh/bullmq/releases)
- [Commits](https://github.com/taskforcesh/bullmq/compare/v5.77.6...v5.78.0)

---
updated-dependencies:
- dependency-name: bullmq
  dependency-version: 5.78.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

4199 of 4800 branches covered (87.48%)

Branch coverage included in aggregate %.

21593 of 23236 relevant lines covered (92.93%)

847.01 hits per line

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

90.31
/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
/**
1✔
40
 * Controller for the `transfers` module. Exposes CRUD over transfers, aggregate and per-category
41
 * summary endpoints used by treasurer dashboards, and a PDF receipt. Direct POST is reserved for
42
 * privileged users; most transfers are created indirectly by the domain that owns the movement
43
 * (deposits, payouts, fines, invoices, write-offs). See the {@link transfers | module page} for
44
 * the full list of back-references.
45
 */
1✔
46
export default class TransferController extends BaseController {
1✔
47
  private logger: Logger = log4js.getLogger('TransferController');
1✔
48

49
  /**
1✔
50
   * Creates a new transfer controller instance.
51
   * @param options - The options passed to the base controller.
52
   */
1✔
53
  public constructor(options: BaseControllerOptions) {
1✔
54
    super(options);
2✔
55
    this.configureLogger(this.logger);
2✔
56
  }
2✔
57

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

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

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

145
    let filters;
11✔
146
    try {
11✔
147
      filters = parseGetTransferAggregateFilters(req);
11✔
148
    } catch (e) {
11✔
149
      res.status(400).send(e.message);
1✔
150
      return;
1✔
151
    }
1✔
152

153
    try {
10✔
154
      const { total, count } = await new TransferService().getTransferAggregate(filters);
10✔
155
      res.json({ total: total.toObject(), count });
10✔
156
    } catch (error) {
11!
157
      this.logger.error('Could not return transfer aggregate:', error);
×
158
      res.status(500).json('Internal server error.');
×
159
    }
×
160
  }
11✔
161

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

179
    let filters;
4✔
180
    try {
4✔
181
      filters = parseGetTransferSummaryFilters(req);
4✔
182
    } catch (e) {
4!
183
      res.status(400).send(e.message);
×
184
      return;
×
185
    }
×
186

187
    try {
4✔
188
      const summary = await new TransferService().getTransferSummary(filters);
4✔
189
      res.json({
4✔
190
        total: { total: summary.total.total.toObject(), count: summary.total.count },
4✔
191
        deposits: { total: summary.deposits.total.toObject(), count: summary.deposits.count },
4✔
192
        payoutRequests: { total: summary.payoutRequests.total.toObject(), count: summary.payoutRequests.count },
4✔
193
        sellerPayouts: { total: summary.sellerPayouts.total.toObject(), count: summary.sellerPayouts.count },
4✔
194
        invoices: { total: summary.invoices.total.toObject(), count: summary.invoices.count },
4✔
195
        creditInvoices: { total: summary.creditInvoices.total.toObject(), count: summary.creditInvoices.count },
4✔
196
        fines: { total: summary.fines.total.toObject(), count: summary.fines.count },
4✔
197
        waivedFines: { total: summary.waivedFines.total.toObject(), count: summary.waivedFines.count },
4✔
198
        writeOffs: { total: summary.writeOffs.total.toObject(), count: summary.writeOffs.count },
4✔
199
        inactiveAdministrativeCosts: { total: summary.inactiveAdministrativeCosts.total.toObject(), count: summary.inactiveAdministrativeCosts.count },
4✔
200
        manualCreations: { total: summary.manualCreations.total.toObject(), count: summary.manualCreations.count },
4✔
201
        manualDeletions: { total: summary.manualDeletions.total.toObject(), count: summary.manualDeletions.count },
4✔
202
      });
4✔
203
    } catch (error) {
4!
204
      this.logger.error('Could not return transfer summary:', error);
×
205
      res.status(500).json('Internal server error.');
×
206
    }
×
207
  }
4✔
208

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

230
    let filters;
14✔
231
    let take;
14✔
232
    let skip;
14✔
233
    try {
14✔
234
      filters = parseGetTransferFilters(req);
14✔
235
      const pagination = parseRequestPagination(req);
14✔
236
      take = pagination.take;
14✔
237
      skip = pagination.skip;
14✔
238
    } catch (e) {
14✔
239
      res.status(400).send(e.message);
3✔
240
      return;
3✔
241
    }
3✔
242

243
    try {
11✔
244
      const [transfers, count] = await new TransferService().getTransfers(filters, { take, skip });
11✔
245
      const records = transfers.map((t) => TransferService.asTransferResponse(t));
11✔
246
      res.json(toResponse(records, count, { take, skip }));
11✔
247
    } catch (error) {
14!
248
      this.logger.error('Could not return all transfers:', error);
×
249
      res.status(500).json('Internal server error.');
×
250
    }
×
251
  }
14✔
252

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

281
  /**
1✔
282
   * POST /transfers
283
   * @summary Post a new transfer.
284
   * @operationId createTransfer
285
   * @tags transfers - Operations of transfer controller
286
   * @param {TransferRequest} request.body.required
287
   * - The transfer which should be created
288
   * @security JWT
289
   * @return {TransferResponse} 200 - The created transfer entity
290
   * @return {string} 400 - Validation error
291
   * @return {string} 500 - Internal server error
292
   */
1✔
293
  public async postTransfer(req: RequestWithToken, res: Response) : Promise<void> {
1✔
294
    const request = req.body as TransferRequest;
2✔
295
    this.logger.trace('Post transfer', request, 'by user', req.token.user);
2✔
296

297
    const transferService = new TransferService();
2✔
298

299
    try {
2✔
300
      if (!(await transferService.verifyTransferRequest(request))) {
2✔
301
        res.status(400).json('Invalid transfer.');
1✔
302
        return;
1✔
303
      }
1✔
304

305
      const transfer = await transferService.postTransfer(request);
1✔
306
      res.json(TransferService.asTransferResponse(transfer));
1✔
307
    } catch (error) {
2!
308
      this.logger.error('Could not create transfer:', error);
×
309
      res.status(500).json('Internal server error.');
×
310
    }
×
311
  }
2✔
312

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

328
    try {
3✔
329
      await new TransferService().deleteTransfer(parseInt(id));
3✔
330
      res.status(204).send();
1✔
331
    } catch (error) {
3✔
332
      if (error.message === 'Transfer not found') {
2✔
333
        res.status(404).json('Transfer not found.');
1✔
334
      } else if (error.message === 'Cannot delete transfer because it is referenced by another entity') {
1✔
335
        res.status(400).json('Cannot delete transfer because it is referenced by another entity.');
1✔
336
      } else {
1!
337
        this.logger.error('Could not delete transfer:', error);
×
338
        res.status(500).json('Internal server error.');
×
339
      }
×
340
    }
2✔
341
  }
3✔
342

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

360
    try {
8✔
361
      const transfer = await Transfer.findOne({
8✔
362
        where: { id: transferId },
8✔
363
      });
8✔
364
      if (!transfer) {
8✔
365
        res.status(404).json('Transfer not found.');
1✔
366
        return;
1✔
367
      }
1✔
368

369
      const pdf = await transfer.createPdf();
7✔
370
      const fileName = `transfer-${transfer.id}.pdf`;
1✔
371
      res.setHeader('Content-Type', 'application/pdf');
1✔
372
      res.setHeader('Content-Disposition', `attachment; filename="${fileName}"`);
1✔
373
      res.status(200).send(pdf);
1✔
374
    } catch (error: any) {
8✔
375
      if (error instanceof PdfError) {
6✔
376
        res.status(400).json(error.message);
5✔
377
        return;
5✔
378
      }
5✔
379
      this.logger.error('Could not return transfer PDF:', error);
1✔
380
      res.status(500).json('Internal server error.');
1✔
381
    }
1✔
382
  }
8✔
383
}
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