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

GEWIS / sudosos-backend / 23805758284

31 Mar 2026 03:33PM UTC coverage: 89.088% (-0.03%) from 89.12%
23805758284

push

github

web-flow
feat: add drift detection endpoint for invoice transfer amounts (#825)

Adds GET /invoices/drift — an admin-only endpoint that returns all
non-deleted invoices where the recorded transfer amount does not match
the sum of the linked sub-transaction row totals (amount × priceInclVat).

Each response entry embeds the full BaseInvoiceResponse so the frontend
has full context without a second fetch, plus actualAmount, expectedAmount
and delta as Dinero objects.

Closes #790

1810 of 2216 branches covered (81.68%)

Branch coverage included in aggregate %.

15 of 22 new or added lines in 2 files covered. (68.18%)

9285 of 10238 relevant lines covered (90.69%)

1004.47 hits per line

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

75.12
/src/controller/invoice-controller.ts
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
 */
20

21
/**
22
 * This is the page of invoice controller.
23
 *
24
 * @module invoicing
25
 */
26

27
import log4js, { Logger } from 'log4js';
2✔
28
import { Response } from 'express';
29
import BaseController, { BaseControllerOptions } from './base-controller';
2✔
30
import Policy from './policy';
31
import { RequestWithToken } from '../middleware/token-middleware';
32
import InvoiceService, { InvoiceFilterParameters, parseInvoiceFilterParameters } from '../service/invoice-service';
2✔
33
import { parseRequestPagination, toResponse } from '../helpers/pagination';
2✔
34
import {
35
  CreateInvoiceParams,
36
  CreateInvoiceRequest,
37
  UpdateInvoiceParams,
38
  UpdateInvoiceRequest,
39
} from './request/invoice-request';
40
import verifyCreateInvoiceRequest, { verifyUpdateInvoiceRequest } from './request/validators/invoice-request-spec';
2✔
41
import { isFail } from '../helpers/specification-validation';
2✔
42
import { asBoolean, asDate, asInvoiceState, asNumber } from '../helpers/validators';
2✔
43
import Invoice from '../entity/invoices/invoice';
2✔
44
import User, { UserType } from '../entity/user/user';
2✔
45
import { UpdateInvoiceUserRequest } from './request/user-request';
46
import InvoiceUser from '../entity/user/invoice-user';
2✔
47
import { parseInvoiceUserToResponse } from '../helpers/revision-to-response';
2✔
48
import { AppDataSource } from '../database/database';
2✔
49
import { NotImplementedError, PdfError } from '../errors';
2✔
50

51
/**
52
 * The Invoice controller.
53
 */
54
export default class InvoiceController extends BaseController {
2✔
55
  private logger: Logger = log4js.getLogger('InvoiceController');
2✔
56

57
  /**
58
    * Creates a new Invoice controller instance.
59
    * @param options - The options passed to the base controller.
60
    */
61
  public constructor(options: BaseControllerOptions) {
62
    super(options);
2✔
63
    this.logger.level = process.env.LOG_LEVEL;
2✔
64
  }
65

66
  /**
67
    * @inhertidoc
68
    */
69
  getPolicy(): Policy {
70
    return {
2✔
71
      '/': {
72
        GET: {
73
          policy: async (req) => this.roleManager.can(req.token.roles, 'get', 'all', 'Invoice', ['*']),
5✔
74
          handler: this.getAllInvoices.bind(this),
75
        },
76
        POST: {
77
          body: { modelName: 'CreateInvoiceRequest' },
78
          policy: async (req) => this.roleManager.can(req.token.roles, 'create', 'all', 'Invoice', ['*']),
7✔
79
          handler: this.createInvoice.bind(this),
80
        },
81
      },
82
      '/users/:id(\\d+)': {
83
        GET: {
84
          policy: async (req) => this.roleManager.can(req.token.roles, 'get', 'all', 'Invoice', ['*']),
5✔
85
          handler: this.getSingleInvoiceUser.bind(this),
86
        },
87
        PUT : {
88
          body: { modelName: 'UpdateInvoiceUserRequest' },
89
          policy: async (req) => this.roleManager.can(req.token.roles, 'update', 'all', 'Invoice', ['*']),
6✔
90
          handler: this.updateInvoiceUser.bind(this),
91
        },
92
        DELETE: {
93
          policy: async (req) => this.roleManager.can(req.token.roles, 'delete', 'all', 'Invoice', ['*']),
3✔
94
          handler: this.deleteInvoiceUser.bind(this),
95
        },
96
      },
97
      '/:id(\\d+)': {
98
        GET: {
99
          policy: async (req) => this.roleManager.can(req.token.roles, 'get', await InvoiceController.getRelation(req), 'Invoice', ['*']),
6✔
100
          handler: this.getSingleInvoice.bind(this),
101
        },
102
        PATCH: {
103
          body: { modelName: 'UpdateInvoiceRequest' },
104
          policy: async (req) => this.roleManager.can(req.token.roles, 'update', 'all', 'Invoice', ['*']),
5✔
105
          handler: this.updateInvoice.bind(this),
106
        },
107
        DELETE: {
108
          policy: async (req) => this.roleManager.can(req.token.roles, 'delete', 'all', 'Invoice', ['*']),
2✔
109
          handler: this.deleteInvoice.bind(this),
110
        },
111
      },
112
      '/:id(\\d+)/pdf': {
113
        GET: {
114
          policy: async (req) => this.roleManager.can(req.token.roles, 'get', await InvoiceController.getRelation(req), 'Invoice', ['*']),
3✔
115
          handler: this.getInvoicePDF.bind(this),
116
        },
117
      },
118
      '/eligible-transactions': {
119
        GET: {
120
          policy: async (req) => this.roleManager.can(req.token.roles, 'get', 'all', 'Invoice', ['*']),
×
121
          handler: this.getEligibleTransactions.bind(this),
122
        },
123
      },
124
      '/drift': {
125
        GET: {
NEW
126
          policy: async (req) => this.roleManager.can(req.token.roles, 'get', 'all', 'Invoice', ['*']),
×
127
          handler: this.getDriftedInvoices.bind(this),
128
        },
129
      },
130
    };
131
  }
132

133
  /**
134
   * GET /invoices
135
   * @summary Returns all invoices in the system.
136
   * @operationId getAllInvoices
137
   * @tags invoices - Operations of the invoices controller
138
   * @security JWT
139
   * @param {integer} toId.query - Filter on Id of the debtor
140
   * @param {number} invoiceId.query - Filter on invoice ID
141
   * @param {Array<string|number>} currentState.query enum:CREATED,SENT,PAID,DELETED - Filter based on Invoice State.
142
   * @param {boolean} returnEntries.query - Boolean if invoice entries should be returned
143
   * @param {string} fromDate.query - Start date for selected invoices (inclusive)
144
   * @param {string} tillDate.query - End date for selected invoices (exclusive)
145
   * @param {string} description.query - Filter invoices by description (partial match)
146
   * @param {integer} take.query - How many entries the endpoint should return
147
   * @param {integer} skip.query - How many entries should be skipped (for pagination)
148
   * @return {PaginatedInvoiceResponse} 200 - All existing invoices
149
   * @return {string} 500 - Internal server error
150
   */
151
  public async getAllInvoices(req: RequestWithToken, res: Response): Promise<void> {
152
    const { body } = req;
4✔
153
    this.logger.trace('Get all invoices', body, 'by user', req.token.user);
4✔
154

155
    let take;
156
    let skip;
157
    let filters: InvoiceFilterParameters;
158
    try {
4✔
159
      const pagination = parseRequestPagination(req);
4✔
160
      filters = parseInvoiceFilterParameters(req);
4✔
161
      take = pagination.take;
4✔
162
      skip = pagination.skip;
4✔
163
    } catch (e) {
164
      res.status(400).send(e.message);
×
165
      return;
×
166
    }
167

168
    // Handle request
169
    try {
4✔
170
      const [invoices, count] = await new InvoiceService().getPaginatedInvoices(
4✔
171
        filters, { take, skip },
172
      );
173

174
      const records = filters.returnInvoiceEntries
4!
175
        ? InvoiceService.toArrayResponse(invoices)
176
        : InvoiceService.toArrayWithoutEntriesResponse(invoices);
177

178
      res.json(toResponse(records, count, { take, skip }));
4✔
179
    } catch (error) {
180
      this.logger.error('Could not return all invoices:', error);
×
181
      res.status(500).json('Internal server error.');
×
182
    }
183
  }
184

185
  /**
186
   * GET /invoices/{id}
187
   * @summary Returns a single invoice in the system.
188
   * @operationId getSingleInvoice
189
   * @param {integer} id.path.required - The id of the requested invoice
190
   * @tags invoices - Operations of the invoices controller
191
   * @security JWT
192
   * @param {boolean} returnEntries.query -
193
   * Boolean if invoice entries should be returned, defaults to true.
194
   * @return {InvoiceResponse} 200 - All existing invoices
195
   * @return {string} 404 - Invoice not found
196
   * @return {string} 500 - Internal server error
197
   */
198
  public async getSingleInvoice(req: RequestWithToken, res: Response): Promise<void> {
199
    const { id } = req.params;
6✔
200
    const invoiceId = parseInt(id, 10);
6✔
201
    this.logger.trace('Get invoice', invoiceId, 'by user', req.token.user);
6✔
202

203
    // Handle request
204
    try {
6✔
205
      const returnInvoiceEntries = asBoolean(req.query.returnEntries) ?? true;
6✔
206

207
      const invoices: Invoice[] = await new InvoiceService().getInvoices(
6✔
208
        { invoiceId, returnInvoiceEntries },
209
      );
210

211
      const invoice = invoices[0];
6✔
212
      if (!invoice) {
6✔
213
        res.status(404).json('Unknown invoice ID.');
1✔
214
        return;
1✔
215
      }
216
      const response = returnInvoiceEntries
5!
217
        ? InvoiceService.asInvoiceResponse(invoice)
218
        : InvoiceService.asBaseInvoiceResponse(invoice);
219

220
      res.json(response);
5✔
221
    } catch (error) {
222
      this.logger.error('Could not return invoice:', error);
×
223
      res.status(500).json('Internal server error.');
×
224
    }
225
  }
226

227
  /**
228
   * POST /invoices
229
   * @summary Adds an invoice to the system.
230
   * @operationId createInvoice
231
   * @tags invoices - Operations of the invoices controller
232
   * @security JWT
233
   * @param {CreateInvoiceRequest} request.body.required -
234
   * The invoice which should be created
235
   * @return {InvoiceResponse} 200 - The created invoice entity
236
   * @return {string} 400 - Validation error
237
   * @return {string} 500 - Internal server error
238
   */
239
  public async createInvoice(req: RequestWithToken, res: Response): Promise<void> {
240
    const body = req.body as CreateInvoiceRequest;
6✔
241
    this.logger.trace('Create Invoice', body, 'by user', req.token.user);
6✔
242

243
    // handle request
244
    try {
6✔
245
      const userDefinedDefaults = await new InvoiceService().getDefaultInvoiceParams(body.forId);
6✔
246

247
      // If no byId is provided we use the token user id.
248
      const params: CreateInvoiceParams = {
6✔
249
        ...userDefinedDefaults,
250
        ...body,
251
        date: body.date ? new Date(body.date) : new Date(),
6✔
252
        byId: body.byId ?? req.token.user.id,
6!
253
        description: body.description ?? '',
6!
254
      };
255

256
      const validation = await verifyCreateInvoiceRequest(params);
6✔
257
      if (isFail(validation)) {
6✔
258
        res.status(400).json(validation.fail.value);
5✔
259
        return;
5✔
260
      }
261

262
      const invoice: Invoice = await AppDataSource.manager.transaction(async (manager) =>
1✔
263
        new InvoiceService(manager).createInvoice(params));
1✔
264
      res.json(InvoiceService.asInvoiceResponse(invoice));
1✔
265
    } catch (error) {
266
      if (error instanceof NotImplementedError) {
×
267
        res.status(501).json(error.message);
×
268
        return;
×
269
      }
270
      this.logger.error('Could not create invoice:', error);
×
271
      res.status(500).json('Internal server error.');
×
272
    }
273
  }
274

275
  /**
276
   * PATCH /invoices/{id}
277
   * @summary Adds an invoice to the system.
278
   * @operationId updateInvoice
279
   * @tags invoices - Operations of the invoices controller
280
   * @security JWT
281
   * @param {integer} id.path.required - The id of the invoice which should be updated
282
   * @param {UpdateInvoiceRequest} request.body.required -
283
   * The invoice update to process
284
   * @return {BaseInvoiceResponse} 200 - The updated invoice entity
285
   * @return {string} 400 - Validation error
286
   * @return {string} 500 - Internal server error
287
   */
288
  public async updateInvoice(req: RequestWithToken, res: Response): Promise<void> {
289
    const body = req.body as UpdateInvoiceRequest;
4✔
290
    const { id } = req.params;
4✔
291
    const invoiceId = parseInt(id, 10);
4✔
292
    this.logger.trace('Update Invoice', body, 'by user', req.token.user);
4✔
293

294
    try {
4✔
295
      // Default byId to token user id.
296
      const params: UpdateInvoiceParams = {
4✔
297
        ...body,
298
        invoiceId,
299
        state: asInvoiceState(body.state),
300
        byId: body.byId ?? req.token.user.id,
8✔
301
      };
302

303
      const validation = await verifyUpdateInvoiceRequest(params);
4✔
304
      if (isFail(validation)) {
4✔
305
        res.status(400).json(validation.fail.value);
3✔
306
        return;
3✔
307
      }
308

309
      const invoice: Invoice = await AppDataSource.manager.transaction(async (manager) =>
1✔
310
        new InvoiceService(manager).updateInvoice(params));
1✔
311

312
      res.json(InvoiceService.asBaseInvoiceResponse(invoice));
1✔
313
    } catch (error) {
314
      this.logger.error('Could not update invoice:', error);
×
315
      res.status(500).json('Internal server error.');
×
316
    }
317
  }
318

319
  /**
320
   * DELETE /invoices/{id}
321
   * @summary Deletes an invoice.
322
   * @operationId deleteInvoice
323
   * @tags invoices - Operations of the invoices controller
324
   * @security JWT
325
   * @param {integer} id.path.required - The id of the invoice which should be deleted
326
   * @return {string} 404 - Invoice not found
327
   * @return 204 - Deletion success
328
   * @return {string} 500 - Internal server error
329
   */
330
  // TODO Deleting of invoices that are not of state CREATED?
331
  public async deleteInvoice(req: RequestWithToken, res: Response): Promise<void> {
332
    const { id } = req.params;
2✔
333
    const invoiceId = parseInt(id, 10);
2✔
334
    this.logger.trace('Delete Invoice', id, 'by user', req.token.user);
2✔
335

336
    try {
2✔
337
      const invoice = await AppDataSource.manager.transaction(async (manager) =>
2✔
338
        new InvoiceService(manager).deleteInvoice(invoiceId, req.token.user.id));
2✔
339
      if (!invoice) {
2✔
340
        res.status(404).json('Invoice not found.');
1✔
341
        return;
1✔
342
      }
343
      res.status(204).send();
1✔
344
    } catch (error) {
345
      this.logger.error('Could not delete invoice:', error);
×
346
      res.status(500).json('Internal server error.');
×
347
    }
348
  }
349

350
  /**
351
   * GET /invoices/{id}/pdf
352
   * @summary Get an invoice pdf.
353
   * @operationId getInvoicePdf
354
   * @tags invoices - Operations of the invoices controller
355
   * @security JWT
356
   * @param {integer} id.path.required - The id of the invoice to return
357
   * @param {boolean} force.query - Force creation of pdf
358
   * @return {string} 404 - Invoice not found
359
   * @return {string} 200 - The pdf location information.
360
   * @return {string} 500 - Internal server error
361
   */
362
  public async getInvoicePDF(req: RequestWithToken, res: Response): Promise<void> {
363
    const { id } = req.params;
2✔
364
    const invoiceId = parseInt(id, 10);
2✔
365
    this.logger.trace('Get Invoice PDF', id, 'by user', req.token.user);
2✔
366

367
    try {
2✔
368
      const invoice = await Invoice.findOne(InvoiceService.getOptions({ invoiceId, returnInvoiceEntries: true }) );
2✔
369
      if (!invoice) {
2✔
370
        res.status(404).json('Invoice not found.');
1✔
371
        return;
1✔
372
      }
373

374
      const pdf = await invoice.getOrCreatePdf(req.query.force === 'true');
1✔
375

376
      res.status(200).json({ pdf: pdf.downloadName });
1✔
377
    } catch (error) {
378
      this.logger.error('Could get invoice PDF:', error);
×
379
      if (error instanceof PdfError) {
×
380
        res.status(502).json('PDF Generator service failed.');
×
381
        return;
×
382
      }
383
      res.status(500).json('Internal server error.');
×
384
    }
385
  }
386

387
  /**
388
   * DELETE /invoices/users/{id}
389
   * @summary Delete invoice user defaults.
390
   * @operationId deleteInvoiceUser
391
   * @tags invoices - Operations of the invoices controller
392
   * @security JWT
393
   * @param {integer} id.path.required - The id of the invoice user to delete.
394
   * @return {string} 404 - Invoice User not found
395
   * @return 204 - Success
396
   * @return {string} 500 - Internal server error
397
   */
398
  public async deleteInvoiceUser(req: RequestWithToken, res: Response): Promise<void> {
399
    const { id } = req.params;
2✔
400
    const userId = parseInt(id, 10);
2✔
401
    this.logger.trace('Delete Invoice User', id, 'by user', req.token.user);
2✔
402

403
    try {
2✔
404
      const invoiceUser = await InvoiceUser.findOne({ where: { userId } });
2✔
405
      if (!invoiceUser) {
2✔
406
        res.status(404).json('Invoice User not found.');
1✔
407
        return;
1✔
408
      }
409

410
      await InvoiceUser.delete(userId);
1✔
411
      res.status(204).json();
1✔
412
    } catch (error) {
413
      this.logger.error('Could not get invoice user:', error);
×
414
      res.status(500).json('Internal server error.');
×
415
    }
416
  }
417

418
  /**
419
   * GET /invoices/users/{id}
420
   * @summary Get invoice user defaults.
421
   * @operationId getSingleInvoiceUser
422
   * @tags invoices - Operations of the invoices controller
423
   * @security JWT
424
   * @param {integer} id.path.required - The id of the invoice user to return.
425
   * @return {string} 404 - Invoice User not found
426
   * @return {string} 404 - User not found
427
   * @return {string} 400 - User is not of type INVOICE
428
   * @return {InvoiceUserResponse} 200 - The requested Invoice User
429
   * @return {string} 500 - Internal server error
430
   */
431
  public async getSingleInvoiceUser(req: RequestWithToken, res: Response): Promise<void> {
432
    const { id } = req.params;
4✔
433
    const userId = parseInt(id, 10);
4✔
434
    this.logger.trace('Get Invoice User', id, 'by user', req.token.user);
4✔
435

436
    try {
4✔
437
      const user = await User.findOne({ where: { id: userId, deleted: false } });
4✔
438
      if (!user) {
4✔
439
        res.status(404).json('User not found.');
1✔
440
        return;
1✔
441
      }
442

443
      if (user.type !== UserType.INVOICE) {
3✔
444
        res.status(400).json(`User is of type ${UserType[user.type]} and not of type INVOICE.`);
1✔
445
        return;
1✔
446
      }
447

448
      const invoiceUser = await InvoiceUser.findOne({ where: { userId }, relations: ['user'] });
2✔
449
      if (!invoiceUser) {
2✔
450
        res.status(404).json('Invoice User not found.');
1✔
451
        return;
1✔
452
      }
453

454
      res.status(200).json(parseInvoiceUserToResponse(invoiceUser));
1✔
455
    } catch (error) {
456
      this.logger.error('Could not get invoice user:', error);
×
457
      res.status(500).json('Internal server error.');
×
458
    }
459
  }
460

461
  /**
462
   * PUT /invoices/users/{id}
463
   * @summary Update or create invoice user defaults.
464
   * @operationId putInvoiceUser
465
   * @tags invoices - Operations of the invoices controller
466
   * @security JWT
467
   * @param {integer} id.path.required - The id of the user to update
468
   * @param {UpdateInvoiceUserRequest} request.body.required - The invoice user which should be updated
469
   * @return {string} 404 - User not found
470
   * @return {string} 400 - User is not of type INVOICE
471
   * @return {InvoiceUserResponse} 200 - The updated / created Invoice User
472
   * @return {string} 500 - Internal server error
473
   */
474
  public async updateInvoiceUser(req: RequestWithToken, res: Response): Promise<void> {
475
    const { id } = req.params;
5✔
476
    const body = req.body as UpdateInvoiceUserRequest;
5✔
477
    const userId = parseInt(id, 10);
5✔
478
    this.logger.trace('Update Invoice User', id, 'by user', req.token.user);
5✔
479

480
    try {
5✔
481
      const user = await User.findOne({ where: { id: userId, deleted: false } });
5✔
482
      if (!user) {
5✔
483
        res.status(404).json('User not found.');
1✔
484
        return;
1✔
485
      }
486

487
      if (!([UserType.INVOICE, UserType.ORGAN].includes(user.type))) {
4✔
488
        res.status(400).json(`User is of type ${UserType[user.type]} and not of type INVOICE or ORGAN.`);
1✔
489
        return;
1✔
490
      }
491

492
      let invoiceUser = Object.assign(new InvoiceUser(), {
3✔
493
        ...body,
494
        user,
495
      }) as InvoiceUser;
496

497
      invoiceUser = await InvoiceUser.save(invoiceUser);
3✔
498

499
      res.status(200).json(parseInvoiceUserToResponse(invoiceUser));
3✔
500
    } catch (error) {
501
      this.logger.error('Could not update invoice user:', error);
×
502
      res.status(500).json('Internal server error.');
×
503
    }
504
  }
505

506
  /**
507
   * GET /invoices/eligible-transactions
508
   * @summary Get eligible transactions for invoice creation.
509
   * @operationId getEligibleTransactions
510
   * @tags invoices - Operations of the invoices controller
511
   * @security JWT
512
   * @param {integer} forId.query.required - Filter on Id of the debtor
513
   * @param {string} fromDate.query.required - Start date for selected transactions (inclusive)
514
   * @param {string} tillDate.query - End date for selected transactions (exclusive)
515
   * @return {TransactionResponse} 200 - The eligible transactions
516
   * @return {string} 500 - Internal server error
517
   */
518
  public async getEligibleTransactions(req: RequestWithToken, res: Response): Promise<void> {
519
    this.logger.trace('Get eligible transactions for invoice creation', req.query, 'by user', req.token.user);
×
520

521
    let fromDate, tillDate;
522
    let forId;
523
    try {
×
524
      forId = asNumber(req.query.forId);
×
525
      fromDate = asDate(req.query.fromDate);
×
526
      tillDate = req.query.tillDate ? asDate(req.query.tillDate) : undefined;
×
527
    } catch (e) {
528
      res.status(400).send(e.message);
×
529
      return;
×
530
    }
531

532
    try {
×
533
      const transactions = await new InvoiceService().getEligibleTransactions({
×
534
        forId,
535
        fromDate,
536
        tillDate,
537
      });
538
      res.json(transactions);
×
539
    } catch (error) {
540
      this.logger.error('Could not get eligible transactions:', error);
×
541
      res.status(500).json('Internal server error.');
×
542
    }
543
  }
544

545

546
  /**
547
   * GET /invoices/drift
548
   * @summary Returns all invoices with transfer amount drift: for active invoices transfer != row sum; for deleted invoices transfer != credit transfer.
549
   * @operationId getInvoiceDrift
550
   * @tags invoices - Operations of the invoices controller
551
   * @security JWT
552
   * @return {Array<InvoiceDriftResponse>} 200 - All invoices with transfer amount drift
553
   * @return {string} 500 - Internal server error
554
   */
555
  public async getDriftedInvoices(req: RequestWithToken, res: Response): Promise<void> {
NEW
556
    this.logger.trace('Get drifted invoices by user', req.token.user);
×
NEW
557
    try {
×
NEW
558
      const results = await new InvoiceService().findDriftedInvoices();
×
NEW
559
      res.json(results.map(InvoiceService.asInvoiceDriftResponse));
×
560
    } catch (error) {
NEW
561
      this.logger.error('Could not return drifted invoices:', error);
×
NEW
562
      res.status(500).json('Internal server error.');
×
563
    }
564
  }
565

566
  /**
567
   * Function to determine which credentials are needed to get invoice
568
   * all if user is not connected to invoice
569
   * own if user is connected to invoice
570
   * @param req
571
   * @return whether invoice is connected to used token
572
   */
573
  static async getRelation(req: RequestWithToken): Promise<string> {
574
    const invoice: Invoice = await Invoice.findOne({ where: { id: parseInt(req.params.id, 10) }, relations: ['to'] });
9✔
575
    if (!invoice) return 'all';
9✔
576
    if (invoice.to.id === req.token.user.id) return 'own';
7✔
577
    return 'all';
6✔
578
  }
579
}
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