<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
/**
* API.php.
*
* @author overtrue <i@overtrue.me>
* @copyright 2015 overtrue <i@overtrue.me>
*
* @see https://github.com/overtrue
* @see http://overtrue.me
*/
namespace EasyWeChat\Payment;
use Doctrine\Common\Cache\Cache;
use Doctrine\Common\Cache\FilesystemCache;
use EasyWeChat\Core\AbstractAPI;
use EasyWeChat\Core\Exception;
use EasyWeChat\Support\Collection;
use EasyWeChat\Support\XML;
use Psr\Http\Message\ResponseInterface;
class API extends AbstractAPI
{
protected $merchant;
protected $sandboxEnabled = false;
protected $sandboxSignKey;
protected $cache;
const API_HOST = 'https://api.mch.weixin.qq.com';
const API_PAY_ORDER = '/pay/micropay';
const API_PREPARE_ORDER = '/pay/unifiedorder';
const API_QUERY = '/pay/orderquery';
const API_CLOSE = '/pay/closeorder';
const API_REVERSE = '/secapi/pay/reverse';
const API_REFUND = '/secapi/pay/refund';
const API_QUERY_REFUND = '/pay/refundquery';
const API_DOWNLOAD_BILL = '/pay/downloadbill';
const API_REPORT = '/payitil/report';
const API_URL_SHORTEN = 'https://api.mch.weixin.qq.com/tools/shorturl';
const API_AUTH_CODE_TO_OPENID = 'https://api.mch.weixin.qq.com/tools/authcodetoopenid';
const API_SANDBOX_SIGN_KEY = 'https://api.mch.weixin.qq.com/sandboxnew/pay/getsignkey';
const TRANSACTION_ID = 'transaction_id';
const OUT_TRADE_NO = 'out_trade_no';
const OUT_REFUND_NO = 'out_refund_no';
const REFUND_ID = 'refund_id';
const BILL_TYPE_ALL = 'ALL';
const BILL_TYPE_SUCCESS = 'SUCCESS';
const BILL_TYPE_REFUND = 'REFUND';
const BILL_TYPE_REVOKED = 'REVOKED';
public function __construct(Merchant $merchant, Cache $cache = null)
{
$this->merchant = $merchant;
$this->cache = $cache;
}
public function pay(Order $order)
{
return $this->request($this->wrapApi(self::API_PAY_ORDER), $order->all());
}
public function prepare(Order $order)
{
$order->notify_url = $order->get('notify_url', $this->merchant->notify_url);
if (is_null($order->spbill_create_ip)) {
$order->spbill_create_ip = (Order::NATIVE === $order->trade_type) ? get_server_ip() : get_client_ip();
}
return $this->request($this->wrapApi(self::API_PREPARE_ORDER), $order->all());
}
public function query($orderNo, $type = self::OUT_TRADE_NO)
{
$params = [
$type => $orderNo,
];
return $this->request($this->wrapApi(self::API_QUERY), $params);
}
public function queryByTransactionId($transactionId)
{
return $this->query($transactionId, self::TRANSACTION_ID);
}
public function close($tradeNo)
{
$params = [
'out_trade_no' => $tradeNo,
];
return $this->request($this->wrapApi(self::API_CLOSE), $params);
}
public function reverse($orderNo, $type = self::OUT_TRADE_NO)
{
$params = [
$type => $orderNo,
];
return $this->safeRequest($this->wrapApi(self::API_REVERSE), $params);
}
public function reverseByTransactionId($transactionId)
{
return $this->reverse($transactionId, self::TRANSACTION_ID);
}
public function refund(
$orderNo,
$refundNo,
$totalFee,
$refundFee = null,
$opUserId = null,
$type = self::OUT_TRADE_NO,
$refundAccount = 'REFUND_SOURCE_UNSETTLED_FUNDS',
$refundReason = ''
) {
$params = [
$type => $orderNo,
'out_refund_no' => $refundNo,
'total_fee' => $totalFee,
'refund_fee' => $refundFee ?: $totalFee,
'refund_fee_type' => $this->merchant->fee_type,
'refund_account' => $refundAccount,
'refund_desc' => $refundReason,
'op_user_id' => $opUserId ?: $this->merchant->merchant_id,
];
return $this->safeRequest($this->wrapApi(self::API_REFUND), $params);
}
public function refundByTransactionId(
$orderNo,
$refundNo,
$totalFee,
$refundFee = null,
$opUserId = null,
$refundAccount = 'REFUND_SOURCE_UNSETTLED_FUNDS',
$refundReason = ''
) {
return $this->refund($orderNo, $refundNo, $totalFee, $refundFee, $opUserId, self::TRANSACTION_ID, $refundAccount, $refundReason);
}
public function queryRefund($orderNo, $type = self::OUT_TRADE_NO)
{
$params = [
$type => $orderNo,
];
return $this->request($this->wrapApi(self::API_QUERY_REFUND), $params);
}
public function queryRefundByRefundNo($refundNo)
{
return $this->queryRefund($refundNo, self::OUT_REFUND_NO);
}
public function queryRefundByTransactionId($transactionId)
{
return $this->queryRefund($transactionId, self::TRANSACTION_ID);
}
public function queryRefundByRefundId($refundId)
{
return $this->queryRefund($refundId, self::REFUND_ID);
}
public function downloadBill($date, $type = self::BILL_TYPE_ALL)
{
$params = [
'bill_date' => $date,
'bill_type' => $type,
];
return $this->request($this->wrapApi(self::API_DOWNLOAD_BILL), $params, 'post', [\GuzzleHttp\RequestOptions::STREAM => true], true)->getBody();
}
public function urlShorten($url)
{
return $this->request(self::API_URL_SHORTEN, ['long_url' => $url]);
}
public function report($api, $timeConsuming, $resultCode, $returnCode, array $other = [])
{
$params = array_merge([
'interface_url' => $api,
'execute_time_' => $timeConsuming,
'return_code' => $returnCode,
'return_msg' => null,
'result_code' => $resultCode,
'user_ip' => get_client_ip(),
'time' => time(),
], $other);
return $this->request($this->wrapApi(self::API_REPORT), $params);
}
public function authCodeToOpenId($authCode)
{
return $this->request(self::API_AUTH_CODE_TO_OPENID, ['auth_code' => $authCode]);
}
public function setMerchant(Merchant $merchant)
{
$this->merchant = $merchant;
}
public function getMerchant()
{
return $this->merchant;
}
public function sandboxMode($enabled = false)
{
$this->sandboxEnabled = $enabled;
return $this;
}
protected function request($api, array $params, $method = 'post', array $options = [], $returnResponse = false)
{
$params = array_merge($params, $this->merchant->only(['sub_appid', 'sub_mch_id']));
$params['appid'] = $this->merchant->app_id;
$params['mch_id'] = $this->merchant->merchant_id;
$params['device_info'] = $this->merchant->device_info;
$params['nonce_str'] = uniqid();
$params = array_filter($params);
$params['sign'] = generate_sign($params, $this->getSignkey($api), 'md5');
$options = array_merge([
'body' => XML::build($params),
], $options);
$response = $this->getHttp()->request($api, $method, $options);
return $returnResponse ? $response : $this->parseResponse($response);
}
public function getSignkey($api)
{
return $this->sandboxEnabled && self::API_SANDBOX_SIGN_KEY !== $api ? $this->getSandboxSignKey() : $this->merchant->key;
}
protected function safeRequest($api, array $params, $method = 'post')
{
$options = [
'cert' => $this->merchant->get('cert_path'),
'ssl_key' => $this->merchant->get('key_path'),
];
return $this->request($api, $params, $method, $options);
}
protected function parseResponse($response)
{
if ($response instanceof ResponseInterface) {
$response = $response->getBody();
}
return new Collection((array) XML::parse($response));
}
protected function wrapApi($resource)
{
return self::API_HOST.($this->sandboxEnabled ? '/sandboxnew' : '').$resource;
}
protected function getSandboxSignKey()
{
if ($this->sandboxSignKey) {
return $this->sandboxSignKey;
}
$cacheKey = 'sandbox_signkey.'.$this->merchant->merchant_id.$this->merchant->sub_merchant_id;
$cache = $this->getCache();
$this->sandboxSignKey = $cache->fetch($cacheKey);
if (!$this->sandboxSignKey) {
$result = $this->request(self::API_SANDBOX_SIGN_KEY, []);
if ('SUCCESS' === $result->return_code) {
$cache->save($cacheKey, $result->sandbox_signkey, 24 * 3600);
return $this->sandboxSignKey = $result->sandbox_signkey;
}
throw new Exception($result->return_msg);
}
return $this->sandboxSignKey;
}
public function getCache()
{
return $this->cache ?: $this->cache = new FilesystemCache(sys_get_temp_dir());
}
}