Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 | 9x 9x 32x 14x 8x 8x 7x 1x 11x 11x 13x 9x 2x 2x | // Communication History Log service (spec: communication_history_log).
//
// The single write path for the append-only, per-inquiry event timeline, plus a
// visibility-filtered read path. Log entries are never edited or deleted.
import type { Dal } from "../dal";
import type { CommunicationLog, InquiryEventType } from "../db/schema";
export type Visibility = "homeowner" | "pro" | "ops";
// Per the spec's "Visible To" table. Callers may override per-call.
const DEFAULT_VISIBILITY: Record<InquiryEventType, Visibility[]> = {
inquiry_submitted: ["homeowner"],
pros_matched: ["homeowner", "ops"],
lead_notification_sent: ["ops"],
lead_notification_delivered: ["ops"],
lead_viewed_by_pro: ["homeowner", "pro"],
quote_submitted_by_pro: ["homeowner", "pro"],
quote_upload_by_homeowner: ["homeowner", "pro"],
comparison_generated: ["homeowner"],
comparison_viewed: ["homeowner", "ops"],
lead_declined_by_pro: ["ops"],
status_changed: ["homeowner", "pro"],
callback_requested: ["homeowner", "ops"],
callback_completed: ["homeowner"],
message_sent: ["homeowner", "pro"],
restriction_activated: ["ops"],
restriction_rolled_back: ["ops"],
};
// Human-readable labels for timeline rendering. Actor names (pro/homeowner) are
// resolved by the UI from actorId; these are the neutral event descriptions.
const EVENT_LABELS: Record<InquiryEventType, string> = {
inquiry_submitted: "Inquiry submitted",
pros_matched: "Pros matched to your inquiry",
lead_notification_sent: "Lead notification sent",
lead_notification_delivered: "Lead notification delivered",
lead_viewed_by_pro: "Pro viewed your inquiry",
quote_submitted_by_pro: "Pro submitted a quote",
quote_upload_by_homeowner: "You uploaded a quote",
comparison_generated: "Your comparison is ready",
comparison_viewed: "Comparison viewed",
lead_declined_by_pro: "Pro declined the lead",
status_changed: "Status updated",
callback_requested: "Callback requested",
callback_completed: "Callback completed",
message_sent: "Message sent",
restriction_activated: "Contact restriction activated",
restriction_rolled_back: "Contact restriction rolled back",
};
export type TimelineEntry = {
id: number;
eventType: InquiryEventType;
label: string;
actorType: CommunicationLog["actorType"];
actorId: string | null;
createdAt: Date;
metadata: Record<string, unknown> | null;
};
export type LogEventInput = {
inquiryId: number;
eventType: InquiryEventType;
actorType: CommunicationLog["actorType"];
actorId?: string | null;
visibility?: Visibility[];
metadata?: Record<string, unknown>;
};
export class CommunicationLogService {
constructor(private dal: Dal) {}
/** Append one event. Returns the created row. */
async logEvent(input: LogEventInput): Promise<CommunicationLog> {
return this.dal.communicationLogs.append({
inquiryId: input.inquiryId,
eventType: input.eventType,
actorType: input.actorType,
actorId: input.actorId ?? null,
visibility: input.visibility ?? DEFAULT_VISIBILITY[input.eventType],
metadata: input.metadata ?? null,
});
}
/**
* Best-effort append that never throws — for use inside request handlers where
* a logging failure must not break the primary action (inquiry create, reply).
* Returns true on success.
*/
async logEventSafe(input: LogEventInput): Promise<boolean> {
try {
await this.logEvent(input);
return true;
} catch {
return false;
}
}
/** Visibility-filtered timeline for a viewer. `ops` sees everything. */
async getTimeline(
inquiryId: number,
viewerRole: Visibility,
): Promise<TimelineEntry[]> {
const rows = await this.dal.communicationLogs.listByInquiry(inquiryId);
return rows
.filter(
(r) =>
viewerRole === "ops" || (r.visibility ?? []).includes(viewerRole),
)
.map((r) => ({
id: r.id,
eventType: r.eventType,
label: EVENT_LABELS[r.eventType] ?? r.eventType,
actorType: r.actorType,
actorId: r.actorId,
createdAt: r.createdAt,
metadata: r.metadata,
}));
}
static visibilityFor(eventType: InquiryEventType): Visibility[] {
return DEFAULT_VISIBILITY[eventType];
}
static labelFor(eventType: InquiryEventType): string {
return EVENT_LABELS[eventType] ?? eventType;
}
}
|