@atbas/m-mcp 0.1.0
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.
- package/LICENSE +21 -0
- package/README.md +364 -0
- package/dist/builder/transaction-builder.d.ts +53 -0
- package/dist/builder/transaction-builder.d.ts.map +1 -0
- package/dist/builder/transaction-builder.js +159 -0
- package/dist/builder/transaction-builder.js.map +1 -0
- package/dist/catalogue/default-catalogue.d.ts +18 -0
- package/dist/catalogue/default-catalogue.d.ts.map +1 -0
- package/dist/catalogue/default-catalogue.js +21 -0
- package/dist/catalogue/default-catalogue.js.map +1 -0
- package/dist/catalogue/endpoints.d.ts +22 -0
- package/dist/catalogue/endpoints.d.ts.map +1 -0
- package/dist/catalogue/endpoints.js +24 -0
- package/dist/catalogue/endpoints.js.map +1 -0
- package/dist/catalogue/flows.d.ts +32 -0
- package/dist/catalogue/flows.d.ts.map +1 -0
- package/dist/catalogue/flows.js +64 -0
- package/dist/catalogue/flows.js.map +1 -0
- package/dist/catalogue/hash-options.d.ts +112 -0
- package/dist/catalogue/hash-options.d.ts.map +1 -0
- package/dist/catalogue/hash-options.js +151 -0
- package/dist/catalogue/hash-options.js.map +1 -0
- package/dist/catalogue/types.d.ts +279 -0
- package/dist/catalogue/types.d.ts.map +1 -0
- package/dist/catalogue/types.js +360 -0
- package/dist/catalogue/types.js.map +1 -0
- package/dist/codegen/generate.d.ts +14 -0
- package/dist/codegen/generate.d.ts.map +1 -0
- package/dist/codegen/generate.js +490 -0
- package/dist/codegen/generate.js.map +1 -0
- package/dist/codegen/languages/node.d.ts +3 -0
- package/dist/codegen/languages/node.d.ts.map +1 -0
- package/dist/codegen/languages/node.js +32 -0
- package/dist/codegen/languages/node.js.map +1 -0
- package/dist/codegen/languages/php.d.ts +3 -0
- package/dist/codegen/languages/php.d.ts.map +1 -0
- package/dist/codegen/languages/php.js +27 -0
- package/dist/codegen/languages/php.js.map +1 -0
- package/dist/codegen/languages/python.d.ts +3 -0
- package/dist/codegen/languages/python.d.ts.map +1 -0
- package/dist/codegen/languages/python.js +27 -0
- package/dist/codegen/languages/python.js.map +1 -0
- package/dist/codegen/renderer.d.ts +24 -0
- package/dist/codegen/renderer.d.ts.map +1 -0
- package/dist/codegen/renderer.js +59 -0
- package/dist/codegen/renderer.js.map +1 -0
- package/dist/codegen/types.d.ts +50 -0
- package/dist/codegen/types.d.ts.map +1 -0
- package/dist/codegen/types.js +5 -0
- package/dist/codegen/types.js.map +1 -0
- package/dist/config/credentials.d.ts +64 -0
- package/dist/config/credentials.d.ts.map +1 -0
- package/dist/config/credentials.js +216 -0
- package/dist/config/credentials.js.map +1 -0
- package/dist/config/environment.d.ts +23 -0
- package/dist/config/environment.d.ts.map +1 -0
- package/dist/config/environment.js +54 -0
- package/dist/config/environment.js.map +1 -0
- package/dist/core/dependencies.d.ts +43 -0
- package/dist/core/dependencies.d.ts.map +1 -0
- package/dist/core/dependencies.js +2 -0
- package/dist/core/dependencies.js.map +1 -0
- package/dist/core/registry.d.ts +33 -0
- package/dist/core/registry.d.ts.map +1 -0
- package/dist/core/registry.js +13 -0
- package/dist/core/registry.js.map +1 -0
- package/dist/core/server.d.ts +61 -0
- package/dist/core/server.d.ts.map +1 -0
- package/dist/core/server.js +90 -0
- package/dist/core/server.js.map +1 -0
- package/dist/dependencies.d.ts +28 -0
- package/dist/dependencies.d.ts.map +1 -0
- package/dist/dependencies.js +2 -0
- package/dist/dependencies.js.map +1 -0
- package/dist/diagnostics/diagnose.d.ts +43 -0
- package/dist/diagnostics/diagnose.d.ts.map +1 -0
- package/dist/diagnostics/diagnose.js +121 -0
- package/dist/diagnostics/diagnose.js.map +1 -0
- package/dist/domain/amount.d.ts +157 -0
- package/dist/domain/amount.d.ts.map +1 -0
- package/dist/domain/amount.js +275 -0
- package/dist/domain/amount.js.map +1 -0
- package/dist/domain/datetime.d.ts +95 -0
- package/dist/domain/datetime.d.ts.map +1 -0
- package/dist/domain/datetime.js +211 -0
- package/dist/domain/datetime.js.map +1 -0
- package/dist/domain/response-codes.d.ts +116 -0
- package/dist/domain/response-codes.d.ts.map +1 -0
- package/dist/domain/response-codes.js +380 -0
- package/dist/domain/response-codes.js.map +1 -0
- package/dist/domain/secure-hash.d.ts +78 -0
- package/dist/domain/secure-hash.d.ts.map +1 -0
- package/dist/domain/secure-hash.js +69 -0
- package/dist/domain/secure-hash.js.map +1 -0
- package/dist/domain/signature/jazzcash.d.ts +56 -0
- package/dist/domain/signature/jazzcash.d.ts.map +1 -0
- package/dist/domain/signature/jazzcash.js +214 -0
- package/dist/domain/signature/jazzcash.js.map +1 -0
- package/dist/domain/signature/registry.d.ts +69 -0
- package/dist/domain/signature/registry.d.ts.map +1 -0
- package/dist/domain/signature/registry.js +92 -0
- package/dist/domain/signature/registry.js.map +1 -0
- package/dist/domain/signature/scheme.d.ts +349 -0
- package/dist/domain/signature/scheme.d.ts.map +1 -0
- package/dist/domain/signature/scheme.js +398 -0
- package/dist/domain/signature/scheme.js.map +1 -0
- package/dist/domain/txn-ref.d.ts +76 -0
- package/dist/domain/txn-ref.d.ts.map +1 -0
- package/dist/domain/txn-ref.js +126 -0
- package/dist/domain/txn-ref.js.map +1 -0
- package/dist/domain/types.d.ts +45 -0
- package/dist/domain/types.d.ts.map +1 -0
- package/dist/domain/types.js +70 -0
- package/dist/domain/types.js.map +1 -0
- package/dist/errors.d.ts +84 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +87 -0
- package/dist/errors.js.map +1 -0
- package/dist/gateway/checkout-form.d.ts +16 -0
- package/dist/gateway/checkout-form.d.ts.map +1 -0
- package/dist/gateway/checkout-form.js +103 -0
- package/dist/gateway/checkout-form.js.map +1 -0
- package/dist/gateway/checkout-output.d.ts +70 -0
- package/dist/gateway/checkout-output.d.ts.map +1 -0
- package/dist/gateway/checkout-output.js +199 -0
- package/dist/gateway/checkout-output.js.map +1 -0
- package/dist/gateway/guards.d.ts +37 -0
- package/dist/gateway/guards.d.ts.map +1 -0
- package/dist/gateway/guards.js +44 -0
- package/dist/gateway/guards.js.map +1 -0
- package/dist/gateway/http-client.d.ts +42 -0
- package/dist/gateway/http-client.d.ts.map +1 -0
- package/dist/gateway/http-client.js +153 -0
- package/dist/gateway/http-client.js.map +1 -0
- package/dist/gateway/rest-client.d.ts +42 -0
- package/dist/gateway/rest-client.d.ts.map +1 -0
- package/dist/gateway/rest-client.js +241 -0
- package/dist/gateway/rest-client.js.map +1 -0
- package/dist/gateway/transport.d.ts +31 -0
- package/dist/gateway/transport.d.ts.map +1 -0
- package/dist/gateway/transport.js +73 -0
- package/dist/gateway/transport.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +75 -0
- package/dist/index.js.map +1 -0
- package/dist/install/cli.d.ts +38 -0
- package/dist/install/cli.d.ts.map +1 -0
- package/dist/install/cli.js +274 -0
- package/dist/install/cli.js.map +1 -0
- package/dist/install/harnesses.d.ts +74 -0
- package/dist/install/harnesses.d.ts.map +1 -0
- package/dist/install/harnesses.js +263 -0
- package/dist/install/harnesses.js.map +1 -0
- package/dist/knowledge/corpus.d.ts +15 -0
- package/dist/knowledge/corpus.d.ts.map +1 -0
- package/dist/knowledge/corpus.js +129 -0
- package/dist/knowledge/corpus.js.map +1 -0
- package/dist/knowledge/search.d.ts +29 -0
- package/dist/knowledge/search.d.ts.map +1 -0
- package/dist/knowledge/search.js +181 -0
- package/dist/knowledge/search.js.map +1 -0
- package/dist/knowledge/source.d.ts +60 -0
- package/dist/knowledge/source.d.ts.map +1 -0
- package/dist/knowledge/source.js +91 -0
- package/dist/knowledge/source.js.map +1 -0
- package/dist/knowledge/types.d.ts +40 -0
- package/dist/knowledge/types.d.ts.map +1 -0
- package/dist/knowledge/types.js +19 -0
- package/dist/knowledge/types.js.map +1 -0
- package/dist/paths.d.ts +26 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +106 -0
- package/dist/paths.js.map +1 -0
- package/dist/prompts/integration-walkthrough.d.ts +11 -0
- package/dist/prompts/integration-walkthrough.d.ts.map +1 -0
- package/dist/prompts/integration-walkthrough.js +52 -0
- package/dist/prompts/integration-walkthrough.js.map +1 -0
- package/dist/prompts/register.d.ts +4 -0
- package/dist/prompts/register.d.ts.map +1 -0
- package/dist/prompts/register.js +4 -0
- package/dist/prompts/register.js.map +1 -0
- package/dist/registry.d.ts +14 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +2 -0
- package/dist/registry.js.map +1 -0
- package/dist/resources/knowledge-resources.d.ts +5 -0
- package/dist/resources/knowledge-resources.d.ts.map +1 -0
- package/dist/resources/knowledge-resources.js +121 -0
- package/dist/resources/knowledge-resources.js.map +1 -0
- package/dist/resources/register.d.ts +4 -0
- package/dist/resources/register.d.ts.map +1 -0
- package/dist/resources/register.js +8 -0
- package/dist/resources/register.js.map +1 -0
- package/dist/server.d.ts +40 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +56 -0
- package/dist/server.js.map +1 -0
- package/dist/telemetry/logger.d.ts +31 -0
- package/dist/telemetry/logger.d.ts.map +1 -0
- package/dist/telemetry/logger.js +67 -0
- package/dist/telemetry/logger.js.map +1 -0
- package/dist/telemetry/redaction.d.ts +88 -0
- package/dist/telemetry/redaction.d.ts.map +1 -0
- package/dist/telemetry/redaction.js +238 -0
- package/dist/telemetry/redaction.js.map +1 -0
- package/dist/tools/build-transaction-parameters.d.ts +10 -0
- package/dist/tools/build-transaction-parameters.d.ts.map +1 -0
- package/dist/tools/build-transaction-parameters.js +154 -0
- package/dist/tools/build-transaction-parameters.js.map +1 -0
- package/dist/tools/call-context.d.ts +47 -0
- package/dist/tools/call-context.d.ts.map +1 -0
- package/dist/tools/call-context.js +15 -0
- package/dist/tools/call-context.js.map +1 -0
- package/dist/tools/codegen-tools.d.ts +4 -0
- package/dist/tools/codegen-tools.d.ts.map +1 -0
- package/dist/tools/codegen-tools.js +131 -0
- package/dist/tools/codegen-tools.js.map +1 -0
- package/dist/tools/credential-tools.d.ts +32 -0
- package/dist/tools/credential-tools.d.ts.map +1 -0
- package/dist/tools/credential-tools.js +65 -0
- package/dist/tools/credential-tools.js.map +1 -0
- package/dist/tools/descriptor.d.ts +52 -0
- package/dist/tools/descriptor.d.ts.map +1 -0
- package/dist/tools/descriptor.js +2 -0
- package/dist/tools/descriptor.js.map +1 -0
- package/dist/tools/diagnostic-tools.d.ts +3 -0
- package/dist/tools/diagnostic-tools.d.ts.map +1 -0
- package/dist/tools/diagnostic-tools.js +128 -0
- package/dist/tools/diagnostic-tools.js.map +1 -0
- package/dist/tools/gateway-tools.d.ts +6 -0
- package/dist/tools/gateway-tools.d.ts.map +1 -0
- package/dist/tools/gateway-tools.js +381 -0
- package/dist/tools/gateway-tools.js.map +1 -0
- package/dist/tools/hash-tools.d.ts +5 -0
- package/dist/tools/hash-tools.d.ts.map +1 -0
- package/dist/tools/hash-tools.js +238 -0
- package/dist/tools/hash-tools.js.map +1 -0
- package/dist/tools/knowledge-tools.d.ts +7 -0
- package/dist/tools/knowledge-tools.d.ts.map +1 -0
- package/dist/tools/knowledge-tools.js +247 -0
- package/dist/tools/knowledge-tools.js.map +1 -0
- package/dist/tools/onboarding-checklist.d.ts +3 -0
- package/dist/tools/onboarding-checklist.d.ts.map +1 -0
- package/dist/tools/onboarding-checklist.js +141 -0
- package/dist/tools/onboarding-checklist.js.map +1 -0
- package/dist/tools/register.d.ts +60 -0
- package/dist/tools/register.d.ts.map +1 -0
- package/dist/tools/register.js +145 -0
- package/dist/tools/register.js.map +1 -0
- package/dist/tools/tool-kit.d.ts +88 -0
- package/dist/tools/tool-kit.d.ts.map +1 -0
- package/dist/tools/tool-kit.js +297 -0
- package/dist/tools/tool-kit.js.map +1 -0
- package/dist/validation/payload-validator.d.ts +29 -0
- package/dist/validation/payload-validator.d.ts.map +1 -0
- package/dist/validation/payload-validator.js +79 -0
- package/dist/validation/payload-validator.js.map +1 -0
- package/dist/validation/rules/amount.d.ts +3 -0
- package/dist/validation/rules/amount.d.ts.map +1 -0
- package/dist/validation/rules/amount.js +77 -0
- package/dist/validation/rules/amount.js.map +1 -0
- package/dist/validation/rules/bill-reference.d.ts +10 -0
- package/dist/validation/rules/bill-reference.d.ts.map +1 -0
- package/dist/validation/rules/bill-reference.js +34 -0
- package/dist/validation/rules/bill-reference.js.map +1 -0
- package/dist/validation/rules/cnic.d.ts +3 -0
- package/dist/validation/rules/cnic.d.ts.map +1 -0
- package/dist/validation/rules/cnic.js +42 -0
- package/dist/validation/rules/cnic.js.map +1 -0
- package/dist/validation/rules/constants.d.ts +9 -0
- package/dist/validation/rules/constants.d.ts.map +1 -0
- package/dist/validation/rules/constants.js +28 -0
- package/dist/validation/rules/constants.js.map +1 -0
- package/dist/validation/rules/constraints.d.ts +3 -0
- package/dist/validation/rules/constraints.d.ts.map +1 -0
- package/dist/validation/rules/constraints.js +113 -0
- package/dist/validation/rules/constraints.js.map +1 -0
- package/dist/validation/rules/datetime.d.ts +3 -0
- package/dist/validation/rules/datetime.d.ts.map +1 -0
- package/dist/validation/rules/datetime.js +49 -0
- package/dist/validation/rules/datetime.js.map +1 -0
- package/dist/validation/rules/empty-string.d.ts +13 -0
- package/dist/validation/rules/empty-string.d.ts.map +1 -0
- package/dist/validation/rules/empty-string.js +32 -0
- package/dist/validation/rules/empty-string.js.map +1 -0
- package/dist/validation/rules/expiry.d.ts +3 -0
- package/dist/validation/rules/expiry.d.ts.map +1 -0
- package/dist/validation/rules/expiry.js +52 -0
- package/dist/validation/rules/expiry.js.map +1 -0
- package/dist/validation/rules/msisdn.d.ts +3 -0
- package/dist/validation/rules/msisdn.d.ts.map +1 -0
- package/dist/validation/rules/msisdn.js +45 -0
- package/dist/validation/rules/msisdn.js.map +1 -0
- package/dist/validation/rules/presence.d.ts +3 -0
- package/dist/validation/rules/presence.d.ts.map +1 -0
- package/dist/validation/rules/presence.js +90 -0
- package/dist/validation/rules/presence.js.map +1 -0
- package/dist/validation/rules/return-url.d.ts +14 -0
- package/dist/validation/rules/return-url.d.ts.map +1 -0
- package/dist/validation/rules/return-url.js +58 -0
- package/dist/validation/rules/return-url.js.map +1 -0
- package/dist/validation/rules/tool-argument.d.ts +16 -0
- package/dist/validation/rules/tool-argument.d.ts.map +1 -0
- package/dist/validation/rules/tool-argument.js +38 -0
- package/dist/validation/rules/tool-argument.js.map +1 -0
- package/dist/validation/rules/txn-ref.d.ts +3 -0
- package/dist/validation/rules/txn-ref.d.ts.map +1 -0
- package/dist/validation/rules/txn-ref.js +47 -0
- package/dist/validation/rules/txn-ref.js.map +1 -0
- package/dist/validation/rules/unknown-fields.d.ts +10 -0
- package/dist/validation/rules/unknown-fields.d.ts.map +1 -0
- package/dist/validation/rules/unknown-fields.js +29 -0
- package/dist/validation/rules/unknown-fields.js.map +1 -0
- package/dist/validation/types.d.ts +36 -0
- package/dist/validation/types.d.ts.map +1 -0
- package/dist/validation/types.js +12 -0
- package/dist/validation/types.js.map +1 -0
- package/dist/version.d.ts +23 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +23 -0
- package/dist/version.js.map +1 -0
- package/knowledge/diagnostics/symptoms.json +321 -0
- package/knowledge/docs/bnpl-guide.md +77 -0
- package/knowledge/docs/card-page-redirection.md +67 -0
- package/knowledge/docs/card-refund.md +91 -0
- package/knowledge/docs/hmac-sha256-calculation.md +72 -0
- package/knowledge/docs/index.json +189 -0
- package/knowledge/docs/ipn-implementation.md +89 -0
- package/knowledge/docs/mwallet-recurring.md +117 -0
- package/knowledge/docs/mwallet-refund.md +65 -0
- package/knowledge/docs/mwallet-rest-v1-1.md +126 -0
- package/knowledge/docs/mwallet-rest-v2-0-cnic.md +113 -0
- package/knowledge/docs/portal-guide.md +45 -0
- package/knowledge/docs/shopify-guide.md +74 -0
- package/knowledge/docs/status-inquiry-2025-26.md +32 -0
- package/knowledge/docs/status-inquiry.md +111 -0
- package/knowledge/docs/wordpress-guide.md +49 -0
- package/knowledge/flow.schema.json +390 -0
- package/knowledge/flows/bnpl-page-redirection-v1.1.json +211 -0
- package/knowledge/flows/card-page-redirection-v1.1.json +211 -0
- package/knowledge/flows/mwallet-rest-v1.1.json +187 -0
- package/knowledge/flows/mwallet-rest-v2.0-cnic.json +197 -0
- package/knowledge/flows/mwallet-token-pay-v4.0.json +130 -0
- package/knowledge/flows/refund-bnpl.json +105 -0
- package/knowledge/flows/refund-card-v1.1.json +112 -0
- package/knowledge/flows/refund-card-v2.0.json +121 -0
- package/knowledge/flows/refund-mwallet-v1.1.json +106 -0
- package/knowledge/flows/status-inquiry-v1.1.json +102 -0
- package/knowledge/flows/status-inquiry-v2.0.json +103 -0
- package/knowledge/flows/token-delete-v1.json +85 -0
- package/knowledge/flows/token-inquiry-v1.json +85 -0
- package/knowledge/flows/wallet-linking-portal.json +103 -0
- package/knowledge/templates/node/checkout-redirect.tmpl +166 -0
- package/knowledge/templates/node/ipn-handler.tmpl +108 -0
- package/knowledge/templates/node/rest-client.tmpl +197 -0
- package/knowledge/templates/node/wallet-linking.tmpl +147 -0
- package/knowledge/templates/php/checkout-redirect.tmpl +149 -0
- package/knowledge/templates/php/ipn-handler.tmpl +105 -0
- package/knowledge/templates/php/rest-client.tmpl +191 -0
- package/knowledge/templates/php/wallet-linking.tmpl +137 -0
- package/knowledge/templates/python/checkout-redirect.tmpl +148 -0
- package/knowledge/templates/python/ipn-handler.tmpl +110 -0
- package/knowledge/templates/python/rest-client.tmpl +179 -0
- package/knowledge/templates/python/wallet-linking.tmpl +133 -0
- package/package.json +84 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""JazzCash — {{FLOW_TITLE}}
|
|
2
|
+
|
|
3
|
+
Generated by the JazzCash MCP server from {{FLOW_ID}} ({{CORPUS_VERSION}} guides).
|
|
4
|
+
Sources: {{DOC_SOURCES}}
|
|
5
|
+
|
|
6
|
+
Builds a signed, auto-submitting form that redirects the customer to the
|
|
7
|
+
JazzCash hosted payment page.
|
|
8
|
+
|
|
9
|
+
SAFETY NOTES
|
|
10
|
+
- Sandbox and production use the SAME URL. The environment is decided by the
|
|
11
|
+
credentials alone.
|
|
12
|
+
- The Integrity Salt is the HMAC key and must never reach the browser. This
|
|
13
|
+
module runs server-side and emits only the finished hash.
|
|
14
|
+
- pp_ReturnURL must be byte-identical to the URL registered in the portal.
|
|
15
|
+
|
|
16
|
+
Requires Python 3.9 or later (zoneinfo).
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import hashlib
|
|
22
|
+
import hmac
|
|
23
|
+
import html
|
|
24
|
+
import os
|
|
25
|
+
import secrets
|
|
26
|
+
import string
|
|
27
|
+
from datetime import datetime, timedelta
|
|
28
|
+
from decimal import Decimal, ROUND_DOWN
|
|
29
|
+
from typing import Any, Mapping
|
|
30
|
+
from zoneinfo import ZoneInfo
|
|
31
|
+
|
|
32
|
+
JAZZCASH_POST_URL = '{{ENDPOINT}}'
|
|
33
|
+
PAKISTAN = ZoneInfo('Asia/Karachi')
|
|
34
|
+
|
|
35
|
+
# pp_TxnRefNo is annotated [20AN] in the guides: at most 20 letters and digits.
|
|
36
|
+
MAX_REFERENCE_LENGTH = 20
|
|
37
|
+
REFERENCE_ALPHABET = string.ascii_letters + string.digits
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def txn_date_time(moment: datetime | None = None) -> str:
|
|
41
|
+
return (moment or datetime.now(PAKISTAN)).astimezone(PAKISTAN).strftime('%Y%m%d%H%M%S')
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def generate_reference(prefix: str, moment: datetime | None = None) -> str:
|
|
45
|
+
"""Reference that stays unique inside a single millisecond.
|
|
46
|
+
|
|
47
|
+
The clock alone is not enough: two orders placed in the same millisecond
|
|
48
|
+
produce the same reference, and the gateway rejects the second as a
|
|
49
|
+
duplicate. The random suffix fills whatever the prefix and the timestamp
|
|
50
|
+
leave of the 20-character budget, drawn from `secrets`, not `random`.
|
|
51
|
+
"""
|
|
52
|
+
stamp = txn_date_time(moment)
|
|
53
|
+
room = MAX_REFERENCE_LENGTH - len(prefix) - len(stamp)
|
|
54
|
+
|
|
55
|
+
if room < 1:
|
|
56
|
+
raise ValueError(
|
|
57
|
+
f'Reference prefix {prefix!r} leaves no room inside the {MAX_REFERENCE_LENGTH}-character limit.'
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
suffix = ''.join(secrets.choice(REFERENCE_ALPHABET) for _ in range(room))
|
|
61
|
+
|
|
62
|
+
return f'{prefix}{stamp}{suffix}'
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def to_minor_units(major_amount: str) -> str:
|
|
66
|
+
"""Convert rupees to minor units. Decimal, never float."""
|
|
67
|
+
value = Decimal(str(major_amount))
|
|
68
|
+
if value <= 0:
|
|
69
|
+
raise ValueError('Amount must be greater than zero.')
|
|
70
|
+
if value != value.quantize(Decimal('0.01'), rounding=ROUND_DOWN):
|
|
71
|
+
raise ValueError('Amount must have at most two decimal places.')
|
|
72
|
+
return str(int(value * 100))
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def secure_hash(parameters: Mapping[str, Any], integrity_salt: str) -> str:
|
|
76
|
+
values = [
|
|
77
|
+
str(parameters[name])
|
|
78
|
+
for name in sorted(parameters)
|
|
79
|
+
if name != 'pp_SecureHash' and parameters[name] is not None and str(parameters[name]) != ''
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
message = integrity_salt if not values else integrity_salt + '&' + '&'.join(values)
|
|
83
|
+
|
|
84
|
+
return hmac.new(integrity_salt.encode('utf-8'), message.encode('utf-8'), hashlib.sha256).hexdigest()
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def build_checkout_parameters(amount_major: str, bill_reference: str, description: str) -> dict[str, str]:
|
|
88
|
+
"""Build the signed parameter set for one order."""
|
|
89
|
+
merchant_id = os.environ.get('JAZZCASH_MERCHANT_ID', '')
|
|
90
|
+
password = os.environ.get('JAZZCASH_PASSWORD', '')
|
|
91
|
+
integrity_salt = os.environ.get('JAZZCASH_INTEGRITY_SALT', '')
|
|
92
|
+
return_url = os.environ.get('JAZZCASH_RETURN_URL', '')
|
|
93
|
+
|
|
94
|
+
if not merchant_id or not password or not integrity_salt or not return_url:
|
|
95
|
+
raise RuntimeError('JazzCash credentials are not configured.')
|
|
96
|
+
|
|
97
|
+
now = datetime.now(PAKISTAN)
|
|
98
|
+
|
|
99
|
+
parameters: dict[str, str] = {
|
|
100
|
+
{{PARAMS}}
|
|
101
|
+
}
|
|
102
|
+
parameters['pp_MerchantID'] = merchant_id
|
|
103
|
+
parameters['pp_Password'] = password
|
|
104
|
+
parameters['pp_ReturnURL'] = return_url
|
|
105
|
+
# Unique per transaction. Replace TRN with your own three-letter merchant
|
|
106
|
+
# prefix, as the guides recommend.
|
|
107
|
+
parameters['pp_TxnRefNo'] = generate_reference('TRN', now)
|
|
108
|
+
parameters['pp_Amount'] = to_minor_units(amount_major)
|
|
109
|
+
parameters['pp_BillReference'] = bill_reference
|
|
110
|
+
parameters['pp_Description'] = description
|
|
111
|
+
parameters['pp_TxnDateTime'] = txn_date_time(now)
|
|
112
|
+
parameters['pp_TxnExpiryDateTime'] = txn_date_time(now + timedelta(days=1))
|
|
113
|
+
|
|
114
|
+
parameters['pp_SecureHash'] = secure_hash(parameters, integrity_salt)
|
|
115
|
+
|
|
116
|
+
return parameters
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def render_checkout_form(parameters: Mapping[str, str]) -> str:
|
|
120
|
+
"""Render an auto-submitting HTML form. Every value is HTML-escaped."""
|
|
121
|
+
inputs = '\n'.join(
|
|
122
|
+
f' <input type="hidden" name="{html.escape(name, quote=True)}" '
|
|
123
|
+
f'value="{html.escape(str(value), quote=True)}">'
|
|
124
|
+
for name, value in parameters.items()
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
'<!DOCTYPE html>\n'
|
|
129
|
+
'<html lang="en">\n'
|
|
130
|
+
'<head><meta charset="UTF-8"><title>Redirecting to JazzCash…</title></head>\n'
|
|
131
|
+
'<body>\n'
|
|
132
|
+
' <p>Redirecting you to the JazzCash secure payment page…</p>\n'
|
|
133
|
+
f' <form id="jazzcash" method="post" action="{html.escape(JAZZCASH_POST_URL, quote=True)}">\n'
|
|
134
|
+
f'{inputs}\n'
|
|
135
|
+
' <noscript><button type="submit">Continue</button></noscript>\n'
|
|
136
|
+
' </form>\n'
|
|
137
|
+
" <script>document.getElementById('jazzcash').submit();</script>\n"
|
|
138
|
+
'</body>\n'
|
|
139
|
+
'</html>'
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
# Example wiring with Flask:
|
|
144
|
+
#
|
|
145
|
+
# @app.post('/checkout')
|
|
146
|
+
# def checkout():
|
|
147
|
+
# parameters = build_checkout_parameters('1.00', 'billref001', 'Order 1024')
|
|
148
|
+
# return render_checkout_form(parameters)
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""JazzCash IPN (Instant Payment Notification) listener — {{FRAMEWORK}}
|
|
2
|
+
|
|
3
|
+
Generated by the JazzCash MCP server ({{CORPUS_VERSION}} guides).
|
|
4
|
+
Sources: {{DOC_SOURCES}}
|
|
5
|
+
|
|
6
|
+
Register this URL under Integration > Credentials in the merchant portal.
|
|
7
|
+
|
|
8
|
+
CONTRACT
|
|
9
|
+
- Respond within 60 seconds. JazzCash retries twice on failure or timeout.
|
|
10
|
+
- Therefore this handler MUST be idempotent: the same notification can
|
|
11
|
+
legitimately arrive three times.
|
|
12
|
+
- This URL is public. Anyone can post to it. VERIFY THE HASH FIRST.
|
|
13
|
+
- In an inbound IPN the payment outcome is in pp_ResponseCode: 121 succeeded,
|
|
14
|
+
199/999 failed. (In a Status Inquiry response it is pp_PaymentResponseCode.)
|
|
15
|
+
|
|
16
|
+
Requires `flask`.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import hashlib
|
|
22
|
+
import hmac
|
|
23
|
+
import logging
|
|
24
|
+
import os
|
|
25
|
+
from typing import Any, Mapping
|
|
26
|
+
|
|
27
|
+
from flask import Flask, jsonify, request
|
|
28
|
+
|
|
29
|
+
app = Flask(__name__)
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
INTEGRITY_SALT = os.environ.get('JAZZCASH_INTEGRITY_SALT', '')
|
|
33
|
+
if not INTEGRITY_SALT:
|
|
34
|
+
raise RuntimeError('JAZZCASH_INTEGRITY_SALT is not configured.')
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def secure_hash(parameters: Mapping[str, Any], integrity_salt: str) -> str:
|
|
38
|
+
values = [
|
|
39
|
+
str(parameters[name])
|
|
40
|
+
for name in sorted(parameters)
|
|
41
|
+
if name not in ('pp_SecureHash', 'SecureHash')
|
|
42
|
+
# The gateway sends JSON null in IPN payloads even though merchants are
|
|
43
|
+
# told never to send null. Treat None as empty when hashing.
|
|
44
|
+
and parameters[name] is not None
|
|
45
|
+
and str(parameters[name]) != ''
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
message = integrity_salt if not values else integrity_salt + '&' + '&'.join(values)
|
|
49
|
+
|
|
50
|
+
return hmac.new(integrity_salt.encode('utf-8'), message.encode('utf-8'), hashlib.sha256).hexdigest()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def acknowledgement(message: str) -> dict[str, str]:
|
|
54
|
+
body = {'pp_ResponseCode': '000', 'pp_ResponseMessage': message}
|
|
55
|
+
body['pp_SecureHash'] = secure_hash(body, INTEGRITY_SALT)
|
|
56
|
+
return body
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@app.post('{{IPN_PATH}}')
|
|
60
|
+
def jazzcash_ipn(): # type: ignore[no-untyped-def]
|
|
61
|
+
notification = request.get_json(silent=True)
|
|
62
|
+
|
|
63
|
+
if not isinstance(notification, dict):
|
|
64
|
+
return jsonify({'pp_ResponseCode': '400', 'pp_ResponseMessage': 'Malformed body'}), 400
|
|
65
|
+
|
|
66
|
+
# 1. Verify the hash BEFORE anything else.
|
|
67
|
+
supplied = str(notification.get('pp_SecureHash', ''))
|
|
68
|
+
recomputed = secure_hash(notification, INTEGRITY_SALT)
|
|
69
|
+
|
|
70
|
+
if not supplied or not hmac.compare_digest(recomputed.lower(), supplied.strip().lower()):
|
|
71
|
+
logger.error('JazzCash IPN rejected: secure hash did not verify.')
|
|
72
|
+
return jsonify({'pp_ResponseCode': '400', 'pp_ResponseMessage': 'Invalid secure hash'}), 400
|
|
73
|
+
|
|
74
|
+
txn_ref_no = str(notification.get('pp_TxnRefNo', ''))
|
|
75
|
+
response_code = str(notification.get('pp_ResponseCode', ''))
|
|
76
|
+
|
|
77
|
+
# 2. Look up the order. Replace with your own persistence layer.
|
|
78
|
+
# order = find_order_by_reference(txn_ref_no)
|
|
79
|
+
# if order is None:
|
|
80
|
+
# return jsonify(acknowledgement('IPN received successfully'))
|
|
81
|
+
|
|
82
|
+
# 3. Idempotency: a retry must not fulfil the order twice.
|
|
83
|
+
# if order.is_paid:
|
|
84
|
+
# return jsonify(acknowledgement('IPN received successfully'))
|
|
85
|
+
|
|
86
|
+
# 4. Confirm the amount matches what you recorded, so a replayed or altered
|
|
87
|
+
# notification cannot mark a large order paid from a small transaction.
|
|
88
|
+
# if str(order.amount_minor_units) != str(notification.get('pp_Amount', '')):
|
|
89
|
+
# logger.error('JazzCash IPN amount mismatch for %s', txn_ref_no)
|
|
90
|
+
# return jsonify(acknowledgement('IPN received successfully'))
|
|
91
|
+
|
|
92
|
+
# 5. Interpret the outcome.
|
|
93
|
+
if response_code == '121':
|
|
94
|
+
# Paid. Mark the order paid, then do slow work asynchronously.
|
|
95
|
+
# mark_order_paid(txn_ref_no, notification)
|
|
96
|
+
pass
|
|
97
|
+
elif response_code in ('199', '999'):
|
|
98
|
+
# Failed. Do not release goods.
|
|
99
|
+
# mark_order_failed(txn_ref_no, notification)
|
|
100
|
+
pass
|
|
101
|
+
else:
|
|
102
|
+
# Undocumented code: unresolved, NOT failed. Reconcile with Status Inquiry.
|
|
103
|
+
logger.error('JazzCash IPN carried undocumented code %s for %s', response_code, txn_ref_no)
|
|
104
|
+
|
|
105
|
+
# 6. Acknowledge well inside the 60-second window.
|
|
106
|
+
return jsonify(acknowledgement('IPN received successfully'))
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
if __name__ == '__main__':
|
|
110
|
+
app.run(port=int(os.environ.get('PORT', '3000')))
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""JazzCash — {{FLOW_TITLE}}
|
|
2
|
+
|
|
3
|
+
Generated by the JazzCash MCP server from {{FLOW_ID}} ({{CORPUS_VERSION}} guides).
|
|
4
|
+
Sources: {{DOC_SOURCES}}
|
|
5
|
+
|
|
6
|
+
SAFETY NOTES
|
|
7
|
+
- Sandbox and production use the SAME URL ({{ENDPOINT}}). The environment is
|
|
8
|
+
decided entirely by the credentials. Confirm which are loaded before running.
|
|
9
|
+
- Moves money: {{MOVES_MONEY}}.
|
|
10
|
+
|
|
11
|
+
Requires Python 3.9 or later (zoneinfo) and `requests`.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import hashlib
|
|
17
|
+
import hmac
|
|
18
|
+
import os
|
|
19
|
+
import secrets
|
|
20
|
+
import string
|
|
21
|
+
from datetime import datetime, timedelta
|
|
22
|
+
from decimal import Decimal, ROUND_DOWN
|
|
23
|
+
from typing import Any, Mapping
|
|
24
|
+
from zoneinfo import ZoneInfo
|
|
25
|
+
|
|
26
|
+
import requests
|
|
27
|
+
|
|
28
|
+
JAZZCASH_ENDPOINT = '{{ENDPOINT}}'
|
|
29
|
+
TIMEOUT_SECONDS = 30
|
|
30
|
+
PAKISTAN = ZoneInfo('Asia/Karachi')
|
|
31
|
+
|
|
32
|
+
# pp_TxnRefNo and pp_RequestID are annotated [20AN] in the guides: at most 20
|
|
33
|
+
# letters and digits.
|
|
34
|
+
MAX_REFERENCE_LENGTH = 20
|
|
35
|
+
REFERENCE_ALPHABET = string.ascii_letters + string.digits
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def txn_date_time(moment: datetime | None = None) -> str:
|
|
39
|
+
"""Format an instant as YYYYMMDDHHMMSS in Pakistan Standard Time.
|
|
40
|
+
|
|
41
|
+
An explicit time zone is used so the stamp is correct regardless of the host
|
|
42
|
+
clock: a server in UTC and one in New York must produce the same value.
|
|
43
|
+
"""
|
|
44
|
+
return (moment or datetime.now(PAKISTAN)).astimezone(PAKISTAN).strftime('%Y%m%d%H%M%S')
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def txn_expiry_date_time(moment: datetime | None = None) -> str:
|
|
48
|
+
"""Expiry stamp, one day after the transaction, as the guides require."""
|
|
49
|
+
return txn_date_time((moment or datetime.now(PAKISTAN)) + timedelta(days=1))
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def to_minor_units(major_amount: str) -> str:
|
|
53
|
+
"""Convert rupees to the minor units the gateway expects.
|
|
54
|
+
|
|
55
|
+
Decimal, never float: binary floating point cannot represent money exactly.
|
|
56
|
+
"""
|
|
57
|
+
value = Decimal(str(major_amount))
|
|
58
|
+
if value <= 0:
|
|
59
|
+
raise ValueError('Amount must be greater than zero.')
|
|
60
|
+
if value != value.quantize(Decimal('0.01'), rounding=ROUND_DOWN):
|
|
61
|
+
raise ValueError('Amount must have at most two decimal places.')
|
|
62
|
+
return str(int(value * 100))
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def generate_reference(prefix: str = 'T', moment: datetime | None = None) -> str:
|
|
66
|
+
"""Reference that stays unique inside a single millisecond.
|
|
67
|
+
|
|
68
|
+
The clock alone is not enough: two orders placed in the same millisecond
|
|
69
|
+
produce the same reference, and the gateway rejects the second as a
|
|
70
|
+
duplicate. The random suffix fills whatever the prefix and the timestamp
|
|
71
|
+
leave of the 20-character budget, drawn from `secrets`, not `random`.
|
|
72
|
+
"""
|
|
73
|
+
stamp = txn_date_time(moment)
|
|
74
|
+
room = MAX_REFERENCE_LENGTH - len(prefix) - len(stamp)
|
|
75
|
+
|
|
76
|
+
if room < 1:
|
|
77
|
+
raise ValueError(
|
|
78
|
+
f'Reference prefix {prefix!r} leaves no room inside the {MAX_REFERENCE_LENGTH}-character limit.'
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
suffix = ''.join(secrets.choice(REFERENCE_ALPHABET) for _ in range(room))
|
|
82
|
+
|
|
83
|
+
return f'{prefix}{stamp}{suffix}'
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def secure_hash(parameters: Mapping[str, Any], integrity_salt: str) -> str:
|
|
87
|
+
"""Build pp_SecureHash.
|
|
88
|
+
|
|
89
|
+
Sort by parameter NAME ascending, join the non-empty VALUES with '&',
|
|
90
|
+
prepend the Integrity Salt, then HMAC-SHA256 with the salt as the key.
|
|
91
|
+
"""
|
|
92
|
+
values = [
|
|
93
|
+
str(parameters[name])
|
|
94
|
+
for name in sorted(parameters)
|
|
95
|
+
if name not in ('pp_SecureHash', 'SecureHash')
|
|
96
|
+
and parameters[name] is not None
|
|
97
|
+
and str(parameters[name]) != ''
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
message = integrity_salt if not values else integrity_salt + '&' + '&'.join(values)
|
|
101
|
+
|
|
102
|
+
return hmac.new(integrity_salt.encode('utf-8'), message.encode('utf-8'), hashlib.sha256).hexdigest()
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def hash_matches(left: str, right: str) -> bool:
|
|
106
|
+
"""Constant-time, case-insensitive comparison.
|
|
107
|
+
|
|
108
|
+
JazzCash returns uppercase hex; hexdigest() produces lowercase. Same digest.
|
|
109
|
+
"""
|
|
110
|
+
return hmac.compare_digest(str(left).strip().lower(), str(right).strip().lower())
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _credentials() -> tuple[str, str, str]:
|
|
114
|
+
merchant_id = os.environ.get('JAZZCASH_MERCHANT_ID', '')
|
|
115
|
+
password = os.environ.get('JAZZCASH_PASSWORD', '')
|
|
116
|
+
integrity_salt = os.environ.get('JAZZCASH_INTEGRITY_SALT', '')
|
|
117
|
+
|
|
118
|
+
if not merchant_id or not password or not integrity_salt:
|
|
119
|
+
raise RuntimeError('JazzCash credentials are not configured.')
|
|
120
|
+
|
|
121
|
+
return merchant_id, password, integrity_salt
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def run(**overrides: str) -> dict[str, Any]:
|
|
125
|
+
"""Execute {{FLOW_ID}}.
|
|
126
|
+
|
|
127
|
+
Every value marked TODO must be supplied by your application, either by
|
|
128
|
+
editing the literal below or by passing it as a keyword argument. pp_Amount
|
|
129
|
+
is in MINOR units: pass `to_minor_units('100.00')` rather than a rupee
|
|
130
|
+
float, so the conversion is exact.
|
|
131
|
+
"""
|
|
132
|
+
merchant_id, password, integrity_salt = _credentials()
|
|
133
|
+
|
|
134
|
+
parameters: dict[str, Any] = {
|
|
135
|
+
{{PARAMS}}
|
|
136
|
+
}
|
|
137
|
+
parameters.update(overrides)
|
|
138
|
+
parameters['pp_MerchantID'] = merchant_id
|
|
139
|
+
parameters['pp_Password'] = password
|
|
140
|
+
{{RUNTIME_OVERRIDES}}
|
|
141
|
+
|
|
142
|
+
# Hash LAST, from exactly the mapping about to be transmitted.
|
|
143
|
+
parameters['pp_SecureHash'] = secure_hash(parameters, integrity_salt)
|
|
144
|
+
|
|
145
|
+
# Never log `parameters`: it carries the password.
|
|
146
|
+
response = requests.post(
|
|
147
|
+
JAZZCASH_ENDPOINT,
|
|
148
|
+
json=parameters,
|
|
149
|
+
headers={'Content-Type': 'application/json', 'Accept': 'application/json'},
|
|
150
|
+
timeout=TIMEOUT_SECONDS,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
try:
|
|
154
|
+
result: dict[str, Any] = response.json()
|
|
155
|
+
except ValueError as error:
|
|
156
|
+
raise RuntimeError(f'JazzCash returned a non-JSON body with HTTP {response.status_code}.') from error
|
|
157
|
+
|
|
158
|
+
if result.get('{{SUCCESS_FIELD}}') == '{{SUCCESS_VALUE}}':
|
|
159
|
+
{{SETTLEMENT_CHECK}} # For Status Inquiry this only means the enquiry worked. Read
|
|
160
|
+
# pp_PaymentResponseCode for the payment outcome: 121 means completed.
|
|
161
|
+
{{SUCCESS_REPORT}}
|
|
162
|
+
else:
|
|
163
|
+
# An unrecognised code is UNRESOLVED, not failed. Resolve it with the
|
|
164
|
+
# Status Inquiry API before refunding or re-charging.
|
|
165
|
+
print('JazzCash call unresolved. Code:', result.get('{{SUCCESS_FIELD}}'))
|
|
166
|
+
|
|
167
|
+
response_hash = result.get('{{RESPONSE_HASH_FIELD}}')
|
|
168
|
+
if isinstance(response_hash, str) and response_hash:
|
|
169
|
+
if not hash_matches(secure_hash(result, integrity_salt), response_hash):
|
|
170
|
+
# JazzCash does not document which response fields feed this hash, so
|
|
171
|
+
# a mismatch means UNVERIFIED, not proven tampering. Reconcile through
|
|
172
|
+
# Status Inquiry rather than rejecting the payment.
|
|
173
|
+
print('JazzCash response hash could not be verified for', result.get('pp_TxnRefNo'))
|
|
174
|
+
|
|
175
|
+
return result
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
if __name__ == '__main__':
|
|
179
|
+
run()
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""JazzCash — MWallet linking form
|
|
2
|
+
|
|
3
|
+
Generated by the JazzCash MCP server from {{FLOW_ID}} ({{CORPUS_VERSION}} guides).
|
|
4
|
+
Sources: {{DOC_SOURCES}}
|
|
5
|
+
|
|
6
|
+
- Sandbox and production use the SAME URL. The environment is decided by the
|
|
7
|
+
credentials alone: going live is a credential swap, not a URL change.
|
|
8
|
+
|
|
9
|
+
TWO CORRECTIONS TO THE OFFICIAL SAMPLE, both deliberate:
|
|
10
|
+
|
|
11
|
+
1. The sample in the guide computes the hash in browser JavaScript with the
|
|
12
|
+
Integrity Salt embedded in the page. The salt is the HMAC key: publishing it
|
|
13
|
+
lets anyone forge a valid pp_SecureHash for your merchant account. This
|
|
14
|
+
module runs server-side and emits only the finished hash.
|
|
15
|
+
|
|
16
|
+
2. The sample posts to https://pgw.jazzcash.com.pk/..., contradicting the
|
|
17
|
+
endpoint table and screenshot in the same document. This module uses the
|
|
18
|
+
endpoint from the table: {{ENDPOINT}}
|
|
19
|
+
|
|
20
|
+
Requires Python 3.9 or later (zoneinfo).
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import hashlib
|
|
26
|
+
import hmac
|
|
27
|
+
import html
|
|
28
|
+
import os
|
|
29
|
+
import re
|
|
30
|
+
import secrets
|
|
31
|
+
import string
|
|
32
|
+
from datetime import datetime
|
|
33
|
+
from typing import Any, Mapping
|
|
34
|
+
from zoneinfo import ZoneInfo
|
|
35
|
+
|
|
36
|
+
JAZZCASH_LINKING_URL = '{{ENDPOINT}}'
|
|
37
|
+
PAKISTAN = ZoneInfo('Asia/Karachi')
|
|
38
|
+
MSISDN_PATTERN = re.compile(r'^03\d{9}$')
|
|
39
|
+
|
|
40
|
+
# pp_RequestID is annotated [20AN] in the guides: at most 20 letters and digits.
|
|
41
|
+
MAX_REFERENCE_LENGTH = 20
|
|
42
|
+
REFERENCE_ALPHABET = string.ascii_letters + string.digits
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def generate_reference(prefix: str, moment: datetime | None = None) -> str:
|
|
46
|
+
"""Reference that stays unique inside a single millisecond.
|
|
47
|
+
|
|
48
|
+
The clock alone is not enough: two linking attempts started in the same
|
|
49
|
+
millisecond produce the same pp_RequestID, and the gateway rejects the
|
|
50
|
+
second as a duplicate. Drawn from `secrets`, not `random`.
|
|
51
|
+
"""
|
|
52
|
+
stamp = (moment or datetime.now(PAKISTAN)).astimezone(PAKISTAN).strftime('%Y%m%d%H%M%S')
|
|
53
|
+
room = MAX_REFERENCE_LENGTH - len(prefix) - len(stamp)
|
|
54
|
+
|
|
55
|
+
if room < 1:
|
|
56
|
+
raise ValueError(
|
|
57
|
+
f'Reference prefix {prefix!r} leaves no room inside the {MAX_REFERENCE_LENGTH}-character limit.'
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
suffix = ''.join(secrets.choice(REFERENCE_ALPHABET) for _ in range(room))
|
|
61
|
+
|
|
62
|
+
return f'{prefix}{stamp}{suffix}'
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def secure_hash(parameters: Mapping[str, Any], integrity_salt: str) -> str:
|
|
66
|
+
values = [
|
|
67
|
+
str(parameters[name])
|
|
68
|
+
for name in sorted(parameters)
|
|
69
|
+
if name != 'pp_SecureHash' and parameters[name] is not None and str(parameters[name]) != ''
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
message = integrity_salt if not values else integrity_salt + '&' + '&'.join(values)
|
|
73
|
+
|
|
74
|
+
return hmac.new(integrity_salt.encode('utf-8'), message.encode('utf-8'), hashlib.sha256).hexdigest()
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def build_linking_parameters(msisdn: str) -> dict[str, str]:
|
|
78
|
+
"""Build the signed linking parameters for one customer wallet number."""
|
|
79
|
+
if not MSISDN_PATTERN.match(str(msisdn)):
|
|
80
|
+
raise ValueError('A Pakistani mobile wallet number in the form 03XXXXXXXXX is required.')
|
|
81
|
+
|
|
82
|
+
merchant_id = os.environ.get('JAZZCASH_MERCHANT_ID', '')
|
|
83
|
+
password = os.environ.get('JAZZCASH_PASSWORD', '')
|
|
84
|
+
integrity_salt = os.environ.get('JAZZCASH_INTEGRITY_SALT', '')
|
|
85
|
+
return_url = os.environ.get('JAZZCASH_RETURN_URL', '')
|
|
86
|
+
|
|
87
|
+
if not merchant_id or not password or not integrity_salt or not return_url:
|
|
88
|
+
raise RuntimeError('JazzCash credentials are not configured.')
|
|
89
|
+
|
|
90
|
+
now = datetime.now(PAKISTAN)
|
|
91
|
+
|
|
92
|
+
parameters: dict[str, str] = {
|
|
93
|
+
{{PARAMS}}
|
|
94
|
+
}
|
|
95
|
+
parameters['pp_MerchantID'] = merchant_id
|
|
96
|
+
parameters['pp_Password'] = password
|
|
97
|
+
parameters['pp_MSISDN'] = str(msisdn)
|
|
98
|
+
# Must be unique for every linking attempt.
|
|
99
|
+
parameters['pp_RequestID'] = generate_reference('R', now)
|
|
100
|
+
parameters['pp_ReturnURL'] = return_url
|
|
101
|
+
|
|
102
|
+
parameters['pp_SecureHash'] = secure_hash(parameters, integrity_salt)
|
|
103
|
+
|
|
104
|
+
return parameters
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def render_linking_form(parameters: Mapping[str, str]) -> str:
|
|
108
|
+
"""Render an auto-submitting linking form. Every value is HTML-escaped."""
|
|
109
|
+
inputs = '\n'.join(
|
|
110
|
+
f' <input type="hidden" name="{html.escape(name, quote=True)}" '
|
|
111
|
+
f'value="{html.escape(str(value), quote=True)}">'
|
|
112
|
+
for name, value in parameters.items()
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
'<!DOCTYPE html>\n'
|
|
117
|
+
'<html lang="en">\n'
|
|
118
|
+
'<head><meta charset="UTF-8"><title>Link your JazzCash wallet</title></head>\n'
|
|
119
|
+
'<body>\n'
|
|
120
|
+
' <p>You will be asked for your MPIN on the JazzCash page.</p>\n'
|
|
121
|
+
f' <form id="link-wallet" method="post" action="{html.escape(JAZZCASH_LINKING_URL, quote=True)}">\n'
|
|
122
|
+
f'{inputs}\n'
|
|
123
|
+
' <noscript><button type="submit">Continue</button></noscript>\n'
|
|
124
|
+
' </form>\n'
|
|
125
|
+
" <script>document.getElementById('link-wallet').submit();</script>\n"
|
|
126
|
+
'</body>\n'
|
|
127
|
+
'</html>'
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# On return, JazzCash posts a payment token to your Return URL. Store it against
|
|
132
|
+
# the customer and charge it later with the token payment API. Call the Token
|
|
133
|
+
# Inquiry API before charging: the customer may have unlinked their wallet.
|
package/package.json
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@atbas/m-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"packageManager": "npm@12.0.2",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"description": "MCP server that performs JazzCash payment-gateway integrations inside a merchant's own AI tooling.",
|
|
7
|
+
"author": "Eren Atbas <eren.atbas@gmail.com>",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/eatbas/m-mcp.git"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://m-mcp.atbas.xyz/",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/eatbas/m-mcp/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"jazzcash",
|
|
18
|
+
"mcp",
|
|
19
|
+
"model-context-protocol",
|
|
20
|
+
"payments",
|
|
21
|
+
"pakistan",
|
|
22
|
+
"payment-gateway",
|
|
23
|
+
"woocommerce",
|
|
24
|
+
"integration"
|
|
25
|
+
],
|
|
26
|
+
"type": "module",
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=24.18.0"
|
|
29
|
+
},
|
|
30
|
+
"bin": {
|
|
31
|
+
"m-mcp": "dist/index.js"
|
|
32
|
+
},
|
|
33
|
+
"main": "dist/index.js",
|
|
34
|
+
"files": [
|
|
35
|
+
"dist",
|
|
36
|
+
"knowledge",
|
|
37
|
+
"README.md"
|
|
38
|
+
],
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "npm run clean && tsc -p tsconfig.build.json",
|
|
41
|
+
"clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
|
|
42
|
+
"prepublishOnly": "npm run release:check",
|
|
43
|
+
"pack:check": "npm run build && npm pack --dry-run",
|
|
44
|
+
"release:check": "npm run scan:credentials && node scripts/require-php.mjs && node scripts/require-python.mjs && npm run typecheck && npm run lint && npm run format:check && npm run test:coverage && npm run pack:check",
|
|
45
|
+
"deploy": "npm run build && node dist/install/cli.js",
|
|
46
|
+
"deploy:list": "npm run build && node dist/install/cli.js --list",
|
|
47
|
+
"dev": "tsx src/index.ts",
|
|
48
|
+
"lint": "eslint .",
|
|
49
|
+
"lint:fix": "eslint . --fix",
|
|
50
|
+
"format": "prettier --write .",
|
|
51
|
+
"format:check": "prettier --check .",
|
|
52
|
+
"typecheck": "tsc --noEmit",
|
|
53
|
+
"test": "vitest run",
|
|
54
|
+
"test:fast": "vitest run tests/domain tests/validation tests/catalogue tests/config tests/telemetry",
|
|
55
|
+
"test:changed": "vitest related --run",
|
|
56
|
+
"test:coverage": "vitest run --coverage",
|
|
57
|
+
"extract-pdf-text": "node scripts/extract-pdf-text.mjs",
|
|
58
|
+
"scan:credentials": "node scripts/scan-credentials.mjs"
|
|
59
|
+
},
|
|
60
|
+
"dependencies": {
|
|
61
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
62
|
+
"zod": "^4.4.3"
|
|
63
|
+
},
|
|
64
|
+
"devDependencies": {
|
|
65
|
+
"@eslint/js": "^10.0.1",
|
|
66
|
+
"@types/node": "^24.13.3",
|
|
67
|
+
"@vitest/coverage-v8": "^4.1.10",
|
|
68
|
+
"eslint": "^10.8.0",
|
|
69
|
+
"eslint-config-prettier": "^10.0.1",
|
|
70
|
+
"globals": "^17.8.0",
|
|
71
|
+
"prettier": "^3.4.2",
|
|
72
|
+
"tsx": "^4.19.2",
|
|
73
|
+
"typescript": "^6.0.3",
|
|
74
|
+
"typescript-eslint": "^8.23.0",
|
|
75
|
+
"vitest": "^4.1.10"
|
|
76
|
+
},
|
|
77
|
+
"publishConfig": {
|
|
78
|
+
"access": "public"
|
|
79
|
+
},
|
|
80
|
+
"allowScripts": {
|
|
81
|
+
"esbuild@0.28.2": true,
|
|
82
|
+
"fsevents": false
|
|
83
|
+
}
|
|
84
|
+
}
|