pardakht 0.8.1
pardakht: ^0.8.1 copied to clipboard
One type-safe API across Iranian payment gateways. Pure Dart, explicit currency units, a unified error taxonomy and server-safe verification.
Changelog #
All notable changes to this project are documented here. The format follows Keep a Changelog, and this project adheres to Semantic Versioning.
Unreleased #
Planned #
- Phase 5 hardening. All seven gateways from the original project brief (Zarinpal, Zibal, IDPay, PayPing, Pay.ir, Vandar, IranDargah) are now shipped.
0.8.1 #
Fixes a real bug in VandarGateway, found by checking its documentation
against a second, more current source before publishing.
Fixed #
VandarGatewaywas pointed at Vandar'sv3IPG endpoints, which are stale. The adapter was originally written against a community-maintained documentation mirror dated 2023; checking it against Vandar's own current documentation and a Postman collection dated 2026-02-25 found the real, current API isv4(/api/v4/send,/v4/{token},/api/v4/transaction,/api/v4/verify) — the request and response shapes are unchanged in substance, only the version segment moved. All four endpoints, the contract test, the adapter tests and the fixtures were updated to match.valid_card_numberis now always sent as an array, matching the current documentation; the previous version occasionally sent a bare string.VandarGateway.inquirenow readstrackingCode(falling back torefnumber) asInquiryResult.referenceIdandpaymentDateasInquiryResult.paidAt— both newly confirmed fields on thetransactionendpoint's response.VandarGateway.verifynow readspaymentDateasVerificationResult.paidAttoo.
Notes for integrators #
- If you built against
pardakht0.8.0'sVandarGateway, no code changes are needed on your side — the public API is unchanged. Requests will now reach the correct, current Vandar endpoint instead of a stale one. doc/gateway_specs/vandar.mdnow documents this correction in full, including a field-by-field comparison of what changed betweenv3andv4, and doubles as a worked example of why this project checks a vendor's current site rather than stopping at the first source that answers.doc/gateway_specs/irandargah.mdgained a second pass too, confirming (via IranDargah's own sandbox documentation, including its raw page text) that thev2API it targets is genuinely live — real test card numbers, a real sandbox transaction cap and rate limit — without resolving the adapter's core open question, what a repeatverifycall returns. It remains@experimental. The same pass added a regression test locking in thatIranDargahGateway.createSessiondecides success from the response's booleansuccessfield alone, never fromstatus_code— two accounts of the same sandbox example disagreed on whether a successful/v2/paymentscall reportsstatus_code: 100or200, which is exactly the ambiguity this design choice was already built to be indifferent to.
0.8.0 #
Adds the seventh and final provider adapter from the original project brief.
Added #
IranDargahGatewayandIranDargahCredentials, covering payment session creation and verification, authenticated with a Bearer token — the first gateway in this package to authenticate that way rather than with a body field — and the first to document (and honour) anIdempotency-Keyheader.- IranDargah's documented error-code table, mapped onto
PaymentErrorCode, with its-2validation error classified further by inspecting which field name appears in the response's nestederror.details.validationobject. doc/gateway_specs/irandargah.md, which documents a genuine, unresolved conflict between IranDargah's current documentation and two independent, well-established community SDKs describing an older API — and the single most important open question in the package: what a repeatverifycall actually returns.
Notes for integrators #
IranDargahGatewayis annotated@experimental, and should be treated with more caution than any other adapter in this package. Two independent community SDKs describe a completely different, older IranDargah API — a different domain, a different authentication scheme, and different field names throughout — from the one this adapter targets. The vendor's own current documentation was used, per this project's source ranking, but this project had no IranDargah account to confirm any of it against a live response. Seedoc/gateway_specs/irandargah.md.- There is no confirmed signal for a repeat verification. Every other
gateway in this package maps an explicit "already verified" code to
VerificationStatus.alreadyVerified; IranDargah's documentation gives no worked example of what a secondverifycall actually returns.IranDargahGateway.verifytherefore maps only a clean success toverifiedand treats everything else as a rejection — the safe direction to be wrong in, but genuinely unconfirmed. This is the first thing to resolve before this adapter is trusted with any payment a merchant might verify twice, which in practice is every payment. - IranDargah is sent Rial, with a documented minimum of 100,000 Rial and a maximum of 4,000,000,000 Rial.
direct_verifyis deliberately never sent. IranDargah documents a flag that lets a callback carry a bank reference without a server-sideverifycall — the exact trust model this package's entire design refuses to build, since a callback is an unauthenticated request anyone can forge.- Inquiry and refund are not implemented. IranDargah documents
GETendpoints for reading transaction state, but no response shape for either — only thePOSTendpoints came with worked examples — so this adapter does not guess at one. No refund-creation endpoint is documented at all, only cancellation of a still-pending payment.
0.7.0 #
Adds the sixth provider adapter.
Added #
VandarGatewayandVandarCredentials, covering payment session creation, verification and status inquiry (transaction).- Vandar's shared HTTP-status error table, used to classify
createSessionrejections sincesendcarries no per-field error code of its own, and itsverify-specificstatustable, mapped ontoPaymentErrorCodeandVerificationStatus. doc/gateway_specs/vandar.md, sourced directly from Vandar's own documentation site, which — unlike Pay.ir's — was fully reachable while this adapter was written.
Notes for integrators #
- Vandar is sent Rial. Its documented minimum is 1,000 Rial, with no currency override anywhere in the IPG documentation.
- A repeat verification arrives as
status: 2("already verified"), the same fieldverifyuses for its own success (status: 1) — not a separate error channel the way Pay.ir's-6arrives.VandarGateway.verifychecks for2before treating any otherstatusas a rejection. callback_urland the redirect'sRefererheader must both match a domain registered in the Vandar panel, itself matching the domain registered with Shaparak for the merchant's terminal. This adapter cannot check either locally — the registered domain list is not available through any API it calls — so a mismatch surfaces only as an ordinary rejection after the call is made. Seedoc/gateway_specs/vandar.md.createSession's rejection carries no per-field error code, only a flat array of Persian messages. Every other gateway in this package maps a numeric code; Vandar'ssenddoes not have one, so this adapter classifies the failure by HTTP status instead — the one structured signalsendactually offers — and preserves every message, joined, asgatewayMessage.- Refund is not implemented. Vandar's refund service lives on a
different host, under a different path shape, authenticated with an OAuth
bearer token from a separate flow rather than the single
api_keythis adapter's credentials model.VandarGateway.refundalways throwsUnsupportedGatewayOperationError. - No sandbox credential or header exists for the IPG service. Vandar's
own docs direct a merchant to ask its support team to enrol a business
named "sandbox" — there is no self-service equivalent of Zibal's
zibalmerchant or Pay.ir'sapi: "test", socapabilities.supportsSandboxisfalseand there is noVandarCredentials.test(). - Settlement splitting is not documented for the IPG service. It appears only under Vandar's separate Settlement and Batch Settlement services.
0.6.0 #
Adds the fifth provider adapter.
Added #
PayIrGatewayandPayIrCredentials, covering payment session creation and verification.PayIrCredentials.test()sends the documentedapi: "test"sandbox value.- Pay.ir's documented
errorCodetable, mapped ontoPaymentErrorCode, and its-6duplicate/already-completed signal mapped ontoVerificationStatus.alreadyVerified. doc/gateway_specs/payir.md, reconstructed from a search engine's index of Pay.ir's own documentation page and cross-checked against a well-starred community PHP client, since the page itself could not be reached directly while writing this adapter.
Notes for integrators #
PayIrGatewayis annotated@experimental. Neitherpay.irnordocs.pay.ircould be reached from the environment this adapter was written in — not even the vendor's own site loaded, which is a step further thanIdPayGateway's situation, where only the API host was down. Every fact comes from a search engine's cached index of the vendor's documentation page, cross-checked against a community client. Test it against Pay.ir'sapi: "test"sandbox, from an environment that can actually reachpay.ir, before trusting it with a real payment, then remove the@experimentalannotation and the// TODO(spec):above it.- Pay.ir is sent Rial. Its documented amount range, 10,000 to 500,000,000 Rial, is expressed as a plain integer with no currency override.
- A repeat verification arrives as
errorCode: -6("تراکنش تکراریست یا قبلا انجام شده" — duplicate, or already completed), through the same error channel a genuine failure uses.PayIrGateway.verifychecks for this specific code before treating astatus: 0response as a rejection, the same shape as Zarinpal's101and Zibal's201. - Pay.ir documents no inquiry endpoint distinct from verify, and no
refund or reversal endpoint at all. Both
PayIrGateway.inquireandPayIrGateway.refundalways throwUnsupportedGatewayOperationError. - Settlement splitting is not implemented for Pay.ir. Its split uses a
percentage tied to a mobile number, a different shape from the fixed-Money,
IBAN-addressed
Wagemodel this package uses for Zarinpal and Zibal. Forcing one shape into the other would either lose precision or misrepresent a mobile number as an IBAN, so it is left unimplemented rather than modelled incorrectly; seedoc/gateway_specs/payir.md.
0.5.0 #
Adds the fourth provider adapter.
Added #
PayPingGatewayandPayPingCredentials, covering payment session creation, verification and reversal (full-amount only), with theisReversibleflag passed throughPaymentRequest.metadata.- PayPing's documented error codes and its
metaData.codealready-verified signal, mapped ontoPaymentErrorCodeandVerificationStatusand pinned by tests. doc/gateway_specs/payping.md, sourced from PayPing's own machine-readable OpenAPI document, with one live probe confirming its authentication behaviour.
Notes for integrators #
- PayPing is sent Toman, not Rial. Confirmed by the provider's own OpenAPI document, which states outright that every service uses Toman — this had been the highest-risk open question in the project before the adapter shipped, since it contradicts most other Iranian providers.
- PayPing's reference is two identifiers, not one, and the second does not
exist until after the payer has paid.
createSessionreturns aPaymentSession.referenceholding onlypaymentCode; thepaymentRefIdneeded to verify or reverse arrives solely in the callback. CallverifywithCallbackPayload.reference— a compositepaymentCode:paymentRefIdstring — never withPaymentSession.reference. - A repeat verification arrives as HTTP
409withmetaData.code: 110, not as a distinguished field on a200response the way other providers signal it.PayPingGateway.verifymaps this combination toVerificationStatus.alreadyVerified. verifycan answer HTTP202while genuinely still processing.PayPingGatewayretries this specific status in a loop scoped to verify alone, deliberately separate fromGatewayHttpClient's own retry-on-5xx — wrapping the whole call in a second retry layer using the same policy was tried during development and multiplied the attempt count instead of bounding it, caught immediately by the shared contract suite. Seedoc/adding_a_gateway.md, section 5a, if writing another adapter with a similar "still deciding" response.- PayPing documents no partial refund.
PayPingGateway.refundthrowsUnsupportedGatewayOperationErrorifRefundRequest.amountis set; otherwise it reverses the full payment. - No PayPing merchant account was available during development. Every
response shape beyond the authentication probe comes from documentation
alone; see
doc/gateway_specs/UNVERIFIED.mdfor exactly what remains unconfirmed. UnlikeIdPayGateway, the adapter is not marked@experimental, since the provider was reachable and its documentation is authoritative — but test it against a real account before trusting it with a real payment.
0.4.0 #
Adds the third provider adapter, completing Phase 3.
Added #
IdPayGatewayandIdPayCredentials, covering payment creation, verification and status inquiry, with the payer name/phone/mail hint.- All of IDPay's documented
error_codevalues and its full transactionstatustable, mapped ontoPaymentErrorCodeandVerificationStatusand pinned by tests. doc/gateway_specs/idpay.md, transcribed from IDPay's official API guide.
Notes for integrators #
IdPayGatewayis annotated@experimental. IDPay's API host (api.idpay.ir) returned502 Bad Gatewayon every attempt made during development, so unlikeZarinpalGatewayandZibalGateway— both confirmed against live traffic — nothing about this adapter has been verified against a real response. It is built entirely from IDPay's own documentation, which is thorough, but test it against IDPay's sandbox yourself before trusting it with a real payment. Seedoc/gateway_specs/idpay.md.- IDPay's sandbox is a header, not a host or a credential.
GatewayOptions.sandboxsendsX-SANDBOX: 1alongside the same API key used in production, unlike Zarinpal's separate host or Zibal's dedicated test merchant. - A paid transaction must be verified within ten minutes, or IDPay refunds the payer automatically — a stricter and more consequential deadline than either Zarinpal or Zibal impose.
- IDPay's callback can arrive as either a GET query string or a POST
body, configured per web service in the IDPay dashboard.
parseCallbackaccepts a plain map either way, so both work without any extra handling. - Double-spend detection is explicitly the merchant's own responsibility,
per IDPay's documentation — store
idandtrack_idand enforce their uniqueness yourself. - IDPay documents no refund or reversal endpoint.
IdPayGateway.refundalways throwsUnsupportedGatewayOperationError.
0.3.0 #
Adds the second provider adapter.
Added #
ZibalGatewayandZibalCredentials, covering payment request, verification and status inquiry, with settlement splitting, card restriction and the payer mobile hint.ZibalCredentials.test()exposes Zibal's permanent test account.- All of Zibal's documented
resultcodes across the request, verify and inquiry endpoints, and its full transactionstatustable, mapped ontoPaymentErrorCodeandVerificationStatusand pinned by tests. example/zibal_sandbox.dart, a real end-to-end payment against Zibal's test account.- Response fixtures under
test/fixtures/zibal/, including bodies captured from the live test account. doc/gateway_specs/zibal.md, sourced from Zibal's own machine-readable OpenAPI document plus a live capture, each fact dated and cited.
Notes for integrators #
- Zibal splits "did the call succeed" from "what state is the payment in"
across two fields,
resultandstatus, and they are not interchangeable. Aninquirecall answersresult: 100— the report was produced — even for a transaction that was never paid; onlystatussays what actually happened.ZibalGateway.verifydecides its outcome entirely fromresult;ZibalGateway.inquiredecides its outcome entirely fromstatus. Treating either field as authoritative for the other operation is the most common Zibal integration bug, and this package's contract test suite pins the distinction so it cannot regress. - Zibal has no separate sandbox host. The literal merchant value
zibalis what selects the test environment, on the samegateway.zibal.irused in production. - Zibal documents no refund or reversal endpoint.
ZibalGateway.refundalways throwsUnsupportedGatewayOperationError.
0.2.0 #
Adds the first provider adapter.
Added #
ZarinpalGatewayandZarinpalCredentials, covering payment request, verification, status inquiry and reversal, with settlement splitting (تسهیم), card-restricted payment, the payer mobile hint and the sandbox environment.- All 33 documented Zarinpal result codes, mapped onto
PaymentErrorCodeand pinned by tests. An unmapped code falls back tounknownand is logged rather than throwing. GatewayCapabilities.requiresDescription, because Zarinpal rejects a request without one and the adapter will not invent text shown to a payer.example/zarinpal_sandbox.dart, a real end-to-end payment against Zarinpal's sandbox that needs no merchant account.- Response fixtures under
test/fixtures/zarinpal/, including bodies captured from the live sandbox with the HTTP status codes Zarinpal actually used. doc/gateway_specs/zarinpal.md, recording every endpoint, the complete result-code table and the currency unit, each with a source URL and a retrieval date.
Notes for integrators #
- Zarinpal is always sent Rial. Its request endpoint accepts a
currencyfield ofIRRorIRT, but its verify endpoint does not, and the amount there is documented as Rial unconditionally. Requesting in Toman and verifying in Rial produces error-50after the payer has been charged, so the adapter never sendscurrency. Callers keep passing whateverMoneyunit they think in. - Zarinpal answers 4xx with a result code in the body, including
401for an unpaid session and422for a rejected amount. These reach the adapter rather than being thrown as transport failures. - Zarinpal's reverse endpoint takes no amount, so refunds are all-or-nothing and only within 30 minutes of payment.
0.1.0 #
First release. The core is complete; no provider adapter has shipped yet.
Added #
Money, an immutable amount that carries its currency unit. Zero and negative amounts are rejected at construction, conversions between Rial and Toman are exact or throw, and every operation is checked for overflow.PaymentGateway, the provider-independent interface, together withPaymentRequest,PaymentSession,VerificationRequest,VerificationResult,InquiryRequest,InquiryResult,RefundRequest,RefundResult,PayerInfo,WageandCallbackPayload.VerificationStatus.alreadyVerifiedas a distinct success state, so a repeat verification cannot be mistaken for a failure.- A sealed
PaymentExceptionhierarchy —GatewayRejectedException,NetworkPaymentException,TimeoutPaymentException,MalformedResponseExceptionandConfigurationException— with the normalisedPaymentErrorCodeenum. Provider codes and messages are preserved verbatim. UnsupportedGatewayOperationErrorand theUnsupportedOperationsmixin, so optional operations report their absence rather than returning null.GatewayCapabilities, for branching on what a provider supports instead of type-checking concrete gateway classes.HttpClientAdapterwith apackage:httpdefault, so every network call is injectable and no test touches the network.RetryPolicyandRetryRunner, with exponential backoff and jitter applied only to idempotent operations.createSessionandrefundare never retried automatically.PaymentLoggerwith a silent default, andRedactor, which strips registered credentials by exact value and masks card numbers, national identifiers, phone numbers, IBANs and authorization headers by pattern.GatewayCredentials, whosetoStringcannot be overridden to disclose a key.GatewayHttpClient, the shared plumbing that gives every adapter the same timeout, retry, redaction and error-translation behaviour.GatewayRegistry, for choosing a provider at runtime.PaymentMessageResolverwith English and Persian message maps.RemotePaymentGatewayand thePaymentGateway.remotefactory, implementing proxy mode for clients that must not hold merchant credentials, with a documented backend contract and a working reference backend inexample/backend/.- A shared contract test suite under
test/contract/, which every adapter must pass.