@hasna/microservices 0.0.9 → 0.0.11

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 (100) hide show
  1. package/bin/index.js +236 -36
  2. package/bin/mcp.js +153 -4
  3. package/dist/index.js +120 -3
  4. package/microservices/microservice-analytics/package.json +27 -0
  5. package/microservices/microservice-analytics/src/cli/index.ts +373 -0
  6. package/microservices/microservice-analytics/src/db/analytics.ts +564 -0
  7. package/microservices/microservice-analytics/src/db/database.ts +93 -0
  8. package/microservices/microservice-analytics/src/db/migrations.ts +50 -0
  9. package/microservices/microservice-analytics/src/index.ts +37 -0
  10. package/microservices/microservice-analytics/src/mcp/index.ts +334 -0
  11. package/microservices/microservice-assets/package.json +27 -0
  12. package/microservices/microservice-assets/src/cli/index.ts +375 -0
  13. package/microservices/microservice-assets/src/db/assets.ts +370 -0
  14. package/microservices/microservice-assets/src/db/database.ts +93 -0
  15. package/microservices/microservice-assets/src/db/migrations.ts +51 -0
  16. package/microservices/microservice-assets/src/index.ts +32 -0
  17. package/microservices/microservice-assets/src/mcp/index.ts +346 -0
  18. package/microservices/microservice-compliance/package.json +27 -0
  19. package/microservices/microservice-compliance/src/cli/index.ts +467 -0
  20. package/microservices/microservice-compliance/src/db/compliance.ts +633 -0
  21. package/microservices/microservice-compliance/src/db/database.ts +93 -0
  22. package/microservices/microservice-compliance/src/db/migrations.ts +63 -0
  23. package/microservices/microservice-compliance/src/index.ts +46 -0
  24. package/microservices/microservice-compliance/src/mcp/index.ts +438 -0
  25. package/microservices/microservice-habits/package.json +27 -0
  26. package/microservices/microservice-habits/src/cli/index.ts +315 -0
  27. package/microservices/microservice-habits/src/db/database.ts +93 -0
  28. package/microservices/microservice-habits/src/db/habits.ts +451 -0
  29. package/microservices/microservice-habits/src/db/migrations.ts +46 -0
  30. package/microservices/microservice-habits/src/index.ts +31 -0
  31. package/microservices/microservice-habits/src/mcp/index.ts +313 -0
  32. package/microservices/microservice-health/package.json +27 -0
  33. package/microservices/microservice-health/src/cli/index.ts +484 -0
  34. package/microservices/microservice-health/src/db/database.ts +93 -0
  35. package/microservices/microservice-health/src/db/health.ts +708 -0
  36. package/microservices/microservice-health/src/db/migrations.ts +70 -0
  37. package/microservices/microservice-health/src/index.ts +63 -0
  38. package/microservices/microservice-health/src/mcp/index.ts +437 -0
  39. package/microservices/microservice-leads/package.json +27 -0
  40. package/microservices/microservice-leads/src/cli/index.ts +596 -0
  41. package/microservices/microservice-leads/src/db/database.ts +93 -0
  42. package/microservices/microservice-leads/src/db/leads.ts +520 -0
  43. package/microservices/microservice-leads/src/db/lists.ts +151 -0
  44. package/microservices/microservice-leads/src/db/migrations.ts +93 -0
  45. package/microservices/microservice-leads/src/index.ts +65 -0
  46. package/microservices/microservice-leads/src/lib/enrichment.ts +202 -0
  47. package/microservices/microservice-leads/src/lib/scoring.ts +134 -0
  48. package/microservices/microservice-leads/src/mcp/index.ts +533 -0
  49. package/microservices/microservice-notifications/package.json +27 -0
  50. package/microservices/microservice-notifications/src/cli/index.ts +349 -0
  51. package/microservices/microservice-notifications/src/db/database.ts +93 -0
  52. package/microservices/microservice-notifications/src/db/migrations.ts +62 -0
  53. package/microservices/microservice-notifications/src/db/notifications.ts +509 -0
  54. package/microservices/microservice-notifications/src/index.ts +41 -0
  55. package/microservices/microservice-notifications/src/mcp/index.ts +422 -0
  56. package/microservices/microservice-products/package.json +27 -0
  57. package/microservices/microservice-products/src/cli/index.ts +416 -0
  58. package/microservices/microservice-products/src/db/categories.ts +154 -0
  59. package/microservices/microservice-products/src/db/database.ts +93 -0
  60. package/microservices/microservice-products/src/db/migrations.ts +58 -0
  61. package/microservices/microservice-products/src/db/pricing-tiers.ts +66 -0
  62. package/microservices/microservice-products/src/db/products.ts +452 -0
  63. package/microservices/microservice-products/src/index.ts +53 -0
  64. package/microservices/microservice-products/src/mcp/index.ts +453 -0
  65. package/microservices/microservice-projects/package.json +27 -0
  66. package/microservices/microservice-projects/src/cli/index.ts +480 -0
  67. package/microservices/microservice-projects/src/db/database.ts +93 -0
  68. package/microservices/microservice-projects/src/db/migrations.ts +65 -0
  69. package/microservices/microservice-projects/src/db/projects.ts +715 -0
  70. package/microservices/microservice-projects/src/index.ts +57 -0
  71. package/microservices/microservice-projects/src/mcp/index.ts +501 -0
  72. package/microservices/microservice-proposals/package.json +27 -0
  73. package/microservices/microservice-proposals/src/cli/index.ts +400 -0
  74. package/microservices/microservice-proposals/src/db/database.ts +93 -0
  75. package/microservices/microservice-proposals/src/db/migrations.ts +52 -0
  76. package/microservices/microservice-proposals/src/db/proposals.ts +532 -0
  77. package/microservices/microservice-proposals/src/index.ts +37 -0
  78. package/microservices/microservice-proposals/src/mcp/index.ts +375 -0
  79. package/microservices/microservice-reading/package.json +27 -0
  80. package/microservices/microservice-reading/src/cli/index.ts +464 -0
  81. package/microservices/microservice-reading/src/db/database.ts +93 -0
  82. package/microservices/microservice-reading/src/db/migrations.ts +59 -0
  83. package/microservices/microservice-reading/src/db/reading.ts +524 -0
  84. package/microservices/microservice-reading/src/index.ts +51 -0
  85. package/microservices/microservice-reading/src/mcp/index.ts +368 -0
  86. package/microservices/microservice-travel/package.json +27 -0
  87. package/microservices/microservice-travel/src/cli/index.ts +505 -0
  88. package/microservices/microservice-travel/src/db/database.ts +93 -0
  89. package/microservices/microservice-travel/src/db/migrations.ts +77 -0
  90. package/microservices/microservice-travel/src/db/travel.ts +802 -0
  91. package/microservices/microservice-travel/src/index.ts +60 -0
  92. package/microservices/microservice-travel/src/mcp/index.ts +495 -0
  93. package/microservices/microservice-wiki/package.json +27 -0
  94. package/microservices/microservice-wiki/src/cli/index.ts +345 -0
  95. package/microservices/microservice-wiki/src/db/database.ts +93 -0
  96. package/microservices/microservice-wiki/src/db/migrations.ts +55 -0
  97. package/microservices/microservice-wiki/src/db/wiki.ts +395 -0
  98. package/microservices/microservice-wiki/src/index.ts +32 -0
  99. package/microservices/microservice-wiki/src/mcp/index.ts +344 -0
  100. package/package.json +1 -1
@@ -0,0 +1,802 @@
1
+ /**
2
+ * Travel CRUD operations — trips, bookings, documents, loyalty programs
3
+ */
4
+
5
+ import { getDatabase } from "./database.js";
6
+
7
+ // ==================== TRIPS ====================
8
+
9
+ export interface Trip {
10
+ id: string;
11
+ name: string;
12
+ destination: string | null;
13
+ start_date: string | null;
14
+ end_date: string | null;
15
+ status: string;
16
+ budget: number | null;
17
+ spent: number;
18
+ currency: string;
19
+ notes: string | null;
20
+ metadata: Record<string, unknown>;
21
+ created_at: string;
22
+ updated_at: string;
23
+ }
24
+
25
+ interface TripRow {
26
+ id: string;
27
+ name: string;
28
+ destination: string | null;
29
+ start_date: string | null;
30
+ end_date: string | null;
31
+ status: string;
32
+ budget: number | null;
33
+ spent: number;
34
+ currency: string;
35
+ notes: string | null;
36
+ metadata: string;
37
+ created_at: string;
38
+ updated_at: string;
39
+ }
40
+
41
+ function rowToTrip(row: TripRow): Trip {
42
+ return {
43
+ ...row,
44
+ metadata: JSON.parse(row.metadata || "{}"),
45
+ };
46
+ }
47
+
48
+ export interface CreateTripInput {
49
+ name: string;
50
+ destination?: string;
51
+ start_date?: string;
52
+ end_date?: string;
53
+ status?: string;
54
+ budget?: number;
55
+ currency?: string;
56
+ notes?: string;
57
+ metadata?: Record<string, unknown>;
58
+ }
59
+
60
+ export function createTrip(input: CreateTripInput): Trip {
61
+ const db = getDatabase();
62
+ const id = crypto.randomUUID();
63
+ const metadata = JSON.stringify(input.metadata || {});
64
+
65
+ db.prepare(
66
+ `INSERT INTO trips (id, name, destination, start_date, end_date, status, budget, currency, notes, metadata)
67
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
68
+ ).run(
69
+ id,
70
+ input.name,
71
+ input.destination || null,
72
+ input.start_date || null,
73
+ input.end_date || null,
74
+ input.status || "planning",
75
+ input.budget ?? null,
76
+ input.currency || "USD",
77
+ input.notes || null,
78
+ metadata
79
+ );
80
+
81
+ return getTrip(id)!;
82
+ }
83
+
84
+ export function getTrip(id: string): Trip | null {
85
+ const db = getDatabase();
86
+ const row = db.prepare("SELECT * FROM trips WHERE id = ?").get(id) as TripRow | null;
87
+ return row ? rowToTrip(row) : null;
88
+ }
89
+
90
+ export interface ListTripsOptions {
91
+ search?: string;
92
+ status?: string;
93
+ destination?: string;
94
+ limit?: number;
95
+ offset?: number;
96
+ }
97
+
98
+ export function listTrips(options: ListTripsOptions = {}): Trip[] {
99
+ const db = getDatabase();
100
+ const conditions: string[] = [];
101
+ const params: unknown[] = [];
102
+
103
+ if (options.search) {
104
+ conditions.push("(name LIKE ? OR destination LIKE ? OR notes LIKE ?)");
105
+ const q = `%${options.search}%`;
106
+ params.push(q, q, q);
107
+ }
108
+
109
+ if (options.status) {
110
+ conditions.push("status = ?");
111
+ params.push(options.status);
112
+ }
113
+
114
+ if (options.destination) {
115
+ conditions.push("destination LIKE ?");
116
+ params.push(`%${options.destination}%`);
117
+ }
118
+
119
+ let sql = "SELECT * FROM trips";
120
+ if (conditions.length > 0) {
121
+ sql += " WHERE " + conditions.join(" AND ");
122
+ }
123
+ sql += " ORDER BY start_date DESC, created_at DESC";
124
+
125
+ if (options.limit) {
126
+ sql += " LIMIT ?";
127
+ params.push(options.limit);
128
+ }
129
+ if (options.offset) {
130
+ sql += " OFFSET ?";
131
+ params.push(options.offset);
132
+ }
133
+
134
+ const rows = db.prepare(sql).all(...params) as TripRow[];
135
+ return rows.map(rowToTrip);
136
+ }
137
+
138
+ export interface UpdateTripInput {
139
+ name?: string;
140
+ destination?: string;
141
+ start_date?: string;
142
+ end_date?: string;
143
+ status?: string;
144
+ budget?: number;
145
+ spent?: number;
146
+ currency?: string;
147
+ notes?: string;
148
+ metadata?: Record<string, unknown>;
149
+ }
150
+
151
+ export function updateTrip(id: string, input: UpdateTripInput): Trip | null {
152
+ const db = getDatabase();
153
+ const existing = getTrip(id);
154
+ if (!existing) return null;
155
+
156
+ const sets: string[] = [];
157
+ const params: unknown[] = [];
158
+
159
+ if (input.name !== undefined) {
160
+ sets.push("name = ?");
161
+ params.push(input.name);
162
+ }
163
+ if (input.destination !== undefined) {
164
+ sets.push("destination = ?");
165
+ params.push(input.destination);
166
+ }
167
+ if (input.start_date !== undefined) {
168
+ sets.push("start_date = ?");
169
+ params.push(input.start_date);
170
+ }
171
+ if (input.end_date !== undefined) {
172
+ sets.push("end_date = ?");
173
+ params.push(input.end_date);
174
+ }
175
+ if (input.status !== undefined) {
176
+ sets.push("status = ?");
177
+ params.push(input.status);
178
+ }
179
+ if (input.budget !== undefined) {
180
+ sets.push("budget = ?");
181
+ params.push(input.budget);
182
+ }
183
+ if (input.spent !== undefined) {
184
+ sets.push("spent = ?");
185
+ params.push(input.spent);
186
+ }
187
+ if (input.currency !== undefined) {
188
+ sets.push("currency = ?");
189
+ params.push(input.currency);
190
+ }
191
+ if (input.notes !== undefined) {
192
+ sets.push("notes = ?");
193
+ params.push(input.notes);
194
+ }
195
+ if (input.metadata !== undefined) {
196
+ sets.push("metadata = ?");
197
+ params.push(JSON.stringify(input.metadata));
198
+ }
199
+
200
+ if (sets.length === 0) return existing;
201
+
202
+ sets.push("updated_at = datetime('now')");
203
+ params.push(id);
204
+
205
+ db.prepare(
206
+ `UPDATE trips SET ${sets.join(", ")} WHERE id = ?`
207
+ ).run(...params);
208
+
209
+ return getTrip(id);
210
+ }
211
+
212
+ export function deleteTrip(id: string): boolean {
213
+ const db = getDatabase();
214
+ const result = db.prepare("DELETE FROM trips WHERE id = ?").run(id);
215
+ return result.changes > 0;
216
+ }
217
+
218
+ // ==================== BOOKINGS ====================
219
+
220
+ export interface Booking {
221
+ id: string;
222
+ trip_id: string;
223
+ type: string;
224
+ provider: string | null;
225
+ confirmation_code: string | null;
226
+ status: string;
227
+ check_in: string | null;
228
+ check_out: string | null;
229
+ cost: number | null;
230
+ details: Record<string, unknown>;
231
+ created_at: string;
232
+ }
233
+
234
+ interface BookingRow {
235
+ id: string;
236
+ trip_id: string;
237
+ type: string;
238
+ provider: string | null;
239
+ confirmation_code: string | null;
240
+ status: string;
241
+ check_in: string | null;
242
+ check_out: string | null;
243
+ cost: number | null;
244
+ details: string;
245
+ created_at: string;
246
+ }
247
+
248
+ function rowToBooking(row: BookingRow): Booking {
249
+ return {
250
+ ...row,
251
+ details: JSON.parse(row.details || "{}"),
252
+ };
253
+ }
254
+
255
+ export interface CreateBookingInput {
256
+ trip_id: string;
257
+ type: string;
258
+ provider?: string;
259
+ confirmation_code?: string;
260
+ status?: string;
261
+ check_in?: string;
262
+ check_out?: string;
263
+ cost?: number;
264
+ details?: Record<string, unknown>;
265
+ }
266
+
267
+ export function createBooking(input: CreateBookingInput): Booking {
268
+ const db = getDatabase();
269
+ const id = crypto.randomUUID();
270
+ const details = JSON.stringify(input.details || {});
271
+
272
+ db.prepare(
273
+ `INSERT INTO bookings (id, trip_id, type, provider, confirmation_code, status, check_in, check_out, cost, details)
274
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
275
+ ).run(
276
+ id,
277
+ input.trip_id,
278
+ input.type,
279
+ input.provider || null,
280
+ input.confirmation_code || null,
281
+ input.status || "confirmed",
282
+ input.check_in || null,
283
+ input.check_out || null,
284
+ input.cost ?? null,
285
+ details
286
+ );
287
+
288
+ // Update trip spent
289
+ if (input.cost && input.cost > 0) {
290
+ db.prepare(
291
+ "UPDATE trips SET spent = spent + ?, updated_at = datetime('now') WHERE id = ?"
292
+ ).run(input.cost, input.trip_id);
293
+ }
294
+
295
+ return getBooking(id)!;
296
+ }
297
+
298
+ export function getBooking(id: string): Booking | null {
299
+ const db = getDatabase();
300
+ const row = db.prepare("SELECT * FROM bookings WHERE id = ?").get(id) as BookingRow | null;
301
+ return row ? rowToBooking(row) : null;
302
+ }
303
+
304
+ export interface ListBookingsOptions {
305
+ trip_id?: string;
306
+ type?: string;
307
+ status?: string;
308
+ limit?: number;
309
+ }
310
+
311
+ export function listBookings(options: ListBookingsOptions = {}): Booking[] {
312
+ const db = getDatabase();
313
+ const conditions: string[] = [];
314
+ const params: unknown[] = [];
315
+
316
+ if (options.trip_id) {
317
+ conditions.push("trip_id = ?");
318
+ params.push(options.trip_id);
319
+ }
320
+ if (options.type) {
321
+ conditions.push("type = ?");
322
+ params.push(options.type);
323
+ }
324
+ if (options.status) {
325
+ conditions.push("status = ?");
326
+ params.push(options.status);
327
+ }
328
+
329
+ let sql = "SELECT * FROM bookings";
330
+ if (conditions.length > 0) {
331
+ sql += " WHERE " + conditions.join(" AND ");
332
+ }
333
+ sql += " ORDER BY check_in ASC, created_at DESC";
334
+
335
+ if (options.limit) {
336
+ sql += " LIMIT ?";
337
+ params.push(options.limit);
338
+ }
339
+
340
+ const rows = db.prepare(sql).all(...params) as BookingRow[];
341
+ return rows.map(rowToBooking);
342
+ }
343
+
344
+ export function cancelBooking(id: string): Booking | null {
345
+ const db = getDatabase();
346
+ const existing = getBooking(id);
347
+ if (!existing) return null;
348
+
349
+ db.prepare("UPDATE bookings SET status = 'cancelled' WHERE id = ?").run(id);
350
+
351
+ // Subtract cost from trip spent if booking had a cost
352
+ if (existing.cost && existing.cost > 0 && existing.status !== "cancelled") {
353
+ db.prepare(
354
+ "UPDATE trips SET spent = MAX(0, spent - ?), updated_at = datetime('now') WHERE id = ?"
355
+ ).run(existing.cost, existing.trip_id);
356
+ }
357
+
358
+ return getBooking(id);
359
+ }
360
+
361
+ export function deleteBooking(id: string): boolean {
362
+ const db = getDatabase();
363
+ const existing = getBooking(id);
364
+ if (!existing) return false;
365
+
366
+ // Subtract cost from trip spent if booking was not cancelled
367
+ if (existing.cost && existing.cost > 0 && existing.status !== "cancelled") {
368
+ db.prepare(
369
+ "UPDATE trips SET spent = MAX(0, spent - ?), updated_at = datetime('now') WHERE id = ?"
370
+ ).run(existing.cost, existing.trip_id);
371
+ }
372
+
373
+ const result = db.prepare("DELETE FROM bookings WHERE id = ?").run(id);
374
+ return result.changes > 0;
375
+ }
376
+
377
+ // ==================== DOCUMENTS ====================
378
+
379
+ export interface TravelDocument {
380
+ id: string;
381
+ trip_id: string;
382
+ type: string;
383
+ name: string;
384
+ number: string | null;
385
+ expires_at: string | null;
386
+ file_path: string | null;
387
+ metadata: Record<string, unknown>;
388
+ created_at: string;
389
+ }
390
+
391
+ interface TravelDocumentRow {
392
+ id: string;
393
+ trip_id: string;
394
+ type: string;
395
+ name: string;
396
+ number: string | null;
397
+ expires_at: string | null;
398
+ file_path: string | null;
399
+ metadata: string;
400
+ created_at: string;
401
+ }
402
+
403
+ function rowToDocument(row: TravelDocumentRow): TravelDocument {
404
+ return {
405
+ ...row,
406
+ metadata: JSON.parse(row.metadata || "{}"),
407
+ };
408
+ }
409
+
410
+ export interface CreateDocumentInput {
411
+ trip_id: string;
412
+ type: string;
413
+ name: string;
414
+ number?: string;
415
+ expires_at?: string;
416
+ file_path?: string;
417
+ metadata?: Record<string, unknown>;
418
+ }
419
+
420
+ export function createDocument(input: CreateDocumentInput): TravelDocument {
421
+ const db = getDatabase();
422
+ const id = crypto.randomUUID();
423
+ const metadata = JSON.stringify(input.metadata || {});
424
+
425
+ db.prepare(
426
+ `INSERT INTO documents (id, trip_id, type, name, number, expires_at, file_path, metadata)
427
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
428
+ ).run(
429
+ id,
430
+ input.trip_id,
431
+ input.type,
432
+ input.name,
433
+ input.number || null,
434
+ input.expires_at || null,
435
+ input.file_path || null,
436
+ metadata
437
+ );
438
+
439
+ return getDocument(id)!;
440
+ }
441
+
442
+ export function getDocument(id: string): TravelDocument | null {
443
+ const db = getDatabase();
444
+ const row = db.prepare("SELECT * FROM documents WHERE id = ?").get(id) as TravelDocumentRow | null;
445
+ return row ? rowToDocument(row) : null;
446
+ }
447
+
448
+ export interface ListDocumentsOptions {
449
+ trip_id?: string;
450
+ type?: string;
451
+ limit?: number;
452
+ }
453
+
454
+ export function listDocuments(options: ListDocumentsOptions = {}): TravelDocument[] {
455
+ const db = getDatabase();
456
+ const conditions: string[] = [];
457
+ const params: unknown[] = [];
458
+
459
+ if (options.trip_id) {
460
+ conditions.push("trip_id = ?");
461
+ params.push(options.trip_id);
462
+ }
463
+ if (options.type) {
464
+ conditions.push("type = ?");
465
+ params.push(options.type);
466
+ }
467
+
468
+ let sql = "SELECT * FROM documents";
469
+ if (conditions.length > 0) {
470
+ sql += " WHERE " + conditions.join(" AND ");
471
+ }
472
+ sql += " ORDER BY expires_at ASC, created_at DESC";
473
+
474
+ if (options.limit) {
475
+ sql += " LIMIT ?";
476
+ params.push(options.limit);
477
+ }
478
+
479
+ const rows = db.prepare(sql).all(...params) as TravelDocumentRow[];
480
+ return rows.map(rowToDocument);
481
+ }
482
+
483
+ export function deleteDocument(id: string): boolean {
484
+ const db = getDatabase();
485
+ const result = db.prepare("DELETE FROM documents WHERE id = ?").run(id);
486
+ return result.changes > 0;
487
+ }
488
+
489
+ // ==================== LOYALTY PROGRAMS ====================
490
+
491
+ export interface LoyaltyProgram {
492
+ id: string;
493
+ program_name: string;
494
+ member_id: string | null;
495
+ tier: string | null;
496
+ points: number;
497
+ miles: number;
498
+ expires_at: string | null;
499
+ metadata: Record<string, unknown>;
500
+ created_at: string;
501
+ }
502
+
503
+ interface LoyaltyProgramRow {
504
+ id: string;
505
+ program_name: string;
506
+ member_id: string | null;
507
+ tier: string | null;
508
+ points: number;
509
+ miles: number;
510
+ expires_at: string | null;
511
+ metadata: string;
512
+ created_at: string;
513
+ }
514
+
515
+ function rowToLoyalty(row: LoyaltyProgramRow): LoyaltyProgram {
516
+ return {
517
+ ...row,
518
+ metadata: JSON.parse(row.metadata || "{}"),
519
+ };
520
+ }
521
+
522
+ export interface CreateLoyaltyInput {
523
+ program_name: string;
524
+ member_id?: string;
525
+ tier?: string;
526
+ points?: number;
527
+ miles?: number;
528
+ expires_at?: string;
529
+ metadata?: Record<string, unknown>;
530
+ }
531
+
532
+ export function createLoyaltyProgram(input: CreateLoyaltyInput): LoyaltyProgram {
533
+ const db = getDatabase();
534
+ const id = crypto.randomUUID();
535
+ const metadata = JSON.stringify(input.metadata || {});
536
+
537
+ db.prepare(
538
+ `INSERT INTO loyalty_programs (id, program_name, member_id, tier, points, miles, expires_at, metadata)
539
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
540
+ ).run(
541
+ id,
542
+ input.program_name,
543
+ input.member_id || null,
544
+ input.tier || null,
545
+ input.points ?? 0,
546
+ input.miles ?? 0,
547
+ input.expires_at || null,
548
+ metadata
549
+ );
550
+
551
+ return getLoyaltyProgram(id)!;
552
+ }
553
+
554
+ export function getLoyaltyProgram(id: string): LoyaltyProgram | null {
555
+ const db = getDatabase();
556
+ const row = db.prepare("SELECT * FROM loyalty_programs WHERE id = ?").get(id) as LoyaltyProgramRow | null;
557
+ return row ? rowToLoyalty(row) : null;
558
+ }
559
+
560
+ export function listLoyaltyPrograms(): LoyaltyProgram[] {
561
+ const db = getDatabase();
562
+ const rows = db.prepare("SELECT * FROM loyalty_programs ORDER BY program_name").all() as LoyaltyProgramRow[];
563
+ return rows.map(rowToLoyalty);
564
+ }
565
+
566
+ export interface UpdateLoyaltyInput {
567
+ program_name?: string;
568
+ member_id?: string;
569
+ tier?: string;
570
+ points?: number;
571
+ miles?: number;
572
+ expires_at?: string;
573
+ metadata?: Record<string, unknown>;
574
+ }
575
+
576
+ export function updateLoyaltyProgram(id: string, input: UpdateLoyaltyInput): LoyaltyProgram | null {
577
+ const db = getDatabase();
578
+ const existing = getLoyaltyProgram(id);
579
+ if (!existing) return null;
580
+
581
+ const sets: string[] = [];
582
+ const params: unknown[] = [];
583
+
584
+ if (input.program_name !== undefined) {
585
+ sets.push("program_name = ?");
586
+ params.push(input.program_name);
587
+ }
588
+ if (input.member_id !== undefined) {
589
+ sets.push("member_id = ?");
590
+ params.push(input.member_id);
591
+ }
592
+ if (input.tier !== undefined) {
593
+ sets.push("tier = ?");
594
+ params.push(input.tier);
595
+ }
596
+ if (input.points !== undefined) {
597
+ sets.push("points = ?");
598
+ params.push(input.points);
599
+ }
600
+ if (input.miles !== undefined) {
601
+ sets.push("miles = ?");
602
+ params.push(input.miles);
603
+ }
604
+ if (input.expires_at !== undefined) {
605
+ sets.push("expires_at = ?");
606
+ params.push(input.expires_at);
607
+ }
608
+ if (input.metadata !== undefined) {
609
+ sets.push("metadata = ?");
610
+ params.push(JSON.stringify(input.metadata));
611
+ }
612
+
613
+ if (sets.length === 0) return existing;
614
+
615
+ params.push(id);
616
+ db.prepare(
617
+ `UPDATE loyalty_programs SET ${sets.join(", ")} WHERE id = ?`
618
+ ).run(...params);
619
+
620
+ return getLoyaltyProgram(id);
621
+ }
622
+
623
+ export function deleteLoyaltyProgram(id: string): boolean {
624
+ const db = getDatabase();
625
+ const result = db.prepare("DELETE FROM loyalty_programs WHERE id = ?").run(id);
626
+ return result.changes > 0;
627
+ }
628
+
629
+ // ==================== SPECIAL QUERIES ====================
630
+
631
+ export function getUpcomingTrips(days: number = 30): Trip[] {
632
+ const db = getDatabase();
633
+ const rows = db.prepare(
634
+ `SELECT * FROM trips
635
+ WHERE start_date IS NOT NULL
636
+ AND start_date >= date('now')
637
+ AND start_date <= date('now', '+' || ? || ' days')
638
+ AND status NOT IN ('completed', 'cancelled')
639
+ ORDER BY start_date ASC`
640
+ ).all(days) as TripRow[];
641
+ return rows.map(rowToTrip);
642
+ }
643
+
644
+ export interface BudgetVsActual {
645
+ trip_id: string;
646
+ trip_name: string;
647
+ budget: number | null;
648
+ spent: number;
649
+ currency: string;
650
+ remaining: number | null;
651
+ over_budget: boolean;
652
+ bookings_by_type: Record<string, number>;
653
+ }
654
+
655
+ export function getTripBudgetVsActual(tripId: string): BudgetVsActual | null {
656
+ const db = getDatabase();
657
+ const trip = getTrip(tripId);
658
+ if (!trip) return null;
659
+
660
+ const bookingCosts = db.prepare(
661
+ `SELECT type, SUM(cost) as total_cost
662
+ FROM bookings
663
+ WHERE trip_id = ? AND status != 'cancelled'
664
+ GROUP BY type`
665
+ ).all(tripId) as { type: string; total_cost: number }[];
666
+
667
+ const bookings_by_type: Record<string, number> = {};
668
+ for (const row of bookingCosts) {
669
+ bookings_by_type[row.type] = row.total_cost;
670
+ }
671
+
672
+ return {
673
+ trip_id: trip.id,
674
+ trip_name: trip.name,
675
+ budget: trip.budget,
676
+ spent: trip.spent,
677
+ currency: trip.currency,
678
+ remaining: trip.budget !== null ? trip.budget - trip.spent : null,
679
+ over_budget: trip.budget !== null ? trip.spent > trip.budget : false,
680
+ bookings_by_type,
681
+ };
682
+ }
683
+
684
+ export function getExpiringDocuments(days: number = 90): TravelDocument[] {
685
+ const db = getDatabase();
686
+ const rows = db.prepare(
687
+ `SELECT * FROM documents
688
+ WHERE expires_at IS NOT NULL
689
+ AND expires_at >= date('now')
690
+ AND expires_at <= date('now', '+' || ? || ' days')
691
+ ORDER BY expires_at ASC`
692
+ ).all(days) as TravelDocumentRow[];
693
+ return rows.map(rowToDocument);
694
+ }
695
+
696
+ export interface LoyaltyPointsSummary {
697
+ total_points: number;
698
+ total_miles: number;
699
+ programs: Array<{
700
+ program_name: string;
701
+ member_id: string | null;
702
+ tier: string | null;
703
+ points: number;
704
+ miles: number;
705
+ expires_at: string | null;
706
+ }>;
707
+ }
708
+
709
+ export function getLoyaltyPointsSummary(): LoyaltyPointsSummary {
710
+ const db = getDatabase();
711
+ const rows = db.prepare(
712
+ "SELECT program_name, member_id, tier, points, miles, expires_at FROM loyalty_programs ORDER BY program_name"
713
+ ).all() as Array<{
714
+ program_name: string;
715
+ member_id: string | null;
716
+ tier: string | null;
717
+ points: number;
718
+ miles: number;
719
+ expires_at: string | null;
720
+ }>;
721
+
722
+ let total_points = 0;
723
+ let total_miles = 0;
724
+ for (const r of rows) {
725
+ total_points += r.points;
726
+ total_miles += r.miles;
727
+ }
728
+
729
+ return {
730
+ total_points,
731
+ total_miles,
732
+ programs: rows,
733
+ };
734
+ }
735
+
736
+ export interface TravelStats {
737
+ trips_taken: number;
738
+ total_spent: number;
739
+ by_destination: Record<string, number>;
740
+ by_booking_type: Record<string, number>;
741
+ }
742
+
743
+ export function getTravelStats(year?: number): TravelStats {
744
+ const db = getDatabase();
745
+
746
+ let tripCondition = "status = 'completed'";
747
+ const tripParams: unknown[] = [];
748
+ if (year) {
749
+ tripCondition += " AND strftime('%Y', start_date) = ?";
750
+ tripParams.push(String(year));
751
+ }
752
+
753
+ const tripCount = db.prepare(
754
+ `SELECT COUNT(*) as count FROM trips WHERE ${tripCondition}`
755
+ ).get(...tripParams) as { count: number };
756
+
757
+ const totalSpent = db.prepare(
758
+ `SELECT COALESCE(SUM(spent), 0) as total FROM trips WHERE ${tripCondition}`
759
+ ).get(...tripParams) as { total: number };
760
+
761
+ // By destination
762
+ const destRows = db.prepare(
763
+ `SELECT destination, COUNT(*) as count FROM trips
764
+ WHERE ${tripCondition} AND destination IS NOT NULL
765
+ GROUP BY destination ORDER BY count DESC`
766
+ ).all(...tripParams) as { destination: string; count: number }[];
767
+
768
+ const by_destination: Record<string, number> = {};
769
+ for (const row of destRows) {
770
+ by_destination[row.destination] = row.count;
771
+ }
772
+
773
+ // By booking type — join with trips for year filter
774
+ let bookingQuery: string;
775
+ const bookingParams: unknown[] = [];
776
+ if (year) {
777
+ bookingQuery = `SELECT b.type, COUNT(*) as count FROM bookings b
778
+ JOIN trips t ON b.trip_id = t.id
779
+ WHERE t.status = 'completed' AND strftime('%Y', t.start_date) = ? AND b.status != 'cancelled'
780
+ GROUP BY b.type ORDER BY count DESC`;
781
+ bookingParams.push(String(year));
782
+ } else {
783
+ bookingQuery = `SELECT b.type, COUNT(*) as count FROM bookings b
784
+ JOIN trips t ON b.trip_id = t.id
785
+ WHERE t.status = 'completed' AND b.status != 'cancelled'
786
+ GROUP BY b.type ORDER BY count DESC`;
787
+ }
788
+
789
+ const bookingRows = db.prepare(bookingQuery).all(...bookingParams) as { type: string; count: number }[];
790
+
791
+ const by_booking_type: Record<string, number> = {};
792
+ for (const row of bookingRows) {
793
+ by_booking_type[row.type] = row.count;
794
+ }
795
+
796
+ return {
797
+ trips_taken: tripCount.count,
798
+ total_spent: totalSpent.total,
799
+ by_destination,
800
+ by_booking_type,
801
+ };
802
+ }