Skip to main content

Provider

Trait Provider 

Source
pub trait Provider:
    Debug
    + Send
    + Sync {
    // Required methods
    fn id(&self) -> ProviderId;
    fn charge<'life0, 'life1, 'async_trait>(
        &'life0 self,
        request: &'life1 ChargeRequest,
    ) -> Pin<Box<dyn Future<Output = Result<Charge, Error>> + Send + 'async_trait>>
       where Self: 'async_trait,
             'life0: 'async_trait,
             'life1: 'async_trait;
    fn resume<'life0, 'life1, 'async_trait>(
        &'life0 self,
        continuation: &'life1 str,
    ) -> Pin<Box<dyn Future<Output = Result<Charge, Error>> + Send + 'async_trait>>
       where Self: 'async_trait,
             'life0: 'async_trait,
             'life1: 'async_trait;
    fn charge_status<'life0, 'life1, 'async_trait>(
        &'life0 self,
        id: &'life1 PaymentId,
    ) -> Pin<Box<dyn Future<Output = Result<Charge, Error>> + Send + 'async_trait>>
       where Self: 'async_trait,
             'life0: 'async_trait,
             'life1: 'async_trait;
    fn capture<'life0, 'life1, 'life2, 'async_trait>(
        &'life0 self,
        id: &'life1 PaymentId,
        amount: Option<Money>,
        idempotency: Option<&'life2 IdempotencyKey>,
    ) -> Pin<Box<dyn Future<Output = Result<Charge, Error>> + Send + 'async_trait>>
       where Self: 'async_trait,
             'life0: 'async_trait,
             'life1: 'async_trait,
             'life2: 'async_trait;
    fn cancel<'life0, 'life1, 'async_trait>(
        &'life0 self,
        id: &'life1 PaymentId,
    ) -> Pin<Box<dyn Future<Output = Result<Charge, Error>> + Send + 'async_trait>>
       where Self: 'async_trait,
             'life0: 'async_trait,
             'life1: 'async_trait;
    fn refund<'life0, 'life1, 'async_trait>(
        &'life0 self,
        request: &'life1 RefundRequest,
    ) -> Pin<Box<dyn Future<Output = Result<Refund, Error>> + Send + 'async_trait>>
       where Self: 'async_trait,
             'life0: 'async_trait,
             'life1: 'async_trait;
    fn lookup<'life0, 'life1, 'async_trait>(
        &'life0 self,
        order: &'life1 OrderRef,
    ) -> Pin<Box<dyn Future<Output = Result<Option<Charge>, Error>> + Send + 'async_trait>>
       where Self: 'async_trait,
             'life0: 'async_trait,
             'life1: 'async_trait;
    fn instruments<'life0, 'life1, 'async_trait>(
        &'life0 self,
        customer: &'life1 str,
    ) -> Pin<Box<dyn Future<Output = Result<Vec<Instrument>, Error>> + Send + 'async_trait>>
       where Self: 'async_trait,
             'life0: 'async_trait,
             'life1: 'async_trait;
    fn capabilities(&self) -> Capabilities;
}
Expand description

Takes a payment and reports on it.

Implementations are cheap to clone and safe to share: hold one per process, not one per request.

Required Methods§

Source

fn id(&self) -> ProviderId

Which provider this is.

Source

fn charge<'life0, 'life1, 'async_trait>( &'life0 self, request: &'life1 ChargeRequest, ) -> Pin<Box<dyn Future<Output = Result<Charge, Error>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait,

Starts a charge.

A returned Charge is not a completed payment. Read its status and its next_action: a provider that redirects the payer answers Status::RequiresAction here, and the payment is only decided once they come back.

§A request that satisfies one provider may not satisfy another

Everything past the order reference and the amount is optional on ChargeRequest, because what is mandatory is the provider’s decision rather than a payment’s. iyzico’s classic API refuses a payment without a buyer’s identity number, an address and an itemised basket; PayTR refuses one without the payer’s own IP address; Stripe and Mollie ask for none of it.

An adapter that is not given a field it needs answers ErrorKind::InvalidRequest naming the field, before a socket opens. So the request that works everywhere is the one carrying what the strictest provider asks, and swapping to a laxer one costs nothing: the extra fields are ignored.

Source

fn resume<'life0, 'life1, 'async_trait>( &'life0 self, continuation: &'life1 str, ) -> Pin<Box<dyn Future<Output = Result<Charge, Error>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait,

Finishes a flow Provider::charge started, by the token it handed over.

§The one call that needs it

A hosted form the payer has not finished is not a payment: the provider has nothing to name it by, so Charge::id is None and Provider::charge_status has nothing to take. What it does have is the continuation on NextAction::Redirect, and this is the call that takes one.

§How a caller decides which to use, without naming a provider

Capabilities::resume_by_continuation. True is a provider that names the payment only once the payer is done, so the continuation is the only handle there is and this is what reads it. False is a provider that named the payment when it opened the flow, so Provider::charge_status finishes it and this answers ErrorKind::Unsupported saying so.

§It finishes what charge started, and nothing else

Not every flow an adapter can open comes back through here. iyzico’s classic API has one result endpoint for a form that takes the money and a form that holds it, and its answer does not say which was opened — so reading a hold back through the wrong one writes a sale into a ledger for money nobody has taken. Provider::charge opens the form that takes the money, this reads that one back, and a hold opened by the adapter’s own call is read back by the adapter’s own call.

Source

fn charge_status<'life0, 'life1, 'async_trait>( &'life0 self, id: &'life1 PaymentId, ) -> Pin<Box<dyn Future<Output = Result<Charge, Error>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait,

Reads a charge back.

id is a Charge::id this provider produced. A provider that names a payment by nothing at all — no identifier of its own and nothing to compose one from — answers ErrorKind::Unsupported rather than accepting an identifier it cannot honour.

A flow that is not yet a payment is not read here at all. iyzico’s classic checkout form has only its own token until the payer finishes, and that token is a different IdKind, so it has its own call rather than a signature this one cannot honestly take.

Source

fn capture<'life0, 'life1, 'life2, 'async_trait>( &'life0 self, id: &'life1 PaymentId, amount: Option<Money>, idempotency: Option<&'life2 IdempotencyKey>, ) -> Pin<Box<dyn Future<Output = Result<Charge, Error>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait,

Takes funds an authorisation is only holding.

A shop authorises when the order is placed and captures when the parcel leaves. amount of None takes the lot; Some takes part of it, which is what a partial shipment needs, and requires Capabilities::partial_capture.

The returned Charge carries the amount that was captured, not the amount that was authorised.

Capture has no inverse. Captured money is refunded, not un-captured.

A provider whose Capabilities::separate_capture is false took the money at authorisation and answers ErrorKind::Unsupported here.

idempotency makes a replayed capture safe where the provider offers it — read ErrorKind::is_retryable before retrying one without a key: unlike Provider::charge, a repeated capture can take the same money twice, and not every provider protects against it.

A provider that cannot honour a key refuses the capture with ErrorKind::Unsupported rather than sending it without one. That is the same rule ChargeRequest::idempotency_key and RefundRequest::idempotency_key state, and it is at its sharpest here: a capture is the call that takes the money, so a key accepted and dropped reads as a guarantee against taking it twice where there is none. iyzico’s classic API is the one that refuses; a provider with no capture step at all answers Unsupported for the capture itself and never reaches the question.

The refusal comes before the request, not after it. A key that is discovered to be unusable only once the capture has been sent has already taken the money.

Source

fn cancel<'life0, 'life1, 'async_trait>( &'life0 self, id: &'life1 PaymentId, ) -> Pin<Box<dyn Future<Output = Result<Charge, Error>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait,

Releases an authorisation that will never be taken.

Cancelling a payment whose funds are already captured is ErrorKind::InvalidRequest rather than a silent success: giving that money back is a refund, a different act with a different entry in the ledger.

No idempotency key: repeating a cancel is harmless. The second call meets a hold that is already released and answers ErrorKind::InvalidRequest rather than releasing anything twice, which is the whole reason Provider::capture carries a key and this does not.

Source

fn refund<'life0, 'life1, 'async_trait>( &'life0 self, request: &'life1 RefundRequest, ) -> Pin<Box<dyn Future<Output = Result<Refund, Error>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait,

Gives money back off a payment.

Capture has no inverse — captured money is refunded, not un-captured — so this is the only way money goes back, and a Provider offering capture and not this is one a shop cannot use.

Three refunds against one payment is ordinary: three returned items on one order. Whether this provider allows that is Capabilities::repeated_refund, and whether it allows one for less than was captured is Capabilities::partial_refund; both are answerable before there is a payment to ask about.

§amount: None is not one call everywhere

None means all of it, and two providers have no request that says so — they take an amount and only an amount. What each adapter does:

amount: Noneits own idempotency
Striperefunds what is left, in one callIdempotency-Key
iyzico classicErrorKind::InvalidRequest: send the amountnone — a key is refused
iyzico in_storerefunds all of it, in one callnone — a key is refused
PayTRErrorKind::InvalidRequest: send the amountnone — a key is refused
Molliereads the payment’s amountRemaining first, so two callsIdempotency-Key
PayPalrefunds what is left, and reads the order first to find the capture, so two callsPayPal-Request-Id

A provider that cannot honour RefundRequest::idempotency_key refuses the refund with ErrorKind::Unsupported rather than sending it without one. That is ChargeRequest::idempotency_key’s own rule, and it matters more here: accepting a key and dropping it reads as a guarantee against giving the money back twice, which is the one thing the caller asked for.

A refund that cannot be replayed safely is read back, not resent. Each adapter has a call that lists what has already gone back — Stripe::refunds, PayTr::refunds, Mollie’s amountRefunded — and reading is always safe.

Source

fn lookup<'life0, 'life1, 'async_trait>( &'life0 self, order: &'life1 OrderRef, ) -> Pin<Box<dyn Future<Output = Result<Option<Charge>, Error>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait,

Asks what became of a request the caller sent under this reference.

For the call whose answer never arrived. A charge that times out is the one case where nobody knows whether the money moved: the request may have been received and acted on, and the reply lost on the way back. Calling Provider::charge again is only safe where the provider honours an idempotency key, and Provider::charge_status cannot be used either — it takes the provider’s own identifier for the payment, which is precisely what a lost reply never delivered.

So this is keyed by ChargeRequest::order, the caller’s own reference, which they had before they sent anything.

  • Ok(None) — the provider has no record of a payment under this reference. Nothing was taken, and sending the charge again is safe.
  • Ok(Some(charge)) — this is what became of it. Read Charge::status; sending the charge again would open a second one.
  • Err(_) — the question could not be answered. Not the same as Ok(None), and the difference is a double payment.
§Two of the five can answer it

Capabilities::lookup_by_order says which before there is a request to ask about, and the four answers are different questions:

iyzico classicyes — reporting reads a payment back by the conversationId it was made with
PayTRyes — its status query is keyed by merchant_oid, which is the reference itself
Stripeno, on purpose — the search API is the only way to find an intent by metadata, and Stripe documents it as eventually consistent and says not to use it in read-after-write flows. Retry the charge with the same ChargeRequest::idempotency_key instead: Stripe answers the original PaymentIntent rather than opening a second
Mollieno — nothing finds a payment by its metadata. Its Idempotency-Key replays the first answer for an hour, which covers the same case for as long as it lasts
PayPalno — Orders v2 has no lookup by PayPal-Request-Id or custom_id. Replaying with the same request id answers the original order
iyzico in_storeno — its query takes iyzico’s own paymentId and nothing else

A false here is not a gap to work around with a search that might be stale. An answer of “no record” that is merely late is how a caller authorises twice, which is the failure this method exists to prevent.

Source

fn instruments<'life0, 'life1, 'async_trait>( &'life0 self, customer: &'life1 str, ) -> Pin<Box<dyn Future<Output = Result<Vec<Instrument>, Error>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait,

Lists what a customer has saved with this provider.

customer is the provider’s own name for them — the same string ChargeRequest::customer carries, and, for iyzico’s classic API, the cardUserKey that names the vault rather than a payer as such.

This is the shape every provider can answer: an identity and something to show somebody choosing between them. It is not a card number and carries no field one could go in. What it is not, on purpose, is a way to charge one or to forget one — those stay each adapter’s own call, because forgetting a card needs iyzico’s cardUserKey and its token where Stripe’s needs only the instrument, and charging one takes a buyer and a basket at iyzico, an off_session flag at Stripe, a sequenceType at Mollie — three requests this trait cannot honestly narrow to one signature. See Capabilities::saved_instruments for what that leaves this trait able to say about charging one.

A provider with no vault at all — or one this crate has no working call against, which is PayTR’s case: it does store a card, but nothing here signs a request against it — answers ErrorKind::Unsupported rather than an empty list, because an empty list would read as “this customer has nothing saved” instead of “asking is not possible here”.

No default: a provider outside this workspace has to answer, the same as every other method here.

Source

fn capabilities(&self) -> Capabilities

What this provider will do, before there is a payment to ask about.

Dyn Compatibility§

This trait is dyn compatible.

In older versions of Rust, dyn compatibility was called "object safety".

Implementors§