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

GEWIS / sudosos-backend / 15969645933

30 Jun 2025 09:50AM UTC coverage: 85.133% (+0.003%) from 85.13%
15969645933

Pull #560

github

web-flow
Merge 606aa0b9b into bb882f2ec
Pull Request #560: feat: add write off pdfs

1301 of 1589 branches covered (81.88%)

Branch coverage included in aggregate %.

38 of 55 new or added lines in 7 files covered. (69.09%)

19 existing lines in 2 files now uncovered.

7008 of 8171 relevant lines covered (85.77%)

1073.35 hits per line

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

70.0
/src/controller/payout-request-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 payout-request-controller.
23
 *
24
 * @module payout-requests
25
 */
26

27
import { Response } from 'express';
28
import log4js, { Logger } from 'log4js';
2✔
29
import BaseController, { BaseControllerOptions } from './base-controller';
2✔
30
import Policy from './policy';
31
import { RequestWithToken } from '../middleware/token-middleware';
32
import { parseRequestPagination } from '../helpers/pagination';
2✔
33
import PayoutRequestService, { parseGetPayoutRequestsFilters } from '../service/payout-request-service';
2✔
34
import { PayoutRequestStatusRequest } from './request/payout-request-status-request';
35
import PayoutRequest from '../entity/transactions/payout/payout-request';
2✔
36
import { PayoutRequestState } from '../entity/transactions/payout/payout-request-status';
2✔
37
import PayoutRequestRequest from './request/payout-request-request';
38
import User from '../entity/user/user';
2✔
39
import BalanceService from '../service/balance-service';
2✔
40
import { PdfUrlResponse } from './response/simple-file-response';
41
import { PdfError } from '../errors';
2✔
42

43
export default class PayoutRequestController extends BaseController {
2✔
44
  private logger: Logger = log4js.getLogger('PayoutRequestController');
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', 'PayoutRequest', ['*']),
18✔
56
          handler: this.returnAllPayoutRequests.bind(this),
57
        },
58
        POST: {
59
          policy: async (req) => this.roleManager.can(req.token.roles, 'create', await PayoutRequestController.getRelation(req), 'PayoutRequest', ['*']),
3✔
60
          handler: this.createPayoutRequest.bind(this),
61
        },
62
      },
63
      '/:id(\\d+)': {
64
        GET: {
65
          policy: async (req) => this.roleManager.can(req.token.roles, 'get', await PayoutRequestController.getRelation(req), 'PayoutRequest', ['*']),
5✔
66
          handler: this.returnSinglePayoutRequest.bind(this),
67
        },
68
      },
69
      '/:id(\\d+)/pdf': {
70
        GET: {
UNCOV
71
          policy: async (req) => this.roleManager.can(req.token.roles, 'get', await PayoutRequestController.getRelation(req), 'PayoutRequest', ['*']),
×
72
          handler: this.getPayoutRequestPdf.bind(this),
73
        },
74
      },
75
      '/:id(\\d+)/status': {
76
        POST: {
77
          policy: async (req) => this.roleManager.can(req.token.roles, 'update', await PayoutRequestController.getRelation(req), 'PayoutRequest', ['*']),
7✔
78
          handler: this.updatePayoutRequestStatus.bind(this),
79
        },
80
      },
81
    };
82
  }
83

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

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

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

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

130
    try {
12✔
131
      const results = await PayoutRequestService.getPayoutRequests(filters, pagination);
12✔
132
      res.status(200).json(results);
12✔
133
    } catch (e) {
UNCOV
134
      res.status(500).send('Internal server error.');
×
135
      this.logger.error(e);
×
136
    }
137
  }
138

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

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

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

168
    res.status(200).json(payoutRequest);
3✔
169
  }
170

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

185
    try {
3✔
186
      const user = await User.findOne({ where: { id: body.forId } });
3✔
187
      if (user === undefined) {
3!
UNCOV
188
        res.status(404).json('Unknown user ID.');
×
189
        return;
×
190
      }
191

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

198
      const payoutRequest = await PayoutRequestService.createPayoutRequest(body, user);
3✔
199
      res.status(200).json(payoutRequest);
3✔
200
    } catch (e) {
UNCOV
201
      res.status(500).send();
×
202
      this.logger.error(e);
×
203
    }
204
  }
205

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

223
    const id = parseInt(parameters.id, 10);
6✔
224

225
    // Check if payout request exists
226
    let payoutRequest;
227
    try {
6✔
228
      payoutRequest = await PayoutRequestService.getSinglePayoutRequest(id);
6✔
229
    } catch (e) {
UNCOV
230
      res.status(500).send();
×
231
      this.logger.error(e);
×
232
      return;
×
233
    }
234

235
    if (payoutRequest === undefined) {
6✔
236
      res.status(404).json('Unknown payout request ID.');
1✔
237
      return;
1✔
238
    }
239

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

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

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

267
    // Execute
268
    try {
2✔
269
      payoutRequest = await PayoutRequestService.updateStatus(id, body.state, req.token.user);
2✔
270
      res.status(200).json(payoutRequest);
2✔
271
    } catch (e) {
UNCOV
272
      res.status(500).send();
×
273
      this.logger.error(e);
×
274
    }
275
  }
276

277

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

294
    try {
×
295
      const payoutRequest = await PayoutRequest.findOne({ where: { id: payoutRequestId }, relations: ['requestedBy', 'approvedBy', 'payoutRequestStatus'] });
×
UNCOV
296
      if (!payoutRequest) {
×
297
        res.status(404).json('Unknown payout request ID.');
×
298
        return;
×
299
      }
300

301
      const pdf = await payoutRequest.getOrCreatePdf();
×
302

UNCOV
303
      res.status(200).json({ pdf: pdf.downloadName } as PdfUrlResponse);
×
304
    } catch (error) {
305
      this.logger.error('Could get payout request PDF:', error);
×
UNCOV
306
      if (error instanceof PdfError) {
×
307
        res.status(502).json('PDF Generator service failed.');
×
UNCOV
308
        return;
×
309
      }
310
      res.status(500).json('Internal server error.');
×
311
    }
312
  }
313
}
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