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

GEWIS / sudosos-backend / 15960206735

29 Jun 2025 10:50PM UTC coverage: 85.143% (+0.01%) from 85.13%
15960206735

Pull #560

github

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

1302 of 1589 branches covered (81.94%)

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.3 hits per line

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

81.82
/src/controller/write-off-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 the write-off-controller.
23
 *
24
 * @module write-offs
25
 */
26

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

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

45
  public constructor(options: BaseControllerOptions) {
46
    super(options);
2✔
47
    this.logger.level = process.env.LOG_LEVEL;
2✔
48
  }
49

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

78

79
  /**
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
   */
95
  public async returnAllWriteOffs(req: RequestWithToken, res: Response): Promise<void> {
96
    this.logger.trace('Get all write offs by ', req.token.user);
5✔
97

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

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

121
  /**
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
   */
131
  public async getSingleWriteOff(req: RequestWithToken, res: Response): Promise<void> {
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
      }
143

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

151
  /**
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
   */
162
  public async createWriteOff(req: RequestWithToken, res: Response): Promise<void> {
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
      }
172

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

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

187
  /**
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
   * @return {PdfUrlResponse} 200 - The pdf location information.
195
   * @return {string} 404 - Nonexistent write off id
196
   * @return {string} 500 - Internal server error
197
   */
198
  public async getWriteOffPdf(req: RequestWithToken, res: Response): Promise<void> {
199
    const { id } = req.params;
2✔
200
    const writeOffId = parseInt(id, 10);
2✔
201
    this.logger.trace('Get write off pdf', id, 'by user', req.token.user);
2✔
202

203
    try {
2✔
204
      const writeOff = await WriteOff.findOne({ where: { id: writeOffId }, relations: ['transfer'] });
2✔
205
      if (!writeOff) {
2✔
206
        res.status(404).json('Write Off not found.');
1✔
207
        return;
1✔
208
      }
209

210
      const pdf = await writeOff.getOrCreatePdf();
1✔
211

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