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§
Sourcefn id(&self) -> ProviderId
fn id(&self) -> ProviderId
Which provider this is.
Sourcefn 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 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.
Sourcefn 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 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.
Sourcefn 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 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.
Sourcefn 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 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.
Sourcefn 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 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.
Sourcefn 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 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: None | its own idempotency | |
|---|---|---|
| Stripe | refunds what is left, in one call | Idempotency-Key |
iyzico classic | ErrorKind::InvalidRequest: send the amount | none — a key is refused |
iyzico in_store | refunds all of it, in one call | none — a key is refused |
| PayTR | ErrorKind::InvalidRequest: send the amount | none — a key is refused |
| Mollie | reads the payment’s amountRemaining first, so two calls | Idempotency-Key |
| PayPal | refunds what is left, and reads the order first to find the capture, so two calls | PayPal-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.
Sourcefn 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 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. ReadCharge::status; sending the charge again would open a second one.Err(_)— the question could not be answered. Not the same asOk(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 classic | yes — reporting reads a payment back by the conversationId it was made with |
| PayTR | yes — its status query is keyed by merchant_oid, which is the reference itself |
| Stripe | no, 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 |
| Mollie | no — 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 |
| PayPal | no — Orders v2 has no lookup by PayPal-Request-Id or custom_id. Replaying with the same request id answers the original order |
iyzico in_store | no — 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.
Sourcefn 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 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.
Sourcefn capabilities(&self) -> Capabilities
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".