@primocaredentgroup/convex-campaigns-component 0.3.3 → 0.3.5

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.
@@ -2,6 +2,7 @@ export {
2
2
  listCampaigns,
3
3
  getCampaign,
4
4
  getSnapshot,
5
+ getSnapshotMemberSample,
5
6
  getCampaignReport,
6
7
  } from "./components/campaigns/functions/queries";
7
8
 
@@ -83,6 +83,15 @@ export const suppressionReasonValidator = v.union(
83
83
  v.literal("LOWER_PRIORITY"),
84
84
  );
85
85
 
86
+ /**
87
+ * params esistenti: monthsSince, daysSince, daysBack, appointmentTypeCodes, patientIds
88
+ * params opzionali anagrafici:
89
+ * genders?: ("M" | "F" | "O")[]
90
+ * minAge?: number -- età minima (anni compiuti, da birthDate)
91
+ * maxAge?: number -- età massima (anni compiuti)
92
+ * residenceCityIds?: string[]
93
+ * birthCityIds?: string[]
94
+ */
86
95
  export const segmentationRulesValidator = v.object({
87
96
  segmentType: segmentTypeValidator,
88
97
  params: v.optional(v.any()),
@@ -157,26 +157,40 @@ export const previewAudience = mutation({
157
157
  args: {
158
158
  campaignId: v.id("campaigns"),
159
159
  segmentationRules: segmentationRulesValidator,
160
+ sampleSize: v.optional(v.number()),
160
161
  },
161
162
  handler: async (ctx, args) => {
162
163
  await assertAuthorized(ctx, ["Admin", "Marketing"]);
163
164
  const campaign = await ctx.db.get(args.campaignId);
164
165
  if (!campaign) throw new Error("Campaign not found");
165
166
 
167
+ const sampleSize = Math.min(Math.max(1, args.sampleSize ?? 10), 50);
168
+ const sample: Array<{
169
+ patientId: string;
170
+ firstName?: string;
171
+ lastName?: string;
172
+ birthDate?: string;
173
+ gender?: string;
174
+ residenceCityName?: string;
175
+ email?: string;
176
+ phone?: string;
177
+ }> = [];
178
+
166
179
  let cursor: string | null = null;
167
180
  let total = 0;
168
181
  let withEmail = 0;
169
182
  let withPhone = 0;
170
183
  let blacklisted = 0;
171
- const maxPages = 5;
184
+ const maxPages = 3;
172
185
  let pages = 0;
173
186
 
174
- while (pages < maxPages) {
187
+ while (pages < maxPages && sample.length < sampleSize) {
175
188
  const page = await patientDataPort.queryCandidates(
176
189
  ctx,
177
190
  { scopeType: campaign.scopeType, scopeClinicIds: campaign.scopeClinicIds },
178
191
  args.segmentationRules as any,
179
192
  cursor,
193
+ { includeAnagrafica: true },
180
194
  );
181
195
  for (const p of page.patients) {
182
196
  total += 1;
@@ -184,6 +198,19 @@ export const previewAudience = mutation({
184
198
  if (p.email && consent.email) withEmail += 1;
185
199
  if (p.phone && consent.phone) withPhone += 1;
186
200
  if (consent.blacklisted) blacklisted += 1;
201
+
202
+ if (sample.length < sampleSize) {
203
+ sample.push({
204
+ patientId: p.patientId,
205
+ firstName: p.firstName,
206
+ lastName: p.lastName,
207
+ birthDate: p.birthDate,
208
+ gender: p.gender,
209
+ residenceCityName: p.residenceCityName,
210
+ email: p.email,
211
+ phone: p.phone,
212
+ });
213
+ }
187
214
  }
188
215
  cursor = page.nextCursor;
189
216
  pages += 1;
@@ -195,6 +222,7 @@ export const previewAudience = mutation({
195
222
  warnings: cursor
196
223
  ? ["Preview limited to first pages for performance. Build snapshot for full results."]
197
224
  : [],
225
+ sample,
198
226
  };
199
227
  },
200
228
  });
@@ -150,6 +150,74 @@ export const getSnapshot = query({
150
150
  },
151
151
  });
152
152
 
153
+ export const getSnapshotMemberSample = query({
154
+ args: {
155
+ snapshotId: v.id("audience_snapshots"),
156
+ limit: v.optional(v.number()),
157
+ },
158
+ handler: async (ctx, args) => {
159
+ try {
160
+ await assertAuthorized(ctx, ["Admin", "Marketing"]);
161
+ const limit = Math.min(Math.max(1, args.limit ?? 10), 100);
162
+
163
+ let members: any[];
164
+ try {
165
+ members = (await ctx.db
166
+ .query("audience_members")
167
+ .withIndex("by_snapshot", (q: any) => q.eq("snapshotId", args.snapshotId))
168
+ .collect()).slice(0, limit);
169
+ } catch {
170
+ members = (await ctx.db
171
+ .query("audience_members")
172
+ .filter((q: any) => q.eq(q.field("snapshotId"), args.snapshotId))
173
+ .collect()).slice(0, limit);
174
+ }
175
+
176
+ const sample: Array<{
177
+ patientId: string;
178
+ firstName?: string;
179
+ lastName?: string;
180
+ birthDate?: string;
181
+ gender?: string;
182
+ residenceCityName?: string;
183
+ email?: string;
184
+ phone?: string;
185
+ }> = [];
186
+
187
+ for (const m of members) {
188
+ const patient = await ctx.db.get(m.patientId);
189
+ if (!patient) continue;
190
+ const p = patient as any;
191
+ let residenceCityName: string | undefined;
192
+ if (p.residenceCityId) {
193
+ try {
194
+ const city = await ctx.db.get(p.residenceCityId);
195
+ residenceCityName = (city as any)?.name;
196
+ } catch {
197
+ // Host may not have cities table
198
+ }
199
+ }
200
+ sample.push({
201
+ patientId: m.patientId,
202
+ firstName: p.firstName,
203
+ lastName: p.lastName,
204
+ birthDate: p.birthDate,
205
+ gender: p.gender,
206
+ residenceCityName,
207
+ email: m.contactRefs?.email ?? p.email,
208
+ phone: m.contactRefs?.phone ?? p.phone ?? p.mobilePhone,
209
+ });
210
+ }
211
+
212
+ return { sample };
213
+ } catch (error) {
214
+ if (!isRecoverableQueryError(error)) throw error;
215
+ console.error("[campaigns.getSnapshotMemberSample] recoverable error:", error);
216
+ return { sample: [] };
217
+ }
218
+ },
219
+ });
220
+
153
221
  export const getCampaignReport = query({
154
222
  args: {
155
223
  campaignId: v.id("campaigns"),
@@ -16,6 +16,11 @@ export type CandidatePatient = {
16
16
  clinicId?: Id<"clinics">;
17
17
  email?: string;
18
18
  phone?: string;
19
+ firstName?: string;
20
+ lastName?: string;
21
+ birthDate?: string;
22
+ gender?: string;
23
+ residenceCityName?: string;
19
24
  };
20
25
 
21
26
  const DEFAULT_PAGE_SIZE = 200;
@@ -23,6 +28,98 @@ const DAY_MS = 24 * 60 * 60 * 1000;
23
28
 
24
29
  type AppointmentDoc = Doc<"appointments">;
25
30
 
31
+ /** Calcola età in anni da birthDate (formato "YYYY-MM-DD" o simile) */
32
+ function ageFromBirthDate(birthDate: string | undefined): number | null {
33
+ if (!birthDate) return null;
34
+ const parts = birthDate.split("-").map(Number);
35
+ if (parts.length < 2) return null;
36
+ const [y, m = 1, d = 1] = parts;
37
+ const birth = new Date(y, m - 1, d).getTime();
38
+ const now = Date.now();
39
+ return Math.floor((now - birth) / (365.25 * DAY_MS));
40
+ }
41
+
42
+ /**
43
+ * Verifica se il paziente passa i filtri demografici (genere, età, città).
44
+ * Se un filtro è assente, non viene applicato.
45
+ */
46
+ export function matchesDemographicFilters(
47
+ patient: { gender?: string; birthDate?: string; residenceCityId?: any; birthCityId?: any },
48
+ params: any,
49
+ ): boolean {
50
+ if (!params) return true;
51
+
52
+ const genders = Array.isArray(params.genders) ? params.genders as string[] : [];
53
+ if (genders.length > 0 && patient.gender) {
54
+ if (!genders.includes(patient.gender)) return false;
55
+ }
56
+
57
+ const minAge = typeof params.minAge === "number" ? params.minAge : undefined;
58
+ const maxAge = typeof params.maxAge === "number" ? params.maxAge : undefined;
59
+ if (minAge != null || maxAge != null) {
60
+ const age = ageFromBirthDate(patient.birthDate);
61
+ if (age === null) return false;
62
+ if (minAge != null && age < minAge) return false;
63
+ if (maxAge != null && age > maxAge) return false;
64
+ }
65
+
66
+ const residenceCityIds = Array.isArray(params.residenceCityIds)
67
+ ? params.residenceCityIds.map(String)
68
+ : [];
69
+ if (residenceCityIds.length > 0 && patient.residenceCityId) {
70
+ if (!residenceCityIds.includes(String(patient.residenceCityId))) return false;
71
+ }
72
+
73
+ const birthCityIds = Array.isArray(params.birthCityIds) ? params.birthCityIds.map(String) : [];
74
+ if (birthCityIds.length > 0 && patient.birthCityId) {
75
+ if (!birthCityIds.includes(String(patient.birthCityId))) return false;
76
+ }
77
+
78
+ return true;
79
+ }
80
+
81
+ async function resolveResidenceCityName(
82
+ ctx: QueryCtx | MutationCtx,
83
+ residenceCityId: any,
84
+ ): Promise<string | undefined> {
85
+ if (!residenceCityId) return undefined;
86
+ try {
87
+ const city = await ctx.db.get(residenceCityId);
88
+ return (city as any)?.name;
89
+ } catch {
90
+ return undefined;
91
+ }
92
+ }
93
+
94
+ function buildCandidatePatient(
95
+ patient: any,
96
+ clinicId: Id<"clinics"> | undefined,
97
+ residenceCityName?: string,
98
+ ): CandidatePatient {
99
+ return {
100
+ patientId: patient._id,
101
+ clinicId,
102
+ email: patient.email,
103
+ phone: patient.phone ?? patient.mobilePhone,
104
+ firstName: patient.firstName,
105
+ lastName: patient.lastName,
106
+ birthDate: patient.birthDate,
107
+ gender: patient.gender,
108
+ residenceCityName,
109
+ };
110
+ }
111
+
112
+ async function buildCandidateWithAnagrafica(
113
+ ctx: QueryCtx | MutationCtx,
114
+ patient: any,
115
+ clinicId: Id<"clinics"> | undefined,
116
+ ): Promise<CandidatePatient> {
117
+ const residenceCityName = patient.residenceCityId
118
+ ? await resolveResidenceCityName(ctx, patient.residenceCityId)
119
+ : undefined;
120
+ return buildCandidatePatient(patient, clinicId, residenceCityName);
121
+ }
122
+
26
123
  async function deriveClinicIdFromAppointments(
27
124
  ctx: QueryCtx | MutationCtx,
28
125
  patient: Doc<"patients">,
@@ -73,6 +170,7 @@ async function queryNoShowCandidates(
73
170
  campaignScope: CampaignScope,
74
171
  params: any,
75
172
  paginationCursor: string | null,
173
+ options?: { includeAnagrafica?: boolean },
76
174
  ): Promise<{ patients: CandidatePatient[]; nextCursor: string | null }> {
77
175
  const daysBack = typeof params?.daysBack === "number" ? params.daysBack : 90;
78
176
  const cutoff = Date.now() - daysBack * DAY_MS;
@@ -96,15 +194,15 @@ async function queryNoShowCandidates(
96
194
  isInScope(campaignScope, appointment.clinicId),
97
195
  );
98
196
  if (!hasNoShow) continue;
197
+ if (!matchesDemographicFilters(patient as any, params)) continue;
99
198
 
100
199
  const clinicId = await deriveClinicIdFromAppointments(ctx, patient, campaignScope);
101
200
  if (campaignScope.scopeType !== "HQ" && !clinicId) continue;
102
- patients.push({
103
- patientId: patient._id,
104
- clinicId,
105
- email: patient.email,
106
- phone: patient.phone,
107
- });
201
+ patients.push(
202
+ options?.includeAnagrafica
203
+ ? await buildCandidateWithAnagrafica(ctx, patient as any, clinicId)
204
+ : buildCandidatePatient(patient as any, clinicId),
205
+ );
108
206
  }
109
207
 
110
208
  return {
@@ -127,6 +225,7 @@ async function queryRecallOrQuoteFollowupCandidates(
127
225
  campaignScope: CampaignScope,
128
226
  segmentationRules: SegmentationRules,
129
227
  paginationCursor: string | null,
228
+ options?: { includeAnagrafica?: boolean },
130
229
  ): Promise<{ patients: CandidatePatient[]; nextCursor: string | null }> {
131
230
  const now = Date.now();
132
231
  const page = await ctx.db
@@ -188,13 +287,13 @@ async function queryRecallOrQuoteFollowupCandidates(
188
287
  ? lastCompleted.appointmentDate <= cutoffRecall
189
288
  : lastCompleted.appointmentDate <= cutoffQuote;
190
289
  if (!qualifies) continue;
290
+ if (!matchesDemographicFilters(patient as any, segmentationRules.params)) continue;
191
291
 
192
- patients.push({
193
- patientId: patient._id,
194
- clinicId,
195
- email: patient.email,
196
- phone: patient.phone,
197
- });
292
+ patients.push(
293
+ options?.includeAnagrafica
294
+ ? await buildCandidateWithAnagrafica(ctx, patient as any, clinicId)
295
+ : buildCandidatePatient(patient as any, clinicId),
296
+ );
198
297
  }
199
298
 
200
299
  return { patients, nextCursor: page.isDone ? null : page.continueCursor };
@@ -205,6 +304,7 @@ export async function queryCandidates(
205
304
  campaignScope: CampaignScope,
206
305
  segmentationRules: SegmentationRules,
207
306
  paginationCursor: string | null,
307
+ options?: { includeAnagrafica?: boolean },
208
308
  ): Promise<{ patients: CandidatePatient[]; nextCursor: string | null }> {
209
309
  if (segmentationRules.segmentType === "CUSTOM_IDS") {
210
310
  const patientIds = Array.isArray(segmentationRules.params?.patientIds)
@@ -217,14 +317,14 @@ export async function queryCandidates(
217
317
  for (const patientId of chunk) {
218
318
  const patient = await ctx.db.get(patientId);
219
319
  if (!patient || patient.status === "archived") continue;
320
+ if (!matchesDemographicFilters(patient as any, segmentationRules.params)) continue;
220
321
  const clinicId = await deriveClinicIdFromAppointments(ctx, patient, campaignScope);
221
322
  if (campaignScope.scopeType !== "HQ" && !clinicId) continue;
222
- patients.push({
223
- patientId: patient._id,
224
- clinicId,
225
- email: patient.email,
226
- phone: patient.phone,
227
- });
323
+ patients.push(
324
+ options?.includeAnagrafica
325
+ ? await buildCandidateWithAnagrafica(ctx, patient as any, clinicId)
326
+ : buildCandidatePatient(patient as any, clinicId),
327
+ );
228
328
  }
229
329
 
230
330
  const next = offset + chunk.length;
@@ -237,6 +337,7 @@ export async function queryCandidates(
237
337
  campaignScope,
238
338
  segmentationRules.params,
239
339
  paginationCursor,
340
+ options,
240
341
  );
241
342
  }
242
343
 
@@ -249,6 +350,7 @@ export async function queryCandidates(
249
350
  campaignScope,
250
351
  segmentationRules,
251
352
  paginationCursor,
353
+ options,
252
354
  );
253
355
  }
254
356
 
@@ -259,14 +361,14 @@ export async function queryCandidates(
259
361
 
260
362
  const patients: CandidatePatient[] = [];
261
363
  for (const patient of page.page) {
364
+ if (!matchesDemographicFilters(patient as any, segmentationRules.params)) continue;
262
365
  const clinicId = await deriveClinicIdFromAppointments(ctx, patient, campaignScope);
263
366
  if (campaignScope.scopeType !== "HQ" && !clinicId) continue;
264
- patients.push({
265
- patientId: patient._id,
266
- clinicId,
267
- email: patient.email,
268
- phone: patient.phone,
269
- });
367
+ patients.push(
368
+ options?.includeAnagrafica
369
+ ? await buildCandidateWithAnagrafica(ctx, patient as any, clinicId)
370
+ : buildCandidatePatient(patient as any, clinicId),
371
+ );
270
372
  }
271
373
 
272
374
  return { patients, nextCursor: page.isDone ? null : page.continueCursor };
@@ -11,6 +11,9 @@ import { components } from "./_generated/api";
11
11
 
12
12
  const c = components as {
13
13
  campaigns?: {
14
+ campaignsQueries?: {
15
+ getSnapshotMemberSample: (args: { snapshotId: string; limit?: number }) => Promise<unknown>;
16
+ };
14
17
  campaignsPlayground?: {
15
18
  listCampaignsV2: (args: { orgId: string; status?: "draft" | "ready" | "archived" }) => Promise<unknown>;
16
19
  getPlaygroundBootstrap: (args: { orgId: string }) => Promise<unknown>;
@@ -79,6 +82,19 @@ export const getAudiencePage = query({
79
82
  },
80
83
  });
81
84
 
85
+ export const getSnapshotMemberSample = query({
86
+ args: {
87
+ snapshotId: v.string(),
88
+ limit: v.optional(v.number()),
89
+ },
90
+ handler: async (ctx, args) => {
91
+ return await ctx.runQuery(c.campaigns!.campaignsQueries!.getSnapshotMemberSample, {
92
+ snapshotId: args.snapshotId,
93
+ limit: args.limit,
94
+ });
95
+ },
96
+ });
97
+
82
98
  export const getPatientCampaignMemberships = query({
83
99
  args: { orgId: v.string(), patientId: v.string() },
84
100
  handler: async (ctx, args) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@primocaredentgroup/convex-campaigns-component",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
4
4
  "description": "Convex Campaigns backend component for PrimoCore",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -9,20 +9,23 @@
9
9
  "convex.config.ts",
10
10
  "README.md",
11
11
  "CHANGELOG.md",
12
- "convex/**",
13
- "scripts/**"
12
+ "convex/**"
14
13
  ],
15
14
  "scripts": {
16
15
  "sync:from-repo": "node ./scripts/sync-from-repo.mjs",
17
16
  "prepack": "npm run sync:from-repo",
18
17
  "test": "tsx --test tests/**/*.test.ts",
19
- "smoke": "node scripts/smoke-test.mjs"
18
+ "smoke": "node scripts/smoke-test.mjs",
19
+ "version:patch": "npm version patch --no-git-tag-version",
20
+ "publish:component": "npm run version:patch && npm publish"
20
21
  },
21
22
  "peerDependencies": {
22
23
  "convex": "^1.14.0"
23
24
  },
24
25
  "peerDependenciesMeta": {
25
- "convex": { "optional": true }
26
+ "convex": {
27
+ "optional": true
28
+ }
26
29
  },
27
30
  "devDependencies": {
28
31
  "convex": "^1.14.0",
@@ -1,171 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Smoke test per componente campaigns.
4
- * Chiama le API pubbliche in sequenza per verificare funzionamento end-to-end.
5
- *
6
- * L'URL Convex viene letto da (in ordine):
7
- * 1. CONVEX_URL o VITE_CONVEX_URL (env)
8
- * 2. .env.local (nella cwd o nelle cartelle parent)
9
- *
10
- * Uso:
11
- * cd /path/alla/repo/convex-campaigns-component # o repo con convex dev
12
- * npm run smoke
13
- *
14
- * # oppure con URL esplicito:
15
- * CONVEX_URL=https://tuo-deployment.convex.cloud npm run smoke
16
- */
17
- import { ConvexHttpClient } from "convex/browser";
18
- import { readFileSync, existsSync } from "node:fs";
19
- import { resolve, dirname } from "node:path";
20
- import { fileURLToPath } from "node:url";
21
-
22
- function loadEnvLocal() {
23
- const dirs = [
24
- process.cwd(),
25
- resolve(process.cwd(), ".."),
26
- resolve(process.cwd(), "../.."),
27
- resolve(dirname(fileURLToPath(import.meta.url)), ".."),
28
- ];
29
- for (const dir of dirs) {
30
- const p = resolve(dir, ".env.local");
31
- if (existsSync(p)) {
32
- try {
33
- const content = readFileSync(p, "utf8");
34
- for (const line of content.split("\n")) {
35
- const m = line.match(/^(?:CONVEX_URL|VITE_CONVEX_URL)=(.+)$/);
36
- if (m) return m[1].trim().replace(/^["']|["']$/g, "");
37
- }
38
- } catch {}
39
- }
40
- }
41
- return null;
42
- }
43
-
44
- const CONVEX_URL =
45
- process.env.CONVEX_URL ?? process.env.VITE_CONVEX_URL ?? loadEnvLocal();
46
- const ORG_ID = process.env.SMOKE_ORG_ID ?? "demo-org";
47
- const CLINIC_IDS = (process.env.SMOKE_CLINIC_IDS ?? "clinic-a,clinic-b")
48
- .split(",")
49
- .map((s) => s.trim());
50
-
51
- if (!CONVEX_URL) {
52
- console.error(`
53
- ERRORE: URL Convex non trovato.
54
-
55
- Opzioni:
56
- 1. Esegui da una cartella con .env.local (creato da "npx convex dev")
57
- 2. Imposta CONVEX_URL:
58
- CONVEX_URL=https://tuo-deployment.convex.cloud npm run smoke
59
- 3. L'URL si trova nel dashboard Convex o in .env.local dopo convex dev
60
- `);
61
- process.exit(1);
62
- }
63
-
64
- const client = new ConvexHttpClient(CONVEX_URL);
65
-
66
- async function run(name, fn) {
67
- try {
68
- const result = await fn();
69
- console.log(`✓ ${name}`);
70
- return result;
71
- } catch (err) {
72
- console.error(`✗ ${name}:`, err.message);
73
- throw err;
74
- }
75
- }
76
-
77
- async function main() {
78
- console.log("Smoke test campaigns component...");
79
- console.log("CONVEX_URL:", CONVEX_URL);
80
- console.log("ORG_ID:", ORG_ID);
81
-
82
- let campaignId;
83
- let versionId;
84
-
85
- if (process.env.SMOKE_SKIP_SEED !== "1") {
86
- const seedResult = await run("seedDemoData", () =>
87
- client.mutation("campaigns:seedDemoData", {
88
- orgId: ORG_ID,
89
- clinicIds: CLINIC_IDS,
90
- nPatients: 50,
91
- }),
92
- );
93
- console.log(" inserted:", seedResult?.insertedPatients);
94
- }
95
-
96
- const createResult = await run("upsertCampaign", () =>
97
- client.mutation("campaigns:upsertCampaign", {
98
- orgId: ORG_ID,
99
- name: "Smoke Test Campaign",
100
- clinicIds: CLINIC_IDS,
101
- rules: {
102
- op: "AND",
103
- rules: [{ fieldKey: "patient.hasCallConsent", operator: "is", value: true }],
104
- },
105
- status: "draft",
106
- }),
107
- );
108
- campaignId = createResult?.id;
109
-
110
- await run("setCampaignLifecycleStatus ready", () =>
111
- client.mutation("campaigns:setCampaignLifecycleStatus", {
112
- campaignId,
113
- status: "ready",
114
- }),
115
- );
116
-
117
- const publishResult = await run("publishCampaignVersion", () =>
118
- client.mutation("campaigns:publishCampaignVersion", {
119
- campaignId,
120
- label: "Smoke test",
121
- }),
122
- );
123
- versionId = publishResult?.versionId;
124
- console.log(" version:", publishResult?.version, "inserted:", publishResult?.inserted);
125
-
126
- const active = await run("listActiveCampaignsForOrg", () =>
127
- client.query("campaigns:listActiveCampaignsForOrg", {
128
- orgId: ORG_ID,
129
- clinicId: CLINIC_IDS[0],
130
- }),
131
- );
132
- console.log(" campaigns:", active?.length);
133
-
134
- const audience = await run("getAudiencePage", () =>
135
- client.query("campaigns:getAudiencePage", {
136
- versionId,
137
- limit: 5,
138
- }),
139
- );
140
- console.log(" page length:", audience?.page?.length);
141
-
142
- const samplePatient = audience?.page?.[0]?.patientId;
143
- if (samplePatient) {
144
- await run("recordContactAttempt", () =>
145
- client.mutation("campaigns:recordContactAttempt", {
146
- orgId: ORG_ID,
147
- clinicId: audience.page[0].clinicId,
148
- patientId: samplePatient,
149
- campaignId,
150
- versionId,
151
- channel: "call",
152
- outcome: "answered",
153
- }),
154
- );
155
- }
156
-
157
- const memberships = await run("getPatientCampaignMemberships", () =>
158
- client.query("campaigns:getPatientCampaignMemberships", {
159
- orgId: ORG_ID,
160
- patientId: samplePatient ?? "demo-smoke-org-clinic-a-1",
161
- }),
162
- );
163
- console.log(" memberships:", memberships?.length);
164
-
165
- console.log("\nSmoke test completato.");
166
- }
167
-
168
- main().catch((err) => {
169
- console.error(err);
170
- process.exit(1);
171
- });
@@ -1,42 +0,0 @@
1
- import { access, cp, mkdir, rm } from "node:fs/promises";
2
- import path from "node:path";
3
- import { fileURLToPath } from "node:url";
4
-
5
- const __filename = fileURLToPath(import.meta.url);
6
- const __dirname = path.dirname(__filename);
7
-
8
- const packageRoot = path.resolve(__dirname, "..");
9
- const repoRoot = path.resolve(packageRoot, "..", "..");
10
-
11
- const sourceComponentDir = path.join(repoRoot, "convex", "components", "campaigns");
12
- const targetComponentDir = path.join(
13
- packageRoot,
14
- "convex",
15
- "components",
16
- "campaigns",
17
- );
18
-
19
- const sourcePublicApi = path.join(repoRoot, "convex", "campaigns.ts");
20
- const targetPublicApi = path.join(packageRoot, "convex", "campaigns.ts");
21
-
22
- async function main() {
23
- const sourceExists = await access(sourceComponentDir).then(
24
- () => true,
25
- () => false,
26
- );
27
- if (!sourceExists) {
28
- console.log("No external monorepo source found, keeping local component files.");
29
- return;
30
- }
31
-
32
- await rm(targetComponentDir, { recursive: true, force: true });
33
- await mkdir(path.dirname(targetComponentDir), { recursive: true });
34
- await cp(sourceComponentDir, targetComponentDir, { recursive: true });
35
- await cp(sourcePublicApi, targetPublicApi);
36
- console.log("Synced campaigns component from repo into npm package.");
37
- }
38
-
39
- main().catch((error) => {
40
- console.error("Failed to sync component files:", error);
41
- process.exit(1);
42
- });