• 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

78.57
/src/controller/payout-request-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 payout-request-controller.
23
 *
24
 * @module payout-requests
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 { parseRequestPagination, toResponse } from '../helpers/pagination';
33
import PayoutRequestService, { parseGetPayoutRequestsFilters } from '../service/payout-request-service';
34
import { PayoutRequestStatusRequest } from './request/payout-request-status-request';
35
import PayoutRequest from '../entity/transactions/payout/payout-request';
36
import { PayoutRequestState } from '../entity/transactions/payout/payout-request-status';
37
import PayoutRequestRequest from './request/payout-request-request';
38
import User from '../entity/user/user';
39
import BalanceService from '../service/balance-service';
40
import { PdfUrlResponse } from './response/simple-file-response';
41
import { PdfError } from '../errors';
42
import { asBoolean } from '../helpers/validators';
43

44
export default class PayoutRequestController extends BaseController {
1✔
45
  private logger: Logger = log4js.getLogger('PayoutRequestController');
1✔
46

47
  public constructor(options: BaseControllerOptions) {
1✔
48
    super(options);
2✔
49
    this.configureLogger(this.logger);
2✔
50
  }
2✔
51

52
  public getPolicy(): Policy {
1✔
53
    return {
2✔
54
      '/': {
2✔
55
        GET: {
2✔
56
          policy: async (req) => this.roleManager.can(req.token.roles, 'get', 'all', 'PayoutRequest', ['*']),
2✔
57
          handler: this.returnAllPayoutRequests.bind(this),
2✔
58
        },
2✔
59
        POST: {
2✔
60
          policy: async (req) => this.roleManager.can(req.token.roles, 'create', await PayoutRequestController.getRelation(req), 'PayoutRequest', ['*']),
2✔
61
          handler: this.createPayoutRequest.bind(this),
2✔
62
        },
2✔
63
      },
2✔
64
      '/:id(\\d+)': {
2✔
65
        GET: {
2✔
66
          policy: async (req) => this.roleManager.can(req.token.roles, 'get', await PayoutRequestController.getRelation(req), 'PayoutRequest', ['*']),
2✔
67
          handler: this.returnSinglePayoutRequest.bind(this),
2✔
68
        },
2✔
69
      },
2✔
70
      '/:id(\\d+)/pdf': {
2✔
71
        GET: {
2✔
72
          policy: async (req) => this.roleManager.can(req.token.roles, 'get', await PayoutRequestController.getRelation(req), 'PayoutRequest', ['*']),
2✔
73
          handler: this.getPayoutRequestPdf.bind(this),
2✔
74
        },
2✔
75
      },
2✔
76
      '/:id(\\d+)/status': {
2✔
77
        POST: {
2✔
78
          policy: async (req) => this.roleManager.can(req.token.roles, 'update', await PayoutRequestController.getRelation(req), 'PayoutRequest', ['*']),
2✔
79
          handler: this.updatePayoutRequestStatus.bind(this),
2✔
80
        },
2✔
81
      },
2✔
82
    };
2✔
83
  }
2✔
84

85
  static async getRelation(req: RequestWithToken): Promise<string> {
1✔
86
    if (req.body.forId != null) {
16✔
87
      if (req.body.forId == req.token.user.id) {
4✔
88
        return 'own';
1✔
89
      } else {
4✔
90
        return 'all';
3✔
91
      }
3✔
92
    }
4✔
93

94
    const { id } = req.params;
12✔
95
    const payoutRequest = await PayoutRequest.findOne({ where: { id: parseInt(id, 10) }, relations: ['requestedBy'] });
12✔
96
    return (payoutRequest != null && payoutRequest.requestedBy.id === req.token.user.id) ? 'own' : 'all';
16✔
97
  }
16✔
98

99
  /**
1✔
100
   * GET /payoutrequests
101
   * @summary Returns all payout requests given the filter parameters
102
   * @operationId getAllPayoutRequests
103
   * @tags payoutRequests - Operations of the payout request controller
104
   * @security JWT
105
   * @param {integer | Array<integer>} requestedById.query - ID of user(s) who requested a payout
106
   * @param {integer | Array<integer>} approvedById.query - ID of user(s) who approved a payout
107
   * @param {string} fromDate.query - Start date for selected transactions (inclusive)
108
   * @param {string} tillDate.query - End date for selected transactions (exclusive)
109
   * @param {string} status.query - Status of the payout requests (OR relation)
110
   * @array
111
   * @items.type {string}
112
   * @param {integer} take.query - How many payout requests the endpoint should return
113
   * @param {integer} skip.query - How many payout requests should be skipped (for pagination)
114
   * @return {PaginatedBasePayoutRequestResponse} 200 - All existing payout requests
115
   * @return {string} 400 - Validation error
116
   * @return {string} 500 - Internal server error
117
   */
1✔
118
  public async returnAllPayoutRequests(req: RequestWithToken, res: Response): Promise<void> {
1✔
119
    this.logger.trace('Get all payout requests by user', req.token.user);
17✔
120

121
    let filters;
17✔
122
    let pagination;
17✔
123
    try {
17✔
124
      filters = parseGetPayoutRequestsFilters(req);
17✔
125
      pagination = parseRequestPagination(req);
17✔
126
    } catch (e) {
17✔
127
      res.status(400).send(e.message);
5✔
128
      return;
5✔
129
    }
5✔
130

131
    try {
12✔
132
      const [data, count] = await PayoutRequestService.getPayoutRequests(filters, pagination);
12✔
133
      const records = data.map((o) => PayoutRequestService.asBasePayoutRequestResponse(o));
12✔
134
      res.status(200).json(toResponse(records, count, pagination));
12✔
135
    } catch (e) {
17!
136
      res.status(500).send('Internal server error.');
×
137
      this.logger.error(e);
×
138
    }
×
139
  }
17✔
140

141
  /**
1✔
142
   * GET /payoutrequests/{id}
143
   * @summary Get a single payout request
144
   * @operationId getSinglePayoutRequest
145
   * @tags payoutRequests - Operations of the payout request controller
146
   * @param {integer} id.path.required - The ID of the payout request object that should be returned
147
   * @security JWT
148
   * @return {PayoutRequestResponse} 200 - Single payout request with given id
149
   * @return {string} 404 - Nonexistent payout request id
150
   */
1✔
151
  public async returnSinglePayoutRequest(req: RequestWithToken, res: Response): Promise<void> {
1✔
152
    const parameters = req.params;
3✔
153
    this.logger.trace('Get single payout request', parameters, 'by user', req.token.user);
3✔
154

155
    let payoutRequest;
3✔
156
    try {
3✔
157
      payoutRequest = await PayoutRequestService
3✔
158
        .getSinglePayoutRequest(parseInt(parameters.id, 10));
3✔
159
    } catch (e) {
3!
160
      res.status(500).send();
×
161
      this.logger.error(e);
×
162
      return;
×
163
    }
×
164

165
    if (payoutRequest === undefined) {
3✔
166
      res.status(404).json('Unknown payout request ID.');
1✔
167
      return;
1✔
168
    }
1✔
169

170
    res.status(200).json(PayoutRequestService.asPayoutRequestResponse(payoutRequest));
2✔
171
  }
2✔
172

173
  /**
1✔
174
   * POST /payoutrequests
175
   * @summary Create a new payout request
176
   * @operationId createPayoutRequest
177
   * @tags payoutRequests - Operations of the payout request controller
178
   * @param {PayoutRequestRequest} request.body.required - New payout request
179
   * @security JWT
180
   * @return {PayoutRequestResponse} 200 - The created payout request.
181
   * @return {string} 400 - Validation error
182
   * @return {string} 400 - User does not exist
183
   */
1✔
184
  public async createPayoutRequest(req: RequestWithToken, res: Response): Promise<void> {
1✔
185
    const body = req.body as PayoutRequestRequest;
3✔
186
    this.logger.trace('Create payout request by user', req.token.user);
3✔
187

188
    try {
3✔
189
      const user = await User.findOne({ where: { id: body.forId } });
3✔
190
      if (!user) {
3✔
191
        res.status(400).json('Unknown user ID.');
1✔
192
        return;
1✔
193
      }
1✔
194

195
      const balance = await new BalanceService().getBalance(user.id);
2✔
196
      if (balance.amount.amount < body.amount.amount) {
3!
197
        res.status(400).json('Insufficient balance.');
×
198
        return;
×
199
      }
✔
200

201
      const payoutRequest = await PayoutRequestService.createPayoutRequest(body, user);
2✔
202
      res.status(200).json(PayoutRequestService.asPayoutRequestResponse(payoutRequest));
2✔
203
    } catch (e) {
3!
204
      res.status(500).send();
×
205
      this.logger.error(e);
×
206
    }
×
207
  }
3✔
208

209
  /**
1✔
210
   * POST /payoutrequests/{id}/status
211
   * @summary Create a new status for a payout request
212
   * @operationId setPayoutRequestStatus
213
   * @tags payoutRequests - Operations of the payout request controller
214
   * @param {integer} id.path.required - The ID of the payout request object that should be returned
215
   * @param {PayoutRequestStatusRequest} request.body.required - New state of payout request
216
   * @security JWT
217
   * @return {PayoutRequestResponse} 200 - The updated payout request
218
   * @return {string} 400 - Validation error
219
   * @return {string} 404 - Nonexistent payout request id
220
   */
1✔
221
  public async updatePayoutRequestStatus(req: RequestWithToken, res: Response): Promise<void> {
1✔
222
    const parameters = req.params;
5✔
223
    const body = req.body as PayoutRequestStatusRequest;
5✔
224
    this.logger.trace('Update single payout request status', parameters, 'by user', req.token.user);
5✔
225

226
    const id = parseInt(parameters.id, 10);
5✔
227

228
    // Check if payout request exists
5✔
229
    let payoutRequest;
5✔
230
    try {
5✔
231
      payoutRequest = await PayoutRequestService.getSinglePayoutRequest(id);
5✔
232
    } catch (e) {
5!
233
      res.status(500).send();
×
234
      this.logger.error(e);
×
235
      return;
×
236
    }
×
237

238
    if (payoutRequest === undefined) {
5✔
239
      res.status(404).json('Unknown payout request ID.');
1✔
240
      return;
1✔
241
    }
1✔
242

243
    // Everyone can cancel their own payout requests, but only admins can update to other states.
4✔
244
    if (body.state !== PayoutRequestState.CANCELLED) {
5✔
245
      if (!this.roleManager.can(req.token.roles, 'update', 'all', 'PayoutRequest', ['*'])) {
3!
246
        res.status(403).send('You can only cancel your own payout requests.');
×
247
        return;
×
248
      }
×
249
    } else if (payoutRequest.requestedBy.id !== req.token.user.id) {
5✔
250
      res.status(403).send('You can only cancel your own payout requests.');
1✔
251
      return;
1✔
252
    }
1✔
253

254
    if (body.state === PayoutRequestState.APPROVED) {
5✔
255
      const balance = await new BalanceService().getBalance(payoutRequest.requestedBy.id);
2✔
256
      if (balance.amount.amount < payoutRequest.amount.getAmount()) {
2✔
257
        res.status(400).json('Insufficient balance.');
1✔
258
        return;
1✔
259
      }
1✔
260
    }
2✔
261

262
    // Verify validity of new status
2✔
263
    try {
2✔
264
      await PayoutRequestService.canUpdateStatus(id, body.state);
2✔
265
    } catch (e) {
1✔
266
      res.status(400).json(e);
1✔
267
      return;
1✔
268
    }
1✔
269

270
    // Execute
1✔
271
    try {
1✔
272
      const updatedPayoutRequest = await PayoutRequestService.updateStatus(id, body.state, req.token.user);
1✔
273
      res.status(200).json(PayoutRequestService.asPayoutRequestResponse(updatedPayoutRequest));
1✔
274
    } catch (e) {
5!
275
      res.status(500).send();
×
276
      this.logger.error(e);
×
277
    }
×
278
  }
5✔
279

280

281
  /**
1✔
282
   * GET /payoutrequests/{id}/pdf
283
   * @summary Get a payout request pdf
284
   * @operationId getPayoutRequestPdf
285
   * @tags payoutRequests - Operations of the payout request controller
286
   * @security JWT
287
   * @param {integer} id.path.required - The ID of the payout request object that should be returned
288
   * @param {boolean} force.query - Whether to force regeneration of the pdf
289
   * @return {PdfUrlResponse} 200 - The pdf location information.
290
   * @return {string} 404 - Nonexistent payout request id
291
   * @return {string} 500 - Internal server error
292
   * @return {string} 502 - PDF generation failed
293
   */
1✔
294
  public async getPayoutRequestPdf(req: RequestWithToken, res: Response): Promise<void> {
1✔
295
    const { id } = req.params;
×
296
    const payoutRequestId = parseInt(id, 10);
×
297
    this.logger.trace('Get payout request pdf', id, 'by user', req.token.user);
×
298

299
    try {
×
300
      const force = !!asBoolean(req.query.force);
×
301
      const payoutRequest = await PayoutRequest.findOne({ where: { id: payoutRequestId }, relations: ['requestedBy', 'approvedBy', 'payoutRequestStatus'] });
×
302
      if (!payoutRequest) {
×
303
        res.status(404).json('Unknown payout request ID.');
×
304
        return;
×
305
      }
×
306

307
      const pdf = await payoutRequest.getOrCreatePdf(force);
×
308

309
      res.status(200).json({ pdf: pdf.downloadName } as PdfUrlResponse);
×
310
    } catch (error) {
×
311
      this.logger.error('Could get payout request PDF:', error);
×
312
      if (error instanceof PdfError) {
×
313
        res.status(502).json('PDF Generator service failed.');
×
314
        return;
×
315
      }
×
316
      res.status(500).json('Internal server error.');
×
317
    }
×
318
  }
×
319
}
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