@bash-app/bash-common 30.314.0 → 30.316.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/dist/__tests__/hostCrmSegments.test.d.ts +2 -0
- package/dist/__tests__/hostCrmSegments.test.d.ts.map +1 -0
- package/dist/__tests__/hostCrmSegments.test.js +188 -0
- package/dist/__tests__/hostCrmSegments.test.js.map +1 -0
- package/dist/__tests__/relationshipHealth.test.d.ts +2 -0
- package/dist/__tests__/relationshipHealth.test.d.ts.map +1 -0
- package/dist/__tests__/relationshipHealth.test.js +265 -0
- package/dist/__tests__/relationshipHealth.test.js.map +1 -0
- package/dist/hostCrmAutomation.d.ts +64 -0
- package/dist/hostCrmAutomation.d.ts.map +1 -0
- package/dist/hostCrmAutomation.js +21 -0
- package/dist/hostCrmAutomation.js.map +1 -0
- package/dist/hostCrmInsights.d.ts +39 -0
- package/dist/hostCrmInsights.d.ts.map +1 -0
- package/dist/hostCrmInsights.js +2 -0
- package/dist/hostCrmInsights.js.map +1 -0
- package/dist/hostCrmSegments.d.ts +80 -2
- package/dist/hostCrmSegments.d.ts.map +1 -1
- package/dist/hostCrmSegments.js +84 -3
- package/dist/hostCrmSegments.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/membershipDefinitions.d.ts +2 -0
- package/dist/membershipDefinitions.d.ts.map +1 -1
- package/dist/membershipDefinitions.js +3 -0
- package/dist/membershipDefinitions.js.map +1 -1
- package/dist/relationshipHealth.d.ts +28 -0
- package/dist/relationshipHealth.d.ts.map +1 -0
- package/dist/relationshipHealth.js +91 -0
- package/dist/relationshipHealth.js.map +1 -0
- package/dist/utils/__tests__/slugUtils.test.js +9 -1
- package/dist/utils/__tests__/slugUtils.test.js.map +1 -1
- package/dist/utils/slugUtils.d.ts +7 -0
- package/dist/utils/slugUtils.d.ts.map +1 -1
- package/dist/utils/slugUtils.js +9 -0
- package/dist/utils/slugUtils.js.map +1 -1
- package/package.json +1 -1
- package/prisma/schema.prisma +126 -1
- package/src/__tests__/hostCrmSegments.test.ts +234 -0
- package/src/__tests__/relationshipHealth.test.ts +370 -0
- package/src/hostCrmAutomation.ts +79 -0
- package/src/hostCrmInsights.ts +39 -0
- package/src/hostCrmSegments.ts +196 -7
- package/src/index.ts +3 -0
- package/src/membershipDefinitions.ts +3 -0
- package/src/relationshipHealth.ts +134 -0
- package/src/utils/__tests__/slugUtils.test.ts +15 -0
- package/src/utils/slugUtils.ts +13 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hostCrmSegments.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/hostCrmSegments.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { applyHostCrmSegmentRules, HOST_CRM_PRESET_SEGMENTS, } from "../hostCrmSegments.js";
|
|
2
|
+
const NOW = new Date("2026-07-14T12:00:00Z");
|
|
3
|
+
function member(overrides = {}) {
|
|
4
|
+
return {
|
|
5
|
+
userId: overrides.userId ?? "u1",
|
|
6
|
+
email: "u@example.com",
|
|
7
|
+
name: "Guest",
|
|
8
|
+
image: null,
|
|
9
|
+
eventCount: 1,
|
|
10
|
+
eventTitles: ["Bash"],
|
|
11
|
+
tags: [],
|
|
12
|
+
notes: null,
|
|
13
|
+
profileId: null,
|
|
14
|
+
socialEventCadence: null,
|
|
15
|
+
reachableEmail: true,
|
|
16
|
+
reachableSms: false,
|
|
17
|
+
lastAttendedAt: null,
|
|
18
|
+
firstAttendedAt: null,
|
|
19
|
+
city: null,
|
|
20
|
+
tierTitles: [],
|
|
21
|
+
sourceEventIds: ["evt-a"],
|
|
22
|
+
emailOnly: false,
|
|
23
|
+
totalSpendCents: 0,
|
|
24
|
+
noShowCount: 0,
|
|
25
|
+
discountResponsive: false,
|
|
26
|
+
fastDecider: false,
|
|
27
|
+
seriesLoyalist: false,
|
|
28
|
+
refundChurn: false,
|
|
29
|
+
bringsGuests: false,
|
|
30
|
+
discoverySource: "direct",
|
|
31
|
+
expectedGapDays: null,
|
|
32
|
+
nextExpectedAt: null,
|
|
33
|
+
...overrides,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
describe("applyHostCrmSegmentRules", () => {
|
|
37
|
+
it("filters by sourceEventId (trims whitespace)", () => {
|
|
38
|
+
const audience = [
|
|
39
|
+
member({ userId: "a", sourceEventIds: ["evt-a", "evt-b"] }),
|
|
40
|
+
member({ userId: "b", sourceEventIds: ["evt-c"] }),
|
|
41
|
+
member({ userId: "c", sourceEventIds: undefined }),
|
|
42
|
+
];
|
|
43
|
+
const filtered = applyHostCrmSegmentRules(audience, {
|
|
44
|
+
sourceEventId: " evt-a ",
|
|
45
|
+
});
|
|
46
|
+
expect(filtered.map((r) => r.userId)).toEqual(["a"]);
|
|
47
|
+
});
|
|
48
|
+
it("filters by excludeEventId", () => {
|
|
49
|
+
const audience = [
|
|
50
|
+
member({ userId: "a", sourceEventIds: ["evt-a"] }),
|
|
51
|
+
member({ userId: "b", sourceEventIds: ["evt-b"] }),
|
|
52
|
+
member({ userId: "c", sourceEventIds: ["evt-a", "evt-b"] }),
|
|
53
|
+
];
|
|
54
|
+
const filtered = applyHostCrmSegmentRules(audience, {
|
|
55
|
+
excludeEventId: "evt-a",
|
|
56
|
+
});
|
|
57
|
+
expect(filtered.map((r) => r.userId)).toEqual(["b"]);
|
|
58
|
+
});
|
|
59
|
+
it("keeps rows with no lastAttendedAt when applying lapsedDays", () => {
|
|
60
|
+
const audience = [
|
|
61
|
+
member({
|
|
62
|
+
userId: "never",
|
|
63
|
+
lastAttendedAt: null,
|
|
64
|
+
}),
|
|
65
|
+
member({
|
|
66
|
+
userId: "recent",
|
|
67
|
+
lastAttendedAt: new Date("2026-07-01T00:00:00Z").toISOString(),
|
|
68
|
+
}),
|
|
69
|
+
member({
|
|
70
|
+
userId: "old",
|
|
71
|
+
lastAttendedAt: new Date("2026-01-01T00:00:00Z").toISOString(),
|
|
72
|
+
}),
|
|
73
|
+
];
|
|
74
|
+
const filtered = applyHostCrmSegmentRules(audience, { lapsedDays: 90 }, NOW);
|
|
75
|
+
expect(filtered.map((r) => r.userId).sort()).toEqual(["never", "old"]);
|
|
76
|
+
});
|
|
77
|
+
it("filters by tag, cadence, minEventCount, tierTitle, and city together", () => {
|
|
78
|
+
const audience = [
|
|
79
|
+
member({
|
|
80
|
+
userId: "keep",
|
|
81
|
+
tags: ["vip"],
|
|
82
|
+
socialEventCadence: "Often",
|
|
83
|
+
eventCount: 3,
|
|
84
|
+
tierTitles: ["GA", "VIP"],
|
|
85
|
+
city: "Brooklyn",
|
|
86
|
+
sourceEventIds: ["evt-a"],
|
|
87
|
+
}),
|
|
88
|
+
member({
|
|
89
|
+
userId: "drop-tag",
|
|
90
|
+
tags: ["regular"],
|
|
91
|
+
socialEventCadence: "Often",
|
|
92
|
+
eventCount: 3,
|
|
93
|
+
tierTitles: ["VIP"],
|
|
94
|
+
city: "Brooklyn",
|
|
95
|
+
}),
|
|
96
|
+
member({
|
|
97
|
+
userId: "drop-city",
|
|
98
|
+
tags: ["vip"],
|
|
99
|
+
socialEventCadence: "Often",
|
|
100
|
+
eventCount: 3,
|
|
101
|
+
tierTitles: ["VIP"],
|
|
102
|
+
city: "Queens",
|
|
103
|
+
}),
|
|
104
|
+
];
|
|
105
|
+
const filtered = applyHostCrmSegmentRules(audience, {
|
|
106
|
+
tag: " vip ",
|
|
107
|
+
cadence: "Often",
|
|
108
|
+
minEventCount: 2,
|
|
109
|
+
tierTitle: "vip",
|
|
110
|
+
city: "brook",
|
|
111
|
+
});
|
|
112
|
+
expect(filtered.map((r) => r.userId)).toEqual(["keep"]);
|
|
113
|
+
});
|
|
114
|
+
it("filters by sourceEventIdIn (matches any of the given event ids)", () => {
|
|
115
|
+
const audience = [
|
|
116
|
+
member({ userId: "a", sourceEventIds: ["evt-a"] }),
|
|
117
|
+
member({ userId: "b", sourceEventIds: ["evt-b"] }),
|
|
118
|
+
member({ userId: "c", sourceEventIds: ["evt-c"] }),
|
|
119
|
+
member({ userId: "d", sourceEventIds: undefined }),
|
|
120
|
+
];
|
|
121
|
+
const filtered = applyHostCrmSegmentRules(audience, {
|
|
122
|
+
sourceEventIdIn: ["evt-a", "evt-b"],
|
|
123
|
+
});
|
|
124
|
+
expect(filtered.map((r) => r.userId).sort()).toEqual(["a", "b"]);
|
|
125
|
+
});
|
|
126
|
+
it("ignores an empty sourceEventIdIn array", () => {
|
|
127
|
+
const audience = [member({ userId: "a" })];
|
|
128
|
+
expect(applyHostCrmSegmentRules(audience, { sourceEventIdIn: [] })).toHaveLength(1);
|
|
129
|
+
});
|
|
130
|
+
it("ignores blank source/exclude/tier/city/tag strings", () => {
|
|
131
|
+
const audience = [member({ userId: "a" })];
|
|
132
|
+
expect(applyHostCrmSegmentRules(audience, {
|
|
133
|
+
tag: " ",
|
|
134
|
+
sourceEventId: "",
|
|
135
|
+
excludeEventId: " ",
|
|
136
|
+
tierTitle: "",
|
|
137
|
+
city: " ",
|
|
138
|
+
})).toHaveLength(1);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
describe("HOST_CRM_PRESET_SEGMENTS", () => {
|
|
142
|
+
it("includes repeat, lapsed, spend, and behavioral presets with expected rules", () => {
|
|
143
|
+
const ids = HOST_CRM_PRESET_SEGMENTS.map((p) => p.id);
|
|
144
|
+
expect(ids).toEqual(expect.arrayContaining([
|
|
145
|
+
"repeat_guests",
|
|
146
|
+
"frequent_guest",
|
|
147
|
+
"lapsed_90",
|
|
148
|
+
"lapsed_180",
|
|
149
|
+
"top_spenders",
|
|
150
|
+
"no_show",
|
|
151
|
+
"uses_promo_codes",
|
|
152
|
+
"last_minute_buyer",
|
|
153
|
+
"refund_churn",
|
|
154
|
+
"series_regular",
|
|
155
|
+
"brings_friends",
|
|
156
|
+
"last3Events",
|
|
157
|
+
]));
|
|
158
|
+
expect(ids).not.toContain("cadence_often");
|
|
159
|
+
expect(HOST_CRM_PRESET_SEGMENTS.find((p) => p.id === "repeat_guests")?.rules).toEqual({ minEventCount: 2 });
|
|
160
|
+
expect(HOST_CRM_PRESET_SEGMENTS.find((p) => p.id === "top_spenders")?.rules).toEqual({});
|
|
161
|
+
expect(HOST_CRM_PRESET_SEGMENTS.find((p) => p.id === "no_show")?.rules).toEqual({ noShow: true });
|
|
162
|
+
// last3Events is resolved at query time (host's most recently ended
|
|
163
|
+
// events) rather than static rules — see filterHostCrmAudience.
|
|
164
|
+
expect(HOST_CRM_PRESET_SEGMENTS.find((p) => p.id === "last3Events")?.rules).toEqual({});
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
describe("applyHostCrmSegmentRules — behavioral + spend rules", () => {
|
|
168
|
+
it("filters by minTotalSpendCents, noShow, and the behavioral flags", () => {
|
|
169
|
+
const audience = [
|
|
170
|
+
member({ userId: "spender", totalSpendCents: 50000 }),
|
|
171
|
+
member({ userId: "casual", totalSpendCents: 500 }),
|
|
172
|
+
];
|
|
173
|
+
expect(applyHostCrmSegmentRules(audience, { minTotalSpendCents: 10000 }).map((r) => r.userId)).toEqual(["spender"]);
|
|
174
|
+
const behavioral = [
|
|
175
|
+
member({ userId: "loyal", seriesLoyalist: true }),
|
|
176
|
+
member({ userId: "not-loyal", seriesLoyalist: false }),
|
|
177
|
+
];
|
|
178
|
+
expect(applyHostCrmSegmentRules(behavioral, { seriesLoyalist: true }).map((r) => r.userId)).toEqual(["loyal"]);
|
|
179
|
+
});
|
|
180
|
+
it("filters by healthStatus when relationshipHealth is attached to the row", () => {
|
|
181
|
+
const audience = [
|
|
182
|
+
{ ...member({ userId: "risky" }), relationshipHealth: { status: "AtRisk", reasons: [], daysSinceLastAttended: null, expectedGapDays: null } },
|
|
183
|
+
{ ...member({ userId: "steady" }), relationshipHealth: { status: "Steady", reasons: [], daysSinceLastAttended: null, expectedGapDays: null } },
|
|
184
|
+
];
|
|
185
|
+
expect(applyHostCrmSegmentRules(audience, { healthStatus: "AtRisk" }).map((r) => r.userId)).toEqual(["risky"]);
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
//# sourceMappingURL=hostCrmSegments.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hostCrmSegments.test.js","sourceRoot":"","sources":["../../src/__tests__/hostCrmSegments.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"relationshipHealth.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/relationshipHealth.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { computeRelationshipHealth, RELATIONSHIP_HEALTH_LABELS } from "../relationshipHealth.js";
|
|
2
|
+
const NOW = new Date("2026-07-13T00:00:00Z");
|
|
3
|
+
describe("computeRelationshipHealth", () => {
|
|
4
|
+
it("returns New with no attendance history", () => {
|
|
5
|
+
const result = computeRelationshipHealth({ eventCount: 0, lastAttendedAt: null, firstAttendedAt: null, tags: [], lastOutreachAt: null }, NOW);
|
|
6
|
+
expect(result.status).toBe("New");
|
|
7
|
+
expect(result.reasons.join(" ")).toMatch(/no attended bashes/i);
|
|
8
|
+
});
|
|
9
|
+
it("returns New for a single recent attendance", () => {
|
|
10
|
+
const result = computeRelationshipHealth({
|
|
11
|
+
eventCount: 1,
|
|
12
|
+
lastAttendedAt: new Date("2026-07-01T00:00:00Z").toISOString(),
|
|
13
|
+
firstAttendedAt: new Date("2026-07-01T00:00:00Z").toISOString(),
|
|
14
|
+
tags: [],
|
|
15
|
+
lastOutreachAt: null,
|
|
16
|
+
}, NOW);
|
|
17
|
+
expect(result.status).toBe("New");
|
|
18
|
+
});
|
|
19
|
+
it("returns Cooling for a single attendance that lapsed a few months", () => {
|
|
20
|
+
const result = computeRelationshipHealth({
|
|
21
|
+
eventCount: 1,
|
|
22
|
+
lastAttendedAt: new Date("2026-04-01T00:00:00Z").toISOString(),
|
|
23
|
+
firstAttendedAt: new Date("2026-04-01T00:00:00Z").toISOString(),
|
|
24
|
+
tags: [],
|
|
25
|
+
lastOutreachAt: null,
|
|
26
|
+
}, NOW);
|
|
27
|
+
expect(result.status).toBe("Cooling");
|
|
28
|
+
});
|
|
29
|
+
it("returns AtRisk for a single attendance that lapsed a long time ago", () => {
|
|
30
|
+
const result = computeRelationshipHealth({
|
|
31
|
+
eventCount: 1,
|
|
32
|
+
lastAttendedAt: new Date("2025-01-01T00:00:00Z").toISOString(),
|
|
33
|
+
firstAttendedAt: new Date("2025-01-01T00:00:00Z").toISOString(),
|
|
34
|
+
tags: [],
|
|
35
|
+
lastOutreachAt: null,
|
|
36
|
+
}, NOW);
|
|
37
|
+
expect(result.status).toBe("AtRisk");
|
|
38
|
+
});
|
|
39
|
+
it("returns Steady for a regular attendee within their own cadence", () => {
|
|
40
|
+
const result = computeRelationshipHealth({
|
|
41
|
+
eventCount: 5,
|
|
42
|
+
firstAttendedAt: new Date("2026-01-13T00:00:00Z").toISOString(),
|
|
43
|
+
lastAttendedAt: new Date("2026-06-23T00:00:00Z").toISOString(),
|
|
44
|
+
tags: [],
|
|
45
|
+
lastOutreachAt: null,
|
|
46
|
+
}, NOW);
|
|
47
|
+
expect(result.status).toBe("Steady");
|
|
48
|
+
expect(result.expectedGapDays).not.toBeNull();
|
|
49
|
+
});
|
|
50
|
+
it("returns Cooling then AtRisk as the gap relative to cadence grows", () => {
|
|
51
|
+
const base = {
|
|
52
|
+
eventCount: 4,
|
|
53
|
+
firstAttendedAt: new Date("2026-01-01T00:00:00Z").toISOString(),
|
|
54
|
+
tags: [],
|
|
55
|
+
lastOutreachAt: null,
|
|
56
|
+
};
|
|
57
|
+
// span Jan1→May1 = 120 days over 3 gaps → expectedGapDays = 40;
|
|
58
|
+
// daysSince (NOW=Jul13) = 73 → ratio ~1.83 → Cooling
|
|
59
|
+
const cooling = computeRelationshipHealth({ ...base, lastAttendedAt: new Date("2026-05-01T00:00:00Z").toISOString() }, NOW);
|
|
60
|
+
expect(cooling.status).toBe("Cooling");
|
|
61
|
+
// span Jan1→Feb1 = 31 days over 3 gaps → expectedGapDays clamps to the 14-day
|
|
62
|
+
// floor; daysSince = 162 → ratio ~11.6 → AtRisk
|
|
63
|
+
const atRisk = computeRelationshipHealth({ ...base, lastAttendedAt: new Date("2026-02-01T00:00:00Z").toISOString() }, NOW);
|
|
64
|
+
expect(atRisk.status).toBe("AtRisk");
|
|
65
|
+
});
|
|
66
|
+
it("escalates a Cooling VIP to AtRisk and records why", () => {
|
|
67
|
+
const withoutVip = computeRelationshipHealth({
|
|
68
|
+
eventCount: 1,
|
|
69
|
+
lastAttendedAt: new Date("2026-04-01T00:00:00Z").toISOString(),
|
|
70
|
+
firstAttendedAt: new Date("2026-04-01T00:00:00Z").toISOString(),
|
|
71
|
+
tags: [],
|
|
72
|
+
lastOutreachAt: null,
|
|
73
|
+
}, NOW);
|
|
74
|
+
expect(withoutVip.status).toBe("Cooling");
|
|
75
|
+
const withVip = computeRelationshipHealth({
|
|
76
|
+
eventCount: 1,
|
|
77
|
+
lastAttendedAt: new Date("2026-04-01T00:00:00Z").toISOString(),
|
|
78
|
+
firstAttendedAt: new Date("2026-04-01T00:00:00Z").toISOString(),
|
|
79
|
+
tags: ["VIP"],
|
|
80
|
+
lastOutreachAt: null,
|
|
81
|
+
}, NOW);
|
|
82
|
+
expect(withVip.status).toBe("AtRisk");
|
|
83
|
+
expect(withVip.reasons.some((r) => /vip/i.test(r))).toBe(true);
|
|
84
|
+
});
|
|
85
|
+
it("includes outreach recency in the inspectable reasons without changing the bucket", () => {
|
|
86
|
+
const result = computeRelationshipHealth({
|
|
87
|
+
eventCount: 5,
|
|
88
|
+
firstAttendedAt: new Date("2026-01-13T00:00:00Z").toISOString(),
|
|
89
|
+
lastAttendedAt: new Date("2026-06-23T00:00:00Z").toISOString(),
|
|
90
|
+
tags: [],
|
|
91
|
+
lastOutreachAt: new Date("2026-07-10T00:00:00Z").toISOString(),
|
|
92
|
+
}, NOW);
|
|
93
|
+
expect(result.status).toBe("Steady");
|
|
94
|
+
expect(result.reasons.some((r) => /contacted/i.test(r))).toBe(true);
|
|
95
|
+
});
|
|
96
|
+
it("treats a single attendance with no lastAttendedAt as New", () => {
|
|
97
|
+
const result = computeRelationshipHealth({
|
|
98
|
+
eventCount: 1,
|
|
99
|
+
lastAttendedAt: null,
|
|
100
|
+
firstAttendedAt: null,
|
|
101
|
+
tags: [],
|
|
102
|
+
lastOutreachAt: null,
|
|
103
|
+
}, NOW);
|
|
104
|
+
expect(result.status).toBe("New");
|
|
105
|
+
expect(result.daysSinceLastAttended).toBeNull();
|
|
106
|
+
});
|
|
107
|
+
it("uses the default cadence when firstAttendedAt is missing for multi-event guests", () => {
|
|
108
|
+
const result = computeRelationshipHealth({
|
|
109
|
+
eventCount: 3,
|
|
110
|
+
firstAttendedAt: null,
|
|
111
|
+
lastAttendedAt: new Date("2026-07-01T00:00:00Z").toISOString(),
|
|
112
|
+
tags: [],
|
|
113
|
+
lastOutreachAt: null,
|
|
114
|
+
}, NOW);
|
|
115
|
+
expect(result.status).toBe("Steady");
|
|
116
|
+
expect(result.expectedGapDays).toBe(60);
|
|
117
|
+
expect(result.reasons.some((r) => /default 60-day cadence/i.test(r))).toBe(true);
|
|
118
|
+
});
|
|
119
|
+
it("uses the default cadence when first and last attendance are the same day", () => {
|
|
120
|
+
const same = new Date("2026-06-01T00:00:00Z").toISOString();
|
|
121
|
+
const result = computeRelationshipHealth({
|
|
122
|
+
eventCount: 3,
|
|
123
|
+
firstAttendedAt: same,
|
|
124
|
+
lastAttendedAt: same,
|
|
125
|
+
tags: [],
|
|
126
|
+
lastOutreachAt: null,
|
|
127
|
+
}, NOW);
|
|
128
|
+
expect(result.expectedGapDays).toBe(60);
|
|
129
|
+
expect(result.reasons.some((r) => /default 60-day cadence/i.test(r))).toBe(true);
|
|
130
|
+
});
|
|
131
|
+
it("clamps personalized cadence to a 14-day minimum", () => {
|
|
132
|
+
// 3 bashes over 10 days → raw gap 5, clamped to 14
|
|
133
|
+
const result = computeRelationshipHealth({
|
|
134
|
+
eventCount: 3,
|
|
135
|
+
firstAttendedAt: new Date("2026-07-01T00:00:00Z").toISOString(),
|
|
136
|
+
lastAttendedAt: new Date("2026-07-11T00:00:00Z").toISOString(),
|
|
137
|
+
tags: [],
|
|
138
|
+
lastOutreachAt: null,
|
|
139
|
+
}, NOW);
|
|
140
|
+
expect(result.expectedGapDays).toBe(14);
|
|
141
|
+
expect(result.status).toBe("Steady");
|
|
142
|
+
});
|
|
143
|
+
it("does not escalate a Steady VIP", () => {
|
|
144
|
+
const result = computeRelationshipHealth({
|
|
145
|
+
eventCount: 5,
|
|
146
|
+
firstAttendedAt: new Date("2026-01-13T00:00:00Z").toISOString(),
|
|
147
|
+
lastAttendedAt: new Date("2026-06-23T00:00:00Z").toISOString(),
|
|
148
|
+
tags: [" VIP "],
|
|
149
|
+
lastOutreachAt: null,
|
|
150
|
+
}, NOW);
|
|
151
|
+
expect(result.status).toBe("Steady");
|
|
152
|
+
expect(result.reasons.some((r) => /vip/i.test(r))).toBe(false);
|
|
153
|
+
});
|
|
154
|
+
it("notes missing outreach for Cooling and AtRisk without lastOutreachAt", () => {
|
|
155
|
+
const cooling = computeRelationshipHealth({
|
|
156
|
+
eventCount: 1,
|
|
157
|
+
lastAttendedAt: new Date("2026-04-01T00:00:00Z").toISOString(),
|
|
158
|
+
firstAttendedAt: new Date("2026-04-01T00:00:00Z").toISOString(),
|
|
159
|
+
tags: [],
|
|
160
|
+
lastOutreachAt: null,
|
|
161
|
+
}, NOW);
|
|
162
|
+
expect(cooling.status).toBe("Cooling");
|
|
163
|
+
expect(cooling.reasons.some((r) => /no outreach/i.test(r))).toBe(true);
|
|
164
|
+
const atRisk = computeRelationshipHealth({
|
|
165
|
+
eventCount: 1,
|
|
166
|
+
lastAttendedAt: new Date("2025-01-01T00:00:00Z").toISOString(),
|
|
167
|
+
firstAttendedAt: new Date("2025-01-01T00:00:00Z").toISOString(),
|
|
168
|
+
tags: [],
|
|
169
|
+
lastOutreachAt: null,
|
|
170
|
+
}, NOW);
|
|
171
|
+
expect(atRisk.status).toBe("AtRisk");
|
|
172
|
+
expect(atRisk.reasons.some((r) => /no outreach/i.test(r))).toBe(true);
|
|
173
|
+
});
|
|
174
|
+
it("uses singular day wording for a one-day gap", () => {
|
|
175
|
+
const result = computeRelationshipHealth({
|
|
176
|
+
eventCount: 1,
|
|
177
|
+
lastAttendedAt: new Date("2026-07-12T00:00:00Z").toISOString(),
|
|
178
|
+
firstAttendedAt: new Date("2026-07-12T00:00:00Z").toISOString(),
|
|
179
|
+
tags: [],
|
|
180
|
+
lastOutreachAt: new Date("2026-07-12T00:00:00Z").toISOString(),
|
|
181
|
+
}, NOW);
|
|
182
|
+
expect(result.reasons.some((r) => r.includes("1 day ago"))).toBe(true);
|
|
183
|
+
expect(result.reasons.some((r) => r.includes("1 days ago"))).toBe(false);
|
|
184
|
+
});
|
|
185
|
+
it("exposes human labels for every status", () => {
|
|
186
|
+
expect(RELATIONSHIP_HEALTH_LABELS.New).toBe("New");
|
|
187
|
+
expect(RELATIONSHIP_HEALTH_LABELS.Steady).toBe("Steady");
|
|
188
|
+
expect(RELATIONSHIP_HEALTH_LABELS.Cooling).toBe("Cooling");
|
|
189
|
+
expect(RELATIONSHIP_HEALTH_LABELS.AtRisk).toBe("At risk");
|
|
190
|
+
});
|
|
191
|
+
it("treats multi-event guests with no lastAttendedAt as Steady (ratio 0)", () => {
|
|
192
|
+
const result = computeRelationshipHealth({
|
|
193
|
+
eventCount: 2,
|
|
194
|
+
firstAttendedAt: new Date("2026-01-01T00:00:00Z").toISOString(),
|
|
195
|
+
lastAttendedAt: null,
|
|
196
|
+
tags: [],
|
|
197
|
+
lastOutreachAt: null,
|
|
198
|
+
}, NOW);
|
|
199
|
+
expect(result.status).toBe("Steady");
|
|
200
|
+
expect(result.daysSinceLastAttended).toBeNull();
|
|
201
|
+
});
|
|
202
|
+
it("uses exact day boundaries for the single-attendance windows", () => {
|
|
203
|
+
const stillNew = computeRelationshipHealth({
|
|
204
|
+
eventCount: 1,
|
|
205
|
+
lastAttendedAt: new Date("2026-05-29T00:00:00Z").toISOString(), // 45 days before NOW
|
|
206
|
+
firstAttendedAt: new Date("2026-05-29T00:00:00Z").toISOString(),
|
|
207
|
+
tags: [],
|
|
208
|
+
lastOutreachAt: null,
|
|
209
|
+
}, NOW);
|
|
210
|
+
expect(stillNew.status).toBe("New");
|
|
211
|
+
const coolingEdge = computeRelationshipHealth({
|
|
212
|
+
eventCount: 1,
|
|
213
|
+
lastAttendedAt: new Date("2026-05-28T00:00:00Z").toISOString(), // 46 days
|
|
214
|
+
firstAttendedAt: new Date("2026-05-28T00:00:00Z").toISOString(),
|
|
215
|
+
tags: [],
|
|
216
|
+
lastOutreachAt: null,
|
|
217
|
+
}, NOW);
|
|
218
|
+
expect(coolingEdge.status).toBe("Cooling");
|
|
219
|
+
const stillCooling = computeRelationshipHealth({
|
|
220
|
+
eventCount: 1,
|
|
221
|
+
lastAttendedAt: new Date("2026-03-15T00:00:00Z").toISOString(), // 120 days
|
|
222
|
+
firstAttendedAt: new Date("2026-03-15T00:00:00Z").toISOString(),
|
|
223
|
+
tags: [],
|
|
224
|
+
lastOutreachAt: null,
|
|
225
|
+
}, NOW);
|
|
226
|
+
expect(stillCooling.status).toBe("Cooling");
|
|
227
|
+
const atRiskEdge = computeRelationshipHealth({
|
|
228
|
+
eventCount: 1,
|
|
229
|
+
lastAttendedAt: new Date("2026-03-14T00:00:00Z").toISOString(), // 121 days
|
|
230
|
+
firstAttendedAt: new Date("2026-03-14T00:00:00Z").toISOString(),
|
|
231
|
+
tags: [],
|
|
232
|
+
lastOutreachAt: null,
|
|
233
|
+
}, NOW);
|
|
234
|
+
expect(atRiskEdge.status).toBe("AtRisk");
|
|
235
|
+
});
|
|
236
|
+
it("keeps Steady at the 1.5x cadence ratio and Cooling just above it", () => {
|
|
237
|
+
// span Jan14→May14 = 120 days over 3 gaps → expectedGapDays = 40
|
|
238
|
+
const base = {
|
|
239
|
+
eventCount: 4,
|
|
240
|
+
firstAttendedAt: new Date("2026-01-14T00:00:00Z").toISOString(),
|
|
241
|
+
tags: [],
|
|
242
|
+
lastOutreachAt: null,
|
|
243
|
+
};
|
|
244
|
+
// daysSince (NOW=Jul13) = 60 → ratio exactly 1.5 → Steady
|
|
245
|
+
const steady = computeRelationshipHealth({ ...base, lastAttendedAt: new Date("2026-05-14T00:00:00Z").toISOString() }, NOW);
|
|
246
|
+
expect(steady.expectedGapDays).toBe(40);
|
|
247
|
+
expect(steady.status).toBe("Steady");
|
|
248
|
+
// one day later lapse → daysSince = 61, span drops to 119 (still rounds to
|
|
249
|
+
// the same 40-day gap) → ratio just above 1.5 → Cooling
|
|
250
|
+
const cooling = computeRelationshipHealth({ ...base, lastAttendedAt: new Date("2026-05-13T00:00:00Z").toISOString() }, NOW);
|
|
251
|
+
expect(cooling.status).toBe("Cooling");
|
|
252
|
+
});
|
|
253
|
+
it("does not re-escalate an AtRisk VIP (already past Cooling)", () => {
|
|
254
|
+
const result = computeRelationshipHealth({
|
|
255
|
+
eventCount: 1,
|
|
256
|
+
lastAttendedAt: new Date("2025-01-01T00:00:00Z").toISOString(),
|
|
257
|
+
firstAttendedAt: new Date("2025-01-01T00:00:00Z").toISOString(),
|
|
258
|
+
tags: ["vip"],
|
|
259
|
+
lastOutreachAt: null,
|
|
260
|
+
}, NOW);
|
|
261
|
+
expect(result.status).toBe("AtRisk");
|
|
262
|
+
expect(result.reasons.some((r) => /escalated from Cooling/i.test(r))).toBe(false);
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
//# sourceMappingURL=relationshipHealth.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"relationshipHealth.test.js","sourceRoot":"","sources":["../../src/__tests__/relationshipHealth.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { HostCrmAutomationAudienceType as HostCrmAutomationAudienceTypeEnum, HostCrmAutomationChannel as HostCrmAutomationChannelEnum, HostCrmAutomationTrigger as HostCrmAutomationTriggerEnum } from "@prisma/client";
|
|
2
|
+
/** Mirrors Prisma `HostCrmAutomationTrigger` — `@prisma/client` browser bundle omits runtime enum exports. */
|
|
3
|
+
export declare const HostCrmAutomationTrigger: {
|
|
4
|
+
readonly Recurring: "Recurring";
|
|
5
|
+
readonly NewlyAtRisk: "NewlyAtRisk";
|
|
6
|
+
readonly PostEvent: "PostEvent";
|
|
7
|
+
};
|
|
8
|
+
export type HostCrmAutomationTrigger = HostCrmAutomationTriggerEnum;
|
|
9
|
+
/** Mirrors Prisma `HostCrmAutomationChannel` — `@prisma/client` browser bundle omits runtime enum exports. */
|
|
10
|
+
export declare const HostCrmAutomationChannel: {
|
|
11
|
+
readonly Email: "Email";
|
|
12
|
+
readonly Sms: "Sms";
|
|
13
|
+
};
|
|
14
|
+
export type HostCrmAutomationChannel = HostCrmAutomationChannelEnum;
|
|
15
|
+
/** Mirrors Prisma `HostCrmAutomationAudienceType` — `@prisma/client` browser bundle omits runtime enum exports. */
|
|
16
|
+
export declare const HostCrmAutomationAudienceType: {
|
|
17
|
+
readonly Preset: "Preset";
|
|
18
|
+
readonly Segment: "Segment";
|
|
19
|
+
};
|
|
20
|
+
export type HostCrmAutomationAudienceType = HostCrmAutomationAudienceTypeEnum;
|
|
21
|
+
/** Minimum days between two runs of a Recurring automation. */
|
|
22
|
+
export declare const HOST_CRM_AUTOMATION_MIN_CADENCE_DAYS = 1;
|
|
23
|
+
/** Default cadence for a newly created Recurring automation, matching the legacy win-back cron interval. */
|
|
24
|
+
export declare const HOST_CRM_AUTOMATION_DEFAULT_CADENCE_DAYS = 30;
|
|
25
|
+
/**
|
|
26
|
+
* API/client shape of a `HostCrmAutomation` row — dates are ISO strings (as
|
|
27
|
+
* returned by the JSON API), not the raw Prisma `Date` type. See
|
|
28
|
+
* `api/src/api/user/privateUser/hostCrmAutomationApi.ts` for the endpoints
|
|
29
|
+
* that produce this shape and `bash-app/src/dataLayer/user/attendeeAudienceFetcher.ts`
|
|
30
|
+
* for the client fetchers.
|
|
31
|
+
*/
|
|
32
|
+
export type HostCrmAutomation = {
|
|
33
|
+
id: string;
|
|
34
|
+
hostUserId: string;
|
|
35
|
+
name: string;
|
|
36
|
+
trigger: HostCrmAutomationTrigger;
|
|
37
|
+
channel: HostCrmAutomationChannel;
|
|
38
|
+
/** Ignored for PostEvent — audience there is implicit (that event's ticket holders). */
|
|
39
|
+
audienceType: HostCrmAutomationAudienceType | null;
|
|
40
|
+
audiencePresetId: string | null;
|
|
41
|
+
audienceSegmentId: string | null;
|
|
42
|
+
subject: string | null;
|
|
43
|
+
body: string;
|
|
44
|
+
/** Only used by the Recurring trigger. */
|
|
45
|
+
cadenceDays: number | null;
|
|
46
|
+
enabled: boolean;
|
|
47
|
+
lastRunAt: string | null;
|
|
48
|
+
createdAt: string;
|
|
49
|
+
updatedAt: string;
|
|
50
|
+
};
|
|
51
|
+
/** Payload for creating or fully updating a `HostCrmAutomation`. */
|
|
52
|
+
export type HostCrmAutomationInput = {
|
|
53
|
+
name: string;
|
|
54
|
+
trigger: HostCrmAutomationTrigger;
|
|
55
|
+
channel: HostCrmAutomationChannel;
|
|
56
|
+
audienceType?: HostCrmAutomationAudienceType | null;
|
|
57
|
+
audiencePresetId?: string | null;
|
|
58
|
+
audienceSegmentId?: string | null;
|
|
59
|
+
subject?: string | null;
|
|
60
|
+
body: string;
|
|
61
|
+
cadenceDays?: number | null;
|
|
62
|
+
enabled?: boolean;
|
|
63
|
+
};
|
|
64
|
+
//# sourceMappingURL=hostCrmAutomation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hostCrmAutomation.d.ts","sourceRoot":"","sources":["../src/hostCrmAutomation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,6BAA6B,IAAI,iCAAiC,EAClE,wBAAwB,IAAI,4BAA4B,EACxD,wBAAwB,IAAI,4BAA4B,EACzD,MAAM,gBAAgB,CAAC;AAExB,8GAA8G;AAC9G,eAAO,MAAM,wBAAwB;;;;CAIkD,CAAC;AAExF,MAAM,MAAM,wBAAwB,GAAG,4BAA4B,CAAC;AAEpE,8GAA8G;AAC9G,eAAO,MAAM,wBAAwB;;;CAGkD,CAAC;AAExF,MAAM,MAAM,wBAAwB,GAAG,4BAA4B,CAAC;AAEpE,mHAAmH;AACnH,eAAO,MAAM,6BAA6B;;;CAMzC,CAAC;AAEF,MAAM,MAAM,6BAA6B,GAAG,iCAAiC,CAAC;AAE9E,+DAA+D;AAC/D,eAAO,MAAM,oCAAoC,IAAI,CAAC;AACtD,4GAA4G;AAC5G,eAAO,MAAM,wCAAwC,KAAK,CAAC;AAE3D;;;;;;GAMG;AACH,MAAM,MAAM,iBAAiB,GAAG;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,wBAAwB,CAAC;IAClC,OAAO,EAAE,wBAAwB,CAAC;IAClC,wFAAwF;IACxF,YAAY,EAAE,6BAA6B,GAAG,IAAI,CAAC;IACnD,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,0CAA0C;IAC1C,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,oEAAoE;AACpE,MAAM,MAAM,sBAAsB,GAAG;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,wBAAwB,CAAC;IAClC,OAAO,EAAE,wBAAwB,CAAC;IAClC,YAAY,CAAC,EAAE,6BAA6B,GAAG,IAAI,CAAC;IACpD,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,CAAC"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Mirrors Prisma `HostCrmAutomationTrigger` — `@prisma/client` browser bundle omits runtime enum exports. */
|
|
2
|
+
export const HostCrmAutomationTrigger = {
|
|
3
|
+
Recurring: "Recurring",
|
|
4
|
+
NewlyAtRisk: "NewlyAtRisk",
|
|
5
|
+
PostEvent: "PostEvent",
|
|
6
|
+
};
|
|
7
|
+
/** Mirrors Prisma `HostCrmAutomationChannel` — `@prisma/client` browser bundle omits runtime enum exports. */
|
|
8
|
+
export const HostCrmAutomationChannel = {
|
|
9
|
+
Email: "Email",
|
|
10
|
+
Sms: "Sms",
|
|
11
|
+
};
|
|
12
|
+
/** Mirrors Prisma `HostCrmAutomationAudienceType` — `@prisma/client` browser bundle omits runtime enum exports. */
|
|
13
|
+
export const HostCrmAutomationAudienceType = {
|
|
14
|
+
Preset: "Preset",
|
|
15
|
+
Segment: "Segment",
|
|
16
|
+
};
|
|
17
|
+
/** Minimum days between two runs of a Recurring automation. */
|
|
18
|
+
export const HOST_CRM_AUTOMATION_MIN_CADENCE_DAYS = 1;
|
|
19
|
+
/** Default cadence for a newly created Recurring automation, matching the legacy win-back cron interval. */
|
|
20
|
+
export const HOST_CRM_AUTOMATION_DEFAULT_CADENCE_DAYS = 30;
|
|
21
|
+
//# sourceMappingURL=hostCrmAutomation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hostCrmAutomation.js","sourceRoot":"","sources":["../src/hostCrmAutomation.ts"],"names":[],"mappings":"AAMA,8GAA8G;AAC9G,MAAM,CAAC,MAAM,wBAAwB,GAAG;IACtC,SAAS,EAAE,WAAW;IACtB,WAAW,EAAE,aAAa;IAC1B,SAAS,EAAE,WAAW;CAC+D,CAAC;AAIxF,8GAA8G;AAC9G,MAAM,CAAC,MAAM,wBAAwB,GAAG;IACtC,KAAK,EAAE,OAAO;IACd,GAAG,EAAE,KAAK;CAC2E,CAAC;AAIxF,mHAAmH;AACnH,MAAM,CAAC,MAAM,6BAA6B,GAAG;IAC3C,MAAM,EAAE,QAAQ;IAChB,OAAO,EAAE,SAAS;CAInB,CAAC;AAIF,+DAA+D;AAC/D,MAAM,CAAC,MAAM,oCAAoC,GAAG,CAAC,CAAC;AACtD,4GAA4G;AAC5G,MAAM,CAAC,MAAM,wCAAwC,GAAG,EAAE,CAAC"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { MembershipTier } from "./membershipDefinitions.js";
|
|
2
|
+
/**
|
|
3
|
+
* One curated predictive-insight observation for the Attendee CRM's
|
|
4
|
+
* Recommended-actions accordion. Deliberately a discriminated union of
|
|
5
|
+
* discrete, computed facts (not prose) — same design choice already made for
|
|
6
|
+
* `HostCrmTimelineEntry` — so the drawer/card renders each fact directly and
|
|
7
|
+
* a future AI layer can consume the same structured data without re-deriving it.
|
|
8
|
+
*/
|
|
9
|
+
export type HostCrmInsight = {
|
|
10
|
+
type: "MostLikelyToReturn";
|
|
11
|
+
nextEventTitle: string;
|
|
12
|
+
nextEventStartsAt: string;
|
|
13
|
+
attendeeCount: number;
|
|
14
|
+
} | {
|
|
15
|
+
type: "AttendanceTrend";
|
|
16
|
+
direction: "up" | "down" | "flat";
|
|
17
|
+
percent: number;
|
|
18
|
+
recentCount: number;
|
|
19
|
+
priorCount: number;
|
|
20
|
+
} | {
|
|
21
|
+
type: "DayOfWeekRetention";
|
|
22
|
+
bestDay: string;
|
|
23
|
+
worstDay: string;
|
|
24
|
+
bestRepeatRate: number;
|
|
25
|
+
worstRepeatRate: number;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Response shape for the Predictive Insights card. Gated server-side —
|
|
29
|
+
* `locked: true` hosts never receive computed `insights`, so a Basic-tier
|
|
30
|
+
* host can't see real data even by inspecting the network response.
|
|
31
|
+
*/
|
|
32
|
+
export type HostCrmInsightsResult = {
|
|
33
|
+
locked: true;
|
|
34
|
+
requiredTier: MembershipTier;
|
|
35
|
+
} | {
|
|
36
|
+
locked: false;
|
|
37
|
+
insights: HostCrmInsight[];
|
|
38
|
+
};
|
|
39
|
+
//# sourceMappingURL=hostCrmInsights.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hostCrmInsights.d.ts","sourceRoot":"","sources":["../src/hostCrmInsights.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAEjE;;;;;;GAMG;AACH,MAAM,MAAM,cAAc,GACtB;IACE,IAAI,EAAE,oBAAoB,CAAC;IAC3B,cAAc,EAAE,MAAM,CAAC;IACvB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,aAAa,EAAE,MAAM,CAAC;CACvB,GACD;IACE,IAAI,EAAE,iBAAiB,CAAC;IACxB,SAAS,EAAE,IAAI,GAAG,MAAM,GAAG,MAAM,CAAC;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;CACpB,GACD;IACE,IAAI,EAAE,oBAAoB,CAAC;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,EAAE,MAAM,CAAC;IACvB,eAAe,EAAE,MAAM,CAAC;CACzB,CAAC;AAEN;;;;GAIG;AACH,MAAM,MAAM,qBAAqB,GAC7B;IAAE,MAAM,EAAE,IAAI,CAAC;IAAC,YAAY,EAAE,cAAc,CAAA;CAAE,GAC9C;IAAE,MAAM,EAAE,KAAK,CAAC;IAAC,QAAQ,EAAE,cAAc,EAAE,CAAA;CAAE,CAAC"}
|