@defra-fish/gafl-webapp-service
Version:
The websales frontend for the GAFL service
300 lines (266 loc) • 11.6 kB
JavaScript
/**
* This handler is called after the user has agreed the licence purchase.
* It locks the transaction and posts it the the API. This will create a licence number and end date
* If the licence has a cost the user is then redirected into the payment pages, otherwise the
* they are redirected immediately to the finalization handler
*
* (1) Agree -> post -> finalise -> complete
* (2) Agree -> post -> payment -> finalise -> complete
* (3) Payment: Required -> dispatched -> [completed|cancelled\failed\apiError]
*
*/
import Boom from '@hapi/boom'
import db from 'debug'
import { salesApi } from '@defra-fish/connectors-lib'
import { prepareApiTransactionPayload, prepareApiFinalisationPayload } from '../processors/api-transaction.js'
import { sendPayment, getPaymentStatus, sendRecurringPayment } from '../services/payment/govuk-pay-service.js'
import { preparePayment, prepareRecurringPaymentAgreement } from '../processors/payment.js'
import { COMPLETION_STATUS, RECURRING_PAYMENT } from '../constants.js'
import { ORDER_COMPLETE, PAYMENT_CANCELLED, PAYMENT_FAILED } from '../uri.js'
import { PAYMENT_JOURNAL_STATUS_CODES, GOVUK_PAY_ERROR_STATUS_CODES } from '@defra-fish/business-rules-lib'
import { v4 as uuidv4 } from 'uuid'
const debug = db('webapp:agreed-handler')
/**
* Send (post) transaction to sales API
* @param request
* @param transaction
* @param status
* @returns {Promise<*>}
*/
const sendToSalesApi = async (request, transaction, status) => {
const apiTransactionPayload = await prepareApiTransactionPayload(request, transaction.id, transaction.agreementId)
let response
try {
response = await salesApi.createTransaction(apiTransactionPayload)
} catch (e) {
debug('Error creating transaction', JSON.stringify(apiTransactionPayload))
throw e
}
transaction.cost = response.cost
status[COMPLETION_STATUS.posted] = true
debug('Transaction identifier: %s', transaction.id)
await request.cache().helpers.transaction.set(transaction)
await request.cache().helpers.status.set(status)
return transaction
}
/**
* Grab the agreement id in GOV.UK pay using the API
* (1) Prepare payment (payload) for the API (processor)
* (2) Send to GOV.UK pay API (connector)
* (3) Handle exceptions and error (agreed handler)
* (4) Write into the journal tables (agreed handler)
* (5) Process the results - write into the cache
* @param request
* @param transaction
* @param status
* @returns {Promise<void>}
*/
const createRecurringPayment = async (request, transaction, status) => {
/*
* Prepare the payment payload
*/
const preparedPayment = await prepareRecurringPaymentAgreement(request, transaction)
/*
* Send the prepared payment to the GOV.UK pay API using the connector
*/
const paymentResponse = await sendRecurringPayment(preparedPayment)
debug(`Created agreement with id ${paymentResponse.agreement_id}`)
status[COMPLETION_STATUS.recurringAgreement] = true
transaction.agreementId = paymentResponse.agreement_id
await request.cache().helpers.status.set(status)
await request.cache().helpers.transaction.set(transaction)
}
/**
* Create a new payment in GOV.UK pay using the API
* (1) Prepare payment (payload) for the API (processor)
* (2) Send to GOV.UK pay API (connector)
* (3) Handle exceptions and error (agreed handler)
* (4) Write into the journal tables (agreed handler)
* (5) Process the results - write into the cache
* @param request
* @param transaction
* @param status
* @returns {Promise<void>}
*/
const createPayment = async (request, transaction, status) => {
const recurring = status && status[COMPLETION_STATUS.recurringAgreement] === true
/*
* Prepare the payment payload
*/
const preparedPayment = preparePayment(request, transaction)
/*
* Send the prepared payment to the GOV.UK pay API using the connector
*/
const paymentResponse = await sendPayment(preparedPayment, recurring)
/*
* Used by the payment mop up job, create the payment journal entry which is removed when the user completes the journey
* it maybe updated multiple times with a new payment id and creation date if the payment is cancelled and retried
*/
if (await salesApi.getPaymentJournal(transaction.id)) {
await salesApi.updatePaymentJournal(transaction.id, {
paymentReference: paymentResponse.payment_id,
paymentTimestamp: paymentResponse.created_date,
paymentStatus: PAYMENT_JOURNAL_STATUS_CODES.InProgress
})
} else {
await salesApi.createPaymentJournal(transaction.id, {
paymentReference: paymentResponse.payment_id,
paymentTimestamp: paymentResponse.created_date,
paymentStatus: PAYMENT_JOURNAL_STATUS_CODES.InProgress
})
}
/*
* Set up the payment details in the transaction and status cache
*/
transaction.payment = {
state: paymentResponse.state,
payment_id: paymentResponse.payment_id,
payment_provider: paymentResponse.payment_provider,
created_date: paymentResponse.created_date,
href: paymentResponse._links.next_url.href,
self_href: paymentResponse._links.self.href
}
status[COMPLETION_STATUS.paymentCreated] = true
await request.cache().helpers.status.set(status)
await request.cache().helpers.transaction.set(transaction)
}
/**
* Called when the user has returned to the service from the payment pages. It queries the
* GOV.UK pay API for the payment status. It updates the journal and determines the
* next page to display which is either order-complete, or the cancelled or failure pages
* @param request
* @param transaction
* @param status
* @returns {Promise<void>}
*/
const processPayment = async (request, transaction, status) => {
const recurring = status && status[COMPLETION_STATUS.recurringAgreement] === true
/*
* Get the payment status
*/
const { state } = await getPaymentStatus(transaction.payment.payment_id, recurring)
if (!state.finished) {
throw Boom.forbidden('Attempt to access the agreed handler during payment journey')
}
let next = null
if (state.status === 'error') {
// The payment failed
await salesApi.updatePaymentJournal(transaction.id, { paymentStatus: PAYMENT_JOURNAL_STATUS_CODES.Failed })
status[COMPLETION_STATUS.paymentFailed] = true
status.payment = { code: state.code }
await request.cache().helpers.status.set(status)
next = PAYMENT_FAILED.uri
}
if (state.status === 'success') {
// Defer setting the completed status in the journal until after finalization
status[COMPLETION_STATUS.paymentCompleted] = true
await request.cache().helpers.status.set(status)
} else {
/*
* This block deals with failed or cancelled payments
*/
// The payment expired
if (state.code === GOVUK_PAY_ERROR_STATUS_CODES.EXPIRED) {
await salesApi.updatePaymentJournal(transaction.id, { paymentStatus: PAYMENT_JOURNAL_STATUS_CODES.Failed })
status[COMPLETION_STATUS.paymentFailed] = true
status.payment = { code: state.code }
await request.cache().helpers.status.set(status)
next = PAYMENT_FAILED.uri
}
// The user cancelled the payment
if (state.code === GOVUK_PAY_ERROR_STATUS_CODES.USER_CANCELLED) {
await salesApi.updatePaymentJournal(transaction.id, { paymentStatus: PAYMENT_JOURNAL_STATUS_CODES.Cancelled })
status[COMPLETION_STATUS.paymentCancelled] = true
status.pay = { code: state.code }
await request.cache().helpers.status.set(status)
next = PAYMENT_CANCELLED.uri
}
// The payment was rejected
if (state.code === GOVUK_PAY_ERROR_STATUS_CODES.REJECTED) {
await salesApi.updatePaymentJournal(transaction.id, { paymentStatus: PAYMENT_JOURNAL_STATUS_CODES.Failed })
status[COMPLETION_STATUS.paymentFailed] = true
status.payment = { code: state.code }
await request.cache().helpers.status.set(status)
next = PAYMENT_FAILED.uri
}
}
return next
}
const finaliseTransaction = async (request, transaction, status) => {
const apiFinalisationPayload = await prepareApiFinalisationPayload(request)
debug('Patch transaction finalisation : %s', JSON.stringify(apiFinalisationPayload, null, 4))
const response = await salesApi.finaliseTransaction(transaction.id, apiFinalisationPayload)
/*
* Write the licence number and end dates into the cache
*/
for (let i = 0; i < response.permissions.length; i++) {
debug(`Setting permission reference number: ${response.permissions[i].referenceNumber}`)
transaction.permissions[i].referenceNumber = response.permissions[i].referenceNumber
debug(`Setting permission issue date: ${response.permissions[i].issueDate}`)
transaction.permissions[i].issueDate = response.permissions[i].issueDate
debug(`Setting permission start date: ${response.permissions[i].startDate}`)
transaction.permissions[i].startDate = response.permissions[i].startDate
debug(`Setting permission end date: ${response.permissions[i].endDate}`)
transaction.permissions[i].endDate = response.permissions[i].endDate
debug(`Setting obfuscated dob: ${response.permissions[i].licensee.obfuscatedDob}`)
transaction.permissions[i].licensee.obfuscatedDob = response.permissions[i].licensee.obfuscatedDob
}
status[COMPLETION_STATUS.finalised] = true
await request.cache().helpers.status.set(status)
await request.cache().helpers.transaction.set(transaction)
// Set the completed status
if (transaction.cost > 0) {
await salesApi.updatePaymentJournal(transaction.id, { paymentStatus: PAYMENT_JOURNAL_STATUS_CODES.Completed })
}
}
/**
* Agreed route handler
* @param request
* @param h
* @returns {Promise}
*/
export default async (request, h) => {
const status = await request.cache().helpers.status.get()
const transaction = await request.cache().helpers.transaction.get()
if (!transaction.id) {
transaction.id = uuidv4()
}
// If the agreed flag is not set to true then throw an exception
if (!status[COMPLETION_STATUS.agreed]) {
throw Boom.forbidden(`Attempt to access the agreed handler with no agreed flag set: ${JSON.stringify(transaction)}`)
}
// Send the transaction to the sales API and process the response
if (!status[COMPLETION_STATUS.posted]) {
// Create the agreement if a recurring payment
if (status[RECURRING_PAYMENT] === true) {
await createRecurringPayment(request, transaction, status)
}
try {
await sendToSalesApi(request, transaction, status)
} catch (e) {
debug('Error sending transaction to Sales Api', JSON.stringify(transaction), JSON.stringify(status))
throw e
}
}
// The payment section is ignored for zero cost transactions
if (transaction.cost > 0) {
// Send the transaction to the GOV.UK payment API and process the response
if (!status[COMPLETION_STATUS.paymentCreated]) {
await createPayment(request, transaction, status)
return h.redirect(transaction.payment.href)
}
// Note: At this point payment completed status is never set
const next = await processPayment(request, transaction, status)
if (next) {
return h.redirectWithLanguageCode(next)
}
}
// If the transaction has already been finalised then redirect to the order completed page
if (!status[COMPLETION_STATUS.finalised]) {
await finaliseTransaction(request, transaction, status)
} else {
debug('Transaction %s already finalised, redirect to order complete: %s', transaction.id)
}
// If we are here we have completed
return h.redirectWithLanguageCode(ORDER_COMPLETE.uri)
}