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

GEWIS / sudosos-backend / 29576813220

17 Jul 2026 11:25AM UTC coverage: 92.193%. First build
29576813220

Pull #967

github

web-flow
Merge 2049ed1cc into 2fdebeff3
Pull Request #967: feat: create (anonymous) transactions using Stripe Terminal

4387 of 4991 branches covered (87.9%)

Branch coverage included in aggregate %.

700 of 738 new or added lines in 18 files covered. (94.85%)

22433 of 24100 relevant lines covered (93.08%)

828.02 hits per line

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

94.26
/src/controller/terminal-payment-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 terminal payment controller
23
 *
24
 * @module stripe/terminal-payment
25
 */
1✔
26

27
import log4js, { Logger } from 'log4js';
28
import { Response } from 'express';
29
import BaseController, { BaseControllerOptions } from './base-controller';
30
import Policy from './policy';
31
import { RequestWithToken } from '../middleware/token-middleware';
32
import { CreateTerminalPaymentRequest, ProcessTerminalPaymentRequest } from './request/terminal-payment-request';
33
import { AppDataSource } from '../database/database';
34
import TerminalPaymentService from '../service/terminal-payment-service';
35
import { TerminalPaymentResponse } from './response/terminal-payment-response';
36
import StripeService from '../service/stripe-service';
37
import { asNumber } from '../helpers/validators';
38
import { TerminalPaymentState } from '../entity/transactions/terminal/terminal-payment';
39
import { UserType } from '../entity/user/user';
40

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

44
  /**
1✔
45
   * Create a new stripe controller instance
46
   * @param options
47
   */
1✔
48
  public constructor(options: BaseControllerOptions) {
1✔
49
    super(options);
1✔
50
    this.configureLogger(this.logger);
1✔
51
  }
1✔
52

53
  /**
1✔
54
   * @inheritDoc
55
   */
1✔
56
  public getPolicy(): Policy {
1✔
57
    return {
1✔
58
      '/': {
1✔
59
        POST: {
1✔
60
          policy: async (req) => this.roleManager.can(
1✔
61
            req.token.roles, 'create', 'all', 'TerminalPayment', ['*'],
8✔
62
          ),
63
          handler: this.createTerminalPayment.bind(this),
1✔
64
          body: { modelName: 'CreateTerminalPaymentRequest' },
1✔
65
        },
1✔
66
      },
1✔
67
      '/:id(\\d+)': {
1✔
68
        GET: {
1✔
69
          policy: async (req) => this.roleManager.can(
1✔
70
            req.token.roles, 'get', await TerminalPaymentController.getRelation(req), 'TerminalPayment', ['*'],
5✔
71
          ),
72
          handler: this.getSingleTerminalPayment.bind(this),
1✔
73
        },
1✔
74
        DELETE: {
1✔
75
          policy: async (req) => this.roleManager.can(
1✔
76
            req.token.roles, 'cancel', await TerminalPaymentController.getRelation(req), 'TerminalPayment', ['*'],
6✔
77
          ),
78
          handler: this.cancelTerminalPayment.bind(this),
1✔
79
        },
1✔
80
      },
1✔
81
      '/:id(\\d+)/process': {
1✔
82
        POST: {
1✔
83
          policy: async (req) => this.roleManager.can(
1✔
84
            req.token.roles, 'create', 'all', 'TerminalPayment', ['*'],
7✔
85
          ),
86
          handler: this.startTerminalPayment.bind(this),
1✔
87
          body: { modelName: 'ProcessTerminalPaymentRequest' },
1✔
88
        },
1✔
89
      },
1✔
90
      '/terminals': {
1✔
91
        GET: {
1✔
92
          policy: async (req) => this.roleManager.can(
1✔
NEW
93
            req.token.roles, 'get', 'all', 'TerminalPayment', ['*'],
×
94
          ),
95
          handler: this.getStripeTerminals.bind(this),
1✔
96
        },
1✔
97
      },
1✔
98
    };
1✔
99
  }
1✔
100

101
  /**
1✔
102
   * POST /terminal-payments
103
   *
104
   * @summary Create a terminal payment before executing.
105
   * @operationId createTerminalPayment
106
   * @tags terminalPayments - Operations of the Terminal Payment Controller
107
   * @security JWT
108
   * @param {CreateTerminalPaymentRequest} request.body.required - The terminal
109
   * payment that should be created.
110
   * @return {TerminalPaymentResponse} 200 - Terminal Payment
111
   * @return {string} 400 - Validation failure
112
   * @return {string} 500 - Internal server error
113
   */
1✔
114
  public async createTerminalPayment(req: RequestWithToken, res: Response): Promise<void> {
1✔
115
    this.logger.trace('Create new terminal payment by user', req.token.user);
7✔
116
    const request = req.body as CreateTerminalPaymentRequest;
7✔
117

118
    try {
7✔
119
      const { valid, context } = await new TerminalPaymentService().verifyTerminalPaymentRequest(request);
7✔
120

121
      if (!valid) {
7✔
122
        res.status(400).send('Could not validate terminalPayment.');
2✔
123
        return;
2✔
124
      }
2✔
125

126
      const allowedUserTypes = [UserType.LOCAL_USER, UserType.LOCAL_ADMIN, UserType.MEMBER, UserType.POINT_OF_SALE];
5✔
127
      const fromUser = context.users.get(request.transaction.from);
5✔
128
      if (!allowedUserTypes.includes(fromUser?.type)) {
7✔
129
        res.status(400).send(`Could not create terminalPayment for user "${fromUser.toString()}", because their account type is "${fromUser.type}"`);
3✔
130
        return;
3✔
131
      }
3✔
132

133
      let result: TerminalPaymentResponse;
2✔
134

135
      await AppDataSource.transaction(async (manager) => {
2✔
136
        const service = new TerminalPaymentService(manager);
2✔
137
        const terminalPayment = await service.createTerminalPayment(request, context);
2✔
138
        result = await TerminalPaymentService.asTerminalPaymentResponse(terminalPayment, context);
1✔
139
      });
1✔
140

141
      res.status(200).json(result);
1✔
142
    } catch (error) {
1✔
143
      this.logger.error('Could not create Terminal Payment:', error);
1✔
144
      res.status(500).send('Internal server error.');
1✔
145
    }
1✔
146
  }
7✔
147

148
  /**
1✔
149
   * GET /terminal-payments/{id}
150
   * @summary Get single terminal payment by ID
151
   * @operationId getSingleTerminalPayment
152
   * @tags terminalPayments - Operations of the Terminal Payment Controller
153
   * @security JWT
154
   * @param {integer} id.path.required - The ID of the terminal payment
155
   * @return {TerminalPaymentResponse} 200 - Terminal Payment
156
   * @return {string} 404 - Not found
157
   * @return {string} 500 - Internal server error
158
   */
1✔
159
  public async getSingleTerminalPayment(req: RequestWithToken, res: Response): Promise<void> {
1✔
160
    this.logger.trace('Get terminal payment with id', req.params.id, 'by user', req.token.user);
4✔
161
    const rawId = req.params.id;
4✔
162

163
    try {
4✔
164
      const id = Number.parseInt(rawId, 10);
4✔
165

166
      const service = new TerminalPaymentService();
4✔
167
      const terminalPayment = await service.getTerminalPayment(id);
4✔
168

169
      if (terminalPayment == null) {
4✔
170
        res.status(404).send(`Terminal Payment with ID "${id}" not found.`);
1✔
171
        return;
1✔
172
      }
1✔
173

174
      const result = await TerminalPaymentService.asTerminalPaymentResponse(terminalPayment);
3✔
175
      res.status(200).json(result);
2✔
176
    } catch (error) {
4✔
177
      this.logger.error('Could not get terminalPayment:', error);
1✔
178
      res.status(500).send('Internal server error.');
1✔
179
    }
1✔
180
  }
4✔
181

182
  /**
1✔
183
   * POST /terminal-payments/{id}/process
184
   * @summary Start the payment process on the terminal for the TerminalPayment
185
   * with the given ID
186
   * @operationId startTerminalPayment
187
   * @tags terminalPayments - Operations of the Terminal Payment Controller
188
   * @security JWT
189
   * @param {integer} id.path.required - The ID of the terminal payment
190
   * @param {ProcessTerminalPaymentRequest} request.body.required - Payment options
191
   * @return 204 - Success
192
   * @return {string} 400 - Validation failure
193
   * @return {string} 404 - Terminal Payment or terminal not found
194
   * @return {string} 422 - Terminal unavailable
195
   * @return {string} 422 - TerminalPayment already paid
196
   * @return {string} 500 - Internal server error
197
   */
1✔
198
  public async startTerminalPayment(req: RequestWithToken, res: Response): Promise<void> {
1✔
199
    this.logger.trace('Start terminal payment by user', req.token.user);
6✔
200
    const rawId = req.params.id;
6✔
201
    const request = req.body as ProcessTerminalPaymentRequest;
6✔
202

203
    try {
6✔
204
      const id = Number.parseInt(rawId, 10);
6✔
205

206
      const service = new TerminalPaymentService();
6✔
207
      const terminalPayment = await service.getTerminalPayment(id);
6✔
208

209
      if (!terminalPayment) {
6✔
210
        res.status(404).send(`Terminal Payment with ID "${id}" not found.`);
1✔
211
        return;
1✔
212
      }
1✔
213

214
      if (terminalPayment.transfer) {
6✔
215
        res.status(422).send('TerminalPayment already paid.');
1✔
216
        return;
1✔
217
      }
1✔
218

219
      const terminal = await new StripeService().getSingleTerminal(request.stripeTerminalId);
4✔
220
      if (!terminal) {
6✔
221
        res.status(404).send(`Stripe terminal with ID "${request.stripeTerminalId}" not found.`);
1✔
222
        return;
1✔
223
      }
1✔
224

225
      if (!terminal.available) {
6✔
226
        res.status(422).send('Terminal unavailable (is it in use?)');
1✔
227
        return;
1✔
228
      }
1✔
229

230
      await AppDataSource.transaction(async (manager) => {
2✔
231
        await new TerminalPaymentService(manager).startTerminalPayment(id, request);
2✔
232
      });
1✔
233
      res.status(204).send();
1✔
234
    } catch (error) {
1✔
235
      this.logger.error('Could not start terminalPayment:', error);
1✔
236
      res.status(500).send('Internal server error.');
1✔
237
    }
1✔
238
  }
6✔
239

240
  /**
1✔
241
  * DELETE /terminal-payments/{id}
242
  * @summary Cancel a Terminal Payment that is created/processing
243
  * @operationId cancelTerminalPayment
244
  * @tags terminalPayments - Operations of the Terminal Payment Controller
245
  * @security JWT
246
  * @param {integer} id.path.required - The ID of the terminal payment
247
  * @return {TerminalPaymentResponse} 200 - Terminal Payment
248
  * @return {string} 404 - TerminalPayment not found
249
  * @return {string} 422 - TerminalPayment not created/processing
250
  * @return {string} 500 - Internal server error
251
   */
1✔
252
  public async cancelTerminalPayment(req: RequestWithToken, res: Response): Promise<void> {
1✔
253
    this.logger.trace('Start terminal payment by user', req.token.user);
5✔
254
    const rawId = req.params.id;
5✔
255

256
    try {
5✔
257
      const id = Number.parseInt(rawId, 10);
5✔
258

259
      const service = new TerminalPaymentService();
5✔
260
      let terminalPayment = await service.getTerminalPayment(id);
5✔
261

262
      if (!terminalPayment) {
5✔
263
        res.status(404).send(`Terminal Payment with ID "${id}" not found.`);
1✔
264
        return;
1✔
265
      }
1✔
266

267
      if (terminalPayment.getState() !== TerminalPaymentState.CREATED && terminalPayment.getState() !== TerminalPaymentState.PROCESSING) {
5✔
268
        res.status(422).send(`Terminal Payment cannot be cancelled, because it has state "${terminalPayment.getState()}"`);
1✔
269
        return;
1✔
270
      }
1✔
271

272
      await AppDataSource.transaction(async (manager) => {
3✔
273
        terminalPayment = await new TerminalPaymentService(manager).cancelTerminalPayment(id);
3✔
274
      });
2✔
275

276
      const response = await TerminalPaymentService.asTerminalPaymentResponse(terminalPayment);
2✔
277
      res.status(200).json(response);
2✔
278
    } catch (error) {
5✔
279
      this.logger.error('Could not cancel terminalPayment:', error);
1✔
280
      res.status(500).send('Internal server error.');
1✔
281
    }
1✔
282
  }
5✔
283

284
  /**
1✔
285
  * GET /terminal-payments/terminals
286
  * @summary Get all Stripe terminals
287
  * @operationId getStripeTerminals
288
  * @tags terminalPayments - Operations of the Terminal Payment Controller
289
  * @security JWT
290
  * @return {Array.<StripePaymentTerminalResponse[]>} 200 - Stripe Terminals
291
  * @return {string} 500 - Internal server error
292
  */
1✔
293
  public async getStripeTerminals(req: RequestWithToken, res: Response): Promise<void> {
1✔
NEW
294
    this.logger.trace('Get all Stripe terminals by user', req.token.user);
×
295

NEW
296
    try {
×
NEW
297
      const service = new StripeService();
×
NEW
298
      const terminals = await service.getTerminals();
×
NEW
299
      const response = terminals.map((t) => StripeService.asStripePaymentTerminalResponse(t));
×
300

NEW
301
      res.status(200).json(response);
×
NEW
302
    } catch (error) {
×
NEW
303
      this.logger.error('Could not get all Stripe terminals:', error);
×
NEW
304
      res.status(500).send('Internal server error.');
×
NEW
305
    }
×
NEW
306
  }
×
307

308
  /**
1✔
309
   * Function to determine which credentials are needed to get terminalPayments:
310
   *   - all if user is not connected
311
   *   - own if user is connected
312
   * @param req - Request with terminalPayment ID as param
313
   * @return whether terminalPayment is connected to user token
314
   */
1✔
315
  private static async getRelation(req: RequestWithToken): Promise<string> {
1✔
316
    const id = asNumber(req.params.id);
11✔
317
    const userId = req.token.user.id;
11✔
318

319
    const t = await new TerminalPaymentService().getTerminalPayment(id);
11✔
320
    if (!t) return 'all';
11✔
321

322
    if (t.createdBy.id === userId
9✔
323
      || t.temporaryTransaction?.from.id === userId || t.temporaryTransaction?.createdBy.id === userId
11✔
324
      || t.finalTransaction?.from.id === userId || t.finalTransaction?.createdBy.id === userId) return 'own';
11!
325
    return 'all';
2✔
326
  }
2✔
327
}
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc