// Package zarinpal is a thin client for ZarinPal Payment Gateway v4. // Ported from the proven implementation in the identity service // (services/identity/.../PaymentService.cs). // // Flow: // request.json → { authority } → redirect user to StartPay/{authority} // user pays, ZarinPal calls back → verify.json → { code, ref_id } // code 100 = success, 101 = already verified (idempotent). package zarinpal import ( "bytes" "context" "encoding/json" "fmt" "net/http" "time" ) const ( prodAPI = "https://api.zarinpal.com/pg/v4/payment" prodStart = "https://www.zarinpal.com/pg/StartPay/" sandboxAPI = "https://sandbox.zarinpal.com/pg/v4/payment" sandStart = "https://sandbox.zarinpal.com/pg/StartPay/" ) type Client struct { http *http.Client } func New() *Client { return &Client{http: &http.Client{Timeout: 20 * time.Second}} } // RequestResult is the outcome of a payment request. type RequestResult struct { Authority string StartPay string Code int Raw json.RawMessage } // VerifyResult is the outcome of a verify call. type VerifyResult struct { Code int RefID string CardPan string Fee int64 Raw json.RawMessage } func apiBase(sandbox bool) (string, string) { if sandbox { return sandboxAPI, sandStart } return prodAPI, prodStart } // Request creates a ZarinPal payment authority. amount is in the unit the merchant // expects (caller converts Rial↔Toman per config). func (c *Client) Request(ctx context.Context, sandbox bool, merchantID string, amount int64, callbackURL, description string, metadata map[string]string) (*RequestResult, error) { base, start := apiBase(sandbox) body := map[string]any{ "merchant_id": merchantID, "amount": amount, "callback_url": callbackURL, "description": description, } if len(metadata) > 0 { body["metadata"] = metadata } root, raw, err := c.post(ctx, base+"/request.json", body) if err != nil { return nil, err } data, ok := root["data"].(map[string]any) if !ok { return nil, fmt.Errorf("zarinpal request: missing data (errors=%v)", root["errors"]) } code := toInt(data["code"]) if code != 100 { return &RequestResult{Code: code, Raw: raw}, fmt.Errorf("zarinpal request failed (code=%d): %v", code, root["errors"]) } authority, _ := data["authority"].(string) return &RequestResult{Authority: authority, StartPay: start + authority, Code: code, Raw: raw}, nil } // Verify confirms a payment by authority. Codes 100/101 mean success. func (c *Client) Verify(ctx context.Context, sandbox bool, merchantID string, amount int64, authority string) (*VerifyResult, error) { base, _ := apiBase(sandbox) body := map[string]any{ "merchant_id": merchantID, "amount": amount, "authority": authority, } root, raw, err := c.post(ctx, base+"/verify.json", body) if err != nil { return nil, err } data, ok := root["data"].(map[string]any) if !ok { return &VerifyResult{Code: 0, Raw: raw}, fmt.Errorf("zarinpal verify: missing data (errors=%v)", root["errors"]) } res := &VerifyResult{Code: toInt(data["code"]), Raw: raw} if ref, ok := data["ref_id"]; ok { res.RefID = fmt.Sprintf("%v", toInt64(ref)) } if pan, ok := data["card_pan"].(string); ok { res.CardPan = pan } if fee, ok := data["fee"]; ok { res.Fee = toInt64(fee) } return res, nil } func (c *Client) post(ctx context.Context, url string, body map[string]any) (map[string]any, json.RawMessage, error) { buf, _ := json.Marshal(body) req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(buf)) if err != nil { return nil, nil, err } req.Header.Set("Content-Type", "application/json") req.Header.Set("Accept", "application/json") resp, err := c.http.Do(req) if err != nil { return nil, nil, err } defer resp.Body.Close() var raw json.RawMessage dec := json.NewDecoder(resp.Body) if err := dec.Decode(&raw); err != nil { return nil, nil, fmt.Errorf("zarinpal: decode response: %w", err) } var root map[string]any if err := json.Unmarshal(raw, &root); err != nil { return nil, raw, fmt.Errorf("zarinpal: parse response: %w", err) } return root, raw, nil } func toInt(v any) int { switch n := v.(type) { case float64: return int(n) case int: return n case json.Number: i, _ := n.Int64() return int(i) } return 0 } func toInt64(v any) int64 { switch n := v.(type) { case float64: return int64(n) case int64: return n case int: return int64(n) case json.Number: i, _ := n.Int64() return i } return 0 }