Skip to content

Commit

Permalink
feat(handler): rename duplicate-request handler and allow customized …
Browse files Browse the repository at this point in the history
…predicate
  • Loading branch information
enylin committed Dec 14, 2021
1 parent 45b5d37 commit 28c552c
Show file tree
Hide file tree
Showing 10 changed files with 162 additions and 69 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { RefundRequestConfig, RefundResponseBody } from '../line-pay-api/refund'
import { createPaymentApi } from '../payment-api/create'
import { isLinePayApiError } from '../line-pay-api/error/line-pay-api'
import { ApiHandler, ApiResponse } from '../payment-api/type'
import { isTimeoutError } from '../line-pay-api/error/timeout'

/**
* Response converter for confirm API. Convert the response body from payment details API to confirm API.
Expand Down Expand Up @@ -58,53 +59,65 @@ export function toRefundResponse<
}
}

// 1172: There is a record of transaction with the same order number.
// 1198: API call request has been duplicated.
const defaultPredicate = (error: unknown) =>
isTimeoutError(error) ||
(isLinePayApiError(error) &&
(error.data.returnCode === '1172' || error.data.returnCode === '1198'))

export type PaymentDetailsConverter<
Req extends ConfirmRequestConfig | RefundRequestConfig,
Res extends ConfirmResponseBody | RefundResponseBody
> = (req: Req, paymentDetailsResponseBody: PaymentDetailsResponseBody) => Res

/**
* Create a handler for confirm and refund API. The handler will handle the 1172 and 1198 error by calling the payment details API and verify the transaction result.
* Create a handler for confirm and refund API. The handler will handle the 1172 and 1198 error and timeout error by calling the payment details API and verify the transaction result.
*
* @param converter convert payment details to response body (confirm/refund)
* @param predicate predicate to determine whether the error should be handled
* @returns API handler
*/
export const createDuplicateRequestHandler =
export const createPaymentDetailsRecoveryHandler =
<
Req extends ConfirmRequestConfig | RefundRequestConfig,
Res extends ConfirmResponseBody | RefundResponseBody
>(
converter: (
req: Req,
paymentDetailsResponseBody: PaymentDetailsResponseBody
) => Res
converter: PaymentDetailsConverter<Req, Res>,
predicate = defaultPredicate
): ApiHandler<Req, ApiResponse<Res>> =>
async (req, next, httpClient) => {
async ({ req, next, httpClient }) => {
try {
return await next(req)
} catch (e) {
// 1172: There is a record of transaction with the same order number.
// 1198: API call request has been duplicated.
if (
isLinePayApiError(e) &&
(e.data.returnCode === '1172' || e.data.returnCode === '1198')
) {
const paymentDetails = createPaymentApi(
paymentDetailsWithClient,
httpClient
)
if (!predicate(e)) throw e

try {
// Check with payment details API
const paymentDetailsResponse = await paymentDetails.send({
params: {
transactionId: [req.transactionId]
}
})
const paymentDetails = createPaymentApi(
'paymentDetails',
paymentDetailsWithClient,
httpClient
)

return {
body: converter(req, paymentDetailsResponse.body),
comments: {}
try {
// Check with payment details API
const paymentDetailsResponse = await paymentDetails.send({
params: {
transactionId: [req.transactionId]
}
} catch (paymentDetailsError) {
throw e
})

const comments: Record<string, unknown> = {}

if (isLinePayApiError(e)) {
comments.originalLinePayApiError = e
}

return {
body: converter(req, paymentDetailsResponse.body),
comments
}
} catch (paymentDetailsError) {
throw e
}
throw e
}
}
18 changes: 14 additions & 4 deletions src/handler/timeout-retry.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { GeneralResponseBody } from '@/line-pay-api/type'
import { GeneralResponseBody } from '../line-pay-api/type'
import { isTimeoutError } from '../line-pay-api/error/timeout'
import { ApiHandler, ApiResponse } from '../payment-api/type'

Expand All @@ -14,11 +14,20 @@ export const createTimeoutRetryHandler =
maxRetry = 10,
timeout = 5000
): ApiHandler<Req, ApiResponse<Res>> =>
async (req, next) =>
async ({ req, next }) =>
new Promise((resolve, reject) => {
const f = async (count: number, originalError: unknown) => {
try {
resolve(await next(req))
const res = await next(req)

if (
originalError !== null &&
res.comments.originalLinePayApiError !== undefined
) {
res.comments.originalLinePayApiError = originalError
}

resolve(res)
} catch (e) {
if (isTimeoutError(e)) {
if (count < maxRetry) setTimeout(() => f(count + 1, e), timeout)
Expand All @@ -27,5 +36,6 @@ export const createTimeoutRetryHandler =
reject(e)
}
}
f(0, new Error())

f(0, null)
})
12 changes: 8 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,13 @@ export function createLinePayClient(config: LineMerchantConfig): LinePayClient {
const httpClient = createAuthHttpClient(config)

return {
request: createPaymentApi(requestWithClient, httpClient),
confirm: createPaymentApi(confirmWithClient, httpClient),
refund: createPaymentApi(refundWithClient, httpClient),
paymentDetails: createPaymentApi(paymentDetailsWithClient, httpClient)
request: createPaymentApi('request', requestWithClient, httpClient),
confirm: createPaymentApi('confirm', confirmWithClient, httpClient),
refund: createPaymentApi('refund', refundWithClient, httpClient),
paymentDetails: createPaymentApi(
'paymentDetails',
paymentDetailsWithClient,
httpClient
)
}
}
9 changes: 5 additions & 4 deletions src/line-pay-api/confirm.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { LinePayApiClients } from '@/payment-api/type'
import { GeneralResponseBody } from './type'
import { Currency, Address, HttpClient } from './type'
import { Currency, Address } from './type'

export type ConfirmRequestBody = {
/**
Expand Down Expand Up @@ -137,9 +138,9 @@ export type ConfirmResponseBody = GeneralResponseBody & {
info: Info
}

export const confirmWithClient =
(httpClient: HttpClient) =>
async ({ transactionId, body }: ConfirmRequestConfig) => {
export const confirmWithClient: LinePayApiClients['confirm'] =
httpClient =>
async ({ transactionId, body }) => {
const { data } = await httpClient.post<
ConfirmRequestBody,
ConfirmResponseBody
Expand Down
7 changes: 4 additions & 3 deletions src/line-pay-api/payment-details.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { LinePayApiClients } from '@/payment-api/type'
import { GeneralResponseBody } from './type'
import { Product, HttpClient, Address } from './type'
import { Product, Address } from './type'

export type Fields = 'ALL' | 'TRANSACTION' | 'ORDER'

Expand Down Expand Up @@ -199,8 +200,8 @@ export type PaymentDetailsResponseBody = GeneralResponseBody & {
info: Info[]
}

export const paymentDetailsWithClient =
(httpClient: HttpClient) => async (config: PaymentDetailsRequestConfig) => {
export const paymentDetailsWithClient: LinePayApiClients['paymentDetails'] =
httpClient => async config => {
const { data } = await httpClient.get<
PaymentDetailsRequestParams,
PaymentDetailsResponseBody
Expand Down
8 changes: 4 additions & 4 deletions src/line-pay-api/refund.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { LinePayApiClients } from '@/payment-api/type'
import { GeneralResponseBody } from './type'
import { HttpClient } from './type'

export type RefundRequestBody = {
/**
Expand Down Expand Up @@ -38,9 +38,9 @@ export type RefundResponseBody = GeneralResponseBody & {
info: Info
}

export const refundWithClient =
(httpClient: HttpClient) =>
async ({ transactionId, body }: RefundRequestConfig) => {
export const refundWithClient: LinePayApiClients['refund'] =
httpClient =>
async ({ transactionId, body }) => {
const { data } = await httpClient.post<
RefundRequestBody,
RefundResponseBody
Expand Down
9 changes: 5 additions & 4 deletions src/line-pay-api/request.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { LinePayApiClients } from '@/payment-api/type'
import { GeneralResponseBody } from './type'
import { Currency, Address, Product, HttpClient } from './type'
import { Currency, Address, Product } from './type'

export type Package = {
/**
Expand Down Expand Up @@ -231,9 +232,9 @@ export type RequestRequestConfig = {
body: RequestRequestBody
}

export const requestWithClient =
(httpClient: HttpClient) =>
async ({ body }: RequestRequestConfig) => {
export const requestWithClient: LinePayApiClients['request'] =
httpClient =>
async ({ body }) => {
const { data } = await httpClient.post<
RequestRequestBody,
RequestResponseBody
Expand Down
4 changes: 4 additions & 0 deletions src/line-pay-api/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
*/
export type QueryParams = Record<string, string | number | boolean>

export type ApiClientBuilder<Req, Res> = (
httpClient: HttpClient
) => (req: Req) => Promise<Res>

/**
* Payment currency (ISO 4217)
*/
Expand Down
48 changes: 36 additions & 12 deletions src/payment-api/create.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,50 @@
import { HttpClient } from '@/line-pay-api/type'
import { ApiResponse, ApiHandler, PaymentApi } from './type'
import { GeneralResponseBody } from '@/line-pay-api/type'
import { ApiResponse, ApiHandler, PaymentApi, LinePayApiClients } from './type'

export function createPaymentApi<Req, Res extends GeneralResponseBody>(
createSender: (httpClient: HttpClient) => (req: Req) => Promise<Res>,
type Awaited<T> = T extends PromiseLike<infer U> ? Awaited<U> : T

type RequestConfig<T extends keyof LinePayApiClients> = Parameters<
ReturnType<LinePayApiClients[T]>
>[0]

type ResponseBody<T extends keyof LinePayApiClients> = Awaited<
ReturnType<ReturnType<LinePayApiClients[T]>>
>

export function createPaymentApi<T extends keyof LinePayApiClients>(
type: T,
createSender: (
httpClient: HttpClient
) => (req: RequestConfig<T>) => Promise<ResponseBody<T>>,
httpClient: HttpClient,
handlers: ApiHandler<Req, ApiResponse<Res>>[] = []
): PaymentApi<Req, ApiResponse<Res>> {
const addHandlers = (...fs: ApiHandler<Req, ApiResponse<Res>>[]) => {
handlers: ApiHandler<RequestConfig<T>, ApiResponse<ResponseBody<T>>>[] = []
): PaymentApi<RequestConfig<T>, ApiResponse<ResponseBody<T>>> {
const addHandlers = (
...fs: ApiHandler<RequestConfig<T>, ApiResponse<ResponseBody<T>>>[]
) => {
handlers.push(...fs)
return createPaymentApi(createSender, httpClient, handlers)
return createPaymentApi(type, createSender, httpClient, handlers)
}

const sender = async (req: Req): Promise<ApiResponse<Res>> => ({
const sender = async (
req: RequestConfig<T>
): Promise<ApiResponse<ResponseBody<T>>> => ({
body: await createSender(httpClient)(req),
comments: {}
})

const getHandler = (i: number) => async (req: Req) =>
i < 0 ? sender(req) : handlers[i](req, getHandler(i - 1), httpClient)
const getHandler = (i: number) => async (req: RequestConfig<T>) =>
i < 0
? sender(req)
: handlers[i]({
type,
req,
next: getHandler(i - 1),
httpClient
})

const send = async (req: Req) => getHandler(handlers.length - 1)(req)
const send = async (req: RequestConfig<T>) =>
getHandler(handlers.length - 1)(req)

return {
addHandler: addHandlers,
Expand Down
43 changes: 39 additions & 4 deletions src/payment-api/type.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,53 @@
import { GeneralResponseBody, HttpClient } from '@/line-pay-api/type'
import {
ConfirmRequestConfig,
ConfirmResponseBody
} from '@/line-pay-api/confirm'
import {
PaymentDetailsRequestConfig,
PaymentDetailsResponseBody
} from '@/line-pay-api/payment-details'
import { RefundRequestConfig, RefundResponseBody } from '@/line-pay-api/refund'
import {
RequestRequestConfig,
RequestResponseBody
} from '@/line-pay-api/request'
import {
ApiClientBuilder,
GeneralResponseBody,
HttpClient
} from '@/line-pay-api/type'

export type ApiHandler<Req, Res> = (
export type LinePayApiClients = {
request: ApiClientBuilder<RequestRequestConfig, RequestResponseBody>
confirm: ApiClientBuilder<ConfirmRequestConfig, ConfirmResponseBody>
refund: ApiClientBuilder<RefundRequestConfig, RefundResponseBody>
paymentDetails: ApiClientBuilder<
PaymentDetailsRequestConfig,
PaymentDetailsResponseBody
>
}

export type ApiHandlerParams<Req, Res> = {
/**
* LINE Pay API type
*/
type: keyof LinePayApiClients
/**
* The request object
*/
req: Req,
req: Req
/**
* The next handler or the request sending function
*/
next: (req: Req) => Promise<Res>,
next: (req: Req) => Promise<Res>
/**
* The HTTP client
*/
httpClient: HttpClient
}

export type ApiHandler<Req, Res> = (
params: ApiHandlerParams<Req, Res>
) => Promise<Res>

export type ApiResponse<Body extends GeneralResponseBody> = {
Expand Down

0 comments on commit 28c552c

Please sign in to comment.