├── 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 |
--------------------------------------------------------------------------------