Shalom Halbert
12/15/2021, 12:11 PMtavish pegram
12/15/2021, 6:29 PMTried to refactor this in a way that
Respects a consistent level of abstraction in each function
Keeps the "top-level" abstraction declarative and for the most part hiding the sad path (though I did leave in a single mapErrors fn in each call
Hides the "glue" or "adapter" functions that make all the different types fit together.
I'm currently wrestling with the idea that
Arrow and FP is good and provides super generic good tools like Either, map, flatMap, etc. that we can use to implement pretty much anything in a Good Way.
Those same tools are "low level" and distracting when reading the code
So what I'm messing around with is trying to keep the main bits of behavior in each file/class at a high level of abstraction (of course, what "high-level abstraction" means depends on the context. In this example its high level to talk about HTTP responses and JSON. In a Payment service, that would be considered low level). We do this by adding a bunch of private extension functions that hide the "glue" which is normally needed to fit all these different types together while giving them expressive and clear names (approaching a DSL, yay!), and keeping the functions small enough that they are easy to update and reason about in isolation (hopefully, its what I was going for).
Another way of thinking about it is that we are taking these great super generic lego pieces and fitting them together into larger (though still modular) "solutions" that we paint with concrete function names before finally composing those concrete pieces together at the "top"
tavish pegram
12/15/2021, 6:31 PMclass AffirmClient(
private val httpClient: HttpClient,
// @Value("\${affirm.au.private_key}")
private val affirmAuPrivateKey: String,
// @Value("\${affirm.au.public_key}")
private val affirmAuPublicKey: String,
// @Value("\${affirm.au.hostname}")
private val affirmAuApiUrl: String,
) {
private val logger = KotlinLogging.logger { }
suspend fun authorizeTransaction(
partnerPaymentMethodId: String,
metadata: Metadata,
): Either<AuthedError, AuthorizeTransactionResponse> =
Either.catch {
httpClient.postJson(
url = "${affirmAuApiUrl}api/v1/transactions",
request = AuthorizeTransactionRequest(
transactionId = partnerPaymentMethodId,
orderId = metadata.orderId,
),
auth = getAuthHeader(),
)
}.mapLeft {
AuthedError.AuthFailureError(it.message)
}.flatMap {
<http://logger.info|logger.info> { "AffirmClient.authorizeTransaction response: ${it.body}" }
if (it.code == 200) {
AuthorizeTransactionResponse.fromHTTPBody(it.body).right()
} else {
AuthedError.AuthFailureError(message = it.body.optString("message").toString()).left()
}
}
suspend fun captureTransaction(
authorizationToken: String
): Either<CapturedError, CaptureTransactionResponse> =
Either.catch {
httpClient.postJson(
url = "${affirmAuApiUrl}api/v1/transactions/$authorizationToken/capture",
auth = getAuthHeader(),
)
}.mapLeft {
CapturedError.CaptureActionError(it.message)
}.flatMap {
<http://logger.info|logger.info> { "AffirmClient.captureTransaction response: ${it.body}" }
if (it.code == 200) {
CaptureTransactionResponse.fromHTTPBody(it.body).right()
} else {
CapturedError.CaptureActionError(
message = it.body.optString("message").toString()
).left()
}
}
suspend fun refundTransaction(
confirmationId: String,
amount: Int,
): Either<RefundError, RefundTransactionResponse> =
Either.catch {
httpClient.postJson(
url = "${affirmAuApiUrl}api/v1/transactions/$confirmationId/refund",
request = RefundTransactionRequest(amount = amount),
auth = getAuthHeader(),
)
}.mapLeft {
RefundError.RefundFailureError(it.message)
}.flatMap {
<http://logger.info|logger.info> { "AffirmClient.refundTransaction response: ${it.body}" }
if (it.code == 200) {
RefundTransactionResponse.fromHTTPBody(it.body).right()
} else {
RefundError.RefundFailureError(
message = it.body.optString("message").toString()
).left()
}
}
suspend fun voidTransaction(
confirmationId: String
): Either<VoidError, Unit> =
Either.catch {
httpClient.postJson(
url = "${affirmAuApiUrl}api/v1/transactions/$confirmationId/void",
auth = getAuthHeader(),
)
}.mapLeft {
VoidError.VoidFailureError(it.message)
}.flatMap {
<http://logger.info|logger.info> { "AffirmClient.voidTransaction response: ${it.body}" }
if (it.code == 200) {
Unit.right()
} else {
VoidError.VoidFailureError(
message = it.body.optString("message").toString()
).left()
}
}
private fun getAuthHeader(): HttpHeader =
HttpHeader(key = affirmAuPublicKey, value = affirmAuPrivateKey)
}
Aftertavish pegram
12/15/2021, 6:31 PMclass AffirmClient(
private val httpClient: HttpClient,
private val affirmAuPrivateKey: String,
private val affirmAuPublicKey: String,
private val affirmAuApiUrl: String,
) {
@Loggable
suspend fun authorizeTransaction(
partnerPaymentMethodId: String,
metadata: Metadata,
): Either<AuthedError, AuthorizeTransactionResponse> =
<http://httpClient.post|httpClient.post>(
url = "${affirmAuApiUrl}api/v1/transactions",
request = AuthorizeTransactionRequest(
transactionId = partnerPaymentMethodId,
orderId = metadata.orderId,
),
auth = getAuthHeader(),
)
.mapAuthErrors()
.parseAuthorizeResponse()
@Loggable
suspend fun captureTransaction(
authorizationToken: String
): Either<CapturedError, CaptureTransactionResponse> =
<http://httpClient.post|httpClient.post>(
url = "${affirmAuApiUrl}api/v1/transactions/$authorizationToken/capture",
auth = getAuthHeader(),
)
.mapCaptureErrors()
.parseCaptureResponse()
@Loggable
suspend fun refundTransaction(
confirmationId: String,
amount: Int,
): Either<RefundError, RefundTransactionResponse> =
<http://httpClient.post|httpClient.post>(
url = "${affirmAuApiUrl}api/v1/transactions/$confirmationId/refund",
request = RefundTransactionRequest(amount = amount),
auth = getAuthHeader(),
)
.mapRefundErrors()
.parseRefundResponse()
@Loggable
suspend fun voidTransaction(
confirmationId: String
): Either<VoidError, Unit> =
<http://httpClient.post|httpClient.post>(
url = "${affirmAuApiUrl}api/v1/transactions/$confirmationId/void",
auth = getAuthHeader(),
)
.mapVoidErrors()
.parseVoidResponse()
private fun getAuthHeader(): HttpHeader =
HttpHeader(key = affirmAuPublicKey, value = affirmAuPrivateKey)
}
@Loggable
private fun <http://HttpClient.post|HttpClient.post>(
url: String,
request: HttpRequest? = null,
auth: HttpHeader? = null,
): Either<Throwable, HttpResponse> =
Either.catch {
this.postJson(
url = url,
request = request,
auth = auth,
)
}
private fun <B> Either<Throwable, B>.mapAuthErrors() =
this.mapLeft { AuthedError.AuthFailureError(it.message) }
private fun Either<AuthedError, HttpResponse>.parseAuthorizeResponse() = this.flatMap {
when (it.code == 200) {
true ->
AuthorizeTransactionResponse
.fromHTTPBody(it.body)
.right()
false -> AuthedError.AuthFailureError(
message = it.body.optString("message").toString()
).left()
}
}
private fun <B> Either<Throwable, B>.mapCaptureErrors() =
this.mapLeft { CapturedError.CaptureActionError(it.message) }
private fun Either<CapturedError, HttpResponse>.parseCaptureResponse() = this.flatMap {
when (it.code == 200) {
true ->
CaptureTransactionResponse
.fromHTTPBody(it.body)
.right()
false -> CapturedError.CaptureActionError(
message = it.body.optString("message").toString()
).left()
}
}
private fun <B> Either<Throwable, B>.mapRefundErrors() =
this.mapLeft { RefundError.RefundFailureError(it.message) }
private fun Either<RefundError, HttpResponse>.parseRefundResponse() = this.flatMap {
when (it.code == 200) {
true ->
RefundTransactionResponse
.fromHTTPBody(it.body)
.right()
false -> RefundError.RefundFailureError(
message = it.body.optString("message").toString()
).left()
}
}
private fun <B> Either<Throwable, B>.mapVoidErrors() =
this.mapLeft { VoidError.VoidFailureError(it.message) }
private fun Either<VoidError, HttpResponse>.parseVoidResponse() = this.flatMap {
when (it.code == 200) {
true -> Unit.right()
false -> VoidError.VoidFailureError(
message = it.body.optString("message").toString()
).left()
}
}
Shalom Halbert
12/16/2021, 9:50 PM