spaps 0.7.3 → 0.7.5

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 (48) hide show
  1. package/AI_TOOLS.json +10 -11
  2. package/README.md +216 -36
  3. package/assets/local-runtime/Dockerfile +28 -0
  4. package/assets/local-runtime/alembic/env.py +101 -0
  5. package/assets/local-runtime/alembic/path_bootstrap.py +71 -0
  6. package/assets/local-runtime/alembic/versions/000000000001_baseline_consolidated_schema.py +1076 -0
  7. package/assets/local-runtime/alembic/versions/000000000002_fix_column_types_to_match_prod.py +83 -0
  8. package/assets/local-runtime/alembic/versions/000000000003_fix_email_template_key_uniqueness.py +49 -0
  9. package/assets/local-runtime/alembic/versions/000000000004_add_hold_duration_minutes_to_dayrate_config.py +30 -0
  10. package/assets/local-runtime/alembic/versions/000000000005_resource_scoped_entitlements.py +77 -0
  11. package/assets/local-runtime/alembic/versions/000000000006_cfo_rbac_add_is_admin.py +37 -0
  12. package/assets/local-runtime/alembic/versions/000000000007_agent_approvals.py +158 -0
  13. package/assets/local-runtime/alembic/versions/000000000008_add_company_id_to_cfo_connections.py +35 -0
  14. package/assets/local-runtime/alembic/versions/000000000009_tx_signing.py +62 -0
  15. package/assets/local-runtime/alembic/versions/000000000010_affiliate_referrals.py +235 -0
  16. package/assets/local-runtime/alembic/versions/000000000011_checkin_call_booking.py +137 -0
  17. package/assets/local-runtime/alembic/versions/000000000012_subscription_application_scoping.py +55 -0
  18. package/assets/local-runtime/alembic/versions/000000000013_refresh_token_anomaly_context.py +61 -0
  19. package/assets/local-runtime/alembic/versions/000000000014_buildooor_dayrate_hire_schedule.py +39 -0
  20. package/assets/local-runtime/alembic/versions/000000000015_support_telemetry_platform.py +112 -0
  21. package/assets/local-runtime/alembic/versions/000000000016_issue_reporting_platform.py +54 -0
  22. package/assets/local-runtime/alembic/versions/000000000017_issue_reporting_platform_import_tracking.py +44 -0
  23. package/assets/local-runtime/alembic/versions/000000000018_authorization_policy_engine.py +76 -0
  24. package/assets/local-runtime/alembic.ini +47 -0
  25. package/assets/local-runtime/docker-compose.yml +61 -0
  26. package/assets/local-runtime/manifest.json +8 -0
  27. package/assets/local-runtime/scripts/container-entrypoint.sh +13 -0
  28. package/assets/local-runtime/scripts/fetch-prod-db.sh +112 -0
  29. package/assets/local-runtime/scripts/run-migrations.sh +96 -0
  30. package/package.json +2 -1
  31. package/src/ai-helper.js +176 -234
  32. package/src/ai-tool-spec.js +52 -20
  33. package/src/auth/api-key.js +119 -0
  34. package/src/auth/client-id.js +136 -0
  35. package/src/auth/client.js +169 -0
  36. package/src/auth/credentials.js +110 -0
  37. package/src/auth/device-flow.js +159 -0
  38. package/src/auth/env.js +57 -0
  39. package/src/auth/handlers.js +462 -0
  40. package/src/auth/http.js +74 -0
  41. package/src/cli-dispatcher.js +134 -21
  42. package/src/docs-system.js +7 -7
  43. package/src/fixture-kernel.js +1143 -0
  44. package/src/handlers.js +202 -11
  45. package/src/help-system.js +2 -0
  46. package/src/local-runtime.js +258 -0
  47. package/src/local-server.js +597 -199
  48. package/src/project-scaffolder.js +185 -45
@@ -0,0 +1,1076 @@
1
+ """baseline_consolidated_schema
2
+
3
+ Revision ID: 000000000001
4
+ Revises:
5
+ Create Date: 2026-02-09 00:00:00.000000
6
+
7
+ SPAPS Python Rewrite - Consolidated baseline migration (51 tables)
8
+ Merged auth.users + user_profiles into unified users table
9
+ No Supabase constructs (RLS, auth schema, current_setting())
10
+ All tables use UUID PKs with gen_random_uuid() defaults
11
+ All timestamps are timestamptz with UTC defaults
12
+ """
13
+ from typing import Sequence, Union
14
+
15
+ from alembic import op
16
+ import sqlalchemy as sa
17
+
18
+
19
+ # revision identifiers, used by Alembic.
20
+ revision: str = '000000000001'
21
+ down_revision: Union[str, None] = None
22
+ branch_labels: Union[str, Sequence[str], None] = None
23
+ depends_on: Union[str, Sequence[str], None] = None
24
+
25
+
26
+ def upgrade() -> None:
27
+ # Enable required extensions
28
+ op.execute('CREATE EXTENSION IF NOT EXISTS "uuid-ossp"')
29
+ op.execute('CREATE EXTENSION IF NOT EXISTS "pgcrypto"')
30
+ op.execute('CREATE EXTENSION IF NOT EXISTS "vector"')
31
+
32
+ # ===== CORE IDENTITY =====
33
+
34
+ # users table (merged from auth.users + user_profiles)
35
+ op.execute("""
36
+ CREATE TABLE users (
37
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
38
+ email TEXT UNIQUE,
39
+ password_hash TEXT,
40
+ username TEXT,
41
+ full_name TEXT,
42
+ avatar_url TEXT,
43
+ phone TEXT,
44
+ phone_number TEXT,
45
+ metadata JSONB,
46
+ stripe_metadata JSONB,
47
+ tier TEXT,
48
+ stripe_customer_id TEXT,
49
+ default_payment_method TEXT,
50
+ is_super_admin BOOLEAN DEFAULT false,
51
+ email_confirmed_at TIMESTAMPTZ,
52
+ phone_confirmed_at TIMESTAMPTZ,
53
+ confirmed_at TIMESTAMPTZ,
54
+ last_sign_in_at TIMESTAMPTZ,
55
+ banned_until TIMESTAMPTZ,
56
+ deleted_at TIMESTAMPTZ,
57
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
58
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
59
+ )
60
+ """)
61
+
62
+ # applications table
63
+ op.execute("""
64
+ CREATE TABLE applications (
65
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
66
+ name TEXT NOT NULL,
67
+ slug TEXT UNIQUE NOT NULL,
68
+ description TEXT,
69
+ api_key TEXT UNIQUE NOT NULL,
70
+ allowed_origins TEXT[],
71
+ webhook_url TEXT,
72
+ settings JSONB,
73
+ whitelist_enabled BOOLEAN DEFAULT false,
74
+ whitelist_count INTEGER DEFAULT 0,
75
+ publishable_key_hash TEXT,
76
+ secret_key_hash TEXT,
77
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
78
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
79
+ )
80
+ """)
81
+
82
+ # user_wallets table
83
+ op.execute("""
84
+ CREATE TABLE user_wallets (
85
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
86
+ user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
87
+ wallet_address TEXT NOT NULL,
88
+ chain_type TEXT NOT NULL,
89
+ is_primary BOOLEAN DEFAULT false,
90
+ verified_at TIMESTAMPTZ,
91
+ metadata JSONB,
92
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
93
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
94
+ )
95
+ """)
96
+ op.execute('CREATE INDEX idx_user_wallets_user_id ON user_wallets(user_id)')
97
+ op.execute('CREATE INDEX idx_user_wallets_wallet_address ON user_wallets(wallet_address)')
98
+ op.execute('CREATE INDEX idx_user_wallets_chain_type ON user_wallets(chain_type)')
99
+
100
+ # user_applications table
101
+ op.execute("""
102
+ CREATE TABLE user_applications (
103
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
104
+ user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
105
+ application_id UUID NOT NULL REFERENCES applications(id) ON DELETE CASCADE,
106
+ permissions JSONB,
107
+ metadata JSONB,
108
+ app_specific_data JSONB,
109
+ usage_balance NUMERIC,
110
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
111
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
112
+ )
113
+ """)
114
+ op.execute('CREATE INDEX idx_user_applications_user_id ON user_applications(user_id)')
115
+ op.execute('CREATE INDEX idx_user_applications_application_id ON user_applications(application_id)')
116
+ op.execute('CREATE UNIQUE INDEX idx_user_applications_unique ON user_applications(user_id, application_id)')
117
+
118
+ # ===== AUTH INFRASTRUCTURE =====
119
+
120
+ # auth_nonces table
121
+ op.execute("""
122
+ CREATE TABLE auth_nonces (
123
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
124
+ wallet_address TEXT NOT NULL,
125
+ nonce TEXT NOT NULL,
126
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
127
+ expires_at TIMESTAMPTZ NOT NULL,
128
+ used BOOLEAN DEFAULT false
129
+ )
130
+ """)
131
+ op.execute('CREATE INDEX idx_auth_nonces_wallet_address ON auth_nonces(wallet_address)')
132
+ op.execute('CREATE INDEX idx_auth_nonces_expires_at ON auth_nonces(expires_at)')
133
+
134
+ # auth_tokens table
135
+ op.execute("""
136
+ CREATE TABLE auth_tokens (
137
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
138
+ user_id UUID REFERENCES users(id) ON DELETE CASCADE,
139
+ token_hash TEXT UNIQUE NOT NULL,
140
+ type TEXT NOT NULL,
141
+ email TEXT,
142
+ application_id UUID REFERENCES applications(id) ON DELETE CASCADE,
143
+ expires_at TIMESTAMPTZ NOT NULL,
144
+ used_at TIMESTAMPTZ,
145
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
146
+ )
147
+ """)
148
+ op.execute('CREATE INDEX idx_auth_tokens_user_id ON auth_tokens(user_id)')
149
+ op.execute('CREATE INDEX idx_auth_tokens_application_id ON auth_tokens(application_id)')
150
+ op.execute('CREATE INDEX idx_auth_tokens_type ON auth_tokens(type)')
151
+ op.execute('CREATE INDEX idx_auth_tokens_expires_at ON auth_tokens(expires_at)')
152
+
153
+ # auth_magic_link_states table
154
+ op.execute("""
155
+ CREATE TABLE auth_magic_link_states (
156
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
157
+ state TEXT UNIQUE NOT NULL,
158
+ email TEXT NOT NULL,
159
+ application_id UUID REFERENCES applications(id) ON DELETE CASCADE,
160
+ metadata JSONB,
161
+ expires_at TIMESTAMPTZ NOT NULL,
162
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
163
+ )
164
+ """)
165
+ op.execute('CREATE INDEX idx_auth_magic_link_states_application_id ON auth_magic_link_states(application_id)')
166
+ op.execute('CREATE INDEX idx_auth_magic_link_states_expires_at ON auth_magic_link_states(expires_at)')
167
+
168
+ # refresh_tokens table
169
+ op.execute("""
170
+ CREATE TABLE refresh_tokens (
171
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
172
+ user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
173
+ application_id UUID REFERENCES applications(id) ON DELETE CASCADE,
174
+ token TEXT UNIQUE NOT NULL,
175
+ expires_at TIMESTAMPTZ NOT NULL,
176
+ used_at TIMESTAMPTZ,
177
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
178
+ )
179
+ """)
180
+ op.execute('CREATE INDEX idx_refresh_tokens_user_id ON refresh_tokens(user_id)')
181
+ op.execute('CREATE INDEX idx_refresh_tokens_application_id ON refresh_tokens(application_id)')
182
+ op.execute('CREATE INDEX idx_refresh_tokens_expires_at ON refresh_tokens(expires_at)')
183
+
184
+ # blacklisted_tokens table
185
+ op.execute("""
186
+ CREATE TABLE blacklisted_tokens (
187
+ jti TEXT PRIMARY KEY,
188
+ user_id UUID REFERENCES users(id) ON DELETE CASCADE,
189
+ expires_at TIMESTAMPTZ NOT NULL,
190
+ blacklisted_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
191
+ reason TEXT,
192
+ ip_address TEXT,
193
+ user_agent TEXT
194
+ )
195
+ """)
196
+ op.execute('CREATE INDEX idx_blacklisted_tokens_user_id ON blacklisted_tokens(user_id)')
197
+ op.execute('CREATE INDEX idx_blacklisted_tokens_expires_at ON blacklisted_tokens(expires_at)')
198
+
199
+ # ===== STRIPE / PAYMENTS =====
200
+
201
+ # stripe_customers table
202
+ op.execute("""
203
+ CREATE TABLE stripe_customers (
204
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
205
+ user_id UUID REFERENCES users(id) ON DELETE CASCADE,
206
+ application_id UUID REFERENCES applications(id) ON DELETE CASCADE,
207
+ email TEXT,
208
+ metadata JSONB,
209
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
210
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
211
+ )
212
+ """)
213
+ op.execute('CREATE INDEX idx_stripe_customers_user_id ON stripe_customers(user_id)')
214
+ op.execute('CREATE INDEX idx_stripe_customers_application_id ON stripe_customers(application_id)')
215
+
216
+ # stripe_products table
217
+ op.execute("""
218
+ CREATE TABLE stripe_products (
219
+ stripe_product_id TEXT PRIMARY KEY,
220
+ application_id UUID REFERENCES applications(id) ON DELETE CASCADE,
221
+ name TEXT NOT NULL,
222
+ description TEXT,
223
+ active BOOLEAN DEFAULT true,
224
+ images TEXT[],
225
+ metadata JSONB,
226
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
227
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
228
+ archived_at TIMESTAMPTZ
229
+ )
230
+ """)
231
+ op.execute('CREATE INDEX idx_stripe_products_application_id ON stripe_products(application_id)')
232
+ op.execute('CREATE INDEX idx_stripe_products_active ON stripe_products(active)')
233
+
234
+ # stripe_prices table
235
+ op.execute("""
236
+ CREATE TABLE stripe_prices (
237
+ stripe_price_id TEXT PRIMARY KEY,
238
+ stripe_product_id TEXT REFERENCES stripe_products(stripe_product_id) ON DELETE CASCADE,
239
+ active BOOLEAN DEFAULT true,
240
+ currency TEXT NOT NULL,
241
+ unit_amount INTEGER,
242
+ type TEXT NOT NULL,
243
+ nickname TEXT,
244
+ recurring JSONB,
245
+ metadata JSONB,
246
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
247
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
248
+ )
249
+ """)
250
+ op.execute('CREATE INDEX idx_stripe_prices_stripe_product_id ON stripe_prices(stripe_product_id)')
251
+ op.execute('CREATE INDEX idx_stripe_prices_active ON stripe_prices(active)')
252
+
253
+ # checkout_sessions table
254
+ op.execute("""
255
+ CREATE TABLE checkout_sessions (
256
+ session_id TEXT PRIMARY KEY,
257
+ user_id UUID REFERENCES users(id) ON DELETE SET NULL,
258
+ application_id UUID REFERENCES applications(id) ON DELETE CASCADE,
259
+ customer_id TEXT,
260
+ status TEXT NOT NULL,
261
+ payment_status TEXT,
262
+ mode TEXT NOT NULL,
263
+ success_url TEXT,
264
+ cancel_url TEXT,
265
+ amount_total INTEGER,
266
+ currency TEXT,
267
+ payment_intent_id TEXT,
268
+ subscription_id TEXT,
269
+ metadata JSONB,
270
+ is_guest BOOLEAN DEFAULT false,
271
+ customer_email TEXT,
272
+ expires_at TIMESTAMPTZ,
273
+ completed_at TIMESTAMPTZ,
274
+ expired_at TIMESTAMPTZ,
275
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
276
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
277
+ )
278
+ """)
279
+ op.execute('CREATE INDEX idx_checkout_sessions_user_id ON checkout_sessions(user_id)')
280
+ op.execute('CREATE INDEX idx_checkout_sessions_application_id ON checkout_sessions(application_id)')
281
+ op.execute('CREATE INDEX idx_checkout_sessions_status ON checkout_sessions(status)')
282
+ op.execute('CREATE INDEX idx_checkout_sessions_customer_email ON checkout_sessions(customer_email)')
283
+
284
+ # stripe_payments table
285
+ op.execute("""
286
+ CREATE TABLE stripe_payments (
287
+ payment_intent_id TEXT PRIMARY KEY,
288
+ application_id UUID REFERENCES applications(id) ON DELETE CASCADE,
289
+ customer_id TEXT,
290
+ amount INTEGER NOT NULL,
291
+ currency TEXT NOT NULL,
292
+ status TEXT NOT NULL,
293
+ stripe_fee INTEGER,
294
+ spaps_fee INTEGER,
295
+ net_amount INTEGER,
296
+ failure_message TEXT,
297
+ metadata JSONB,
298
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
299
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
300
+ )
301
+ """)
302
+ op.execute('CREATE INDEX idx_stripe_payments_application_id ON stripe_payments(application_id)')
303
+ op.execute('CREATE INDEX idx_stripe_payments_customer_id ON stripe_payments(customer_id)')
304
+ op.execute('CREATE INDEX idx_stripe_payments_status ON stripe_payments(status)')
305
+
306
+ # stripe_webhook_events table
307
+ op.execute("""
308
+ CREATE TABLE stripe_webhook_events (
309
+ event_id TEXT PRIMARY KEY,
310
+ type TEXT NOT NULL,
311
+ data JSONB NOT NULL,
312
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
313
+ processed_at TIMESTAMPTZ
314
+ )
315
+ """)
316
+ op.execute('CREATE INDEX idx_stripe_webhook_events_type ON stripe_webhook_events(type)')
317
+ op.execute('CREATE INDEX idx_stripe_webhook_events_processed_at ON stripe_webhook_events(processed_at)')
318
+
319
+ # subscriptions table
320
+ op.execute("""
321
+ CREATE TABLE subscriptions (
322
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
323
+ user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
324
+ stripe_customer_id TEXT,
325
+ stripe_subscription_id TEXT UNIQUE,
326
+ price_id TEXT,
327
+ status TEXT NOT NULL,
328
+ current_period_start TIMESTAMPTZ,
329
+ current_period_end TIMESTAMPTZ,
330
+ cancel_at TIMESTAMPTZ,
331
+ canceled_at TIMESTAMPTZ,
332
+ metadata JSONB,
333
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
334
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
335
+ )
336
+ """)
337
+ op.execute('CREATE INDEX idx_subscriptions_user_id ON subscriptions(user_id)')
338
+ op.execute('CREATE INDEX idx_subscriptions_stripe_customer_id ON subscriptions(stripe_customer_id)')
339
+ op.execute('CREATE INDEX idx_subscriptions_status ON subscriptions(status)')
340
+
341
+ # stripe_invoices table
342
+ op.execute("""
343
+ CREATE TABLE stripe_invoices (
344
+ invoice_id TEXT PRIMARY KEY,
345
+ subscription_id TEXT REFERENCES subscriptions(stripe_subscription_id) ON DELETE SET NULL,
346
+ customer_id TEXT,
347
+ amount_paid INTEGER,
348
+ amount_due INTEGER,
349
+ currency TEXT NOT NULL,
350
+ status TEXT NOT NULL,
351
+ metadata JSONB,
352
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
353
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
354
+ )
355
+ """)
356
+ op.execute('CREATE INDEX idx_stripe_invoices_subscription_id ON stripe_invoices(subscription_id)')
357
+ op.execute('CREATE INDEX idx_stripe_invoices_customer_id ON stripe_invoices(customer_id)')
358
+ op.execute('CREATE INDEX idx_stripe_invoices_status ON stripe_invoices(status)')
359
+
360
+ # payment_history table
361
+ op.execute("""
362
+ CREATE TABLE payment_history (
363
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
364
+ user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
365
+ subscription_id UUID REFERENCES subscriptions(id) ON DELETE SET NULL,
366
+ stripe_payment_intent_id TEXT,
367
+ amount INTEGER NOT NULL,
368
+ currency TEXT NOT NULL,
369
+ status TEXT NOT NULL,
370
+ description TEXT,
371
+ metadata JSONB,
372
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
373
+ )
374
+ """)
375
+ op.execute('CREATE INDEX idx_payment_history_user_id ON payment_history(user_id)')
376
+ op.execute('CREATE INDEX idx_payment_history_subscription_id ON payment_history(subscription_id)')
377
+ op.execute('CREATE INDEX idx_payment_history_created_at ON payment_history(created_at)')
378
+
379
+ # wallet_payments table
380
+ op.execute("""
381
+ CREATE TABLE wallet_payments (
382
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
383
+ user_id UUID REFERENCES users(id) ON DELETE SET NULL,
384
+ wallet_address TEXT NOT NULL,
385
+ chain_type TEXT NOT NULL,
386
+ transaction_hash TEXT UNIQUE NOT NULL,
387
+ amount NUMERIC NOT NULL,
388
+ token_symbol TEXT NOT NULL,
389
+ usd_value NUMERIC,
390
+ status TEXT NOT NULL,
391
+ confirmed_at TIMESTAMPTZ,
392
+ metadata JSONB,
393
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
394
+ )
395
+ """)
396
+ op.execute('CREATE INDEX idx_wallet_payments_user_id ON wallet_payments(user_id)')
397
+ op.execute('CREATE INDEX idx_wallet_payments_wallet_address ON wallet_payments(wallet_address)')
398
+ op.execute('CREATE INDEX idx_wallet_payments_status ON wallet_payments(status)')
399
+
400
+ # guest_checkout_conversions table
401
+ op.execute("""
402
+ CREATE TABLE guest_checkout_conversions (
403
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
404
+ session_id TEXT REFERENCES checkout_sessions(session_id) ON DELETE CASCADE,
405
+ guest_email TEXT NOT NULL,
406
+ converted_user_id UUID REFERENCES users(id) ON DELETE SET NULL,
407
+ converted_at TIMESTAMPTZ NOT NULL,
408
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
409
+ )
410
+ """)
411
+ op.execute('CREATE INDEX idx_guest_checkout_conversions_session_id ON guest_checkout_conversions(session_id)')
412
+ op.execute('CREATE INDEX idx_guest_checkout_conversions_guest_email ON guest_checkout_conversions(guest_email)')
413
+
414
+ # ===== CRYPTO =====
415
+
416
+ # crypto_invoices table
417
+ op.execute("""
418
+ CREATE TABLE crypto_invoices (
419
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
420
+ application_id UUID REFERENCES applications(id) ON DELETE CASCADE,
421
+ asset TEXT NOT NULL,
422
+ network TEXT NOT NULL,
423
+ amount NUMERIC NOT NULL,
424
+ status TEXT NOT NULL,
425
+ underpaid BOOLEAN DEFAULT false,
426
+ overpaid BOOLEAN DEFAULT false,
427
+ beneficiary TEXT,
428
+ metadata JSONB,
429
+ destination JSONB,
430
+ settlement JSONB,
431
+ expires_at TIMESTAMPTZ,
432
+ settled_at TIMESTAMPTZ,
433
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
434
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
435
+ )
436
+ """)
437
+ op.execute('CREATE INDEX idx_crypto_invoices_application_id ON crypto_invoices(application_id)')
438
+ op.execute('CREATE INDEX idx_crypto_invoices_status ON crypto_invoices(status)')
439
+
440
+ # ledger_transactions table
441
+ op.execute("""
442
+ CREATE TABLE ledger_transactions (
443
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
444
+ invoice_id UUID REFERENCES crypto_invoices(id) ON DELETE CASCADE,
445
+ application_id UUID REFERENCES applications(id) ON DELETE CASCADE,
446
+ asset TEXT NOT NULL,
447
+ network TEXT NOT NULL,
448
+ amount NUMERIC NOT NULL,
449
+ source TEXT NOT NULL,
450
+ dedupe_key TEXT UNIQUE NOT NULL,
451
+ state_detail TEXT,
452
+ settlement JSONB,
453
+ settled_at TIMESTAMPTZ,
454
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
455
+ )
456
+ """)
457
+ op.execute('CREATE INDEX idx_ledger_transactions_invoice_id ON ledger_transactions(invoice_id)')
458
+ op.execute('CREATE INDEX idx_ledger_transactions_application_id ON ledger_transactions(application_id)')
459
+
460
+ # crypto_webhook_dedupes table
461
+ op.execute("""
462
+ CREATE TABLE crypto_webhook_dedupes (
463
+ dedupe_key TEXT PRIMARY KEY,
464
+ payload JSONB NOT NULL,
465
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
466
+ )
467
+ """)
468
+
469
+ # app_receiving_addresses table
470
+ op.execute("""
471
+ CREATE TABLE app_receiving_addresses (
472
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
473
+ application_id UUID REFERENCES applications(id) ON DELETE CASCADE,
474
+ asset TEXT NOT NULL,
475
+ network TEXT NOT NULL,
476
+ destination_type TEXT NOT NULL,
477
+ destination_address TEXT NOT NULL,
478
+ destination_details JSONB,
479
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
480
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
481
+ )
482
+ """)
483
+ op.execute('CREATE INDEX idx_app_receiving_addresses_application_id ON app_receiving_addresses(application_id)')
484
+
485
+ # chain_sync_state table
486
+ op.execute("""
487
+ CREATE TABLE chain_sync_state (
488
+ provider TEXT PRIMARY KEY,
489
+ cursor TEXT,
490
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
491
+ )
492
+ """)
493
+
494
+ # ===== ENTITLEMENTS =====
495
+
496
+ # entitlements table
497
+ op.execute("""
498
+ CREATE TABLE entitlements (
499
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
500
+ crypto_invoice_id UUID REFERENCES crypto_invoices(id) ON DELETE SET NULL,
501
+ application_id UUID REFERENCES applications(id) ON DELETE CASCADE,
502
+ beneficiary_user_id UUID REFERENCES users(id) ON DELETE CASCADE,
503
+ beneficiary_email TEXT,
504
+ entitlement_type TEXT NOT NULL,
505
+ entitlement_key TEXT NOT NULL,
506
+ source TEXT NOT NULL,
507
+ stripe_product_id TEXT REFERENCES stripe_products(stripe_product_id) ON DELETE SET NULL,
508
+ stripe_price_id TEXT REFERENCES stripe_prices(stripe_price_id) ON DELETE SET NULL,
509
+ stripe_subscription_id TEXT,
510
+ stripe_session_id TEXT,
511
+ stripe_invoice_id TEXT,
512
+ manual_grant_id TEXT,
513
+ manual_grant_reason TEXT,
514
+ granted_by_user_id UUID REFERENCES users(id) ON DELETE SET NULL,
515
+ revoked_by_user_id UUID REFERENCES users(id) ON DELETE SET NULL,
516
+ revoked_reason TEXT,
517
+ starts_at TIMESTAMPTZ,
518
+ ends_at TIMESTAMPTZ,
519
+ revoked_at TIMESTAMPTZ,
520
+ metadata JSONB,
521
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
522
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
523
+ )
524
+ """)
525
+ op.execute('CREATE INDEX idx_entitlements_crypto_invoice_id ON entitlements(crypto_invoice_id)')
526
+ op.execute('CREATE INDEX idx_entitlements_application_id ON entitlements(application_id)')
527
+ op.execute('CREATE INDEX idx_entitlements_beneficiary_user_id ON entitlements(beneficiary_user_id)')
528
+ op.execute('CREATE INDEX idx_entitlements_beneficiary_email ON entitlements(beneficiary_email)')
529
+ op.execute('CREATE INDEX idx_entitlements_entitlement_key ON entitlements(entitlement_key)')
530
+ op.execute('CREATE INDEX idx_entitlements_source ON entitlements(source)')
531
+
532
+ # entitlement_mappings table
533
+ op.execute("""
534
+ CREATE TABLE entitlement_mappings (
535
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
536
+ application_id UUID REFERENCES applications(id) ON DELETE CASCADE,
537
+ stripe_price_id TEXT REFERENCES stripe_prices(stripe_price_id) ON DELETE CASCADE,
538
+ entitlement_key TEXT NOT NULL,
539
+ entitlement_type TEXT NOT NULL,
540
+ duration_days INTEGER,
541
+ metadata JSONB,
542
+ is_active BOOLEAN DEFAULT true,
543
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
544
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
545
+ )
546
+ """)
547
+ op.execute('CREATE INDEX idx_entitlement_mappings_application_id ON entitlement_mappings(application_id)')
548
+ op.execute('CREATE INDEX idx_entitlement_mappings_stripe_price_id ON entitlement_mappings(stripe_price_id)')
549
+
550
+ # entitlement_events table
551
+ op.execute("""
552
+ CREATE TABLE entitlement_events (
553
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
554
+ entitlement_id UUID REFERENCES entitlements(id) ON DELETE CASCADE,
555
+ event_type TEXT NOT NULL,
556
+ application_id UUID REFERENCES applications(id) ON DELETE CASCADE,
557
+ user_id UUID REFERENCES users(id) ON DELETE SET NULL,
558
+ email TEXT,
559
+ entitlement_key TEXT,
560
+ source TEXT,
561
+ previous_state JSONB,
562
+ new_state JSONB,
563
+ triggered_by TEXT,
564
+ metadata JSONB,
565
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
566
+ )
567
+ """)
568
+ op.execute('CREATE INDEX idx_entitlement_events_entitlement_id ON entitlement_events(entitlement_id)')
569
+ op.execute('CREATE INDEX idx_entitlement_events_application_id ON entitlement_events(application_id)')
570
+ op.execute('CREATE INDEX idx_entitlement_events_user_id ON entitlement_events(user_id)')
571
+ op.execute('CREATE INDEX idx_entitlement_events_event_type ON entitlement_events(event_type)')
572
+
573
+ # entitlement_projection_log table
574
+ op.execute("""
575
+ CREATE TABLE entitlement_projection_log (
576
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
577
+ invoice_id UUID REFERENCES crypto_invoices(id) ON DELETE CASCADE,
578
+ status TEXT NOT NULL,
579
+ detail TEXT,
580
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
581
+ processed_at TIMESTAMPTZ
582
+ )
583
+ """)
584
+ op.execute('CREATE INDEX idx_entitlement_projection_log_invoice_id ON entitlement_projection_log(invoice_id)')
585
+
586
+ # ===== WEBHOOKS =====
587
+
588
+ # webhooks table
589
+ op.execute("""
590
+ CREATE TABLE webhooks (
591
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
592
+ application_id UUID REFERENCES applications(id) ON DELETE CASCADE,
593
+ url TEXT NOT NULL,
594
+ secret TEXT NOT NULL,
595
+ events TEXT[] NOT NULL,
596
+ is_active BOOLEAN DEFAULT true,
597
+ headers JSONB,
598
+ retry_config JSONB,
599
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
600
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
601
+ )
602
+ """)
603
+ op.execute('CREATE INDEX idx_webhooks_application_id ON webhooks(application_id)')
604
+
605
+ # webhook_events table
606
+ op.execute("""
607
+ CREATE TABLE webhook_events (
608
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
609
+ type TEXT NOT NULL,
610
+ application_id UUID REFERENCES applications(id) ON DELETE CASCADE,
611
+ user_id UUID REFERENCES users(id) ON DELETE SET NULL,
612
+ data JSONB NOT NULL,
613
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
614
+ schema_version INTEGER DEFAULT 1
615
+ )
616
+ """)
617
+ op.execute('CREATE INDEX idx_webhook_events_application_id ON webhook_events(application_id)')
618
+ op.execute('CREATE INDEX idx_webhook_events_type ON webhook_events(type)')
619
+
620
+ # webhook_deliveries table
621
+ op.execute("""
622
+ CREATE TABLE webhook_deliveries (
623
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
624
+ webhook_id UUID REFERENCES webhooks(id) ON DELETE CASCADE,
625
+ event_type TEXT NOT NULL,
626
+ payload JSONB NOT NULL,
627
+ response_status INTEGER,
628
+ response_body TEXT,
629
+ delivered_at TIMESTAMPTZ,
630
+ retry_count INTEGER DEFAULT 0,
631
+ attempt INTEGER DEFAULT 1,
632
+ error_message TEXT
633
+ )
634
+ """)
635
+ op.execute('CREATE INDEX idx_webhook_deliveries_webhook_id ON webhook_deliveries(webhook_id)')
636
+ op.execute('CREATE INDEX idx_webhook_deliveries_delivered_at ON webhook_deliveries(delivered_at)')
637
+
638
+ # ===== EMAIL =====
639
+
640
+ # email_templates table
641
+ op.execute("""
642
+ CREATE TABLE email_templates (
643
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
644
+ application_id UUID REFERENCES applications(id) ON DELETE CASCADE,
645
+ template_key TEXT UNIQUE NOT NULL,
646
+ name TEXT NOT NULL,
647
+ description TEXT,
648
+ subject TEXT NOT NULL,
649
+ html_body TEXT NOT NULL,
650
+ text_body TEXT,
651
+ from_email TEXT,
652
+ from_name TEXT,
653
+ reply_to TEXT,
654
+ variables JSONB,
655
+ sample_context JSONB,
656
+ is_active BOOLEAN DEFAULT true,
657
+ category TEXT,
658
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
659
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
660
+ )
661
+ """)
662
+ op.execute('CREATE INDEX idx_email_templates_application_id ON email_templates(application_id)')
663
+ op.execute('CREATE INDEX idx_email_templates_category ON email_templates(category)')
664
+
665
+ # email_log table
666
+ op.execute("""
667
+ CREATE TABLE email_log (
668
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
669
+ application_id UUID REFERENCES applications(id) ON DELETE CASCADE,
670
+ template_id UUID REFERENCES email_templates(id) ON DELETE SET NULL,
671
+ user_id UUID REFERENCES users(id) ON DELETE SET NULL,
672
+ owner_id UUID REFERENCES users(id) ON DELETE SET NULL,
673
+ to_email TEXT NOT NULL,
674
+ template_key TEXT,
675
+ subject TEXT NOT NULL,
676
+ status TEXT NOT NULL,
677
+ mailgun_message_id TEXT,
678
+ error_message TEXT,
679
+ sent_at TIMESTAMPTZ,
680
+ delivered_at TIMESTAMPTZ,
681
+ opened_at TIMESTAMPTZ,
682
+ clicked_at TIMESTAMPTZ,
683
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
684
+ )
685
+ """)
686
+ op.execute('CREATE INDEX idx_email_log_application_id ON email_log(application_id)')
687
+ op.execute('CREATE INDEX idx_email_log_template_id ON email_log(template_id)')
688
+ op.execute('CREATE INDEX idx_email_log_user_id ON email_log(user_id)')
689
+ op.execute('CREATE INDEX idx_email_log_to_email ON email_log(to_email)')
690
+ op.execute('CREATE INDEX idx_email_log_status ON email_log(status)')
691
+
692
+ # email_whitelist table
693
+ op.execute("""
694
+ CREATE TABLE email_whitelist (
695
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
696
+ application_id UUID REFERENCES applications(id) ON DELETE CASCADE,
697
+ email TEXT NOT NULL,
698
+ tier TEXT,
699
+ bypass_payment BOOLEAN DEFAULT false,
700
+ metadata JSONB,
701
+ created_by TEXT,
702
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
703
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
704
+ )
705
+ """)
706
+ op.execute('CREATE INDEX idx_email_whitelist_application_id ON email_whitelist(application_id)')
707
+ op.execute('CREATE INDEX idx_email_whitelist_email ON email_whitelist(email)')
708
+
709
+ # ===== CFO / QUICKBOOKS =====
710
+
711
+ # cfo_connections table
712
+ op.execute("""
713
+ CREATE TABLE cfo_connections (
714
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
715
+ user_id UUID REFERENCES users(id) ON DELETE CASCADE,
716
+ client_name TEXT NOT NULL,
717
+ realm_id TEXT NOT NULL,
718
+ company_name TEXT NOT NULL,
719
+ encrypted_refresh_token TEXT NOT NULL,
720
+ token_expires_at TIMESTAMPTZ NOT NULL,
721
+ accounting_method TEXT,
722
+ risk_tier TEXT,
723
+ status TEXT NOT NULL,
724
+ connected_at TIMESTAMPTZ,
725
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
726
+ )
727
+ """)
728
+ op.execute('CREATE INDEX idx_cfo_connections_user_id ON cfo_connections(user_id)')
729
+ op.execute('CREATE INDEX idx_cfo_connections_realm_id ON cfo_connections(realm_id)')
730
+
731
+ # cfo_auth_codes table
732
+ op.execute("""
733
+ CREATE TABLE cfo_auth_codes (
734
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
735
+ code TEXT UNIQUE NOT NULL,
736
+ user_id UUID REFERENCES users(id) ON DELETE CASCADE,
737
+ company_name TEXT NOT NULL,
738
+ realm_id TEXT NOT NULL,
739
+ encrypted_auth_data TEXT NOT NULL,
740
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
741
+ expires_at TIMESTAMPTZ NOT NULL,
742
+ used_at TIMESTAMPTZ
743
+ )
744
+ """)
745
+ op.execute('CREATE INDEX idx_cfo_auth_codes_user_id ON cfo_auth_codes(user_id)')
746
+ op.execute('CREATE INDEX idx_cfo_auth_codes_expires_at ON cfo_auth_codes(expires_at)')
747
+
748
+ # cfo_magic_links table
749
+ op.execute("""
750
+ CREATE TABLE cfo_magic_links (
751
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
752
+ token TEXT UNIQUE NOT NULL,
753
+ client_name TEXT NOT NULL,
754
+ connection_id UUID REFERENCES cfo_connections(id) ON DELETE CASCADE,
755
+ expires_at TIMESTAMPTZ NOT NULL,
756
+ used_at TIMESTAMPTZ,
757
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
758
+ )
759
+ """)
760
+ op.execute('CREATE INDEX idx_cfo_magic_links_connection_id ON cfo_magic_links(connection_id)')
761
+ op.execute('CREATE INDEX idx_cfo_magic_links_expires_at ON cfo_magic_links(expires_at)')
762
+
763
+ # ===== DAYRATE =====
764
+
765
+ # dayrate_config table
766
+ op.execute("""
767
+ CREATE TABLE dayrate_config (
768
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
769
+ application_id UUID REFERENCES applications(id) ON DELETE CASCADE,
770
+ base_rate INTEGER NOT NULL,
771
+ available_days TEXT[],
772
+ slots TEXT[],
773
+ fill_tiers JSONB,
774
+ time_tiers JSONB,
775
+ horizon_weeks INTEGER,
776
+ meeting_link TEXT,
777
+ min_notice_hours INTEGER,
778
+ timezone TEXT,
779
+ slot_definitions JSONB,
780
+ currency TEXT,
781
+ product_name TEXT,
782
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
783
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
784
+ )
785
+ """)
786
+ op.execute('CREATE INDEX idx_dayrate_config_application_id ON dayrate_config(application_id)')
787
+
788
+ # dayrate_bookings table
789
+ op.execute("""
790
+ CREATE TABLE dayrate_bookings (
791
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
792
+ application_id UUID REFERENCES applications(id) ON DELETE CASCADE,
793
+ user_id UUID REFERENCES users(id) ON DELETE SET NULL,
794
+ date DATE NOT NULL,
795
+ slot_type TEXT NOT NULL,
796
+ client_email TEXT NOT NULL,
797
+ client_name TEXT,
798
+ price_paid INTEGER NOT NULL,
799
+ stripe_session_id TEXT,
800
+ stripe_payment_intent_id TEXT,
801
+ status TEXT NOT NULL,
802
+ metadata JSONB,
803
+ booking_group_id TEXT,
804
+ expires_at TIMESTAMPTZ,
805
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
806
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
807
+ )
808
+ """)
809
+ op.execute('CREATE INDEX idx_dayrate_bookings_application_id ON dayrate_bookings(application_id)')
810
+ op.execute('CREATE INDEX idx_dayrate_bookings_user_id ON dayrate_bookings(user_id)')
811
+ op.execute('CREATE INDEX idx_dayrate_bookings_date ON dayrate_bookings(date)')
812
+ op.execute('CREATE INDEX idx_dayrate_bookings_status ON dayrate_bookings(status)')
813
+
814
+ # ===== SECURE MESSAGES =====
815
+
816
+ # secure_messages table
817
+ op.execute("""
818
+ CREATE TABLE secure_messages (
819
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
820
+ application_id UUID REFERENCES applications(id) ON DELETE CASCADE,
821
+ patient_id UUID REFERENCES users(id) ON DELETE CASCADE,
822
+ practitioner_id UUID REFERENCES users(id) ON DELETE CASCADE,
823
+ encrypted_content TEXT NOT NULL,
824
+ metadata JSONB,
825
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
826
+ )
827
+ """)
828
+ op.execute('CREATE INDEX idx_secure_messages_application_id ON secure_messages(application_id)')
829
+ op.execute('CREATE INDEX idx_secure_messages_patient_id ON secure_messages(patient_id)')
830
+ op.execute('CREATE INDEX idx_secure_messages_practitioner_id ON secure_messages(practitioner_id)')
831
+
832
+ # ===== DOCUMENTATION =====
833
+
834
+ # documentation_categories table
835
+ op.execute("""
836
+ CREATE TABLE documentation_categories (
837
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
838
+ name TEXT NOT NULL,
839
+ slug TEXT UNIQUE NOT NULL,
840
+ description TEXT,
841
+ order_index INTEGER,
842
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
843
+ )
844
+ """)
845
+
846
+ # documentation table
847
+ op.execute("""
848
+ CREATE TABLE documentation (
849
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
850
+ slug TEXT UNIQUE NOT NULL,
851
+ title TEXT NOT NULL,
852
+ content TEXT NOT NULL,
853
+ category_id UUID REFERENCES documentation_categories(id) ON DELETE SET NULL,
854
+ category TEXT,
855
+ tags TEXT[],
856
+ metadata JSONB,
857
+ related_endpoints JSONB,
858
+ last_updated TIMESTAMPTZ NOT NULL DEFAULT NOW(),
859
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
860
+ version INTEGER DEFAULT 1,
861
+ is_published BOOLEAN DEFAULT true
862
+ )
863
+ """)
864
+ op.execute('CREATE INDEX idx_documentation_category_id ON documentation(category_id)')
865
+ op.execute('CREATE INDEX idx_documentation_is_published ON documentation(is_published)')
866
+
867
+ # doc_embeddings table
868
+ op.execute("""
869
+ CREATE TABLE doc_embeddings (
870
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
871
+ doc_id UUID REFERENCES documentation(id) ON DELETE CASCADE,
872
+ embedding vector,
873
+ chunk_index INTEGER NOT NULL,
874
+ chunk_text TEXT NOT NULL,
875
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
876
+ )
877
+ """)
878
+ op.execute('CREATE INDEX idx_doc_embeddings_doc_id ON doc_embeddings(doc_id)')
879
+
880
+ # error_documentation table
881
+ op.execute("""
882
+ CREATE TABLE error_documentation (
883
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
884
+ code TEXT UNIQUE NOT NULL,
885
+ http_status INTEGER NOT NULL,
886
+ message TEXT NOT NULL,
887
+ description TEXT,
888
+ possible_causes TEXT[],
889
+ solutions TEXT[],
890
+ related_errors TEXT[],
891
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
892
+ )
893
+ """)
894
+
895
+ # endpoint_documentation table
896
+ op.execute("""
897
+ CREATE TABLE endpoint_documentation (
898
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
899
+ method TEXT NOT NULL,
900
+ path TEXT NOT NULL,
901
+ description TEXT,
902
+ authentication TEXT,
903
+ request_body JSONB,
904
+ response_body JSONB,
905
+ error_codes JSONB[],
906
+ rate_limit TEXT,
907
+ deprecated BOOLEAN DEFAULT false,
908
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
909
+ )
910
+ """)
911
+ op.execute('CREATE INDEX idx_endpoint_documentation_method_path ON endpoint_documentation(method, path)')
912
+
913
+ # code_examples table
914
+ op.execute("""
915
+ CREATE TABLE code_examples (
916
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
917
+ title TEXT NOT NULL,
918
+ description TEXT,
919
+ language TEXT NOT NULL,
920
+ code TEXT NOT NULL,
921
+ dependencies JSONB,
922
+ output TEXT,
923
+ doc_id UUID REFERENCES documentation(id) ON DELETE CASCADE,
924
+ endpoint TEXT,
925
+ error_code TEXT,
926
+ feature TEXT,
927
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
928
+ )
929
+ """)
930
+ op.execute('CREATE INDEX idx_code_examples_doc_id ON code_examples(doc_id)')
931
+ op.execute('CREATE INDEX idx_code_examples_language ON code_examples(language)')
932
+
933
+ # ===== AUDIT & SECURITY =====
934
+
935
+ # audit_logs table
936
+ op.execute("""
937
+ CREATE TABLE audit_logs (
938
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
939
+ admin_user_id UUID REFERENCES users(id) ON DELETE SET NULL,
940
+ admin_email TEXT,
941
+ action TEXT NOT NULL,
942
+ resource_type TEXT,
943
+ resource_id TEXT,
944
+ resource_data JSONB,
945
+ error_details JSONB,
946
+ ip_address TEXT,
947
+ user_agent TEXT,
948
+ application_id UUID REFERENCES applications(id) ON DELETE SET NULL,
949
+ severity TEXT,
950
+ timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW()
951
+ )
952
+ """)
953
+ op.execute('CREATE INDEX idx_audit_logs_admin_user_id ON audit_logs(admin_user_id)')
954
+ op.execute('CREATE INDEX idx_audit_logs_application_id ON audit_logs(application_id)')
955
+ op.execute('CREATE INDEX idx_audit_logs_action ON audit_logs(action)')
956
+ op.execute('CREATE INDEX idx_audit_logs_timestamp ON audit_logs(timestamp)')
957
+
958
+ # audit_logs_archive table
959
+ op.execute("""
960
+ CREATE TABLE audit_logs_archive (
961
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
962
+ admin_user_id UUID REFERENCES users(id) ON DELETE SET NULL,
963
+ admin_email TEXT,
964
+ action TEXT NOT NULL,
965
+ resource_type TEXT,
966
+ resource_id TEXT,
967
+ resource_data JSONB,
968
+ error_details JSONB,
969
+ ip_address TEXT,
970
+ user_agent TEXT,
971
+ application_id UUID REFERENCES applications(id) ON DELETE SET NULL,
972
+ severity TEXT,
973
+ timestamp TIMESTAMPTZ NOT NULL,
974
+ archived_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
975
+ )
976
+ """)
977
+ op.execute('CREATE INDEX idx_audit_logs_archive_admin_user_id ON audit_logs_archive(admin_user_id)')
978
+ op.execute('CREATE INDEX idx_audit_logs_archive_timestamp ON audit_logs_archive(timestamp)')
979
+
980
+ # security_alerts table
981
+ op.execute("""
982
+ CREATE TABLE security_alerts (
983
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
984
+ alert_type TEXT NOT NULL,
985
+ user_id UUID REFERENCES users(id) ON DELETE SET NULL,
986
+ elevated_by TEXT,
987
+ severity TEXT NOT NULL,
988
+ details JSONB,
989
+ timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW(),
990
+ ip_address TEXT,
991
+ user_agent TEXT
992
+ )
993
+ """)
994
+ op.execute('CREATE INDEX idx_security_alerts_user_id ON security_alerts(user_id)')
995
+ op.execute('CREATE INDEX idx_security_alerts_alert_type ON security_alerts(alert_type)')
996
+ op.execute('CREATE INDEX idx_security_alerts_severity ON security_alerts(severity)')
997
+ op.execute('CREATE INDEX idx_security_alerts_timestamp ON security_alerts(timestamp)')
998
+
999
+ # ===== USAGE =====
1000
+
1001
+ # usage_records table
1002
+ op.execute("""
1003
+ CREATE TABLE usage_records (
1004
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
1005
+ user_id UUID REFERENCES users(id) ON DELETE CASCADE,
1006
+ application_id UUID REFERENCES applications(id) ON DELETE CASCADE,
1007
+ feature TEXT NOT NULL,
1008
+ quantity INTEGER NOT NULL,
1009
+ metadata JSONB,
1010
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
1011
+ )
1012
+ """)
1013
+ op.execute('CREATE INDEX idx_usage_records_user_id ON usage_records(user_id)')
1014
+ op.execute('CREATE INDEX idx_usage_records_application_id ON usage_records(application_id)')
1015
+ op.execute('CREATE INDEX idx_usage_records_feature ON usage_records(feature)')
1016
+ op.execute('CREATE INDEX idx_usage_records_created_at ON usage_records(created_at)')
1017
+
1018
+
1019
+ def downgrade() -> None:
1020
+ # Drop tables in reverse dependency order
1021
+ op.execute('DROP TABLE IF EXISTS usage_records CASCADE')
1022
+ op.execute('DROP TABLE IF EXISTS security_alerts CASCADE')
1023
+ op.execute('DROP TABLE IF EXISTS audit_logs_archive CASCADE')
1024
+ op.execute('DROP TABLE IF EXISTS audit_logs CASCADE')
1025
+ op.execute('DROP TABLE IF EXISTS code_examples CASCADE')
1026
+ op.execute('DROP TABLE IF EXISTS endpoint_documentation CASCADE')
1027
+ op.execute('DROP TABLE IF EXISTS error_documentation CASCADE')
1028
+ op.execute('DROP TABLE IF EXISTS doc_embeddings CASCADE')
1029
+ op.execute('DROP TABLE IF EXISTS documentation CASCADE')
1030
+ op.execute('DROP TABLE IF EXISTS documentation_categories CASCADE')
1031
+ op.execute('DROP TABLE IF EXISTS secure_messages CASCADE')
1032
+ op.execute('DROP TABLE IF EXISTS dayrate_bookings CASCADE')
1033
+ op.execute('DROP TABLE IF EXISTS dayrate_config CASCADE')
1034
+ op.execute('DROP TABLE IF EXISTS cfo_magic_links CASCADE')
1035
+ op.execute('DROP TABLE IF EXISTS cfo_auth_codes CASCADE')
1036
+ op.execute('DROP TABLE IF EXISTS cfo_connections CASCADE')
1037
+ op.execute('DROP TABLE IF EXISTS email_whitelist CASCADE')
1038
+ op.execute('DROP TABLE IF EXISTS email_log CASCADE')
1039
+ op.execute('DROP TABLE IF EXISTS email_templates CASCADE')
1040
+ op.execute('DROP TABLE IF EXISTS webhook_deliveries CASCADE')
1041
+ op.execute('DROP TABLE IF EXISTS webhook_events CASCADE')
1042
+ op.execute('DROP TABLE IF EXISTS webhooks CASCADE')
1043
+ op.execute('DROP TABLE IF EXISTS entitlement_projection_log CASCADE')
1044
+ op.execute('DROP TABLE IF EXISTS entitlement_events CASCADE')
1045
+ op.execute('DROP TABLE IF EXISTS entitlement_mappings CASCADE')
1046
+ op.execute('DROP TABLE IF EXISTS entitlements CASCADE')
1047
+ op.execute('DROP TABLE IF EXISTS chain_sync_state CASCADE')
1048
+ op.execute('DROP TABLE IF EXISTS app_receiving_addresses CASCADE')
1049
+ op.execute('DROP TABLE IF EXISTS crypto_webhook_dedupes CASCADE')
1050
+ op.execute('DROP TABLE IF EXISTS ledger_transactions CASCADE')
1051
+ op.execute('DROP TABLE IF EXISTS crypto_invoices CASCADE')
1052
+ op.execute('DROP TABLE IF EXISTS guest_checkout_conversions CASCADE')
1053
+ op.execute('DROP TABLE IF EXISTS wallet_payments CASCADE')
1054
+ op.execute('DROP TABLE IF EXISTS payment_history CASCADE')
1055
+ op.execute('DROP TABLE IF EXISTS stripe_invoices CASCADE')
1056
+ op.execute('DROP TABLE IF EXISTS subscriptions CASCADE')
1057
+ op.execute('DROP TABLE IF EXISTS stripe_webhook_events CASCADE')
1058
+ op.execute('DROP TABLE IF EXISTS stripe_payments CASCADE')
1059
+ op.execute('DROP TABLE IF EXISTS checkout_sessions CASCADE')
1060
+ op.execute('DROP TABLE IF EXISTS stripe_prices CASCADE')
1061
+ op.execute('DROP TABLE IF EXISTS stripe_products CASCADE')
1062
+ op.execute('DROP TABLE IF EXISTS stripe_customers CASCADE')
1063
+ op.execute('DROP TABLE IF EXISTS blacklisted_tokens CASCADE')
1064
+ op.execute('DROP TABLE IF EXISTS refresh_tokens CASCADE')
1065
+ op.execute('DROP TABLE IF EXISTS auth_magic_link_states CASCADE')
1066
+ op.execute('DROP TABLE IF EXISTS auth_tokens CASCADE')
1067
+ op.execute('DROP TABLE IF EXISTS auth_nonces CASCADE')
1068
+ op.execute('DROP TABLE IF EXISTS user_applications CASCADE')
1069
+ op.execute('DROP TABLE IF EXISTS user_wallets CASCADE')
1070
+ op.execute('DROP TABLE IF EXISTS applications CASCADE')
1071
+ op.execute('DROP TABLE IF EXISTS users CASCADE')
1072
+
1073
+ # Drop extensions
1074
+ op.execute('DROP EXTENSION IF EXISTS "vector"')
1075
+ op.execute('DROP EXTENSION IF EXISTS "pgcrypto"')
1076
+ op.execute('DROP EXTENSION IF EXISTS "uuid-ossp"')