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

GEWIS / sudosos-backend / 26295640216

22 May 2026 03:06PM UTC coverage: 91.958% (+0.001%) from 91.957%
26295640216

Pull #897

github

web-flow
Merge 55574dfe6 into 2e3f987c7
Pull Request #897: Add PaymentRequest HTTP API

4182 of 4769 branches covered (87.69%)

Branch coverage included in aggregate %.

298 of 379 new or added lines in 6 files covered. (78.63%)

2 existing lines in 1 file now uncovered.

21225 of 22860 relevant lines covered (92.85%)

860.02 hits per line

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

79.35
/src/controller/payment-request-public-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 payment-request-public-controller.
23
 *
24
 * Unauthenticated share-link surface for {@link stripe/payment-request!PaymentRequest | PaymentRequest}.
25
 * Mirrors the pattern of {@link stripe!StripeWebhookController | StripeWebhookController}:
26
 * mounted *before* `setupAuthentication` in `src/index.ts`, with an
27
 * `async () => true` policy on every endpoint. Do **not** reuse endpoint
28
 * paths from the authenticated controller — the mount order is significant.
29
 *
30
 * The response shape here is {@link PublicPaymentRequestResponse}
31
 * — it deliberately omits `createdBy`, `cancelledBy`, and other internal
32
 * audit fields, because anyone holding the link can hit these endpoints.
33
 *
34
 * @module stripe/payment-request
35
 */
1✔
36

37
import { Response } from 'express';
38
import log4js, { Logger } from 'log4js';
39
import BaseController, { BaseControllerOptions } from './base-controller';
40
import Policy from './policy';
41
import { RequestWithToken } from '../middleware/token-middleware';
42
import PaymentRequestService, {
43
  IllegalPaymentRequestTransitionError,
44
  InvalidPaymentRequestBeneficiaryError,
45
} from '../service/payment-request-service';
46
import PaymentRequestCheckoutService from '../service/payment-request-checkout-service';
47
import { PaymentRequestStartResponse } from './response/payment-request-response';
48

49
export default class PaymentRequestPublicController extends BaseController {
1✔
50
  private logger: Logger = log4js.getLogger('PaymentRequestPublicController');
1✔
51

52
  public constructor(options: BaseControllerOptions) {
1✔
53
    super(options);
1✔
54
    this.configureLogger(this.logger);
1✔
55
  }
1✔
56

57
  /**
1✔
58
   * @inheritDoc
59
   *
60
   * All endpoints here bypass authentication (policy is `async () => true`).
61
   * The controller is mounted before `setupAuthentication` in `src/index.ts`.
62
   */
1✔
63
  public getPolicy(): Policy {
1✔
64
    return {
1✔
65
      '/:id': {
1✔
66
        GET: {
1✔
67
          policy: async () => true,
1✔
68
          handler: this.returnSinglePaymentRequest.bind(this),
1✔
69
        },
1✔
70
      },
1✔
71
      '/:id/start': {
1✔
72
        POST: {
1✔
73
          policy: async () => true,
1✔
74
          handler: this.startPaymentPublic.bind(this),
1✔
75
        },
1✔
76
      },
1✔
77
    };
1✔
78
  }
1✔
79

80
  /**
1✔
81
   * GET /payment-requests-public/{id}
82
   * @summary Fetch a PaymentRequest via the public share link. Returns a
83
   *   trimmed response that omits internal audit fields.
84
   * @operationId getPublicPaymentRequest
85
   * @tags paymentRequestsPublic - Unauthenticated share-link surface
86
   * @param {string} id.path.required - UUID v4 of the payment request.
87
   * @return {PublicPaymentRequestResponse} 200 - Single payment request (trimmed shape)
88
   * @return {string} 404 - Unknown id
89
   * @return {string} 500 - Internal server error
90
   */
1✔
91
  public async returnSinglePaymentRequest(req: RequestWithToken, res: Response): Promise<void> {
1✔
92
    this.logger.trace('Public get payment request', req.params.id);
4✔
93

94
    try {
4✔
95
      const service = new PaymentRequestService();
4✔
96
      const request = await service.getPublicPaymentRequest(req.params.id);
4✔
97
      if (!request) {
4✔
98
        res.status(404).send();
1✔
99
        return;
1✔
100
      }
1✔
101
      res.status(200).json(PaymentRequestService.asPublicPaymentRequestResponse(request));
3✔
102
    } catch (e) {
4!
NEW
103
      this.logger.error('Could not get payment request (public):', e);
×
NEW
104
      res.status(500).send('Internal server error.');
×
NEW
105
    }
×
106
  }
4✔
107

108
  /**
1✔
109
   * POST /payment-requests-public/{id}/start
110
   * @summary Start a Stripe payment session for the given PaymentRequest
111
   *   without authentication — the share link IS the credential.
112
   * @operationId startPaymentRequestPublic
113
   * @tags paymentRequestsPublic - Unauthenticated share-link surface
114
   * @param {string} id.path.required - UUID v4 of the payment request.
115
   * @return {PaymentRequestStartResponse} 200 - Stripe client secret
116
   * @return {string} 400 - Invalid beneficiary
117
   * @return {string} 404 - Unknown id
118
   * @return {string} 409 - Request is not in PENDING state
119
   * @return {string} 500 - Internal server error
120
   */
1✔
121
  public async startPaymentPublic(req: RequestWithToken, res: Response): Promise<void> {
1✔
122
    this.logger.trace('Public start payment', req.params.id);
4✔
123

124
    try {
4✔
125
      const service = new PaymentRequestService();
4✔
126
      const request = await service.getPublicPaymentRequest(req.params.id);
4✔
127
      if (!request) {
4✔
128
        res.status(404).send();
1✔
129
        return;
1✔
130
      }
1✔
131
      const checkout = new PaymentRequestCheckoutService();
3✔
132
      const { deposit, clientSecret } = await checkout.startPayment(request);
3!
NEW
133
      const response: PaymentRequestStartResponse = {
×
NEW
134
        paymentRequestId: request.id,
×
NEW
135
        stripeId: deposit.stripePaymentIntent.stripeId,
×
NEW
136
        clientSecret,
×
NEW
137
      };
×
NEW
138
      res.status(200).json(response);
×
139
    } catch (e) {
4✔
140
      if (e instanceof IllegalPaymentRequestTransitionError) {
3✔
141
        res.status(409).send(e.message);
3✔
142
        return;
3✔
143
      }
3!
NEW
144
      if (e instanceof InvalidPaymentRequestBeneficiaryError) {
×
NEW
145
        res.status(400).send(e.message);
×
NEW
146
        return;
×
NEW
147
      }
×
NEW
148
      this.logger.error('Could not start public payment:', e);
×
NEW
149
      res.status(500).send('Internal server error.');
×
NEW
150
    }
×
151
  }
4✔
152
}
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