Skip to content

Migration Guide

Amir Iranmanesh edited this page Jul 31, 2026 · 1 revision

Migration Guide

Most Iranian Go services grew their own IPG layer with a GetToken / Confirm / Reverse interface and a switch over gateway constants. Moving that onto Payvand is mechanical, and it can be done one gateway at a time.

The mapping

Your old code Payvand
NewIPG(cfg, db, gateway, terminalInfo) pv.Gateway(name, payvand.Config{…})
GetToken(ctx, GetTokenReq{…}) Purchase(ctx, payvand.PurchaseRequest{…})
res.PaymentToken res.Token
res.URL res.Redirect (.String(), .Send(w, r))
Confirm(ctx, ConfirmReq{…}) Verify(ctx, payvand.VerifyRequest{…})
res.FinalReferenceNumber res.ReferenceNumber
res.CardNumber res.CardNumber
Reverse(ctx, ReverseReq{…}) Refund(ctx, payvand.RefundRequest{…})
hand-parsed callback query/form ParseCallback(r) + Callback.VerifyRequest(amount)
TerminalInfo{Username, Password, TerminalID, MerchantID, MerchantKey, IBAN} payvand.Config — same fields, same meaning
amount int64 in Rial payvand.Rial(amount)
switch gateway { case ZIBAL: … } the registry: the name is data
"this gateway cannot reverse" scattered in the code gw.Capabilities().Refund

Step 1 — an adapter

Keep every call site untouched by wrapping Payvand in your existing interface:

type ipgAdapter struct{ gw payvand.Gateway }

func (a ipgAdapter) GetToken(ctx context.Context, req dto.GetTokenReq) (dto.GetTokenRes, error) {
	res, err := a.gw.Purchase(ctx, payvand.PurchaseRequest{
		Amount:      payvand.Rial(req.Amount),
		OrderID:     req.OrderID,
		CallbackURL: req.CallbackUrl,
		Mobile:      req.Mobile,
		NationalID:  req.NationalId,
	})
	if err != nil {
		return dto.GetTokenRes{}, err
	}
	return dto.GetTokenRes{PaymentToken: res.Token, URL: res.Redirect.String()}, nil
}

func (a ipgAdapter) Confirm(ctx context.Context, req dto.ConfirmReq) (dto.ConfirmRes, error) {
	res, err := a.gw.Verify(ctx, payvand.VerifyRequest{
		Token:           req.PaymentToken,
		Amount:          payvand.Rial(req.Amount),
		ReferenceNumber: req.ReferenceNumber,
		TraceNumber:     req.TraceNumber,
		CardNumber:      req.CardNumber,
	})
	if err != nil {
		return dto.ConfirmRes{}, err
	}
	return dto.ConfirmRes{
		Amount:               res.Amount.Rial(),
		FinalReferenceNumber: res.ReferenceNumber,
		CardNumber:           res.CardNumber,
	}, nil
}

func (a ipgAdapter) Reverse(ctx context.Context, req dto.ReverseReq) (dto.ReverseRes, error) {
	_, err := a.gw.Refund(ctx, payvand.RefundRequest{
		Token:           req.PaymentToken,
		ReferenceNumber: req.FinalReferenceNumber,
		Amount:          payvand.Rial(req.Amount),
	})
	return dto.ReverseRes{}, err
}

Your factory becomes one line:

func NewIPG(gateway string, terminal TerminalInfo) (IPG, error) {
	gw, err := payvand.New(payvand.Name(gateway), payvand.Config{
		Username:    terminal.Username,
		Password:    terminal.Password,
		TerminalID:  terminal.TerminalID,
		MerchantID:  terminal.MerchantID,
		MerchantKey: terminal.MerchantKey,
		IBAN:        terminal.IBAN,
	})
	if err != nil {
		return nil, err
	}
	return ipgAdapter{gw: gw}, nil
}

At this point the old per-gateway files can be deleted.

Step 2 — migrate the callback

The hand-written callback parsing is usually the messiest part, and it is the one Payvand shortens the most:

cb, err := gw.ParseCallback(r)
if err != nil || !cb.Succeeded {
	return errPaymentCanceled
}
order := orders.ByToken(cb.Token)
verified, err := gw.Verify(ctx, cb.VerifyRequest(order.Amount))

Note what disappears: per-gateway field names, casing fallbacks, and the manual copying of reference and trace numbers into the confirm request.

Step 3 — drop the adapter

Once the callback path uses Payvand types, change the call sites to payvand.Gateway directly and delete the adapter. Order matters less than finishing: the adapter can live for a release or two without harm.

Behaviour changes to expect

Change Why it matters
Amounts are typed payvand.Rial(x) vs payvand.Toman(x); the gateway converts, so double conversions in your own code must go
Credentials are validated in New a broken terminal now fails at start-up instead of at payment time
Settlement is inside Verify if you called Mellat's settle or AsanPardakht's settlement yourself, remove it
Unsupported operations return ErrNotSupported replace "this gateway does not support reverse" branches with Capabilities().Refund
Repeated verification returns ErrAlreadyVerified treat it as success rather than as a failure
Redirects can be POST forms replace a bare http.Redirect with purchase.Redirect.Send(w, r)
Pasargad needs the invoice date store PurchaseResponse.Extra[pasargad.InvoiceDateKey] with the order

Verifying the move

  1. Point staging at payvand.Virtual and run your existing payment tests.
  2. Re-run them against a fake provider with payvand.WithBaseURL, replaying the payloads you captured from the real gateway.
  3. Move one low-traffic terminal to production and compare reference numbers with the provider panel for a day.
  4. Migrate the rest.

Next: Extending · Testing

Clone this wiki locally