@growsober/sdk 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/README.md +276 -0
  2. package/dist/__tests__/e2e.test.d.ts +7 -0
  3. package/dist/__tests__/e2e.test.js +472 -0
  4. package/dist/api/client.d.ts +11 -0
  5. package/dist/api/client.js +61 -0
  6. package/dist/api/mutations/admin.d.ts +167 -0
  7. package/dist/api/mutations/admin.js +326 -0
  8. package/dist/api/mutations/ambassadors.d.ts +52 -0
  9. package/dist/api/mutations/ambassadors.js +148 -0
  10. package/dist/api/mutations/auth.d.ts +267 -0
  11. package/dist/api/mutations/auth.js +332 -0
  12. package/dist/api/mutations/bookings.d.ts +59 -0
  13. package/dist/api/mutations/bookings.js +143 -0
  14. package/dist/api/mutations/event-chat.d.ts +35 -0
  15. package/dist/api/mutations/event-chat.js +147 -0
  16. package/dist/api/mutations/events.d.ts +87 -0
  17. package/dist/api/mutations/events.js +205 -0
  18. package/dist/api/mutations/grow90.d.ts +36 -0
  19. package/dist/api/mutations/grow90.js +132 -0
  20. package/dist/api/mutations/hubs.d.ts +111 -0
  21. package/dist/api/mutations/hubs.js +240 -0
  22. package/dist/api/mutations/index.d.ts +22 -0
  23. package/dist/api/mutations/index.js +39 -0
  24. package/dist/api/mutations/jack.d.ts +61 -0
  25. package/dist/api/mutations/jack.js +104 -0
  26. package/dist/api/mutations/library.d.ts +67 -0
  27. package/dist/api/mutations/library.js +168 -0
  28. package/dist/api/mutations/map.d.ts +153 -0
  29. package/dist/api/mutations/map.js +181 -0
  30. package/dist/api/mutations/matching.d.ts +130 -0
  31. package/dist/api/mutations/matching.js +204 -0
  32. package/dist/api/mutations/notifications.d.ts +63 -0
  33. package/dist/api/mutations/notifications.js +106 -0
  34. package/dist/api/mutations/offers.d.ts +26 -0
  35. package/dist/api/mutations/offers.js +47 -0
  36. package/dist/api/mutations/subscriptions.d.ts +127 -0
  37. package/dist/api/mutations/subscriptions.js +140 -0
  38. package/dist/api/mutations/support.d.ts +165 -0
  39. package/dist/api/mutations/support.js +307 -0
  40. package/dist/api/mutations/users.d.ts +211 -0
  41. package/dist/api/mutations/users.js +261 -0
  42. package/dist/api/queries/admin.d.ts +257 -0
  43. package/dist/api/queries/admin.js +320 -0
  44. package/dist/api/queries/ambassadors.d.ts +53 -0
  45. package/dist/api/queries/ambassadors.js +98 -0
  46. package/dist/api/queries/auth.d.ts +16 -0
  47. package/dist/api/queries/auth.js +25 -0
  48. package/dist/api/queries/bookings.d.ts +91 -0
  49. package/dist/api/queries/bookings.js +102 -0
  50. package/dist/api/queries/businesses.d.ts +212 -0
  51. package/dist/api/queries/businesses.js +154 -0
  52. package/dist/api/queries/event-chat.d.ts +19 -0
  53. package/dist/api/queries/event-chat.js +75 -0
  54. package/dist/api/queries/events.d.ts +322 -0
  55. package/dist/api/queries/events.js +221 -0
  56. package/dist/api/queries/grow90.d.ts +26 -0
  57. package/dist/api/queries/grow90.js +85 -0
  58. package/dist/api/queries/hubs.d.ts +165 -0
  59. package/dist/api/queries/hubs.js +143 -0
  60. package/dist/api/queries/index.d.ts +23 -0
  61. package/dist/api/queries/index.js +40 -0
  62. package/dist/api/queries/jack.d.ts +63 -0
  63. package/dist/api/queries/jack.js +92 -0
  64. package/dist/api/queries/library.d.ts +132 -0
  65. package/dist/api/queries/library.js +120 -0
  66. package/dist/api/queries/map.d.ts +216 -0
  67. package/dist/api/queries/map.js +278 -0
  68. package/dist/api/queries/matching.d.ts +136 -0
  69. package/dist/api/queries/matching.js +161 -0
  70. package/dist/api/queries/notifications.d.ts +78 -0
  71. package/dist/api/queries/notifications.js +88 -0
  72. package/dist/api/queries/offers.d.ts +91 -0
  73. package/dist/api/queries/offers.js +103 -0
  74. package/dist/api/queries/subscriptions.d.ts +56 -0
  75. package/dist/api/queries/subscriptions.js +73 -0
  76. package/dist/api/queries/support.d.ts +106 -0
  77. package/dist/api/queries/support.js +202 -0
  78. package/dist/api/queries/users.d.ts +293 -0
  79. package/dist/api/queries/users.js +370 -0
  80. package/dist/api/types.d.ts +464 -0
  81. package/dist/api/types.js +9 -0
  82. package/dist/hooks/useAuth.d.ts +5 -0
  83. package/dist/hooks/useAuth.js +39 -0
  84. package/dist/hooks/useUser.d.ts +43 -0
  85. package/dist/hooks/useUser.js +44 -0
  86. package/dist/index.d.ts +36 -0
  87. package/dist/index.js +67 -0
  88. package/package.json +62 -0
  89. package/src/__tests__/e2e.test.ts +502 -0
  90. package/src/api/client.ts +71 -0
  91. package/src/api/mutations/admin.ts +531 -0
  92. package/src/api/mutations/ambassadors.ts +185 -0
  93. package/src/api/mutations/auth.ts +350 -0
  94. package/src/api/mutations/bookings.ts +190 -0
  95. package/src/api/mutations/event-chat.ts +177 -0
  96. package/src/api/mutations/events.ts +273 -0
  97. package/src/api/mutations/grow90.ts +169 -0
  98. package/src/api/mutations/hubs.ts +385 -0
  99. package/src/api/mutations/index.ts +23 -0
  100. package/src/api/mutations/jack.ts +130 -0
  101. package/src/api/mutations/library.ts +212 -0
  102. package/src/api/mutations/map.ts +230 -0
  103. package/src/api/mutations/matching.ts +271 -0
  104. package/src/api/mutations/notifications.ts +114 -0
  105. package/src/api/mutations/offers.ts +73 -0
  106. package/src/api/mutations/subscriptions.ts +162 -0
  107. package/src/api/mutations/support.ts +390 -0
  108. package/src/api/mutations/users.ts +271 -0
  109. package/src/api/queries/admin.ts +480 -0
  110. package/src/api/queries/ambassadors.ts +139 -0
  111. package/src/api/queries/auth.ts +24 -0
  112. package/src/api/queries/bookings.ts +135 -0
  113. package/src/api/queries/businesses.ts +203 -0
  114. package/src/api/queries/event-chat.ts +78 -0
  115. package/src/api/queries/events.ts +272 -0
  116. package/src/api/queries/grow90.ts +98 -0
  117. package/src/api/queries/hubs.ts +211 -0
  118. package/src/api/queries/index.ts +24 -0
  119. package/src/api/queries/jack.ts +127 -0
  120. package/src/api/queries/library.ts +166 -0
  121. package/src/api/queries/map.ts +331 -0
  122. package/src/api/queries/matching.ts +238 -0
  123. package/src/api/queries/notifications.ts +103 -0
  124. package/src/api/queries/offers.ts +136 -0
  125. package/src/api/queries/subscriptions.ts +91 -0
  126. package/src/api/queries/support.ts +235 -0
  127. package/src/api/queries/users.ts +393 -0
  128. package/src/api/types.ts +596 -0
  129. package/src/index.ts +57 -0
@@ -0,0 +1,36 @@
1
+ /**
2
+ * GrowSober SDK
3
+ *
4
+ * TypeScript SDK for the GrowSober API with TanStack Query hooks.
5
+ *
6
+ * @example
7
+ * ```tsx
8
+ * import { configureSDK, useCurrentUser, useLogin } from '@growsober/sdk';
9
+ *
10
+ * // Configure SDK with your API base URL and token management
11
+ * configureSDK({
12
+ * baseURL: 'https://api.growsober.app',
13
+ * getAccessToken: () => localStorage.getItem('accessToken'),
14
+ * refreshAccessToken: async () => {
15
+ * // Your token refresh logic
16
+ * return newAccessToken;
17
+ * },
18
+ * onUnauthorized: () => {
19
+ * // Handle unauthorized access
20
+ * window.location.href = '/login';
21
+ * },
22
+ * });
23
+ *
24
+ * // Use hooks in your components
25
+ * function App() {
26
+ * const { data: user } = useCurrentUser();
27
+ * const { mutate: login } = useLogin();
28
+ * // ...
29
+ * }
30
+ * ```
31
+ */
32
+ export { configureSDK, getApiClient } from './api/client';
33
+ export type { SDKConfig } from './api/client';
34
+ export * from './api/types';
35
+ export * from './api/queries';
36
+ export * from './api/mutations';
package/dist/index.js ADDED
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+ /**
3
+ * GrowSober SDK
4
+ *
5
+ * TypeScript SDK for the GrowSober API with TanStack Query hooks.
6
+ *
7
+ * @example
8
+ * ```tsx
9
+ * import { configureSDK, useCurrentUser, useLogin } from '@growsober/sdk';
10
+ *
11
+ * // Configure SDK with your API base URL and token management
12
+ * configureSDK({
13
+ * baseURL: 'https://api.growsober.app',
14
+ * getAccessToken: () => localStorage.getItem('accessToken'),
15
+ * refreshAccessToken: async () => {
16
+ * // Your token refresh logic
17
+ * return newAccessToken;
18
+ * },
19
+ * onUnauthorized: () => {
20
+ * // Handle unauthorized access
21
+ * window.location.href = '/login';
22
+ * },
23
+ * });
24
+ *
25
+ * // Use hooks in your components
26
+ * function App() {
27
+ * const { data: user } = useCurrentUser();
28
+ * const { mutate: login } = useLogin();
29
+ * // ...
30
+ * }
31
+ * ```
32
+ */
33
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
34
+ if (k2 === undefined) k2 = k;
35
+ var desc = Object.getOwnPropertyDescriptor(m, k);
36
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
37
+ desc = { enumerable: true, get: function() { return m[k]; } };
38
+ }
39
+ Object.defineProperty(o, k2, desc);
40
+ }) : (function(o, m, k, k2) {
41
+ if (k2 === undefined) k2 = k;
42
+ o[k2] = m[k];
43
+ }));
44
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
45
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
46
+ };
47
+ Object.defineProperty(exports, "__esModule", { value: true });
48
+ exports.getApiClient = exports.configureSDK = void 0;
49
+ // ============================================================================
50
+ // API CLIENT
51
+ // ============================================================================
52
+ var client_1 = require("./api/client");
53
+ Object.defineProperty(exports, "configureSDK", { enumerable: true, get: function () { return client_1.configureSDK; } });
54
+ Object.defineProperty(exports, "getApiClient", { enumerable: true, get: function () { return client_1.getApiClient; } });
55
+ // ============================================================================
56
+ // TYPES
57
+ // ============================================================================
58
+ __exportStar(require("./api/types"), exports);
59
+ // ============================================================================
60
+ // QUERY HOOKS
61
+ // ============================================================================
62
+ __exportStar(require("./api/queries"), exports);
63
+ // ============================================================================
64
+ // MUTATION HOOKS
65
+ // ============================================================================
66
+ __exportStar(require("./api/mutations"), exports);
67
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvaW5kZXgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IjtBQUFBOzs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7R0E4Qkc7Ozs7Ozs7Ozs7Ozs7Ozs7O0FBRUgsK0VBQStFO0FBQy9FLGFBQWE7QUFDYiwrRUFBK0U7QUFFL0UsdUNBQTBEO0FBQWpELHNHQUFBLFlBQVksT0FBQTtBQUFFLHNHQUFBLFlBQVksT0FBQTtBQUduQywrRUFBK0U7QUFDL0UsUUFBUTtBQUNSLCtFQUErRTtBQUUvRSw4Q0FBNEI7QUFFNUIsK0VBQStFO0FBQy9FLGNBQWM7QUFDZCwrRUFBK0U7QUFFL0UsZ0RBQThCO0FBRTlCLCtFQUErRTtBQUMvRSxpQkFBaUI7QUFDakIsK0VBQStFO0FBRS9FLGtEQUFnQyIsInNvdXJjZXNDb250ZW50IjpbIi8qKlxuICogR3Jvd1NvYmVyIFNES1xuICpcbiAqIFR5cGVTY3JpcHQgU0RLIGZvciB0aGUgR3Jvd1NvYmVyIEFQSSB3aXRoIFRhblN0YWNrIFF1ZXJ5IGhvb2tzLlxuICpcbiAqIEBleGFtcGxlXG4gKiBgYGB0c3hcbiAqIGltcG9ydCB7IGNvbmZpZ3VyZVNESywgdXNlQ3VycmVudFVzZXIsIHVzZUxvZ2luIH0gZnJvbSAnQGdyb3dzb2Jlci9zZGsnO1xuICpcbiAqIC8vIENvbmZpZ3VyZSBTREsgd2l0aCB5b3VyIEFQSSBiYXNlIFVSTCBhbmQgdG9rZW4gbWFuYWdlbWVudFxuICogY29uZmlndXJlU0RLKHtcbiAqICAgYmFzZVVSTDogJ2h0dHBzOi8vYXBpLmdyb3dzb2Jlci5hcHAnLFxuICogICBnZXRBY2Nlc3NUb2tlbjogKCkgPT4gbG9jYWxTdG9yYWdlLmdldEl0ZW0oJ2FjY2Vzc1Rva2VuJyksXG4gKiAgIHJlZnJlc2hBY2Nlc3NUb2tlbjogYXN5bmMgKCkgPT4ge1xuICogICAgIC8vIFlvdXIgdG9rZW4gcmVmcmVzaCBsb2dpY1xuICogICAgIHJldHVybiBuZXdBY2Nlc3NUb2tlbjtcbiAqICAgfSxcbiAqICAgb25VbmF1dGhvcml6ZWQ6ICgpID0+IHtcbiAqICAgICAvLyBIYW5kbGUgdW5hdXRob3JpemVkIGFjY2Vzc1xuICogICAgIHdpbmRvdy5sb2NhdGlvbi5ocmVmID0gJy9sb2dpbic7XG4gKiAgIH0sXG4gKiB9KTtcbiAqXG4gKiAvLyBVc2UgaG9va3MgaW4geW91ciBjb21wb25lbnRzXG4gKiBmdW5jdGlvbiBBcHAoKSB7XG4gKiAgIGNvbnN0IHsgZGF0YTogdXNlciB9ID0gdXNlQ3VycmVudFVzZXIoKTtcbiAqICAgY29uc3QgeyBtdXRhdGU6IGxvZ2luIH0gPSB1c2VMb2dpbigpO1xuICogICAvLyAuLi5cbiAqIH1cbiAqIGBgYFxuICovXG5cbi8vID09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT1cbi8vIEFQSSBDTElFTlRcbi8vID09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT1cblxuZXhwb3J0IHsgY29uZmlndXJlU0RLLCBnZXRBcGlDbGllbnQgfSBmcm9tICcuL2FwaS9jbGllbnQnO1xuZXhwb3J0IHR5cGUgeyBTREtDb25maWcgfSBmcm9tICcuL2FwaS9jbGllbnQnO1xuXG4vLyA9PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09XG4vLyBUWVBFU1xuLy8gPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PVxuXG5leHBvcnQgKiBmcm9tICcuL2FwaS90eXBlcyc7XG5cbi8vID09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT1cbi8vIFFVRVJZIEhPT0tTXG4vLyA9PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09XG5cbmV4cG9ydCAqIGZyb20gJy4vYXBpL3F1ZXJpZXMnO1xuXG4vLyA9PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09XG4vLyBNVVRBVElPTiBIT09LU1xuLy8gPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PVxuXG5leHBvcnQgKiBmcm9tICcuL2FwaS9tdXRhdGlvbnMnO1xuXG4iXX0=
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "@growsober/sdk",
3
+ "version": "1.0.0",
4
+ "description": "Shared TypeScript SDK for GrowSober API - TanStack Query hooks, API client, and utilities",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.js",
7
+ "source": "src/index.ts",
8
+ "types": "dist/index.d.ts",
9
+ "react-native": "src/index.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js",
14
+ "require": "./dist/index.js"
15
+ }
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "src"
20
+ ],
21
+ "publishConfig": {
22
+ "access": "public"
23
+ },
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "https://github.com/souldevsoul/growsober-sdk.git"
27
+ },
28
+ "scripts": {
29
+ "build": "tsc",
30
+ "build:watch": "tsc --watch",
31
+ "type-check": "tsc --noEmit",
32
+ "lint": "eslint src --ext .ts",
33
+ "prepublishOnly": "npm run build",
34
+ "test:e2e": "npx ts-node src/__tests__/e2e.test.ts"
35
+ },
36
+ "keywords": [
37
+ "growsober",
38
+ "api",
39
+ "sdk",
40
+ "tanstack-query",
41
+ "react-query",
42
+ "typescript"
43
+ ],
44
+ "author": "SoulToSoul",
45
+ "license": "MIT",
46
+ "peerDependencies": {
47
+ "@tanstack/react-query": "^5.0.0",
48
+ "axios": "^1.6.0",
49
+ "react": ">=18.0.0",
50
+ "zod": "^3.0.0"
51
+ },
52
+ "dependencies": {
53
+ "@growsober/types": "^1.0.0"
54
+ },
55
+ "devDependencies": {
56
+ "@types/node": "^20.0.0",
57
+ "@types/react": "^18.0.0",
58
+ "axios": "^1.13.2",
59
+ "ts-node": "^10.9.0",
60
+ "typescript": "^5.9.3"
61
+ }
62
+ }
@@ -0,0 +1,502 @@
1
+ /**
2
+ * GrowSober API E2E Tests
3
+ *
4
+ * Tests all SDK endpoints against the real API.
5
+ * Run with: npx ts-node src/__tests__/e2e.test.ts
6
+ */
7
+
8
+ import axios, { AxiosInstance } from 'axios';
9
+
10
+ const BASE_URL = 'http://localhost:3001/api/v1';
11
+ const TEST_EMAIL = `e2e-${Date.now()}@growsober.com`;
12
+ const TEST_PASSWORD = 'SecurePass123!';
13
+
14
+ // Colors for output
15
+ const c = {
16
+ reset: '\x1b[0m',
17
+ green: '\x1b[32m',
18
+ red: '\x1b[31m',
19
+ yellow: '\x1b[33m',
20
+ cyan: '\x1b[36m',
21
+ dim: '\x1b[2m',
22
+ };
23
+
24
+ // State shared across tests
25
+ const state: {
26
+ accessToken: string;
27
+ refreshToken: string;
28
+ userId: string;
29
+ eventId?: string;
30
+ hubId?: string;
31
+ bookingId?: string;
32
+ chatId?: string;
33
+ conversationId?: string;
34
+ } = {
35
+ accessToken: '',
36
+ refreshToken: '',
37
+ userId: '',
38
+ };
39
+
40
+ // Stats
41
+ let passed = 0;
42
+ let failed = 0;
43
+
44
+ // Create axios client
45
+ function api(token?: string): AxiosInstance {
46
+ return axios.create({
47
+ baseURL: BASE_URL,
48
+ headers: token ? { Authorization: `Bearer ${token}` } : {},
49
+ validateStatus: () => true,
50
+ });
51
+ }
52
+
53
+ // Test runner
54
+ async function test(name: string, fn: () => Promise<void>): Promise<boolean> {
55
+ const start = Date.now();
56
+ try {
57
+ await fn();
58
+ console.log(`${c.green}PASS${c.reset} ${name} ${c.dim}(${Date.now() - start}ms)${c.reset}`);
59
+ passed++;
60
+ return true;
61
+ } catch (e: any) {
62
+ console.log(`${c.red}FAIL${c.reset} ${name} ${c.dim}(${Date.now() - start}ms)${c.reset}`);
63
+ console.log(` ${c.yellow}${e.message}${c.reset}`);
64
+ failed++;
65
+ return false;
66
+ }
67
+ }
68
+
69
+ // Unwrap API response
70
+ function unwrap<T>(res: { data: { data: T } }): T {
71
+ return res.data.data;
72
+ }
73
+
74
+ // ============================================================================
75
+ // TESTS
76
+ // ============================================================================
77
+
78
+ 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}`);
82
+ console.log(`${c.dim}Base URL: ${BASE_URL}${c.reset}`);
83
+ console.log(`${c.dim}Test Email: ${TEST_EMAIL}${c.reset}\n`);
84
+
85
+ // Health check
86
+ const health = await api().get('/health');
87
+ if (health.status !== 200) {
88
+ console.log(`${c.red}API is not reachable${c.reset}`);
89
+ process.exit(1);
90
+ }
91
+ console.log(`${c.green}API is healthy${c.reset}\n`);
92
+
93
+ // =========================================================================
94
+ // AUTH
95
+ // =========================================================================
96
+ console.log(`\n${c.cyan}--- AUTHENTICATION ---${c.reset}\n`);
97
+
98
+ await test('Register new user', async () => {
99
+ const res = await api().post('/auth/register', {
100
+ email: TEST_EMAIL,
101
+ password: TEST_PASSWORD,
102
+ name: 'E2E Test User',
103
+ });
104
+ if (res.status !== 201) throw new Error(`Status ${res.status}: ${JSON.stringify(res.data)}`);
105
+ const data = unwrap<any>(res);
106
+ state.accessToken = data.accessToken;
107
+ state.refreshToken = data.refreshToken;
108
+ state.userId = data.user.id;
109
+ });
110
+
111
+ await test('Login with email', async () => {
112
+ const res = await api().post('/auth/login', {
113
+ email: TEST_EMAIL,
114
+ password: TEST_PASSWORD,
115
+ });
116
+ if (res.status !== 200) throw new Error(`Status ${res.status}`);
117
+ const data = unwrap<any>(res);
118
+ state.accessToken = data.accessToken;
119
+ state.refreshToken = data.refreshToken;
120
+ });
121
+
122
+ await test('Refresh token', async () => {
123
+ const res = await api().post('/auth/refresh', {
124
+ refreshToken: state.refreshToken,
125
+ });
126
+ if (res.status !== 200) throw new Error(`Status ${res.status}: ${JSON.stringify(res.data)}`);
127
+ const data = unwrap<any>(res);
128
+ state.accessToken = data.accessToken;
129
+ state.refreshToken = data.refreshToken;
130
+ });
131
+
132
+ await test('Get current user (auth/me)', async () => {
133
+ const res = await api(state.accessToken).get('/auth/me');
134
+ if (res.status !== 200) throw new Error(`Status ${res.status}`);
135
+ });
136
+
137
+ await test('Invalid login returns 401', async () => {
138
+ const res = await api().post('/auth/login', {
139
+ email: TEST_EMAIL,
140
+ password: 'WrongPassword',
141
+ });
142
+ if (res.status !== 401) throw new Error(`Expected 401, got ${res.status}`);
143
+ });
144
+
145
+ // =========================================================================
146
+ // USERS
147
+ // =========================================================================
148
+ console.log(`\n${c.cyan}--- USERS ---${c.reset}\n`);
149
+
150
+ await test('Get current user profile', async () => {
151
+ const res = await api(state.accessToken).get('/users/me');
152
+ if (res.status !== 200) throw new Error(`Status ${res.status}`);
153
+ });
154
+
155
+ await test('Update profile', async () => {
156
+ const res = await api(state.accessToken).put('/users/me', {
157
+ name: 'Updated E2E User',
158
+ bio: 'E2E test bio',
159
+ });
160
+ if (res.status !== 200) throw new Error(`Status ${res.status}: ${JSON.stringify(res.data)}`);
161
+ });
162
+
163
+ await test('Get user by ID', async () => {
164
+ const res = await api(state.accessToken).get(`/users/${state.userId}`);
165
+ if (res.status !== 200) throw new Error(`Status ${res.status}`);
166
+ });
167
+
168
+ await test('Get user public profile', async () => {
169
+ const res = await api(state.accessToken).get(`/users/${state.userId}/public`);
170
+ if (res.status !== 200) throw new Error(`Status ${res.status}`);
171
+ });
172
+
173
+ // =========================================================================
174
+ // EVENTS
175
+ // =========================================================================
176
+ console.log(`\n${c.cyan}--- EVENTS ---${c.reset}\n`);
177
+
178
+ await test('List events', async () => {
179
+ const res = await api().get('/events');
180
+ if (res.status !== 200) throw new Error(`Status ${res.status}`);
181
+ });
182
+
183
+ await test('Get upcoming events', async () => {
184
+ const res = await api().get('/events/upcoming');
185
+ if (res.status !== 200) throw new Error(`Status ${res.status}`);
186
+ });
187
+
188
+ await test('Create event', async () => {
189
+ const res = await api(state.accessToken).post('/events', {
190
+ title: 'E2E Test Event',
191
+ description: 'A test event created by E2E tests',
192
+ startDate: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(), // 7 days from now
193
+ totalSpots: 20,
194
+ locationName: 'Test Location',
195
+ locationAddress: '123 Test Street',
196
+ });
197
+ if (res.status !== 201) throw new Error(`Status ${res.status}: ${JSON.stringify(res.data)}`);
198
+ const data = unwrap<any>(res);
199
+ state.eventId = data.id;
200
+ });
201
+
202
+ if (state.eventId) {
203
+ await test('Get event by ID', async () => {
204
+ const res = await api().get(`/events/${state.eventId}`);
205
+ if (res.status !== 200) throw new Error(`Status ${res.status}`);
206
+ });
207
+
208
+ await test('Update event', async () => {
209
+ const res = await api(state.accessToken).put(`/events/${state.eventId}`, {
210
+ title: 'E2E Test Event (Updated)',
211
+ });
212
+ if (res.status !== 200) throw new Error(`Status ${res.status}: ${JSON.stringify(res.data)}`);
213
+ });
214
+
215
+ await test('Publish event', async () => {
216
+ const res = await api(state.accessToken).post(`/events/${state.eventId}/publish`);
217
+ if (res.status !== 200) throw new Error(`Status ${res.status}`);
218
+ });
219
+
220
+ await test('Get my events', async () => {
221
+ const res = await api(state.accessToken).get('/events/my-events');
222
+ if (res.status !== 200) throw new Error(`Status ${res.status}`);
223
+ });
224
+ }
225
+
226
+ // =========================================================================
227
+ // BOOKINGS
228
+ // =========================================================================
229
+ console.log(`\n${c.cyan}--- BOOKINGS ---${c.reset}\n`);
230
+
231
+ if (state.eventId) {
232
+ await test('RSVP to event', async () => {
233
+ 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)}`);
236
+ if (res.status === 201) {
237
+ const data = unwrap<any>(res);
238
+ state.bookingId = data.id;
239
+ }
240
+ });
241
+
242
+ await test('Get my bookings', async () => {
243
+ const res = await api(state.accessToken).get('/bookings/me');
244
+ if (res.status !== 200) throw new Error(`Status ${res.status}`);
245
+ });
246
+ }
247
+
248
+ // =========================================================================
249
+ // EVENT CHAT
250
+ // =========================================================================
251
+ console.log(`\n${c.cyan}--- EVENT CHAT ---${c.reset}\n`);
252
+
253
+ if (state.eventId) {
254
+ await test('Get or create event chat', async () => {
255
+ const res = await api(state.accessToken).get(`/events/${state.eventId}/chat`);
256
+ if (res.status !== 200) throw new Error(`Status ${res.status}: ${JSON.stringify(res.data)}`);
257
+ const data = unwrap<any>(res);
258
+ state.chatId = data.id;
259
+ });
260
+
261
+ await test('Join event chat', async () => {
262
+ 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)}`);
265
+ });
266
+
267
+ await test('Send chat message', async () => {
268
+ const res = await api(state.accessToken).post(`/events/${state.eventId}/chat/messages`, {
269
+ content: 'Hello from E2E test!',
270
+ });
271
+ if (res.status !== 201) throw new Error(`Status ${res.status}: ${JSON.stringify(res.data)}`);
272
+ });
273
+
274
+ await test('Get chat messages', async () => {
275
+ 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)}`);
277
+ });
278
+
279
+ await test('Get chat members', async () => {
280
+ const res = await api(state.accessToken).get(`/events/${state.eventId}/chat/members`);
281
+ if (res.status !== 200) throw new Error(`Status ${res.status}`);
282
+ });
283
+
284
+ await test('Mark chat as read', async () => {
285
+ const res = await api(state.accessToken).post(`/events/${state.eventId}/chat/read`);
286
+ if (res.status !== 204) throw new Error(`Status ${res.status}`);
287
+ });
288
+ }
289
+
290
+ // =========================================================================
291
+ // HUBS
292
+ // =========================================================================
293
+ console.log(`\n${c.cyan}--- HUBS ---${c.reset}\n`);
294
+
295
+ await test('List hubs', async () => {
296
+ const res = await api().get('/hubs');
297
+ if (res.status !== 200) throw new Error(`Status ${res.status}`);
298
+ // Get first hub if exists
299
+ const data = res.data.data;
300
+ if (Array.isArray(data) && data.length > 0) {
301
+ state.hubId = data[0].id;
302
+ }
303
+ });
304
+
305
+ if (state.hubId) {
306
+ await test('Get hub by ID', async () => {
307
+ const res = await api().get(`/hubs/${state.hubId}`);
308
+ if (res.status !== 200) throw new Error(`Status ${res.status}`);
309
+ });
310
+
311
+ await test('Join hub', async () => {
312
+ const res = await api(state.accessToken).post(`/hubs/${state.hubId}/join`);
313
+ // 200/201 for join, 409 if already member
314
+ if (res.status !== 200 && res.status !== 201 && res.status !== 409) {
315
+ throw new Error(`Status ${res.status}: ${JSON.stringify(res.data)}`);
316
+ }
317
+ });
318
+
319
+ await test('Get hub members', async () => {
320
+ const res = await api(state.accessToken).get(`/hubs/${state.hubId}/members`);
321
+ if (res.status !== 200) throw new Error(`Status ${res.status}`);
322
+ });
323
+ }
324
+
325
+ // =========================================================================
326
+ // JACK AI (Support Conversations)
327
+ // =========================================================================
328
+ console.log(`\n${c.cyan}--- JACK AI ---${c.reset}\n`);
329
+
330
+ await test('Start Jack conversation', async () => {
331
+ const res = await api(state.accessToken).post('/support/jack/conversations/new', {
332
+ message: 'Hello Jack!',
333
+ });
334
+ if (res.status !== 201 && res.status !== 200) throw new Error(`Status ${res.status}: ${JSON.stringify(res.data)}`);
335
+ const data = unwrap<any>(res);
336
+ state.conversationId = data.conversation?.id || data.id;
337
+ });
338
+
339
+ await test('Get Jack conversations', async () => {
340
+ const res = await api(state.accessToken).get('/support/jack/conversations');
341
+ if (res.status !== 200) throw new Error(`Status ${res.status}`);
342
+ });
343
+
344
+ await test('Get Jack chat history', async () => {
345
+ const res = await api(state.accessToken).get('/support/jack/history');
346
+ if (res.status !== 200) throw new Error(`Status ${res.status}`);
347
+ });
348
+
349
+ if (state.conversationId) {
350
+ await test('Get Jack conversation by ID', async () => {
351
+ const res = await api(state.accessToken).get(`/support/jack/conversations/${state.conversationId}`);
352
+ if (res.status !== 200) throw new Error(`Status ${res.status}`);
353
+ });
354
+ }
355
+
356
+ // =========================================================================
357
+ // SUPPORT (Check-ins, Wins)
358
+ // =========================================================================
359
+ console.log(`\n${c.cyan}--- SUPPORT ---${c.reset}\n`);
360
+
361
+ await test('Create daily check-in', async () => {
362
+ const res = await api(state.accessToken).post('/support/check-ins', {
363
+ date: new Date().toISOString().split('T')[0],
364
+ mood: 4, // Valid range 1-5
365
+ energy: 4, // Valid range 1-5
366
+ stayedSober: true,
367
+ notes: 'E2E test check-in',
368
+ });
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)}`);
371
+ });
372
+
373
+ await test('Get check-ins', async () => {
374
+ const res = await api(state.accessToken).get('/support/check-ins');
375
+ if (res.status !== 200) throw new Error(`Status ${res.status}`);
376
+ });
377
+
378
+ await test('Get today check-in', async () => {
379
+ const res = await api(state.accessToken).get('/support/check-ins/today');
380
+ if (res.status !== 200 && res.status !== 404) throw new Error(`Status ${res.status}`);
381
+ });
382
+
383
+ await test('Get check-in streak', async () => {
384
+ const res = await api(state.accessToken).get('/support/check-ins/streak');
385
+ if (res.status !== 200) throw new Error(`Status ${res.status}`);
386
+ });
387
+
388
+ await test('Log a win', async () => {
389
+ const res = await api(state.accessToken).post('/support/wins', {
390
+ title: 'Completed E2E tests',
391
+ description: 'Successfully ran all API tests',
392
+ category: 'PERSONAL', // Valid: SOBRIETY, HEALTH, RELATIONSHIP, CAREER, PERSONAL, FINANCIAL, OTHER
393
+ });
394
+ if (res.status !== 201) throw new Error(`Status ${res.status}: ${JSON.stringify(res.data)}`);
395
+ });
396
+
397
+ await test('Get wins', async () => {
398
+ const res = await api(state.accessToken).get('/support/wins');
399
+ if (res.status !== 200) throw new Error(`Status ${res.status}`);
400
+ });
401
+
402
+ await test('Get wins count', async () => {
403
+ const res = await api(state.accessToken).get('/support/wins/count');
404
+ if (res.status !== 200) throw new Error(`Status ${res.status}`);
405
+ });
406
+
407
+ // =========================================================================
408
+ // NOTIFICATIONS
409
+ // =========================================================================
410
+ console.log(`\n${c.cyan}--- NOTIFICATIONS ---${c.reset}\n`);
411
+
412
+ await test('Get notifications', async () => {
413
+ const res = await api(state.accessToken).get('/notifications');
414
+ if (res.status !== 200) throw new Error(`Status ${res.status}`);
415
+ });
416
+
417
+ await test('Get unread count', async () => {
418
+ const res = await api(state.accessToken).get('/notifications/unread-count');
419
+ if (res.status !== 200) throw new Error(`Status ${res.status}`);
420
+ });
421
+
422
+ // =========================================================================
423
+ // SUBSCRIPTIONS
424
+ // =========================================================================
425
+ console.log(`\n${c.cyan}--- SUBSCRIPTIONS ---${c.reset}\n`);
426
+
427
+ await test('Get subscription plans', async () => {
428
+ const res = await api(state.accessToken).get('/subscriptions/plans');
429
+ if (res.status !== 200) throw new Error(`Status ${res.status}`);
430
+ });
431
+
432
+ await test('Get my subscription', async () => {
433
+ const res = await api(state.accessToken).get('/subscriptions/me');
434
+ // 200 if subscribed, 404 if not
435
+ if (res.status !== 200 && res.status !== 404) throw new Error(`Status ${res.status}`);
436
+ });
437
+
438
+ // =========================================================================
439
+ // LIBRARY
440
+ // =========================================================================
441
+ console.log(`\n${c.cyan}--- LIBRARY ---${c.reset}\n`);
442
+
443
+ await test('Get library content', async () => {
444
+ const res = await api(state.accessToken).get('/library');
445
+ if (res.status !== 200) throw new Error(`Status ${res.status}`);
446
+ });
447
+
448
+ await test('Get featured content', async () => {
449
+ const res = await api(state.accessToken).get('/library/featured');
450
+ if (res.status !== 200) throw new Error(`Status ${res.status}`);
451
+ });
452
+
453
+ // =========================================================================
454
+ // MAP
455
+ // =========================================================================
456
+ console.log(`\n${c.cyan}--- MAP ---${c.reset}\n`);
457
+
458
+ await test('Get map hubs', async () => {
459
+ const res = await api(state.accessToken).get('/map/hubs');
460
+ if (res.status !== 200) throw new Error(`Status ${res.status}`);
461
+ });
462
+
463
+ await test('Get map events', async () => {
464
+ const res = await api(state.accessToken).get('/map/events');
465
+ if (res.status !== 200) throw new Error(`Status ${res.status}`);
466
+ });
467
+
468
+ // =========================================================================
469
+ // CLEANUP
470
+ // =========================================================================
471
+ console.log(`\n${c.cyan}--- CLEANUP ---${c.reset}\n`);
472
+
473
+ if (state.bookingId) {
474
+ await test('Cancel booking', async () => {
475
+ const res = await api(state.accessToken).delete(`/bookings/${state.bookingId}`);
476
+ if (res.status !== 204 && res.status !== 200) throw new Error(`Status ${res.status}`);
477
+ });
478
+ }
479
+
480
+ if (state.eventId) {
481
+ await test('Cancel event', async () => {
482
+ const res = await api(state.accessToken).post(`/events/${state.eventId}/cancel`);
483
+ if (res.status !== 200) throw new Error(`Status ${res.status}`);
484
+ });
485
+ }
486
+
487
+ // =========================================================================
488
+ // SUMMARY
489
+ // =========================================================================
490
+ console.log(`\n${c.cyan}${'='.repeat(60)}${c.reset}`);
491
+ 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`);
497
+
498
+ process.exit(failed > 0 ? 1 : 0);
499
+ }
500
+
501
+ // Run tests
502
+ runTests().catch(console.error);