12 |
13 |
28 |
29 |
30 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/midtransclient/snap.py:
--------------------------------------------------------------------------------
1 | from .config import ApiConfig
2 | from .http_client import HttpClient
3 | from .transactions import Transactions
4 |
5 | class Snap:
6 | """
7 | Snap object used to do request to Midtrans Snap API
8 | """
9 |
10 | def __init__(self,
11 | is_production=False,
12 | server_key='',
13 | client_key='',
14 | custom_headers=dict(),
15 | proxies=dict()):
16 |
17 | self.api_config = ApiConfig(is_production,server_key,client_key,custom_headers,proxies)
18 | self.http_client = HttpClient()
19 | self.transactions = Transactions(self)
20 |
21 | @property
22 | def api_config(self):
23 | return self.__api_config
24 |
25 | @api_config.setter
26 | def api_config(self, new_value):
27 | self.__api_config = new_value
28 |
29 | def create_transaction(self,parameters=dict()):
30 | """
31 | Trigger API call to Snap API
32 | :param parameters: dictionary of SNAP API JSON body as parameter, will be converted to JSON
33 | (more params detail refer to: https://snap-docs.midtrans.com)
34 |
35 | :return: Dictionary from JSON decoded response, that contains `token` and `redirect_url`
36 | """
37 | api_url = self.api_config.get_snap_base_url()+'/transactions'
38 |
39 | response_dict, response_object = self.http_client.request(
40 | 'post',
41 | self.api_config.server_key,
42 | api_url,
43 | parameters,
44 | self.api_config.custom_headers,
45 | self.api_config.proxies)
46 | return response_dict
47 |
48 | def create_transaction_token(self,parameters=dict()):
49 | """
50 | Wrapper method that call `create_transaction` and directly :return: `token`
51 | """
52 | return self.create_transaction(parameters)['token']
53 |
54 | def create_transaction_redirect_url(self,parameters=dict()):
55 | """
56 | Wrapper method that call `create_transaction` and directly :return: `redirect_url`
57 | """
58 | return self.create_transaction(parameters)['redirect_url']
59 |
--------------------------------------------------------------------------------
/Maintaining.md:
--------------------------------------------------------------------------------
1 | > Warning: This note is for developer/maintainer of this package only
2 |
3 | ## Updating Package
4 |
5 | - If from scratch, using `pipenv`
6 | - Install pipenv `pip install pipenv` or `pip3 install pipenv`
7 | - If fail, you may need to prefix the command with `sudo `
8 | - CD to project directory
9 | - Install using pipenv `pipenv install`
10 | - If fail, you may need to specify which python bin file by `pipenv install --python /usr/bin/python3`. (Run `which python` or `which python3` to know the file path)
11 | - Activate and enter python env for current folder `pipenv shell`, now you are inside python env
12 | - Install project as local package `pip install -e .`
13 | - Make your code changes
14 | - Increase `version` value on:
15 | - `./setup.py` file
16 | - `./midtransclient/__init__.py` file
17 | - `./midtransclient/http_client.py` file on User-Agent value
18 | - To install the package locally with a symlink `pip install -e .`
19 | - To run test, run `pytest`
20 | - To run specific test, e.g: `pytest -k "test_core_api_charge_fail_401"`
21 | - If fail, you may need to install pytest first `pip install pytest`
22 | - To update https://pypi.org repo, run these on terminal:
23 | ```bash
24 | # install setuptools & wheel
25 | python -m pip install --upgrade setuptools wheel
26 |
27 | # Generate `dist/` folder for upload
28 | python setup.py sdist bdist_wheel
29 |
30 | # Update / install Twine
31 | python -m pip install --upgrade twine
32 |
33 | # upload to pypi / pip repository
34 | # you will be asked for username and password for https://pypi.org account
35 | twine upload dist/* --skip-existing
36 |
37 | # To upload to test pypi use this instead
38 | # twine upload --repository-url https://test.pypi.org/legacy/ dist/* --skip-existing;
39 | ```
40 | - if fail to upload, clean up all files within your `./dist` folder, then re-try the above commands
41 |
42 | ## Dev & Test via Docker Compose
43 |
44 | - To use docker-compose to test and run project, `cd` to repo dir
45 | - Run `docker-compose up`, which basically run pytest on container
46 | - Run `docker-compose down`, to clean up when done
47 |
48 | ## TODO
49 | - need a better test cases / coverage
50 | - check if any test cases break due to post-1OMS API behavior changes
--------------------------------------------------------------------------------
/examples/transaction_actions/notification_example.py:
--------------------------------------------------------------------------------
1 | import midtransclient
2 | # This is just for very basic implementation reference, in production, you should validate the incoming requests and implement your backend more securely.
3 | # Please refer to this docs for sample HTTP POST notifications:
4 | # https://docs.midtrans.com/en/after-payment/http-notification?id=sample-of-different-payment-channels
5 |
6 | # Initialize api client object
7 | # You can find it in Merchant Portal -> Settings -> Access keys
8 | api_client = midtransclient.CoreApi(
9 | is_production=False,
10 | server_key='YOUR_SERVER_KEY',
11 | client_key='YOUR_CLIENT_KEY'
12 | )
13 |
14 | mock_notification = {
15 | 'currency': 'IDR',
16 | 'fraud_status': 'accept',
17 | 'gross_amount': '24145.00',
18 | 'order_id': 'test-transaction-321',
19 | 'payment_type': 'bank_transfer',
20 | 'status_code': '201',
21 | 'status_message': 'Success, Bank Transfer transaction is created',
22 | 'transaction_id': '6ee793df-9b1d-4343-8eda-cc9663b4222f',
23 | 'transaction_status': 'pending',
24 | 'transaction_time': '2018-10-24 15:34:33',
25 | 'va_numbers': [{'bank': 'bca', 'va_number': '490526303019299'}]
26 | }
27 | # handle notification JSON sent by Midtrans, it auto verify it by doing get status
28 | # parameter can be Dictionary or String of JSON
29 | status_response = api_client.transactions.notification(mock_notification)
30 |
31 | order_id = status_response['order_id']
32 | transaction_status = status_response['transaction_status']
33 | fraud_status = status_response['fraud_status']
34 |
35 | print('Transaction notification received. Order ID: {0}. Transaction status: {1}. Fraud status: {2}'.format(order_id,
36 | transaction_status,
37 | fraud_status))
38 |
39 | # Sample transaction_status handling logic
40 |
41 | if transaction_status == 'capture':
42 | if fraud_status == 'challenge':
43 | # TODO set transaction status on your databaase to 'challenge'
44 | None
45 | elif fraud_status == 'accept':
46 | # TODO set transaction status on your databaase to 'success'
47 | None
48 | elif transaction_status == 'cancel' or transaction_status == 'deny' or transaction_status == 'expire':
49 | # TODO set transaction status on your databaase to 'failure'
50 | None
51 | elif transaction_status == 'pending':
52 | # TODO set transaction status on your databaase to 'pending' / waiting payment
53 | None
54 |
--------------------------------------------------------------------------------
/midtransclient/config.py:
--------------------------------------------------------------------------------
1 | class ApiConfig:
2 | """
3 | Config Object that used to store is_production, server_key, client_key.
4 | And also API base urls.
5 | note: client_key is not necessarily required for API call.
6 | """
7 | CORE_SANDBOX_BASE_URL = 'https://api.sandbox.midtrans.com';
8 | CORE_PRODUCTION_BASE_URL = 'https://api.midtrans.com';
9 | SNAP_SANDBOX_BASE_URL = 'https://app.sandbox.midtrans.com/snap/v1';
10 | SNAP_PRODUCTION_BASE_URL = 'https://app.midtrans.com/snap/v1';
11 |
12 | def __init__(self,
13 | is_production=False,
14 | server_key='',
15 | client_key='',
16 | custom_headers=dict(),
17 | proxies=dict()):
18 | self.is_production = is_production
19 | self.server_key = server_key
20 | self.client_key = client_key
21 | self.custom_headers = custom_headers
22 | self.proxies = proxies
23 |
24 | def get_core_api_base_url(self):
25 | if self.is_production:
26 | return self.CORE_PRODUCTION_BASE_URL
27 | return self.CORE_SANDBOX_BASE_URL
28 |
29 | def get_snap_base_url(self):
30 | if self.is_production:
31 | return self.SNAP_PRODUCTION_BASE_URL
32 | return self.SNAP_SANDBOX_BASE_URL
33 |
34 | # properties setter
35 | def set(self,
36 | is_production=None,
37 | server_key=None,
38 | client_key=None):
39 | if is_production is not None:
40 | self.is_production = is_production
41 | if server_key is not None:
42 | self.server_key = server_key
43 | if client_key is not None:
44 | self.client_key = client_key
45 |
46 | @property
47 | def server_key(self):
48 | return self.__server_key
49 |
50 | @server_key.setter
51 | def server_key(self, new_value):
52 | self.__server_key = new_value
53 |
54 | @property
55 | def client_key(self):
56 | return self.__client_key
57 |
58 | @client_key.setter
59 | def client_key(self, new_value):
60 | self.__client_key = new_value
61 |
62 | @property
63 | def custom_headers(self):
64 | return self.__custom_headers
65 |
66 | @custom_headers.setter
67 | def custom_headers(self, new_value):
68 | self.__custom_headers = new_value
69 |
70 | @property
71 | def proxies(self):
72 | return self.__proxies
73 |
74 | @proxies.setter
75 | def proxies(self, new_value):
76 | self.__proxies = new_value
77 |
78 | def __repr__(self):
79 | return ("
".format(self.is_production,
80 | self.server_key,
81 | self.client_key,
82 | self.custom_headers,
83 | self.proxies))
--------------------------------------------------------------------------------
/examples/core_api/core_api_credit_card_example.py:
--------------------------------------------------------------------------------
1 | import midtransclient
2 | # This is just for very basic implementation reference, in production, you should validate the incoming requests and implement your backend more securely.
3 |
4 | # Initialize core api client object
5 | # You can find it in Merchant Portal -> Settings -> Access keys
6 | core = midtransclient.CoreApi(
7 | is_production=False,
8 | server_key='YOUR_SERVER_KEY',
9 | client_key='YOUR_CLIENT_KEY'
10 | )
11 |
12 | # Alternative way to initialize CoreApi client object:
13 | # core = midtransclient.CoreApi()
14 | # core.api_config.set(
15 | # is_production=False,
16 | # server_key='YOUR_SERVER_KEY',
17 | # client_key='YOUR_CLIENT_KEY'
18 | # )
19 |
20 | # Another alternative way to initialize CoreApi client object:
21 | # core = midtransclient.CoreApi()
22 | # core.api_config.is_production=False
23 | # core.api_config.server_key='YOUR_SERVER_KEY'
24 | # core.api_config.client_key='YOUR_CLIENT_KEY'
25 |
26 | # IMPORTANT NOTE: You should do credit card get token via frontend using `midtrans-new-3ds.min.js`, to avoid card data breach risks on your backend
27 | # ( refer to: https://docs.midtrans.com/en/core-api/credit-card?id=_1-getting-the-card-token )
28 | # For full example on Credit Card 3DS transaction refer to:
29 | # (/examples/flask_app) that implement Snap & Core Api
30 |
31 | # prepare CORE API parameter to get credit card token
32 | # another sample of card number can refer to https://docs.midtrans.com/en/technical-reference/sandbox-test?id=card-payments
33 | params = {
34 | 'card_number': '5264 2210 3887 4659',
35 | 'card_exp_month': '12',
36 | 'card_exp_year': '2025',
37 | 'card_cvv': '123',
38 | 'client_key': core.api_config.client_key,
39 | }
40 | card_token_response = core.card_token(params)
41 | cc_token = card_token_response['token_id']
42 |
43 | # prepare CORE API parameter to charge credit card ( refer to: https://docs.midtrans.com/en/core-api/credit-card?id=_2-sending-transaction-data-to-charge-api )
44 | param = {
45 | "payment_type": "credit_card",
46 | "transaction_details": {
47 | "gross_amount": 12145,
48 | "order_id": "test-transaction-54321",
49 | },
50 | "credit_card":{
51 | "token_id": cc_token
52 | }
53 | }
54 |
55 | # charge transaction
56 | charge_response = core.charge(param)
57 | print('charge_response:')
58 | print(charge_response)
59 |
60 | # charge_response is dictionary representation of API JSON response
61 | # sample:
62 | # {
63 | # 'approval_code': '1540370521462',
64 | # 'bank': 'bni',
65 | # 'card_type': 'debit',
66 | # 'channel_response_code': '00',
67 | # 'channel_response_message': 'Approved',
68 | # 'currency': 'IDR',
69 | # 'fraud_status': 'accept',
70 | # 'gross_amount': '12145.00',
71 | # 'masked_card': '526422-4659',
72 | # 'order_id': 'test-transaction-54321',
73 | # 'payment_type': 'credit_card',
74 | # 'status_code': '200',
75 | # 'status_message': 'Success, Credit Card transaction is successful',
76 | # 'transaction_id': '2bc57149-b52b-46ff-b901-86418ad1abcc',
77 | # 'transaction_status': 'capture',
78 | # 'transaction_time': '2018-10-24 15:42:01'
79 | # }
80 |
--------------------------------------------------------------------------------
/tests/test_http_client.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from unittest.mock import patch
3 | from .helpers import is_str
4 | from .context import HttpClient
5 | import datetime
6 | from pprint import pprint
7 |
8 | def test_http_client_class():
9 | http_client = HttpClient()
10 | assert type(http_client.http_client).__name__ == 'module'
11 |
12 | def test_has_request_method():
13 | http_client = HttpClient()
14 | methods = dir(http_client)
15 | assert 'request' in methods
16 |
17 | def test_can_raw_request_to_snap():
18 | http_client = HttpClient()
19 | response_dict, response_object = http_client.request(method='post',
20 | server_key='SB-Mid-server-GwUP_WGbJPXsDzsNEBRs8IYA',
21 | request_url='https://app.sandbox.midtrans.com/snap/v1/transactions',
22 | parameters=generate_param_min())
23 | assert isinstance(response_dict, dict)
24 | assert is_str(response_dict['token'])
25 |
26 | def test_fail_request_401_to_snap():
27 | http_client = HttpClient()
28 | err = ''
29 | try:
30 | response_dict, response_object = http_client.request(method='post',
31 | server_key='wrong-server-key',
32 | request_url='https://app.sandbox.midtrans.com/snap/v1/transactions',
33 | parameters=generate_param_min())
34 | except Exception as e:
35 | err = e
36 | assert 'MidtransAPIError' in err.__class__.__name__
37 | assert is_str(err.message)
38 | assert isinstance(err.api_response_dict, dict)
39 | assert 401 == err.http_status_code
40 |
41 | def test_response_not_json_exception():
42 | http_client = HttpClient()
43 | try:
44 | response = http_client.request(method='post',
45 | server_key='',
46 | request_url='https://midtrans.com/',
47 | parameters='')
48 | except Exception as e:
49 | assert 'JSONDecodeError' in repr(e)
50 |
51 | def test_is_custom_headers_applied():
52 | http_client = HttpClient()
53 |
54 | custom_headers = {
55 | 'X-Override-Notification':'https://example.org'
56 | }
57 |
58 | # Mock requests
59 | with patch('requests.request') as mock_request:
60 | # Set status code to 200 to prevent MidtransAPIError
61 | mock_request.return_value.status_code = 200
62 |
63 | # Trigger request
64 | http_client.request(method='post',
65 | server_key='SB-Mid-server-GwUP_WGbJPXsDzsNEBRs8IYA',
66 | request_url='https://app.sandbox.midtrans.com/snap/v1/transactions',
67 | parameters=generate_param_min(),
68 | custom_headers=custom_headers)
69 |
70 | # Fetch the headers from requests.request arguments
71 | headers = mock_request.call_args[1]['headers']
72 |
73 | # Make sure default header still exist
74 | assert headers.get('content-type') == 'application/json'
75 |
76 | # Assert custom headers
77 | assert 'X-Override-Notification' in headers
78 | assert headers.get('X-Override-Notification') == 'https://example.org'
79 |
80 | # TODO test GET request
81 |
82 | # ======== HELPER FUNCTIONS BELOW ======== #
83 | def generate_param_min():
84 | return {
85 | "transaction_details": {
86 | "order_id": "py-midtransclient-test-"+datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
87 | "gross_amount": 200000
88 | }, "credit_card":{
89 | "secure" : True
90 | }
91 | }
--------------------------------------------------------------------------------
/tests/test_tokenization.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from .config import USED_SERVER_KEY, USED_CLIENT_KEY
3 | from .helpers import is_str
4 | from .context import midtransclient
5 | import datetime
6 | import json
7 | from pprint import pprint
8 |
9 | PHONEUNREGISTERED = "123450001"
10 | PHONEBLOCKED = "123450002"
11 | ACCOUNT_ID = ''
12 |
13 | def test_tokenization_class():
14 | tokenization = generate_tokenization_instance()
15 | methods = dir(tokenization)
16 | assert "link_payment_account" in methods
17 | assert is_str(tokenization.api_config.server_key)
18 | assert is_str(tokenization.api_config.client_key)
19 |
20 | def test_tokenization_link_account():
21 | tokenization = generate_tokenization_instance()
22 | parameters = generate_param('81234567891')
23 | response = tokenization.link_payment_account(parameters)
24 | global ACCOUNT_ID
25 | ACCOUNT_ID = response['account_id']
26 | assert isinstance(response, dict)
27 | assert 'account_id' in response.keys()
28 | assert response['status_code'] == '201'
29 | assert response['account_status'] == 'PENDING'
30 |
31 | def test_tokenization_link_account_user_not_found():
32 | tokenization = generate_tokenization_instance()
33 | parameters = generate_param(PHONEUNREGISTERED)
34 | response = tokenization.link_payment_account(parameters)
35 | assert isinstance(response, dict)
36 | assert response['status_code'] == '202'
37 | assert response['channel_response_message'] == 'User Not Found'
38 |
39 | def test_tokenization_link_account_user_blocked():
40 | tokenization = generate_tokenization_instance()
41 | parameters = generate_param(PHONEBLOCKED)
42 | response = tokenization.link_payment_account(parameters)
43 | assert isinstance(response, dict)
44 | assert response['status_code'] == '202'
45 | assert response['channel_response_message'] == 'Wallet is Blocked'
46 |
47 | def test_tokenization_link_account_phone_start_with_0():
48 | tokenization = generate_tokenization_instance()
49 | parameters = generate_param('081234567891')
50 | err = ''
51 | try:
52 | response = tokenization.link_payment_account(parameters)
53 | except Exception as e:
54 | err = e
55 | assert 'MidtransAPIError' in err.__class__.__name__
56 | assert '400' in err.message
57 | assert 'gopay_partner.phone_number must be numeric digits and must not begin with 0' in err.message
58 |
59 | def test_tokenization_get_account():
60 | tokenization = generate_tokenization_instance()
61 | response = tokenization.get_payment_account(ACCOUNT_ID)
62 | assert isinstance(response, dict)
63 | assert response['status_code'] == '201'
64 | assert response['account_id'] == ACCOUNT_ID
65 |
66 | def test_tokenization_unlink_account():
67 | tokenization = generate_tokenization_instance()
68 | err = ''
69 | try:
70 | response = tokenization.unlink_payment_account(ACCOUNT_ID)
71 | except Exception as e:
72 | err = e
73 | assert 'MidtransAPIError' in err.__class__.__name__
74 | assert '412' in err.message
75 | assert 'Account status cannot be updated.' in err.message
76 |
77 |
78 | # ======== HELPER FUNCTIONS BELOW ======== #
79 | def generate_tokenization_instance():
80 | tokenization = midtransclient.CoreApi(is_production=False,
81 | server_key=USED_SERVER_KEY,
82 | client_key=USED_CLIENT_KEY)
83 | return tokenization
84 |
85 | def generate_param(phone_number):
86 | return {
87 | "payment_type": "gopay",
88 | "gopay_partner": {
89 | "phone_number": phone_number,
90 | "country_code": "62",
91 | "redirect_url": "https://mywebstore.com/gopay-linking-finish"
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/midtransclient/http_client.py:
--------------------------------------------------------------------------------
1 | import requests
2 | import json
3 | import sys
4 | from .error_midtrans import MidtransAPIError
5 | from .error_midtrans import JSONDecodeError
6 |
7 | class HttpClient(object):
8 | """
9 | Http Client Class that is wrapper to Python's `requests` module
10 | Used to do API call to Midtrans API urls.
11 | Capable of doing http :request:
12 | """
13 | def __init__(self):
14 | self.http_client = requests
15 |
16 | def request(self, method, server_key, request_url, parameters=dict(),
17 | custom_headers=dict(), proxies=dict()):
18 | """
19 | Perform http request to an url (supposedly Midtrans API url)
20 | :param method: http method
21 | :param server_key: Midtrans API server_key that will be used as basic auth header
22 | :param request_url: target http url
23 | :param parameters: dictionary of Midtrans API JSON body as parameter, will be converted to JSON
24 |
25 | :return: tuple of:
26 | response_dict: Dictionary from JSON decoded response
27 | response_object: Response object from `requests`
28 | """
29 |
30 | # allow string of JSON to be used as parameters
31 | is_parameters_string = isinstance(parameters, str)
32 | if is_parameters_string:
33 | try:
34 | parameters = json.loads(parameters)
35 | except Exception as e:
36 | raise JSONDecodeError('fail to parse `parameters` string as JSON. Use JSON string or Dict as `parameters`. with message: `{0}`'.format(repr(e)))
37 |
38 | payload = json.dumps(parameters) if method != 'get' else parameters
39 | default_headers = {
40 | 'content-type': 'application/json',
41 | 'accept': 'application/json',
42 | 'user-agent': 'midtransclient-python/1.4.2'
43 | }
44 | headers = default_headers
45 |
46 | # only merge if custom headers exist
47 | if custom_headers:
48 | headers = {**default_headers, **custom_headers}
49 |
50 | response_object = self.http_client.request(
51 | method,
52 | request_url,
53 | auth=requests.auth.HTTPBasicAuth(server_key, ''),
54 | data=payload if method != 'get' else None,
55 | params=payload if method == 'get' else None,
56 | headers=headers,
57 | proxies=proxies,
58 | allow_redirects=True
59 | )
60 | # catch response JSON decode error
61 | try:
62 | response_dict = response_object.json()
63 | except json.decoder.JSONDecodeError as e:
64 | raise JSONDecodeError('Fail to decode API response as JSON, API response is not JSON: `{0}`. with message: `{1}`'.format(response_object.text,repr(e)))
65 |
66 | # raise API error HTTP status code
67 | if response_object.status_code >= 400:
68 | raise MidtransAPIError(
69 | message='Midtrans API is returning API error. HTTP status code: `{0}`. '
70 | 'API response: `{1}`'.format(response_object.status_code,response_object.text),
71 | api_response_dict=response_dict,
72 | http_status_code=response_object.status_code,
73 | raw_http_client_data=response_object
74 | )
75 | # raise core API error status code
76 | if 'status_code' in response_dict.keys() and int(response_dict['status_code']) >= 400 and int(response_dict['status_code']) != 407:
77 | raise MidtransAPIError(
78 | 'Midtrans API is returning API error. API status code: `{0}`. '
79 | 'API response: `{1}`'.format(response_dict['status_code'],response_object.text),
80 | api_response_dict=response_dict,
81 | http_status_code=response_object.status_code,
82 | raw_http_client_data=response_object
83 | )
84 |
85 | return response_dict, response_object
86 |
--------------------------------------------------------------------------------
/examples/subscription/credit_card_subscription_example.py:
--------------------------------------------------------------------------------
1 | # This is just for very basic implementation reference, in production, you should validate the incoming requests and implement your backend more securely.
2 | import midtransclient
3 | import datetime
4 |
5 | # Initialize core api client object
6 | # You can find it in Merchant Portal -> Settings -> Access keys
7 | core_api = midtransclient.CoreApi(
8 | is_production=False,
9 | server_key='YOUR_SERVER_KEY',
10 | client_key='YOUR_CLIENT_KEY'
11 | )
12 |
13 | # To use API subscription for credit card, you should first obtain the 1 click token
14 | # Refer to this docs: https://docs.midtrans.com/en/core-api/advanced-features?id=recurring-transaction-with-subscriptions-api
15 |
16 | # You will receive saved_token_id as part of the response when the initial card payment is accepted (will also available in the HTTP notification's JSON)
17 | # Refer to this docs: https://docs.midtrans.com/en/core-api/advanced-features?id=sample-3ds-authenticate-json-response-for-the-first-transaction
18 | # {
19 | # ...
20 | # "card_type": "credit",
21 | # "saved_token_id":"481111xDUgxnnredRMAXuklkvAON1114",
22 | # "saved_token_id_expired_at": "2022-12-31 07:00:00",
23 | # ...
24 | # }
25 | # Sample saved token id for testing purpose
26 | SAVED_TOKEN_ID = '436502qFfqfAQKScMtPRPdZDOaeg7199'
27 |
28 | # prepare subscription parameter ( refer to: https://api-docs.midtrans.com/#create-subscription )
29 | param = {
30 | "name": "SUBS-PY-"+datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S"),
31 | "amount": "100000",
32 | "currency": "IDR",
33 | "payment_type": "credit_card",
34 | "token": SAVED_TOKEN_ID,
35 | "schedule": {
36 | "interval": 1,
37 | "interval_unit": "day",
38 | "max_interval": 7
39 | },
40 | "metadata": {
41 | "description": "Recurring payment for A"
42 | },
43 | "customer_details": {
44 | "first_name": "John A",
45 | "last_name": "Doe A",
46 | "email": "johndoe@email.com",
47 | "phone": "+62812345678"
48 | }
49 | }
50 |
51 | # create subscription
52 | create_subscription_response = core_api.create_subscription(param)
53 | print('create_subscription_response:')
54 | print(create_subscription_response)
55 |
56 | # subscription_response is dictionary representation of API JSON response
57 | # sample:
58 | # {
59 | # 'id': 'b6eb6a04-33e6-46a2-a298-cd78e55b3a3f',
60 | # 'name': 'SUBS-PY-1',
61 | # 'amount': '100000',
62 | # 'currency': 'IDR',
63 | # 'created_at': '2021-10-27 13:29:51',
64 | # 'schedule': {
65 | # 'interval': 1,
66 | # 'current_interval': 0,
67 | # 'max_interval': 7,
68 | # 'interval_unit': 'day',
69 | # 'start_time': '2021-10-27 13:30:01',
70 | # 'next_execution_at': '2021-10-27 13:30:01'
71 | # },
72 | # 'status': 'active',
73 | # 'token': '436502qFfqfAQKScMtPRPdZDOaeg7199',
74 | # 'payment_type': 'credit_card',
75 | # 'transaction_ids': [
76 |
77 | # ],
78 | # 'metadata': {
79 | # 'description': 'Recurring payment for A'
80 | # },
81 | # 'customer_details': {
82 | # 'email': 'johndoe@email.com',
83 | # 'first_name': 'John',
84 | # 'last_name': 'Doe',
85 | # 'phone': '+62812345678'
86 | # }
87 | # }
88 |
89 | subscription_id_response = create_subscription_response['id']
90 |
91 | # get subscription by subscription_id
92 | get_subscription_response = core_api.get_subscription(subscription_id_response)
93 | print('get_subscription_response:')
94 | print(get_subscription_response)
95 |
96 | # enable subscription by subscription_id
97 | enable_subscription_response = core_api.enable_subscription(subscription_id_response)
98 | print('enable_subscription_response:')
99 | print(enable_subscription_response)
100 |
101 | # update subscription by subscription_id
102 | update_param = {
103 | "name": "SUBS-PY-UPDATE",
104 | "amount": "100000",
105 | "currency": "IDR",
106 | "token": SAVED_TOKEN_ID,
107 | "schedule": {
108 | "interval": 1
109 | }
110 | }
111 | update_subscription_response = core_api.update_subscription(subscription_id_response, update_param)
112 | print('update_subscription_response:')
113 | print(update_subscription_response)
114 |
115 | # disable subscription by subscription_id
116 | disable_subscription_response = core_api.disable_subscription(subscription_id_response)
117 | print('disable_subscription_response:')
118 | print(disable_subscription_response)
119 |
--------------------------------------------------------------------------------
/examples/tokenization/tokenization_example.py:
--------------------------------------------------------------------------------
1 | # This is just for very basic implementation reference, in production, you should validate the incoming requests and implement your backend more securely.
2 | import midtransclient
3 | import datetime
4 |
5 | # Initialize core api client object
6 | # You can find it in Merchant Portal -> Settings -> Access keys
7 | core_api = midtransclient.CoreApi(
8 | is_production=False,
9 | server_key='YOUR_SERVER_KEY',
10 | client_key='YOUR_CLIENT_KEY'
11 | )
12 |
13 | # prepare parameter ( refer to: https://api-docs.midtrans.com/#create-pay-account )
14 | param = {
15 | "payment_type": "gopay",
16 | "gopay_partner": {
17 | "phone_number": "81234567891",
18 | "country_code": "62",
19 | "redirect_url": "https://mywebstore.com/gopay-linking-finish" #please update with your redirect URL
20 | }
21 | }
22 |
23 | # link payment account
24 | link_payment_account_response = core_api.link_payment_account(param)
25 | print('link_payment_account_response:')
26 | print(link_payment_account_response)
27 |
28 | # link_payment_account_response is dictionary representation of API JSON response
29 | # sample:
30 | # {
31 | # "status_code": "201",
32 | # "payment_type": "gopay",
33 | # "account_id": "6e902848-c4c3-4f40-abe6-109e8749df21",
34 | # "account_status": "PENDING",
35 | # "actions": [
36 | # {
37 | # "name": "activation-deeplink",
38 | # "method": "GET",
39 | # "url": "https://api-v2.sandbox.midtrans.com/v2/pay/account/gpar_2b78cfea-2afa-49b3-86d5-3866626ce015/link"
40 | # },
41 | # {
42 | # "name": "activation-link-url",
43 | # "method": "GET",
44 | # "url": "https://api-v2.sandbox.midtrans.com/v2/pay/account/gpar_2b78cfea-2afa-49b3-86d5-3866626ce015/link"
45 | # },
46 | # {
47 | # "name": "activation-link-app",
48 | # "method": "GET",
49 | # "url": "https://simulator-v2.sandbox.midtrans.com/gopay/partner/web/otp?id=e4514b08-cc16-486e-9e4a-d8d8dd0bfe49"
50 | # }
51 | # ],
52 | # "metadata": {
53 | # "reference_id": "48b854c9-cc65-4af2-a3f0-38ae81798512"
54 | # }
55 | # }
56 | # for the first link, the account status is PENDING, you must activate it by accessing one of the URLs on the actions object
57 |
58 | # Sample active account id for testing purpose
59 | active_account_id = "6975fc98-8d44-490d-b50a-28d2810d6856"
60 | # get payment account by account_id
61 | get_payment_account_response = core_api.get_payment_account(active_account_id)
62 | print('get_payment_account_response:')
63 | print(get_payment_account_response)
64 | # sample
65 | # {
66 | # "status_code": "200",
67 | # "payment_type": "gopay",
68 | # "account_id": "6975fc98-8d44-490d-b50a-28d2810d6856",
69 | # "account_status": "ENABLED",
70 | # "metadata": {
71 | # "payment_options": [
72 | # {
73 | # "name": "PAY_LATER",
74 | # "active": true,
75 | # "balance": {
76 | # "value": "4649999.00",
77 | # "currency": "IDR"
78 | # },
79 | # "metadata": {},
80 | # "token": "04ed77b7-5ad5-4ba5-b631-d72aa369c2f7"
81 | # },
82 | # {
83 | # "name": "GOPAY_WALLET",
84 | # "active": true,
85 | # "balance": {
86 | # "value": "6100000.00",
87 | # "currency": "IDR"
88 | # },
89 | # "metadata": {},
90 | # "token": "8035816f-462e-4fa1-a5ef-c30bf71e2ee6"
91 | # }
92 | # ]
93 | # }
94 | # }
95 |
96 | # request charge
97 | params = {
98 | "payment_type": "gopay",
99 | "gopay": {
100 | "account_id": get_payment_account['account_id'],
101 | "payment_option_token": get_payment_account['metadata']['payment_options'][0]['token'],
102 | "callback_url": "https://mywebstore.com/gopay-linking-finish" #please update with your redirect URL
103 | },
104 | "transaction_details": {
105 | "gross_amount": 100000,
106 | "order_id": "GOPAY-LINK-"+datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S")
107 | }
108 | }
109 | charge_response = core_api.charge(params)
110 | print('charge_response:')
111 | print(charge_response)
112 |
113 |
114 | # unlink payment account by account_id
115 | # when account status still PENDING, you will get status code 412
116 | # sample response
117 | # {
118 | # "status_code": "412",
119 | # "status_message": "Account status cannot be updated.",
120 | # "id": "19eda9e4-37c9-4bfd-abb2-c60bb3a91084"
121 | # }
122 | try:
123 | unlink_payment_account_response = core_api.unlink_payment_account(link_payment_account['account_id'])
124 | print('unlink_payment_account_response:')
125 | print(unlink_payment_account_response)
126 | except Exception as e:
127 | print('unlink_failure_response:')
128 | print(e)
129 |
--------------------------------------------------------------------------------
/midtransclient/transactions.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import json
3 |
4 | class Transactions:
5 | """
6 | These are wrapper/implementation of API methods described on:
7 | https://api-docs.midtrans.com/#midtrans-api
8 | """
9 |
10 | def __init__(self,parent):
11 | self.parent = parent
12 |
13 | def status(self, transaction_id):
14 | api_url = self.parent.api_config.get_core_api_base_url()+'/v2/'+transaction_id+'/status'
15 | response_dict, response_object = self.parent.http_client.request(
16 | 'get',
17 | self.parent.api_config.server_key,
18 | api_url,
19 | dict(),
20 | self.parent.api_config.custom_headers,
21 | self.parent.api_config.proxies)
22 | return response_dict
23 |
24 | def statusb2b(self, transaction_id):
25 | api_url = self.parent.api_config.get_core_api_base_url()+'/v2/'+transaction_id+'/status/b2b'
26 | response_dict, response_object = self.parent.http_client.request(
27 | 'get',
28 | self.parent.api_config.server_key,
29 | api_url,
30 | dict(),
31 | self.parent.api_config.custom_headers,
32 | self.parent.api_config.proxies)
33 | return response_dict
34 |
35 | def approve(self, transaction_id):
36 | api_url = self.parent.api_config.get_core_api_base_url()+'/v2/'+transaction_id+'/approve'
37 | response_dict, response_object = self.parent.http_client.request(
38 | 'post',
39 | self.parent.api_config.server_key,
40 | api_url,
41 | dict(),
42 | self.parent.api_config.custom_headers,
43 | self.parent.api_config.proxies)
44 | return response_dict
45 |
46 | def deny(self, transaction_id):
47 | api_url = self.parent.api_config.get_core_api_base_url()+'/v2/'+transaction_id+'/deny'
48 | response_dict, response_object = self.parent.http_client.request(
49 | 'post',
50 | self.parent.api_config.server_key,
51 | api_url,
52 | dict(),
53 | self.parent.api_config.custom_headers,
54 | self.parent.api_config.proxies)
55 | return response_dict
56 |
57 | def cancel(self, transaction_id):
58 | api_url = self.parent.api_config.get_core_api_base_url()+'/v2/'+transaction_id+'/cancel'
59 | response_dict, response_object = self.parent.http_client.request(
60 | 'post',
61 | self.parent.api_config.server_key,
62 | api_url,
63 | dict(),
64 | self.parent.api_config.custom_headers,
65 | self.parent.api_config.proxies)
66 | return response_dict
67 |
68 | def expire(self, transaction_id):
69 | api_url = self.parent.api_config.get_core_api_base_url()+'/v2/'+transaction_id+'/expire'
70 | response_dict, response_object = self.parent.http_client.request(
71 | 'post',
72 | self.parent.api_config.server_key,
73 | api_url,
74 | dict(),
75 | self.parent.api_config.custom_headers,
76 | self.parent.api_config.proxies)
77 | return response_dict
78 |
79 | def refund(self, transaction_id,parameters=dict()):
80 | api_url = self.parent.api_config.get_core_api_base_url()+'/v2/'+transaction_id+'/refund'
81 | response_dict, response_object = self.parent.http_client.request(
82 | 'post',
83 | self.parent.api_config.server_key,
84 | api_url,
85 | parameters,
86 | self.parent.api_config.custom_headers,
87 | self.parent.api_config.proxies)
88 | return response_dict
89 |
90 | def refundDirect(self, transaction_id,parameters=dict()):
91 | api_url = self.parent.api_config.get_core_api_base_url()+'/v2/'+transaction_id+'/refund/online/direct'
92 | response_dict, response_object = self.parent.http_client.request(
93 | 'post',
94 | self.parent.api_config.server_key,
95 | api_url,
96 | parameters,
97 | self.parent.api_config.custom_headers,
98 | self.parent.api_config.proxies)
99 | return response_dict
100 |
101 | def notification(self, notification=dict()):
102 | is_notification_string = isinstance(notification, str)
103 | if is_notification_string:
104 | try:
105 | notification = json.loads(notification)
106 | except Exception as e:
107 | raise JSONDecodeError('fail to parse `notification` string as JSON. Use JSON string or Dict as `notification`. with message: `{0}`'.format(repr(e)))
108 |
109 | transaction_id = notification['transaction_id']
110 | api_url = self.parent.api_config.get_core_api_base_url()+'/v2/'+transaction_id+'/status'
111 | response_dict, response_object = self.parent.http_client.request(
112 | 'get',
113 | self.parent.api_config.server_key,
114 | api_url,
115 | self.parent.api_config.custom_headers,
116 | self.parent.api_config.proxies)
117 | return response_dict
118 |
119 | class JSONDecodeError(Exception):
120 | pass
--------------------------------------------------------------------------------
/examples/snap/snap_advanced_example.py:
--------------------------------------------------------------------------------
1 | import midtransclient
2 | # This is just for very basic implementation reference, in production, you should validate the incoming requests and implement your backend more securely.
3 | # Please refer to this docs for snap popup:
4 | # https://docs.midtrans.com/en/snap/integration-guide?id=integration-steps-overview
5 |
6 | # Please refer to this docs for snap-redirect:
7 | # https://docs.midtrans.com/en/snap/integration-guide?id=alternative-way-to-display-snap-payment-page-via-redirect
8 |
9 | # Initialize snap client object
10 | # You can find it in Merchant Portal -> Settings -> Access keys
11 | snap = midtransclient.Snap(
12 | is_production=False,
13 | server_key='YOUR_SERVER_KEY',
14 | client_key='YOUR_CLIENT_KEY'
15 | )
16 |
17 | # Alternative way to initialize snap client object:
18 | # snap = midtransclient.Snap()
19 | # snap.api_config.set(
20 | # is_production=False,
21 | # server_key='YOUR_SERVER_KEY',
22 | # client_key='YOUR_CLIENT_KEY'
23 | # )
24 |
25 | # Another alternative way to initialize snap client object:
26 | # snap = midtransclient.Snap()
27 | # snap.api_config.is_production=False
28 | # snap.api_config.server_key='YOUR_SERVER_KEY'
29 | # snap.api_config.client_key='YOUR_CLIENT_KEY'
30 |
31 | # prepare SNAP API parameter ( refer to: https://snap-docs.midtrans.com ) this is full parameter including optionals parameter.
32 | param = {
33 | "transaction_details": {
34 | "order_id": "test-transaction-1234",
35 | "gross_amount": 10000
36 | },
37 | "item_details": [{
38 | "id": "ITEM1",
39 | "price": 10000,
40 | "quantity": 1,
41 | "name": "Midtrans Bear",
42 | "brand": "Midtrans",
43 | "category": "Toys",
44 | "merchant_name": "Midtrans"
45 | }],
46 | "customer_details": {
47 | "first_name": "John",
48 | "last_name": "Watson",
49 | "email": "test@example.com",
50 | "phone": "+628123456",
51 | "billing_address": {
52 | "first_name": "John",
53 | "last_name": "Watson",
54 | "email": "test@example.com",
55 | "phone": "081 2233 44-55",
56 | "address": "Sudirman",
57 | "city": "Jakarta",
58 | "postal_code": "12190",
59 | "country_code": "IDN"
60 | },
61 | "shipping_address": {
62 | "first_name": "John",
63 | "last_name": "Watson",
64 | "email": "test@example.com",
65 | "phone": "0 8128-75 7-9338",
66 | "address": "Sudirman",
67 | "city": "Jakarta",
68 | "postal_code": "12190",
69 | "country_code": "IDN"
70 | }
71 | },
72 | "enabled_payments": ["credit_card", "mandiri_clickpay", "cimb_clicks","bca_klikbca", "bca_klikpay", "bri_epay", "echannel", "indosat_dompetku","mandiri_ecash", "permata_va", "bca_va", "bni_va", "other_va", "gopay","kioson", "indomaret", "gci", "danamon_online"],
73 | "credit_card": {
74 | "secure": True,
75 | "bank": "bca",
76 | "installment": {
77 | "required": False,
78 | "terms": {
79 | "bni": [3, 6, 12],
80 | "mandiri": [3, 6, 12],
81 | "cimb": [3],
82 | "bca": [3, 6, 12],
83 | "offline": [6, 12]
84 | }
85 | },
86 | "whitelist_bins": [
87 | "48111111",
88 | "41111111"
89 | ]
90 | },
91 | "bca_va": {
92 | "va_number": "12345678911",
93 | "free_text": {
94 | "inquiry": [
95 | {
96 | "en": "text in English",
97 | "id": "text in Bahasa Indonesia"
98 | }
99 | ],
100 | "payment": [
101 | {
102 | "en": "text in English",
103 | "id": "text in Bahasa Indonesia"
104 | }
105 | ]
106 | }
107 | },
108 | "bni_va": {
109 | "va_number": "12345678"
110 | },
111 | "permata_va": {
112 | "va_number": "1234567890",
113 | "recipient_name": "SUDARSONO"
114 | },
115 | "callbacks": {
116 | "finish": "https://demo.midtrans.com"
117 | },
118 | "expiry": {
119 | "start_time": "2025-12-20 18:11:08 +0700",
120 | "unit": "minute",
121 | "duration": 9000
122 | },
123 | "custom_field1": "custom field 1 content",
124 | "custom_field2": "custom field 2 content",
125 | "custom_field3": "custom field 3 content"
126 | }
127 |
128 | # create transaction
129 | transaction = snap.create_transaction(param)
130 |
131 | # transaction token
132 | transaction_token = transaction['token']
133 | print('transaction_token:')
134 | print(transaction_token)
135 |
136 | # transaction redirect url
137 | transaction_url = transaction['redirect_url']
138 | print('transaction_url:')
139 | print(transaction_url)
140 |
141 | # alternative way to create transaction_token:
142 | transaction_token = snap.create_transaction_token(param)
143 | print('transaction_token:')
144 | print(transaction_token)
145 |
146 | # alternative way to create transaction_url:
147 | transaction_url = snap.create_transaction_redirect_url(param)
148 | print('transaction_url:')
149 | print(transaction_url)
150 |
--------------------------------------------------------------------------------
/examples/subscription/gopay_subscription_example.py:
--------------------------------------------------------------------------------
1 | # This is just for very basic implementation reference, in production, you should validate the incoming requests and implement your backend more securely.
2 | import midtransclient
3 | import datetime
4 |
5 | # Initialize core api client object
6 | # You can find it in Merchant Portal -> Settings -> Access keys
7 | core_api = midtransclient.CoreApi(
8 | is_production=False,
9 | server_key='YOUR_SERVER_KEY',
10 | client_key='YOUR_CLIENT_KEY'
11 | )
12 |
13 | # To use API subscription for gopay, you should first link your customer gopay account with gopay tokenization
14 | # Refer to this docs: https://api-docs.midtrans.com/#gopay-tokenization
15 |
16 | # You will receive gopay payment token using `get_payment_account` API call.
17 | # You can see some Tokenization API examples here (examples/tokenization)
18 | # {
19 | # "status_code": "200",
20 | # "payment_type": "gopay",
21 | # "account_id": "6975fc98-8d44-490d-b50a-28d2810d6856",
22 | # "account_status": "ENABLED",
23 | # "metadata": {
24 | # "payment_options": [
25 | # {
26 | # "name": "PAY_LATER",
27 | # "active": true,
28 | # "balance": {
29 | # "value": "4649999.00",
30 | # "currency": "IDR"
31 | # },
32 | # "metadata": {},
33 | # "token": "04ed77b7-5ad5-4ba5-b631-d72aa369c2f7"
34 | # },
35 | # {
36 | # "name": "GOPAY_WALLET",
37 | # "active": true,
38 | # "balance": {
39 | # "value": "6100000.00",
40 | # "currency": "IDR"
41 | # },
42 | # "metadata": {},
43 | # "token": "8035816f-462e-4fa1-a5ef-c30bf71e2ee6"
44 | # }
45 | # ]
46 | # }
47 | # }
48 | # Sample gopay payment option token and gopay account id for testing purpose that has been already activated before
49 | GOPAY_PAYMENT_OPTION_TOKEN = '04ed77b7-5ad5-4ba5-b631-d72aa369c2f7'
50 | ACTIVE_ACCOUNT_ID = '6975fc98-8d44-490d-b50a-28d2810d6856'
51 |
52 | # prepare subscription parameter ( refer to: https://api-docs.midtrans.com/#create-subscription )
53 | param = {
54 | "name": "SUBS-PY-"+datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S"),
55 | "amount": "100000",
56 | "currency": "IDR",
57 | "payment_type": "gopay",
58 | "token": GOPAY_PAYMENT_OPTION_TOKEN,
59 | "schedule": {
60 | "interval": 1,
61 | "interval_unit": "day",
62 | "max_interval": 7
63 | },
64 | "metadata": {
65 | "description": "Recurring payment for A"
66 | },
67 | "customer_details": {
68 | "first_name": "John A",
69 | "last_name": "Doe A",
70 | "email": "johndoe@email.com",
71 | "phone": "+62812345678"
72 | },
73 | "gopay": {
74 | "account_id": ACTIVE_ACCOUNT_ID
75 | }
76 | }
77 |
78 | # create subscription
79 | create_subscription_response = core_api.create_subscription(param)
80 | print('create_subscription_response:')
81 | print(create_subscription_response)
82 |
83 | # subscription_response is dictionary representation of API JSON response
84 | # sample:
85 | # {
86 | # 'id': 'b6eb6a04-33e6-46a2-a298-cd78e55b3a3f',
87 | # 'name': 'SUBS-PY-1',
88 | # 'amount': '100000',
89 | # 'currency': 'IDR',
90 | # 'created_at': '2021-10-27 13:29:51',
91 | # 'schedule': {
92 | # 'interval': 1,
93 | # 'current_interval': 0,
94 | # 'max_interval': 7,
95 | # 'interval_unit': 'day',
96 | # 'start_time': '2021-10-27 13:30:01',
97 | # 'next_execution_at': '2021-10-27 13:30:01'
98 | # },
99 | # 'status': 'active',
100 | # 'token': '436502qFfqfAQKScMtPRPdZDOaeg7199',
101 | # 'payment_type': 'gopay',
102 | # 'transaction_ids': [
103 |
104 | # ],
105 | # 'metadata': {
106 | # 'description': 'Recurring payment for A'
107 | # },
108 | # 'customer_details': {
109 | # 'email': 'johndoe@email.com',
110 | # 'first_name': 'John',
111 | # 'last_name': 'Doe',
112 | # 'phone': '+62812345678'
113 | # }
114 | # }
115 |
116 | subscription_id_response = create_subscription_response['id']
117 |
118 | # get subscription by subscription_id
119 | get_subscription_response = core_api.get_subscription(subscription_id_response)
120 | print('get_subscription_response:')
121 | print(get_subscription_response)
122 |
123 | # enable subscription by subscription_id
124 | enable_subscription_response = core_api.enable_subscription(subscription_id_response)
125 | print('enable_subscription_response:')
126 | print(enable_subscription_response)
127 |
128 | # update subscription by subscription_id
129 | update_param = {
130 | "name": "SUBS-PY-UPDATE",
131 | "amount": "100000",
132 | "currency": "IDR",
133 | "token": GOPAY_PAYMENT_OPTION_TOKEN,
134 | "schedule": {
135 | "interval": 1
136 | }
137 | }
138 | update_subscription_response = core_api.update_subscription(subscription_id_response, update_param)
139 | print('update_subscription_response:')
140 | print(update_subscription_response)
141 |
142 | # disable subscription by subscription_id
143 | disable_subscription_response = core_api.disable_subscription(subscription_id_response)
144 | print('disable_subscription_response:')
145 | print(disable_subscription_response)
146 |
--------------------------------------------------------------------------------
/tests/test_subscription.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from .config import USED_SERVER_KEY, USED_CLIENT_KEY
3 | from .helpers import is_str
4 | from .context import midtransclient
5 | import datetime
6 | import json
7 | from pprint import pprint
8 |
9 | SUBSCRIPTION_ID = ''
10 |
11 | def test_subscription_class():
12 | subscription = generate_subscription_instance()
13 | methods = dir(subscription)
14 | assert "create_subscription" in methods
15 | assert is_str(subscription.api_config.server_key)
16 | assert is_str(subscription.api_config.client_key)
17 |
18 | def test_subscription_create_subscription():
19 | subscription = generate_subscription_instance()
20 | parameters = generate_param()
21 | response = subscription.create_subscription(parameters)
22 | global SUBSCRIPTION_ID
23 | SUBSCRIPTION_ID = response['id']
24 | assert isinstance(response, dict)
25 | assert 'id' in response.keys()
26 | assert response['status'] == 'active'
27 |
28 | def test_subscription_fail_empty_param():
29 | subscription = generate_subscription_instance()
30 | parameters = None
31 | err = ''
32 | try:
33 | response = subscription.create_subscription(parameters)
34 | except Exception as e:
35 | err = e
36 | assert 'MidtransAPIError' in err.__class__.__name__
37 | assert '400' in err.message
38 | assert 'Bad request' in err.message
39 |
40 | def test_subscription_fail_zero_amount():
41 | subscription = generate_subscription_instance()
42 | parameters = generate_param()
43 | parameters['amount'] = 0
44 | err = ''
45 | try:
46 | response = subscription.create_subscription(parameters)
47 | except Exception as e:
48 | err = e
49 | assert 'MidtransAPIError' in err.__class__.__name__
50 | assert '400' in err.message
51 | assert 'subscription.amount must be between 0.01 - 99999999999.00' in err.message
52 |
53 | def test_subscription_get_subscription():
54 | subscription = generate_subscription_instance()
55 | parameters = generate_param()
56 | response = subscription.get_subscription(SUBSCRIPTION_ID)
57 | assert isinstance(response, dict)
58 | assert response['id'] == SUBSCRIPTION_ID
59 | assert response['status'] == 'active'
60 |
61 | def test_subscription_get_subscription_not_found():
62 | subscription = generate_subscription_instance()
63 | err = ''
64 | try:
65 | response = subscription.get_subscription('123')
66 | except Exception as e:
67 | err = e
68 | assert 'MidtransAPIError' in err.__class__.__name__
69 | assert '404' in err.message
70 | assert 'Subscription doesn\'t exist.' in err.message
71 |
72 | def test_subscription_disable_subscription():
73 | subscription = generate_subscription_instance()
74 | response = subscription.disable_subscription(SUBSCRIPTION_ID)
75 | assert isinstance(response, dict)
76 | assert response['status_message'] == 'Subscription is updated.'
77 | get_subscription = subscription.get_subscription(SUBSCRIPTION_ID)
78 | assert get_subscription['id'] == SUBSCRIPTION_ID
79 | assert get_subscription['status'] == 'inactive'
80 |
81 | def test_subscription_enable_subscription():
82 | subscription = generate_subscription_instance()
83 | response = subscription.enable_subscription(SUBSCRIPTION_ID)
84 | assert isinstance(response, dict)
85 | assert response['status_message'] == 'Subscription is updated.'
86 | get_subscription = subscription.get_subscription(SUBSCRIPTION_ID)
87 | assert get_subscription['id'] == SUBSCRIPTION_ID
88 | assert get_subscription['status'] == 'active'
89 | # disable subscription to prevent Core API continue to execute subscription
90 | response = subscription.disable_subscription(SUBSCRIPTION_ID)
91 |
92 | def test_subscription_update_subscription():
93 | subscription = generate_subscription_instance()
94 | parameters = generate_param()
95 | parameters['metadata']['description'] = 'update recurring payment to ABC'
96 | response = subscription.update_subscription(SUBSCRIPTION_ID,parameters)
97 | assert isinstance(response, dict)
98 | assert response['status_message'] == 'Subscription is updated.'
99 | get_subscription = subscription.get_subscription(SUBSCRIPTION_ID)
100 | assert get_subscription['id'] == SUBSCRIPTION_ID
101 | assert get_subscription['metadata']['description'] == 'update recurring payment to ABC'
102 |
103 | # ======== HELPER FUNCTIONS BELOW ======== #
104 | def generate_subscription_instance():
105 | subscription = midtransclient.CoreApi(is_production=False,
106 | server_key=USED_SERVER_KEY,
107 | client_key=USED_CLIENT_KEY)
108 | return subscription
109 |
110 | def generate_param():
111 | return {
112 | "name": "SUBS-PY-"+datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S"),
113 | "amount": "100000",
114 | "currency": "IDR",
115 | "payment_type": "credit_card",
116 | "token": "436502qFfqfAQKScMtPRPdZDOaeg7199",
117 | "schedule": {
118 | "interval": 1,
119 | "interval_unit": "day",
120 | "max_interval": 7
121 | },
122 | "metadata": {
123 | "description": "Recurring payment for A"
124 | },
125 | "customer_details": {
126 | "first_name": "John A",
127 | "last_name": "Doe A",
128 | "email": "johndoe@email.com",
129 | "phone": "+62812345678"
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/examples/flask_app/templates/simple_core_api_checkout.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Checkout
5 |
6 |
7 |
8 |
9 |
10 |
11 | Checkout
12 |
41 |
42 |
43 | Transaction Result:
44 | Awaiting transactions...
45 | Transaction verified status result:
46 | Awaiting transactions...
47 |
48 | Testing cards:
49 |
50 | For 3D Secure:
51 | Visa success 4811 1111 1111 1114
52 | Visa deny by bank 4711 1111 1111 1115
53 | Visa deny by FDS 4611 1111 1111 1116
54 |
55 | MasterCard success 5211 1111 1111 1117
56 | MasterCard deny by bank 5111 1111 1111 1118
57 | MasterCard deny by FDS 5411 1111 1111 1115
58 |
59 | Challenge by FDS 4511 1111 1111 1117
60 |
61 |
62 |
63 |
64 | Check `web.py` file, section `Using Core API - Credit Card` for the backend implementation
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
189 |
190 |
--------------------------------------------------------------------------------
/tests/test_snap.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from .config import USED_SERVER_KEY, USED_CLIENT_KEY
3 | from .helpers import is_str
4 | from .context import midtransclient
5 | import datetime
6 | from pprint import pprint
7 |
8 | reused_order_id = "py-midtransclient-test-"+str(datetime.datetime.now()).replace(" ", "").replace(":", "")
9 |
10 | def test_snap_class():
11 | snap = generate_snap_instance()
12 | methods = dir(snap)
13 | assert "create_transaction" in methods
14 | assert "create_transaction_token" in methods
15 | assert "create_transaction_redirect_url" in methods
16 | assert is_str(snap.api_config.server_key)
17 | assert is_str(snap.api_config.client_key)
18 |
19 | def test_snap_create_transaction_min():
20 | snap = generate_snap_instance()
21 | param = generate_param_min()
22 | param['transaction_details']['order_id'] = reused_order_id
23 | transaction = snap.create_transaction(param)
24 | assert isinstance(transaction, dict)
25 | assert is_str(transaction['token'])
26 | assert is_str(transaction['redirect_url'])
27 |
28 | def test_snap_create_transaction_max():
29 | snap = generate_snap_instance()
30 | param = generate_param_max()
31 | transaction = snap.create_transaction(param)
32 | assert isinstance(transaction, dict)
33 | assert is_str(transaction['token'])
34 | assert is_str(transaction['redirect_url'])
35 |
36 | def test_snap_create_transaction_token():
37 | snap = generate_snap_instance()
38 | param = generate_param_min()
39 | token = snap.create_transaction_token(param)
40 | assert is_str(token)
41 |
42 | def test_snap_create_transaction_redirect_url():
43 | snap = generate_snap_instance()
44 | param = generate_param_min()
45 | redirect_url = snap.create_transaction_redirect_url(param)
46 | assert is_str(redirect_url)
47 |
48 | def test_snap_status_fail_404():
49 | snap = generate_snap_instance()
50 | err = ''
51 | try:
52 | response = snap.transactions.status('non-exist-order-id')
53 | except Exception as e:
54 | err = e
55 | assert 'MidtransAPIError' in err.__class__.__name__
56 | assert '404' in err.message
57 | assert 'exist' in err.message
58 |
59 | def test_snap_request_fail_401():
60 | snap = generate_snap_instance()
61 | snap.api_config.server_key='dummy'
62 | param = generate_param_min()
63 | err = ''
64 | try:
65 | transaction = snap.create_transaction(param)
66 | except Exception as e:
67 | err = e
68 | assert 'MidtransAPIError' in err.__class__.__name__
69 | assert '401' in err.message
70 | assert 'unauthorized' in err.message
71 |
72 | def test_snap_request_fail_empty_param():
73 | snap = generate_snap_instance()
74 | param = None
75 | err = ''
76 | try:
77 | transaction = snap.create_transaction(param)
78 | except Exception as e:
79 | err = e
80 | assert 'MidtransAPIError' in err.__class__.__name__
81 | assert '400' in err.message
82 | assert 'is required' in err.message
83 |
84 | def test_snap_request_fail_zero_gross_amount():
85 | snap = generate_snap_instance()
86 | param = generate_param_min()
87 | param['transaction_details']['gross_amount'] = 0
88 | err = ''
89 | try:
90 | transaction = snap.create_transaction(param)
91 | except Exception as e:
92 | err = e
93 | assert 'MidtransAPIError' in err.__class__.__name__
94 |
95 | def test_snap_exception_MidtransAPIError():
96 | snap = generate_snap_instance()
97 | snap.api_config.server_key='dummy'
98 | param = generate_param_min()
99 | err = ''
100 | try:
101 | transaction = snap.create_transaction(param)
102 | except Exception as e:
103 | err = e
104 | assert 'MidtransAPIError' in err.__class__.__name__
105 | assert is_str(err.message)
106 | assert isinstance(err.api_response_dict, dict)
107 | assert isinstance(err.http_status_code,int)
108 |
109 | def test_snap_create_transaction_min_with_custom_headers_via_setter():
110 | snap = generate_snap_instance()
111 | snap.api_config.custom_headers = {
112 | 'X-Override-Notification':'https://example.org'
113 | }
114 | param = generate_param_min()
115 | param['transaction_details']['order_id'] = reused_order_id
116 | transaction = snap.create_transaction(param)
117 | assert isinstance(transaction, dict)
118 | assert is_str(transaction['token'])
119 | assert is_str(transaction['redirect_url'])
120 |
121 | # ======== HELPER FUNCTIONS BELOW ======== #
122 | def generate_snap_instance():
123 | snap = midtransclient.Snap(is_production=False,
124 | server_key=USED_SERVER_KEY,
125 | client_key=USED_CLIENT_KEY)
126 | return snap
127 |
128 | def generate_param_min():
129 | return {
130 | "transaction_details": {
131 | "order_id": "py-midtransclient-test-"+datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
132 | "gross_amount": 200000
133 | }, "credit_card":{
134 | "secure" : True
135 | }
136 | }
137 |
138 | def generate_param_max():
139 | return {
140 | "transaction_details": {
141 | "order_id": "py-midtransclient-test-"+datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
142 | "gross_amount": 10000
143 | },
144 | "item_details": [{
145 | "id": "ITEM1",
146 | "price": 10000,
147 | "quantity": 1,
148 | "name": "Midtrans Bear",
149 | "brand": "Midtrans",
150 | "category": "Toys",
151 | "merchant_name": "Midtrans"
152 | }],
153 | "customer_details": {
154 | "first_name": "John",
155 | "last_name": "Watson",
156 | "email": "test@example.com",
157 | "phone": "+628123456",
158 | "billing_address": {
159 | "first_name": "John",
160 | "last_name": "Watson",
161 | "email": "test@example.com",
162 | "phone": "081 2233 44-55",
163 | "address": "Sudirman",
164 | "city": "Jakarta",
165 | "postal_code": "12190",
166 | "country_code": "IDN"
167 | },
168 | "shipping_address": {
169 | "first_name": "John",
170 | "last_name": "Watson",
171 | "email": "test@example.com",
172 | "phone": "0 8128-75 7-9338",
173 | "address": "Sudirman",
174 | "city": "Jakarta",
175 | "postal_code": "12190",
176 | "country_code": "IDN"
177 | }
178 | },
179 | "enabled_payments": ["credit_card", "mandiri_clickpay", "cimb_clicks","bca_klikbca", "bca_klikpay", "bri_epay", "echannel", "indosat_dompetku","mandiri_ecash", "permata_va", "bca_va", "bni_va", "other_va", "gopay","kioson", "indomaret", "gci", "danamon_online"],
180 | "credit_card": {
181 | "secure": True,
182 | "channel": "migs",
183 | "bank": "bca",
184 | "installment": {
185 | "required": False,
186 | "terms": {
187 | "bni": [3, 6, 12],
188 | "mandiri": [3, 6, 12],
189 | "cimb": [3],
190 | "bca": [3, 6, 12],
191 | "offline": [6, 12]
192 | }
193 | },
194 | "whitelist_bins": [
195 | "48111111",
196 | "41111111"
197 | ]
198 | },
199 | "bca_va": {
200 | "va_number": "12345678911",
201 | "free_text": {
202 | "inquiry": [
203 | {
204 | "en": "text in English",
205 | "id": "text in Bahasa Indonesia"
206 | }
207 | ],
208 | "payment": [
209 | {
210 | "en": "text in English",
211 | "id": "text in Bahasa Indonesia"
212 | }
213 | ]
214 | }
215 | },
216 | "bni_va": {
217 | "va_number": "12345678"
218 | },
219 | "permata_va": {
220 | "va_number": "1234567890",
221 | "recipient_name": "SUDARSONO"
222 | },
223 | "callbacks": {
224 | "finish": "https://demo.midtrans.com"
225 | },
226 | "expiry": {
227 | "start_time": "2030-12-20 18:11:08 +0700",
228 | "unit": "minutes",
229 | "duration": 1
230 | },
231 | "custom_field1": "custom field 1 content",
232 | "custom_field2": "custom field 2 content",
233 | "custom_field3": "custom field 3 content"
234 | }
--------------------------------------------------------------------------------
/examples/flask_app/web.py:
--------------------------------------------------------------------------------
1 | # This is just for very basic implementation reference, in production, you should validate the incoming requests and implement your backend more securely.
2 |
3 | import datetime
4 | import json
5 | import os
6 | from flask import Flask, render_template, request, jsonify
7 | from midtransclient import Snap, CoreApi
8 |
9 | # @TODO: Change/fill the following API Keys variable with Your own server & client keys
10 | # You can find it in Merchant Portal -> Settings -> Access keys
11 | SERVER_KEY = 'SB-Mid-server-GwUP_WGbJPXsDzsNEBRs8IYA'
12 | CLIENT_KEY = 'SB-Mid-client-61XuGAwQ8Bj8LxSS'
13 | # Note: by default it uses hardcoded sandbox demo API keys for demonstration purpose
14 |
15 | app = Flask(__name__)
16 |
17 | #==============#
18 | # Using SNAP
19 | #==============#
20 |
21 | # Very simple Snap checkout
22 | @app.route('/simple_checkout')
23 | def simple_checkout():
24 | snap = Snap(
25 | is_production=False,
26 | server_key=SERVER_KEY,
27 | client_key=CLIENT_KEY
28 | )
29 | timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
30 | transaction_token = snap.create_transaction_token({
31 | "transaction_details": {
32 | "order_id": "order-id-python-"+timestamp,
33 | "gross_amount": 200000
34 | }, "credit_card":{
35 | "secure" : True
36 | }
37 | })
38 |
39 | return render_template('simple_checkout.html',
40 | token = transaction_token,
41 | client_key = snap.api_config.client_key)
42 |
43 | #==============#
44 | # Using Core API - Credit Card
45 | #==============#
46 |
47 | # [0] Setup API client and config
48 | core = CoreApi(
49 | is_production=False,
50 | server_key=SERVER_KEY,
51 | client_key=CLIENT_KEY
52 | )
53 | # [1] Render HTML+JS web page to get card token_id and [3] 3DS authentication
54 | @app.route('/simple_core_api_checkout')
55 | def simple_core_api_checkout():
56 | return render_template('simple_core_api_checkout.html',
57 | client_key = core.api_config.client_key)
58 |
59 | # [2] Handle Core API credit card token_id charge
60 | @app.route('/charge_core_api_ajax', methods=['POST'])
61 | def charge_core_api_ajax():
62 | request_json = request.get_json()
63 | timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
64 | try:
65 | charge_api_response = core.charge({
66 | "payment_type": "credit_card",
67 | "transaction_details": {
68 | "gross_amount": 200000,
69 | "order_id": "order-id-python-"+timestamp,
70 | },
71 | "credit_card":{
72 | "token_id": request_json['token_id'],
73 | "authentication": request_json['authenticate_3ds'],
74 | }
75 | })
76 | except Exception as e:
77 | charge_api_response = e.api_response_dict
78 | return charge_api_response
79 |
80 | # [4] Handle Core API check transaction status
81 | @app.route('/check_transaction_status', methods=['POST'])
82 | def check_transaction_status():
83 | request_json = request.get_json()
84 | transaction_status = core.transactions.status(request_json['transaction_id'])
85 |
86 | # [5.A] Handle transaction status on your backend
87 | # Sample transaction_status handling logic
88 | if transaction_status == 'capture':
89 | if fraud_status == 'challenge':
90 | # TODO set transaction status on your databaase to 'challenge'
91 | None
92 | elif fraud_status == 'accept':
93 | # TODO set transaction status on your databaase to 'success'
94 | None
95 | elif transaction_status == 'settlement':
96 | # TODO set transaction status on your databaase to 'success'
97 | # Note: Non-card transaction will become 'settlement' on payment success
98 | # Card transaction will also become 'settlement' D+1, which you can ignore
99 | # because most of the time 'capture' is enough to be considered as success
100 | None
101 | elif transaction_status == 'cancel' or transaction_status == 'deny' or transaction_status == 'expire':
102 | # TODO set transaction status on your databaase to 'failure'
103 | None
104 | elif transaction_status == 'pending':
105 | # TODO set transaction status on your databaase to 'pending' / waiting payment
106 | None
107 | elif transaction_status == 'refund':
108 | # TODO set transaction status on your databaase to 'refund'
109 | None
110 | return jsonify(transaction_status)
111 |
112 | #==============#
113 | # Handling HTTP Post Notification
114 | #==============#
115 |
116 | # [4] Handle Core API check transaction status
117 | @app.route('/notification_handler', methods=['POST'])
118 | def notification_handler():
119 | request_json = request.get_json()
120 | transaction_status_dict = core.transactions.notification(request_json)
121 |
122 | order_id = request_json['order_id']
123 | transaction_status = request_json['transaction_status']
124 | fraud_status = request_json['fraud_status']
125 | transaction_json = json.dumps(transaction_status_dict)
126 |
127 | summary = 'Transaction notification received. Order ID: {order_id}. Transaction status: {transaction_status}. Fraud status: {fraud_status}. Raw notification object:{transaction_json} '.format(order_id=order_id,transaction_status=transaction_status,fraud_status=fraud_status,transaction_json=transaction_json)
128 |
129 | # [5.B] Handle transaction status on your backend
130 | # Sample transaction_status handling logic
131 | if transaction_status == 'capture':
132 | if fraud_status == 'challenge':
133 | # TODO set transaction status on your databaase to 'challenge'
134 | None
135 | elif fraud_status == 'accept':
136 | # TODO set transaction status on your databaase to 'success'
137 | None
138 | elif transaction_status == 'settlement':
139 | # TODO set transaction status on your databaase to 'success'
140 | # Note: Non card transaction will become 'settlement' on payment success
141 | # Credit card will also become 'settlement' D+1, which you can ignore
142 | # because most of the time 'capture' is enough to be considered as success
143 | None
144 | elif transaction_status == 'cancel' or transaction_status == 'deny' or transaction_status == 'expire':
145 | # TODO set transaction status on your databaase to 'failure'
146 | None
147 | elif transaction_status == 'pending':
148 | # TODO set transaction status on your databaase to 'pending' / waiting payment
149 | None
150 | elif transaction_status == 'refund':
151 | # TODO set transaction status on your databaase to 'refund'
152 | None
153 | app.logger.info(summary)
154 | return jsonify(summary)
155 |
156 | #==============#
157 | # Using Core API - other payment method, example: Permata VA
158 | #==============#
159 | @app.route('/simple_core_api_checkout_permata', methods=['GET'])
160 | def simple_core_api_checkout_permata():
161 | timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
162 | charge_api_response = core.charge({
163 | "payment_type": "bank_transfer",
164 | "transaction_details": {
165 | "gross_amount": 200000,
166 | "order_id": "order-id-python-"+timestamp,
167 | }
168 | })
169 |
170 | return render_template('simple_core_api_checkout_permata.html',
171 | permata_va_number = charge_api_response['permata_va_number'],
172 | gross_amount = charge_api_response['gross_amount'],
173 | order_id = charge_api_response['order_id'])
174 |
175 | #==============#
176 | # Run Flask app
177 | #==============#
178 |
179 | # Homepage of this web app
180 | @app.route('/')
181 | def index():
182 | if not SERVER_KEY or not CLIENT_KEY:
183 | # non-relevant function only used for demo/example purpose
184 | return printExampleWarningMessage()
185 |
186 | return render_template('index.html')
187 |
188 | # credit card frontend demo
189 | @app.route('/core_api_credit_card_frontend_sample')
190 | def core_api_credit_card_frontend_sample():
191 | return render_template('core_api_credit_card_frontend_sample.html',
192 | client_key = core.api_config.client_key)
193 |
194 |
195 | def printExampleWarningMessage():
196 | pathfile = os.path.abspath("web.py")
197 | message = "Please set your server key and client key from sandbox In file: " + pathfile
198 | message += " # Set Your server key"
199 | message += " # You can find it in Merchant Portal -> Settings -> Access keys"
200 | message += " SERVER_KEY = ''"
201 | message += " CLIENT_KEY = ''"
202 | return message
203 |
204 | if __name__ == '__main__':
205 | app.run(debug=True,port=5000,host='0.0.0.0')
206 |
--------------------------------------------------------------------------------
/examples/flask_app/static/checkout.css:
--------------------------------------------------------------------------------
1 | /* CSS credits to `usf` at https://codepen.io/usf/pen/GgbEa */
2 | * {
3 | -moz-transition: all 0.3s;
4 | -o-transition: all 0.3s;
5 | -webkit-transition: all 0.3s;
6 | transition: all 0.3s;
7 | -moz-box-sizing: border-box;
8 | -webkit-box-sizing: border-box;
9 | box-sizing: border-box;
10 | }
11 |
12 | body {
13 | background: #ecece6;
14 | }
15 |
16 | @-webkit-keyframes $animation_name {
17 | from {
18 | -moz-transform: rotate(45deg) translate(-80px, 40px);
19 | -ms-transform: rotate(45deg) translate(-80px, 40px);
20 | -webkit-transform: rotate(45deg) translate(-80px, 40px);
21 | transform: rotate(45deg) translate(-80px, 40px);
22 | }
23 | to {
24 | -moz-transform: rotate(0deg) translate(0px, 0);
25 | -ms-transform: rotate(0deg) translate(0px, 0);
26 | -webkit-transform: rotate(0deg) translate(0px, 0);
27 | transform: rotate(0deg) translate(0px, 0);
28 | }
29 | }
30 | @-moz-keyframes $animation_name {
31 | from {
32 | -moz-transform: rotate(45deg) translate(-80px, 40px);
33 | -ms-transform: rotate(45deg) translate(-80px, 40px);
34 | -webkit-transform: rotate(45deg) translate(-80px, 40px);
35 | transform: rotate(45deg) translate(-80px, 40px);
36 | }
37 | to {
38 | -moz-transform: rotate(0deg) translate(0px, 0);
39 | -ms-transform: rotate(0deg) translate(0px, 0);
40 | -webkit-transform: rotate(0deg) translate(0px, 0);
41 | transform: rotate(0deg) translate(0px, 0);
42 | }
43 | }
44 | @-o-keyframes $animation_name {
45 | from {
46 | -moz-transform: rotate(45deg) translate(-80px, 40px);
47 | -ms-transform: rotate(45deg) translate(-80px, 40px);
48 | -webkit-transform: rotate(45deg) translate(-80px, 40px);
49 | transform: rotate(45deg) translate(-80px, 40px);
50 | }
51 | to {
52 | -moz-transform: rotate(0deg) translate(0px, 0);
53 | -ms-transform: rotate(0deg) translate(0px, 0);
54 | -webkit-transform: rotate(0deg) translate(0px, 0);
55 | transform: rotate(0deg) translate(0px, 0);
56 | }
57 | }
58 | @keyframes $animation_name {
59 | from {
60 | -moz-transform: rotate(45deg) translate(-80px, 40px);
61 | -ms-transform: rotate(45deg) translate(-80px, 40px);
62 | -webkit-transform: rotate(45deg) translate(-80px, 40px);
63 | transform: rotate(45deg) translate(-80px, 40px);
64 | }
65 | to {
66 | -moz-transform: rotate(0deg) translate(0px, 0);
67 | -ms-transform: rotate(0deg) translate(0px, 0);
68 | -webkit-transform: rotate(0deg) translate(0px, 0);
69 | transform: rotate(0deg) translate(0px, 0);
70 | }
71 | }
72 | /* button style */
73 | .cart {
74 | width: 40px;
75 | height: 40px;
76 | padding: 0;
77 | margin: 0;
78 | margin-top: 100px;
79 | position: absolute;
80 | left: 50%;
81 | margin-left: -20px;
82 | -moz-border-radius: 9999em;
83 | -webkit-border-radius: 9999em;
84 | border-radius: 9999em;
85 | border: none;
86 | background: #e54040;
87 | cursor: pointer;
88 | }
89 | .cart:hover {
90 | -moz-box-shadow: inset 0 0 7px 0 rgba(0, 0, 0, 0.5);
91 | -webkit-box-shadow: inset 0 0 7px 0 rgba(0, 0, 0, 0.5);
92 | box-shadow: inset 0 0 7px 0 rgba(0, 0, 0, 0.5);
93 | }
94 | .cart/*:hover*/ .popup {
95 | visibility: visible;
96 | opacity: 1;
97 | pointer-events: auto;
98 | -webkit-animation-duration: 200ms;
99 | -webkit-animation-name: show-popup;
100 | -webkit-animation-direction: normal;
101 | -webkit-animation-timing-function: cubic-bezier(1, 0.18, 1, 0.93);
102 | -moz-animation-duration: 200ms;
103 | -moz-animation-name: show-popup;
104 | -moz-animation-direction: normal;
105 | -moz-animation-timing-function: cubic-bezier(1, 0.18, 1, 0.93);
106 | -o-animation-duration: 200ms;
107 | -o-animation-name: show-popup;
108 | -o-animation-direction: normal;
109 | -o-animation-timing-function: cubic-bezier(1, 0.18, 1, 0.93);
110 | animation-duration: 200ms;
111 | animation-name: show-popup;
112 | animation-direction: normal;
113 | animation-timing-function: cubic-bezier(1, 0.18, 1, 0.93);
114 | }
115 |
116 | /* popup window style */
117 | .popup {
118 | visibility: hidden;
119 | opacity: 0;
120 | pointer-events: none;
121 | position: absolute;
122 | top: 100%;
123 | width: 250px;
124 | margin-left: -105px;
125 | margin-top: 20px;
126 | background: #ffffff;
127 | border: 1px solid #cbcbcb;
128 | -moz-border-radius: 5px;
129 | -webkit-border-radius: 5px;
130 | border-radius: 5px;
131 | -moz-box-shadow: 0 0 3px 0 rgba(0, 0, 0, 0.3);
132 | -webkit-box-shadow: 0 0 3px 0 rgba(0, 0, 0, 0.3);
133 | box-shadow: 0 0 3px 0 rgba(0, 0, 0, 0.3);
134 | }
135 | .popup:after {
136 | position: absolute;
137 | content: ' ';
138 | top: -30px;
139 | height: 30px;
140 | width: 100%;
141 | }
142 | .popup:before {
143 | position: absolute;
144 | content: ' ';
145 | left: 117px;
146 | top: -9px;
147 | width: 16px;
148 | height: 16px;
149 | border-top: 1px solid #cbcbcb;
150 | border-right: 1px solid #cbcbcb;
151 | background: #ffffff;
152 | -moz-box-shadow: 1px -1px 1px 0 rgba(0, 0, 0, 0.2);
153 | -webkit-box-shadow: 1px -1px 1px 0 rgba(0, 0, 0, 0.2);
154 | box-shadow: 1px -1px 1px 0 rgba(0, 0, 0, 0.2);
155 | -moz-transform: rotate(-45deg);
156 | -ms-transform: rotate(-45deg);
157 | -webkit-transform: rotate(-45deg);
158 | transform: rotate(-45deg);
159 | }
160 |
161 | /* data rows */
162 | .row {
163 | padding: 15px 20px;
164 | overflow: hidden;
165 | }
166 | .row.header {
167 | background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgeDE9IjAuNSIgeTE9IjAuMCIgeDI9IjAuNSIgeTI9IjEuMCI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2ZmZmZmZiIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iI2U3ZTdlNyIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA==');
168 | background-size: 100%;
169 | background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #ffffff), color-stop(100%, #e7e7e7));
170 | background-image: -moz-linear-gradient(top, #ffffff 0%, #e7e7e7 100%);
171 | background-image: -webkit-linear-gradient(top, #ffffff 0%, #e7e7e7 100%);
172 | background-image: linear-gradient(to bottom, #ffffff 0%, #e7e7e7 100%);
173 | background-image: -ms-linear-gradient(top, #ffffff 0%, #e7e7e7 100%);
174 | -moz-box-shadow: 0 1px 0 0 rgba(203, 203, 203, 0.75);
175 | -webkit-box-shadow: 0 1px 0 0 rgba(203, 203, 203, 0.75);
176 | box-shadow: 0 1px 0 0 rgba(203, 203, 203, 0.75);
177 | -moz-border-radius: 5px 5px 0 0;
178 | -webkit-border-radius: 5px;
179 | border-radius: 5px 5px 0 0;
180 | color: #747474;
181 | text-shadow: 1px 1px 1px rgba(255, 255, 255, 0.75);
182 | font: bold 11px Arial;
183 | }
184 | .row.items {
185 | color: #e54040;
186 | font: bold 18px Arial;
187 | position: relative;
188 | }
189 | .row.items span:first-child {
190 | color: #000000;
191 | }
192 | .row.items:after {
193 | content: '';
194 | position: absolute;
195 | height: 1px;
196 | width: 100%;
197 | background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgeDE9IjAuMCIgeTE9IjAuNSIgeDI9IjEuMCIgeTI9IjAuNSI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2ZmZmZmZiIvPjxzdG9wIG9mZnNldD0iNTAlIiBzdG9wLWNvbG9yPSIjZGRkZGRkIi8+PHN0b3Agb2Zmc2V0PSIxMDAlIiBzdG9wLWNvbG9yPSIjZmZmZmZmIi8+PC9saW5lYXJHcmFkaWVudD48L2RlZnM+PHJlY3QgeD0iMCIgeT0iMCIgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgZmlsbD0idXJsKCNncmFkKSIgLz48L3N2Zz4g');
198 | background-size: 100%;
199 | background-image: -webkit-gradient(linear, 0% 50%, 100% 50%, color-stop(0%, #ffffff), color-stop(50%, #dddddd), color-stop(100%, #ffffff));
200 | background-image: -moz-linear-gradient(left, #ffffff 0%, #dddddd 50%, #ffffff 100%);
201 | background-image: -webkit-linear-gradient(left, #ffffff 0%, #dddddd 50%, #ffffff 100%);
202 | background-image: linear-gradient(to right, #ffffff 0%, #dddddd 50%, #ffffff 100%);
203 | left: 0;
204 | top: 97%;
205 | }
206 | .row.checkout {
207 | font: normal 12px Arial;
208 | }
209 | .row.checkout span:first-child {
210 | padding: 3px 0;
211 | }
212 | .row.checkout a {
213 | color: #e54040;
214 | text-decoration: none;
215 | }
216 | .row.checkout a:hover {
217 | text-decoration: underline;
218 | }
219 | .row span:first-child {
220 | float: left;
221 | }
222 | .row span:last-child {
223 | float: right;
224 | }
225 |
226 | .checkout-button {
227 | float: right;
228 | padding: 3px 5px;
229 | background: #e54040;
230 | -moz-box-shadow: inset 0 2px 7px 0 rgba(255, 255, 255, 0.3);
231 | -webkit-box-shadow: inset 0 2px 7px 0 rgba(255, 255, 255, 0.3);
232 | box-shadow: inset 0 2px 7px 0 rgba(255, 255, 255, 0.3);
233 | border: 1px solid #e06b6b;
234 | -moz-border-radius: 3px;
235 | -webkit-border-radius: 3px;
236 | border-radius: 3px;
237 | color: #ffffff;
238 | }
239 | .checkout-button:hover {
240 | background: #e54040;
241 | -moz-box-shadow: none;
242 | -webkit-box-shadow: none;
243 | box-shadow: none;
244 | }
245 | .checkout-button:active {
246 | background: #e54040;
247 | -moz-box-shadow: inset 0 1px 2px 0 rgba(0, 0, 0, 0.2);
248 | -webkit-box-shadow: inset 0 1px 2px 0 rgba(0, 0, 0, 0.2);
249 | box-shadow: inset 0 1px 2px 0 rgba(0, 0, 0, 0.2);
250 | }
251 |
252 | .cart {
253 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAIGNIUk0AAHolAACAgwAA+f8AAIDoAABSCAABFVgAADqXAAAXb9daH5AAAAGPSURBVHjajNNNi85RGAbw3zwzI/XMmEHeFYmSvJRSYsOWfAHZ+wAWakp8AAs+A5ImKzujUfKapQnZzIaVxzCYGcMzXRbuydO/Wcxdd+ecq3Ouc67rvk9fkqcY8T8mcA3frSIGcLKBfcMaq4wWpjCNjwi2o71aggGcQT92YBybcLTIZ7ABX7G+xlF8qvWgJMs5kuRFksUkDypPJLmd5FSSW0lOJ7lT+GSSt70E7SQ38i+eJDmSZE+SA0n21bi35heTdJMstnrkzOMlfmELhnEVQxjDOlzBDxwv2c/7kvR6sh+Py8QLeFde9GMJ3arQw/JqrNUwtVMVaWMbFnC51pfwGQfrhR1MNgnm8LpuPVsNdr0236xKncdgyZ0eaBD8xhss4hyOlea1hQ1ha0mZwM8mwRKe4VVJ6K9Daj6PD9V0j7DUNBH6sLNcn8efOtytp7cxW82U3j5Yzs1J7ieZSnI3yXDho0nGC7+XZGMSKxEcSjJbDdVJsrvwww18VxKtFf7HTH2wObwvGfClgS/A3wEAjJAlBBKFdqAAAAAASUVORK5CYII=);
254 | background-repeat: no-repeat;
255 | background-position: center;
256 | }
257 |
--------------------------------------------------------------------------------
/examples/flask_app/templates/core_api_credit_card_frontend_sample.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Core API Credit Card Frontend Sample
5 |
6 |
7 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | This page is to further demonstrate how to implement frontend processes for Credit Card transactions
27 |
28 |
59 |
60 |
61 |
100 |
101 |
102 |
122 |
123 |
124 |
150 |
151 |
152 |
223 |
224 |
225 |
274 |
275 |
276 |
--------------------------------------------------------------------------------
/midtransclient/core_api.py:
--------------------------------------------------------------------------------
1 | from .config import ApiConfig
2 | from .http_client import HttpClient
3 | from .transactions import Transactions
4 |
5 | class CoreApi:
6 | """
7 | CoreApi object used to do request to Midtrans Core API
8 | """
9 |
10 | def __init__(self,
11 | is_production=False,
12 | server_key='',
13 | client_key='',
14 | custom_headers=dict(),
15 | proxies=dict()):
16 |
17 | self.api_config = ApiConfig(is_production,server_key,client_key,custom_headers,proxies)
18 | self.http_client = HttpClient()
19 | self.transactions = Transactions(self)
20 |
21 | @property
22 | def api_config(self):
23 | return self.__api_config
24 |
25 | @api_config.setter
26 | def api_config(self, new_value):
27 | self.__api_config = new_value
28 |
29 | def charge(self,parameters=dict()):
30 | """
31 | Trigger `/charge` API call to Core API
32 | :param parameters: dictionary of Core API JSON body as parameter, will be converted to JSON
33 | (more params detail refer to: https://api-docs.midtrans.com)
34 |
35 | :return: Dictionary from JSON decoded response
36 | """
37 | api_url = self.api_config.get_core_api_base_url()+'/v2/charge'
38 |
39 | response_dict, response_object = self.http_client.request(
40 | 'post',
41 | self.api_config.server_key,
42 | api_url,
43 | parameters,
44 | self.api_config.custom_headers,
45 | self.api_config.proxies)
46 |
47 | return response_dict
48 |
49 | def capture(self,parameters=dict()):
50 | """
51 | Trigger `/capture` API call to Core API
52 | Capture is only used for pre-authorize transaction only
53 | :param parameters: dictionary of Core API JSON body as parameter, will be converted to JSON
54 | (more params detail refer to: https://api-docs.midtrans.com)
55 |
56 | :return: Dictionary from JSON decoded response
57 | """
58 | api_url = self.api_config.get_core_api_base_url()+'/v2/capture'
59 |
60 | response_dict, response_object = self.http_client.request(
61 | 'post',
62 | self.api_config.server_key,
63 | api_url,
64 | parameters,
65 | self.api_config.custom_headers,
66 | self.api_config.proxies)
67 |
68 | return response_dict
69 |
70 | def card_register(self,parameters=dict()):
71 | """
72 | Trigger `/card/register` API call to Core API
73 | :param parameters: dictionary of Core API JSON body as parameter, will be converted to JSON
74 | (more params detail refer to: https://api-docs.midtrans.com)
75 |
76 | :return: Dictionary from JSON decoded response
77 | """
78 | api_url = self.api_config.get_core_api_base_url()+'/v2/card/register'
79 |
80 | response_dict, response_object = self.http_client.request(
81 | 'get',
82 | self.api_config.server_key,
83 | api_url,
84 | parameters,
85 | self.api_config.custom_headers,
86 | self.api_config.proxies)
87 |
88 | return response_dict
89 |
90 | def card_token(self,parameters=dict()):
91 | """
92 | Trigger `/token` API call to Core API
93 | :param parameters: dictionary of Core API JSON body as parameter, will be converted to JSON
94 | (more params detail refer to: https://api-docs.midtrans.com)
95 |
96 | :return: Dictionary from JSON decoded response
97 | """
98 | api_url = self.api_config.get_core_api_base_url()+'/v2/token'
99 |
100 | response_dict, response_object = self.http_client.request(
101 | 'get',
102 | self.api_config.server_key,
103 | api_url,
104 | parameters,
105 | self.api_config.custom_headers,
106 | self.api_config.proxies)
107 |
108 | return response_dict
109 |
110 | def card_point_inquiry(self,token_id):
111 | """
112 | Trigger `/point_inquiry/` API call to Core API
113 | :param token_id: token id of credit card
114 | (more params detail refer to: https://api-docs.midtrans.com)
115 |
116 | :return: Dictionary from JSON decoded response
117 | """
118 | api_url = self.api_config.get_core_api_base_url()+'/v2/point_inquiry/'+token_id
119 |
120 | response_dict, response_object = self.http_client.request(
121 | 'get',
122 | self.api_config.server_key,
123 | api_url,
124 | dict(),
125 | self.api_config.custom_headers,
126 | self.api_config.proxies)
127 |
128 | return response_dict
129 |
130 | def create_subscription(self,parameters=dict()):
131 | """
132 | Trigger `/v1/subscriptions` API call to Core API
133 | Create a subscription transaction by sending all the details required to create a transaction
134 | :param parameters: dictionary of Core API JSON body as parameter, will be converted to JSON
135 | (more params detail refer to: https://api-docs.midtrans.com/#create-subscription)
136 |
137 | :return: Dictionary from JSON decoded response
138 | """
139 | api_url = self.api_config.get_core_api_base_url()+'/v1/subscriptions'
140 |
141 | response_dict, response_object = self.http_client.request(
142 | 'post',
143 | self.api_config.server_key,
144 | api_url,
145 | parameters,
146 | self.api_config.custom_headers,
147 | self.api_config.proxies)
148 |
149 | return response_dict
150 |
151 | def get_subscription(self,subscription_id):
152 | """
153 | Trigger `/v1/subscriptions/` API call to Core API
154 | Retrieve the subscription details of a customer using the subscription_id
155 | (more params detail refer to: https://api-docs.midtrans.com/#get-subscription)
156 |
157 | :return: Dictionary from JSON decoded response
158 | """
159 | api_url = self.api_config.get_core_api_base_url()+'/v1/subscriptions/'+subscription_id
160 |
161 | response_dict, response_object = self.http_client.request(
162 | 'get',
163 | self.api_config.server_key,
164 | api_url,
165 | dict(),
166 | self.api_config.custom_headers,
167 | self.api_config.proxies)
168 |
169 | return response_dict
170 |
171 | def disable_subscription(self,subscription_id):
172 | """
173 | Trigger `/v1/subscriptions//disable` API call to Core API
174 | Disable the customer's subscription. The customer will not be charged in the future for this subscription
175 | (more params detail refer to: https://api-docs.midtrans.com/#disable-subscription)
176 |
177 | :return: Dictionary from JSON decoded response
178 | """
179 | api_url = self.api_config.get_core_api_base_url()+'/v1/subscriptions/'+subscription_id+'/disable'
180 |
181 | response_dict, response_object = self.http_client.request(
182 | 'post',
183 | self.api_config.server_key,
184 | api_url,
185 | dict(),
186 | self.api_config.custom_headers,
187 | self.api_config.proxies)
188 |
189 | return response_dict
190 |
191 | def enable_subscription(self,subscription_id):
192 | """
193 | Trigger `/v1/subscriptions//enable` API call to Core API
194 | Enable the customer's subscription
195 | (more params detail refer to: https://api-docs.midtrans.com/#enable-subscription)
196 |
197 | :return: Dictionary from JSON decoded response
198 | """
199 | api_url = self.api_config.get_core_api_base_url()+'/v1/subscriptions/'+subscription_id+'/enable'
200 |
201 | response_dict, response_object = self.http_client.request(
202 | 'post',
203 | self.api_config.server_key,
204 | api_url,
205 | dict(),
206 | self.api_config.custom_headers,
207 | self.api_config.proxies)
208 |
209 | return response_dict
210 |
211 | def update_subscription(self,subscription_id,parameters=dict()):
212 | """
213 | Trigger `/v1/subscriptions/` API call to Core API
214 | Update existing subscription details
215 | (more params detail refer to: https://api-docs.midtrans.com/#update-subscription)
216 |
217 | :return: Dictionary from JSON decoded response
218 | """
219 | api_url = self.api_config.get_core_api_base_url()+'/v1/subscriptions/'+subscription_id
220 |
221 | response_dict, response_object = self.http_client.request(
222 | 'patch',
223 | self.api_config.server_key,
224 | api_url,
225 | parameters,
226 | self.api_config.custom_headers,
227 | self.api_config.proxies)
228 |
229 | return response_dict
230 |
231 | def link_payment_account(self,parameters=dict()):
232 | """
233 | Trigger `/v2/pay/account` API call to Core API
234 | Link the customer account to be used for specific payment channels.
235 | :param parameters: dictionary of Core API JSON body as parameter, will be converted to JSON
236 | (more params detail refer to: https://api-docs.midtrans.com/#create-pay-account)
237 |
238 | :return: Dictionary from JSON decoded response
239 | """
240 | api_url = self.api_config.get_core_api_base_url()+'/v2/pay/account'
241 |
242 | response_dict, response_object = self.http_client.request(
243 | 'post',
244 | self.api_config.server_key,
245 | api_url,
246 | parameters,
247 | self.api_config.custom_headers,
248 | self.api_config.proxies)
249 |
250 | return response_dict
251 |
252 | def get_payment_account(self,account_id):
253 | """
254 | Trigger `/v2/pay/account/` API call to Core API
255 | Retrieve the payment account details of a customer using the account_id
256 | (more params detail refer to: https://api-docs.midtrans.com/#get-pay-account)
257 |
258 | :return: Dictionary from JSON decoded response
259 | """
260 | api_url = self.api_config.get_core_api_base_url()+'/v2/pay/account/'+account_id
261 |
262 | response_dict, response_object = self.http_client.request(
263 | 'get',
264 | self.api_config.server_key,
265 | api_url,
266 | dict(),
267 | self.api_config.custom_headers,
268 | self.api_config.proxies)
269 |
270 | return response_dict
271 |
272 | def unlink_payment_account(self,account_id):
273 | """
274 | Trigger `/v2/pay/account//unbind` API call to Core API
275 | To remove the linked customer account
276 | (more params detail refer to: https://api-docs.midtrans.com/#unbind-pay-account)
277 |
278 | :return: Dictionary from JSON decoded response
279 | """
280 | api_url = self.api_config.get_core_api_base_url()+'/v2/pay/account/'+account_id+'/unbind'
281 |
282 | response_dict, response_object = self.http_client.request(
283 | 'post',
284 | self.api_config.server_key,
285 | api_url,
286 | dict(),
287 | self.api_config.custom_headers,
288 | self.api_config.proxies)
289 |
290 | return response_dict
291 |
--------------------------------------------------------------------------------
/tests/test_core_api.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from .config import USED_SERVER_KEY, USED_CLIENT_KEY
3 | from .helpers import is_str
4 | from .context import midtransclient
5 | import datetime
6 | import json
7 | from pprint import pprint
8 |
9 | REUSED_ORDER_ID = [
10 | "py-midtransclient-test1-"+str(datetime.datetime.now()).replace(" ", "").replace(":", ""),
11 | "py-midtransclient-test2-"+str(datetime.datetime.now()).replace(" ", "").replace(":", ""),
12 | "py-midtransclient-test3-"+str(datetime.datetime.now()).replace(" ", "").replace(":", ""),
13 | ]
14 | CC_TOKEN = ''
15 | SAVED_CC_TOKEN = ''
16 | API_RESPONSE = ''
17 |
18 | def test_core_api_class():
19 | core = generate_core_api_instance()
20 | methods = dir(core)
21 | assert "charge" in methods
22 | assert is_str(core.api_config.server_key)
23 | assert is_str(core.api_config.client_key)
24 |
25 | def test_core_api_card_token():
26 | core = generate_core_api_instance()
27 | params = {
28 | 'card_number': '5264 2210 3887 4659',
29 | 'card_exp_month': '12',
30 | 'card_exp_year': '2030',
31 | 'card_cvv': '123',
32 | 'client_key': core.api_config.client_key,
33 | }
34 | response = core.card_token(params)
35 | assert isinstance(response, dict)
36 | assert int(response['status_code']) == 200
37 | global CC_TOKEN
38 | CC_TOKEN = response['token_id']
39 | assert is_str(response['token_id'])
40 |
41 | def test_core_api_card_register():
42 | core = generate_core_api_instance()
43 | params = {
44 | 'card_number': '4811 1111 1111 1114',
45 | 'card_exp_month': '12',
46 | 'card_exp_year': '2030',
47 | 'card_cvv': '123',
48 | 'client_key': core.api_config.client_key,
49 | }
50 | response = core.card_register(params)
51 | assert isinstance(response, dict)
52 | assert int(response['status_code']) == 200
53 | global SAVED_CC_TOKEN
54 | SAVED_CC_TOKEN = response['saved_token_id']
55 | assert is_str(response['saved_token_id'])
56 |
57 | def test_core_api_card_point_inquiry_valid_bni_card():
58 | core = generate_core_api_instance()
59 | try:
60 | response = core.card_point_inquiry(CC_TOKEN)
61 | except Exception as e:
62 | err = e
63 | assert is_str(response['status_message'])
64 | assert 'Success' in response['status_message']
65 | assert is_str(response['point_balance_amount'])
66 |
67 |
68 | def test_core_api_charge_cc_simple():
69 | core = generate_core_api_instance()
70 | parameters = generate_param_cc_min(order_id=REUSED_ORDER_ID[1],cc_token=CC_TOKEN)
71 | response = core.charge(parameters)
72 | assert isinstance(response, dict)
73 | assert int(response['status_code']) == 200
74 | assert response['transaction_status'] == 'capture'
75 | assert response['fraud_status'] == 'accept'
76 |
77 | def test_core_api_charge_cc_one_click():
78 | core = generate_core_api_instance()
79 | parameters = generate_param_cc_min(order_id=REUSED_ORDER_ID[2],cc_token=SAVED_CC_TOKEN)
80 | response = core.charge(parameters)
81 | assert isinstance(response, dict)
82 | assert int(response['status_code']) == 200
83 | assert response['transaction_status'] == 'capture'
84 | assert response['fraud_status'] == 'accept'
85 |
86 | def test_core_api_charge_bank_transfer_bca_simple():
87 | core = generate_core_api_instance()
88 | parameters = generate_param_min(REUSED_ORDER_ID[0])
89 | response = core.charge(parameters)
90 | assert isinstance(response, dict)
91 | assert int(response['status_code']) == 201
92 | assert response['transaction_status'] == 'pending'
93 |
94 | def test_core_api_status():
95 | core = generate_core_api_instance()
96 | response = core.transactions.status(REUSED_ORDER_ID[0])
97 | global API_RESPONSE
98 | API_RESPONSE = response
99 | assert isinstance(response, dict)
100 | assert int(response['status_code']) == 201
101 | assert response['transaction_status'] == 'pending'
102 |
103 | # TODO test statusb2b
104 |
105 | def test_core_api_notification_from_dict():
106 | core = generate_core_api_instance()
107 | response = core.transactions.notification(API_RESPONSE)
108 | assert isinstance(response, dict)
109 | assert int(response['status_code']) == 201
110 | assert response['transaction_status'] == 'pending'
111 |
112 | def test_core_api_notification_from_json():
113 | core = generate_core_api_instance()
114 | response = core.transactions.notification(json.dumps(API_RESPONSE))
115 | assert isinstance(response, dict)
116 | assert int(response['status_code']) == 201
117 | assert response['transaction_status'] == 'pending'
118 |
119 | def test_core_api_notification_from_json_fail():
120 | core = generate_core_api_instance()
121 | err = ''
122 | try:
123 | response = core.transactions.notification('')
124 | except Exception as e:
125 | err = e
126 | assert 'JSONDecodeError' in repr(err)
127 |
128 | def test_core_api_expire():
129 | core = generate_core_api_instance()
130 | response = core.transactions.expire(REUSED_ORDER_ID[0])
131 | assert isinstance(response, dict)
132 | assert int(response['status_code']) == 407
133 | assert response['transaction_status'] == 'expire'
134 |
135 | def test_core_api_approve_fail_cannot_be_updated():
136 | core = generate_core_api_instance()
137 | err = ''
138 | try:
139 | response = core.transactions.approve(REUSED_ORDER_ID[1])
140 | except Exception as e:
141 | err = e
142 | assert 'MidtransAPIError' in err.__class__.__name__
143 | assert '412' in err.message
144 |
145 | def test_core_api_deny_cannot_be_updated():
146 | core = generate_core_api_instance()
147 | err = ''
148 | try:
149 | response = core.transactions.deny(REUSED_ORDER_ID[1])
150 | except Exception as e:
151 | err = e
152 | assert 'MidtransAPIError' in err.__class__.__name__
153 | assert '412' in err.message
154 |
155 | def test_core_api_cancel():
156 | core = generate_core_api_instance()
157 | response = core.transactions.cancel(REUSED_ORDER_ID[1])
158 | assert isinstance(response, dict)
159 | assert int(response['status_code']) == 200
160 | assert response['transaction_status'] == 'cancel'
161 |
162 | def test_core_api_refund_fail_not_yet_settlement():
163 | core = generate_core_api_instance()
164 | params = {
165 | "refund_key": "order1-ref1",
166 | "amount": 5000,
167 | "reason": "for some reason"
168 | }
169 | err = ''
170 | try:
171 | response = core.transactions.refund(REUSED_ORDER_ID[2],params)
172 | except Exception as e:
173 | err = e
174 | assert 'MidtransAPIError' in err.__class__.__name__
175 | assert '412' in err.message
176 |
177 | def test_core_api_direct_refund_fail_not_yet_settlement():
178 | core = generate_core_api_instance()
179 | params = {
180 | "refund_key": "order1-ref1",
181 | "amount": 5000,
182 | "reason": "for some reason"
183 | }
184 | err = ''
185 | try:
186 | response = core.transactions.refundDirect(REUSED_ORDER_ID[2],params)
187 | except Exception as e:
188 | err = e
189 | assert 'MidtransAPIError' in err.__class__.__name__
190 | assert '412' in err.message
191 |
192 | def test_core_api_status_fail_404():
193 | core = generate_core_api_instance()
194 | err = ''
195 | try:
196 | response = core.transactions.status('non-exist-order-id')
197 | except Exception as e:
198 | err = e
199 | assert 'MidtransAPIError' in err.__class__.__name__
200 | assert '404' in err.message
201 | assert 'exist' in err.message
202 |
203 | def test_core_api_status_server_key_change_via_property():
204 | core = midtransclient.CoreApi(is_production=False,server_key='',client_key='')
205 | core.api_config.server_key = USED_SERVER_KEY
206 | response = core.transactions.status(REUSED_ORDER_ID[1])
207 | assert isinstance(response, dict)
208 | assert int(response['status_code']) == 200
209 | assert response['transaction_status'] == 'cancel'
210 |
211 | def test_core_api_status_server_key_change_via_setter():
212 | core = midtransclient.CoreApi(is_production=False,
213 | server_key=USED_SERVER_KEY,
214 | client_key='')
215 | assert core.api_config.is_production == False
216 | assert core.api_config.server_key == USED_SERVER_KEY
217 | try:
218 | response = core.transactions.status('non-exist-order-id')
219 | except Exception as e:
220 | assert '404' in e.message
221 |
222 | core.api_config.set(is_production=True,
223 | server_key='abc')
224 | assert core.api_config.is_production == True
225 | assert core.api_config.server_key == 'abc'
226 | try:
227 | response = core.transactions.status(REUSED_ORDER_ID[0])
228 | except Exception as e:
229 | assert '401' in e.message
230 |
231 | core.api_config.set(is_production=False,
232 | server_key=USED_SERVER_KEY,
233 | client_key=USED_CLIENT_KEY)
234 | assert core.api_config.is_production == False
235 | assert core.api_config.server_key == USED_SERVER_KEY
236 | assert core.api_config.client_key == USED_CLIENT_KEY
237 | response = core.transactions.status(REUSED_ORDER_ID[1])
238 | assert isinstance(response, dict)
239 | assert int(response['status_code']) == 200
240 | assert response['transaction_status'] == 'cancel'
241 |
242 | def test_core_api_charge_fail_401():
243 | core = generate_core_api_instance()
244 | core.api_config.server_key='invalidkey'
245 | parameters = generate_param_min()
246 | err = ''
247 | try:
248 | response = core.charge(parameters)
249 | except Exception as e:
250 | err = e
251 | assert 'MidtransAPIError' in err.__class__.__name__
252 | assert '401' in err.message
253 | # assert 'authorized' in err.message # disabled due to 1OMS changed the err.message to no longer contains this keyword
254 |
255 | def test_core_api_charge_fail_empty_param():
256 | core = generate_core_api_instance()
257 | parameters = None
258 | err = ''
259 | try:
260 | response = core.charge(parameters)
261 | except Exception as e:
262 | err = e
263 | assert 'MidtransAPIError' in err.__class__.__name__
264 | assert '500' in err.message
265 | assert 'unexpected' in err.message
266 |
267 | def test_core_api_charge_fail_zero_gross_amount():
268 | core = generate_core_api_instance()
269 | parameters = generate_param_min()
270 | parameters['transaction_details']['gross_amount'] = 0
271 | err = ''
272 | try:
273 | response = core.charge(parameters)
274 | except Exception as e:
275 | err = e
276 | assert 'MidtransAPIError' in err.__class__.__name__
277 | assert '400' in err.message
278 |
279 | def test_core_api_exception_MidtransAPIError():
280 | core = generate_core_api_instance()
281 | err = ''
282 | try:
283 | response = core.transactions.status('non-exist-order-id')
284 | except Exception as e:
285 | err = e
286 | assert 'MidtransAPIError' in err.__class__.__name__
287 | assert is_str(err.message)
288 | assert isinstance(err.api_response_dict, dict)
289 | assert isinstance(err.http_status_code,int)
290 |
291 | # ======== HELPER FUNCTIONS BELOW ======== #
292 | def generate_core_api_instance():
293 | core_api = midtransclient.CoreApi(is_production=False,
294 | server_key=USED_SERVER_KEY,
295 | client_key=USED_CLIENT_KEY)
296 | return core_api
297 |
298 | def generate_param_min(order_id=None):
299 | return {
300 | "payment_type": "bank_transfer",
301 | "transaction_details": {
302 | "gross_amount": 44145,
303 | "order_id": "py-midtransclient-test-"+datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") if order_id == None else order_id,
304 | },
305 | "bank_transfer":{
306 | "bank": "bca"
307 | }
308 | }
309 |
310 | def generate_param_cc_min(order_id=None,cc_token=None):
311 | return {
312 | "payment_type": "credit_card",
313 | "transaction_details": {
314 | "gross_amount": 12145,
315 | "order_id": "py-midtransclient-test-"+datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") if order_id == None else order_id,
316 | },
317 | "credit_card":{
318 | "token_id": cc_token
319 | }
320 | }
321 |
322 | def generate_param_max():
323 | return {}
--------------------------------------------------------------------------------
/examples/flask_app/Pipfile.lock:
--------------------------------------------------------------------------------
1 | {
2 | "_meta": {
3 | "hash": {
4 | "sha256": "4ea80d9da2829797976093ea53f673bce7374a60a8bd2777f2f7cfbdccdebd56"
5 | },
6 | "pipfile-spec": 6,
7 | "requires": {
8 | "python_version": "3.7"
9 | },
10 | "sources": [
11 | {
12 | "name": "pypi",
13 | "url": "https://pypi.org/simple",
14 | "verify_ssl": true
15 | }
16 | ]
17 | },
18 | "default": {
19 | "certifi": {
20 | "hashes": [
21 | "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3",
22 | "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"
23 | ],
24 | "markers": "python_version >= '3.6'",
25 | "version": "==2022.12.7"
26 | },
27 | "charset-normalizer": {
28 | "hashes": [
29 | "sha256:00d3ffdaafe92a5dc603cb9bd5111aaa36dfa187c8285c543be562e61b755f6b",
30 | "sha256:024e606be3ed92216e2b6952ed859d86b4cfa52cd5bc5f050e7dc28f9b43ec42",
31 | "sha256:0298eafff88c99982a4cf66ba2efa1128e4ddaca0b05eec4c456bbc7db691d8d",
32 | "sha256:02a51034802cbf38db3f89c66fb5d2ec57e6fe7ef2f4a44d070a593c3688667b",
33 | "sha256:083c8d17153ecb403e5e1eb76a7ef4babfc2c48d58899c98fcaa04833e7a2f9a",
34 | "sha256:0a11e971ed097d24c534c037d298ad32c6ce81a45736d31e0ff0ad37ab437d59",
35 | "sha256:0bf2dae5291758b6f84cf923bfaa285632816007db0330002fa1de38bfcb7154",
36 | "sha256:0c0a590235ccd933d9892c627dec5bc7511ce6ad6c1011fdf5b11363022746c1",
37 | "sha256:0f438ae3532723fb6ead77e7c604be7c8374094ef4ee2c5e03a3a17f1fca256c",
38 | "sha256:109487860ef6a328f3eec66f2bf78b0b72400280d8f8ea05f69c51644ba6521a",
39 | "sha256:11b53acf2411c3b09e6af37e4b9005cba376c872503c8f28218c7243582df45d",
40 | "sha256:12db3b2c533c23ab812c2b25934f60383361f8a376ae272665f8e48b88e8e1c6",
41 | "sha256:14e76c0f23218b8f46c4d87018ca2e441535aed3632ca134b10239dfb6dadd6b",
42 | "sha256:16a8663d6e281208d78806dbe14ee9903715361cf81f6d4309944e4d1e59ac5b",
43 | "sha256:292d5e8ba896bbfd6334b096e34bffb56161c81408d6d036a7dfa6929cff8783",
44 | "sha256:2c03cc56021a4bd59be889c2b9257dae13bf55041a3372d3295416f86b295fb5",
45 | "sha256:2e396d70bc4ef5325b72b593a72c8979999aa52fb8bcf03f701c1b03e1166918",
46 | "sha256:2edb64ee7bf1ed524a1da60cdcd2e1f6e2b4f66ef7c077680739f1641f62f555",
47 | "sha256:31a9ddf4718d10ae04d9b18801bd776693487cbb57d74cc3458a7673f6f34639",
48 | "sha256:356541bf4381fa35856dafa6a965916e54bed415ad8a24ee6de6e37deccf2786",
49 | "sha256:358a7c4cb8ba9b46c453b1dd8d9e431452d5249072e4f56cfda3149f6ab1405e",
50 | "sha256:37f8febc8ec50c14f3ec9637505f28e58d4f66752207ea177c1d67df25da5aed",
51 | "sha256:39049da0ffb96c8cbb65cbf5c5f3ca3168990adf3551bd1dee10c48fce8ae820",
52 | "sha256:39cf9ed17fe3b1bc81f33c9ceb6ce67683ee7526e65fde1447c772afc54a1bb8",
53 | "sha256:3ae1de54a77dc0d6d5fcf623290af4266412a7c4be0b1ff7444394f03f5c54e3",
54 | "sha256:3b590df687e3c5ee0deef9fc8c547d81986d9a1b56073d82de008744452d6541",
55 | "sha256:3e45867f1f2ab0711d60c6c71746ac53537f1684baa699f4f668d4c6f6ce8e14",
56 | "sha256:3fc1c4a2ffd64890aebdb3f97e1278b0cc72579a08ca4de8cd2c04799a3a22be",
57 | "sha256:4457ea6774b5611f4bed5eaa5df55f70abde42364d498c5134b7ef4c6958e20e",
58 | "sha256:44ba614de5361b3e5278e1241fda3dc1838deed864b50a10d7ce92983797fa76",
59 | "sha256:4a8fcf28c05c1f6d7e177a9a46a1c52798bfe2ad80681d275b10dcf317deaf0b",
60 | "sha256:4b0d02d7102dd0f997580b51edc4cebcf2ab6397a7edf89f1c73b586c614272c",
61 | "sha256:502218f52498a36d6bf5ea77081844017bf7982cdbe521ad85e64cabee1b608b",
62 | "sha256:503e65837c71b875ecdd733877d852adbc465bd82c768a067badd953bf1bc5a3",
63 | "sha256:5995f0164fa7df59db4746112fec3f49c461dd6b31b841873443bdb077c13cfc",
64 | "sha256:59e5686dd847347e55dffcc191a96622f016bc0ad89105e24c14e0d6305acbc6",
65 | "sha256:601f36512f9e28f029d9481bdaf8e89e5148ac5d89cffd3b05cd533eeb423b59",
66 | "sha256:608862a7bf6957f2333fc54ab4399e405baad0163dc9f8d99cb236816db169d4",
67 | "sha256:62595ab75873d50d57323a91dd03e6966eb79c41fa834b7a1661ed043b2d404d",
68 | "sha256:70990b9c51340e4044cfc394a81f614f3f90d41397104d226f21e66de668730d",
69 | "sha256:71140351489970dfe5e60fc621ada3e0f41104a5eddaca47a7acb3c1b851d6d3",
70 | "sha256:72966d1b297c741541ca8cf1223ff262a6febe52481af742036a0b296e35fa5a",
71 | "sha256:74292fc76c905c0ef095fe11e188a32ebd03bc38f3f3e9bcb85e4e6db177b7ea",
72 | "sha256:761e8904c07ad053d285670f36dd94e1b6ab7f16ce62b9805c475b7aa1cffde6",
73 | "sha256:772b87914ff1152b92a197ef4ea40efe27a378606c39446ded52c8f80f79702e",
74 | "sha256:79909e27e8e4fcc9db4addea88aa63f6423ebb171db091fb4373e3312cb6d603",
75 | "sha256:7e189e2e1d3ed2f4aebabd2d5b0f931e883676e51c7624826e0a4e5fe8a0bf24",
76 | "sha256:7eb33a30d75562222b64f569c642ff3dc6689e09adda43a082208397f016c39a",
77 | "sha256:81d6741ab457d14fdedc215516665050f3822d3e56508921cc7239f8c8e66a58",
78 | "sha256:8499ca8f4502af841f68135133d8258f7b32a53a1d594aa98cc52013fff55678",
79 | "sha256:84c3990934bae40ea69a82034912ffe5a62c60bbf6ec5bc9691419641d7d5c9a",
80 | "sha256:87701167f2a5c930b403e9756fab1d31d4d4da52856143b609e30a1ce7160f3c",
81 | "sha256:88600c72ef7587fe1708fd242b385b6ed4b8904976d5da0893e31df8b3480cb6",
82 | "sha256:8ac7b6a045b814cf0c47f3623d21ebd88b3e8cf216a14790b455ea7ff0135d18",
83 | "sha256:8b8af03d2e37866d023ad0ddea594edefc31e827fee64f8de5611a1dbc373174",
84 | "sha256:8c7fe7afa480e3e82eed58e0ca89f751cd14d767638e2550c77a92a9e749c317",
85 | "sha256:8eade758719add78ec36dc13201483f8e9b5d940329285edcd5f70c0a9edbd7f",
86 | "sha256:911d8a40b2bef5b8bbae2e36a0b103f142ac53557ab421dc16ac4aafee6f53dc",
87 | "sha256:93ad6d87ac18e2a90b0fe89df7c65263b9a99a0eb98f0a3d2e079f12a0735837",
88 | "sha256:95dea361dd73757c6f1c0a1480ac499952c16ac83f7f5f4f84f0658a01b8ef41",
89 | "sha256:9ab77acb98eba3fd2a85cd160851816bfce6871d944d885febf012713f06659c",
90 | "sha256:9cb3032517f1627cc012dbc80a8ec976ae76d93ea2b5feaa9d2a5b8882597579",
91 | "sha256:9cf4e8ad252f7c38dd1f676b46514f92dc0ebeb0db5552f5f403509705e24753",
92 | "sha256:9d9153257a3f70d5f69edf2325357251ed20f772b12e593f3b3377b5f78e7ef8",
93 | "sha256:a152f5f33d64a6be73f1d30c9cc82dfc73cec6477ec268e7c6e4c7d23c2d2291",
94 | "sha256:a16418ecf1329f71df119e8a65f3aa68004a3f9383821edcb20f0702934d8087",
95 | "sha256:a60332922359f920193b1d4826953c507a877b523b2395ad7bc716ddd386d866",
96 | "sha256:a8d0fc946c784ff7f7c3742310cc8a57c5c6dc31631269876a88b809dbeff3d3",
97 | "sha256:ab5de034a886f616a5668aa5d098af2b5385ed70142090e2a31bcbd0af0fdb3d",
98 | "sha256:c22d3fe05ce11d3671297dc8973267daa0f938b93ec716e12e0f6dee81591dc1",
99 | "sha256:c2ac1b08635a8cd4e0cbeaf6f5e922085908d48eb05d44c5ae9eabab148512ca",
100 | "sha256:c512accbd6ff0270939b9ac214b84fb5ada5f0409c44298361b2f5e13f9aed9e",
101 | "sha256:c75ffc45f25324e68ab238cb4b5c0a38cd1c3d7f1fb1f72b5541de469e2247db",
102 | "sha256:c95a03c79bbe30eec3ec2b7f076074f4281526724c8685a42872974ef4d36b72",
103 | "sha256:cadaeaba78750d58d3cc6ac4d1fd867da6fc73c88156b7a3212a3cd4819d679d",
104 | "sha256:cd6056167405314a4dc3c173943f11249fa0f1b204f8b51ed4bde1a9cd1834dc",
105 | "sha256:db72b07027db150f468fbada4d85b3b2729a3db39178abf5c543b784c1254539",
106 | "sha256:df2c707231459e8a4028eabcd3cfc827befd635b3ef72eada84ab13b52e1574d",
107 | "sha256:e62164b50f84e20601c1ff8eb55620d2ad25fb81b59e3cd776a1902527a788af",
108 | "sha256:e696f0dd336161fca9adbb846875d40752e6eba585843c768935ba5c9960722b",
109 | "sha256:eaa379fcd227ca235d04152ca6704c7cb55564116f8bc52545ff357628e10602",
110 | "sha256:ebea339af930f8ca5d7a699b921106c6e29c617fe9606fa7baa043c1cdae326f",
111 | "sha256:f4c39b0e3eac288fedc2b43055cfc2ca7a60362d0e5e87a637beac5d801ef478",
112 | "sha256:f5057856d21e7586765171eac8b9fc3f7d44ef39425f85dbcccb13b3ebea806c",
113 | "sha256:f6f45710b4459401609ebebdbcfb34515da4fc2aa886f95107f556ac69a9147e",
114 | "sha256:f97e83fa6c25693c7a35de154681fcc257c1c41b38beb0304b9c4d2d9e164479",
115 | "sha256:f9d0c5c045a3ca9bedfc35dca8526798eb91a07aa7a2c0fee134c6c6f321cbd7",
116 | "sha256:ff6f3db31555657f3163b15a6b7c6938d08df7adbfc9dd13d9d19edad678f1e8"
117 | ],
118 | "markers": "python_version >= '3.6'",
119 | "version": "==3.0.1"
120 | },
121 | "click": {
122 | "hashes": [
123 | "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e",
124 | "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"
125 | ],
126 | "markers": "python_version >= '3.7'",
127 | "version": "==8.1.3"
128 | },
129 | "flask": {
130 | "hashes": [
131 | "sha256:13f9f196f330c7c2c5d7a5cf91af894110ca0215ac051b5844701f2bfd934d52",
132 | "sha256:45eb5a6fd193d6cf7e0cf5d8a5b31f83d5faae0293695626f539a823e93b13f6"
133 | ],
134 | "index": "pypi",
135 | "version": "==1.1.1"
136 | },
137 | "idna": {
138 | "hashes": [
139 | "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4",
140 | "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"
141 | ],
142 | "markers": "python_version >= '3.5'",
143 | "version": "==3.4"
144 | },
145 | "importlib-metadata": {
146 | "hashes": [
147 | "sha256:7efb448ec9a5e313a57655d35aa54cd3e01b7e1fbcf72dce1bf06119420f5bad",
148 | "sha256:e354bedeb60efa6affdcc8ae121b73544a7aa74156d047311948f6d711cd378d"
149 | ],
150 | "markers": "python_version < '3.8'",
151 | "version": "==6.0.0"
152 | },
153 | "itsdangerous": {
154 | "hashes": [
155 | "sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44",
156 | "sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a"
157 | ],
158 | "markers": "python_version >= '3.7'",
159 | "version": "==2.1.2"
160 | },
161 | "jinja2": {
162 | "hashes": [
163 | "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852",
164 | "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"
165 | ],
166 | "markers": "python_version >= '3.7'",
167 | "version": "==3.1.2"
168 | },
169 | "markupsafe": {
170 | "hashes": [
171 | "sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed",
172 | "sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc",
173 | "sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2",
174 | "sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460",
175 | "sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7",
176 | "sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0",
177 | "sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1",
178 | "sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa",
179 | "sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03",
180 | "sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323",
181 | "sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65",
182 | "sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013",
183 | "sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036",
184 | "sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f",
185 | "sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4",
186 | "sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419",
187 | "sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2",
188 | "sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619",
189 | "sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a",
190 | "sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a",
191 | "sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd",
192 | "sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7",
193 | "sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666",
194 | "sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65",
195 | "sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859",
196 | "sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625",
197 | "sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff",
198 | "sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156",
199 | "sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd",
200 | "sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba",
201 | "sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f",
202 | "sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1",
203 | "sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094",
204 | "sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a",
205 | "sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513",
206 | "sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed",
207 | "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d",
208 | "sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3",
209 | "sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147",
210 | "sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c",
211 | "sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603",
212 | "sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601",
213 | "sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a",
214 | "sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1",
215 | "sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d",
216 | "sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3",
217 | "sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54",
218 | "sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2",
219 | "sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6",
220 | "sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58"
221 | ],
222 | "markers": "python_version >= '3.7'",
223 | "version": "==2.1.2"
224 | },
225 | "midtransclient": {
226 | "hashes": [
227 | "sha256:02502fa63026f3294357f5234d06d56ac12bd4e793c898795d66ceefd614151a",
228 | "sha256:7107e2447633b8beb57701ccd2f4772a7a77813e6ab9a2a6d70ce0e008aca22d"
229 | ],
230 | "index": "pypi",
231 | "version": "==1.1.0"
232 | },
233 | "requests": {
234 | "hashes": [
235 | "sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa",
236 | "sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf"
237 | ],
238 | "markers": "python_version >= '3.7' and python_version < '4'",
239 | "version": "==2.28.2"
240 | },
241 | "typing-extensions": {
242 | "hashes": [
243 | "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb",
244 | "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4"
245 | ],
246 | "markers": "python_version < '3.8'",
247 | "version": "==4.5.0"
248 | },
249 | "urllib3": {
250 | "hashes": [
251 | "sha256:076907bf8fd355cde77728471316625a4d2f7e713c125f51953bb5b3eecf4f72",
252 | "sha256:75edcdc2f7d85b137124a6c3c9fc3933cdeaa12ecb9a6a959f22797a0feca7e1"
253 | ],
254 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
255 | "version": "==1.26.14"
256 | },
257 | "werkzeug": {
258 | "hashes": [
259 | "sha256:2e1ccc9417d4da358b9de6f174e3ac094391ea1d4fbef2d667865d819dfd0afe",
260 | "sha256:56433961bc1f12533306c624f3be5e744389ac61d722175d543e1751285da612"
261 | ],
262 | "index": "pypi",
263 | "version": "==2.2.3"
264 | },
265 | "zipp": {
266 | "hashes": [
267 | "sha256:23f70e964bc11a34cef175bc90ba2914e1e4545ea1e3e2f67c079671883f9cb6",
268 | "sha256:e8b2a36ea17df80ffe9e2c4fda3f693c3dad6df1697d3cd3af232db680950b0b"
269 | ],
270 | "markers": "python_version >= '3.7'",
271 | "version": "==3.13.0"
272 | }
273 | },
274 | "develop": {}
275 | }
276 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Midtrans Client - Python
2 | ===============
3 |
4 | [](https://badge.fury.io/py/midtransclient)
5 | [](https://pepy.tech/project/midtransclient)
6 | [](https://pepy.tech/project/midtransclient)
7 |
8 | Midtrans ❤️ Python! 🐍
9 |
10 | This is the Official Python API client/library for Midtrans Payment API. Visit [https://midtrans.com](https://midtrans.com). More information about the product and see documentation at [http://docs.midtrans.com](https://docs.midtrans.com) for more technical details.
11 |
12 | ## 1. Installation
13 |
14 | ### 1.a Using Pip
15 |
16 | ```
17 | pip install midtransclient
18 | ```
19 |
20 | ### 1.b Manual Installation
21 |
22 | If you are not using Pip, you can clone or [download](https://github.com/midtrans/midtrans-python-client/archive/master.zip) this repository.
23 | Then import from `midtransclient` folder.
24 |
25 | Or run Pip install from the repo folder.
26 | ```
27 | pip install .
28 | ```
29 |
30 | ## 2. Usage
31 |
32 | ### 2.1 Choose Product/Method
33 |
34 | We have [2 different products](https://docs.midtrans.com/en/welcome/index.html) of payment that you can use:
35 | - [Snap](#22A-snap) - Customizable payment popup will appear on **your web/app** (no redirection). [doc ref](https://snap-docs.midtrans.com/)
36 | - [Snap Redirect](#22B-snap-redirect) - Customer need to be redirected to payment url **hosted by midtrans**. [doc ref](https://snap-docs.midtrans.com/)
37 | - [Core API (VT-Direct)](#22C-core-api-vt-direct) - Basic backend implementation, you can customize the frontend embedded on **your web/app** as you like (no redirection). [doc ref](https://api-docs.midtrans.com/)
38 |
39 | Choose one that you think best for your unique needs.
40 |
41 | ### 2.2 Client Initialization and Configuration
42 |
43 | Get your client key and server key from [Midtrans Dashboard](https://dashboard.midtrans.com)
44 |
45 | Create API client object
46 |
47 | ```python
48 | # Create Core API instance
49 | core_api = midtransclient.CoreApi(
50 | is_production=False,
51 | server_key='YOUR_SERVER_KEY',
52 | client_key='YOUR_CLIENT_KEY'
53 | )
54 | ```
55 |
56 |
57 | ```python
58 | # Create Snap API instance
59 | snap = midtransclient.Snap(
60 | is_production=False,
61 | server_key='YOUR_SERVER_KEY',
62 | client_key='YOUR_CLIENT_KEY'
63 | )
64 | ```
65 |
66 | You can also re-set config using `Snap.api_config.set( ... )`
67 | example:
68 |
69 | ```python
70 |
71 | # initialize object, empty config
72 | snap = midtransclient.Snap()
73 |
74 | # re-set full config
75 | snap.api_config.set(
76 | is_production=False,
77 | server_key='YOUR_SERVER_KEY',
78 | client_key='YOUR_CLIENT_KEY'
79 | )
80 |
81 | # re-set server_key only
82 | snap.api_config.set(server_key='YOUR_SERVER_KEY')
83 |
84 | # re-set is_production only
85 | snap.api_config.set(is_production=True)
86 | ```
87 |
88 | You can also set config directly from attribute
89 | ```python
90 | # initialize object, empty config
91 | snap = midtransclient.Snap()
92 |
93 | # set config
94 | snap.api_config.is_production=False
95 | snap.api_config.server_key='YOUR_SERVER_KEY'
96 | snap.api_config.client='YOUR_CLIENT_KEY'
97 | ```
98 |
99 |
100 | ### 2.2.A Snap
101 | You can see Snap example [here](examples/snap).
102 |
103 | Available methods for `Snap` class
104 | ```python
105 | # return Snap API /transaction response as Dictionary
106 | def create_transactions(parameter):
107 |
108 | # return Snap API /transaction token as String
109 | def create_transactions_token(parameter):
110 |
111 | # return Snap API /transaction redirect_url as String
112 | def create_transactions_redirect_url(parameter):
113 | ```
114 | `parameter` is Dictionary or String of JSON of [SNAP Parameter](https://snap-docs.midtrans.com/#json-objects)
115 |
116 |
117 | #### Get Snap Token
118 |
119 | ```python
120 | # Create Snap API instance
121 | snap = midtransclient.Snap(
122 | is_production=False,
123 | server_key='YOUR_SERVER_KEY',
124 | client_key='YOUR_CLIENT_KEY'
125 | )
126 | # Prepare parameter
127 | param = {
128 | "transaction_details": {
129 | "order_id": "test-transaction-123",
130 | "gross_amount": 200000
131 | }, "credit_card":{
132 | "secure" : True
133 | }
134 | }
135 |
136 | transaction = snap.create_transaction(param)
137 |
138 | transaction_token = transaction['token']
139 | # alternative way to create transaction_token:
140 | # transaction_token = snap.create_transaction_token(param)
141 | ```
142 |
143 |
144 | #### Initialize Snap JS when customer click pay button
145 |
146 | Replace `PUT_TRANSACTION_TOKEN_HERE` with `transaction_token` acquired above
147 | ```html
148 |
149 |
150 | Pay!
151 | JSON result will appear here after payment:
152 |
153 |
154 |
155 |
174 |
175 |
176 | ```
177 |
178 | #### Implement Notification Handler
179 | [Refer to this section](#23-handle-http-notification)
180 |
181 | ### 2.2.B Snap Redirect
182 |
183 | Also available as examples [here](examples/snap).
184 |
185 | #### Get Redirection URL of a Payment Page
186 |
187 | ```python
188 | # Create Snap API instance
189 | snap = midtransclient.Snap(
190 | is_production=False,
191 | server_key='YOUR_SERVER_KEY',
192 | client_key='YOUR_CLIENT_KEY'
193 | )
194 | # Prepare parameter
195 | param = {
196 | "transaction_details": {
197 | "order_id": "test-transaction-123",
198 | "gross_amount": 200000
199 | }, "credit_card":{
200 | "secure" : True
201 | }
202 | }
203 |
204 | transaction = snap.create_transaction(param)
205 |
206 | transaction_redirect_url = transaction['redirect_url']
207 | # alternative way to create redirect_url:
208 | # transaction_redirect_url = snap.create_redirect_url(param)
209 | ```
210 | #### Implement Notification Handler
211 | [Refer to this section](#23-handle-http-notification)
212 |
213 | ### 2.2.C Core API (VT-Direct)
214 |
215 | You can see some Core API examples [here](examples/core_api).
216 |
217 | Available methods for `CoreApi` class
218 | ```python
219 | def charge(self,parameters=dict()):
220 | """
221 | Trigger `/charge` API call to Core API
222 | :param parameters: dictionary of Core API JSON body as parameter, will be converted to JSON
223 | (more params detail refer to: https://api-docs.midtrans.com)
224 |
225 | :return: Dictionary from JSON decoded response
226 | """
227 |
228 | def capture(self,parameters=dict()):
229 | """
230 | Trigger `/capture` API call to Core API
231 | Capture is only used for pre-authorize transaction only
232 | :param parameters: dictionary of Core API JSON body as parameter, will be converted to JSON
233 | (more params detail refer to: https://api-docs.midtrans.com)
234 |
235 | :return: Dictionary from JSON decoded response
236 | """
237 |
238 | def card_register(self,parameters=dict()):
239 | """
240 | Trigger `/card/register` API call to Core API
241 | :param parameters: dictionary of Core API JSON body as parameter, will be converted to JSON
242 | (more params detail refer to: https://api-docs.midtrans.com)
243 |
244 | :return: Dictionary from JSON decoded response
245 | """
246 |
247 | def card_token(self,parameters=dict()):
248 | """
249 | Trigger `/token` API call to Core API
250 | :param parameters: dictionary of Core API JSON body as parameter, will be converted to JSON
251 | (more params detail refer to: https://api-docs.midtrans.com)
252 |
253 | :return: Dictionary from JSON decoded response
254 | """
255 |
256 | def card_point_inquiry(self,token_id):
257 | """
258 | Trigger `/point_inquiry/` API call to Core API
259 | :param parameters: dictionary of Core API JSON body as parameter, will be converted to JSON
260 | (more params detail refer to: https://api-docs.midtrans.com)
261 |
262 | :return: Dictionary from JSON decoded response
263 | """
264 | ```
265 | `parameter` is Dictionary or String of JSON of [Core API Parameter](https://api-docs.midtrans.com/#json-objects)
266 |
267 | #### Credit Card Get Token
268 |
269 | Get token should be handled on Frontend please refer to [API docs](https://api-docs.midtrans.com)
270 |
271 | #### Credit Card Charge
272 |
273 | ```python
274 | # Create Core API instance
275 | core_api = midtransclient.Snap(
276 | is_production=False,
277 | server_key='YOUR_SERVER_KEY',
278 | client_key='YOUR_CLIENT_KEY'
279 | )
280 | # Prepare parameter
281 | param = {
282 | "payment_type": "credit_card",
283 | "transaction_details": {
284 | "gross_amount": 12145,
285 | "order_id": "test-transaction-54321",
286 | },
287 | "credit_card":{
288 | "token_id": 'CREDIT_CARD_TOKEN', # change with your card token
289 | "authentication": True
290 | }
291 | }
292 |
293 | # charge transaction
294 | charge_response = core_api.charge(param)
295 | print('charge_response:')
296 | print(charge_response)
297 | ```
298 |
299 | #### Credit Card 3DS Authentication
300 |
301 | The credit card charge result may contains `redirect_url` for 3DS authentication. 3DS Authentication should be handled on Frontend please refer to [API docs](https://api-docs.midtrans.com/#card-features-3d-secure)
302 |
303 | For full example on Credit Card 3DS transaction refer to:
304 | - [Flask App examples](/examples/flask_app) that implement Snap & Core Api
305 |
306 | ### 2.2.D Subscription API
307 |
308 | You can see some Subscription API examples [here](examples/subscription), [Subscription API Docs](https://api-docs.midtrans.com/#subscription-api)
309 |
310 | #### Subscription API for Credit Card
311 |
312 | To use subscription API for credit card, you should first obtain the 1-click saved token, [refer to this docs.](https://docs.midtrans.com/en/core-api/advanced-features?id=recurring-transaction-with-subscriptions-api)
313 |
314 | You will receive `saved_token_id` as part of the response when the initial card payment is accepted (will also available in the HTTP notification's JSON), [refer to this docs.](https://docs.midtrans.com/en/core-api/advanced-features?id=sample-3ds-authenticate-json-response-for-the-first-transaction)
315 |
316 | ```python
317 | # Create Subscription API instance
318 | core_api = midtransclient.CoreApi(
319 | is_production=False,
320 | server_key='YOUR_SERVER_KEY',
321 | client_key='YOUR_CLIENT_KEY'
322 | )
323 | # Prepare parameter
324 | param = {
325 | "name": "SUBSCRIPTION-STARTER-1",
326 | "amount": "100000",
327 | "currency": "IDR",
328 | "payment_type": "credit_card",
329 | "token": "436502qFfqfAQKScMtPRPdZDOaeg7199",
330 | "schedule": {
331 | "interval": 1,
332 | "interval_unit": "month",
333 | "max_interval": 3,
334 | "start_time": "2021-10-01 07:25:01 +0700"
335 | },
336 | "metadata": {
337 | "description": "Recurring payment for STARTER 1"
338 | },
339 | "customer_details": {
340 | "first_name": "John A",
341 | "last_name": "Doe A",
342 | "email": "johndoe@email.com",
343 | "phone": "+62812345678"
344 | }
345 | }
346 | create_subscription_response = core_api.create_subscription(param)
347 |
348 | subscription_id_response = create_subscription_response['id']
349 | # get subscription by subscription_id
350 | get_subscription_response = core_api.get_subscription(subscription_id_response)
351 |
352 | # disable subscription by subscription_id
353 | disable_subscription_response = core_api.disable_subscription(subscription_id_response)
354 |
355 | # enable subscription by subscription_id
356 | enable_subscription_response = core_api.enable_subscription(subscription_id_response)
357 |
358 | # update subscription by subscription_id
359 | update_param = {
360 | "name": "SUBSCRIPTION-STARTER-1-UPDATE",
361 | "amount": "100000",
362 | "currency": "IDR",
363 | "token": "436502qFfqfAQKScMtPRPdZDOaeg7199",
364 | "schedule": {
365 | "interval": 1
366 | }
367 | update_subscription_response = core_api.update_subscription(subscription_id_response, update_param)
368 | ```
369 |
370 | #### Subscription API for Gopay
371 |
372 | To use subscription API for gopay, you should first link your customer gopay account with gopay tokenization API, [refer to this section](#22e-tokenization-api)
373 |
374 | You will receive gopay payment token using `get_payment_account` API call
375 |
376 | You can see some Subscription API examples [here](examples/subscription)
377 |
378 | ### 2.2.E Tokenization API
379 | You can see some Tokenization API examples [here](examples/tokenization), [Tokenization API Docs](https://api-docs.midtrans.com/#gopay-tokenization)
380 |
381 | ```python
382 | # Create Tokenization API instance
383 | core_api = midtransclient.CoreApi(
384 | is_production=False,
385 | server_key='YOUR_SERVER_KEY',
386 | client_key='YOUR_CLIENT_KEY'
387 | )
388 | # Prepare parameter
389 | param = {
390 | "payment_type": "gopay",
391 | "gopay_partner": {
392 | "phone_number": "81234567891",
393 | "country_code": "62",
394 | "redirect_url": "https://mywebstore.com/gopay-linking-finish" #please update with your redirect URL
395 | }
396 | }
397 |
398 | # link payment account
399 | link_payment_account_response = core_api.link_payment_account(param)
400 |
401 | # get payment account
402 | get_payment_account_response = core_api.get_payment_account(active_account_id)
403 |
404 | # unlink account
405 | unlink_payment_account_response = core_api.unlink_payment_account(active_account_id)
406 | ```
407 |
408 | ### 2.3 Handle HTTP Notification
409 |
410 | > **IMPORTANT NOTE**: To update transaction status on your backend/database, **DO NOT** solely rely on frontend callbacks! For security reason to make sure the status is authentically coming from Midtrans, only update transaction status based on HTTP Notification or API Get Status.
411 |
412 | Create separated web endpoint (notification url) to receive HTTP POST notification callback/webhook.
413 | HTTP notification will be sent whenever transaction status is changed.
414 | Example also available [here](examples/transaction_actions/notification_example.py)
415 |
416 | ```python
417 | # Create Core API / Snap instance (both have shared `transactions` methods)
418 | api_client = midtransclient.CoreApi(
419 | is_production=False,
420 | server_key='YOUR_SERVER_KEY',
421 | client_key='YOUR_CLIENT_KEY'
422 | )
423 | status_response = api_client.transactions.notification(mock_notification)
424 |
425 | order_id = status_response['order_id']
426 | transaction_status = status_response['transaction_status']
427 | fraud_status = status_response['fraud_status']
428 |
429 | print('Transaction notification received. Order ID: {0}. Transaction status: {1}. Fraud status: {3}'.format(order_id,
430 | transaction_status,
431 | fraud_status))
432 |
433 | # Sample transaction_status handling logic
434 |
435 | if transaction_status == 'capture':
436 | if fraud_status == 'challenge':
437 | # TODO set transaction status on your databaase to 'challenge'
438 | else if fraud_status == 'accept':
439 | # TODO set transaction status on your databaase to 'success'
440 | else if transaction_status == 'cancel' or
441 | transaction_status == 'deny' or
442 | transaction_status == 'expire':
443 | # TODO set transaction status on your databaase to 'failure'
444 | else if transaction_status == 'pending':
445 | # TODO set transaction status on your databaase to 'pending' / waiting payment
446 | ```
447 |
448 | ### 2.4 Transaction Action
449 | Also available as examples [here](examples/transaction_actions)
450 | #### Get Status
451 | ```python
452 | # get status of transaction that already recorded on midtrans (already `charge`-ed)
453 | status_response = api_client.transactions.status('YOUR_ORDER_ID OR TRANSACTION_ID')
454 | ```
455 | #### Get Status B2B
456 | ```python
457 | # get transaction status of VA b2b transaction
458 | statusb2b_response = api_client.transactions.statusb2b('YOUR_ORDER_ID OR TRANSACTION_ID')
459 | ```
460 | #### Approve Transaction
461 | ```python
462 | # approve a credit card transaction with `challenge` fraud status
463 | approve_response = api_client.transactions.approve('YOUR_ORDER_ID OR TRANSACTION_ID')
464 | ```
465 | #### Deny Transaction
466 | ```python
467 | # deny a credit card transaction with `challenge` fraud status
468 | deny_response = api_client.transactions.deny('YOUR_ORDER_ID OR TRANSACTION_ID')
469 | ```
470 | #### Cancel Transaction
471 | ```python
472 | # cancel a credit card transaction or pending transaction
473 | cancel_response = api_client.transactions.cancel('YOUR_ORDER_ID OR TRANSACTION_ID')
474 | ```
475 | #### Expire Transaction
476 | ```python
477 | # expire a pending transaction
478 | expire_response = api_client.transactions.expire('YOUR_ORDER_ID OR TRANSACTION_ID')
479 | ```
480 | #### Refund Transaction
481 | ```python
482 | # refund a transaction (not all payment channel allow refund via API)
483 | param = {
484 | "refund_key": "order1-ref1",
485 | "amount": 5000,
486 | "reason": "Item out of stock"
487 | }
488 | refund_response = api_client.transactions.refund('YOUR_ORDER_ID OR TRANSACTION_ID',param)
489 | ```
490 |
491 | #### Refund Transaction with Direct Refund
492 | ```python
493 | # refund a transaction (not all payment channel allow refund via API) with Direct Refund
494 | param = {
495 | "refund_key": "order1-ref1",
496 | "amount": 5000,
497 | "reason": "Item out of stock"
498 | }
499 | refund_response = api_client.transactions.refundDirect('YOUR_ORDER_ID OR TRANSACTION_ID',param)
500 | ```
501 |
502 | ## 3. Handling Error / Exception
503 | When using function that result in Midtrans API call e.g: `core.charge(...)` or `snap.create_transaction(...)`
504 | there's a chance it may throw error (`MidtransAPIError` object), the error object will contains below properties that can be used as information to your error handling logic:
505 | ```python
506 | err = None
507 | try:
508 | transaction = snap.create_transaction(param)
509 | except Exception as e:
510 | err = e
511 | err.message
512 | err.api_response_dict
513 | err.http_status_code
514 | err.raw_http_client_data
515 | ```
516 | ## 4. Advanced Usage
517 |
518 | ### Custom Http Headers
519 |
520 | You can set custom headers via the value of this `.api_config.custom_headers` dict, e.g:
521 | ```python
522 | # Create Snap API instance
523 | snap = midtransclient.Snap(
524 | is_production=False,
525 | server_key='YOUR_SERVER_KEY',
526 | client_key='YOUR_CLIENT_KEY'
527 | )
528 |
529 | # set custom HTTP header for every request from this instance
530 | snap.api_config.custom_headers = {
531 | 'my-custom-header':'my value',
532 | 'x-override-notification':'https://example.org',
533 | }
534 | ```
535 |
536 | ### Override/Append Http Notification Url
537 | As [described in API docs](https://snap-docs.midtrans.com/#override-notification-url), merchant can opt to change or add custom notification urls on every transaction. It can be achieved by adding additional HTTP headers into charge request.
538 |
539 | This can be achived by:
540 | ```python
541 | # create instance of api client
542 | snap = midtransclient.Snap(
543 | is_production=False,
544 | server_key='YOUR_SERVER_KEY',
545 | client_key='YOUR_CLIENT_KEY'
546 | )
547 | # set custom HTTP header that will be used by Midtrans API to override notification url:
548 | snap.api_config.custom_headers = {
549 | 'x-override-notification':'https://example.org',
550 | }
551 | ```
552 |
553 | or append notification:
554 | ```python
555 | snap.api_config.custom_headers = {
556 | 'x-append-notification':'https://example.org',
557 | }
558 | ```
559 |
560 | ### Custom Http Proxy
561 |
562 | You can set custom http(s) proxies via the value of this `.api_config.proxies` dict, e.g:
563 |
564 | ```python
565 | # create instance of api client
566 | snap = midtransclient.Snap(
567 | is_production=False,
568 | server_key='YOUR_SERVER_KEY',
569 | client_key='YOUR_CLIENT_KEY'
570 | )
571 |
572 | snap.api_config.proxies = {
573 | 'http': 'http://10.10.1.10:3128',
574 | 'https': 'http://10.10.1.10:1080',
575 | }
576 | ```
577 |
578 | Under the hood this API wrapper is using [Requests](https://github.com/requests/requests) as http client. You can further [learn about proxies on its documentation](https://requests.readthedocs.io/en/master/user/advanced/#proxies)
579 |
580 | ## Examples
581 | Examples are available on [/examples](/examples) folder.
582 | There are:
583 | - [Core Api examples](/examples/core_api)
584 | - [Subscription examples](/examples/subscription)
585 | - [Tokenization examples](/examples/tokenization)
586 | - [Snap examples](/examples/snap)
587 | - [Flask App examples](/examples/flask_app) that implement Snap & Core Api
588 |
589 | ## Important Changes
590 | ### v1.3.0
591 | - **Drop support for Python 2** (because Python 2 has reached its end of life), in favor of better compatibility with Python 3 and to prevent package unable to be properly installed on Windows OS env.
592 |
593 | #### Get help
594 |
595 | * [Midtrans Docs](https://docs.midtrans.com)
596 | * [Midtrans Dashboard ](https://dashboard.midtrans.com/)
597 | * [SNAP documentation](http://snap-docs.midtrans.com)
598 | * [Core API documentation](http://api-docs.midtrans.com)
599 | * Can't find answer you looking for? email to [support@midtrans.com](mailto:support@midtrans.com)
600 |
--------------------------------------------------------------------------------