├── Tests ├── .gitkeep └── MerchantTest.php ├── .gitignore ├── Exceptions └── TradeInfoException.php ├── Constants ├── Payment │ ├── BarcodeSubType.php │ ├── CvsSubType.php │ ├── AtmSubType.php │ └── WebAtmSubType.php ├── PeriodType.php ├── LanguageType.php ├── UnionPayType.php └── PaymentType.php ├── Contracts ├── QuickCreditInterface.php └── OrderInterface.php ├── ItemNameTrait.php ├── Info ├── Decorator │ ├── ExtraInfo.php │ ├── Remark.php │ ├── MerchandisingUrl.php │ ├── PayInInstallments.php │ ├── ClientBack.php │ ├── PayComplete.php │ ├── SubMerchant.php │ ├── Platform.php │ ├── Cvs.php │ ├── Barcode.php │ ├── CustomField.php │ ├── AbstractOfflinePay.php │ ├── Language.php │ ├── UnionPay.php │ ├── IgnorePayment.php │ ├── Credit.php │ ├── AbstractCvs.php │ ├── Atm.php │ └── PayInPeriods.php ├── BasicInfo.php └── Info.php ├── composer.json ├── phpunit.xml.dist ├── LICENSE ├── Cryption.php ├── Merchant.php ├── Response.php ├── README.md └── Ecpay.php /Tests/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | composer.lock 3 | phpunit.xml -------------------------------------------------------------------------------- /Exceptions/TradeInfoException.php: -------------------------------------------------------------------------------- 1 | items = $items; 13 | } 14 | 15 | /** @return string */ 16 | public function getItemName() 17 | { 18 | if (! $this->items) { 19 | throw new \LogicException('empty items'); 20 | } 21 | 22 | return mb_substr(implode('#', $this->items), 0, 400); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Contracts/OrderInterface.php: -------------------------------------------------------------------------------- 1 | info = $info; 17 | } 18 | 19 | /** 20 | * 是否需要額外的付款資訊 21 | * 注意事項: 額外回傳的參數全部都需要加入檢查碼計算 22 | * 23 | * @return array 24 | */ 25 | public function getInfo() 26 | { 27 | return $this->info->getInfo() + 28 | [ 29 | 'NeedExtraPaidInfo' => 'Y' 30 | ]; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Info/Decorator/Remark.php: -------------------------------------------------------------------------------- 1 | info = $info; 23 | 24 | $this->remark = $remark; 25 | } 26 | 27 | public function getInfo() 28 | { 29 | return $this->info->getInfo() + 30 | [ 31 | 'Remark' => $this->remark, 32 | ]; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Info/Decorator/MerchandisingUrl.php: -------------------------------------------------------------------------------- 1 | info = $info; 23 | 24 | $this->url = $url; 25 | } 26 | 27 | public function getInfo() 28 | { 29 | return $this->info->getInfo() + 30 | [ 31 | 'ItemURL' => $this->url, 32 | ]; 33 | } 34 | } -------------------------------------------------------------------------------- /Info/Decorator/PayInInstallments.php: -------------------------------------------------------------------------------- 1 | info = $info; 22 | 23 | $this->instFlag = $instFlag; 24 | } 25 | 26 | public function getInfo() 27 | { 28 | return $this->info->getInfo() + 29 | [ 30 | 'CreditInstallment' => $this->instFlag, 31 | ]; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Info/Decorator/ClientBack.php: -------------------------------------------------------------------------------- 1 | info = $info; 23 | 24 | $this->clientBackUrl = $clientBackUrl; 25 | } 26 | 27 | public function getInfo() 28 | { 29 | return $this->info->getInfo() + 30 | [ 31 | 'ClientBackURL' => $this->clientBackUrl, 32 | ]; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Constants/Payment/AtmSubType.php: -------------------------------------------------------------------------------- 1 | info = $info; 23 | 24 | $this->orderResultUrl = $orderResultUrl; 25 | } 26 | 27 | public function getInfo() 28 | { 29 | return $this->info->getInfo() + 30 | [ 31 | 'OrderResultURL' => $this->orderResultUrl, 32 | ]; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fall1600/ecpay", 3 | "description": "Payment solution of ECPay(綠界科技), implementing by pure PHP", 4 | "keywords": ["ecpay", "payment"], 5 | "type": "library", 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "fall1600", 10 | "email": "fall1600@gmail.com" 11 | } 12 | ], 13 | "minimum-stability": "stable", 14 | "require": { 15 | "ext-curl": "*", 16 | "ext-mbstring": "*", 17 | "ext-json": "*", 18 | "myclabs/php-enum": "^1.7" 19 | }, 20 | "autoload": { 21 | "psr-4": { 22 | "fall1600\\Package\\Ecpay\\": "" 23 | }, 24 | "exclude-from-classmap": [ 25 | "/Tests/" 26 | ] 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Info/Decorator/SubMerchant.php: -------------------------------------------------------------------------------- 1 | info = $info; 24 | 25 | $this->subMerchantId = $subMerchantId; 26 | } 27 | 28 | public function getInfo() 29 | { 30 | return $this->info->getInfo() + 31 | [ 32 | 'StoreID' => $this->subMerchantId, 33 | ]; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Info/Decorator/Platform.php: -------------------------------------------------------------------------------- 1 | info = $info; 22 | 23 | $this->platformId = $platformId; 24 | } 25 | 26 | /** 27 | * 特約合作平台商代號(由綠界提供) 28 | * @return array 29 | */ 30 | public function getInfo() 31 | { 32 | return $this->info->getInfo() + 33 | [ 34 | 'PlatformID' => $this->platformId, 35 | ]; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Info/Decorator/Cvs.php: -------------------------------------------------------------------------------- 1 | ttl = $ttl; 23 | } 24 | 25 | protected function setSubPaymentType(string $subPaymentType = null) 26 | { 27 | if ($subPaymentType && ! CvsSubType::isValid($subPaymentType)) { 28 | throw new \LogicException('unsupported sub payment type of csv'); 29 | } 30 | 31 | $this->subPaymentType = $subPaymentType; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Info/Decorator/Barcode.php: -------------------------------------------------------------------------------- 1 | ttl = $ttl; 23 | } 24 | 25 | protected function setSubPaymentType(string $subPaymentType = null) 26 | { 27 | if ($subPaymentType && ! BarcodeSubType::isValid($subPaymentType)) { 28 | throw new \LogicException('unsupported sub payment type of csv barcode'); 29 | } 30 | 31 | $this->subPaymentType = $subPaymentType; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Info/BasicInfo.php: -------------------------------------------------------------------------------- 1 | $this->merchantId, 15 | 'MerchantTradeNo' => $this->order->getMerchantTradeNo(), 16 | 'MerchantTradeDate' => date('Y/m/d H:i:s'), 17 | 'PaymentType' => 'aio', 18 | 'TotalAmount' => $this->order->getTotalAmount(), 19 | 'TradeDesc' => $this->order->getTradeDesc(), 20 | 'ItemName' => $this->order->getItemName(), 21 | 'ReturnURL' => $this->returnUrl, 22 | 'ChoosePayment' => $this->paymentType, 23 | 'EncryptType' => 1, 24 | ]; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Info/Decorator/CustomField.php: -------------------------------------------------------------------------------- 1 | info = $info; 24 | 25 | $this->fields = $fields; 26 | } 27 | 28 | public function getInfo() 29 | { 30 | $result = $this->info->getInfo(); 31 | 32 | for ($i = 0, $max = min(static::SIZE, count($this->fields)); $i < $max; $i++) { 33 | $result += [ 34 | 'CustomField'.($i+1) => $this->fields[$i], 35 | ]; 36 | } 37 | 38 | return $result; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Constants/Payment/WebAtmSubType.php: -------------------------------------------------------------------------------- 1 | info = $info; 31 | 32 | $this->paymentInfoUrl = $paymentInfoUrl; 33 | 34 | $this->clientRedirectUrl = $clientRedirectUrl; 35 | 36 | $this->setTtl($ttl); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | ./Tests/ 18 | 19 | 20 | 21 | 22 | 23 | ./ 24 | 25 | ./Tests 26 | ./vendor 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /Info/Decorator/Language.php: -------------------------------------------------------------------------------- 1 | info = $info; 23 | 24 | $this->setLanguage($language); 25 | } 26 | 27 | public function getInfo() 28 | { 29 | return $this->info->getInfo() + 30 | [ 31 | 'Language' => $this->language, 32 | ]; 33 | } 34 | 35 | protected function setLanguage(string $language) 36 | { 37 | if (! LanguageType::isValid($language)) { 38 | throw new \LogicException('unsupported language'); 39 | } 40 | 41 | $this->language = $language; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Info/Decorator/UnionPay.php: -------------------------------------------------------------------------------- 1 | info = $info; 23 | 24 | $this->setUnionPayType($unionPayType); 25 | } 26 | 27 | public function getInfo() 28 | { 29 | return $this->info->getInfo() + 30 | [ 31 | 'UnionPay' => $this->unionPayType, 32 | ]; 33 | } 34 | 35 | protected function setUnionPayType(int $unionPayType) 36 | { 37 | if (! UnionPayType::isValid($unionPayType)) { 38 | throw new \LogicException('unsupported this type of unionpay'); 39 | } 40 | 41 | $this->unionPayType = $unionPayType; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016-2020 Lin Jinghong 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /Info/Decorator/IgnorePayment.php: -------------------------------------------------------------------------------- 1 | info = $info; 23 | 24 | $this->setIgnores($ignores); 25 | } 26 | 27 | public function getInfo() 28 | { 29 | return $this->info->getInfo() + 30 | [ 31 | 'IgnorePayment' => implode('#', $this->ignores), 32 | ]; 33 | } 34 | 35 | protected function setIgnores(array $ignores) 36 | { 37 | foreach ($ignores as $ignore) { 38 | if (! PaymentType::isValid($ignore)) { 39 | throw new \LogicException("unsupported payment type $ignore"); 40 | } 41 | } 42 | 43 | $this->ignores = $ignores; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Info/Decorator/Credit.php: -------------------------------------------------------------------------------- 1 | info = $info; 30 | 31 | $this->quickCredit = $quickCredit; 32 | 33 | $this->isRedeem = $isRedeem; 34 | } 35 | 36 | public function getInfo() 37 | { 38 | $result = array_merge($this->info->getInfo(), [ 39 | 'ChoosePayment' => PaymentType::CREDIT, 40 | ]); 41 | 42 | // 有帶QuickCredit 就是要啟用快速結帳, 所以BindingCard 帶1 43 | if ($this->quickCredit) { 44 | $result += [ 45 | 'BindingCard' => 1, 46 | 'MerchantMemberID' => $this->quickCredit->getMerchantMemberId(), 47 | ]; 48 | } 49 | 50 | if ($this->isRedeem) { 51 | $result += [ 52 | 'Redeem' => 'Y', 53 | ]; 54 | } 55 | 56 | return $result; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Info/Info.php: -------------------------------------------------------------------------------- 1 | merchantId = $merchantId; 27 | 28 | $this->returnUrl = $returnUrl; 29 | 30 | $this->order = $order; 31 | 32 | $this->paymentType = $paymentType; 33 | } 34 | 35 | /** 36 | * @return string 37 | */ 38 | public function getMerchantId() 39 | { 40 | return $this->merchantId; 41 | } 42 | 43 | /** 44 | * @return string 45 | */ 46 | public function getReturnUrl() 47 | { 48 | return $this->returnUrl; 49 | } 50 | 51 | /** 52 | * @return OrderInterface 53 | */ 54 | public function getOrder() 55 | { 56 | return $this->order; 57 | } 58 | 59 | /** 60 | * @return string 61 | */ 62 | public function getPaymentType() 63 | { 64 | return $this->paymentType; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Info/Decorator/AbstractCvs.php: -------------------------------------------------------------------------------- 1 | info->getInfo(), [ 22 | 'ChoosePayment' => PaymentType::CVS, 23 | 'StoreExpireDate' => $this->ttl, 24 | 'PaymentInfoURL' => $this->paymentInfoUrl, 25 | 'ClientRedirectURL' => $this->clientRedirectUrl, 26 | ]); 27 | 28 | for ($i = 1; $i < count($this->descriptions); $i++) { 29 | $result += [ 30 | "Desc_$i" => $this->descriptions[$i], 31 | ]; 32 | } 33 | 34 | if ($this->subPaymentType) { 35 | $result += [ 36 | 'ChooseSubPayment' => $this->subPaymentType, 37 | ]; 38 | } 39 | 40 | return $result; 41 | } 42 | 43 | public function __construct(Info $info, string $paymentInfoUrl, string $clientRedirectUrl = null, int $ttl = null, string $subPaymentType = null , string ... $descriptions) 44 | { 45 | parent::__construct($info, $paymentInfoUrl, $clientRedirectUrl, $ttl); 46 | 47 | $this->setSubPaymentType($subPaymentType); 48 | 49 | $this->descriptions = array_slice($descriptions, 0, static::DESCRIPTION_SIZE); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Info/Decorator/Atm.php: -------------------------------------------------------------------------------- 1 | setSubPaymentType($subPaymentType); 28 | } 29 | 30 | public function getInfo() 31 | { 32 | $result = array_merge($this->info->getInfo(), [ 33 | 'ChoosePayment' => PaymentType::ATM, 34 | 'ExpireDate' => $this->ttl, 35 | 'PaymentInfoURL' => $this->paymentInfoUrl, 36 | 'ClientRedirectURL' => $this->clientRedirectUrl, 37 | ]); 38 | 39 | if ($this->subPaymentType) { 40 | $result += [ 41 | 'ChooseSubPayment' => $this->subPaymentType, 42 | ]; 43 | } 44 | 45 | return $result; 46 | } 47 | 48 | protected function setTtl(int $ttl = null) 49 | { 50 | if ($ttl > 60 || $ttl < 1) { 51 | throw new \LogicException('ttl only in 1~60 days'); 52 | } 53 | 54 | $this->ttl = $ttl; 55 | } 56 | 57 | protected function setSubPaymentType(?string $subPaymentType) 58 | { 59 | if ($subPaymentType && ! AtmSubType::isValid($subPaymentType)) { 60 | throw new \LogicException('unsupported sub payment of atm'); 61 | } 62 | 63 | $this->subPaymentType = $subPaymentType; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Cryption.php: -------------------------------------------------------------------------------- 1 | getInfo(); 22 | 23 | return $this->countChecksumByArray($infoPayload); 24 | } 25 | 26 | public function countChecksumByArray(array $infoPayload) 27 | { 28 | // 參數依照字母排序 29 | ksort($infoPayload); 30 | 31 | // 不能直接使用http_build_query, 否則中文會預先url encode 過 32 | $str = "HashKey=$this->hashKey"; 33 | foreach ($infoPayload as $key => $value) { 34 | $str .= "&$key=$value"; 35 | } 36 | $str .= "&HashIV=$this->hashIv"; 37 | 38 | // url encode 39 | $strEncoded = urlencode($str); 40 | 41 | // 轉小寫 42 | $strLower = strtolower($strEncoded); 43 | 44 | // 代換成 .Net 成接受的字元 45 | $strReplaced = $this->replaceSymbol($strLower); 46 | 47 | // 用sha256 hash 48 | $strHashed = hash('sha256', $strReplaced); 49 | 50 | // 轉大寫 51 | return strtoupper($strHashed); 52 | } 53 | 54 | protected function replaceSymbol(string $str) 55 | { 56 | if(! empty($str)) { 57 | $str = str_replace('%2D', '-', $str); 58 | $str = str_replace('%2d', '-', $str); 59 | $str = str_replace('%5F', '_', $str); 60 | $str = str_replace('%5f', '_', $str); 61 | $str = str_replace('%2E', '.', $str); 62 | $str = str_replace('%2e', '.', $str); 63 | $str = str_replace('%21', '!', $str); 64 | $str = str_replace('%2A', '*', $str); 65 | $str = str_replace('%2a', '*', $str); 66 | $str = str_replace('%28', '(', $str); 67 | $str = str_replace('%29', ')', $str); 68 | } 69 | 70 | return $str ; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Merchant.php: -------------------------------------------------------------------------------- 1 | id = $id; 32 | 33 | $this->hashKey = $hashKey; 34 | 35 | $this->hashIv = $hashIv; 36 | } 37 | 38 | /** 39 | * @param string $id 40 | * @param string $hashKey 41 | * @param string $hashIv 42 | * @return $this 43 | */ 44 | public function reset(string $id, string $hashKey, string $hashIv) 45 | { 46 | $this->id = $id; 47 | $this->hashKey = $hashKey; 48 | $this->hashIv = $hashIv; 49 | 50 | $this->response = null; 51 | 52 | return $this; 53 | } 54 | 55 | /** 56 | * @return string 57 | */ 58 | public function getId() 59 | { 60 | return $this->id; 61 | } 62 | 63 | /** 64 | * @return string 65 | */ 66 | public function getHashKey() 67 | { 68 | return $this->hashKey; 69 | } 70 | 71 | /** 72 | * @return string 73 | */ 74 | public function getHashIv() 75 | { 76 | return $this->hashIv; 77 | } 78 | 79 | /** 80 | * @param array $rawData 81 | * @return $this 82 | * @throws TradeInfoException 83 | */ 84 | public function setRawData(array $rawData) 85 | { 86 | if (! isset($rawData['CheckMacValue']) || ! isset($rawData['MerchantID']) || ! isset($rawData['MerchantTradeNo'])) { 87 | throw new TradeInfoException('invalid data'); 88 | } 89 | 90 | $this->response = new Response($rawData); 91 | return $this; 92 | } 93 | 94 | /** 95 | * 用來驗證綠界來的response 資料是否可信 96 | * @return bool 97 | */ 98 | public function validateResponse() 99 | { 100 | if (! $this->response) { 101 | throw new \LogicException('set rawData first'); 102 | } 103 | 104 | $responseChecksum = $this->response->getChecksum(); 105 | $countedChecksum = $this->countChecksumByArray($this->response->getOriginInfoPayload()); 106 | return $responseChecksum === $countedChecksum; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /Response.php: -------------------------------------------------------------------------------- 1 | data = $data; 13 | } 14 | 15 | /** 16 | * @return string|null 17 | */ 18 | public function getMerchantId() 19 | { 20 | return $this->data['MerchantID'] ?? null; 21 | } 22 | 23 | /** 24 | * OrderInterface 提供給綠界的 MerchantTradeNo, 由特店(應用層系統)提供 25 | * @return string|null 26 | */ 27 | public function getMerchantTradeNo() 28 | { 29 | return $this->data['MerchantTradeNo'] ?? null; 30 | } 31 | 32 | /** 33 | * 特店旗下店舖代號 34 | * @return string|null 35 | */ 36 | public function getSubMerchant() 37 | { 38 | return $this->data['StoreID'] ?? null; 39 | } 40 | 41 | /** 42 | * 交易狀態 43 | * 若回傳值為 1 時, 為付款成功其餘代碼皆為交易異常, 請至廠商管理後台確認後再出貨。 44 | * @return int|null 45 | */ 46 | public function getReturnCode() 47 | { 48 | return $this->data['RtnCode'] ?? null; 49 | } 50 | 51 | /** 52 | * Server POST 成功回傳:交易成功 53 | * Server POST 補送通知回傳:paid 54 | * Client POST 成功回傳:Succeeded 55 | * @return string|null 56 | */ 57 | public function getReturnMessage() 58 | { 59 | return $this->data['RtnMsg'] ?? null; 60 | } 61 | 62 | /** 63 | * @return string|null 64 | */ 65 | public function getChecksum() 66 | { 67 | return $this->data['CheckMacValue'] ?? null; 68 | } 69 | 70 | /** 71 | * 綠界的交易編號, 由綠界提供 72 | * @return string|null 73 | */ 74 | public function getTradeNo() 75 | { 76 | return $this->data['TradeNo'] ?? null; 77 | } 78 | 79 | /** 80 | * 交易金額 81 | * @return int|null 82 | */ 83 | public function getTradeAmt() 84 | { 85 | return $this->data['TradeAmt'] ?? null; 86 | } 87 | 88 | /** 89 | * 付款時間(yyyy/MM/dd HH:mm:ss) 90 | * @return string|null 91 | */ 92 | public function getPaymentDate() 93 | { 94 | return $this->data['PaymentDate'] ?? null; 95 | } 96 | 97 | /** 98 | * 付款方式 99 | * @return string|null 100 | */ 101 | public function getPaymentType() 102 | { 103 | return $this->data['PaymentType'] ?? null; 104 | } 105 | 106 | /** 107 | * 通路費 108 | * @return int|null 109 | */ 110 | public function getPaymentTypeChargeFee() 111 | { 112 | return $this->data['PaymentTypeChargeFee'] ?? null; 113 | } 114 | 115 | /** 116 | * 訂單成立時間(yyyy/MM/dd HH:mm:ss) 117 | * @return string|null 118 | */ 119 | public function getTradeDate() 120 | { 121 | return $this->data['TradeDate'] ?? null; 122 | } 123 | 124 | /** 125 | * 是否為模擬付款 126 | * @return bool 127 | */ 128 | public function isSimulated() 129 | { 130 | return (bool) $this->data['SimulatePaid']; 131 | } 132 | 133 | /** 134 | * 原始交易資訊, 可用此payload 計算checksum, 確認綠界來的checksum 是否相符 135 | * @return array 136 | */ 137 | public function getOriginInfoPayload() 138 | { 139 | return array_diff_key($this->data, ['CheckMacValue' => 1]); 140 | } 141 | 142 | /** 143 | * @return array 144 | */ 145 | public function getData() 146 | { 147 | return $this->data; 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EcPay 綠界金流 2 | 3 | [Official Doc](https://www.ecpay.com.tw/Content/files/ecpay_011.pdf) 4 | 5 | ## How to use 6 | 7 | #### 建立交易資訊 (BasicInfo) 8 | - $merchantId: 你在綠界申請的商店代號 9 | - $returnUrl: 用來接收綠界付款通知的callback url 10 | - $order: 你的訂單物件, 務必實作package 中的OrderInterface 11 | - $paymentType: 預設交易方式全部開啟 12 | ```php 13 | $info = new BasicInfo($merchantId, $returnUrl, $order, $paymentType = 'ALL'); 14 | ``` 15 | 16 | #### 控制交易方式 17 | ```php 18 | // 反向的設定概念, 依需求關閉付款方式(可參考PaymentType 付款方式) 19 | $info = new IgnorePayment($info, 'ATM', 'BARCODE'); 20 | // 信用卡設定, quickCredit 可開啟記憶信用卡(需實作QuickCreditInterface), 以及設定紅利折抵 21 | $info = new Credit($info, $quickCredit, true); 22 | // 信用卡分期付款設定 23 | $info = new PayInInstallments($info, '3,6,12,18,24'); 24 | // 虛擬ATM 繳費設定, 接收取號的webhook url, 要號完成的回導位置, 繳費期限(天), 預設3天 25 | $info = new Atm($info, $paymentInfoUrl, $clientRedirectUrl, 10); 26 | // 超商繳費設定, 繳費期限(分鐘), 預設10080分鐘=7天 27 | $info = new Cvs($info, $paymentInfoUrl, $clientRedirectUrl, 30); 28 | // 超商條碼繳費設定, 繳費期限(天), 預設7天 29 | $info = new Barcode($info, $paymentInfoUrl, $clientRedirectUrl, 3); 30 | // 是否需要額外的付款資訊 31 | $info = new ExtraInfo($info); 32 | // 特店子商城id 33 | $info = new SubMerchant($info, $subMerchantId); 34 | ``` 35 | 36 | #### 建立Ecpay 物件, 注入商店資訊, 帶著交易資訊前往綠界付款 37 | - $merchantId: 你在綠界商店代號 38 | - $hashKey: 你在綠界商店專屬的HashKey 39 | - $hashIv: 你在綠界商店專屬的HashIV 40 | 41 | ```php 42 | $ecpay = new Ecpay(); 43 | $ecpay 44 | ->setIsProduction(false) // 設定環境, 預設就是走正式機 45 | ->setMerchant(new Merchant($merchantId, $hashKey, $hashIv)) 46 | ->checkout($info); 47 | ``` 48 | 49 | #### 請在你的訂單物件實作 OrderInterface 50 | 51 | ```php 52 | setRawData($request->all())->validateResponse(); //確認為true 後再往下走 68 | 69 | // response 封裝了通知交易的結果, 以下僅列常用methods 70 | $response = $merchant->getResponse(); 71 | // 付款成敗 72 | $response->getReturnCode(); 73 | // 取得交易序號 74 | $response->getTradeNo(); 75 | // 取得訂單編號, 就是OrderInterface 實作的getMerchantOrderNo 76 | $response->getMerchantOrderNo(); 77 | // 付款時間 78 | $response->getPaymentDate(); 79 | // 整包payload 80 | $response->getData(); 81 | ``` 82 | 83 | #### 單筆交易查詢 84 | ```php 85 | $resp = $ecpay 86 | ->setMerchant($merchant) 87 | ->query($order, $platformId = null); 88 | 89 | $isValid = $merchant->setRawData($resp)->validResponse(); // 查詢的response, 有需要也可以validate 90 | 91 | ``` 92 | 93 | #### 各種url 你分的清楚嗎? 94 | | Name | 用途 | 設定的物件 | 備註 | 95 | |:-----------------|:------------------------------------ |:-------------|:---------------------------------------------------------| 96 | | ReturnURL | 通知你系統交易資訊的callback url | BasicInfo | 通常用在訂單付款狀態切換, 最重要,所以BasicInfo 就要設定了, 此webhook 檢查完checksum 後要return 1|OK (半形的|) 給綠界 | 97 | | OrderResultURL | 付款完成回到你系統的位置 | PayComplete | 沒設定就是顯示在綠界 | 98 | | PaymentInfoURL | 離線付款取號完成通知你系統的callback url | Atm, Barcode, Cvs | 用在紀錄離線付款的取號, 務必設定, 此webhook 檢查完checksum 後要return 1|OK (半形的|) 給綠界 | 99 | | ClientRedirectURL| 離線付款取號完成要回到你系統的位置 | Atm, Barcode, Cvs | 沒設定就是顯示在綠界 | 100 | | ClientBackURL | 任何時候在綠界想返回你系統的位置 | ClientBack | 沒設定在綠界就不會顯示[返回商店] | 101 | | PeriodReturnURL | 定期定額授權結果回傳通知你系統的 callback url | PayInPeriods | 用在定期定額的執行結果, 務必設定 | 102 | 103 | -------------------------------------------------------------------------------- /Info/Decorator/PayInPeriods.php: -------------------------------------------------------------------------------- 1 | info = $info; 39 | 40 | $this->setPeriodType($periodType); 41 | 42 | $this->setFrequency($frequency); 43 | 44 | $this->setTimes($times); 45 | 46 | $this->periodReturnUrl = $periodReturnUrl; 47 | } 48 | 49 | /** 50 | * 綠界會依此次授權金額[PeriodAmount]所設定的金額做為之後固定授權的金額。 51 | * 交易金額[TotalAmount]設定金額必須和授權金額[PeriodAmount]相同。 52 | * 請帶整數,不可有小數點。僅限新台幣。 53 | * @return array 54 | */ 55 | public function getInfo() 56 | { 57 | $result = $this->info->getInfo(); 58 | 59 | return $result + 60 | [ 61 | 'PeriodAmount' => $result['TotalAmount'], 62 | 'PeriodType' => $this->periodType, 63 | 'Frequency' => $this->frequency, 64 | 'ExecTimes' => $this->times, 65 | 'PeriodReturnURL' => $this->periodReturnUrl, 66 | ]; 67 | } 68 | 69 | protected function setPeriodType(string $periodType) 70 | { 71 | if (! PeriodType::isValid($periodType)) { 72 | throw new \LogicException('unsupported period type'); 73 | } 74 | 75 | $this->periodType = $periodType; 76 | } 77 | 78 | protected function setFrequency(int $frequency) 79 | { 80 | switch ($this->periodType) { 81 | case PeriodType::DAY: 82 | if ($frequency < 1 || $frequency > 365) { 83 | throw new \LogicException('unsupported frequency in day'); 84 | } 85 | break; 86 | case PeriodType::MONTH: 87 | if ($frequency < 1 || $frequency > 12) { 88 | throw new \LogicException('unsupported frequency in month'); 89 | } 90 | break; 91 | case PeriodType::YEAR: 92 | if ($frequency != 1) { 93 | throw new \LogicException('unsupported frequency in year'); 94 | } 95 | break; 96 | default: 97 | throw new \LogicException('unsupported frequency'); 98 | } 99 | 100 | $this->frequency = $frequency; 101 | } 102 | 103 | protected function setTimes(int $times) 104 | { 105 | switch ($this->periodType) { 106 | case PeriodType::DAY: 107 | if ($times <= 1 || $times > 999) { 108 | throw new \LogicException('unsupported times in day'); 109 | } 110 | break; 111 | case PeriodType::MONTH: 112 | if ($times <= 1 || $times > 99) { 113 | throw new \LogicException('unsupported times in month'); 114 | } 115 | break; 116 | case PeriodType::YEAR: 117 | if ($times <= 1 || $times > 9) { 118 | throw new \LogicException('unsupported times in year'); 119 | } 120 | break; 121 | default: 122 | throw new \LogicException('unsupported times'); 123 | } 124 | 125 | $this->times = $times; 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /Tests/MerchantTest.php: -------------------------------------------------------------------------------- 1 | getMockBuilder(OrderInterface::class) 27 | ->getMock(); 28 | 29 | $order->expects($this->once()) 30 | ->method('getMerchantTradeNo') 31 | ->willReturn('ecpay20130312153023'); 32 | 33 | $order->expects($this->once()) 34 | ->method('getTradeDesc') 35 | ->willReturn('促銷方案'); 36 | 37 | $order->expects($this->once()) 38 | ->method('getItemName') 39 | ->willReturn('Apple iphone 7 手機殼'); 40 | 41 | $order->expects($this->once()) 42 | ->method('getTotalAmount') 43 | ->willReturn(1000); 44 | 45 | $info = $this->getMockBuilder(BasicInfo::class) 46 | ->disableOriginalConstructor() 47 | ->getMock(); 48 | 49 | $info->expects($this->once()) 50 | ->method('getInfo') 51 | ->willReturn([ 52 | 'MerchantID' => $merchantId, 53 | 'MerchantTradeNo' => $order->getMerchantTradeNo(), 54 | 'MerchantTradeDate' => '2013/03/12 15:30:23', 55 | 'PaymentType' => 'aio', 56 | 'TotalAmount' => $order->getTotalAmount(), 57 | 'TradeDesc' => $order->getTradeDesc(), 58 | 'ItemName' => $order->getItemName(), 59 | 'ReturnURL' => $returnUrl, 60 | 'ChoosePayment' => PaymentType::ALL, 61 | 'EncryptType' => 1, 62 | ]); 63 | 64 | $expected = 'CFA9BDE377361FBDD8F160274930E815D1A8A2E3E80CE7D404C45FC9A0A1E407'; 65 | 66 | //act 67 | $result = $merchant->countChecksum($info); 68 | 69 | //assert 70 | $this->assertEquals($expected, $result); 71 | } 72 | 73 | public function test_validateResponse() 74 | { 75 | //arrange 76 | $merchantId = '2000132'; 77 | 78 | $hashKey = '5294y06JbISpM5x9'; 79 | 80 | $hashIv = 'v77hoKGq4kWxNNIS'; 81 | 82 | $merchant = new Merchant($merchantId, $hashKey, $hashIv); 83 | 84 | $str = '{"CustomField1":"abc","CustomField2":"def","CustomField3":"fff","CustomField4":null,"MerchantID":"2000132","MerchantTradeNo":"1589634536","PaymentDate":"2020/05/16 21:09:36","PaymentType":"Credit_CreditCard","PaymentTypeChargeFee":"20","RtnCode":"1","RtnMsg":"交易成功","SimulatePaid":"0","StoreID":null,"TradeAmt":"1000","TradeDate":"2020/05/16 21:08:56","TradeNo":"2005162108564884","CheckMacValue":"1FE882456C87E84D69CD9386B886E70BAD482513D0742D828AE724AEEA4AACFA"}'; 85 | $payload = json_decode($str, true); 86 | 87 | //act 88 | $result = $merchant->setRawData($payload) 89 | ->validateResponse(); 90 | 91 | //assert 92 | $this->assertTrue($result); 93 | } 94 | 95 | public function test_validateResponse_false() 96 | { 97 | //arrange 98 | $merchantId = '2000132'; 99 | 100 | $hashKey = 'forge.key'; 101 | 102 | $hashIv = 'forge.iv'; 103 | 104 | $merchant = new Merchant($merchantId, $hashKey, $hashIv); 105 | 106 | $str = '{"CustomField1":"abc","CustomField2":"def","CustomField3":"fff","CustomField4":null,"MerchantID":"2000132","MerchantTradeNo":"1589634536","PaymentDate":"2020/05/16 21:09:36","PaymentType":"Credit_CreditCard","PaymentTypeChargeFee":"20","RtnCode":"1","RtnMsg":"交易成功","SimulatePaid":"0","StoreID":null,"TradeAmt":"1000","TradeDate":"2020/05/16 21:08:56","TradeNo":"2005162108564884","CheckMacValue":"1FE882456C87E84D69CD9386B886E70BAD482513D0742D828AE724AEEA4AACFA"}'; 107 | $payload = json_decode($str, true); 108 | 109 | //act 110 | $result = $merchant->setRawData($payload) 111 | ->validateResponse(); 112 | 113 | //assert 114 | $this->assertFalse($result); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /Ecpay.php: -------------------------------------------------------------------------------- 1 | 52 | 53 | 54 | 55 | 56 | 57 | {$this->generateForm($info)} 58 | 62 | 63 | 64 | EOT; 65 | } 66 | 67 | public function checkoutForApi(Info $info) 68 | { 69 | $url = $this->isProduction ? static::CHECKOUT_URL_PRODUCTION : static::CHECKOUT_URL_TEST; 70 | 71 | $formParams = [ 72 | 'CheckMacValue' => $this->merchant->countChecksum($info) 73 | ]; 74 | 75 | foreach ($info->getInfo() as $key => $value) { 76 | $formParams[$key] = $value; 77 | } 78 | 79 | return [ 80 | 'url' => $url, 81 | 'form_params' => $formParams, 82 | ]; 83 | } 84 | 85 | /** 86 | * @param OrderInterface $order 87 | * @param string|null $platformId 88 | * @return array 89 | */ 90 | public function query(OrderInterface $order, string $platformId = null) 91 | { 92 | if (! $this->merchant) { 93 | throw new \LogicException('empty merchant'); 94 | } 95 | 96 | $url = $this->isProduction? static::QUERY_URL_PRODUCTION: static::QUERY_URL_TEST; 97 | 98 | $payload = [ 99 | 'MerchantID' => $this->merchant->getId(), 100 | 'MerchantTradeNo' => $order->getMerchantTradeNo(), 101 | 'TimeStamp' => time(), 102 | ]; 103 | 104 | if ($platformId) { 105 | $payload['PlatformID'] = $platformId; 106 | } 107 | 108 | $payload['CheckMacValue'] = $this->merchant->countChecksumByArray($payload); 109 | 110 | $ch = curl_init($url); 111 | curl_setopt($ch, CURLOPT_POST, 1); 112 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); 113 | curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($payload)); 114 | 115 | $resp = curl_exec($ch); 116 | curl_close($ch); 117 | 118 | parse_str($resp, $result); 119 | return $result; 120 | } 121 | 122 | public function generateForm(Info $info) 123 | { 124 | $url = $this->isProduction? static::CHECKOUT_URL_PRODUCTION: static::CHECKOUT_URL_TEST; 125 | 126 | $checksum = $this->merchant->countChecksum($info); 127 | 128 | $form = ""; 136 | 137 | return $form; 138 | } 139 | 140 | /** 141 | * @param Merchant $merchant 142 | * @return $this 143 | */ 144 | public function setMerchant(Merchant $merchant) 145 | { 146 | $this->merchant = $merchant; 147 | 148 | return $this; 149 | } 150 | 151 | /** 152 | * @param bool $isProduction 153 | * @return $this 154 | */ 155 | public function setIsProduction(bool $isProduction) 156 | { 157 | $this->isProduction = $isProduction; 158 | 159 | return $this; 160 | } 161 | } 162 | --------------------------------------------------------------------------------