@nextsparkjs/theme-blog 0.1.0-beta.1

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 (64) hide show
  1. package/README.md +65 -0
  2. package/about.md +93 -0
  3. package/api/authors/[username]/route.ts +150 -0
  4. package/api/authors/route.ts +63 -0
  5. package/api/posts/public/route.ts +151 -0
  6. package/components/ExportPostsButton.tsx +102 -0
  7. package/components/ImportPostsDialog.tsx +284 -0
  8. package/components/PostsToolbar.tsx +24 -0
  9. package/components/editor/FeaturedImageUpload.tsx +185 -0
  10. package/components/editor/WysiwygEditor.tsx +340 -0
  11. package/components/index.ts +4 -0
  12. package/components/public/AuthorBio.tsx +105 -0
  13. package/components/public/AuthorCard.tsx +130 -0
  14. package/components/public/BlogFooter.tsx +185 -0
  15. package/components/public/BlogNavbar.tsx +201 -0
  16. package/components/public/PostCard.tsx +306 -0
  17. package/components/public/ReadingProgress.tsx +70 -0
  18. package/components/public/RelatedPosts.tsx +78 -0
  19. package/config/app.config.ts +200 -0
  20. package/config/billing.config.ts +146 -0
  21. package/config/dashboard.config.ts +333 -0
  22. package/config/dev.config.ts +48 -0
  23. package/config/features.config.ts +196 -0
  24. package/config/flows.config.ts +333 -0
  25. package/config/permissions.config.ts +101 -0
  26. package/config/theme.config.ts +128 -0
  27. package/entities/categories/categories.config.ts +60 -0
  28. package/entities/categories/categories.fields.ts +115 -0
  29. package/entities/categories/categories.service.ts +333 -0
  30. package/entities/categories/categories.types.ts +58 -0
  31. package/entities/categories/messages/en.json +33 -0
  32. package/entities/categories/messages/es.json +33 -0
  33. package/entities/posts/messages/en.json +100 -0
  34. package/entities/posts/messages/es.json +100 -0
  35. package/entities/posts/migrations/001_posts_table.sql +110 -0
  36. package/entities/posts/migrations/002_add_featured.sql +19 -0
  37. package/entities/posts/migrations/003_post_categories_pivot.sql +47 -0
  38. package/entities/posts/posts.config.ts +61 -0
  39. package/entities/posts/posts.fields.ts +234 -0
  40. package/entities/posts/posts.service.ts +464 -0
  41. package/entities/posts/posts.types.ts +80 -0
  42. package/lib/selectors.ts +179 -0
  43. package/messages/en.json +113 -0
  44. package/messages/es.json +113 -0
  45. package/migrations/002_author_profile_fields.sql +37 -0
  46. package/migrations/003_categories_table.sql +90 -0
  47. package/migrations/999_sample_data.sql +412 -0
  48. package/migrations/999_theme_sample_data.sql +1070 -0
  49. package/package.json +18 -0
  50. package/permissions-matrix.md +63 -0
  51. package/styles/article.css +333 -0
  52. package/styles/components.css +204 -0
  53. package/styles/globals.css +327 -0
  54. package/styles/theme.css +167 -0
  55. package/templates/(public)/author/[username]/page.tsx +247 -0
  56. package/templates/(public)/authors/page.tsx +161 -0
  57. package/templates/(public)/layout.tsx +44 -0
  58. package/templates/(public)/page.tsx +276 -0
  59. package/templates/(public)/posts/[slug]/page.tsx +342 -0
  60. package/templates/dashboard/(main)/page.tsx +385 -0
  61. package/templates/dashboard/(main)/posts/[id]/edit/page.tsx +529 -0
  62. package/templates/dashboard/(main)/posts/[id]/page.tsx +33 -0
  63. package/templates/dashboard/(main)/posts/create/page.tsx +353 -0
  64. package/templates/dashboard/(main)/posts/page.tsx +833 -0
@@ -0,0 +1,1070 @@
1
+ -- ============================================================================
2
+ -- Blog Theme - Multi-Author Sample Data Migration
3
+ -- Scenario: Medium-style multi-author blog platform
4
+ -- Teams Mode: single-user (each author has isolated team, public feed aggregates all)
5
+ -- ============================================================================
6
+
7
+ -- ============================================
8
+ -- STEP 0: CLEANUP (preserves superadmin)
9
+ -- ============================================
10
+ DO $$
11
+ DECLARE
12
+ v_superadmin_id TEXT;
13
+ BEGIN
14
+ -- Get superadmin ID to preserve
15
+ SELECT id INTO v_superadmin_id FROM "users" WHERE role = 'superadmin' LIMIT 1;
16
+
17
+ -- Clean theme entities (if tables exist)
18
+ IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'posts') THEN
19
+ TRUNCATE "posts" CASCADE;
20
+ END IF;
21
+
22
+ IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'categories') THEN
23
+ TRUNCATE "categories" CASCADE;
24
+ END IF;
25
+
26
+ IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'invoices') THEN
27
+ TRUNCATE "invoices" CASCADE;
28
+ END IF;
29
+
30
+ -- Clean team-related data (except superadmin's)
31
+ IF v_superadmin_id IS NOT NULL THEN
32
+ DELETE FROM "api_audit_log" WHERE "userId" != v_superadmin_id;
33
+ DELETE FROM "api_key" WHERE "userId" != v_superadmin_id;
34
+ DELETE FROM "team_members" WHERE "userId" != v_superadmin_id;
35
+ DELETE FROM "teams" WHERE "ownerId" != v_superadmin_id;
36
+ DELETE FROM "users_metas" WHERE "userId" != v_superadmin_id;
37
+ DELETE FROM "session" WHERE "userId" != v_superadmin_id;
38
+ DELETE FROM "account" WHERE "userId" != v_superadmin_id;
39
+ DELETE FROM "users" WHERE id != v_superadmin_id;
40
+ ELSE
41
+ -- No superadmin, clean everything
42
+ DELETE FROM "api_audit_log";
43
+ DELETE FROM "api_key";
44
+ DELETE FROM "team_members";
45
+ DELETE FROM "teams";
46
+ DELETE FROM "users_metas";
47
+ DELETE FROM "session";
48
+ DELETE FROM "account";
49
+ DELETE FROM "users";
50
+ END IF;
51
+
52
+ RAISE NOTICE 'Blog theme cleanup complete. Superadmin preserved.';
53
+ END $$;
54
+
55
+ -- ============================================
56
+ -- STEP 1: CREATE 3 BLOG AUTHORS
57
+ -- ============================================
58
+ -- Multi-author Medium-style platform with 3 content creators
59
+
60
+ INSERT INTO "users" (
61
+ id,
62
+ email,
63
+ name,
64
+ "firstName",
65
+ "lastName",
66
+ username,
67
+ bio,
68
+ social_twitter,
69
+ social_linkedin,
70
+ social_website,
71
+ role,
72
+ "emailVerified",
73
+ language,
74
+ country,
75
+ timezone,
76
+ "createdAt",
77
+ "updatedAt"
78
+ ) VALUES
79
+ -- Marcos Tech - Technology & Startups niche
80
+ (
81
+ 'usr-blog-marcos',
82
+ 'blog_author_marcos@nextspark.dev',
83
+ 'Marcos Tech',
84
+ 'Marcos',
85
+ 'Tech',
86
+ 'marcos_tech',
87
+ 'Tech entrepreneur and developer. Building the future one line of code at a time. Writing about AI, SaaS, and startup life.',
88
+ 'https://twitter.com/marcostech',
89
+ 'https://linkedin.com/in/marcostech',
90
+ 'https://marcostech.dev',
91
+ 'member',
92
+ true,
93
+ 'en',
94
+ 'US',
95
+ 'America/Los_Angeles',
96
+ NOW() - INTERVAL '6 months',
97
+ NOW()
98
+ ),
99
+ -- Lucia Lifestyle - Travel & Lifestyle niche
100
+ (
101
+ 'usr-blog-lucia',
102
+ 'blog_author_lucia@nextspark.dev',
103
+ 'Lucia Lifestyle',
104
+ 'Lucia',
105
+ 'Lifestyle',
106
+ 'lucia_lifestyle',
107
+ 'Digital nomad exploring the world. Sharing travel tips, remote work advice, and lifestyle adventures from 30+ countries.',
108
+ 'https://twitter.com/lucialifestyle',
109
+ 'https://linkedin.com/in/lucialifestyle',
110
+ 'https://lucialifestyle.com',
111
+ 'member',
112
+ true,
113
+ 'es',
114
+ 'ES',
115
+ 'Europe/Madrid',
116
+ NOW() - INTERVAL '5 months',
117
+ NOW()
118
+ ),
119
+ -- Carlos Finance - Business & Finance niche
120
+ (
121
+ 'usr-blog-carlos',
122
+ 'blog_author_carlos@nextspark.dev',
123
+ 'Carlos Finance',
124
+ 'Carlos',
125
+ 'Finance',
126
+ 'carlos_finance',
127
+ 'Financial advisor helping everyday people build wealth. Making personal finance simple and accessible for everyone.',
128
+ 'https://twitter.com/carlosfinance',
129
+ 'https://linkedin.com/in/carlosfinance',
130
+ NULL,
131
+ 'member',
132
+ true,
133
+ 'en',
134
+ 'US',
135
+ 'America/New_York',
136
+ NOW() - INTERVAL '4 months',
137
+ NOW()
138
+ )
139
+ ON CONFLICT (email) DO NOTHING;
140
+
141
+ -- ============================================
142
+ -- STEP 2: CREATE 3 AUTHOR TEAMS (BLOGS)
143
+ -- ============================================
144
+ -- Each author has their own isolated blog
145
+
146
+ INSERT INTO "teams" (
147
+ id,
148
+ name,
149
+ slug,
150
+ description,
151
+ "ownerId",
152
+ "createdAt",
153
+ "updatedAt"
154
+ ) VALUES
155
+ (
156
+ 'team-blog-marcos',
157
+ 'Marcos Tech Blog',
158
+ 'marcos-tech',
159
+ 'Technology insights and startup wisdom',
160
+ 'usr-blog-marcos',
161
+ NOW() - INTERVAL '6 months',
162
+ NOW()
163
+ ),
164
+ (
165
+ 'team-blog-lucia',
166
+ 'Lucia Lifestyle Blog',
167
+ 'lucia-lifestyle',
168
+ 'Travel adventures and lifestyle tips',
169
+ 'usr-blog-lucia',
170
+ NOW() - INTERVAL '5 months',
171
+ NOW()
172
+ ),
173
+ (
174
+ 'team-blog-carlos',
175
+ 'Carlos Finance Blog',
176
+ 'carlos-finance',
177
+ 'Personal finance and investment strategies',
178
+ 'usr-blog-carlos',
179
+ NOW() - INTERVAL '4 months',
180
+ NOW()
181
+ )
182
+ ON CONFLICT (id) DO NOTHING;
183
+
184
+ -- ============================================
185
+ -- STEP 3: CREATE TEAM MEMBERSHIPS
186
+ -- ============================================
187
+
188
+ INSERT INTO "team_members" (
189
+ id,
190
+ "teamId",
191
+ "userId",
192
+ role,
193
+ "joinedAt"
194
+ ) VALUES
195
+ ('tm-blog-marcos', 'team-blog-marcos', 'usr-blog-marcos', 'owner', NOW() - INTERVAL '6 months'),
196
+ ('tm-blog-lucia', 'team-blog-lucia', 'usr-blog-lucia', 'owner', NOW() - INTERVAL '5 months'),
197
+ ('tm-blog-carlos', 'team-blog-carlos', 'usr-blog-carlos', 'owner', NOW() - INTERVAL '4 months')
198
+ ON CONFLICT (id) DO NOTHING;
199
+
200
+ -- ============================================
201
+ -- STEP 4: CREATE ACCOUNTS (Password: Test1234)
202
+ -- ============================================
203
+ -- Hash: 3db9e98e2b4d3caca97fdf2783791cbc:34b293de615caf277a237773208858e960ea8aa10f1f5c5c309b632f192cac34d52ceafbd338385616f4929e4b1b6c055b67429c6722ffdb80b01d9bf4764866
204
+
205
+ INSERT INTO "account" (
206
+ id,
207
+ "userId",
208
+ "accountId",
209
+ "providerId",
210
+ "accessToken",
211
+ "refreshToken",
212
+ "idToken",
213
+ "accessTokenExpiresAt",
214
+ "refreshTokenExpiresAt",
215
+ "scope",
216
+ "password",
217
+ "createdAt",
218
+ "updatedAt"
219
+ ) VALUES
220
+ (
221
+ 'acc-blog-marcos',
222
+ 'usr-blog-marcos',
223
+ 'blog_author_marcos@nextspark.dev',
224
+ 'credential',
225
+ NULL, NULL, NULL, NULL, NULL, NULL,
226
+ '3db9e98e2b4d3caca97fdf2783791cbc:34b293de615caf277a237773208858e960ea8aa10f1f5c5c309b632f192cac34d52ceafbd338385616f4929e4b1b6c055b67429c6722ffdb80b01d9bf4764866',
227
+ NOW() - INTERVAL '6 months',
228
+ NOW()
229
+ ),
230
+ (
231
+ 'acc-blog-lucia',
232
+ 'usr-blog-lucia',
233
+ 'blog_author_lucia@nextspark.dev',
234
+ 'credential',
235
+ NULL, NULL, NULL, NULL, NULL, NULL,
236
+ '3db9e98e2b4d3caca97fdf2783791cbc:34b293de615caf277a237773208858e960ea8aa10f1f5c5c309b632f192cac34d52ceafbd338385616f4929e4b1b6c055b67429c6722ffdb80b01d9bf4764866',
237
+ NOW() - INTERVAL '5 months',
238
+ NOW()
239
+ ),
240
+ (
241
+ 'acc-blog-carlos',
242
+ 'usr-blog-carlos',
243
+ 'blog_author_carlos@nextspark.dev',
244
+ 'credential',
245
+ NULL, NULL, NULL, NULL, NULL, NULL,
246
+ '3db9e98e2b4d3caca97fdf2783791cbc:34b293de615caf277a237773208858e960ea8aa10f1f5c5c309b632f192cac34d52ceafbd338385616f4929e4b1b6c055b67429c6722ffdb80b01d9bf4764866',
247
+ NOW() - INTERVAL '4 months',
248
+ NOW()
249
+ )
250
+ ON CONFLICT ("providerId", "accountId") DO NOTHING;
251
+
252
+ -- ============================================
253
+ -- STEP 5: CREATE USER METADATA
254
+ -- ============================================
255
+
256
+ INSERT INTO "users_metas" (
257
+ "userId",
258
+ "metaKey",
259
+ "metaValue",
260
+ "dataType",
261
+ "isPublic",
262
+ "isSearchable",
263
+ "createdAt",
264
+ "updatedAt"
265
+ ) VALUES
266
+ -- Marcos metadata
267
+ ('usr-blog-marcos', 'activeTeamId', '"team-blog-marcos"', 'json', false, false, NOW(), NOW()),
268
+ ('usr-blog-marcos', 'uiPreferences', '{"theme": "dark", "sidebarCollapsed": false}', 'json', false, false, NOW(), NOW()),
269
+ -- Lucia metadata
270
+ ('usr-blog-lucia', 'activeTeamId', '"team-blog-lucia"', 'json', false, false, NOW(), NOW()),
271
+ ('usr-blog-lucia', 'uiPreferences', '{"theme": "light", "sidebarCollapsed": false}', 'json', false, false, NOW(), NOW()),
272
+ -- Carlos metadata
273
+ ('usr-blog-carlos', 'activeTeamId', '"team-blog-carlos"', 'json', false, false, NOW(), NOW()),
274
+ ('usr-blog-carlos', 'uiPreferences', '{"theme": "light", "sidebarCollapsed": true}', 'json', false, false, NOW(), NOW())
275
+ ON CONFLICT ("userId", "metaKey") DO NOTHING;
276
+
277
+ -- ============================================
278
+ -- STEP 6: CREATE SAMPLE CATEGORIES
279
+ -- ============================================
280
+ -- Each author has 3 categories for their niche
281
+
282
+ INSERT INTO "categories" (
283
+ id,
284
+ "teamId",
285
+ "userId",
286
+ name,
287
+ slug,
288
+ description,
289
+ "createdAt",
290
+ "updatedAt"
291
+ ) VALUES
292
+ -- Marcos Tech categories (Technology & Startups)
293
+ ('cat-marcos-tech', 'team-blog-marcos', 'usr-blog-marcos', 'Technology', 'technology', 'Tech trends, programming, and software development', NOW() - INTERVAL '6 months', NOW()),
294
+ ('cat-marcos-startups', 'team-blog-marcos', 'usr-blog-marcos', 'Startups', 'startups', 'Startup advice, MVPs, and entrepreneurship', NOW() - INTERVAL '6 months', NOW()),
295
+ ('cat-marcos-ai', 'team-blog-marcos', 'usr-blog-marcos', 'AI & ML', 'ai-ml', 'Artificial intelligence and machine learning insights', NOW() - INTERVAL '6 months', NOW()),
296
+
297
+ -- Lucia Lifestyle categories (Travel & Lifestyle)
298
+ ('cat-lucia-travel', 'team-blog-lucia', 'usr-blog-lucia', 'Travel', 'travel', 'Travel guides, destinations, and tips', NOW() - INTERVAL '5 months', NOW()),
299
+ ('cat-lucia-lifestyle', 'team-blog-lucia', 'usr-blog-lucia', 'Lifestyle', 'lifestyle', 'Digital nomad lifestyle and work-life balance', NOW() - INTERVAL '5 months', NOW()),
300
+ ('cat-lucia-remote', 'team-blog-lucia', 'usr-blog-lucia', 'Remote Work', 'remote-work', 'Working remotely, cafes, and productivity', NOW() - INTERVAL '5 months', NOW()),
301
+
302
+ -- Carlos Finance categories (Business & Finance)
303
+ ('cat-carlos-investing', 'team-blog-carlos', 'usr-blog-carlos', 'Investing', 'investing', 'Investment strategies and financial markets', NOW() - INTERVAL '4 months', NOW()),
304
+ ('cat-carlos-finance', 'team-blog-carlos', 'usr-blog-carlos', 'Personal Finance', 'personal-finance', 'Budgeting, saving, and money management', NOW() - INTERVAL '4 months', NOW()),
305
+ ('cat-carlos-income', 'team-blog-carlos', 'usr-blog-carlos', 'Side Hustles', 'side-hustles', 'Extra income streams and entrepreneurship', NOW() - INTERVAL '4 months', NOW())
306
+ ON CONFLICT (id) DO NOTHING;
307
+
308
+ -- ============================================
309
+ -- STEP 7: CREATE API KEYS
310
+ -- ============================================
311
+
312
+ INSERT INTO "api_key" (
313
+ id,
314
+ "userId",
315
+ name,
316
+ "keyHash",
317
+ "keyPrefix",
318
+ scopes,
319
+ "expiresAt",
320
+ "createdAt",
321
+ "updatedAt",
322
+ status
323
+ ) VALUES
324
+ (
325
+ 'apikey-blog-marcos',
326
+ 'usr-blog-marcos',
327
+ 'Marcos Blog API Key',
328
+ 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
329
+ 'testkey_marcos01',
330
+ ARRAY['posts:read', 'posts:write', 'posts:delete'],
331
+ NULL,
332
+ NOW(),
333
+ NOW(),
334
+ 'active'
335
+ ),
336
+ (
337
+ 'apikey-blog-lucia',
338
+ 'usr-blog-lucia',
339
+ 'Lucia Blog API Key',
340
+ 'a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2',
341
+ 'testkey_lucia001',
342
+ ARRAY['posts:read', 'posts:write', 'posts:delete'],
343
+ NULL,
344
+ NOW(),
345
+ NOW(),
346
+ 'active'
347
+ ),
348
+ (
349
+ 'apikey-blog-carlos',
350
+ 'usr-blog-carlos',
351
+ 'Carlos Blog API Key',
352
+ 'f0e1d2c3b4a5968778695a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3',
353
+ 'testkey_carlos1',
354
+ ARRAY['posts:read', 'posts:write', 'posts:delete'],
355
+ NULL,
356
+ NOW(),
357
+ NOW(),
358
+ 'active'
359
+ )
360
+ ON CONFLICT (id) DO NOTHING;
361
+
362
+ -- ============================================
363
+ -- STEP 8: CREATE SAMPLE POSTS (13 total)
364
+ -- ============================================
365
+ -- Marcos Tech: 5 posts (4 published, 1 draft)
366
+ -- Lucia Lifestyle: 4 posts (3 published, 1 draft)
367
+ -- Carlos Finance: 4 posts (3 published, 1 draft)
368
+
369
+ INSERT INTO "posts" (
370
+ id,
371
+ title,
372
+ slug,
373
+ excerpt,
374
+ content,
375
+ category,
376
+ tags,
377
+ status,
378
+ "publishedAt",
379
+ "userId",
380
+ "teamId",
381
+ "createdAt",
382
+ "updatedAt"
383
+ ) VALUES
384
+ -- ========================================
385
+ -- MARCOS TECH - Technology & Startups (5 posts)
386
+ -- ========================================
387
+ (
388
+ 'post-marcos-001',
389
+ 'The Future of AI in SaaS',
390
+ 'the-future-of-ai-in-saas',
391
+ 'How artificial intelligence is transforming the software-as-a-service industry and what founders need to know.',
392
+ E'# The Future of AI in SaaS\n\nArtificial intelligence is no longer a buzzword - it''s reshaping how we build and deliver software.\n\n## The Current Landscape\n\nEvery SaaS company is now an AI company, whether they realize it or not. The integration of AI capabilities has become table stakes.\n\n### Key Trends\n\n1. **Intelligent Automation** - From customer support to data analysis\n2. **Predictive Analytics** - Understanding user behavior before it happens\n3. **Natural Language Interfaces** - Conversational UX is becoming standard\n4. **Personalization at Scale** - One-size-fits-all is dead\n\n## What This Means for Founders\n\nIf you''re building a SaaS product in 2024, you need to think about AI from day one.\n\n### Start Here\n\n- Identify repetitive tasks that AI could automate\n- Look for patterns in your data that could drive insights\n- Consider conversational interfaces for complex workflows\n\n## The Risks\n\n- Over-engineering solutions\n- Privacy concerns with user data\n- The "AI washing" trap\n\n## My Prediction\n\nIn 5 years, the distinction between "AI-powered" and regular software will disappear. AI will simply be how software works.\n\nThe question isn''t whether to adopt AI. It''s how fast you can do it well.',
393
+ 'Technology',
394
+ '["ai", "saas", "startups", "machine-learning", "future"]'::jsonb,
395
+ 'published',
396
+ NOW() - INTERVAL '30 days',
397
+ 'usr-blog-marcos',
398
+ 'team-blog-marcos',
399
+ NOW() - INTERVAL '30 days',
400
+ NOW() - INTERVAL '30 days'
401
+ ),
402
+ (
403
+ 'post-marcos-002',
404
+ 'Building MVPs Fast',
405
+ 'building-mvps-fast',
406
+ 'A practical guide to launching your minimum viable product in weeks, not months.',
407
+ E'# Building MVPs Fast\n\nSpeed is a startup''s greatest advantage. Here''s how to ship your MVP in weeks, not months.\n\n## The MVP Mindset\n\nAn MVP isn''t a crappy version of your product. It''s the smallest thing that delivers real value.\n\n## My Framework\n\n### Week 1: Define\n- Identify ONE core problem\n- List the minimum features to solve it\n- Cut that list in half\n- Cut it again\n\n### Week 2: Design\n- Paper sketches first\n- Low-fidelity wireframes\n- Don''t touch high-fidelity until validated\n\n### Week 3-4: Build\n- Use existing tools (no custom auth!)\n- Deploy early, deploy often\n- Get something in front of users ASAP\n\n## Tools I Recommend\n\n- **Auth**: Clerk, Auth0, or Better Auth\n- **Database**: Supabase or PlanetScale\n- **Payments**: Stripe (always)\n- **Hosting**: Vercel or Railway\n\n## Common Mistakes\n\n1. Building features nobody asked for\n2. Perfecting UI before validating value\n3. Not talking to users daily\n4. Building everything from scratch\n\n## The Golden Rule\n\nIf you''re not embarrassed by your first release, you launched too late.\n\nShip it. Learn. Iterate.',
408
+ 'Startups',
409
+ '["mvp", "startups", "development", "product", "speed"]'::jsonb,
410
+ 'published',
411
+ NOW() - INTERVAL '21 days',
412
+ 'usr-blog-marcos',
413
+ 'team-blog-marcos',
414
+ NOW() - INTERVAL '21 days',
415
+ NOW() - INTERVAL '21 days'
416
+ ),
417
+ (
418
+ 'post-marcos-003',
419
+ 'Why TypeScript Won',
420
+ 'why-typescript-won',
421
+ 'The rise of TypeScript and why it became the default choice for modern web development.',
422
+ E'# Why TypeScript Won\n\nFive years ago, TypeScript was "that Microsoft thing." Today, it''s the default for serious JavaScript development.\n\n## The Numbers Don''t Lie\n\n- 78% of JS developers use TypeScript\n- Every major framework supports it natively\n- GitHub''s own platform runs on it\n\n## Why Developers Love It\n\n### 1. Catch Bugs Before They Ship\n```typescript\nfunction greet(name: string) {\n return `Hello, ${name}!`\n}\n\ngreet(42) // Error: Argument of type ''number'' is not assignable\n```\n\n### 2. Better IDE Experience\n- Autocomplete that actually works\n- Refactoring without fear\n- Documentation built into the types\n\n### 3. Self-Documenting Code\n```typescript\ninterface User {\n id: string\n email: string\n role: ''admin'' | ''member''\n createdAt: Date\n}\n```\n\n## The Migration Path\n\n1. Rename `.js` to `.ts`\n2. Add types incrementally\n3. Enable strict mode when ready\n\n## What Changed My Mind\n\nI was a TypeScript skeptic. Then I maintained a large JavaScript codebase.\n\nThe time "lost" to writing types is nothing compared to the time saved debugging runtime errors.\n\n## The Verdict\n\nTypeScript didn''t win by being perfect. It won by solving real problems for real developers.',
423
+ 'Technology',
424
+ '["typescript", "javascript", "programming", "web-development"]'::jsonb,
425
+ 'published',
426
+ NOW() - INTERVAL '14 days',
427
+ 'usr-blog-marcos',
428
+ 'team-blog-marcos',
429
+ NOW() - INTERVAL '14 days',
430
+ NOW() - INTERVAL '14 days'
431
+ ),
432
+ (
433
+ 'post-marcos-004',
434
+ 'Startup Metrics That Matter',
435
+ 'startup-metrics-that-matter',
436
+ 'Stop tracking vanity metrics. Focus on the numbers that actually predict success.',
437
+ E'# Startup Metrics That Matter\n\nNot all metrics are created equal. Here''s what to actually track.\n\n## The Vanity Trap\n\nPage views. App downloads. Social followers.\n\nThese feel good but tell you nothing about your business health.\n\n## Metrics That Matter\n\n### 1. Monthly Recurring Revenue (MRR)\nThe lifeblood of any SaaS. Track it religiously.\n\n### 2. Customer Acquisition Cost (CAC)\nHow much to acquire one customer?\n```\nCAC = Marketing Spend / New Customers\n```\n\n### 3. Lifetime Value (LTV)\nHow much does a customer pay over their lifetime?\n```\nLTV = Average Revenue per User * Average Lifespan\n```\n\n### 4. The Magic Ratio: LTV/CAC\n- < 1: You''re losing money on every customer\n- 1-3: Sustainable but tight\n- > 3: Healthy business (aim here)\n\n### 5. Churn Rate\nThe percentage of customers who leave each month.\n```\nChurn = Lost Customers / Total Customers * 100\n```\n\n## The Dashboard\n\nI check these daily:\n- MRR and growth rate\n- Trial-to-paid conversion\n- Active users (DAU/MAU)\n\nWeekly:\n- CAC and LTV\n- Churn by cohort\n- NPS score\n\n## The One Number\n\nIf you can only track one thing: **Net Revenue Retention**\n\nAre existing customers paying you more or less over time?',
438
+ 'Startups',
439
+ '["metrics", "saas", "business", "analytics", "growth"]'::jsonb,
440
+ 'published',
441
+ NOW() - INTERVAL '7 days',
442
+ 'usr-blog-marcos',
443
+ 'team-blog-marcos',
444
+ NOW() - INTERVAL '7 days',
445
+ NOW() - INTERVAL '7 days'
446
+ ),
447
+ (
448
+ 'post-marcos-005',
449
+ 'Remote Team Culture',
450
+ 'remote-team-culture',
451
+ 'Building strong company culture when your team is distributed across the globe.',
452
+ E'# Remote Team Culture\n\nBuilding culture is hard. Building remote culture is harder. But it''s not impossible.\n\n## The Challenge\n\nNo water cooler conversations. No spontaneous lunches. No reading the room.\n\nHow do you build connection without proximity?\n\n## What Works\n\n### 1. Over-communicate\n- Default to public channels\n- Write things down\n- Share context, not just decisions\n\n### 2. Create Rituals\n- Monday kickoffs\n- Friday wins\n- Virtual coffee chats\n\n### 3. Invest in IRL\n- Quarterly team meetups\n- Annual company retreat\n- Budget for local coworking\n\n## Tools That Help\n\n- **Async video**: Loom for updates\n- **Virtual office**: Gather or similar\n- **Documentation**: Notion or Confluence\n- **Chat**: Slack with threading discipline\n\n## Common Mistakes\n\n1. Too many meetings\n2. Expecting always-on availability\n3. Not trusting people to work\n4. Forgetting timezones exist\n\n## Our Practices\n\n[Draft - adding our specific examples]',
453
+ 'Startups',
454
+ '["remote-work", "culture", "team-building", "management", "startup"]'::jsonb,
455
+ 'draft',
456
+ NULL,
457
+ 'usr-blog-marcos',
458
+ 'team-blog-marcos',
459
+ NOW() - INTERVAL '2 days',
460
+ NOW() - INTERVAL '2 days'
461
+ ),
462
+
463
+ -- ========================================
464
+ -- LUCIA LIFESTYLE - Travel & Lifestyle (4 posts)
465
+ -- ========================================
466
+ (
467
+ 'post-lucia-001',
468
+ 'Best Cafes for Remote Work in Lisbon',
469
+ 'best-cafes-remote-work-lisbon',
470
+ 'A digital nomad''s guide to the best spots to work in Portugal''s vibrant capital.',
471
+ E'# Best Cafes for Remote Work in Lisbon\n\nLisbon has become a digital nomad hotspot. Here are my favorite spots to work.\n\n## What I Look For\n\n- Stable WiFi (minimum 50 Mbps)\n- Power outlets\n- Good coffee (obviously)\n- Not too crowded\n- Laptop-friendly vibes\n\n## My Top Picks\n\n### 1. Fabrica Coffee Roasters\n**Rua das Portas de Santo Antao**\n- WiFi: Excellent (80+ Mbps)\n- Outlets: Plenty\n- Vibe: Industrial, creative\n- Best for: Deep focus work\n- Pro tip: Go early, gets crowded after 11am\n\n### 2. Copenhagen Coffee Lab\n**Rua Nova da Piedade**\n- WiFi: Great (60 Mbps)\n- Outlets: Limited (bring a power bank)\n- Vibe: Minimalist Scandinavian\n- Best for: Client calls (quiet)\n- Pro tip: Their flat white is perfect\n\n### 3. Dear Breakfast\n**Multiple locations**\n- WiFi: Good (50 Mbps)\n- Outlets: Adequate\n- Vibe: Instagram-worthy\n- Best for: Brunch + emails\n- Pro tip: Try the avocado toast (yes, really)\n\n### 4. Village Underground\n**Creative hub in Alcantara**\n- WiFi: Excellent\n- Outlets: Everywhere\n- Vibe: Old buses turned into offices!\n- Best for: Community + coworking\n- Pro tip: Book a day pass for full access\n\n## The Coworking Alternative\n\nSometimes you need a proper desk:\n- **Heden**: Beautiful space, great events\n- **Second Home**: Design-focused, pricey but worth it\n\n## WiFi Backup Plan\n\nAlways have a local SIM with data. I use NOS - 10GB for around 15 euros.\n\nSee you at the next cafe!',
472
+ 'Travel',
473
+ '["lisbon", "digital-nomad", "remote-work", "cafes", "portugal"]'::jsonb,
474
+ 'published',
475
+ NOW() - INTERVAL '25 days',
476
+ 'usr-blog-lucia',
477
+ 'team-blog-lucia',
478
+ NOW() - INTERVAL '25 days',
479
+ NOW() - INTERVAL '25 days'
480
+ ),
481
+ (
482
+ 'post-lucia-002',
483
+ 'Digital Nomad Essentials',
484
+ 'digital-nomad-essentials',
485
+ 'Everything I pack for a location-independent lifestyle, refined over 3 years on the road.',
486
+ E'# Digital Nomad Essentials\n\nAfter 3 years of nomading, I''ve learned what actually matters. Here''s my packing list.\n\n## The Non-Negotiables\n\n### Tech\n- MacBook Air M2 (light, powerful, all-day battery)\n- iPhone with dual SIM capability\n- AirPods Pro (noise canceling for flights)\n- Universal power adapter (I use EPICKA)\n- Portable charger (20,000 mAh minimum)\n- Kindle Paperwhite\n\n### Work Accessories\n- Lightweight laptop stand (Roost)\n- Compact mouse (Logitech MX Anywhere)\n- Collapsible water bottle\n- Blue light glasses\n- Noise-canceling headphones for calls\n\n## Clothing Philosophy\n\n**The capsule wardrobe approach:**\n- 3-4 tops that mix and match\n- 2 bottoms (one jeans, one versatile)\n- 1 light jacket\n- Comfortable walking shoes\n- Flip flops\n\nEverything should:\n- Wash easily\n- Dry quickly\n- Not wrinkle\n- Match everything else\n\n## The Bag\n\n**Osprey Farpoint 40L** - My ride or die\n- Fits carry-on requirements\n- Laptop compartment\n- Comfortable for long walks\n- Locks together for security\n\n## What I Stopped Bringing\n\n- Books (Kindle is life)\n- Separate camera (iPhone is enough)\n- "Just in case" items\n- More than 7 days of clothes\n\n## Pro Tips\n\n1. Ship heavy items to your next destination\n2. Buy toiletries locally\n3. Backup everything to cloud\n4. Get travel insurance (World Nomads)\n\nLess stuff = more freedom.',
487
+ 'Lifestyle',
488
+ '["digital-nomad", "packing", "travel", "minimalism", "remote-work"]'::jsonb,
489
+ 'published',
490
+ NOW() - INTERVAL '18 days',
491
+ 'usr-blog-lucia',
492
+ 'team-blog-lucia',
493
+ NOW() - INTERVAL '18 days',
494
+ NOW() - INTERVAL '18 days'
495
+ ),
496
+ (
497
+ 'post-lucia-003',
498
+ 'Hidden Gems of Southeast Asia',
499
+ 'hidden-gems-southeast-asia',
500
+ 'Beyond Bali: Discovering the less-traveled corners of Southeast Asia.',
501
+ E'# Hidden Gems of Southeast Asia\n\nEveryone goes to Bali. Here are the places I loved even more.\n\n## Vietnam: Phong Nha\n\n**Why it''s special:** World''s largest cave systems\n\n- Skip Ha Long Bay (too touristy)\n- Phong Nha is raw, beautiful, uncrowded\n- Stay at Easy Tiger hostel\n- Do the Paradise Cave tour\n\n**Best for:** Adventure seekers, nature lovers\n\n## Philippines: Siargao\n\n**Why it''s special:** The next Bali (but still unspoiled)\n\n- Perfect for beginners to learn surfing\n- Cloud 9 break for experienced surfers\n- Island hopping to empty beaches\n- Incredible food scene developing\n\n**Best for:** Surfers, chill vibes\n\n## Thailand: Pai\n\n**Why it''s special:** Hippie mountain town\n\n- Skip Chiang Mai''s crowds\n- 3-hour winding drive worth it\n- Hot springs, waterfalls, canyons\n- Best pad thai I''ve ever had\n\n**Best for:** Motorcyclists, nature\n\n## Malaysia: Penang\n\n**Why it''s special:** Food capital of Asia\n\n- Georgetown''s street art\n- Hawker food heaven\n- Mix of cultures\n- Great digital nomad infrastructure\n\n**Best for:** Foodies, culture lovers\n\n## Indonesia: Lombok\n\n**Why it''s special:** Bali 20 years ago\n\n- Same beauty, fraction of tourists\n- Gili Islands nearby\n- Mount Rinjani trekking\n- Authentic village experiences\n\n**Best for:** Beach lovers, trekkers\n\n## Getting Off the Beaten Path\n\n1. Talk to locals, not TripAdvisor\n2. Take buses, not flights\n3. Stay longer in fewer places\n4. Learn a few words in local language\n\nThe real magic is where the tourists aren''t.',
502
+ 'Travel',
503
+ '["southeast-asia", "travel", "hidden-gems", "adventure", "digital-nomad"]'::jsonb,
504
+ 'published',
505
+ NOW() - INTERVAL '10 days',
506
+ 'usr-blog-lucia',
507
+ 'team-blog-lucia',
508
+ NOW() - INTERVAL '10 days',
509
+ NOW() - INTERVAL '10 days'
510
+ ),
511
+ (
512
+ 'post-lucia-004',
513
+ 'Work-Life Balance Tips',
514
+ 'work-life-balance-tips',
515
+ 'How to actually disconnect when your office is everywhere.',
516
+ E'# Work-Life Balance Tips\n\nWhen your office is everywhere, balance gets tricky. Here''s what I''ve learned.\n\n## The Problem\n\nAs a digital nomad, there''s no physical separation between work and life.\n\nThe beach IS your office. The cafe IS your meeting room.\n\n## Setting Boundaries\n\n### Time Boundaries\n- Set fixed work hours (I do 9am-5pm local time)\n- No emails after dinner\n- Weekends are sacred\n\n### Space Boundaries\n- Never work from bed\n- Designate a "work spot" even in small spaces\n- Close the laptop when done\n\n[Draft - more tips coming]\n\n## The Hardest Part\n\nGiving yourself permission to not work.\n\nMore coming soon...',
517
+ 'Lifestyle',
518
+ '["work-life-balance", "remote-work", "mental-health", "productivity", "digital-nomad"]'::jsonb,
519
+ 'draft',
520
+ NULL,
521
+ 'usr-blog-lucia',
522
+ 'team-blog-lucia',
523
+ NOW() - INTERVAL '3 days',
524
+ NOW() - INTERVAL '3 days'
525
+ ),
526
+
527
+ -- ========================================
528
+ -- CARLOS FINANCE - Business & Finance (4 posts)
529
+ -- ========================================
530
+ (
531
+ 'post-carlos-001',
532
+ 'Investing 101 for Beginners',
533
+ 'investing-101-for-beginners',
534
+ 'Everything you need to know to start investing today, explained in plain English.',
535
+ E'# Investing 101 for Beginners\n\nInvesting seems complicated. It doesn''t have to be. Let''s break it down.\n\n## Why Invest?\n\nMoney in a savings account loses value to inflation every year.\n\n$10,000 today = ~$7,500 purchasing power in 10 years (at 3% inflation)\n\nInvesting is how you make your money work for you.\n\n## The Basics\n\n### Stocks\nOwning a tiny piece of a company.\n- Higher risk, higher potential reward\n- Apple, Google, thousands of companies\n\n### Bonds\nLending money to governments or companies.\n- Lower risk, lower returns\n- More stable than stocks\n\n### Index Funds\nA basket of many stocks in one purchase.\n- S&P 500 = 500 largest US companies\n- Instant diversification\n- My recommendation for beginners\n\n## Getting Started\n\n### Step 1: Emergency Fund First\n3-6 months expenses in savings BEFORE investing.\n\n### Step 2: Open a Brokerage Account\n- Fidelity, Vanguard, or Schwab\n- All have no minimums\n- All have index funds\n\n### Step 3: Start Small\n- $50/month is a great start\n- Automate it\n- Increase as you can\n\n## The Power of Time\n\n$200/month starting at 25 = ~$500,000 at 65\n$200/month starting at 35 = ~$230,000 at 65\n\nSame money. Different outcomes. Time matters.\n\n## Golden Rules\n\n1. Don''t invest money you need soon\n2. Don''t try to time the market\n3. Diversify (index funds do this automatically)\n4. Stay the course during downturns\n5. Keep fees low (under 0.2%)\n\n## My Simple Portfolio\n\n- 80% Total US Stock Market Index (VTI)\n- 20% Total International (VXUS)\n\nThat''s it. Boring wins.',
536
+ 'Finance',
537
+ '["investing", "beginners", "personal-finance", "stocks", "index-funds"]'::jsonb,
538
+ 'published',
539
+ NOW() - INTERVAL '28 days',
540
+ 'usr-blog-carlos',
541
+ 'team-blog-carlos',
542
+ NOW() - INTERVAL '28 days',
543
+ NOW() - INTERVAL '28 days'
544
+ ),
545
+ (
546
+ 'post-carlos-002',
547
+ 'Side Hustles That Actually Work',
548
+ 'side-hustles-that-actually-work',
549
+ 'Realistic ways to earn extra income without falling for get-rich-quick schemes.',
550
+ E'# Side Hustles That Actually Work\n\nForget the "make $10k/month passive income" gurus. Here''s what actually works.\n\n## The Reality Check\n\nMost side hustles:\n- Take time to build\n- Require real work\n- Won''t make you rich overnight\n\nBut they CAN:\n- Add $500-2000/month\n- Build valuable skills\n- Potentially become full-time income\n\n## Tier 1: Skill-Based (Highest Pay)\n\n### Freelance Writing\n- Start: $50-100/article\n- Experienced: $200-500/article\n- Platform: Contently, LinkedIn\n\n### Web Development\n- Start: $500-1000/project\n- Experienced: $2000-5000/project\n- Platform: Upwork, word of mouth\n\n### Design\n- Start: $100-300/project\n- Experienced: $500-2000/project\n- Platform: 99designs, Fiverr Pro\n\n## Tier 2: Service-Based (Consistent)\n\n### Virtual Assistant\n- $15-25/hour\n- Admin, email, scheduling\n- Platforms: Belay, Time Etc\n\n### Tutoring\n- $20-50/hour\n- Online or local\n- Platform: Wyzant, Varsity Tutors\n\n### Consulting\n- Bill your expertise hourly\n- LinkedIn is your friend\n- Start with former colleagues\n\n## Tier 3: Digital Products (Scalable)\n\n### Online Courses\n- Create once, sell forever\n- Takes 3-6 months to build\n- Platforms: Teachable, Podia\n\n### Ebooks/Guides\n- Lower effort than courses\n- Price $10-50\n- Sell through Gumroad\n\n## What Doesn''t Work\n\n- MLM (it''s always MLM)\n- Survey sites (pennies/hour)\n- "Passive income" dropshipping\n- Crypto day trading\n\n## My Advice\n\n1. Start with skills you already have\n2. Dedicate consistent hours (5-10/week)\n3. Reinvest earnings\n4. Be patient (3-6 months to see results)',
551
+ 'Finance',
552
+ '["side-hustle", "income", "freelance", "money", "career"]'::jsonb,
553
+ 'published',
554
+ NOW() - INTERVAL '20 days',
555
+ 'usr-blog-carlos',
556
+ 'team-blog-carlos',
557
+ NOW() - INTERVAL '20 days',
558
+ NOW() - INTERVAL '20 days'
559
+ ),
560
+ (
561
+ 'post-carlos-003',
562
+ 'Understanding Crypto Markets',
563
+ 'understanding-crypto-markets',
564
+ 'A balanced look at cryptocurrency: the technology, the hype, and the reality.',
565
+ E'# Understanding Crypto Markets\n\nCrypto is either the future of money or a giant scam, depending on who you ask. Let''s look at it objectively.\n\n## What Is Cryptocurrency?\n\nDigital money that runs on blockchain technology.\n\n**Blockchain**: A shared database that no single entity controls.\n\n## The Big Players\n\n### Bitcoin (BTC)\n- The original\n- "Digital gold" narrative\n- Limited to 21 million coins ever\n- Most institutional acceptance\n\n### Ethereum (ETH)\n- Platform for apps and smart contracts\n- Powers DeFi and NFTs\n- More utility than Bitcoin\n- Higher risk/reward\n\n## The Bull Case\n\n1. Decentralization from governments/banks\n2. Inflation hedge (limited supply)\n3. Global, borderless transactions\n4. New financial infrastructure\n\n## The Bear Case\n\n1. Extreme volatility\n2. Regulatory uncertainty\n3. Environmental concerns (mining)\n4. Used for illegal activity\n5. Many projects are scams\n\n## My Honest Take\n\nCrypto is:\n- NOT a get-rich-quick scheme\n- NOT replacing the dollar soon\n- NOT completely without value\n\nIt IS:\n- Interesting technology\n- A speculative asset\n- High risk, high potential reward\n\n## If You''re Going to Invest\n\n1. Only money you can afford to lose (1-5% of portfolio max)\n2. Stick to BTC and ETH (everything else is gambling)\n3. Use real exchanges (Coinbase, Kraken)\n4. Never invest based on TikTok\n5. Plan to hold 5+ years\n\n## Red Flags\n\n- "Guaranteed returns"\n- Celebrity endorsements\n- New coins promising 1000x\n- "Send me crypto, I''ll send back double"\n\n## The Bottom Line\n\nEducate yourself. Be skeptical. Only invest what you understand.',
566
+ 'Finance',
567
+ '["crypto", "bitcoin", "ethereum", "investing", "blockchain"]'::jsonb,
568
+ 'published',
569
+ NOW() - INTERVAL '12 days',
570
+ 'usr-blog-carlos',
571
+ 'team-blog-carlos',
572
+ NOW() - INTERVAL '12 days',
573
+ NOW() - INTERVAL '12 days'
574
+ ),
575
+ (
576
+ 'post-carlos-004',
577
+ 'Building Passive Income',
578
+ 'building-passive-income',
579
+ 'Real passive income strategies that work, without the Instagram guru BS.',
580
+ E'# Building Passive Income\n\nLet me be clear: TRUE passive income takes years to build. But it''s worth it.\n\n## What Passive Income Actually Means\n\n**Not**: Money for nothing\n**Actually**: Income that doesn''t require active work AFTER initial effort\n\n## The Math of Financial Freedom\n\nPassive income goal = Monthly expenses x 12 / 0.04\n\nExample: $5,000/month expenses\n$5,000 x 12 = $60,000/year\n$60,000 / 0.04 = $1,500,000 invested\n\nThat''s the 4% rule - withdraw 4% annually without depleting principal.\n\n## Realistic Passive Income Sources\n\n### Dividend Investing\n- Stocks that pay quarterly\n- 3-4% yield typically\n- Requires significant capital\n- Most "passive" option\n\n### Rental Property\n- Monthly cash flow\n- Requires management (not fully passive)\n- Tax advantages\n- Leverage possible\n\n[Draft - more strategies coming]\n\n## The Timeline\n\nYear 1-3: Build active income, save aggressively\nYear 3-5: Invest consistently, reinvest everything\nYear 5-10: Compound growth kicks in\nYear 10+: Passive income becomes meaningful\n\nMore coming soon...',
581
+ 'Finance',
582
+ '["passive-income", "investing", "financial-freedom", "money", "wealth"]'::jsonb,
583
+ 'draft',
584
+ NULL,
585
+ 'usr-blog-carlos',
586
+ 'team-blog-carlos',
587
+ NOW() - INTERVAL '5 days',
588
+ NOW() - INTERVAL '5 days'
589
+ )
590
+ ON CONFLICT (id) DO NOTHING;
591
+
592
+ -- ============================================
593
+ -- STEP 9: MARK FEATURED POSTS
594
+ -- ============================================
595
+
596
+ UPDATE "posts" SET "featured" = true WHERE id IN (
597
+ 'post-marcos-001', -- The Future of AI in SaaS
598
+ 'post-marcos-003', -- Why TypeScript Won
599
+ 'post-lucia-001', -- Best Cafes for Remote Work in Lisbon
600
+ 'post-lucia-003', -- Hidden Gems of Southeast Asia
601
+ 'post-carlos-001', -- Investing 101 for Beginners
602
+ 'post-carlos-002' -- Side Hustles That Actually Work
603
+ );
604
+
605
+ -- ============================================
606
+ -- STEP 10: CREATE INVOICES (6 per team = 18 total)
607
+ -- ============================================
608
+ -- Pro Plan subscription: $29/month
609
+ -- Status: 5 paid + 1 pending per team
610
+
611
+ INSERT INTO "invoices" (
612
+ id,
613
+ "teamId",
614
+ "invoiceNumber",
615
+ date,
616
+ amount,
617
+ currency,
618
+ status,
619
+ "pdfUrl",
620
+ description
621
+ ) VALUES
622
+ -- Marcos Tech Blog invoices
623
+ ('inv-marcos-001', 'team-blog-marcos', 'INV-MARCOS-001', NOW() - INTERVAL '5 months', 29.00, 'USD', 'paid', 'https://billing.example.com/invoices/inv-marcos-001.pdf', 'Pro Plan - Monthly subscription'),
624
+ ('inv-marcos-002', 'team-blog-marcos', 'INV-MARCOS-002', NOW() - INTERVAL '4 months', 29.00, 'USD', 'paid', 'https://billing.example.com/invoices/inv-marcos-002.pdf', 'Pro Plan - Monthly subscription'),
625
+ ('inv-marcos-003', 'team-blog-marcos', 'INV-MARCOS-003', NOW() - INTERVAL '3 months', 29.00, 'USD', 'paid', 'https://billing.example.com/invoices/inv-marcos-003.pdf', 'Pro Plan - Monthly subscription'),
626
+ ('inv-marcos-004', 'team-blog-marcos', 'INV-MARCOS-004', NOW() - INTERVAL '2 months', 29.00, 'USD', 'paid', 'https://billing.example.com/invoices/inv-marcos-004.pdf', 'Pro Plan - Monthly subscription'),
627
+ ('inv-marcos-005', 'team-blog-marcos', 'INV-MARCOS-005', NOW() - INTERVAL '1 month', 29.00, 'USD', 'paid', 'https://billing.example.com/invoices/inv-marcos-005.pdf', 'Pro Plan - Monthly subscription'),
628
+ ('inv-marcos-006', 'team-blog-marcos', 'INV-MARCOS-006', NOW(), 29.00, 'USD', 'pending', NULL, 'Pro Plan - Monthly subscription'),
629
+
630
+ -- Lucia Lifestyle Blog invoices
631
+ ('inv-lucia-001', 'team-blog-lucia', 'INV-LUCIA-001', NOW() - INTERVAL '5 months', 29.00, 'USD', 'paid', 'https://billing.example.com/invoices/inv-lucia-001.pdf', 'Pro Plan - Monthly subscription'),
632
+ ('inv-lucia-002', 'team-blog-lucia', 'INV-LUCIA-002', NOW() - INTERVAL '4 months', 29.00, 'USD', 'paid', 'https://billing.example.com/invoices/inv-lucia-002.pdf', 'Pro Plan - Monthly subscription'),
633
+ ('inv-lucia-003', 'team-blog-lucia', 'INV-LUCIA-003', NOW() - INTERVAL '3 months', 29.00, 'USD', 'paid', 'https://billing.example.com/invoices/inv-lucia-003.pdf', 'Pro Plan - Monthly subscription'),
634
+ ('inv-lucia-004', 'team-blog-lucia', 'INV-LUCIA-004', NOW() - INTERVAL '2 months', 29.00, 'USD', 'paid', 'https://billing.example.com/invoices/inv-lucia-004.pdf', 'Pro Plan - Monthly subscription'),
635
+ ('inv-lucia-005', 'team-blog-lucia', 'INV-LUCIA-005', NOW() - INTERVAL '1 month', 29.00, 'USD', 'paid', 'https://billing.example.com/invoices/inv-lucia-005.pdf', 'Pro Plan - Monthly subscription'),
636
+ ('inv-lucia-006', 'team-blog-lucia', 'INV-LUCIA-006', NOW(), 29.00, 'USD', 'pending', NULL, 'Pro Plan - Monthly subscription'),
637
+
638
+ -- Carlos Finance Blog invoices
639
+ ('inv-carlos-001', 'team-blog-carlos', 'INV-CARLOS-001', NOW() - INTERVAL '5 months', 29.00, 'USD', 'paid', 'https://billing.example.com/invoices/inv-carlos-001.pdf', 'Pro Plan - Monthly subscription'),
640
+ ('inv-carlos-002', 'team-blog-carlos', 'INV-CARLOS-002', NOW() - INTERVAL '4 months', 29.00, 'USD', 'paid', 'https://billing.example.com/invoices/inv-carlos-002.pdf', 'Pro Plan - Monthly subscription'),
641
+ ('inv-carlos-003', 'team-blog-carlos', 'INV-CARLOS-003', NOW() - INTERVAL '3 months', 29.00, 'USD', 'paid', 'https://billing.example.com/invoices/inv-carlos-003.pdf', 'Pro Plan - Monthly subscription'),
642
+ ('inv-carlos-004', 'team-blog-carlos', 'INV-CARLOS-004', NOW() - INTERVAL '2 months', 29.00, 'USD', 'paid', 'https://billing.example.com/invoices/inv-carlos-004.pdf', 'Pro Plan - Monthly subscription'),
643
+ ('inv-carlos-005', 'team-blog-carlos', 'INV-CARLOS-005', NOW() - INTERVAL '1 month', 29.00, 'USD', 'paid', 'https://billing.example.com/invoices/inv-carlos-005.pdf', 'Pro Plan - Monthly subscription'),
644
+ ('inv-carlos-006', 'team-blog-carlos', 'INV-CARLOS-006', NOW(), 29.00, 'USD', 'pending', NULL, 'Pro Plan - Monthly subscription')
645
+ ON CONFLICT (id) DO NOTHING;
646
+
647
+ -- ============================================
648
+ -- STEP 11: SUBSCRIPTIONS (B2C - with userId optimization)
649
+ -- ============================================
650
+ -- Note: Blog theme is single-user B2C - we use userId for optimization
651
+ -- Each author = 1 team = 1 subscription = 1 user
652
+
653
+ -- First, delete auto-created free subscriptions from the trigger
654
+ -- (the trigger creates a free subscription for each new team)
655
+ DELETE FROM public."subscriptions" WHERE "teamId" IN (
656
+ 'team-blog-marcos', 'team-blog-lucia', 'team-blog-carlos'
657
+ );
658
+
659
+ INSERT INTO public."subscriptions" (
660
+ id, "teamId", "userId", "planId", status,
661
+ "currentPeriodStart", "currentPeriodEnd", "billingInterval", "paymentProvider",
662
+ "externalSubscriptionId", "externalCustomerId", "createdAt"
663
+ ) VALUES
664
+ -- Marcos Tech → Pro Plan $29/mo (active, monthly)
665
+ ('sub-blog-marcos', 'team-blog-marcos', 'usr-blog-marcos', 'plan_pro', 'active',
666
+ NOW() - INTERVAL '30 days', NOW() + INTERVAL '30 days', 'monthly', 'stripe',
667
+ 'sub_stripe_marcos', 'cus_marcos', NOW() - INTERVAL '6 months'),
668
+ -- Lucia Lifestyle → Pro Plan $29/mo (active, monthly)
669
+ ('sub-blog-lucia', 'team-blog-lucia', 'usr-blog-lucia', 'plan_pro', 'active',
670
+ NOW() - INTERVAL '15 days', NOW() + INTERVAL '15 days', 'monthly', 'stripe',
671
+ 'sub_stripe_lucia', 'cus_lucia', NOW() - INTERVAL '5 months'),
672
+ -- Carlos Finance → Pro Plan (trialing, monthly)
673
+ ('sub-blog-carlos', 'team-blog-carlos', 'usr-blog-carlos', 'plan_pro', 'trialing',
674
+ NOW(), NOW() + INTERVAL '14 days', 'monthly', NULL, NULL, NULL, NOW() - INTERVAL '4 months')
675
+ ON CONFLICT (id) DO NOTHING;
676
+
677
+ UPDATE public."subscriptions" SET "trialEndsAt" = NOW() + INTERVAL '14 days'
678
+ WHERE id = 'sub-blog-carlos';
679
+
680
+ -- ============================================
681
+ -- STEP 12: BILLING EVENTS
682
+ -- ============================================
683
+
684
+ INSERT INTO public."billing_events" (id, "subscriptionId", type, status, amount, currency, "createdAt")
685
+ VALUES
686
+ -- Marcos pagos (6 meses @ $29)
687
+ ('be-blog-marcos-001', 'sub-blog-marcos', 'payment', 'succeeded', 2900, 'usd', NOW() - INTERVAL '6 months'),
688
+ ('be-blog-marcos-002', 'sub-blog-marcos', 'payment', 'succeeded', 2900, 'usd', NOW() - INTERVAL '5 months'),
689
+ ('be-blog-marcos-003', 'sub-blog-marcos', 'payment', 'succeeded', 2900, 'usd', NOW() - INTERVAL '4 months'),
690
+ ('be-blog-marcos-004', 'sub-blog-marcos', 'payment', 'succeeded', 2900, 'usd', NOW() - INTERVAL '3 months'),
691
+ ('be-blog-marcos-005', 'sub-blog-marcos', 'payment', 'succeeded', 2900, 'usd', NOW() - INTERVAL '2 months'),
692
+ ('be-blog-marcos-006', 'sub-blog-marcos', 'payment', 'succeeded', 2900, 'usd', NOW() - INTERVAL '1 month'),
693
+ -- Lucia pagos (5 meses @ $29)
694
+ ('be-blog-lucia-001', 'sub-blog-lucia', 'payment', 'succeeded', 2900, 'usd', NOW() - INTERVAL '5 months'),
695
+ ('be-blog-lucia-002', 'sub-blog-lucia', 'payment', 'succeeded', 2900, 'usd', NOW() - INTERVAL '4 months'),
696
+ ('be-blog-lucia-003', 'sub-blog-lucia', 'payment', 'succeeded', 2900, 'usd', NOW() - INTERVAL '3 months'),
697
+ ('be-blog-lucia-004', 'sub-blog-lucia', 'payment', 'succeeded', 2900, 'usd', NOW() - INTERVAL '2 months'),
698
+ ('be-blog-lucia-005', 'sub-blog-lucia', 'payment', 'succeeded', 2900, 'usd', NOW() - INTERVAL '1 month')
699
+ ON CONFLICT (id) DO NOTHING;
700
+
701
+ -- ============================================
702
+ -- STEP 13: TEAM METADATA (B2C author attributes)
703
+ -- ============================================
704
+
705
+ UPDATE public."teams" SET metadata = '{"authorNiche": "technology", "postsPublished": 4, "featuredCount": 2}'::jsonb WHERE id = 'team-blog-marcos';
706
+ UPDATE public."teams" SET metadata = '{"authorNiche": "travel", "postsPublished": 3, "featuredCount": 2}'::jsonb WHERE id = 'team-blog-lucia';
707
+ UPDATE public."teams" SET metadata = '{"authorNiche": "finance", "postsPublished": 3, "featuredCount": 2}'::jsonb WHERE id = 'team-blog-carlos';
708
+
709
+ -- ============================================
710
+ -- STEP 14: 10 NEW BLOGGERS WITH DIVERSE SCENARIOS
711
+ -- ============================================
712
+ -- Mix of monthly/yearly, active/canceled/past_due
713
+ -- Password: Test1234 (same hash as above)
714
+
715
+ INSERT INTO "users" (
716
+ id, email, name, "firstName", "lastName", username, bio,
717
+ social_twitter, role, "emailVerified", language, country, timezone, "createdAt", "updatedAt"
718
+ ) VALUES
719
+ -- 1. Diana Design - Pro Monthly Active
720
+ ('usr-blog-diana', 'blog_author_diana@nextspark.dev', 'Diana Design', 'Diana', 'Design',
721
+ 'diana_design', 'UX/UI designer creating beautiful digital experiences.',
722
+ 'https://twitter.com/dianadesign', 'member', true, 'en', 'US', 'America/New_York',
723
+ NOW() - INTERVAL '8 months', NOW()),
724
+ -- 2. Elena Eco - Pro Monthly Active
725
+ ('usr-blog-elena', 'blog_author_elena@nextspark.dev', 'Elena Eco', 'Elena', 'Eco',
726
+ 'elena_eco', 'Sustainable living advocate. Small changes, big impact.',
727
+ 'https://twitter.com/elenaeco', 'member', true, 'en', 'DE', 'Europe/Berlin',
728
+ NOW() - INTERVAL '7 months', NOW()),
729
+ -- 3. Felix Fitness - Pro Yearly Active
730
+ ('usr-blog-felix', 'blog_author_felix@nextspark.dev', 'Felix Fitness', 'Felix', 'Fitness',
731
+ 'felix_fitness', 'Personal trainer helping you achieve your fitness goals.',
732
+ 'https://twitter.com/felixfitness', 'member', true, 'en', 'AU', 'Australia/Sydney',
733
+ NOW() - INTERVAL '14 months', NOW()),
734
+ -- 4. Gloria Gourmet - Pro Yearly Active
735
+ ('usr-blog-gloria', 'blog_author_gloria@nextspark.dev', 'Gloria Gourmet', 'Gloria', 'Gourmet',
736
+ 'gloria_gourmet', 'Food blogger exploring cuisines from around the world.',
737
+ 'https://twitter.com/gloriagourmet', 'member', true, 'es', 'MX', 'America/Mexico_City',
738
+ NOW() - INTERVAL '12 months', NOW()),
739
+ -- 5. Hugo Health - Pro Monthly Active
740
+ ('usr-blog-hugo', 'blog_author_hugo@nextspark.dev', 'Hugo Health', 'Hugo', 'Health',
741
+ 'hugo_health', 'Medical doctor sharing health tips for everyday wellness.',
742
+ 'https://twitter.com/hugohealth', 'member', true, 'en', 'UK', 'Europe/London',
743
+ NOW() - INTERVAL '6 months', NOW()),
744
+ -- 6. Iris Inspire - Pro Monthly Past Due
745
+ ('usr-blog-iris', 'blog_author_iris@nextspark.dev', 'Iris Inspire', 'Iris', 'Inspire',
746
+ 'iris_inspire', 'Motivation coach helping you unlock your potential.',
747
+ 'https://twitter.com/irisinspire', 'member', true, 'en', 'CA', 'America/Toronto',
748
+ NOW() - INTERVAL '5 months', NOW()),
749
+ -- 7. Jorge Journey - Pro Monthly Canceled (voluntary)
750
+ ('usr-blog-jorge', 'blog_author_jorge@nextspark.dev', 'Jorge Journey', 'Jorge', 'Journey',
751
+ 'jorge_journey', 'Adventure seeker documenting epic journeys.',
752
+ 'https://twitter.com/jorgejourney', 'member', true, 'es', 'AR', 'America/Argentina/Buenos_Aires',
753
+ NOW() - INTERVAL '9 months', NOW()),
754
+ -- 8. Karen Kitchen - Pro Yearly Canceled (payment failed)
755
+ ('usr-blog-karen', 'blog_author_karen@nextspark.dev', 'Karen Kitchen', 'Karen', 'Kitchen',
756
+ 'karen_kitchen', 'Home cook sharing family recipes and cooking tips.',
757
+ 'https://twitter.com/karenkitchen', 'member', true, 'en', 'US', 'America/Chicago',
758
+ NOW() - INTERVAL '15 months', NOW()),
759
+ -- 9. Leo Learn - Free Plan Active
760
+ ('usr-blog-leo', 'blog_author_leo@nextspark.dev', 'Leo Learn', 'Leo', 'Learn',
761
+ 'leo_learn', 'Educator making learning fun and accessible.',
762
+ 'https://twitter.com/leolearn', 'member', true, 'en', 'IN', 'Asia/Kolkata',
763
+ NOW() - INTERVAL '2 months', NOW()),
764
+ -- 10. Maria Music - Pro Monthly Trialing
765
+ ('usr-blog-maria', 'blog_author_maria@nextspark.dev', 'Maria Music', 'Maria', 'Music',
766
+ 'maria_music', 'Musician and producer exploring sounds and stories.',
767
+ 'https://twitter.com/mariamusic', 'member', true, 'es', 'ES', 'Europe/Madrid',
768
+ NOW() - INTERVAL '5 days', NOW())
769
+ ON CONFLICT (email) DO NOTHING;
770
+
771
+ -- Teams for 10 new bloggers
772
+ INSERT INTO "teams" (id, name, slug, description, "ownerId", "createdAt", "updatedAt") VALUES
773
+ ('team-blog-diana', 'Diana Design Blog', 'diana-design', 'UX/UI design insights', 'usr-blog-diana', NOW() - INTERVAL '8 months', NOW()),
774
+ ('team-blog-elena', 'Elena Eco Blog', 'elena-eco', 'Sustainable living tips', 'usr-blog-elena', NOW() - INTERVAL '7 months', NOW()),
775
+ ('team-blog-felix', 'Felix Fitness Blog', 'felix-fitness', 'Fitness and health content', 'usr-blog-felix', NOW() - INTERVAL '14 months', NOW()),
776
+ ('team-blog-gloria', 'Gloria Gourmet Blog', 'gloria-gourmet', 'Food and recipes', 'usr-blog-gloria', NOW() - INTERVAL '12 months', NOW()),
777
+ ('team-blog-hugo', 'Hugo Health Blog', 'hugo-health', 'Health and wellness', 'usr-blog-hugo', NOW() - INTERVAL '6 months', NOW()),
778
+ ('team-blog-iris', 'Iris Inspire Blog', 'iris-inspire', 'Motivation and self-improvement', 'usr-blog-iris', NOW() - INTERVAL '5 months', NOW()),
779
+ ('team-blog-jorge', 'Jorge Journey Blog', 'jorge-journey', 'Adventure travel stories', 'usr-blog-jorge', NOW() - INTERVAL '9 months', NOW()),
780
+ ('team-blog-karen', 'Karen Kitchen Blog', 'karen-kitchen', 'Home cooking recipes', 'usr-blog-karen', NOW() - INTERVAL '15 months', NOW()),
781
+ ('team-blog-leo', 'Leo Learn Blog', 'leo-learn', 'Educational content', 'usr-blog-leo', NOW() - INTERVAL '2 months', NOW()),
782
+ ('team-blog-maria', 'Maria Music Blog', 'maria-music', 'Music and production', 'usr-blog-maria', NOW() - INTERVAL '5 days', NOW())
783
+ ON CONFLICT (id) DO NOTHING;
784
+
785
+ -- Team memberships
786
+ INSERT INTO "team_members" (id, "teamId", "userId", role, "joinedAt") VALUES
787
+ ('tm-blog-diana', 'team-blog-diana', 'usr-blog-diana', 'owner', NOW() - INTERVAL '8 months'),
788
+ ('tm-blog-elena', 'team-blog-elena', 'usr-blog-elena', 'owner', NOW() - INTERVAL '7 months'),
789
+ ('tm-blog-felix', 'team-blog-felix', 'usr-blog-felix', 'owner', NOW() - INTERVAL '14 months'),
790
+ ('tm-blog-gloria', 'team-blog-gloria', 'usr-blog-gloria', 'owner', NOW() - INTERVAL '12 months'),
791
+ ('tm-blog-hugo', 'team-blog-hugo', 'usr-blog-hugo', 'owner', NOW() - INTERVAL '6 months'),
792
+ ('tm-blog-iris', 'team-blog-iris', 'usr-blog-iris', 'owner', NOW() - INTERVAL '5 months'),
793
+ ('tm-blog-jorge', 'team-blog-jorge', 'usr-blog-jorge', 'owner', NOW() - INTERVAL '9 months'),
794
+ ('tm-blog-karen', 'team-blog-karen', 'usr-blog-karen', 'owner', NOW() - INTERVAL '15 months'),
795
+ ('tm-blog-leo', 'team-blog-leo', 'usr-blog-leo', 'owner', NOW() - INTERVAL '2 months'),
796
+ ('tm-blog-maria', 'team-blog-maria', 'usr-blog-maria', 'owner', NOW() - INTERVAL '5 days')
797
+ ON CONFLICT (id) DO NOTHING;
798
+
799
+ -- Accounts (Password: Test1234)
800
+ INSERT INTO "account" (id, "userId", "accountId", "providerId", "password", "createdAt", "updatedAt") VALUES
801
+ ('acc-blog-diana', 'usr-blog-diana', 'blog_author_diana@nextspark.dev', 'credential', '3db9e98e2b4d3caca97fdf2783791cbc:34b293de615caf277a237773208858e960ea8aa10f1f5c5c309b632f192cac34d52ceafbd338385616f4929e4b1b6c055b67429c6722ffdb80b01d9bf4764866', NOW() - INTERVAL '8 months', NOW()),
802
+ ('acc-blog-elena', 'usr-blog-elena', 'blog_author_elena@nextspark.dev', 'credential', '3db9e98e2b4d3caca97fdf2783791cbc:34b293de615caf277a237773208858e960ea8aa10f1f5c5c309b632f192cac34d52ceafbd338385616f4929e4b1b6c055b67429c6722ffdb80b01d9bf4764866', NOW() - INTERVAL '7 months', NOW()),
803
+ ('acc-blog-felix', 'usr-blog-felix', 'blog_author_felix@nextspark.dev', 'credential', '3db9e98e2b4d3caca97fdf2783791cbc:34b293de615caf277a237773208858e960ea8aa10f1f5c5c309b632f192cac34d52ceafbd338385616f4929e4b1b6c055b67429c6722ffdb80b01d9bf4764866', NOW() - INTERVAL '14 months', NOW()),
804
+ ('acc-blog-gloria', 'usr-blog-gloria', 'blog_author_gloria@nextspark.dev', 'credential', '3db9e98e2b4d3caca97fdf2783791cbc:34b293de615caf277a237773208858e960ea8aa10f1f5c5c309b632f192cac34d52ceafbd338385616f4929e4b1b6c055b67429c6722ffdb80b01d9bf4764866', NOW() - INTERVAL '12 months', NOW()),
805
+ ('acc-blog-hugo', 'usr-blog-hugo', 'blog_author_hugo@nextspark.dev', 'credential', '3db9e98e2b4d3caca97fdf2783791cbc:34b293de615caf277a237773208858e960ea8aa10f1f5c5c309b632f192cac34d52ceafbd338385616f4929e4b1b6c055b67429c6722ffdb80b01d9bf4764866', NOW() - INTERVAL '6 months', NOW()),
806
+ ('acc-blog-iris', 'usr-blog-iris', 'blog_author_iris@nextspark.dev', 'credential', '3db9e98e2b4d3caca97fdf2783791cbc:34b293de615caf277a237773208858e960ea8aa10f1f5c5c309b632f192cac34d52ceafbd338385616f4929e4b1b6c055b67429c6722ffdb80b01d9bf4764866', NOW() - INTERVAL '5 months', NOW()),
807
+ ('acc-blog-jorge', 'usr-blog-jorge', 'blog_author_jorge@nextspark.dev', 'credential', '3db9e98e2b4d3caca97fdf2783791cbc:34b293de615caf277a237773208858e960ea8aa10f1f5c5c309b632f192cac34d52ceafbd338385616f4929e4b1b6c055b67429c6722ffdb80b01d9bf4764866', NOW() - INTERVAL '9 months', NOW()),
808
+ ('acc-blog-karen', 'usr-blog-karen', 'blog_author_karen@nextspark.dev', 'credential', '3db9e98e2b4d3caca97fdf2783791cbc:34b293de615caf277a237773208858e960ea8aa10f1f5c5c309b632f192cac34d52ceafbd338385616f4929e4b1b6c055b67429c6722ffdb80b01d9bf4764866', NOW() - INTERVAL '15 months', NOW()),
809
+ ('acc-blog-leo', 'usr-blog-leo', 'blog_author_leo@nextspark.dev', 'credential', '3db9e98e2b4d3caca97fdf2783791cbc:34b293de615caf277a237773208858e960ea8aa10f1f5c5c309b632f192cac34d52ceafbd338385616f4929e4b1b6c055b67429c6722ffdb80b01d9bf4764866', NOW() - INTERVAL '2 months', NOW()),
810
+ ('acc-blog-maria', 'usr-blog-maria', 'blog_author_maria@nextspark.dev', 'credential', '3db9e98e2b4d3caca97fdf2783791cbc:34b293de615caf277a237773208858e960ea8aa10f1f5c5c309b632f192cac34d52ceafbd338385616f4929e4b1b6c055b67429c6722ffdb80b01d9bf4764866', NOW() - INTERVAL '5 days', NOW())
811
+ ON CONFLICT ("providerId", "accountId") DO NOTHING;
812
+
813
+ -- Users meta
814
+ INSERT INTO "users_metas" ("userId", "metaKey", "metaValue", "dataType", "isPublic", "isSearchable", "createdAt", "updatedAt") VALUES
815
+ ('usr-blog-diana', 'activeTeamId', '"team-blog-diana"', 'json', false, false, NOW(), NOW()),
816
+ ('usr-blog-elena', 'activeTeamId', '"team-blog-elena"', 'json', false, false, NOW(), NOW()),
817
+ ('usr-blog-felix', 'activeTeamId', '"team-blog-felix"', 'json', false, false, NOW(), NOW()),
818
+ ('usr-blog-gloria', 'activeTeamId', '"team-blog-gloria"', 'json', false, false, NOW(), NOW()),
819
+ ('usr-blog-hugo', 'activeTeamId', '"team-blog-hugo"', 'json', false, false, NOW(), NOW()),
820
+ ('usr-blog-iris', 'activeTeamId', '"team-blog-iris"', 'json', false, false, NOW(), NOW()),
821
+ ('usr-blog-jorge', 'activeTeamId', '"team-blog-jorge"', 'json', false, false, NOW(), NOW()),
822
+ ('usr-blog-karen', 'activeTeamId', '"team-blog-karen"', 'json', false, false, NOW(), NOW()),
823
+ ('usr-blog-leo', 'activeTeamId', '"team-blog-leo"', 'json', false, false, NOW(), NOW()),
824
+ ('usr-blog-maria', 'activeTeamId', '"team-blog-maria"', 'json', false, false, NOW(), NOW())
825
+ ON CONFLICT ("userId", "metaKey") DO NOTHING;
826
+
827
+ -- ============================================
828
+ -- STEP 15: SUBSCRIPTIONS FOR 10 NEW BLOGGERS
829
+ -- ============================================
830
+ -- Diverse scenarios: active/canceled/past_due, monthly/yearly
831
+
832
+ -- Delete auto-created free subscriptions from the trigger
833
+ DELETE FROM public."subscriptions" WHERE "teamId" IN (
834
+ 'team-blog-diana', 'team-blog-elena', 'team-blog-felix', 'team-blog-gloria',
835
+ 'team-blog-hugo', 'team-blog-iris', 'team-blog-jorge', 'team-blog-karen',
836
+ 'team-blog-leo', 'team-blog-maria'
837
+ );
838
+
839
+ INSERT INTO public."subscriptions" (
840
+ id, "teamId", "userId", "planId", status,
841
+ "currentPeriodStart", "currentPeriodEnd", "billingInterval",
842
+ "paymentProvider", "externalSubscriptionId", "externalCustomerId",
843
+ "createdAt", "canceledAt", "cancelAtPeriodEnd", metadata
844
+ ) VALUES
845
+ -- 1. Diana Design - Pro Monthly ACTIVE (8 months customer)
846
+ ('sub-blog-diana', 'team-blog-diana', 'usr-blog-diana', 'plan_pro', 'active',
847
+ NOW() - INTERVAL '25 days', NOW() + INTERVAL '5 days', 'monthly',
848
+ 'stripe', 'sub_stripe_diana', 'cus_diana',
849
+ NOW() - INTERVAL '8 months', NULL, false, '{}'::jsonb),
850
+
851
+ -- 2. Elena Eco - Pro Monthly ACTIVE (7 months customer)
852
+ ('sub-blog-elena', 'team-blog-elena', 'usr-blog-elena', 'plan_pro', 'active',
853
+ NOW() - INTERVAL '20 days', NOW() + INTERVAL '10 days', 'monthly',
854
+ 'stripe', 'sub_stripe_elena', 'cus_elena',
855
+ NOW() - INTERVAL '7 months', NULL, false, '{}'::jsonb),
856
+
857
+ -- 3. Felix Fitness - Pro Yearly ACTIVE (14 months = 2nd year)
858
+ ('sub-blog-felix', 'team-blog-felix', 'usr-blog-felix', 'plan_pro', 'active',
859
+ NOW() - INTERVAL '60 days', NOW() + INTERVAL '305 days', 'yearly',
860
+ 'stripe', 'sub_stripe_felix', 'cus_felix',
861
+ NOW() - INTERVAL '14 months', NULL, false, '{}'::jsonb),
862
+
863
+ -- 4. Gloria Gourmet - Pro Yearly ACTIVE (12 months)
864
+ ('sub-blog-gloria', 'team-blog-gloria', 'usr-blog-gloria', 'plan_pro', 'active',
865
+ NOW() - INTERVAL '30 days', NOW() + INTERVAL '335 days', 'yearly',
866
+ 'stripe', 'sub_stripe_gloria', 'cus_gloria',
867
+ NOW() - INTERVAL '12 months', NULL, false, '{}'::jsonb),
868
+
869
+ -- 5. Hugo Health - Pro Monthly ACTIVE (6 months customer)
870
+ ('sub-blog-hugo', 'team-blog-hugo', 'usr-blog-hugo', 'plan_pro', 'active',
871
+ NOW() - INTERVAL '18 days', NOW() + INTERVAL '12 days', 'monthly',
872
+ 'stripe', 'sub_stripe_hugo', 'cus_hugo',
873
+ NOW() - INTERVAL '6 months', NULL, false, '{}'::jsonb),
874
+
875
+ -- 6. Iris Inspire - Pro Monthly PAST_DUE (failed payment)
876
+ ('sub-blog-iris', 'team-blog-iris', 'usr-blog-iris', 'plan_pro', 'past_due',
877
+ NOW() - INTERVAL '35 days', NOW() - INTERVAL '5 days', 'monthly',
878
+ 'stripe', 'sub_stripe_iris', 'cus_iris',
879
+ NOW() - INTERVAL '5 months', NULL, false,
880
+ '{"lastPaymentAttempt": "failed", "retryCount": 2}'::jsonb),
881
+
882
+ -- 7. Jorge Journey - Pro Monthly CANCELED (voluntary - user decided to cancel)
883
+ ('sub-blog-jorge', 'team-blog-jorge', 'usr-blog-jorge', 'plan_pro', 'canceled',
884
+ NOW() - INTERVAL '45 days', NOW() - INTERVAL '15 days', 'monthly',
885
+ 'stripe', 'sub_stripe_jorge', 'cus_jorge',
886
+ NOW() - INTERVAL '9 months', NOW() - INTERVAL '15 days', false,
887
+ '{"cancelReason": "voluntary", "canceledBy": "user", "feedback": "taking a break"}'::jsonb),
888
+
889
+ -- 8. Karen Kitchen - Pro Yearly CANCELED (payment failed 4 times)
890
+ ('sub-blog-karen', 'team-blog-karen', 'usr-blog-karen', 'plan_pro', 'canceled',
891
+ NOW() - INTERVAL '400 days', NOW() - INTERVAL '35 days', 'yearly',
892
+ 'stripe', 'sub_stripe_karen', 'cus_karen',
893
+ NOW() - INTERVAL '15 months', NOW() - INTERVAL '35 days', false,
894
+ '{"cancelReason": "payment_failed", "failedAttempts": 4, "lastCardEnding": "4242"}'::jsonb),
895
+
896
+ -- 9. Leo Learn - Free Plan ACTIVE (no billing)
897
+ ('sub-blog-leo', 'team-blog-leo', 'usr-blog-leo', 'plan_free', 'active',
898
+ NOW() - INTERVAL '2 months', NOW() + INTERVAL '100 years', 'monthly',
899
+ NULL, NULL, NULL,
900
+ NOW() - INTERVAL '2 months', NULL, false, '{}'::jsonb),
901
+
902
+ -- 10. Maria Music - Pro Monthly TRIALING (5 days in)
903
+ ('sub-blog-maria', 'team-blog-maria', 'usr-blog-maria', 'plan_pro', 'trialing',
904
+ NOW() - INTERVAL '5 days', NOW() + INTERVAL '25 days', 'monthly',
905
+ NULL, NULL, NULL,
906
+ NOW() - INTERVAL '5 days', NULL, false, '{}'::jsonb)
907
+ ON CONFLICT (id) DO NOTHING;
908
+
909
+ -- Set trial end date for Maria
910
+ UPDATE public."subscriptions" SET "trialEndsAt" = NOW() + INTERVAL '9 days'
911
+ WHERE id = 'sub-blog-maria';
912
+
913
+ -- ============================================
914
+ -- STEP 16: BILLING EVENTS FOR 10 NEW BLOGGERS
915
+ -- ============================================
916
+ -- Coherent payment history matching subscription scenarios
917
+
918
+ INSERT INTO public."billing_events" (
919
+ id, "subscriptionId", type, status, amount, currency, "externalPaymentId", "createdAt"
920
+ ) VALUES
921
+ -- ========================================
922
+ -- DIANA DESIGN - 8 months @ $29/mo = 8 payments
923
+ -- ========================================
924
+ ('be-diana-m1', 'sub-blog-diana', 'payment', 'succeeded', 2900, 'usd', 'pi_diana_m1', NOW() - INTERVAL '8 months'),
925
+ ('be-diana-m2', 'sub-blog-diana', 'payment', 'succeeded', 2900, 'usd', 'pi_diana_m2', NOW() - INTERVAL '7 months'),
926
+ ('be-diana-m3', 'sub-blog-diana', 'payment', 'succeeded', 2900, 'usd', 'pi_diana_m3', NOW() - INTERVAL '6 months'),
927
+ ('be-diana-m4', 'sub-blog-diana', 'payment', 'succeeded', 2900, 'usd', 'pi_diana_m4', NOW() - INTERVAL '5 months'),
928
+ ('be-diana-m5', 'sub-blog-diana', 'payment', 'succeeded', 2900, 'usd', 'pi_diana_m5', NOW() - INTERVAL '4 months'),
929
+ ('be-diana-m6', 'sub-blog-diana', 'payment', 'succeeded', 2900, 'usd', 'pi_diana_m6', NOW() - INTERVAL '3 months'),
930
+ ('be-diana-m7', 'sub-blog-diana', 'payment', 'succeeded', 2900, 'usd', 'pi_diana_m7', NOW() - INTERVAL '2 months'),
931
+ ('be-diana-m8', 'sub-blog-diana', 'payment', 'succeeded', 2900, 'usd', 'pi_diana_m8', NOW() - INTERVAL '1 month'),
932
+
933
+ -- ========================================
934
+ -- ELENA ECO - 7 months @ $29/mo = 7 payments
935
+ -- ========================================
936
+ ('be-elena-m1', 'sub-blog-elena', 'payment', 'succeeded', 2900, 'usd', 'pi_elena_m1', NOW() - INTERVAL '7 months'),
937
+ ('be-elena-m2', 'sub-blog-elena', 'payment', 'succeeded', 2900, 'usd', 'pi_elena_m2', NOW() - INTERVAL '6 months'),
938
+ ('be-elena-m3', 'sub-blog-elena', 'payment', 'succeeded', 2900, 'usd', 'pi_elena_m3', NOW() - INTERVAL '5 months'),
939
+ ('be-elena-m4', 'sub-blog-elena', 'payment', 'succeeded', 2900, 'usd', 'pi_elena_m4', NOW() - INTERVAL '4 months'),
940
+ ('be-elena-m5', 'sub-blog-elena', 'payment', 'succeeded', 2900, 'usd', 'pi_elena_m5', NOW() - INTERVAL '3 months'),
941
+ ('be-elena-m6', 'sub-blog-elena', 'payment', 'succeeded', 2900, 'usd', 'pi_elena_m6', NOW() - INTERVAL '2 months'),
942
+ ('be-elena-m7', 'sub-blog-elena', 'payment', 'succeeded', 2900, 'usd', 'pi_elena_m7', NOW() - INTERVAL '1 month'),
943
+
944
+ -- ========================================
945
+ -- FELIX FITNESS - 2 yearly payments @ $290/yr
946
+ -- ========================================
947
+ ('be-felix-y1', 'sub-blog-felix', 'payment', 'succeeded', 29000, 'usd', 'pi_felix_y1', NOW() - INTERVAL '14 months'),
948
+ ('be-felix-y2', 'sub-blog-felix', 'payment', 'succeeded', 29000, 'usd', 'pi_felix_y2', NOW() - INTERVAL '2 months'),
949
+
950
+ -- ========================================
951
+ -- GLORIA GOURMET - 1 yearly payment @ $290/yr
952
+ -- ========================================
953
+ ('be-gloria-y1', 'sub-blog-gloria', 'payment', 'succeeded', 29000, 'usd', 'pi_gloria_y1', NOW() - INTERVAL '1 month'),
954
+
955
+ -- ========================================
956
+ -- HUGO HEALTH - 6 months @ $29/mo = 6 payments
957
+ -- ========================================
958
+ ('be-hugo-m1', 'sub-blog-hugo', 'payment', 'succeeded', 2900, 'usd', 'pi_hugo_m1', NOW() - INTERVAL '6 months'),
959
+ ('be-hugo-m2', 'sub-blog-hugo', 'payment', 'succeeded', 2900, 'usd', 'pi_hugo_m2', NOW() - INTERVAL '5 months'),
960
+ ('be-hugo-m3', 'sub-blog-hugo', 'payment', 'succeeded', 2900, 'usd', 'pi_hugo_m3', NOW() - INTERVAL '4 months'),
961
+ ('be-hugo-m4', 'sub-blog-hugo', 'payment', 'succeeded', 2900, 'usd', 'pi_hugo_m4', NOW() - INTERVAL '3 months'),
962
+ ('be-hugo-m5', 'sub-blog-hugo', 'payment', 'succeeded', 2900, 'usd', 'pi_hugo_m5', NOW() - INTERVAL '2 months'),
963
+ ('be-hugo-m6', 'sub-blog-hugo', 'payment', 'succeeded', 2900, 'usd', 'pi_hugo_m6', NOW() - INTERVAL '1 month'),
964
+
965
+ -- ========================================
966
+ -- IRIS INSPIRE - 4 successful + 2 failed (PAST_DUE scenario)
967
+ -- ========================================
968
+ ('be-iris-m1', 'sub-blog-iris', 'payment', 'succeeded', 2900, 'usd', 'pi_iris_m1', NOW() - INTERVAL '5 months'),
969
+ ('be-iris-m2', 'sub-blog-iris', 'payment', 'succeeded', 2900, 'usd', 'pi_iris_m2', NOW() - INTERVAL '4 months'),
970
+ ('be-iris-m3', 'sub-blog-iris', 'payment', 'succeeded', 2900, 'usd', 'pi_iris_m3', NOW() - INTERVAL '3 months'),
971
+ ('be-iris-m4', 'sub-blog-iris', 'payment', 'succeeded', 2900, 'usd', 'pi_iris_m4', NOW() - INTERVAL '2 months'),
972
+ -- Failed payment - 1st attempt
973
+ ('be-iris-fail1', 'sub-blog-iris', 'payment', 'failed', 2900, 'usd', 'pi_iris_fail1', NOW() - INTERVAL '32 days'),
974
+ -- Failed payment - 2nd attempt (retry)
975
+ ('be-iris-fail2', 'sub-blog-iris', 'payment', 'failed', 2900, 'usd', 'pi_iris_fail2', NOW() - INTERVAL '25 days'),
976
+
977
+ -- ========================================
978
+ -- JORGE JOURNEY - 8 successful then VOLUNTARY CANCEL
979
+ -- ========================================
980
+ ('be-jorge-m1', 'sub-blog-jorge', 'payment', 'succeeded', 2900, 'usd', 'pi_jorge_m1', NOW() - INTERVAL '9 months'),
981
+ ('be-jorge-m2', 'sub-blog-jorge', 'payment', 'succeeded', 2900, 'usd', 'pi_jorge_m2', NOW() - INTERVAL '8 months'),
982
+ ('be-jorge-m3', 'sub-blog-jorge', 'payment', 'succeeded', 2900, 'usd', 'pi_jorge_m3', NOW() - INTERVAL '7 months'),
983
+ ('be-jorge-m4', 'sub-blog-jorge', 'payment', 'succeeded', 2900, 'usd', 'pi_jorge_m4', NOW() - INTERVAL '6 months'),
984
+ ('be-jorge-m5', 'sub-blog-jorge', 'payment', 'succeeded', 2900, 'usd', 'pi_jorge_m5', NOW() - INTERVAL '5 months'),
985
+ ('be-jorge-m6', 'sub-blog-jorge', 'payment', 'succeeded', 2900, 'usd', 'pi_jorge_m6', NOW() - INTERVAL '4 months'),
986
+ ('be-jorge-m7', 'sub-blog-jorge', 'payment', 'succeeded', 2900, 'usd', 'pi_jorge_m7', NOW() - INTERVAL '3 months'),
987
+ ('be-jorge-m8', 'sub-blog-jorge', 'payment', 'succeeded', 2900, 'usd', 'pi_jorge_m8', NOW() - INTERVAL '2 months'),
988
+ -- Final invoice before cancellation
989
+ ('be-jorge-last', 'sub-blog-jorge', 'invoice', 'succeeded', 2900, 'usd', 'in_jorge_last', NOW() - INTERVAL '45 days'),
990
+
991
+ -- ========================================
992
+ -- KAREN KITCHEN - 1 yearly success + 4 failed attempts = CANCELED
993
+ -- ========================================
994
+ ('be-karen-y1', 'sub-blog-karen', 'payment', 'succeeded', 29000, 'usd', 'pi_karen_y1', NOW() - INTERVAL '15 months'),
995
+ -- Renewal failed - attempt 1
996
+ ('be-karen-fail1', 'sub-blog-karen', 'payment', 'failed', 29000, 'usd', 'pi_karen_fail1', NOW() - INTERVAL '3 months'),
997
+ -- Renewal failed - attempt 2
998
+ ('be-karen-fail2', 'sub-blog-karen', 'payment', 'failed', 29000, 'usd', 'pi_karen_fail2', NOW() - INTERVAL '75 days'),
999
+ -- Renewal failed - attempt 3
1000
+ ('be-karen-fail3', 'sub-blog-karen', 'payment', 'failed', 29000, 'usd', 'pi_karen_fail3', NOW() - INTERVAL '55 days'),
1001
+ -- Renewal failed - attempt 4 (final, then canceled)
1002
+ ('be-karen-fail4', 'sub-blog-karen', 'payment', 'failed', 29000, 'usd', 'pi_karen_fail4', NOW() - INTERVAL '40 days')
1003
+
1004
+ -- LEO LEARN: Free plan - no billing events
1005
+ -- MARIA MUSIC: Trialing - no billing events yet
1006
+ ON CONFLICT (id) DO NOTHING;
1007
+
1008
+ -- ============================================
1009
+ -- STEP 17: TEAM METADATA FOR NEW BLOGGERS
1010
+ -- ============================================
1011
+
1012
+ UPDATE public."teams" SET metadata = '{"authorNiche": "design", "segment": "creator"}'::jsonb WHERE id = 'team-blog-diana';
1013
+ UPDATE public."teams" SET metadata = '{"authorNiche": "sustainability", "segment": "creator"}'::jsonb WHERE id = 'team-blog-elena';
1014
+ UPDATE public."teams" SET metadata = '{"authorNiche": "fitness", "segment": "creator", "billingCycle": "annual"}'::jsonb WHERE id = 'team-blog-felix';
1015
+ UPDATE public."teams" SET metadata = '{"authorNiche": "food", "segment": "creator", "billingCycle": "annual"}'::jsonb WHERE id = 'team-blog-gloria';
1016
+ UPDATE public."teams" SET metadata = '{"authorNiche": "health", "segment": "creator"}'::jsonb WHERE id = 'team-blog-hugo';
1017
+ UPDATE public."teams" SET metadata = '{"authorNiche": "motivation", "segment": "creator", "atRisk": true}'::jsonb WHERE id = 'team-blog-iris';
1018
+ UPDATE public."teams" SET metadata = '{"authorNiche": "adventure", "segment": "creator", "churned": true}'::jsonb WHERE id = 'team-blog-jorge';
1019
+ UPDATE public."teams" SET metadata = '{"authorNiche": "cooking", "segment": "creator", "churned": true}'::jsonb WHERE id = 'team-blog-karen';
1020
+ UPDATE public."teams" SET metadata = '{"authorNiche": "education", "segment": "free"}'::jsonb WHERE id = 'team-blog-leo';
1021
+ UPDATE public."teams" SET metadata = '{"authorNiche": "music", "segment": "trialing"}'::jsonb WHERE id = 'team-blog-maria';
1022
+
1023
+ -- ============================================
1024
+ -- SUCCESS SUMMARY
1025
+ -- ============================================
1026
+
1027
+ DO $$
1028
+ BEGIN
1029
+ RAISE NOTICE '';
1030
+ RAISE NOTICE '================================================================';
1031
+ RAISE NOTICE ' Blog Theme Sample Data - Multi-Author Platform (Medium-style)';
1032
+ RAISE NOTICE '================================================================';
1033
+ RAISE NOTICE '';
1034
+ RAISE NOTICE ' ORIGINAL AUTHORS (3):';
1035
+ RAISE NOTICE ' ------------------------------------------------------------';
1036
+ RAISE NOTICE ' 1. blog_author_marcos@nextspark.dev / Test1234 - Pro Monthly Active';
1037
+ RAISE NOTICE ' 2. blog_author_lucia@nextspark.dev / Test1234 - Pro Monthly Active';
1038
+ RAISE NOTICE ' 3. blog_author_carlos@nextspark.dev / Test1234 - Pro Monthly Trialing';
1039
+ RAISE NOTICE '';
1040
+ RAISE NOTICE ' NEW AUTHORS (10):';
1041
+ RAISE NOTICE ' ------------------------------------------------------------';
1042
+ RAISE NOTICE ' 4. blog_author_diana@nextspark.dev / Test1234 - Pro Monthly Active';
1043
+ RAISE NOTICE ' 5. blog_author_elena@nextspark.dev / Test1234 - Pro Monthly Active';
1044
+ RAISE NOTICE ' 6. blog_author_felix@nextspark.dev / Test1234 - Pro Yearly Active';
1045
+ RAISE NOTICE ' 7. blog_author_gloria@nextspark.dev / Test1234 - Pro Yearly Active';
1046
+ RAISE NOTICE ' 8. blog_author_hugo@nextspark.dev / Test1234 - Pro Monthly Active';
1047
+ RAISE NOTICE ' 9. blog_author_iris@nextspark.dev / Test1234 - Pro Monthly PAST_DUE';
1048
+ RAISE NOTICE ' 10. blog_author_jorge@nextspark.dev / Test1234 - Pro CANCELED (voluntary)';
1049
+ RAISE NOTICE ' 11. blog_author_karen@nextspark.dev / Test1234 - Pro CANCELED (payment failed)';
1050
+ RAISE NOTICE ' 12. blog_author_leo@nextspark.dev / Test1234 - Free Plan Active';
1051
+ RAISE NOTICE ' 13. blog_author_maria@nextspark.dev / Test1234 - Pro Monthly Trialing';
1052
+ RAISE NOTICE '';
1053
+ RAISE NOTICE ' SUBSCRIPTION SUMMARY:';
1054
+ RAISE NOTICE ' ------------------------------------------------------------';
1055
+ RAISE NOTICE ' - Monthly Active: 6 (Marcos, Lucia, Diana, Elena, Hugo)';
1056
+ RAISE NOTICE ' - Yearly Active: 2 (Felix, Gloria)';
1057
+ RAISE NOTICE ' - Trialing: 2 (Carlos, Maria)';
1058
+ RAISE NOTICE ' - Past Due: 1 (Iris)';
1059
+ RAISE NOTICE ' - Canceled Voluntary: 1 (Jorge)';
1060
+ RAISE NOTICE ' - Canceled Payment Failed: 1 (Karen)';
1061
+ RAISE NOTICE ' - Free Plan: 1 (Leo)';
1062
+ RAISE NOTICE '';
1063
+ RAISE NOTICE ' BILLING (Pro Plan = $29/mo, $290/yr):';
1064
+ RAISE NOTICE ' ------------------------------------------------------------';
1065
+ RAISE NOTICE ' - MRR (monthly active): ~$203 (7 x $29)';
1066
+ RAISE NOTICE ' - ARR (yearly active): $580 (2 x $290)';
1067
+ RAISE NOTICE ' - At-risk revenue: $29 (Iris past_due)';
1068
+ RAISE NOTICE '';
1069
+ RAISE NOTICE '================================================================';
1070
+ END $$;