heartraite 1.0.193 → 1.0.195

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.
@@ -7,13 +7,23 @@ import { ConsultationInterestStatus } from "../enum";
7
7
  * (`Consultation`) so subsequent professionals can land here
8
8
  * without a schema rename.
9
9
  *
10
- * One pending record per user at a time — the
11
- * `/consultation/express-interest` endpoint upserts rather than
12
- * appending so resubmits don't duplicate.
10
+ * `expertId` scopes the record to a specific professional. v1 has
11
+ * exactly one value (`"bohm"`); future additions (other
12
+ * psychologists, therapists, sex therapists, financial planners,
13
+ * etc.) plug in without a schema migration. Idempotency at the
14
+ * backend is `(userId, expertId)` — a user can express interest in
15
+ * multiple experts independently, but only one pending record per
16
+ * (user, expert) pair at any time.
17
+ *
18
+ * The slug format (e.g. `"bohm"`) reads cleanly in logs / admin
19
+ * dashboards. Future v2 may move expert metadata (name, photo,
20
+ * pricing, credentials) into DatoCMS and key entries by their CMS
21
+ * slug — the runtime contract stays the same string.
13
22
  */
14
23
  export type ConsultationInterest = {
15
24
  id: string;
16
25
  userId: string;
26
+ expertId: string;
17
27
  submittedAt: string;
18
28
  status: ConsultationInterestStatus;
19
29
  /**
@@ -35,11 +45,17 @@ export type ConsultationInterest = {
35
45
  * to show the "Anmäl intresse" CTA or the post-submit "Tack — vi
36
46
  * hör av oss" confirmation state.
37
47
  *
48
+ * Scoped to a single `expertId` (the one the client asked about) —
49
+ * a user with interest in multiple experts gets multiple summaries
50
+ * via multiple calls. v1 only ever asks about `"bohm"`, so the app
51
+ * still has a single CTA flow.
52
+ *
38
53
  * Carries just enough for the UI to render the two states; the
39
54
  * full `ConsultationInterest` record (notes, internal status
40
55
  * machinery) stays admin-only.
41
56
  */
42
57
  export type ConsultationInterestSummary = {
58
+ expertId: string;
43
59
  hasInterest: boolean;
44
60
  submittedAt: string | null;
45
61
  status: ConsultationInterestStatus | null;
@@ -109,6 +109,24 @@ export interface GetQuestionResponse {
109
109
  quotaResetsAt?: string;
110
110
  /** Number of questions remaining for today */
111
111
  questionsRemainingToday: number;
112
+ /**
113
+ * True when the user has answered every question currently in the
114
+ * DQ pool. Distinct from `quotaExhausted` (the daily 3/day cap):
115
+ *
116
+ * - `quotaExhausted: true` → user used today's 3 — come back tomorrow.
117
+ * - `allQuestionsCompleted: true` → user has cleared the entire pool;
118
+ * no more questions exist until new ones
119
+ * are added. Tomorrow's quota reset won't
120
+ * bring new questions.
121
+ *
122
+ * Optional for backward compatibility — older clients that don't
123
+ * read this field will continue inferring "no question available"
124
+ * from `question: null && !quotaExhausted`, but they'll show the
125
+ * generic "come back tomorrow" message which is misleading in this
126
+ * state. New clients should render a dedicated "all caught up"
127
+ * surface when this is `true`.
128
+ */
129
+ allQuestionsCompleted?: boolean;
112
130
  /** User's discovery progress (streaks, counts) */
113
131
  progress?: DiscoveryProgress;
114
132
  }
@@ -59,14 +59,23 @@ export type SubmitFeedbackRequest = {
59
59
  context?: string;
60
60
  rating?: number;
61
61
  };
62
- export type ExpressConsultationInterestRequest = undefined;
63
- export type GetMyConsultationInterestRequest = undefined;
62
+ export type ExpressConsultationInterestRequest = {
63
+ expertId: string;
64
+ };
65
+ export type GetMyConsultationInterestRequest = {
66
+ expertId: string;
67
+ };
64
68
  export type GetConsultationInterestsRequest = {
65
69
  /**
66
70
  * Optional status filter. Omit to fetch every record. Most
67
71
  * common use is `PENDING` for the team's outreach worklist.
68
72
  */
69
73
  status?: ConsultationInterestStatus;
74
+ /**
75
+ * Optional expert filter. Omit for the cross-expert worklist;
76
+ * pass to scope a single professional's outreach queue.
77
+ */
78
+ expertId?: string;
70
79
  };
71
80
  export type UpdateConsultationInterestRequest = {
72
81
  id: string;
@@ -4,9 +4,9 @@ exports.calculateAgeFromDateString = calculateAgeFromDateString;
4
4
  function calculateAgeFromDateString(isoDateString) {
5
5
  const birthDate = new Date(isoDateString);
6
6
  const today = new Date();
7
- const ageDiff = today.getFullYear() - birthDate.getFullYear();
8
- const hasBirthdayPassed = today.getMonth() > birthDate.getMonth() ||
9
- (today.getMonth() === birthDate.getMonth() &&
10
- today.getDate() >= birthDate.getDate());
7
+ const ageDiff = today.getUTCFullYear() - birthDate.getUTCFullYear();
8
+ const hasBirthdayPassed = today.getUTCMonth() > birthDate.getUTCMonth() ||
9
+ (today.getUTCMonth() === birthDate.getUTCMonth() &&
10
+ today.getUTCDate() >= birthDate.getUTCDate());
11
11
  return hasBirthdayPassed ? ageDiff : ageDiff - 1;
12
12
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "heartraite",
3
- "version": "1.0.193",
3
+ "version": "1.0.195",
4
4
  "description": "Heartraite npm package for common functionality",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -8,13 +8,23 @@ import { ConsultationInterestStatus } from "../enum";
8
8
  * (`Consultation`) so subsequent professionals can land here
9
9
  * without a schema rename.
10
10
  *
11
- * One pending record per user at a time — the
12
- * `/consultation/express-interest` endpoint upserts rather than
13
- * appending so resubmits don't duplicate.
11
+ * `expertId` scopes the record to a specific professional. v1 has
12
+ * exactly one value (`"bohm"`); future additions (other
13
+ * psychologists, therapists, sex therapists, financial planners,
14
+ * etc.) plug in without a schema migration. Idempotency at the
15
+ * backend is `(userId, expertId)` — a user can express interest in
16
+ * multiple experts independently, but only one pending record per
17
+ * (user, expert) pair at any time.
18
+ *
19
+ * The slug format (e.g. `"bohm"`) reads cleanly in logs / admin
20
+ * dashboards. Future v2 may move expert metadata (name, photo,
21
+ * pricing, credentials) into DatoCMS and key entries by their CMS
22
+ * slug — the runtime contract stays the same string.
14
23
  */
15
24
  export type ConsultationInterest = {
16
25
  id: string;
17
26
  userId: string;
27
+ expertId: string;
18
28
  submittedAt: string;
19
29
  status: ConsultationInterestStatus;
20
30
  /**
@@ -37,11 +47,17 @@ export type ConsultationInterest = {
37
47
  * to show the "Anmäl intresse" CTA or the post-submit "Tack — vi
38
48
  * hör av oss" confirmation state.
39
49
  *
50
+ * Scoped to a single `expertId` (the one the client asked about) —
51
+ * a user with interest in multiple experts gets multiple summaries
52
+ * via multiple calls. v1 only ever asks about `"bohm"`, so the app
53
+ * still has a single CTA flow.
54
+ *
40
55
  * Carries just enough for the UI to render the two states; the
41
56
  * full `ConsultationInterest` record (notes, internal status
42
57
  * machinery) stays admin-only.
43
58
  */
44
59
  export type ConsultationInterestSummary = {
60
+ expertId: string;
45
61
  hasInterest: boolean;
46
62
  submittedAt: string | null;
47
63
  status: ConsultationInterestStatus | null;
@@ -149,6 +149,25 @@ export interface GetQuestionResponse {
149
149
  /** Number of questions remaining for today */
150
150
  questionsRemainingToday: number;
151
151
 
152
+ /**
153
+ * True when the user has answered every question currently in the
154
+ * DQ pool. Distinct from `quotaExhausted` (the daily 3/day cap):
155
+ *
156
+ * - `quotaExhausted: true` → user used today's 3 — come back tomorrow.
157
+ * - `allQuestionsCompleted: true` → user has cleared the entire pool;
158
+ * no more questions exist until new ones
159
+ * are added. Tomorrow's quota reset won't
160
+ * bring new questions.
161
+ *
162
+ * Optional for backward compatibility — older clients that don't
163
+ * read this field will continue inferring "no question available"
164
+ * from `question: null && !quotaExhausted`, but they'll show the
165
+ * generic "come back tomorrow" message which is misleading in this
166
+ * state. New clients should render a dedicated "all caught up"
167
+ * surface when this is `true`.
168
+ */
169
+ allQuestionsCompleted?: boolean;
170
+
152
171
  /** User's discovery progress (streaks, counts) */
153
172
  progress?: DiscoveryProgress;
154
173
  }
@@ -90,14 +90,20 @@ export type SubmitFeedbackRequest = {
90
90
 
91
91
  // consultation — 1:1 video sessions with credentialed professionals
92
92
  //
93
- // v1 endpoints are AuthGuard'd from the user-facing side (express +
94
- // my-interest) and AdminGuard'd from the team side (admin-interests
95
- // + admin-update-interest). The express + my-interest payloads are
96
- // intentionally empty / minimal userId comes from the request's
97
- // authed token, never the client.
98
- export type ExpressConsultationInterestRequest = undefined;
93
+ // v1 endpoints are CAGuard'd from the user-facing side (express +
94
+ // my-interest, gated on completed CA) and AdminGuard'd from the
95
+ // team side (admin-interests + admin-update-interest). userId
96
+ // always comes from the authed token, never the client. `expertId`
97
+ // scopes the request to a specific professional — v1 only sends
98
+ // `"bohm"` but the field is required so the schema is stable when
99
+ // future experts land.
100
+ export type ExpressConsultationInterestRequest = {
101
+ expertId: string;
102
+ };
99
103
 
100
- export type GetMyConsultationInterestRequest = undefined;
104
+ export type GetMyConsultationInterestRequest = {
105
+ expertId: string;
106
+ };
101
107
 
102
108
  export type GetConsultationInterestsRequest = {
103
109
  /**
@@ -105,6 +111,11 @@ export type GetConsultationInterestsRequest = {
105
111
  * common use is `PENDING` for the team's outreach worklist.
106
112
  */
107
113
  status?: ConsultationInterestStatus;
114
+ /**
115
+ * Optional expert filter. Omit for the cross-expert worklist;
116
+ * pass to scope a single professional's outreach queue.
117
+ */
118
+ expertId?: string;
108
119
  };
109
120
 
110
121
  export type UpdateConsultationInterestRequest = {
@@ -2,11 +2,11 @@ export function calculateAgeFromDateString(isoDateString: string): number {
2
2
  const birthDate = new Date(isoDateString);
3
3
  const today = new Date();
4
4
 
5
- const ageDiff = today.getFullYear() - birthDate.getFullYear();
5
+ const ageDiff = today.getUTCFullYear() - birthDate.getUTCFullYear();
6
6
  const hasBirthdayPassed =
7
- today.getMonth() > birthDate.getMonth() ||
8
- (today.getMonth() === birthDate.getMonth() &&
9
- today.getDate() >= birthDate.getDate());
7
+ today.getUTCMonth() > birthDate.getUTCMonth() ||
8
+ (today.getUTCMonth() === birthDate.getUTCMonth() &&
9
+ today.getUTCDate() >= birthDate.getUTCDate());
10
10
 
11
11
  return hasBirthdayPassed ? ageDiff : ageDiff - 1;
12
12
  }