• 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

87.74
/src/controller/write-off-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
 * @module write-offs
23
 */
1✔
24

25
import { Response } from 'express';
26
import BaseController, { BaseControllerOptions } from './base-controller';
27
import log4js, { Logger } from 'log4js';
28
import Policy from './policy';
29
import { RequestWithToken } from '../middleware/token-middleware';
30
import { parseRequestPagination, toResponse } from '../helpers/pagination';
31
import WriteOffService, { parseWriteOffFilterParameters } from '../service/write-off-service';
32
import WriteOff from '../entity/transactions/write-off';
33
import WriteOffRequest from './request/write-off-request';
34
import User from '../entity/user/user';
35
import BalanceService from '../service/balance-service';
36
import { PdfError } from '../errors';
37
import { PdfUrlResponse } from './response/simple-file-response';
38
import { asBoolean } from '../helpers/validators';
39

40
/**
1✔
41
 * Controller for the `/writeoffs` endpoints in the {@link write-offs | write-offs}
42
 * module. `POST /writeoffs` is the only mutating endpoint and is irreversible: it zeroes
43
 * the user's negative balance and closes the account in one go. The rest are reads and
44
 * the PDF receipt.
45
 */
1✔
46
export default class WriteOffController extends BaseController {
1✔
47
  private logger: Logger = log4js.getLogger(' WriteOffController');
1✔
48

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

54
  public getPolicy(): Policy {
1✔
55
    return {
2✔
56
      '/': {
2✔
57
        GET: {
2✔
58
          policy: async (req) => this.roleManager.can(req.token.roles, 'get', 'all', 'WriteOff', ['*']),
2✔
59
          handler: this.returnAllWriteOffs.bind(this),
2✔
60
        },
2✔
61
        POST: {
2✔
62
          policy: async (req) => this.roleManager.can(req.token.roles, 'create', 'all', 'WriteOff', ['*']),
2✔
63
          handler: this.createWriteOff.bind(this),
2✔
64
          body: { modelName: 'WriteOffRequest' },
2✔
65
        },
2✔
66
      },
2✔
67
      '/:id(\\d+)': {
2✔
68
        GET: {
2✔
69
          policy: async (req) => this.roleManager.can(req.token.roles, 'get', 'all', 'WriteOff', ['*']),
2✔
70
          handler: this.getSingleWriteOff.bind(this),
2✔
71
        },
2✔
72
      },
2✔
73
      '/:id(\\d+)/pdf': {
2✔
74
        GET: {
2✔
75
          policy: async (req) => this.roleManager.can(req.token.roles, 'get', 'all', 'WriteOff', ['*']),
2✔
76
          handler: this.getWriteOffPdf.bind(this),
2✔
77
        },
2✔
78
      },
2✔
79
    };
2✔
80
  }
2✔
81

82

83
  /**
1✔
84
   * GET /writeoffs
85
   * @summary Returns all write-offs in the system.
86
   * @operationId getAllWriteOffs
87
   * @tags writeoffs - Operations of the writeoffs controller
88
   * @security JWT
89
   * @param {integer} toId.query - Filter on Id of the debtor
90
   * @param {integer} amount.query - Filter on the amount of the write-off
91
   * @param {integer} take.query - Number of write-offs to return
92
   * @param {integer} skip.query - Number of write-offs to skip
93
   * @param {string} fromDate.query - Start date for selected write-offs (inclusive)
94
   * @param {string} tillDate.query - End date for selected write-offs (exclusive)
95
   * @return {PaginatedWriteOffResponse} 200 - All existing write-offs
96
   * @return {string} 400 - Validation error
97
   * @return {string} 500 - Internal server error
98
   */
1✔
99
  public async returnAllWriteOffs(req: RequestWithToken, res: Response): Promise<void> {
1✔
100
    this.logger.trace('Get all write offs by ', req.token.user);
5✔
101

102
    let take;
5✔
103
    let skip;
5✔
104
    try {
5✔
105
      const pagination = parseRequestPagination(req);
5✔
106
      take = pagination.take;
5✔
107
      skip = pagination.skip;
5✔
108
    } catch (e) {
5!
109
      res.status(400).json(e.message);
×
110
      return;
×
111
    }
×
112

113
    try {
5✔
114
      const filters = parseWriteOffFilterParameters(req);
5✔
115
      const [writeOffs, count] = await WriteOffService.getWriteOffs(
5✔
116
        filters, { take, skip },
5✔
117
      );
118
      res.json(toResponse(writeOffs.map(WriteOffService.asWriteOffResponse), count, { take, skip }));
5✔
119
    } catch (error) {
5!
120
      this.logger.error('Could not return all write offs:', error);
×
121
      res.status(500).json('Internal server error.');
×
122
    }
×
123
  }
5✔
124

125
  /**
1✔
126
   * GET /writeoffs/{id}
127
   * @summary Get a single write-off
128
   * @operationId getSingleWriteOff
129
   * @tags writeoffs - Operations of the writeoff controller
130
   * @param {integer} id.path.required - The ID of the write-off object that should be returned
131
   * @security JWT
132
   * @return {WriteOffResponse} 200 - Single write off with given id
133
   * @return {string} 404 - Nonexistent write off id
134
   */
1✔
135
  public async getSingleWriteOff(req: RequestWithToken, res: Response): Promise<void> {
1✔
136
    const { id } = req.params;
3✔
137
    this.logger.trace('Get single write off', id, 'by user', req.token.user);
3✔
138

139
    try {
3✔
140
      const writeOffId = parseInt(id, 10);
3✔
141
      const options = WriteOffService.getOptions({ writeOffId });
3✔
142
      const writeOff = await WriteOff.findOne({ ...options });
3✔
143
      if (!writeOff) {
3✔
144
        res.status(404).json('Unknown write off ID.');
1✔
145
        return;
1✔
146
      }
1✔
147

148
      res.status(200).json(WriteOffService.asWriteOffResponse(writeOff));
2✔
149
    } catch (error) {
3!
150
      this.logger.error('Could not return single write off:', error);
×
151
      res.status(500).json('Internal server error.');
×
152
    }
×
153
  }
3✔
154

155
  /**
1✔
156
   * POST /writeoffs
157
   * @summary Creates a new write-off in the system. Creating a write-off will also close and delete the user's account.
158
   * @operationId createWriteOff
159
   * @tags writeoffs - Operations of the writeoff controller
160
   * @param {WriteOffRequest} request.body.required - New write off
161
   * @security JWT
162
   * @return {WriteOffResponse} 200 - The created write off.
163
   * @return {string} 400 - Validation error
164
   * @return {string} 500 - Internal server error.
165
   */
1✔
166
  public async createWriteOff(req: RequestWithToken, res: Response): Promise<void> {
1✔
167
    const body = req.body as WriteOffRequest;
3✔
168
    this.logger.trace('Create write off by user', req.token.user);
3✔
169

170
    try {
3✔
171
      const user = await User.findOne({ where: { id: body.toId, deleted: false } });
3✔
172
      if (!user) {
3✔
173
        res.status(404).json('User not found.');
1✔
174
        return;
1✔
175
      }
1✔
176

177
      const balance = await new BalanceService().getBalance(user.id);
2✔
178
      if (balance.amount.amount > 0) {
3✔
179
        res.status(400).json('User has balance, cannot create write off');
1✔
180
        return;
1✔
181
      }
1✔
182

183
      const writeOff = await new WriteOffService().createWriteOffAndCloseUser(user);
1✔
184
      res.status(200).json(WriteOffService.asWriteOffResponse(writeOff));
1✔
185
    } catch (error) {
3!
186
      this.logger.error('Could not create write off:', error);
×
187
      res.status(500).json('Internal server error.');
×
188
    }
×
189
  }
3✔
190

191
  /**
1✔
192
   * GET /writeoffs/{id}/pdf
193
   * @summary Get a write-off pdf
194
   * @operationId getWriteOffPdf
195
   * @tags writeoffs - Operations of the writeoff controller
196
   * @security JWT
197
   * @param {integer} id.path.required - The ID of the write-off object that should be returned
198
   * @param {boolean} force.query - Whether to force regeneration of the pdf
199
   * @return {PdfUrlResponse} 200 - The pdf location information.
200
   * @return {string} 404 - Nonexistent write off id
201
   * @return {string} 500 - Internal server error
202
   * @return {string} 502 - PDF generation failed
203
   */
1✔
204
  public async getWriteOffPdf(req: RequestWithToken, res: Response): Promise<void> {
1✔
205
    const { id } = req.params;
3✔
206
    const writeOffId = parseInt(id, 10);
3✔
207
    this.logger.trace('Get write off pdf', id, 'by user', req.token.user);
3✔
208

209
    try {
3✔
210
      const force = !!asBoolean(req.query.force);
3✔
211
      const writeOff = await WriteOff.findOne({ where: { id: writeOffId }, relations: {
3✔
212
        transfer: true,
3✔
213
      } });
3✔
214
      if (!writeOff) {
3✔
215
        res.status(404).json('Write Off not found.');
1✔
216
        return;
1✔
217
      }
1✔
218

219
      const pdf = await writeOff.getOrCreatePdf(force);
2✔
220

221
      res.status(200).json({ pdf: pdf.downloadName } as PdfUrlResponse);
1✔
222
    } catch (error) {
1✔
223
      this.logger.error('Could get write off PDF:', error);
1✔
224
      if (error instanceof PdfError) {
1✔
225
        res.status(502).json('PDF Generator service failed.');
1✔
226
        return;
1✔
227
      }
1!
228
      res.status(500).json('Internal server error.');
×
229
    }
×
230
  }
3✔
231
}
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