Compare commits

..

266 Commits

Author SHA1 Message Date
61f0d29f37 Merge branch 'qa'
All checks were successful
Deploy FitLien services / Deploy (push) Successful in 3m30s
2025-09-11 14:32:04 +05:30
209c7c65b0 Merge branch 'dev' into qa
All checks were successful
Deploy FitLien services to QA / Deploy to QA (push) Successful in 3m37s
2025-09-11 14:25:39 +05:30
ad31bc8e80 notification-bug-fix (#114)
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 4m30s
Co-authored-by: Dhansh A S <dhanshas@cosq.net>
Reviewed-on: #114
Reviewed-by: Dhansh A S <dhanshas@cosq.net>
Co-authored-by: Sharon Dcruz <sharondcruz@cosq.net>
Co-committed-by: Sharon Dcruz <sharondcruz@cosq.net>
2025-09-11 08:47:46 +00:00
f0d167a671 notification-bug-fix (#113)
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 4m32s
Co-authored-by: Dhansh A S <dhanshas@cosq.net>
Reviewed-on: #113
Reviewed-by: Dhansh A S <dhanshas@cosq.net>
Co-authored-by: Sharon Dcruz <sharondcruz@cosq.net>
Co-committed-by: Sharon Dcruz <sharondcruz@cosq.net>
2025-09-09 09:06:58 +00:00
e762d0fb79 notification-bug-fix (#112)
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 3m47s
Co-authored-by: Dhansh A S <dhanshas@cosq.net>
Reviewed-on: #112
Reviewed-by: Allen T J <allentj@cosq.net>
Co-authored-by: Sharon Dcruz <sharondcruz@cosq.net>
Co-committed-by: Sharon Dcruz <sharondcruz@cosq.net>
2025-08-26 06:27:20 +00:00
5ca05c6490 Merge branch 'dev'
All checks were successful
Deploy FitLien services / Deploy (push) Successful in 4m1s
2025-08-25 18:39:53 +05:30
7ec65e4ab1 Merge branch 'dev' into qa
All checks were successful
Deploy FitLien services to QA / Deploy to QA (push) Successful in 4m1s
2025-08-25 18:35:11 +05:30
b41431b151 notification-bug-fix (#111)
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 1m48s
Co-authored-by: Dhansh A S <dhanshas@cosq.net>
Reviewed-on: #111
Reviewed-by: Allen T J <allentj@cosq.net>
Co-authored-by: Sharon Dcruz <sharondcruz@cosq.net>
Co-committed-by: Sharon Dcruz <sharondcruz@cosq.net>
2025-08-25 09:57:03 +00:00
218ff1d02e notification-bug-fix (#110)
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 3m58s
Co-authored-by: Dhansh A S <dhanshas@cosq.net>
Reviewed-on: #110
Reviewed-by: Allen T J <allentj@cosq.net>
Co-authored-by: Sharon Dcruz <sharondcruz@cosq.net>
Co-committed-by: Sharon Dcruz <sharondcruz@cosq.net>
2025-08-25 09:50:08 +00:00
4f71ca273b Merge branch 'qa'
All checks were successful
Deploy FitLien services / Deploy (push) Successful in 1m40s
2025-08-22 17:39:15 +05:30
4d52d1c7f8 Merged from dev
All checks were successful
Deploy FitLien services to QA / Deploy to QA (push) Successful in 1m40s
2025-08-22 17:36:45 +05:30
aee40521d3 Merge branch 'dev' into qa 2025-08-22 17:35:21 +05:30
a450e93e2b notification-bug-fix (#109)
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 1m39s
Co-authored-by: Dhansh A S <dhanshas@cosq.net>
Reviewed-on: #109
Co-authored-by: Sharon Dcruz <sharondcruz@cosq.net>
Co-committed-by: Sharon Dcruz <sharondcruz@cosq.net>
2025-08-22 12:00:31 +00:00
36015d2b83 Removed npm install at root level
All checks were successful
Deploy FitLien services / Deploy (push) Successful in 3m42s
2025-08-22 17:18:57 +05:30
51fa0825ca Removed npm install from root
Some checks failed
Deploy FitLien services to QA / Deploy to QA (push) Successful in 3m36s
Deploy FitLien services / Deploy (push) Failing after 8s
2025-08-22 17:04:54 +05:30
3e455fc83a Merge branch 'dev' into qa
Some checks failed
Deploy FitLien services to QA / Deploy to QA (push) Failing after 8s
2025-08-22 16:58:23 +05:30
0672a19a60 notification-bug-fix (#108)
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 2m14s
Co-authored-by: Dhansh A S <dhanshas@cosq.net>
Reviewed-on: #108
Reviewed-by: Dhansh A S <dhanshas@cosq.net>
Co-authored-by: Sharon Dcruz <sharondcruz@cosq.net>
Co-committed-by: Sharon Dcruz <sharondcruz@cosq.net>
2025-08-22 11:13:19 +00:00
172fa2edae notification-bug-fix (#107)
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 1m53s
Co-authored-by: Dhansh A S <dhanshas@cosq.net>
Reviewed-on: #107
Co-authored-by: Sharon Dcruz <sharondcruz@cosq.net>
Co-committed-by: Sharon Dcruz <sharondcruz@cosq.net>
2025-08-20 13:38:54 +00:00
d0c00d8172 notification-bug-fix (#106)
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 3m42s
Co-authored-by: Dhansh A S <dhanshas@cosq.net>
Reviewed-on: #106
Co-authored-by: Sharon Dcruz <sharondcruz@cosq.net>
Co-committed-by: Sharon Dcruz <sharondcruz@cosq.net>
2025-08-20 13:30:53 +00:00
f93931867c notification-bug-fix (#105)
Some checks failed
Deploy FitLien services to Dev / Deploy to Dev (push) Failing after 1m52s
Co-authored-by: Dhansh A S <dhanshas@cosq.net>
Reviewed-on: #105
Co-authored-by: Sharon Dcruz <sharondcruz@cosq.net>
Co-committed-by: Sharon Dcruz <sharondcruz@cosq.net>
2025-08-20 13:22:29 +00:00
70d76bab2e notification-bug-fix (#104)
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 1m32s
Co-authored-by: Dhansh A S <dhanshas@cosq.net>
Reviewed-on: #104
Co-authored-by: Sharon Dcruz <sharondcruz@cosq.net>
Co-committed-by: Sharon Dcruz <sharondcruz@cosq.net>
2025-08-19 11:51:37 +00:00
308cb0fab6 notification-bug-fix (#103)
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 3m42s
Co-authored-by: Dhansh A S <dhanshas@cosq.net>
Reviewed-on: #103
Co-authored-by: Sharon Dcruz <sharondcruz@cosq.net>
Co-committed-by: Sharon Dcruz <sharondcruz@cosq.net>
2025-08-19 11:43:38 +00:00
165cd74a17 notification-bug-fix (#102)
Some checks failed
Deploy FitLien services to Dev / Deploy to Dev (push) Failing after 1m29s
Co-authored-by: Dhansh A S <dhanshas@cosq.net>
Reviewed-on: #102
Reviewed-by: Dhansh A S <dhanshas@cosq.net>
Co-authored-by: Sharon Dcruz <sharondcruz@cosq.net>
Co-committed-by: Sharon Dcruz <sharondcruz@cosq.net>
2025-08-19 11:18:53 +00:00
b190a371b6 notification-bug-fix (#101)
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 1m34s
Co-authored-by: Dhansh A S <dhanshas@cosq.net>
Reviewed-on: #101
Co-authored-by: Sharon Dcruz <sharondcruz@cosq.net>
Co-committed-by: Sharon Dcruz <sharondcruz@cosq.net>
2025-08-19 10:21:00 +00:00
5543ba5e7a notification-bug-fix (#100)
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 3m40s
Co-authored-by: Dhansh A S <dhanshas@cosq.net>
Reviewed-on: #100
Co-authored-by: Sharon Dcruz <sharondcruz@cosq.net>
Co-committed-by: Sharon Dcruz <sharondcruz@cosq.net>
2025-08-19 09:52:14 +00:00
5d6824a6f4 notification-bug-fix (#99)
Some checks failed
Deploy FitLien services to Dev / Deploy to Dev (push) Failing after 2m0s
Co-authored-by: Dhansh A S <dhanshas@cosq.net>
Reviewed-on: #99
Co-authored-by: Sharon Dcruz <sharondcruz@cosq.net>
Co-committed-by: Sharon Dcruz <sharondcruz@cosq.net>
2025-08-19 09:43:44 +00:00
abf7a04633 notification-bug-fix (#98)
Some checks failed
Deploy FitLien services to Dev / Deploy to Dev (push) Failing after 1m25s
Co-authored-by: Dhansh A S <dhanshas@cosq.net>
Reviewed-on: #98
Co-authored-by: Sharon Dcruz <sharondcruz@cosq.net>
Co-committed-by: Sharon Dcruz <sharondcruz@cosq.net>
2025-08-19 07:21:41 +00:00
835f478665 notification-bug-fix (#97)
Some checks failed
Deploy FitLien services to Dev / Deploy to Dev (push) Failing after 1m28s
Co-authored-by: Dhansh A S <dhanshas@cosq.net>
Reviewed-on: #97
Co-authored-by: Sharon Dcruz <sharondcruz@cosq.net>
Co-committed-by: Sharon Dcruz <sharondcruz@cosq.net>
2025-08-19 07:11:25 +00:00
5f43a86036 notification-bug-fix (#96)
Some checks failed
Deploy FitLien services to Dev / Deploy to Dev (push) Failing after 1m24s
Reviewed-on: #96
Co-authored-by: Sharon Dcruz <sharondcruz@cosq.net>
Co-committed-by: Sharon Dcruz <sharondcruz@cosq.net>
2025-08-19 07:06:26 +00:00
1e09f7a676 notification-bug-fix (#95)
Some checks failed
Deploy FitLien services to Dev / Deploy to Dev (push) Failing after 1m34s
Reviewed-on: #95
Co-authored-by: Sharon Dcruz <sharondcruz@cosq.net>
Co-committed-by: Sharon Dcruz <sharondcruz@cosq.net>
2025-08-19 06:54:45 +00:00
5e48f695f8 notification-bug-fix (#94)
Some checks failed
Deploy FitLien services to Dev / Deploy to Dev (push) Failing after 1m31s
Reviewed-on: #94
Co-authored-by: Sharon Dcruz <sharondcruz@cosq.net>
Co-committed-by: Sharon Dcruz <sharondcruz@cosq.net>
2025-08-19 06:51:02 +00:00
e483b7ad46 notification-bug-fix (#93)
Some checks failed
Deploy FitLien services to Dev / Deploy to Dev (push) Failing after 1m34s
Reviewed-on: #93
Co-authored-by: Sharon Dcruz <sharondcruz@cosq.net>
Co-committed-by: Sharon Dcruz <sharondcruz@cosq.net>
2025-08-19 06:42:11 +00:00
cd59b9890d notification-bug-fix (#92)
Some checks failed
Deploy FitLien services to Dev / Deploy to Dev (push) Failing after 1m5s
Reviewed-on: #92
Reviewed-by: Allen T J <allentj@cosq.net>
Co-authored-by: Sharon Dcruz <sharondcruz@cosq.net>
Co-committed-by: Sharon Dcruz <sharondcruz@cosq.net>
2025-08-19 06:23:43 +00:00
939567f7c0 notification-bug-fix (#91)
Some checks failed
Deploy FitLien services to Dev / Deploy to Dev (push) Failing after 1m42s
Reviewed-on: #91
Reviewed-by: Allen T J <allentj@cosq.net>
Co-authored-by: Sharon Dcruz <sharondcruz@cosq.net>
Co-committed-by: Sharon Dcruz <sharondcruz@cosq.net>
2025-08-19 05:39:54 +00:00
7c494154ba notification-bug-fix (#90)
Some checks failed
Deploy FitLien services to Dev / Deploy to Dev (push) Failing after 1m38s
Reviewed-on: #90
Reviewed-by: Allen T J <allentj@cosq.net>
Co-authored-by: Sharon Dcruz <sharondcruz@cosq.net>
Co-committed-by: Sharon Dcruz <sharondcruz@cosq.net>
2025-08-19 04:59:34 +00:00
8f5956a825 notification-bug-fix (#89)
Some checks failed
Deploy FitLien services to Dev / Deploy to Dev (push) Failing after 1m30s
Reviewed-on: #89
Reviewed-by: Allen T J <allentj@cosq.net>
Co-authored-by: Sharon Dcruz <sharondcruz@cosq.net>
Co-committed-by: Sharon Dcruz <sharondcruz@cosq.net>
2025-08-19 04:49:31 +00:00
5ff7b8bb84 notification-bug-fix (#88)
Some checks failed
Deploy FitLien services to Dev / Deploy to Dev (push) Failing after 1m44s
Reviewed-on: #88
Reviewed-by: Allen T J <allentj@cosq.net>
Co-authored-by: Sharon Dcruz <sharondcruz@cosq.net>
Co-committed-by: Sharon Dcruz <sharondcruz@cosq.net>
2025-08-19 04:30:40 +00:00
7259e67833 REMOVED clean install from project root
Some checks failed
Deploy FitLien services to Dev / Deploy to Dev (push) Failing after 1m24s
2025-08-18 19:20:36 +05:30
81c5241e95 Merge branch 'dev' of cosqnet.com:cosqnet/fitlien-services into dev
Some checks failed
Deploy FitLien services to Dev / Deploy to Dev (push) Failing after 8s
2025-08-18 19:16:41 +05:30
209354ec6b UPDATED firebase-functions package 2025-08-18 19:16:18 +05:30
39ea6dcafb notification-bug-fix (#87)
Some checks failed
Deploy FitLien services to Dev / Deploy to Dev (push) Failing after 1m55s
Reviewed-on: #87
Reviewed-by: Allen T J <allentj@cosq.net>
Co-authored-by: Sharon Dcruz <sharondcruz@cosq.net>
Co-committed-by: Sharon Dcruz <sharondcruz@cosq.net>
2025-08-18 13:42:26 +00:00
f6b1545cf6 Merge branch 'dev' into qa
Some checks failed
Deploy FitLien services to QA / Deploy to QA (push) Failing after 1m33s
2025-08-18 19:05:38 +05:30
195262a6de Merge branch 'qa'
Some checks failed
Deploy FitLien services / Deploy (push) Failing after 2m8s
Deploy FitLien services to QA / Deploy to QA (push) Failing after 1m42s
2025-08-18 18:53:46 +05:30
d8ae223cce notification-bug-fix (#86)
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 3m37s
Reviewed-on: #86
Reviewed-by: Dhansh A S <dhanshas@cosq.net>
Co-authored-by: Sharon Dcruz <sharondcruz@cosq.net>
Co-committed-by: Sharon Dcruz <sharondcruz@cosq.net>
2025-08-14 08:35:40 +00:00
c2914b16bb notification-bug-fix (#85)
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 3m38s
Reviewed-on: #85
Reviewed-by: Dhansh A S <dhanshas@cosq.net>
Co-authored-by: Sharon Dcruz <sharondcruz@cosq.net>
Co-committed-by: Sharon Dcruz <sharondcruz@cosq.net>
2025-08-14 06:41:07 +00:00
b66f1603cc notification-bug-fix (#84)
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 4m10s
Reviewed-on: #84
Co-authored-by: Sharon Dcruz <sharondcruz@cosq.net>
Co-committed-by: Sharon Dcruz <sharondcruz@cosq.net>
2025-08-14 05:30:52 +00:00
d3c9e86c7c Merge branch 'dev'
All checks were successful
Deploy FitLien services / Deploy (push) Successful in 4m10s
2025-08-08 18:53:15 +05:30
4cf5692386 plan-expiry-in-two-days (#83)
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 4m20s
Reviewed-on: #83
Reviewed-by: Dhansh A S <dhanshas@cosq.net>
Co-authored-by: Sharon Dcruz <sharondcruz@cosq.net>
Co-committed-by: Sharon Dcruz <sharondcruz@cosq.net>
2025-08-08 07:55:52 +00:00
8b308fb9a6 Merge branch 'dev'
All checks were successful
Deploy FitLien services / Deploy (push) Successful in 5m14s
2025-08-07 20:17:17 +05:30
237dd8a263 Changes Updated (#82)
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 3m28s
Reviewed-on: #82
Co-authored-by: Sharon Dcruz <sharondcruz@cosq.net>
Co-committed-by: Sharon Dcruz <sharondcruz@cosq.net>
2025-08-05 14:03:40 +00:00
fb23661080 expiry-using-payment (#81)
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 3m54s
Reviewed-on: #81
Reviewed-by: Dhansh A S <dhanshas@cosq.net>
Co-authored-by: Sharon Dcruz <sharondcruz@cosq.net>
Co-committed-by: Sharon Dcruz <sharondcruz@cosq.net>
2025-08-05 13:20:00 +00:00
cba945c282 expiry-using-payment (#80)
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 4m5s
Reviewed-on: #80
Reviewed-by: Dhansh A S <dhanshas@cosq.net>
Co-authored-by: Sharon Dcruz <sharondcruz@cosq.net>
Co-committed-by: Sharon Dcruz <sharondcruz@cosq.net>
2025-08-05 09:22:10 +00:00
ef166a209c expiry-using-payment (#79)
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 3m54s
Reviewed-on: #79
Reviewed-by: Dhansh A S <dhanshas@cosq.net>
Co-authored-by: Sharon Dcruz <sharondcruz@cosq.net>
Co-committed-by: Sharon Dcruz <sharondcruz@cosq.net>
2025-08-05 06:16:47 +00:00
5d47a78baa expiry-notification (#78)
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 4m4s
Reviewed-on: #78
Reviewed-by: Dhansh A S <dhanshas@cosq.net>
Co-authored-by: Sharon Dcruz <sharondcruz@cosq.net>
Co-committed-by: Sharon Dcruz <sharondcruz@cosq.net>
2025-08-04 07:30:25 +00:00
b594579158 expiry-notification (#77)
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 4m8s
Reviewed-on: #77
Co-authored-by: Sharon Dcruz <sharondcruz@cosq.net>
Co-committed-by: Sharon Dcruz <sharondcruz@cosq.net>
2025-08-04 05:47:40 +00:00
943cff74d5 expiry-notification (#76)
Some checks failed
Deploy FitLien services to Dev / Deploy to Dev (push) Failing after 1m40s
Reviewed-on: #76
Co-authored-by: Sharon Dcruz <sharondcruz@cosq.net>
Co-committed-by: Sharon Dcruz <sharondcruz@cosq.net>
2025-08-04 04:43:03 +00:00
1fc089a7cb expiry-notification (#75)
Some checks failed
Deploy FitLien services to Dev / Deploy to Dev (push) Failing after 1m42s
Reviewed-on: #75
Reviewed-by: Dhansh A S <dhanshas@cosq.net>
Co-authored-by: Sharon Dcruz <sharondcruz@cosq.net>
Co-committed-by: Sharon Dcruz <sharondcruz@cosq.net>
2025-08-01 16:33:09 +00:00
76a75330c8 Changes Updated (#74)
Some checks failed
Deploy FitLien services to Dev / Deploy to Dev (push) Failing after 1m32s
Reviewed-on: #74
Reviewed-by: Dhansh A S <dhanshas@cosq.net>
Co-authored-by: Sharon Dcruz <sharondcruz@cosq.net>
Co-committed-by: Sharon Dcruz <sharondcruz@cosq.net>
2025-08-01 13:20:36 +00:00
ca08d83f98 notification-issue (#73)
Some checks failed
Deploy FitLien services to Dev / Deploy to Dev (push) Failing after 2m3s
PLan Expiry Notification added

Reviewed-on: #73
Reviewed-by: Dhansh A S <dhanshas@cosq.net>
Co-authored-by: Sharon Dcruz <sharondcruz@cosq.net>
Co-committed-by: Sharon Dcruz <sharondcruz@cosq.net>
2025-07-29 13:50:50 +00:00
0a32e15d05 notification-issue (#72)
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 4m29s
Reviewed-on: #72
Reviewed-by: Dhansh A S <dhanshas@cosq.net>
Co-authored-by: Sharon Dcruz <sharondcruz@cosq.net>
Co-committed-by: Sharon Dcruz <sharondcruz@cosq.net>
2025-07-29 07:46:19 +00:00
3223efc392 notification-issue (#71)
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 4m23s
Reviewed-on: #71
Reviewed-by: Dhansh A S <dhanshas@cosq.net>
Co-authored-by: Sharon Dcruz <sharondcruz@cosq.net>
Co-committed-by: Sharon Dcruz <sharondcruz@cosq.net>
2025-07-28 13:01:53 +00:00
9c2431fb7b Changes Updated (#70)
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 4m48s
Reviewed-on: #70
Co-authored-by: Sharon Dcruz <sharondcruz@cosq.net>
Co-committed-by: Sharon Dcruz <sharondcruz@cosq.net>
2025-07-25 07:57:10 +00:00
7492cdedc1 Accesslog done (#69)
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 4m11s
Reviewed-on: #69
Co-authored-by: DhanshCOSQ <dhanshas@cosq.net>
Co-committed-by: DhanshCOSQ <dhanshas@cosq.net>
2025-07-16 05:31:33 +00:00
d8bf928da8 feature/fitlien-updated-index (#68)
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 4m4s
Reviewed-on: #68
Co-authored-by: DhanshCOSQ <dhanshas@cosq.net>
Co-committed-by: DhanshCOSQ <dhanshas@cosq.net>
2025-07-03 08:45:25 +00:00
9d51393aa5 Merge branch 'dev'
All checks were successful
Deploy FitLien services / Deploy (push) Successful in 3m39s
2025-07-02 21:28:42 +05:30
cc5b6d6987 Merge branch 'dev' into qa
All checks were successful
Deploy FitLien services to QA / Deploy to QA (push) Successful in 3m48s
2025-07-02 21:28:06 +05:30
cbe59ee4f1 updated index (#67)
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 2m3s
Reviewed-on: #67
Co-authored-by: DhanshCOSQ <dhanshas@cosq.net>
Co-committed-by: DhanshCOSQ <dhanshas@cosq.net>
2025-07-02 15:56:59 +00:00
a3241afd45 phonepe (#66)
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 4m0s
Co-authored-by: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com>
Reviewed-on: #66
2025-06-25 14:28:07 +00:00
d49be73784 feature/fitlien-email (#65)
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 3m17s
Co-authored-by: Sharon Dcruz <sharondcruz@cosq.net>
Reviewed-on: #65
Co-authored-by: DhanshCOSQ <dhanshas@cosq.net>
Co-committed-by: DhanshCOSQ <dhanshas@cosq.net>
2025-06-24 12:23:03 +00:00
fe328e2e64 Changed services region (#64)
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 3m21s
Reviewed-on: #64
Reviewed-by: Dhansh A S <dhanshas@cosq.net>
Co-authored-by: Sharon Dcruz <sharondcruz@cosq.net>
Co-committed-by: Sharon Dcruz <sharondcruz@cosq.net>
2025-06-24 12:13:31 +00:00
c80fb0c4b4 phonepe (#63)
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 3m32s
Co-authored-by: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com>
Reviewed-on: #63
2025-06-24 11:53:03 +00:00
ef2cd80226 Merge branch 'qa'
All checks were successful
Deploy FitLien services / Deploy (push) Successful in 4m2s
2025-06-24 15:19:54 +05:30
f19e3b012d Merge branch 'dev' into qa
All checks were successful
Deploy FitLien services to QA / Deploy to QA (push) Successful in 3m53s
2025-06-24 15:19:20 +05:30
9cbdcf3c37 Update package-lock.json
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 3m44s
2025-06-24 15:18:54 +05:30
f518d4bc9d phonepe (#62)
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 3m30s
Co-authored-by: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com>
Reviewed-on: #62
2025-06-24 08:55:48 +00:00
56f3f1e0bd phonepe (#61)
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 4m14s
Co-authored-by: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com>
Reviewed-on: #61
2025-06-24 08:40:11 +00:00
f2e37e88ed Merge branch 'dev'
All checks were successful
Deploy FitLien services / Deploy (push) Successful in 4m24s
2025-06-12 19:47:27 +05:30
a0134466ee Merge branch 'dev' into qa
All checks were successful
Deploy FitLien services to QA / Deploy to QA (push) Successful in 3m42s
2025-06-12 19:45:40 +05:30
b9346de5e5 Setting private key for prod
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 2m6s
2025-06-12 19:44:35 +05:30
03f6941531 feature/essl-password (#60)
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 3m34s
Reviewed-on: #60
Co-authored-by: DhanshCOSQ <dhanshas@cosq.net>
Co-committed-by: DhanshCOSQ <dhanshas@cosq.net>
2025-06-12 09:41:37 +00:00
e27e189f7e phone number updated (#59)
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 3m45s
Reviewed-on: #59
Co-authored-by: Sharon Dcruz <sharondcruz@cosq.net>
Co-committed-by: Sharon Dcruz <sharondcruz@cosq.net>
2025-06-12 08:25:49 +00:00
3e1a51c093 feature/essl-password (#58)
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 3m50s
Reviewed-on: #58
Co-authored-by: DhanshCOSQ <dhanshas@cosq.net>
Co-committed-by: DhanshCOSQ <dhanshas@cosq.net>
2025-06-11 09:45:33 +00:00
77d642eac1 password complete (#57)
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 4m42s
Reviewed-on: #57
Co-authored-by: DhanshCOSQ <dhanshas@cosq.net>
Co-committed-by: DhanshCOSQ <dhanshas@cosq.net>
2025-06-11 08:12:30 +00:00
5e680c3947 Adding createGetEmployeePunchLogsRequest
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 4m12s
2025-06-09 18:47:58 +05:30
cbe98e8cd0 Replaced esslDeleteEmployee to esslDeleteUser
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 3m54s
2025-06-09 15:26:53 +05:30
72b6bb3cd6 Added esslDeleteEmployee
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 4m13s
2025-06-09 15:08:00 +05:30
bb3e966daf Adding updateEmployeeEx
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 4m14s
2025-06-09 14:24:19 +05:30
09068fe731 Update .gitignore
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 4m40s
2025-06-09 10:04:35 +05:30
47bd8610d2 Adding esslGetUserDetails 2025-06-09 10:03:16 +05:30
e517367967 Renamed modules
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 3m58s
2025-06-01 09:38:57 +05:30
ee27b1b3cc Adding missing type inferences 2025-06-01 09:36:02 +05:30
ecbe9d184b Merge branch 'dev' into qa
All checks were successful
Deploy FitLien services to QA / Deploy to QA (push) Successful in 3m47s
Deploy FitLien services / Deploy (push) Successful in 4m30s
2025-05-30 12:01:13 +05:30
3f3b42e2ec Adding "database": "(default)"
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 1m50s
2025-05-30 11:58:02 +05:30
866bdc1b77 Adding pipeline for main
Some checks failed
Deploy FitLien services to Dev / Deploy to Dev (push) Failing after 1m41s
2025-05-30 11:39:54 +05:30
0ccb7dc16f phonepe (#56)
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 1m43s
Co-authored-by: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com>
Reviewed-on: #56
2025-05-29 09:25:34 +00:00
22beb38465 phonepe (#55)
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 3m53s
Co-authored-by: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com>
Reviewed-on: #55
2025-05-28 11:19:52 +00:00
7a796243b0 Merge branch 'dev' into qa
All checks were successful
Deploy FitLien services to Dev / Deploy to QA (push) Successful in 4m13s
2025-05-28 10:55:32 +05:30
c43c9cf26c Removed dependency of mailgun
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 4m4s
2025-05-28 10:55:08 +05:30
3f4b307431 phonepe (#54)
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 3m38s
Co-authored-by: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com>
Reviewed-on: #54
2025-05-27 19:14:40 +00:00
ee1619ae38 phonepe (#53)
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 3m21s
Co-authored-by: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com>
Reviewed-on: #53
2025-05-27 19:00:58 +00:00
7482b20526 phonepe (#52)
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 3m17s
Co-authored-by: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com>
Reviewed-on: #52
2025-05-27 18:46:11 +00:00
d9cd772f6e phonepe (#51)
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 3m42s
Co-authored-by: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com>
Reviewed-on: #51
2025-05-27 18:26:13 +00:00
7db9e479ad Merge branch 'dev' into qa
All checks were successful
Deploy FitLien services to Dev / Deploy to QA (push) Successful in 4m13s
2025-05-26 23:38:30 +05:30
59c4f88ff0 Create deploy-qa.yaml
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 2m9s
2025-05-26 23:37:47 +05:30
3f3bf53e94 Update index.ts
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 3m39s
2025-05-26 20:12:00 +05:30
be7470f1e4 Update deploy-dev.yaml
Some checks failed
Deploy FitLien services to Dev / Deploy to Dev (push) Failing after 42s
2025-05-26 20:08:07 +05:30
a89168bb74 Removed unused function
Some checks failed
Deploy FitLien services to Dev / Deploy to Dev (push) Failing after 29s
2025-05-26 20:01:19 +05:30
e12cbf4148 phonepe (#50)
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 3m21s
Co-authored-by: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com>
Reviewed-on: #50
2025-05-23 09:31:52 +00:00
fbacda6806 phonepe (#49)
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 3m16s
Co-authored-by: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com>
Reviewed-on: #49
2025-05-23 09:11:01 +00:00
33b01e5e5b phonepe (#48)
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 3m39s
Co-authored-by: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com>
Reviewed-on: #48
2025-05-23 09:00:10 +00:00
a146315536 phonepe (#47)
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 3m40s
Co-authored-by: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com>
Reviewed-on: #47
2025-05-23 08:14:11 +00:00
fab7e618d4 phonepe (#46)
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 3m33s
Co-authored-by: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com>
Reviewed-on: #46
2025-05-23 07:54:53 +00:00
e2deb441e9 phonepe (#45)
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 3m15s
Co-authored-by: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com>
Reviewed-on: #45
2025-05-23 07:37:20 +00:00
916293e8b1 phonepe (#44)
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 4m21s
Co-authored-by: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com>
Reviewed-on: #44
2025-05-23 07:15:32 +00:00
d3cc6b3710 phonepe (#43)
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 3m35s
Co-authored-by: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com>
Reviewed-on: #43
2025-05-21 11:31:44 +00:00
4b054fac56 phonepe (#42)
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 3m42s
Co-authored-by: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com>
Reviewed-on: #42
2025-05-21 10:10:13 +00:00
e53310b4cf phonepe (#41)
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 4m6s
Co-authored-by: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com>
Reviewed-on: #41
2025-05-21 08:43:39 +00:00
e382ca3789 phonepe (#40)
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 1m45s
Co-authored-by: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com>
Reviewed-on: #40
2025-05-20 10:59:07 +00:00
fe7839b899 phonepe (#39)
Some checks failed
Deploy FitLien services to Dev / Deploy to Dev (push) Failing after 3m11s
Co-authored-by: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com>
Reviewed-on: #39
2025-05-20 10:53:11 +00:00
271858a4d3 phonepe (#38)
Some checks failed
Deploy FitLien services to Dev / Deploy to Dev (push) Failing after 3m11s
Co-authored-by: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com>
Reviewed-on: #38
2025-05-20 10:47:25 +00:00
038d85708f phonepe (#37)
Some checks failed
Deploy FitLien services to Dev / Deploy to Dev (push) Failing after 3m17s
Co-authored-by: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com>
Reviewed-on: #37
2025-05-20 10:40:05 +00:00
ba54655b8c Update firebase.json (#36)
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 1m37s
Co-authored-by: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com>
Reviewed-on: #36
2025-05-20 10:16:12 +00:00
769311bb1b Merge pull request 'phonepe' (#35) from phonepe into dev
Some checks failed
Deploy FitLien services to Dev / Deploy to Dev (push) Failing after 3m37s
Reviewed-on: #35
2025-05-20 10:10:58 +00:00
AllenTJ7
a4c3cbbba2 added logging 2025-05-20 15:40:30 +05:30
AllenTJ7
c6f8b7fc35 Update webhook.ts 2025-05-20 15:19:01 +05:30
92320f166a phonepe (#34)
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 1m41s
Co-authored-by: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com>
Reviewed-on: #34
2025-05-20 09:14:04 +00:00
AllenTJ7
9c002a4d61 Update firebase.json 2025-05-20 14:41:50 +05:30
b372d81ff0 phonepe (#33)
Some checks failed
Deploy FitLien services to Dev / Deploy to Dev (push) Failing after 3m20s
Co-authored-by: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com>
Reviewed-on: #33
2025-05-20 09:04:45 +00:00
AllenTJ7
6b1d86afc2 Merge branch 'dev' into phonepe 2025-05-20 14:34:17 +05:30
AllenTJ7
06c5f018c3 changed pdf again 2025-05-20 14:32:59 +05:30
815e15b5ae phonepe (#32)
Some checks failed
Deploy FitLien services to Dev / Deploy to Dev (push) Failing after 3m6s
Co-authored-by: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com>
Reviewed-on: #32
2025-05-20 08:47:28 +00:00
AllenTJ7
9d17bd39ce Merge branch 'dev' into phonepe 2025-05-20 14:16:21 +05:30
AllenTJ7
762e6b77e5 Update invoiceService.ts 2025-05-20 14:15:36 +05:30
8bb10cba8c phonepe (#31)
Some checks failed
Deploy FitLien services to Dev / Deploy to Dev (push) Failing after 3m17s
Co-authored-by: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com>
Reviewed-on: #31
2025-05-20 08:22:02 +00:00
AllenTJ7
80b3829347 Merge branch 'dev' into phonepe 2025-05-20 13:51:47 +05:30
AllenTJ7
d8dfd8a6f2 pdf changes 2025-05-20 13:50:04 +05:30
ae19a611b3 phonepe (#30)
Some checks failed
Deploy FitLien services to Dev / Deploy to Dev (push) Failing after 3m47s
Co-authored-by: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com>
Reviewed-on: #30
2025-05-20 08:04:46 +00:00
AllenTJ7
756b6fed83 Merge branch 'dev' into phonepe 2025-05-20 13:34:23 +05:30
AllenTJ7
3ef81f8273 Update index.ts 2025-05-20 13:32:54 +05:30
AllenTJ7
b11cefbab4 Update invoiceService.ts 2025-05-20 13:32:51 +05:30
91e2af5890 phonepe (#29)
Some checks failed
Deploy FitLien services to Dev / Deploy to Dev (push) Failing after 2m0s
Co-authored-by: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com>
Reviewed-on: #29
2025-05-19 08:59:37 +00:00
AllenTJ7
2d6b14663b changed pdf to use encoding supported package 2025-05-19 14:28:51 +05:30
d335fb38e9 phonepe (#28)
Some checks failed
Deploy FitLien services to Dev / Deploy to Dev (push) Failing after 1m23s
Co-authored-by: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com>
Reviewed-on: #28
2025-05-19 07:52:03 +00:00
AllenTJ7
4fe2ff7abf Merge branch 'dev' into phonepe 2025-05-19 13:20:59 +05:30
AllenTJ7
2d55e1f461 Update webhook.ts 2025-05-19 13:18:52 +05:30
b75cb415e7 phonepe (#27)
Some checks failed
Deploy FitLien services to Dev / Deploy to Dev (push) Failing after 55s
Co-authored-by: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com>
Reviewed-on: #27
2025-05-19 07:26:12 +00:00
AllenTJ7
f62dfdfad2 dev merged 2025-05-19 12:55:57 +05:30
AllenTJ7
e8710074c4 Invoice 2025-05-19 12:52:06 +05:30
aee28a6050 phonepe (#26)
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 3m44s
Co-authored-by: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com>
Reviewed-on: #26
2025-05-14 13:09:09 +00:00
AllenTJ7
6d64f1e4d7 Logging and client profile change 2025-05-14 18:34:58 +05:30
AllenTJ7
38d5092ee3 Update webhook.ts 2025-05-12 13:13:27 +05:30
5f89a5cda4 Email with attachment issue fixed (#25)
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 3m43s
Reviewed-on: #25
Co-authored-by: Sharon Dcruz <sharondcruz@cosq.net>
Co-committed-by: Sharon Dcruz <sharondcruz@cosq.net>
2025-05-07 07:20:27 +00:00
1e040212e4 Update deploy-dev.yaml
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 3m22s
2025-05-06 18:17:09 +05:30
ccd0ee61bc Update deploy-dev.yaml
Some checks failed
Deploy FitLien services to Dev / Deploy to Dev (push) Has been cancelled
2025-05-06 18:06:46 +05:30
9b42fdcac1 Update deploy-dev.yaml
Some checks failed
Deploy FitLien services to Dev / Deploy to Dev (push) Has been cancelled
2025-05-06 18:04:34 +05:30
4b745d587f Update deploy-dev.yaml
Some checks failed
Deploy FitLien services to Dev / Deploy to Dev (push) Failing after 18s
2025-05-06 17:59:11 +05:30
3adf2a3aac Update deploy-dev.yaml
Some checks failed
Deploy FitLien services to Dev / Deploy to Dev (push) Has been cancelled
2025-05-06 17:56:58 +05:30
868ae3dea4 Update deploy-dev.yaml
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 3m19s
2025-05-06 17:53:13 +05:30
b8b518be9c Update deploy-dev.yaml
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 1m59s
2025-05-06 17:51:13 +05:30
0f86c468d2 Update deploy-dev.yaml
Some checks failed
Deploy FitLien services to Dev / Deploy to Dev (push) Failing after 19s
2025-05-06 17:47:06 +05:30
bef6b4085b Merge pull request 'phonepe' (#24) from phonepe into dev
Some checks failed
Deploy FitLien services to Dev / Deploy to Dev (push) Failing after 19s
Reviewed-on: #24
2025-05-06 11:40:13 +00:00
AllenTJ7
c377b7243d Update deploy-dev.yaml 2025-05-06 16:45:26 +05:30
9efa31b6cc phonepe (#23)
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 4m15s
Co-authored-by: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com>
Reviewed-on: #23
Co-authored-by: Allen T J <allentj@cosq.net>
Co-committed-by: Allen T J <allentj@cosq.net>
2025-05-05 14:09:08 +00:00
AllenTJ7
54006c44cf Update .env.example 2025-05-05 18:12:30 +05:30
AllenTJ7
c28597f3ee phonepe function completed 2025-05-05 17:59:23 +05:30
AllenTJ7
22fb060cb9 phonepe 2025-05-02 19:04:21 +05:30
72ad257e07 Update deploy-dev.yaml
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 3m59s
2025-04-25 19:17:01 +05:30
6e15bd3034 Adding @aws-sdk/client-ses
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 44s
2025-04-25 19:05:37 +05:30
ade7cfe30a Adding ws-sdk
Some checks failed
Deploy FitLien services to Dev / Deploy to Dev (push) Failing after 42s
2025-04-25 19:00:42 +05:30
71d9c2e670 Update deploy-dev.yaml
Some checks failed
Deploy FitLien services to Dev / Deploy to Dev (push) Failing after 45s
2025-04-25 18:57:45 +05:30
da1430ebc3 Update deploy-dev.yaml
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 19s
2025-04-25 18:54:56 +05:30
dcee3e6af9 Update deploy-dev.yaml
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 20s
2025-04-25 18:51:49 +05:30
853dd0e1e0 Update .env.example
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 19s
2025-04-25 18:46:04 +05:30
4e1724b1ec Update deploy-dev.yaml
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 20s
2025-04-25 18:44:42 +05:30
e7d6fd54e8 Update deploy-dev.yaml
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 19s
2025-04-25 18:28:04 +05:30
2d3c967037 Update deploy-dev.yaml
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 19s
2025-04-25 18:25:35 +05:30
b40201cf93 Update deploy-dev.yaml
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 20s
2025-04-25 18:17:21 +05:30
3d8e1c8db0 Update deploy-dev.yaml
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 19s
2025-04-25 17:49:39 +05:30
b6c72adfb8 Update deploy-dev.yaml
Some checks failed
Deploy FitLien services to Dev / Deploy to Dev (push) Failing after 20s
2025-04-25 17:44:48 +05:30
59a9932ca8 Added more steps
Some checks failed
Deploy FitLien services to Dev / Deploy to Dev (push) Failing after 58s
2025-04-25 17:40:47 +05:30
bb1a33daa4 Adding actions
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 1m11s
2025-04-25 16:53:08 +05:30
c11f0447a6 Adding AWS SES based email sending 2025-04-25 15:18:30 +05:30
e41ecc0852 Merge pull request 'feature/add-client' (#22) from feature/add-client into dev
Reviewed-on: #22
Reviewed-by: Benoy Bose <benoybose@cosq.net>
2025-04-21 10:59:44 +00:00
bb0420ee0a Merge branch 'dev' into feature/add-client 2025-04-21 16:28:35 +05:30
47851da81b changed name from corseHandler to getCorseHander in place details function 2025-04-21 16:28:03 +05:30
0fba4756c7 feature/add-client (#21)
Co-authored-by: Benoy Bose <benoybose@cosq.net>
Reviewed-on: #21
Reviewed-by: Benoy Bose <benoybose@cosq.net>
Co-authored-by: DhanshCOSQ <dhanshas@cosq.net>
Co-committed-by: DhanshCOSQ <dhanshas@cosq.net>
2025-04-21 09:19:03 +00:00
1f476a3071 Added the validation using id token and checking if the user is a gym_owner 2025-04-21 14:46:35 +05:30
accf136896 Merge branch 'dev' into feature/add-client 2025-04-21 14:17:09 +05:30
ba77cccd76 Fixed some errors and added the function to register client when a owner added a memmb 2025-04-21 14:16:58 +05:30
d39b53357b Merge branch 'dev' into feature/add-client 2025-04-21 14:12:19 +05:30
d241b09fd8 Removed env var FITLIENHOST 2025-04-21 12:51:00 +05:30
cf6f4625ad Removed env var FITLIENHOST 2025-04-21 12:28:07 +05:30
6434f6e3fa Merge branch 'dev' into qa 2025-04-21 12:17:34 +05:30
59bc1fde6b Avoiding recursive references 2025-04-17 19:55:26 +05:30
370573de1a Refactoring for maintenance 2025-04-17 19:18:46 +05:30
f8b3930fcc Merge branch 'dev' 2025-04-16 18:12:34 +05:30
2147963523 Merge branch 'dev' into qa 2025-04-16 18:12:16 +05:30
7642268606 Update index.ts 2025-04-16 18:11:53 +05:30
103684dc4f Merge branch 'dev' 2025-04-16 04:36:14 +05:30
18569d38d3 Merge branch 'dev' into qa 2025-04-16 04:35:58 +05:30
6351cb87ef Removed notify url 2025-04-16 04:35:34 +05:30
0e60f34500 Merge branch 'dev' 2025-04-16 03:53:59 +05:30
5bc3d6dfff Merge branch 'dev' into qa 2025-04-16 03:53:43 +05:30
66ba3b88ab Adding notify_url 2025-04-15 20:04:50 +05:30
e8cec7e0f4 Added linkQRCode on response 2025-04-15 16:23:15 +05:30
e2d87fa095 Update index.ts 2025-04-14 23:20:30 +05:30
1b5eb33859 Update index.ts 2025-04-14 19:01:14 +05:30
07bb778244 Added logs 2025-04-14 18:27:16 +05:30
9bd4a06dd9 Merge branch 'dev' 2025-04-14 15:02:58 +05:30
e8ca80df48 Merge branch 'dev' into qa 2025-04-14 15:02:28 +05:30
f156c1936c Adding createCashfreeLink 2025-04-14 15:01:53 +05:30
0a8c2295bb Merge branch 'qa' 2025-04-14 05:03:36 +05:30
26ae8aeaf2 Merge branch 'dev' into qa 2025-04-14 05:03:24 +05:30
e5c636f8c5 feature/test-deploy (#20)
Co-authored-by: aswincosq <aswinbs@cosq.net>
Reviewed-on: #20
Co-authored-by: DhanshCOSQ <dhanshas@cosq.net>
Co-committed-by: DhanshCOSQ <dhanshas@cosq.net>
2025-04-13 23:32:58 +00:00
0b699b203c Merge branch 'qa' 2025-04-14 04:53:29 +05:30
f642b744b4 Merge branch 'dev' into qa 2025-04-14 04:53:15 +05:30
564677d6c2 Update .env.example 2025-04-14 04:52:58 +05:30
77efd40c86 Merge branch 'qa' 2025-04-14 04:27:59 +05:30
90a196494a Merge branch 'dev' into qa 2025-04-14 04:27:35 +05:30
7887e87200 Fixed function (#19)
Reviewed-on: #19
Co-authored-by: aswincosq <aswinbs@cosq.net>
Co-committed-by: aswincosq <aswinbs@cosq.net>
2025-04-13 22:43:31 +00:00
f5770faf9f Merge branch 'qa' 2025-04-14 00:13:14 +05:30
a555223dbd Merge branch 'dev' into qa 2025-04-14 00:11:27 +05:30
9f259de4f6 Merge branch 'dev' of cosqnet.com:cosqnet/fitlien-services into dev 2025-04-14 00:11:12 +05:30
dc91c123bb Updated bucket permissions 2025-04-14 00:11:09 +05:30
fcd5f5db47 feature/fitlien-add-cors (#18)
Reviewed-on: #18
Co-authored-by: DhanshCOSQ <dhanshas@cosq.net>
Co-committed-by: DhanshCOSQ <dhanshas@cosq.net>
2025-04-13 10:07:51 +00:00
f6ab4fa510 Merge branch 'dev' 2025-04-13 13:50:32 +05:30
8e37d87268 Merge branch 'dev' into qa 2025-04-13 13:50:17 +05:30
01c87ffcfa Changed collection names (#17)
Reviewed-on: #17
Co-authored-by: DhanshCOSQ <dhanshas@cosq.net>
Co-committed-by: DhanshCOSQ <dhanshas@cosq.net>
2025-04-13 08:19:03 +00:00
5a09e5a82f Merge branch 'dev' 2025-04-13 13:30:59 +05:30
3c6e8ec583 Merge branch 'dev' into qa 2025-04-13 13:30:29 +05:30
0ecef0c051 Merge branch 'dev' of cosqnet.com:cosqnet/fitlien-services into dev 2025-04-13 13:29:42 +05:30
7a346f62a8 Updated firestore rules and index 2025-04-13 13:29:36 +05:30
094476072e Merge pull request 'Updated return url.' (#16) from feature/test-deploy into dev
Reviewed-on: #16
2025-04-13 06:19:20 +00:00
a9e4b31eb9 Updated return url. 2025-04-13 11:46:50 +05:30
08f1243729 feature/fitlien-add-cors (#15)
Reviewed-on: #15
Co-authored-by: DhanshCOSQ <dhanshas@cosq.net>
Co-committed-by: DhanshCOSQ <dhanshas@cosq.net>
2025-04-12 03:23:13 +00:00
8cad670373 Merge branch 'qa' 2025-04-12 07:50:48 +05:30
a245ec7d7b Merge branch 'dev' into qa 2025-04-12 07:50:03 +05:30
50d23a21d2 Ading FITLIENHOST for redirect URL 2025-04-12 06:32:44 +05:30
0055896229 feature/fitlien-add-cors (#14)
Reviewed-on: #14
Co-authored-by: DhanshCOSQ <dhanshas@cosq.net>
Co-committed-by: DhanshCOSQ <dhanshas@cosq.net>
2025-04-11 15:14:01 +00:00
ada17a85d9 feature/fitlien-add-cors (#13)
Reviewed-on: #13
Co-authored-by: DhanshCOSQ <dhanshas@cosq.net>
Co-committed-by: DhanshCOSQ <dhanshas@cosq.net>
2025-04-09 11:43:53 +00:00
4d77104c59 feature/fitlien-add-cors (#12)
Reviewed-on: #12
Co-authored-by: DhanshCOSQ <dhanshas@cosq.net>
Co-committed-by: DhanshCOSQ <dhanshas@cosq.net>
2025-04-09 10:13:03 +00:00
dc78edbbf3 feature/fitlien-cashfree (#11)
Co-authored-by: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com>
Reviewed-on: #11
Co-authored-by: Allen T J <allentj@cosq.net>
Co-committed-by: Allen T J <allentj@cosq.net>
2025-04-08 14:09:56 +00:00
912706aee7 feature/fitlien-add-cors (#10)
Changed variable name

Reviewed-on: #10
Co-authored-by: DhanshCOSQ <dhanshas@cosq.net>
Co-committed-by: DhanshCOSQ <dhanshas@cosq.net>
2025-04-08 12:46:53 +00:00
b68bda68c2 feature/fitlien-add-cors (#9)
Reviewed-on: #9
Co-authored-by: DhanshCOSQ <dhanshas@cosq.net>
Co-committed-by: DhanshCOSQ <dhanshas@cosq.net>
2025-04-08 10:41:35 +00:00
0be5cfe30c feature/fitlien-add-cors (#8)
Reviewed-on: #8
Co-authored-by: DhanshCOSQ <dhanshas@cosq.net>
Co-committed-by: DhanshCOSQ <dhanshas@cosq.net>
2025-04-07 21:06:25 +05:30
7d37e295fe Added cors createCashfreeOrder (#7)
Reviewed-on: #7
Co-authored-by: DhanshCOSQ <dhanshas@cosq.net>
Co-committed-by: DhanshCOSQ <dhanshas@cosq.net>
2025-04-07 21:06:16 +05:30
a333b9520d feature/fitlien-add-cors (#8)
Reviewed-on: #8
Co-authored-by: DhanshCOSQ <dhanshas@cosq.net>
Co-committed-by: DhanshCOSQ <dhanshas@cosq.net>
2025-04-07 21:05:55 +05:30
1991da23e6 Added cors createCashfreeOrder (#7)
Reviewed-on: #7
Co-authored-by: DhanshCOSQ <dhanshas@cosq.net>
Co-committed-by: DhanshCOSQ <dhanshas@cosq.net>
2025-04-07 21:05:38 +05:30
439353c593 feature/fitlien-add-cors (#8)
Reviewed-on: #8
Co-authored-by: DhanshCOSQ <dhanshas@cosq.net>
Co-committed-by: DhanshCOSQ <dhanshas@cosq.net>
2025-04-07 15:08:21 +00:00
f6facf40f8 Added cors createCashfreeOrder (#7)
Reviewed-on: #7
Co-authored-by: DhanshCOSQ <dhanshas@cosq.net>
Co-committed-by: DhanshCOSQ <dhanshas@cosq.net>
2025-04-07 14:36:46 +00:00
688619bd45 Merge branch 'qa' 2025-04-07 19:02:53 +05:30
8d20162922 Update fitlien-services-qa-pipeline.yaml 2025-04-07 19:01:42 +05:30
960fbb1e92 Update fitlien-services-qa-pipeline.yaml 2025-04-06 20:38:56 +05:30
d4090faa82 Update fitlien-services-qa-pipeline.yaml 2025-04-06 20:38:32 +05:30
0f11768b65 Adding QA pipeline 2025-04-06 19:58:54 +05:30
7be90b6284 Adding QA pipeline 2025-04-06 19:57:50 +05:30
fd7e7f528b Update .firebaserc 2025-04-06 19:48:49 +05:30
c93a4f4718 Update .firebaserc 2025-04-06 19:48:37 +05:30
0eb5ca1baf Merge pull request 'Changed region' (#6) from feature/fitlien-signed-url into dev
Reviewed-on: #6
2025-04-05 09:38:31 +00:00
27feed4711 Merge branch 'dev' into feature/fitlien-signed-url 2025-04-05 14:59:51 +05:30
b4a75f4968 Changed region 2025-04-05 14:58:52 +05:30
f00ff4d1d6 Signed url complete (#5)
Reviewed-on: #5
Reviewed-by: Benoy Bose <benoybose@cosq.net>
Co-authored-by: DhanshCOSQ <dhanshas@cosq.net>
Co-committed-by: DhanshCOSQ <dhanshas@cosq.net>
2025-04-05 09:20:27 +00:00
e553bd673a Time changed to one hour 2025-04-05 14:22:47 +05:30
f221f4850e Signed url complete 2025-04-05 12:13:49 +05:30
f9930b143c feature/fitlien-cashfree (#4)
Co-authored-by: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com>
Reviewed-on: #4
Reviewed-by: Dhansh A S <dhanshas@cosq.net>
Co-authored-by: Allen T J <allentj@cosq.net>
Co-committed-by: Allen T J <allentj@cosq.net>
2025-04-03 13:51:57 +00:00
97a13bde56 Merge pull request 'Added function to send email for invoice' (#3) from faeture/fitlien-invoice into dev
Reviewed-on: #3
2025-04-02 07:45:27 +00:00
55 changed files with 9617 additions and 1395 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

View File

@ -1,6 +1,8 @@
{
"projects": {
"debug": "fitlien-dev",
"release": "fitlien"
"qa": "fitlien-qa",
"release": "fitlien",
"default": "fitlien-dev"
}
}

View File

@ -0,0 +1,67 @@
name: Deploy FitLien services to Dev
on:
push:
branches:
- dev
jobs:
deploy:
name: Deploy to Dev
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: 22
- name: Copy .env.example to .env
run: cp functions/.env.example functions/.env
- name: Create private key file
run: |
mkdir -p functions/assets/keys
echo "${{ secrets.FITLIEN_PRIVATEKEY_DEV }}" > functions/assets/keys/fitLien_private.pem
chmod 600 functions/assets/keys/fitLien_private.pem
- name: Replace variables in .env
run: |
sed -i "s/#{TWILIO_ACCOUNT_SID}#/${{ secrets.TWILIO_ACCOUNT_SID }}/" functions/.env
sed -i "s/#{TWILIO_AUTH_TOKEN}#/${{ secrets.TWILIO_AUTH_TOKEN }}/" functions/.env
sed -i "s/#{TWILIO_PHONE_NUMBER}#/${{ secrets.TWILIO_PHONE_NUMBER }}/" functions/.env
sed -i "s/#{SERVICES_RGN}#/${{ vars.SERVICES_RGN }}/" functions/.env
sed -i "s/#{GOOGLE_MAPS_API_KEY}#/${{ secrets.GOOGLE_MAPS_API_KEY }}/" functions/.env
sed -i "s/#{SES_FROM_EMAIL}#/${{ vars.SES_FROM_EMAIL }}/" functions/.env
sed -i "s/#{SES_REPLY_TO_EMAIL}#/${{ vars.SES_REPLY_TO_EMAIL }}/" functions/.env
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: |
find . -type f -name "*.ts" -exec sed -i "s/#{SERVICES_RGN}#/${{ vars.SERVICES_RGN }}/g" {} +
- name: Build
run: |
npm install -g typescript
cd functions
npm install
npx tsc
cd ..
ls -la
- name: Deploy
run: |
curl -sL firebase.tools | upgrade=true bash
firebase use --token ${{ secrets.FIREBASE_TOKEN }} debug
firebase deploy --token "${{ secrets.FIREBASE_TOKEN }}" --force --non-interactive

View File

@ -0,0 +1,67 @@
name: Deploy FitLien services to QA
on:
push:
branches:
- qa
jobs:
deploy:
name: Deploy to QA
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: 22
- name: Copy .env.example to .env
run: cp functions/.env.example functions/.env
- name: Create private key file
run: |
mkdir -p functions/assets/keys
echo "${{ secrets.FITLIEN_PRIVATEKEY_DEV }}" > functions/assets/keys/fitLien_private.pem
chmod 600 functions/assets/keys/fitLien_private.pem
- name: Replace variables in .env
run: |
sed -i "s/#{TWILIO_ACCOUNT_SID}#/${{ secrets.TWILIO_ACCOUNT_SID }}/" functions/.env
sed -i "s/#{TWILIO_AUTH_TOKEN}#/${{ secrets.TWILIO_AUTH_TOKEN }}/" functions/.env
sed -i "s/#{TWILIO_PHONE_NUMBER}#/${{ secrets.TWILIO_PHONE_NUMBER }}/" functions/.env
sed -i "s/#{SERVICES_RGN}#/${{ vars.SERVICES_RGN }}/" functions/.env
sed -i "s/#{GOOGLE_MAPS_API_KEY}#/${{ secrets.GOOGLE_MAPS_API_KEY }}/" functions/.env
sed -i "s/#{SES_FROM_EMAIL}#/${{ vars.SES_FROM_EMAIL }}/" functions/.env
sed -i "s/#{SES_REPLY_TO_EMAIL}#/${{ vars.SES_REPLY_TO_EMAIL }}/" functions/.env
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: |
find . -type f -name "*.ts" -exec sed -i "s/#{SERVICES_RGN}#/${{ vars.SERVICES_RGN }}/g" {} +
- name: Build
run: |
npm install -g typescript
cd functions
npm install
npx tsc
cd ..
ls -la
- name: Deploy
run: |
curl -sL firebase.tools | upgrade=true bash
firebase use --token ${{ secrets.FIREBASE_TOKEN }} qa
firebase deploy --token "${{ secrets.FIREBASE_TOKEN }}" --force --non-interactive

View File

@ -0,0 +1,67 @@
name: Deploy FitLien services
on:
push:
branches:
- main
jobs:
deploy:
name: Deploy
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: 22
- name: Copy .env.example to .env
run: cp functions/.env.example functions/.env
- name: Create private key file
run: |
mkdir -p functions/assets/keys
echo "${{ secrets.FITLIEN_PRIVATEKEY }}" > functions/assets/keys/fitLien_private.pem
chmod 600 functions/assets/keys/fitLien_private.pem
- name: Replace variables in .env
run: |
sed -i "s/#{TWILIO_ACCOUNT_SID}#/${{ secrets.TWILIO_ACCOUNT_SID }}/" functions/.env
sed -i "s/#{TWILIO_AUTH_TOKEN}#/${{ secrets.TWILIO_AUTH_TOKEN }}/" functions/.env
sed -i "s/#{TWILIO_PHONE_NUMBER}#/${{ secrets.TWILIO_PHONE_NUMBER }}/" functions/.env
sed -i "s/#{SERVICES_RGN}#/${{ vars.SERVICES_RGN }}/" functions/.env
sed -i "s/#{GOOGLE_MAPS_API_KEY}#/${{ secrets.GOOGLE_MAPS_API_KEY }}/" functions/.env
sed -i "s/#{SES_FROM_EMAIL}#/${{ vars.SES_FROM_EMAIL }}/" functions/.env
sed -i "s/#{SES_REPLY_TO_EMAIL}#/${{ vars.SES_REPLY_TO_EMAIL }}/" functions/.env
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: |
find . -type f -name "*.ts" -exec sed -i "s/#{SERVICES_RGN}#/${{ vars.SERVICES_RGN }}/g" {} +
- name: Build
run: |
npm install -g typescript
cd functions
npm install
npx tsc
cd ..
ls -la
- name: Deploy
run: |
curl -sL firebase.tools | upgrade=true bash
firebase use --token ${{ secrets.FIREBASE_TOKEN }} release
firebase deploy --token "${{ secrets.FIREBASE_TOKEN }}" --force --non-interactive

6
.gitignore vendored
View File

@ -26,6 +26,9 @@ pids
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Private key
/functions/assets/keys/fitLien_private.pem
# Coverage directory used by tools like istanbul
coverage
@ -67,3 +70,6 @@ node_modules/
# dataconnect generated files
.dataconnect
.DS_Store
**/.DS_Store

View File

@ -1,12 +1,15 @@
{
"firestore": {
"rules": "firestore.rules",
"indexes": "firestore.indexes.json"
"indexes": "firestore.indexes.json",
"database": "(default)"
},
"functions": [
{
"source": "functions",
"codebase": "default",
"timeoutSeconds": 540,
"memory": "1GiB",
"ignore": [
"node_modules",
".git",
@ -14,14 +17,31 @@
"firebase-debug.*.log",
"*.local"
],
"predeploy": [
]
"predeploy": []
}
],
"storage": {
"rules": "storage.rules"
},
"emulators": {
"functions": {
"port": 5005
},
"firestore": {
"port": 8086
},
"storage": {
"port": 9199
},
"ui": {
"enabled": true,
"port": 4008
},
"auth": {
"port": 9099
}
},
"remoteconfig": {
"template": "remoteconfig.template.json"
}
}
}

View File

@ -1,5 +1,33 @@
{
"indexes": [
{
"collectionGroup": "day_pass_bookings",
"queryScope": "COLLECTION",
"fields": [
{
"fieldPath": "userId",
"order": "ASCENDING"
},
{
"fieldPath": "createdAt",
"order": "DESCENDING"
}
]
},
{
"collectionGroup": "day_pass_entries",
"queryScope": "COLLECTION",
"fields": [
{
"fieldPath": "bookingId",
"order": "ASCENDING"
},
{
"fieldPath": "entryDate",
"order": "ASCENDING"
}
]
},
{
"collectionGroup": "gyms",
"queryScope": "COLLECTION_GROUP",
@ -13,7 +41,155 @@
"order": "ASCENDING"
}
]
},
{
"collectionGroup": "gyms",
"queryScope": "COLLECTION",
"fields": [
{
"fieldPath": "userId",
"order": "ASCENDING"
},
{
"fieldPath": "name",
"order": "ASCENDING"
}
]
},
{
"collectionGroup": "gyms",
"queryScope": "COLLECTION",
"fields": [
{
"fieldPath": "userId",
"order": "ASCENDING"
},
{
"fieldPath": "createdAt",
"order": "DESCENDING"
}
]
},
{
"collectionGroup": "gyms",
"queryScope": "COLLECTION",
"fields": [
{
"fieldPath": "isApproved",
"order": "ASCENDING"
},
{
"fieldPath": "createdAt",
"order": "ASCENDING"
}
]
},
{
"collectionGroup": "memberships",
"queryScope": "COLLECTION",
"fields": [
{
"fieldPath": "gymId",
"order": "ASCENDING"
},
{
"fieldPath": "createdAt",
"order": "DESCENDING"
}
]
},
{
"collectionGroup": "notifications",
"queryScope": "COLLECTION",
"fields": [
{
"fieldPath": "type",
"order": "ASCENDING"
},
{
"fieldPath": "userId",
"order": "ASCENDING"
},
{
"fieldPath": "timestamp",
"order": "DESCENDING"
}
]
},
{
"collectionGroup": "notifications",
"queryScope": "COLLECTION",
"fields": [
{
"fieldPath": "recipientId",
"order": "ASCENDING"
},
{
"fieldPath": "timestamp",
"order": "DESCENDING"
}
]
},
{
"collectionGroup": "workout_logs",
"queryScope": "COLLECTION",
"fields": [
{
"fieldPath": "user_id",
"order": "ASCENDING"
},
{
"fieldPath": "date",
"order": "DESCENDING"
}
]
},
{
"collectionGroup": "workout_logs",
"queryScope": "COLLECTION",
"fields": [
{
"fieldPath": "user_id",
"order": "ASCENDING"
},
{
"fieldPath": "date",
"order": "ASCENDING"
}
]
},
{
"collectionGroup": "workout_logs",
"queryScope": "COLLECTION",
"fields": [
{
"fieldPath": "user_id",
"order": "ASCENDING"
},
{
"fieldPath": "start_time",
"order": "ASCENDING"
},
{
"fieldPath": "date",
"order": "ASCENDING"
}
]
},
{
"collectionGroup": "terms_and_conditions",
"queryScope": "COLLECTION",
"fields": [
{
"fieldPath": "normalizedName",
"order": "ASCENDING"
},
{
"fieldPath": "userUid",
"order": "ASCENDING"
}
]
}
],
"fieldOverrides": []
}
}

View File

@ -1,19 +1,16 @@
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// This rule allows anyone with your Firestore database reference to view, edit,
// and delete all data in your Firestore database. It is useful for getting
// started, but it is configured to expire after 30 days because it
// leaves your app open to attackers. At that time, all client
// requests to your Firestore database will be denied.
//
// Make sure to write security rules for your app before that time, or else
// all client requests to your Firestore database will be denied until you Update
// your rules
match /{document=**} {
allow read, write: if request.time < timestamp.date(2025, 1, 10);
allow read, write: if request.auth != null;
}
match /day_pass_bookings/{bookingId} {
allow read: if true;
allow write: if request.auth != null;
}
match /gyms/{gymId} {
allow read: if true;
allow write: if request.auth != null;
}
}
}

View File

@ -0,0 +1,30 @@
trigger:
- qa
pool:
vmImage: "ubuntu-latest"
variables:
major: $(VERSION_MAJOR)
minor: $(VERSION_MINOR)
prefix: $[format('{0}.{1}', variables['major'], variables['minor'])]
patch: $[counter(variables['prefix'], 100)]
buildNumber: $(major).$(minor).$(patch)
resources:
repositories:
- repository: templateRepo
endpoint: cosq-network
type: github
name: cosq-network/azure-build-pipeline-templates
extends:
template: firebase-functions-deploy.yaml@templateRepo
parameters:
nodeVersion: "20"
firebaseTokenSecret: $(FIREBASE_TOKEN)
functionsWorkingDirectory: "$(Build.SourcesDirectory)/functions"
envExamplePath: "$(Build.SourcesDirectory)/functions/.env.example"
envPath: "$(Build.SourcesDirectory)/functions/.env"
buildNumber: $(buildNumber)
buildType: qa

View File

@ -1,7 +1,21 @@
MAILGUN_API_KEY=#{MAILGUN_API_KEY}#
MAILGUN_SERVER=#{MAILGUN_SERVER}#
MAILGUN_FROM_ADDRESS=#{MAILGUN_FROM_ADDRESS}#
TWILIO_ACCOUNT_SID=#{TWILIO_ACCOUNT_SID}#
TWILIO_AUTH_TOKEN=#{TWILIO_AUTH_TOKEN}#
TWILIO_PHONE_NUMBER=#{TWILIO_PHONE_NUMBER}#
SERVICES_RGN=#{SERVICES_RGN}#
CASHFREE_CLIENT_ID=#{CASHFREE_CLIENT_ID}#
CASHFREE_CLIENT_SECRET=#{CASHFREE_CLIENT_SECRET}#
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}#
AWS_ACCESS_KEY_ID=#{AWS_ACCESS_KEY_ID}#
AWS_SECRET_ACCESS_KEY=#{AWS_SECRET_ACCESS_KEY}#
AWS_REGION=#{AWS_REGION}#

File diff suppressed because it is too large Load Diff

View File

@ -15,21 +15,32 @@
},
"main": "lib/index.js",
"dependencies": {
"@aws-sdk/client-ses": "^3.798.0",
"@types/node-fetch": "^2.6.12",
"axios": "^1.8.4",
"aws-sdk": "^2.1692.0",
"axios": "^1.9.0",
"cors": "^2.8.5",
"date-fns": "^4.1.0",
"firebase-admin": "^12.6.0",
"firebase-functions": "^6.0.1",
"firebase-functions": "^6.4.0",
"form-data": "^4.0.1",
"functions": "file:",
"html-to-text": "^9.0.5",
"long": "^4.0.0",
"mailgun.js": "^10.4.0",
"jspdf": "^3.0.1",
"jspdf-autotable": "^5.0.2",
"long": "^5.3.2",
"node-fetch": "^2.7.0",
"pdfjs-dist": "^5.0.375",
"twilio": "^5.4.0"
"pdfmake": "^0.2.20",
"twilio": "^5.4.0",
"xmldom": "^0.6.0"
},
"devDependencies": {
"@types/long": "^5.0.0",
"@types/node": "^22.13.14",
"@types/mime-types": "^2.1.4",
"@types/node": "^22.15.31",
"@types/pdfmake": "^0.2.11",
"@types/xmldom": "^0.1.34",
"firebase-functions-test": "^3.1.0",
"typescript": "^5.8.2"
},

View File

@ -0,0 +1,7 @@
export type DoorAccessUser = {
name: string;
location: string;
role: string;
expireFrom: Date | null;
expireTo: Date | null;
};

View File

@ -0,0 +1,571 @@
import { onRequest } from "firebase-functions/https";
import { DoorAccessUser } from "./doorAccessUser";
import { Request } from "firebase-functions/v2/https";
import { Response } from "express";
import { getCorsHandler } from "../shared/middleware";
import { getLogger } from "../shared/config";
import { DOMParser } from 'xmldom';
import { RSADecryption } from "../shared/decrypt";
const logger = getLogger();
const corsHandler = getCorsHandler();
export interface EmployeeCodeRequest {
employeeCode: string;
}
export interface PushLogRequest extends EmployeeCodeRequest {
attendanceDate: string;
}
export interface UpdateEmployeeExRequest {
employeeCode: string;
employeeName: string;
employeeLocation: string;
employeeRole: string;
employeeVerificationType: string;
employeeExpiryFrom: string;
employeeExpiryTo: string;
employeeCardNumber: string;
groupId: string;
employeePhoto: string;
}
function getDecryptedPassword(password: string | null): string {
if (!password) {
throw new Error('Password is required');
}
return RSADecryption.decryptPassword(password);
}
const escapeXml = (str: string) => {
return str
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;');
};
function createGetEmployeeDetailsRequest(username: string, password: string, employeeCode: string): string | null {
const soapRequest = `<?xml version="1.0" encoding="utf-8"?>
<soap12:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap12="http://www.w3.org/2003/05/soap-envelope">
<soap12:Body>
<GetEmployeeDetails xmlns="http://tempuri.org/">
<UserName>${escapeXml(username)}</UserName>
<Password>${escapeXml(password)}</Password>
<EmployeeCode>${escapeXml(employeeCode)}</EmployeeCode>
</GetEmployeeDetails>
</soap12:Body>
</soap12:Envelope>`;
return soapRequest;
}
function parseGetEmployeeDetailsResponse(soapResponse: string): DoorAccessUser {
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(soapResponse, "text/xml");
if (xmlDoc.documentElement.tagName !== 'soap:Envelope') {
throw new Error("Invalid SOAP response");
}
if (null == xmlDoc.documentElement.firstChild) {
throw new Error("Invalid SOAP response");
}
let currentElement = xmlDoc.documentElement.firstChild as HTMLElement;
if (currentElement.tagName !== 'soap:Body') {
throw new Error("Invalid SOAP response");
}
currentElement = currentElement.firstChild as HTMLElement;
if (currentElement.tagName !== 'GetEmployeeDetailsResponse') {
throw new Error("Invalid SOAP response");
}
currentElement = currentElement.firstChild as HTMLElement;
if (currentElement.tagName !== 'GetEmployeeDetailsResult') {
throw new Error("Invalid SOAP response");
}
const resultText = currentElement.textContent;
if (!resultText) {
throw new Error("GetEmployeeDetailsResult is empty");
}
const userDetails: DoorAccessUser =
{
name: '',
location: '',
role: '',
expireFrom: null,
expireTo: null
};
const pairs = resultText.split(',');
pairs.forEach(pair => {
const [key, value] = pair.split('=');
if (key && value !== undefined) {
const cleanKey = key.trim();
const cleanValue = value.trim();
switch (cleanKey) {
case 'EmployeeName':
userDetails.name = cleanValue;
break;
case 'EmployeeLocation':
userDetails.location = cleanValue;
break;
case 'EmployeeRole':
userDetails.role = cleanValue;
break;
case 'EmployeeExpiryFrom':
userDetails.expireFrom = new Date(cleanValue);
break;
case 'EmployeeExpiryTo':
userDetails.expireTo = new Date(cleanValue);
break;
default:
break;
}
}
});
return userDetails;
}
function isValidDateString(dateString: string): boolean {
const dateRegex = /^\d{4}\-\d{2}\-\d{2}$/;
return dateRegex.test(dateString);
}
function createUpdateEmployeeExRequest(username: string, password: string, request: UpdateEmployeeExRequest): string | null {
if (!username || !password || !request.employeeCode || !request.employeeName || !request.employeeLocation) {
throw new Error('Missing required fields');
}
if (!isValidDateString(request.employeeExpiryFrom) || !isValidDateString(request.employeeExpiryTo)) {
throw new Error('Invalid date format');
}
const soapRequest = `<?xml version="1.0" encoding="utf-8"?>
<soap12:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap12="http://www.w3.org/2003/05/soap-envelope">
<soap12:Body>
<UpdateEmployeeEx xmlns="http://tempuri.org/">
<UserName>${escapeXml(username)}</UserName>
<Password>${escapeXml(password)}</Password>
<EmployeeCode>${escapeXml(request.employeeCode)}</EmployeeCode>
<EmployeeName>${escapeXml(request.employeeName)}</EmployeeName>
<EmployeeLocation>${escapeXml(request.employeeLocation)}</EmployeeLocation>
<EmployeeRole>${escapeXml(request.employeeRole)}</EmployeeRole>
<EmployeeVerificationType>Card</EmployeeVerificationType>
<EmployeeExpiryFrom>${escapeXml(request.employeeExpiryFrom)}</EmployeeExpiryFrom>
<EmployeeExpiryTo>${escapeXml(request.employeeExpiryTo)}</EmployeeExpiryTo>
<EmployeeCardNumber>${escapeXml(request.employeeCardNumber)}</EmployeeCardNumber>
<GroupId></GroupId>
<EmployeePhoto></EmployeePhoto>
</UpdateEmployeeEx>
</soap12:Body>
</soap12:Envelope>
`;
return soapRequest;
}
function parseUpdateEmployeeExResponse(soapResponse: string): string | null {
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(soapResponse, "text/xml");
const currentElement = xmlDoc.documentElement.firstChild as HTMLElement;
const resultText = currentElement.textContent;
return resultText;
}
function createDeleteEmployeeRequest(username: string, password: string, employeeCode: string): string | null {
if (!username || !password || !employeeCode) {
throw new Error('Missing required fields');
}
const soapRequst = `<?xml version="1.0" encoding="utf-8"?>
<soap12:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap12="http://www.w3.org/2003/05/soap-envelope">
<soap12:Body>
<DeleteEmployee xmlns="http://tempuri.org/">
<UserName>${escapeXml(username)}</UserName>
<Password>${escapeXml(password)}</Password>
<EmployeeCode>${escapeXml(employeeCode)}</EmployeeCode>
</DeleteEmployee>
</soap12:Body>
</soap12:Envelope>`;
return soapRequst;
}
function parseDeleteEmployeeResponse(soapResponse: string): string | null {
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(soapResponse, "text/xml");
const currentElement = xmlDoc.documentElement.firstChild as HTMLElement;
const resultText = currentElement.textContent;
return resultText;
}
function createGetEmployeePunchLogsRequest(username: string, password: string,
employeeCode: string, attendanceDate: string): string | null {
const soapRequest = `<?xml version="1.0" encoding="utf-8"?>
<soap12:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap12="http://www.w3.org/2003/05/soap-envelope">
<soap12:Body>
<GetEmployeePunchLogs xmlns="http://tempuri.org/">
<UserName>${escapeXml(username)}</UserName>
<Password>${escapeXml(password)}</Password>
<EmployeeCode>${escapeXml(employeeCode)}</EmployeeCode>
<AttendanceDate>${escapeXml(attendanceDate)}</AttendanceDate>
</GetEmployeePunchLogs>
</soap12:Body>
</soap12:Envelope>`;
return soapRequest;
}
function createDateFromTime(date: Date, timeString: string): Date {
const [hour, minute, second] = timeString.split(':').map(str => parseInt(str, 10));
const newDate = new Date(date.getTime());
newDate.setHours(hour);
newDate.setMinutes(minute);
newDate.setSeconds(second);
return newDate;
}
function parseGetEmployeePunchLogsResponse(soapResponse: string, attendanceDate: string): Date[] {
const rootDate = new Date(attendanceDate);
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(soapResponse, "text/xml");
const currentElement = xmlDoc.documentElement.firstChild as HTMLElement;
const resultText = currentElement.textContent;
if (!resultText || resultText.trim() === '' || resultText.trim() === ';;' || resultText.trim() === ';') {
return [];
}
const punchLogs: Date[] = [];
const parts = resultText.split(';');
for (const part of parts) {
if (!part || part.trim() === '') {
continue;
}
try {
const logDateTime = new Date(part);
if (isNaN(logDateTime.getTime())) {
throw new Error('Invalid date format');
}
punchLogs.push(logDateTime);
} catch {
try {
const timeParts = part.split(',');
for (const timePart of timeParts) {
if (!timePart || timePart.trim() === '') {
continue;
}
try {
const logDateTime = createDateFromTime(rootDate, timePart.trim());
punchLogs.push(logDateTime);
} catch {
continue;
}
}
} catch {
continue;
}
}
}
const sortedLogs = punchLogs.sort((a, b) => b.getTime() - a.getTime());
return sortedLogs;
}
async function sendSoapRequest(soapRequest: string, endpoint: string) {
try {
const headers: any = {
'Content-Type': 'application/soap+xml; charset=utf-8',
'Content-Length': soapRequest.length.toString()
};
const response = await fetch(endpoint, {
method: 'POST',
headers: headers,
body: soapRequest
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.text();
} catch (error: any) {
throw new Error(`SOAP request failed: ${error.message}`);
}
}
async function getUserDetails(username: string,
password: string,
employeeCode: string, endpoint: string): Promise<DoorAccessUser> {
const soapRequest = createGetEmployeeDetailsRequest(username, password, employeeCode);
const soapResponse = await sendSoapRequest(soapRequest!, endpoint);
const parsedResponse = parseGetEmployeeDetailsResponse(soapResponse);
return parsedResponse;
}
async function updateUserEx(username: string,
password: string,
request: UpdateEmployeeExRequest,
endpoint: string) {
const soapRequest = createUpdateEmployeeExRequest(username, password, request);
const soapResponse = await sendSoapRequest(soapRequest!, endpoint);
const parsedResponse = parseUpdateEmployeeExResponse(soapResponse);
return parsedResponse;
}
async function deleteEmplyee(username: string,
password: string,
employeeCode: string, endpoint: string) {
const soapRequest = createDeleteEmployeeRequest(username, password, employeeCode);
const soapResponse = await sendSoapRequest(soapRequest!, endpoint);
const parsedResponse = parseDeleteEmployeeResponse(soapResponse);
return parsedResponse;
}
async function getEmployeePunchLogs(username: string,
password: string,
employeeCode: string,
attendanceDate: string, endpoint: string): Promise<Date[]> {
const soapRequest = createGetEmployeePunchLogsRequest(username, password, employeeCode, attendanceDate);
const soapResponse = await sendSoapRequest(soapRequest!, endpoint);
const parsedResponse = parseGetEmployeePunchLogsResponse(soapResponse, attendanceDate);
return parsedResponse;
}
export const esslGetUserDetails = onRequest({
region: '#{SERVICES_RGN}#'
}, async (request: Request, response: Response) => {
return corsHandler(request, response, async () => {
try {
let username: string | null = request.body.username as string;
let password: string | null = request.body.password as string;
let endpoint: string | null = request.body.endpoint as string;
let gymId: string | null = request.body.gymId as string;
const getEmployeeDetailsRequest = request.body.params as EmployeeCodeRequest;
if (!username) {
throw new Error('Missing username or password');
}
username = username.trim();
if (!password) {
if (!gymId) {
throw new Error('Missing password or gymId');
}
// todo: Get password from gym configuration by decrypting with private key
throw new Error('Gym-based password retrieval not implemented yet');
}
password = getDecryptedPassword(password);
if (!getEmployeeDetailsRequest) {
throw new Error('Missing request params');
}
const employeeCode = getEmployeeDetailsRequest.employeeCode;
if (!employeeCode) {
throw new Error('Missing employeeCode');
}
if (!endpoint) {
throw new Error('Missing endpoint');
}
if (!endpoint || endpoint.trim() === '') {
throw new Error('Missing endpoint');
}
try {
new URL(endpoint);
} catch (_) {
throw new Error('Endpoint is not a valid URI or URL');
}
if (!endpoint.endsWith('/webservice.asmx')) {
if (endpoint.endsWith('/')) {
endpoint = endpoint.substring(0, endpoint.length - 1);
}
endpoint += '/webservice.asmx';
}
const userDetails = await getUserDetails(username, password, employeeCode, endpoint);
response.send(userDetails);
} catch (error: any) {
logger.error(error);
response.status(500).send({ error: error.message });
}
})
});
export const esslUpdateUser = onRequest({
region: '#{SERVICES_RGN}#'
}, async (request: Request, response: Response) => {
return corsHandler(request, response, async () => {
try {
let username: string | null = request.body.username as string;
let password: string | null = request.body.password as string;
let endpoint: string | null = request.body.endpoint as string;
let gymId: string | null = request.body.gymId as string;
const updateEmployeeExRequest = request.body.params as UpdateEmployeeExRequest;
if (!username) {
throw new Error('Missing username');
}
username = username.trim();
if (!password) {
if (!gymId) {
throw new Error('Missing password or gymId');
}
// TODO: Get password from gym configuration by decrypting with private key
throw new Error('Gym-based password retrieval not implemented yet');
}
password = getDecryptedPassword(password);
if (!endpoint) {
throw new Error('Missing endpoint');
}
endpoint = endpoint.trim();
if (!endpoint || endpoint.trim() === '') {
throw new Error('Missing endpoint');
}
try {
new URL(endpoint);
} catch (_) {
throw new Error('Endpoint is not a valid URI or URL');
}
if (!endpoint.endsWith('/webservice.asmx')) {
if (endpoint.endsWith('/')) {
endpoint = endpoint.substring(0, endpoint.length - 1);
}
endpoint += '/webservice.asmx';
}
if (!updateEmployeeExRequest) {
throw new Error('Missing request params');
}
const result = await updateUserEx(username, password, updateEmployeeExRequest, endpoint);
response.send(result);
} catch (error: any) {
logger.error(error);
response.status(500).send({ error: error.message });
}
});
});
export const esslDeleteUser = onRequest({
region: '#{SERVICES_RGN}#'
}, async (request: Request, response: Response) => {
return corsHandler(request, response, async () => {
try {
let username: string | null = request.body.username as string;
let password: string | null = request.body.password as string;
let endpoint: string | null = request.body.endpoint as string;
let gymId: string | null = request.body.gymId as string;
const getEmployeeDetailsRequest = request.body.params as EmployeeCodeRequest;
if (!username) {
throw new Error('Missing username');
}
username = username.trim();
if (!password) {
if (!gymId) {
throw new Error('Missing password or gymId');
}
// TODO: Get password from gym configuration by decrypting with private key
throw new Error('Gym-based password retrieval not implemented yet');
}
password = getDecryptedPassword(password);
if (!getEmployeeDetailsRequest) {
throw new Error('Missing request params');
}
const employeeCode = getEmployeeDetailsRequest.employeeCode;
if (!employeeCode) {
throw new Error('Missing employeeCode');
}
if (!endpoint) {
throw new Error('Missing endpoint');
}
if (!endpoint || endpoint.trim() === '') {
throw new Error('Missing endpoint');
}
try {
new URL(endpoint);
} catch (_) {
throw new Error('Endpoint is not a valid URI or URL');
}
if (!endpoint.endsWith('/webservice.asmx')) {
if (endpoint.endsWith('/')) {
endpoint = endpoint.substring(0, endpoint.length - 1);
}
endpoint += '/webservice.asmx';
}
const result = await deleteEmplyee(username, password, employeeCode, endpoint);
response.send(result);
} catch (error: any) {
logger.error(error);
response.status(500).send({ error: error.message });
}
});
});
export const esslGetEmployeePunchLogs = onRequest({
region: '#{SERVICES_RGN}#'
}, async (request: Request, response: Response) => {
return corsHandler(request, response, async () => {
try {
let username: string | null = request.body.username as string;
let password: string | null = request.body.password as string;
let endpoint: string | null = request.body.endpoint as string;
let gymId: string | null = request.body.gymId as string;
const pushLogRequst = request.body.params as PushLogRequest;
if (!username) {
throw new Error('Missing username');
}
username = username.trim();
if (!password) {
if (!gymId) {
throw new Error('Missing password or gymId');
}
// TODO: Get password from gym configuration by decrypting with private key
throw new Error('Gym-based password retrieval not implemented yet');
}
password = getDecryptedPassword(password);
if (!pushLogRequst) {
throw new Error('Missing request params');
}
const employeeCode = pushLogRequst.employeeCode;
if (!employeeCode) {
throw new Error('Missing employeeCode');
}
const attendanceDate = pushLogRequst.attendanceDate;
if (!attendanceDate) {
throw new Error('Missing attendanceDate');
}
const isValidDate = /^\d{4}-\d{2}-\d{2}$/;
if (!attendanceDate.match(isValidDate)) {
throw new Error('attendanceDate is not in the valid format YYYY-MM-DD');
}
if (!endpoint) {
throw new Error('Missing endpoint');
}
if (!endpoint || endpoint.trim() === '') {
throw new Error('Missing endpoint');
}
try {
new URL(endpoint);
} catch (_) {
throw new Error('Endpoint is not a valid URI or URL');
}
if (!endpoint.endsWith('/webservice.asmx')) {
if (endpoint.endsWith('/')) {
endpoint = endpoint.substring(0, endpoint.length - 1);
}
endpoint += '/webservice.asmx';
}
const result = await getEmployeePunchLogs(username,
password,
employeeCode,
attendanceDate,
endpoint);
response.send(result);
} catch (error: any) {
logger.error(error);
response.status(500).send({ error: error.message });
}
});
});

View File

@ -0,0 +1,4 @@
export {
esslGetUserDetails, esslUpdateUser,
esslDeleteUser, esslGetEmployeePunchLogs
} from './essl';

View File

@ -0,0 +1 @@
export { sendEmailSES } from './sendEmailSES';

View File

@ -0,0 +1,209 @@
import { getLogger } from "../shared/config";
import { getCorsHandler } from "../shared/middleware";
import { onRequest } from "firebase-functions/v2/https";
import { Request } from "firebase-functions/v2/https";
import { Response } from "express";
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();
interface EmailRequest {
to: string | string[];
subject: string;
html: string;
text?: string;
from: string;
replyTo?: string;
attachments?: Attachment[];
fileUrl?: string;
fileName?: string;
}
interface Attachment {
filename: string;
content: string | Buffer;
contentType?: string;
}
const stripHtml = (html: string): string => {
if (!html) return '';
return html.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim();
}
async function sendSimpleEmail(data: EmailRequest, recipients: string[]) {
const ses = new SESClient({
region: process.env.AWS_REGION,
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID || '',
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY || ''
}
});
const command = new SendEmailCommand({
Source: data.from,
Destination: { ToAddresses: recipients },
Message: {
Subject: { Data: data.subject },
Body: {
Html: { Data: data.html },
Text: { Data: data.text || stripHtml(data.html) }
}
},
ReplyToAddresses: data.replyTo ? [data.replyTo] : undefined,
});
const result = await ses.send(command);
return { messageId: result.MessageId };
}
async function sendEmailWithAttachments(data: EmailRequest, recipients: string[]) {
const ses = new SESClient({
region: process.env.AWS_REGION,
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID || '',
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY || ''
}
});
const boundary = `boundary_${Math.random().toString(16).substr(2)}`;
let rawMessage = `From: ${data.from}\n`;
rawMessage += `To: ${recipients.join(', ')}\n`;
rawMessage += `Subject: ${data.subject}\n`;
rawMessage += `MIME-Version: 1.0\n`;
rawMessage += `Content-Type: multipart/mixed; boundary="${boundary}"\n\n`;
// Add email body (multipart/alternative)
rawMessage += `--${boundary}\n`;
rawMessage += `Content-Type: multipart/alternative; boundary="alt_${boundary}"\n\n`;
// Text part
if (data.text) {
rawMessage += `--alt_${boundary}\n`;
rawMessage += `Content-Type: text/plain; charset=UTF-8\n\n`;
rawMessage += `${data.text}\n\n`;
}
// HTML part
rawMessage += `--alt_${boundary}\n`;
rawMessage += `Content-Type: text/html; charset=UTF-8\n\n`;
rawMessage += `${data.html}\n\n`;
// Close alternative part
rawMessage += `--alt_${boundary}--\n\n`;
// Add attachments
for (const attachment of data.attachments || []) {
const contentType = attachment.contentType ||
mime.lookup(attachment.filename) ||
'application/octet-stream';
rawMessage += `--${boundary}\n`;
rawMessage += `Content-Type: ${contentType}; name="${attachment.filename}"\n`;
rawMessage += `Content-Disposition: attachment; filename="${attachment.filename}"\n`;
rawMessage += `Content-Transfer-Encoding: base64\n\n`;
const contentBuffer = typeof attachment.content === 'string'
? Buffer.from(attachment.content, 'base64')
: attachment.content;
rawMessage += contentBuffer.toString('base64') + '\n\n';
}
// Close message
rawMessage += `--${boundary}--`;
const command = new SendRawEmailCommand({
RawMessage: { Data: Buffer.from(rawMessage) }
});
const result = await ses.send(command);
return { messageId: result.MessageId };
}
async function downloadFileFromUrl(url: string): Promise<Buffer> {
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: '#{SERVICES_RGN}#'
}, (request: Request, response: Response) => {
return corsHandler(request, response, async () => {
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);
} else {
const messageResult = await sendSimpleEmail(data, recipients);
response.status(200).json(messageResult);
}
} catch (e) {
logger.error(`Error while sending E-mail. Error: ${e}`);
console.error(`Error while sending E-mail. Error: ${e}`);
response.status(500).json({
success: false,
error: 'Error while sending E-mail'
});
}
});
});

View File

@ -1,208 +1,23 @@
import { onRequest } from "firebase-functions/v2/https";
import { Request } from "firebase-functions/v2/https";
import * as admin from 'firebase-admin';
import * as express from "express";
import * as logger from "firebase-functions/logger";
import { onDocumentCreated } from "firebase-functions/firestore";
import * as os from 'os';
import * as path from 'path';
import * as fs from 'fs';
import * as https from 'https';
const formData = require('form-data');
const Mailgun = require('mailgun.js');
const { convert } = require('html-to-text');
const twilio = require('twilio');
if (!admin.apps.length) {
admin.initializeApp();
}
export const sendEmailWithAttachment = onRequest({
region: '#{SERVICES_RGN}#'
}, async (request: Request, response: express.Response) => {
try {
const { toAddress, subject, message, fileUrl, fileName } = request.body;
if (!toAddress || !subject || !message || !fileUrl) {
response.status(400).json({
error: 'Missing required fields (toAddress, subject, message, fileUrl)'
});
return;
}
const tempFilePath = path.join(os.tmpdir(), fileName || 'attachment.pdf');
await new Promise<void>((resolve, reject) => {
const file = fs.createWriteStream(tempFilePath);
https.get(fileUrl, (res) => {
res.pipe(file);
file.on('finish', () => {
file.close();
resolve();
});
}).on('error', (err) => {
fs.unlink(tempFilePath, () => { });
reject(err);
});
});
const mailgun = new Mailgun(formData);
const client = mailgun.client({ username: 'api', key: process.env.MAILGUN_API_KEY });
const options = {
wordwrap: 130,
};
const textMessage = convert(message, options);
const fileBuffer = fs.readFileSync(tempFilePath);
const attachmentFilename = fileName || path.basename(fileUrl.split('?')[0]);
const data = {
from: process.env.MAILGUN_FROM_ADDRESS,
to: toAddress,
subject: subject,
text: textMessage,
html: message,
attachment: {
data: fileBuffer,
filename: attachmentFilename,
contentType: 'application/pdf',
}
};
const result = await client.messages.create(process.env.MAILGUN_SERVER, data);
fs.unlinkSync(tempFilePath);
logger.info('Email with attachment from URL sent successfully');
response.json({ success: true, result });
} catch (error) {
logger.error('Error sending email with attachment from URL:', error);
response.status(500).json({ success: false, error: error instanceof Error ? error.message : String(error) });
}
});
export const sendEmailMessage = onRequest({
region: '#{SERVICES_RGN}#'
}, (request: Request, response: express.Response) => {
const mailgun = new Mailgun(formData);
const mailGunClient = mailgun.client({ username: 'api', key: process.env.MAILGUN_API_KEY });
const toAddress = request.body.toAddress;
const subject = request.body.subject;
const message = request.body.message;
const options = {
wordwrap: 130,
};
const textMessage = convert(message, options);
mailGunClient.messages.create(process.env.MAILGUN_SERVER, {
from: process.env.MAILGUN_FROM_ADDRESS,
to: toAddress,
subject: subject,
text: textMessage,
html: message
}).then((res: any) => {
logger.info(res);
response.send(res);
}).catch((err: any) => {
logger.error(err);
response.send(err);
});
});
export const sendSMSMessage = onRequest({
region: '#{SERVICES_RGN}#'
}, (request: Request, response: express.Response) => {
const client = twilio(process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN);
const { to, body } = request.body;
client.messages
.create({
body: body,
from: process.env.TWILIO_PHONE_NUMBER,
to: to
})
.then((message: any) => {
logger.info('SMS sent successfully:', message.sid);
response.json({ success: true, messageId: message.sid });
})
.catch((error: any) => {
logger.error('Error sending SMS:', error);
response.status(500).json({ success: false, error: error.message });
});
});
interface Invitation {
email: string;
phoneNumber: string;
gymName: string;
invitedByName: string;
}
export const notifyInvitation = onDocumentCreated({
document: 'notifications/{notificationId}',
region: '#{SERVICES_RGN}#'
}, async (event: any) => {
const invitation = event.data?.data() as Invitation;
const invitationId = event.params.invitationId;
if (!invitation) {
console.error('Invitation data is missing.');
return null;
}
try {
const userQuery = await admin
.firestore()
.collection('users')
.where('email', '==', invitation.email)
.where('phoneNumber', '==', invitation.phoneNumber)
.limit(1)
.get();
if (userQuery.empty) {
console.log(
`User not found for email: ${invitation.email} and phone: ${invitation.phoneNumber}.`
);
return null;
}
const user = userQuery.docs[0].data();
const fcmToken = user.fcmToken;
if (!fcmToken) {
console.log(`FCM token not found for user: ${invitation.email}.`);
return null;
}
const message: admin.messaging.Message = {
notification: {
title: 'New Gym Invitation',
body: `${invitation.invitedByName} has invited you to join ${invitation.gymName}`,
},
data: {
type: 'invitation',
invitationId: invitationId,
gymName: invitation.gymName,
senderName: invitation.invitedByName,
},
android: {
priority: 'high',
notification: {
channelId: 'invitations_channel',
priority: 'high',
defaultSound: true,
defaultVibrateTimings: true,
icon: '@mipmap/ic_launcher',
clickAction: 'FLUTTER_NOTIFICATION_CLICK',
},
},
token: fcmToken,
};
await admin.messaging().send(message);
console.log(`Invitation notification sent to ${invitation.email}.`);
return null;
} catch (error) {
console.error('Error sending invitation notification:', error);
return null;
}
});
import { setGlobalOptions } from "firebase-functions/v2";
setGlobalOptions({
region: "#{SERVICES_RGN}#",
memory: "1GiB",
timeoutSeconds: 540,
minInstances: 0,
maxInstances: 10,
concurrency: 80
});
export * from './shared/config';
export { sendEmailSES } from './email';
export { sendSMSMessage } from './sms';
export { accessFile } from './storage';
export { processNotificationOnCreate,checkExpiredMemberships } from './notifications';
export * from './payments';
export { getPlaceDetails, getPlacesAutocomplete } from './places';
export { registerClient } from './users';
export {
esslGetUserDetails, esslUpdateUser,
esslDeleteUser, esslGetEmployeePunchLogs
} from './dooraccess';

View File

@ -0,0 +1,3 @@
export { processNotificationOnCreate } from './processNotification';
export { checkExpiredMemberships } from "./membershipStatusNotifications";

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,422 @@
import { onDocumentCreated } from "firebase-functions/v2/firestore";
import { getLogger } from "../shared/config";
import { getAdmin } from "../shared/config";
import * as admin from "firebase-admin";
const app = getAdmin();
const logger = getLogger();
interface NotificationData {
senderId?: string;
recipientId?: string;
type?: string;
notificationSent?: boolean;
timestamp?: admin.firestore.FieldValue;
read?: boolean;
data?: { [key: string]: any };
}
export const processNotificationOnCreate = onDocumentCreated(
{
region: "#{SERVICES_RGN}#",
document: "notifications/{notificationId}",
},
async (event) => {
try {
const notificationSnapshot = event.data;
const notificationId = event.params.notificationId;
if (!notificationSnapshot) {
logger.error(`No data found for notification ${notificationId}`);
return;
}
const notification = notificationSnapshot.data() as NotificationData;
if (notification.notificationSent === true) {
logger.info(`Notification ${notificationId} already sent, skipping.`);
return;
}
logger.info(
`Processing notification ${notificationId} of type: ${notification.type}`
);
const { userId, fcmToken } = await getUserAndFCMToken(notification);
if (!fcmToken) {
logger.error(
`FCM token not found for notification ${notificationId}, user: ${userId}`
);
await updateNotificationWithError(
notificationId,
"FCM token not found for user"
);
return;
}
const message = prepareNotificationMessage(notification, fcmToken);
try {
const fcmResponse = await app.messaging().send(message);
logger.info(`FCM notification sent successfully: ${fcmResponse}`);
await markNotificationAsSent(notificationId);
} catch (error) {
logger.error(`Error sending notification ${notificationId}:`, error);
await updateNotificationWithError(
notificationId,
error instanceof Error ? error.message : String(error)
);
}
} catch (error) {
logger.error("Error processing notification:", error);
}
}
);
async function getUserAndFCMToken(
notification: NotificationData
): Promise<{ userId: string | null; fcmToken: string | null }> {
let targetUserId: string | null = null;
let fcmToken: string | null = null;
if (notification.recipientId) {
targetUserId = notification.recipientId;
logger.info(`Using top-level recipientId: ${targetUserId}`);
} else if (notification.data?.phoneNumber) {
logger.info(
`Looking up user by phone number from data: ${notification.data.phoneNumber}`
);
const userQuery = await app
.firestore()
.collection("users")
.where("phoneNumber", "==", notification.data.phoneNumber)
.limit(1)
.get();
if (!userQuery.empty) {
const userDoc = userQuery.docs[0];
targetUserId = userDoc.id;
fcmToken = userDoc.data()?.fcmToken;
logger.info(`Found user by phone: ${targetUserId}`);
} else {
logger.warn(
`No user found with phone number from data: ${notification.data.phoneNumber}`
);
}
} else {
logger.error("No valid user identifier found in notification or its data");
}
if (targetUserId && !fcmToken) {
fcmToken = await getFCMTokenFromUserDoc(targetUserId);
}
if (targetUserId && !fcmToken) {
logger.warn(`User ${targetUserId} found but no FCM token available`);
}
return { userId: targetUserId, fcmToken };
}
async function getFCMTokenFromUserDoc(userId: string): Promise<string | null> {
try {
const userDoc = await app.firestore().collection("users").doc(userId).get();
if (userDoc.exists) {
const userData = userDoc.data();
const fcmToken = userData?.fcmToken;
if (!fcmToken) {
logger.warn(`User ${userId} exists but has no FCM token`);
}
return fcmToken;
} else {
logger.warn(`User document not found: ${userId}`);
return null;
}
} catch (error) {
logger.error(`Error fetching user ${userId}:`, error);
return null;
}
}
function prepareNotificationMessage(
notification: NotificationData,
fcmToken: string
): admin.messaging.TokenMessage {
let title = notification.data?.title || "New Notification";
let body = notification.data?.message || "You have a new notification";
let fcmData: Record<string, string> = {
type: notification.type || "general",
notificationId: "notification_" + Date.now().toString(),
};
if (notification.senderId) fcmData.senderId = notification.senderId;
if (notification.recipientId) fcmData.recipientId = notification.recipientId;
if (notification.read !== undefined) fcmData.read = String(notification.read);
if (notification.data) {
for (const key in notification.data) {
if (Object.prototype.hasOwnProperty.call(notification.data, key)) {
const value = notification.data[key];
if (typeof value === "object" && value !== null) {
fcmData[key] = JSON.stringify(value);
} else {
fcmData[key] = String(value);
}
}
}
}
switch (notification.type) {
case "trainer_response":
title =
notification.data?.title ||
(notification.data?.status === "accepted"
? "Trainer Invitation Accepted"
: "Trainer Invitation Update");
body =
notification.data?.message ||
`${
notification.data?.trainerName
} has ${notification.data?.status?.toLowerCase()} your request`;
break;
case "trainer_assignment":
title = notification.data?.title || "New Client Assignment";
body =
notification.data?.message ||
`You have been assigned to train ${notification.data?.name}.`;
break;
case "trainer_assigned_to_client":
title = notification.data?.title || "Trainer Assigned";
body =
notification.data?.message ||
`${notification.data?.trainerName} has been assigned as your trainer.`;
break;
case "trainer_update_owner":
title = notification.data?.title || "Trainer Schedule Update";
body =
notification.data?.message || "A trainer has updated their schedule";
break;
case "trainer_update_client":
title = notification.data?.title || "Schedule Update";
body =
notification.data?.message || "Your training schedule has been updated";
if (notification.data?.oldTimeSlot && notification.data?.newTimeSlot) {
body += ` from ${notification.data.oldTimeSlot} to ${notification.data.newTimeSlot}`;
if (notification.data?.formattedDate) {
body += ` on ${notification.data.formattedDate}`;
}
}
break;
case "plan_renewal":
title = notification.data?.title || "Plan Renewal";
body =
notification.data?.message ||
`Plan ${notification.data?.subscriptionName} has been renewed`;
break;
case "plan_assigned":
title = notification.data?.title || "New Plan Assigned";
body =
notification.data?.message ||
`You have been assigned ${notification.data?.subscriptionName} at ${notification.data?.gymName}`;
break;
case "plan_expired":
title = notification.data?.title || "Plan Expired";
body =
notification.data?.message ||
`${notification.data?.clientName}'s membership for ${notification.data?.planName} has expired.`;
break;
case "plan_expiring_soon":
title = notification.data?.title || "Plan Expiring Soon";
body =
notification.data?.message ||
`${notification.data?.clientName}'s membership for ${notification.data?.planName} will expire on ${notification.data?.formattedExpiryDate}.`;
break;
case "trainer_client_plan_expired":
title = notification.data?.title || "Client Plan Expired";
body =
notification.data?.message ||
`${notification.data?.clientName}'s membership for ${notification.data?.planName} has expired.`;
break;
case "trainer_client_plan_expiring":
title = notification.data?.title || "Client Plan Expiring Soon";
body =
notification.data?.message ||
`${notification.data?.clientName}'s membership for ${notification.data?.planName} will expire on ${notification.data?.formattedExpiryDate}.`;
break;
case "schedule_update":
title = notification.data?.title || "Schedule Update";
body =
notification.data?.message || "Your training schedule has been updated";
if (notification.data?.oldTimeSlot && notification.data?.newTimeSlot) {
body += ` from ${notification.data.oldTimeSlot} to ${notification.data.newTimeSlot}`;
if (notification.data?.formattedDate) {
body += ` on ${notification.data.formattedDate}`;
}
}
break;
case "attendance_dispute":
title = notification.data?.title || "Attendance Dispute";
body =
notification.data?.message ||
`${notification.data?.name} has disputed an attendance record`;
if (notification.data?.logTime) {
body += ` for ${notification.data.logTime}`;
}
break;
case "day_pass_entry":
const isAccepted = notification.data?.status === "ACCEPTED";
title =
notification.data?.title ||
(isAccepted ? "Day Pass Approved" : "Day Pass Denied");
body =
notification.data?.message ||
(isAccepted
? "Your day pass has been approved"
: "Your day pass has been denied");
break;
case "client_invitations":
if (notification.data?.userId || notification.data?.invitorId) {
const isAccept = notification.data?.status === "ACCEPTED";
title =
notification.data?.title ||
(isAccept ? "Invitation Accepted" : "Invitation Rejected");
body =
notification.data?.message ||
(isAccept
? `The invitation for ${notification.data?.subscriptionName} you shared with ${notification.data?.name} has been accepted`
: `The invitation for ${notification.data?.subscriptionName} you shared with ${notification.data?.name} has been rejected`);
} else if (notification.data?.phoneNumber) {
const invitationStatus = getInvitationStatus(notification.data?.status);
title =
notification.data?.title || getInvitationTitle(invitationStatus);
body =
notification.data?.message ||
getInvitationBody(invitationStatus, notification.data?.name);
fcmData.status = invitationStatus;
}
break;
default:
logger.info(
`Using default handling for notification type: ${notification.type}`
);
title =
notification.data?.title ||
(notification.type
? `${notification.type.replace("_", " ").toUpperCase()}`
: "Notification");
break;
}
const notificationMessage: admin.messaging.TokenMessage = {
notification: { title, body },
data: fcmData,
android: {
priority: "high",
notification: {
channelId: "notifications_channel",
priority: "high",
defaultSound: true,
defaultVibrateTimings: true,
icon: "@mipmap/ic_launcher",
clickAction: "FLUTTER_NOTIFICATION_CLICK",
},
},
apns: {
payload: {
aps: {
sound: "default",
badge: 1,
},
},
},
token: fcmToken,
};
logger.info(`Prepared notification: ${title} - ${body}`);
return notificationMessage;
}
function getInvitationStatus(status?: string): string {
if (status === "ACCEPTED") return "accepted";
if (status === "REJECTED") return "rejected";
if (status === "PENDING") return "pending";
return "unknown";
}
function getInvitationTitle(status: string): string {
switch (status) {
case "accepted":
return "Invitation Accepted";
case "rejected":
return "Invitation Rejected";
case "pending":
return "New Invitation";
default:
return "Invitation Update";
}
}
function getInvitationBody(status: string, name?: string): string {
switch (status) {
case "accepted":
return `You have accepted the invitation from ${name}`;
case "rejected":
return `You have rejected the invitation from ${name}`;
case "pending":
return `You have a new invitation pending from ${name}`;
default:
return "There is an update to your invitation";
}
}
async function markNotificationAsSent(notificationId: string): Promise<void> {
try {
await app
.firestore()
.collection("notifications")
.doc(notificationId)
.update({
notificationSent: true,
sentAt: admin.firestore.FieldValue.serverTimestamp(),
});
logger.info(`Notification ${notificationId} marked as sent`);
} catch (error) {
logger.error(`Error marking notification as sent: ${error}`);
}
}
async function updateNotificationWithError(
notificationId: string,
error: string
): Promise<void> {
try {
await app
.firestore()
.collection("notifications")
.doc(notificationId)
.update({
notificationError: error,
notificationSent: false,
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
});
logger.info(`Notification ${notificationId} marked with error: ${error}`);
} catch (updateError) {
logger.error(`Error updating notification with error: ${updateError}`);
}
}

View File

@ -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 { v4: uuidv4 } = require('uuid');
const corsHandler = getCorsHandler();
const admin = getAdmin();
const logger = getLogger();
interface CashfreeLinkRequest {
amount: number;
customerName?: string;
customerEmail: string;
customerPhone: string;
productInfo?: string;
userId?: string;
gymId?: string;
orderId: string;
}
interface CashfreeLinkResponse {
link_id: string;
link_url: string;
link_expiry_time: string;
link_status: string;
link_qrcode: string;
[key: string]: any;
}
export const createCashfreeLink = 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];
const decodedToken = await admin.auth().verifyIdToken(idToken);
const uid = decodedToken.uid;
const linkRequest = request.body as CashfreeLinkRequest;
if (!linkRequest.amount || !linkRequest.customerEmail || !linkRequest.customerPhone) {
response.status(400).json({ error: 'Missing required fields' });
return;
}
const clientId = process.env.CASHFREE_CLIENT_ID;
const clientSecret = process.env.CASHFREE_CLIENT_SECRET;
if (!clientId || !clientSecret) {
logger.error('Cashfree credentials not configured');
response.status(500).json({ error: 'Payment gateway configuration error' });
return;
}
const expirationDate = new Date(Date.now() + 24 * 60 * 60 * 1000);
const expirationString = expirationDate.toISOString();
const apiUrl = process.env.CASHFREE_LINK_URL;
const linkId = uuidv4();
const requestHeaders = {
'x-client-id': clientId,
'x-client-secret': clientSecret,
'x-api-version': '2025-01-01',
'Content-Type': 'application/json'
};
const requestBody = {
link_id: linkId,
link_amount: linkRequest.amount,
link_currency: "INR",
link_purpose: linkRequest.productInfo,
customer_details: {
customer_phone: linkRequest.customerPhone,
customer_email: linkRequest.customerEmail,
customer_name: linkRequest.customerName,
},
link_partial_payments: false,
link_notify: {
send_sms: true,
send_email: true
},
link_expiry_time: expirationString,
link_notes: {
order_id: linkRequest.orderId,
gym_id: linkRequest.gymId,
user_id: linkRequest.userId
}
};
const cashfreeResponse = await axios.post<CashfreeLinkResponse>(
apiUrl!,
requestBody,
{ headers: requestHeaders }
);
await admin.firestore().collection('payment_links').doc(linkRequest.orderId).set({
requestUserId: uid,
amount: linkRequest.amount,
customerEmail: linkRequest.customerEmail,
customerPhone: linkRequest.customerPhone,
userId: linkRequest.userId,
gymId: linkRequest.gymId,
orderId: linkRequest.orderId,
...cashfreeResponse.data,
createdAt: admin.firestore.FieldValue.serverTimestamp(),
});
response.json({
success: true,
linkId: linkId,
linkUrl: cashfreeResponse.data.link_url,
linkExpiryTime: cashfreeResponse.data.link_expiry_time,
linkStatus: cashfreeResponse.data.link_status,
linkQRCode: cashfreeResponse.data.link_qrcode
});
} catch (error: any) {
logger.error('Cashfree link creation error:', error);
const statusCode = error.response?.status || 500;
response.status(statusCode).json({
success: false,
error: error.response?.data?.message || 'Failed to create payment link',
details: error.response?.data || error.message
});
}
});
});

View File

@ -0,0 +1,118 @@
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 CashfreeOrderRequest {
amount: number;
customerName?: string;
customerEmail: string;
customerPhone: string;
productInfo?: string;
userId?: string;
gymId?: string;
orderId: string;
webHostUrl: string;
}
interface CashfreeOrderResponse {
order_id: string;
payment_session_id: string;
[key: string]: any;
}
export const createCashfreeOrder = 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];
const decodedToken = await admin.auth().verifyIdToken(idToken);
const uid = decodedToken.uid;
const orderRequest = request.body as CashfreeOrderRequest;
if (!orderRequest.amount || !orderRequest.customerEmail || !orderRequest.customerPhone) {
response.status(400).json({ error: 'Missing required fields' });
return;
}
const clientId = process.env.CASHFREE_CLIENT_ID;
const clientSecret = process.env.CASHFREE_CLIENT_SECRET;
if (!clientId || !clientSecret) {
logger.error('Cashfree credentials not configured');
response.status(500).json({ error: 'Payment gateway configuration error' });
return;
}
const hashKey = `hash_${Date.now()}_${uid.substring(0, 1)}_${orderRequest.orderId}`;
const apiUrl = process.env.CASHFREE_URL;
const cashfreeResponse = await axios.post<CashfreeOrderResponse>(
apiUrl!,
{
order_id: orderRequest.orderId,
hash_key: hashKey,
order_amount: orderRequest.amount,
order_currency: 'INR',
customer_details: {
customer_id: uid,
customer_name: orderRequest.customerName || 'Fitlien User',
customer_email: orderRequest.customerEmail,
customer_phone: orderRequest.customerPhone
},
order_meta: {
return_url: `https://${orderRequest.webHostUrl}?order_id=${orderRequest.orderId}&hash_key=${hashKey}&user_id=${orderRequest.userId}&gym_id=${orderRequest.gymId}#/payment-status-screen`,
},
order_note: orderRequest.productInfo || 'Fitlien Membership'
},
{
headers: {
'x-api-version': '2022-09-01',
'x-client-id': clientId,
'x-client-secret': clientSecret,
'Content-Type': 'application/json'
}
}
);
await admin.firestore().collection('payment_orders').doc(orderRequest.orderId).set({
userId: uid,
amount: orderRequest.amount,
customerEmail: orderRequest.customerEmail,
customerPhone: orderRequest.customerPhone,
orderStatus: 'CREATED',
paymentGateway: 'Cashfree',
createdAt: admin.firestore.FieldValue.serverTimestamp(),
hashKey: hashKey,
clientId: orderRequest.userId,
gymId: orderRequest.gymId,
orderId: orderRequest.orderId,
...cashfreeResponse.data
});
response.json({
success: true,
order_id: cashfreeResponse.data.order_id,
payment_session_id: cashfreeResponse.data.payment_session_id
});
} catch (error: any) {
logger.error('Cashfree order creation error:', error);
const statusCode = error.response?.status || 500;
response.status(statusCode).json({
success: false,
error: error.response?.data?.message || 'Failed to create payment order',
details: error.response?.data || error.message
});
}
});
});

View File

@ -0,0 +1,3 @@
export { createCashfreeLink } from './createLink';
export { verifyCashfreePayment } from './verifyPayment';
export { createCashfreeOrder } from './createOrder';

View File

@ -0,0 +1,65 @@
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 logger = getLogger();
const corsHandler = getCorsHandler();
const admin = getAdmin();
interface CashfreePaymentResponse {
order_status: string;
[key: string]: any;
}
export const verifyCashfreePayment = onRequest({
region: '#{SERVICES_RGN}#'
}, async (request: Request, response) => {
return corsHandler(request, response, async () => {
try {
const orderId = request.body.order_id || request.query.order_id;
if (!orderId) {
response.status(400).json({ error: 'Order ID is required' });
return;
}
const clientId = process.env.CASHFREE_CLIENT_ID;
const clientSecret = process.env.CASHFREE_CLIENT_SECRET;
const apiUrl = `${process.env.CASHFREE_URL}/${orderId}`;
const cashfreeResponse = await axios.get<CashfreePaymentResponse>(
apiUrl!,
{
headers: {
'x-api-version': '2022-09-01',
'x-client-id': clientId,
'x-client-secret': clientSecret
}
}
);
await admin.firestore().collection('payment_orders').doc(orderId).update({
orderStatus: cashfreeResponse.data.order_status,
paymentDetails: cashfreeResponse.data,
updatedAt: admin.firestore.FieldValue.serverTimestamp()
});
if (request.headers['x-webhook-source'] === 'cashfree') {
response.status(200).send('OK');
return;
}
response.json({
status: cashfreeResponse.data.order_status,
paymentDetails: cashfreeResponse.data
});
} catch (error: any) {
logger.error('Cashfree payment verification error:', error);
const statusCode = error.response?.status || 500;
response.status(statusCode).json({
error: 'Failed to verify payment status',
details: error.response?.data || error.message
});
}
});
});

View File

@ -0,0 +1,2 @@
export * from './cashfree';
export * from './phonepe';

View File

@ -0,0 +1,257 @@
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";
import { updatePaymentDataAfterSuccess } from "./paymentData";
import { InvoiceService } from "./invoice/invoiceService";
const admin = getAdmin();
const logger = getLogger();
const corsHandler = getCorsHandler();
const invoiceService = new InvoiceService();
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];
const orderData = orderDoc.data();
await orderDoc.ref.update({
orderStatus: statusResponse.data.state || 'UNKNOWN',
lastChecked: new Date(),
statusResponse: statusResponse.data
});
if (statusResponse.data.state === 'COMPLETED') {
try {
// Update payment data
const paymentUpdateSuccess = await updatePaymentDataAfterSuccess(
merchantOrderId,
statusResponse.data.orderId,
statusResponse.data
);
if (paymentUpdateSuccess) {
// Extract membership ID from metaInfo
const membershipId = orderData.metaInfo?.membershipId;
if (membershipId) {
try {
// Get user data for invoice
const membershipDoc = await admin.firestore()
.collection('memberships')
.doc(membershipId)
.get();
if (membershipDoc.exists) {
const membershipData = membershipDoc.data();
const userId = membershipData?.userId;
// Get user details
const userDoc = await admin.firestore()
.collection('users')
.doc(userId)
.get();
if (userDoc.exists) {
const userData = userDoc.data();
// Get gym details
const gymId = orderData.metaInfo?.gymId || membershipData?.gymId;
let gymName = 'Fitlien';
let gymAddress = '';
let gstNumber = '';
if (gymId) {
const gymDoc = await admin.firestore()
.collection('gyms')
.doc(gymId)
.get();
if (gymDoc.exists) {
const gymData = gymDoc.data();
gymName = gymData?.name || 'Fitlien';
gymAddress = gymData?.address || '';
gstNumber = gymData?.gstNumber || '';
}
}
// Generate invoice data
const invoiceData = {
invoiceNumber: `INV-${merchantOrderId.substring(0, 8)}`,
businessName: gymName,
address: gymAddress,
gstNumber: gstNumber,
customerName: userData?.displayName || `${membershipData?.['first-name'] || ''} ${membershipData?.['last-name'] || ''}`.trim(),
phoneNumber: membershipData?.['phone-number'] || orderData.metaInfo?.phoneNumber || '',
email: membershipData?.['email'] || '',
planName: orderData.metaInfo?.planName || 'Membership',
amount: orderData.amount,
transactionId: statusResponse.data.orderId,
paymentDate: new Date(),
paymentMethod: 'Online'
};
const invoicePath = await invoiceService.generateInvoice(invoiceData);
// Update payment record with invoice path
await admin.firestore()
.collection('membership_payments')
.doc(membershipId)
.get()
.then(async (doc) => {
if (doc.exists) {
const paymentsData = doc.data()?.payments || [];
for (let i = 0; i < paymentsData.length; i++) {
if (paymentsData[i].referenceNumber === merchantOrderId ||
paymentsData[i].transactionId === statusResponse.data.orderId) {
paymentsData[i].invoicePath = invoicePath;
break;
}
}
await doc.ref.update({
'payments': paymentsData,
'updatedAt': admin.firestore.FieldValue.serverTimestamp(),
});
}
});
logger.info(`Generated invoice for payment: ${merchantOrderId}, path: ${invoicePath}`);
}
}
} catch (invoiceError) {
logger.error('Error generating invoice:', invoiceError);
// Continue processing - don't fail the response
}
}
}
logger.info(`Payment data updated for completed payment: ${merchantOrderId}`);
} catch (paymentUpdateError) {
logger.error('Error updating payment data:', paymentUpdateError);
// Continue processing - don't fail the response
}
}
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
});
}
});
});

View File

@ -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
});
}
});
});

View File

@ -0,0 +1,5 @@
export { createPhonePeOrder } from './createPhonepeOrder';
export { checkPhonePePaymentStatus } from './checkStatus';
export { phonePeWebhook } from './webhook';
export { updatePaymentDataAfterSuccess } from './paymentData';
export * from './invoice';

View File

@ -0,0 +1,104 @@
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 { InvoiceService, InvoiceData } from "./invoiceService";
const admin = getAdmin();
const logger = getLogger();
const corsHandler = getCorsHandler();
const invoiceService = new InvoiceService();
export const directGenerateInvoice = 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 {
invoiceNumber,
businessName,
address,
gstNumber,
customerName,
phoneNumber,
email,
planName,
amount,
transactionId,
paymentDate,
paymentMethod,
sendEmail,
emailOptions
} = request.body;
if (!invoiceNumber || !businessName || !customerName || !amount || !transactionId) {
response.status(400).json({
success: false,
error: 'Missing required fields'
});
return;
}
const invoiceData: InvoiceData = {
invoiceNumber,
businessName,
address: address || '',
gstNumber,
customerName,
phoneNumber: phoneNumber || '',
email: email || '',
planName: planName || 'Membership',
amount: parseFloat(amount),
transactionId,
paymentDate: paymentDate ? new Date(paymentDate) : new Date(),
paymentMethod: paymentMethod || 'Online'
};
const invoicePath = await invoiceService.generateInvoice(invoiceData);
const downloadUrl = await invoiceService.getInvoiceDownloadUrl(invoicePath);
let emailSent = false;
if (sendEmail && email) {
emailSent = await invoiceService.sendInvoiceEmail(invoicePath, {
recipientEmail: email,
recipientName: customerName,
...emailOptions
});
}
response.json({
success: true,
invoicePath,
downloadUrl,
emailSent
});
} catch (authError: any) {
logger.error('Authentication error:', authError);
response.status(401).json({
success: false,
error: 'Invalid authentication token',
details: authError.message
});
}
} catch (error: any) {
logger.error('Error generating invoice:', error);
response.status(500).json({
success: false,
error: 'Failed to generate invoice',
details: error.message
});
}
});
});

View File

@ -0,0 +1,62 @@
// 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 { InvoiceService } from "./invoiceService";
// const admin = getAdmin();
// const logger = getLogger();
// const corsHandler = getCorsHandler();
// const invoiceService = new InvoiceService();
// export const getInvoiceUrl = 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 { invoicePath } = request.query;
// if (!invoicePath) {
// response.status(400).json({
// success: false,
// error: 'Missing invoice path'
// });
// return;
// }
// const downloadUrl = await invoiceService.getInvoiceDownloadUrl(invoicePath as string);
// response.json({
// success: true,
// downloadUrl
// });
// } catch (authError: any) {
// logger.error('Authentication error:', authError);
// response.status(401).json({
// success: false,
// error: 'Invalid authentication token',
// details: authError.message
// });
// }
// } catch (error: any) {
// logger.error('Error getting invoice URL:', error);
// response.status(500).json({
// success: false,
// error: 'Failed to get invoice URL',
// details: error.message
// });
// }
// });
// });

View File

@ -0,0 +1,13 @@
// import { getInvoiceUrl } from './getInvoiceUrl';
import { InvoiceService } from './invoiceService';
// import { processInvoice } from './processInvoice';
// import { sendInvoiceEmail } from './sendInvoiceEmail';
import { directGenerateInvoice } from './directInvoice';
export {
// getInvoiceUrl,
InvoiceService,
// processInvoice,
// sendInvoiceEmail,
directGenerateInvoice,
};

View File

@ -0,0 +1,386 @@
import { getAdmin, getLogger } from "../../../shared/config";
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { format } from 'date-fns';
import { sendEmailWithAttachmentUtil } from "../../../utils/emailService";
import { jsPDF } from "jspdf";
import autoTable from 'jspdf-autotable';
const admin = getAdmin();
const logger = getLogger();
export interface InvoiceData {
invoiceNumber: string;
businessName: string;
address: string;
gstNumber?: string;
customerName: string;
phoneNumber: string;
email: string;
planName: string;
amount: number;
transactionId: string;
paymentDate: Date;
paymentMethod: string;
}
export interface EmailOptions {
recipientEmail: string;
recipientName?: string;
subject?: string;
customHtml?: string;
additionalData?: {
gymName?: string;
planName?: string;
amount?: number;
transactionId?: string;
paymentDate?: Date;
paymentMethod?: string;
};
}
export class InvoiceService {
async generateInvoice(data: InvoiceData): Promise<string> {
try {
const tempFilePath = path.join(os.tmpdir(), `invoice_${data.invoiceNumber}.pdf`);
const hasGst = data.gstNumber && data.gstNumber.length > 0;
const baseAmount = hasGst ? data.amount / 1.18 : data.amount;
const sgst = hasGst ? baseAmount * 0.09 : 0;
const cgst = hasGst ? baseAmount * 0.09 : 0;
const formattedDate = format(data.paymentDate, 'dd/MM/yyyy');
const doc = new jsPDF();
doc.setFontSize(20);
doc.setFont('helvetica', 'bold');
doc.text(data.businessName, 15, 20);
doc.setFontSize(12);
doc.setFont('helvetica', 'normal');
const maxWidth = 170;
const lineHeight = 5;
const addressLines = doc.splitTextToSize(data.address, maxWidth);
if (addressLines.length <= 2) {
for (let i = 0; i < addressLines.length; i++) {
doc.text(addressLines[i], 15, 30 + (i * lineHeight));
}
} else {
doc.text(addressLines[0], 15, 30);
let secondLine = addressLines[1];
if (secondLine.length > 3) {
secondLine = secondLine.substring(0, secondLine.length - 3) + '...';
} else {
secondLine += '...';
}
doc.text(secondLine, 15, 35);
}
const gstYPosition = 40;
if (hasGst) {
doc.text(`GSTIN: ${data.gstNumber}`, 20, gstYPosition);
}
doc.setFontSize(24);
doc.setFont('helvetica', 'bold');
doc.text('RECEIPT', 195, 20, { align: 'right' });
doc.setFontSize(12);
doc.text(`Receipt #: ${data.invoiceNumber}`, 195, 30, { align: 'right' });
doc.text(`Date: ${formattedDate}`, 195, 40, { align: 'right' });
doc.setLineWidth(0.5);
doc.line(15, 45, 195, 45);
doc.setFontSize(12);
const receiptToBoxX = 15;
const receiptToBoxY = 50;
const receiptToBoxWidth = 100;
const receiptToBoxHeight = 36;
doc.setDrawColor(0, 0, 0);
doc.setLineWidth(0.5);
doc.rect(receiptToBoxX, receiptToBoxY, receiptToBoxWidth, receiptToBoxHeight);
doc.setFont('helvetica', 'bold');
doc.text('Receipt To:', 18, 60);
doc.setFont('helvetica', 'normal');
doc.text(data.customerName, 18, 70);
doc.text(`${data.phoneNumber}`, 18, 75);
doc.text(`${data.email}`, 18, 80);
autoTable(doc, {
startY: 110,
margin: {left: 15, right: 15},
head: [
[
{content: 'No.', styles: {halign: 'center'}},
{content: 'Description', styles: {halign: 'left'}},
{content: 'HSN/SAC', styles: {halign: 'center'}},
{content: 'Amount (INR)', styles: {halign: 'right'}}
]
],
body: [
['1', `${data.planName} Subscription`, '999723', baseAmount.toFixed(2)]
],
headStyles: {
fillColor: [220, 220, 220],
textColor: [0, 0, 0],
fontStyle: 'bold',
lineWidth: 0.5,
lineColor: [0, 0, 0]
},
styles: {
halign: 'center',
lineWidth: 0.5,
lineColor: [0, 0, 0]
},
columnStyles: {
0: { halign: 'center', cellWidth: 20 },
1: { halign: 'left' },
2: { halign: 'center', cellWidth: 40 },
3: { halign: 'right', cellWidth: 40 }
},
theme: 'grid',
tableLineWidth: 0.5,
tableLineColor: [0, 0, 0],
});
const finalY = (doc as any).lastAutoTable.finalY + 20;
if (hasGst) {
doc.text('Taxable Amount:', 150, finalY, { align: 'right' });
doc.text(`${baseAmount.toFixed(2)} INR`, 195, finalY, { align: 'right' });
doc.text('SGST (9%):', 150, finalY + 10, { align: 'right' });
doc.text(`${sgst.toFixed(2)} INR`, 195, finalY + 10, { align: 'right' });
doc.text('CGST (9%):', 150, finalY + 20, { align: 'right' });
doc.text(`${cgst.toFixed(2)} INR`, 195, finalY + 20, { align: 'right' });
doc.setLineWidth(0.5);
doc.line(15, finalY + 25, 195, finalY + 25);
doc.setFont('helvetica', 'bold');
doc.text('Total Amount:', 150, finalY + 30, { align: 'right' });
doc.text(`${data.amount.toFixed(2)} INR`, 195, finalY + 30, { align: 'right' });
} else {
doc.setLineWidth(0.5);
doc.line(15, finalY - 5, 195, finalY - 5);
doc.setFont('helvetica', 'bold');
doc.text('Total Amount:', 150, finalY, { align: 'right' });
doc.text(`${data.amount.toFixed(2)} INR`, 195, finalY, { align: 'right' });
}
const paymentY = hasGst ? finalY + 50 : finalY + 20;
const boxX = 15;
const boxY = paymentY - 10;
const boxWidth = 100;
const boxHeight = 36;
doc.setDrawColor(0, 0, 0);
doc.setLineWidth(0.5);
doc.rect(boxX, boxY, boxWidth, boxHeight);
doc.setFont('helvetica', 'bold');
doc.text('Payment Information:', 18, paymentY);
doc.setFont('helvetica', 'normal');
doc.text(`Transaction ID: ${data.transactionId}`, 18, paymentY + 10);
doc.text(`Payment Method: ${data.paymentMethod}`, 18, paymentY + 15);
doc.text(`Payment Date: ${formattedDate}`, 18, paymentY + 20);
doc.setFontSize(12);
doc.setFont('helvetica', 'italic');
doc.text('Thank you for your business!', 105, 270, { align: 'center' });
doc.setFontSize(10);
doc.setFont('helvetica', 'normal');
doc.text('This is a computer-generated receipt and does not require a signature.', 105, 280, { align: 'center' });
fs.writeFileSync(tempFilePath, Buffer.from(doc.output('arraybuffer')));
const invoicePath = `invoices/${data.invoiceNumber}.pdf`;
const bucket = admin.storage().bucket();
await bucket.upload(tempFilePath, {
destination: invoicePath,
metadata: {
contentType: 'application/pdf',
},
});
fs.unlinkSync(tempFilePath);
return invoicePath;
} catch (error: any) {
logger.error('Error generating invoice:', error);
throw new Error(`Failed to generate invoice: ${error.message}`);
}
}
async getInvoiceDownloadUrl(invoicePath: string): Promise<string> {
try {
const bucket = admin.storage().bucket();
const file = bucket.file(invoicePath);
const expirationMs = 7 * 24 * 60 * 60 * 1000;
const [signedUrl] = await file.getSignedUrl({
action: 'read',
expires: Date.now() + expirationMs,
responseDisposition: `attachment; filename="${path.basename(invoicePath)}"`,
});
return signedUrl;
} catch (error: any) {
logger.error('Error getting invoice download URL:', error);
throw new Error(`Failed to get invoice download URL: ${error.message}`);
}
}
async updateInvoicePath(membershipId: string, paymentId: string, invoicePath: string): Promise<boolean> {
try {
const membershipPaymentsRef = admin.firestore()
.collection('membership_payments')
.doc(membershipId);
const docSnapshot = await membershipPaymentsRef.get();
if (!docSnapshot.exists) {
logger.error(`No membership payments found for membershipId: ${membershipId}`);
return false;
}
const data = docSnapshot.data();
const paymentsData = data?.payments || [];
let found = false;
for (let i = 0; i < paymentsData.length; i++) {
if (paymentsData[i].referenceNumber === paymentId ||
paymentsData[i].transactionId === paymentId) {
paymentsData[i].invoicePath = invoicePath;
found = true;
break;
}
}
if (!found) {
logger.error(`No payment found with ID: ${paymentId} in membership: ${membershipId}`);
return false;
}
await membershipPaymentsRef.update({
'payments': paymentsData,
'updatedAt': admin.firestore.FieldValue.serverTimestamp(),
});
logger.info(`Successfully updated invoice path for payment: ${paymentId}`);
return true;
} catch (error: any) {
logger.error('Error updating payment with invoice path:', error);
return false;
}
}
async sendInvoiceEmail(invoicePath: string, emailOptions: EmailOptions): Promise<boolean> {
try {
const downloadUrl = await this.getInvoiceDownloadUrl(invoicePath);
const formattedDate = emailOptions.additionalData?.paymentDate
? new Date(emailOptions.additionalData.paymentDate).toLocaleDateString('en-GB')
: new Date().toLocaleDateString('en-GB');
const emailHtml = emailOptions.customHtml || `
<html>
<body>
<h2>Thank you for your payment</h2>
<p>Dear ${emailOptions.recipientName || 'Valued Customer'},</p>
<p>Thank you for your payment. Your membership has been successfully activated.</p>
<p>Please find attached your invoice for the payment.</p>
<p>Membership Details:</p>
<ul>
<li>Gym: ${emailOptions.additionalData?.gymName || 'Fitlien'}</li>
<li>Plan: ${emailOptions.additionalData?.planName || 'Membership'}</li>
<li>Amount: ${emailOptions.additionalData?.amount || '0'}</li>
<li>Transaction ID: ${emailOptions.additionalData?.transactionId || 'N/A'}</li>
<li>Date: ${formattedDate}</li>
<li>Payment Method: ${emailOptions.additionalData?.paymentMethod || 'Online'}</li>
</ul>
<p>If you have any questions, please contact us.</p>
<p>Regards,<br>Fitlien Team</p>
</body>
</html>
`;
await sendEmailWithAttachmentUtil(
emailOptions.recipientEmail,
emailOptions.subject || 'Your Fitlien Membership Invoice',
emailHtml,
downloadUrl,
`Invoice_${path.basename(invoicePath)}`
);
logger.info(`Invoice email sent to ${emailOptions.recipientEmail}`);
return true;
} catch (error: any) {
logger.error('Error sending invoice email:', error);
return false;
}
}
async processInvoice(
membershipId: string,
paymentId: string,
invoiceData: InvoiceData,
emailOptions?: EmailOptions
): Promise<{
success: boolean;
invoicePath?: string;
downloadUrl?: string;
emailSent: boolean;
error?: string;
}> {
try {
const invoicePath = await this.generateInvoice(invoiceData);
const updateSuccess = await this.updateInvoicePath(membershipId, paymentId, invoicePath);
if (!updateSuccess) {
return {
success: false,
invoicePath,
emailSent: false,
error: 'Failed to update payment with invoice path'
};
}
const downloadUrl = await this.getInvoiceDownloadUrl(invoicePath);
let emailSent = false;
if (emailOptions && emailOptions.recipientEmail) {
emailSent = await this.sendInvoiceEmail(invoicePath, emailOptions);
}
return {
success: true,
invoicePath,
downloadUrl,
emailSent
};
} catch (error: any) {
logger.error('Error processing invoice:', error);
return {
success: false,
emailSent: false,
error: error.message
};
}
}
}

View File

@ -0,0 +1,83 @@
// 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 { InvoiceService } from "./invoiceService";
// const admin = getAdmin();
// const logger = getLogger();
// const corsHandler = getCorsHandler();
// const invoiceService = new InvoiceService();
// export const processInvoice = 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 {
// membershipId,
// paymentId,
// invoiceData,
// emailOptions
// } = request.body;
// if (!membershipId || !paymentId || !invoiceData) {
// response.status(400).json({
// success: false,
// error: 'Missing required fields'
// });
// return;
// }
// const result = await invoiceService.processInvoice(
// membershipId,
// paymentId,
// invoiceData,
// emailOptions
// );
// if (!result.success) {
// response.status(400).json({
// success: false,
// error: result.error || 'Failed to process invoice'
// });
// return;
// }
// response.json({
// success: true,
// message: 'Invoice processed successfully',
// invoicePath: result.invoicePath,
// downloadUrl: result.downloadUrl,
// emailSent: result.emailSent
// });
// } catch (authError: any) {
// logger.error('Authentication error:', authError);
// response.status(401).json({
// success: false,
// error: 'Invalid authentication token',
// details: authError.message
// });
// }
// } catch (error: any) {
// logger.error('Error processing invoice:', error);
// response.status(500).json({
// success: false,
// error: 'Failed to process invoice',
// details: error.message
// });
// }
// });
// });

View File

@ -0,0 +1,91 @@
// 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 { InvoiceService, EmailOptions } from "./invoiceService";
// const admin = getAdmin();
// const logger = getLogger();
// const corsHandler = getCorsHandler();
// const invoiceService = new InvoiceService();
// export const sendInvoiceEmail = 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 {
// invoicePath,
// recipientEmail,
// recipientName,
// subject,
// customHtml,
// gymName,
// planName,
// amount,
// transactionId,
// paymentDate,
// paymentMethod
// } = request.body;
// if (!invoicePath || !recipientEmail) {
// response.status(400).json({
// success: false,
// error: 'Missing required fields'
// });
// return;
// }
// const emailOptions: EmailOptions = {
// recipientEmail,
// recipientName,
// subject,
// customHtml,
// additionalData: {
// gymName,
// planName,
// amount,
// transactionId,
// paymentDate: paymentDate ? new Date(paymentDate) : undefined,
// paymentMethod
// }
// };
// const emailSent = await invoiceService.sendInvoiceEmail(invoicePath, emailOptions);
// const downloadUrl = await invoiceService.getInvoiceDownloadUrl(invoicePath);
// response.json({
// success: true,
// message: emailSent ? 'Invoice email sent successfully' : 'Failed to send email but URL generated',
// downloadUrl
// });
// } catch (authError: any) {
// logger.error('Authentication error:', authError);
// response.status(401).json({
// success: false,
// error: 'Invalid authentication token',
// details: authError.message
// });
// }
// } catch (error: any) {
// logger.error('Error sending invoice email:', error);
// response.status(500).json({
// success: false,
// error: 'Failed to send invoice email',
// details: error.message
// });
// }
// });
// });

View File

@ -0,0 +1,122 @@
import { getAdmin, getLogger } from "../../shared/config";
const admin = getAdmin();
const logger = getLogger();
interface PaymentData {
id: string;
date: string;
dateTimestamp: FirebaseFirestore.Timestamp;
amount: any;
paymentMethod: string;
referenceNumber: string;
discount: any;
transactionId: string;
createdAt: Date;
invoicePath?: string;
}
export async function updatePaymentDataAfterSuccess(
merchantOrderId: string,
orderId: string,
paymentDetails: any,
invoicePath?: string
): Promise<boolean> {
try {
const orderQuery = await admin.firestore()
.collection('payment_orders')
.where('merchantOrderId', '==', merchantOrderId)
.limit(1)
.get();
if (orderQuery.empty) {
logger.error(`No payment order found with merchantOrderId: ${merchantOrderId}`);
return false;
}
const orderDoc = orderQuery.docs[0];
const orderData = orderDoc.data();
const membershipId = orderData.metaInfo?.membershipId;
if (!membershipId) {
logger.error(`No membershipId found in metaInfo for order: ${merchantOrderId}`);
return false;
}
const isoDate = new Date().toLocaleDateString('en-GB').split('/').join('-');
const dateTimestamp = admin.firestore.Timestamp.now();
const paymentData: PaymentData = {
id: admin.firestore().collection('_').doc().id,
date: isoDate,
dateTimestamp: dateTimestamp,
amount: orderData.amount,
paymentMethod: 'Online',
referenceNumber: merchantOrderId,
discount: orderData.metaInfo?.discount || null,
transactionId: orderId,
createdAt: new Date()
};
if (invoicePath) {
paymentData.invoicePath = invoicePath;
}
const membershipPaymentsRef = admin.firestore()
.collection('membership_payments')
.doc(membershipId);
const docSnapshot = await membershipPaymentsRef.get();
if (docSnapshot.exists) {
await membershipPaymentsRef.update({
'payments': admin.firestore.FieldValue.arrayUnion(paymentData),
'updatedAt': admin.firestore.FieldValue.serverTimestamp(),
'updatedBy': orderData.userId,
});
} else {
await membershipPaymentsRef.set({
'membershipId': membershipId,
'payments': [paymentData],
'createdAt': admin.firestore.FieldValue.serverTimestamp(),
'createdBy': orderData.userId,
'updatedAt': admin.firestore.FieldValue.serverTimestamp(),
'updatedBy': orderData.userId,
});
}
await updateMembershipStatus(membershipId, orderData.userId);
logger.info(`Successfully updated payment data for membership: ${membershipId}`);
return true;
} catch (error: any) {
logger.error('Error updating payment data:', error);
return false;
}
}
async function updateMembershipStatus(membershipId: string, userId: string): Promise<void> {
try {
const membershipDoc = await admin.firestore()
.collection('memberships')
.doc(membershipId)
.get();
if (!membershipDoc.exists) {
throw new Error(`Membership not found for id: ${membershipId}`);
}
await admin.firestore()
.collection('memberships')
.doc(membershipId)
.update({
'status': 'ACTIVE',
'updatedAt': admin.firestore.FieldValue.serverTimestamp(),
});
logger.info(`Successfully updated membership status for: ${membershipId}`);
} catch (error: any) {
logger.error('Error updating membership status:', error);
throw error;
}
}

View File

@ -0,0 +1,842 @@
import { onRequest } from "firebase-functions/v2/https";
import { Request } from "firebase-functions/v2/https";
import { getAdmin, getLogger } from "../../shared/config";
import crypto from "crypto";
import { updatePaymentDataAfterSuccess } from "./paymentData";
import { InvoiceService } from "./invoice/invoiceService";
import * as path from 'path';
import { sendEmailWithAttachmentUtil } from "../../utils/emailService";
import { format } from 'date-fns';
const admin = getAdmin();
const logger = getLogger();
const invoiceService = new InvoiceService();
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;
if (!authHeader || !username || !password) {
logger.error('Missing authorization header or webhook credentials');
response.status(401).json({ error: 'Unauthorized' });
return;
}
const credentialString = `${username}:${password}`;
const expectedAuth = crypto
.createHash('sha256')
.update(credentialString)
.digest('hex');
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();
let orderDoc;
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;
}
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 {
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}`);
}
logger.info(`Checking payment state`, {
state: payload.state,
stateType: typeof payload.state,
stateLength: payload.state ? payload.state.length : 0,
stateUpperCase: payload.state ? payload.state.toUpperCase() : null,
stateComparison: payload.state === 'COMPLETED'
});
if (payload.state && payload.state.trim().toUpperCase() === 'COMPLETED') {
try {
logger.info(`Starting payment update process for merchantOrderId: ${payload.merchantOrderId}`);
const orderData = orderDoc.data();
const membershipId = orderData.metaInfo?.membershipId;
const bookingId = orderData.metaInfo?.bookingId;
const paymentId = orderData.metaInfo?.paymentId;
if (bookingId) {
await processDayPassBooking(payload, orderData, bookingId);
} else if (membershipId) {
const paymentUpdateSuccess = await updatePaymentDataAfterSuccess(
payload.merchantOrderId,
payload.orderId,
payload
);
logger.info(`Payment update result for membershipId: ${membershipId}`, {
success: paymentUpdateSuccess,
orderId: payload.orderId
});
if (paymentUpdateSuccess) {
await processMembershipPayment(payload, orderData, membershipId);
}
} else if (paymentId) {
await processServicePayment(payload, orderData, paymentId);
} else {
logger.error(`No membershipId, bookingId, or paymentId found in metaInfo for order: ${payload.merchantOrderId}`);
}
logger.info(`Payment data updated for completed payment: ${payload.merchantOrderId}`);
} catch (paymentUpdateError) {
logger.error('Error updating payment data:', paymentUpdateError);
}
}
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
});
}
});
async function processDayPassBooking(payload: any, orderData: any, bookingId: string) {
try {
logger.info(`Processing day pass booking for bookingId: ${bookingId}`);
const bookingRef = admin.firestore().collection('day_pass_bookings').doc(bookingId);
const bookingDoc = await bookingRef.get();
if (!bookingDoc.exists) {
logger.error(`Day pass booking not found for bookingId: ${bookingId}`);
return;
}
await bookingRef.update({
status: 'ACCEPTED',
paymentDetails: {
transactionId: payload.orderId,
merchantOrderId: payload.merchantOrderId,
amount: orderData.amount,
paymentDate: new Date(),
paymentMethod: 'PhonePe'
},
updatedAt: admin.firestore.FieldValue.serverTimestamp()
});
logger.info(`Updated day pass booking status to 'Accepted' for bookingId: ${bookingId}`);
const bookingData = bookingDoc.data();
const gymId = orderData.metaInfo?.gymId || bookingData?.gymId;
if (gymId) {
try {
const gymDoc = await admin.firestore().collection('gyms').doc(gymId).get();
let gymName = 'Fitlien';
let gymAddress = '';
let gymOwnerEmail = '';
if (gymDoc.exists) {
const gymData = gymDoc.data();
gymName = gymData?.name || 'Fitlien';
gymAddress = gymData?.address || '';
if (gymData?.userId) {
const gymOwnerDoc = await admin.firestore()
.collection('users')
.doc(gymData.userId)
.get();
if (gymOwnerDoc.exists) {
const gymOwnerData = gymOwnerDoc.data();
gymOwnerEmail = gymOwnerData?.email || '';
}
}
}
const invoiceNumber = `INV-${payload.merchantOrderId.substring(0, 8)}`;
logger.info(`Generated invoice number for day pass: ${invoiceNumber}`);
const invoiceData = {
invoiceNumber,
businessName: gymName,
address: gymAddress,
gstNumber: orderData.metaInfo?.gstNumber,
customerName: orderData.metaInfo?.customerName || bookingData?.customerName || '',
phoneNumber: orderData.metaInfo?.customerPhone || bookingData?.phoneNumber || '',
email: orderData.metaInfo?.customerEmail || bookingData?.email || '',
planName: 'Day Pass',
amount: orderData.amount,
transactionId: payload.orderId,
paymentDate: new Date(),
paymentMethod: 'Online'
};
const invoicePath = await invoiceService.generateInvoice(invoiceData);
logger.info(`Day pass invoice generated successfully at path: ${invoicePath}`);
await bookingRef.update({
invoicePath: invoicePath,
invoiceNumber: invoiceNumber
});
logger.info(`Updated day pass booking with invoice path: ${invoicePath}`);
const downloadUrl = await invoiceService.getInvoiceDownloadUrl(invoicePath);
const formattedDate = format(new Date(), 'dd/MM/yyyy');
if (gymOwnerEmail) {
logger.info(`Preparing to send day pass invoice email to gym owner: ${gymOwnerEmail}`);
try {
const ownerEmailSubject = `New Day Pass Payment - ${gymName}`;
const gymOwnerEmailHtml = `
<html>
<body>
<h2>New Day Pass Payment Received</h2>
<p>Dear Gym Owner,</p>
<p>A new day pass payment has been received for your gym.</p>
<p>Customer Details:</p>
<ul>
<li>Name: ${invoiceData.customerName}</li>
<li>Phone: ${invoiceData.phoneNumber}</li>
</ul>
<p>Day Pass Details:</p>
<ul>
<li>Service: Day Pass</li>
<li>Amount: ${orderData.amount.toFixed(2)}</li>
<li>Transaction ID: ${payload.merchantOrderId}</li>
<li>Date: ${formattedDate}</li>
</ul>
<p>Please find the invoice attached.</p>
<p>Regards,<br>Fitlien Team</p>
</body>
</html>
`;
await sendEmailWithAttachmentUtil(
gymOwnerEmail,
ownerEmailSubject,
gymOwnerEmailHtml,
downloadUrl,
`Invoice_${path.basename(invoicePath)}`
);
logger.info(`Day pass invoice email sent to gym owner (${gymOwnerEmail}) for payment: ${payload.merchantOrderId}`);
} catch (ownerEmailError) {
logger.error('Error sending gym owner day pass invoice email:', ownerEmailError);
}
}
} catch (invoiceError) {
logger.error('Error generating day pass invoice:', invoiceError);
}
}
} catch (error) {
logger.error('Error processing day pass booking:', error);
}
}
async function processMembershipPayment(payload: any, orderData: any, membershipId: string) {
logger.info(`Processing membership for completed payment`, {
merchantOrderId: payload.merchantOrderId,
orderId: payload.orderId,
membershipId: membershipId || 'not-provided'
});
if (membershipId) {
try {
logger.info(`Fetching membership data for membershipId: ${membershipId}`);
const membershipDoc = await admin.firestore()
.collection('memberships')
.doc(membershipId)
.get();
if (membershipDoc.exists) {
logger.info(`Membership data retrieved successfully for membershipId: ${membershipId}`);
const membershipData = membershipDoc.data();
const uid = membershipData?.userId;
logger.info(`Fetching user data for uid(Client): ${uid}`);
const userDoc = await admin.firestore()
.collection('client_profiles')
.doc(uid)
.get();
if (userDoc.exists) {
logger.info(`User data retrieved successfully for uid(Client): ${uid}`);
logger.info(`Starting invoice generation process for payment: ${payload.merchantOrderId}`);
const userData = userDoc.data();
const gymId = orderData.metaInfo?.gymId || membershipData?.gymId;
let gymName = 'Fitlien';
let gymAddress = '';
let subscriptionName = '';
let gymOwnerEmail = '';
let paymentType = orderData.metaInfo?.paymentType || 'Gym Membership';
let trainerId = orderData.metaInfo?.trainerId;
let trainerData = null;
let emailCustomer = membershipData?.fields?.['email'] || membershipData?.fields?.['Email Address'];
const discountPercentage = orderData.metaInfo?.discount || 0;
const hasDiscount = discountPercentage > 0;
const isFreeplan = discountPercentage === 100;
const originalAmount = hasDiscount ?
orderData.amount / (1 - discountPercentage / 100) :
orderData.amount;
const discountText = isFreeplan ?
" (Free Plan)" :
hasDiscount ? ` (${discountPercentage.toFixed(0)}% discount applied)` :
'';
const amountSaved = hasDiscount ?
originalAmount - orderData.amount :
0;
if (gymId) {
const gymDoc = await admin.firestore()
.collection('gyms')
.doc(gymId)
.get();
if (gymDoc.exists) {
const gymData = gymDoc.data();
gymName = gymData?.name || 'Fitlien';
gymAddress = gymData?.address || '';
subscriptionName = membershipData?.subscription?.normalizedName || '';
if (gymData?.userId) {
const gymOwnerDoc = await admin.firestore()
.collection('users')
.doc(gymData.userId)
.get();
if (gymOwnerDoc.exists) {
const gymOwnerData = gymOwnerDoc.data();
gymOwnerEmail = gymOwnerData?.email || '';
}
}
}
}
if (paymentType === 'Gym Membership with Personal Training' && trainerId) {
try {
const trainerDoc = await admin.firestore()
.collection('trainer_profiles')
.doc(trainerId)
.get();
if (trainerDoc.exists) {
trainerData = trainerDoc.data();
}
} catch (trainerError) {
logger.error('Error fetching trainer data:', trainerError);
}
}
const invoiceNumber = `INV-${payload.merchantOrderId.substring(0, 8)}`;
logger.info(`Generated invoice number: ${invoiceNumber}`);
logger.info(`Preparing invoice data for generation`, {
invoiceNumber,
merchantOrderId: payload.merchantOrderId,
gymName: gymName
});
const invoiceData = {
invoiceNumber,
businessName: gymName,
address: gymAddress,
gstNumber: userData?.gstNumber,
customerName: userData?.displayName || `${membershipData?.fields?.['first-name'] || ''} ${membershipData?.fields?.['last-name'] || ''}`.trim() || membershipData?.fields?.['First Name'] || '',
phoneNumber: membershipData?.fields?.['phone-number'] || membershipData?.fields?.['Phone Number'] || orderData.metaInfo?.phoneNumber || '',
email: membershipData?.fields?.['email'] || membershipData?.fields?.['Email Address'] || '',
planName: orderData.metaInfo?.planName || subscriptionName,
amount: orderData.amount,
transactionId: payload.orderId,
paymentDate: new Date(),
paymentMethod: 'Online'
};
const invoicePath = await invoiceService.generateInvoice(invoiceData);
logger.info(`Invoice generated successfully at path: ${invoicePath}`);
logger.info(`Updating membership payment with invoice path`, {
membershipId,
invoicePath
});
await admin.firestore()
.collection('membership_payments')
.doc(membershipId)
.get()
.then(async (doc) => {
if (doc.exists) {
logger.info(`Found membership payment document for membershipId: ${membershipId}`);
const paymentsData = doc.data()?.payments || [];
let paymentFound = false;
for (let i = 0; i < paymentsData.length; i++) {
if (paymentsData[i].referenceNumber === payload.merchantOrderId ||
paymentsData[i].transactionId === payload.orderId) {
paymentsData[i].invoicePath = invoicePath;
paymentFound = true;
break;
}
}
logger.info(`Payment record ${paymentFound ? 'found' : 'not found'} in membership payments`, {
membershipId,
merchantOrderId: payload.merchantOrderId,
orderId: payload.orderId
});
await doc.ref.update({
'payments': paymentsData,
'updatedAt': admin.firestore.FieldValue.serverTimestamp(),
});
logger.info(`Successfully updated membership payment with invoice path`, {
membershipId,
invoicePath
});
} else {
logger.warn(`No membership payment document found for membershipId: ${membershipId}`);
}
});
logger.info(`Generated invoice for payment: ${payload.merchantOrderId}, path: ${invoicePath}`);
logger.info(`Getting download URL for invoice: ${invoicePath}`);
const downloadUrl = await invoiceService.getInvoiceDownloadUrl(invoicePath);
logger.info(`Generated download URL for invoice: ${invoicePath}`);
const formattedDate = format(new Date(), 'dd/MM/yyyy');
if (emailCustomer) {
logger.info(`Preparing to send invoice email to customer: ${membershipData?.fields?.['email']}`);
try {
const emailSubject = isFreeplan
? `Free Plan Assigned - ${gymName}`
: `New Membership - ${gymName}`;
const customerEmailHtml = `
<html>
<body>
<h2>${isFreeplan ? 'Free Plan Assigned' : 'Thank you for your payment'}</h2>
<p>Dear ${invoiceData.customerName},</p>
<p>${isFreeplan ? 'Your free membership has been successfully activated.' : 'Thank you for your payment. Your membership has been successfully activated.'}</p>
<p>Please find attached your invoice for the ${isFreeplan ? 'membership' : 'payment'}.</p>
<p>Membership Details:</p>
<ul>
<li>Gym: ${gymName}</li>
${trainerData ? `<li>Trainer: ${trainerData.fullName || 'Your Personal Trainer'}</li>` : ''}
<li>Plan: ${invoiceData.planName}</li>
${hasDiscount ? `<li>Original Price: ₹${originalAmount.toFixed(2)}</li>` : ''}
${hasDiscount ? `<li>Discount: ${discountPercentage.toFixed(1)}%</li>` : ''}
${hasDiscount ? `<li>You Save: ₹${amountSaved.toFixed(2)}</li>` : ''}
<li>Amount: ${orderData.amount.toFixed(2)}${discountText}</li>
<li>Transaction ID: ${payload.merchantOrderId}</li>
<li>Date: ${formattedDate}</li>
${isFreeplan ? '<li>Payment Method: Online}</li>' : ''}
</ul>
<p>If you have any questions, please contact us.</p>
<p>Regards,<br>Fitlien Team</p>
</body>
</html>
`;
await sendEmailWithAttachmentUtil(
emailCustomer,
emailSubject,
customerEmailHtml,
downloadUrl,
`Invoice_${path.basename(invoicePath)}`
);
logger.info(`Invoice email sent to ${membershipData?.fields?.['email']} for payment: ${payload.merchantOrderId}`);
} catch (emailError) {
logger.error('Error sending customer invoice email:', emailError);
}
}
if (gymOwnerEmail) {
logger.info(`Preparing to send invoice email to gym owner: ${gymOwnerEmail}`);
try {
const ownerEmailSubject = isFreeplan
? `Free Plan Assigned${paymentType === 'Gym Membership with Personal Training' ? ' with Personal Training' : ''} - ${gymName}`
: `New Membership${paymentType === 'Gym Membership with Personal Training' ? ' with Personal Training' : ''} - ${gymName}`;
const gymOwnerEmailHtml = `
<html>
<body>
<h2>${isFreeplan ? 'Free Plan Assigned' : `New ${paymentType} Booking Received`}</h2>
<p>Dear Gym Owner,</p>
<p>${isFreeplan ? 'A free membership' : 'A new membership'}${paymentType === 'Gym Membership with Personal Training' ? ' with personal training' : ''} has been ${isFreeplan ? 'assigned' : 'received'} for your gym.</p>
<p>Customer Details:</p>
<ul>
<li>Name: ${invoiceData.customerName}</li>
<li>Email: ${invoiceData.email}</li>
<li>Phone: ${invoiceData.phoneNumber}</li>
</ul>
<p>Booking Details:</p>
<ul>
<li>Type: ${invoiceData.planName}</li>
${trainerData ? `<li>Trainer: ${trainerData.fullName || 'Personal Trainer'}</li>` : ''}
${hasDiscount ? `<li>Original Price: ₹${originalAmount.toFixed(2)}</li>` : ''}
${hasDiscount ? `<li>Discount: ${discountPercentage.toFixed(1)}%</li>` : ''}
${hasDiscount ? `<li>Amount Saved by Customer: ₹${amountSaved.toFixed(2)}</li>` : ''}
<li>Amount: ${orderData.amount.toFixed(2)}${discountText}</li>
<li>Transaction ID: ${payload.merchantOrderId}</li>
<li>Date: ${formattedDate}</li>
</ul>
<p>Please find the invoice attached.</p>
<p>Regards,<br>Fitlien Team</p>
</body>
</html>
`;
await sendEmailWithAttachmentUtil(
gymOwnerEmail,
ownerEmailSubject,
gymOwnerEmailHtml,
downloadUrl,
`Invoice_${path.basename(invoicePath)}`
);
logger.info(`Invoice email sent to gym owner (${gymOwnerEmail}) for payment: ${payload.merchantOrderId}`);
} catch (ownerEmailError) {
logger.error('Error sending gym owner invoice email:', ownerEmailError);
}
}
if (paymentType === 'Gym Membership with Personal Training' && trainerData && trainerData.email) {
try {
const trainerEmailHtml = `
<html>
<body>
<h2>New Personal Training Client</h2>
<p>Dear ${trainerData.fullName || 'Trainer'},</p>
<p>A new client has signed up for personal training with you at ${gymName}.</p>
<p>Client Details:</p>
<ul>
<li>Name: ${invoiceData.customerName}</li>
<li>Email: ${invoiceData.email}</li>
<li>Phone: ${invoiceData.phoneNumber}</li>
</ul>
<p>Booking Details:</p>
<ul>
<li>Type: Personal Training Membership</li>
${hasDiscount ? `<li>Original Price: ₹${originalAmount.toFixed(2)}</li>` : ''}
${hasDiscount ? `<li>Discount: ${discountPercentage.toFixed(1)}%</li>` : ''}
<li>Amount: ${orderData.amount.toFixed(2)}${discountText}</li>
<li>Transaction ID: ${payload.merchantOrderId}</li>
<li>Date: ${formattedDate}</li>
</ul>
<p>Please find the invoice attached.</p>
<p>Regards,<br>Fitlien Team</p>
</body>
</html>
`;
await sendEmailWithAttachmentUtil(
trainerData.email,
`New Personal Training Client - ${gymName}`,
trainerEmailHtml,
downloadUrl,
`Invoice_${path.basename(invoicePath)}`
);
logger.info(`Invoice email sent to trainer (${trainerData.email}) for payment: ${payload.merchantOrderId}`);
} catch (trainerEmailError) {
logger.error('Error sending trainer invoice email:', trainerEmailError);
}
}
}
}
} catch (invoiceError) {
logger.error('Error generating invoice:', invoiceError);
}
}
}
async function processServicePayment(payload: any, orderData: any, paymentId: string) {
try {
logger.info(`Processing service payment for serviceId: ${paymentId}`);
const serviceRef = admin.firestore().collection('service_payments').doc(paymentId);
const serviceDoc = await serviceRef.get();
if (!serviceDoc.exists) {
logger.error(`Service booking not found for serviceId: ${paymentId}`);
return;
}
const serviceData = serviceDoc.data();
if (serviceData?.status === 'ACCEPTED' && serviceData?.paymentDetails?.merchantOrderId) {
logger.warn(`Service payment already processed for serviceId: ${paymentId}, merchantOrderId: ${serviceData.paymentDetails.merchantOrderId}`);
return;
}
if (serviceData?.invoicePath && serviceData?.invoiceNumber) {
logger.warn(`Invoice already exists for serviceId: ${paymentId}, invoicePath: ${serviceData.invoicePath}`);
return;
}
await serviceRef.update({
status: 'ACCEPTED',
paymentDetails: {
transactionId: payload.orderId,
merchantOrderId: payload.merchantOrderId,
amount: orderData.amount,
paymentDate: new Date(),
paymentMethod: 'PhonePe'
},
updatedAt: admin.firestore.FieldValue.serverTimestamp()
});
logger.info(`Updated service booking status to 'CONFIRMED' for paymentId: ${paymentId}`);
const gymId = orderData.metaInfo?.gymId || serviceData?.gymId;
if (gymId) {
try {
const gymDoc = await admin.firestore().collection('gyms').doc(gymId).get();
let gymName = 'Fitlien';
let gymAddress = '';
let gymOwnerEmail = '';
if (gymDoc.exists) {
const gymData = gymDoc.data();
gymName = gymData?.name || 'Fitlien';
gymAddress = gymData?.address || '';
if (gymData?.userId) {
const gymOwnerDoc = await admin.firestore()
.collection('users')
.doc(gymData.userId)
.get();
if (gymOwnerDoc.exists) {
const gymOwnerData = gymOwnerDoc.data();
gymOwnerEmail = gymOwnerData?.email || '';
}
}
}
const invoiceNumber = `INV-${payload.merchantOrderId.substring(0, 8)}`;
logger.info(`Generated invoice number for service: ${invoiceNumber}`);
const discountPercentage = orderData.metaInfo?.discount || 0;
const hasDiscount = discountPercentage > 0;
const isFreeService = discountPercentage === 100;
const originalAmount = hasDiscount ?
orderData.amount / (1 - discountPercentage / 100) :
orderData.amount;
const discountText = isFreeService ?
" (Free Service)" :
hasDiscount ? ` (${discountPercentage.toFixed(0)}% discount applied)` :
'';
const amountSaved = hasDiscount ?
originalAmount - orderData.amount :
0;
const invoiceData = {
invoiceNumber,
businessName: gymName,
address: gymAddress,
gstNumber: orderData.metaInfo?.gstNumber,
customerName: orderData.metaInfo?.customerName || serviceData?.normalizedName || '',
phoneNumber: orderData.metaInfo?.customerPhone || serviceData?.phoneNumber || '',
email: orderData.metaInfo?.customerEmail || serviceData?.email || '',
planName: orderData.metaInfo?.serviceName || serviceData?.serviceName || 'Service',
amount: orderData.amount,
transactionId: payload.orderId,
paymentDate: new Date(),
paymentMethod: 'Online'
};
const invoicePath = await invoiceService.generateInvoice(invoiceData);
logger.info(`Service invoice generated successfully at path: ${invoicePath}`);
await serviceRef.update({
invoicePath: invoicePath,
invoiceNumber: invoiceNumber
});
logger.info(`Updated service booking with invoice path: ${invoicePath}`);
const downloadUrl = await invoiceService.getInvoiceDownloadUrl(invoicePath);
const formattedDate = format(new Date(), 'dd/MM/yyyy');
if (invoiceData.email) {
logger.info(`Preparing to send service invoice email to customer: ${invoiceData.email}`);
try {
const emailSubject = isFreeService
? `Free Service Confirmed - ${gymName}`
: `Service Booking Confirmed - ${gymName}`;
const customerEmailHtml = `
<html>
<body>
<h2>${isFreeService ? 'Free Service Confirmed' : 'Service Booking Confirmed'}</h2>
<p>Dear ${invoiceData.customerName},</p>
<p>${isFreeService ? 'Your free service has been successfully confirmed.' : 'Thank you for your payment. Your service booking has been confirmed.'}</p>
<p>Please find attached your invoice for the service.</p>
<p>Service Details:</p>
<ul>
<li>Gym: ${gymName}</li>
<li>Service: ${invoiceData.planName}</li>
${hasDiscount ? `<li>Original Price: ₹${originalAmount.toFixed(2)}</li>` : ''}
${hasDiscount ? `<li>Discount: ${discountPercentage.toFixed(1)}%</li>` : ''}
${hasDiscount ? `<li>You Save: ₹${amountSaved.toFixed(2)}</li>` : ''}
<li>Amount: ${orderData.amount.toFixed(2)}${discountText}</li>
<li>Transaction ID: ${payload.merchantOrderId}</li>
<li>Date: ${formattedDate}</li>
</ul>
<p>If you have any questions, please contact us.</p>
<p>Regards,<br>Fitlien Team</p>
</body>
</html>
`;
await sendEmailWithAttachmentUtil(
invoiceData.email,
emailSubject,
customerEmailHtml,
downloadUrl,
`Invoice_${path.basename(invoicePath)}`
);
logger.info(`Service invoice email sent to customer (${invoiceData.email}) for payment: ${payload.merchantOrderId}`);
} catch (emailError) {
logger.error('Error sending customer service invoice email:', emailError);
}
}
if (gymOwnerEmail) {
logger.info(`Preparing to send service invoice email to gym owner: ${gymOwnerEmail}`);
try {
const ownerEmailSubject = isFreeService
? `Free Service Assigned - ${gymName}`
: `New Service Booking - ${gymName}`;
const gymOwnerEmailHtml = `
<html>
<body>
<h2>${isFreeService ? 'Free Service Assigned' : 'New Service Booking Received'}</h2>
<p>Dear Gym Owner,</p>
<p>${isFreeService ? 'A free service has been assigned' : 'A new service booking has been received'} for your gym.</p>
<p>Customer Details:</p>
<ul>
<li>Name: ${invoiceData.customerName}</li>
<li>Phone: ${invoiceData.phoneNumber}</li>
<li>Email: ${invoiceData.email}</li>
</ul>
<p>Service Details:</p>
<ul>
<li>Service: ${invoiceData.planName}</li>
${hasDiscount ? `<li>Original Price: ₹${originalAmount.toFixed(2)}</li>` : ''}
${hasDiscount ? `<li>Discount: ${discountPercentage.toFixed(1)}%</li>` : ''}
${hasDiscount ? `<li>Amount Saved by Customer: ₹${amountSaved.toFixed(2)}</li>` : ''}
<li>Amount: ${orderData.amount.toFixed(2)}${discountText}</li>
<li>Transaction ID: ${payload.merchantOrderId}</li>
<li>Date: ${formattedDate}</li>
</ul>
<p>Please find the invoice attached.</p>
<p>Regards,<br>Fitlien Team</p>
</body>
</html>
`;
await sendEmailWithAttachmentUtil(
gymOwnerEmail,
ownerEmailSubject,
gymOwnerEmailHtml,
downloadUrl,
`Invoice_${path.basename(invoicePath)}`
);
logger.info(`Service invoice email sent to gym owner (${gymOwnerEmail}) for payment: ${payload.merchantOrderId}`);
} catch (ownerEmailError) {
logger.error('Error sending gym owner service invoice email:', ownerEmailError);
}
}
} catch (invoiceError) {
logger.error('Error generating service invoice:', invoiceError);
}
}
} catch (error) {
logger.error('Error processing service payment:', error);
}
}

View File

@ -0,0 +1,67 @@
import { onRequest } from "firebase-functions/v2/https";
import { Request } from "firebase-functions/v2/https";
import * as express from "express";
import { getLogger } from "../shared/config";
import { getCorsHandler } from "../shared/middleware";
import axios from "axios";
const logger = getLogger();
const corsHandler = getCorsHandler();
export const getPlacesAutocomplete = onRequest({
region: '#{SERVICES_RGN}#'
}, async (request: Request, response: express.Response) => {
return corsHandler(request, response, async () => {
try {
const { input, location, radius, types, components, sessiontoken } = request.query;
if (!input) {
response.status(400).json({
error: 'Input parameter is required for autocomplete'
});
return;
}
const apiKey = process.env.GOOGLE_MAPS_API_KEY;
if (!apiKey) {
logger.error('Google Places API key is not configured');
response.status(500).json({ error: 'Server configuration error' });
return;
}
const url = 'https://maps.googleapis.com/maps/api/place/autocomplete/json';
const params: any = {
key: apiKey,
input: input
};
if (location && radius) {
params.location = location;
params.radius = radius;
}
if (types) {
params.types = types;
}
if (components) {
params.components = components;
}
if (sessiontoken) {
params.sessiontoken = sessiontoken;
}
const result = await axios.get(url, { params });
logger.info('Google Places Autocomplete API request completed successfully');
response.json(result.data);
} catch (error) {
logger.error('Error fetching place autocomplete suggestions:', error);
response.status(500).json({
success: false,
error: error instanceof Error ? error.message : String(error)
});
}
});
});

View File

@ -0,0 +1,51 @@
import { onRequest } from "firebase-functions/v2/https";
import { Request } from "firebase-functions/v2/https";
import * as express from "express";
import axios from "axios";
const { getCorsHandler } = require('../shared/middleware');
const corsHandler = getCorsHandler();
const { getLogger } = require('../shared/config');
const logger = getLogger();
export const getPlaceDetails = onRequest({
region: '#{SERVICES_RGN}#'
}, async (request: Request, response: express.Response) => {
return corsHandler(request, response, async () => {
try {
const { place_id, fields } = request.query;
if (!place_id) {
response.status(400).json({
error: 'place_id parameter is required'
});
return;
}
const apiKey = process.env.GOOGLE_MAPS_API_KEY;
if (!apiKey) {
logger.error('Google Places API key is not configured');
response.status(500).json({ error: 'Server configuration error' });
return;
}
const url = 'https://maps.googleapis.com/maps/api/place/details/json';
const params: any = {
key: apiKey,
place_id: place_id,
fields: fields || 'geometry'
};
const result = await axios.get(url, { params });
logger.info('Google Places Details API request completed successfully');
response.json(result.data);
} catch (error) {
logger.error('Error fetching place details:', error);
response.status(500).json({
success: false,
error: error instanceof Error ? error.message : String(error)
});
}
});
});

View File

@ -0,0 +1,2 @@
export { getPlaceDetails } from './details';
export { getPlacesAutocomplete } from './autocomplete';

View File

@ -0,0 +1,9 @@
import * as admin from 'firebase-admin';
import * as logger from 'firebase-functions/logger';
if (!admin.apps.length) {
admin.initializeApp();
}
export const getAdmin = () => admin;
export const getLogger = () => logger;

View File

@ -0,0 +1,51 @@
import * as crypto from 'crypto';
import * as fs from 'fs';
import * as path from 'path';
export class RSADecryption {
private static privateKeyObject: crypto.KeyObject | null = null;
private static getPrivateKeyObject(): crypto.KeyObject {
if (!this.privateKeyObject) {
const keyPath = path.join(__dirname, '../../assets/keys/fitLien_private.pem');
const keyContent = fs.readFileSync(keyPath, 'utf8');
this.privateKeyObject = crypto.createPrivateKey({
key: keyContent,
format: 'pem'
});
}
return this.privateKeyObject;
}
public static decryptPassword(encryptedPassword: string): string {
try {
if (!encryptedPassword || encryptedPassword.trim() === '') {
throw new Error('Encrypted password cannot be empty');
}
const privateKeyObject = this.getPrivateKeyObject();
const encryptedBuffer = Buffer.from(encryptedPassword, 'base64');
if (encryptedBuffer.length === 0) {
throw new Error('Encrypted password buffer is empty');
}
const decryptedBuffer = crypto.privateDecrypt(
{
key: privateKeyObject,
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
oaepHash: 'sha1'
},
encryptedBuffer
);
return decryptedBuffer.toString('utf8');
} catch (error) {
console.error('Decryption error details:', {
message: error instanceof Error ? error.message : 'Unknown error',
encryptedPasswordLength: encryptedPassword?.length || 0
});
throw new Error(`Failed to decrypt password: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
}

View File

@ -0,0 +1,3 @@
import cors from 'cors';
export const getCorsHandler = () => cors({ origin: true });

View File

@ -0,0 +1 @@
export { sendSMSMessage } from './sendSMS';

View File

@ -0,0 +1,77 @@
import { onRequest } from "firebase-functions/v2/https";
import { Request } from "firebase-functions/v2/https";
import { getCorsHandler } from "../shared/middleware";
import { getLogger } from "../shared/config";
import twilio from 'twilio';
const corsHandler = getCorsHandler();
const logger = getLogger();
// Initialize Twilio client
const twilioClient = twilio(process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN);
interface SMSRequest {
to: string;
body: string;
}
export const sendSMSMessage = onRequest({
region: '#{SERVICES_RGN}#'
}, async (request: Request, response) => {
return corsHandler(request, response, async () => {
try {
const { to, body } = request.body as SMSRequest;
// Input validation
if (!to || !body) {
logger.error('Missing required SMS parameters');
response.status(400).json({
success: false,
error: 'Both "to" and "body" parameters are required'
});
return;
}
// Validate phone number format (basic check)
if (!/^\+?[1-9]\d{1,14}$/.test(to)) {
logger.error('Invalid phone number format', { to });
response.status(400).json({
success: false,
error: 'Invalid phone number format'
});
return;
}
// Send SMS
const message = await twilioClient.messages.create({
body: body,
from: process.env.TWILIO_PHONE_NUMBER,
to: to
});
logger.info('SMS sent successfully', {
messageId: message.sid,
to: to,
length: body.length
});
response.json({
success: true,
messageId: message.sid,
timestamp: message.dateCreated
});
} catch (error: any) {
logger.error('Error sending SMS:', error);
const statusCode = error.status === 401 ? 401 : 500;
response.status(statusCode).json({
success: false,
error: error.message,
code: error.code,
moreInfo: error.moreInfo
});
}
});
});

View File

@ -0,0 +1,45 @@
import { onRequest } from "firebase-functions/v2/https";
import { Request } from "firebase-functions/v2/https";
import * as path from 'path';
import { getCorsHandler } from "../shared/middleware";
import { getLogger, getAdmin } from "../shared/config";
const corsHandler = getCorsHandler();
const admin = getAdmin();
const logger = getLogger();
export const accessFile = onRequest({
region: '#{SERVICES_RGN}#'
}, async (request: Request, response) => {
return corsHandler(request, response, async () => {
try {
const filePath = request.query.path as string;
if (!filePath) {
response.status(400).send('File path is required');
return;
}
const expirationMs = 60 * 60 * 1000;
const bucket = admin.storage().bucket();
const file = bucket.file(filePath);
const [exists] = await file.exists();
if (!exists) {
response.status(404).send('File not found');
return;
}
const [signedUrl] = await file.getSignedUrl({
action: 'read',
expires: Date.now() + expirationMs,
responseDisposition: `attachment; filename="${path.basename(filePath)}"`,
});
response.redirect(signedUrl);
logger.info(`File access redirect for ${filePath}`);
} catch (error) {
logger.error('Error accessing file:', error);
response.status(500).send('Error accessing file');
}
});
});

View File

@ -0,0 +1 @@
export { accessFile } from './accessFile';

View File

@ -0,0 +1,117 @@
import { onRequest } from "firebase-functions/v2/https";
import { getCorsHandler } from "../shared/middleware";
import { getAdmin, getLogger } from "../shared/config";
import { Request } from "firebase-functions/v2/https";
import { Response } from "express";
const corsHandler = getCorsHandler();
const admin = getAdmin();
const logger = getLogger();
export const registerClient = onRequest({
region: '#{SERVICES_RGN}#'
}, async (req: Request, res: Response) => {
return corsHandler(req, res, async () => {
try {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed. Please use POST.' });
}
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Unauthorized. Missing or invalid authorization header.' });
}
const idToken = authHeader.split('Bearer ')[1];
try {
const decodedToken = await admin.auth().verifyIdToken(idToken);
const uid = decodedToken.uid;
const userDoc = await admin.firestore().collection('users').doc(uid).get();
if (!userDoc.exists) {
return res.status(403).json({ error: 'Forbidden. User not found.' });
}
const userData = userDoc.data();
if (!userData || !userData.roles || !userData.roles.includes('gym_owner')) {
return res.status(403).json({ error: 'Forbidden. Only gym owners can register clients.' });
}
const gymUser = req.body;
if (!gymUser.fields["phone-number"]) {
return res.status(400).json({ error: 'Phone number is required' });
}
let clientUid;
try {
const userRecord = await admin.auth().getUserByPhoneNumber(gymUser.fields["phone-number"])
.catch(() => null);
if (userRecord) {
clientUid = userRecord.uid;
} else {
const newUser = await admin.auth().createUser({
phoneNumber: gymUser.fields["phone-number"],
displayName: gymUser.fields["first-name"] || '',
});
clientUid = newUser.uid;
}
} catch (error) {
logger.error('Error creating authentication user:', error);
return res.status(500).json({
error: 'Failed to create authentication user',
details: error
});
}
try {
gymUser.uid = clientUid;
gymUser.registeredBy = uid;
if (gymUser.name) {
gymUser.normalizedName = gymUser.name.toLowerCase();
}
if (gymUser.dateOfBirth && !(typeof gymUser.dateOfBirth === 'string')) {
gymUser.dateOfBirth = new Date(gymUser.dateOfBirth).toISOString();
}
const clientData = {
...gymUser,
};
await admin.firestore().collection('client_profiles').doc(clientUid).set(clientData);
return res.status(201).json({
success: true,
message: 'Client registered successfully',
clientId: clientUid
});
} catch (error) {
logger.error('Error creating client profile:', error);
try {
if (!gymUser.uid) {
await admin.auth().deleteUser(clientUid);
}
} catch (deleteError) {
logger.error('Error deleting auth user after failed profile creation:', deleteError);
}
return res.status(500).json({
error: 'Failed to create client profile',
details: error
});
}
} catch (authError) {
logger.error('Authentication error:', authError);
return res.status(401).json({ error: 'Unauthorized. Invalid token.' });
}
} catch (error) {
logger.error('Unexpected error in client registration:', error);
return res.status(500).json({
error: 'Internal server error',
details: error
});
}
});
});

View File

@ -0,0 +1 @@
export { registerClient } from './clientRegistration';

View File

@ -0,0 +1,186 @@
import { getLogger } from "../shared/config";
import { SESClient } from "@aws-sdk/client-ses";
import { SendEmailCommand, SendRawEmailCommand } from "@aws-sdk/client-ses";
import * as mime from 'mime-types';
import axios from 'axios';
const logger = getLogger();
interface EmailRequest {
to: string | string[];
subject: string;
html: string;
text?: string;
from: string;
replyTo?: string;
attachments?: Attachment[];
fileUrl?: string;
fileName?: string;
}
interface Attachment {
filename: string;
content: string | Buffer;
contentType?: string;
}
const stripHtml = (html: string): string => {
if (!html) return '';
return html.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim();
}
async function sendSimpleEmail(data: EmailRequest, recipients: string[]) {
const ses = new SESClient({
region: process.env.AWS_REGION,
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID || '',
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY || ''
}
});
const command = new SendEmailCommand({
Source: data.from,
Destination: { ToAddresses: recipients },
Message: {
Subject: { Data: data.subject },
Body: {
Html: { Data: data.html },
Text: { Data: data.text || stripHtml(data.html) }
}
},
ReplyToAddresses: data.replyTo ? [data.replyTo] : undefined,
});
const result = await ses.send(command);
return { messageId: result.MessageId };
}
async function sendEmailWithAttachments(data: EmailRequest, recipients: string[]) {
const ses = new SESClient({
region: process.env.AWS_REGION,
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID || '',
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY || ''
}
});
const boundary = `boundary_${Math.random().toString(16).substr(2)}`;
let rawMessage = `From: ${data.from}\n`;
rawMessage += `To: ${recipients.join(', ')}\n`;
rawMessage += `Subject: ${data.subject}\n`;
rawMessage += `MIME-Version: 1.0\n`;
rawMessage += `Content-Type: multipart/mixed; boundary="${boundary}"\n\n`;
rawMessage += `--${boundary}\n`;
rawMessage += `Content-Type: multipart/alternative; boundary="alt_${boundary}"\n\n`;
if (data.text) {
rawMessage += `--alt_${boundary}\n`;
rawMessage += `Content-Type: text/plain; charset=UTF-8\n\n`;
rawMessage += `${data.text}\n\n`;
}
rawMessage += `--alt_${boundary}\n`;
rawMessage += `Content-Type: text/html; charset=UTF-8\n\n`;
rawMessage += `${data.html}\n\n`;
rawMessage += `--alt_${boundary}--\n\n`;
for (const attachment of data.attachments || []) {
const contentType = attachment.contentType ||
mime.lookup(attachment.filename) ||
'application/octet-stream';
rawMessage += `--${boundary}\n`;
rawMessage += `Content-Type: ${contentType}; name="${attachment.filename}"\n`;
rawMessage += `Content-Disposition: attachment; filename="${attachment.filename}"\n`;
rawMessage += `Content-Transfer-Encoding: base64\n\n`;
const contentBuffer = typeof attachment.content === 'string'
? Buffer.from(attachment.content, 'base64')
: attachment.content;
rawMessage += contentBuffer.toString('base64') + '\n\n';
}
rawMessage += `--${boundary}--`;
const command = new SendRawEmailCommand({
RawMessage: { Data: Buffer.from(rawMessage) }
});
const result = await ses.send(command);
return { messageId: result.MessageId };
}
async function downloadFileFromUrl(url: string): Promise<Buffer> {
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 async function sendEmailWithAttachmentUtil(
toAddress: string,
subject: string,
message: string,
fileUrl: string,
fileName?: string
): Promise<any> {
try {
logger.info(`Sending email with attachment to: ${toAddress}`);
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: []
};
if (fileUrl && fileName) {
logger.info(`Downloading attachment from URL: ${fileUrl}`);
try {
const fileContent = await downloadFileFromUrl(fileUrl);
data.attachments!.push({
filename: fileName,
content: fileContent,
contentType: mime.lookup(fileName) || 'application/octet-stream'
});
logger.info(`Successfully downloaded attachment: ${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 Error('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];
let result;
if (data.attachments && data.attachments.length > 0) {
result = await sendEmailWithAttachments(data, recipients);
} else {
result = await sendSimpleEmail(data, recipients);
}
logger.info('Email sent successfully via SES');
return { success: true, result };
} catch (error) {
logger.error('Error sending email with attachment via SES:', error);
throw error;
}
}

View File

@ -9,7 +9,9 @@
"outDir": "lib",
"sourceMap": true,
"strict": true,
"target": "es2017"
"target": "es2017",
"types": [],
"skipLibCheck": true
},
"compileOnSave": true,
"include": [

72
package-lock.json generated
View File

@ -1,72 +0,0 @@
{
"name": "fitlien-services",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"@types/busboy": "^1.5.4",
"busboy": "^1.6.0"
},
"devDependencies": {
"@types/long": "^5.0.0"
}
},
"node_modules/@types/busboy": {
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/@types/busboy/-/busboy-1.5.4.tgz",
"integrity": "sha512-kG7WrUuAKK0NoyxfQHsVE6j1m01s6kMma64E+OZenQABMQyTJop1DumUWcLwAQ2JzpefU7PDYoRDKl8uZosFjw==",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/long": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@types/long/-/long-5.0.0.tgz",
"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,
"dependencies": {
"long": "*"
}
},
"node_modules/@types/node": {
"version": "22.10.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz",
"integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==",
"dependencies": {
"undici-types": "~6.20.0"
}
},
"node_modules/busboy": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
"integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
"dependencies": {
"streamsearch": "^1.1.0"
},
"engines": {
"node": ">=10.16.0"
}
},
"node_modules/long": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/long/-/long-5.3.1.tgz",
"integrity": "sha512-ka87Jz3gcx/I7Hal94xaN2tZEOPoUOEVftkQqZx2EeQRN7LGdfLlI3FvZ+7WDplm+vK2Urx9ULrvSowtdCieng==",
"dev": true
},
"node_modules/streamsearch": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
"integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/undici-types": {
"version": "6.20.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="
}
}
}

View File

@ -1,9 +0,0 @@
{
"dependencies": {
"@types/busboy": "^1.5.4",
"busboy": "^1.6.0"
},
"devDependencies": {
"@types/long": "^5.0.0"
}
}

View File

@ -1,12 +1,9 @@
rules_version = '2';
// Craft rules based on data in your Firestore database
// allow write: if firestore.get(
// /databases/(default)/documents/users/$(request.auth.uid)).data.isAdmin;
service firebase.storage {
match /b/{bucket}/o {
match /{allPaths=**} {
allow read, write: if false;
allow read, write: if request.auth != null;
}
}
}
}