From 9efa31b6cce3966b4118fda0cb0e5f88cda4163e Mon Sep 17 00:00:00 2001 From: Allen T J Date: Mon, 5 May 2025 14:09:08 +0000 Subject: [PATCH 01/11] phonepe (#23) Co-authored-by: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com> Reviewed-on: https://git.cosqnet.com/cosqnet/fitlien-services/pulls/23 Co-authored-by: Allen T J Co-committed-by: Allen T J --- firebase.json | 6 +- functions/.env.example | 5 + 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 | 3 + functions/src/payments/phonepe/webhook.ts | 106 ++++++++ functions/tsconfig.json | 4 +- package.json | 3 - 12 files changed, 518 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/webhook.ts diff --git a/firebase.json b/firebase.json index dd93bf2..d8682a0 100644 --- a/firebase.json +++ b/firebase.json @@ -22,17 +22,17 @@ }, "emulators": { "functions": { - "port": 5001 + "port": 5005 }, "firestore": { - "port": 8079 + "port": 8085 }, "storage": { "port": 9199 }, "ui": { "enabled": true, - "port": 4000 + "port": 4008 }, "auth": { "port": 9099 diff --git a/functions/.env.example b/functions/.env.example index 397672a..e62bb23 100644 --- a/functions/.env.example +++ b/functions/.env.example @@ -11,6 +11,11 @@ GOOGLE_MAPS_API_KEY=#{GOOGLE_MAPS_API_KEY}# CASHFREE_URL=#{CASHFREE_URL}# CASHFREE_LINK_URL=#{CASHFREE_LINK_URL}# CASHFREE_LINK_NOTIFY_URL=#{CASHFREE_LINK_NOTIFY_URL}# +PHONEPE_CLIENT_ID=#{PHONEPE_CLIENT_ID}# +PHONEPE_CLIENT_SECRET=#{PHONEPE_CLIENT_SECRET}# +PHONEPE_API_URL=#{PHONEPE_API_URL}# +PHONEPE_WEBHOOK_USERNAME=#{PHONEPE_WEBHOOK_USERNAME}# +PHONEPE_WEBHOOK_PASSWORD=#{PHONEPE_WEBHOOK_PASSWORD}# SES_FROM_EMAIL=#{SES_FROM_EMAIL}# SES_REPLY_TO_EMAIL=#{SES_REPLY_TO_EMAIL}# 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..258f121 --- /dev/null +++ b/functions/src/payments/phonepe/index.ts @@ -0,0 +1,3 @@ +export { createPhonePeOrder } from './createPhonepeOrder'; +export { checkPhonePePaymentStatus } from './checkStatus'; +export { phonePeWebhook } from './webhook'; diff --git a/functions/src/payments/phonepe/webhook.ts b/functions/src/payments/phonepe/webhook.ts new file mode 100644 index 0000000..d8696dc --- /dev/null +++ b/functions/src/payments/phonepe/webhook.ts @@ -0,0 +1,106 @@ +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 authHeader = request.headers['authorization'] as string; + const username = process.env.PHONEPE_WEBHOOK_USERNAME; + const password = process.env.PHONEPE_WEBHOOK_PASSWORD; + + if (!authHeader || !username || !password) { + logger.error('Missing authorization header or webhook credentials'); + response.status(401).json({ error: 'Unauthorized' }); + return; + } + + // Calculate expected authorization value + const credentialString = `${username}:${password}`; + const expectedAuth = crypto + .createHash('sha256') + .update(credentialString) + .digest('hex'); + + // PhonePe may send the header with a prefix like "SHA256 " or just the hash + const receivedAuth = authHeader.replace(/^SHA256\s+/i, ''); + + if (receivedAuth.toLowerCase() !== expectedAuth.toLowerCase()) { + logger.error('Invalid webhook authorization'); + response.status(401).json({ error: 'Invalid authorization' }); + return; + } + + const { event, payload } = request.body; + + if (!event || !payload || !payload.merchantOrderId || !payload.orderId) { + logger.error('Invalid webhook payload', request.body); + response.status(400).json({ error: 'Invalid payload' }); + return; + } + + logger.info(`Received PhonePe webhook: ${event}`, { + merchantOrderId: payload.merchantOrderId, + orderId: payload.orderId, + state: payload.state + }); + + const orderQuery = await admin.firestore() + .collection('payment_orders') + .where('orderId', '==', payload.orderId) + .limit(1) + .get(); + + if (orderQuery.empty) { + const merchantOrderQuery = await admin.firestore() + .collection('payment_orders') + .where('merchantOrderId', '==', payload.merchantOrderId) + .limit(1) + .get(); + + if (merchantOrderQuery.empty) { + logger.error(`No payment order found for PhonePe orderId: ${payload.orderId} or merchantOrderId: ${payload.merchantOrderId}`); + response.status(404).json({ + success: false, + error: 'Payment order not found' + }); + return; + } + + const orderDoc = merchantOrderQuery.docs[0]; + await orderDoc.ref.update({ + orderStatus: payload.state || 'UNKNOWN', + lastUpdated: new Date(), + webhookEvent: event, + webhookData: payload + }); + + logger.info(`Updated order status via webhook for merchantOrderId: ${payload.merchantOrderId} to ${payload.state}`); + } else { + const orderDoc = orderQuery.docs[0]; + await orderDoc.ref.update({ + orderStatus: payload.state || 'UNKNOWN', + lastUpdated: new Date(), + webhookEvent: event, + webhookData: payload + }); + + logger.info(`Updated order status via webhook for orderId: ${payload.orderId} to ${payload.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" } } From 0f86c468d2edc7cd3a7faeb9f764a11e5ee3cd08 Mon Sep 17 00:00:00 2001 From: Benoy Bose Date: Tue, 6 May 2025 17:47:06 +0530 Subject: [PATCH 02/11] Update deploy-dev.yaml --- .gitea/workflows/deploy-dev.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitea/workflows/deploy-dev.yaml b/.gitea/workflows/deploy-dev.yaml index 7862777..90c6243 100644 --- a/.gitea/workflows/deploy-dev.yaml +++ b/.gitea/workflows/deploy-dev.yaml @@ -45,7 +45,6 @@ jobs: sed -i "s/#{PHONEPE_API_URL}#/${{ secrets.PHONEPE_API_URL }}/" functions/.env sed -i "s/#{PHONEPE_WEBHOOK_USERNAME}#/${{ secrets.PHONEPE_WEBHOOK_USERNAME }}/" functions/.env sed -i "s/#{PHONEPE_WEBHOOK_PASSWORD}#/${{ secrets.PHONEPE_WEBHOOK_PASSWORD }}/" functions/.env - cat functions/.env - name: "Replace #{SERVICES_RGN}# in all .ts files" run: | From b8b518be9ca2d8646c0c774ff25b0c20b20cf161 Mon Sep 17 00:00:00 2001 From: Benoy Bose Date: Tue, 6 May 2025 17:51:13 +0530 Subject: [PATCH 03/11] Update deploy-dev.yaml --- .gitea/workflows/deploy-dev.yaml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.gitea/workflows/deploy-dev.yaml b/.gitea/workflows/deploy-dev.yaml index 90c6243..2df9af5 100644 --- a/.gitea/workflows/deploy-dev.yaml +++ b/.gitea/workflows/deploy-dev.yaml @@ -40,11 +40,6 @@ jobs: sed -i "s/#{AWS_ACCESS_KEY_ID}#/${{ secrets.AWS_ACCESS_KEY_ID }}/" functions/.env sed -i "s/#{AWS_SECRET_ACCESS_KEY}#/${{ secrets.AWS_SECRET_ACCESS_KEY }}/" functions/.env sed -i "s/#{AWS_REGION}#/${{ secrets.AWS_REGION }}/" functions/.env - sed -i "s/#{PHONEPE_CLIENT_ID}#/${{ secrets.PHONEPE_CLIENT_ID }}/" functions/.env - sed -i "s/#{PHONEPE_CLIENT_SECRET}#/${{ secrets.PHONEPE_CLIENT_SECRET }}/" functions/.env - sed -i "s/#{PHONEPE_API_URL}#/${{ secrets.PHONEPE_API_URL }}/" functions/.env - sed -i "s/#{PHONEPE_WEBHOOK_USERNAME}#/${{ secrets.PHONEPE_WEBHOOK_USERNAME }}/" functions/.env - sed -i "s/#{PHONEPE_WEBHOOK_PASSWORD}#/${{ secrets.PHONEPE_WEBHOOK_PASSWORD }}/" functions/.env cat functions/.env - name: "Replace #{SERVICES_RGN}# in all .ts files" run: | From 868ae3dea4f18f8a4b34f5e23eec2cbec6cf988d Mon Sep 17 00:00:00 2001 From: Benoy Bose Date: Tue, 6 May 2025 17:53:13 +0530 Subject: [PATCH 04/11] Update deploy-dev.yaml --- .gitea/workflows/deploy-dev.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitea/workflows/deploy-dev.yaml b/.gitea/workflows/deploy-dev.yaml index 2df9af5..7e98863 100644 --- a/.gitea/workflows/deploy-dev.yaml +++ b/.gitea/workflows/deploy-dev.yaml @@ -40,6 +40,7 @@ jobs: sed -i "s/#{AWS_ACCESS_KEY_ID}#/${{ secrets.AWS_ACCESS_KEY_ID }}/" functions/.env sed -i "s/#{AWS_SECRET_ACCESS_KEY}#/${{ secrets.AWS_SECRET_ACCESS_KEY }}/" functions/.env sed -i "s/#{AWS_REGION}#/${{ secrets.AWS_REGION }}/" functions/.env + sed -i "s/#{PHONEPE_CLIENT_ID}#/${{ secrets.PHONEPE_CLIENT_ID }}/" functions/.env cat functions/.env - name: "Replace #{SERVICES_RGN}# in all .ts files" run: | From 3adf2a3aac1596883ca3ee5077c66933ddc37a91 Mon Sep 17 00:00:00 2001 From: Benoy Bose Date: Tue, 6 May 2025 17:56:58 +0530 Subject: [PATCH 05/11] Update deploy-dev.yaml --- .gitea/workflows/deploy-dev.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitea/workflows/deploy-dev.yaml b/.gitea/workflows/deploy-dev.yaml index 7e98863..5e39312 100644 --- a/.gitea/workflows/deploy-dev.yaml +++ b/.gitea/workflows/deploy-dev.yaml @@ -41,6 +41,7 @@ jobs: sed -i "s/#{AWS_SECRET_ACCESS_KEY}#/${{ secrets.AWS_SECRET_ACCESS_KEY }}/" functions/.env sed -i "s/#{AWS_REGION}#/${{ secrets.AWS_REGION }}/" functions/.env sed -i "s/#{PHONEPE_CLIENT_ID}#/${{ secrets.PHONEPE_CLIENT_ID }}/" functions/.env + sed -i "s/#{PHONEPE_CLIENT_SECRET}#/${{ secrets.PHONEPE_CLIENT_SECRET }}/" functions/.env cat functions/.env - name: "Replace #{SERVICES_RGN}# in all .ts files" run: | From 4b745d587fd49baf40cfb849251f5ebfd658827e Mon Sep 17 00:00:00 2001 From: Benoy Bose Date: Tue, 6 May 2025 17:59:11 +0530 Subject: [PATCH 06/11] Update deploy-dev.yaml --- .gitea/workflows/deploy-dev.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitea/workflows/deploy-dev.yaml b/.gitea/workflows/deploy-dev.yaml index 5e39312..9a6691d 100644 --- a/.gitea/workflows/deploy-dev.yaml +++ b/.gitea/workflows/deploy-dev.yaml @@ -42,6 +42,7 @@ jobs: sed -i "s/#{AWS_REGION}#/${{ secrets.AWS_REGION }}/" functions/.env sed -i "s/#{PHONEPE_CLIENT_ID}#/${{ secrets.PHONEPE_CLIENT_ID }}/" functions/.env sed -i "s/#{PHONEPE_CLIENT_SECRET}#/${{ secrets.PHONEPE_CLIENT_SECRET }}/" functions/.env + sed -i "s/#{PHONEPE_API_URL}#/${{ secrets.PHONEPE_API_URL }}/" functions/.env cat functions/.env - name: "Replace #{SERVICES_RGN}# in all .ts files" run: | From 9b42fdcac14d6e849a056a6beeb63ff98aed2695 Mon Sep 17 00:00:00 2001 From: Benoy Bose Date: Tue, 6 May 2025 18:04:34 +0530 Subject: [PATCH 07/11] Update deploy-dev.yaml --- .gitea/workflows/deploy-dev.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitea/workflows/deploy-dev.yaml b/.gitea/workflows/deploy-dev.yaml index 9a6691d..5e39312 100644 --- a/.gitea/workflows/deploy-dev.yaml +++ b/.gitea/workflows/deploy-dev.yaml @@ -42,7 +42,6 @@ jobs: sed -i "s/#{AWS_REGION}#/${{ secrets.AWS_REGION }}/" functions/.env sed -i "s/#{PHONEPE_CLIENT_ID}#/${{ secrets.PHONEPE_CLIENT_ID }}/" functions/.env sed -i "s/#{PHONEPE_CLIENT_SECRET}#/${{ secrets.PHONEPE_CLIENT_SECRET }}/" functions/.env - sed -i "s/#{PHONEPE_API_URL}#/${{ secrets.PHONEPE_API_URL }}/" functions/.env cat functions/.env - name: "Replace #{SERVICES_RGN}# in all .ts files" run: | From ccd0ee61bcb130318e4828cb169ca877924a8c4a Mon Sep 17 00:00:00 2001 From: Benoy Bose Date: Tue, 6 May 2025 18:06:46 +0530 Subject: [PATCH 08/11] Update deploy-dev.yaml --- .gitea/workflows/deploy-dev.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitea/workflows/deploy-dev.yaml b/.gitea/workflows/deploy-dev.yaml index 5e39312..9a6691d 100644 --- a/.gitea/workflows/deploy-dev.yaml +++ b/.gitea/workflows/deploy-dev.yaml @@ -42,6 +42,7 @@ jobs: sed -i "s/#{AWS_REGION}#/${{ secrets.AWS_REGION }}/" functions/.env sed -i "s/#{PHONEPE_CLIENT_ID}#/${{ secrets.PHONEPE_CLIENT_ID }}/" functions/.env sed -i "s/#{PHONEPE_CLIENT_SECRET}#/${{ secrets.PHONEPE_CLIENT_SECRET }}/" functions/.env + sed -i "s/#{PHONEPE_API_URL}#/${{ secrets.PHONEPE_API_URL }}/" functions/.env cat functions/.env - name: "Replace #{SERVICES_RGN}# in all .ts files" run: | From 1e040212e49154e3e2e3bf54dab4e4a8b714e0ed Mon Sep 17 00:00:00 2001 From: Benoy Bose Date: Tue, 6 May 2025 18:17:09 +0530 Subject: [PATCH 09/11] Update deploy-dev.yaml --- .gitea/workflows/deploy-dev.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitea/workflows/deploy-dev.yaml b/.gitea/workflows/deploy-dev.yaml index 9a6691d..90c6243 100644 --- a/.gitea/workflows/deploy-dev.yaml +++ b/.gitea/workflows/deploy-dev.yaml @@ -43,6 +43,8 @@ jobs: sed -i "s/#{PHONEPE_CLIENT_ID}#/${{ secrets.PHONEPE_CLIENT_ID }}/" functions/.env sed -i "s/#{PHONEPE_CLIENT_SECRET}#/${{ secrets.PHONEPE_CLIENT_SECRET }}/" functions/.env sed -i "s/#{PHONEPE_API_URL}#/${{ secrets.PHONEPE_API_URL }}/" functions/.env + sed -i "s/#{PHONEPE_WEBHOOK_USERNAME}#/${{ secrets.PHONEPE_WEBHOOK_USERNAME }}/" functions/.env + sed -i "s/#{PHONEPE_WEBHOOK_PASSWORD}#/${{ secrets.PHONEPE_WEBHOOK_PASSWORD }}/" functions/.env cat functions/.env - name: "Replace #{SERVICES_RGN}# in all .ts files" run: | From 5f89a5cda4977a7afb76af3f3aa0c9d50dd96e9b Mon Sep 17 00:00:00 2001 From: Sharon Dcruz Date: Wed, 7 May 2025 07:20:27 +0000 Subject: [PATCH 10/11] Email with attachment issue fixed (#25) Reviewed-on: https://git.cosqnet.com/cosqnet/fitlien-services/pulls/25 Co-authored-by: Sharon Dcruz Co-committed-by: Sharon Dcruz --- functions/package-lock.json | 24 ++++------- functions/package.json | 4 +- functions/src/email/sendEmailSES.ts | 67 +++++++++++++++++++++++------ 3 files changed, 65 insertions(+), 30 deletions(-) diff --git a/functions/package-lock.json b/functions/package-lock.json index 28053b5..b53c64d 100644 --- a/functions/package-lock.json +++ b/functions/package-lock.json @@ -11,14 +11,14 @@ "@aws-sdk/client-ses": "^3.798.0", "@types/node-fetch": "^2.6.12", "aws-sdk": "^2.1692.0", - "axios": "^1.8.4", + "axios": "^1.9.0", "cors": "^2.8.5", "firebase-admin": "^12.6.0", "firebase-functions": "^6.0.1", "form-data": "^4.0.1", "functions": "file:", "html-to-text": "^9.0.5", - "long": "^4.0.0", + "long": "^5.3.2", "mailgun.js": "^10.4.0", "node-fetch": "^2.7.0", "pdfjs-dist": "^5.0.375", @@ -1373,12 +1373,6 @@ "node": ">=6" } }, - "node_modules/@grpc/proto-loader/node_modules/long": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", - "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", - "optional": true - }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -2755,6 +2749,7 @@ "integrity": "sha512-eQs9RsucA/LNjnMoJvWG/nXa7Pot/RbBzilF/QRIU/xRl+0ApxrSUFsV5lmf01SvSlqMzJ7Zwxe440wmz2SJGA==", "deprecated": "This is a stub types definition. long provides its own type definitions, so you do not need this installed.", "dev": true, + "license": "MIT", "dependencies": { "long": "*" } @@ -3051,6 +3046,7 @@ "version": "1.9.0", "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz", "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==", + "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", @@ -6059,9 +6055,10 @@ "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" }, "node_modules/long": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", - "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" }, "node_modules/lru-cache": { "version": "5.1.1", @@ -6687,11 +6684,6 @@ "node": ">=12.0.0" } }, - "node_modules/protobufjs/node_modules/long": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", - "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==" - }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", diff --git a/functions/package.json b/functions/package.json index 59f0fdf..f83c0fd 100644 --- a/functions/package.json +++ b/functions/package.json @@ -18,14 +18,14 @@ "@aws-sdk/client-ses": "^3.798.0", "@types/node-fetch": "^2.6.12", "aws-sdk": "^2.1692.0", - "axios": "^1.8.4", + "axios": "^1.9.0", "cors": "^2.8.5", "firebase-admin": "^12.6.0", "firebase-functions": "^6.0.1", "form-data": "^4.0.1", "functions": "file:", "html-to-text": "^9.0.5", - "long": "^4.0.0", + "long": "^5.3.2", "mailgun.js": "^10.4.0", "node-fetch": "^2.7.0", "pdfjs-dist": "^5.0.375", diff --git a/functions/src/email/sendEmailSES.ts b/functions/src/email/sendEmailSES.ts index a99c3b1..78e6875 100644 --- a/functions/src/email/sendEmailSES.ts +++ b/functions/src/email/sendEmailSES.ts @@ -6,6 +6,7 @@ import { SESClient } from "@aws-sdk/client-ses"; import { SendEmailCommand, SendRawEmailCommand } from "@aws-sdk/client-ses"; import { HttpsError } from "firebase-functions/v2/https"; import * as mime from 'mime-types'; +import axios from 'axios'; const logger = getLogger(); const corsHandler = getCorsHandler(); @@ -18,6 +19,8 @@ interface EmailRequest { from: string; replyTo?: string; attachments?: Attachment[]; + fileUrl?: string; + fileName?: string; } interface Attachment { @@ -121,31 +124,71 @@ async function sendEmailWithAttachments(data: EmailRequest, recipients: string[] return { messageId: result.MessageId }; } +async function downloadFileFromUrl(url: string): Promise { + try { + const response = await axios.get(url, { responseType: 'arraybuffer' }); + return Buffer.from(response.data); + } catch (error) { + logger.error(`Error downloading file from URL: ${error}`); + throw new Error(`Failed to download file: ${error}`); + } +} + export const sendEmailSES = onRequest({ region: 'asia-south1' }, (request: Request, response) => { return corsHandler(request, response, async () => { - const toAddress = request.body.toAddress; - const subject = request.body.subject; - const message = request.body.message; - const data: EmailRequest = { - to: toAddress, - subject: subject, - html: message, - text: stripHtml(message), - from: process.env.SES_FROM_EMAIL || 'support@fitlien.com', - replyTo: process.env.SES_REPLY_TO_EMAIL || 'support@fitlien.com', - attachments: request.body.attachments as Attachment[] || [] - }; try { + const toAddress = request.body.toAddress; + const subject = request.body.subject; + const message = request.body.message; + + // Initialize data with basic fields + const data: EmailRequest = { + to: toAddress, + html: message, + subject: subject, + text: stripHtml(message), + from: process.env.SES_FROM_EMAIL || 'support@fitlien.com', + replyTo: process.env.SES_REPLY_TO_EMAIL || 'support@fitlien.com', + attachments: request.body.attachments as Attachment[] || [] + }; + + // Handle file URL if provided + if (request.body.fileUrl && request.body.fileName) { + logger.info(`Downloading attachment from URL: ${request.body.fileUrl}`); + try { + const fileContent = await downloadFileFromUrl(request.body.fileUrl); + + // If attachments array doesn't exist, create it + if (!data.attachments) { + data.attachments = []; + } + + // Add the downloaded file as an attachment + data.attachments.push({ + filename: request.body.fileName, + content: fileContent, + contentType: mime.lookup(request.body.fileName) || 'application/octet-stream' + }); + + logger.info(`Successfully downloaded attachment: ${request.body.fileName}`); + } catch (downloadError) { + logger.error(`Failed to download attachment: ${downloadError}`); + throw new Error(`Failed to process attachment: ${downloadError}`); + } + } + if (!data.to || !data.subject || !data.html || !data.from) { throw new HttpsError( 'invalid-argument', 'Missing required email fields' ); } + logger.info(`Sending Email '${data.subject}' to '${data.to}' from '${data.from}'`); const recipients = Array.isArray(data.to) ? data.to : [data.to]; + if (data.attachments && data.attachments.length > 0) { const messageResult = await sendEmailWithAttachments(data, recipients); response.status(200).json(messageResult); From aee28a60500546000f15dda50d021fac1ac3b6d3 Mon Sep 17 00:00:00 2001 From: Allen T J Date: Wed, 14 May 2025 13:09:09 +0000 Subject: [PATCH 11/11] phonepe (#26) Co-authored-by: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com> Reviewed-on: https://git.cosqnet.com/cosqnet/fitlien-services/pulls/26 --- functions/src/clientRegistration/clientRegistration.ts | 2 +- functions/src/payments/phonepe/webhook.ts | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/functions/src/clientRegistration/clientRegistration.ts b/functions/src/clientRegistration/clientRegistration.ts index 8ed51fb..fe2fafc 100644 --- a/functions/src/clientRegistration/clientRegistration.ts +++ b/functions/src/clientRegistration/clientRegistration.ts @@ -82,7 +82,7 @@ export const registerClient = onRequest({ phoneNumber: formattedPhoneNumber, }; - await admin.firestore().collection('client_profile').doc(clientUid).set(clientData); + await admin.firestore().collection('client_profiles').doc(clientUid).set(clientData); return res.status(201).json({ success: true, diff --git a/functions/src/payments/phonepe/webhook.ts b/functions/src/payments/phonepe/webhook.ts index d8696dc..9f522f9 100644 --- a/functions/src/payments/phonepe/webhook.ts +++ b/functions/src/payments/phonepe/webhook.ts @@ -10,6 +10,13 @@ export const phonePeWebhook = onRequest({ region: '#{SERVICES_RGN}#' }, async (request: Request, response) => { try { + + logger.info('Received webhook request', { + headers: request.headers, + body: request.body, + method: request.method + }); + const authHeader = request.headers['authorization'] as string; const username = process.env.PHONEPE_WEBHOOK_USERNAME; const password = process.env.PHONEPE_WEBHOOK_PASSWORD; @@ -20,14 +27,12 @@ export const phonePeWebhook = onRequest({ return; } - // Calculate expected authorization value const credentialString = `${username}:${password}`; const expectedAuth = crypto .createHash('sha256') .update(credentialString) .digest('hex'); - // PhonePe may send the header with a prefix like "SHA256 " or just the hash const receivedAuth = authHeader.replace(/^SHA256\s+/i, ''); if (receivedAuth.toLowerCase() !== expectedAuth.toLowerCase()) {