@zodic/shared 0.0.400 → 0.0.402

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 (61) hide show
  1. package/app/api/index.ts +0 -99
  2. package/app/base/AppContext.ts +0 -41
  3. package/app/services/ConceptService.ts +172 -244
  4. package/app/services/PaymentService.ts +61 -2
  5. package/db/migrations/{0000_little_sleeper.sql → 0000_workable_the_hand.sql} +197 -24
  6. package/db/migrations/meta/0000_snapshot.json +1663 -451
  7. package/db/migrations/meta/_journal.json +2 -156
  8. package/db/schema.ts +0 -31
  9. package/drizzle.config.ts +2 -2
  10. package/package.json +10 -5
  11. package/types/scopes/cloudflare.ts +2 -118
  12. package/types/scopes/generic.ts +1 -1
  13. package/utils/buildMessages.ts +1 -32
  14. package/wrangler.toml +24 -3
  15. package/app/durable/ConceptNameDO.ts +0 -199
  16. package/app/durable/index.ts +0 -1
  17. package/app/workflow/old/ArchetypeWorkflow.ts +0 -156
  18. package/db/migrations/0001_mysterious_mystique.sql +0 -47
  19. package/db/migrations/0002_reflective_firelord.sql +0 -13
  20. package/db/migrations/0003_thin_valeria_richards.sql +0 -28
  21. package/db/migrations/0004_loose_iron_monger.sql +0 -15
  22. package/db/migrations/0005_famous_cammi.sql +0 -26
  23. package/db/migrations/0006_fine_manta.sql +0 -1
  24. package/db/migrations/0007_typical_grim_reaper.sql +0 -1
  25. package/db/migrations/0008_fine_betty_brant.sql +0 -1
  26. package/db/migrations/0009_spooky_doctor_spectrum.sql +0 -20
  27. package/db/migrations/0010_tricky_lord_hawal.sql +0 -23
  28. package/db/migrations/0011_hard_king_bedlam.sql +0 -1
  29. package/db/migrations/0012_sudden_doctor_spectrum.sql +0 -27
  30. package/db/migrations/0013_lean_frightful_four.sql +0 -7
  31. package/db/migrations/0014_green_marvel_apes.sql +0 -10
  32. package/db/migrations/0015_zippy_sersi.sql +0 -10
  33. package/db/migrations/0016_awesome_squadron_sinister.sql +0 -1
  34. package/db/migrations/0017_vengeful_electro.sql +0 -1
  35. package/db/migrations/0018_wooden_sersi.sql +0 -16
  36. package/db/migrations/0019_abandoned_orphan.sql +0 -59
  37. package/db/migrations/0020_smiling_blob.sql +0 -1
  38. package/db/migrations/0021_flawless_wallflower.sql +0 -1
  39. package/db/migrations/0022_pale_marvex.sql +0 -1
  40. package/db/migrations/meta/0001_snapshot.json +0 -2200
  41. package/db/migrations/meta/0002_snapshot.json +0 -2284
  42. package/db/migrations/meta/0003_snapshot.json +0 -2417
  43. package/db/migrations/meta/0004_snapshot.json +0 -2417
  44. package/db/migrations/meta/0005_snapshot.json +0 -2417
  45. package/db/migrations/meta/0006_snapshot.json +0 -2425
  46. package/db/migrations/meta/0007_snapshot.json +0 -2433
  47. package/db/migrations/meta/0008_snapshot.json +0 -2425
  48. package/db/migrations/meta/0009_snapshot.json +0 -2425
  49. package/db/migrations/meta/0010_snapshot.json +0 -2594
  50. package/db/migrations/meta/0011_snapshot.json +0 -2602
  51. package/db/migrations/meta/0012_snapshot.json +0 -2602
  52. package/db/migrations/meta/0013_snapshot.json +0 -2649
  53. package/db/migrations/meta/0014_snapshot.json +0 -2723
  54. package/db/migrations/meta/0015_snapshot.json +0 -2711
  55. package/db/migrations/meta/0016_snapshot.json +0 -2715
  56. package/db/migrations/meta/0017_snapshot.json +0 -2723
  57. package/db/migrations/meta/0018_snapshot.json +0 -2834
  58. package/db/migrations/meta/0019_snapshot.json +0 -3244
  59. package/db/migrations/meta/0020_snapshot.json +0 -3252
  60. package/db/migrations/meta/0021_snapshot.json +0 -3254
  61. package/db/migrations/meta/0022_snapshot.json +0 -3260
package/app/api/index.ts CHANGED
@@ -448,105 +448,6 @@ export const Api = (env: BackendBindings) => ({
448
448
  }
449
449
  },
450
450
  },
451
- callImageDescriber: async (imageUrl: string): Promise<string> => {
452
- const mimeType = 'image/png';
453
- const endpoint =
454
- 'https://us-central1-describepicture.cloudfunctions.net/describe_picture_api';
455
- const hexKey = env.DESCRIBER_API_KEY;
456
- if (!hexKey) throw new Error('Describer API Key not found');
457
-
458
- const appId = env.DESCRIBER_APP_ID;
459
- if (!appId) throw new Error('Describer APP ID not found');
460
-
461
- const data = JSON.stringify({
462
- imageUrl,
463
- prompt: env.PROMPT_IMAGE_DESCRIBER,
464
- mimeType,
465
- appId,
466
- imageBase64: '',
467
- });
468
-
469
- const encryptData = async (
470
- data: string,
471
- hexKey: string
472
- ): Promise<{ iv: string; encryptedData: string }> => {
473
- const keyBytes = Uint8Array.from(
474
- hexKey.match(/.{2}/g)?.map((byte) => parseInt(byte, 16)) || []
475
- );
476
-
477
- const key = await crypto.subtle.importKey(
478
- 'raw',
479
- keyBytes,
480
- { name: 'AES-GCM' },
481
- false,
482
- ['encrypt']
483
- );
484
-
485
- const iv = crypto.getRandomValues(new Uint8Array(12));
486
- const encodedData = new TextEncoder().encode(data);
487
-
488
- const encrypted = await crypto.subtle.encrypt(
489
- { name: 'AES-GCM', iv },
490
- key,
491
- encodedData
492
- );
493
-
494
- return {
495
- iv: btoa(String.fromCharCode(...iv)),
496
- encryptedData: btoa(String.fromCharCode(...new Uint8Array(encrypted))),
497
- };
498
- };
499
-
500
- const { iv, encryptedData } = await encryptData(data, hexKey);
501
-
502
- const payload = { iv, encryptedData };
503
-
504
- try {
505
- const response = await fetch(endpoint, {
506
- method: 'POST',
507
- headers: { 'Content-Type': 'application/json' },
508
- body: JSON.stringify(payload),
509
- });
510
-
511
- if (!response.ok) {
512
- const error = await response.json();
513
- console.error('Error from Image Describer API:', error);
514
- throw new Error(`Image Describer API Error: ${response.status}`);
515
- }
516
-
517
- const result = (await response.json()) as string;
518
- return result;
519
- } catch (err: any) {
520
- console.error('Error calling Image Describer API:', err.message);
521
- throw err;
522
- }
523
- },
524
-
525
- callUForm: async (imageUrl: string): Promise<string> => {
526
- try {
527
- const res = await fetch(imageUrl);
528
- console.log('IMAGE URL -> ', imageUrl);
529
- const blob = await res.arrayBuffer();
530
-
531
- const input = {
532
- image: [...new Uint8Array(blob)],
533
- prompt: env.PROMPT_IMAGE_DESCRIBER,
534
- max_tokens: 512,
535
- };
536
-
537
- const response = await env.AI.run('@cf/llava-hf/llava-1.5-7b-hf', input);
538
-
539
- if (!response) {
540
- throw new Error('UForm model did not return a response.');
541
- }
542
-
543
- console.log('UForm Model Response ->', response.description);
544
- return response.description;
545
- } catch (err: any) {
546
- console.error('Error using UForm model:', err.message);
547
- throw err;
548
- }
549
- },
550
451
 
551
452
  callAstrology: {
552
453
  getNatalChartInterpretation: async ({
@@ -13,31 +13,6 @@ export class AppContext {
13
13
  return buildLLMMessages(this.env);
14
14
  }
15
15
 
16
- kvCosmicMirrorArchetypesStore() {
17
- if (!this.env.KV_COSMIC_MIRROR_ARCHETYPES) {
18
- throw new Error(
19
- 'KV_COSMIC_MIRROR_ARCHETYPES is not defined in the environment.'
20
- );
21
- }
22
- return this.env.KV_COSMIC_MIRROR_ARCHETYPES;
23
- }
24
-
25
- kvCosmicMirrorManagementStore() {
26
- if (!this.env.KV_COSMIC_MIRROR_MANAGEMENT) {
27
- throw new Error(
28
- 'KV_COSMIC_MIRROR_MANAGEMENT is not defined in the environment.'
29
- );
30
- }
31
- return this.env.KV_COSMIC_MIRROR_MANAGEMENT;
32
- }
33
-
34
- kvConceptsStore() {
35
- if (!this.env.KV_CONCEPTS) {
36
- throw new Error('KV_CONCEPTS is not defined in the environment.');
37
- }
38
- return this.env.KV_CONCEPTS;
39
- }
40
-
41
16
  kvConceptFailuresStore() {
42
17
  if (!this.env.KV_CONCEPT_FAILURES) {
43
18
  throw new Error('KV_CONCEPT_FAILURES is not defined in the environment.');
@@ -45,22 +20,6 @@ export class AppContext {
45
20
  return this.env.KV_CONCEPT_FAILURES;
46
21
  }
47
22
 
48
- kvConceptNamesStore() {
49
- if (!this.env.KV_CONCEPT_NAMES) {
50
- throw new Error('KV_CONCEPT_NAMES is not defined in the environment.');
51
- }
52
- return this.env.KV_CONCEPT_NAMES;
53
- }
54
-
55
- kvConceptsManagementStore() {
56
- if (!this.env.KV_CONCEPTS_MANAGEMENT) {
57
- throw new Error(
58
- 'KV_CONCEPTS_MANAGEMENT is not defined in the environment.'
59
- );
60
- }
61
- return this.env.KV_CONCEPTS_MANAGEMENT;
62
- }
63
-
64
23
  kvAstroStore() {
65
24
  if (!this.env.KV_ASTRO) {
66
25
  throw new Error('KV_ASTRO is not defined in the environment.');
@@ -2,7 +2,7 @@ import {
2
2
  KVNamespaceListKey,
3
3
  KVNamespaceListResult,
4
4
  } from '@cloudflare/workers-types';
5
- import { and, eq, inArray, isNull } from 'drizzle-orm';
5
+ import { and, eq, inArray, isNull, sql } from 'drizzle-orm';
6
6
  import { drizzle } from 'drizzle-orm/d1';
7
7
  import { inject, injectable } from 'inversify';
8
8
  import 'reflect-metadata';
@@ -16,6 +16,7 @@ import {
16
16
  concepts,
17
17
  conceptsData,
18
18
  houseReports,
19
+ users,
19
20
  } from '../../db/schema';
20
21
  import {
21
22
  AstrologicalReport,
@@ -44,6 +45,7 @@ export class ConceptService {
44
45
  const drizzle = this.context.drizzle();
45
46
  const conceptsList = await drizzle
46
47
  .select({
48
+ id: concepts.id,
47
49
  slug: concepts.slug,
48
50
  planet1: concepts.planet1,
49
51
  planet2: concepts.planet2,
@@ -58,156 +60,6 @@ export class ConceptService {
58
60
  /**
59
61
  * Generate basic info for a concept: name, description, and poem.
60
62
  */
61
- async generateBasicInfo(
62
- conceptSlug: Concept,
63
- combinationString: string,
64
- override: boolean = false
65
- ): Promise<void> {
66
- console.log(
67
- `🚀 Generating basic info for concept: ${conceptSlug}, combination: ${combinationString}, override: ${override}`
68
- );
69
-
70
- const kvStore = this.context.kvConceptsStore();
71
- const kvFailuresStore = this.context.kvConceptFailuresStore();
72
- const kvKeyEN = buildConceptKVKey('en-us', conceptSlug, combinationString);
73
- const kvKeyPT = buildConceptKVKey('pt-br', conceptSlug, combinationString);
74
-
75
- // ✅ Use Durable Object stub
76
- const id = this.context.env.CONCEPT_NAMES_DO.idFromName(conceptSlug);
77
- const stub = this.context.env.CONCEPT_NAMES_DO.get(id);
78
-
79
- console.log(`📡 Fetching existing KV data for ${conceptSlug}...`);
80
- const existingEN = await this.getKVConcept(kvKeyEN);
81
- const existingPT = await this.getKVConcept(kvKeyPT);
82
-
83
- if (!override && existingEN.name && existingPT.name) {
84
- console.log(`⚡ Basic info already exists for ${conceptSlug}, skipping.`);
85
- return;
86
- }
87
-
88
- let attempts = 0;
89
- const maxAttempts = 3;
90
- const MAX_NAME_LENGTH = 44;
91
-
92
- while (attempts < maxAttempts) {
93
- let phase = 'generation';
94
- try {
95
- attempts++;
96
- console.log(
97
- `🔄 Attempt ${attempts} to generate basic info for ${conceptSlug}...`
98
- );
99
-
100
- let allNamesEN: string[] = [];
101
- let allNamesPT: string[] = [];
102
- const response = await stub.fetch(`https://internal/names`);
103
-
104
- if (response.ok) {
105
- const data = (await response.json()) as {
106
- 'en-us': string[];
107
- 'pt-br': string[];
108
- };
109
- allNamesEN = data['en-us'] || [];
110
- allNamesPT = data['pt-br'] || [];
111
- }
112
-
113
- console.log(`✏️ Generating new name...`);
114
- const messages = this.context
115
- .buildLLMMessages()
116
- .generateConceptBasicInfo({
117
- combination: combinationString,
118
- conceptSlug,
119
- existingNames:
120
- allNamesEN.length > 100 ? allNamesEN.slice(-100) : allNamesEN,
121
- });
122
-
123
- let aiResponse = await this.context
124
- .api()
125
- .callTogether.single(messages, {});
126
- if (!aiResponse) throw new Error(`❌ AI returned an empty response`);
127
-
128
- phase = 'cleaning';
129
- aiResponse = this.cleanAIResponse(aiResponse);
130
-
131
- console.log('!-- aiResponse -> ', aiResponse);
132
- console.log('!-- aiResponse length -> ', aiResponse.length);
133
-
134
- phase = 'parsing';
135
- let { nameEN, descriptionEN, poemEN, namePT, descriptionPT, poemPT } =
136
- this.parseBasicInfoResponse(aiResponse, conceptSlug);
137
-
138
- console.log(`🎭 Generated names: EN - "${nameEN}", PT - "${namePT}"`);
139
-
140
- // ✅ Forcefully trim the name to exactly 5 words if it's too long
141
- const enforceWordLimit = (name: string): string => {
142
- const words = name.split(/\s+/);
143
- if (words.length > 6) {
144
- return words.slice(0, 6).join(' ');
145
- }
146
- return name;
147
- };
148
-
149
- nameEN = enforceWordLimit(nameEN);
150
- namePT = enforceWordLimit(namePT);
151
-
152
- console.log(`✂️ Trimmed names: EN - "${nameEN}", PT - "${namePT}"`);
153
-
154
- // ✅ Check uniqueness before storing
155
- if (allNamesEN.includes(nameEN) || allNamesPT.includes(namePT)) {
156
- console.warn(
157
- `⚠️ Duplicate Name Detected: "${nameEN}" or "${namePT}"`
158
- );
159
- if (attempts >= maxAttempts) {
160
- console.log(
161
- `🚨 Max attempts reached. Storing name despite duplicate.`
162
- );
163
- await kvFailuresStore.put(
164
- `failures:duplicates:${conceptSlug}:${combinationString}`,
165
- JSON.stringify({
166
- nameEN,
167
- namePT,
168
- attempts,
169
- conceptSlug,
170
- combinationString,
171
- timestamp: new Date().toISOString(),
172
- })
173
- );
174
- break;
175
- }
176
- continue;
177
- }
178
-
179
- console.log(`📝 Storing names in Durable Object...`);
180
- // await stub.fetch(`https://internal/add-name`, { method: 'POST', body: JSON.stringify({ language: 'en-us', name: nameEN }), headers: { 'Content-Type': 'application/json' } });
181
- // await stub.fetch(`https://internal/add-name`, { method: 'POST', body: JSON.stringify({ language: 'pt-br', name: namePT }), headers: { 'Content-Type': 'application/json' } });
182
-
183
- Object.assign(existingEN, {
184
- name: nameEN,
185
- description: descriptionEN,
186
- poem: poemEN,
187
- status: 'idle',
188
- });
189
- await kvStore.put(kvKeyEN, JSON.stringify(existingEN));
190
-
191
- Object.assign(existingPT, {
192
- name: namePT,
193
- description: descriptionPT,
194
- poem: poemPT,
195
- status: 'idle',
196
- });
197
- await kvStore.put(kvKeyPT, JSON.stringify(existingPT));
198
-
199
- console.log(
200
- `✅ Stored basic info for ${conceptSlug}, combination: ${combinationString}.`
201
- );
202
- return;
203
- } catch (error) {
204
- console.error(
205
- `❌ Attempt ${attempts} failed at phase: ${phase}`,
206
- error
207
- );
208
- }
209
- }
210
- }
211
63
 
212
64
  private cleanAIResponse(response: string): string {
213
65
  console.log('🧼 Cleaning AI Response...');
@@ -1049,99 +901,7 @@ export class ConceptService {
1049
901
  /**
1050
902
  * Fetch and initialize a KVConcept object if not present.
1051
903
  */
1052
- private async getKVConcept(kvKey: string): Promise<KVConcept> {
1053
- const existingData = await this.context
1054
- .kvConceptsStore()
1055
- .get<KVConcept>(kvKey, 'json');
1056
-
1057
- if (existingData) {
1058
- return existingData;
1059
- }
1060
-
1061
- console.log(
1062
- `No existing KV data found. Creating default entry for key: ${kvKey}`
1063
- );
1064
- return {
1065
- name: '',
1066
- description: '',
1067
- content: [],
1068
- poem: [],
1069
- leonardoPrompt: '',
1070
- postImages: [],
1071
- reelImages: [null, null, null],
1072
- status: 'idle',
1073
- };
1074
- }
1075
-
1076
- private async findDuplicateNamesAndUsedNames(limit?: number) {
1077
- console.log(`🔍 Scanning KV for duplicate names...`);
1078
-
1079
- const kvStore = this.context.env.KV_CONCEPTS;
1080
- let cursor: string | undefined = undefined;
1081
- let nameUsageMapEN: Record<string, string[]> = {}; // { enName: [combination1, combination2] }
1082
- let usedNamesEN = new Set<string>(); // ✅ All used EN names
1083
- let usedNamesPT = new Set<string>(); // ✅ All used PT names
1084
- let totalKeysScanned = 0;
1085
-
1086
- do {
1087
- const response: KVNamespaceListResult<KVConcept> = (await kvStore.list({
1088
- prefix: 'concepts:',
1089
- cursor,
1090
- })) as KVNamespaceListResult<KVConcept, string>;
1091
-
1092
- const {
1093
- keys,
1094
- cursor: newCursor,
1095
- }: { keys: KVNamespaceListKey<KVConcept>[]; cursor?: string } = response;
1096
-
1097
- totalKeysScanned += keys.length;
1098
-
1099
- for (const key of keys) {
1100
- const kvData = (await kvStore.get(key.name, 'json')) as {
1101
- name: string;
1102
- ptName?: string;
1103
- };
1104
-
1105
- if (kvData?.name) {
1106
- usedNamesEN.add(kvData.name); // ✅ Store all used EN names
1107
-
1108
- if (!nameUsageMapEN[kvData.name]) {
1109
- nameUsageMapEN[kvData.name] = [];
1110
- }
1111
- nameUsageMapEN[kvData.name].push(key.name); // Store the key for that name
1112
- }
1113
-
1114
- if (kvData?.ptName) {
1115
- usedNamesPT.add(kvData.ptName); // ✅ Store all used PT names
1116
- }
1117
- }
1118
904
 
1119
- cursor = newCursor;
1120
- } while (cursor && (!limit || totalKeysScanned < limit));
1121
-
1122
- // ✅ Find names used more than once (EN)
1123
- let duplicateENNames = Object.entries(nameUsageMapEN)
1124
- .filter(([_, combinations]) => combinations.length > 1)
1125
- .map(([name, combinations]) => ({
1126
- name,
1127
- combinations,
1128
- }));
1129
-
1130
- // ✅ If limit is set, trim duplicates list
1131
- if (limit && duplicateENNames.length > limit) {
1132
- duplicateENNames = duplicateENNames.slice(0, limit);
1133
- }
1134
-
1135
- console.log(
1136
- `✅ Found ${duplicateENNames.length} duplicate EN names out of ${totalKeysScanned} entries.`
1137
- );
1138
-
1139
- return {
1140
- duplicateEntries: duplicateENNames, // ✅ List of EN names that need regeneration
1141
- usedNamesEN, // ✅ Set of all unique EN names
1142
- usedNamesPT, // ✅ Set of all unique PT names
1143
- };
1144
- }
1145
905
 
1146
906
  async generateAstroReportContent(
1147
907
  params:
@@ -1831,7 +1591,7 @@ export class ConceptService {
1831
1591
  );
1832
1592
  }
1833
1593
 
1834
- // src/services/ConceptService.ts
1594
+ // src/services/ts
1835
1595
  async checkConceptCompletion(
1836
1596
  conceptSlug: Concept,
1837
1597
  combinationString: string,
@@ -1866,4 +1626,172 @@ export class ConceptService {
1866
1626
 
1867
1627
  return hasPrompt && !!hasPostImages;
1868
1628
  }
1629
+
1630
+ async getLang(userId: string): Promise<'pt-br' | 'en-us'> {
1631
+ try {
1632
+ console.log(
1633
+ `🌍 [LANG QUERY] Fetching language preference for user: ${userId}`
1634
+ );
1635
+
1636
+ const drizzle = this.context.drizzle();
1637
+ const user = await drizzle
1638
+ .select({ language: users.language })
1639
+ .from(users)
1640
+ .where(eq(users.id, userId))
1641
+ .get();
1642
+
1643
+ if (!user || !user.language) {
1644
+ console.warn(
1645
+ `⚠️ [LANG WARNING] No language preference found for user: ${userId}, defaulting to 'pt-br'`
1646
+ );
1647
+ return 'pt-br';
1648
+ }
1649
+
1650
+ console.log(
1651
+ `✅ [LANG SUCCESS] Retrieved language for user: ${userId} -> ${user.language}`
1652
+ );
1653
+ return user.language as 'pt-br' | 'en-us';
1654
+ } catch (error) {
1655
+ console.error(
1656
+ `❌ [ERROR] Failed to fetch language for user ${userId}:`,
1657
+ error
1658
+ );
1659
+ throw new Error('Failed to fetch user language');
1660
+ }
1661
+ }
1662
+
1663
+ async queueUserConcepts(userId: string): Promise<void> {
1664
+ console.log(
1665
+ `[${new Date().toISOString()}] 🔧 Starting queueing of user concepts for user ${userId}`
1666
+ );
1667
+
1668
+ const lang = await this.getLang(userId);
1669
+ console.log(
1670
+ `[${new Date().toISOString()}] 📝 Retrieved language for user ${userId}: ${lang}`
1671
+ );
1672
+
1673
+ const db = this.context.drizzle();
1674
+ const { userConcepts } = schema;
1675
+
1676
+ // Fetch all available concepts
1677
+ const concepts = await this.getAllConcepts();
1678
+ if (!concepts || concepts.length === 0) {
1679
+ console.log(
1680
+ `[${new Date().toISOString()}] ⚠️ No concepts found to queue for user ${userId}`
1681
+ );
1682
+ return;
1683
+ }
1684
+ console.log(
1685
+ `[${new Date().toISOString()}] ✅ Retrieved ${
1686
+ concepts.length
1687
+ } concepts for user ${userId}`
1688
+ );
1689
+
1690
+ for (const concept of concepts) {
1691
+ const {
1692
+ id: conceptId,
1693
+ slug: conceptSlug,
1694
+ planet1,
1695
+ planet2,
1696
+ planet3,
1697
+ } = concept;
1698
+
1699
+ console.log(
1700
+ `[${new Date().toISOString()}] 🔧 Processing concept ${conceptSlug} for user ${userId}`
1701
+ );
1702
+
1703
+ const planets = [planet1, planet2, planet3];
1704
+ const userSigns = await this.getUserSigns(userId, planets);
1705
+ if (userSigns.length !== 3) {
1706
+ console.log(
1707
+ `[${new Date().toISOString()}] ⚠️ Missing astrological data for user ${userId} in concept ${conceptSlug}, found ${
1708
+ userSigns.length
1709
+ } signs`
1710
+ );
1711
+ continue; // Skip to the next concept if signs are missing
1712
+ }
1713
+ console.log(
1714
+ `[${new Date().toISOString()}] ✅ Retrieved user signs for ${conceptSlug}: ${userSigns
1715
+ .map((s) => `${s.name}: ${s.sign}`)
1716
+ .join(', ')}`
1717
+ );
1718
+
1719
+ const signMap = new Map(userSigns.map((s) => [s.name, s.sign]));
1720
+ const combinationString = [
1721
+ signMap.get(planet1),
1722
+ signMap.get(planet2),
1723
+ signMap.get(planet3),
1724
+ ].join('-');
1725
+ console.log(
1726
+ `[${new Date().toISOString()}] 🔗 Generated combination string for ${conceptSlug}: ${combinationString}`
1727
+ );
1728
+
1729
+ // Create or retrieve conceptCombination
1730
+ const conceptCombinationId = await this.getOrCreateConceptCombination(
1731
+ conceptId,
1732
+ combinationString,
1733
+ planets,
1734
+ userSigns
1735
+ );
1736
+ console.log(
1737
+ `[${new Date().toISOString()}] ✅ Created/Retrieved concept combination ID for ${conceptSlug}: ${conceptCombinationId}`
1738
+ );
1739
+
1740
+ // Check if a userConcept exists; create if not
1741
+ const existingUserConcept = await db
1742
+ .select({ conceptCombinationId: userConcepts.conceptCombinationId })
1743
+ .from(userConcepts)
1744
+ .where(
1745
+ and(
1746
+ eq(userConcepts.userId, userId),
1747
+ eq(userConcepts.conceptId, conceptId)
1748
+ )
1749
+ )
1750
+ .limit(1)
1751
+ .execute();
1752
+
1753
+ if (!existingUserConcept.length) {
1754
+ const userConceptId = uuidv4();
1755
+ await db
1756
+ .insert(userConcepts)
1757
+ .values({
1758
+ id: userConceptId,
1759
+ userId,
1760
+ conceptId,
1761
+ conceptCombinationId,
1762
+ createdAt: sql`CURRENT_TIMESTAMP`,
1763
+ updatedAt: sql`CURRENT_TIMESTAMP`,
1764
+ })
1765
+ .execute();
1766
+ console.log(
1767
+ `[${new Date().toISOString()}] ✅ Inserted new user concept with ID: ${userConceptId} for ${conceptSlug}`
1768
+ );
1769
+ } else {
1770
+ console.log(
1771
+ `[${new Date().toISOString()}] 🔍 Found existing user concept for user ${userId}, concept ${conceptSlug}`
1772
+ );
1773
+ }
1774
+
1775
+ // Queue the generation
1776
+ const queuePayload = {
1777
+ userId,
1778
+ conceptSlug,
1779
+ combinationString,
1780
+ lang,
1781
+ conceptCombinationId,
1782
+ };
1783
+ console.log(
1784
+ `[${new Date().toISOString()}] ✅ Queue Payload for ${conceptSlug}:`,
1785
+ queuePayload
1786
+ );
1787
+ await this.context.env.CONCEPT_GENERATION_QUEUE.send(queuePayload);
1788
+ console.log(
1789
+ `[${new Date().toISOString()}] ⏳ Queued concept generation for ${conceptSlug}:${combinationString}`
1790
+ );
1791
+ }
1792
+
1793
+ console.log(
1794
+ `[${new Date().toISOString()}] ✅ Completed queueing all concepts for user ${userId}`
1795
+ );
1796
+ }
1869
1797
  }
@@ -1,11 +1,15 @@
1
1
  import { eq } from 'drizzle-orm';
2
2
  import { injectable } from 'inversify';
3
+ import { payments, products, userProducts } from '../../db/schema';
3
4
  import { AppContext } from '../base';
4
- import { payments, userProducts } from '../../db/schema';
5
+ import { ConceptService } from './ConceptService';
5
6
 
6
7
  @injectable()
7
8
  export class PaymentService {
8
- constructor(private context: AppContext) {}
9
+ constructor(
10
+ private context: AppContext,
11
+ private conceptService: ConceptService
12
+ ) {}
9
13
 
10
14
  async handleCheckoutEvent(payload: any): Promise<void> {
11
15
  const { event, checkout } = payload;
@@ -57,6 +61,60 @@ export class PaymentService {
57
61
  private async handleCheckoutPaid(checkout: any): Promise<void> {
58
62
  const checkoutId = checkout.id;
59
63
  const db = this.context.drizzle();
64
+
65
+ // Fetch the userProduct to get userId and productId
66
+ const userProduct = await db
67
+ .select({
68
+ userId: userProducts.userId,
69
+ productId: userProducts.productId,
70
+ })
71
+ .from(userProducts)
72
+ .where(eq(userProducts.checkoutId, checkoutId))
73
+ .limit(1)
74
+ .get();
75
+
76
+ if (!userProduct) {
77
+ console.log(
78
+ `[${new Date().toISOString()}] ⚠️ No user product found for checkoutId: ${checkoutId}`
79
+ );
80
+ return;
81
+ }
82
+
83
+ // Fetch the product slug
84
+ const product = await db
85
+ .select({ slug: products.slug })
86
+ .from(products)
87
+ .where(eq(products.id, userProduct.productId))
88
+ .limit(1)
89
+ .get();
90
+
91
+ if (!product) {
92
+ console.log(
93
+ `[${new Date().toISOString()}] ⚠️ No product found for productId: ${
94
+ userProduct.productId
95
+ }`
96
+ );
97
+ return;
98
+ }
99
+
100
+ // Queue concept generations if the product slug is "concepts"
101
+ if (product.slug === 'concepts') {
102
+ try {
103
+ await this.conceptService.queueUserConcepts(userProduct.userId);
104
+ console.log(
105
+ `[${new Date().toISOString()}] ⏳ Queued concept generations for user ${
106
+ userProduct.userId
107
+ } after successful payment`
108
+ );
109
+ } catch {
110
+ console.log(
111
+ `[${new Date().toISOString()}] ⚠️ Error queuing concept generations for user ${
112
+ userProduct.userId
113
+ } after successful payment`
114
+ );
115
+ }
116
+ }
117
+
60
118
  // Update userProducts to 'unlocked'
61
119
  await db
62
120
  .update(userProducts)
@@ -78,6 +136,7 @@ export class PaymentService {
78
136
  const checkoutId = checkout.id;
79
137
  const db = this.context.drizzle();
80
138
  // Update userProducts to 'locked'
139
+
81
140
  await db
82
141
  .update(userProducts)
83
142
  .set({ status: 'locked', updatedAt: new Date() })