mmpay-python-sdk 0.1.0__tar.gz → 0.1.1__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.
@@ -1,8 +1,8 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mmpay-python-sdk
3
- Version: 0.1.0
3
+ Version: 0.1.1
4
4
  Summary: Python SDK for MMPay (Ported from JS)
5
- Author: Your Name
5
+ Author: MyanMyanPay
6
6
  Requires-Python: >=3.6
7
7
  Requires-Dist: requests
8
8
  Dynamic: author
@@ -64,8 +64,8 @@ Passed to `sdk.pay(params)` or `sdk.sandbox_pay(params)`.
64
64
  | `orderId` | `str` | Yes | Unique identifier for the order (e.g., "ORD-001"). |
65
65
  | `amount` | `number` | Yes | Total transaction amount. |
66
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
67
  | `callbackUrl` | `str` | No | URL where the webhook callback will be sent. |
68
+ | `customMessage` | `str` | No | URL where the webhook callback will be sent. |
69
69
 
70
70
  ### 2. Item Object
71
71
 
@@ -83,9 +83,9 @@ try:
83
83
  payment_request = {
84
84
  "orderId": "ORD-SANDBOX-001",
85
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": [
86
+ "callbackUrl": "https://your-site.com/webhook/mmpay", # Optional
87
+ "customMessage": "Your Custom Messages", # Optional
88
+ "items": [ # Optional
89
89
  {
90
90
  "name": "Premium Subscription",
91
91
  "amount": 5000,
@@ -109,9 +109,15 @@ For production environments, use the `pay` method.
109
109
  try:
110
110
  payment_request = {
111
111
  "orderId": "ORD-LIVE-98765",
112
- "amount": 10000,
113
- "items": [
114
- {"name": "E-Commerce Item", "amount": 10000, "quantity": 1}
112
+ "amount": 5000, # Amount in minor units (e.g., cents) or as required
113
+ "callbackUrl": "https://your-site.com/webhook/mmpay", # Optional
114
+ "customMessage": "Your Custom Messages", # Optional
115
+ "items": [ # Optional
116
+ {
117
+ "name": "Premium Subscription",
118
+ "amount": 5000,
119
+ "quantity": 1
120
+ }
115
121
  ]
116
122
  }
117
123
 
@@ -125,15 +131,34 @@ except Exception as e:
125
131
 
126
132
  ### 3. Verify Callback (Webhook)
127
133
 
128
- When MMPay sends a callback to your `callbackUrl`, you must verify the request signature to ensure it is genuine.
134
+ When MMPay sends a callback to your `callbackUrl`, you must verify the request signature to ensure it is genuine. If not provide, it will fallback to default cb URL provided in your console
135
+
136
+ **Handling callbacks**
137
+
138
+ Incoming HTTP POST Parameters
139
+
140
+ Header
129
141
 
130
- ### Callback Verification (`verify_cb`)
142
+ | Field Name | Type | Required | Description |
143
+ | :--- | :--- | :--- | :--- |
144
+ | **Content-Type** | `string` | Yes | 'application/json' |
145
+ | **X-Mmpay-Signature** | `string` | Yes | '34834890vfgh9hnf94irfg_48932i4rt90349849' |
146
+ | **X-Mmpay-Nonce** | `string` | Yes | '94843943949349' |
147
+
148
+ Body
149
+
150
+ | Field Name | Type | Required | Description |
151
+ | :--- | :--- | :--- | :--- |
152
+ | **orderId** | `string` | Yes | Unique identifier for the specific order. |
153
+ | **amount** | `number` | Yes | The transaction amount. |
154
+ | **currency** | `string` | Yes | The 3-letter currency code (e.g., MMK, USD). |
155
+ | **vendor** | `string` | Yes | Identifier for the vendor initiating the request. |
156
+ | **method** | `'QR', 'PIN', 'PWA', 'CARD'` | Yes | Identifier for the method. |
157
+ | **status** | `'PENDING','SUCCESS','FAILED','REFUNDED'` | Yes | Current status of the transaction. |
158
+ | **transactionRefId** | `string` | Yes | The reference ID generated by the payment provider. |
159
+ | **callbackUrl** | `string` | No | Optional URL to receive webhooks or updates. |
160
+ | **customMessage** | `string` | No | User provided custom message |
131
161
 
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
162
 
138
163
  **Example using Flask:**
139
164
 
@@ -144,6 +169,7 @@ from flask import request
144
169
  def mmpay_webhook():
145
170
  # 1. Get the raw payload body as a string (Crucial for signature check)
146
171
  payload_str = request.data.decode('utf-8')
172
+ payload = request.data
147
173
 
148
174
  # 2. Get headers
149
175
  nonce = request.headers.get('X-Mmpay-Nonce')
@@ -155,6 +181,7 @@ def mmpay_webhook():
155
181
 
156
182
  if is_valid:
157
183
  # Process the order (e.g., mark as paid in DB)
184
+
158
185
  return "Verified", 200
159
186
  else:
160
187
  return "Invalid Signature", 400
@@ -164,6 +191,28 @@ def mmpay_webhook():
164
191
  ```
165
192
 
166
193
 
194
+ ### 4. Error Codes
195
+
196
+ ##### Api Key Layer Authentication [SERVER SDK]
197
+ | Code | Description |
198
+ | :--- | :--- |
199
+ | **`KA0001`** | Bearer Token Not Included In Your Request |
200
+ | **`KA0002`** | API Key Not 'LIVE' |
201
+ | **`KA0003`** | Signature mismatch |
202
+ | **`KA0004`** | Internal Server Error ( Talk to our support immediately fot this ) |
203
+ | **`KA0005`** | IP Not whitelisted |
204
+ | **`429`** | Ratelimit hit only 1000 request / minute allowed |
205
+
206
+
207
+ ##### JWT Layer Authentication [SERVER SDK]
208
+ | Code | Description |
209
+ | :--- | :--- |
210
+ | **`BA001`** | `Btoken` is nonce one time token is not included |
211
+ | **`BA002`** | `Btoken` one time nonce mismatch |
212
+ | **`BA000`** | Internal Server Error ( Talk to our support immediately fot this ) |
213
+ | **`429`** | Ratelimit hit only 1000 request / minute allowed |
214
+
215
+
167
216
  ## License
168
217
 
169
218
  MIT
@@ -3,21 +3,22 @@ import json
3
3
  import hmac
4
4
  import hashlib
5
5
  import requests
6
- from typing import List, Optional, TypedDict, Dict, Any
6
+ from typing import List, Optional, TypedDict, Dict, Any, Union
7
7
 
8
- # --- Type Definitions to mirror your Interfaces ---
8
+ # --- Type Definitions ---
9
9
 
10
10
  class Item(TypedDict):
11
11
  name: str
12
12
  amount: float
13
13
  quantity: int
14
14
 
15
- class PaymentRequest(TypedDict):
15
+ class PaymentRequest(TypedDict, total=False):
16
16
  orderId: str
17
17
  amount: float
18
- items: List[Item]
18
+ items: Optional[List[Item]]
19
19
  currency: Optional[str]
20
20
  callbackUrl: Optional[str]
21
+ customMessage: Optional[str] # Added to match TS
21
22
 
22
23
  class XPaymentRequest(PaymentRequest, total=False):
23
24
  appId: str
@@ -27,6 +28,21 @@ class HandShakeRequest(TypedDict):
27
28
  orderId: str
28
29
  nonce: str
29
30
 
31
+ class HandShakeResponse(TypedDict):
32
+ token: str
33
+
34
+ class CallbackIncomingData(TypedDict):
35
+ orderId: str
36
+ amount: float
37
+ method: str
38
+ currency: str
39
+ vendor: str
40
+ status: str
41
+ condition: str
42
+ transactionRefId: str
43
+ callbackUrl: Optional[str]
44
+ customMessage: Optional[str]
45
+
30
46
  class SDKOptions(TypedDict):
31
47
  appId: str
32
48
  publishableKey: str
@@ -43,12 +59,14 @@ class MMPaySDK:
43
59
  self._app_id = options['appId']
44
60
  self._publishable_key = options['publishableKey']
45
61
  self._secret_key = options['secretKey']
62
+ # Remove trailing slash if present to prevent double slashes in endpoints
46
63
  self._api_base_url = options['apiBaseUrl'].rstrip('/')
47
64
  self._btoken: Optional[str] = None
48
65
 
49
66
  def _generate_signature(self, body_string: str, nonce: str) -> str:
50
67
  """
51
68
  Generates an HMAC SHA256 signature for request integrity.
69
+ Matches: CryptoJS.HmacSHA256(nonce + "." + bodyString, secret)
52
70
  """
53
71
  string_to_sign = f"{nonce}.{body_string}"
54
72
  return hmac.new(
@@ -70,11 +88,10 @@ class MMPaySDK:
70
88
 
71
89
  # --- Sandbox Methods ---
72
90
 
73
- def sandbox_handshake(self, payload: HandShakeRequest) -> Dict[str, Any]:
91
+ def sandbox_handshake(self, payload: HandShakeRequest) -> Union[HandShakeResponse, Dict[str, Any]]:
74
92
  endpoint = f"{self._api_base_url}/payments/sandbox-handshake"
75
93
  nonce = self._get_nonce()
76
94
 
77
- # Ensure payload is serialized exactly as the signature expects
78
95
  body_string = self._json_stringify(payload)
79
96
  signature = self._generate_signature(body_string, nonce)
80
97
 
@@ -93,31 +110,39 @@ class MMPaySDK:
93
110
  self._btoken = data['token']
94
111
  return data
95
112
  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}
113
+ return {"error": str(e), "details": getattr(e.response, 'text', '')}
98
114
 
99
115
  def sandbox_pay(self, params: PaymentRequest) -> Dict[str, Any]:
100
116
  endpoint = f"{self._api_base_url}/payments/sandbox-create"
101
117
  nonce = self._get_nonce()
102
118
 
103
119
  # Construct payload
104
- xpayload: XPaymentRequest = {
120
+ # Note: We manually build the dict to ensure we don't include None/null
121
+ # for optional fields, which would break the signature vs JS stringify.
122
+ xpayload: Dict[str, Any] = {
105
123
  "appId": self._app_id,
106
124
  "nonce": nonce,
107
125
  "amount": params['amount'],
108
126
  "orderId": params['orderId'],
109
- "items": params['items'],
110
127
  }
111
- if 'callbackUrl' in params:
128
+
129
+ if params.get('items'):
130
+ xpayload['items'] = params['items']
131
+ if params.get('callbackUrl'):
112
132
  xpayload['callbackUrl'] = params['callbackUrl']
113
- if 'currency' in params:
114
- xpayload['currency'] = params['currency']
133
+ if params.get('customMessage'):
134
+ xpayload['customMessage'] = params['customMessage']
115
135
 
116
136
  body_string = self._json_stringify(xpayload)
117
137
  signature = self._generate_signature(body_string, nonce)
118
138
 
119
- # Perform handshake first (as per TS logic)
120
- handshake_res = self.sandbox_handshake({'orderId': xpayload['orderId'], 'nonce': xpayload['nonce']})
139
+ # Perform handshake first (using the nonce from the payload logic as per TS)
140
+ handshake_payload: HandShakeRequest = {
141
+ 'orderId': str(xpayload['orderId']),
142
+ 'nonce': str(xpayload['nonce'])
143
+ }
144
+
145
+ handshake_res = self.sandbox_handshake(handshake_payload)
121
146
  if 'error' in handshake_res:
122
147
  return handshake_res
123
148
 
@@ -134,11 +159,11 @@ class MMPaySDK:
134
159
  response.raise_for_status()
135
160
  return response.json()
136
161
  except requests.exceptions.RequestException as e:
137
- return {"error": str(e), "details": response.text if response else None}
162
+ return {"error": str(e), "details": getattr(e.response, 'text', '')}
138
163
 
139
164
  # --- Production Methods ---
140
165
 
141
- def handshake(self, payload: HandShakeRequest) -> Dict[str, Any]:
166
+ def handshake(self, payload: HandShakeRequest) -> Union[HandShakeResponse, Dict[str, Any]]:
142
167
  endpoint = f"{self._api_base_url}/payments/handshake"
143
168
  nonce = self._get_nonce()
144
169
 
@@ -160,29 +185,36 @@ class MMPaySDK:
160
185
  self._btoken = data['token']
161
186
  return data
162
187
  except requests.exceptions.RequestException as e:
163
- return {"error": str(e), "details": response.text if response else None}
188
+ return {"error": str(e), "details": getattr(e.response, 'text', '')}
164
189
 
165
190
  def pay(self, params: PaymentRequest) -> Dict[str, Any]:
166
191
  endpoint = f"{self._api_base_url}/payments/create"
167
192
  nonce = self._get_nonce()
168
193
 
169
- xpayload: XPaymentRequest = {
194
+ xpayload: Dict[str, Any] = {
170
195
  "appId": self._app_id,
171
196
  "nonce": nonce,
172
197
  "amount": params['amount'],
173
198
  "orderId": params['orderId'],
174
- "items": params['items'],
175
199
  }
176
- if 'callbackUrl' in params:
200
+
201
+ if params.get('items'):
202
+ xpayload['items'] = params['items']
203
+ if params.get('callbackUrl'):
177
204
  xpayload['callbackUrl'] = params['callbackUrl']
178
- if 'currency' in params:
179
- xpayload['currency'] = params['currency']
205
+ if params.get('customMessage'):
206
+ xpayload['customMessage'] = params['customMessage']
180
207
 
181
208
  body_string = self._json_stringify(xpayload)
182
209
  signature = self._generate_signature(body_string, nonce)
183
210
 
184
211
  # Perform handshake
185
- handshake_res = self.handshake({'orderId': xpayload['orderId'], 'nonce': xpayload['nonce']})
212
+ handshake_payload: HandShakeRequest = {
213
+ 'orderId': str(xpayload['orderId']),
214
+ 'nonce': str(xpayload['nonce'])
215
+ }
216
+
217
+ handshake_res = self.handshake(handshake_payload)
186
218
  if 'error' in handshake_res:
187
219
  return handshake_res
188
220
 
@@ -199,7 +231,7 @@ class MMPaySDK:
199
231
  response.raise_for_status()
200
232
  return response.json()
201
233
  except requests.exceptions.RequestException as e:
202
- return {"error": str(e), "details": response.text if response else None}
234
+ return {"error": str(e), "details": getattr(e.response, 'text', '')}
203
235
 
204
236
  # --- Verification ---
205
237
 
@@ -218,6 +250,7 @@ class MMPaySDK:
218
250
  ).hexdigest()
219
251
 
220
252
  if generated_signature != expected_signature:
253
+ # Using print for logging as per TS console.error
221
254
  print(f"Signature mismatch: gen={generated_signature}, exp={expected_signature}")
222
255
  return False
223
256
 
@@ -1,8 +1,8 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mmpay-python-sdk
3
- Version: 0.1.0
3
+ Version: 0.1.1
4
4
  Summary: Python SDK for MMPay (Ported from JS)
5
- Author: Your Name
5
+ Author: MyanMyanPay
6
6
  Requires-Python: >=3.6
7
7
  Requires-Dist: requests
8
8
  Dynamic: author
@@ -2,9 +2,9 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name="mmpay-python-sdk",
5
- version="0.1.0",
5
+ version="0.1.1",
6
6
  description="Python SDK for MMPay (Ported from JS)",
7
- author="Your Name",
7
+ author="MyanMyanPay",
8
8
  packages=find_packages(),
9
9
  install_requires=[
10
10
  "requests",
@@ -0,0 +1,76 @@
1
+ import time
2
+ import os
3
+ import random
4
+ import string
5
+ from dotenv import load_dotenv
6
+
7
+ from mmpay import MMPaySDK
8
+ # from mmpay_sdk import MMPaySDK # Assumes the SDK is saved in mmpay_sdk.py
9
+
10
+ # Load environment variables from .env file
11
+ load_dotenv()
12
+
13
+ def generate_secure_random_string(length: int) -> str:
14
+ """
15
+ Generates a random alphanumeric string.
16
+ """
17
+ # Using random.choices to generate a string of requested length
18
+ # This mimics the base36 behavior of the JS example
19
+ chars = string.ascii_lowercase + string.digits
20
+ return ''.join(random.choices(chars, k=length))
21
+
22
+ def start():
23
+ """
24
+ Executes the payment call and measures network latency.
25
+ """
26
+ # Initialize SDK
27
+ # specific env keys matching your JS example
28
+ mmpay = MMPaySDK({
29
+ 'appId': os.getenv('APP_ID', ''),
30
+ 'publishableKey': os.getenv('PUB_KEY', ''),
31
+ 'secretKey': os.getenv('SEC_KEY', ''),
32
+ 'apiBaseUrl': os.getenv('BASEURL', '')
33
+ })
34
+
35
+ # Generate Order ID
36
+ order_id = generate_secure_random_string(6)
37
+
38
+ # Start Timer
39
+ # time.perf_counter() is the Python equivalent to performance.now()
40
+ start_time = time.perf_counter()
41
+
42
+ try:
43
+ payload = {
44
+ "orderId": order_id,
45
+ "amount": 1500,
46
+ "customMessage": "MyanMyanPay Is The Best",
47
+ "items": [{"name": "Items", "amount": 3000, "quantity": 1}]
48
+ }
49
+
50
+ # Execute Payment (Synchronous in Python version)
51
+ response = mmpay.sandbox_pay(payload)
52
+
53
+ # End Timer
54
+ end_time = time.perf_counter()
55
+
56
+ # Calculate latency in milliseconds
57
+ latency_ms = (end_time - start_time) * 1000
58
+
59
+ print(f"\n--- Transaction Request Successful ---")
60
+ print(f"Order ID: {order_id}")
61
+ print(f"**Network Latency: {latency_ms:.3f} ms**")
62
+ print(f"Response: {response}")
63
+ print(f"------------------------------\n")
64
+
65
+ except Exception as error:
66
+ end_time = time.perf_counter()
67
+ latency_ms = (end_time - start_time) * 1000
68
+
69
+ print(f"\n--- Transaction Request Failed ---")
70
+ print(f"Order ID: {order_id}")
71
+ print(f"**Network Latency: {latency_ms:.3f} ms**")
72
+ print(f"Error Message: {str(error)}")
73
+ print(f"--------------------------\n")
74
+
75
+ if __name__ == "__main__":
76
+ start()
@@ -1,21 +0,0 @@
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)