@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.
- package/bin/index.js +236 -36
- package/bin/mcp.js +153 -4
- package/dist/index.js +120 -3
- package/microservices/microservice-analytics/package.json +27 -0
- package/microservices/microservice-analytics/src/cli/index.ts +373 -0
- package/microservices/microservice-analytics/src/db/analytics.ts +564 -0
- package/microservices/microservice-analytics/src/db/database.ts +93 -0
- package/microservices/microservice-analytics/src/db/migrations.ts +50 -0
- package/microservices/microservice-analytics/src/index.ts +37 -0
- package/microservices/microservice-analytics/src/mcp/index.ts +334 -0
- package/microservices/microservice-assets/package.json +27 -0
- package/microservices/microservice-assets/src/cli/index.ts +375 -0
- package/microservices/microservice-assets/src/db/assets.ts +370 -0
- package/microservices/microservice-assets/src/db/database.ts +93 -0
- package/microservices/microservice-assets/src/db/migrations.ts +51 -0
- package/microservices/microservice-assets/src/index.ts +32 -0
- package/microservices/microservice-assets/src/mcp/index.ts +346 -0
- package/microservices/microservice-compliance/package.json +27 -0
- package/microservices/microservice-compliance/src/cli/index.ts +467 -0
- package/microservices/microservice-compliance/src/db/compliance.ts +633 -0
- package/microservices/microservice-compliance/src/db/database.ts +93 -0
- package/microservices/microservice-compliance/src/db/migrations.ts +63 -0
- package/microservices/microservice-compliance/src/index.ts +46 -0
- package/microservices/microservice-compliance/src/mcp/index.ts +438 -0
- package/microservices/microservice-habits/package.json +27 -0
- package/microservices/microservice-habits/src/cli/index.ts +315 -0
- package/microservices/microservice-habits/src/db/database.ts +93 -0
- package/microservices/microservice-habits/src/db/habits.ts +451 -0
- package/microservices/microservice-habits/src/db/migrations.ts +46 -0
- package/microservices/microservice-habits/src/index.ts +31 -0
- package/microservices/microservice-habits/src/mcp/index.ts +313 -0
- package/microservices/microservice-health/package.json +27 -0
- package/microservices/microservice-health/src/cli/index.ts +484 -0
- package/microservices/microservice-health/src/db/database.ts +93 -0
- package/microservices/microservice-health/src/db/health.ts +708 -0
- package/microservices/microservice-health/src/db/migrations.ts +70 -0
- package/microservices/microservice-health/src/index.ts +63 -0
- package/microservices/microservice-health/src/mcp/index.ts +437 -0
- package/microservices/microservice-leads/package.json +27 -0
- package/microservices/microservice-leads/src/cli/index.ts +596 -0
- package/microservices/microservice-leads/src/db/database.ts +93 -0
- package/microservices/microservice-leads/src/db/leads.ts +520 -0
- package/microservices/microservice-leads/src/db/lists.ts +151 -0
- package/microservices/microservice-leads/src/db/migrations.ts +93 -0
- package/microservices/microservice-leads/src/index.ts +65 -0
- package/microservices/microservice-leads/src/lib/enrichment.ts +202 -0
- package/microservices/microservice-leads/src/lib/scoring.ts +134 -0
- package/microservices/microservice-leads/src/mcp/index.ts +533 -0
- package/microservices/microservice-notifications/package.json +27 -0
- package/microservices/microservice-notifications/src/cli/index.ts +349 -0
- package/microservices/microservice-notifications/src/db/database.ts +93 -0
- package/microservices/microservice-notifications/src/db/migrations.ts +62 -0
- package/microservices/microservice-notifications/src/db/notifications.ts +509 -0
- package/microservices/microservice-notifications/src/index.ts +41 -0
- package/microservices/microservice-notifications/src/mcp/index.ts +422 -0
- package/microservices/microservice-products/package.json +27 -0
- package/microservices/microservice-products/src/cli/index.ts +416 -0
- package/microservices/microservice-products/src/db/categories.ts +154 -0
- package/microservices/microservice-products/src/db/database.ts +93 -0
- package/microservices/microservice-products/src/db/migrations.ts +58 -0
- package/microservices/microservice-products/src/db/pricing-tiers.ts +66 -0
- package/microservices/microservice-products/src/db/products.ts +452 -0
- package/microservices/microservice-products/src/index.ts +53 -0
- package/microservices/microservice-products/src/mcp/index.ts +453 -0
- package/microservices/microservice-projects/package.json +27 -0
- package/microservices/microservice-projects/src/cli/index.ts +480 -0
- package/microservices/microservice-projects/src/db/database.ts +93 -0
- package/microservices/microservice-projects/src/db/migrations.ts +65 -0
- package/microservices/microservice-projects/src/db/projects.ts +715 -0
- package/microservices/microservice-projects/src/index.ts +57 -0
- package/microservices/microservice-projects/src/mcp/index.ts +501 -0
- package/microservices/microservice-proposals/package.json +27 -0
- package/microservices/microservice-proposals/src/cli/index.ts +400 -0
- package/microservices/microservice-proposals/src/db/database.ts +93 -0
- package/microservices/microservice-proposals/src/db/migrations.ts +52 -0
- package/microservices/microservice-proposals/src/db/proposals.ts +532 -0
- package/microservices/microservice-proposals/src/index.ts +37 -0
- package/microservices/microservice-proposals/src/mcp/index.ts +375 -0
- package/microservices/microservice-reading/package.json +27 -0
- package/microservices/microservice-reading/src/cli/index.ts +464 -0
- package/microservices/microservice-reading/src/db/database.ts +93 -0
- package/microservices/microservice-reading/src/db/migrations.ts +59 -0
- package/microservices/microservice-reading/src/db/reading.ts +524 -0
- package/microservices/microservice-reading/src/index.ts +51 -0
- package/microservices/microservice-reading/src/mcp/index.ts +368 -0
- package/microservices/microservice-travel/package.json +27 -0
- package/microservices/microservice-travel/src/cli/index.ts +505 -0
- package/microservices/microservice-travel/src/db/database.ts +93 -0
- package/microservices/microservice-travel/src/db/migrations.ts +77 -0
- package/microservices/microservice-travel/src/db/travel.ts +802 -0
- package/microservices/microservice-travel/src/index.ts +60 -0
- package/microservices/microservice-travel/src/mcp/index.ts +495 -0
- package/microservices/microservice-wiki/package.json +27 -0
- package/microservices/microservice-wiki/src/cli/index.ts +345 -0
- package/microservices/microservice-wiki/src/db/database.ts +93 -0
- package/microservices/microservice-wiki/src/db/migrations.ts +55 -0
- package/microservices/microservice-wiki/src/db/wiki.ts +395 -0
- package/microservices/microservice-wiki/src/index.ts +32 -0
- package/microservices/microservice-wiki/src/mcp/index.ts +344 -0
- 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
|
+
}
|