@contractspec/example.crm-pipeline 0.0.0-canary-20260113170453

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.
Files changed (206) hide show
  1. package/.turbo/turbo-build$colon$bundle.log +172 -0
  2. package/.turbo/turbo-build.log +173 -0
  3. package/CHANGELOG.md +436 -0
  4. package/LICENSE +21 -0
  5. package/README.md +139 -0
  6. package/dist/crm-pipeline.feature.d.ts +12 -0
  7. package/dist/crm-pipeline.feature.d.ts.map +1 -0
  8. package/dist/crm-pipeline.feature.js +166 -0
  9. package/dist/crm-pipeline.feature.js.map +1 -0
  10. package/dist/deal/deal.enum.d.ts +14 -0
  11. package/dist/deal/deal.enum.d.ts.map +1 -0
  12. package/dist/deal/deal.enum.js +25 -0
  13. package/dist/deal/deal.enum.js.map +1 -0
  14. package/dist/deal/deal.operation.d.ts +513 -0
  15. package/dist/deal/deal.operation.d.ts.map +1 -0
  16. package/dist/deal/deal.operation.js +270 -0
  17. package/dist/deal/deal.operation.js.map +1 -0
  18. package/dist/deal/deal.schema.d.ts +300 -0
  19. package/dist/deal/deal.schema.d.ts.map +1 -0
  20. package/dist/deal/deal.schema.js +286 -0
  21. package/dist/deal/deal.schema.js.map +1 -0
  22. package/dist/deal/deal.test-spec.d.ts +8 -0
  23. package/dist/deal/deal.test-spec.d.ts.map +1 -0
  24. package/dist/deal/deal.test-spec.js +65 -0
  25. package/dist/deal/deal.test-spec.js.map +1 -0
  26. package/dist/deal/index.d.ts +4 -0
  27. package/dist/deal/index.js +5 -0
  28. package/dist/docs/crm-pipeline.docblock.d.ts +1 -0
  29. package/dist/docs/crm-pipeline.docblock.js +100 -0
  30. package/dist/docs/crm-pipeline.docblock.js.map +1 -0
  31. package/dist/docs/index.d.ts +1 -0
  32. package/dist/docs/index.js +1 -0
  33. package/dist/entities/company.entity.d.ts +40 -0
  34. package/dist/entities/company.entity.d.ts.map +1 -0
  35. package/dist/entities/company.entity.js +63 -0
  36. package/dist/entities/company.entity.js.map +1 -0
  37. package/dist/entities/contact.entity.d.ts +44 -0
  38. package/dist/entities/contact.entity.d.ts.map +1 -0
  39. package/dist/entities/contact.entity.js +78 -0
  40. package/dist/entities/contact.entity.js.map +1 -0
  41. package/dist/entities/deal.entity.d.ts +73 -0
  42. package/dist/entities/deal.entity.d.ts.map +1 -0
  43. package/dist/entities/deal.entity.js +120 -0
  44. package/dist/entities/deal.entity.js.map +1 -0
  45. package/dist/entities/index.d.ts +15 -0
  46. package/dist/entities/index.d.ts.map +1 -0
  47. package/dist/entities/index.js +33 -0
  48. package/dist/entities/index.js.map +1 -0
  49. package/dist/entities/task.entity.d.ts +65 -0
  50. package/dist/entities/task.entity.d.ts.map +1 -0
  51. package/dist/entities/task.entity.js +129 -0
  52. package/dist/entities/task.entity.js.map +1 -0
  53. package/dist/events/contact.event.d.ts +29 -0
  54. package/dist/events/contact.event.d.ts.map +1 -0
  55. package/dist/events/contact.event.js +45 -0
  56. package/dist/events/contact.event.js.map +1 -0
  57. package/dist/events/deal.event.d.ts +111 -0
  58. package/dist/events/deal.event.d.ts.map +1 -0
  59. package/dist/events/deal.event.js +172 -0
  60. package/dist/events/deal.event.js.map +1 -0
  61. package/dist/events/index.d.ts +4 -0
  62. package/dist/events/index.js +5 -0
  63. package/dist/events/task.event.d.ts +29 -0
  64. package/dist/events/task.event.d.ts.map +1 -0
  65. package/dist/events/task.event.js +45 -0
  66. package/dist/events/task.event.js.map +1 -0
  67. package/dist/example.d.ts +7 -0
  68. package/dist/example.d.ts.map +1 -0
  69. package/dist/example.js +53 -0
  70. package/dist/example.js.map +1 -0
  71. package/dist/handlers/crm.handlers.d.ts +89 -0
  72. package/dist/handlers/crm.handlers.d.ts.map +1 -0
  73. package/dist/handlers/crm.handlers.js +172 -0
  74. package/dist/handlers/crm.handlers.js.map +1 -0
  75. package/dist/handlers/deal.handlers.d.ts +94 -0
  76. package/dist/handlers/deal.handlers.d.ts.map +1 -0
  77. package/dist/handlers/deal.handlers.js +120 -0
  78. package/dist/handlers/deal.handlers.js.map +1 -0
  79. package/dist/handlers/index.d.ts +4 -0
  80. package/dist/handlers/index.js +5 -0
  81. package/dist/handlers/mock-data.d.ts +49 -0
  82. package/dist/handlers/mock-data.d.ts.map +1 -0
  83. package/dist/handlers/mock-data.js +188 -0
  84. package/dist/handlers/mock-data.js.map +1 -0
  85. package/dist/index.d.ts +47 -0
  86. package/dist/index.d.ts.map +1 -0
  87. package/dist/index.js +56 -0
  88. package/dist/index.js.map +1 -0
  89. package/dist/operations/index.d.ts +5 -0
  90. package/dist/operations/index.js +6 -0
  91. package/dist/presentations/dashboard.presentation.d.ts +14 -0
  92. package/dist/presentations/dashboard.presentation.d.ts.map +1 -0
  93. package/dist/presentations/dashboard.presentation.js +62 -0
  94. package/dist/presentations/dashboard.presentation.js.map +1 -0
  95. package/dist/presentations/index.d.ts +3 -0
  96. package/dist/presentations/index.js +4 -0
  97. package/dist/presentations/pipeline.presentation.d.ts +22 -0
  98. package/dist/presentations/pipeline.presentation.d.ts.map +1 -0
  99. package/dist/presentations/pipeline.presentation.js +122 -0
  100. package/dist/presentations/pipeline.presentation.js.map +1 -0
  101. package/dist/seeders/index.d.ts +10 -0
  102. package/dist/seeders/index.d.ts.map +1 -0
  103. package/dist/seeders/index.js +47 -0
  104. package/dist/seeders/index.js.map +1 -0
  105. package/dist/shared/overlay-types.d.ts +34 -0
  106. package/dist/shared/overlay-types.d.ts.map +1 -0
  107. package/dist/shared/overlay-types.js +0 -0
  108. package/dist/ui/CrmDashboard.d.ts +7 -0
  109. package/dist/ui/CrmDashboard.d.ts.map +1 -0
  110. package/dist/ui/CrmDashboard.js +304 -0
  111. package/dist/ui/CrmDashboard.js.map +1 -0
  112. package/dist/ui/CrmDealCard.d.ts +15 -0
  113. package/dist/ui/CrmDealCard.d.ts.map +1 -0
  114. package/dist/ui/CrmDealCard.js +49 -0
  115. package/dist/ui/CrmDealCard.js.map +1 -0
  116. package/dist/ui/CrmPipelineBoard.d.ts +23 -0
  117. package/dist/ui/CrmPipelineBoard.d.ts.map +1 -0
  118. package/dist/ui/CrmPipelineBoard.js +98 -0
  119. package/dist/ui/CrmPipelineBoard.js.map +1 -0
  120. package/dist/ui/hooks/index.d.ts +3 -0
  121. package/dist/ui/hooks/index.js +6 -0
  122. package/dist/ui/hooks/useDealList.d.ts +35 -0
  123. package/dist/ui/hooks/useDealList.d.ts.map +1 -0
  124. package/dist/ui/hooks/useDealList.js +94 -0
  125. package/dist/ui/hooks/useDealList.js.map +1 -0
  126. package/dist/ui/hooks/useDealMutations.d.ts +26 -0
  127. package/dist/ui/hooks/useDealMutations.d.ts.map +1 -0
  128. package/dist/ui/hooks/useDealMutations.js +159 -0
  129. package/dist/ui/hooks/useDealMutations.js.map +1 -0
  130. package/dist/ui/index.d.ts +14 -0
  131. package/dist/ui/index.js +15 -0
  132. package/dist/ui/modals/CreateDealModal.d.ts +33 -0
  133. package/dist/ui/modals/CreateDealModal.d.ts.map +1 -0
  134. package/dist/ui/modals/CreateDealModal.js +183 -0
  135. package/dist/ui/modals/CreateDealModal.js.map +1 -0
  136. package/dist/ui/modals/DealActionsModal.d.ts +51 -0
  137. package/dist/ui/modals/DealActionsModal.d.ts.map +1 -0
  138. package/dist/ui/modals/DealActionsModal.js +372 -0
  139. package/dist/ui/modals/DealActionsModal.js.map +1 -0
  140. package/dist/ui/modals/index.d.ts +3 -0
  141. package/dist/ui/modals/index.js +4 -0
  142. package/dist/ui/overlays/demo-overlays.d.ts +19 -0
  143. package/dist/ui/overlays/demo-overlays.d.ts.map +1 -0
  144. package/dist/ui/overlays/demo-overlays.js +68 -0
  145. package/dist/ui/overlays/demo-overlays.js.map +1 -0
  146. package/dist/ui/overlays/index.d.ts +2 -0
  147. package/dist/ui/overlays/index.js +3 -0
  148. package/dist/ui/renderers/index.d.ts +3 -0
  149. package/dist/ui/renderers/index.js +4 -0
  150. package/dist/ui/renderers/pipeline.markdown.d.ts +23 -0
  151. package/dist/ui/renderers/pipeline.markdown.d.ts.map +1 -0
  152. package/dist/ui/renderers/pipeline.markdown.js +118 -0
  153. package/dist/ui/renderers/pipeline.markdown.js.map +1 -0
  154. package/dist/ui/renderers/pipeline.renderer.d.ts +9 -0
  155. package/dist/ui/renderers/pipeline.renderer.d.ts.map +1 -0
  156. package/dist/ui/renderers/pipeline.renderer.js +28 -0
  157. package/dist/ui/renderers/pipeline.renderer.js.map +1 -0
  158. package/example.ts +1 -0
  159. package/package.json +127 -0
  160. package/src/crm-pipeline.feature.ts +100 -0
  161. package/src/deal/deal.enum.ts +21 -0
  162. package/src/deal/deal.operation.ts +291 -0
  163. package/src/deal/deal.schema.ts +154 -0
  164. package/src/deal/deal.test-spec.ts +55 -0
  165. package/src/deal/index.ts +26 -0
  166. package/src/docs/crm-pipeline.docblock.ts +98 -0
  167. package/src/docs/index.ts +1 -0
  168. package/src/entities/company.entity.ts +77 -0
  169. package/src/entities/contact.entity.ts +93 -0
  170. package/src/entities/deal.entity.ts +160 -0
  171. package/src/entities/index.ts +45 -0
  172. package/src/entities/task.entity.ts +137 -0
  173. package/src/events/contact.event.ts +31 -0
  174. package/src/events/deal.event.ts +104 -0
  175. package/src/events/index.ts +3 -0
  176. package/src/events/task.event.ts +28 -0
  177. package/src/example.ts +38 -0
  178. package/src/handlers/crm.handlers.ts +415 -0
  179. package/src/handlers/deal.handlers.ts +253 -0
  180. package/src/handlers/index.ts +30 -0
  181. package/src/handlers/mock-data.ts +198 -0
  182. package/src/index.ts +32 -0
  183. package/src/operations/index.ts +20 -0
  184. package/src/presentations/dashboard.presentation.ts +59 -0
  185. package/src/presentations/index.ts +2 -0
  186. package/src/presentations/pipeline.presentation.ts +117 -0
  187. package/src/seeders/index.ts +35 -0
  188. package/src/shared/overlay-types.ts +39 -0
  189. package/src/ui/CrmDashboard.tsx +311 -0
  190. package/src/ui/CrmDealCard.tsx +83 -0
  191. package/src/ui/CrmPipelineBoard.tsx +136 -0
  192. package/src/ui/hooks/index.ts +10 -0
  193. package/src/ui/hooks/useDealList.ts +113 -0
  194. package/src/ui/hooks/useDealMutations.ts +174 -0
  195. package/src/ui/index.ts +18 -0
  196. package/src/ui/modals/CreateDealModal.tsx +239 -0
  197. package/src/ui/modals/DealActionsModal.tsx +424 -0
  198. package/src/ui/modals/index.ts +2 -0
  199. package/src/ui/overlays/demo-overlays.ts +68 -0
  200. package/src/ui/overlays/index.ts +1 -0
  201. package/src/ui/renderers/index.ts +6 -0
  202. package/src/ui/renderers/pipeline.markdown.ts +198 -0
  203. package/src/ui/renderers/pipeline.renderer.tsx +35 -0
  204. package/tsconfig.json +10 -0
  205. package/tsconfig.tsbuildinfo +1 -0
  206. package/tsdown.config.js +7 -0
@@ -0,0 +1,415 @@
1
+ /**
2
+ * Runtime-local CRM handlers
3
+ *
4
+ * These handlers work with the in-browser SQLite database
5
+ * instead of in-memory mock arrays.
6
+ */
7
+ import type { DatabasePort, DbRow } from '@contractspec/lib.runtime-sandbox';
8
+ /* eslint-disable @typescript-eslint/no-non-null-assertion */
9
+ import { web } from '@contractspec/lib.runtime-sandbox';
10
+ const { generateId } = web;
11
+
12
+ // ============ Types ============
13
+
14
+ export interface Deal {
15
+ id: string;
16
+ projectId: string;
17
+ name: string;
18
+ value: number;
19
+ currency: string;
20
+ pipelineId: string;
21
+ stageId: string;
22
+ status: 'OPEN' | 'WON' | 'LOST' | 'STALE';
23
+ contactId?: string;
24
+ companyId?: string;
25
+ ownerId: string;
26
+ expectedCloseDate?: Date;
27
+ wonSource?: string;
28
+ lostReason?: string;
29
+ notes?: string;
30
+ createdAt: Date;
31
+ updatedAt: Date;
32
+ }
33
+
34
+ export interface Stage {
35
+ id: string;
36
+ pipelineId: string;
37
+ name: string;
38
+ position: number;
39
+ }
40
+
41
+ export interface CreateDealInput {
42
+ name: string;
43
+ value: number;
44
+ currency?: string;
45
+ pipelineId: string;
46
+ stageId: string;
47
+ contactId?: string;
48
+ companyId?: string;
49
+ expectedCloseDate?: Date;
50
+ }
51
+
52
+ export interface MoveDealInput {
53
+ dealId: string;
54
+ stageId: string;
55
+ }
56
+
57
+ export interface WinDealInput {
58
+ dealId: string;
59
+ wonSource?: string;
60
+ notes?: string;
61
+ }
62
+
63
+ export interface LoseDealInput {
64
+ dealId: string;
65
+ lostReason: string;
66
+ notes?: string;
67
+ }
68
+
69
+ export interface ListDealsInput {
70
+ projectId: string;
71
+ pipelineId?: string;
72
+ stageId?: string;
73
+ status?: 'OPEN' | 'WON' | 'LOST' | 'all';
74
+ ownerId?: string;
75
+ search?: string;
76
+ limit?: number;
77
+ offset?: number;
78
+ }
79
+
80
+ export interface ListDealsOutput {
81
+ deals: Deal[];
82
+ total: number;
83
+ totalValue: number;
84
+ }
85
+
86
+ // ============ Row Type ============
87
+
88
+ // Note: We don't use DbRow generics here - just cast the results
89
+ // to the expected types since we know the schema
90
+
91
+ interface DealRow {
92
+ id: string;
93
+ projectId: string;
94
+ name: string;
95
+ value: number;
96
+ currency: string;
97
+ pipelineId: string;
98
+ stageId: string;
99
+ status: string;
100
+ contactId: string | null;
101
+ companyId: string | null;
102
+ ownerId: string;
103
+ expectedCloseDate: string | null;
104
+ wonSource: string | null;
105
+ lostReason: string | null;
106
+ notes: string | null;
107
+ createdAt: string;
108
+ updatedAt: string;
109
+ }
110
+
111
+ interface StageRow {
112
+ id: string;
113
+ pipelineId: string;
114
+ name: string;
115
+ position: number;
116
+ }
117
+
118
+ function rowToDeal(row: DealRow): Deal {
119
+ return {
120
+ id: row.id,
121
+ projectId: row.projectId,
122
+ name: row.name,
123
+ value: row.value,
124
+ currency: row.currency,
125
+ pipelineId: row.pipelineId,
126
+ stageId: row.stageId,
127
+ status: row.status as Deal['status'],
128
+ contactId: row.contactId ?? undefined,
129
+ companyId: row.companyId ?? undefined,
130
+ ownerId: row.ownerId,
131
+ expectedCloseDate: row.expectedCloseDate
132
+ ? new Date(row.expectedCloseDate)
133
+ : undefined,
134
+ wonSource: row.wonSource ?? undefined,
135
+ lostReason: row.lostReason ?? undefined,
136
+ notes: row.notes ?? undefined,
137
+ createdAt: new Date(row.createdAt),
138
+ updatedAt: new Date(row.updatedAt),
139
+ };
140
+ }
141
+
142
+ // ============ Handler Factory ============
143
+
144
+ export function createCrmHandlers(db: DatabasePort) {
145
+ /**
146
+ * List deals with filtering
147
+ */
148
+ async function listDeals(input: ListDealsInput): Promise<ListDealsOutput> {
149
+ const {
150
+ projectId,
151
+ pipelineId,
152
+ stageId,
153
+ status,
154
+ ownerId,
155
+ search,
156
+ limit = 20,
157
+ offset = 0,
158
+ } = input;
159
+
160
+ let whereClause = 'WHERE projectId = ?';
161
+ const params: (string | number)[] = [projectId];
162
+
163
+ if (pipelineId) {
164
+ whereClause += ' AND pipelineId = ?';
165
+ params.push(pipelineId);
166
+ }
167
+
168
+ if (stageId) {
169
+ whereClause += ' AND stageId = ?';
170
+ params.push(stageId);
171
+ }
172
+
173
+ if (status && status !== 'all') {
174
+ whereClause += ' AND status = ?';
175
+ params.push(status);
176
+ }
177
+
178
+ if (ownerId) {
179
+ whereClause += ' AND ownerId = ?';
180
+ params.push(ownerId);
181
+ }
182
+
183
+ if (search) {
184
+ whereClause += ' AND name LIKE ?';
185
+ params.push(`%${search}%`);
186
+ }
187
+
188
+ // Get total count
189
+ const countResult = (
190
+ await db.query(
191
+ `SELECT COUNT(*) as count FROM crm_deal ${whereClause}`,
192
+ params
193
+ )
194
+ ).rows as DbRow[];
195
+ const total = (countResult[0]?.count as number) ?? 0;
196
+
197
+ // Get total value
198
+ const valueResult = (
199
+ await db.query(
200
+ `SELECT COALESCE(SUM(value), 0) as total FROM crm_deal ${whereClause}`,
201
+ params
202
+ )
203
+ ).rows as DbRow[];
204
+ const totalValue = (valueResult[0]?.total as number) ?? 0;
205
+
206
+ // Get paginated deals
207
+ const dealRows = (
208
+ await db.query(
209
+ `SELECT * FROM crm_deal ${whereClause} ORDER BY value DESC LIMIT ? OFFSET ?`,
210
+ [...params, limit, offset]
211
+ )
212
+ ).rows as unknown as DealRow[];
213
+
214
+ return {
215
+ deals: dealRows.map(rowToDeal),
216
+ total,
217
+ totalValue,
218
+ };
219
+ }
220
+
221
+ /**
222
+ * Create a new deal
223
+ */
224
+ async function createDeal(
225
+ input: CreateDealInput,
226
+ context: { projectId: string; ownerId: string }
227
+ ): Promise<Deal> {
228
+ const id = generateId('deal');
229
+ const now = new Date().toISOString();
230
+
231
+ await db.execute(
232
+ `INSERT INTO crm_deal (id, projectId, pipelineId, stageId, name, value, currency, status, contactId, companyId, ownerId, expectedCloseDate, createdAt, updatedAt)
233
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
234
+ [
235
+ id,
236
+ context.projectId,
237
+ input.pipelineId,
238
+ input.stageId,
239
+ input.name,
240
+ input.value,
241
+ input.currency ?? 'USD',
242
+ 'OPEN',
243
+ input.contactId ?? null,
244
+ input.companyId ?? null,
245
+ context.ownerId,
246
+ input.expectedCloseDate?.toISOString() ?? null,
247
+ now,
248
+ now,
249
+ ]
250
+ );
251
+
252
+ const rows = (await db.query(`SELECT * FROM crm_deal WHERE id = ?`, [id]))
253
+ .rows as unknown as DealRow[];
254
+
255
+ if (!rows[0]) {
256
+ throw new Error('Failed to create deal');
257
+ }
258
+
259
+ return rowToDeal(rows[0]);
260
+ }
261
+
262
+ /**
263
+ * Move a deal to a different stage
264
+ */
265
+ async function moveDeal(input: MoveDealInput): Promise<Deal> {
266
+ const now = new Date().toISOString();
267
+
268
+ // Verify deal exists
269
+ const existing = (
270
+ await db.query(`SELECT * FROM crm_deal WHERE id = ?`, [input.dealId])
271
+ ).rows as unknown as DealRow[];
272
+
273
+ if (!existing[0]) {
274
+ throw new Error('NOT_FOUND');
275
+ }
276
+
277
+ // Verify stage exists
278
+ const stage = (
279
+ await db.query(`SELECT * FROM crm_stage WHERE id = ?`, [input.stageId])
280
+ ).rows as unknown as StageRow[];
281
+
282
+ if (!stage[0]) {
283
+ throw new Error('INVALID_STAGE');
284
+ }
285
+
286
+ await db.execute(
287
+ `UPDATE crm_deal SET stageId = ?, updatedAt = ? WHERE id = ?`,
288
+ [input.stageId, now, input.dealId]
289
+ );
290
+
291
+ const rows = (
292
+ await db.query(`SELECT * FROM crm_deal WHERE id = ?`, [input.dealId])
293
+ ).rows as unknown as DealRow[];
294
+
295
+ return rowToDeal(rows[0]!);
296
+ }
297
+
298
+ /**
299
+ * Mark a deal as won
300
+ */
301
+ async function winDeal(input: WinDealInput): Promise<Deal> {
302
+ const now = new Date().toISOString();
303
+
304
+ // Verify deal exists
305
+ const existing = (
306
+ await db.query(`SELECT * FROM crm_deal WHERE id = ?`, [input.dealId])
307
+ ).rows as unknown as DealRow[];
308
+
309
+ if (!existing[0]) {
310
+ throw new Error('NOT_FOUND');
311
+ }
312
+
313
+ await db.execute(
314
+ `UPDATE crm_deal SET status = 'WON', wonSource = ?, notes = ?, updatedAt = ? WHERE id = ?`,
315
+ [input.wonSource ?? null, input.notes ?? null, now, input.dealId]
316
+ );
317
+
318
+ const rows = (
319
+ await db.query(`SELECT * FROM crm_deal WHERE id = ?`, [input.dealId])
320
+ ).rows as unknown as DealRow[];
321
+
322
+ return rowToDeal(rows[0]!);
323
+ }
324
+
325
+ /**
326
+ * Mark a deal as lost
327
+ */
328
+ async function loseDeal(input: LoseDealInput): Promise<Deal> {
329
+ const now = new Date().toISOString();
330
+
331
+ // Verify deal exists
332
+ const existing = (
333
+ await db.query(`SELECT * FROM crm_deal WHERE id = ?`, [input.dealId])
334
+ ).rows as unknown as DealRow[];
335
+
336
+ if (!existing[0]) {
337
+ throw new Error('NOT_FOUND');
338
+ }
339
+
340
+ await db.execute(
341
+ `UPDATE crm_deal SET status = 'LOST', lostReason = ?, notes = ?, updatedAt = ? WHERE id = ?`,
342
+ [input.lostReason, input.notes ?? null, now, input.dealId]
343
+ );
344
+
345
+ const rows = (
346
+ await db.query(`SELECT * FROM crm_deal WHERE id = ?`, [input.dealId])
347
+ ).rows as unknown as DealRow[];
348
+
349
+ return rowToDeal(rows[0]!);
350
+ }
351
+
352
+ /**
353
+ * Get deals grouped by stage
354
+ */
355
+ async function getDealsByStage(input: {
356
+ projectId: string;
357
+ pipelineId: string;
358
+ }): Promise<Record<string, Deal[]>> {
359
+ const deals = (
360
+ await db.query(
361
+ `SELECT * FROM crm_deal WHERE projectId = ? AND pipelineId = ? AND status = 'OPEN' ORDER BY value DESC`,
362
+ [input.projectId, input.pipelineId]
363
+ )
364
+ ).rows as unknown as DealRow[];
365
+
366
+ const stages = (
367
+ await db.query(
368
+ `SELECT * FROM crm_stage WHERE pipelineId = ? ORDER BY position`,
369
+ [input.pipelineId]
370
+ )
371
+ ).rows as unknown as StageRow[];
372
+
373
+ const grouped: Record<string, Deal[]> = {};
374
+ for (const stage of stages) {
375
+ grouped[stage.id] = deals
376
+ .filter((d) => d.stageId === stage.id)
377
+ .map(rowToDeal);
378
+ }
379
+
380
+ return grouped;
381
+ }
382
+
383
+ /**
384
+ * Get pipeline stages
385
+ */
386
+ async function getPipelineStages(input: {
387
+ pipelineId: string;
388
+ }): Promise<Stage[]> {
389
+ const rows = (
390
+ await db.query(
391
+ `SELECT * FROM crm_stage WHERE pipelineId = ? ORDER BY position`,
392
+ [input.pipelineId]
393
+ )
394
+ ).rows as unknown as StageRow[];
395
+
396
+ return rows.map((row) => ({
397
+ id: row.id,
398
+ pipelineId: row.pipelineId,
399
+ name: row.name,
400
+ position: row.position,
401
+ }));
402
+ }
403
+
404
+ return {
405
+ listDeals,
406
+ createDeal,
407
+ moveDeal,
408
+ winDeal,
409
+ loseDeal,
410
+ getDealsByStage,
411
+ getPipelineStages,
412
+ };
413
+ }
414
+
415
+ export type CrmHandlers = ReturnType<typeof createCrmHandlers>;
@@ -0,0 +1,253 @@
1
+ /**
2
+ * Mock handlers for Deal contracts
3
+ */
4
+ import { MOCK_DEALS, MOCK_STAGES } from './mock-data';
5
+
6
+ // Types inferred from contract schemas
7
+ export interface Deal {
8
+ id: string;
9
+ name: string;
10
+ value: number;
11
+ currency: string;
12
+ pipelineId: string;
13
+ stageId: string;
14
+ status: 'OPEN' | 'WON' | 'LOST' | 'STALE';
15
+ contactId?: string;
16
+ companyId?: string;
17
+ ownerId: string;
18
+ expectedCloseDate?: Date;
19
+ createdAt: Date;
20
+ updatedAt: Date;
21
+ }
22
+
23
+ export interface CreateDealInput {
24
+ name: string;
25
+ value: number;
26
+ currency?: string;
27
+ pipelineId: string;
28
+ stageId: string;
29
+ contactId?: string;
30
+ companyId?: string;
31
+ expectedCloseDate?: Date;
32
+ }
33
+
34
+ export interface MoveDealInput {
35
+ dealId: string;
36
+ stageId: string;
37
+ position?: number;
38
+ }
39
+
40
+ export interface WinDealInput {
41
+ dealId: string;
42
+ wonSource?: string;
43
+ notes?: string;
44
+ }
45
+
46
+ export interface LoseDealInput {
47
+ dealId: string;
48
+ lostReason: string;
49
+ notes?: string;
50
+ }
51
+
52
+ export interface ListDealsInput {
53
+ pipelineId?: string;
54
+ stageId?: string;
55
+ status?: 'OPEN' | 'WON' | 'LOST' | 'all';
56
+ ownerId?: string;
57
+ search?: string;
58
+ limit?: number;
59
+ offset?: number;
60
+ }
61
+
62
+ export interface ListDealsOutput {
63
+ deals: Deal[];
64
+ total: number;
65
+ totalValue: number;
66
+ }
67
+
68
+ /**
69
+ * Mock handler for ListDealsContract
70
+ */
71
+ export async function mockListDealsHandler(
72
+ input: ListDealsInput
73
+ ): Promise<ListDealsOutput> {
74
+ const {
75
+ pipelineId,
76
+ stageId,
77
+ status,
78
+ ownerId,
79
+ search,
80
+ limit = 20,
81
+ offset = 0,
82
+ } = input;
83
+
84
+ let filtered = [...MOCK_DEALS];
85
+
86
+ if (pipelineId) {
87
+ filtered = filtered.filter((d) => d.pipelineId === pipelineId);
88
+ }
89
+
90
+ if (stageId) {
91
+ filtered = filtered.filter((d) => d.stageId === stageId);
92
+ }
93
+
94
+ if (status && status !== 'all') {
95
+ filtered = filtered.filter((d) => d.status === status);
96
+ }
97
+
98
+ if (ownerId) {
99
+ filtered = filtered.filter((d) => d.ownerId === ownerId);
100
+ }
101
+
102
+ if (search) {
103
+ const q = search.toLowerCase();
104
+ filtered = filtered.filter((d) => d.name.toLowerCase().includes(q));
105
+ }
106
+
107
+ // Sort by value descending
108
+ filtered.sort((a, b) => b.value - a.value);
109
+
110
+ const total = filtered.length;
111
+ const totalValue = filtered.reduce((sum, d) => sum + d.value, 0);
112
+ const deals = filtered.slice(offset, offset + limit);
113
+
114
+ return {
115
+ deals,
116
+ total,
117
+ totalValue,
118
+ };
119
+ }
120
+
121
+ /**
122
+ * Mock handler for CreateDealContract
123
+ */
124
+ export async function mockCreateDealHandler(
125
+ input: CreateDealInput,
126
+ context: { ownerId: string }
127
+ ): Promise<Deal> {
128
+ const now = new Date();
129
+
130
+ const deal: Deal = {
131
+ id: `deal-${Date.now()}`,
132
+ name: input.name,
133
+ value: input.value,
134
+ currency: input.currency ?? 'USD',
135
+ pipelineId: input.pipelineId,
136
+ stageId: input.stageId,
137
+ status: 'OPEN',
138
+ contactId: input.contactId,
139
+ companyId: input.companyId,
140
+ ownerId: context.ownerId,
141
+ expectedCloseDate: input.expectedCloseDate,
142
+ createdAt: now,
143
+ updatedAt: now,
144
+ };
145
+
146
+ MOCK_DEALS.push(deal);
147
+
148
+ return deal;
149
+ }
150
+
151
+ /**
152
+ * Mock handler for MoveDealContract
153
+ */
154
+ export async function mockMoveDealHandler(input: MoveDealInput): Promise<Deal> {
155
+ const dealIndex = MOCK_DEALS.findIndex((d) => d.id === input.dealId);
156
+ if (dealIndex === -1) {
157
+ throw new Error('NOT_FOUND');
158
+ }
159
+ const deal = MOCK_DEALS[dealIndex];
160
+ if (!deal) {
161
+ throw new Error('NOT_FOUND');
162
+ }
163
+
164
+ const stage = MOCK_STAGES.find((s) => s.id === input.stageId);
165
+ if (!stage) {
166
+ throw new Error('INVALID_STAGE');
167
+ }
168
+
169
+ const updatedDeal: Deal = {
170
+ ...deal,
171
+ stageId: input.stageId,
172
+ updatedAt: new Date(),
173
+ };
174
+
175
+ MOCK_DEALS[dealIndex] = updatedDeal;
176
+
177
+ return updatedDeal;
178
+ }
179
+
180
+ /**
181
+ * Mock handler for WinDealContract
182
+ */
183
+ export async function mockWinDealHandler(input: WinDealInput): Promise<Deal> {
184
+ const dealIndex = MOCK_DEALS.findIndex((d) => d.id === input.dealId);
185
+ if (dealIndex === -1) {
186
+ throw new Error('NOT_FOUND');
187
+ }
188
+ const deal = MOCK_DEALS[dealIndex];
189
+ if (!deal) {
190
+ throw new Error('NOT_FOUND');
191
+ }
192
+
193
+ const updatedDeal: Deal = {
194
+ ...deal,
195
+ status: 'WON' as const,
196
+ updatedAt: new Date(),
197
+ };
198
+
199
+ MOCK_DEALS[dealIndex] = updatedDeal;
200
+
201
+ return updatedDeal;
202
+ }
203
+
204
+ /**
205
+ * Mock handler for LoseDealContract
206
+ */
207
+ export async function mockLoseDealHandler(input: LoseDealInput): Promise<Deal> {
208
+ const dealIndex = MOCK_DEALS.findIndex((d) => d.id === input.dealId);
209
+ if (dealIndex === -1) {
210
+ throw new Error('NOT_FOUND');
211
+ }
212
+ const deal = MOCK_DEALS[dealIndex];
213
+ if (!deal) {
214
+ throw new Error('NOT_FOUND');
215
+ }
216
+
217
+ const updatedDeal: Deal = {
218
+ ...deal,
219
+ status: 'LOST' as const,
220
+ updatedAt: new Date(),
221
+ };
222
+
223
+ MOCK_DEALS[dealIndex] = updatedDeal;
224
+
225
+ return updatedDeal;
226
+ }
227
+
228
+ /**
229
+ * Get deals grouped by stage for Kanban view
230
+ */
231
+ export async function mockGetDealsByStageHandler(input: {
232
+ pipelineId: string;
233
+ }): Promise<Record<string, Deal[]>> {
234
+ const deals = MOCK_DEALS.filter(
235
+ (d) => d.pipelineId === input.pipelineId && d.status === 'OPEN'
236
+ );
237
+
238
+ const grouped: Record<string, Deal[]> = {};
239
+ for (const stage of MOCK_STAGES) {
240
+ grouped[stage.id] = deals.filter((d) => d.stageId === stage.id);
241
+ }
242
+
243
+ return grouped;
244
+ }
245
+
246
+ /**
247
+ * Get pipeline stages
248
+ */
249
+ export async function mockGetPipelineStagesHandler(input: {
250
+ pipelineId: string;
251
+ }) {
252
+ return MOCK_STAGES.filter((s) => s.pipelineId === input.pipelineId);
253
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Mock handlers for crm-pipeline example
3
+ *
4
+ * These handlers provide mock implementations of all contracts
5
+ * for use in demos, tests, and the sandbox environment.
6
+ */
7
+
8
+ // Mock data
9
+ export * from './mock-data';
10
+
11
+ // Deal handlers
12
+ export {
13
+ mockListDealsHandler,
14
+ mockCreateDealHandler,
15
+ mockMoveDealHandler,
16
+ mockWinDealHandler,
17
+ mockLoseDealHandler,
18
+ mockGetDealsByStageHandler,
19
+ mockGetPipelineStagesHandler,
20
+ type Deal,
21
+ type CreateDealInput,
22
+ type MoveDealInput,
23
+ type WinDealInput,
24
+ type LoseDealInput,
25
+ type ListDealsInput,
26
+ type ListDealsOutput,
27
+ } from './deal.handlers';
28
+
29
+ // Runtime handlers (PGLite)
30
+ export * from './crm.handlers';