Skip to content

Commit

Permalink
Cleanup and refactoring of Stripe error collection and reporting
Browse files Browse the repository at this point in the history
  • Loading branch information
Kjell M. Myksvoll committed Nov 29, 2019
1 parent 733f190 commit 87c9fab
Show file tree
Hide file tree
Showing 8 changed files with 235 additions and 99 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,16 +85,18 @@ 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
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
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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. */
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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()
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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

Expand All @@ -1607,20 +1613,20 @@ 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()

/* 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())
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -2745,16 +2751,19 @@ object Neo4jStoreSingleton : GraphStore {
// For refunds
//

private fun checkPurchaseRecordForRefund(purchaseRecord: PurchaseRecord): Either<PaymentError, Unit> {
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<PaymentError, Unit> =
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,
Expand All @@ -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()
Expand All @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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()
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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<PaymentError, String> {
val errorMessage = "Failed to authorize the charge for customerId $customerId sourceId $sourceId amount $amount currency $currency"
return when (amount) {
Expand All @@ -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<PaymentError, String> {
val errorMessage = "Failed to capture charge for customerId $customerId chargeId $chargeId"
return when (amount) {
Expand All @@ -340,15 +343,15 @@ 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 {
charge.capture()
charge.id.right()
} catch (e: Exception) {
logger.warn(errorMessage, e)
BadGatewayError(errorMessage).left()
ChargeError(errorMessage).left()
}
}
}
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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()
Expand Down
Loading

0 comments on commit 87c9fab

Please sign in to comment.