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

GEWIS / sudosos-backend / 19103828027

05 Nov 2025 01:37PM UTC coverage: 89.835% (+0.06%) from 89.779%
19103828027

push

github

web-flow
feat: add an index to organ membership for consistent ordering (#619)

1400 of 1671 branches covered (83.78%)

Branch coverage included in aggregate %.

32 of 32 new or added lines in 4 files covered. (100.0%)

16 existing lines in 1 file now uncovered.

7508 of 8245 relevant lines covered (91.06%)

1096.83 hits per line

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

87.34
/src/controller/transaction-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 transaction-controller.
23
 *
24
 * @module transactions
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 TransactionService, {
2✔
33
  parseGetTransactionsFilters,
34
} from '../service/transaction-service';
35
import { TransactionResponse } from './response/transaction-response';
36
import { parseRequestPagination } from '../helpers/pagination';
2✔
37
import { TransactionRequest } from './request/transaction-request';
38
import Transaction from '../entity/transactions/transaction';
2✔
39
import User from '../entity/user/user';
2✔
40
import { asNumber } from '../helpers/validators';
2✔
41
import userTokenInOrgan from '../helpers/token-helper';
2✔
42
import UserService from '../service/user-service';
2✔
43
import InvoiceService from '../service/invoice-service';
2✔
44

45
export default class TransactionController extends BaseController {
2✔
46
  private logger: Logger = log4js.getLogger('TransactionController');
3✔
47

48
  /**
49
   * Creates a new transaction controller instance.
50
   * @param options - The options passed to the base controller.
51
   */
52
  public constructor(options: BaseControllerOptions) {
53
    super(options);
3✔
54
    this.logger.level = process.env.LOG_LEVEL;
3✔
55
  }
56

57
  /**
58
   * @inheritDoc
59
   */
60
  public getPolicy(): Policy {
61
    return {
3✔
62
      '/': {
63
        GET: {
64
          policy: async (req) => this.roleManager.can(req.token.roles, 'get', await TransactionController.filterRelation(req), 'Transaction', ['*']),
34✔
65
          handler: this.getAllTransactions.bind(this),
66
        },
67
        POST: {
68
          body: { modelName: 'TransactionRequest' },
69
          policy: async (req) => this.roleManager.can(req.token.roles, 'create', await TransactionController.postRelation(req), 'Transaction', ['*']),
8✔
70
          handler: this.createTransaction.bind(this),
71
        },
72
      },
73
      '/:id(\\d+)': {
74
        GET: {
75
          policy: async (req) => this.roleManager.can(req.token.roles, 'get', await TransactionController.getRelation(req), 'Transaction', ['*']),
7✔
76
          handler: this.getTransaction.bind(this),
77
        },
78
        PATCH: {
79
          body: { modelName: 'TransactionRequest' },
80
          policy: async (req) => this.roleManager.can(req.token.roles, 'update', await TransactionController.postRelation(req), 'Transaction', ['*']),
4✔
81
          handler: this.updateTransaction.bind(this),
82
        },
83
        DELETE: {
84
          policy: async (req) => this.roleManager.can(req.token.roles, 'delete', await TransactionController.getRelation(req), 'Transaction', ['*']),
3✔
85
          handler: this.deleteTransaction.bind(this),
86
        },
87
      },
88
      '/:id(\\d+)/invoices': {
89
        GET: {
90
          policy: async (req) => this.roleManager.can(req.token.roles, 'get', 'all', 'Invoice', ['*']),
3✔
91
          handler: this.getTransactionInvoices.bind(this),
92
        },
93
      },
94
      '/:validate': {
95
        POST: {
96
          policy: async (req) => this.roleManager.can(req.token.roles, 'create', await TransactionController.getRelation(req), 'Transaction', ['*']),
2✔
97
          handler: this.validateTransaction.bind(this),
98
        },
99
      },
100
    };
101
  }
102

103
  /**
104
   * GET /transactions
105
   * @summary Get a list of all transactions
106
   * @operationId getAllTransactions
107
   * @tags transactions - Operations of the transaction controller
108
   * @security JWT
109
   * @param {integer} fromId.query - From-user for selected transactions
110
   * @param {integer} createdById.query - User that created selected transaction
111
   * @param {integer} toId.query - To-user for selected transactions
112
   * @param {integer} excludeById.query - Created by user to exclude from transactions
113
   * @param {integer} excludeFromId.query - From user to exclude from transactions
114
   * @param {integer} pointOfSaleId.query - Point of sale ID for selected transactions
115
   * @param {integer} productId.query - Product ID for selected transactions
116
   * @param {integer} productRevision.query - Product Revision for selected transactions. Requires ProductID
117
   * @param {string} fromDate.query - Start date for selected transactions (inclusive)
118
   * @param {string} tillDate.query - End date for selected transactions (exclusive)
119
   * @param {integer} take.query - How many transactions the endpoint should return
120
   * @param {integer} skip.query - How many transactions should be skipped (for pagination)
121
   * @return {PaginatedBaseTransactionResponse} 200 - A list of all transactions
122
   */
123
  public async getAllTransactions(req: RequestWithToken, res: Response): Promise<void> {
124
    this.logger.trace('Get all transactions by user', req.token.user);
30✔
125

126
    // Parse the filters given in the query parameters. If there are any issues,
127
    // the parse method will throw an exception. We will then return a 400 error.
128
    let filters;
129
    let take;
130
    let skip;
131
    try {
30✔
132
      filters = parseGetTransactionsFilters(req);
30✔
133
      const pagination = parseRequestPagination(req);
22✔
134
      take = pagination.take;
19✔
135
      skip = pagination.skip;
19✔
136
    } catch (e) {
137
      res.status(400).json(e.message);
11✔
138
      return;
11✔
139
    }
140

141
    try {
19✔
142
      const transactions = await new TransactionService().getTransactions(filters, { take, skip });
19✔
143
      res.status(200).json(transactions);
19✔
144
    } catch (e) {
UNCOV
145
      res.status(500).send();
×
UNCOV
146
      this.logger.error(e);
×
147
    }
148
  }
149

150
  /**
151
   * POST /transactions
152
   * @summary Creates a new transaction
153
   * @operationId createTransaction
154
   * @tags transactions - Operations of the transaction controller
155
   * @param {TransactionRequest} request.body.required -
156
   * The transaction which should be created
157
   * @security JWT
158
   * @return {TransactionResponse} 200 - The created transaction entity
159
   * @return {string} 400 - Validation error
160
   * @return {string} 403 - Insufficient balance error
161
   * @return {string} 500 - Internal server error
162
   */
163
  public async createTransaction(req: RequestWithToken, res: Response): Promise<void> {
164
    const body = req.body as TransactionRequest;
6✔
165
    this.logger.trace('Create transaction', body, 'by user', req.token.user);
6✔
166

167
    // handle request
168
    try {
6✔
169
      if (!await new TransactionService().verifyTransaction(body)) {
6✔
170
        res.status(400).json('Invalid transaction.');
1✔
171
        return;
1✔
172
      }
173

174
      // verify balance if user cannot have negative balance.
175
      const user = await User.findOne({ where: { id: body.from } });
5✔
176
      if (!user.canGoIntoDebt && !await new TransactionService().verifyBalance(body)) {
5✔
177
        res.status(403).json('Insufficient balance.');
1✔
178
      } else {
179
        // create the transaction
180
        res.json(await new TransactionService().createTransaction(body));
4✔
181
      }
182
    } catch (error) {
UNCOV
183
      this.logger.error('Could not create transaction:', error);
×
UNCOV
184
      res.status(500).json('Internal server error.');
×
185
    }
186
  }
187

188
  /**
189
   * GET /transactions/{id}
190
   * @summary Get a single transaction
191
   * @operationId getSingleTransaction
192
   * @tags transactions - Operations of the transaction controller
193
   * @param {integer} id.path.required - The id of the transaction which should be returned
194
   * @security JWT
195
   * @return {TransactionResponse} 200 - Single transaction with given id
196
   * @return {string} 404 - Nonexistent transaction id
197
   */
198
  public async getTransaction(req: RequestWithToken, res: Response): Promise<TransactionResponse> {
199
    const parameters = req.params;
6✔
200
    this.logger.trace('Get single transaction', parameters, 'by user', req.token.user);
6✔
201

202
    let transaction;
203
    try {
6✔
204
      transaction = await new TransactionService().getSingleTransaction(parseInt(parameters.id, 10));
6✔
205
    } catch (e) {
206
      res.status(500).send();
×
207
      this.logger.error(e);
×
UNCOV
208
      return;
×
209
    }
210

211
    // If the transaction is undefined, there does not exist a transaction with the given ID
212
    if (transaction === undefined) {
6!
UNCOV
213
      res.status(404).json('Unknown transaction ID.');
×
UNCOV
214
      return;
×
215
    }
216

217
    res.status(200).json(transaction);
6✔
218
  }
219

220
  /**
221
   * PATCH /transactions/{id}
222
   * @summary Updates the requested transaction
223
   * @operationId updateTransaction
224
   * @tags transactions - Operations of transaction controller
225
   * @param {integer} id.path.required - The id of the transaction which should be updated
226
   * @param {TransactionRequest} request.body.required -
227
   * The updated transaction
228
   * @security JWT
229
   * @return {TransactionResponse} 200 - The requested transaction entity
230
   * @return {string} 400 - Validation error
231
   * @return {string} 404 - Not found error
232
   * @return {string} 500 - Internal server error
233
   */
234
  public async updateTransaction(req: RequestWithToken, res: Response): Promise<void> {
235
    const body = req.body as TransactionRequest;
3✔
236
    const { id } = req.params;
3✔
237
    this.logger.trace('Update Transaction', id, 'by user', req.token.user);
3✔
238

239
    // handle request
240
    try {
3✔
241
      if (await Transaction.findOne({ where: { id: parseInt(id, 10) } })) {
3✔
242
        if (await new TransactionService().verifyTransaction(body, true)) {
2✔
243
          res.status(200).json(await new TransactionService().updateTransaction(
1✔
244
            parseInt(id, 10), body,
245
          ));
246
        } else {
247
          res.status(400).json('Invalid transaction.');
1✔
248
        }
249
      } else {
250
        res.status(404).json('Transaction not found.');
1✔
251
      }
252
    } catch (error) {
UNCOV
253
      this.logger.error('Could not update transaction:', error);
×
UNCOV
254
      res.status(500).json('Internal server error.');
×
255
    }
256
  }
257

258
  /**
259
   * DELETE /transactions/{id}
260
   * @summary Deletes a transaction
261
   * @operationId deleteTransaction
262
   * @tags transactions - Operations of the transaction controller
263
   * @param {integer} id.path.required - The id of the transaction which should be deleted
264
   * @security JWT
265
   * @return 204 - No content
266
   * @return {string} 404 - Nonexistent transaction id
267
   */
268
  // eslint-disable-next-line class-methods-use-this
269
  public async deleteTransaction(req: RequestWithToken, res: Response): Promise<void> {
270
    const { id } = req.params;
2✔
271
    this.logger.trace('Delete transaction', id, 'by user', req.token.user);
2✔
272

273
    // handle request
274
    try {
2✔
275
      if (await Transaction.findOne({ where: { id: parseInt(id, 10) } })) {
2✔
276
        await new TransactionService().deleteTransaction(parseInt(id, 10));
1✔
277
        res.status(204).json();
1✔
278
      } else {
279
        res.status(404).json('Transaction not found.');
1✔
280
      }
281
    } catch (error) {
UNCOV
282
      this.logger.error('Could not delete transaction:', error);
×
UNCOV
283
      res.status(500).json('Internal server error.');
×
284
    }
285
  }
286

287
  /**
288
   * GET /transactions/{id}/invoices
289
   * @summary Get all invoices containing subtransaction rows from this transaction
290
   * @operationId getTransactionInvoices
291
   * @tags transactions - Operations of the transaction controller
292
   * @param {integer} id.path.required - The transaction ID
293
   * @security JWT
294
   * @return {Array<BaseInvoiceResponse>} 200 - List of invoices
295
   * @return {string} 404 - Transaction not found
296
   * @return {string} 500 - Internal server error
297
   */
298
  public async getTransactionInvoices(req: RequestWithToken, res: Response): Promise<void> {
299
    const { id } = req.params;
2✔
300
    const transactionId = parseInt(id, 10);
2✔
301
    this.logger.trace('Get transaction invoices', id, 'by user', req.token.user);
2✔
302

303
    try {
2✔
304
      const transaction = await Transaction.findOne({ where: { id: transactionId } });
2✔
305
      if (!transaction) {
2✔
306
        res.status(404).json('Transaction not found.');
1✔
307
        return;
1✔
308
      }
309

310
      const invoices = await new InvoiceService().getTransactionInvoices(transactionId);
1✔
311
      res.status(200).json(invoices);
1✔
312
    } catch (error) {
UNCOV
313
      this.logger.error('Could not return transaction invoices:', error);
×
UNCOV
314
      res.status(500).json('Internal server error.');
×
315
    }
316
  }
317

318
  /**
319
   * POST /transactions/validate
320
   * @summary Function to validate the transaction immediatly after it is created
321
   * @operationId validateTransaction
322
   * @tags transactions - Operations of the transaction controller
323
   * @param {TransactionRequest} request.body.required -
324
   * The transaction which should be validated
325
   * @return {boolean} 200 - Transaction validated
326
   * @security JWT
327
   * @return {string} 400 - Validation error
328
   * @return {string} 500 - Internal server error
329
   */
330
  public async validateTransaction(req: RequestWithToken, res: Response): Promise<void> {
331
    const body = req.body as TransactionRequest;
2✔
332
    this.logger.trace('Validate transaction', body, 'by user', req.token.user);
2✔
333

334
    try {
2✔
335
      if (await new TransactionService().verifyTransaction(body)) {
2✔
336
        res.status(200).json(true);
1✔
337
      } else  {
338
        res.status(400).json('Transaction is invalid');
1✔
339
        return;
1✔
340
      }
341
    } catch (error) {
UNCOV
342
      this.logger.error('Could not validate transaction:', error);
×
UNCOV
343
      res.status(500).json('Internal server error');
×
344
    }
345
  }
346

347

348
  /**
349
   * Determines the relation between the user and the transaction (by filters in the request).
350
   * - Returns 'own' if user is from, to, or createdBy.
351
   * - Returns 'organ' if user shares an organ with any of those users.
352
   * - Returns 'all' otherwise.
353
   *
354
   * @param req - Express request with user token and filters in query params.
355
   * @returns 'own' | 'organ' | 'all'
356
   */
357
  static async filterRelation(
358
    req: RequestWithToken,
359
  ): Promise<'own' | 'organ' | 'all'> {
360
    try {
34✔
361
      const userId = req.token.user.id;
34✔
362
      const { fromId, toId, createdById } = parseGetTransactionsFilters(req);
34✔
363

364
      // Check direct involvement
365
      if (fromId === userId || toId === userId || createdById === userId) {
26✔
366
        return 'own';
5✔
367
      }
368

369
      // Check organ relation
370
      if (
21!
371
        (fromId && await UserService.areInSameOrgan(userId, fromId)) ||
68✔
372
          (toId && await UserService.areInSameOrgan(userId, toId)) ||
373
          (createdById && await UserService.areInSameOrgan(userId, createdById))
374
      ) {
UNCOV
375
        return 'organ';
×
376
      }
377

378
      return 'all';
21✔
379
    } catch (error) {
380
      return 'all';
8✔
381
    }
382
  }
383
  
384
  /**
385
   * Function to determine which credentials are needed to post transaction
386
   *    all if user is not connected to transaction
387
   *    other if transaction createdby is and linked via organ
388
   *    own if user is connected to transaction
389
   * @param req - Request with TransactionRequest in the body
390
   * @return whether transaction is connected to user token
391
   */
392
  static async postRelation(req: RequestWithToken): Promise<string> {
393
    const request = req.body as TransactionRequest;
12✔
394
    if (request.createdBy !== req.token.user.id) {
12✔
395
      if (await UserService.areInSameOrgan(request.createdBy, req.token.user.id)) {
4✔
396
        return 'organ';
1✔
397
      }
398
      return 'all';
3✔
399
    }
400
    if (request.from === req.token.user.id) return 'own';
8✔
401
    return 'all';
3✔
402
  }
403

404
  /**
405
   * Function to determine which credentials are needed to get transactions
406
   *    all if user is not connected to transaction
407
   *    organ if user is not connected to transaction via organ
408
   *    own if user is connected to transaction
409
   * @param req - Request with transaction id as param
410
   * @return whether transaction is connected to user token
411
   */
412
  static async getRelation(req: RequestWithToken): Promise<string> {
413
    const transaction = await Transaction.findOne({
12✔
414
      where: { id: asNumber(req.params.id) },
415
      relations: ['from', 'createdBy', 'pointOfSale', 'pointOfSale.pointOfSale', 'pointOfSale.pointOfSale.owner'],
416
    });
417
    if (!transaction) return 'all';
12✔
418
    if (userTokenInOrgan(req, transaction.from.id)
10✔
419
        || userTokenInOrgan(req, transaction.createdBy.id)
420
        || userTokenInOrgan(req, transaction.pointOfSale.pointOfSale.owner.id)) return 'organ';
1✔
421
    if (transaction.from.id === req.token.user.id || transaction.createdBy.id === req.token.user.id) return 'own';
9✔
422
    return 'all';
4✔
423
  }
424
}
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