• 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

87.42
/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
 * This is the module page of the write-off-controller.
23
 *
24
 * @module write-offs
25
 */
1✔
26

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

42
export default class WriteOffController extends BaseController {
1✔
43
  private logger: Logger = log4js.getLogger(' WriteOffController');
1✔
44

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

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

78

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

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

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

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

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

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

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

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

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

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

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

205
    try {
3✔
206
      const force = !!asBoolean(req.query.force);
3✔
207
      const writeOff = await WriteOff.findOne({ where: { id: writeOffId }, relations: ['transfer'] });
3✔
208
      if (!writeOff) {
3✔
209
        res.status(404).json('Write Off not found.');
1✔
210
        return;
1✔
211
      }
1✔
212

213
      const pdf = await writeOff.getOrCreatePdf(force);
2✔
214

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