Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: add env parameter to Stripe metadata, check value before processing events #6234

Merged
merged 7 commits into from
May 4, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions docs/DEPLOYMENT_SETUP.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,11 +110,11 @@ The list of categories can be inferred by looking at the file `.ebextensions/env

#### AWS Systems Manager

| Variable | Description |
| :----------- | --------------------------------------------------------------------------------------------------------------------- |
| `SSM_PREFIX` | String prefix (typically the environment name) for AWS SSM parameter names to create a .env file for FormSG. (`staging`, `prod`)|
| `SECRET_ENV` | String (typically the environment name) to be used in building of AWS Secrets Manager keys in different environments.|
| `SSM_ENV_SITE_NAME` | String (the specific environment site name) to be used in building of AWS Secrets Manager keys in different environments. Optional. (`staging-alt`, `staging-alt2`)|
| Variable | Description |
| :------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `SSM_PREFIX` | String prefix (typically the environment type) for AWS SSM parameter names to create a .env file for FormSG. (`staging`, `prod`, `uat`) |
| `SECRET_ENV` | String (typically the environment type) to be used in building of AWS Secrets Manager keys in different environments. |
| `SSM_ENV_SITE_NAME` | String (the specific environment site name) to be used in building of AWS Secrets Manager keys in different environments. (`staging`, `staging-alt`, `staging-alt2`, `prod`, `uat`) |

#### App Config

Expand Down
1 change: 1 addition & 0 deletions src/app/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ const config: Config = {
reactMigration: basicVars.reactMigration,
configureAws,
secretEnv: basicVars.core.secretEnv,
envSiteName: basicVars.core.envSiteName,
}

export = config
6 changes: 6 additions & 0 deletions src/app/config/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,12 @@ export const compulsoryVarsSchema: Schema<ICompulsoryVarsSchema> = {
default: null,
env: 'SECRET_ENV',
},
envSiteName: {
doc: 'Environment site name used to build key for AWS Secrets Manager',
format: String,
default: null,
env: 'SSM_ENV_SITE_NAME',
},
},
reactMigration: {
adminSwitchEnvFeedbackFormId: {
Expand Down
69 changes: 24 additions & 45 deletions src/app/modules/payments/payments.controller.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { celebrate, Joi, Segments } from 'celebrate'
import { StatusCodes } from 'http-status-codes'

import { createLoggerWithLabel } from '../../config/logger'
Expand All @@ -9,30 +8,39 @@ import { findLatestSuccessfulPaymentByEmailAndFormId } from './payments.service'

const logger = createLoggerWithLabel(module)

// exported for testing
export const _handleGetPreviousPaymentId: ControllerHandler<
{
formId: string
},
/**
* Handler for GET /api/v3/:formId/payments/previous/:email
* Finds and return the latest successful payment made by the specific
* respondent based on their email. Email will be acquired from the request body
* @params formId id of related form to retrieve payment
*
* @returns 200 with payment document if successful payment is found
* @returns 404 without data if no payment has been made
* @returns 500 if there is an unexpected error
*/
export const handleGetPreviousPaymentId: ControllerHandler<
{ formId: string },
string,
{ email: string }
> = (req, res) => {
const { formId } = req.params
const { email } = req.body
// Step 1 get Payment document from email and formId

const logMeta = {
action: 'handleGetPreviousPayment',
email,
formId,
}

// Step 1: Get Payment document from email and formId
return (
findLatestSuccessfulPaymentByEmailAndFormId(email, formId)
// If payment found, return payment id
.map((payment) => {
logger.info({
message:
'Found latest successful payment document from email and formId',
meta: {
action: 'handleGetPreviousPayment',
email,
formId,
payment,
},
meta: { ...logMeta, paymentId: payment._id },
})
return res.status(StatusCodes.OK).send(payment._id)
})
Expand All @@ -44,46 +52,17 @@ export const _handleGetPreviousPaymentId: ControllerHandler<
logger.info({
message:
'Did not find previous successful payment from email and formId',
meta: {
action: 'handleGetPreviousPayment',
email,
formId,
error,
},
meta: logMeta,
})
return res.sendStatus(StatusCodes.NOT_FOUND)
}
// Database error
logger.error({
message: 'Error retrieving payment documents using email and formId',
meta: {
action: 'handleGetPreviousPayment',
email,
formId,
error,
},
meta: logMeta,
error,
})
return res.sendStatus(StatusCodes.INTERNAL_SERVER_ERROR)
})
)
}

/**
* Handler for GET /api/v3/:formId/payments/previous/:email
* Finds and return the latest successful payment made by the
* specific respondent based on their email.
* Email will be acquired from the request body
*
* @params formId formId of related form to retrieve payment
*
* @returns 200 with payment document if successful payment is found
* @returns 404 without data if no payment has been made
* @returns 500 if there is an unexpected error
*/ export const handleGetPreviousPaymentId = [
celebrate({
[Segments.QUERY]: {
formId: Joi.string().required,
},
}),
_handleGetPreviousPaymentId,
] as ControllerHandler[]
25 changes: 17 additions & 8 deletions src/app/modules/payments/stripe.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,16 @@ import { checkFormIsEncryptMode } from '../submission/encrypt-submission/encrypt

import { PaymentAccountInformationError } from './payments.errors'
import * as PaymentService from './payments.service'
import { StripeFetchError } from './stripe.errors'
import {
StripeFetchError,
StripeMetadataIncorrectEnvError,
} from './stripe.errors'
import * as StripeService from './stripe.service'
import {
convertToInvoiceFormat,
getChargeIdFromNestedCharge,
getMetadataPaymentId,
mapRouteErr,
mapRouteError,
} from './stripe.utils'

const logger = createLoggerWithLabel(module)
Expand All @@ -52,13 +55,15 @@ const validateStripeEvent = celebrate({
* Receives Stripe webhooks and updates the database with transaction details.
*
* @returns 200 if webhook is successfully processed
* @returns 400 if the Stripe-Signature header is missing or invalid
* @returns 202 if webhooks is not meant for this environment and will be processed by another environment
* @returns 400 if the Stripe-Signature header is missing or invalid, or the event is malformed
* @returns 404 if the payment or submission linked to the event cannot be found
* @returns 422 if any errors occurs in processing the webhook or saving payment to DB
* @returns 500 if any unexpected errors occur
*/
const _handleStripeEventUpdates: ControllerHandler<
unknown,
never,
void | ErrorDto,
string
> = async (req, res) => {
// Step 1: Verify the payload and ensure that it is indeed sent from Stripe.
Expand Down Expand Up @@ -234,14 +239,19 @@ const _handleStripeEventUpdates: ControllerHandler<
result.match(
() => res.sendStatus(StatusCodes.OK),
(error) => {
if (error instanceof StripeMetadataIncorrectEnvError) {
// Intercept this error and return 202 Accepted instead, indicating
// the request will be processed by another environment server.
return res.sendStatus(StatusCodes.ACCEPTED)
}
// Additional logging with error details
logger.error({
message: 'Error thrown in webhook handler',
meta: logMeta,
error,
})
// TODO: Add map route error here
return res.sendStatus(StatusCodes.UNPROCESSABLE_ENTITY)
const { errorMessage, statusCode } = mapRouteError(error)
return res.status(statusCode).json({ message: errorMessage })
},
)
}
Expand Down Expand Up @@ -609,8 +619,7 @@ export const getPaymentInfo: ControllerHandler<
})
})
.mapErr((error) => {
const { errorMessage, statusCode } = mapRouteErr(error)

const { errorMessage, statusCode } = mapRouteError(error)
return res.status(statusCode).json({ message: errorMessage })
})
}
12 changes: 12 additions & 0 deletions src/app/modules/payments/stripe.errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,24 @@ export class MalformedStripeChargeObjectError extends ApplicationError {
}
}

export class StripeMetadataInvalidError extends ApplicationError {
constructor(message = 'Invalid shape found for Stripe metadata') {
super(message)
}
}

export class StripeMetadataValidPaymentIdNotFoundError extends ApplicationError {
constructor(message = 'Valid payment id not found in Stripe metadata') {
super(message)
}
}

export class StripeMetadataIncorrectEnvError extends ApplicationError {
constructor(message = 'Stripe webhook sent to incorrect application') {
super(message)
}
}

export class StripeFetchError extends ApplicationError {
constructor(message = 'Error while requesting Stripe data') {
super(message)
Expand Down
2 changes: 0 additions & 2 deletions src/app/modules/payments/stripe.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,6 @@ const confirmStripePaymentPendingSubmission = (
* @param {mongoose.ClientSession} session the mongoose session to use for all db operations
*
* @returns ok() if event was successfully processed
* @returns err(EventMetadataPaymentIdInvalidError) if the payment id is not found in the event metadata or is found but an invalid BSON object id
* @returns err(MalformedStripeChargeObjectError) if the shape of the charge object returned by Stripe does not have expected fields
* @returns err(PaymentNotFoundError) if the payment document does not exist
* @returns err(PendingSubmissionNotFoundError) if the pending submission being referenced by the payment document does not exist
Expand Down Expand Up @@ -255,7 +254,6 @@ export const processStripeEventWithinSession = (
* @param {Stripe.Event} event the new Stripe Event causing the update operation to occur
*
* @returns ok() if event was successfully processed
* @returns err(EventMetadataPaymentIdInvalidError) if the payment id is not found in the event metadata or is found but an invalid BSON object id
* @returns err(MalformedStripeChargeObjectError) if the shape of the charge object returned by Stripe does not have expected fields
* @returns err(PaymentNotFoundError) if the payment document does not exist
* @returns err(PendingSubmissionNotFoundError) if the pending submission being referenced by the payment document does not exist
Expand Down
Loading