evaldoc 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.staging.example +19 -0
- package/.firebaserc +6 -0
- package/.github/workflows/auto-merge-to-main.yml +28 -0
- package/.github/workflows/ci.yml +27 -0
- package/.github/workflows/npxhub-publish.yml +170 -0
- package/.prettierignore +4 -0
- package/.prettierrc.json +7 -0
- package/README.md +142 -0
- package/SRS.md +448 -0
- package/apphosting.staging.yaml +48 -0
- package/apphosting.yaml +50 -0
- package/bin/evaldoc.cjs +105 -0
- package/cors.json +8 -0
- package/docs/ARCHITECTURE.md +769 -0
- package/docs/ARCHITECTURE_SUMMARY.md +323 -0
- package/docs/DEPLOYMENT.md +86 -0
- package/docs/DEPLOYMENT_STEPS.md +373 -0
- package/docs/DEPLOY_FIRESTORE_RULES.md +239 -0
- package/docs/DEPLOY_STORAGE_RULES.md +82 -0
- package/docs/DOCTOR_TUTORIAL.md +467 -0
- package/docs/FIREBASE_STORAGE_CORS_FIX.md +131 -0
- package/docs/FIXES_APPLIED.md +128 -0
- package/docs/IMPLEMENTATION.md +498 -0
- package/docs/IMPLEMENTATION_COMPLETE.md +317 -0
- package/docs/LLM-SETUP.md +216 -0
- package/docs/MANUAL_TEST_GUIDE.md +327 -0
- package/docs/PATIENT_GUIDE.md +451 -0
- package/docs/PLATFORM_OVERVIEW.md +194 -0
- package/docs/QUICKSTART.md +252 -0
- package/docs/QUICK_REFERENCE.md +222 -0
- package/docs/READY_TO_TEST.md +383 -0
- package/docs/RUN.md +237 -0
- package/docs/START_TESTING.md +130 -0
- package/docs/STRIPE_CONNECT_SETUP.md +454 -0
- package/docs/SYSTEM_READY.md +345 -0
- package/docs/THREADS_IMPLEMENTATION_COMPLETE.md +431 -0
- package/docs/THREAD_IMPLEMENTATION_GUIDE.md +345 -0
- package/docs/firebasefiles.md +21 -0
- package/docs/homepage.md +1208 -0
- package/docs/payments.md +159 -0
- package/eslint.config.js +76 -0
- package/firebase.json +27 -0
- package/firestore.indexes.json +88 -0
- package/firestore.rules +125 -0
- package/functions/package-lock.json +2884 -0
- package/functions/package.json +20 -0
- package/functions/src/analytics.js +101 -0
- package/functions/src/audit.js +28 -0
- package/functions/src/auth.js +19 -0
- package/functions/src/drafts.js +128 -0
- package/functions/src/feedback.js +164 -0
- package/functions/src/firebase-init.js +29 -0
- package/functions/src/i18n.js +134 -0
- package/functions/src/limits.js +27 -0
- package/functions/src/llm.js +271 -0
- package/functions/src/ner.js +162 -0
- package/functions/src/notifications.js +273 -0
- package/functions/src/orgs.js +127 -0
- package/functions/src/patient-api.js +545 -0
- package/functions/src/payment.js +830 -0
- package/functions/src/safety.js +188 -0
- package/functions/src/stripe-config.js +33 -0
- package/functions/src/topics.js +94 -0
- package/functions/src/triage.js +191 -0
- package/functions/src/utils.js +28 -0
- package/functions/src/validate.js +177 -0
- package/functions/test-firebase.js +36 -0
- package/jest.config.js +18 -0
- package/package.json +52 -0
- package/public/404.html +93 -0
- package/public/app/analytics.html +336 -0
- package/public/app/analytics.js +177 -0
- package/public/app/app.css +1400 -0
- package/public/app/app.js +1754 -0
- package/public/app/config.js +14 -0
- package/public/app/inbox-threads.js +843 -0
- package/public/app/inbox.html +598 -0
- package/public/app/inbox.js +397 -0
- package/public/app/index.html +612 -0
- package/public/assets/logo.jpg +0 -0
- package/public/chat.html +137 -0
- package/public/css/chat.css +870 -0
- package/public/css/landing-enhance.css +298 -0
- package/public/css/styles.css +1198 -0
- package/public/favicon.svg +5 -0
- package/public/find-doctor.html +220 -0
- package/public/index.html +399 -0
- package/public/js/chat-threads.js +513 -0
- package/public/js/chat.js +832 -0
- package/public/js/search.js +41 -0
- package/public/privacy.html +101 -0
- package/public/robots.txt +6 -0
- package/public/site.webmanifest +12 -0
- package/public/terms.html +110 -0
- package/scripts/create-test-provider.js +55 -0
- package/scripts/deploy-staging.sh +51 -0
- package/scripts/fix-provider.js +39 -0
- package/scripts/quick-test.sh +37 -0
- package/scripts/test-chat.sh +54 -0
- package/scripts/test-multi-turn.js +137 -0
- package/scripts/test-ner-simple.js +28 -0
- package/scripts/test-ner.js +60 -0
- package/scripts/test-stripe-connect.js +304 -0
- package/scripts/test-webhook-secret.sh +24 -0
- package/server.js +3864 -0
- package/storage.rules +22 -0
- package/test-api.json +1 -0
- package/tests/emulator/README.md +38 -0
- package/tests/emulator/counters.emulator.test.js +81 -0
- package/tests/integration/api-account-selfserve.test.js +312 -0
- package/tests/integration/api-admin-digest.test.js +202 -0
- package/tests/integration/api-chat-satisfaction.test.js +259 -0
- package/tests/integration/api-doctor-drafts.test.js +236 -0
- package/tests/integration/api-doctor-inbox.test.js +401 -0
- package/tests/integration/api-org.test.js +328 -0
- package/tests/integration/api-payment.test.js +664 -0
- package/tests/integration/api-provider-settings.test.js +475 -0
- package/tests/integration/api-qr.test.js +121 -0
- package/tests/integration/api-security.test.js +241 -0
- package/tests/integration/api-stripe-search.test.js +303 -0
- package/tests/integration/api-webhook-deletion.test.js +231 -0
- package/tests/mocks/firebase-admin.js +220 -0
- package/tests/unit/analytics.test.js +151 -0
- package/tests/unit/audit-bugs.test.js +203 -0
- package/tests/unit/chat-ui-logic.test.js +283 -0
- package/tests/unit/drafts.test.js +104 -0
- package/tests/unit/feedback.test.js +148 -0
- package/tests/unit/i18n.test.js +229 -0
- package/tests/unit/llm-retry.test.js +122 -0
- package/tests/unit/notifications.test.js +155 -0
- package/tests/unit/orgs.test.js +328 -0
- package/tests/unit/payment.test.js +889 -0
- package/tests/unit/safety-edge-cases.test.js +509 -0
- package/tests/unit/safety-srs-compliance.test.js +252 -0
- package/tests/unit/safety.test.js +291 -0
- package/tests/unit/server-helpers.test.js +774 -0
- package/tests/unit/topics-limits.test.js +158 -0
- package/tests/unit/triage.test.js +152 -0
- package/tests/unit/validate.test.js +408 -0
package/docs/payments.md
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
You are a senior full-stack engineer. Implement Stripe payments for EvalDoc (a healthcare AI link platform) using Node.js + Firebase (Firestore + Auth) + Stripe.
|
|
2
|
+
|
|
3
|
+
Goal:
|
|
4
|
+
- Doctors set their own subscription prices (daily/weekly/monthly).
|
|
5
|
+
- Patients must pay before accessing the doctor’s AI session.
|
|
6
|
+
- Platform (EvalDoc) takes 25% of the doctor’s fee.
|
|
7
|
+
- Doctor receives 75% via Stripe Connect Express payouts.
|
|
8
|
+
- Access is controlled strictly by Stripe webhook events (not by client redirects).
|
|
9
|
+
|
|
10
|
+
Tech stack:
|
|
11
|
+
- Backend: Node.js (Express) deployed on Firebase Cloud Functions (or Cloud Run if you prefer).
|
|
12
|
+
- DB: Firestore.
|
|
13
|
+
- Auth: Firebase Auth.
|
|
14
|
+
- Payments: Stripe Checkout + Stripe Connect (Express).
|
|
15
|
+
- Use Stripe test mode for development, environment variables for keys.
|
|
16
|
+
|
|
17
|
+
Deliverables:
|
|
18
|
+
1) A working backend with endpoints to:
|
|
19
|
+
A. Onboard a doctor to Stripe Connect Express.
|
|
20
|
+
B. Create/update doctor-specific Products + Prices (daily/weekly/monthly).
|
|
21
|
+
C. Create a patient Stripe Customer if needed.
|
|
22
|
+
D. Create a Stripe Checkout Session (subscription) for a patient buying a doctor plan, charging on behalf of the doctor with a 25% application fee.
|
|
23
|
+
E. Webhook endpoint to process Stripe events and update Firestore access.
|
|
24
|
+
2) Firestore data model for doctors, patients, prices, subscriptions, and access.
|
|
25
|
+
3) Minimal client-side flow description (no full UI needed, just required calls and checks).
|
|
26
|
+
|
|
27
|
+
IMPORTANT BUSINESS LOGIC:
|
|
28
|
+
- A patient can only access a doctor AI session if Firestore shows access.status == "ACTIVE" for that doctor + patient, based on Stripe webhooks.
|
|
29
|
+
- A doctor must complete Stripe onboarding (charges_enabled / payouts_enabled) before patients can purchase.
|
|
30
|
+
- Platform fee: 25% of the subscription amount.
|
|
31
|
+
- Use destination charges / connected account routing so money flows to doctor and platform gets its fee.
|
|
32
|
+
|
|
33
|
+
========================
|
|
34
|
+
FIRESTORE MODEL
|
|
35
|
+
========================
|
|
36
|
+
Create collections:
|
|
37
|
+
|
|
38
|
+
doctors/{doctorId}
|
|
39
|
+
- displayName
|
|
40
|
+
- stripeAccountId (string)
|
|
41
|
+
- stripeOnboardingStatus: "NOT_STARTED" | "PENDING" | "COMPLETE"
|
|
42
|
+
- chargesEnabled (bool)
|
|
43
|
+
- payoutsEnabled (bool)
|
|
44
|
+
- stripeProductId (string)
|
|
45
|
+
- stripePriceIds: { day?: string, week?: string, month?: string }
|
|
46
|
+
- currency (e.g., "usd")
|
|
47
|
+
- createdAt, updatedAt
|
|
48
|
+
|
|
49
|
+
patients/{patientId}
|
|
50
|
+
- stripeCustomerId (string)
|
|
51
|
+
- createdAt, updatedAt
|
|
52
|
+
|
|
53
|
+
subscriptions/{subId}
|
|
54
|
+
- doctorId
|
|
55
|
+
- patientId
|
|
56
|
+
- stripeSubscriptionId
|
|
57
|
+
- stripeCustomerId
|
|
58
|
+
- stripePriceId
|
|
59
|
+
- status (active, past_due, canceled, unpaid, etc.)
|
|
60
|
+
- currentPeriodEnd (timestamp)
|
|
61
|
+
- createdAt, updatedAt
|
|
62
|
+
|
|
63
|
+
access/{doctorId_patientId}
|
|
64
|
+
- doctorId
|
|
65
|
+
- patientId
|
|
66
|
+
- status: "ACTIVE" | "INACTIVE"
|
|
67
|
+
- stripeSubscriptionId
|
|
68
|
+
- currentPeriodEnd (timestamp)
|
|
69
|
+
- updatedAt
|
|
70
|
+
|
|
71
|
+
========================
|
|
72
|
+
ENDPOINTS TO IMPLEMENT
|
|
73
|
+
========================
|
|
74
|
+
|
|
75
|
+
1) POST /stripe/doctor/create-connect-account
|
|
76
|
+
Auth: doctor must be logged in (Firebase Auth).
|
|
77
|
+
- Create Stripe Connect Express account for the doctor if not exists.
|
|
78
|
+
- Save stripeAccountId to doctors/{doctorId}.
|
|
79
|
+
- Return accountId.
|
|
80
|
+
|
|
81
|
+
2) POST /stripe/doctor/create-account-link
|
|
82
|
+
Auth: doctor logged in.
|
|
83
|
+
- Create an Account Link (Stripe-hosted onboarding) using doctor.stripeAccountId
|
|
84
|
+
- Requires refresh_url + return_url from env
|
|
85
|
+
- Return the url for the doctor to complete onboarding.
|
|
86
|
+
|
|
87
|
+
3) POST /stripe/doctor/set-pricing
|
|
88
|
+
Auth: doctor logged in.
|
|
89
|
+
Input: currency, amounts for intervals (day/week/month), enabledIntervals list.
|
|
90
|
+
- Validate min amounts, currency format, etc.
|
|
91
|
+
- Ensure doctor has stripeAccountId and (optionally) charges_enabled/payouts_enabled or allow pricing setup before completion.
|
|
92
|
+
- Create a Stripe Product for the doctor if not exists (name: "EvalDoc AI Access - Dr. {displayName}")
|
|
93
|
+
- For each enabled interval, create (or replace) Stripe Price objects:
|
|
94
|
+
- recurring.interval = "day" | "week" | "month"
|
|
95
|
+
- unit_amount in cents
|
|
96
|
+
- Save stripeProductId and stripePriceIds.* in Firestore.
|
|
97
|
+
|
|
98
|
+
4) POST /stripe/patient/create-checkout-session
|
|
99
|
+
Auth: patient logged in.
|
|
100
|
+
Input: doctorId, interval ("day"/"week"/"month")
|
|
101
|
+
- Load doctor document; require:
|
|
102
|
+
- stripeAccountId exists
|
|
103
|
+
- chargesEnabled == true (or at least doctor onboarding complete)
|
|
104
|
+
- selected priceId exists
|
|
105
|
+
- Ensure patient has stripeCustomerId (create stripe customer if missing).
|
|
106
|
+
- Create a Stripe Checkout Session in mode="subscription", line_items=[{price: doctorPriceId, quantity:1}], customer=patientCustomerId.
|
|
107
|
+
- IMPORTANT: route funds to doctor + take 25% platform fee.
|
|
108
|
+
- Use Stripe Connect approach for subscriptions:
|
|
109
|
+
- Use application_fee_percent = 25 (or application_fee_amount if you compute fixed cents)
|
|
110
|
+
- Ensure the payment is made for the connected account (destination / transfer settings).
|
|
111
|
+
- If there are multiple Stripe ways, choose the correct one for subscriptions with Connect and document it in comments.
|
|
112
|
+
- Provide success_url and cancel_url including session_id.
|
|
113
|
+
- Return checkout session url.
|
|
114
|
+
|
|
115
|
+
5) POST /stripe/webhook
|
|
116
|
+
No auth; verify Stripe signature using STRIPE_WEBHOOK_SECRET.
|
|
117
|
+
- Handle these events at minimum:
|
|
118
|
+
- checkout.session.completed
|
|
119
|
+
- customer.subscription.created
|
|
120
|
+
- customer.subscription.updated
|
|
121
|
+
- customer.subscription.deleted
|
|
122
|
+
- invoice.payment_succeeded
|
|
123
|
+
- invoice.payment_failed
|
|
124
|
+
- For each relevant event, resolve doctorId + patientId:
|
|
125
|
+
- Store doctorId and patientId as metadata when creating the Checkout Session (recommended).
|
|
126
|
+
- Update Firestore:
|
|
127
|
+
- subscriptions/{subId} upsert by stripeSubscriptionId
|
|
128
|
+
- access/{doctorId_patientId} set ACTIVE if subscription is active and paid; set INACTIVE on canceled/unpaid/past_due/payment_failed depending on policy.
|
|
129
|
+
- Save currentPeriodEnd from Stripe subscription.
|
|
130
|
+
|
|
131
|
+
========================
|
|
132
|
+
ACCESS POLICY
|
|
133
|
+
========================
|
|
134
|
+
- ACTIVE only when subscription.status == "active" and latest invoice is paid (or invoice.payment_succeeded).
|
|
135
|
+
- INACTIVE on:
|
|
136
|
+
- invoice.payment_failed (immediately) OR after a grace period (choose and implement one; default immediate for now).
|
|
137
|
+
- subscription canceled/deleted.
|
|
138
|
+
|
|
139
|
+
========================
|
|
140
|
+
SECURITY REQUIREMENTS
|
|
141
|
+
========================
|
|
142
|
+
- All doctor endpoints require Firebase Auth and ensure role=doctor (use a custom claim or doctors collection check).
|
|
143
|
+
- Patient endpoint requires Firebase Auth and ensure role=patient.
|
|
144
|
+
- Prevent patients from purchasing if doctor onboarding incomplete.
|
|
145
|
+
- Never trust client to mark access active; only webhooks do that.
|
|
146
|
+
- Add idempotency in webhook handling (avoid duplicate writes; use stripe event id as processed marker, or upsert by subscription id).
|
|
147
|
+
|
|
148
|
+
========================
|
|
149
|
+
OUTPUT FORMAT
|
|
150
|
+
========================
|
|
151
|
+
- Provide:
|
|
152
|
+
1) Folder/file structure.
|
|
153
|
+
2) Environment variables list.
|
|
154
|
+
3) Implementation code (backend) with clear comments.
|
|
155
|
+
4) Example Firestore documents.
|
|
156
|
+
5) Example requests/responses.
|
|
157
|
+
6) A short “How to test in Stripe test mode” checklist.
|
|
158
|
+
|
|
159
|
+
Proceed now. Make reasonable assumptions. If multiple Stripe Connect approaches exist for subscriptions, pick one that works and explain why in code comments.
|
package/eslint.config.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// ESLint 9 flat config for a Node.js CommonJS (Express) project.
|
|
4
|
+
//
|
|
5
|
+
// Node + Jest globals are declared inline so this config has no external
|
|
6
|
+
// dependencies at parse time (works even before `npm install`). If you prefer,
|
|
7
|
+
// you can replace the inline maps below with the `globals` package:
|
|
8
|
+
// const globals = require('globals');
|
|
9
|
+
// ...globals.node, ...globals.jest
|
|
10
|
+
|
|
11
|
+
const nodeGlobals = {
|
|
12
|
+
process: 'readonly',
|
|
13
|
+
__dirname: 'readonly',
|
|
14
|
+
__filename: 'readonly',
|
|
15
|
+
module: 'writable',
|
|
16
|
+
require: 'readonly',
|
|
17
|
+
exports: 'writable',
|
|
18
|
+
console: 'readonly',
|
|
19
|
+
Buffer: 'readonly',
|
|
20
|
+
global: 'readonly',
|
|
21
|
+
globalThis: 'readonly',
|
|
22
|
+
setTimeout: 'readonly',
|
|
23
|
+
clearTimeout: 'readonly',
|
|
24
|
+
setInterval: 'readonly',
|
|
25
|
+
clearInterval: 'readonly',
|
|
26
|
+
setImmediate: 'readonly',
|
|
27
|
+
clearImmediate: 'readonly',
|
|
28
|
+
queueMicrotask: 'readonly',
|
|
29
|
+
URL: 'readonly',
|
|
30
|
+
URLSearchParams: 'readonly',
|
|
31
|
+
TextEncoder: 'readonly',
|
|
32
|
+
TextDecoder: 'readonly',
|
|
33
|
+
AbortController: 'readonly',
|
|
34
|
+
fetch: 'readonly',
|
|
35
|
+
structuredClone: 'readonly'
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const jestGlobals = {
|
|
39
|
+
describe: 'readonly',
|
|
40
|
+
it: 'readonly',
|
|
41
|
+
test: 'readonly',
|
|
42
|
+
expect: 'readonly',
|
|
43
|
+
beforeAll: 'readonly',
|
|
44
|
+
afterAll: 'readonly',
|
|
45
|
+
beforeEach: 'readonly',
|
|
46
|
+
afterEach: 'readonly',
|
|
47
|
+
jest: 'readonly',
|
|
48
|
+
xdescribe: 'readonly',
|
|
49
|
+
xit: 'readonly',
|
|
50
|
+
xtest: 'readonly',
|
|
51
|
+
fdescribe: 'readonly',
|
|
52
|
+
fit: 'readonly'
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
module.exports = [
|
|
56
|
+
{
|
|
57
|
+
ignores: ['node_modules/**', 'coverage/**', 'public/vendor/**', 'functions/node_modules/**']
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
files: ['**/*.js'],
|
|
61
|
+
languageOptions: {
|
|
62
|
+
ecmaVersion: 2022,
|
|
63
|
+
sourceType: 'commonjs',
|
|
64
|
+
globals: {
|
|
65
|
+
...nodeGlobals,
|
|
66
|
+
...jestGlobals
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
rules: {
|
|
70
|
+
'no-unused-vars': 'warn',
|
|
71
|
+
'no-undef': 'error',
|
|
72
|
+
eqeqeq: 'warn',
|
|
73
|
+
'no-console': 'off'
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
];
|
package/firebase.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hosting": {
|
|
3
|
+
"public": "public",
|
|
4
|
+
"ignore": [
|
|
5
|
+
"firebase.json",
|
|
6
|
+
"**/.*",
|
|
7
|
+
"**/node_modules/**"
|
|
8
|
+
],
|
|
9
|
+
"rewrites": [
|
|
10
|
+
{
|
|
11
|
+
"source": "/app/**",
|
|
12
|
+
"destination": "/app/index.html"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"source": "**",
|
|
16
|
+
"destination": "/index.html"
|
|
17
|
+
}
|
|
18
|
+
]
|
|
19
|
+
},
|
|
20
|
+
"firestore": {
|
|
21
|
+
"rules": "firestore.rules",
|
|
22
|
+
"indexes": "firestore.indexes.json"
|
|
23
|
+
},
|
|
24
|
+
"storage": {
|
|
25
|
+
"rules": "storage.rules"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
{
|
|
2
|
+
"indexes": [
|
|
3
|
+
{
|
|
4
|
+
"collectionGroup": "conversations",
|
|
5
|
+
"queryScope": "COLLECTION",
|
|
6
|
+
"fields": [
|
|
7
|
+
{ "fieldPath": "providerId", "order": "ASCENDING" },
|
|
8
|
+
{ "fieldPath": "sessionId", "order": "ASCENDING" },
|
|
9
|
+
{ "fieldPath": "createdAt", "order": "DESCENDING" }
|
|
10
|
+
]
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"collectionGroup": "conversations",
|
|
14
|
+
"queryScope": "COLLECTION",
|
|
15
|
+
"fields": [
|
|
16
|
+
{ "fieldPath": "providerId", "order": "ASCENDING" },
|
|
17
|
+
{ "fieldPath": "updatedAt", "order": "DESCENDING" }
|
|
18
|
+
]
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"collectionGroup": "threads",
|
|
22
|
+
"queryScope": "COLLECTION",
|
|
23
|
+
"fields": [
|
|
24
|
+
{ "fieldPath": "doctorId", "order": "ASCENDING" },
|
|
25
|
+
{ "fieldPath": "lastMessageAt", "order": "DESCENDING" }
|
|
26
|
+
]
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"collectionGroup": "threads",
|
|
30
|
+
"queryScope": "COLLECTION",
|
|
31
|
+
"fields": [
|
|
32
|
+
{ "fieldPath": "doctorId", "order": "ASCENDING" },
|
|
33
|
+
{ "fieldPath": "status", "order": "ASCENDING" },
|
|
34
|
+
{ "fieldPath": "lastMessageAt", "order": "DESCENDING" }
|
|
35
|
+
]
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"collectionGroup": "threads",
|
|
39
|
+
"queryScope": "COLLECTION",
|
|
40
|
+
"fields": [
|
|
41
|
+
{ "fieldPath": "doctorId", "order": "ASCENDING" },
|
|
42
|
+
{ "fieldPath": "triageCategory", "order": "ASCENDING" },
|
|
43
|
+
{ "fieldPath": "lastMessageAt", "order": "DESCENDING" }
|
|
44
|
+
]
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"collectionGroup": "threads",
|
|
48
|
+
"queryScope": "COLLECTION",
|
|
49
|
+
"fields": [
|
|
50
|
+
{ "fieldPath": "patientId", "order": "ASCENDING" },
|
|
51
|
+
{ "fieldPath": "lastMessageAt", "order": "DESCENDING" }
|
|
52
|
+
]
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"collectionGroup": "notifications",
|
|
56
|
+
"queryScope": "COLLECTION",
|
|
57
|
+
"fields": [
|
|
58
|
+
{ "fieldPath": "doctorId", "order": "ASCENDING" },
|
|
59
|
+
{ "fieldPath": "createdAt", "order": "DESCENDING" }
|
|
60
|
+
]
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"collectionGroup": "notifications",
|
|
64
|
+
"queryScope": "COLLECTION",
|
|
65
|
+
"fields": [
|
|
66
|
+
{ "fieldPath": "doctorId", "order": "ASCENDING" },
|
|
67
|
+
{ "fieldPath": "read", "order": "ASCENDING" }
|
|
68
|
+
]
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"collectionGroup": "doctorExamples",
|
|
72
|
+
"queryScope": "COLLECTION",
|
|
73
|
+
"fields": [
|
|
74
|
+
{ "fieldPath": "topic", "order": "ASCENDING" },
|
|
75
|
+
{ "fieldPath": "createdAt", "order": "DESCENDING" }
|
|
76
|
+
]
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"collectionGroup": "waitlist",
|
|
80
|
+
"queryScope": "COLLECTION",
|
|
81
|
+
"fields": [
|
|
82
|
+
{ "fieldPath": "email", "order": "ASCENDING" },
|
|
83
|
+
{ "fieldPath": "approved", "order": "ASCENDING" }
|
|
84
|
+
]
|
|
85
|
+
}
|
|
86
|
+
],
|
|
87
|
+
"fieldOverrides": []
|
|
88
|
+
}
|
package/firestore.rules
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
rules_version = '2';
|
|
2
|
+
service cloud.firestore {
|
|
3
|
+
match /databases/{database}/documents {
|
|
4
|
+
function isSignedIn() {
|
|
5
|
+
return request.auth != null;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function isOwner(ownerUid) {
|
|
9
|
+
return isSignedIn() && request.auth.uid == ownerUid;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Provider documents
|
|
13
|
+
match /providers/{providerId} {
|
|
14
|
+
// Public: Anyone can read published providers
|
|
15
|
+
allow read: if resource.data.status == "published";
|
|
16
|
+
|
|
17
|
+
// Providers can only be created via backend API (Admin SDK)
|
|
18
|
+
allow create: if false;
|
|
19
|
+
|
|
20
|
+
// Providers can update their own profile
|
|
21
|
+
allow update: if isOwner(resource.data.ownerUid);
|
|
22
|
+
|
|
23
|
+
// Only backend can delete
|
|
24
|
+
allow delete: if false;
|
|
25
|
+
|
|
26
|
+
// Analytics subcollection
|
|
27
|
+
match /analyticsDaily/{dayId} {
|
|
28
|
+
allow read: if isOwner(providerId);
|
|
29
|
+
allow write: if false;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Doctor examples subcollection
|
|
33
|
+
match /doctorExamples/{exampleId} {
|
|
34
|
+
allow read: if isSignedIn() && request.auth.uid == get(/databases/$(database)/documents/providers/$(providerId)).data.ownerUid;
|
|
35
|
+
allow write: if false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Handle lookup
|
|
40
|
+
match /handles/{handle} {
|
|
41
|
+
allow read: if true;
|
|
42
|
+
allow write: if false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Conversations (legacy)
|
|
46
|
+
match /conversations/{conversationId} {
|
|
47
|
+
allow read, write: if false;
|
|
48
|
+
|
|
49
|
+
match /messages/{messageId} {
|
|
50
|
+
allow read, write: if false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Safety events
|
|
55
|
+
match /safetyEvents/{eventId} {
|
|
56
|
+
allow read, write: if false;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Audit log — backend-only, no client access
|
|
60
|
+
match /auditLog/{logId} {
|
|
61
|
+
allow read, write: if false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Platform configuration
|
|
65
|
+
match /platformConfig/{docId} {
|
|
66
|
+
allow read: if true;
|
|
67
|
+
allow write: if false;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Payment access records: patients can read their own access status
|
|
71
|
+
match /access/{accessId} {
|
|
72
|
+
allow read: if isSignedIn() && resource.data.patientId == request.auth.uid;
|
|
73
|
+
allow write: if false;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Subscriptions: patients can read their own
|
|
77
|
+
match /subscriptions/{subscriptionId} {
|
|
78
|
+
allow read: if isSignedIn() && resource.data.patientId == request.auth.uid;
|
|
79
|
+
allow write: if false;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Transactions: patients can read their own
|
|
83
|
+
match /transactions/{transactionId} {
|
|
84
|
+
allow read: if isSignedIn() && resource.data.patientId == request.auth.uid;
|
|
85
|
+
allow write: if false;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Patients collection
|
|
89
|
+
match /patients/{patientId} {
|
|
90
|
+
allow read, update: if isSignedIn() && request.auth.uid == patientId;
|
|
91
|
+
allow create, delete: if false;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Notifications: doctors read their own
|
|
95
|
+
match /notifications/{notificationId} {
|
|
96
|
+
allow read: if isSignedIn() && resource.data.doctorId == request.auth.uid;
|
|
97
|
+
allow write: if false;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Threads
|
|
101
|
+
match /threads/{threadId} {
|
|
102
|
+
allow read: if isSignedIn() && resource.data.patientId == request.auth.uid;
|
|
103
|
+
allow read: if isSignedIn() && request.auth.token.doctorId == resource.data.doctorId;
|
|
104
|
+
allow write: if false;
|
|
105
|
+
|
|
106
|
+
match /messages/{messageId} {
|
|
107
|
+
allow read: if isSignedIn() && (
|
|
108
|
+
get(/databases/$(database)/documents/threads/$(threadId)).data.patientId == request.auth.uid ||
|
|
109
|
+
request.auth.token.doctorId == get(/databases/$(database)/documents/threads/$(threadId)).data.doctorId
|
|
110
|
+
);
|
|
111
|
+
allow write: if false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Waitlist — all access via server-side Admin SDK only
|
|
116
|
+
match /waitlist/{docId} {
|
|
117
|
+
allow read, write: if false;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Deny all other documents by default
|
|
121
|
+
match /{document=**} {
|
|
122
|
+
allow read, write: if false;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|