# راهنمای اتصال به درگاه پرداخت پارسیان

## فهرست APIها و تعاملات این سند

| کاربرد | نوع | Method | URL / Operation |
|---|---|---|---|
| دریافت توکن پرداخت | SOAP | `POST` | `${PARSIAN_SALE_BASE_URL}.asmx` — `SalePaymentRequest` |
| هدایت کاربر به صفحه پرداخت | Redirect | `GET` | `${PARSIAN_GATEWAY_URL}?token=${PARSIAN_PAYMENT_TOKEN}` |
| دریافت نتیجه پرداخت | Callback ورودی | `POST` | `${PARSIAN_CALLBACK_URL}` |
| تأیید نهایی پرداخت | SOAP | `POST` | `${PARSIAN_CONFIRM_BASE_URL}.asmx` — `ConfirmPayment` |

## فرآیند کلی پرداخت

اتصال به درگاه پارسیان در چهار مرحله انجام می‌شود:

1. با سرویس SOAP به نام `SalePaymentRequest` یک توکن پرداخت دریافت می‌کنید.
2. کاربر را همراه توکن به صفحه درگاه پارسیان هدایت می‌کنید.
3. پارسیان نتیجه پرداخت را به `Callback URL` شما ارسال می‌کند.
4. اگر Callback موفق و معتبر بود، سرویس SOAP به نام `ConfirmPayment` را
   فراخوانی می‌کنید. پرداخت فقط وقتی موفق است که پاسخ این سرویس نیز
   `Status = 0` باشد.

## کلیدهای لازم در `.env`

```dotenv
# آدرس صفحه درگاه پارسیان
PARSIAN_GATEWAY_URL=

# آدرس پایه سرویس Sale، بدون .asmx و ?wsdl
PARSIAN_SALE_BASE_URL=

# آدرس پایه سرویس Confirm، بدون .asmx و ?wsdl
PARSIAN_CONFIRM_BASE_URL=

# آدرس عمومی HTTPS برای دریافت نتیجه پرداخت از پارسیان
PARSIAN_CALLBACK_URL=

# اطلاعات پذیرنده OMID
PARSIAN_OMID_LOGIN=
PARSIAN_OMID_TERMINAL=

# اطلاعات پذیرنده GANJINEH
PARSIAN_GANJINEH_LOGIN=
PARSIAN_GANJINEH_TERMINAL=

# اطلاعات پذیرنده NAVID
PARSIAN_NAVID_LOGIN=
PARSIAN_NAVID_TERMINAL=

# اطلاعات لازم برای ساخت AdditionalData
MANA_KEY_B64=
MANA_IV_B64=
MANA_THIRD_PARTY_CODE=
```

نکات:

- مقادیر `PARSIAN_*_LOGIN` و `PARSIAN_*_TERMINAL` اطلاعات محرمانه هر
  پذیرنده هستند.
- برای هر پرداخت باید Login و Terminal مربوط به یک پذیرنده انتخاب شوند.
- کلید و IV مانا پس از Decode شدن از Base64 باید دقیقاً ۱۶ بایت باشند.
- اطلاعات محرمانه نباید به فرانت‌اند ارسال شوند.

متغیرهایی مانند `ORDER_ID`، `AMOUNT_RIAL`، `PARSIAN_PAYMENT_TOKEN` و `RRN`
کلید `.env` نیستند؛ این مقادیر هنگام اجرای هر پرداخت ساخته یا از پاسخ پارسیان
دریافت می‌شوند.

## مرحله ۱: ساخت `AdditionalData`

مقدار `AdditionalData` در درخواست Sale یک رشته JSON با ساختار زیر است:

```json
{
  "NationalEncryptedId": "ENCRYPTED_IDENTIFIER_BASE64",
  "ThirdPartyCode": "MANA_THIRD_PARTY_CODE",
  "Data": ""
}
```

مقدار `NationalEncryptedId` به این صورت ساخته می‌شود:

```text
Plain text:
PERSONALITY|NATIONAL_IDENTIFIER|ORDER_ID

Encryption:
AES-128-CBC

Key:
Base64 decode of MANA_KEY_B64

IV:
Base64 decode of MANA_IV_B64

Output:
Base64
```

مقدار `PERSONALITY`:

- شخص حقیقی: `0`
- شخص حقوقی: `1`

رشته JSON نهایی باید XML Escape شود و داخل تگ `AdditionalData` قرار گیرد.

### کد JavaScript ساخت `AdditionalData`

این تابع همان خروجی موردنیاز فیلد `AdditionalData` را با استفاده از کلیدهای
`.env` تولید می‌کند:

> برای راهنمایی بیشتر در نحوه تولید `AdditionalData` می‌توانید از تابع زیر
> استفاده کنید یا منطق معادل آن را در پیاده‌سازی خود بنویسید.

```js
import crypto from "node:crypto";

const buildAdditionalData = ({
  identifier,
  personality = "0",
  salt,
  data = "",
}) => {
  const key = Buffer.from(process.env.MANA_KEY_B64 || "", "base64");
  const iv = Buffer.from(process.env.MANA_IV_B64 || "", "base64");

  if (key.length !== 16 || iv.length !== 16) {
    throw new Error(
      "Invalid MANA key/iv. AES-128-CBC requires 16 bytes each.",
    );
  }

  if (!identifier) {
    throw new Error("National identifier is required.");
  }

  const plainText = `${personality}|${identifier}|${salt}`;
  const cipher = crypto.createCipheriv("aes-128-cbc", key, iv);

  const encryptedIdentifier = Buffer.concat([
    cipher.update(plainText, "utf8"),
    cipher.final(),
  ]).toString("base64");

  return JSON.stringify({
    NationalEncryptedId: encryptedIdentifier,
    ThirdPartyCode: process.env.MANA_THIRD_PARTY_CODE,
    Data: data,
  });
};
```

نمونه استفاده:

```js
const additionalData = buildAdditionalData({
  identifier: "NATIONAL_IDENTIFIER",
  personality: "0",
  salt: orderId,
});
```

- مقدار `identifier` کد ملی شخص حقیقی یا شناسه ملی شخص حقوقی است.
- مقدار `salt` باید همان `OrderId` پرداخت باشد.
- خروجی تابع یک رشته JSON آماده برای قرار گرفتن در `AdditionalData` است.
- هنگام قراردادن خروجی در XML باید کاراکترهای ویژه XML مانند `&`، `<`، `>`،
  `"` و `'` Escape شوند.

## مرحله ۲: دریافت توکن پرداخت

### مقادیر درخواست

| فیلد | مقدار |
|---|---|
| URL | `${PARSIAN_SALE_BASE_URL}.asmx` |
| `SOAPAction` | `${PARSIAN_SALE_BASE_URL}/SalePaymentRequest` |
| `LoginAccount` | یکی از کلیدهای `PARSIAN_*_LOGIN` |
| `OrderId` | شماره سفارش یکتا؛ در این اتصال یک عدد ۱۷ رقمی |
| `Amount` | مبلغ صحیح به ریال |
| `CallBackUrl` | مقدار `PARSIAN_CALLBACK_URL` |
| `AdditionalData` | مقدار ساخته‌شده در مرحله قبل |
| `Originator` | برابر `OrderId` |

برای نمونه، ابتدا Login پذیرنده موردنظر را انتخاب کنید:

```bash
export PARSIAN_LOGIN_ACCOUNT="${PARSIAN_OMID_LOGIN}"
export PARSIAN_TERMINAL_NUMBER="${PARSIAN_OMID_TERMINAL}"
```

### درخواست SOAP

```bash
curl --request POST \
  --url "${PARSIAN_SALE_BASE_URL}.asmx" \
  --header "Content-Type: text/xml; charset=utf-8" \
  --header "SOAPAction: ${PARSIAN_SALE_BASE_URL}/SalePaymentRequest" \
  --data-binary "<?xml version=\"1.0\" encoding=\"utf-8\"?>
<soap:Envelope xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"
               xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\"
               xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\">
  <soap:Body>
    <SalePaymentRequest xmlns=\"${PARSIAN_SALE_BASE_URL}\">
      <requestData>
        <LoginAccount>${PARSIAN_LOGIN_ACCOUNT}</LoginAccount>
        <OrderId>${ORDER_ID}</OrderId>
        <Amount>${AMOUNT_RIAL}</Amount>
        <CallBackUrl>${PARSIAN_CALLBACK_URL}</CallBackUrl>
        <AdditionalData>${ADDITIONAL_DATA_XML_ESCAPED}</AdditionalData>
        <Originator>${ORDER_ID}</Originator>
      </requestData>
    </SalePaymentRequest>
  </soap:Body>
</soap:Envelope>"
```

### پاسخ Sale

فیلدهای مهم پاسخ:

| فیلد | توضیح |
|---|---|
| `Status` | در صورت موفقیت برابر `0` |
| `Token` | توکن پرداخت |
| `Message` | پیام پارسیان |

نمونه بخش اصلی پاسخ موفق:

```xml
<SalePaymentRequestResult>
  <Status>0</Status>
  <Token>PARSIAN_PAYMENT_TOKEN</Token>
  <Message>...</Message>
</SalePaymentRequestResult>
```

فقط اگر `Status` صفر و `Token` دارای مقدار بود، به مرحله بعد بروید.

این اطلاعات را تا پایان فرآیند پرداخت نگهداری کنید:

```text
OrderId
Token
Amount
LoginAccount
TerminalNo
```

## مرحله ۳: هدایت کاربر به درگاه

پس از دریافت توکن، کاربر باید به آدرس زیر هدایت شود:

```text
${PARSIAN_GATEWAY_URL}?token=PARSIAN_PAYMENT_TOKEN
```

نمونه HTML:

```html
<script>
  window.location.assign(
    `${PARSIAN_GATEWAY_URL}?token=${encodeURIComponent(PARSIAN_PAYMENT_TOKEN)}`,
  );
</script>
```

`PARSIAN_PAYMENT_TOKEN` همان مقدار `Token` پاسخ Sale است.

## مرحله ۴: دریافت Callback از پارسیان

پارسیان نتیجه پرداخت را با درخواست زیر به مقدار
`PARSIAN_CALLBACK_URL` ارسال می‌کند:

```text
Method: POST
Content-Type: application/x-www-form-urlencoded
```

فیلدهای Callback:

| فیلد | توضیح |
|---|---|
| `OrderId` | شماره سفارش |
| `Token` | توکن پرداخت |
| `status` یا `Status` | کد نتیجه پرداخت |
| `RRN` | شماره مرجع بانکی |
| `Amount` | مبلغ پرداخت |
| `SwAmount` | مبلغ نهایی تراکنش |
| `TerminalNo` | شماره ترمینال |
| `HashCardNumber` | شماره کارت هش‌شده یا ماسک‌شده |

نمونه معادل Callback:

```bash
curl --request POST \
  --url "${PARSIAN_CALLBACK_URL}" \
  --header "Content-Type: application/x-www-form-urlencoded" \
  --data-urlencode "OrderId=${ORDER_ID}" \
  --data-urlencode "Token=${PARSIAN_PAYMENT_TOKEN}" \
  --data-urlencode "status=0" \
  --data-urlencode "RRN=${RRN}" \
  --data-urlencode "Amount=${AMOUNT_RIAL}" \
  --data-urlencode "SwAmount=${AMOUNT_RIAL}" \
  --data-urlencode "TerminalNo=${PARSIAN_TERMINAL_NUMBER}" \
  --data-urlencode "HashCardNumber=${HASH_CARD_NUMBER}"
```

این Curl فقط نمایش ساختار درخواست Callback است؛ در پرداخت واقعی خود پارسیان
آن را ارسال می‌کند.

### کنترل Callback

Callback فقط در شرایط زیر موفق و قابل Confirm است:

```text
Status = 0
Token callback = Token پاسخ Sale
OrderId callback = OrderId درخواست Sale
RRN > 0
Amount یا SwAmount = مبلغ اولیه پرداخت
TerminalNo = مقدار PARSIAN_*_TERMINAL همان پذیرنده
```

اگر پارسیان مبلغ یا شماره ترمینال را در Callback ارسال کرد، حتماً آن‌ها را با
اطلاعات اولیه تطبیق دهید.

کد `-138` به معنی انصراف کاربر از پرداخت است و نباید Confirm انجام شود.

## مرحله ۵: تأیید نهایی پرداخت

بعد از دریافت Callback موفق و عبور از کنترل‌های بالا، درخواست
`ConfirmPayment` را ارسال کنید.

### مقادیر درخواست

| فیلد | مقدار |
|---|---|
| URL | `${PARSIAN_CONFIRM_BASE_URL}.asmx` |
| `SOAPAction` | `${PARSIAN_CONFIRM_BASE_URL}/ConfirmPayment` |
| `LoginAccount` | همان `PARSIAN_*_LOGIN` استفاده‌شده در Sale |
| `Token` | مقدار Token موجود در Callback |

### درخواست SOAP

```bash
curl --request POST \
  --url "${PARSIAN_CONFIRM_BASE_URL}.asmx" \
  --header "Content-Type: text/xml; charset=utf-8" \
  --header "SOAPAction: ${PARSIAN_CONFIRM_BASE_URL}/ConfirmPayment" \
  --data-binary "<?xml version=\"1.0\" encoding=\"utf-8\"?>
<soap:Envelope xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"
               xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\"
               xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\">
  <soap:Body>
    <ConfirmPayment xmlns=\"${PARSIAN_CONFIRM_BASE_URL}\">
      <requestData>
        <LoginAccount>${PARSIAN_LOGIN_ACCOUNT}</LoginAccount>
        <Token>${PARSIAN_PAYMENT_TOKEN}</Token>
      </requestData>
    </ConfirmPayment>
  </soap:Body>
</soap:Envelope>"
```

### پاسخ Confirm

فیلدهای مهم پاسخ:

| فیلد | توضیح |
|---|---|
| `Status` | پرداخت فقط در صورت مقدار `0` موفق است |
| `RRN` | شماره مرجع بانکی |
| `CardNumberMasked` | شماره کارت ماسک‌شده |
| `HashCardNumber` | جایگزین شماره کارت در صورت نبود `CardNumberMasked` |

نمونه بخش اصلی پاسخ موفق:

```xml
<ConfirmPaymentResult>
  <Status>0</Status>
  <RRN>REFERENCE_NUMBER</RRN>
  <CardNumberMasked>621986******1234</CardNumberMasked>
</ConfirmPaymentResult>
```

## نتیجه نهایی

- `Sale Status = 0` و وجود Token: آماده هدایت به درگاه
- Callback با `status = -138`: انصراف کاربر
- Callback نامعتبر: پرداخت ناموفق؛ Confirm نباید فراخوانی شود
- Callback معتبر و `Confirm Status = 0`: پرداخت موفق
- Callback معتبر و `Confirm Status != 0`: پرداخت ناموفق

موفق بودن Callback به‌تنهایی به معنی موفق بودن پرداخت نیست. نتیجه نهایی فقط
بعد از دریافت `Status = 0` از `ConfirmPayment` مشخص می‌شود.
