mmpay-python-sdk 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,11 @@
1
+ Metadata-Version: 2.4
2
+ Name: mmpay-python-sdk
3
+ Version: 0.1.0
4
+ Summary: Python SDK for MMPay (Ported from JS)
5
+ Author: Your Name
6
+ Requires-Python: >=3.6
7
+ Requires-Dist: requests
8
+ Dynamic: author
9
+ Dynamic: requires-dist
10
+ Dynamic: requires-python
11
+ Dynamic: summary
@@ -0,0 +1,169 @@
1
+
2
+ # MMPay Python SDK
3
+
4
+ A Python client library for integrating with the MMPay Payment Gateway. This SDK is a direct port of the official Node.js SDK, providing utilities for payment creation, handshake authentication, and callback verification.
5
+
6
+ ## Features
7
+
8
+ - **Sandbox & Production Support**: dedicated methods for both environments.
9
+ - **HMAC SHA256 Signing**: Automatic signature generation for request integrity.
10
+ - **Callback Verification**: Utility to verify incoming webhooks from MMPay.
11
+ - **Type Definitions**: Includes `TypedDict` definitions for clear payload structuring.
12
+
13
+ ## Installation
14
+
15
+ Install the package via pip:
16
+
17
+ ```bash
18
+ pip install mmpay-python-sdk
19
+ ```
20
+
21
+ ## Configuration
22
+
23
+ To use the SDK, you need your **App ID**, **Publishable Key**, and **Secret Key** provided by the MMPay dashboard.
24
+
25
+ Used when instantiating `MMPaySDK(options)`.
26
+
27
+ | Parameter | Type | Required | Description |
28
+ | :--- | :--- | :--- | :--- |
29
+ | `appId` | `str` | Yes | Your unique Application ID. |
30
+ | `publishableKey` | `str` | Yes | Public key for authentication. |
31
+ | `secretKey` | `str` | Yes | Private key used for signing requests (HMAC). |
32
+ | `apiBaseUrl` | `str` | Yes | The base URL for the MMPay API. |
33
+
34
+ ```python
35
+ from mmpay import MMPaySDK
36
+
37
+ # Initialize the SDK
38
+ options = {
39
+ "appId": "YOUR_APP_ID",
40
+ "publishableKey": "YOUR_PUBLISHABLE_KEY",
41
+ "secretKey": "YOUR_SECRET_KEY",
42
+ "apiBaseUrl": "[https://xxx.myanmyanpay.com](https://xxx.myanmyanpay.com)" # Replace with actual API Base URL [ Register With Us]
43
+ }
44
+
45
+ sdk = MMPaySDK(options)
46
+ ```
47
+
48
+
49
+
50
+
51
+ ## Usage
52
+
53
+ ### 1. Create a Payment (Sandbox)
54
+
55
+ Use `sandbox_pay` to create a payment order in the Sandbox environment. This handles the handshake and signature generation automatically.
56
+
57
+
58
+ ### 1. Payment Request (`pay` / `sandbox_pay`)
59
+
60
+ Passed to `sdk.pay(params)` or `sdk.sandbox_pay(params)`.
61
+
62
+ | Parameter | Type | Required | Description |
63
+ | :--- | :--- | :--- | :--- |
64
+ | `orderId` | `str` | Yes | Unique identifier for the order (e.g., "ORD-001"). |
65
+ | `amount` | `number` | Yes | Total transaction amount. |
66
+ | `items` | `List[Item]` | Yes | A list of items included in the order (see table below). |
67
+ | `currency` | `str` | No | Currency code (e.g., "MMK", "USD"). |
68
+ | `callbackUrl` | `str` | No | URL where the webhook callback will be sent. |
69
+
70
+ ### 2. Item Object
71
+
72
+ Used inside the `items` list of a Payment Request.
73
+
74
+ | Parameter | Type | Required | Description |
75
+ | :--- | :--- | :--- | :--- |
76
+ | `name` | `str` | Yes | Name of the product or service. |
77
+ | `amount` | `number` | Yes | Price per unit. |
78
+ | `quantity` | `int` | Yes | Quantity of the item. |
79
+
80
+
81
+ ```python
82
+ try:
83
+ payment_request = {
84
+ "orderId": "ORD-SANDBOX-001",
85
+ "amount": 5000, # Amount in minor units (e.g., cents) or as required
86
+ "currency": "MMK",
87
+ "callbackUrl": "[https://your-site.com/webhook/mmpay](https://your-site.com/webhook/mmpay)",
88
+ "items": [
89
+ {
90
+ "name": "Premium Subscription",
91
+ "amount": 5000,
92
+ "quantity": 1
93
+ }
94
+ ]
95
+ }
96
+
97
+ response = sdk.sandbox_pay(payment_request)
98
+ print("Payment Response:", response)
99
+
100
+ except Exception as e:
101
+ print("Error creating payment:", e)
102
+ ```
103
+
104
+ ### 2. Create a Payment (Production)
105
+
106
+ For production environments, use the `pay` method.
107
+
108
+ ```python
109
+ try:
110
+ payment_request = {
111
+ "orderId": "ORD-LIVE-98765",
112
+ "amount": 10000,
113
+ "items": [
114
+ {"name": "E-Commerce Item", "amount": 10000, "quantity": 1}
115
+ ]
116
+ }
117
+
118
+ # Helper automatically handles the handshake and signing
119
+ response = sdk.pay(payment_request)
120
+ print("Production Payment URL:", response.get('url'))
121
+
122
+ except Exception as e:
123
+ print("Error:", e)
124
+ ```
125
+
126
+ ### 3. Verify Callback (Webhook)
127
+
128
+ When MMPay sends a callback to your `callbackUrl`, you must verify the request signature to ensure it is genuine.
129
+
130
+ ### Callback Verification (`verify_cb`)
131
+
132
+ | Parameter | Type | Description |
133
+ | :--- | :--- | :--- |
134
+ | `payload` | `str` | The **raw** JSON string body of the incoming request. |
135
+ | `nonce` | `str` | The value of the `X-Mmpay-Nonce` header. |
136
+ | `expected_signature` | `str` | The value of the `X-Mmpay-Signature` header. |
137
+
138
+ **Example using Flask:**
139
+
140
+ ```python
141
+ from flask import request
142
+
143
+ @app.route('/webhook/mmpay', methods=['POST'])
144
+ def mmpay_webhook():
145
+ # 1. Get the raw payload body as a string (Crucial for signature check)
146
+ payload_str = request.data.decode('utf-8')
147
+
148
+ # 2. Get headers
149
+ nonce = request.headers.get('X-Mmpay-Nonce')
150
+ signature = request.headers.get('X-Mmpay-Signature')
151
+
152
+ try:
153
+ # 3. Verify
154
+ is_valid = sdk.verify_cb(payload_str, nonce, signature)
155
+
156
+ if is_valid:
157
+ # Process the order (e.g., mark as paid in DB)
158
+ return "Verified", 200
159
+ else:
160
+ return "Invalid Signature", 400
161
+
162
+ except ValueError as e:
163
+ return str(e), 400
164
+ ```
165
+
166
+
167
+ ## License
168
+
169
+ MIT
@@ -0,0 +1,3 @@
1
+ from .client import MMPaySDK
2
+
3
+ __all__ = ["MMPaySDK"]
@@ -0,0 +1,224 @@
1
+ import time
2
+ import json
3
+ import hmac
4
+ import hashlib
5
+ import requests
6
+ from typing import List, Optional, TypedDict, Dict, Any
7
+
8
+ # --- Type Definitions to mirror your Interfaces ---
9
+
10
+ class Item(TypedDict):
11
+ name: str
12
+ amount: float
13
+ quantity: int
14
+
15
+ class PaymentRequest(TypedDict):
16
+ orderId: str
17
+ amount: float
18
+ items: List[Item]
19
+ currency: Optional[str]
20
+ callbackUrl: Optional[str]
21
+
22
+ class XPaymentRequest(PaymentRequest, total=False):
23
+ appId: str
24
+ nonce: str
25
+
26
+ class HandShakeRequest(TypedDict):
27
+ orderId: str
28
+ nonce: str
29
+
30
+ class SDKOptions(TypedDict):
31
+ appId: str
32
+ publishableKey: str
33
+ secretKey: str
34
+ apiBaseUrl: str
35
+
36
+ # --- Main SDK Class ---
37
+
38
+ class MMPaySDK:
39
+ def __init__(self, options: SDKOptions):
40
+ """
41
+ Initializes the SDK with the merchant's keys and the API endpoint.
42
+ """
43
+ self._app_id = options['appId']
44
+ self._publishable_key = options['publishableKey']
45
+ self._secret_key = options['secretKey']
46
+ self._api_base_url = options['apiBaseUrl'].rstrip('/')
47
+ self._btoken: Optional[str] = None
48
+
49
+ def _generate_signature(self, body_string: str, nonce: str) -> str:
50
+ """
51
+ Generates an HMAC SHA256 signature for request integrity.
52
+ """
53
+ string_to_sign = f"{nonce}.{body_string}"
54
+ return hmac.new(
55
+ self._secret_key.encode('utf-8'),
56
+ string_to_sign.encode('utf-8'),
57
+ hashlib.sha256
58
+ ).hexdigest()
59
+
60
+ def _get_nonce(self) -> str:
61
+ """Helper to get current timestamp as string (milliseconds)"""
62
+ return str(int(time.time() * 1000))
63
+
64
+ def _json_stringify(self, data: Any) -> str:
65
+ """
66
+ Mimics JS JSON.stringify exactly (no spaces).
67
+ Crucial for signature verification.
68
+ """
69
+ return json.dumps(data, separators=(',', ':'))
70
+
71
+ # --- Sandbox Methods ---
72
+
73
+ def sandbox_handshake(self, payload: HandShakeRequest) -> Dict[str, Any]:
74
+ endpoint = f"{self._api_base_url}/payments/sandbox-handshake"
75
+ nonce = self._get_nonce()
76
+
77
+ # Ensure payload is serialized exactly as the signature expects
78
+ body_string = self._json_stringify(payload)
79
+ signature = self._generate_signature(body_string, nonce)
80
+
81
+ headers = {
82
+ 'Authorization': f"Bearer {self._publishable_key}",
83
+ 'X-Mmpay-Nonce': nonce,
84
+ 'X-Mmpay-Signature': signature,
85
+ 'Content-Type': 'application/json',
86
+ }
87
+
88
+ try:
89
+ response = requests.post(endpoint, data=body_string, headers=headers)
90
+ response.raise_for_status()
91
+ data = response.json()
92
+ if 'token' in data:
93
+ self._btoken = data['token']
94
+ return data
95
+ except requests.exceptions.RequestException as e:
96
+ # In a real SDK, you might want to raise custom exceptions here
97
+ return {"error": str(e), "details": response.text if response else None}
98
+
99
+ def sandbox_pay(self, params: PaymentRequest) -> Dict[str, Any]:
100
+ endpoint = f"{self._api_base_url}/payments/sandbox-create"
101
+ nonce = self._get_nonce()
102
+
103
+ # Construct payload
104
+ xpayload: XPaymentRequest = {
105
+ "appId": self._app_id,
106
+ "nonce": nonce,
107
+ "amount": params['amount'],
108
+ "orderId": params['orderId'],
109
+ "items": params['items'],
110
+ }
111
+ if 'callbackUrl' in params:
112
+ xpayload['callbackUrl'] = params['callbackUrl']
113
+ if 'currency' in params:
114
+ xpayload['currency'] = params['currency']
115
+
116
+ body_string = self._json_stringify(xpayload)
117
+ signature = self._generate_signature(body_string, nonce)
118
+
119
+ # Perform handshake first (as per TS logic)
120
+ handshake_res = self.sandbox_handshake({'orderId': xpayload['orderId'], 'nonce': xpayload['nonce']})
121
+ if 'error' in handshake_res:
122
+ return handshake_res
123
+
124
+ headers = {
125
+ 'Authorization': f"Bearer {self._publishable_key}",
126
+ 'X-Mmpay-Btoken': self._btoken,
127
+ 'X-Mmpay-Nonce': nonce,
128
+ 'X-Mmpay-Signature': signature,
129
+ 'Content-Type': 'application/json',
130
+ }
131
+
132
+ try:
133
+ response = requests.post(endpoint, data=body_string, headers=headers)
134
+ response.raise_for_status()
135
+ return response.json()
136
+ except requests.exceptions.RequestException as e:
137
+ return {"error": str(e), "details": response.text if response else None}
138
+
139
+ # --- Production Methods ---
140
+
141
+ def handshake(self, payload: HandShakeRequest) -> Dict[str, Any]:
142
+ endpoint = f"{self._api_base_url}/payments/handshake"
143
+ nonce = self._get_nonce()
144
+
145
+ body_string = self._json_stringify(payload)
146
+ signature = self._generate_signature(body_string, nonce)
147
+
148
+ headers = {
149
+ 'Authorization': f"Bearer {self._publishable_key}",
150
+ 'X-Mmpay-Nonce': nonce,
151
+ 'X-Mmpay-Signature': signature,
152
+ 'Content-Type': 'application/json',
153
+ }
154
+
155
+ try:
156
+ response = requests.post(endpoint, data=body_string, headers=headers)
157
+ response.raise_for_status()
158
+ data = response.json()
159
+ if 'token' in data:
160
+ self._btoken = data['token']
161
+ return data
162
+ except requests.exceptions.RequestException as e:
163
+ return {"error": str(e), "details": response.text if response else None}
164
+
165
+ def pay(self, params: PaymentRequest) -> Dict[str, Any]:
166
+ endpoint = f"{self._api_base_url}/payments/create"
167
+ nonce = self._get_nonce()
168
+
169
+ xpayload: XPaymentRequest = {
170
+ "appId": self._app_id,
171
+ "nonce": nonce,
172
+ "amount": params['amount'],
173
+ "orderId": params['orderId'],
174
+ "items": params['items'],
175
+ }
176
+ if 'callbackUrl' in params:
177
+ xpayload['callbackUrl'] = params['callbackUrl']
178
+ if 'currency' in params:
179
+ xpayload['currency'] = params['currency']
180
+
181
+ body_string = self._json_stringify(xpayload)
182
+ signature = self._generate_signature(body_string, nonce)
183
+
184
+ # Perform handshake
185
+ handshake_res = self.handshake({'orderId': xpayload['orderId'], 'nonce': xpayload['nonce']})
186
+ if 'error' in handshake_res:
187
+ return handshake_res
188
+
189
+ headers = {
190
+ 'Authorization': f"Bearer {self._publishable_key}",
191
+ 'X-Mmpay-Btoken': self._btoken,
192
+ 'X-Mmpay-Nonce': nonce,
193
+ 'X-Mmpay-Signature': signature,
194
+ 'Content-Type': 'application/json',
195
+ }
196
+
197
+ try:
198
+ response = requests.post(endpoint, data=body_string, headers=headers)
199
+ response.raise_for_status()
200
+ return response.json()
201
+ except requests.exceptions.RequestException as e:
202
+ return {"error": str(e), "details": response.text if response else None}
203
+
204
+ # --- Verification ---
205
+
206
+ def verify_cb(self, payload: str, nonce: str, expected_signature: str) -> bool:
207
+ """
208
+ Verifies the signature of a callback request.
209
+ """
210
+ if not payload or not nonce or not expected_signature:
211
+ raise ValueError("Callback verification failed: Missing payload, nonce, or signature.")
212
+
213
+ string_to_sign = f"{nonce}.{payload}"
214
+ generated_signature = hmac.new(
215
+ self._secret_key.encode('utf-8'),
216
+ string_to_sign.encode('utf-8'),
217
+ hashlib.sha256
218
+ ).hexdigest()
219
+
220
+ if generated_signature != expected_signature:
221
+ print(f"Signature mismatch: gen={generated_signature}, exp={expected_signature}")
222
+ return False
223
+
224
+ return True
@@ -0,0 +1,11 @@
1
+ Metadata-Version: 2.4
2
+ Name: mmpay-python-sdk
3
+ Version: 0.1.0
4
+ Summary: Python SDK for MMPay (Ported from JS)
5
+ Author: Your Name
6
+ Requires-Python: >=3.6
7
+ Requires-Dist: requests
8
+ Dynamic: author
9
+ Dynamic: requires-dist
10
+ Dynamic: requires-python
11
+ Dynamic: summary
@@ -0,0 +1,10 @@
1
+ README.md
2
+ setup.py
3
+ mmpay/__init__.py
4
+ mmpay/client.py
5
+ mmpay_python_sdk.egg-info/PKG-INFO
6
+ mmpay_python_sdk.egg-info/SOURCES.txt
7
+ mmpay_python_sdk.egg-info/dependency_links.txt
8
+ mmpay_python_sdk.egg-info/requires.txt
9
+ mmpay_python_sdk.egg-info/top_level.txt
10
+ test/test_sdk.py
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,13 @@
1
+ from setuptools import setup, find_packages
2
+
3
+ setup(
4
+ name="mmpay-python-sdk",
5
+ version="0.1.0",
6
+ description="Python SDK for MMPay (Ported from JS)",
7
+ author="Your Name",
8
+ packages=find_packages(),
9
+ install_requires=[
10
+ "requests",
11
+ ],
12
+ python_requires=">=3.6",
13
+ )
@@ -0,0 +1,21 @@
1
+ from mmpay import MMPaySDK
2
+
3
+ # Initialize
4
+ sdk = MMPaySDK({
5
+ "appId": "your_app_id",
6
+ "publishableKey": "your_pub_key",
7
+ "secretKey": "your_secret_key",
8
+ "apiBaseUrl": "https://api.mmpay.com" # Example URL
9
+ })
10
+
11
+ # Create a Payment (Sandbox)
12
+ response = sdk.sandbox_pay({
13
+ "orderId": "ORD-123456789",
14
+ "amount": 1000,
15
+ "callbackUrl": "https://yoursite.com/callback",
16
+ "items": [
17
+ {"name": "Test Item", "amount": 1000, "quantity": 1}
18
+ ]
19
+ })
20
+
21
+ print(response)