@teamscala/service-boot 0.23.438 → 0.23.440
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/package.json
CHANGED
|
@@ -22,6 +22,7 @@ interface CapturedEmail {
|
|
|
22
22
|
text: string;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
let capturedResponseRow: Record<string, unknown> | null = null;
|
|
25
26
|
let capturedEmail: CapturedEmail | null = null;
|
|
26
27
|
let emailResolve: ((email: CapturedEmail) => void) | null = null;
|
|
27
28
|
let capturedEvent: { kind: string; payload: Record<string, unknown> } | null = null;
|
|
@@ -197,7 +198,10 @@ function makeFakePrisma() {
|
|
|
197
198
|
// client_database deliberately absent — the quiz prisma client is
|
|
198
199
|
// scoped without it, so the CRM branch must skip without error.
|
|
199
200
|
assessment_responses: {
|
|
200
|
-
create: async (
|
|
201
|
+
create: async (args: { data: Record<string, unknown> }) => {
|
|
202
|
+
capturedResponseRow = args?.data ?? null;
|
|
203
|
+
return { id: "resp-1" };
|
|
204
|
+
},
|
|
201
205
|
},
|
|
202
206
|
email_send_log: {
|
|
203
207
|
create: async () => ({}),
|
|
@@ -218,7 +222,10 @@ type SubmitHandler = (opts: {
|
|
|
218
222
|
context: { prisma: unknown; headers: Headers };
|
|
219
223
|
}) => Promise<Record<string, unknown>>;
|
|
220
224
|
|
|
221
|
-
async function submitWith45(
|
|
225
|
+
async function submitWith45(
|
|
226
|
+
headers = new Headers(),
|
|
227
|
+
extraInput: Record<string, unknown> = {},
|
|
228
|
+
) {
|
|
222
229
|
// The raw handler under ~orpc only reads { input, context }; the other
|
|
223
230
|
// ProcedureHandlerOptions fields are middleware plumbing this procedure
|
|
224
231
|
// (no middlewares) never touches, so the narrowed call shape is cast.
|
|
@@ -229,6 +236,7 @@ async function submitWith45(headers = new Headers()) {
|
|
|
229
236
|
firstName: "Alex Reader",
|
|
230
237
|
email: "alex@example.com",
|
|
231
238
|
answers: answersForScore45(),
|
|
239
|
+
...extraInput,
|
|
232
240
|
},
|
|
233
241
|
context: { prisma: makeFakePrisma(), headers },
|
|
234
242
|
});
|
|
@@ -237,6 +245,7 @@ async function submitWith45(headers = new Headers()) {
|
|
|
237
245
|
beforeEach(() => {
|
|
238
246
|
capturedEmail = null;
|
|
239
247
|
capturedEvent = null;
|
|
248
|
+
capturedResponseRow = null;
|
|
240
249
|
brandRowStub = null;
|
|
241
250
|
});
|
|
242
251
|
|
|
@@ -374,3 +383,55 @@ describe("publicAssessment.submit — email band single-sourced from stage_bands
|
|
|
374
383
|
expect(email.html).not.toMatch(/<img/i);
|
|
375
384
|
});
|
|
376
385
|
});
|
|
386
|
+
|
|
387
|
+
describe("prospect segmentation fields are captured without touching the score", () => {
|
|
388
|
+
const SEGMENTATION = {
|
|
389
|
+
linkedinUrl: "https://www.linkedin.com/in/alex-reader",
|
|
390
|
+
website: "https://alexreader.example",
|
|
391
|
+
teamSize: "Me + team of 2–5",
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
test("all three persist to their own columns", async () => {
|
|
395
|
+
await submitWith45(new Headers(), SEGMENTATION);
|
|
396
|
+
expect(capturedResponseRow?.linkedin_url).toBe(SEGMENTATION.linkedinUrl);
|
|
397
|
+
expect(capturedResponseRow?.website).toBe(SEGMENTATION.website);
|
|
398
|
+
expect(capturedResponseRow?.team_size).toBe(SEGMENTATION.teamSize);
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
test("the Authority Index is IDENTICAL with and without them", async () => {
|
|
402
|
+
// The operator's binding constraint: these are segmentation only and must
|
|
403
|
+
// not move the score. Asserting the same answers produce the same score
|
|
404
|
+
// either way is the only check that actually proves it — asserting the
|
|
405
|
+
// columns were written says nothing about the scorer.
|
|
406
|
+
const without = (await submitWith45()) as {
|
|
407
|
+
totalScore: number;
|
|
408
|
+
categoryScores: Record<string, number>;
|
|
409
|
+
};
|
|
410
|
+
const withFields = (await submitWith45(
|
|
411
|
+
new Headers(),
|
|
412
|
+
SEGMENTATION,
|
|
413
|
+
)) as { totalScore: number; categoryScores: Record<string, number> };
|
|
414
|
+
|
|
415
|
+
expect(withFields.totalScore).toBe(without.totalScore);
|
|
416
|
+
expect(withFields.categoryScores).toEqual(without.categoryScores);
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
test("omitting them writes null, not empty string", async () => {
|
|
420
|
+
// A skipped optional field must be absent in the data, so segmentation
|
|
421
|
+
// queries can tell "not asked" from "answered blank".
|
|
422
|
+
await submitWith45();
|
|
423
|
+
expect(capturedResponseRow?.linkedin_url).toBeNull();
|
|
424
|
+
expect(capturedResponseRow?.website).toBeNull();
|
|
425
|
+
expect(capturedResponseRow?.team_size).toBeNull();
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
test("the existing answers payload is unchanged by their presence", async () => {
|
|
429
|
+
// Guards the other direction: segmentation must not leak into `answers`,
|
|
430
|
+
// which is the set the scorer reads.
|
|
431
|
+
await submitWith45(new Headers(), SEGMENTATION);
|
|
432
|
+
const answers = capturedResponseRow?.answers as Array<{ question_id: string }>;
|
|
433
|
+
expect(answers).toHaveLength(20);
|
|
434
|
+
expect(JSON.stringify(answers)).not.toContain("linkedin");
|
|
435
|
+
expect(JSON.stringify(answers)).not.toContain("team of 2");
|
|
436
|
+
});
|
|
437
|
+
});
|
|
@@ -41,6 +41,10 @@ const submitSchema = v.object({
|
|
|
41
41
|
email: v.string(),
|
|
42
42
|
company: v.optional(v.string()),
|
|
43
43
|
phone: v.optional(v.string()),
|
|
44
|
+
// Prospect segmentation, captured at the company step. All optional and all
|
|
45
|
+
// written to their own columns — none is an input to scoring, which reads
|
|
46
|
+
// `answers` alone.
|
|
47
|
+
linkedinUrl: v.optional(v.string()),
|
|
44
48
|
website: v.optional(v.string()),
|
|
45
49
|
industry: v.optional(v.string()),
|
|
46
50
|
teamSize: v.optional(v.string()),
|
|
@@ -178,6 +182,9 @@ export const publicAssessmentRouter = {
|
|
|
178
182
|
first_name: firstName,
|
|
179
183
|
last_name: lastName,
|
|
180
184
|
company: input.company ?? null,
|
|
185
|
+
linkedin_url: input.linkedinUrl ?? null,
|
|
186
|
+
website: input.website ?? null,
|
|
187
|
+
team_size: input.teamSize ?? null,
|
|
181
188
|
total_score: totalScore,
|
|
182
189
|
category_scores: categoryScores,
|
|
183
190
|
answers: input.answers.map((a) => ({
|
package/src/mount-orpc-router.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import { join } from "node:path";
|
|
13
13
|
import { getAppLogger } from "@teamscala/logger/app-loggers";
|
|
14
14
|
import { registerFnRouters } from "./fn-routers/index.ts";
|
|
15
|
+
import type { InjectedDb8 } from "@teamscala/orpc/configure";
|
|
15
16
|
import type { Hono } from "hono";
|
|
16
17
|
import { bodyLimit } from "hono/body-limit";
|
|
17
18
|
|
|
@@ -70,6 +71,29 @@ export async function mountOrpcRouter(
|
|
|
70
71
|
"@teamscala/orpc/configure"
|
|
71
72
|
);
|
|
72
73
|
|
|
74
|
+
// Coexistence (Prisma 8 migration): create the v8 chained client when
|
|
75
|
+
// the shared platform contract ships inside @teamscala/db. Absence is
|
|
76
|
+
// the EXPECTED state for services that have not migrated — db8 stays
|
|
77
|
+
// undefined and v7 procedures are untouched. One boot-time creation,
|
|
78
|
+
// closed over by the request handler (connects lazily on first query).
|
|
79
|
+
let db8: InjectedDb8 | undefined;
|
|
80
|
+
try {
|
|
81
|
+
if (process.env.DATABASE_URL) {
|
|
82
|
+
const { createDbClientV8 } = await import(
|
|
83
|
+
"@teamscala/db/create-db-client-v8"
|
|
84
|
+
);
|
|
85
|
+
db8 = (await createDbClientV8({
|
|
86
|
+
connectionString: process.env.DATABASE_URL,
|
|
87
|
+
serviceName: serviceSlug,
|
|
88
|
+
})) as never;
|
|
89
|
+
}
|
|
90
|
+
} catch (err) {
|
|
91
|
+
log.info(
|
|
92
|
+
`[scala-install:${serviceSlug}] db8 (Prisma 8 chained client) unavailable: ${err instanceof Error ? err.message : err} — serving v7 procedures only`,
|
|
93
|
+
);
|
|
94
|
+
db8 = undefined;
|
|
95
|
+
}
|
|
96
|
+
|
|
73
97
|
// Bridge the composed router into tool-executor so in-process tool
|
|
74
98
|
// dispatch (chat agents' work_items/meetings/agreements/proposals
|
|
75
99
|
// tools, etc.) resolves against this service's ORPC router + Prisma
|
|
@@ -160,7 +184,7 @@ export async function mountOrpcRouter(
|
|
|
160
184
|
const prisma = getConfiguredPrisma();
|
|
161
185
|
const result = await handler.handle(c.req.raw, {
|
|
162
186
|
prefix: "/api/rpc",
|
|
163
|
-
context: { prisma, headers: c.req.raw.headers, resolveUser },
|
|
187
|
+
context: { prisma, db8, headers: c.req.raw.headers, resolveUser },
|
|
164
188
|
});
|
|
165
189
|
if (!result?.matched) return c.notFound();
|
|
166
190
|
return result.response;
|