├── src ├── Exception │ ├── DANAException.php │ ├── DANACoreException.php │ ├── DANACreateOrderException.php │ ├── DANAPayGetTokenException.php │ ├── DANASignSignatureException.php │ ├── DANAPayUnBindingAllException.php │ └── DANAVerifySignatureException.php ├── Facades │ ├── DANACalculation.php │ ├── DANACore.php │ └── DANAPay.php ├── Validation │ └── Validation.php ├── DanaCoreServiceProvider.php ├── Helpers │ ├── Calculation.php │ └── CreateOrder.php └── Services │ ├── DANACoreService.php │ └── DANAPayService.php ├── .gitignore ├── .github ├── FUNDING.yml └── workflows │ └── tests.yml ├── tests ├── Feature │ └── DANAPayFacadeTest.php ├── Unit │ ├── DANAPayServiceTest.php │ └── CalculationTest.php └── TestCase.php ├── phpunit.xml ├── composer.json ├── CHANGELOG.md ├── config └── dana.php └── README.md /src/Exception/DANAException.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(DANAPayService::class, $service); 14 | } 15 | 16 | public function test_can_generate_oauth_url_via_service(): void 17 | { 18 | $service = new DANAPayService(); 19 | $url = $service->generateOauthUrl('WEB', 'https://example.com/callback'); 20 | 21 | $this->assertStringContainsString('https://m.sandbox.dana.id/d/portal/oauth', $url); 22 | $this->assertStringContainsString('clientId=test_client_id', $url); 23 | $this->assertStringContainsString('terminalType=WEB', $url); 24 | $this->assertStringContainsString('redirectUrl=https%3A%2F%2Fexample.com%2Fcallback', $url); 25 | } 26 | 27 | public function test_can_access_service_via_container(): void 28 | { 29 | $service = app('DANAPay'); 30 | $this->assertInstanceOf(DANAPayService::class, $service); 31 | } 32 | } -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | ./tests/Unit 10 | 11 | 12 | ./tests/Feature 13 | 14 | 15 | 16 | 17 | ./src 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/DanaCoreServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->singleton('DANACore', DANACoreService::class); 19 | $this->app->singleton('DANAPay', DANAPayService::class); 20 | $this->app->singleton('DANACalculation', Calculation::class); 21 | } 22 | 23 | /** 24 | * Bootstrap services. 25 | */ 26 | public function boot(): void 27 | { 28 | // Publish configuration file 29 | $this->publishes([ 30 | __DIR__ . '/../config/dana.php' => config_path('dana.php'), 31 | ], 'dana-config'); 32 | 33 | // Load configuration if not already loaded 34 | if (!$this->app->configurationIsCached()) { 35 | $this->mergeConfigFrom(__DIR__ . '/../config/dana.php', 'dana'); 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /tests/Unit/DANAPayServiceTest.php: -------------------------------------------------------------------------------- 1 | generateOauthUrl('WEB', 'https://example.com/callback'); 14 | 15 | $this->assertStringContainsString('https://m.sandbox.dana.id/d/portal/oauth', $url); 16 | $this->assertStringContainsString('clientId=test_client_id', $url); 17 | $this->assertStringContainsString('terminalType=WEB', $url); 18 | $this->assertStringContainsString('redirectUrl=https%3A%2F%2Fexample.com%2Fcallback', $url); 19 | } 20 | 21 | public function test_can_generate_oauth_url_with_different_terminal_types(): void 22 | { 23 | $service = new DANAPayService(); 24 | 25 | $url1 = $service->generateOauthUrl('APP', 'https://example.com/callback'); 26 | $this->assertStringContainsString('terminalType=APP', $url1); 27 | 28 | $url2 = $service->generateOauthUrl('WAP', 'https://example.com/callback'); 29 | $this->assertStringContainsString('terminalType=WAP', $url2); 30 | 31 | $url3 = $service->generateOauthUrl('SYSTEM', 'https://example.com/callback'); 32 | $this->assertStringContainsString('terminalType=SYSTEM', $url3); 33 | } 34 | 35 | public function test_can_create_service_instance(): void 36 | { 37 | $service = new DANAPayService(); 38 | $this->assertInstanceOf(DANAPayService::class, $service); 39 | } 40 | } -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "otnansirk/laravel-dana", 3 | "description": "This Laravel wrapper for DANA Payment API", 4 | "keywords": [ 5 | "otnansirk", 6 | "laravel-DANA", 7 | "DANA Laravel", 8 | "payment gateway", 9 | "dana payment gateway", 10 | "payment" 11 | ], 12 | "support": { 13 | "issues": "https://github.com/otnansirk/laravel-dana/issues", 14 | "source": "https://github.com/otnansirk/laravel-dana" 15 | }, 16 | "authors": [ 17 | { 18 | "name": "otnansirk", 19 | "email": "iam.otnansirk@gmail.com" 20 | } 21 | ], 22 | "type": "library", 23 | "minimum-stability": "dev", 24 | "require": { 25 | "php": "^8.1", 26 | "laravel/framework": "^9.19|^10.0|^11.0|^12.0", 27 | "guzzlehttp/guzzle": "^7.0" 28 | }, 29 | "require-dev": { 30 | "orchestra/testbench": "^7.0|^8.0|^9.0", 31 | "phpunit/phpunit": "^9.0|^10.0" 32 | }, 33 | "license": "MIT", 34 | "autoload": { 35 | "psr-4": { 36 | "Otnansirk\\Dana\\": "src/" 37 | } 38 | }, 39 | "autoload-dev": { 40 | "psr-4": { 41 | "Tests\\": "tests/" 42 | } 43 | }, 44 | "extra": { 45 | "laravel": { 46 | "providers": [ 47 | "Otnansirk\\Dana\\DanaCoreServiceProvider" 48 | ] 49 | } 50 | }, 51 | "config": { 52 | "sort-packages": true, 53 | "allow-plugins": { 54 | "pestphp/pest-plugin": true, 55 | "php-http/discovery": true 56 | } 57 | }, 58 | "replace": { 59 | "paragonie/random_compat": "2.*" 60 | } 61 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## [3.0.0](https://github.com/otnansirk/laravel-dana/releases/tag/v3.0.0) — 2025-08-14 11 | 12 | ### Feat 13 | - Added support for Laravel 9.x, 10.x, 11.x, and 12.x 14 | - Updated PHP requirement to 8.1+ 15 | - Improved service provider with configuration merging 16 | - Enhanced testing with Orchestra Testbench 17 | - Added comprehensive version compatibility matrix 18 | 19 | ### Changed 20 | - Updated composer.json to support multiple Laravel versions 21 | - Updated service provider to use version-agnostic patterns 22 | - Simplified test setup to work with Orchestra Testbench 23 | 24 | ## [2.3.0](https://github.com/otnansirk/laravel-dana/releases/tag/v2.3.0) — 2024-08-07 25 | - **Feat:** Added support for Laravel 10 and 11 26 | 27 | ## [2.2.1](https://github.com/otnansirk/laravel-dana/releases/tag/v2.2.1) — 2024-01-25 28 | - **Fix:** Resolved issue with missing or incorrect transaction status information 29 | 30 | ## [2.2.0](https://github.com/otnansirk/laravel-dana/releases/tag/v2.2.0) — 2024-01-25 31 | - **Feature:** Added ability to query orders for checking transaction status 32 | 33 | ## [2.1.2](https://github.com/otnansirk/laravel-dana/releases/tag/v2.1.2) — 2023-07-31 34 | - **Feature:** Introduced callback for MDR (Merchant Discount Rate) calculation 35 | - **Bug Fixes:** 36 | - Made `merchantTransId` dynamic 37 | - Fixed MDR calculation in callback function 38 | 39 | ## [2.0.0](https://github.com/otnansirk/laravel-dana/releases/tag/v2.0.0) — 2023-07-17 40 | - **Feature:** Added support for Laravel 9 41 | 42 | ## [1.0.0](https://github.com/otnansirk/laravel-dana/releases/tag/v1.0.0) — 2022-11-01 43 | - **Initial Release:** First release :) -------------------------------------------------------------------------------- /tests/Unit/CalculationTest.php: -------------------------------------------------------------------------------- 1 | taxValue(); 14 | 15 | $this->assertIsFloat($taxValue); 16 | $this->assertEquals(0.11, $taxValue); 17 | } 18 | 19 | public function test_can_get_fees(): void 20 | { 21 | $calculation = new Calculation(); 22 | $fees = $calculation->fees(); 23 | 24 | $this->assertIsArray($fees); 25 | $this->assertArrayHasKey('CREDIT_CARD', $fees); 26 | $this->assertArrayHasKey('DEBIT_CARD', $fees); 27 | $this->assertArrayHasKey('BALANCE', $fees); 28 | $this->assertArrayHasKey('DIRECT_DEBIT_CREDIT_CARD', $fees); 29 | $this->assertArrayHasKey('DIRECT_DEBIT_DEBIT_CARD', $fees); 30 | $this->assertArrayHasKey('VIRTUAL_ACCOUNT', $fees); 31 | $this->assertArrayHasKey('ONLINE_CREDIT', $fees); 32 | } 33 | 34 | public function test_can_calculate_mdr_for_balance(): void 35 | { 36 | $calculation = new Calculation(); 37 | $result = $calculation->calculateMDR(100000, 'BALANCE'); 38 | 39 | $this->assertIsArray($result); 40 | $this->assertArrayHasKey('mdr_percent', $result); 41 | $this->assertArrayHasKey('mdr_before_tax', $result); 42 | $this->assertArrayHasKey('mdr_include_tax', $result); 43 | $this->assertArrayHasKey('tax_percent', $result); 44 | $this->assertArrayHasKey('tax', $result); 45 | $this->assertArrayHasKey('payment_method', $result); 46 | $this->assertArrayHasKey('settle_amount', $result); 47 | $this->assertEquals('BALANCE', $result['payment_method']); 48 | $this->assertEquals(0.012, $result['mdr_percent']); 49 | } 50 | 51 | public function test_can_calculate_mdr_for_credit_card(): void 52 | { 53 | $calculation = new Calculation(); 54 | $result = $calculation->calculateMDR(100000, 'CREDIT_CARD'); 55 | 56 | $this->assertIsArray($result); 57 | $this->assertEquals('CREDIT_CARD', $result['payment_method']); 58 | $this->assertEquals(0.018, $result['mdr_percent']); 59 | } 60 | } -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | set('database.default', 'testbench'); 21 | $app['config']->set('database.connections.testbench', [ 22 | 'driver' => 'sqlite', 23 | 'database' => ':memory:', 24 | 'prefix' => '', 25 | ]); 26 | 27 | // Setup DANA configuration for testing 28 | $app['config']->set('dana.env', 'development'); 29 | $app['config']->set('dana.active', false); 30 | $app['config']->set('dana.is_production', false); 31 | $app['config']->set('dana.api_url', 'https://api-sandbox.saas.dana.id'); 32 | $app['config']->set('dana.web_url', 'https://m.sandbox.dana.id'); 33 | $app['config']->set('dana.merchant_id', 'test_merchant_id'); 34 | $app['config']->set('dana.client_id', 'test_client_id'); 35 | $app['config']->set('dana.client_secret', 'test_client_secret'); 36 | $app['config']->set('dana.version', '2.0'); 37 | $app['config']->set('dana.date_format', 'Y-m-d\TH:i:sP'); 38 | $app['config']->set('dana.expired_after', 60); 39 | $app['config']->set('dana.order_notify_url', ''); 40 | $app['config']->set('dana.pay_return_url', ''); 41 | $app['config']->set('dana.ssh_public_key', 'test_public_key'); 42 | $app['config']->set('dana.ssh_private_key', 'test_private_key'); 43 | $app['config']->set('dana.fee_tax', 0.11); 44 | $app['config']->set('dana.mdr_percent.credit_card', 0.018); 45 | $app['config']->set('dana.mdr_percent.debit_card', 0.018); 46 | $app['config']->set('dana.mdr_percent.balance', 0.012); 47 | $app['config']->set('dana.mdr_percent.direct_debit_credit_card', 0.012); 48 | $app['config']->set('dana.mdr_percent.direct_debit_debit_card', 0.012); 49 | $app['config']->set('dana.mdr_percent.online_credit', 0.012); 50 | $app['config']->set('dana.mdr_before_tax.virtual_account', 2000); 51 | $app['config']->set('dana.oauth_scopes', 'CASHIER,QUERY_BALANCE,DEFAULT_BASIC_PROFILE,MINI_DANA'); 52 | $app['config']->set('dana.user_resources', ['BALANCE', 'TRANSACTION_URL', 'MASK_DANA_ID', 'TOPUP_URL', 'OTT']); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Helpers/Calculation.php: -------------------------------------------------------------------------------- 1 | [ 22 | "mdr_percent" => config("dana.mdr_percent.credit_card", 0.018), 23 | // is equal to 1.8% 24 | ], 25 | "DEBIT_CARD" => [ 26 | "mdr_percent" => config("dana.mdr_percent.debit_card", 0.018), 27 | // is equal to 1.8% 28 | ], 29 | "BALANCE" => [ 30 | "mdr_percent" => config("dana.mdr_percent.balance", 0.012), 31 | // is equal to 1.2% 32 | ], 33 | "DIRECT_DEBIT_CREDIT_CARD" => [ 34 | "mdr_percent" => config("dana.mdr_percent.direct_debit_credit_card", 0.012), 35 | // is equal to 1.2% 36 | ], 37 | "DIRECT_DEBIT_DEBIT_CARD" => [ 38 | "mdr_percent" => config("dana.mdr_percent.direct_debit_debit_card", 0.012), 39 | // is equal to 1.2% 40 | ], 41 | "VIRTUAL_ACCOUNT" => [ 42 | "mdr_before_tax" => config("dana.mdr_before_tax.virtual_account", 2000), 43 | // is equal to 2000 Rupiah 44 | ], 45 | "ONLINE_CREDIT" => [ 46 | "mdr_percent" => config("dana.mdr_percent.online_credit", 0.012), 47 | // is equal to 1.2% 48 | ] 49 | ]; 50 | } 51 | 52 | /** 53 | * Get calculation dana fee 54 | */ 55 | public function calculateMDR(int $payAmount, string $payMethod): array 56 | { 57 | $mdr = data_get($this->fees(), "$payMethod.mdr_percent", null); 58 | 59 | $mdrBeforeTax = ($mdr) ? $mdr * $payAmount : data_get($this->fees(), "$payMethod.mdr_before_tax", null); 60 | $taxValue = $mdrBeforeTax * $this->taxValue(); 61 | $mdrIncludeTax = $mdrBeforeTax + $taxValue; 62 | $settleAmount = $payAmount - $mdrIncludeTax; 63 | 64 | return [ 65 | "mdr_percent" => $mdr, 66 | "mdr_before_tax" => $mdrBeforeTax, 67 | "mdr_include_tax" => $mdrIncludeTax, 68 | "tax_percent" => $this->taxValue(), 69 | "tax" => $taxValue, 70 | "payment_method" => $payMethod, 71 | "settle_amount" => $settleAmount 72 | ]; 73 | } 74 | } -------------------------------------------------------------------------------- /config/dana.php: -------------------------------------------------------------------------------- 1 | "2.0", 6 | 7 | "env" => env('DANA_ENV', 'development'), 8 | 9 | "active" => env('DANA_ACTIVE', false), 10 | 11 | /** 12 | * True for production 13 | * false for sandbox mode 14 | * 15 | */ 16 | "is_production" => (env('DANA_ENV', 'development') == 'production') ? true : false, 17 | 18 | /** 19 | * for the API url value 20 | * example = https://api-sandbox.saas.dana.id 21 | * 22 | */ 23 | "api_url" => env('DANA_API_URL', 'https://api-sandbox.saas.dana.id'), 24 | 25 | /** 26 | * for the WEB url value 27 | * example = https://api-sandbox.saas.dana.id 28 | * 29 | */ 30 | "web_url" => env('DANA_WEB_URL', 'https://m.sandbox.dana.id'), 31 | 32 | /** 33 | * for clientId value 34 | * example = 212640060018011593493 35 | * 36 | */ 37 | "merchant_id" => env("DANA_MARCHANT_ID", ""), 38 | 39 | /** 40 | * for clientId value 41 | * example = 2018122812174155520063 42 | * 43 | */ 44 | "client_id" => env("DANA_CLIENT_ID", ""), 45 | 46 | /** 47 | * for clientSecret value 48 | * example = 3f5798274c9b427e9e0aa2c5db0a6454 49 | * 50 | */ 51 | "client_secret" => env("DANA_CLIENT_SECRET", ""), 52 | 53 | /** 54 | * for oauthRedirectUrl value 55 | * Put your redirect url for OAuth flow/account binding, to redirect the authCode 56 | * example = https://api.merchant.com/oauth-callback 57 | * 58 | */ 59 | "oauth_redirect_url" => 'https://api.merchant.com/oauth-callback', 60 | 61 | /** 62 | * for oauthScopes value 63 | * Account binding 64 | * 65 | */ 66 | "oauth_scopes" => 'CASHIER,QUERY_BALANCE,DEFAULT_BASIC_PROFILE,MINI_DANA', 67 | 68 | /** 69 | * for get user profile 70 | * user resources 71 | * 72 | */ 73 | "user_resources" => [ 74 | "BALANCE", 75 | "TRANSACTION_URL", 76 | "MASK_DANA_ID", 77 | "TOPUP_URL", 78 | "OTT" 79 | ], 80 | 81 | /** 82 | * for refundDestination value 83 | * Api configuration 84 | * 85 | */ 86 | "refund_destination" => 'TO_BALANCE', 87 | 88 | /** 89 | * For date format 90 | */ 91 | "date_format" => "Y-m-d\TH:i:sP", 92 | 93 | /** 94 | * For expired date after. Unit is minutes 95 | */ 96 | "expired_after" => 60, 97 | // Equivalent to 1 hours 98 | 99 | /** 100 | * For get notif every status order is changed 101 | */ 102 | "order_notify_url" => env("DANA_ORDER_NOTIFY_URL", ""), 103 | 104 | /** 105 | * For get redirect user to merchant website 106 | */ 107 | "pay_return_url" => env("DANA_PAY_RETURN_URL", ""), 108 | 109 | /** 110 | * Get DANA public key 111 | */ 112 | "ssh_public_key" => env("DANA_PUB_KEY", ""), 113 | 114 | /** 115 | * Get local private key 116 | */ 117 | "ssh_private_key" => env("DANA_PRIVATE_KEY", ""), 118 | 119 | /** 120 | * mdr percent update on 2023 121 | */ 122 | "mdr_percent" => [ 123 | /** 124 | * mdr persent for credit card 125 | * 0.0018 is equal to 1.8% 126 | */ 127 | "credit_card" => env("DANA_MDR_CC", 0.018), 128 | 129 | /** 130 | * mdr persent for credit card 131 | * 0.0018 is equal to 1.8% 132 | */ 133 | "debit_card" => env("DANA_MDR_DC", 0.018), 134 | 135 | /** 136 | * mdr persent for balance 137 | * 0.012 is equal to 1.2% 138 | */ 139 | "balance" => env("DANA_MDR_BALANCE", 0.012), 140 | 141 | /** 142 | * mdr persent for credit card 143 | * 0.0012 is equal to 1.2% 144 | */ 145 | "direct_debit_credit_card" => env("DANA_MDR_DD_CC", 0.012), 146 | 147 | /** 148 | * mdr persent for credit card 149 | * 0.0012 is equal to 1.2% 150 | */ 151 | "direct_debit_debit_card" => env("DANA_MDR_DD_DC", 0.012), 152 | 153 | /** 154 | * mdr persent for credit card 155 | * 0.0012 is equal to 1.2% 156 | */ 157 | "online_credit" => env("DANA_MDR_ONLINE_CREDIT", 0.012), 158 | 159 | ], 160 | "mdr_before_tax" => [ 161 | /** 162 | * mdr before tax for virtual account 163 | * 2000 is equal to 2000 Rupiah 164 | */ 165 | "online_credit" => env("DANA_MDR_BEFORE_VA", 2000) 166 | ], 167 | 168 | /** 169 | * fee tax 170 | * 171 | * 0.11 is equal to 11% 172 | */ 173 | "fee_tax" => env("DANA_FEE_TAX", 0.11) 174 | ]; -------------------------------------------------------------------------------- /src/Services/DANACoreService.php: -------------------------------------------------------------------------------- 1 | config('dana.version'), 27 | "clientId" => config('dana.client_id'), 28 | "clientSecret" => config('dana.client_secret'), 29 | "reqTime" => date(config("dana.date_format")), 30 | "reqMsgId" => Str::uuid()->toString(), 31 | "reserve" => "{}" 32 | ]; 33 | } 34 | 35 | /** 36 | * Initialize Response Header 37 | */ 38 | public static function getResHeader(): array 39 | { 40 | return [ 41 | "version" => config('dana.version'), 42 | "clientId" => config('dana.client_id'), 43 | "respTime" => date(config("dana.date_format")), 44 | "reqMsgId" => Str::uuid()->toString(), 45 | ]; 46 | } 47 | 48 | /** 49 | * Main api function to call to DANA 50 | */ 51 | public static function api(string $path, array $heads = [], array $bodys = []): self 52 | { 53 | $defaultHead = self::getReqHeader(); 54 | $request = [ 55 | "head" => array_merge($defaultHead, $heads), 56 | "body" => $bodys 57 | ]; 58 | $payloadParsedAry = [ 59 | "request" => $request, 60 | "signature" => self::signSignature($request) 61 | ]; 62 | 63 | $res = Http::post(config('dana.api_url') . $path, $payloadParsedAry); 64 | 65 | Log::info("Request DANA To " . config('dana.api_url')); 66 | Log::info($payloadParsedAry); 67 | Log::info("Response DANA"); 68 | Log::info($res->json()); 69 | 70 | if ($res->failed()) { 71 | Log::critical("Error when request dana dana.oauth.auth.applyToken"); 72 | Log::critical($res->json()); 73 | throw new DANACoreException("Error Processing Request DANA", 400); 74 | } 75 | 76 | self::$danaData = $res; 77 | self::$heads = $heads; 78 | self::$bodys = $bodys; 79 | return new self; 80 | } 81 | 82 | /** 83 | * Return all response from http client as is 84 | */ 85 | public function all(): Response 86 | { 87 | return self::$danaData; 88 | } 89 | 90 | /** 91 | * Return only message code and status from dana API 92 | */ 93 | public function message(): object 94 | { 95 | $data = json_decode(self::$danaData->body())->response; 96 | 97 | return (object) [ 98 | "code" => ($data->body->resultInfo->resultCode !== "SUCCESS") ? 400 : 200, 99 | "status" => $data->body->resultInfo->resultCode, 100 | "msg" => $data->body->resultInfo->resultMsg 101 | ]; 102 | } 103 | 104 | /** 105 | * Return data body with format object json 106 | */ 107 | public function body(): Collection 108 | { 109 | $msg = (array) $this->message(); 110 | $resp = collect((array) json_decode(self::$danaData->body())->response); 111 | $data = collect($resp->get('body')) 112 | ->put( 113 | 'transactionTime', 114 | $resp->get('head')->respTime 115 | ); 116 | return (collect($msg)->merge($data->toArray())); 117 | } 118 | 119 | /** 120 | * Sign signature 121 | * See this doc API DANA 122 | * https://dashboard.dana.id/api-docs/read/45 123 | */ 124 | public static function signSignature(array $data): string 125 | { 126 | $signature = ''; 127 | $privateKey = config("dana.ssh_private_key", ""); 128 | if (!$privateKey) { 129 | throw new DANASignSignatureException("Please set your app private key. SSH Private Key"); 130 | } 131 | openssl_sign( 132 | json_encode($data), 133 | $signature, 134 | $privateKey, 135 | OPENSSL_ALGO_SHA256 136 | ); 137 | 138 | return base64_encode($signature); 139 | } 140 | 141 | /** 142 | * Verify signature 143 | * @param array $data string data in json 144 | * @param string $signature string of signature in base64 encoded 145 | * 146 | * @return int|false base 64 signature 147 | */ 148 | public function verifySignature(array $data, string $signature): int|false 149 | { 150 | $publicKey = config("dana.ssh_public_key", ""); 151 | if (!$publicKey) { 152 | throw new DANAVerifySignatureException("Please set your dana public key"); 153 | } 154 | $binarySignature = base64_decode($signature); 155 | 156 | return openssl_verify( 157 | json_encode($data), 158 | $binarySignature, 159 | $publicKey, 160 | OPENSSL_ALGO_SHA256 161 | ); 162 | } 163 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel DANA Payment Package 2 | 3 | This Laravel wrapper/library for DANA Payment API. Visit https://dana.id for more information about the product and see documentation at https://dashboard.dana.id/api-docs for more technical details. 4 | 5 | ## Requirements 6 | 7 | - PHP 8.1 or higher 8 | - Laravel 9.x, 10.x, 11.x, or 12.x 9 | 10 | ## Installation 11 | 12 | #### 1. You can install the package via composer. 13 | ```sh 14 | composer require otnansirk/laravel-dana 15 | ``` 16 | 17 | #### 2. The service provider will automatically get registered. Or you may manually add the service provider in your `config/app.php` file. 18 | ```php 19 | 'providers' => [ 20 | // ... 21 | Otnansirk\Dana\DanaCoreServiceProvider::class, 22 | ]; 23 | ``` 24 | 25 | #### 3. You should publish the `config/dana.php` config file with this php artisan command. 26 | ```sh 27 | php artisan vendor:publish --provider="Otnansirk\Dana\DanaCoreServiceProvider" --tag="dana-config" 28 | ``` 29 | 30 | ## Configuration 31 | 32 | All configuration are stored in `config/dana.php`. Customize everything you need. 33 | 34 | Make sure to set the following environment variables in your `.env` file: 35 | 36 | ```env 37 | DANA_ENV=development 38 | DANA_ACTIVE=false 39 | DANA_API_URL=https://api-sandbox.saas.dana.id 40 | DANA_WEB_URL=https://m.sandbox.dana.id 41 | DANA_MARCHANT_ID=your_merchant_id 42 | DANA_CLIENT_ID=your_client_id 43 | DANA_CLIENT_SECRET=your_client_secret 44 | DANA_ORDER_NOTIFY_URL=your_notify_url 45 | DANA_PAY_RETURN_URL=your_return_url 46 | DANA_PUB_KEY=your_public_key 47 | DANA_PRIVATE_KEY=your_private_key 48 | ``` 49 | 50 | ## Usage 51 | 52 | ### 1. Create order | `DANAPay::createOrder($orderData)` 53 | ```php 54 | $orderData = [ 55 | "order" => [ 56 | "orderTitle" => "Dummy product", 57 | "orderAmount" => [ 58 | "currency" => "IDR", 59 | "value" => "100" 60 | ], 61 | "merchantTransId" => "201505080001", 62 | "merchantTransType" => "dummy transaction type", 63 | "orderMemo" => "Memo", 64 | "goods" => [ 65 | [ 66 | "merchantGoodsId" => "24525635625623", 67 | "description" => "dummy description", 68 | "category" => "dummy category", 69 | "price" => [ 70 | "currency" => "IDR", 71 | "value" => "100" 72 | ], 73 | "unit" => "Kg", 74 | "quantity" => "3.2", 75 | "merchantShippingId" => "564314314574327545", 76 | "snapshotUrl" => "[http://snap.url.com]", 77 | "extendInfo" => [ 78 | "key" => "value", 79 | ] 80 | ] 81 | ] 82 | ], 83 | "merchantId" => "216820000000006553000", 84 | "subMerchantId" => "12345678", 85 | "productCode" => "51051000100000000001" 86 | ]; 87 | 88 | DANAPay::createOrder($orderData); 89 | ``` 90 | 91 | About all possible payloads for `$orderData` please check the official DANA documentation.
92 | Ref: https://dashboard.dana.id/api-docs/read/33 93 | 94 | ### 2. Get Transaction by acquirementId | `DANAPay::queryOrder($acquirementId)` 95 | ```php 96 | $acquirementId = "20240125111212800110166050101920928"; 97 | DANAPay::queryOrder($acquirementId); 98 | ``` 99 | 100 | You can get transaction detail and status transaction with this method
101 | Ref: https://dashboard.dana.id/api-docs/read/42 102 | 103 | ### 3. Get oAuth URL | `DANAPay::generateOauthUrl($terminalType, $redirectUrl)` 104 | ```php 105 | $terminalType = "WEB"; 106 | $redirectUrl = "https://your-app-url.com/oauth/callback"; 107 | DANAPay::generateOauthUrl($terminalType, $redirectUrl); 108 | ``` 109 | 110 | For more information please check the official DANA documentation.
111 | Ref: https://dashboard.dana.id/api-docs/read/47 112 | 113 | ### 4. Get Token and Refresh Token | `DANAPay::getToken($authToken)` 114 | ```php 115 | $authToken = "your-auth-token"; 116 | DANAPay::getToken($authToken); 117 | ``` 118 | 119 | You can get value of `$authToken` from oAuth callback process.
120 | From this function you will receive `token` and `refresh_token`.
121 | Ref: https://dashboard.dana.id/api-docs/read/32 122 | 123 | ### 5. Get User Profile | `DANAPay::profile($accessToken)` 124 | ```php 125 | $accessToken = "your_user_profile_access_token"; 126 | DANAPay::profile($accessToken); 127 | ``` 128 | 129 | You can get value for `$accessToken` from `DANAPay::getToken` function
130 | Ref: https://dashboard.dana.id/api-docs/read/38 131 | 132 | ### 6. Unbinding Access Token | `DANAPay::unBindAllAccount()` 133 | ```php 134 | DANAPay::unBindAllAccount(); 135 | ``` 136 | 137 | This function used for revoke or unbind all access token registered from the merchant.
138 | Ref: https://dashboard.dana.id/api-docs/read/46 139 | 140 | ### 7. Function for provide callback response 141 | ```php 142 | $status = true; 143 | DANAPay::responseFinishNotifyCallback($status); 144 | ``` 145 | 146 | This function will generate valid response for DANA API.
147 | `$status` is boolean data type. 148 | 149 | ### 8. Function for calculation MDR 150 | ```php 151 | $payAmount = 100000; 152 | $payMethod = 'BALANCE'; 153 | DANACalculation::calculateMDR($payAmount, $payMethod); 154 | ``` 155 | 156 | This function will calculate MDR fee for DANA. 157 | You will get value `$payMethod` and `$payAmount` from callback DANA. 158 | 159 | ## Testing 160 | 161 | ```bash 162 | composer test 163 | ``` 164 | 165 | ## Laravel Version Compatibility 166 | 167 | This package supports multiple Laravel versions: 168 | 169 | | Laravel Version | PHP Version | Status | 170 | |----------------|-------------|---------| 171 | | Laravel 9.x | PHP 8.1+ | ✅ Supported | 172 | | Laravel 10.x | PHP 8.1+ | ✅ Supported | 173 | | Laravel 11.x | PHP 8.2+ | ✅ Supported | 174 | | Laravel 12.x | PHP 8.2+ | ✅ Supported | 175 | 176 | ## Changelog 177 | 178 | Please see [CHANGELOG.md](CHANGELOG.md) for a list of what has changed since the last version. 179 | 180 | ## Contributing 181 | 182 | This project is far from perfect. Many DANA APIs that have not been implemented. I would be very happy if any of you could contribute to this project. 183 | 184 | ## License 185 | 186 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 187 | -------------------------------------------------------------------------------- /src/Services/DANAPayService.php: -------------------------------------------------------------------------------- 1 | "dana.oauth.auth.applyToken" 26 | ]; 27 | $bodys = [ 28 | "grantType" => "AUTHORIZATION_CODE", 29 | "authCode" => $authCode 30 | ]; 31 | 32 | $data = DanaCore::api($path, $heads, $bodys); 33 | if ($data->message()->status !== "SUCCESS") { 34 | throw new DANAPayGetTokenException($data->message()->msg, $data->message()->code); 35 | } 36 | 37 | return collect([ 38 | "token" => $data->body()->get('accessTokenInfo')->accessToken, 39 | "refresh_token" => $data->body()->get('accessTokenInfo')->accessToken, 40 | "expires_in" => $data->body()->get('accessTokenInfo')->expiresIn, 41 | "status" => $data->body()->get('accessTokenInfo')->tokenStatus 42 | ]); 43 | } 44 | 45 | /** 46 | * Unbind access token use for merchant to revoke all tokens registered for its user 47 | */ 48 | public function unBindAllAccount(): Collection 49 | { 50 | $path = "/dana/oauth/unbind/revokeAllTokens.htm"; 51 | $heads = [ 52 | "function" => "dana.oauth.unbind.revokeAllTokens" 53 | ]; 54 | $bodys = [ 55 | "merchantId" => config("dana.merchant_id") 56 | ]; 57 | 58 | $data = DanaCore::api($path, $heads, $bodys); 59 | if ($data->message()->status !== "SUCCESS") { 60 | throw new DANAPayUnBindingAllException($data->message()->msg, $data->message()->code); 61 | } 62 | return collect($data->message()); 63 | } 64 | 65 | /** 66 | * Get user profile 67 | */ 68 | public function profile(string $accessToken): Collection 69 | { 70 | $path = "/dana/member/query/queryUserProfile.htm"; 71 | $heads = [ 72 | "function" => "dana.member.query.queryUserProfile", 73 | "accessToken" => $accessToken 74 | ]; 75 | $bodys = [ 76 | "userResources" => config("dana.user_resources"), 77 | ]; 78 | $data = DanaCore::api($path, $heads, $bodys); 79 | 80 | if ($data->message()->status !== "SUCCESS") { 81 | throw new DANAException($data->message()->msg, $data->message()->code); 82 | } 83 | 84 | $res = collect($data->body()->get('userResourceInfos')) 85 | ->map(function ($val) { 86 | return [strtolower($val->resourceType) => $val->value]; 87 | }) 88 | ->flatMap(function ($values) { 89 | return $values; 90 | }); 91 | $res->put('topup_url', $res->get("topup_url") . "?ott=" . $res->get("ott")); 92 | $res->put('transaction_url', $res->get("transaction_url") . "?ott=" . $res->get("ott")); 93 | $res->forget('ott'); 94 | 95 | return $res; 96 | } 97 | 98 | /** 99 | * Create order 100 | */ 101 | public function createOrder(array $bodys): Collection 102 | { 103 | $path = "/dana/acquiring/order/createOrder.htm"; 104 | $heads = [ 105 | "function" => "dana.acquiring.order.createOrder" 106 | ]; 107 | 108 | $orderData = new CreateOrder($bodys); 109 | $payload = $orderData->payload(); 110 | $res = DanaCore::api($path, $heads, $payload); 111 | 112 | if ($res->message()->status !== "SUCCESS") { 113 | throw new DANACreateOrderException("DANA " . $res->message()->msg, $res->message()->code); 114 | } 115 | 116 | return $res->body() 117 | ->forget(["resultInfo", "code", "status", "msg"]) 118 | ->map(function ($val, $key) { 119 | $data = ($key === 'transactionTime') ? \Carbon\Carbon::parse($val) : $val; 120 | return [$key => $data]; 121 | }) 122 | ->flatMap(function ($values) { 123 | return $values; 124 | }); 125 | } 126 | 127 | /** 128 | * To query transaction detail by DANA's acquirementId or merchantTransId. 129 | * When DANA's acquirementId and merchantTransId are both provided, this API assigns 130 | * a higher priority to DANA's acquirementId, merchantTransId would be ignored. 131 | */ 132 | public function queryOrder(string $acquirementId): Collection 133 | { 134 | $path = "/dana/acquiring/order/query.htm"; 135 | $heads = [ 136 | "function" => "dana.acquiring.order.query", 137 | ]; 138 | 139 | $payload = [ 140 | "merchantId" => config("dana.merchant_id", ""), 141 | "acquirementId" => $acquirementId 142 | ]; 143 | $res = DanaCore::api($path, $heads, $payload); 144 | return collect([ 145 | "code" => data_get($res->body(), 'code', 200), 146 | "message" => data_get($res->body(), 'msg', ""), 147 | "goods" => data_get($res->body(), 'goods', ""), 148 | "status" => data_get($res->body(), 'statusDetail', ""), 149 | "acquirementId" => data_get($res->body(), 'acquirementId', ""), 150 | "merchantTransId" => data_get($res->body(), 'merchantTransId', ""), 151 | ]); 152 | } 153 | 154 | /** 155 | * Generate url oauth 156 | */ 157 | public function generateOauthUrl(string $terminalType = "WEB", string $redirectUrl = ""): string 158 | { 159 | Validation::terminalType($terminalType); 160 | 161 | $baseAPIUrl = config("dana.web_url"); 162 | $path = "/d/portal/oauth?"; 163 | $params = [ 164 | "clientId" => config("dana.client_id"), 165 | "scopes" => config("dana.oauth_scopes"), 166 | "requestId" => Str::uuid()->toString(), 167 | "terminalType" => $terminalType, 168 | "redirectUrl" => $redirectUrl 169 | ]; 170 | 171 | $oauthUrl = $baseAPIUrl . $path; 172 | $oauthUrl .= http_build_query($params); 173 | 174 | return $oauthUrl; 175 | } 176 | 177 | /** 178 | * Response for finish payment notify callback 179 | */ 180 | public function responseFinishNotifyCallback(bool $status = true): array 181 | { 182 | $header = DanaCore::getResHeader(); 183 | 184 | $resultInfo = [ 185 | "resultStatus" => "S", 186 | "resultCodeId" => "00000000", 187 | "resultCode" => "SUCCESS", 188 | "resultMsg" => "success" 189 | ]; 190 | 191 | if (!$status) { 192 | $resultInfo = [ 193 | "resultStatus" => "U", 194 | "resultCodeId" => "00000900", 195 | "resultCode" => "SYSTEM_ERROR", 196 | "resultMsg" => "System error" 197 | ]; 198 | } 199 | 200 | $optionalHeader = [ 201 | "function" => "dana.acquiring.order.finishNotify", 202 | ]; 203 | $body = [ 204 | "resultInfo" => $resultInfo 205 | ]; 206 | $response = [ 207 | "head" => array_merge($header, $optionalHeader), 208 | "body" => $body 209 | ]; 210 | 211 | return [ 212 | "response" => $response, 213 | "signature" => DanaCore::signSignature($response) 214 | ]; 215 | } 216 | } -------------------------------------------------------------------------------- /src/Helpers/CreateOrder.php: -------------------------------------------------------------------------------- 1 | currency = "IDR"; 23 | $this->body = $body; 24 | $this->order = Arr::get($body, "order", []); 25 | $this->envInfo = Arr::get($body, "envInfo", []); 26 | $this->shopInfo = Arr::get($body, "shopInfo", []); 27 | $this->productCode = Arr::get($body, "productCode", ""); 28 | $this->merchantId = config("dana.merchant_id", ""); 29 | $this->paymentPreference = Arr::get($body, "paymentPreference", []); 30 | } 31 | 32 | /** 33 | * Get all payload 34 | */ 35 | public function payload(): array 36 | { 37 | $mandatoryPayload = [ 38 | "order" => $this->order(), 39 | "merchantId" => $this->merchantId, 40 | "productCode" => $this->productCode, 41 | "envInfo" => $this->envInfo(), 42 | "notificationUrls" => $this->notificationUrls() 43 | ]; 44 | 45 | return array_merge( 46 | $mandatoryPayload, 47 | $this->shopInfo(), 48 | $this->mcc(), 49 | $this->paymentPreference() 50 | ); 51 | } 52 | 53 | /** 54 | * Order mapper 55 | */ 56 | protected function order(): array 57 | { 58 | $goods = collect(Arr::get($this->order, "goods", []))->map(function ($good) { 59 | return [ 60 | "merchantGoodsId" => Arr::get($good, "merchantGoodsId", ""), 61 | "description" => Arr::get($good, "description", ""), 62 | "category" => Arr::get($good, "category", ""), 63 | "price" => [ 64 | "currency" => Arr::get($good, "price.currency", $this->currency), 65 | "value" => Arr::get($good, "price.value", 0) 66 | ], 67 | "unit" => Arr::get($good, "unit", ""), 68 | "quantity" => Arr::get($good, "quantity", ""), 69 | "snapshotUrl" => Arr::get($good, "snapshotUrl", "[]"), 70 | "merchantShippingId" => Arr::get($good, "merchantShippingId", ""), 71 | "extendInfo" => Arr::get($good, "extendInfo", ""), 72 | ]; 73 | })->toArray(); 74 | 75 | $shippingInfo = collect(Arr::get($this->order, "shippingInfo", []))->map(function ($shippingInfo) { 76 | return [ 77 | "merchantShippingId" => Arr::get($shippingInfo, "merchantShippingId", ""), 78 | "trackingNo" => Arr::get($shippingInfo, "trackingNo", ""), 79 | "carrier" => Arr::get($shippingInfo, "carrier", ""), 80 | "chargeAmount" => [ 81 | "currency" => Arr::get($shippingInfo, "chargeAmount.currency", ""), 82 | "value" => Arr::get($shippingInfo, "chargeAmount.value", "") 83 | ], 84 | "countryName" => Arr::get($shippingInfo, "countryName", ""), 85 | "stateName" => Arr::get($shippingInfo, "stateName", ""), 86 | "cityName" => Arr::get($shippingInfo, "cityName", ""), 87 | "areaName" => Arr::get($shippingInfo, "areaName", ""), 88 | "address1" => Arr::get($shippingInfo, "address1", ""), 89 | "address2" => Arr::get($shippingInfo, "address2", ""), 90 | "firstName" => Arr::get($shippingInfo, "firstName", ""), 91 | "lastName" => Arr::get($shippingInfo, "lastName", ""), 92 | "mobileNo" => Arr::get($shippingInfo, "mobileNo", ""), 93 | "phoneNo" => Arr::get($shippingInfo, "phoneNo", ""), 94 | "zipCode" => Arr::get($shippingInfo, "zipCode", ""), 95 | "email" => Arr::get($shippingInfo, "email", ""), 96 | "faxNo" => Arr::get($shippingInfo, "faxNo", "") 97 | ]; 98 | })->toArray(); 99 | 100 | $orderData = [ 101 | "orderTitle" => Arr::get($this->order, "orderTitle", ""), 102 | "orderAmount" => [ 103 | "currency" => Arr::get($this->order, "orderAmount.currency", $this->currency), 104 | "value" => Arr::get($this->order, "orderAmount.value", 0) 105 | // Default in Dana is use cent. so is value is 100 its equivalent to 1 Rp 106 | // Ref: https://dashboard.dana.id/api-docs/read/31#Money 107 | ], 108 | "merchantTransId" => Arr::get($this->order, "merchantTransId", Str::uuid()->toString()), 109 | "merchantTransType" => Arr::get($this->order, "merchantTransType", "APP"), 110 | "orderMemo" => Arr::get($this->order, "orderMemo", ""), 111 | "createdTime" => Arr::get( 112 | $this->order, 113 | "createdTime", 114 | Carbon::now()->format(config("dana.date_format")) 115 | ), 116 | "expiryTime" => Arr::get( 117 | $this->order, 118 | "expiryTime", 119 | Carbon::now() 120 | ->addMinutes(config("dana.expired_after", 60)) 121 | ->format(config("dana.date_format")) 122 | ), 123 | "goods" => $goods, 124 | "shippingInfo" => $shippingInfo 125 | ]; 126 | 127 | return $orderData; 128 | } 129 | 130 | /** 131 | * Env info 132 | */ 133 | protected function envInfo(): array 134 | { 135 | return [ 136 | 'terminalType' => Arr::get($this->envInfo, "terminalType", "SYSTEM"), 137 | 'osType' => Arr::get($this->envInfo, "osType", ""), 138 | 'extendInfo' => Arr::get($this->envInfo, "extendInfo", ""), 139 | 'orderOsType' => Arr::get($this->envInfo, "orderOsType", ""), 140 | 'sdkVersion' => Arr::get($this->envInfo, "sdkVersion", ""), 141 | 'websiteLanguage' => Arr::get($this->envInfo, "websiteLanguage", ""), 142 | 'tokenId' => Arr::get($this->envInfo, "tokenId", ""), 143 | 'sessionId' => Arr::get($this->envInfo, "sessionId", ""), 144 | 'appVersion' => Arr::get($this->envInfo, "appVersion", ""), 145 | 'merchantAppVersion' => Arr::get($this->envInfo, "merchantAppVersion", ""), 146 | 'clientKey' => Arr::get($this->envInfo, "clientKey", ""), 147 | 'orderTerminalType' => Arr::get($this->envInfo, "orderTerminalType", "SYSTEM"), 148 | 'clientIp' => Arr::get($this->envInfo, "clientIp", ""), 149 | 'sourcePlatform' => Arr::get($this->envInfo, "sourcePlatform", "IPG") 150 | ]; 151 | } 152 | 153 | /** 154 | * Notification url 155 | */ 156 | public function notificationUrls(): array 157 | { 158 | return Arr::get($this->body, "notificationUrls", [ 159 | [ 160 | "url" => config("dana.pay_return_url"), 161 | "type" => "PAY_RETURN" 162 | ], 163 | [ 164 | "url" => config("dana.order_notify_url"), 165 | "type" => "NOTIFICATION" 166 | ] 167 | ]); 168 | } 169 | 170 | /** 171 | * Shoping info 172 | */ 173 | public function shopInfo(): array 174 | { 175 | if (collect($this->shopInfo)->isNotEmpty()) { 176 | return [ 177 | "shopInfo" => [ 178 | "shopId" => Arr::get($this->shopInfo, "shopId", ""), 179 | "operatorId" => Arr::get($this->shopInfo, "operatorId", "") 180 | ] 181 | ]; 182 | } 183 | return []; 184 | } 185 | 186 | /** 187 | * Mcc 188 | */ 189 | public function mcc(): array 190 | { 191 | if (Arr::get($this->body, "mcc", null)) { 192 | return [ 193 | "mcc" => Arr::get($this->body, "mcc", "") 194 | ]; 195 | } 196 | return []; 197 | } 198 | 199 | /** 200 | * Payment preference 201 | */ 202 | public function paymentPreference(): array 203 | { 204 | if (collect($this->paymentPreference)->isNotEmpty()) { 205 | $payOptionBills = collect(Arr::get($this->paymentPreference, "payOptionBills", [])) 206 | ->map( 207 | function ($payOption) { 208 | return [ 209 | "payOption" => Arr::get($payOption, "payOption", ""), 210 | "payMethod" => Arr::get($payOption, "payMethod", ""), 211 | "transAmount" => [ 212 | "currency" => Arr::get($payOption, "transAmount.currency", ""), 213 | "value" => Arr::get($payOption, "transAmount.value", "") 214 | ], 215 | "chargeAmount" => [ 216 | "currency" => Arr::get($payOption, "chargeAmount.currency", ""), 217 | "value" => Arr::get($payOption, "chargeAmount.value", "") 218 | ], 219 | "payerAccountNo" => Arr::get($payOption, "payerAccount", ""), 220 | "cardCacheToken" => Arr::get($payOption, "cardCacheToken", ""), 221 | "saveCardAfterPay" => Arr::get($payOption, "saveCardAfterPay", ""), 222 | "channelInfo" => Arr::get($payOption, "channelInfo", ""), 223 | "issuingCountry" => Arr::get($payOption, "issuingCountry", ""), 224 | "assetType" => Arr::get($payOption, "assetType", ""), 225 | "extendInfo" => Arr::get($payOption, "extendInfo", "") 226 | ]; 227 | } 228 | )->toArray(); 229 | 230 | return [ 231 | "paymentPreference" => [ 232 | "disabledPayMethods" => Arr::get($this->paymentPreference, "disabledPayMethods", ""), 233 | "payOptionBills" => $payOptionBills 234 | ] 235 | ]; 236 | } 237 | return []; 238 | } 239 | } --------------------------------------------------------------------------------