https://kotlinlang.org logo
#arrow
Title
# arrow
j

jean

03/23/2023, 11:51 AM
I have the following code. It returns a flow where I emit a payment status while it’s being processed. • I start by requesting a qrCode, customers use it to open a payment app preconfigured with payment recipient and sum. • I then need to poll an api that will answer “not approved” until the customer has pay. • Once the customer has pay, the process is done I need to constraint the polling to an arbitrary timeout value, is there a better/cleaner way to do this?
Copy code
fun processPayment(price: Int) = flow {
    emit(PaymentStatus.Working)
    either {
        val qrCode = getQrCode(price).bind()
        emit(PaymentStatus.ShowQrCode(qrCode.url))
        try {
            withTimeout(twoMinutes()) {
                var paymentIsApproved = false
                while (!paymentIsApproved) {
                    val paymentStatus = getPaymentFromApi(qrCode.reference).bind()
                    paymentIsApproved = paymentStatus.aggregate.authorizedAmount.value == price.toLong()
                    delay(2000)
                }
                emit(PaymentStatus.Success)
            }
        } catch (e: Exception) {
            emit(paymentError(PaymentError.CouldNotVerifyPaymentBeforeTimeout))
        }
    }.mapLeft(::paymentError)
}
a

Alejandro Serrano Mena

03/23/2023, 1:31 PM
Copy code
fun processPayment(price: Int) = flow {
    emit(PaymentStatus.Working)
    either {
        val qrCode = getQrCode(price).bind()
        emit(PaymentStatus.ShowQrCode(qrCode.url))
        Schedule.spaced(2.seconds).whileOutput { it < 2.minutes }.retryOrElse {
          fa = {
             val paymentStatus = getPaymentFromApi(qrCode.reference).bind()
             val paymentIsApproved = paymentStatus.aggregate.authorizedAmount.value == price.toLong()
             if (paymentIsApproved) emit(PaymentStatus.Success)
          },
          orElse = {
             emit(paymentError(PaymentError.CouldNotVerifyPaymentBeforeTimeout))
          }
        }
    }.mapLeft(::paymentError)
}
the code above does not compile, it’s just a sketch; but the idea is that using Schedule you can retry something following a policy (like “every 2 seconds, up to 2 minutes”)
f

Francis Reynders

03/23/2023, 3:26 PM
a bit offtopic, but the
flow
lambda argument type returns
Unit
so I don't think the Either value is returned. Makes me wonder what is the best way to capture the getQrCode logic failure. Maybe define processPayment with Raise context or extension?
j

jean

03/27/2023, 1:02 PM
@simon.vergauwen do you have an opinion on this?
s

simon.vergauwen

03/27/2023, 1:03 PM
I think the code using
Schedule
is indeed nicer instead of manually implementing the
Schedule
, but Francis is also correct in that the `Eitherem`` value is never returned or rather emitted.
j

jean

03/27/2023, 1:05 PM
Shouldn’t I use a
else
branch after
if(paymentApproved)
to throw a
Throwable
? Otherwise it would not retry, right?
And the return type after
retryOrElse
is
Any
not
Either<PaymentError, PaymentStatus>
s

simon.vergauwen

03/27/2023, 1:15 PM
I am a bit confused by the snippet. do
PaymentError
and
PaymentStatus
share a common parent?
j

jean

03/27/2023, 1:22 PM
I guess my naming here wasn't optimal hehe.
PaymentStatus
is a sealed class with
Working
,
Success
and
Error
as sub-elements.
Error
takes a
PaymentError
as parameter. It can be
Timeout
or whatever comes from the
bind()
s

simon.vergauwen

03/27/2023, 1:23 PM
So the return type is
Flow<PaymentStatus>
?
j

jean

03/27/2023, 1:24 PM
Well I only care about the final result. So success or whatever was the reason for it to fail. And I would emit that value int
flow
{ ... } That contains the schedule logic
s

simon.vergauwen

03/27/2023, 1:38 PM
So you only care about the final value? 🤔 So why use
Flow
?
j

jean

03/27/2023, 5:35 PM
I use the flow to emit the status of the payment :
Copy code
flow {
    emit(PaymentStatus.Working) // the process start, I emit "Working", the ui can show a spinner for example
    val result = Schedule... // Here I want to poll my endpoint until I get the expected response, then I emit that final "Success" or "Error" if the IO requests return 4xx, 5xx or if I get a timeout
    emit(result)
}
This does what I want I think: 1. emit “Working” 2. emit “ShowQrCode” 3. emit “Success” when the payment is authorized OR “Error” if anything went wrong or timeout happened
s

simon.vergauwen

03/27/2023, 6:00 PM
I think you can use something like this to not rely on
Throwable
there. So basically repeat polling until you reach
Either.Right
with the desired value.
Copy code
Schedule
  .spaced<Either<VippsError, PaymentStatus>>(twoSeconds)
  .whileOutput { it < twoMinutes }
  .whileInput {
    it.map { it.aggregate.authorizedAmount.value != price.toLong() }
      .getOrElse { true }
  }
  .zipRight(Schedule.identity())
  .repeatOrElseEither(
    fa = { getPayment(qrCode)) },
    orElse = { VippsError.from(throwable).left() }
  ).fold(
    { emit(PaymentStatus.Success) },
    { emit(it) }
  )
j

jean

03/27/2023, 6:40 PM
Nice, thanks for the help! 👏🏻
3 Views