From 22fb060cb9cff57655737d822b297198ae9e2d38 Mon Sep 17 00:00:00 2001 From: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com> Date: Fri, 2 May 2025 19:04:21 +0530 Subject: [PATCH] phonepe --- firebase.json | 6 +- functions/package-lock.json | 246 +++++++++--------- functions/package.json | 2 +- functions/src/index.ts | 2 +- functions/src/payments/index.ts | 1 + functions/src/payments/phonepe/checkStatus.ts | 139 ++++++++++ .../payments/phonepe/createPhonepeOrder.ts | 133 ++++++++++ functions/src/payments/phonepe/index.ts | 5 + functions/src/payments/phonepe/paymentLink.ts | 183 +++++++++++++ .../src/payments/phonepe/paymentLinkStatus.ts | 122 +++++++++ functions/src/payments/phonepe/webhook.ts | 102 ++++++++ functions/tsconfig.json | 4 +- package.json | 3 - 13 files changed, 816 insertions(+), 132 deletions(-) create mode 100644 functions/src/payments/phonepe/checkStatus.ts create mode 100644 functions/src/payments/phonepe/createPhonepeOrder.ts create mode 100644 functions/src/payments/phonepe/index.ts create mode 100644 functions/src/payments/phonepe/paymentLink.ts create mode 100644 functions/src/payments/phonepe/paymentLinkStatus.ts create mode 100644 functions/src/payments/phonepe/webhook.ts diff --git a/firebase.json b/firebase.json index dd93bf2..0eb205c 100644 --- a/firebase.json +++ b/firebase.json @@ -22,17 +22,17 @@ }, "emulators": { "functions": { - "port": 5001 + "port": 5005 }, "firestore": { - "port": 8079 + "port": 8081 }, "storage": { "port": 9199 }, "ui": { "enabled": true, - "port": 4000 + "port": 4008 }, "auth": { "port": 9099 diff --git a/functions/package-lock.json b/functions/package-lock.json index d37a055..28053b5 100644 --- a/functions/package-lock.json +++ b/functions/package-lock.json @@ -8,7 +8,7 @@ "name": "functions", "version": "0.0.0", "dependencies": { - "@aws-sdk/client-ses": "^3.796.0", + "@aws-sdk/client-ses": "^3.798.0", "@types/node-fetch": "^2.6.12", "aws-sdk": "^2.1692.0", "axios": "^1.8.4", @@ -165,44 +165,44 @@ } }, "node_modules/@aws-sdk/client-ses": { - "version": "3.796.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-ses/-/client-ses-3.796.0.tgz", - "integrity": "sha512-gg1z0uXtm6v3wCMrGM5eCh69aWT+VOzYvNdbSMA5EPLfW9qsOCH1Zz/q9XzUZjWN25OnGvfm1LzypzQgRypSeQ==", + "version": "3.798.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-ses/-/client-ses-3.798.0.tgz", + "integrity": "sha512-s12ntssTgau7ySHoOXVAliVM1a1JyjsFYwauKh/7gCriJ+lIBlo25BFgCjZAHxxBn2FfCete6BVQFEbN/Ishkg==", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.796.0", - "@aws-sdk/credential-provider-node": "3.796.0", + "@aws-sdk/core": "3.798.0", + "@aws-sdk/credential-provider-node": "3.798.0", "@aws-sdk/middleware-host-header": "3.775.0", "@aws-sdk/middleware-logger": "3.775.0", "@aws-sdk/middleware-recursion-detection": "3.775.0", - "@aws-sdk/middleware-user-agent": "3.796.0", + "@aws-sdk/middleware-user-agent": "3.798.0", "@aws-sdk/region-config-resolver": "3.775.0", "@aws-sdk/types": "3.775.0", "@aws-sdk/util-endpoints": "3.787.0", "@aws-sdk/util-user-agent-browser": "3.775.0", - "@aws-sdk/util-user-agent-node": "3.796.0", + "@aws-sdk/util-user-agent-node": "3.798.0", "@smithy/config-resolver": "^4.1.0", - "@smithy/core": "^3.2.0", + "@smithy/core": "^3.3.0", "@smithy/fetch-http-handler": "^5.0.2", "@smithy/hash-node": "^4.0.2", "@smithy/invalid-dependency": "^4.0.2", "@smithy/middleware-content-length": "^4.0.2", - "@smithy/middleware-endpoint": "^4.1.0", - "@smithy/middleware-retry": "^4.1.0", + "@smithy/middleware-endpoint": "^4.1.1", + "@smithy/middleware-retry": "^4.1.1", "@smithy/middleware-serde": "^4.0.3", "@smithy/middleware-stack": "^4.0.2", "@smithy/node-config-provider": "^4.0.2", "@smithy/node-http-handler": "^4.0.4", "@smithy/protocol-http": "^5.1.0", - "@smithy/smithy-client": "^4.2.0", + "@smithy/smithy-client": "^4.2.1", "@smithy/types": "^4.2.0", "@smithy/url-parser": "^4.0.2", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-body-length-node": "^4.0.0", - "@smithy/util-defaults-mode-browser": "^4.0.8", - "@smithy/util-defaults-mode-node": "^4.0.8", + "@smithy/util-defaults-mode-browser": "^4.0.9", + "@smithy/util-defaults-mode-node": "^4.0.9", "@smithy/util-endpoints": "^3.0.2", "@smithy/util-middleware": "^4.0.2", "@smithy/util-retry": "^4.0.2", @@ -215,43 +215,43 @@ } }, "node_modules/@aws-sdk/client-sso": { - "version": "3.796.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.796.0.tgz", - "integrity": "sha512-EJExg8mbwqP0VG+RNFV4ZPuUo7QsDsUfTnuFQY51V8iXrbOdV+PDLRr4psXj2fxvrLxc9AlGUMNqd/j4VZtQzA==", + "version": "3.798.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.798.0.tgz", + "integrity": "sha512-Si4W7kFflNXC48lr05n2Fc5nrD6whbfgR7c5/7hYSXP52DOqy2kMle+bZx5EkmQ/e/5nAPW0DS4ABeLprVSghw==", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.796.0", + "@aws-sdk/core": "3.798.0", "@aws-sdk/middleware-host-header": "3.775.0", "@aws-sdk/middleware-logger": "3.775.0", "@aws-sdk/middleware-recursion-detection": "3.775.0", - "@aws-sdk/middleware-user-agent": "3.796.0", + "@aws-sdk/middleware-user-agent": "3.798.0", "@aws-sdk/region-config-resolver": "3.775.0", "@aws-sdk/types": "3.775.0", "@aws-sdk/util-endpoints": "3.787.0", "@aws-sdk/util-user-agent-browser": "3.775.0", - "@aws-sdk/util-user-agent-node": "3.796.0", + "@aws-sdk/util-user-agent-node": "3.798.0", "@smithy/config-resolver": "^4.1.0", - "@smithy/core": "^3.2.0", + "@smithy/core": "^3.3.0", "@smithy/fetch-http-handler": "^5.0.2", "@smithy/hash-node": "^4.0.2", "@smithy/invalid-dependency": "^4.0.2", "@smithy/middleware-content-length": "^4.0.2", - "@smithy/middleware-endpoint": "^4.1.0", - "@smithy/middleware-retry": "^4.1.0", + "@smithy/middleware-endpoint": "^4.1.1", + "@smithy/middleware-retry": "^4.1.1", "@smithy/middleware-serde": "^4.0.3", "@smithy/middleware-stack": "^4.0.2", "@smithy/node-config-provider": "^4.0.2", "@smithy/node-http-handler": "^4.0.4", "@smithy/protocol-http": "^5.1.0", - "@smithy/smithy-client": "^4.2.0", + "@smithy/smithy-client": "^4.2.1", "@smithy/types": "^4.2.0", "@smithy/url-parser": "^4.0.2", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-body-length-node": "^4.0.0", - "@smithy/util-defaults-mode-browser": "^4.0.8", - "@smithy/util-defaults-mode-node": "^4.0.8", + "@smithy/util-defaults-mode-browser": "^4.0.9", + "@smithy/util-defaults-mode-node": "^4.0.9", "@smithy/util-endpoints": "^3.0.2", "@smithy/util-middleware": "^4.0.2", "@smithy/util-retry": "^4.0.2", @@ -263,17 +263,17 @@ } }, "node_modules/@aws-sdk/core": { - "version": "3.796.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.796.0.tgz", - "integrity": "sha512-tH8Sp7lCxISVoLnkyv4AouuXs2CDlMhTuesWa0lq2NX1f+DXsMwSBtN37ttZdpFMw3F8mWdsJt27X9h2Oq868A==", + "version": "3.798.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.798.0.tgz", + "integrity": "sha512-hITxDE4pVkeJqz0LXjQRDgR+noxJ5oOxG38fgmQXjPXsdwVKnNIiMJ5S2WFMVSszU7ebGSyHdPHENQKu6TReVA==", "dependencies": { "@aws-sdk/types": "3.775.0", - "@smithy/core": "^3.2.0", + "@smithy/core": "^3.3.0", "@smithy/node-config-provider": "^4.0.2", "@smithy/property-provider": "^4.0.2", "@smithy/protocol-http": "^5.1.0", "@smithy/signature-v4": "^5.1.0", - "@smithy/smithy-client": "^4.2.0", + "@smithy/smithy-client": "^4.2.1", "@smithy/types": "^4.2.0", "@smithy/util-middleware": "^4.0.2", "fast-xml-parser": "4.4.1", @@ -305,11 +305,11 @@ } }, "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.796.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.796.0.tgz", - "integrity": "sha512-kQzGKm4IOYYO6vUrai2JocNwhJm4Aml2BsAV+tBhFhhkutE7khf9PUucoVjB78b0J48nF+kdSacqzY+gB81/Uw==", + "version": "3.798.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.798.0.tgz", + "integrity": "sha512-EsfzTEeoaHY1E+g3S6AmC3bF6euZN5SrLcLh5Oxhx5q2qjWUsKEK0fwek+jlt2GH7zB3F9IArV4z+8CsDQdKYw==", "dependencies": { - "@aws-sdk/core": "3.796.0", + "@aws-sdk/core": "3.798.0", "@aws-sdk/types": "3.775.0", "@smithy/property-provider": "^4.0.2", "@smithy/types": "^4.2.0", @@ -320,17 +320,17 @@ } }, "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.796.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.796.0.tgz", - "integrity": "sha512-wWOT6VAHIKOuHdKFGm1iyKvx7f6+Kc/YTzFWJPuT+l+CPlXR6ylP1UMIDsHHLKpMzsrh3CH77QDsjkhQrnKkfg==", + "version": "3.798.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.798.0.tgz", + "integrity": "sha512-bw5TmcJqpBVQlXzkL63545iHQ9mxwQeXTS/rgUQ5rmNNS3yiGDekVZOLXo/Gs4wmt2/59UN/sWIRFxvxDpMQEg==", "dependencies": { - "@aws-sdk/core": "3.796.0", + "@aws-sdk/core": "3.798.0", "@aws-sdk/types": "3.775.0", "@smithy/fetch-http-handler": "^5.0.2", "@smithy/node-http-handler": "^4.0.4", "@smithy/property-provider": "^4.0.2", "@smithy/protocol-http": "^5.1.0", - "@smithy/smithy-client": "^4.2.0", + "@smithy/smithy-client": "^4.2.1", "@smithy/types": "^4.2.0", "@smithy/util-stream": "^4.2.0", "tslib": "^2.6.2" @@ -340,17 +340,17 @@ } }, "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.796.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.796.0.tgz", - "integrity": "sha512-qGWBDn9aO8avFfYU7daps7Sy6OglF1x0q0w48slt0KMXbHd2/LvKVIiYwyofYCXed0yzcEOF2IYm9FjXdcn+ug==", + "version": "3.798.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.798.0.tgz", + "integrity": "sha512-zqWwKhhdf5CVRL6+4vNNTZVHWH9OiiwUWA3ka44jJaAMBRbbryjRedzwkWbgDaL1EbfTbcBZTYzE7N/vK7UUVA==", "dependencies": { - "@aws-sdk/core": "3.796.0", - "@aws-sdk/credential-provider-env": "3.796.0", - "@aws-sdk/credential-provider-http": "3.796.0", - "@aws-sdk/credential-provider-process": "3.796.0", - "@aws-sdk/credential-provider-sso": "3.796.0", - "@aws-sdk/credential-provider-web-identity": "3.796.0", - "@aws-sdk/nested-clients": "3.796.0", + "@aws-sdk/core": "3.798.0", + "@aws-sdk/credential-provider-env": "3.798.0", + "@aws-sdk/credential-provider-http": "3.798.0", + "@aws-sdk/credential-provider-process": "3.798.0", + "@aws-sdk/credential-provider-sso": "3.798.0", + "@aws-sdk/credential-provider-web-identity": "3.798.0", + "@aws-sdk/nested-clients": "3.798.0", "@aws-sdk/types": "3.775.0", "@smithy/credential-provider-imds": "^4.0.2", "@smithy/property-provider": "^4.0.2", @@ -363,16 +363,16 @@ } }, "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.796.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.796.0.tgz", - "integrity": "sha512-WeNK7OWPrsOvhO3DAgpUO0FtmVghMaZ/IpPJHJ4Y0nBIsWOBXLrbZ2Y1mdT8N2bGGUaM91tJaV8Yf8COc3gvmA==", + "version": "3.798.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.798.0.tgz", + "integrity": "sha512-Mrhl4wS4lMpuw2NCga5/rtQehNfyRs8NUHfvrLK5bZvJbjanrh8QtdRVhrAjw71OwFh3GK49QMByGkUssALJ+g==", "dependencies": { - "@aws-sdk/credential-provider-env": "3.796.0", - "@aws-sdk/credential-provider-http": "3.796.0", - "@aws-sdk/credential-provider-ini": "3.796.0", - "@aws-sdk/credential-provider-process": "3.796.0", - "@aws-sdk/credential-provider-sso": "3.796.0", - "@aws-sdk/credential-provider-web-identity": "3.796.0", + "@aws-sdk/credential-provider-env": "3.798.0", + "@aws-sdk/credential-provider-http": "3.798.0", + "@aws-sdk/credential-provider-ini": "3.798.0", + "@aws-sdk/credential-provider-process": "3.798.0", + "@aws-sdk/credential-provider-sso": "3.798.0", + "@aws-sdk/credential-provider-web-identity": "3.798.0", "@aws-sdk/types": "3.775.0", "@smithy/credential-provider-imds": "^4.0.2", "@smithy/property-provider": "^4.0.2", @@ -385,11 +385,11 @@ } }, "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.796.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.796.0.tgz", - "integrity": "sha512-r4e8/4AdKn/qQbRVocW7oXkpoiuXdTv0qty8AASNLnbQnT1vjD1bvmP6kp4fbHPWgwY8I9h0Dqjp49uy9Bqyuw==", + "version": "3.798.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.798.0.tgz", + "integrity": "sha512-BbRq8bhCHC94OTRIg5edgGTaWUzBH0h/IZJZ0vERle8A9nfl+5jUplvC8cvh3/8cNgHIRXj5HzlDjeSVe9dySg==", "dependencies": { - "@aws-sdk/core": "3.796.0", + "@aws-sdk/core": "3.798.0", "@aws-sdk/types": "3.775.0", "@smithy/property-provider": "^4.0.2", "@smithy/shared-ini-file-loader": "^4.0.2", @@ -401,13 +401,13 @@ } }, "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.796.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.796.0.tgz", - "integrity": "sha512-RUYsQ1t6UdzkpZ7pocUt1l/9l9GCYCaopIhv0DU6CipA8rkWtoweKsLHKdv+8wE4p6gqDfDIHGam1ivswiCIzg==", + "version": "3.798.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.798.0.tgz", + "integrity": "sha512-MLpQRb7xkqI9w0slEA76QiHGzM0PDMcpVcQG0wFHrpLKkQYjYlD9H3VfxdYGUh+FPOaR1fFpRZb18Gz9MR/2eQ==", "dependencies": { - "@aws-sdk/client-sso": "3.796.0", - "@aws-sdk/core": "3.796.0", - "@aws-sdk/token-providers": "3.796.0", + "@aws-sdk/client-sso": "3.798.0", + "@aws-sdk/core": "3.798.0", + "@aws-sdk/token-providers": "3.798.0", "@aws-sdk/types": "3.775.0", "@smithy/property-provider": "^4.0.2", "@smithy/shared-ini-file-loader": "^4.0.2", @@ -419,12 +419,12 @@ } }, "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.796.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.796.0.tgz", - "integrity": "sha512-dpmFJT4IyjT09vruvMu/rWQQjVreqdxAe8pLPpGhoeKyA1O6+PS73b+VNXKvD31rQT8e4g6dVpA6KMxNW63aag==", + "version": "3.798.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.798.0.tgz", + "integrity": "sha512-OWBDy/ZiC0pxLzp1Nhah5jxDZ/onLTjouIVGPyc9E8/KzUJxqQbR6fk43VqhpYdVp/S7yDDbaOpO072RRZJQrw==", "dependencies": { - "@aws-sdk/core": "3.796.0", - "@aws-sdk/nested-clients": "3.796.0", + "@aws-sdk/core": "3.798.0", + "@aws-sdk/nested-clients": "3.798.0", "@aws-sdk/types": "3.775.0", "@smithy/property-provider": "^4.0.2", "@smithy/types": "^4.2.0", @@ -476,14 +476,14 @@ } }, "node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.796.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.796.0.tgz", - "integrity": "sha512-IeNg+3jNWT37J45opi5Jx89hGF0lOnZjiNwlMp3rKq7PlOqy8kWq5J1Gxk0W3tIkPpuf68CtBs/QFrRXWOjsZw==", + "version": "3.798.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.798.0.tgz", + "integrity": "sha512-nb3YvLokpu/2meKVH5hGVLNg+hz3IyFCESEJW+SpK7bW/SfaKpukGY1lqwqbf+edl+s20MRXeK/by1rvBChixQ==", "dependencies": { - "@aws-sdk/core": "3.796.0", + "@aws-sdk/core": "3.798.0", "@aws-sdk/types": "3.775.0", "@aws-sdk/util-endpoints": "3.787.0", - "@smithy/core": "^3.2.0", + "@smithy/core": "^3.3.0", "@smithy/protocol-http": "^5.1.0", "@smithy/types": "^4.2.0", "tslib": "^2.6.2" @@ -493,43 +493,43 @@ } }, "node_modules/@aws-sdk/nested-clients": { - "version": "3.796.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.796.0.tgz", - "integrity": "sha512-jJ8a0ldWtXh/ice7nldUjTqja7KYlSYk1pwfIIvJLIqEn2SvQHK/pyCINTmmOmFAWXMKBQBeWUMxo1pPYNytzQ==", + "version": "3.798.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.798.0.tgz", + "integrity": "sha512-14iBJgg2Qqf74IeUY+z1nP5GIJIBZj8lv9mdpXrHlK8k+FcMXjpHg/B+JguSMhb2sbLeb5N0H8HLJGIRNALVWw==", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.796.0", + "@aws-sdk/core": "3.798.0", "@aws-sdk/middleware-host-header": "3.775.0", "@aws-sdk/middleware-logger": "3.775.0", "@aws-sdk/middleware-recursion-detection": "3.775.0", - "@aws-sdk/middleware-user-agent": "3.796.0", + "@aws-sdk/middleware-user-agent": "3.798.0", "@aws-sdk/region-config-resolver": "3.775.0", "@aws-sdk/types": "3.775.0", "@aws-sdk/util-endpoints": "3.787.0", "@aws-sdk/util-user-agent-browser": "3.775.0", - "@aws-sdk/util-user-agent-node": "3.796.0", + "@aws-sdk/util-user-agent-node": "3.798.0", "@smithy/config-resolver": "^4.1.0", - "@smithy/core": "^3.2.0", + "@smithy/core": "^3.3.0", "@smithy/fetch-http-handler": "^5.0.2", "@smithy/hash-node": "^4.0.2", "@smithy/invalid-dependency": "^4.0.2", "@smithy/middleware-content-length": "^4.0.2", - "@smithy/middleware-endpoint": "^4.1.0", - "@smithy/middleware-retry": "^4.1.0", + "@smithy/middleware-endpoint": "^4.1.1", + "@smithy/middleware-retry": "^4.1.1", "@smithy/middleware-serde": "^4.0.3", "@smithy/middleware-stack": "^4.0.2", "@smithy/node-config-provider": "^4.0.2", "@smithy/node-http-handler": "^4.0.4", "@smithy/protocol-http": "^5.1.0", - "@smithy/smithy-client": "^4.2.0", + "@smithy/smithy-client": "^4.2.1", "@smithy/types": "^4.2.0", "@smithy/url-parser": "^4.0.2", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-body-length-node": "^4.0.0", - "@smithy/util-defaults-mode-browser": "^4.0.8", - "@smithy/util-defaults-mode-node": "^4.0.8", + "@smithy/util-defaults-mode-browser": "^4.0.9", + "@smithy/util-defaults-mode-node": "^4.0.9", "@smithy/util-endpoints": "^3.0.2", "@smithy/util-middleware": "^4.0.2", "@smithy/util-retry": "^4.0.2", @@ -557,11 +557,11 @@ } }, "node_modules/@aws-sdk/token-providers": { - "version": "3.796.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.796.0.tgz", - "integrity": "sha512-Sxr/EqJBxOwLsXHv8C91N/Aao8Rgjn5bcpzplrTZ7wrfDrzqQfSCvjh7apCxdLYMKPBV+an75blCAd7JD4/bAg==", + "version": "3.798.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.798.0.tgz", + "integrity": "sha512-iYhNmHXfWLUwcMP9ldb/H+RMRLHZbBUWBgsoQqfb7sl6z24nH0qBJyL+oXHTCVBUYLP20CvUrVkcwlejDzyoRw==", "dependencies": { - "@aws-sdk/nested-clients": "3.796.0", + "@aws-sdk/nested-clients": "3.798.0", "@aws-sdk/types": "3.775.0", "@smithy/property-provider": "^4.0.2", "@smithy/shared-ini-file-loader": "^4.0.2", @@ -621,11 +621,11 @@ } }, "node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.796.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.796.0.tgz", - "integrity": "sha512-9fQpNcHgVFitf1tbTT8V1xGRoRHSmOAWjrhevo6Tc0WoINMAKz+4JNqfVGWRE5Tmtpq0oHKo1RmvxXQQtJYciA==", + "version": "3.798.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.798.0.tgz", + "integrity": "sha512-yncgNd2inI+y5kdfn2i0oBwgCxwdtcVShNNVQ+5b/nuC1Lgjgcb+hmHAeTFMge7vhDP2Md8I+ih6bPMpK79lQQ==", "dependencies": { - "@aws-sdk/middleware-user-agent": "3.796.0", + "@aws-sdk/middleware-user-agent": "3.798.0", "@aws-sdk/types": "3.775.0", "@smithy/node-config-provider": "^4.0.2", "@smithy/types": "^4.2.0", @@ -2062,9 +2062,9 @@ } }, "node_modules/@smithy/core": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.2.0.tgz", - "integrity": "sha512-k17bgQhVZ7YmUvA8at4af1TDpl0NDMBuBKJl8Yg0nrefwmValU+CnA5l/AriVdQNthU/33H3nK71HrLgqOPr1Q==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.3.0.tgz", + "integrity": "sha512-r6gvs5OfRq/w+9unPm7B3po4rmWaGh0CIL/OwHntGGux7+RhOOZLGuurbeMgWV6W55ZuyMTypJLeH0vn/ZRaWQ==", "dependencies": { "@smithy/middleware-serde": "^4.0.3", "@smithy/protocol-http": "^5.1.0", @@ -2160,11 +2160,11 @@ } }, "node_modules/@smithy/middleware-endpoint": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.1.0.tgz", - "integrity": "sha512-xhLimgNCbCzsUppRTGXWkZywksuTThxaIB0HwbpsVLY5sceac4e1TZ/WKYqufQLaUy+gUSJGNdwD2jo3cXL0iA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.1.1.tgz", + "integrity": "sha512-z5RmcHxjvScL+LwEDU2mTNCOhgUs4lu5PGdF1K36IPRmUHhNFxNxgenSB7smyDiYD4vdKQ7CAZtG5cUErqib9w==", "dependencies": { - "@smithy/core": "^3.2.0", + "@smithy/core": "^3.3.0", "@smithy/middleware-serde": "^4.0.3", "@smithy/node-config-provider": "^4.0.2", "@smithy/shared-ini-file-loader": "^4.0.2", @@ -2178,14 +2178,14 @@ } }, "node_modules/@smithy/middleware-retry": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.1.0.tgz", - "integrity": "sha512-2zAagd1s6hAaI/ap6SXi5T3dDwBOczOMCSkkYzktqN1+tzbk1GAsHNAdo/1uzxz3Ky02jvZQwbi/vmDA6z4Oyg==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.1.1.tgz", + "integrity": "sha512-mBJOxn9aUYwcBUPQpKv9ifzrCn4EbhPUFguEZv3jB57YOMh0caS4P8HoLvUeNUI1nx4bIVH2SIbogbDfFI9DUA==", "dependencies": { "@smithy/node-config-provider": "^4.0.2", "@smithy/protocol-http": "^5.1.0", "@smithy/service-error-classification": "^4.0.2", - "@smithy/smithy-client": "^4.2.0", + "@smithy/smithy-client": "^4.2.1", "@smithy/types": "^4.2.0", "@smithy/util-middleware": "^4.0.2", "@smithy/util-retry": "^4.0.2", @@ -2352,12 +2352,12 @@ } }, "node_modules/@smithy/smithy-client": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.2.0.tgz", - "integrity": "sha512-Qs65/w30pWV7LSFAez9DKy0Koaoh3iHhpcpCCJ4waj/iqwsuSzJna2+vYwq46yBaqO5ZbP9TjUsATUNxrKeBdw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.2.1.tgz", + "integrity": "sha512-fbniZef60QdsBc4ZY0iyI8xbFHIiC/QRtPi66iE4ufjiE/aaz7AfUXzcWMkpO8r+QhLeNRIfmPchIG+3/QDZ6g==", "dependencies": { - "@smithy/core": "^3.2.0", - "@smithy/middleware-endpoint": "^4.1.0", + "@smithy/core": "^3.3.0", + "@smithy/middleware-endpoint": "^4.1.1", "@smithy/middleware-stack": "^4.0.2", "@smithy/protocol-http": "^5.1.0", "@smithy/types": "^4.2.0", @@ -2451,12 +2451,12 @@ } }, "node_modules/@smithy/util-defaults-mode-browser": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.0.8.tgz", - "integrity": "sha512-ZTypzBra+lI/LfTYZeop9UjoJhhGRTg3pxrNpfSTQLd3AJ37r2z4AXTKpq1rFXiiUIJsYyFgNJdjWRGP/cbBaQ==", + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.0.9.tgz", + "integrity": "sha512-B8j0XsElvyhv6+5hlFf6vFV/uCSyLKcInpeXOGnOImX2mGXshE01RvPoGipTlRpIk53e6UfYj7WdDdgbVfXDZw==", "dependencies": { "@smithy/property-provider": "^4.0.2", - "@smithy/smithy-client": "^4.2.0", + "@smithy/smithy-client": "^4.2.1", "@smithy/types": "^4.2.0", "bowser": "^2.11.0", "tslib": "^2.6.2" @@ -2466,15 +2466,15 @@ } }, "node_modules/@smithy/util-defaults-mode-node": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.0.8.tgz", - "integrity": "sha512-Rgk0Jc/UDfRTzVthye/k2dDsz5Xxs9LZaKCNPgJTRyoyBoeiNCnHsYGOyu1PKN+sDyPnJzMOz22JbwxzBp9NNA==", + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.0.9.tgz", + "integrity": "sha512-wTDU8P/zdIf9DOpV5qm64HVgGRXvqjqB/fJZTEQbrz3s79JHM/E7XkMm/876Oq+ZLHJQgnXM9QHDo29dlM62eA==", "dependencies": { "@smithy/config-resolver": "^4.1.0", "@smithy/credential-provider-imds": "^4.0.2", "@smithy/node-config-provider": "^4.0.2", "@smithy/property-provider": "^4.0.2", - "@smithy/smithy-client": "^4.2.0", + "@smithy/smithy-client": "^4.2.1", "@smithy/types": "^4.2.0", "tslib": "^2.6.2" }, diff --git a/functions/package.json b/functions/package.json index 4d2e4a9..59f0fdf 100644 --- a/functions/package.json +++ b/functions/package.json @@ -15,7 +15,7 @@ }, "main": "lib/index.js", "dependencies": { - "@aws-sdk/client-ses": "^3.796.0", + "@aws-sdk/client-ses": "^3.798.0", "@types/node-fetch": "^2.6.12", "aws-sdk": "^2.1692.0", "axios": "^1.8.4", diff --git a/functions/src/index.ts b/functions/src/index.ts index a0fb365..1c5b36c 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -3,6 +3,6 @@ export { sendEmailMessage, sendEmailWithAttachment, sendEmailSES } from './email export { sendSMSMessage } from './sms'; export { accessFile } from './storage'; export { processNotificationOnCreate } from './notifications'; -export { createCashfreeLink, createCashfreeOrder, verifyCashfreePayment } from './payments'; +export { createCashfreeLink, createCashfreeOrder, verifyCashfreePayment, checkPhonePePaymentStatus, createPhonePeOrder, phonePeWebhook } from './payments'; export { getPlaceDetails, getPlacesAutocomplete } from './places'; export { registerClient } from './clientRegistration'; diff --git a/functions/src/payments/index.ts b/functions/src/payments/index.ts index 8d52c66..beda940 100644 --- a/functions/src/payments/index.ts +++ b/functions/src/payments/index.ts @@ -1 +1,2 @@ export * from './cashfree'; +export * from './phonepe'; diff --git a/functions/src/payments/phonepe/checkStatus.ts b/functions/src/payments/phonepe/checkStatus.ts new file mode 100644 index 0000000..df2fa36 --- /dev/null +++ b/functions/src/payments/phonepe/checkStatus.ts @@ -0,0 +1,139 @@ +import axios from "axios"; +import { onRequest } from "firebase-functions/v2/https"; +import { Request} from "firebase-functions/v2/https"; +import { getCorsHandler } from "../../shared/middleware"; +import { getAdmin, getLogger } from "../../shared/config"; +const admin = getAdmin(); +const logger = getLogger(); +const corsHandler = getCorsHandler(); + +export const checkPhonePePaymentStatus = onRequest({ + region: '#{SERVICES_RGN}#' + }, async (request: Request, response) => { + return corsHandler(request, response, async () => { + try { + const authHeader = request.headers.authorization || ''; + if (!authHeader || !authHeader.startsWith('Bearer ')) { + response.status(401).json({ error: 'Unauthorized' }); + return; + } + + const idToken = authHeader.split('Bearer ')[1]; + try { + await admin.auth().verifyIdToken(idToken); + + const merchantOrderId = request.query.merchantOrderId as string; + if (!merchantOrderId) { + response.status(400).json({ error: 'Missing merchant order ID' }); + return; + } + + const details = request.query.details === 'true'; + const errorContext = request.query.errorContext === 'true'; + + const clientId = process.env.PHONEPE_CLIENT_ID; + const clientSecret = process.env.PHONEPE_CLIENT_SECRET; + const apiUrl = process.env.PHONEPE_API_URL; + const clientVersion = process.env.PHONEPE_CLIENT_VERSION || '1'; + + if (!clientId || !clientSecret || !apiUrl) { + logger.error('PhonePe credentials not configured'); + response.status(500).json({ error: 'Payment gateway configuration error' }); + return; + } + + const tokenResponse = await axios.post( + `${apiUrl}/v1/oauth/token`, + { + client_id: clientId, + client_version: clientVersion, + client_secret: clientSecret, + grant_type: 'client_credentials', + }, + { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + } + ); + + const accessToken = tokenResponse.data.access_token; + + const queryParams = new URLSearchParams(); + if (details) queryParams.append('details', 'true'); + if (errorContext) queryParams.append('errorContext', 'true'); + const queryString = queryParams.toString() ? `?${queryParams.toString()}` : ''; + + const statusResponse = await axios.get( + `${apiUrl}/checkout/v2/order/${merchantOrderId}/status${queryString}`, + { + headers: { + 'Content-Type': 'application/json', + 'Authorization': `O-Bearer ${accessToken}`, + }, + } + ); + + const orderQuery = await admin.firestore() + .collection('payment_orders') + .where('merchantOrderId', '==', merchantOrderId) + .limit(1) + .get(); + + if (orderQuery.empty) { + logger.error(`No payment order found with PhonePe orderId: ${merchantOrderId}`); + response.status(404).json({ + success: false, + error: 'Payment order not found', + message: `No record found for PhonePe order ID: ${merchantOrderId}` + }); + return; + } + + const orderDoc = orderQuery.docs[0]; + + await orderDoc.ref.update({ + orderStatus: statusResponse.data.state || 'UNKNOWN', + lastChecked: new Date(), + statusResponse: statusResponse.data + }); + logger.info('PhonePe status response data:', JSON.stringify(statusResponse.data)); + + response.json({ + success: true, + state: statusResponse.data.state, + data: statusResponse.data + }); + + } catch (authError: any) { + logger.error('Authentication error:', authError); + + if (authError.response) { + logger.error('API error details:', { + status: authError.response.status, + data: authError.response.data + }); + + response.status(authError.response.status).json({ + success: false, + error: 'API error', + details: authError.response.data + }); + } else { + response.status(401).json({ + success: false, + error: 'Invalid authentication token or API error', + message: authError.message + }); + } + } + } catch (error: any) { + logger.error('PhonePe payment status check error:', error); + response.status(500).json({ + success: false, + error: 'Failed to check payment status', + details: error.message + }); + } + }); + }); diff --git a/functions/src/payments/phonepe/createPhonepeOrder.ts b/functions/src/payments/phonepe/createPhonepeOrder.ts new file mode 100644 index 0000000..fcad1c2 --- /dev/null +++ b/functions/src/payments/phonepe/createPhonepeOrder.ts @@ -0,0 +1,133 @@ +import { onRequest } from "firebase-functions/v2/https"; +import { Request} from "firebase-functions/v2/https"; +import { getCorsHandler } from "../../shared/middleware"; +import { getAdmin, getLogger } from "../../shared/config"; +import axios from "axios"; +const admin = getAdmin(); +const logger = getLogger(); +const corsHandler = getCorsHandler(); + +export const createPhonePeOrder = onRequest({ + region: '#{SERVICES_RGN}#' + }, async (request: Request, response) => { + return corsHandler(request, response, async () => { + try { + const authHeader = request.headers.authorization || ''; + if (!authHeader || !authHeader.startsWith('Bearer ')) { + response.status(401).json({ error: 'Unauthorized' }); + return; + } + + const idToken = authHeader.split('Bearer ')[1]; + try { + const decodedToken = await admin.auth().verifyIdToken(idToken); + const uid = decodedToken.uid; + + const { + merchantOrderId, + amount, + expireAfter, + metaInfo, + paymentFlow + } = request.body; + + if (!merchantOrderId || !amount || !paymentFlow || !expireAfter) { + response.status(400).json({ error: 'Missing required fields' }); + return; + } + + if (!paymentFlow.type || !paymentFlow.merchantUrls || !paymentFlow.merchantUrls.redirectUrl) { + response.status(400).json({ error: 'Invalid payment flow configuration' }); + return; + } + + const clientId = process.env.PHONEPE_CLIENT_ID; + const clientSecret = process.env.PHONEPE_CLIENT_SECRET; + const apiUrl = process.env.PHONEPE_API_URL; + const clientVersion = process.env.PHONEPE_CLIENT_VERSION || '1'; + + if (!clientId || !clientSecret || !apiUrl) { + logger.error('PhonePe credentials not configured'); + response.status(500).json({ error: 'Payment gateway configuration error' }); + return; + } + + try { + const tokenResponse = await axios.post( + `${apiUrl}/v1/oauth/token`, + { + client_id: clientId, + client_version: clientVersion, + client_secret: clientSecret, + grant_type: 'client_credentials', + }, + { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + } + ); + + const accessToken = tokenResponse.data.access_token; + + const paymentResponse = await axios.post( + `${apiUrl}/checkout/v2/pay`, + request.body, + { + headers: { + 'Content-Type': 'application/json', + 'Authorization': `O-Bearer ${accessToken}`, + }, + } + ); + + try { + await admin.firestore().collection('payment_orders').doc(merchantOrderId).set({ + userId: uid, + amount: amount / 100, + orderStatus: paymentResponse.data.state || 'PENDING', + paymentGateway: 'PhonePe', + createdAt: new Date(), + merchantOrderId: merchantOrderId, + paymentUrl: paymentResponse.data.redirectUrl, + orderId: paymentResponse.data.orderId, + expireAt: new Date(paymentResponse.data.expireAt), + // rawResponse: paymentResponse.data, + metaInfo: metaInfo || {} + }); + } catch (firestoreError) { + logger.error('Error storing order in Firestore:', firestoreError); + } + + response.json({ + ...paymentResponse.data, + merchantOrderId: merchantOrderId + }); + + logger.info(`PhonePe order created: ${merchantOrderId}`); + } catch (apiError: any) { + logger.error('PhonePe API error:', apiError); + response.status(apiError.response?.status || 500).json({ + success: false, + error: 'Payment gateway error', + details: apiError.response?.data || apiError.message, + code: apiError.code + }); + } + } catch (authError) { + logger.error('Authentication error:', authError); + response.status(401).json({ + success: false, + error: 'Invalid authentication token' + }); + } + } catch (error: any) { + logger.error('PhonePe order creation error:', error); + response.status(500).json({ + success: false, + error: 'Failed to create payment order', + details: error.message + }); + } + }); + }); diff --git a/functions/src/payments/phonepe/index.ts b/functions/src/payments/phonepe/index.ts new file mode 100644 index 0000000..2fe0eda --- /dev/null +++ b/functions/src/payments/phonepe/index.ts @@ -0,0 +1,5 @@ +export { createPhonePeOrder } from './createPhonepeOrder'; +export { checkPhonePePaymentStatus } from './checkStatus'; +// export { createPhonePePaymentLink } from './paymentLink'; +export { phonePeWebhook } from './webhook'; +// export { checkPhonePePaymentLinkStatus } from './paymentLinkStatus'; diff --git a/functions/src/payments/phonepe/paymentLink.ts b/functions/src/payments/phonepe/paymentLink.ts new file mode 100644 index 0000000..a676868 --- /dev/null +++ b/functions/src/payments/phonepe/paymentLink.ts @@ -0,0 +1,183 @@ +import { onRequest } from "firebase-functions/v2/https"; +import { Request } from "firebase-functions/v2/https"; +import { getCorsHandler } from "../../shared/middleware"; +import { getAdmin, getLogger } from "../../shared/config"; +import axios from "axios"; +const admin = getAdmin(); +const logger = getLogger(); +const corsHandler = getCorsHandler(); + +interface PaymentLinkPayload { + merchantId: string; + merchantOrderId: string; + merchantUserId: string; + amount: number; + mobileNumber?: string; + email?: string; + shortName: string; + expiryDate: number; + redirectUrl: string; + redirectMode: string; + paymentInstrument: { + type: string; + }; + notifyCustomer?: boolean; +} + +export const createPhonePePaymentLink = onRequest({ + region: '#{SERVICES_RGN}#' + }, async (request: Request, response) => { + return corsHandler(request, response, async () => { + try { + const authHeader = request.headers.authorization || ''; + if (!authHeader || !authHeader.startsWith('Bearer ')) { + response.status(401).json({ error: 'Unauthorized' }); + return; + } + + const idToken = authHeader.split('Bearer ')[1]; + try { + const decodedToken = await admin.auth().verifyIdToken(idToken); + const uid = decodedToken.uid; + + const { + amount, + orderId, + customerName, + customerEmail, + customerPhone, + productInfo, + expiryDays = 7, + callbackUrl, + notifyCustomer = false + } = request.body; + + if (!amount || !orderId) { + response.status(400).json({ error: 'Missing required fields' }); + return; + } + + const clientId = process.env.PHONEPE_CLIENT_ID; + const clientSecret = process.env.PHONEPE_CLIENT_SECRET; + const apiUrl = process.env.PHONEPE_API_URL; + const merchantId = process.env.PHONEPE_MERCHANT_ID; + const clientVersion = process.env.PHONEPE_CLIENT_VERSION || '1'; + + if (!clientId || !clientSecret || !apiUrl || !merchantId) { + logger.error('PhonePe credentials not configured'); + response.status(500).json({ error: 'Payment gateway configuration error' }); + return; + } + + try { + const tokenResponse = await axios.post( + `${apiUrl}/v1/oauth/token`, + { + client_id: clientId, + client_version: clientVersion, + client_secret: clientSecret, + grant_type: 'client_credentials', + }, + { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + } + ); + + const accessToken = tokenResponse.data.access_token; + + const expiryInSeconds = Math.floor(Date.now() / 1000) + (expiryDays * 24 * 60 * 60); + + const paymentLinkPayload: PaymentLinkPayload = { + merchantId: merchantId, + merchantOrderId: orderId, + merchantUserId: uid, + amount: parseInt(amount) * 100, + shortName: productInfo || "Payment", + expiryDate: expiryInSeconds, + redirectUrl: callbackUrl, + redirectMode: "REDIRECT", + paymentInstrument: { + type: "PAY_PAGE" + } + }; + + if (customerPhone) { + paymentLinkPayload.mobileNumber = customerPhone; + } + + if (customerEmail) { + paymentLinkPayload.email = customerEmail; + } + + if (notifyCustomer && (customerEmail || customerPhone)) { + paymentLinkPayload.notifyCustomer = true; + } + + const paymentLinkResponse = await axios.post( + `${apiUrl}/v3/payment-links/create`, + paymentLinkPayload, + { + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${accessToken}`, + }, + } + ); + + try { + await admin.firestore().collection('payment_links').doc(orderId).set({ + userId: uid, + amount: amount, + customerName: customerName, + customerEmail: customerEmail, + customerPhone: customerPhone, + orderStatus: 'CREATED', + paymentGateway: 'PhonePe', + createdAt: new Date(), + expiryDate: new Date(expiryInSeconds * 1000), + orderId: orderId, + paymentLinkId: paymentLinkResponse.data.data?.linkId || paymentLinkResponse.data.linkId, + paymentLinkUrl: paymentLinkResponse.data.data?.linkUrl || paymentLinkResponse.data.linkUrl, + rawResponse: paymentLinkResponse.data + }); + } catch (firestoreError) { + logger.error('Error storing payment link in Firestore:', firestoreError); + } + + response.json({ + success: true, + orderId: orderId, + paymentLinkId: paymentLinkResponse.data.data?.linkId || paymentLinkResponse.data.linkId, + paymentLinkUrl: paymentLinkResponse.data.data?.linkUrl || paymentLinkResponse.data.linkUrl, + response: paymentLinkResponse.data + }); + + logger.info(`PhonePe payment link created: ${orderId}`); + } catch (apiError: any) { + logger.error('PhonePe API error:', apiError.response?.data || apiError); + response.status(apiError.response?.status || 500).json({ + success: false, + error: 'Payment gateway error', + details: apiError.response?.data || apiError.message, + code: apiError.code + }); + } + } catch (authError) { + logger.error('Authentication error:', authError); + response.status(401).json({ + success: false, + error: 'Invalid authentication token' + }); + } + } catch (error: any) { + logger.error('PhonePe payment link creation error:', error); + response.status(500).json({ + success: false, + error: 'Failed to create payment link', + details: error.message + }); + } + }); + }); diff --git a/functions/src/payments/phonepe/paymentLinkStatus.ts b/functions/src/payments/phonepe/paymentLinkStatus.ts new file mode 100644 index 0000000..1364fe4 --- /dev/null +++ b/functions/src/payments/phonepe/paymentLinkStatus.ts @@ -0,0 +1,122 @@ +import { onRequest } from "firebase-functions/v2/https"; +import { Request } from "firebase-functions/v2/https"; +import { getCorsHandler } from "../../shared/middleware"; +import { getAdmin, getLogger } from "../../shared/config"; +import axios from "axios"; +const admin = getAdmin(); +const logger = getLogger(); +const corsHandler = getCorsHandler(); + +export const checkPhonePePaymentLinkStatus = onRequest({ + region: '#{SERVICES_RGN}#' +}, async (request: Request, response) => { + return corsHandler(request, response, async () => { + try { + const authHeader = request.headers.authorization || ''; + if (!authHeader || !authHeader.startsWith('Bearer ')) { + response.status(401).json({ error: 'Unauthorized' }); + return; + } + + const idToken = authHeader.split('Bearer ')[1]; + try { + await admin.auth().verifyIdToken(idToken); + + const linkId = request.query.linkId as string; + if (!linkId) { + response.status(400).json({ error: 'Missing payment link ID' }); + return; + } + + const clientId = process.env.PHONEPE_CLIENT_ID; + const clientSecret = process.env.PHONEPE_CLIENT_SECRET; + const apiUrl = process.env.PHONEPE_API_URL; + const clientVersion = process.env.PHONEPE_CLIENT_VERSION || '1'; + + if (!clientId || !clientSecret || !apiUrl) { + logger.error('PhonePe credentials not configured'); + response.status(500).json({ error: 'Payment gateway configuration error' }); + return; + } + + const tokenResponse = await axios.post( + `${apiUrl}/v1/oauth/token`, + { + client_id: clientId, + client_version: clientVersion, + client_secret: clientSecret, + grant_type: 'client_credentials', + }, + { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + } + ); + + const accessToken = tokenResponse.data.access_token; + + const statusResponse = await axios.get( + `${apiUrl}/v3/payment-links/${linkId}/status`, + { + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${accessToken}`, + }, + } + ); + + const linkQuery = await admin.firestore() + .collection('payment_links') + .where('paymentLinkId', '==', linkId) + .limit(1) + .get(); + + if (!linkQuery.empty) { + const linkDoc = linkQuery.docs[0]; + + await linkDoc.ref.update({ + orderStatus: statusResponse.data.data?.state || statusResponse.data.state || 'UNKNOWN', + lastChecked: new Date(), + statusResponse: statusResponse.data + }); + } + + response.json({ + success: true, + state: statusResponse.data.data?.state || statusResponse.data.state, + data: statusResponse.data + }); + + } catch (authError: any) { + logger.error('Authentication or API error:', authError); + + if (authError.response) { + logger.error('API error details:', { + status: authError.response.status, + data: authError.response.data + }); + + response.status(authError.response.status).json({ + success: false, + error: 'API error', + details: authError.response.data + }); + } else { + response.status(401).json({ + success: false, + error: 'Invalid authentication token or API error', + message: authError.message + }); + } + } + } catch (error: any) { + logger.error('PhonePe payment link status check error:', error); + response.status(500).json({ + success: false, + error: 'Failed to check payment link status', + details: error.message + }); + } + }); +}); diff --git a/functions/src/payments/phonepe/webhook.ts b/functions/src/payments/phonepe/webhook.ts new file mode 100644 index 0000000..5f52274 --- /dev/null +++ b/functions/src/payments/phonepe/webhook.ts @@ -0,0 +1,102 @@ +import { onRequest } from "firebase-functions/v2/https"; +import { Request } from "firebase-functions/v2/https"; +import { getAdmin, getLogger } from "../../shared/config"; +import crypto from "crypto"; + +const admin = getAdmin(); +const logger = getLogger(); + +export const phonePeWebhook = onRequest({ + region: '#{SERVICES_RGN}#' +}, async (request: Request, response) => { + try { + const signature = request.headers['x-verify'] as string; + const webhookSecret = process.env.PHONEPE_WEBHOOK_SECRET; + + if (!signature || !webhookSecret) { + logger.error('Missing signature or webhook secret'); + response.status(401).json({ error: 'Unauthorized' }); + return; + } + + const rawBody = JSON.stringify(request.body); + + const expectedSignature = crypto + .createHmac('sha256', webhookSecret) + .update(rawBody) + .digest('hex'); + + if (signature !== expectedSignature) { + logger.error('Invalid webhook signature'); + response.status(401).json({ error: 'Invalid signature' }); + return; + } + + const { event, data } = request.body; + + if (!event || !data || !data.merchantOrderId || !data.orderId) { + logger.error('Invalid webhook payload', request.body); + response.status(400).json({ error: 'Invalid payload' }); + return; + } + + logger.info(`Received PhonePe webhook: ${event}`, { + merchantOrderId: data.merchantOrderId, + orderId: data.orderId, + state: data.state + }); + + const orderQuery = await admin.firestore() + .collection('payment_orders') + .where('orderId', '==', data.orderId) + .limit(1) + .get(); + + if (orderQuery.empty) { + const merchantOrderQuery = await admin.firestore() + .collection('payment_orders') + .where('merchantOrderId', '==', data.merchantOrderId) + .limit(1) + .get(); + + if (merchantOrderQuery.empty) { + logger.error(`No payment order found for PhonePe orderId: ${data.orderId} or merchantOrderId: ${data.merchantOrderId}`); + response.status(404).json({ + success: false, + error: 'Payment order not found' + }); + return; + } + + const orderDoc = merchantOrderQuery.docs[0]; + await orderDoc.ref.update({ + orderStatus: data.state || 'UNKNOWN', + lastUpdated: new Date(), + webhookEvent: event, + webhookData: data + }); + + logger.info(`Updated order status via webhook for merchantOrderId: ${data.merchantOrderId} to ${data.state}`); + } else { + const orderDoc = orderQuery.docs[0]; + await orderDoc.ref.update({ + orderStatus: data.state || 'UNKNOWN', + lastUpdated: new Date(), + webhookEvent: event, + webhookData: data + }); + + logger.info(`Updated order status via webhook for orderId: ${data.orderId} to ${data.state}`); + } + + response.status(200).json({ success: true }); + + } catch (error: any) { + logger.error('PhonePe webhook processing error:', error); + response.status(500).json({ + success: false, + error: 'Failed to process webhook', + details: error.message + }); + } +}); diff --git a/functions/tsconfig.json b/functions/tsconfig.json index 752b2ab..ff92031 100644 --- a/functions/tsconfig.json +++ b/functions/tsconfig.json @@ -9,7 +9,9 @@ "outDir": "lib", "sourceMap": true, "strict": true, - "target": "es2017" + "target": "es2017", + "types": [], + "skipLibCheck": true }, "compileOnSave": true, "include": [ diff --git a/package.json b/package.json index 5f79027..474c153 100644 --- a/package.json +++ b/package.json @@ -2,8 +2,5 @@ "dependencies": { "@types/busboy": "^1.5.4", "busboy": "^1.6.0" - }, - "devDependencies": { - "@types/long": "^5.0.0" } }