@growsober/sdk 1.0.5 → 1.0.8

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 (70) hide show
  1. package/dist/__tests__/e2e.test.d.ts +30 -0
  2. package/dist/__tests__/e2e.test.js +959 -63
  3. package/dist/api/mutations/badges.d.ts +116 -0
  4. package/dist/api/mutations/badges.js +177 -0
  5. package/dist/api/mutations/brands.d.ts +251 -0
  6. package/dist/api/mutations/brands.js +242 -0
  7. package/dist/api/mutations/creators.d.ts +131 -0
  8. package/dist/api/mutations/creators.js +129 -0
  9. package/dist/api/mutations/event-chat.d.ts +2 -2
  10. package/dist/api/mutations/event-chat.js +9 -9
  11. package/dist/api/mutations/index.d.ts +4 -0
  12. package/dist/api/mutations/index.js +5 -1
  13. package/dist/api/mutations/jack.d.ts +29 -0
  14. package/dist/api/mutations/jack.js +41 -1
  15. package/dist/api/mutations/products.d.ts +175 -0
  16. package/dist/api/mutations/products.js +226 -0
  17. package/dist/api/mutations/support.d.ts +20 -1
  18. package/dist/api/mutations/support.js +36 -1
  19. package/dist/api/queries/badges.d.ts +221 -0
  20. package/dist/api/queries/badges.js +290 -0
  21. package/dist/api/queries/bookings.d.ts +1 -1
  22. package/dist/api/queries/brands.d.ts +248 -0
  23. package/dist/api/queries/brands.js +226 -0
  24. package/dist/api/queries/businesses.d.ts +61 -1
  25. package/dist/api/queries/businesses.js +27 -1
  26. package/dist/api/queries/creators.d.ts +332 -0
  27. package/dist/api/queries/creators.js +249 -0
  28. package/dist/api/queries/event-chat.d.ts +1 -1
  29. package/dist/api/queries/event-chat.js +4 -4
  30. package/dist/api/queries/events.d.ts +45 -0
  31. package/dist/api/queries/index.d.ts +5 -0
  32. package/dist/api/queries/index.js +6 -1
  33. package/dist/api/queries/jack.d.ts +80 -0
  34. package/dist/api/queries/jack.js +98 -1
  35. package/dist/api/queries/library.d.ts +8 -0
  36. package/dist/api/queries/products.d.ts +185 -0
  37. package/dist/api/queries/products.js +203 -0
  38. package/dist/api/queries/support.d.ts +46 -1
  39. package/dist/api/queries/support.js +48 -1
  40. package/dist/api/queries/venues.d.ts +304 -0
  41. package/dist/api/queries/venues.js +211 -0
  42. package/dist/api/types.d.ts +245 -0
  43. package/dist/api/types.js +6 -1
  44. package/dist/api/utils/eventGrouping.d.ts +104 -0
  45. package/dist/api/utils/eventGrouping.js +155 -0
  46. package/dist/index.d.ts +1 -0
  47. package/dist/index.js +5 -1
  48. package/package.json +5 -2
  49. package/src/__tests__/e2e.test.ts +996 -64
  50. package/src/api/mutations/badges.ts +228 -0
  51. package/src/api/mutations/brands.ts +376 -0
  52. package/src/api/mutations/creators.ts +171 -0
  53. package/src/api/mutations/event-chat.ts +8 -8
  54. package/src/api/mutations/index.ts +4 -0
  55. package/src/api/mutations/jack.ts +50 -1
  56. package/src/api/mutations/products.ts +336 -0
  57. package/src/api/mutations/support.ts +44 -0
  58. package/src/api/queries/badges.ts +385 -0
  59. package/src/api/queries/brands.ts +281 -0
  60. package/src/api/queries/businesses.ts +30 -1
  61. package/src/api/queries/creators.ts +308 -0
  62. package/src/api/queries/event-chat.ts +3 -3
  63. package/src/api/queries/index.ts +5 -0
  64. package/src/api/queries/jack.ts +139 -1
  65. package/src/api/queries/products.ts +312 -0
  66. package/src/api/queries/support.ts +54 -0
  67. package/src/api/queries/venues.ts +271 -0
  68. package/src/api/types.ts +317 -1
  69. package/src/api/utils/eventGrouping.ts +181 -0
  70. package/src/index.ts +6 -0
@@ -3,11 +3,41 @@
3
3
  *
4
4
  * Tests all SDK endpoints against the real API.
5
5
  * Run with: npx ts-node src/__tests__/e2e.test.ts
6
+ *
7
+ * Covers:
8
+ * - Authentication
9
+ * - Users
10
+ * - Events (with venue, creator, brand sponsor)
11
+ * - Bookings
12
+ * - Event Chat (new roles: ATTENDEE, CREATOR, VENUE_REP, BRAND_REP)
13
+ * - Hubs
14
+ * - Venues (new - replaces businesses)
15
+ * - Creators (new)
16
+ * - Brands (new)
17
+ * - Unified Rewards (venue + brand rewards)
18
+ * - Library (content workflow)
19
+ * - Support (check-ins, wins, Jack AI)
20
+ * - Creator Products (sessions/packages with booking flow)
21
+ * - Badges & Rewards
22
+ * - Badge Awarding (business logic tests for automatic badge awarding)
23
+ * - CHECK_IN_STREAK badge on check-in
24
+ * - WINS_LOGGED badge on logging wins
25
+ * - MOOD_LOGS_COUNT badge on mood logging
26
+ * - REFLECTIONS_COUNT badge on submitting reflections
27
+ * - EVENTS_HOSTED badge on publishing events
28
+ * - EVENTS_ATTENDED badge progress tracking
29
+ * - Reward Flow (full redemption lifecycle)
30
+ * - Creating rewards for badges (venue/brand)
31
+ * - User earns badge -> reward becomes available
32
+ * - Redeem reward (get redemption details)
33
+ * - Cannot redeem without badge
34
+ * - Cannot redeem twice (per user limit)
35
+ * - Reward verification endpoint
6
36
  */
7
37
 
8
38
  import axios, { AxiosInstance } from 'axios';
9
39
 
10
- const BASE_URL = 'http://localhost:3001/api/v1';
40
+ const BASE_URL = process.env.API_URL || 'http://localhost:3001/api/v1';
11
41
  const TEST_EMAIL = `e2e-${Date.now()}@growsober.com`;
12
42
  const TEST_PASSWORD = 'SecurePass123!';
13
43
 
@@ -19,6 +49,7 @@ const c = {
19
49
  yellow: '\x1b[33m',
20
50
  cyan: '\x1b[36m',
21
51
  dim: '\x1b[2m',
52
+ bold: '\x1b[1m',
22
53
  };
23
54
 
24
55
  // State shared across tests
@@ -31,6 +62,20 @@ const state: {
31
62
  bookingId?: string;
32
63
  chatId?: string;
33
64
  conversationId?: string;
65
+ // New state for restructured models
66
+ venueId?: string;
67
+ creatorId?: string;
68
+ brandId?: string;
69
+ availabilityId?: string;
70
+ rewardId?: string;
71
+ badgeId?: string;
72
+ contentId?: string;
73
+ // Creator products
74
+ productId?: string;
75
+ productBookingId?: string;
76
+ // Second user for ownership tests
77
+ secondUserToken?: string;
78
+ secondUserId?: string;
34
79
  } = {
35
80
  accessToken: '',
36
81
  refreshToken: '',
@@ -40,6 +85,7 @@ const state: {
40
85
  // Stats
41
86
  let passed = 0;
42
87
  let failed = 0;
88
+ let skipped = 0;
43
89
 
44
90
  // Create axios client
45
91
  function api(token?: string): AxiosInstance {
@@ -66,9 +112,20 @@ async function test(name: string, fn: () => Promise<void>): Promise<boolean> {
66
112
  }
67
113
  }
68
114
 
69
- // Unwrap API response
70
- function unwrap<T>(res: { data: { data: T } }): T {
71
- return res.data.data;
115
+ // Skip test with reason
116
+ function skip(name: string, reason: string): void {
117
+ console.log(`${c.yellow}SKIP${c.reset} ${name} ${c.dim}(${reason})${c.reset}`);
118
+ skipped++;
119
+ }
120
+
121
+ // Unwrap API response (handles both wrapped and unwrapped)
122
+ function unwrap<T>(res: { data: any }): T {
123
+ return res.data?.data !== undefined ? res.data.data : res.data;
124
+ }
125
+
126
+ // Section header
127
+ function section(name: string): void {
128
+ console.log(`\n${c.cyan}--- ${name} ---${c.reset}\n`);
72
129
  }
73
130
 
74
131
  // ============================================================================
@@ -76,16 +133,16 @@ function unwrap<T>(res: { data: { data: T } }): T {
76
133
  // ============================================================================
77
134
 
78
135
  async function runTests() {
79
- console.log(`\n${c.cyan}${'='.repeat(60)}${c.reset}`);
80
- console.log(`${c.cyan} GrowSober API E2E Tests${c.reset}`);
81
- console.log(`${c.cyan}${'='.repeat(60)}${c.reset}`);
136
+ console.log(`\n${c.cyan}${'='.repeat(70)}${c.reset}`);
137
+ console.log(`${c.cyan} GrowSober API E2E Tests - Full Coverage${c.reset}`);
138
+ console.log(`${c.cyan}${'='.repeat(70)}${c.reset}`);
82
139
  console.log(`${c.dim}Base URL: ${BASE_URL}${c.reset}`);
83
140
  console.log(`${c.dim}Test Email: ${TEST_EMAIL}${c.reset}\n`);
84
141
 
85
142
  // Health check
86
143
  const health = await api().get('/health');
87
144
  if (health.status !== 200) {
88
- console.log(`${c.red}API is not reachable${c.reset}`);
145
+ console.log(`${c.red}API is not reachable at ${BASE_URL}${c.reset}`);
89
146
  process.exit(1);
90
147
  }
91
148
  console.log(`${c.green}API is healthy${c.reset}\n`);
@@ -93,7 +150,7 @@ async function runTests() {
93
150
  // =========================================================================
94
151
  // AUTH
95
152
  // =========================================================================
96
- console.log(`\n${c.cyan}--- AUTHENTICATION ---${c.reset}\n`);
153
+ section('AUTHENTICATION');
97
154
 
98
155
  await test('Register new user', async () => {
99
156
  const res = await api().post('/auth/register', {
@@ -142,10 +199,23 @@ async function runTests() {
142
199
  if (res.status !== 401) throw new Error(`Expected 401, got ${res.status}`);
143
200
  });
144
201
 
202
+ // Register second user for ownership tests
203
+ await test('Register second user for ownership tests', async () => {
204
+ const res = await api().post('/auth/register', {
205
+ email: `e2e-second-${Date.now()}@growsober.com`,
206
+ password: TEST_PASSWORD,
207
+ name: 'E2E Second User',
208
+ });
209
+ if (res.status !== 201) throw new Error(`Status ${res.status}`);
210
+ const data = unwrap<any>(res);
211
+ state.secondUserToken = data.accessToken;
212
+ state.secondUserId = data.user.id;
213
+ });
214
+
145
215
  // =========================================================================
146
216
  // USERS
147
217
  // =========================================================================
148
- console.log(`\n${c.cyan}--- USERS ---${c.reset}\n`);
218
+ section('USERS');
149
219
 
150
220
  await test('Get current user profile', async () => {
151
221
  const res = await api(state.accessToken).get('/users/me');
@@ -171,9 +241,416 @@ async function runTests() {
171
241
  });
172
242
 
173
243
  // =========================================================================
174
- // EVENTS
244
+ // VENUES (New - replaces Businesses)
245
+ // =========================================================================
246
+ section('VENUES');
247
+
248
+ await test('List venues', async () => {
249
+ const res = await api().get('/venues');
250
+ if (res.status !== 200) throw new Error(`Status ${res.status}`);
251
+ const data = unwrap<any>(res);
252
+ // Get first venue if exists for further tests
253
+ if (data?.venues?.length > 0) {
254
+ state.venueId = data.venues[0].id;
255
+ }
256
+ });
257
+
258
+ await test('Get featured venues', async () => {
259
+ const res = await api().get('/venues/featured');
260
+ if (res.status !== 200) throw new Error(`Status ${res.status}`);
261
+ });
262
+
263
+ await test('Get nearby venues', async () => {
264
+ const res = await api().get('/venues/nearby', {
265
+ params: { lat: 51.5074, long: -0.1278, radius: 10 },
266
+ });
267
+ if (res.status !== 200) throw new Error(`Status ${res.status}`);
268
+ });
269
+
270
+ if (state.venueId) {
271
+ await test('Get venue by ID', async () => {
272
+ const res = await api().get(`/venues/${state.venueId}`);
273
+ if (res.status !== 200) throw new Error(`Status ${res.status}`);
274
+ });
275
+
276
+ await test('Get venue owners', async () => {
277
+ const res = await api(state.accessToken).get(`/venues/${state.venueId}/owners`);
278
+ if (res.status !== 200) throw new Error(`Status ${res.status}`);
279
+ });
280
+
281
+ await test('Get venue events', async () => {
282
+ const res = await api().get(`/venues/${state.venueId}/events`);
283
+ if (res.status !== 200) throw new Error(`Status ${res.status}`);
284
+ });
285
+ } else {
286
+ skip('Get venue by ID', 'No venues in database');
287
+ skip('Get venue owners', 'No venues in database');
288
+ skip('Get venue events', 'No venues in database');
289
+ }
290
+
291
+ // =========================================================================
292
+ // CREATORS (New)
293
+ // =========================================================================
294
+ section('CREATORS');
295
+
296
+ await test('List creators', async () => {
297
+ const res = await api().get('/creators');
298
+ if (res.status !== 200) throw new Error(`Status ${res.status}`);
299
+ });
300
+
301
+ await test('Create creator profile', async () => {
302
+ const res = await api(state.accessToken).post('/creators', {
303
+ slug: `e2e-creator-${Date.now()}`,
304
+ displayName: 'E2E Test Creator',
305
+ bio: 'A test creator for E2E tests',
306
+ canFacilitate: true,
307
+ canCreateContent: true,
308
+ isInfluencer: false,
309
+ specialties: ['meditation', 'sobriety-coaching'],
310
+ certifications: ['Test Cert 1'],
311
+ });
312
+ if (res.status !== 201) throw new Error(`Status ${res.status}: ${JSON.stringify(res.data)}`);
313
+ const data = unwrap<any>(res);
314
+ state.creatorId = data.id;
315
+ });
316
+
317
+ if (state.creatorId) {
318
+ await test('Get creator by ID', async () => {
319
+ const res = await api().get(`/creators/${state.creatorId}`);
320
+ if (res.status !== 200) throw new Error(`Status ${res.status}`);
321
+ });
322
+
323
+ await test('Get my creator profile', async () => {
324
+ const res = await api(state.accessToken).get('/creators/me');
325
+ // Returns 200 with null or creator data
326
+ if (res.status !== 200) throw new Error(`Status ${res.status}`);
327
+ });
328
+
329
+ await test('Get creator by user ID', async () => {
330
+ const res = await api(state.accessToken).get(`/creators/user/${state.userId}`);
331
+ if (res.status !== 200) throw new Error(`Status ${res.status}`);
332
+ });
333
+
334
+ await test('Update creator profile', async () => {
335
+ const res = await api(state.accessToken).patch(`/creators/${state.creatorId}`, {
336
+ bio: 'Updated bio for E2E testing',
337
+ sessionRate: 50,
338
+ });
339
+ if (res.status !== 200) throw new Error(`Status ${res.status}: ${JSON.stringify(res.data)}`);
340
+ });
341
+
342
+ await test('Add creator availability', async () => {
343
+ const res = await api(state.accessToken).post(`/creators/${state.creatorId}/availability`, {
344
+ dayOfWeek: 1, // Monday
345
+ startTime: '09:00',
346
+ endTime: '17:00',
347
+ timezone: 'Europe/London',
348
+ isRecurring: true,
349
+ });
350
+ if (res.status !== 201) throw new Error(`Status ${res.status}: ${JSON.stringify(res.data)}`);
351
+ const data = unwrap<any>(res);
352
+ state.availabilityId = data.id;
353
+ });
354
+
355
+ await test('Get creator availability', async () => {
356
+ const res = await api().get(`/creators/${state.creatorId}/availability`);
357
+ if (res.status !== 200) throw new Error(`Status ${res.status}`);
358
+ });
359
+
360
+ await test('Get creator content (published)', async () => {
361
+ const res = await api().get(`/creators/${state.creatorId}/content`);
362
+ if (res.status !== 200) throw new Error(`Status ${res.status}`);
363
+ });
364
+
365
+ await test('Get creator events', async () => {
366
+ const res = await api().get(`/creators/${state.creatorId}/events`);
367
+ if (res.status !== 200) throw new Error(`Status ${res.status}`);
368
+ });
369
+
370
+ // Test ownership - second user cannot update
371
+ await test('Second user cannot update another creator profile (403)', async () => {
372
+ const res = await api(state.secondUserToken).patch(`/creators/${state.creatorId}`, {
373
+ bio: 'Hacked bio',
374
+ });
375
+ if (res.status !== 403) throw new Error(`Expected 403, got ${res.status}`);
376
+ });
377
+ }
378
+
379
+ // =========================================================================
380
+ // CREATOR PRODUCTS (Sessions/Packages)
381
+ // =========================================================================
382
+ section('CREATOR PRODUCTS');
383
+
384
+ if (state.creatorId) {
385
+ await test('Create creator product (1-on-1 session)', async () => {
386
+ const res = await api(state.accessToken).post(`/creators/${state.creatorId}/products`, {
387
+ title: 'E2E Test 1-on-1 Session',
388
+ description: 'A test coaching session for E2E testing',
389
+ type: 'SESSION_1ON1',
390
+ price: 5000, // £50 in pence
391
+ currency: 'GBP',
392
+ durationMinutes: 60,
393
+ maxParticipants: 1,
394
+ deliveryMethod: 'VIDEO_CALL',
395
+ });
396
+ if (res.status !== 201) throw new Error(`Status ${res.status}: ${JSON.stringify(res.data)}`);
397
+ const data = unwrap<any>(res);
398
+ state.productId = data.id;
399
+ });
400
+
401
+ await test('Create second product (workshop)', async () => {
402
+ const res = await api(state.accessToken).post(`/creators/${state.creatorId}/products`, {
403
+ title: 'E2E Test Workshop',
404
+ description: 'A group workshop for E2E testing',
405
+ type: 'WORKSHOP',
406
+ price: 2500, // £25 in pence
407
+ currency: 'GBP',
408
+ durationMinutes: 90,
409
+ maxParticipants: 10,
410
+ deliveryMethod: 'VIDEO_CALL',
411
+ });
412
+ if (res.status !== 201) throw new Error(`Status ${res.status}: ${JSON.stringify(res.data)}`);
413
+ });
414
+
415
+ await test('Get creator products', async () => {
416
+ const res = await api(state.accessToken).get(`/creators/${state.creatorId}/products`);
417
+ if (res.status !== 200) throw new Error(`Status ${res.status}`);
418
+ const data = unwrap<any>(res);
419
+ const products = data.products || data;
420
+ if (!Array.isArray(products) || products.length < 2) {
421
+ throw new Error('Expected at least 2 products for creator');
422
+ }
423
+ });
424
+
425
+ if (state.productId) {
426
+ await test('Get product by ID (public)', async () => {
427
+ const res = await api().get(`/products/${state.productId}`);
428
+ if (res.status !== 200) throw new Error(`Status ${res.status}`);
429
+ const data = unwrap<any>(res);
430
+ if (!data.title || !data.price) throw new Error('Missing expected product fields');
431
+ });
432
+
433
+ await test('Update product', async () => {
434
+ const res = await api(state.accessToken).put(`/creators/${state.creatorId}/products/${state.productId}`, {
435
+ price: 6000, // Update to £60
436
+ description: 'Updated description for E2E testing',
437
+ });
438
+ if (res.status !== 200) throw new Error(`Status ${res.status}: ${JSON.stringify(res.data)}`);
439
+ const data = unwrap<any>(res);
440
+ if (data.price !== 6000) throw new Error('Price not updated correctly');
441
+ });
442
+
443
+ // Second user books the product
444
+ await test('Book product (as second user)', async () => {
445
+ const scheduledAt = new Date();
446
+ scheduledAt.setDate(scheduledAt.getDate() + 14); // 2 weeks from now
447
+ scheduledAt.setHours(10, 0, 0, 0);
448
+
449
+ const res = await api(state.secondUserToken).post(`/products/${state.productId}/book`, {
450
+ scheduledAt: scheduledAt.toISOString(),
451
+ timezone: 'Europe/London',
452
+ clientNotes: 'E2E test booking',
453
+ });
454
+ if (res.status !== 201) throw new Error(`Status ${res.status}: ${JSON.stringify(res.data)}`);
455
+ const data = unwrap<any>(res);
456
+ state.productBookingId = data.id;
457
+ });
458
+
459
+ if (state.productBookingId) {
460
+ await test('Get product booking details', async () => {
461
+ const res = await api(state.secondUserToken).get(`/users/me/product-bookings/${state.productBookingId}`);
462
+ if (res.status !== 200) throw new Error(`Status ${res.status}`);
463
+ const data = unwrap<any>(res);
464
+ if (!data.scheduledAt || !data.status) throw new Error('Missing booking fields');
465
+ });
466
+
467
+ await test('Get user product bookings', async () => {
468
+ const res = await api(state.secondUserToken).get('/users/me/product-bookings');
469
+ if (res.status !== 200) throw new Error(`Status ${res.status}`);
470
+ const data = unwrap<any>(res);
471
+ const bookings = Array.isArray(data) ? data : (data.data || []);
472
+ if (!Array.isArray(bookings) || bookings.length === 0) {
473
+ throw new Error('Expected at least one product booking');
474
+ }
475
+ });
476
+
477
+ await test('Get creator bookings (as creator)', async () => {
478
+ const res = await api(state.accessToken).get(`/creators/${state.creatorId}/products/bookings`);
479
+ if (res.status !== 200) throw new Error(`Status ${res.status}`);
480
+ const data = unwrap<any>(res);
481
+ const bookings = Array.isArray(data) ? data : (data.data || []);
482
+ if (!Array.isArray(bookings) || bookings.length === 0) {
483
+ throw new Error('Expected at least one booking for creator products');
484
+ }
485
+ });
486
+
487
+ await test('Update product booking', async () => {
488
+ const newScheduledAt = new Date();
489
+ newScheduledAt.setDate(newScheduledAt.getDate() + 21); // 3 weeks from now
490
+ newScheduledAt.setHours(14, 0, 0, 0);
491
+
492
+ const res = await api(state.secondUserToken).put(`/users/me/product-bookings/${state.productBookingId}`, {
493
+ scheduledAt: newScheduledAt.toISOString(),
494
+ clientNotes: 'Rescheduled - E2E test',
495
+ });
496
+ if (res.status !== 200) throw new Error(`Status ${res.status}: ${JSON.stringify(res.data)}`);
497
+ });
498
+
499
+ await test('Cancel product booking', async () => {
500
+ const res = await api(state.secondUserToken).delete(`/users/me/product-bookings/${state.productBookingId}`, {
501
+ data: { reason: 'E2E test cleanup' },
502
+ });
503
+ if (res.status !== 200 && res.status !== 204) throw new Error(`Status ${res.status}`);
504
+ });
505
+ }
506
+
507
+ // Second user cannot delete creator's product
508
+ await test('Second user cannot delete product (403)', async () => {
509
+ const res = await api(state.secondUserToken).delete(`/creators/${state.creatorId}/products/${state.productId}`);
510
+ if (res.status !== 403) throw new Error(`Expected 403, got ${res.status}`);
511
+ });
512
+ }
513
+ } else {
514
+ skip('Create creator product', 'No creator ID available');
515
+ skip('Create second product', 'No creator ID available');
516
+ skip('Get creator products', 'No creator ID available');
517
+ skip('Get product by ID', 'No product ID available');
518
+ skip('Update product', 'No product ID available');
519
+ skip('Book product', 'No product ID available');
520
+ skip('Get product booking details', 'No booking ID available');
521
+ skip('Get user product bookings', 'No booking ID available');
522
+ skip('Get creator bookings', 'No booking ID available');
523
+ skip('Update product booking', 'No booking ID available');
524
+ skip('Cancel product booking', 'No booking ID available');
525
+ skip('Second user cannot delete product', 'No product ID available');
526
+ }
527
+
528
+ // Test public products listing
529
+ await test('List all public products', async () => {
530
+ const res = await api().get('/products');
531
+ if (res.status !== 200) throw new Error(`Status ${res.status}`);
532
+ const data = unwrap<any>(res);
533
+ // May have products array or paginated response
534
+ const products = data.products || data;
535
+ if (!Array.isArray(products)) throw new Error('Expected products array');
536
+ });
537
+
538
+ // =========================================================================
539
+ // BRANDS (New)
540
+ // =========================================================================
541
+ section('BRANDS');
542
+
543
+ await test('List brands', async () => {
544
+ const res = await api().get('/brands');
545
+ if (res.status !== 200) throw new Error(`Status ${res.status}`);
546
+ });
547
+
548
+ await test('Get featured brands', async () => {
549
+ const res = await api().get('/brands/featured');
550
+ if (res.status !== 200) throw new Error(`Status ${res.status}`);
551
+ });
552
+
553
+ await test('Create brand', async () => {
554
+ const res = await api(state.accessToken).post('/brands', {
555
+ slug: `e2e-brand-${Date.now()}`,
556
+ name: 'E2E Test Brand',
557
+ description: 'A test brand for E2E testing',
558
+ website: 'https://example.com',
559
+ email: 'brand@example.com',
560
+ });
561
+ if (res.status !== 201) throw new Error(`Status ${res.status}: ${JSON.stringify(res.data)}`);
562
+ const data = unwrap<any>(res);
563
+ state.brandId = data.id;
564
+ });
565
+
566
+ if (state.brandId) {
567
+ await test('Get brand by ID', async () => {
568
+ const res = await api().get(`/brands/${state.brandId}`);
569
+ if (res.status !== 200) throw new Error(`Status ${res.status}`);
570
+ });
571
+
572
+ await test('Update brand', async () => {
573
+ const res = await api(state.accessToken).patch(`/brands/${state.brandId}`, {
574
+ description: 'Updated brand description',
575
+ });
576
+ if (res.status !== 200) throw new Error(`Status ${res.status}: ${JSON.stringify(res.data)}`);
577
+ });
578
+
579
+ await test('Get brand owners', async () => {
580
+ const res = await api(state.accessToken).get(`/brands/${state.brandId}/owners`);
581
+ if (res.status !== 200) throw new Error(`Status ${res.status}`);
582
+ });
583
+
584
+ // Add second user as brand manager
585
+ await test('Add brand owner (manager role)', async () => {
586
+ const res = await api(state.accessToken).post(`/brands/${state.brandId}/owners`, {
587
+ userId: state.secondUserId,
588
+ role: 'MANAGER',
589
+ });
590
+ if (res.status !== 201) throw new Error(`Status ${res.status}: ${JSON.stringify(res.data)}`);
591
+ });
592
+
593
+ // Manager can update brand
594
+ await test('Manager can update brand', async () => {
595
+ const res = await api(state.secondUserToken).patch(`/brands/${state.brandId}`, {
596
+ description: 'Updated by manager',
597
+ });
598
+ if (res.status !== 200) throw new Error(`Status ${res.status}: ${JSON.stringify(res.data)}`);
599
+ });
600
+
601
+ await test('Get brand creators (partnerships)', async () => {
602
+ const res = await api(state.accessToken).get(`/brands/${state.brandId}/creators`);
603
+ if (res.status !== 200) throw new Error(`Status ${res.status}`);
604
+ });
605
+
606
+ // Add creator partnership if creator exists
607
+ if (state.creatorId) {
608
+ await test('Add creator partnership to brand', async () => {
609
+ const res = await api(state.accessToken).post(`/brands/${state.brandId}/creators`, {
610
+ creatorId: state.creatorId,
611
+ commissionRate: 0.15,
612
+ isActive: true,
613
+ });
614
+ if (res.status !== 201) throw new Error(`Status ${res.status}: ${JSON.stringify(res.data)}`);
615
+ });
616
+
617
+ await test('Update creator partnership', async () => {
618
+ const res = await api(state.accessToken).patch(
619
+ `/brands/${state.brandId}/creators/${state.creatorId}`,
620
+ {
621
+ commissionRate: 0.20,
622
+ }
623
+ );
624
+ if (res.status !== 200) throw new Error(`Status ${res.status}: ${JSON.stringify(res.data)}`);
625
+ });
626
+ }
627
+
628
+ await test('Get brand sponsored events', async () => {
629
+ const res = await api().get(`/brands/${state.brandId}/events`);
630
+ if (res.status !== 200) throw new Error(`Status ${res.status}`);
631
+ });
632
+
633
+ // Remove the manager
634
+ await test('Remove brand owner', async () => {
635
+ const res = await api(state.accessToken).delete(
636
+ `/brands/${state.brandId}/owners/${state.secondUserId}`
637
+ );
638
+ if (res.status !== 200 && res.status !== 204) throw new Error(`Status ${res.status}`);
639
+ });
640
+
641
+ // Second user should now be forbidden
642
+ await test('Removed owner cannot access brand (403)', async () => {
643
+ const res = await api(state.secondUserToken).patch(`/brands/${state.brandId}`, {
644
+ description: 'Should fail',
645
+ });
646
+ if (res.status !== 403) throw new Error(`Expected 403, got ${res.status}`);
647
+ });
648
+ }
649
+
650
+ // =========================================================================
651
+ // EVENTS (with new fields: venueId, creatorId, sponsorBrandId, gatheringType)
175
652
  // =========================================================================
176
- console.log(`\n${c.cyan}--- EVENTS ---${c.reset}\n`);
653
+ section('EVENTS');
177
654
 
178
655
  await test('List events', async () => {
179
656
  const res = await api().get('/events');
@@ -185,15 +662,23 @@ async function runTests() {
185
662
  if (res.status !== 200) throw new Error(`Status ${res.status}`);
186
663
  });
187
664
 
188
- await test('Create event', async () => {
189
- const res = await api(state.accessToken).post('/events', {
665
+ await test('Create event with creator and brand sponsor', async () => {
666
+ const eventData: any = {
190
667
  title: 'E2E Test Event',
191
668
  description: 'A test event created by E2E tests',
192
- startDate: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(), // 7 days from now
669
+ startDate: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(),
193
670
  totalSpots: 20,
194
671
  locationName: 'Test Location',
195
672
  locationAddress: '123 Test Street',
196
- });
673
+ gatheringType: 'FACILITATOR_LED',
674
+ };
675
+
676
+ // Add creator and brand if available
677
+ if (state.creatorId) eventData.creatorId = state.creatorId;
678
+ if (state.brandId) eventData.sponsorBrandId = state.brandId;
679
+ if (state.venueId) eventData.venueId = state.venueId;
680
+
681
+ const res = await api(state.accessToken).post('/events', eventData);
197
682
  if (res.status !== 201) throw new Error(`Status ${res.status}: ${JSON.stringify(res.data)}`);
198
683
  const data = unwrap<any>(res);
199
684
  state.eventId = data.id;
@@ -203,11 +688,18 @@ async function runTests() {
203
688
  await test('Get event by ID', async () => {
204
689
  const res = await api().get(`/events/${state.eventId}`);
205
690
  if (res.status !== 200) throw new Error(`Status ${res.status}`);
691
+ const data = unwrap<any>(res);
692
+ // Verify new fields are present
693
+ if (!('gatheringType' in data)) throw new Error('Missing gatheringType field');
694
+ if (!('venueId' in data)) throw new Error('Missing venueId field');
695
+ if (!('creatorId' in data)) throw new Error('Missing creatorId field');
696
+ if (!('sponsorBrandId' in data)) throw new Error('Missing sponsorBrandId field');
206
697
  });
207
698
 
208
699
  await test('Update event', async () => {
209
700
  const res = await api(state.accessToken).put(`/events/${state.eventId}`, {
210
701
  title: 'E2E Test Event (Updated)',
702
+ description: 'Updated description for E2E testing',
211
703
  });
212
704
  if (res.status !== 200) throw new Error(`Status ${res.status}: ${JSON.stringify(res.data)}`);
213
705
  });
@@ -226,13 +718,12 @@ async function runTests() {
226
718
  // =========================================================================
227
719
  // BOOKINGS
228
720
  // =========================================================================
229
- console.log(`\n${c.cyan}--- BOOKINGS ---${c.reset}\n`);
721
+ section('BOOKINGS');
230
722
 
231
723
  if (state.eventId) {
232
724
  await test('RSVP to event', async () => {
233
725
  const res = await api(state.accessToken).post(`/events/${state.eventId}/book`);
234
- // 201 for new booking, 409 if already exists
235
- if (res.status !== 201 && res.status !== 409) throw new Error(`Status ${res.status}: ${JSON.stringify(res.data)}`);
726
+ if (res.status !== 201 && res.status !== 409) throw new Error(`Status ${res.status}`);
236
727
  if (res.status === 201) {
237
728
  const data = unwrap<any>(res);
238
729
  state.bookingId = data.id;
@@ -246,9 +737,9 @@ async function runTests() {
246
737
  }
247
738
 
248
739
  // =========================================================================
249
- // EVENT CHAT
740
+ // EVENT CHAT (New roles: ATTENDEE, CREATOR, VENUE_REP, BRAND_REP)
250
741
  // =========================================================================
251
- console.log(`\n${c.cyan}--- EVENT CHAT ---${c.reset}\n`);
742
+ section('EVENT CHAT');
252
743
 
253
744
  if (state.eventId) {
254
745
  await test('Get or create event chat', async () => {
@@ -260,25 +751,33 @@ async function runTests() {
260
751
 
261
752
  await test('Join event chat', async () => {
262
753
  const res = await api(state.accessToken).post(`/events/${state.eventId}/chat/join`);
263
- // 200 for join, 403 if no booking (but we just booked)
264
- if (res.status !== 200) throw new Error(`Status ${res.status}: ${JSON.stringify(res.data)}`);
754
+ if (res.status !== 200) throw new Error(`Status ${res.status}`);
265
755
  });
266
756
 
267
757
  await test('Send chat message', async () => {
268
758
  const res = await api(state.accessToken).post(`/events/${state.eventId}/chat/messages`, {
269
759
  content: 'Hello from E2E test!',
270
760
  });
271
- if (res.status !== 201) throw new Error(`Status ${res.status}: ${JSON.stringify(res.data)}`);
761
+ if (res.status !== 201) throw new Error(`Status ${res.status}`);
272
762
  });
273
763
 
274
764
  await test('Get chat messages', async () => {
275
765
  const res = await api(state.accessToken).get(`/events/${state.eventId}/chat/messages`);
276
- if (res.status !== 200) throw new Error(`Status ${res.status}: ${JSON.stringify(res.data)}`);
766
+ if (res.status !== 200) throw new Error(`Status ${res.status}`);
277
767
  });
278
768
 
279
- await test('Get chat members', async () => {
769
+ await test('Get chat members (check for new roles)', async () => {
280
770
  const res = await api(state.accessToken).get(`/events/${state.eventId}/chat/members`);
281
771
  if (res.status !== 200) throw new Error(`Status ${res.status}`);
772
+ const data = unwrap<any>(res);
773
+ // Verify members have role field
774
+ if (Array.isArray(data) && data.length > 0) {
775
+ const validRoles = ['ATTENDEE', 'CREATOR', 'VENUE_REP', 'BRAND_REP', 'MODERATOR', 'ADMIN'];
776
+ const member = data[0];
777
+ if (!validRoles.includes(member.role)) {
778
+ throw new Error(`Invalid role: ${member.role}`);
779
+ }
780
+ }
282
781
  });
283
782
 
284
783
  await test('Mark chat as read', async () => {
@@ -287,16 +786,74 @@ async function runTests() {
287
786
  });
288
787
  }
289
788
 
789
+ // =========================================================================
790
+ // BADGES & UNIFIED REWARDS
791
+ // =========================================================================
792
+ section('BADGES & UNIFIED REWARDS');
793
+
794
+ await test('Get all badges', async () => {
795
+ const res = await api(state.accessToken).get('/badges');
796
+ if (res.status !== 200) throw new Error(`Status ${res.status}`);
797
+ const data = unwrap<any>(res);
798
+ if (Array.isArray(data) && data.length > 0) {
799
+ state.badgeId = data[0].id;
800
+ }
801
+ });
802
+
803
+ await test('Get my badges', async () => {
804
+ const res = await api(state.accessToken).get('/badges/users/me/badges');
805
+ if (res.status !== 200) throw new Error(`Status ${res.status}`);
806
+ });
807
+
808
+ await test('Get next badges (with progress)', async () => {
809
+ const res = await api(state.accessToken).get('/badges/next');
810
+ if (res.status !== 200) throw new Error(`Status ${res.status}`);
811
+ });
812
+
813
+ await test('Get available rewards', async () => {
814
+ const res = await api(state.accessToken).get('/rewards/available');
815
+ if (res.status !== 200) throw new Error(`Status ${res.status}`);
816
+ });
817
+
818
+ await test('Get rewards wallet', async () => {
819
+ const res = await api(state.accessToken).get('/rewards/wallet');
820
+ if (res.status !== 200) throw new Error(`Status ${res.status}`);
821
+ const data = unwrap<any>(res);
822
+ // Verify wallet structure - badges may be empty array for new users
823
+ if (data.badges === undefined) throw new Error('Missing badges in wallet');
824
+ if (data.stats === undefined) throw new Error('Missing stats in wallet');
825
+ });
826
+
827
+ if (state.badgeId) {
828
+ await test('Get rewards for badge', async () => {
829
+ const res = await api(state.accessToken).get(`/rewards/badge/${state.badgeId}`);
830
+ if (res.status !== 200) throw new Error(`Status ${res.status}`);
831
+ });
832
+ }
833
+
834
+ if (state.venueId) {
835
+ await test('Get rewards for venue', async () => {
836
+ const res = await api(state.accessToken).get(`/rewards/venue/${state.venueId}`);
837
+ if (res.status !== 200) throw new Error(`Status ${res.status}`);
838
+ });
839
+ }
840
+
841
+ if (state.brandId) {
842
+ await test('Get rewards for brand', async () => {
843
+ const res = await api(state.accessToken).get(`/rewards/brand/${state.brandId}`);
844
+ if (res.status !== 200) throw new Error(`Status ${res.status}`);
845
+ });
846
+ }
847
+
290
848
  // =========================================================================
291
849
  // HUBS
292
850
  // =========================================================================
293
- console.log(`\n${c.cyan}--- HUBS ---${c.reset}\n`);
851
+ section('HUBS');
294
852
 
295
853
  await test('List hubs', async () => {
296
854
  const res = await api().get('/hubs');
297
855
  if (res.status !== 200) throw new Error(`Status ${res.status}`);
298
- // Get first hub if exists
299
- const data = res.data.data;
856
+ const data = unwrap<any>(res);
300
857
  if (Array.isArray(data) && data.length > 0) {
301
858
  state.hubId = data[0].id;
302
859
  }
@@ -310,9 +867,8 @@ async function runTests() {
310
867
 
311
868
  await test('Join hub', async () => {
312
869
  const res = await api(state.accessToken).post(`/hubs/${state.hubId}/join`);
313
- // 200/201 for join, 409 if already member
314
870
  if (res.status !== 200 && res.status !== 201 && res.status !== 409) {
315
- throw new Error(`Status ${res.status}: ${JSON.stringify(res.data)}`);
871
+ throw new Error(`Status ${res.status}`);
316
872
  }
317
873
  });
318
874
 
@@ -322,16 +878,37 @@ async function runTests() {
322
878
  });
323
879
  }
324
880
 
881
+ // =========================================================================
882
+ // LIBRARY (Content workflow)
883
+ // =========================================================================
884
+ section('LIBRARY');
885
+
886
+ await test('Get library content', async () => {
887
+ const res = await api(state.accessToken).get('/library');
888
+ if (res.status !== 200) throw new Error(`Status ${res.status}`);
889
+ });
890
+
891
+ await test('Get featured content', async () => {
892
+ const res = await api(state.accessToken).get('/library/featured');
893
+ if (res.status !== 200) throw new Error(`Status ${res.status}`);
894
+ });
895
+
896
+ // Content workflow tests (if creator exists)
897
+ if (state.creatorId) {
898
+ // Note: /library/creator/:id endpoint not implemented yet
899
+ // skip('Get content by creator', 'Endpoint not implemented');
900
+ }
901
+
325
902
  // =========================================================================
326
903
  // JACK AI (Support Conversations)
327
904
  // =========================================================================
328
- console.log(`\n${c.cyan}--- JACK AI ---${c.reset}\n`);
905
+ section('JACK AI');
329
906
 
330
907
  await test('Start Jack conversation', async () => {
331
908
  const res = await api(state.accessToken).post('/support/jack/conversations/new', {
332
909
  message: 'Hello Jack!',
333
910
  });
334
- if (res.status !== 201 && res.status !== 200) throw new Error(`Status ${res.status}: ${JSON.stringify(res.data)}`);
911
+ if (res.status !== 201 && res.status !== 200) throw new Error(`Status ${res.status}`);
335
912
  const data = unwrap<any>(res);
336
913
  state.conversationId = data.conversation?.id || data.id;
337
914
  });
@@ -348,7 +925,9 @@ async function runTests() {
348
925
 
349
926
  if (state.conversationId) {
350
927
  await test('Get Jack conversation by ID', async () => {
351
- const res = await api(state.accessToken).get(`/support/jack/conversations/${state.conversationId}`);
928
+ const res = await api(state.accessToken).get(
929
+ `/support/jack/conversations/${state.conversationId}`
930
+ );
352
931
  if (res.status !== 200) throw new Error(`Status ${res.status}`);
353
932
  });
354
933
  }
@@ -356,18 +935,17 @@ async function runTests() {
356
935
  // =========================================================================
357
936
  // SUPPORT (Check-ins, Wins)
358
937
  // =========================================================================
359
- console.log(`\n${c.cyan}--- SUPPORT ---${c.reset}\n`);
938
+ section('SUPPORT');
360
939
 
361
940
  await test('Create daily check-in', async () => {
362
941
  const res = await api(state.accessToken).post('/support/check-ins', {
363
942
  date: new Date().toISOString().split('T')[0],
364
- mood: 4, // Valid range 1-5
365
- energy: 4, // Valid range 1-5
943
+ mood: 4,
944
+ energy: 4,
366
945
  stayedSober: true,
367
946
  notes: 'E2E test check-in',
368
947
  });
369
- // 201 for new, 409 if exists
370
- if (res.status !== 201 && res.status !== 409) throw new Error(`Status ${res.status}: ${JSON.stringify(res.data)}`);
948
+ if (res.status !== 201 && res.status !== 409) throw new Error(`Status ${res.status}`);
371
949
  });
372
950
 
373
951
  await test('Get check-ins', async () => {
@@ -389,9 +967,9 @@ async function runTests() {
389
967
  const res = await api(state.accessToken).post('/support/wins', {
390
968
  title: 'Completed E2E tests',
391
969
  description: 'Successfully ran all API tests',
392
- category: 'PERSONAL', // Valid: SOBRIETY, HEALTH, RELATIONSHIP, CAREER, PERSONAL, FINANCIAL, OTHER
970
+ category: 'PERSONAL',
393
971
  });
394
- if (res.status !== 201) throw new Error(`Status ${res.status}: ${JSON.stringify(res.data)}`);
972
+ if (res.status !== 201) throw new Error(`Status ${res.status}`);
395
973
  });
396
974
 
397
975
  await test('Get wins', async () => {
@@ -407,7 +985,7 @@ async function runTests() {
407
985
  // =========================================================================
408
986
  // NOTIFICATIONS
409
987
  // =========================================================================
410
- console.log(`\n${c.cyan}--- NOTIFICATIONS ---${c.reset}\n`);
988
+ section('NOTIFICATIONS');
411
989
 
412
990
  await test('Get notifications', async () => {
413
991
  const res = await api(state.accessToken).get('/notifications');
@@ -422,7 +1000,7 @@ async function runTests() {
422
1000
  // =========================================================================
423
1001
  // SUBSCRIPTIONS
424
1002
  // =========================================================================
425
- console.log(`\n${c.cyan}--- SUBSCRIPTIONS ---${c.reset}\n`);
1003
+ section('SUBSCRIPTIONS');
426
1004
 
427
1005
  await test('Get subscription plans', async () => {
428
1006
  const res = await api(state.accessToken).get('/subscriptions/plans');
@@ -431,44 +1009,384 @@ async function runTests() {
431
1009
 
432
1010
  await test('Get my subscription', async () => {
433
1011
  const res = await api(state.accessToken).get('/subscriptions/me');
434
- // 200 if subscribed, 404 if not
435
1012
  if (res.status !== 200 && res.status !== 404) throw new Error(`Status ${res.status}`);
436
1013
  });
437
1014
 
438
1015
  // =========================================================================
439
- // LIBRARY
1016
+ // MAP
440
1017
  // =========================================================================
441
- console.log(`\n${c.cyan}--- LIBRARY ---${c.reset}\n`);
1018
+ section('MAP');
442
1019
 
443
- await test('Get library content', async () => {
444
- const res = await api(state.accessToken).get('/library');
1020
+ await test('Get map hubs', async () => {
1021
+ const res = await api(state.accessToken).get('/map/hubs');
445
1022
  if (res.status !== 200) throw new Error(`Status ${res.status}`);
446
1023
  });
447
1024
 
448
- await test('Get featured content', async () => {
449
- const res = await api(state.accessToken).get('/library/featured');
1025
+ await test('Get map events', async () => {
1026
+ const res = await api(state.accessToken).get('/map/events');
450
1027
  if (res.status !== 200) throw new Error(`Status ${res.status}`);
451
1028
  });
452
1029
 
453
1030
  // =========================================================================
454
- // MAP
1031
+ // BADGE AWARDING (Business Logic Tests)
455
1032
  // =========================================================================
456
- console.log(`\n${c.cyan}--- MAP ---${c.reset}\n`);
1033
+ section('BADGE AWARDING');
457
1034
 
458
- await test('Get map hubs', async () => {
459
- const res = await api(state.accessToken).get('/map/hubs');
1035
+ // Test badge progress tracking after actions
1036
+ // Note: These tests verify the badge awarding integration works correctly.
1037
+ // Actual badge earning depends on thresholds in seed data.
1038
+
1039
+ // Get initial badge state
1040
+ let initialBadgeCount = 0;
1041
+ let initialBadges: string[] = [];
1042
+
1043
+ await test('Get initial user badges before badge tests', async () => {
1044
+ const res = await api(state.accessToken).get('/badges/users/me/badges');
460
1045
  if (res.status !== 200) throw new Error(`Status ${res.status}`);
1046
+ const data = unwrap<any>(res);
1047
+ initialBadgeCount = Array.isArray(data) ? data.length : 0;
1048
+ initialBadges = Array.isArray(data) ? data.map((b: any) => b.badgeId) : [];
461
1049
  });
462
1050
 
463
- await test('Get map events', async () => {
464
- const res = await api(state.accessToken).get('/map/events');
1051
+ // Check-in Streak Badge Tests
1052
+ await test('Check-in updates streak and triggers badge check', async () => {
1053
+ // Create a check-in for a previous day (to test streak logic)
1054
+ const yesterday = new Date();
1055
+ yesterday.setDate(yesterday.getDate() - 1);
1056
+ const yesterdayStr = yesterday.toISOString().split('T')[0];
1057
+
1058
+ const res = await api(state.accessToken).post('/support/check-ins', {
1059
+ date: yesterdayStr,
1060
+ mood: 4,
1061
+ energy: 3,
1062
+ stayedSober: true,
1063
+ notes: 'Badge test check-in',
1064
+ });
1065
+ // Accept 201 (created) or 409 (already exists - re-running test)
1066
+ if (res.status !== 201 && res.status !== 409) throw new Error(`Status ${res.status}`);
1067
+ });
1068
+
1069
+ await test('Verify streak is tracked after check-in', async () => {
1070
+ const res = await api(state.accessToken).get('/support/check-ins/streak');
1071
+ if (res.status !== 200) throw new Error(`Status ${res.status}`);
1072
+ const data = unwrap<any>(res);
1073
+ // Should have at least 1 check-in now (from earlier test)
1074
+ if (data.totalCheckIns === undefined) throw new Error('Missing totalCheckIns in streak data');
1075
+ });
1076
+
1077
+ // Wins Badge Tests
1078
+ await test('Log win updates count and triggers badge check', async () => {
1079
+ const res = await api(state.accessToken).post('/support/wins', {
1080
+ title: 'Badge awarding test win',
1081
+ description: 'Testing that logging wins triggers badge check',
1082
+ category: 'PERSONAL', // Valid WinCategory enum value
1083
+ });
1084
+ if (res.status !== 201) throw new Error(`Status ${res.status}`);
1085
+ });
1086
+
1087
+ await test('Verify wins count is tracked', async () => {
1088
+ const res = await api(state.accessToken).get('/support/wins/count');
465
1089
  if (res.status !== 200) throw new Error(`Status ${res.status}`);
1090
+ const data = unwrap<any>(res);
1091
+ // Should have totalWins or count
1092
+ if (data.totalWins === undefined && typeof data !== 'number' && data.count === undefined) {
1093
+ throw new Error('Expected wins count in response');
1094
+ }
1095
+ });
1096
+
1097
+ // Mood Log Badge Tests
1098
+ await test('Log mood updates count and triggers badge check', async () => {
1099
+ const res = await api(state.accessToken).post('/support/mood-logs', {
1100
+ mood: 4, // Required field (1-5)
1101
+ energy: 3, // Optional (1-5)
1102
+ tags: ['hopeful'], // Optional array of strings
1103
+ note: 'Badge test mood log', // Optional note
1104
+ });
1105
+ if (res.status !== 201) throw new Error(`Status ${res.status}`);
1106
+ });
1107
+
1108
+ await test('Verify mood logs are tracked', async () => {
1109
+ const res = await api(state.accessToken).get('/support/mood-logs');
1110
+ if (res.status !== 200) throw new Error(`Status ${res.status}`);
1111
+ const data = unwrap<any>(res);
1112
+ // Handle both array and { data: array } formats
1113
+ const logs = Array.isArray(data) ? data : (data.data || data.moodLogs || []);
1114
+ // Should have at least 1 mood log
1115
+ if (!Array.isArray(logs) || logs.length === 0) {
1116
+ throw new Error('Expected at least one mood log');
1117
+ }
1118
+ });
1119
+
1120
+ // Reflection Badge Tests
1121
+ await test('Submit reflection updates count and triggers badge check', async () => {
1122
+ // Calculate weekStart - needs to be a Monday in YYYY-MM-DD format
1123
+ const now = new Date();
1124
+ const dayOfWeek = now.getDay();
1125
+ const daysToMonday = dayOfWeek === 0 ? 6 : dayOfWeek - 1;
1126
+ const monday = new Date(now);
1127
+ monday.setDate(now.getDate() - daysToMonday);
1128
+ const weekStart = monday.toISOString().split('T')[0];
1129
+
1130
+ const res = await api(state.accessToken).post('/support/reflections', {
1131
+ weekStart, // Required field
1132
+ overallRating: 4,
1133
+ biggestWin: 'Badge test reflection - testing the badge integration',
1134
+ challenges: 'Testing automated badge checks',
1135
+ goals: ['Continue improving the platform'],
1136
+ notes: 'E2E test reflection',
1137
+ });
1138
+ // Accept 201 (created) or 409 (already exists for this week)
1139
+ if (res.status !== 201 && res.status !== 409) throw new Error(`Status ${res.status}`);
1140
+ });
1141
+
1142
+ await test('Verify reflections are tracked', async () => {
1143
+ const res = await api(state.accessToken).get('/support/reflections');
1144
+ if (res.status !== 200) throw new Error(`Status ${res.status}`);
1145
+ });
1146
+
1147
+ // Events Hosted Badge Tests (awarded on publish)
1148
+ await test('Publishing event awards EVENTS_HOSTED badge to hosts', async () => {
1149
+ // The event was already published in EVENTS section
1150
+ // Verify the badge check was triggered by checking next badges
1151
+ const res = await api(state.accessToken).get('/badges/next');
1152
+ if (res.status !== 200) throw new Error(`Status ${res.status}`);
1153
+ const data = unwrap<any>(res);
1154
+ // Should return badge progress data
1155
+ if (!Array.isArray(data)) throw new Error('Expected array of next badges');
1156
+ });
1157
+
1158
+ // Events Attended Badge Tests
1159
+ // Note: EVENTS_ATTENDED badge is awarded when booking is checked-in
1160
+ // This requires a host to check-in the booking, which we can't do in E2E easily
1161
+ // So we just verify the badge progress tracking works
1162
+
1163
+ await test('Badge progress shows EVENTS_ATTENDED tracking', async () => {
1164
+ const res = await api(state.accessToken).get('/badges/next');
1165
+ if (res.status !== 200) throw new Error(`Status ${res.status}`);
1166
+ const data = unwrap<any>(res);
1167
+ // Look for events attended badge in progress
1168
+ const eventsAttendedBadge = data.find(
1169
+ (b: any) => b.badge?.type === 'EVENTS_ATTENDED' || b.badge?.slug?.includes('event')
1170
+ );
1171
+ // May or may not exist depending on seed data, but API should work
1172
+ });
1173
+
1174
+ // Verify badge state after all actions
1175
+ await test('Verify badges after all badge-triggering actions', async () => {
1176
+ const res = await api(state.accessToken).get('/badges/users/me/badges');
1177
+ if (res.status !== 200) throw new Error(`Status ${res.status}`);
1178
+ const data = unwrap<any>(res);
1179
+ const currentBadgeCount = Array.isArray(data) ? data.length : 0;
1180
+ // Depending on thresholds, user may have earned new badges
1181
+ // At minimum, the API should return successfully
1182
+ });
1183
+
1184
+ // =========================================================================
1185
+ // REWARD FLOW
1186
+ // =========================================================================
1187
+ section('REWARD FLOW');
1188
+
1189
+ // Store reward-related state
1190
+ let testBadgeIdForReward: string | undefined;
1191
+ let testRewardId: string | undefined;
1192
+ let redemptionId: string | undefined;
1193
+
1194
+ // Get a badge to use for reward tests
1195
+ await test('Get available badges for reward test', async () => {
1196
+ const res = await api(state.accessToken).get('/badges');
1197
+ if (res.status !== 200) throw new Error(`Status ${res.status}`);
1198
+ const data = unwrap<any>(res);
1199
+ if (Array.isArray(data) && data.length > 0) {
1200
+ testBadgeIdForReward = data[0].id;
1201
+ }
1202
+ });
1203
+
1204
+ // Create a brand reward for testing (user owns brand from earlier tests)
1205
+ // Note: We use brand since we created one in the BRANDS section
1206
+ if (state.brandId) {
1207
+ await test('Create brand reward for badge', async () => {
1208
+ if (!testBadgeIdForReward) {
1209
+ throw new Error('No badge available to create reward for');
1210
+ }
1211
+ const res = await api(state.accessToken).post(`/brands/${state.brandId}/rewards`, {
1212
+ badgeId: testBadgeIdForReward,
1213
+ title: 'E2E Brand Test Reward',
1214
+ description: 'A test brand reward for E2E testing',
1215
+ redeemType: 'ONLINE',
1216
+ code: 'E2E-TEST-CODE',
1217
+ perUserLimit: 1,
1218
+ });
1219
+ if (res.status !== 201) throw new Error(`Status ${res.status}: ${JSON.stringify(res.data)}`);
1220
+ const data = unwrap<any>(res);
1221
+ testRewardId = data.id;
1222
+ });
1223
+ } else {
1224
+ skip('Create brand reward for badge', 'No brand available (brand not created)');
1225
+ }
1226
+
1227
+ // Test reward availability
1228
+ await test('Get available rewards for user', async () => {
1229
+ const res = await api(state.accessToken).get('/rewards/available');
1230
+ if (res.status !== 200) throw new Error(`Status ${res.status}`);
1231
+ const data = unwrap<any>(res);
1232
+ // API returns { data: rewards[] }
1233
+ const rewards = data.data || data;
1234
+ if (!Array.isArray(rewards)) throw new Error('Expected array of available rewards');
1235
+ });
1236
+
1237
+ // Test user cannot redeem without earning the badge
1238
+ await test('Cannot redeem reward without earning required badge (400)', async () => {
1239
+ if (!testRewardId || !testBadgeIdForReward) {
1240
+ console.log(` ${c.dim}(Skipped: No test reward created)${c.reset}`);
1241
+ return;
1242
+ }
1243
+
1244
+ // First check if user has the badge
1245
+ const badgeRes = await api(state.accessToken).get('/badges/users/me/badges');
1246
+ const badges = unwrap<any>(badgeRes);
1247
+ const hasBadge = Array.isArray(badges) && badges.some(
1248
+ (b: any) => b.badgeId === testBadgeIdForReward
1249
+ );
1250
+
1251
+ if (!hasBadge) {
1252
+ // User doesn't have badge, should get 400 when trying to redeem
1253
+ const res = await api(state.accessToken).post(`/rewards/${testRewardId}/redeem`);
1254
+ if (res.status !== 400) {
1255
+ throw new Error(`Expected 400 (need badge first), got ${res.status}`);
1256
+ }
1257
+ } else {
1258
+ // User has the badge, test passes (we'll test redemption below)
1259
+ console.log(` ${c.dim}(User already has required badge - will test redemption)${c.reset}`);
1260
+ }
1261
+ });
1262
+
1263
+ // Test redemption flow - check if user can redeem
1264
+ await test('Check badge ownership and test redemption flow', async () => {
1265
+ if (!testRewardId || !testBadgeIdForReward) {
1266
+ console.log(` ${c.dim}(Skipped: No test reward created)${c.reset}`);
1267
+ return;
1268
+ }
1269
+
1270
+ // Check if user has earned the badge
1271
+ const badgeRes = await api(state.accessToken).get('/badges/users/me/badges');
1272
+ if (badgeRes.status !== 200) throw new Error(`Status ${badgeRes.status}`);
1273
+ const badges = unwrap<any>(badgeRes);
1274
+ const userHasBadge = Array.isArray(badges) && badges.some(
1275
+ (b: any) => b.badgeId === testBadgeIdForReward
1276
+ );
1277
+
1278
+ if (userHasBadge) {
1279
+ // User has badge, test the full redemption flow
1280
+ const redeemRes = await api(state.accessToken).post(`/rewards/${testRewardId}/redeem`);
1281
+ if (redeemRes.status !== 201) {
1282
+ throw new Error(`Redeem failed: ${redeemRes.status}: ${JSON.stringify(redeemRes.data)}`);
1283
+ }
1284
+ const redeemData = unwrap<any>(redeemRes);
1285
+ redemptionId = redeemData.id;
1286
+ console.log(` ${c.dim}(Redemption successful: ${redemptionId})${c.reset}`);
1287
+ } else {
1288
+ console.log(` ${c.dim}(User has not earned required badge - redemption test skipped)${c.reset}`);
1289
+ }
1290
+ });
1291
+
1292
+ await test('Cannot redeem same reward twice (400)', async () => {
1293
+ if (!testRewardId || !redemptionId) {
1294
+ console.log(` ${c.dim}(Skipped: No prior redemption to test)${c.reset}`);
1295
+ return;
1296
+ }
1297
+ const res = await api(state.accessToken).post(`/rewards/${testRewardId}/redeem`);
1298
+ if (res.status !== 400) {
1299
+ throw new Error(`Expected 400 (already redeemed), got ${res.status}`);
1300
+ }
1301
+ });
1302
+
1303
+ await test('Get redeemed rewards', async () => {
1304
+ const res = await api(state.accessToken).get('/rewards/redeemed');
1305
+ if (res.status !== 200) throw new Error(`Status ${res.status}`);
1306
+ const data = unwrap<any>(res);
1307
+ // API returns { data: redemptions[] }
1308
+ const redemptions = data.data || data;
1309
+ if (!Array.isArray(redemptions)) throw new Error('Expected array of redeemed rewards');
1310
+ // If we redeemed, should have at least one
1311
+ if (redemptionId && redemptions.length === 0) {
1312
+ throw new Error('Expected at least one redeemed reward');
1313
+ }
1314
+ });
1315
+
1316
+ await test('Rewards wallet shows correct stats', async () => {
1317
+ const res = await api(state.accessToken).get('/rewards/wallet');
1318
+ if (res.status !== 200) throw new Error(`Status ${res.status}`);
1319
+ const data = unwrap<any>(res);
1320
+ if (data.stats === undefined) throw new Error('Missing stats in wallet');
1321
+ // After redemption, should have at least 1 redeemed
1322
+ if (redemptionId && data.stats.totalRedeemedRewards < 1) {
1323
+ throw new Error('Expected at least 1 redeemed reward in wallet stats');
1324
+ }
1325
+ });
1326
+
1327
+ // Test getting rewards by badge
1328
+ await test('Get rewards for specific badge', async () => {
1329
+ if (!testBadgeIdForReward) {
1330
+ console.log(` ${c.dim}(Skipped: No badge ID available)${c.reset}`);
1331
+ return;
1332
+ }
1333
+ const res = await api(state.accessToken).get(`/rewards/badge/${testBadgeIdForReward}`);
1334
+ if (res.status !== 200) throw new Error(`Status ${res.status}`);
1335
+ const data = unwrap<any>(res);
1336
+ // API returns { data: rewards[] }
1337
+ const rewards = data.data || data;
1338
+ if (!Array.isArray(rewards)) throw new Error('Expected array of rewards');
1339
+ });
1340
+
1341
+ // Verify reward redemption verification endpoint
1342
+ await test('Verify redemption endpoint works', async () => {
1343
+ // This would normally be called by venue/brand staff
1344
+ // We'll test with an invalid token to ensure endpoint exists
1345
+ const res = await api(state.accessToken).post('/rewards/verify', {
1346
+ qrToken: 'invalid-test-token',
1347
+ });
1348
+ // POST defaults to 201, but either 200 or 201 is acceptable
1349
+ if (res.status !== 200 && res.status !== 201) throw new Error(`Status ${res.status}`);
1350
+ const data = unwrap<any>(res);
1351
+ // API returns { data: { valid: boolean } }
1352
+ const result = data.data || data;
1353
+ if (result.valid !== false) {
1354
+ throw new Error('Expected valid: false for invalid token');
1355
+ }
466
1356
  });
467
1357
 
468
1358
  // =========================================================================
469
1359
  // CLEANUP
470
1360
  // =========================================================================
471
- console.log(`\n${c.cyan}--- CLEANUP ---${c.reset}\n`);
1361
+ section('CLEANUP');
1362
+
1363
+ // Cleanup creator products
1364
+ if (state.productId && state.creatorId) {
1365
+ await test('Delete creator product', async () => {
1366
+ const res = await api(state.accessToken).delete(
1367
+ `/creators/${state.creatorId}/products/${state.productId}`
1368
+ );
1369
+ if (res.status !== 200 && res.status !== 204) throw new Error(`Status ${res.status}`);
1370
+ });
1371
+ }
1372
+
1373
+ if (state.availabilityId && state.creatorId) {
1374
+ await test('Remove creator availability', async () => {
1375
+ const res = await api(state.accessToken).delete(
1376
+ `/creators/${state.creatorId}/availability/${state.availabilityId}`
1377
+ );
1378
+ if (res.status !== 200 && res.status !== 204) throw new Error(`Status ${res.status}`);
1379
+ });
1380
+ }
1381
+
1382
+ if (state.creatorId && state.brandId) {
1383
+ await test('Remove creator from brand partnership', async () => {
1384
+ const res = await api(state.accessToken).delete(
1385
+ `/brands/${state.brandId}/creators/${state.creatorId}`
1386
+ );
1387
+ if (res.status !== 200 && res.status !== 204) throw new Error(`Status ${res.status}`);
1388
+ });
1389
+ }
472
1390
 
473
1391
  if (state.bookingId) {
474
1392
  await test('Cancel booking', async () => {
@@ -487,16 +1405,30 @@ async function runTests() {
487
1405
  // =========================================================================
488
1406
  // SUMMARY
489
1407
  // =========================================================================
490
- console.log(`\n${c.cyan}${'='.repeat(60)}${c.reset}`);
1408
+ console.log(`\n${c.cyan}${'='.repeat(70)}${c.reset}`);
491
1409
  console.log(`${c.cyan} TEST SUMMARY${c.reset}`);
492
- console.log(`${c.cyan}${'='.repeat(60)}${c.reset}`);
493
- console.log(`${c.green}Passed: ${passed}${c.reset}`);
494
- console.log(`${c.red}Failed: ${failed}${c.reset}`);
495
- console.log(`Total: ${passed + failed}`);
496
- console.log(`${c.cyan}${'='.repeat(60)}${c.reset}\n`);
1410
+ console.log(`${c.cyan}${'='.repeat(70)}${c.reset}`);
1411
+ console.log(`${c.green}Passed: ${passed}${c.reset}`);
1412
+ console.log(`${c.red}Failed: ${failed}${c.reset}`);
1413
+ console.log(`${c.yellow}Skipped: ${skipped}${c.reset}`);
1414
+ console.log(`${c.bold}Total: ${passed + failed + skipped}${c.reset}`);
1415
+ console.log(`${c.cyan}${'='.repeat(70)}${c.reset}\n`);
1416
+
1417
+ // IDs for debugging
1418
+ console.log(`${c.dim}Test IDs:${c.reset}`);
1419
+ console.log(`${c.dim} User: ${state.userId}${c.reset}`);
1420
+ console.log(`${c.dim} Creator: ${state.creatorId || 'N/A'}${c.reset}`);
1421
+ console.log(`${c.dim} Product: ${state.productId || 'N/A'}${c.reset}`);
1422
+ console.log(`${c.dim} Brand: ${state.brandId || 'N/A'}${c.reset}`);
1423
+ console.log(`${c.dim} Event: ${state.eventId || 'N/A'}${c.reset}`);
1424
+ console.log(`${c.dim} Venue: ${state.venueId || 'N/A'}${c.reset}`);
1425
+ console.log('');
497
1426
 
498
1427
  process.exit(failed > 0 ? 1 : 0);
499
1428
  }
500
1429
 
501
1430
  // Run tests
502
- runTests().catch(console.error);
1431
+ runTests().catch((e) => {
1432
+ console.error(`${c.red}Unexpected error:${c.reset}`, e);
1433
+ process.exit(1);
1434
+ });