From 87c9fab88f109f278ced3ccb7e46df6282cb9615 Mon Sep 17 00:00:00 2001 From: "Kjell M. Myksvoll" Date: Fri, 29 Nov 2019 11:16:37 +0100 Subject: [PATCH] Cleanup and refactoring of Stripe error collection and reporting --- .../support/resources/HoustonResources.kt | 4 +- .../ostelco/prime/storage/graph/Neo4jStore.kt | 81 +++++++++------- .../prime/paymentprocessor/StripeMonitor.kt | 4 +- .../StripePaymentProcessor.kt | 19 ++-- .../prime/paymentprocessor/StripeUtils.kt | 94 +++++++++++------- .../resources/StripeMonitorResource.kt | 4 +- .../org/ostelco/prime/apierror/ApiError.kt | 31 ++++-- .../paymentprocessor/core/PaymentError.kt | 97 +++++++++++++++++-- 8 files changed, 235 insertions(+), 99 deletions(-) diff --git a/customer-support-endpoint/src/main/kotlin/org/ostelco/prime/support/resources/HoustonResources.kt b/customer-support-endpoint/src/main/kotlin/org/ostelco/prime/support/resources/HoustonResources.kt index 1ab32db4b..d9731e27b 100644 --- a/customer-support-endpoint/src/main/kotlin/org/ostelco/prime/support/resources/HoustonResources.kt +++ b/customer-support-endpoint/src/main/kotlin/org/ostelco/prime/support/resources/HoustonResources.kt @@ -25,7 +25,7 @@ import org.ostelco.prime.model.SimProfile import org.ostelco.prime.model.Subscription import org.ostelco.prime.module.getResource import org.ostelco.prime.notifications.NOTIFY_OPS_MARKER -import org.ostelco.prime.paymentprocessor.core.ForbiddenError +import org.ostelco.prime.paymentprocessor.core.InvalidRequestError import org.ostelco.prime.paymentprocessor.core.ProductInfo import org.ostelco.prime.storage.AdminDataSource import org.ostelco.prime.storage.AuditLogStore @@ -308,7 +308,7 @@ class RefundResource { storage.refundPurchase(identity, purchaseRecordId, reason) }.mapLeft { when (it) { - is ForbiddenError -> org.ostelco.prime.apierror.ForbiddenError("Failed to refund purchase. ${it.description}", ApiErrorCode.FAILED_TO_REFUND_PURCHASE) + is InvalidRequestError -> org.ostelco.prime.apierror.ForbiddenError("Failed to refund purchase. ${it.description}", ApiErrorCode.FAILED_TO_REFUND_PURCHASE) else -> NotFoundError("Failed to refund purchase. ${it.toString()}", ApiErrorCode.FAILED_TO_REFUND_PURCHASE) } } diff --git a/neo4j-store/src/main/kotlin/org/ostelco/prime/storage/graph/Neo4jStore.kt b/neo4j-store/src/main/kotlin/org/ostelco/prime/storage/graph/Neo4jStore.kt index e47d2c72f..754cccddd 100644 --- a/neo4j-store/src/main/kotlin/org/ostelco/prime/storage/graph/Neo4jStore.kt +++ b/neo4j-store/src/main/kotlin/org/ostelco/prime/storage/graph/Neo4jStore.kt @@ -85,8 +85,7 @@ import org.ostelco.prime.module.getResource import org.ostelco.prime.notifications.EmailNotifier import org.ostelco.prime.notifications.NOTIFY_OPS_MARKER import org.ostelco.prime.paymentprocessor.PaymentProcessor -import org.ostelco.prime.paymentprocessor.core.BadGatewayError -import org.ostelco.prime.paymentprocessor.core.ForbiddenError +import org.ostelco.prime.paymentprocessor.core.InvalidRequestError import org.ostelco.prime.paymentprocessor.core.InvoicePaymentInfo import org.ostelco.prime.paymentprocessor.core.PaymentError import org.ostelco.prime.paymentprocessor.core.PaymentStatus @@ -94,7 +93,10 @@ import org.ostelco.prime.paymentprocessor.core.PaymentTransactionInfo import org.ostelco.prime.paymentprocessor.core.PlanAlredyPurchasedError import org.ostelco.prime.paymentprocessor.core.ProductInfo import org.ostelco.prime.paymentprocessor.core.ProfileInfo +import org.ostelco.prime.paymentprocessor.core.StorePurchaseError import org.ostelco.prime.paymentprocessor.core.SubscriptionDetailsInfo +import org.ostelco.prime.paymentprocessor.core.SubscriptionError +import org.ostelco.prime.paymentprocessor.core.UpdatePurchaseError import org.ostelco.prime.securearchive.SecureArchiveService import org.ostelco.prime.sim.SimManager import org.ostelco.prime.storage.AlreadyExistsError @@ -1282,13 +1284,13 @@ object Neo4jStoreSingleton : GraphStore { .mapLeft { org.ostelco.prime.paymentprocessor.core.NotFoundError( "Failed to get customer data for customer with identity - $identity", - error = it) + internalError = it) }.bind() val product = getProduct(identity, sku) .mapLeft { org.ostelco.prime.paymentprocessor.core.NotFoundError("Product $sku is unavailable", - error = it) + internalError = it) } .bind() @@ -1328,10 +1330,11 @@ object Neo4jStoreSingleton : GraphStore { will ensure that the invoice will be voided. */ createPurchaseRecord(customer.id, purchaseRecord) .mapLeft { - logger.error("Failed to save purchase record for customer ${customer.id}, invoice-id $invoiceId, invoice will be voided in Stripe") - AuditLog.error(customerId = customer.id, message = "Failed to save purchase record - invoice-id $invoiceId, invoice will be voided in Stripe") - BadGatewayError("Failed to save purchase record", - error = it) + logger.error("Failed to save purchase record for customer ${customer.id}, invoice: $invoiceId, invoice will be voided in Stripe") + AuditLog.error(customerId = customer.id, + message = "Failed to save purchase record - invoice: $invoiceId, invoice will be voided in Stripe") + StorePurchaseError("Failed to save purchase record", + internalError = it) }.bind() /* TODO: While aborting transactions, send a record with "reverted" status. */ @@ -1347,7 +1350,9 @@ object Neo4jStoreSingleton : GraphStore { customerId = customer.id, product = product ).mapLeft { - BadGatewayError(description = it.message, error = it.error).left().bind() + StorePurchaseError(description = it.message, internalError = it.error) + .left() + .bind() }.bind() ProductInfo(product.sku) @@ -1447,8 +1452,8 @@ object Neo4jStoreSingleton : GraphStore { if (sourceId != null) { val sourceDetails = paymentProcessor.getSavedSources(customer.id) .mapLeft { - BadGatewayError("Failed to fetch sources for customer: ${customer.id}", - error = it) + org.ostelco.prime.paymentprocessor.core.NotFoundError("Failed to fetch sources for customer: ${customer.id}", + internalError = it) }.bind() if (!sourceDetails.any { sourceDetailsInfo -> sourceDetailsInfo.id == sourceId }) { paymentProcessor.addSource(customer.id, sourceId) @@ -1462,8 +1467,8 @@ object Neo4jStoreSingleton : GraphStore { taxRegionId = taxRegionId) .mapLeft { AuditLog.error(customerId = customer.id, message = "Failed to subscribe to plan $sku") - BadGatewayError("Failed to subscribe ${customer.id} to plan $sku", - error = it) + SubscriptionError("Failed to subscribe ${customer.id} to plan $sku", + internalError = it) } .bind() }.fix() @@ -1531,7 +1536,7 @@ object Neo4jStoreSingleton : GraphStore { } PaymentStatus.REQUIRES_PAYMENT_METHOD -> { NotCreatedError(type = planEntity.name, id = "Failed to subscribe $customerId to ${plan.id}", - error = ForbiddenError("Payment method failed")) + error = InvalidRequestError("Payment method failed")) .left().bind() } PaymentStatus.REQUIRES_ACTION, @@ -1580,7 +1585,8 @@ object Neo4jStoreSingleton : GraphStore { if (sourceId != null) { val sourceDetails = paymentProcessor.getSavedSources(customer.id) .mapLeft { - BadGatewayError("Failed to fetch sources for user", error = it) + org.ostelco.prime.paymentprocessor.core.NotFoundError("Failed to fetch sources for user", + internalError = it) }.bind() addedSourceId = sourceId @@ -1607,8 +1613,8 @@ object Neo4jStoreSingleton : GraphStore { it }.linkReversalActionToTransaction(transaction) { paymentProcessor.removeInvoice(it.id) - logger.error(NOTIFY_OPS_MARKER, - """Failed to create or pay invoice for customer ${customer.id}, invoice-id: ${it.id}. + logger.warn(NOTIFY_OPS_MARKER, + """Failed to pay invoice for customer ${customer.id}, invoice-id: ${it.id}. Verify that the invoice has been deleted or voided in Stripe dashboard. """.trimIndent()) }.bind() @@ -1616,11 +1622,11 @@ object Neo4jStoreSingleton : GraphStore { /* Force immediate payment of the invoice. */ val invoicePaymentInfo = paymentProcessor.payInvoice(invoice.id) .mapLeft { - logger.error("Payment of invoice ${invoice.id} failed for customer ${customer.id}.") + logger.warn("Payment of invoice ${invoice.id} failed for customer ${customer.id}.") it }.linkReversalActionToTransaction(transaction) { paymentProcessor.refundCharge(it.chargeId) - logger.error(NOTIFY_OPS_MARKER, + logger.warn(NOTIFY_OPS_MARKER, """Refunded customer ${customer.id} for invoice: ${it.id}. Verify that the invoice has been refunded in Stripe dashboard. """.trimIndent()) @@ -2677,8 +2683,8 @@ object Neo4jStoreSingleton : GraphStore { val purchaseRecords = getPurchaseTransactions(startPadded, endPadded) .mapLeft { - BadGatewayError("Error when fetching purchase records", - error = it) + org.ostelco.prime.paymentprocessor.core.NotFoundError("Error when fetching purchase records", + internalError = it) }.bind() val paymentRecords = getPaymentTransactions(startPadded, endPadded) .bind() @@ -2745,16 +2751,19 @@ object Neo4jStoreSingleton : GraphStore { // For refunds // - private fun checkPurchaseRecordForRefund(purchaseRecord: PurchaseRecord): Either { - if (purchaseRecord.refund != null) { - logger.error("Trying to refund again, ${purchaseRecord.id}, refund ${purchaseRecord.refund?.id}") - return Either.left(ForbiddenError("Trying to refund again")) - } else if (purchaseRecord.product.price.amount == 0) { - logger.error("Trying to refund a free product, ${purchaseRecord.id}") - return Either.left(ForbiddenError("Trying to refund a free purchase")) - } - return Unit.right() - } + private fun checkPurchaseRecordForRefund(purchaseRecord: PurchaseRecord): Either = + if (purchaseRecord.refund != null) { + logger.error("Trying to refund again, ${purchaseRecord.id}, refund ${purchaseRecord.refund?.id}") + InvalidRequestError("Attempt at refunding again the purchase ${purchaseRecord.id} " + + "of product ${purchaseRecord.product.sku}, refund ${purchaseRecord.refund?.id}") + .left() + } else if (purchaseRecord.product.price.amount == 0) { + logger.error("Trying to refund a free product, ${purchaseRecord.id}") + InvalidRequestError("Trying to refund a free purchase of product ${purchaseRecord.product.sku}") + .left() + } else { + Unit.right() + } override fun refundPurchase( identity: ModelIdentity, @@ -2766,13 +2775,13 @@ object Neo4jStoreSingleton : GraphStore { .mapLeft { logger.error("Failed to find customer with identity - $identity") NotFoundPaymentError("Failed to find customer with identity - $identity", - error = it) + internalError = it) }.bind() val purchaseRecord = get(PurchaseRecord withId purchaseRecordId) // If we can't find the record, return not-found .mapLeft { org.ostelco.prime.paymentprocessor.core.NotFoundError("Purchase Record unavailable", - error = it) + internalError = it) }.bind() checkPurchaseRecordForRefund(purchaseRecord) .bind() @@ -2788,9 +2797,9 @@ object Neo4jStoreSingleton : GraphStore { ) update { changedPurchaseRecord } .mapLeft { - logger.error("failed to update purchase record, for refund $refund.id, chargeId $purchaseRecordId, payment has been refunded in Stripe") - BadGatewayError("Failed to update purchase record for refund ${refund.id}", - error = it) + logger.error("Failed to update purchase record, for refund $refund.id, chargeId $purchaseRecordId, payment has been refunded in Stripe") + UpdatePurchaseError("Failed to update purchase record for refund ${refund.id}", + internalError = it) }.bind() analyticsReporter.reportRefund( diff --git a/payment-processor/src/main/kotlin/org/ostelco/prime/paymentprocessor/StripeMonitor.kt b/payment-processor/src/main/kotlin/org/ostelco/prime/paymentprocessor/StripeMonitor.kt index e739dbff9..cd2ef0e77 100644 --- a/payment-processor/src/main/kotlin/org/ostelco/prime/paymentprocessor/StripeMonitor.kt +++ b/payment-processor/src/main/kotlin/org/ostelco/prime/paymentprocessor/StripeMonitor.kt @@ -10,7 +10,7 @@ import com.stripe.model.WebhookEndpoint import org.ostelco.prime.getLogger import org.ostelco.prime.notifications.NOTIFY_OPS_MARKER import org.ostelco.prime.paymentprocessor.StripeUtils.either -import org.ostelco.prime.paymentprocessor.core.BadGatewayError +import org.ostelco.prime.paymentprocessor.core.NotFoundError import org.ostelco.prime.paymentprocessor.core.PaymentConfigurationError import org.ostelco.prime.paymentprocessor.core.PaymentError @@ -234,7 +234,7 @@ class StripeMonitor { }.right() else { logger.error("No webhooks found on check for Stripe events state") - BadGatewayError("No webhooks found on check for Stripe events state") + NotFoundError("No webhooks found on check for Stripe events state") .left() } } diff --git a/payment-processor/src/main/kotlin/org/ostelco/prime/paymentprocessor/StripePaymentProcessor.kt b/payment-processor/src/main/kotlin/org/ostelco/prime/paymentprocessor/StripePaymentProcessor.kt index 3f2f5d2ab..550528b01 100644 --- a/payment-processor/src/main/kotlin/org/ostelco/prime/paymentprocessor/StripePaymentProcessor.kt +++ b/payment-processor/src/main/kotlin/org/ostelco/prime/paymentprocessor/StripePaymentProcessor.kt @@ -23,8 +23,8 @@ import com.stripe.net.RequestOptions import org.ostelco.prime.getLogger import org.ostelco.prime.notifications.NOTIFY_OPS_MARKER import org.ostelco.prime.paymentprocessor.StripeUtils.either -import org.ostelco.prime.paymentprocessor.core.BadGatewayError -import org.ostelco.prime.paymentprocessor.core.ForbiddenError +import org.ostelco.prime.paymentprocessor.core.ChargeError +import org.ostelco.prime.paymentprocessor.core.InvoiceError import org.ostelco.prime.paymentprocessor.core.InvoicePaymentInfo import org.ostelco.prime.paymentprocessor.core.InvoiceInfo import org.ostelco.prime.paymentprocessor.core.InvoiceItemInfo @@ -36,6 +36,7 @@ import org.ostelco.prime.paymentprocessor.core.PlanInfo import org.ostelco.prime.paymentprocessor.core.ProductInfo import org.ostelco.prime.paymentprocessor.core.ProfileInfo import org.ostelco.prime.paymentprocessor.core.SourceDetailsInfo +import org.ostelco.prime.paymentprocessor.core.SourceError import org.ostelco.prime.paymentprocessor.core.SourceInfo import org.ostelco.prime.paymentprocessor.core.SubscriptionDetailsInfo import org.ostelco.prime.paymentprocessor.core.SubscriptionInfo @@ -303,6 +304,7 @@ class StripePaymentProcessor : PaymentProcessor { SubscriptionInfo(id = subscription.id) } + /* TODO (kmm): Fix missing exception handling of the 'Charge.create()' call. */ override fun authorizeCharge(customerId: String, sourceId: String?, amount: Int, currency: String): Either { val errorMessage = "Failed to authorize the charge for customerId $customerId sourceId $sourceId amount $amount currency $currency" return when (amount) { @@ -323,12 +325,13 @@ class StripePaymentProcessor : PaymentProcessor { Either.cond( test = (review == null), ifTrue = { charge.id }, - ifFalse = { ForbiddenError("Review required, $errorMessage $review") } + ifFalse = { ChargeError("Review required, $errorMessage $review") } ) } } } + /* TODO (kmm): Fix missing exception handling of the 'Charge.retrive()' call. */ override fun captureCharge(chargeId: String, customerId: String, amount: Int, currency: String): Either { val errorMessage = "Failed to capture charge for customerId $customerId chargeId $chargeId" return when (amount) { @@ -340,7 +343,7 @@ class StripePaymentProcessor : PaymentProcessor { Either.cond( test = (review == null), ifTrue = { charge }, - ifFalse = { ForbiddenError("Review required, $errorMessage $review") } + ifFalse = { ChargeError("Review required, $errorMessage $review") } ) }.flatMap { charge -> try { @@ -348,7 +351,7 @@ class StripePaymentProcessor : PaymentProcessor { charge.id.right() } catch (e: Exception) { logger.warn(errorMessage, e) - BadGatewayError(errorMessage).left() + ChargeError(errorMessage).left() } } } @@ -378,7 +381,7 @@ class StripePaymentProcessor : PaymentProcessor { is Card -> accountInfo.delete() is Source -> accountInfo.detach() else -> - BadGatewayError("Attempt to remove unsupported account-type $accountInfo") + SourceError("Attempt to remove unsupported account-type $accountInfo") .left() } SourceInfo(sourceId) @@ -448,9 +451,9 @@ class StripePaymentProcessor : PaymentProcessor { removeInvoice(invoice) .fold({ - BadGatewayError(errorMessage, error = it) + InvoiceError(errorMessage, internalError = it) }, { - BadGatewayError(errorMessage) + InvoiceError(errorMessage) }).left() } else { InvoiceInfo(invoice.id).right() diff --git a/payment-processor/src/main/kotlin/org/ostelco/prime/paymentprocessor/StripeUtils.kt b/payment-processor/src/main/kotlin/org/ostelco/prime/paymentprocessor/StripeUtils.kt index e9bfefc46..7b6190bdb 100644 --- a/payment-processor/src/main/kotlin/org/ostelco/prime/paymentprocessor/StripeUtils.kt +++ b/payment-processor/src/main/kotlin/org/ostelco/prime/paymentprocessor/StripeUtils.kt @@ -1,6 +1,7 @@ package org.ostelco.prime.paymentprocessor import arrow.core.Either +import arrow.core.left import com.stripe.exception.ApiConnectionException import com.stripe.exception.AuthenticationException import com.stripe.exception.CardException @@ -8,9 +9,14 @@ import com.stripe.exception.InvalidRequestException import com.stripe.exception.RateLimitException import com.stripe.exception.StripeException import org.ostelco.prime.getLogger -import org.ostelco.prime.paymentprocessor.core.BadGatewayError -import org.ostelco.prime.paymentprocessor.core.ForbiddenError +import org.ostelco.prime.paymentprocessor.core.ApiConnectionError +import org.ostelco.prime.paymentprocessor.core.AuthenticationError +import org.ostelco.prime.paymentprocessor.core.CardError +import org.ostelco.prime.paymentprocessor.core.GenericError +import org.ostelco.prime.paymentprocessor.core.InvalidRequestError import org.ostelco.prime.paymentprocessor.core.PaymentError +import org.ostelco.prime.paymentprocessor.core.PaymentVendorError +import org.ostelco.prime.paymentprocessor.core.RateLimitError object StripeUtils { @@ -19,38 +25,54 @@ object StripeUtils { /** * Convenience function for handling Stripe I/O errors. */ - fun either(errorDescription: String, action: () -> RETURN): Either { - return try { - Either.right(action()) - } catch (e: CardException) { - // If something is decline with a card purchase, CardException will be caught - logger.warn("Payment error : $errorDescription , Stripe Error Code: ${e.code}") - Either.left(ForbiddenError(errorDescription, e.message)) - } catch (e: RateLimitException) { - // Too many requests made to the API too quickly - logger.warn("Payment error : $errorDescription , Stripe Error Code: ${e.code}") - Either.left(BadGatewayError(errorDescription, e.message)) - } catch (e: InvalidRequestException) { - // Invalid parameters were supplied to Stripe's API - logger.warn("Payment error : $errorDescription , Stripe Error Code: ${e.code}") - Either.left(ForbiddenError(errorDescription, e.message)) - } catch (e: AuthenticationException) { - // Authentication with Stripe's API failed - // (maybe you changed API keys recently) - logger.warn("Payment error : $errorDescription , Stripe Error Code: ${e.code}", e) - Either.left(BadGatewayError(errorDescription)) - } catch (e: ApiConnectionException) { - // Network communication with Stripe failed - logger.warn("Payment error : $errorDescription , Stripe Error Code: ${e.code}", e) - Either.left(BadGatewayError(errorDescription)) - } catch (e: StripeException) { - // Unknown Stripe error - logger.error("Payment error : $errorDescription , Stripe Error Code: ${e.code}", e) - Either.left(BadGatewayError(errorDescription)) - } catch (e: Exception) { - // Something else happened, could be completely unrelated to Stripe - logger.error(errorDescription, e) - Either.left(BadGatewayError(errorDescription)) - } - } + fun either(description: String, action: () -> RETURN): Either = + try { + Either.right(action()) + } catch (e: CardException) { + /* Card got declined. */ + logger.warn("Payment card error : $description, Stripe codes: ${e.code}, ${e.declineCode}, ${e.statusCode}") + CardError(description = description, + message = e.message, + code = e.code, + declineCode = e.declineCode).left() + } catch (e: RateLimitException) { + /* Too many requests made to the Stripe API at the same time. */ + logger.error("Payment rate limiting error : $description, Stripe codes: ${e.code}, ${e.statusCode}") + RateLimitError(description = description, + message = e.message, + code = e.code).left() + } catch (e: InvalidRequestException) { + /* Invalid parameters were supplied to Stripe's API. */ + logger.error("Payment invalid request : $description, Stripe codes: ${e.code}, ${e.statusCode}") + InvalidRequestError(description = description, + message = e.message, + code = e.code).left() + } catch (e: AuthenticationException) { + /* Authentication with Stripe's API failed. + (Maybe you changed API keys recently?) */ + logger.error("Payment authentication error : $description, Stripe codes: ${e.code}, ${e.statusCode}", + e) + AuthenticationError(description = description, + message = e.message, + code = e.code).left() + } catch (e: ApiConnectionException) { + /* Network communication with Stripe failed. */ + logger.error("Payment API connection error : $description", + e) + ApiConnectionError(description = description, + message = e.message).left() + } catch (e: StripeException) { + /* Unknown Stripe error. */ + logger.error("Payment unknown Stripe error : $description", + e) + PaymentVendorError(description = description, + message = e.message).left() + } catch (e: Exception) { + /* Something else happened, completely unrelated to + Stripe. */ + logger.error("Payment unknown error : $description", + e) + GenericError(description = description, + message = e.message).left() + } } diff --git a/payment-processor/src/main/kotlin/org/ostelco/prime/paymentprocessor/resources/StripeMonitorResource.kt b/payment-processor/src/main/kotlin/org/ostelco/prime/paymentprocessor/resources/StripeMonitorResource.kt index b678f842e..f6f305ffa 100644 --- a/payment-processor/src/main/kotlin/org/ostelco/prime/paymentprocessor/resources/StripeMonitorResource.kt +++ b/payment-processor/src/main/kotlin/org/ostelco/prime/paymentprocessor/resources/StripeMonitorResource.kt @@ -11,7 +11,7 @@ import org.ostelco.prime.getLogger import org.ostelco.prime.jsonmapper.asJson import org.ostelco.prime.paymentprocessor.StripeEventState import org.ostelco.prime.paymentprocessor.StripeMonitor -import org.ostelco.prime.paymentprocessor.core.BadGatewayError +import org.ostelco.prime.paymentprocessor.core.GenericError import org.ostelco.prime.paymentprocessor.core.PaymentError import org.ostelco.prime.paymentprocessor.publishers.StripeEventPublisher import java.time.Instant @@ -148,7 +148,7 @@ class StripeMonitorResource(val monitor: StripeMonitor) { events.size }.toEither { logger.error("Failed to publish retrieved failed events - ${it.message}") - BadGatewayError("Failed to publish failed retrieved events - ${it.message}") + GenericError("Failed to publish failed retrieved events - ${it.message}") } private fun ok(value: Map) = diff --git a/prime-modules/src/main/kotlin/org/ostelco/prime/apierror/ApiError.kt b/prime-modules/src/main/kotlin/org/ostelco/prime/apierror/ApiError.kt index eebe0ac44..708681923 100644 --- a/prime-modules/src/main/kotlin/org/ostelco/prime/apierror/ApiError.kt +++ b/prime-modules/src/main/kotlin/org/ostelco/prime/apierror/ApiError.kt @@ -31,15 +31,32 @@ object ApiErrorMapper { val logger by getLogger() - fun mapPaymentErrorToApiError(description: String, errorCode: ApiErrorCode, paymentError: PaymentError) : ApiError { - logger.error("{}: {}, paymentError: {}", errorCode, description, asJson(paymentError)) - return when(paymentError) { - is org.ostelco.prime.paymentprocessor.core.PlanAlredyPurchasedError -> ForbiddenError(description, errorCode, paymentError) - is org.ostelco.prime.paymentprocessor.core.ForbiddenError -> ForbiddenError(description, errorCode, paymentError) - // FIXME vihang: remove PaymentError from BadGatewayError - is org.ostelco.prime.paymentprocessor.core.BadGatewayError -> InternalServerError(description, errorCode, paymentError) + /* Log level depends on the type of payment error. */ + fun mapPaymentErrorToApiError(description: String, errorCode: ApiErrorCode, paymentError: PaymentError): ApiError { + if (paymentError is org.ostelco.prime.paymentprocessor.core.CardError) { + logger.warn("{}: {}, paymentError: {}", errorCode, description, asJson(paymentError)) + } else { + logger.error("{}: {}, paymentError: {}", errorCode, description, asJson(paymentError)) + } + return when (paymentError) { + /* Self made. */ + is org.ostelco.prime.paymentprocessor.core.ChargeError -> BadRequestError(description, errorCode, paymentError) + is org.ostelco.prime.paymentprocessor.core.InvoiceError -> BadRequestError(description, errorCode, paymentError) is org.ostelco.prime.paymentprocessor.core.NotFoundError -> NotFoundError(description, errorCode, paymentError) is org.ostelco.prime.paymentprocessor.core.PaymentConfigurationError -> InternalServerError(description, errorCode, paymentError) + is org.ostelco.prime.paymentprocessor.core.PlanAlredyPurchasedError -> ForbiddenError(description, errorCode, paymentError) + is org.ostelco.prime.paymentprocessor.core.StorePurchaseError -> BadRequestError(description, errorCode, paymentError) + is org.ostelco.prime.paymentprocessor.core.SourceError -> BadRequestError(description, errorCode, paymentError) + is org.ostelco.prime.paymentprocessor.core.SubscriptionError -> BadRequestError(description, errorCode, paymentError) + is org.ostelco.prime.paymentprocessor.core.UpdatePurchaseError -> BadRequestError(description, errorCode, paymentError) + /* Caused by upstream payment vendor. */ + is org.ostelco.prime.paymentprocessor.core.CardError -> ForbiddenError(description, errorCode, paymentError) + is org.ostelco.prime.paymentprocessor.core.RateLimitError -> InternalServerError(description, errorCode, paymentError) + is org.ostelco.prime.paymentprocessor.core.AuthenticationError -> BadRequestError(description, errorCode, paymentError) + is org.ostelco.prime.paymentprocessor.core.ApiConnectionError -> InternalServerError(description, errorCode, paymentError) + is org.ostelco.prime.paymentprocessor.core.InvalidRequestError -> ForbiddenError(description, errorCode, paymentError) + is org.ostelco.prime.paymentprocessor.core.PaymentVendorError -> InternalServerError(description, errorCode, paymentError) + is org.ostelco.prime.paymentprocessor.core.GenericError -> InternalServerError(description, errorCode, paymentError) } } diff --git a/prime-modules/src/main/kotlin/org/ostelco/prime/paymentprocessor/core/PaymentError.kt b/prime-modules/src/main/kotlin/org/ostelco/prime/paymentprocessor/core/PaymentError.kt index 34031e8f7..99321e329 100644 --- a/prime-modules/src/main/kotlin/org/ostelco/prime/paymentprocessor/core/PaymentError.kt +++ b/prime-modules/src/main/kotlin/org/ostelco/prime/paymentprocessor/core/PaymentError.kt @@ -2,14 +2,99 @@ package org.ostelco.prime.paymentprocessor.core import org.ostelco.prime.apierror.InternalError -sealed class PaymentError(val description: String, var message : String? = null, val error: InternalError?) : InternalError() +/** + * For specific Stripe error messages a few 'code' fields are included + * which can provide more details about the cause for the error. + * + * - code : Mainly intended for programmatically handling of + * errors but can be useful for giving more context. + * https://stripe.com/docs/error-codes + * - decline code : For card errors resulting from a card + * issuer decline. Not always set. + * https://stripe.com/docs/declines/codes + * - status code : HTTP status code. + * + * The 'codes' fields are included in the error reporting when they are + * present. + * @param description Error description + * @param message Error description as provided upstream (Stripe) + * @param code A short string indicating the type of error from upstream + * @param declineCode A short string indication the reason for a card + * error from the card issuer + * @param internalError Internal error chaining + */ +sealed class PaymentError(val description: String, + val message: String? = null, + val code: String? = null, + val declineCode: String? = null, + val internalError: InternalError?) : InternalError() -class PlanAlredyPurchasedError(description: String, message: String? = null, error: InternalError? = null) : PaymentError(description, message, error) +class ChargeError(description: String, + message: String? = null, + internalError: InternalError? = null) : PaymentError(description, message, null, null, internalError) -class ForbiddenError(description: String, message: String? = null, error: InternalError? = null) : PaymentError(description, message, error) +class InvoiceError(description: String, + message: String? = null, + internalError: InternalError? = null) : PaymentError(description, message, null, null, internalError) -class NotFoundError(description: String, message: String? = null, error: InternalError? = null) : PaymentError(description, message, error) +class NotFoundError(description: String, + message: String? = null, + code: String? = null, + internalError: InternalError? = null) : PaymentError(description, message, code, null, internalError) -class BadGatewayError(description: String, message: String? = null, error: InternalError? = null) : PaymentError(description, message, error) +class PaymentConfigurationError(description: String, + message: String? = null, + internalError: InternalError? = null) : PaymentError(description, message, null, null, internalError) -class PaymentConfigurationError(description: String, message: String? = null, error: InternalError? = null) : PaymentError(description, message, error) +class PlanAlredyPurchasedError(description: String, + message: String? = null, + internalError: InternalError? = null) : PaymentError(description, message, null, null, internalError) + +class StorePurchaseError(description: String, + message: String? = null, + internalError: InternalError? = null) : PaymentError(description, message, null, null, internalError) + +class SourceError(description: String, + message: String? = null, + internalError: InternalError? = null) : PaymentError(description, message, null, null, internalError) + +class SubscriptionError(description: String, + message: String? = null, + internalError: InternalError? = null) : PaymentError(description, message, null, null, internalError) + +class UpdatePurchaseError(description: String, + message: String? = null, + internalError: InternalError? = null) : PaymentError(description, message, null, null, internalError) + +class CardError(description: String, + message: String? = null, + code: String? = null, + declineCode: String? = null, + internalError: InternalError? = null) : PaymentError(description, message, code, declineCode, internalError) + +class RateLimitError(description: String, + message: String? = null, + code: String? = null, + internalError: InternalError? = null) : PaymentError(description, message, code, null, internalError) + +class InvalidRequestError(description: String, + message: String? = null, + code: String? = null, + internalError: InternalError? = null) : PaymentError(description, message, code, null, internalError) + +class AuthenticationError(description: String, + message: String? = null, + code: String? = null, + internalError: InternalError? = null) : PaymentError(description, message, code, null, internalError) + +class ApiConnectionError(description: String, + message: String? = null, + internalError: InternalError? = null) : PaymentError(description, message, null, null, internalError) + +class PaymentVendorError(description: String, + message: String? = null, + internalError: InternalError? = null) : PaymentError(description, message, null, null, internalError) + +class GenericError(description: String, + message: String? = null, + internalError: InternalError? = null) : PaymentError(description, message, null, null, internalError)