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