├── .phpunit.cache └── test-results ├── composer.json ├── src ├── Billplz.php ├── BillplzServiceProvider.php ├── Exceptions │ └── ValidationException.php ├── Http │ └── Requests │ │ ├── PaymentCompletion.php │ │ ├── Redirection.php │ │ └── Webhook.php └── Testing │ ├── Concerns │ └── PreparesBillplz.php │ ├── RedirectionTests.php │ └── WebhookTests.php └── testbench.yaml /.phpunit.cache/test-results: -------------------------------------------------------------------------------- 1 | {"version":"pest_2.34.6","defects":[],"times":{"P\\Tests\\Feature\\BillplzServiceProviderTest::__pest_evaluable_it_has_proper_signature":0.046,"P\\Tests\\Feature\\BillplzServiceProviderTest::__pest_evaluable_it_provides_the_service":0.017,"P\\Tests\\Feature\\BillplzServiceProviderTest::__pest_evaluable_it_can_configure_api_version":0,"P\\Tests\\Feature\\BillplzServiceProviderTest::__pest_evaluable_it_can_use_sandbox_environment":0,"P\\Tests\\Feature\\PaymentCompletion\\RedirectionTest::__pest_evaluable_it_can_accept_redirection_callback":0.045,"P\\Tests\\Feature\\PaymentCompletion\\RedirectionTest::__pest_evaluable_it_can_accept_redirection_callback_with_extra_payment_info":0.002,"P\\Tests\\Feature\\PaymentCompletion\\RedirectionTest::__pest_evaluable_it_can_accept_redirection_callback_without_signature":0.001,"P\\Tests\\Feature\\PaymentCompletion\\RedirectionTest::__pest_evaluable_it_cant_accept_redirection_callback_with_invalid_signature":0.022,"P\\Tests\\Feature\\PaymentCompletion\\RedirectionTest::__pest_evaluable_it_cant_accept_redirection_callback_given_bad_data":0.011,"P\\Tests\\Feature\\PaymentCompletion\\WebhookTest::__pest_evaluable_it_can_accept_webhook_callback":0.006,"P\\Tests\\Feature\\PaymentCompletion\\WebhookTest::__pest_evaluable_it_can_accept_webhook_callback_with_extra_payment_info":0.002,"P\\Tests\\Feature\\PaymentCompletion\\WebhookTest::__pest_evaluable_it_can_accept_webhook_callback_when_phone_number_is_null":0.003,"P\\Tests\\Feature\\PaymentCompletion\\WebhookTest::__pest_evaluable_it_can_accept_webhook_callback_without_signature":0.002,"P\\Tests\\Feature\\PaymentCompletion\\WebhookTest::__pest_evaluable_it_cant_accept_webhook_callback_with_invalid_signature":0.002,"P\\Tests\\Feature\\PaymentCompletion\\WebhookTest::__pest_evaluable_it_cant_accept_webhook_callback_given_invalid_data":0.003,"P\\Tests\\Unit\\Exceptions\\ValidationExceptionTest::__pest_evaluable_it_has_proper_signature":0.014,"P\\Tests\\Unit\\Exceptions\\ValidationExceptionTest::__pest_evaluable_it_can_override_error_bag":0,"P\\Tests\\Unit\\Exceptions\\ValidationExceptionTest::__pest_evaluable_it_can_override_status_code":0}} -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jomweb/billplz-laravel", 3 | "description": "Laravel adapter for Billplz", 4 | "type": "library", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Mior Muhammad Zaki", 9 | "email": "crynobone@gmail.com" 10 | } 11 | ], 12 | "autoload": { 13 | "psr-4": { 14 | "Billplz\\Laravel\\": "src/" 15 | } 16 | }, 17 | "autoload-dev": { 18 | "psr-4": { 19 | "Billplz\\Laravel\\Tests\\": "tests/" 20 | } 21 | }, 22 | "require": { 23 | "php": "^8.1", 24 | "guzzlehttp/guzzle": "^7.2", 25 | "illuminate/support": "^10.0 || ^11.0", 26 | "jomweb/billplz": "^5.2", 27 | "php-http/guzzle7-adapter": "^1.0" 28 | }, 29 | "require-dev": { 30 | "larastan/larastan": "^2.0", 31 | "nunomaduro/collision": "^7.5 || ^8.0", 32 | "orchestra/pest-plugin-testbench": "^2.0", 33 | "orchestra/testbench": "^8.22 || ^9.0", 34 | "phpunit/phpunit": "^10.5" 35 | }, 36 | "config": { 37 | "sort-packages": true, 38 | "allow-plugins": { 39 | "php-http/discovery": true, 40 | "pestphp/pest-plugin": true 41 | } 42 | }, 43 | "extra": { 44 | "branch-alias": { 45 | "dev-master": "4.x-dev" 46 | }, 47 | "laravel": { 48 | "providers": [ 49 | "Billplz\\Laravel\\BillplzServiceProvider" 50 | ], 51 | "aliases": { 52 | "Billplz": "Billplz\\Laravel\\Billplz" 53 | } 54 | } 55 | }, 56 | "prefer-stable": true, 57 | "minimum-stability": "dev" 58 | } 59 | -------------------------------------------------------------------------------- /src/Billplz.php: -------------------------------------------------------------------------------- 1 | app->scoped('billplz', function (Container $app) { 22 | /** @var array{key: string, x-signature: string|null, sandbox: bool|null, version: string|null} $config */ 23 | $config = transform($app->make('config'), function (Repository $repository) { 24 | return $repository->get('services.billplz'); 25 | }); 26 | 27 | return $this->createBillplzClient($config); 28 | }); 29 | 30 | $this->app->alias('billplz', Client::class); 31 | } 32 | 33 | /** 34 | * Create Billplz Client. 35 | * 36 | * @param array{key: string, x-signature: string|null, sandbox: bool|null, version: string|null} $config 37 | * @return \Billplz\Client 38 | */ 39 | protected function createBillplzClient(array $config) 40 | { 41 | $signature = $config['x-signature'] ?? null; 42 | $sandbox = $config['sandbox'] ?? false; 43 | 44 | $billplz = new Client($this->createHttpClient(), $config['key'], $signature); 45 | 46 | $billplz->useVersion($config['version'] ?? 'v4'); 47 | 48 | if ($sandbox == true) { 49 | $billplz->useSandbox(); 50 | } 51 | 52 | return $billplz; 53 | } 54 | 55 | /** 56 | * Create HTTP Client. 57 | * 58 | * @return \Http\Client\Common\HttpMethodsClient 59 | */ 60 | protected function createHttpClient() 61 | { 62 | return Discovery::client(); 63 | } 64 | 65 | /** 66 | * Get the services provided by the provider. 67 | * 68 | * @return array 69 | */ 70 | public function provides() 71 | { 72 | return [ 73 | 'billplz', 74 | Client::class, 75 | ]; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Exceptions/ValidationException.php: -------------------------------------------------------------------------------- 1 | response = $response; 51 | $this->errorBag = $errorBag; 52 | $this->validator = $validator; 53 | } 54 | 55 | /** 56 | * Get all of the validation error messages. 57 | * 58 | * @return array 59 | */ 60 | public function errors() 61 | { 62 | return $this->validator->errors()->messages(); 63 | } 64 | 65 | /** 66 | * Set the HTTP status code to be used for the response. 67 | * 68 | * @param int $statusCode 69 | * @return $this 70 | */ 71 | public function status($statusCode) 72 | { 73 | $this->statusCode = $statusCode; 74 | 75 | return $this; 76 | } 77 | 78 | /** 79 | * Set the error bag on the exception. 80 | * 81 | * @param string $errorBag 82 | * @return $this 83 | */ 84 | public function errorBag($errorBag) 85 | { 86 | $this->errorBag = $errorBag; 87 | 88 | return $this; 89 | } 90 | 91 | /** 92 | * Get the underlying response instance. 93 | * 94 | * @return \Symfony\Component\HttpFoundation\Response|null 95 | */ 96 | public function getResponse() 97 | { 98 | return $this->response; 99 | } 100 | 101 | /** 102 | * Get the response status code. 103 | */ 104 | public function getStatusCode(): int 105 | { 106 | return $this->statusCode; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/Http/Requests/PaymentCompletion.php: -------------------------------------------------------------------------------- 1 | errorBag); 28 | } 29 | 30 | /** 31 | * Get client instance. 32 | */ 33 | public function getClientInstance(): Client 34 | { 35 | if (! isset($this->billplzClient)) { 36 | /** @var \Billplz\Client $client */ 37 | $client = $this->container->make('billplz'); 38 | 39 | $this->billplzClient = $client; 40 | } 41 | 42 | return $this->billplzClient; 43 | } 44 | 45 | /** 46 | * Get resource instance. 47 | */ 48 | public function getResourceInstance(): Bill 49 | { 50 | return $this->getClientInstance()->bill(); 51 | } 52 | 53 | /** 54 | * Check if Billplz if configured with signature. 55 | */ 56 | protected function hasSignatureKey(): bool 57 | { 58 | return ! \is_null($this->getClientInstance()->getSignatureKey()); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Http/Requests/Redirection.php: -------------------------------------------------------------------------------- 1 | > 15 | */ 16 | public function rules(): array 17 | { 18 | $rules = [ 19 | 'billplz.id' => ['required', 'alpha_dash'], 20 | ]; 21 | 22 | if ($this->hasSignatureKey()) { 23 | $rules['billplz.paid'] = ['required', Rule::in(['true', 'false', true, false])]; 24 | $rules['billplz.paid_at'] = ['nullable', 'date']; 25 | $rules['billplz.x_signature'] = ['required']; 26 | $rules['billplz.transaction_id'] = ['sometimes', 'nullable']; 27 | $rules['billplz.transaction_status'] = ['sometimes', 'nullable', Rule::in('pending', 'completed', 'failed')]; 28 | } 29 | 30 | return $rules; 31 | } 32 | 33 | /** 34 | * Get the validated data from the request. 35 | * 36 | * @param string|null $key 37 | * @param string|array|null $default 38 | */ 39 | public function validated($key = null, $default = null): array 40 | { 41 | try { 42 | $validated = $this->getResourceInstance()->redirect($this->query()); 43 | } catch (FailedSignatureVerification $e) { 44 | throw new HttpException(419, 'Unable to verify X-Signature.', $e); 45 | } 46 | 47 | return $validated; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Http/Requests/Webhook.php: -------------------------------------------------------------------------------- 1 | > 15 | */ 16 | public function rules(): array 17 | { 18 | return [ 19 | 'id' => ['required', 'alpha_dash'], 20 | 'collection_id' => ['required'], 21 | 'amount' => ['required', 'numeric'], 22 | 'state' => ['required', 'string'], 23 | 'paid' => ['required', Rule::in(['true', 'false', true, false])], 24 | 'paid_at' => ['nullable', 'date'], 25 | 'paid_amount' => ['required', 'numeric'], 26 | 'x_signature' => [$this->hasSignatureKey() ? 'required' : 'sometimes'], 27 | 'transaction_id' => ['sometimes', 'nullable'], 28 | 'transaction_status' => ['sometimes', 'nullable', Rule::in('pending', 'completed', 'failed')], 29 | ]; 30 | } 31 | 32 | /** 33 | * Get the validated data from the request. 34 | * 35 | * @param string|null $key 36 | * @param string|array|null $default 37 | */ 38 | public function validated($key = null, $default = null): array 39 | { 40 | try { 41 | $validated = $this->getResourceInstance()->webhook($this->post()); 42 | } catch (FailedSignatureVerification $e) { 43 | throw new HttpException(419, 'Unable to verify X-Signature.', $e); 44 | } 45 | 46 | return $validated; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Testing/Concerns/PreparesBillplz.php: -------------------------------------------------------------------------------- 1 | app['config']->set('services.billplz', [ 13 | 'key' => '73eb57f0-7d4e-42b9-a544-aeac6e4b0f81', 14 | 'version' => 'v4', 15 | 'x-signature' => 'secret', 16 | 'sandbox' => false, 17 | ]); 18 | } 19 | 20 | /** 21 | * Prepare configuration without signature. 22 | */ 23 | protected function prepareConfigurationWithoutSignature(): void 24 | { 25 | $this->app['config']->set('services.billplz', [ 26 | 'key' => '73eb57f0-7d4e-42b9-a544-aeac6e4b0f81', 27 | 'version' => 'v4', 28 | 'x-signature' => null, 29 | 'sandbox' => false, 30 | ]); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Testing/RedirectionTests.php: -------------------------------------------------------------------------------- 1 | prepareConfiguration(); 19 | 20 | $data = [ 21 | 'billplz' => array_merge( 22 | [ 23 | 'id' => 'W_79pJDk', 24 | 'paid' => 'true', 25 | 'paid_at' => '2015-03-09 16:23:59 +0800', 26 | ], 27 | $payload, 28 | ), 29 | ]; 30 | 31 | $signature = new Signature(config('services.billplz.x-signature'), Signature::REDIRECT_PARAMETERS); 32 | 33 | $encodedData = collect($data['billplz']) 34 | ->mapWithKeys(function ($value, $key) { 35 | return ["billplz$key" => $value]; 36 | }) 37 | ->toArray(); 38 | 39 | $data['billplz']['x_signature'] = $signature->create($encodedData); 40 | 41 | $query = http_build_query($data); 42 | 43 | return $this->get("{$uri}?{$query}")->assertStatus(200); 44 | } 45 | 46 | /** 47 | * Make successful redirection without x-signature. 48 | * 49 | * @return \Illuminate\Testing\TestResponse 50 | */ 51 | protected function makeSuccessfulRedirectionWithoutSignature(string $uri) 52 | { 53 | $this->prepareConfigurationWithoutSignature(); 54 | 55 | $data = [ 56 | 'billplz' => [ 57 | 'id' => 'W_79pJDk', 58 | ], 59 | ]; 60 | 61 | $query = http_build_query($data); 62 | 63 | return $this->get("{$uri}?{$query}")->assertStatus(200); 64 | } 65 | 66 | /** 67 | * Make unsuccessful redirection. 68 | * 69 | * @return \Illuminate\Testing\TestResponse 70 | */ 71 | protected function makeUnsuccessfulRedirection(string $uri) 72 | { 73 | $this->prepareConfiguration(); 74 | 75 | $data = [ 76 | 'billplz' => [ 77 | 'id' => 'W_79pJDk', 78 | 'paid' => 'true', 79 | 'paid_at' => '2015-03-09 16:23:59 +0800', 80 | ], 81 | ]; 82 | 83 | $query = http_build_query($data); 84 | 85 | return $this->get("{$uri}?{$query}")->assertStatus(422); 86 | } 87 | 88 | /** 89 | * Make unsuccessful redirection with invalid x-signature. 90 | * 91 | * @return \Illuminate\Testing\TestResponse 92 | */ 93 | protected function makeUnsuccessfulRedirectionWithInvalidSignature(string $uri) 94 | { 95 | $this->prepareConfiguration(); 96 | 97 | $data = [ 98 | 'billplz' => [ 99 | 'id' => 'W_79pJDk', 100 | 'paid' => 'true', 101 | 'paid_at' => '2015-03-09 16:23:59 +0800', 102 | 'x_signature' => '01bdc1167f8b4dd1f591d8af7ada00061d39ca2b63e66c6588474a918a04796c', 103 | ], 104 | ]; 105 | 106 | $query = http_build_query($data); 107 | 108 | return $this->get("{$uri}?{$query}")->assertStatus(419); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/Testing/WebhookTests.php: -------------------------------------------------------------------------------- 1 | prepareConfiguration(); 19 | 20 | $data = array_merge([ 21 | 'id' => 'W_79pJDk', 22 | 'collection_id' => '599', 23 | 'paid' => 'true', 24 | 'state' => 'paid', 25 | 'amount' => '200', 26 | 'paid_amount' => '200', 27 | 'due_at' => '2020-12-31', 28 | 'email' => 'api@billplz.com', 29 | 'mobile' => '+60112223333', 30 | 'name' => 'MICHAEL API', 31 | 'url' => 'http://billplz.dev/bills/W_79pJDk', 32 | 'paid_at' => '2015-03-09 16:23:59 +0800', 33 | ], $payload); 34 | 35 | $signature = new Signature(config('services.billplz.x-signature'), Signature::WEBHOOK_PARAMETERS); 36 | 37 | $data['x_signature'] = $signature->create($data); 38 | 39 | return $this->post($uri, $data, ['Content-Type' => 'application/x-www-form-urlencoded']) 40 | ->assertStatus(200); 41 | } 42 | 43 | /** 44 | * Make successful webhook without signature. 45 | * 46 | * @return \Illuminate\Testing\TestResponse 47 | */ 48 | protected function makeSuccessfulWebhookWithoutSignature(string $uri, array $payload = []) 49 | { 50 | $this->prepareConfigurationWithoutSignature(); 51 | 52 | $data = array_merge([ 53 | 'id' => 'W_79pJDk', 54 | 'collection_id' => '599', 55 | 'paid' => 'true', 56 | 'state' => 'paid', 57 | 'amount' => '200', 58 | 'paid_amount' => '200', 59 | 'due_at' => '2020-12-31', 60 | 'email' => 'api@billplz.com', 61 | 'mobile' => '+60112223333', 62 | 'name' => 'MICHAEL API', 63 | 'url' => 'http://billplz.dev/bills/W_79pJDk', 64 | 'paid_at' => '2015-03-09 16:23:59 +0800', 65 | ], $payload); 66 | 67 | return $this->post($uri, $data, ['Content-Type' => 'application/x-www-form-urlencoded']) 68 | ->assertStatus(200); 69 | } 70 | 71 | /** 72 | * Make unsuccessful webhook. 73 | * 74 | * @return \Illuminate\Testing\TestResponse 75 | */ 76 | protected function makeUnsuccessfulWebhook(string $uri) 77 | { 78 | $data = [ 79 | 'id' => 'W_79pJDk', 80 | 'collection_id' => '599', 81 | 'paid' => 'true', 82 | 'state' => 'paid', 83 | 'amount' => '200', 84 | 'paid_amount' => '0', 85 | 'due_at' => '2020-12-31', 86 | 'email' => 'api@billplz.com', 87 | 'mobile' => '+60112223333', 88 | 'name' => 'MICHAEL API', 89 | 'url' => 'http://billplz.dev/bills/W_79pJDk', 90 | 'paid_at' => '2015-03-09 16:23:59 +0800', 91 | ]; 92 | 93 | return $this->post($uri, $data, ['Content-Type' => 'application/x-www-form-urlencoded']) 94 | ->assertStatus(422); 95 | } 96 | 97 | /** 98 | * Make unsuccessful webhook with invalid signature. 99 | * 100 | * @return \Illuminate\Testing\TestResponse 101 | */ 102 | protected function makeUnsuccessfulWebhookWithInvalidSignature(string $uri) 103 | { 104 | $this->prepareConfiguration(); 105 | 106 | $data = [ 107 | 'id' => 'W_79pJDk', 108 | 'collection_id' => '599', 109 | 'paid' => 'true', 110 | 'state' => 'paid', 111 | 'amount' => '200', 112 | 'paid_amount' => '200', 113 | 'due_at' => '2020-12-31', 114 | 'email' => 'api@billplz.com', 115 | 'mobile' => '+60112223333', 116 | 'name' => 'MICHAEL API', 117 | 'url' => 'http://billplz.dev/bills/W_79pJDk', 118 | 'paid_at' => '2015-03-09 16:23:59 +0800', 119 | 'x_signature' => '01bdc1167f8b4dd1f591d8af7ada00061d39ca2b63e66c6588474a918a04796c', 120 | ]; 121 | 122 | return $this->post($uri, $data, ['Content-Type' => 'application/x-www-form-urlencoded']) 123 | ->assertStatus(419); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /testbench.yaml: -------------------------------------------------------------------------------- 1 | providers: 2 | - Billplz\Laravel\BillplzServiceProvider 3 | 4 | migrations: false 5 | --------------------------------------------------------------------------------