Auspicious14 commited on
Commit
f78b36a
·
1 Parent(s): 2b70e7f

Initial backend deployment with Docker and Cron jobs

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .dockerignore +11 -0
  2. .env.example +23 -0
  3. .gitignore +46 -0
  4. DEPLOYMENT.md +40 -0
  5. Dockerfile +40 -0
  6. README.md +167 -9
  7. nest-cli.json +8 -0
  8. package-lock.json +0 -0
  9. package.json +75 -0
  10. prisma.config.ts +14 -0
  11. prisma/schema.prisma +282 -0
  12. src/app.controller.ts +30 -0
  13. src/app.module.ts +50 -0
  14. src/auth/auth.controller.ts +129 -0
  15. src/auth/auth.module.ts +46 -0
  16. src/auth/auth.service.ts +343 -0
  17. src/auth/dto/auth-response.dto.ts +10 -0
  18. src/auth/dto/forgot-password.dto.ts +17 -0
  19. src/auth/dto/login.dto.ts +23 -0
  20. src/auth/dto/refresh-token.dto.ts +9 -0
  21. src/auth/dto/register.dto.ts +31 -0
  22. src/auth/dto/reset-password.dto.ts +17 -0
  23. src/auth/guards/jwt-auth.guard.ts +14 -0
  24. src/auth/strategies/jwt.strategy.ts +39 -0
  25. src/blood-pressure/blood-pressure.controller.ts +92 -0
  26. src/blood-pressure/blood-pressure.module.ts +27 -0
  27. src/blood-pressure/blood-pressure.service.ts +151 -0
  28. src/blood-pressure/dto/create-blood-pressure.dto.ts +30 -0
  29. src/care-priority/care-priority.controller.ts +71 -0
  30. src/care-priority/care-priority.module.ts +21 -0
  31. src/care-priority/care-priority.service.ts +298 -0
  32. src/care-priority/health-assessment.service.ts +104 -0
  33. src/care-priority/types/care-priority.types.ts +32 -0
  34. src/clinic-finder/clinic-finder.controller.ts +33 -0
  35. src/clinic-finder/clinic-finder.module.ts +11 -0
  36. src/clinic-finder/clinic-finder.service.ts +73 -0
  37. src/clinic-finder/data/clinics.data.ts +125 -0
  38. src/database/database.module.ts +33 -0
  39. src/database/prisma.service.ts +102 -0
  40. src/education/data/data.txt +123 -0
  41. src/education/data/education.content.ts +177 -0
  42. src/education/education.controller.ts +43 -0
  43. src/education/education.module.ts +22 -0
  44. src/education/education.service.ts +44 -0
  45. src/health.controller.ts +18 -0
  46. src/main.ts +19 -0
  47. src/notifications/dto/register-push-token.dto.ts +7 -0
  48. src/notifications/email.service.ts +41 -0
  49. src/notifications/notifications.controller.ts +25 -0
  50. src/notifications/notifications.module.ts +31 -0
.dockerignore ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ node_modules
2
+ dist
3
+ npm-debug.log
4
+ Dockerfile
5
+ .dockerignore
6
+ .git
7
+ .env
8
+ README.md
9
+ test
10
+ coverage
11
+ prisma/migrations
.env.example ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Application
2
+ NODE_ENV=development
3
+ PORT=3000
4
+
5
+ # Database (PostgreSQL) - Prisma Connection String
6
+ DATABASE_URL="postgresql://postgres:your_password_here@localhost:5432/maternal_health?schema=public"
7
+
8
+ # JWT Authentication
9
+ JWT_SECRET=your_jwt_secret_here_change_in_production
10
+ JWT_EXPIRATION=1h
11
+ JWT_REFRESH_SECRET=your_refresh_secret_here_change_in_production
12
+ JWT_REFRESH_EXPIRATION=7d
13
+
14
+ # Frontend Configuration
15
+ FRONTEND_URL=http://localhost:8081
16
+
17
+ # Email Configuration (SMTP)
18
+ SMTP_HOST=smtp.gmail.com
19
+ SMTP_PORT=587
20
+ SMTP_USER=your-email@gmail.com
21
+ SMTP_PASS=your-app-password
22
+ SMTP_FROM=MaternAlert <no-reply@maternalert.com>
23
+
.gitignore ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # compiled output
2
+ /dist
3
+ /node_modules
4
+
5
+ # Logs
6
+ logs
7
+ *.log
8
+ npm-debug.log*
9
+ pnpm-debug.log*
10
+ yarn-debug.log*
11
+ yarn-error.log*
12
+ lerna-debug.log*
13
+
14
+ # OS
15
+ .DS_Store
16
+
17
+ # Tests
18
+ /coverage
19
+ /.nyc_output
20
+
21
+ # IDEs and editors
22
+ /.idea
23
+ .project
24
+ .classpath
25
+ .c9/
26
+ *.launch
27
+ .settings/
28
+ *.sublime-workspace
29
+
30
+ # IDE - VSCode
31
+ .vscode/*
32
+ !.vscode/settings.json
33
+ !.vscode/tasks.json
34
+ !.vscode/launch.json
35
+ !.vscode/extensions.json
36
+
37
+ # Environment
38
+ # .env
39
+ .env.local
40
+ .env.*.local
41
+
42
+ /generated/prisma
43
+
44
+ # Firebase Service Account
45
+ *-firebase-adminsdk-*.json
46
+ google-services.json
DEPLOYMENT.md ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # MaternAlert Backend Deployment Guide (Hugging Face Spaces)
2
+
3
+ This guide covers the deployment of the MaternAlert backend as a persistent, Docker-based environment on Hugging Face Spaces. This setup ensures 99.9% uptime for background tasks and cron jobs.
4
+
5
+ ## 🚀 Deployment Steps
6
+
7
+ ### 1. Prepare Hugging Face Space
8
+ 1. Create a new Space on [Hugging Face](https://huggingface.co/new-space).
9
+ 2. Choose **Docker** as the SDK.
10
+ 3. Select the **Blank** template or **NestJS** if available.
11
+ 4. Choose your hardware (the free tier is sufficient for this backend).
12
+
13
+ ### 2. Configure Environment Variables
14
+ In your Space settings, add the following secrets:
15
+ - `DATABASE_URL`: Your production PostgreSQL connection string.
16
+ - `JWT_SECRET`: A strong secret for token signing.
17
+ - `EXPO_ACCESS_TOKEN`: Your Expo access token for push notifications.
18
+ - `EMAIL_HOST`, `EMAIL_PORT`, `EMAIL_USER`, `EMAIL_PASS`: SMTP credentials for email alerts.
19
+ - `FRONTEND_URL`: The URL of your deployed frontend.
20
+
21
+ ### 3. Deploy Code
22
+ 1. Clone the Space repository.
23
+ 2. Copy the contents of the `maternalert-be` directory into the Space repository.
24
+ 3. Ensure the `Dockerfile` and `.dockerignore` are in the root of the Space repository.
25
+ 4. Push your changes to the Space.
26
+
27
+ ## 🕒 Scheduled Tasks (Cron Jobs)
28
+ The backend is configured with `@nestjs/schedule` to run the following persistent tasks:
29
+ - **Daily Inactivity Check**: Runs at midnight to remind users who haven't logged BP in 5 days.
30
+ - **Trend Analysis**: Runs daily at 1 AM to detect dangerous BP patterns.
31
+ - **4-Hour Follow-Up**: Runs every 30 minutes to check for missed rechecks after elevated readings.
32
+
33
+ ## 🏥 Health & Monitoring
34
+ - **Health Endpoint**: Access `https://your-space-url/api/v1/health` to verify service status and uptime.
35
+ - **Logs**: Monitor execution logs directly in the Hugging Face Space "Logs" tab.
36
+
37
+ ## 🛠 Troubleshooting
38
+ - **Cold Starts**: Hugging Face Spaces stay active as long as they receive traffic or are configured as "Persistent." Ensure the Space is set to "Public" or "Private" with persistence enabled.
39
+ - **Database Connectivity**: Verify that your database allows connections from the Hugging Face IP range.
40
+ - **Permissions**: The Dockerfile is configured to run as a non-root user (UID 1000) as required by Hugging Face.
Dockerfile ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Build stage
2
+ FROM node:20-alpine AS builder
3
+
4
+ WORKDIR /app
5
+
6
+ # Install dependencies first (better caching)
7
+ COPY package*.json ./
8
+ COPY prisma ./prisma/
9
+
10
+ RUN npm install
11
+
12
+ # Copy source and build
13
+ COPY . .
14
+ RUN npx prisma generate
15
+ RUN npm run build
16
+
17
+ # Production stage
18
+ FROM node:20-alpine
19
+
20
+ WORKDIR /app
21
+
22
+ # Create a non-root user for security (Hugging Face requirement)
23
+ RUN adduser -D -u 1000 appuser
24
+ USER appuser
25
+
26
+ # Copy built assets and production dependencies
27
+ COPY --from=builder /app/package*.json ./
28
+ COPY --from=builder /app/node_modules ./node_modules
29
+ COPY --from=builder /app/dist ./dist
30
+ COPY --from=builder /app/prisma ./prisma
31
+
32
+ # Set production environment
33
+ ENV NODE_ENV=production
34
+ ENV PORT=7860
35
+
36
+ # Expose the port (Hugging Face default is 7860)
37
+ EXPOSE 7860
38
+
39
+ # Run the app
40
+ CMD ["npm", "run", "start:prod"]
README.md CHANGED
@@ -1,11 +1,169 @@
1
- ---
2
- title: MaternAlert
3
- emoji: 👁
4
- colorFrom: gray
5
- colorTo: indigo
6
- sdk: docker
7
- pinned: false
8
- short_description: 'Clinical-safe maternal health support backend '
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  ---
10
 
11
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
+ # Maternal Health Support Backend
2
+
3
+ ⚠️ **CLINICAL SAFETY NOTICE**
4
+
5
+ This application is a **CARE-SUPPORT and ESCALATION TOOL**.
6
+
7
+ - ❌ This is **NOT** a diagnostic system
8
+ - This is **NOT** a medical device
9
+ - ✅ This **IS** a care-support and escalation tool for early warning awareness
10
+
11
+ ## Purpose
12
+
13
+ This backend supports a maternal health application focused on:
14
+ - Early warning awareness for high blood pressure disorders in pregnancy
15
+ - Care escalation based on objective measurements
16
+ - Educational content delivery
17
+ - Connection to healthcare providers
18
+
19
+ ## Non-Diagnostic Design Principles
20
+
21
+ This system is built with strict clinical safety constraints:
22
+
23
+ 1. **No Diagnosis**: The system never diagnoses conditions
24
+ 2. **No Medical Advice**: No treatment recommendations or medical guidance
25
+ 3. **No Risk Scoring**: No probability calculations or risk percentages
26
+ 4. **Data Minimization**: Only essential data is collected
27
+ 5. **Escalation Bias**: When uncertain, always escalate to higher priority
28
+ 6. **Template-Only Messaging**: No dynamic medical text generation
29
+
30
+ ## Architecture
31
+
32
+ ### Modular Domain-Driven Design
33
+
34
+ ```
35
+ src/
36
+ ├── auth/ # Authentication (minimal PII)
37
+ ├── user-profile/ # User profile (data minimization)
38
+ ├── blood-pressure/ # BP readings (neutral storage)
39
+ ├── symptoms/ # Symptom tracking (atomic, non-scored)
40
+ ├── care-priority/ # Care priority engine (rule-based)
41
+ ├── notifications/ # Alerts (template-only)
42
+ └── education/ # Educational content
43
+ ```
44
+
45
+ ### Technology Stack
46
+
47
+ - **Framework**: NestJS with TypeScript (strict mode)
48
+ - **Database**: PostgreSQL (to be configured in STEP 2)
49
+ - **Authentication**: JWT-based
50
+ - **Validation**: class-validator with strict settings
51
+
52
+ ## Getting Started
53
+
54
+ ### Prerequisites
55
+
56
+ - Node.js 18+
57
+ - PostgreSQL 14+
58
+ - npm or yarn
59
+
60
+ ### Installation
61
+
62
+ ```bash
63
+ # Install dependencies
64
+ npm install
65
+
66
+ # Copy environment template
67
+ cp .env.example .env
68
+
69
+ # Edit .env with your configuration
70
+ # (Database credentials, JWT secrets, etc.)
71
+ ```
72
+
73
+ ### Development
74
+
75
+ ```bash
76
+ # Start in development mode with hot reload
77
+ npm run start:dev
78
+
79
+ # Build for production
80
+ npm run build
81
+
82
+ # Start production server
83
+ npm run start:prod
84
+ ```
85
+
86
+ ### Testing
87
+
88
+ ```bash
89
+ # Run unit tests
90
+ npm test
91
+
92
+ # Run tests in watch mode
93
+ npm test:watch
94
+
95
+ # Generate coverage report
96
+ npm test:cov
97
+ ```
98
+
99
+ ## Deployment
100
+
101
+ For persistent background tasks and cron jobs, the backend is optimized for deployment on **Hugging Face Spaces** using Docker.
102
+
103
+ - [Deployment Guide](./DEPLOYMENT.md)
104
+ - [Health Check API](http://localhost:3000/api/v1/health) (when running locally)
105
+
106
+ ## Implementation Status
107
+
108
+ This project follows a 12-step implementation process:
109
+
110
+ ### Phase 1: Foundation
111
+ - [x] **STEP 1**: Project Bootstrap & Architecture ✅
112
+ - [ ] **STEP 2**: Database & ORM Setup
113
+ - [ ] **STEP 3**: Authentication Module
114
+
115
+ ### Phase 2: Core Data Modules
116
+ - [ ] **STEP 4**: User Profile Module (Data Minimization)
117
+ - [ ] **STEP 5**: Blood Pressure Module (Neutral Handling)
118
+ - [ ] **STEP 6**: Symptom Module (Atomic, Non-Scored)
119
+
120
+ ### Phase 3: Care Logic & Safety
121
+ - [ ] **STEP 7**: Care Priority Engine (Most Critical)
122
+ - [ ] **STEP 8**: Care Priority API Endpoint
123
+ - [ ] **STEP 9**: Notifications & Alerts
124
+
125
+ ### Phase 4: Support & Security
126
+ - [ ] **STEP 10**: Education Content Module
127
+ - [ ] **STEP 11**: Security, Logging & Rate Limiting
128
+ - [ ] **STEP 12**: Final Safety Audit
129
+
130
+ ## API Documentation
131
+
132
+ Once running, API documentation will be available at:
133
+ - Development: `http://localhost:3000/api/v1`
134
+ - Swagger docs: (to be added in later steps)
135
+
136
+ ## Clinical Safety Commitments
137
+
138
+ Every module in this system adheres to:
139
+
140
+ 1. **Deterministic Logic**: No AI/ML for medical decisions
141
+ 2. **Transparent Rules**: All escalation rules are documented
142
+ 3. **Conservative Defaults**: Favor care-seeking when uncertain
143
+ 4. **Audit Trail**: All decisions are logged (without PII)
144
+ 5. **No Diagnostic Language**: Careful wording throughout
145
+
146
+ ## Regulatory Considerations
147
+
148
+ This system is designed for:
149
+ - NGO deployments
150
+ - Public health pilots
151
+ - Clinical partnerships
152
+ - Research studies
153
+
154
+ **Not intended for**:
155
+ - Direct-to-consumer medical diagnosis
156
+ - Replacement of clinical judgment
157
+ - Unsupervised medical decision-making
158
+
159
+ ## License
160
+
161
+ ISC
162
+
163
+ ## Support
164
+
165
+ For questions about clinical safety design or implementation, please review the inline documentation in each module.
166
+
167
  ---
168
 
169
+ **Remember**: This is a care-support tool. Always encourage users to seek professional medical advice.
nest-cli.json ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "https://json.schemastore.org/nest-cli",
3
+ "collection": "@nestjs/schematics",
4
+ "sourceRoot": "src",
5
+ "compilerOptions": {
6
+ "deleteOutDir": true
7
+ }
8
+ }
package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
package.json ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "maternal-health-backend",
3
+ "version": "1.0.0",
4
+ "description": "Clinical-safe maternal health support backend - Care escalation tool (NOT a diagnostic system)",
5
+ "main": "dist/main.js",
6
+ "scripts": {
7
+ "build": "prisma generate && prisma migrate deploy && nest build",
8
+ "postinstall": "prisma generate",
9
+ "format": "prettier --write \"src/**/*.ts\"",
10
+ "start": "nest start",
11
+ "start:dev": "nest start --watch",
12
+ "start:debug": "nest start --debug --watch",
13
+ "start:prod": "node dist/main",
14
+ "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
15
+ "test": "jest",
16
+ "test:watch": "jest --watch",
17
+ "test:cov": "jest --coverage",
18
+ "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
19
+ "prisma:generate": "prisma generate",
20
+ "prisma:migrate": "prisma migrate dev",
21
+ "prisma:studio": "prisma studio",
22
+ "prisma:push": "prisma db push"
23
+ },
24
+ "keywords": [
25
+ "maternal-health",
26
+ "pregnancy",
27
+ "hypertension",
28
+ "care-escalation",
29
+ "clinical-safety"
30
+ ],
31
+ "author": "",
32
+ "license": "ISC",
33
+ "type": "commonjs",
34
+ "devDependencies": {
35
+ "@nestjs/cli": "^11.0.14",
36
+ "@nestjs/schematics": "^11.0.9",
37
+ "@nestjs/testing": "^11.1.11",
38
+ "@types/bcrypt": "^6.0.0",
39
+ "@types/compression": "^1.8.1",
40
+ "@types/express": "^5.0.6",
41
+ "@types/node": "^25.0.6",
42
+ "@types/nodemailer": "^7.0.11",
43
+ "@types/passport-jwt": "^4.0.1",
44
+ "@types/pg": "^8.16.0",
45
+ "dotenv": "^17.2.3",
46
+ "prisma": "^7.2.0",
47
+ "ts-loader": "^9.5.4",
48
+ "ts-node": "^10.9.2",
49
+ "typescript": "^5.9.3"
50
+ },
51
+ "dependencies": {
52
+ "@googlemaps/google-maps-services-js": "^3.4.2",
53
+ "@nestjs/config": "^4.0.2",
54
+ "@nestjs/jwt": "^11.0.2",
55
+ "@nestjs/passport": "^11.0.5",
56
+ "@nestjs/platform-express": "^11.1.11",
57
+ "@nestjs/schedule": "^6.1.1",
58
+ "@nestjs/swagger": "^11.2.4",
59
+ "@nestjs/throttler": "^6.5.0",
60
+ "@prisma/adapter-pg": "^7.2.0",
61
+ "@prisma/client": "^7.2.0",
62
+ "axios": "^1.13.5",
63
+ "bcrypt": "^6.0.0",
64
+ "class-transformer": "^0.5.1",
65
+ "class-validator": "^0.14.3",
66
+ "compression": "^1.8.1",
67
+ "expo-server-sdk": "^6.1.0",
68
+ "helmet": "^8.1.0",
69
+ "nodemailer": "^8.0.1",
70
+ "passport": "^0.7.0",
71
+ "passport-jwt": "^4.0.1",
72
+ "pg": "^8.16.3",
73
+ "swagger-ui-express": "^5.0.1"
74
+ }
75
+ }
prisma.config.ts ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // This file was generated by Prisma, and assumes you have installed the following:
2
+ // npm install --save-dev prisma dotenv
3
+ import "dotenv/config";
4
+ import { defineConfig } from "prisma/config";
5
+
6
+ export default defineConfig({
7
+ schema: "prisma/schema.prisma",
8
+ migrations: {
9
+ path: "prisma/migrations",
10
+ },
11
+ datasource: {
12
+ url: process.env["DATABASE_URL"],
13
+ },
14
+ });
prisma/schema.prisma ADDED
@@ -0,0 +1,282 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Prisma Schema for Maternal Health Support Backend
3
+ *
4
+ * ORM CHOICE: Prisma
5
+ *
6
+ * JUSTIFICATION:
7
+ * - Type-safe database client with excellent TypeScript support
8
+ * - Intuitive schema definition language
9
+ * - Automatic migration generation
10
+ * - Built-in connection pooling
11
+ * - Great developer experience with Prisma Studio
12
+ * - Strong PostgreSQL support
13
+ *
14
+ * CLINICAL SAFETY PRINCIPLES:
15
+ * - Data minimization enforced at schema level
16
+ * - No free-text medical fields
17
+ * - Enums for controlled vocabularies
18
+ * - Timestamps for audit trails
19
+ * - Strict validation rules
20
+ */
21
+
22
+ generator client {
23
+ provider = "prisma-client-js"
24
+ engineType = "library"
25
+ }
26
+
27
+ datasource db {
28
+ provider = "postgresql"
29
+ }
30
+
31
+ /**
32
+ * UserAuth Entity
33
+ *
34
+ * CLINICAL SAFETY CONSTRAINTS:
35
+ * - Minimal PII only (email OR phone, not both required)
36
+ * - No health data in this table
37
+ * - No logging of credentials
38
+ * - Passwords are hashed (never stored in plain text)
39
+ *
40
+ * DATA MINIMIZATION:
41
+ * - Only authentication-related fields
42
+ * - No names, addresses, or demographic data
43
+ * - Health data stored in separate UserProfile table
44
+ */
45
+ model UserAuth {
46
+ id String @id @default(uuid())
47
+ email String? @unique
48
+ phone String? @unique
49
+ passwordHash String // bcrypt hashed password
50
+ refreshToken String? // For JWT refresh token rotation
51
+ pushToken String? // For Expo push notifications
52
+ resetPasswordToken String? // For forgot password
53
+ resetPasswordExpires DateTime? // For forgot password
54
+ isActive Boolean @default(true)
55
+ createdAt DateTime @default(now())
56
+ updatedAt DateTime @updatedAt
57
+
58
+ // Relation to UserProfile (optional - created after registration)
59
+ profile UserProfile?
60
+
61
+ // Relation to BloodPressureReadings
62
+ bloodPressureReadings BloodPressureReading[]
63
+
64
+ // Relation to SymptomRecords
65
+ symptomRecords SymptomRecord[]
66
+
67
+ // Relation to Notifications
68
+ notifications Notification[]
69
+
70
+ @@index([email])
71
+ @@index([phone])
72
+ @@map("user_auth")
73
+ }
74
+
75
+ /**
76
+ * Age Range Enum
77
+ *
78
+ * DATA MINIMIZATION:
79
+ * - Use age ranges instead of exact DOB
80
+ * - Prevents unnecessary PII collection
81
+ */
82
+ enum AgeRange {
83
+ UNDER_18
84
+ AGE_18_34
85
+ AGE_35_PLUS
86
+ }
87
+
88
+ /**
89
+ * Emergency Contact Relationship Enum
90
+ *
91
+ * DATA MINIMIZATION:
92
+ * - Relationship type only (no names stored)
93
+ * - Supports midwife or closest relative
94
+ */
95
+ enum EmergencyContactRelationship {
96
+ MIDWIFE
97
+ PARTNER
98
+ FAMILY_MEMBER
99
+ OTHER
100
+ }
101
+
102
+ /**
103
+ * Known Conditions Enum
104
+ *
105
+ * CLINICAL SAFETY:
106
+ * - Enumerated list prevents free-text medical history
107
+ * - Only high-risk pregnancy conditions relevant to hypertension
108
+ * - No diagnostic information stored
109
+ */
110
+ enum KnownCondition {
111
+ CHRONIC_HYPERTENSION
112
+ GESTATIONAL_DIABETES
113
+ PREECLAMPSIA_HISTORY
114
+ KIDNEY_DISEASE
115
+ AUTOIMMUNE_DISORDER
116
+ MULTIPLE_PREGNANCY
117
+ NONE
118
+ }
119
+
120
+ /**
121
+ * UserProfile Entity
122
+ *
123
+ * DATA MINIMIZATION PRINCIPLES:
124
+ * - No DOB (age range only)
125
+ * - No address
126
+ * - No free-text medical history
127
+ * - No name or demographic data
128
+ * - Only pregnancy-relevant information
129
+ *
130
+ * CLINICAL SAFETY:
131
+ * - All fields are enumerated or structured
132
+ * - No diagnostic interpretations stored
133
+ * - Links to UserAuth via userId
134
+ */
135
+ model UserProfile {
136
+ id String @id @default(uuid())
137
+ userId String @unique
138
+ ageRange AgeRange
139
+ pregnancyWeeks Int // Current week of pregnancy (0-42)
140
+ firstPregnancy Boolean
141
+ knownConditions KnownCondition[] // Array of enumerated conditions
142
+ emergencyContactPhone String? // Phone number only (no name)
143
+ emergencyContactRelationship EmergencyContactRelationship? // Relationship enum (e.g. MIDWIFE, PARTNER)
144
+
145
+ // Clinic Information
146
+ clinicName String?
147
+ clinicAddress String?
148
+ clinicPhone String?
149
+
150
+ // Notification Preferences
151
+ notifyCarePriority Boolean @default(true)
152
+ notifyBpAlert Boolean @default(true)
153
+ notifySymptomAlert Boolean @default(true)
154
+ notifyReminders Boolean @default(true)
155
+ reminderTime String? @default("09:00") // 24h format (HH:mm)
156
+
157
+ createdAt DateTime @default(now())
158
+ updatedAt DateTime @updatedAt
159
+
160
+ // Relation to UserAuth
161
+ userAuth UserAuth @relation(fields: [userId], references: [id], onDelete: Cascade)
162
+
163
+ @@index([userId])
164
+ @@map("user_profile")
165
+ }
166
+
167
+ /**
168
+ * Blood Pressure Reading Entity
169
+ *
170
+ * CLINICAL SAFETY CONSTRAINTS:
171
+ * - Manual entry only (no device integration)
172
+ * - Do NOT label readings as normal/abnormal
173
+ * - Do NOT store interpretations
174
+ * - Neutral data handling only
175
+ *
176
+ * VALIDATION:
177
+ * - Systolic: 60-260 mmHg (physiologically possible range)
178
+ * - Diastolic: 40-160 mmHg (physiologically possible range)
179
+ * - Timestamp for temporal tracking
180
+ *
181
+ * PURPOSE:
182
+ * - Store raw BP measurements
183
+ * - Used by care priority engine (Phase 3)
184
+ * - No diagnostic labels attached to data
185
+ */
186
+ model BloodPressureReading {
187
+ id String @id @default(uuid())
188
+ userId String
189
+ systolic Int // mmHg (60-260)
190
+ diastolic Int // mmHg (40-160)
191
+ recordedAt DateTime @default(now())
192
+ createdAt DateTime @default(now())
193
+
194
+ // Relation to UserAuth
195
+ userAuth UserAuth @relation(fields: [userId], references: [id], onDelete: Cascade)
196
+
197
+ @@index([userId])
198
+ @@index([recordedAt])
199
+ @@map("blood_pressure_reading")
200
+ }
201
+
202
+ /**
203
+ * Symptom Type Enum
204
+ *
205
+ * CLINICAL SAFETY:
206
+ * - Enumerated list of pregnancy hypertension warning signs
207
+ * - No free-text symptoms
208
+ * - No severity levels
209
+ * - Based on clinical guidelines for preeclampsia/eclampsia
210
+ */
211
+ enum SymptomType {
212
+ HEADACHE // Severe or persistent headache
213
+ BLURRED_VISION // Visual disturbances
214
+ SWELLING // Edema (face, hands, feet)
215
+ UPPER_ABDOMINAL_PAIN // Right upper quadrant pain
216
+ REDUCED_URINE // Decreased urine output
217
+ NAUSEA_VOMITING // Persistent nausea/vomiting
218
+ SHORTNESS_OF_BREATH // Difficulty breathing
219
+ }
220
+
221
+ /**
222
+ * Symptom Record Entity
223
+ *
224
+ * CLINICAL SAFETY CONSTRAINTS:
225
+ * - Atomic recording (one symptom per record)
226
+ * - No severity levels
227
+ * - No numeric scoring
228
+ * - No interpretation or diagnosis
229
+ *
230
+ * PURPOSE:
231
+ * - Track presence of warning symptoms
232
+ * - Used by care priority engine for escalation
233
+ * - Each symptom is a separate, timestamped record
234
+ */
235
+ model SymptomRecord {
236
+ id String @id @default(uuid())
237
+ userId String
238
+ symptomType SymptomType
239
+ recordedAt DateTime @default(now())
240
+ createdAt DateTime @default(now())
241
+
242
+ // Relation to UserAuth
243
+ userAuth UserAuth @relation(fields: [userId], references: [id], onDelete: Cascade)
244
+
245
+ @@index([userId])
246
+ @@index([recordedAt])
247
+ @@index([symptomType])
248
+ @@map("symptom_record")
249
+ }
250
+
251
+ /**
252
+ * Notification Type Enum
253
+ */
254
+ enum NotificationType {
255
+ CARE_PRIORITY
256
+ BP_ALERT
257
+ SYMPTOM_ALERT
258
+ REMINDER
259
+ }
260
+
261
+ /**
262
+ * Notification Entity
263
+ *
264
+ * PURPOSE:
265
+ * - Store history of alerts and notifications
266
+ * - Track read status
267
+ */
268
+ model Notification {
269
+ id String @id @default(uuid())
270
+ userId String
271
+ type NotificationType
272
+ title String
273
+ message String
274
+ read Boolean @default(false)
275
+ createdAt DateTime @default(now())
276
+
277
+ userAuth UserAuth @relation(fields: [userId], references: [id], onDelete: Cascade)
278
+
279
+ @@index([userId])
280
+ @@index([createdAt])
281
+ @@map("notification")
282
+ }
src/app.controller.ts ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Controller, Get } from '@nestjs/common';
2
+ import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
3
+
4
+ @ApiTags('Health')
5
+ @Controller()
6
+ export class AppController {
7
+ @Get('health')
8
+ @ApiOperation({ summary: 'Check API health status' })
9
+ @ApiResponse({ status: 200, description: 'API is healthy' })
10
+ getHealth() {
11
+ return {
12
+ status: 'ok',
13
+ timestamp: new Date().toISOString(),
14
+ service: 'Maternal Health Support API',
15
+ clinical_safety_notice: 'CARE-SUPPORT TOOL - NOT A DIAGNOSTIC SYSTEM',
16
+ };
17
+ }
18
+
19
+ @Get()
20
+ @ApiOperation({ summary: 'API Root' })
21
+ @ApiResponse({ status: 200, description: 'Welcome message' })
22
+ getHello() {
23
+ return {
24
+ message: 'Welcome to Maternal Health Support API',
25
+ version: '1.0.0',
26
+ docs: '/api/v1/docs',
27
+ health: '/api/v1/health',
28
+ };
29
+ }
30
+ }
src/app.module.ts ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Module } from "@nestjs/common";
2
+ import { ConfigModule, ConfigService } from "@nestjs/config";
3
+ import { ThrottlerModule, ThrottlerGuard } from "@nestjs/throttler";
4
+ import { APP_GUARD } from "@nestjs/core";
5
+ import { ScheduleModule } from "@nestjs/schedule";
6
+ import { DatabaseModule } from "./database/database.module";
7
+ import { AuthModule } from "./auth/auth.module";
8
+ import { UserProfileModule } from "./user-profile/user-profile.module";
9
+ import { BloodPressureModule } from "./blood-pressure/blood-pressure.module";
10
+ import { SymptomsModule } from "./symptoms/symptoms.module";
11
+ import { CarePriorityModule } from "./care-priority/care-priority.module";
12
+ import { NotificationsModule } from "./notifications/notifications.module";
13
+ import { EducationModule } from "./education/education.module";
14
+ import { ClinicFinderModule } from "./clinic-finder/clinic-finder.module";
15
+ import { AppController } from "./app.controller";
16
+ import { HealthController } from "./health.controller";
17
+
18
+ @Module({
19
+ imports: [
20
+ // Configuration
21
+ ConfigModule.forRoot({
22
+ isGlobal: true,
23
+ envFilePath: ".env",
24
+ }),
25
+
26
+ // Scheduling
27
+ ScheduleModule.forRoot(),
28
+
29
+ // Database (Prisma) - Global module
30
+ DatabaseModule,
31
+
32
+ // Domain modules
33
+ AuthModule,
34
+ UserProfileModule,
35
+ BloodPressureModule,
36
+ SymptomsModule,
37
+ CarePriorityModule,
38
+ NotificationsModule,
39
+ EducationModule,
40
+ ClinicFinderModule,
41
+ ],
42
+ controllers: [AppController, HealthController],
43
+ providers: [
44
+ {
45
+ provide: APP_GUARD,
46
+ useClass: ThrottlerGuard,
47
+ },
48
+ ],
49
+ })
50
+ export class AppModule {}
src/auth/auth.controller.ts ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {
2
+ Body,
3
+ Controller,
4
+ HttpCode,
5
+ HttpStatus,
6
+ Post,
7
+ Req,
8
+ UseGuards,
9
+ } from "@nestjs/common";
10
+ import { AuthService } from "./auth.service";
11
+ import { RegisterDto } from "./dto/register.dto";
12
+ import { LoginDto } from "./dto/login.dto";
13
+ import { RefreshTokenDto } from "./dto/refresh-token.dto";
14
+ import { AuthResponseDto } from "./dto/auth-response.dto";
15
+ import { ForgotPasswordDto } from "./dto/forgot-password.dto";
16
+ import { ResetPasswordDto } from "./dto/reset-password.dto";
17
+ import { JwtAuthGuard } from "./guards/jwt-auth.guard";
18
+
19
+ /**
20
+ * Authentication Controller
21
+ *
22
+ * ENDPOINTS:
23
+ * - POST /auth/register - Register new user
24
+ * - POST /auth/login - Login user
25
+ * - POST /auth/refresh - Refresh access token
26
+ * - POST /auth/forgot-password - Forgot password request
27
+ * - POST /auth/reset-password - Reset password with token
28
+ *
29
+ * CLINICAL SAFETY:
30
+ * - No health data collected
31
+ * - Minimal PII only
32
+ * - No credential logging
33
+ */
34
+
35
+ @Controller("auth")
36
+ export class AuthController {
37
+ constructor(private readonly authService: AuthService) {}
38
+
39
+ /**
40
+ * Register a new user
41
+ *
42
+ * @param registerDto - Email/phone and password
43
+ * @returns Access token, refresh token, and user ID
44
+ */
45
+ @Post("register")
46
+ @HttpCode(HttpStatus.CREATED)
47
+ async register(@Body() registerDto: RegisterDto): Promise<AuthResponseDto> {
48
+ return this.authService.register(registerDto);
49
+ }
50
+
51
+ /**
52
+ * Login user
53
+ *
54
+ * @param loginDto - Email/phone and password
55
+ * @returns Access token, refresh token, and user ID
56
+ */
57
+ @Post("login")
58
+ @HttpCode(HttpStatus.OK)
59
+ async login(@Body() loginDto: LoginDto): Promise<AuthResponseDto> {
60
+ return this.authService.login(loginDto);
61
+ }
62
+
63
+ /**
64
+ * Forgot password request
65
+ *
66
+ * @param forgotPasswordDto - Email or phone
67
+ */
68
+ @Post("forgot-password")
69
+ @HttpCode(HttpStatus.OK)
70
+ async forgotPassword(
71
+ @Body() forgotPasswordDto: ForgotPasswordDto
72
+ ): Promise<void> {
73
+ return this.authService.forgotPassword(forgotPasswordDto);
74
+ }
75
+
76
+ /**
77
+ * Reset password with token
78
+ *
79
+ * @param resetPasswordDto - Token and new password
80
+ */
81
+ @Post("reset-password")
82
+ @HttpCode(HttpStatus.OK)
83
+ async resetPassword(
84
+ @Body() resetPasswordDto: ResetPasswordDto
85
+ ): Promise<void> {
86
+ return this.authService.resetPassword(resetPasswordDto);
87
+ }
88
+
89
+ /**
90
+ * Refresh access token
91
+ *
92
+ * @param refreshTokenDto - Refresh token
93
+ * @returns New access token and refresh token
94
+ */
95
+ @Post("refresh")
96
+ @HttpCode(HttpStatus.OK)
97
+ async refresh(
98
+ @Body() refreshTokenDto: RefreshTokenDto
99
+ ): Promise<AuthResponseDto> {
100
+ return this.authService.refreshToken(refreshTokenDto.refreshToken);
101
+ }
102
+
103
+ /**
104
+ * Logout user
105
+ *
106
+ * SECURITY:
107
+ * - Requires valid access token
108
+ * - Clears stored refresh token so it cannot be reused
109
+ */
110
+ @Post("logout")
111
+ @UseGuards(JwtAuthGuard)
112
+ @HttpCode(HttpStatus.OK)
113
+ async logout(@Req() req: any): Promise<void> {
114
+ await this.authService.logout(req.user.id);
115
+ }
116
+
117
+ /**
118
+ * Get current authenticated user
119
+ *
120
+ * @param req - Request with authenticated user
121
+ * @returns Current user details
122
+ */
123
+ @Post("me")
124
+ @UseGuards(JwtAuthGuard)
125
+ @HttpCode(HttpStatus.OK)
126
+ async getMe(@Req() req: any) {
127
+ return req.user;
128
+ }
129
+ }
src/auth/auth.module.ts ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Module } from "@nestjs/common";
2
+ import { JwtModule } from "@nestjs/jwt";
3
+ import { PassportModule } from "@nestjs/passport";
4
+ import { ConfigModule, ConfigService } from "@nestjs/config";
5
+ import { AuthController } from "./auth.controller";
6
+ import { AuthService } from "./auth.service";
7
+ import { JwtStrategy } from "./strategies/jwt.strategy";
8
+ import { NotificationsModule } from "../notifications/notifications.module";
9
+
10
+ /**
11
+ * Authentication Module
12
+ *
13
+ * RESPONSIBILITIES:
14
+ * - User authentication (JWT-based)
15
+ * - Minimal PII storage
16
+ * - No health data in auth tables
17
+ * - No credential logging
18
+ *
19
+ * FEATURES:
20
+ * - Email OR phone login
21
+ * - Password hashing with bcrypt
22
+ * - JWT access tokens (short-lived)
23
+ * - Refresh token rotation
24
+ * - Secure authentication flow
25
+ */
26
+
27
+ @Module({
28
+ imports: [
29
+ NotificationsModule,
30
+ PassportModule.register({ defaultStrategy: "jwt" }),
31
+ JwtModule.registerAsync({
32
+ imports: [ConfigModule],
33
+ useFactory: (configService: ConfigService) => ({
34
+ secret: configService.get("JWT_SECRET"),
35
+ signOptions: {
36
+ expiresIn: configService.get("JWT_EXPIRATION"),
37
+ },
38
+ }),
39
+ inject: [ConfigService],
40
+ }),
41
+ ],
42
+ controllers: [AuthController],
43
+ providers: [AuthService, JwtStrategy],
44
+ exports: [AuthService, JwtStrategy, PassportModule],
45
+ })
46
+ export class AuthModule {}
src/auth/auth.service.ts ADDED
@@ -0,0 +1,343 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {
2
+ Injectable,
3
+ UnauthorizedException,
4
+ ConflictException,
5
+ Logger,
6
+ NotFoundException,
7
+ } from "@nestjs/common";
8
+ import { JwtService } from "@nestjs/jwt";
9
+ import { ConfigService } from "@nestjs/config";
10
+ import * as bcrypt from "bcrypt";
11
+ import * as crypto from "crypto";
12
+ import { PrismaService } from "../database/prisma.service";
13
+ import { RegisterDto } from "./dto/register.dto";
14
+ import { LoginDto } from "./dto/login.dto";
15
+ import { AuthResponseDto } from "./dto/auth-response.dto";
16
+ import { ForgotPasswordDto } from "./dto/forgot-password.dto";
17
+ import { ResetPasswordDto } from "./dto/reset-password.dto";
18
+ import { NotificationsService } from "../notifications/notifications.service";
19
+
20
+ /**
21
+ * Authentication Service
22
+ *
23
+ * CLINICAL SAFETY PRINCIPLES:
24
+ * - Minimal PII storage
25
+ * - No health data in auth tables
26
+ * - No credential logging
27
+ * - Secure password hashing (bcrypt)
28
+ * - JWT-based stateless authentication
29
+ * - Refresh token rotation for security
30
+ */
31
+
32
+ @Injectable()
33
+ export class AuthService {
34
+ private readonly logger = new Logger(AuthService.name);
35
+ private readonly SALT_ROUNDS = 10;
36
+
37
+ constructor(
38
+ private readonly prisma: PrismaService,
39
+ private readonly jwtService: JwtService,
40
+ private readonly configService: ConfigService,
41
+ private readonly notificationsService: NotificationsService,
42
+ ) {}
43
+
44
+ /**
45
+ * Forgot password request
46
+ */
47
+ async forgotPassword(forgotPasswordDto: ForgotPasswordDto): Promise<void> {
48
+ const { email, phone } = forgotPasswordDto;
49
+
50
+ if (!email && !phone) {
51
+ this.logger.warn("Forgot password attempt with no email or phone");
52
+ return;
53
+ }
54
+
55
+ // Find user — build conditions dynamically to avoid empty {} matching all rows
56
+ const conditions: any[] = [];
57
+ if (email) conditions.push({ email });
58
+ if (phone) conditions.push({ phone });
59
+
60
+ const user = await this.prisma.userAuth.findFirst({
61
+ where: conditions.length === 1 ? conditions[0] : { OR: conditions },
62
+ });
63
+
64
+ if (!user) {
65
+ // Don't reveal if user exists for security
66
+ this.logger.warn(
67
+ `Forgot password attempt for non-existent user: ${email || phone}`,
68
+ );
69
+ return;
70
+ }
71
+
72
+ // Generate token (valid for 1 hour)
73
+ const token = crypto.randomBytes(32).toString("hex");
74
+ const expires = new Date();
75
+ expires.setHours(expires.getHours() + 1);
76
+
77
+ // Save token
78
+ await this.prisma.userAuth.update({
79
+ where: { id: user.id },
80
+ data: {
81
+ resetPasswordToken: token,
82
+ resetPasswordExpires: expires,
83
+ },
84
+ });
85
+
86
+ // Send reset message (via notification service)
87
+ await this.notificationsService.sendResetPasswordNotification(
88
+ user.id,
89
+ token,
90
+ );
91
+
92
+ this.logger.log(`Password reset token generated for user: ${user.id}`);
93
+ }
94
+
95
+ /**
96
+ * Reset password with token
97
+ */
98
+ async resetPassword(resetPasswordDto: ResetPasswordDto): Promise<void> {
99
+ const { token, newPassword } = resetPasswordDto;
100
+
101
+ // Find user with valid token
102
+ const user = await this.prisma.userAuth.findFirst({
103
+ where: {
104
+ resetPasswordToken: token,
105
+ resetPasswordExpires: { gt: new Date() },
106
+ },
107
+ });
108
+
109
+ if (!user) {
110
+ throw new UnauthorizedException("Invalid or expired reset token");
111
+ }
112
+
113
+ // Hash new password
114
+ const passwordHash = await bcrypt.hash(newPassword, this.SALT_ROUNDS);
115
+
116
+ // Update user
117
+ await this.prisma.userAuth.update({
118
+ where: { id: user.id },
119
+ data: {
120
+ passwordHash,
121
+ resetPasswordToken: null,
122
+ resetPasswordExpires: null,
123
+ refreshToken: null, // Force logout from all devices
124
+ },
125
+ });
126
+
127
+ this.logger.log(`Password reset successful for user: ${user.id}`);
128
+ }
129
+
130
+ /**
131
+ * Register a new user
132
+ *
133
+ * CONSTRAINTS:
134
+ * - Email OR phone required (not both)
135
+ * - Password must be at least 8 characters
136
+ * - No duplicate email/phone
137
+ */
138
+ async register(registerDto: RegisterDto): Promise<AuthResponseDto> {
139
+ this.logger.log(
140
+ `Registration attempt for: ${registerDto.email || registerDto.phone}`,
141
+ );
142
+ const { email, phone, password } = registerDto;
143
+
144
+ // Validate that at least one identifier is provided
145
+ if (!email && !phone) {
146
+ throw new ConflictException("Either email or phone must be provided");
147
+ }
148
+
149
+ // Check for existing user — build conditions dynamically
150
+ const conditions: any[] = [];
151
+ if (email) conditions.push({ email });
152
+ if (phone) conditions.push({ phone });
153
+
154
+ const existingUser = await this.prisma.userAuth.findFirst({
155
+ where: conditions.length === 1 ? conditions[0] : { OR: conditions },
156
+ });
157
+
158
+ if (existingUser) {
159
+ throw new ConflictException(
160
+ "User with this email or phone already exists",
161
+ );
162
+ }
163
+
164
+ // Hash password (NEVER log the password)
165
+ const passwordHash = await bcrypt.hash(password, this.SALT_ROUNDS);
166
+
167
+ // Create user
168
+ try {
169
+ const user = await this.prisma.userAuth.create({
170
+ data: {
171
+ email,
172
+ phone,
173
+ passwordHash,
174
+ },
175
+ });
176
+ this.logger.log(`New user registered: ${user.id}`);
177
+ return this.generateTokens(user.id);
178
+ } catch (error: any) {
179
+ this.logger.error(
180
+ `Database error during registration: ${error.message}`,
181
+ error.stack,
182
+ );
183
+ throw error;
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Login user
189
+ *
190
+ * CONSTRAINTS:
191
+ * - Email OR phone required
192
+ * - Password verification
193
+ * - No credential logging
194
+ */
195
+ async login(loginDto: LoginDto): Promise<AuthResponseDto> {
196
+ const { email, phone, password } = loginDto;
197
+
198
+ // Validate that at least one identifier is provided
199
+ if (!email && !phone) {
200
+ throw new UnauthorizedException("Either email or phone must be provided");
201
+ }
202
+
203
+ // Find user — build conditions dynamically
204
+ const loginConditions: any[] = [];
205
+ if (email) loginConditions.push({ email });
206
+ if (phone) loginConditions.push({ phone });
207
+
208
+ const user = await this.prisma.userAuth.findFirst({
209
+ where:
210
+ loginConditions.length === 1
211
+ ? loginConditions[0]
212
+ : { OR: loginConditions },
213
+ });
214
+
215
+ if (!user) {
216
+ // Generic error to prevent user enumeration
217
+ throw new UnauthorizedException("Invalid credentials");
218
+ }
219
+
220
+ // Verify password (NEVER log the password)
221
+ const isPasswordValid = await bcrypt.compare(password, user.passwordHash);
222
+
223
+ if (!isPasswordValid) {
224
+ throw new UnauthorizedException("Invalid credentials");
225
+ }
226
+
227
+ // Check if user is active
228
+ if (!user.isActive) {
229
+ throw new UnauthorizedException("Account is inactive");
230
+ }
231
+
232
+ this.logger.log(`User logged in: ${user.id}`);
233
+
234
+ // Generate tokens
235
+ return this.generateTokens(user.id);
236
+ }
237
+
238
+ /**
239
+ * Refresh access token
240
+ */
241
+ async refreshToken(refreshToken: string): Promise<AuthResponseDto> {
242
+ try {
243
+ // Verify refresh token
244
+ const payload = this.jwtService.verify(refreshToken, {
245
+ secret: this.configService.get("JWT_REFRESH_SECRET"),
246
+ });
247
+
248
+ // Find user
249
+ const user = await this.prisma.userAuth.findUnique({
250
+ where: { id: payload.sub },
251
+ });
252
+
253
+ if (!user || !user.isActive) {
254
+ throw new UnauthorizedException("Invalid refresh token");
255
+ }
256
+
257
+ // Verify stored refresh token matches
258
+ if (user.refreshToken !== refreshToken) {
259
+ throw new UnauthorizedException("Invalid refresh token");
260
+ }
261
+
262
+ // Generate new tokens
263
+ return this.generateTokens(user.id);
264
+ } catch (error) {
265
+ throw new UnauthorizedException("Invalid refresh token");
266
+ }
267
+ }
268
+
269
+ /**
270
+ * Logout user by invalidating stored refresh token
271
+ *
272
+ * SECURITY:
273
+ * - Clears refresh token from database so it can no longer be used
274
+ */
275
+ async logout(userId: string): Promise<void> {
276
+ await this.prisma.userAuth.update({
277
+ where: { id: userId },
278
+ data: {
279
+ refreshToken: null,
280
+ pushToken: null, // Clear push token on logout for security
281
+ },
282
+ });
283
+
284
+ this.logger.log(`User logged out: ${userId}`);
285
+ }
286
+
287
+ /**
288
+ * Generate JWT access and refresh tokens
289
+ *
290
+ * SECURITY:
291
+ * - Access token: short-lived (1 hour)
292
+ * - Refresh token: longer-lived (7 days)
293
+ * - Refresh token stored in database for rotation
294
+ */
295
+ private async generateTokens(userId: string): Promise<AuthResponseDto> {
296
+ const payload = { sub: userId };
297
+
298
+ // Generate access token
299
+ const accessToken = this.jwtService.sign(payload, {
300
+ secret: this.configService.get("JWT_SECRET"),
301
+ expiresIn: this.configService.get("JWT_EXPIRATION"),
302
+ });
303
+
304
+ // Generate refresh token
305
+ const refreshToken = this.jwtService.sign(payload, {
306
+ secret: this.configService.get("JWT_REFRESH_SECRET"),
307
+ expiresIn: this.configService.get("JWT_REFRESH_EXPIRATION"),
308
+ });
309
+
310
+ // Store refresh token in database
311
+ await this.prisma.userAuth.update({
312
+ where: { id: userId },
313
+ data: { refreshToken },
314
+ });
315
+
316
+ return {
317
+ accessToken,
318
+ refreshToken,
319
+ userId,
320
+ };
321
+ }
322
+
323
+ /**
324
+ * Validate user by ID (used by JWT strategy)
325
+ */
326
+ async validateUser(userId: string) {
327
+ const user = await this.prisma.userAuth.findUnique({
328
+ where: { id: userId },
329
+ select: {
330
+ id: true,
331
+ email: true,
332
+ phone: true,
333
+ isActive: true,
334
+ },
335
+ });
336
+
337
+ if (!user || !user.isActive) {
338
+ return null;
339
+ }
340
+
341
+ return user;
342
+ }
343
+ }
src/auth/dto/auth-response.dto.ts ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Auth Response DTO
3
+ *
4
+ * Returned after successful login or registration
5
+ */
6
+ export class AuthResponseDto {
7
+ accessToken!: string;
8
+ refreshToken!: string;
9
+ userId!: string;
10
+ }
src/auth/dto/forgot-password.dto.ts ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { IsEmail, IsNotEmpty, IsOptional, IsString } from 'class-validator';
2
+
3
+ /**
4
+ * Forgot Password DTO
5
+ *
6
+ * Supports both email and phone for reset
7
+ */
8
+ export class ForgotPasswordDto {
9
+ @IsEmail()
10
+ @IsOptional()
11
+ email?: string;
12
+
13
+ @IsString()
14
+ @IsNotEmpty()
15
+ @IsOptional()
16
+ phone?: string;
17
+ }
src/auth/dto/login.dto.ts ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { IsEmail, IsOptional, IsString, ValidateIf } from "class-validator";
2
+
3
+ /**
4
+ * Login DTO
5
+ *
6
+ * CLINICAL SAFETY:
7
+ * - Email OR phone for login
8
+ * - No credential logging
9
+ */
10
+ export class LoginDto {
11
+ @IsOptional()
12
+ @IsEmail()
13
+ @ValidateIf((o) => !o.phone)
14
+ email?: string;
15
+
16
+ @IsOptional()
17
+ @IsString()
18
+ @ValidateIf((o) => !o.email)
19
+ phone?: string;
20
+
21
+ @IsString()
22
+ password!: string;
23
+ }
src/auth/dto/refresh-token.dto.ts ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ import { IsString } from "class-validator";
2
+
3
+ /**
4
+ * Refresh Token DTO
5
+ */
6
+ export class RefreshTokenDto {
7
+ @IsString()
8
+ refreshToken!: string;
9
+ }
src/auth/dto/register.dto.ts ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {
2
+ IsEmail,
3
+ IsOptional,
4
+ IsString,
5
+ MinLength,
6
+ ValidateIf,
7
+ } from "class-validator";
8
+
9
+ /**
10
+ * Register DTO
11
+ *
12
+ * CLINICAL SAFETY:
13
+ * - Email OR phone required (not both)
14
+ * - No health data collected during registration
15
+ * - Minimal PII only
16
+ */
17
+ export class RegisterDto {
18
+ @IsOptional()
19
+ @IsEmail()
20
+ @ValidateIf((o) => !o.phone) // Email required if no phone
21
+ email?: string;
22
+
23
+ @IsOptional()
24
+ @IsString()
25
+ @ValidateIf((o) => !o.email) // Phone required if no email
26
+ phone?: string;
27
+
28
+ @IsString()
29
+ @MinLength(8, { message: "Password must be at least 8 characters long" })
30
+ password!: string;
31
+ }
src/auth/dto/reset-password.dto.ts ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { IsNotEmpty, IsString, MinLength } from 'class-validator';
2
+
3
+ /**
4
+ * Reset Password DTO
5
+ *
6
+ * Used with a reset token to set a new password
7
+ */
8
+ export class ResetPasswordDto {
9
+ @IsString()
10
+ @IsNotEmpty()
11
+ token!: string;
12
+
13
+ @IsString()
14
+ @IsNotEmpty()
15
+ @MinLength(8, { message: 'Password must be at least 8 characters' })
16
+ newPassword!: string;
17
+ }
src/auth/guards/jwt-auth.guard.ts ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Injectable } from "@nestjs/common";
2
+ import { AuthGuard } from "@nestjs/passport";
3
+
4
+ /**
5
+ * JWT Auth Guard
6
+ *
7
+ * Use this guard to protect routes that require authentication
8
+ *
9
+ * Usage:
10
+ * @UseGuards(JwtAuthGuard)
11
+ */
12
+
13
+ @Injectable()
14
+ export class JwtAuthGuard extends AuthGuard("jwt") {}
src/auth/strategies/jwt.strategy.ts ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Injectable, UnauthorizedException } from "@nestjs/common";
2
+ import { PassportStrategy } from "@nestjs/passport";
3
+ import { ExtractJwt, Strategy } from "passport-jwt";
4
+ import { ConfigService } from "@nestjs/config";
5
+ import { AuthService } from "../auth.service";
6
+
7
+ /**
8
+ * JWT Strategy
9
+ *
10
+ * Validates JWT tokens and attaches user to request
11
+ */
12
+
13
+ @Injectable()
14
+ export class JwtStrategy extends PassportStrategy(Strategy) {
15
+ constructor(
16
+ private readonly configService: ConfigService,
17
+ private readonly authService: AuthService
18
+ ) {
19
+ super({
20
+ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
21
+ ignoreExpiration: false,
22
+ secretOrKey: configService.get<string>("JWT_SECRET") || "fallback_secret",
23
+ });
24
+ }
25
+
26
+ /**
27
+ * Validate JWT payload
28
+ * Called automatically by Passport after token verification
29
+ */
30
+ async validate(payload: any) {
31
+ const user = await this.authService.validateUser(payload.sub);
32
+
33
+ if (!user) {
34
+ throw new UnauthorizedException();
35
+ }
36
+
37
+ return user;
38
+ }
39
+ }
src/blood-pressure/blood-pressure.controller.ts ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {
2
+ Controller,
3
+ Get,
4
+ Post,
5
+ Delete,
6
+ Body,
7
+ Param,
8
+ Query,
9
+ UseGuards,
10
+ Request,
11
+ HttpCode,
12
+ HttpStatus,
13
+ ParseIntPipe,
14
+ Inject,
15
+ forwardRef,
16
+ } from "@nestjs/common";
17
+ import { BloodPressureService } from "./blood-pressure.service";
18
+ import { CreateBloodPressureDto } from "./dto/create-blood-pressure.dto";
19
+ import { JwtAuthGuard } from "../auth/guards/jwt-auth.guard";
20
+ import { HealthAssessmentService } from "../care-priority/health-assessment.service";
21
+
22
+ @Controller("blood-pressure")
23
+ @UseGuards(JwtAuthGuard)
24
+ export class BloodPressureController {
25
+ constructor(
26
+ private readonly bloodPressureService: BloodPressureService,
27
+ @Inject(forwardRef(() => HealthAssessmentService))
28
+ private readonly healthAssessmentService: HealthAssessmentService
29
+ ) {}
30
+
31
+ @Post()
32
+ @HttpCode(HttpStatus.CREATED)
33
+ async create(
34
+ @Request() req: any,
35
+ @Body() createBloodPressureDto: CreateBloodPressureDto
36
+ ) {
37
+ const userId = req.user.id;
38
+ const reading = await this.bloodPressureService.create(
39
+ userId,
40
+ createBloodPressureDto
41
+ );
42
+
43
+ // Trigger health assessment and notifications (don't await to avoid blocking response)
44
+ this.healthAssessmentService.assessAndNotify(userId);
45
+
46
+ return reading;
47
+ }
48
+
49
+ /**
50
+ * Get all BP readings for authenticated user
51
+ *
52
+ * @param req - Request with authenticated user
53
+ * @param limit - Optional limit (default: 50)
54
+ * @returns Array of BP readings
55
+ */
56
+ @Get()
57
+ @HttpCode(HttpStatus.OK)
58
+ async getReadings(
59
+ @Request() req: any,
60
+ @Query("limit", new ParseIntPipe({ optional: true })) limit?: number
61
+ ) {
62
+ const userId = req.user.id;
63
+ return this.bloodPressureService.findByUserId(userId, limit);
64
+ }
65
+
66
+ /**
67
+ * Get latest BP reading
68
+ *
69
+ * @param req - Request with authenticated user
70
+ * @returns Latest BP reading or null
71
+ */
72
+ @Get("latest")
73
+ @HttpCode(HttpStatus.OK)
74
+ async getLatest(@Request() req: any) {
75
+ const userId = req.user.id;
76
+ return this.bloodPressureService.getLatestReading(userId);
77
+ }
78
+
79
+ /**
80
+ * Delete a BP reading
81
+ *
82
+ * @param req - Request with authenticated user
83
+ * @param id - Reading ID
84
+ * @returns Success message
85
+ */
86
+ @Delete(":id")
87
+ @HttpCode(HttpStatus.OK)
88
+ async delete(@Request() req: any, @Param("id") id: string) {
89
+ const userId = req.user.id;
90
+ return this.bloodPressureService.delete(id, userId);
91
+ }
92
+ }
src/blood-pressure/blood-pressure.module.ts ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Module, forwardRef } from "@nestjs/common";
2
+ import { BloodPressureController } from "./blood-pressure.controller";
3
+ import { BloodPressureService } from "./blood-pressure.service";
4
+ import { CarePriorityModule } from "../care-priority/care-priority.module";
5
+
6
+ /**
7
+ * Blood Pressure Module
8
+ *
9
+ * RESPONSIBILITIES:
10
+ * - Store BP readings (systolic/diastolic)
11
+ * - Manual entry only
12
+ * - Timestamp tracking
13
+ *
14
+ * CLINICAL SAFETY CONSTRAINTS:
15
+ * - Do NOT label readings as normal/abnormal
16
+ * - Do NOT store interpretations
17
+ * - Neutral data handling only
18
+ * - Raw measurements for care priority engine
19
+ */
20
+
21
+ @Module({
22
+ imports: [forwardRef(() => CarePriorityModule)],
23
+ controllers: [BloodPressureController],
24
+ providers: [BloodPressureService],
25
+ exports: [BloodPressureService],
26
+ })
27
+ export class BloodPressureModule {}
src/blood-pressure/blood-pressure.service.ts ADDED
@@ -0,0 +1,151 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Injectable, Logger } from "@nestjs/common";
2
+ import { PrismaService } from "../database/prisma.service";
3
+ import { CreateBloodPressureDto } from "./dto/create-blood-pressure.dto";
4
+
5
+ /**
6
+ * Blood Pressure Service
7
+ *
8
+ * CLINICAL SAFETY PRINCIPLES:
9
+ * - Manual entry only (no device integration)
10
+ * - Do NOT label readings as normal/abnormal
11
+ * - Do NOT store interpretations
12
+ * - Neutral data handling only
13
+ *
14
+ * RESPONSIBILITIES:
15
+ * - Store BP readings with validation
16
+ * - Retrieve readings for user
17
+ * - Provide latest reading for care priority engine
18
+ * - Query readings by date range
19
+ */
20
+
21
+ @Injectable()
22
+ export class BloodPressureService {
23
+ private readonly logger = new Logger(BloodPressureService.name);
24
+
25
+ constructor(private readonly prisma: PrismaService) {}
26
+
27
+ /**
28
+ * Create a new blood pressure reading
29
+ *
30
+ * CONSTRAINTS:
31
+ * - Systolic: 60-260 mmHg
32
+ * - Diastolic: 40-160 mmHg
33
+ * - No interpretation stored
34
+ */
35
+ async create(userId: string, createBloodPressureDto: CreateBloodPressureDto) {
36
+ const { systolic, diastolic, recordedAt } = createBloodPressureDto;
37
+
38
+ const reading = await this.prisma.bloodPressureReading.create({
39
+ data: {
40
+ userId,
41
+ systolic,
42
+ diastolic,
43
+ recordedAt: recordedAt ? new Date(recordedAt) : new Date(),
44
+ },
45
+ });
46
+
47
+ this.logger.log(
48
+ `BP reading created for user: ${userId} (${systolic}/${diastolic})`
49
+ );
50
+
51
+ return reading;
52
+ }
53
+
54
+ /**
55
+ * Get all BP readings for a user
56
+ *
57
+ * @param userId - User ID
58
+ * @param limit - Optional limit (default: 50)
59
+ * @returns Array of BP readings, newest first
60
+ */
61
+ async findByUserId(userId: string, limit: number = 50) {
62
+ const readings = await this.prisma.bloodPressureReading.findMany({
63
+ where: { userId },
64
+ orderBy: { recordedAt: "desc" },
65
+ take: limit,
66
+ });
67
+
68
+ return readings;
69
+ }
70
+
71
+ /**
72
+ * Get latest BP reading for a user
73
+ *
74
+ * Used by care priority engine
75
+ */
76
+ async getLatestReading(userId: string) {
77
+ const reading = await this.prisma.bloodPressureReading.findFirst({
78
+ where: { userId },
79
+ orderBy: { recordedAt: "desc" },
80
+ });
81
+
82
+ return reading;
83
+ }
84
+
85
+ /**
86
+ * Get BP readings within a date range
87
+ *
88
+ * @param userId - User ID
89
+ * @param startDate - Start date
90
+ * @param endDate - End date
91
+ * @returns Array of BP readings in range
92
+ */
93
+ async getReadingsInRange(userId: string, startDate: Date, endDate: Date) {
94
+ const readings = await this.prisma.bloodPressureReading.findMany({
95
+ where: {
96
+ userId,
97
+ recordedAt: {
98
+ gte: startDate,
99
+ lte: endDate,
100
+ },
101
+ },
102
+ orderBy: { recordedAt: "desc" },
103
+ });
104
+
105
+ return readings;
106
+ }
107
+
108
+ /**
109
+ * Get readings from last N hours
110
+ *
111
+ * Used by care priority engine to check recent trends
112
+ */
113
+ async getRecentReadings(userId: string, hours: number = 48) {
114
+ const cutoffDate = new Date();
115
+ cutoffDate.setHours(cutoffDate.getHours() - hours);
116
+
117
+ const readings = await this.prisma.bloodPressureReading.findMany({
118
+ where: {
119
+ userId,
120
+ recordedAt: {
121
+ gte: cutoffDate,
122
+ },
123
+ },
124
+ orderBy: { recordedAt: "desc" },
125
+ });
126
+
127
+ return readings;
128
+ }
129
+
130
+ /**
131
+ * Delete a BP reading
132
+ */
133
+ async delete(id: string, userId: string) {
134
+ // Ensure user owns the reading
135
+ const reading = await this.prisma.bloodPressureReading.findFirst({
136
+ where: { id, userId },
137
+ });
138
+
139
+ if (!reading) {
140
+ throw new Error("Reading not found or unauthorized");
141
+ }
142
+
143
+ await this.prisma.bloodPressureReading.delete({
144
+ where: { id },
145
+ });
146
+
147
+ this.logger.log(`BP reading deleted: ${id}`);
148
+
149
+ return { message: "Reading deleted successfully" };
150
+ }
151
+ }
src/blood-pressure/dto/create-blood-pressure.dto.ts ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { IsInt, Min, Max, IsOptional, IsDateString } from "class-validator";
2
+
3
+ /**
4
+ * Create Blood Pressure Reading DTO
5
+ *
6
+ * VALIDATION RULES:
7
+ * - Systolic: 60-260 mmHg (physiologically possible range)
8
+ * - Diastolic: 40-160 mmHg (physiologically possible range)
9
+ * - Optional recordedAt (defaults to now)
10
+ *
11
+ * CLINICAL SAFETY:
12
+ * - No interpretation or labeling
13
+ * - Neutral data collection only
14
+ * - Manual entry only
15
+ */
16
+ export class CreateBloodPressureDto {
17
+ @IsInt()
18
+ @Min(60, { message: "Systolic must be at least 60 mmHg" })
19
+ @Max(260, { message: "Systolic must not exceed 260 mmHg" })
20
+ systolic!: number;
21
+
22
+ @IsInt()
23
+ @Min(40, { message: "Diastolic must be at least 40 mmHg" })
24
+ @Max(160, { message: "Diastolic must not exceed 160 mmHg" })
25
+ diastolic!: number;
26
+
27
+ @IsOptional()
28
+ @IsDateString()
29
+ recordedAt?: string;
30
+ }
src/care-priority/care-priority.controller.ts ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {
2
+ Controller,
3
+ Get,
4
+ UseGuards,
5
+ Request,
6
+ HttpCode,
7
+ HttpStatus,
8
+ } from "@nestjs/common";
9
+ import { CarePriorityService } from "./care-priority.service";
10
+ import { JwtAuthGuard } from "../auth/guards/jwt-auth.guard";
11
+
12
+ /**
13
+ * Care Priority Controller
14
+ *
15
+ * ENDPOINT:
16
+ * - GET /care-priority - Get current care priority (authenticated, read-only)
17
+ *
18
+ * CLINICAL SAFETY:
19
+ * - Read-only endpoint (no POST/PUT/DELETE)
20
+ * - Returns priority level and safe next step message
21
+ * - Non-diagnostic language only
22
+ * - No explanation of medical reasoning
23
+ * - Template-based messaging
24
+ *
25
+ * RESPONSE FORMAT:
26
+ * {
27
+ * priority: 'ROUTINE' | 'INCREASED_MONITORING' | 'URGENT_REVIEW' | 'EMERGENCY',
28
+ * message: 'Safe next step message',
29
+ * reasons: ['Factor 1', 'Factor 2'],
30
+ * timestamp: '2024-01-01T00:00:00.000Z'
31
+ * }
32
+ */
33
+
34
+ @Controller("care-priority")
35
+ @UseGuards(JwtAuthGuard)
36
+ export class CarePriorityController {
37
+ constructor(private readonly carePriorityService: CarePriorityService) {}
38
+
39
+ /**
40
+ * Get current care priority for authenticated user
41
+ *
42
+ * CONSTRAINTS:
43
+ * - Read-only operation
44
+ * - Calculates priority based on latest data
45
+ * - Returns predefined safe message
46
+ * - No diagnostic information
47
+ *
48
+ * @param req - Request with authenticated user
49
+ * @returns Care priority result with safe message
50
+ */
51
+ @Get()
52
+ @HttpCode(HttpStatus.OK)
53
+ async getCarePriority(@Request() req: any) {
54
+ const userId = req.user.id;
55
+
56
+ // Calculate care priority
57
+ const result = await this.carePriorityService.calculateCarePriority(userId);
58
+
59
+ // Get safe next step message
60
+ const message = this.carePriorityService.getSafeNextStepMessage(
61
+ result.priority
62
+ );
63
+
64
+ return {
65
+ priority: result.priority,
66
+ message,
67
+ reasons: result.reasons,
68
+ timestamp: result.timestamp,
69
+ };
70
+ }
71
+ }
src/care-priority/care-priority.module.ts ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Module, forwardRef } from "@nestjs/common";
2
+ import { CarePriorityService } from "./care-priority.service";
3
+ import { CarePriorityController } from "./care-priority.controller";
4
+ import { BloodPressureModule } from "../blood-pressure/blood-pressure.module";
5
+ import { SymptomsModule } from "../symptoms/symptoms.module";
6
+ import { UserProfileModule } from "../user-profile/user-profile.module";
7
+ import { NotificationsModule } from "../notifications/notifications.module";
8
+ import { HealthAssessmentService } from "./health-assessment.service";
9
+
10
+ @Module({
11
+ imports: [
12
+ forwardRef(() => BloodPressureModule),
13
+ forwardRef(() => SymptomsModule),
14
+ UserProfileModule,
15
+ NotificationsModule,
16
+ ],
17
+ controllers: [CarePriorityController],
18
+ providers: [CarePriorityService, HealthAssessmentService],
19
+ exports: [CarePriorityService, HealthAssessmentService],
20
+ })
21
+ export class CarePriorityModule {}
src/care-priority/care-priority.service.ts ADDED
@@ -0,0 +1,298 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Injectable, Logger } from "@nestjs/common";
2
+ import { BloodPressureService } from "../blood-pressure/blood-pressure.service";
3
+ import { SymptomsService } from "../symptoms/symptoms.service";
4
+ import { UserProfileService } from "../user-profile/user-profile.service";
5
+ import { CarePriority, CarePriorityResult } from "./types/care-priority.types";
6
+ import { KnownCondition, SymptomType } from "@prisma/client";
7
+
8
+ /**
9
+ * Care Priority Engine Service
10
+ *
11
+ * ⚠️ MOST CRITICAL MODULE - CLINICAL SAFETY ⚠️
12
+ *
13
+ * PRINCIPLES:
14
+ * - Deterministic, rule-based logic (NO AI/ML)
15
+ * - NO diagnosis
16
+ * - NO medical advice
17
+ * - NO risk scoring or probability calculations
18
+ * - Default to HIGHER priority when uncertain
19
+ * - Based on clinical guidelines for hypertensive disorders in pregnancy
20
+ *
21
+ * RULES SOURCE:
22
+ * - ACOG (American College of Obstetricians and Gynecologists) guidelines
23
+ * - Preeclampsia Foundation recommendations
24
+ * - NHS (UK) hypertension in pregnancy guidelines
25
+ *
26
+ * PURPOSE:
27
+ * - Provide care escalation recommendations
28
+ * - Guide users to seek appropriate level of care
29
+ * - NOT to replace clinical judgment
30
+ */
31
+
32
+ @Injectable()
33
+ export class CarePriorityService {
34
+ private readonly logger = new Logger(CarePriorityService.name);
35
+
36
+ constructor(
37
+ private readonly bloodPressureService: BloodPressureService,
38
+ private readonly symptomsService: SymptomsService,
39
+ private readonly userProfileService: UserProfileService
40
+ ) {}
41
+
42
+ /**
43
+ * Calculate care priority for a user
44
+ *
45
+ * ALGORITHM:
46
+ * 1. Check for EMERGENCY conditions (immediate medical attention)
47
+ * 2. Check for URGENT_REVIEW conditions (contact provider within 24h)
48
+ * 3. Check for INCREASED_MONITORING conditions (more frequent monitoring)
49
+ * 4. Default to ROUTINE if no concerning factors
50
+ *
51
+ * CONSERVATIVE APPROACH:
52
+ * - If multiple factors present, use highest priority
53
+ * - If uncertain, escalate to higher priority
54
+ * - Better to over-escalate than under-escalate
55
+ */
56
+ async calculateCarePriority(userId: string): Promise<CarePriorityResult> {
57
+ const reasons: string[] = [];
58
+ let priority: CarePriority = CarePriority.ROUTINE;
59
+
60
+ try {
61
+ // Get user data
62
+ const profile = await this.userProfileService.findByUserId(userId);
63
+ const latestBP = await this.bloodPressureService.getLatestReading(userId);
64
+ const recentBPs = await this.bloodPressureService.getRecentReadings(
65
+ userId,
66
+ 48
67
+ );
68
+ const recentSymptoms = await this.symptomsService.getRecentSymptoms(
69
+ userId,
70
+ 72
71
+ );
72
+
73
+ // ========================================
74
+ // EMERGENCY CHECKS
75
+ // ========================================
76
+
77
+ // EMERGENCY: Severe hypertension (BP ≥160/110)
78
+ if (latestBP && (latestBP.systolic >= 160 || latestBP.diastolic >= 110)) {
79
+ priority = CarePriority.EMERGENCY;
80
+ reasons.push("Blood pressure reading indicates severe hypertension");
81
+ this.logger.warn(
82
+ `EMERGENCY priority for user ${userId}: BP ${latestBP.systolic}/${latestBP.diastolic}`
83
+ );
84
+ }
85
+
86
+ // EMERGENCY: Dangerous symptom combinations
87
+ const symptomTypes = new Set(
88
+ recentSymptoms.map((s: any) => s.symptomType)
89
+ );
90
+
91
+ // Severe preeclampsia warning signs
92
+ if (
93
+ symptomTypes.has(SymptomType.HEADACHE) &&
94
+ symptomTypes.has(SymptomType.BLURRED_VISION)
95
+ ) {
96
+ priority = CarePriority.EMERGENCY;
97
+ reasons.push("Combination of severe headache and vision changes");
98
+ this.logger.warn(
99
+ `EMERGENCY priority for user ${userId}: Headache + Vision changes`
100
+ );
101
+ }
102
+
103
+ if (
104
+ symptomTypes.has(SymptomType.UPPER_ABDOMINAL_PAIN) &&
105
+ symptomTypes.has(SymptomType.NAUSEA_VOMITING)
106
+ ) {
107
+ priority = CarePriority.EMERGENCY;
108
+ reasons.push("Upper abdominal pain with nausea/vomiting");
109
+ this.logger.warn(
110
+ `EMERGENCY priority for user ${userId}: Abdominal pain + Nausea`
111
+ );
112
+ }
113
+
114
+ if (symptomTypes.has(SymptomType.SHORTNESS_OF_BREATH)) {
115
+ priority = CarePriority.EMERGENCY;
116
+ reasons.push("Difficulty breathing reported");
117
+ this.logger.warn(
118
+ `EMERGENCY priority for user ${userId}: Shortness of breath`
119
+ );
120
+ }
121
+
122
+ // If already EMERGENCY, return immediately
123
+ if (priority === CarePriority.EMERGENCY) {
124
+ return {
125
+ priority,
126
+ reasons,
127
+ timestamp: new Date(),
128
+ };
129
+ }
130
+
131
+ // ========================================
132
+ // URGENT_REVIEW CHECKS
133
+ // ========================================
134
+
135
+ // URGENT: Confirmed hypertension (BP ≥140/90 on two occasions)
136
+ if (recentBPs.length >= 2) {
137
+ const elevatedReadings = recentBPs.filter(
138
+ (bp: any) => bp.systolic >= 140 || bp.diastolic >= 90
139
+ );
140
+
141
+ if (elevatedReadings.length >= 2) {
142
+ priority = CarePriority.URGENT_REVIEW;
143
+ reasons.push("Multiple elevated blood pressure readings");
144
+ }
145
+ }
146
+
147
+ // URGENT: High-risk conditions + elevated BP
148
+ const highRiskConditions = [
149
+ KnownCondition.CHRONIC_HYPERTENSION,
150
+ KnownCondition.PREECLAMPSIA_HISTORY,
151
+ KnownCondition.KIDNEY_DISEASE,
152
+ ];
153
+
154
+ const hasHighRiskCondition = profile.knownConditions.some(
155
+ (condition: any) => highRiskConditions.includes(condition)
156
+ );
157
+
158
+ if (
159
+ hasHighRiskCondition &&
160
+ latestBP &&
161
+ (latestBP.systolic >= 130 || latestBP.diastolic >= 85)
162
+ ) {
163
+ priority = CarePriority.URGENT_REVIEW;
164
+ reasons.push("High-risk condition with elevated blood pressure");
165
+ }
166
+
167
+ // URGENT: Any severe symptoms individually
168
+ if (
169
+ symptomTypes.has(SymptomType.HEADACHE) ||
170
+ symptomTypes.has(SymptomType.BLURRED_VISION) ||
171
+ symptomTypes.has(SymptomType.UPPER_ABDOMINAL_PAIN)
172
+ ) {
173
+ if (priority !== CarePriority.URGENT_REVIEW) {
174
+ priority = CarePriority.URGENT_REVIEW;
175
+ }
176
+ reasons.push("Warning symptoms present");
177
+ }
178
+
179
+ // URGENT: Reduced urine output
180
+ if (symptomTypes.has(SymptomType.REDUCED_URINE)) {
181
+ priority = CarePriority.URGENT_REVIEW;
182
+ reasons.push("Reduced urine output reported");
183
+ }
184
+
185
+ // If URGENT_REVIEW, return
186
+ if (priority === CarePriority.URGENT_REVIEW) {
187
+ return {
188
+ priority,
189
+ reasons,
190
+ timestamp: new Date(),
191
+ };
192
+ }
193
+
194
+ // ========================================
195
+ // INCREASED_MONITORING CHECKS
196
+ // ========================================
197
+
198
+ // INCREASED: Borderline BP (130-159 / 85-109)
199
+ if (
200
+ latestBP &&
201
+ (latestBP.systolic >= 130 || latestBP.diastolic >= 85)
202
+ ) {
203
+ priority = CarePriority.INCREASED_MONITORING;
204
+ reasons.push("Blood pressure reading is above normal range");
205
+ }
206
+
207
+ // INCREASED: High-risk conditions present
208
+ if (hasHighRiskCondition) {
209
+ priority = CarePriority.INCREASED_MONITORING;
210
+ reasons.push("High-risk pregnancy condition present");
211
+ }
212
+
213
+ // INCREASED: Advanced maternal age (35+)
214
+ if (profile.ageRange === "AGE_35_PLUS") {
215
+ if (priority !== CarePriority.INCREASED_MONITORING) {
216
+ priority = CarePriority.INCREASED_MONITORING;
217
+ }
218
+ reasons.push("Advanced maternal age");
219
+ }
220
+
221
+ // INCREASED: Multiple pregnancy
222
+ if (profile.knownConditions.includes(KnownCondition.MULTIPLE_PREGNANCY)) {
223
+ priority = CarePriority.INCREASED_MONITORING;
224
+ reasons.push("Multiple pregnancy");
225
+ }
226
+
227
+ // INCREASED: Any symptoms present (even mild)
228
+ if (recentSymptoms.length > 0) {
229
+ if (priority !== CarePriority.INCREASED_MONITORING) {
230
+ priority = CarePriority.INCREASED_MONITORING;
231
+ }
232
+ reasons.push("Symptoms reported");
233
+ }
234
+
235
+ // If INCREASED_MONITORING, return
236
+ if (priority === CarePriority.INCREASED_MONITORING) {
237
+ return {
238
+ priority,
239
+ reasons,
240
+ timestamp: new Date(),
241
+ };
242
+ }
243
+
244
+ // ========================================
245
+ // ROUTINE (Default)
246
+ // ========================================
247
+
248
+ reasons.push("No concerning factors identified");
249
+
250
+ return {
251
+ priority: CarePriority.ROUTINE,
252
+ reasons,
253
+ timestamp: new Date(),
254
+ };
255
+ } catch (error) {
256
+ // SAFETY: If calculation fails, default to INCREASED_MONITORING
257
+ this.logger.error(
258
+ `Error calculating care priority for user ${userId}:`,
259
+ error
260
+ );
261
+
262
+ return {
263
+ priority: CarePriority.INCREASED_MONITORING,
264
+ reasons: [
265
+ "Unable to complete assessment - please contact your healthcare provider",
266
+ ],
267
+ timestamp: new Date(),
268
+ };
269
+ }
270
+ }
271
+
272
+ /**
273
+ * Get safe next step message based on priority
274
+ *
275
+ * CONSTRAINTS:
276
+ * - Predefined templates only
277
+ * - No dynamic medical text
278
+ * - No diagnostic language
279
+ * - No fear-based messaging
280
+ */
281
+ getSafeNextStepMessage(priority: CarePriority): string {
282
+ const messages = {
283
+ [CarePriority.EMERGENCY]:
284
+ "Seek immediate medical attention. Call emergency services or go to the nearest emergency room.",
285
+
286
+ [CarePriority.URGENT_REVIEW]:
287
+ "Contact your healthcare provider within the next 24 hours to discuss your readings.",
288
+
289
+ [CarePriority.INCREASED_MONITORING]:
290
+ "Continue monitoring your blood pressure regularly and discuss with your healthcare provider at your next appointment.",
291
+
292
+ [CarePriority.ROUTINE]:
293
+ "Continue routine prenatal care and monitoring as recommended by your healthcare provider.",
294
+ };
295
+
296
+ return messages[priority];
297
+ }
298
+ }
src/care-priority/health-assessment.service.ts ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Injectable, Logger } from "@nestjs/common";
2
+ import { CarePriorityService } from "./care-priority.service";
3
+ import { NotificationsService } from "../notifications/notifications.service";
4
+ import { BloodPressureService } from "../blood-pressure/blood-pressure.service";
5
+ import { SymptomsService } from "../symptoms/symptoms.service";
6
+ import { CarePriority } from "./types/care-priority.types";
7
+ import { SymptomType } from "@prisma/client";
8
+
9
+ @Injectable()
10
+ export class HealthAssessmentService {
11
+ private readonly logger = new Logger(HealthAssessmentService.name);
12
+
13
+ constructor(
14
+ private readonly carePriorityService: CarePriorityService,
15
+ private readonly notificationsService: NotificationsService,
16
+ private readonly bloodPressureService: BloodPressureService,
17
+ private readonly symptomsService: SymptomsService
18
+ ) {}
19
+
20
+ /**
21
+ * Assess user health after a new BP reading or symptom is recorded
22
+ * and trigger appropriate notifications.
23
+ */
24
+ async assessAndNotify(userId: string): Promise<void> {
25
+ try {
26
+ this.logger.log(`Starting health assessment for user: ${userId}`);
27
+
28
+ // 1. Calculate current care priority
29
+ const assessment = await this.carePriorityService.calculateCarePriority(
30
+ userId
31
+ );
32
+ const { priority } = assessment;
33
+
34
+ // 2. Get latest data for specific alerts
35
+ const latestBP = await this.bloodPressureService.getLatestReading(userId);
36
+ const recentSymptoms = await this.symptomsService.getRecentSymptoms(
37
+ userId,
38
+ 24
39
+ );
40
+ const symptomTypes = new Set(
41
+ recentSymptoms.map((s: any) => s.symptomType)
42
+ );
43
+
44
+ // 3. Trigger specific BP alerts if needed
45
+ if (latestBP) {
46
+ if (latestBP.systolic >= 160 || latestBP.diastolic >= 110) {
47
+ await this.notificationsService.sendSevereBPAlert(
48
+ userId,
49
+ latestBP.systolic,
50
+ latestBP.diastolic
51
+ );
52
+ } else if (latestBP.systolic >= 140 || latestBP.diastolic >= 90) {
53
+ await this.notificationsService.sendElevatedBPNotification(
54
+ userId,
55
+ latestBP.systolic,
56
+ latestBP.diastolic
57
+ );
58
+ }
59
+ }
60
+
61
+ // 4. Trigger symptom alerts if dangerous symptoms are present
62
+ const dangerousSymptoms = [
63
+ SymptomType.HEADACHE,
64
+ SymptomType.BLURRED_VISION,
65
+ SymptomType.UPPER_ABDOMINAL_PAIN,
66
+ SymptomType.SHORTNESS_OF_BREATH,
67
+ ];
68
+
69
+ const hasDangerousSymptom = Array.from(symptomTypes).some((type) =>
70
+ dangerousSymptoms.includes(type)
71
+ );
72
+
73
+ if (hasDangerousSymptom) {
74
+ const activeDangerous = Array.from(symptomTypes).filter((type) =>
75
+ dangerousSymptoms.includes(type)
76
+ );
77
+ await this.notificationsService.sendDangerousSymptomAlert(
78
+ userId,
79
+ activeDangerous.map((s) => s.toString())
80
+ );
81
+ } else if (symptomTypes.size > 0) {
82
+ await this.notificationsService.sendWarningSymptomNotification(
83
+ userId,
84
+ Array.from(symptomTypes)[0].toString()
85
+ );
86
+ }
87
+
88
+ // 5. Trigger care priority notification if priority is not ROUTINE
89
+ if (priority !== CarePriority.ROUTINE) {
90
+ await this.notificationsService.sendCarePriorityNotification(
91
+ userId,
92
+ priority
93
+ );
94
+ }
95
+
96
+ this.logger.log(`Health assessment completed for user: ${userId}`);
97
+ } catch (error) {
98
+ this.logger.error(
99
+ `Error during health assessment for user ${userId}:`,
100
+ error
101
+ );
102
+ }
103
+ }
104
+ }
src/care-priority/types/care-priority.types.ts ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Care Priority Levels
3
+ *
4
+ * CLINICAL SAFETY PRINCIPLES:
5
+ * - These are NOT diagnoses
6
+ * - These are NOT risk scores
7
+ * - These are care escalation recommendations
8
+ * - When uncertain, escalate to higher priority
9
+ *
10
+ * LEVELS:
11
+ * - ROUTINE: Normal monitoring, next scheduled appointment
12
+ * - INCREASED_MONITORING: More frequent self-monitoring, contact provider soon
13
+ * - URGENT_REVIEW: Contact healthcare provider within 24 hours
14
+ * - EMERGENCY: Seek immediate medical attention
15
+ */
16
+ export enum CarePriority {
17
+ ROUTINE = "ROUTINE",
18
+ INCREASED_MONITORING = "INCREASED_MONITORING",
19
+ URGENT_REVIEW = "URGENT_REVIEW",
20
+ EMERGENCY = "EMERGENCY",
21
+ }
22
+
23
+ /**
24
+ * Care Priority Result
25
+ *
26
+ * Returned by the care priority engine
27
+ */
28
+ export interface CarePriorityResult {
29
+ priority: CarePriority;
30
+ reasons: string[]; // List of factors that contributed to this priority
31
+ timestamp: Date;
32
+ }
src/clinic-finder/clinic-finder.controller.ts ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Controller, Get, HttpCode, HttpStatus, Query, UseGuards } from "@nestjs/common";
2
+ import { ClinicFinderService } from "./clinic-finder.service";
3
+ import { JwtAuthGuard } from "../auth/guards/jwt-auth.guard";
4
+
5
+ @Controller("clinic-finder")
6
+ @UseGuards(JwtAuthGuard)
7
+ export class ClinicFinderController {
8
+ constructor(private readonly clinicFinderService: ClinicFinderService) {}
9
+
10
+ @Get()
11
+ @HttpCode(HttpStatus.OK)
12
+ findAll(
13
+ @Query("city") city?: string,
14
+ @Query("lat") lat?: string,
15
+ @Query("lng") lng?: string,
16
+ @Query("radius") radius?: string
17
+ ) {
18
+ if (lat && lng) {
19
+ return this.clinicFinderService.findNearby(
20
+ parseFloat(lat),
21
+ parseFloat(lng),
22
+ radius ? parseInt(radius) : 5000
23
+ );
24
+ }
25
+
26
+ if (city) {
27
+ return this.clinicFinderService.findByCity(city);
28
+ }
29
+
30
+ return this.clinicFinderService.findAll();
31
+ }
32
+ }
33
+
src/clinic-finder/clinic-finder.module.ts ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Module } from "@nestjs/common";
2
+ import { ClinicFinderController } from "./clinic-finder.controller";
3
+ import { ClinicFinderService } from "./clinic-finder.service";
4
+
5
+ @Module({
6
+ controllers: [ClinicFinderController],
7
+ providers: [ClinicFinderService],
8
+ exports: [ClinicFinderService],
9
+ })
10
+ export class ClinicFinderModule {}
11
+
src/clinic-finder/clinic-finder.service.ts ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Injectable, Logger } from "@nestjs/common";
2
+ import axios from "axios";
3
+ import { CLINIC_LOCATIONS, ClinicLocation } from "./data/clinics.data";
4
+
5
+ @Injectable()
6
+ export class ClinicFinderService {
7
+ private readonly logger = new Logger(ClinicFinderService.name);
8
+ private readonly overpassUrl = "https://overpass-api.de/api/interpreter";
9
+
10
+ async findNearby(
11
+ lat: number,
12
+ lng: number,
13
+ radius: number = 5000
14
+ ): Promise<ClinicLocation[]> {
15
+ try {
16
+ this.logger.log(`Searching for hospitals near ${lat}, ${lng} within ${radius}m using Overpass API`);
17
+
18
+ const query = `
19
+ [out:json][timeout:25];
20
+ (
21
+ node["amenity"="hospital"](around:${radius},${lat},${lng});
22
+ way["amenity"="hospital"](around:${radius},${lat},${lng});
23
+ node["amenity"="clinic"](around:${radius},${lat},${lng});
24
+ way["amenity"="clinic"](around:${radius},${lat},${lng});
25
+ );
26
+ out center;
27
+ `;
28
+
29
+ const response = await axios.post(this.overpassUrl, `data=${encodeURIComponent(query)}`);
30
+
31
+ if (response.data && response.data.elements) {
32
+ return response.data.elements.map((element: any) => ({
33
+ id: element.id.toString(),
34
+ name: element.tags.name || "Unnamed Hospital",
35
+ latitude: element.lat || element.center?.lat,
36
+ longitude: element.lon || element.center?.lon,
37
+ address: element.tags["addr:street"]
38
+ ? `${element.tags["addr:housenumber"] || ""} ${element.tags["addr:street"]}`.trim()
39
+ : "Address not available",
40
+ city: element.tags["addr:city"] || "Detected",
41
+ country: "Nigeria",
42
+ phone: element.tags.phone || element.tags["contact:phone"] || "Contact via Maps",
43
+ isEmergency: element.tags.amenity === "hospital",
44
+ }));
45
+ }
46
+
47
+ return [];
48
+ } catch (error: any) {
49
+ this.logger.error(`Failed to fetch nearby hospitals from Overpass: ${error.message}`);
50
+ return this.findAll(); // Fallback to seed data on error
51
+ }
52
+ }
53
+
54
+ findAll(): ClinicLocation[] {
55
+ return CLINIC_LOCATIONS;
56
+ }
57
+
58
+ findByCity(city: string): ClinicLocation[] {
59
+ const normalized = city.trim().toLowerCase();
60
+
61
+ // If city is "Ikeja", include "Lagos" clinics as well, as Ikeja is part of Lagos
62
+ const isIkeja = normalized === "ikeja";
63
+
64
+ return CLINIC_LOCATIONS.filter((clinic) => {
65
+ const clinicCity = clinic.city.toLowerCase();
66
+ if (isIkeja) {
67
+ return clinicCity === "ikeja" || clinicCity === "lagos";
68
+ }
69
+ return clinicCity === normalized;
70
+ });
71
+ }
72
+ }
73
+
src/clinic-finder/data/clinics.data.ts ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export interface ClinicLocation {
2
+ id: string;
3
+ name: string;
4
+ latitude: number;
5
+ longitude: number;
6
+ address: string;
7
+ city: string;
8
+ country: string;
9
+ phone?: string;
10
+ isEmergency?: boolean;
11
+ }
12
+
13
+ export const CLINIC_LOCATIONS: ClinicLocation[] = [
14
+ {
15
+ id: "lagos-luth",
16
+ name: "Lagos University Teaching Hospital",
17
+ latitude: 6.5069,
18
+ longitude: 3.3673,
19
+ address: "Ishaga Road, Idi-Araba, Surulere",
20
+ city: "Lagos",
21
+ country: "Nigeria",
22
+ phone: "+234-1-877-7000",
23
+ isEmergency: true,
24
+ },
25
+ {
26
+ id: "lagos-island-maternity",
27
+ name: "Island Maternity Hospital",
28
+ latitude: 6.4513,
29
+ longitude: 3.3940,
30
+ address: "Broad Street, Lagos Island",
31
+ city: "Lagos",
32
+ country: "Nigeria",
33
+ phone: "+234-1-264-3018",
34
+ isEmergency: true,
35
+ },
36
+ {
37
+ id: "abuja-national-hospital",
38
+ name: "National Hospital Abuja",
39
+ latitude: 9.0360,
40
+ longitude: 7.4899,
41
+ address: "Plot 132 Central District (Phase II), Garki",
42
+ city: "Abuja",
43
+ country: "Nigeria",
44
+ phone: "+234-9-234-0940",
45
+ isEmergency: true,
46
+ },
47
+ {
48
+ id: "abuja-nyanya-general",
49
+ name: "Nyanya General Hospital",
50
+ latitude: 8.9953,
51
+ longitude: 7.6207,
52
+ address: "Nyanya",
53
+ city: "Abuja",
54
+ country: "Nigeria",
55
+ phone: "+234-9-123-4567",
56
+ isEmergency: false,
57
+ },
58
+ {
59
+ id: "ikeja-lasuth",
60
+ name: "Lagos State University Teaching Hospital (LASUTH)",
61
+ latitude: 6.5962,
62
+ longitude: 3.3514,
63
+ address: "1-5 Oba Akinjobi Way, Ikeja",
64
+ city: "Ikeja",
65
+ country: "Nigeria",
66
+ phone: "+234-1-493-4500",
67
+ isEmergency: true,
68
+ },
69
+ {
70
+ id: "ikeja-medical-centre",
71
+ name: "Ikeja Medical Centre",
72
+ latitude: 6.6015,
73
+ longitude: 3.3421,
74
+ address: "11-15 Olufemi Road, Ikeja",
75
+ city: "Ikeja",
76
+ country: "Nigeria",
77
+ phone: "+234-1-497-2345",
78
+ isEmergency: true,
79
+ },
80
+ {
81
+ id: "reddington-hospital-ikeja",
82
+ name: "Reddington Hospital Ikeja",
83
+ latitude: 6.5921,
84
+ longitude: 3.3587,
85
+ address: "12 Isaac John St, GRA, Ikeja",
86
+ city: "Ikeja",
87
+ country: "Nigeria",
88
+ phone: "+234-1-271-5341",
89
+ isEmergency: true,
90
+ },
91
+ {
92
+ id: "eunice-clinic-ikeja",
93
+ name: "Eunice Clinic & Hospital",
94
+ latitude: 6.6054,
95
+ longitude: 3.3489,
96
+ address: "24 Allen Ave, Ikeja",
97
+ city: "Ikeja",
98
+ country: "Nigeria",
99
+ phone: "+234-803-123-4567",
100
+ isEmergency: false,
101
+ },
102
+ {
103
+ id: "nicholas-hospital-ikeja",
104
+ name: "St. Nicholas Hospital Ikeja",
105
+ latitude: 6.5901,
106
+ longitude: 3.3602,
107
+ address: "7b Maryland Est, Ikeja",
108
+ city: "Ikeja",
109
+ country: "Nigeria",
110
+ phone: "+234-1-280-5000",
111
+ isEmergency: true,
112
+ },
113
+ {
114
+ id: "blue-cross-ikeja",
115
+ name: "Blue Cross Hospital",
116
+ latitude: 6.6123,
117
+ longitude: 3.3345,
118
+ address: "14 Ogba Road, Ikeja",
119
+ city: "Ikeja",
120
+ country: "Nigeria",
121
+ phone: "+234-1-492-3456",
122
+ isEmergency: false,
123
+ },
124
+ ];
125
+
src/database/database.module.ts ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Module, Global } from "@nestjs/common";
2
+ import { PrismaService } from "./prisma.service";
3
+
4
+ /**
5
+ * Database Module (Prisma)
6
+ *
7
+ * ORM CHOICE: Prisma
8
+ *
9
+ * JUSTIFICATION:
10
+ * - Type-safe database client with excellent TypeScript support
11
+ * - Intuitive schema definition language
12
+ * - Automatic migration generation
13
+ * - Built-in connection pooling
14
+ * - Great developer experience with Prisma Studio
15
+ * - Strong PostgreSQL support
16
+ *
17
+ * CLINICAL SAFETY CONSIDERATIONS:
18
+ * - All entities will enforce strict validation at schema level
19
+ * - Type safety prevents invalid data operations
20
+ * - Audit logging capabilities via middleware
21
+ * - Transaction support for data integrity
22
+ * - No raw SQL for sensitive operations (use Prisma queries)
23
+ *
24
+ * This module is marked as @Global so PrismaService is available
25
+ * throughout the application without repeated imports
26
+ */
27
+
28
+ @Global()
29
+ @Module({
30
+ providers: [PrismaService],
31
+ exports: [PrismaService],
32
+ })
33
+ export class DatabaseModule {}
src/database/prisma.service.ts ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {
2
+ Injectable,
3
+ OnModuleInit,
4
+ OnModuleDestroy,
5
+ Logger,
6
+ } from "@nestjs/common";
7
+ import { PrismaClient } from "@prisma/client";
8
+ import { PrismaPg } from "@prisma/adapter-pg";
9
+ import { Pool } from "pg";
10
+
11
+ /**
12
+ * Prisma Service
13
+ *
14
+ * Provides database access throughout the application
15
+ * Handles connection lifecycle and health checks
16
+ *
17
+ * CLINICAL SAFETY CONSIDERATIONS:
18
+ * - All queries are type-safe
19
+ * - Automatic connection pooling
20
+ * - Transaction support for data integrity
21
+ * - Query logging in development
22
+ */
23
+
24
+ @Injectable()
25
+ export class PrismaService
26
+ extends PrismaClient
27
+ implements OnModuleInit, OnModuleDestroy
28
+ {
29
+ private readonly logger = new Logger(PrismaService.name);
30
+
31
+ constructor() {
32
+ const pool = new Pool({
33
+ connectionString: process.env.DATABASE_URL,
34
+ max: 10, // Maintain up to 10 connections
35
+ idleTimeoutMillis: 30000,
36
+ connectionTimeoutMillis: 10000,
37
+ ssl: {
38
+ rejectUnauthorized: false, // Required for Neon in some environments
39
+ },
40
+ });
41
+
42
+ // Log pool errors
43
+ pool.on('error', (err) => {
44
+ this.logger.error('Unexpected error on idle client', err);
45
+ });
46
+
47
+ super({
48
+ adapter: new PrismaPg(pool),
49
+ log:
50
+ process.env.NODE_ENV === "development"
51
+ ? ["query", "info", "warn", "error"]
52
+ : ["error"],
53
+ });
54
+ }
55
+
56
+ /**
57
+ * Connect to database on module initialization
58
+ */
59
+ async onModuleInit() {
60
+ try {
61
+ await this.$connect();
62
+ this.logger.log("✅ Database connection successful");
63
+
64
+ // Test the connection
65
+ await this.$queryRaw`SELECT 1`;
66
+ this.logger.log("📊 Database health check passed");
67
+ } catch (error) {
68
+ this.logger.error("❌ Database connection failed", error);
69
+ throw error;
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Disconnect from database on module destruction
75
+ */
76
+ async onModuleDestroy() {
77
+ await this.$disconnect();
78
+ this.logger.log("🔌 Database disconnected");
79
+ }
80
+
81
+ /**
82
+ * Get database health status
83
+ */
84
+ async isHealthy(): Promise<boolean> {
85
+ try {
86
+ await this.$queryRaw`SELECT 1`;
87
+ return true;
88
+ } catch {
89
+ return false;
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Get database info (safe, no credentials)
95
+ */
96
+ getDatabaseInfo() {
97
+ return {
98
+ provider: "postgresql",
99
+ isConnected: true, // If we can call this, we're connected
100
+ };
101
+ }
102
+ }
src/education/data/data.txt ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ THIS IS ACCORDING TO WHO
2
+
3
+ Pre-eclampsia is a high blood pressure disorder that typically develops after 20 weeks into pregnancy. It can present serious risks to both mother and baby. Early detection and management are crucial to prevent progression to eclampsia, which involves seizures. Both conditions can be life-threatening.
4
+
5
+ Diagnosis
6
+
7
+ Pre-eclampsia is diagnosed based on the onset of hypertension (blood pressure ≥140/90 mm Hg) proteinuria (protein in the urine) (≥0.3 g/24 hours) after 20 weeks of gestation. Severe pre-eclampsia may include symptoms such as severe headaches, visual disturbances and upper abdominal pain.
8
+
9
+ Risk factors
10
+
11
+ Several factors can increase the risk of developing pre-eclampsia during pregnancy. Understanding these risk factors is essential for proactive monitoring and management. Having a risk factor doesn't always mean pre-eclampsia will occur, but closer medical supervision beyond routine screening is recommended.
12
+
13
+ Several factors can increase the risk of developing pre-eclampsia, including:
14
+
15
+ first-time pregnancies
16
+ multiple pregnancies (twins, triplets, etc.)
17
+ obesity
18
+ pre-existing conditions such as hypertension, diabetes, or kidney disease
19
+ family history of pre-eclampsia.
20
+ Symptoms
21
+
22
+ Symptoms of pre-eclampsia can vary significantly among individuals. While some may experience a range of noticeable symptoms, others may remain asymptomatic. It is important to be aware of potential indicators and seek medical attention if any concerns arise during pregnancy or after childbirth.
23
+
24
+ Common symptoms of pre-eclampsia include:
25
+
26
+ persistent high blood pressure
27
+ proteinuria
28
+ severe headaches
29
+ visual disturbances (e.g., blurred vision, seeing spots)
30
+ upper abdominal pain
31
+ nausea and vomiting (after the first trimester)
32
+ swelling in the hands and face.
33
+ Complications
34
+
35
+ Pre-eclampsia, if left untreated, can lead to serious complications for both mother and baby. These complications can range from short-term issues to long-term health problems. Prompt medical intervention is crucial to minimize these risks.
36
+
37
+ Complications can be severe and include:
38
+
39
+ eclampsia (seizures)
40
+ HELLP syndrome (hemolysis, elevated liver enzymes, low platelet count)
41
+ organ damage (kidneys, liver, brain)
42
+ placental abruption
43
+ preterm birth
44
+ fetal growth restriction
45
+ maternal and fetal death.
46
+ Treatment and management
47
+
48
+ The primary treatment for pre-eclampsia is the administration of magnesium sulfate to prevent seizures.
49
+
50
+ The treatment and management of pre-eclampsia depend on the severity of the condition and the gestational age of the pregnancy. The goal is to prevent complications.
51
+
52
+ Other management strategies include:
53
+
54
+ antihypertensive medications to control blood pressure
55
+ corticosteroids to accelerate fetal lung maturity if preterm delivery is anticipated
56
+ close monitoring of maternal and fetal health.
57
+ Prevention
58
+
59
+ While there's no guaranteed way to prevent pre-eclampsia, certain strategies can help lower the risk. Early and consistent prenatal care is essential for monitoring and managing potential risk factors.
60
+
61
+ Preventive measures focus on regular prenatal care to monitor for early signs of pre-eclampsia. Recommendations include:
62
+
63
+ regular blood pressure checks
64
+ urine tests for protein
65
+ monitoring for symptoms such as headaches and visual disturbances
66
+ lifestyle considerations, such as maintaining a healthy weight and activity (when permitted)
67
+ managing pre-existing conditions, especially pre-existing high blood pressure.
68
+ Additional prevention measures include:
69
+
70
+ low dose of aspirin by 20 weeks or when antenatal care begins
71
+ calcium supplementation during pregnancy in settings with low dietary intake
72
+ treatment of pre-existing high blood pressure with antihypertensive medications.
73
+ WHO response
74
+
75
+ The World Health Organization (WHO) has developed guidelines to improve health during pregnancy. This includes prevention and treatment of pre-eclampsia and eclampsia and continuously reviewing evidence to see if revisions in the recommendations are needed so that improvements in care can be effected. These guidelines aim to reduce maternal and perinatal morbidity and mortality by promoting evidence-based clinical practices. Key WHO recommendations include:
76
+
77
+ calcium supplementation during pregnancy in areas with low dietary calcium intake
78
+ low-dose aspirin during pregnancy for women at high risk of pre-eclampsia
79
+ use of magnesium sulfate for the prevention of eclampsia
80
+ training healthcare providers in the early detection and management of pre-eclampsia
81
+ strengthening health systems to ensure timely and effective care for pregnant women.
82
+ By implementing these guidelines, WHO aims to address the profound inequities in maternal and perinatal health globally and achieve the health targets of the Sustainable Development Goals (SDGs).
83
+
84
+
85
+ END OF WHO
86
+
87
+
88
+
89
+
90
+ The causes of preeclampsia and eclampsia are not known. These disorders previously were believed to be caused by a toxin, called “toxemia,” in the blood, but health care providers now know that is not true. Nevertheless, preeclampsia is sometimes still referred to as “toxemia.”
91
+
92
+ To learn more about preeclampsia and eclampsia, scientists are investigating many factors that could contribute to the development and progression of these diseases, including:
93
+
94
+ Placental abnormalities, such as insufficient blood flow
95
+ Genetic factors
96
+ Environmental exposures
97
+ Nutritional factors
98
+ Maternal immunology and autoimmune disorders
99
+ Cardiovascular and inflammatory changes
100
+ Hormonal imbalances
101
+
102
+ Risks During Pregnancy
103
+ Preeclampsia during pregnancy is mild in the majority of cases.1 However, a woman can progress from mild to severe preeclampsia or to full eclampsia very quickly―even in a matter of days. Both preeclampsia and eclampsia can cause serious health problems for the mother and infant.
104
+
105
+ Women with preeclampsia are at increased risk for damage to the kidneys, liver, brain, and other organ and blood systems. Preeclampsia may also affect the placenta. The condition could lead to a separation of the placenta from the uterus (referred to as placental abruption), preterm birth, and pregnancy loss or stillbirth. In some cases, preeclampsia can lead to organ failure or stroke.
106
+
107
+ In severe cases, preeclampsia can develop into eclampsia, which includes seizures. Seizures in eclampsia may cause a woman to lose consciousness and twitch uncontrollably.2 If the fetus is not delivered, these conditions can cause the death of the mother and/or the fetus.
108
+
109
+ Although most pregnant women in developed countries survive preeclampsia, it is still a major cause of illness and death globally.3 According to the World Health Organization, preeclampsia and eclampsia cause 14% of maternal deaths each year, or about 50,000 to 75,000 women worldwide.4
110
+
111
+
112
+ Risks After Pregnancy
113
+ In "uncomplicated preeclampsia," the mother's high blood pressure and other symptoms usually go back to normal within 6 weeks of the infant's birth. However, studies have shown that women who had preeclampsia are four times more likely to later develop hypertension (high blood pressure) and are twice as likely to later develop ischemic heart disease (reduced blood supply to the heart muscle, which can cause heart attacks), a blood clot in a vein, and stroke as are women who did not have preeclampsia.5
114
+
115
+ Less commonly, mothers who had preeclampsia can experience permanent damage to their organs, such as their kidneys and liver. They can also experience fluid in the lungs. In the days following birth, women with preeclampsia remain at increased risk for developing eclampsia and seizures.3,6
116
+
117
+ In some women, preeclampsia develops between 48 hours and 6 weeks after they deliver their baby—a condition called postpartum preeclampsia.7,8 Postpartum preeclampsia can occur in women who had preeclampsia during pregnancy and among those who did not. One study found that slightly more than one-half of women who had postpartum preeclampsia did not have preeclampsia during pregnancy.9 If a woman has seizures within 72 hours of delivery, she may have postpartum eclampsia. It is important to recognize and treat postpartum preeclampsia and eclampsia because the risk of complications may be higher than if the conditions had occurred during pregnancy.10 Postpartum preeclampsia and eclampsia can progress very quickly if not treated and may lead to stroke or death. Visit the Preeclampsia Foundation website for mor
118
+
119
+
120
+ https://youtu.be/dqMXyDLiUqg
121
+
122
+
123
+ Video: Overview of Preeclampsia and Eclampsia-MSD Manual Professional Edition https://www.msdmanuals.com/professional/multimedia/video/overview-of-preeclampsia-and-eclampsia?utm_source=chatgpt.com
src/education/data/education.content.ts ADDED
@@ -0,0 +1,177 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export interface EducationArticle {
2
+ id: string;
3
+ title: string;
4
+ category: "HYPERTENSION" | "NUTRITION" | "WARNING_SIGNS" | "POSTPARTUM";
5
+ summary: string;
6
+ content: string; // Plain text or safe markdown
7
+ readTimeMinutes: number;
8
+ source: string;
9
+ sourceUrl?: string;
10
+ videoUrl?: string;
11
+ }
12
+
13
+ /**
14
+ * Static Education Content
15
+ *
16
+ * CLINICAL SAFETY:
17
+ * - Hardcoded content to ensure clinical accuracy
18
+ * - No dynamic generation
19
+ * - Curated from reputable sources (ACOG, WHO, Preeclampsia Foundation)
20
+ * - Non-alarmist language
21
+ */
22
+ export const EDUCATION_CONTENT: EducationArticle[] = [
23
+ {
24
+ id: "understanding-bp",
25
+ title: "Understanding Blood Pressure in Pregnancy",
26
+ category: "HYPERTENSION",
27
+ summary:
28
+ "Learn what blood pressure numbers mean and why they matter during pregnancy.",
29
+ content: `Blood pressure is the force of your blood pushing against the walls of your arteries. It is measured with two numbers:
30
+
31
+ 1. Systolic (top number): The pressure when your heart beats.
32
+ 2. Diastolic (bottom number): The pressure when your heart rests between beats.
33
+
34
+ In pregnancy, it's important to keep track of these numbers. A normal reading is typically below 120/80 mmHg. If your numbers are higher, your healthcare provider will want to monitor you more closely to ensure both you and your baby stay healthy.`,
35
+ readTimeMinutes: 3,
36
+ source: "World Health Organization (WHO) patient information on hypertension in pregnancy",
37
+ sourceUrl:
38
+ "https://www.who.int/health-topics/hypertension#tab=tab_1",
39
+ videoUrl:
40
+ "https://www.preeclampsia.org/video-library",
41
+ },
42
+ {
43
+ id: "warning-signs",
44
+ title: "Warning Signs to Watch For",
45
+ category: "WARNING_SIGNS",
46
+ summary:
47
+ "Key symptoms that should prompt you to call your healthcare provider.",
48
+ content: `Most pregnancies go smoothly, but knowing which symptoms to watch for can help you get care quickly if needed. Contact your provider if you experience:
49
+
50
+ - Severe headaches that won't go away with medication
51
+ - Changes in vision (blurring, flashing lights, or spots)
52
+ - Sudden swelling in your face or hands
53
+ - Pain in the upper right side of your belly
54
+ - Trouble breathing
55
+ - Nausea or vomiting (especially after mid-pregnancy)
56
+
57
+ If you are unsure, it is always safer to call your provider and ask.`,
58
+ readTimeMinutes: 4,
59
+ source:
60
+ "Preeclampsia Foundation - 7 Signs and Symptoms Every Pregnant Woman Should Know",
61
+ sourceUrl:
62
+ "https://www.preeclampsia.org/health-information/signs-and-symptoms",
63
+ videoUrl:
64
+ "https://www.preeclampsia.org/video-library",
65
+ },
66
+ {
67
+ id: "nutrition-hypertension",
68
+ title: "Nutrition for Healthy Blood Pressure",
69
+ category: "NUTRITION",
70
+ summary: "Dietary tips to help manage blood pressure naturally.",
71
+ content: `Eating a balanced diet is good for you and your baby. To help support healthy blood pressure:
72
+
73
+ - Stay hydrated: Drink plenty of water throughout the day.
74
+ - Watch sodium intake: Try to limit processed foods which are often high in salt.
75
+ - Eat plenty of fruits and vegetables: These provide essential nutrients like potassium.
76
+ - Choose whole grains: Brown rice, oats, and whole wheat bread are great options.
77
+
78
+ Always talk to your doctor or a nutritionist before making major changes to your diet.`,
79
+ readTimeMinutes: 3,
80
+ source:
81
+ "American College of Obstetricians and Gynecologists (ACOG) – Nutrition During Pregnancy",
82
+ sourceUrl:
83
+ "https://www.acog.org/womens-health/faqs/nutrition-during-pregnancy",
84
+ videoUrl: undefined,
85
+ },
86
+ {
87
+ id: "postpartum-care",
88
+ title: "Taking Care After Birth",
89
+ category: "POSTPARTUM",
90
+ summary:
91
+ "Why monitoring your health remains important after your baby arrives.",
92
+ content: `The postpartum period (after birth) is a time of recovery. It is important to continue listening to your body.
93
+
94
+ Some conditions, like postpartum preeclampsia, can occur even after the baby is born. Continue to watch for severe headaches, vision changes, or shortness of breath.
95
+
96
+ Make sure to attend your postpartum check-ups so your provider can ensure you are healing well.`,
97
+ readTimeMinutes: 2,
98
+ source:
99
+ "American College of Obstetricians and Gynecologists (ACOG) – Postpartum Preeclampsia information",
100
+ sourceUrl:
101
+ "https://www.acog.org/womens-health/faqs/preeclampsia-and-high-blood-pressure-during-pregnancy",
102
+ videoUrl:
103
+ "https://www.preeclampsia.org/video-library",
104
+ },
105
+ {
106
+ id: "preeclampsia-eclampsia-overview",
107
+ title: "Preeclampsia and Eclampsia: What You Should Know",
108
+ category: "HYPERTENSION",
109
+ summary:
110
+ "Overview of causes, warning signs, and care for preeclampsia and eclampsia.",
111
+ content: `Preeclampsia is a blood pressure condition that usually develops after 20 weeks of pregnancy. It involves high blood pressure and signs that organs such as the kidneys or liver are under strain. If it is not treated, it can progress to eclampsia, which includes seizures. Early recognition and close medical care greatly reduce the chance of serious problems.
112
+
113
+ Diagnosis
114
+
115
+ - Blood pressure at or above 140/90 mmHg on more than one occasion
116
+ - Protein in the urine (proteinuria)
117
+ - Sometimes other changes in blood tests or organ function
118
+
119
+ Risk factors
120
+
121
+ - First pregnancy
122
+ - Multiple pregnancy (twins, triplets, etc.)
123
+ - History of high blood pressure, diabetes, kidney disease, or autoimmune disease
124
+ - Obesity
125
+ - Family history of preeclampsia
126
+
127
+ Warning symptoms
128
+
129
+ - Severe or persistent headache
130
+ - Changes in vision (blurred vision, flashing lights, or spots)
131
+ - Pain in the upper right side of the abdomen
132
+ - Nausea or vomiting after the first trimester
133
+ - Sudden swelling of the face, hands, or around the eyes
134
+ - Shortness of breath
135
+
136
+ Possible complications
137
+
138
+ - Seizures (eclampsia)
139
+ - HELLP syndrome (problems with the liver and blood clotting)
140
+ - Problems with the kidneys, liver, brain, or lungs
141
+ - Placental abruption (the placenta pulling away from the uterus)
142
+ - Restricted growth of the baby or early birth
143
+
144
+ Treatment and monitoring
145
+
146
+ - Close monitoring of blood pressure, urine, and blood tests
147
+ - Medications to lower blood pressure when needed
148
+ - Magnesium sulfate in some cases to help prevent seizures
149
+ - Corticosteroids to help the baby's lungs if early delivery is likely
150
+ - Timing of birth based on the health of you and your baby
151
+
152
+ After pregnancy
153
+
154
+ Preeclampsia can sometimes develop after birth (postpartum preeclampsia), usually within the first six weeks. Headache, vision changes, shortness of breath, or severe pain after delivery should be checked urgently.
155
+
156
+ Long-term health
157
+
158
+ Women who have had preeclampsia have a higher chance of high blood pressure, heart disease, and stroke later in life than women who did not. Regular follow-up with a primary care provider is important.
159
+
160
+ Prevention and self-care
161
+
162
+ - Attend all antenatal and postpartum visits
163
+ - Have blood pressure and urine checked as recommended
164
+ - Report warning symptoms promptly
165
+ - Follow advice on healthy eating, activity, and weight management when appropriate
166
+ - Take low-dose aspirin and calcium supplements if recommended by your care team
167
+
168
+ This summary is based on guidance from the World Health Organization and other international expert groups.`,
169
+ readTimeMinutes: 8,
170
+ source:
171
+ "World Health Organization (WHO) and international guidelines on preeclampsia and eclampsia",
172
+ sourceUrl:
173
+ "https://www.who.int/health-topics/hypertension#tab=tab_1",
174
+ videoUrl:
175
+ "https://www.msdmanuals.com/professional/multimedia/video/overview-of-preeclampsia-and-eclampsia?utm_source=chatgpt.com",
176
+ },
177
+ ];
src/education/education.controller.ts ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {
2
+ Controller,
3
+ Get,
4
+ Param,
5
+ Query,
6
+ UseGuards,
7
+ HttpCode,
8
+ HttpStatus,
9
+ } from "@nestjs/common";
10
+ import { EducationService } from "./education.service";
11
+ import { JwtAuthGuard } from "../auth/guards/jwt-auth.guard";
12
+
13
+ /**
14
+ * Education Controller
15
+ *
16
+ * ENDPOINTS:
17
+ * - GET /education - List all articles (optional category filter)
18
+ * - GET /education/:id - Get specific article
19
+ *
20
+ * RESTRICTIONS:
21
+ * - Authenticated users only
22
+ * - Read-only access
23
+ */
24
+ @Controller("education")
25
+ @UseGuards(JwtAuthGuard)
26
+ export class EducationController {
27
+ constructor(private readonly educationService: EducationService) {}
28
+
29
+ @Get()
30
+ @HttpCode(HttpStatus.OK)
31
+ findAll(@Query("category") category?: string) {
32
+ if (category) {
33
+ return this.educationService.findByCategory(category);
34
+ }
35
+ return this.educationService.findAll();
36
+ }
37
+
38
+ @Get(":id")
39
+ @HttpCode(HttpStatus.OK)
40
+ findOne(@Param("id") id: string) {
41
+ return this.educationService.findOne(id);
42
+ }
43
+ }
src/education/education.module.ts ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Module } from "@nestjs/common";
2
+ import { EducationController } from "./education.controller";
3
+ import { EducationService } from "./education.service";
4
+
5
+ /**
6
+ * Education Module
7
+ *
8
+ * RESPONSIBILITIES:
9
+ * CONSTRAINTS:
10
+ * - No personalization
11
+ * - No interpretation
12
+ * - Content delivery only
13
+ *
14
+ * To be implemented in STEP 10
15
+ */
16
+
17
+ @Module({
18
+ controllers: [EducationController],
19
+ providers: [EducationService],
20
+ exports: [EducationService],
21
+ })
22
+ export class EducationModule {}
src/education/education.service.ts ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Injectable, NotFoundException } from "@nestjs/common";
2
+ import { EDUCATION_CONTENT, EducationArticle } from "./data/education.content";
3
+
4
+ /**
5
+ * Education Service
6
+ *
7
+ * RESPONSIBILITIES:
8
+ * - Serve static, curated education content
9
+ * - Filter by category
10
+ * - Retrieve single articles
11
+ *
12
+ * CLINICAL SAFETY:
13
+ * - Read-only access to curated content
14
+ * - No dynamic user-generated content allowed
15
+ */
16
+ @Injectable()
17
+ export class EducationService {
18
+ /**
19
+ * Get all education articles
20
+ */
21
+ findAll(): EducationArticle[] {
22
+ return EDUCATION_CONTENT;
23
+ }
24
+
25
+ /**
26
+ * Get articles by category
27
+ */
28
+ findByCategory(category: string): EducationArticle[] {
29
+ return EDUCATION_CONTENT.filter((article) => article.category === category);
30
+ }
31
+
32
+ /**
33
+ * Get single article by ID
34
+ */
35
+ findOne(id: string): EducationArticle {
36
+ const article = EDUCATION_CONTENT.find((a) => a.id === id);
37
+
38
+ if (!article) {
39
+ throw new NotFoundException("Article not found");
40
+ }
41
+
42
+ return article;
43
+ }
44
+ }
src/health.controller.ts ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Controller, Get } from '@nestjs/common';
2
+ import { ApiTags, ApiOperation } from '@nestjs/swagger';
3
+
4
+ @ApiTags('Health')
5
+ @Controller('health')
6
+ export class HealthController {
7
+ @Get()
8
+ @ApiOperation({ summary: 'Check API health and uptime' })
9
+ check() {
10
+ return {
11
+ status: 'ok',
12
+ timestamp: new Date().toISOString(),
13
+ uptime: process.uptime(),
14
+ service: 'MaternAlert Backend',
15
+ version: '1.0.0',
16
+ };
17
+ }
18
+ }
src/main.ts ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NestFactory } from "@nestjs/core";
2
+ import { AppModule } from "./app.module";
3
+ import { setupGlobalMiddleware } from "./setup";
4
+
5
+ async function bootstrap() {
6
+ const app = await NestFactory.create(AppModule);
7
+
8
+ setupGlobalMiddleware(app);
9
+
10
+ const port = process.env.PORT || 3000;
11
+ await app.listen(port, '0.0.0.0');
12
+
13
+ console.log(`\n🏥 Maternal Health Support Backend`);
14
+ console.log(`⚠️ CARE-SUPPORT TOOL - NOT A DIAGNOSTIC SYSTEM`);
15
+ console.log(`🚀 Server running on: http://localhost:${port}/api/v1`);
16
+ console.log(`📡 Network access: http://10.120.165.24:${port}/api/v1\n`);
17
+ }
18
+
19
+ bootstrap();
src/notifications/dto/register-push-token.dto.ts ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ import { IsNotEmpty, IsString } from "class-validator";
2
+
3
+ export class RegisterPushTokenDto {
4
+ @IsNotEmpty()
5
+ @IsString()
6
+ token!: string;
7
+ }
src/notifications/email.service.ts ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Injectable, Logger } from '@nestjs/common';
2
+ import { ConfigService } from '@nestjs/config';
3
+ import * as nodemailer from 'nodemailer';
4
+
5
+ @Injectable()
6
+ export class EmailService {
7
+ private readonly logger = new Logger(EmailService.name);
8
+ private transporter: nodemailer.Transporter;
9
+
10
+ constructor(private readonly configService: ConfigService) {
11
+ this.transporter = nodemailer.createTransport({
12
+ host: this.configService.get('SMTP_HOST'),
13
+ port: this.configService.get('SMTP_PORT'),
14
+ secure: false, // true for 465, false for other ports
15
+ auth: {
16
+ user: this.configService.get('SMTP_USER'),
17
+ pass: this.configService.get('SMTP_PASS'),
18
+ },
19
+ });
20
+ }
21
+
22
+ async sendEmail(to: string, subject: string, text: string, html?: string): Promise<boolean> {
23
+ try {
24
+ const from = this.configService.get('SMTP_FROM');
25
+
26
+ const info = await this.transporter.sendMail({
27
+ from,
28
+ to,
29
+ subject,
30
+ text,
31
+ html: html || text,
32
+ });
33
+
34
+ this.logger.log(`Email sent successfully: ${info.messageId} to ${to}`);
35
+ return true;
36
+ } catch (error: any) {
37
+ this.logger.error(`Failed to send email to ${to}: ${error.message}`, error.stack);
38
+ return false;
39
+ }
40
+ }
41
+ }
src/notifications/notifications.controller.ts ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Controller, Get, Patch, Post, Param, Body, UseGuards, Request } from "@nestjs/common";
2
+ import { NotificationsService } from "./notifications.service";
3
+ import { JwtAuthGuard } from "../auth/guards/jwt-auth.guard";
4
+ import { RegisterPushTokenDto } from "./dto/register-push-token.dto";
5
+
6
+ @Controller("notifications")
7
+ @UseGuards(JwtAuthGuard)
8
+ export class NotificationsController {
9
+ constructor(private readonly notificationsService: NotificationsService) {}
10
+
11
+ @Get()
12
+ async getUserNotifications(@Request() req: any) {
13
+ return this.notificationsService.getNotificationHistory(req.user.id);
14
+ }
15
+
16
+ @Post("register")
17
+ async registerPushToken(@Request() req: any, @Body() dto: RegisterPushTokenDto) {
18
+ return this.notificationsService.registerPushToken(req.user.id, dto.token);
19
+ }
20
+
21
+ @Patch(":id/read")
22
+ async markAsRead(@Param("id") id: string, @Request() req: any) {
23
+ return this.notificationsService.markAsRead(id, req.user.id);
24
+ }
25
+ }
src/notifications/notifications.module.ts ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Module } from "@nestjs/common";
2
+ import { NotificationsService } from "./notifications.service";
3
+ import { NotificationsController } from "./notifications.controller";
4
+ import { EmailService } from "./email.service";
5
+
6
+ /**
7
+ * Notifications Module
8
+ *
9
+ * RESPONSIBILITIES:
10
+ * - Send care escalation alerts
11
+ * - Template-based messaging only
12
+ * - No dynamic medical text generation
13
+ *
14
+ * CLINICAL SAFETY CONSTRAINTS:
15
+ * - No fear-based language
16
+ * - No predictions
17
+ * - Predefined templates only
18
+ * - Clear, actionable guidance
19
+ *
20
+ * DELIVERY:
21
+ * - Currently stub implementation (console logging)
22
+ * - Ready for integration with email/SMS services
23
+ * - Examples: SendGrid, Twilio, AWS SNS
24
+ */
25
+
26
+ @Module({
27
+ controllers: [NotificationsController],
28
+ providers: [NotificationsService, EmailService],
29
+ exports: [NotificationsService, EmailService],
30
+ })
31
+ export class NotificationsModule {}