# 退款

根据支付平台订单号完成退款，可支持同一笔订单分多次退款。

退款不限制退款次数，可操作至剩余金额为0。退款成功后，手续费按照退款金额占订单金额比例系数乘以订单总手续费进行退还。

**重要**：
- sn与client_sn不能同时为空，优先按照sn查找订单，如果没有，再按照client_sn查询
- 退款序列号（refund_request_no）用于唯一标识某次退款请求。正常情况下对同一笔订单进行多次退款请求时该字段不能重复；通信质量不佳时重试则务必要保持序列号不变
- 三个月以内的订单都可以进行退款

## 接口说明

- 请求方式：`POST`
- 请求地址：`https://vsi-api.shouqianba.com/upay/v2/refund`
- Operation ID：`refund`

## 请求参数

无请求参数。

## 请求体

| 字段名 | 类型 | 必填 | 描述 |
| --- | --- | --- | --- |
| client_sn | string | 否 | 商户订单号。和sn二选一（不能同时为空） |
| extended | object | 否 | 扩展参数集合，JSON格式。最多支持24个字段，每个字段key长度不超过64字节，value长度不超过256字节。 |
| extended.attach | string | 否 | 附加数据。小程序扫码点餐订单必传：OrderSource=FoodOrder |
| extended.extend_params | object | 否 | 扩展参数。如：花呗分期参数 hb_fq_num、hb_fq_seller_percent 等。 |
| extended.extend_params.hb_fq_num | string | 否 | 花呗分期数，支持3/6/12期 |
| extended.extend_params.hb_fq_seller_percent | string | 否 | 花呗分期卖家承担手续费比例。100：商家承担；0：用户承担 |
| extended.goods_tag | string | 否 | 商品标记 |
| extended.sub_appid | string | 否 | 指定上送appid获取用户身份标识 |
| goods_details | array[object] | 否 | 商品详情。每个元素包含goods_id、goods_name、refund_amount（选填）、quantity、price、promotion_type |
| goods_details[].goods_id | string | 是 | 商品的编号 |
| goods_details[].goods_name | string | 是 | 商品名称，如ipad |
| goods_details[].price | number | 是 | 商品单价，单位为分，如2000 |
| goods_details[].promotion_type | number | 是 | 优惠类型。0：没有优惠；1：支付机构优惠 |
| goods_details[].quantity | number | 是 | 商品数量，如10 |
| goods_details[].refund_amount | number | 否 | 商品退款金额，单位为分。若存在单品有优惠的订单且需要部分退款时必传。 |
| operator | string | 是 | 操作员。执行本次退款的操作员 |
| reflect | string | 否 | 反射参数。任何调用者希望原样返回的信息 |
| refund_amount | string | 是 | 退款金额。以分为单位 |
| refund_request_no | string | 是 | 退款序列号。商户退款所需序列号，用于唯一标识某次退款请求，防止意外重复退款 |
| sn | string | 否 | 收钱吧唯一订单号。和client_sn二选一（不能同时为空），优先使用sn |
| terminal_sn | string | 是 | 收钱吧终端ID |

## 请求示例

```json
{
  "operator": "Obama",
  "refund_amount": "100",
  "refund_request_no": "23030349",
  "sn": "7892259488292938",
  "terminal_sn": "00101010029201012912"
}
```

## 响应体

| 字段名 | 类型 | 必填 | 描述 |
| --- | --- | --- | --- |
| biz_response | object | 否 |  |
| biz_response.data | object | 否 | 退款业务数据（业务成功时返回） |
| biz_response.data.channel_finish_time | string | 否 | 退款动作在支付平台完成的时间（时间戳）。退款成功有值返回 |
| biz_response.data.client_sn | string | 是 | 商户订单号 |
| biz_response.data.finish_time | string | 否 | 退款动作在收钱吧的完成时间（时间戳）。退款成功有值返回 |
| biz_response.data.net_amount | string | 是 | 剩余金额。实收金额减退款金额 |
| biz_response.data.operator | string | 是 | 执行本次退款的操作员 |
| biz_response.data.order_status | string | 是 | 订单状态 |
| biz_response.data.payway | string | 是 | 支付方式 |
| biz_response.data.payway_name | string | 是 | 支付方式名称 |
| biz_response.data.settlement_amount | string | 是 | 本次操作退款金额 |
| biz_response.data.sn | string | 是 | 收钱吧唯一订单号 |
| biz_response.data.status | string | 是 | 退款流水状态 |
| biz_response.data.subject | string | 是 | 商品概述 |
| biz_response.data.terminal_sn | string | 是 | 收钱吧终端ID |
| biz_response.data.total_amount | string | 是 | 原始交易实收金额 |
| biz_response.data.trade_no | string | 是 | 支付平台的订单凭证号 |
| biz_response.error_code | string | 否 | 业务执行错误码。业务执行失败时返回。 |
| biz_response.error_message | string | 否 | 业务执行错误信息。 |
| biz_response.result_code | string | 是 | 业务执行响应码。SUCCESS：成功；FAIL：失败；INPROGRESS：进行中；ERROR：未知。具体取值见附录《业务数据响应码列表》 |
| error_code | string | 否 | 通讯错误码。通讯失败时返回。见附录《公共错误码》 |
| error_message | string | 否 | 通讯错误信息描述。通讯失败时返回。 |
| result_code | string | 是 | 通讯响应码。200：通讯成功；400：客户端错误；500：服务端错误 |

## 响应示例

```json
{
  "biz_response": {
    "data": {
      "channel_finish_time": "string_value",
      "client_sn": "string_value",
      "finish_time": "string_value",
      "net_amount": "string_value",
      "operator": "string_value",
      "order_status": "string_value",
      "payway": "string_value",
      "payway_name": "string_value",
      "settlement_amount": "string_value",
      "sn": "string_value",
      "status": "string_value",
      "subject": "string_value",
      "terminal_sn": "string_value",
      "total_amount": "string_value",
      "trade_no": "string_value"
    },
    "error_code": "string_value",
    "error_message": "string_value",
    "result_code": "string_value"
  },
  "error_code": "string_value",
  "error_message": "string_value",
  "result_code": "string_value"
}
```

## 示例代码

### cURL

```bash
curl -X POST "https://vsi-api.shouqianba.com/upay/v2/refund" \
  -H "Authorization: 100000000000000000 0123456789abcdef0123456789abcdef" \
  -H "Content-Type: application/json" \
  -d '{"operator":"Obama","refund_amount":"100","refund_request_no":"23030349","sn":"7892259488292938","terminal_sn":"00101010029201012912"}'
```

### Python

```python
import requests

url = "https://vsi-api.shouqianba.com/upay/v2/refund"
headers = {"Content-Type": "application/json",
    "Authorization": "100000000000000000 0123456789abcdef0123456789abcdef",
}
json_data = {
    "operator": "Obama",
    "refund_amount": "100",
    "refund_request_no": "23030349",
    "sn": "7892259488292938",
    "terminal_sn": "00101010029201012912"
}

response = requests.post(
    url,
    headers=headers,
    json=json_data,
)

print(response.json())
```

### Java

```java
// 退款
OkHttpClient client = new OkHttpClient();
String json = "{\"operator\":\"Obama\",\"refund_amount\":\"100\",\"refund_request_no\":\"23030349\",\"sn\":\"7892259488292938\",\"terminal_sn\":\"00101010029201012912\"}";
RequestBody body = RequestBody.create(json, MediaType.parse("application/json"));

Request request = new Request.Builder()
    .url("https://vsi-api.shouqianba.com/upay/v2/refund")
    .addHeader("Authorization", "100000000000000000 0123456789abcdef0123456789abcdef")
    .post(body)
    .build();
Response response = client.newCall(request).execute();
System.out.println(response.body().string());
```

### Go

```go
package main

import (
    "fmt"
    "io"
    "net/http"
    "bytes"
)

func main() {
	jsonBody := []byte(`{"operator":"Obama","refund_amount":"100","refund_request_no":"23030349","sn":"7892259488292938","terminal_sn":"00101010029201012912"}`)
	reqBody := bytes.NewBuffer(jsonBody)
	req, err := http.NewRequest("POST", "https://vsi-api.shouqianba.com/upay/v2/refund", reqBody)
	if err != nil {
		panic(err)
	}
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", "100000000000000000 0123456789abcdef0123456789abcdef")
	resp, err := (&http.Client{}).Do(req)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()
	respBytes, err := io.ReadAll(resp.Body)
	if err != nil {
		panic(err)
	}
	fmt.Println(string(respBytes))
}
```

### Node.js

```javascript
const https = require("https");

const options = {
  method: "POST",
  hostname: "vsi-api.shouqianba.com",
  path: "/upay/v2/refund",
  headers: {
    "Authorization": "100000000000000000 0123456789abcdef0123456789abcdef",
    "Content-Type": "application/json"
  }
};

const postData = JSON.stringify({"operator":"Obama","refund_amount":"100","refund_request_no":"23030349","sn":"7892259488292938","terminal_sn":"00101010029201012912"});

const req = https.request(options, (res) => {
  let data = "";
  res.on("data", (chunk) => { data += chunk; });
  res.on("end", () => { console.log(JSON.parse(data)); });
});
req.write(postData);
req.end();
```
