@defra-fish/connectors-lib
Version:
Shared connectors
333 lines (300 loc) • 10 kB
JavaScript
import fetch from 'node-fetch'
import querystring from 'querystring'
import db from 'debug'
const SALES_API_URL_DEFAULT = 'http://0.0.0.0:4000'
const SALES_API_TIMEOUT_MS_DEFAULT = 20000
const urlBase = process.env.SALES_API_URL || SALES_API_URL_DEFAULT
const debug = db('connectors:sales-api')
/**
* Make a request to the sales API
*
* @param url the URL of the endpoint to make a request to
* @param method the HTTP method (defaults to get)
* @param payload the payload to include with a put/post/patch request
* @returns {Promise<{statusText: *, ok: *, body: *, status: *}>}
*/
export const call = async (url, method = 'get', payload = null) => {
const requestTimestamp = new Date().toISOString()
const response = await fetch(url.href, {
method,
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
...(payload && { body: JSON.stringify(payload) }),
timeout: process.env.SALES_API_TIMEOUT_MS || SALES_API_TIMEOUT_MS_DEFAULT
})
const responseTimestamp = new Date().toISOString()
const responseData = {
ok: response.ok,
status: response.status,
statusText: response.statusText,
body: response.status !== 204 ? await parseResponseBody(response) : undefined
}
debug(
'Request sent (%s): %s %s with payload %o. Response received (%s): %o',
requestTimestamp,
method,
url.href,
payload,
responseTimestamp,
responseData
)
return responseData
}
/**
* Retrieve the response json, falling back to reading text on error
*
* @param response node-fetch response object
* @returns {Promise<{}>}
*/
const parseResponseBody = async response => {
const body = await response.text()
try {
return JSON.parse(body)
} catch (e) {
return {
text: body
}
}
}
const exec2xxOrThrow = async requestPromise => {
const response = await requestPromise
if (response.ok) {
return response.body
}
throw Object.assign(new Error(`Unexpected response from the Sales API: ${JSON.stringify(response, null, 2)}`), { ...response })
}
const exec2xxOrNull = async requestPromise => {
const response = await requestPromise
return (response.ok && response.body) || null
}
/**
* Create a new transaction in the sales API
*
* @param transaction the payload for the request
* @returns {Promise<*>}
* @throws on a non-2xx response
*/
export const createTransaction = async transaction => exec2xxOrThrow(call(new URL('/transactions', urlBase), 'post', transaction))
/**
* Create new transactions in the sales API in batch mode
*
* @param transactionArr the array containing multiple transaction payloads
* @returns {Promise<*>}
* @throws on a non-2xx response
*/
export const createTransactions = async transactionArr =>
exec2xxOrThrow(call(new URL('/transactions/$batch', urlBase), 'post', transactionArr))
/**
* Finalise a transaction in the sales API
*
* @param id the transaction id to finalise
* @param payload the finalise-transaction payload to supply on the request
* @returns {Promise<*>}
* @throws on a non-2xx response
*/
export const finaliseTransaction = async (id, payload) => exec2xxOrThrow(call(new URL(`/transactions/${id}`, urlBase), 'patch', payload))
/**
* Retrieve the details of a transaction file. Returns null if not found.
*
* @param filename the name of the transaction file record to retrieve
* @returns {Promise<*|null>}
*/
export const getTransactionFile = async filename => exec2xxOrNull(call(new URL(`/transaction-files/${filename}`, urlBase), 'get'))
/**
* Create/update a transaction file
*
* @param filename the name of the transaction file record to upsert
* @param data the payload with which to update the transaction file record
* @returns {Promise<*>}
* @throws on a non-2xx response
*/
export const upsertTransactionFile = async (filename, data) =>
exec2xxOrThrow(call(new URL(`/transaction-files/${filename}`, urlBase), 'put', data))
/**
* Create a payment journal
*
* @param data the payload with which to create the payment journal
* @returns {Promise<*>}
* @throws on a non-2xx response
*/
export const createPaymentJournal = async (id, data) => exec2xxOrThrow(call(new URL(`/paymentJournals/${id}`, urlBase), 'put', data))
/**
* Retrieve an existing payment journal
*
* @param {string} id the identifier of the payment journal to retrieve
* @returns {Promise<*>}
*/
export const getPaymentJournal = async id => exec2xxOrNull(call(new URL(`/paymentJournals/${id}`, urlBase), 'get'))
/**
* Update an existing payment journal
*
* @param {string} id the identifier of the payment journal to update
* @param data the payload with which to update the payment journal
* @returns {Promise<*>}
* @throws on a non-2xx response
*/
export const updatePaymentJournal = async (id, data) => exec2xxOrThrow(call(new URL(`/paymentJournals/${id}`, urlBase), 'patch', data))
/**
* Create a new staging exception
*
* @param data the payload with which to create the staging exception
* @returns {Promise<*>}
* @throws on a non-2xx response
*/
export const createStagingException = async data => exec2xxOrThrow(call(new URL('/stagingExceptions', urlBase), 'post', data))
/**
* Retrieve all POCL validation errors which have a "Ready for processing" status
*
* @returns {Promise<*>}
* @throws on a non-2xx response
*/
export const getPoclValidationErrorsForProcessing = async () => exec2xxOrThrow(call(new URL('/poclValidationErrors', urlBase), 'get'))
/**
* Update POCL validation error with the given id
*
* @param {string} id the identifier of the POCL validation error to update
* @param data the payload with which to update the POCL validation error
* @returns {Promise<*>}
* @throws on a non-2xx response
*/
export const updatePoclValidationError = async (id, data) =>
exec2xxOrThrow(call(new URL(`/poclValidationErrors/${id}`, urlBase), 'patch', data))
/**
* Retrieve details of a system user
*
* @param oid the Azure object ID pertaining to the system user
* @returns {Promise<*>}
*/
export const getSystemUser = async oid => exec2xxOrNull(call(new URL(`/systemUsers/${oid}`, urlBase), 'get'))
/**
* Supports querying of reference data from the Sales API
*/
class QueryBuilder {
constructor (baseUrl) {
this._baseUrl = baseUrl
}
/**
* Retrieve all entries for the given criteria
*
* @param {Object} [criteria] an object whose fields are used to filter the results
* @returns {Promise<*>}
*/
async getAll (criteria) {
return exec2xxOrThrow(call(new URL(`?${querystring.stringify(criteria)}`, this._baseUrl)))
}
/**
* Find the first matching entity for the given criteria
*
* @param [criteria]
* @returns {Promise<void>}
*/
async find (criteria) {
const result = await this.getAll(criteria)
return (result.length && result[0]) || undefined
}
}
/**
* Query support for permits
* @type {QueryBuilder}
*/
export const permits = new QueryBuilder(new URL('permits', urlBase))
/**
* Query support for concessions
* @type {QueryBuilder}
*/
export const concessions = new QueryBuilder(new URL('concessions', urlBase))
/**
* Query support for permit-concession mappings
* @type {QueryBuilder}
*/
export const permitConcessions = new QueryBuilder(new URL('permitConcessions', urlBase))
/**
* Query support for transaction currencies
* @type {QueryBuilder}
*/
export const transactionCurrencies = new QueryBuilder(new URL('transactionCurrencies', urlBase))
/**
* Query support for payment journals
* @type {QueryBuilder}
*/
export const paymentJournals = new QueryBuilder(new URL('paymentJournals', urlBase))
/**
* Query support for country codes
* @type {QueryBuilder}
*/
export const countries = new QueryBuilder(new URL('/option-sets/defra_country', urlBase))
/**
* Support for easy-renewal authentication
* @param referenceNumber
* @param birthDate
* @param postcode
* @returns {Promise<*>}
*/
export const authenticate = async (referenceNumber, birthDate, postcode) =>
exec2xxOrNull(
call(
new URL(
`/authenticate/renewal/${referenceNumber}?${querystring.stringify({
licenseeBirthDate: birthDate,
licenseePostcode: postcode
})}`,
urlBase
),
'get'
)
)
/**
* Helper to check if an HTTP status code is classed as a system error
*
* @param {number} statusCode the HTTP status code to test
* @returns {boolean} true if the status code represents a system error, false otherwise
*/
export const isSystemError = statusCode => Math.floor(statusCode / 100) === 5
/**
* Grab due recurring payments
*
* @param date the current date
* @returns {Promise<*>}
* @throws on a non-2xx response
*/
export const getDueRecurringPayments = async date => exec2xxOrThrow(call(new URL(`/dueRecurringPayments/${date}`, urlBase), 'get'))
/**
* Prepare permission data for renewal
*
* @param referenceNumber
* @returns {Promise<*>}
* @throws on a non-2xx response
*/
export const preparePermissionDataForRenewal = async referenceNumber =>
exec2xxOrThrow(call(new URL(`/permissionRenewalData/${referenceNumber}`, urlBase), 'get'))
/**
* Process a recurring payment result
*
* @param transactionId
* @param paymentId
* @param createdDate
* @returns {Promise<*>}
* @throws on a non-2xx response
*/
export const processRPResult = async (transactionId, paymentId, createdDate) => {
return exec2xxOrThrow(call(new URL(`/processRPResult/${transactionId}/${paymentId}/${createdDate}`, urlBase), 'get'))
}
/**
* Cancel a RecurringPayment
*
* @param id
* @returns {Promise<*>}
* @throws on a non-2xx response
*/
export const cancelRecurringPayment = async id => {
return exec2xxOrThrow(call(new URL(`/cancelRecurringPayment/${id}`, urlBase), 'get'))
}
/**
* Retrieve a staged transaction
*
* @param id
* @returns {Promise<*>}
* @throws on a non-2xx response
*/
export const retrieveStagedTransaction = async id => {
return exec2xxOrThrow(call(new URL(`/retrieveStagedTransaction/${id}`, urlBase), 'get'))
}