docit-ai 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +1 -0
- package/INSTRUCTIONS.MD +70 -0
- package/dist/ai/engine.d.ts +24 -0
- package/dist/ai/engine.d.ts.map +1 -0
- package/dist/ai/engine.js +69 -0
- package/dist/ai/engine.js.map +1 -0
- package/dist/ai/prompts.d.ts +11 -0
- package/dist/ai/prompts.d.ts.map +1 -0
- package/dist/ai/prompts.js +116 -0
- package/dist/ai/prompts.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +384 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/formatter/index.d.ts +13 -0
- package/dist/formatter/index.d.ts.map +1 -0
- package/dist/formatter/index.js +76 -0
- package/dist/formatter/index.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/parser/extractor.d.ts +23 -0
- package/dist/parser/extractor.d.ts.map +1 -0
- package/dist/parser/extractor.js +148 -0
- package/dist/parser/extractor.js.map +1 -0
- package/dist/scanner/index.d.ts +11 -0
- package/dist/scanner/index.d.ts.map +1 -0
- package/dist/scanner/index.js +30 -0
- package/dist/scanner/index.js.map +1 -0
- package/package.json +31 -0
- package/server/.env.example +22 -0
- package/server/package-lock.json +4426 -0
- package/server/package.json +31 -0
- package/server/src/config/database.d.ts +18 -0
- package/server/src/config/database.d.ts.map +1 -0
- package/server/src/config/database.js +8 -0
- package/server/src/config/database.js.map +1 -0
- package/server/src/config/database.ts +31 -0
- package/server/src/index.d.ts +2 -0
- package/server/src/index.d.ts.map +1 -0
- package/server/src/index.js +33 -0
- package/server/src/index.js.map +1 -0
- package/server/src/index.ts +55 -0
- package/server/src/middleware/auth.d.ts +12 -0
- package/server/src/middleware/auth.d.ts.map +1 -0
- package/server/src/middleware/auth.js +35 -0
- package/server/src/middleware/auth.js.map +1 -0
- package/server/src/middleware/auth.ts +56 -0
- package/server/src/routes/auth.d.ts +3 -0
- package/server/src/routes/auth.d.ts.map +1 -0
- package/server/src/routes/auth.js +145 -0
- package/server/src/routes/auth.js.map +1 -0
- package/server/src/routes/auth.ts +185 -0
- package/server/src/routes/dashboard.ts +243 -0
- package/server/src/routes/generate.d.ts +3 -0
- package/server/src/routes/generate.d.ts.map +1 -0
- package/server/src/routes/generate.js +55 -0
- package/server/src/routes/generate.js.map +1 -0
- package/server/src/routes/generate.ts +75 -0
- package/server/src/routes/payment.ts +192 -0
- package/server/src/services/ai.d.ts +10 -0
- package/server/src/services/ai.d.ts.map +1 -0
- package/server/src/services/ai.js +75 -0
- package/server/src/services/ai.js.map +1 -0
- package/server/src/services/ai.ts +99 -0
- package/server/src/services/payment.ts +141 -0
- package/server/src/types/flutterwave.d.ts +60 -0
- package/server/supabase_payments.sql +35 -0
- package/server/supabase_schema.sql +90 -0
- package/server/tsconfig.json +17 -0
- package/server/vercel.json +15 -0
- package/server/verify_dashboard.ts +126 -0
- package/src/ai/engine.ts +103 -0
- package/src/ai/prompts.ts +123 -0
- package/src/cli/index.ts +552 -0
- package/src/formatter/index.ts +110 -0
- package/src/index.ts +11 -0
- package/src/parser/extractor.ts +211 -0
- package/src/scanner/index.ts +49 -0
- package/tsconfig.json +43 -0
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { Router } from "express";
|
|
2
|
+
import { supabase } from "../config/database.js";
|
|
3
|
+
import { authMiddleware, type AuthRequest } from "../middleware/auth.js";
|
|
4
|
+
import { PLANS, getPlanById } from "../services/payment.js";
|
|
5
|
+
|
|
6
|
+
const router = Router();
|
|
7
|
+
|
|
8
|
+
// GET /dashboard/stats - Dashboard statistics
|
|
9
|
+
router.get("/stats", authMiddleware, async (req: AuthRequest, res) => {
|
|
10
|
+
try {
|
|
11
|
+
const user = req.user!;
|
|
12
|
+
|
|
13
|
+
// Get total documents generated count
|
|
14
|
+
const { count: documentsGenerated } = await supabase
|
|
15
|
+
.from("document_generations")
|
|
16
|
+
.select("*", { count: "exact", head: true })
|
|
17
|
+
.eq("user_id", user.id);
|
|
18
|
+
|
|
19
|
+
// Get total files processed
|
|
20
|
+
const { data: filesData } = await supabase
|
|
21
|
+
.from("document_generations")
|
|
22
|
+
.select("files_count")
|
|
23
|
+
.eq("user_id", user.id);
|
|
24
|
+
|
|
25
|
+
const filesProcessed =
|
|
26
|
+
filesData?.reduce((sum, doc) => sum + (doc.files_count ?? 0), 0) ?? 0;
|
|
27
|
+
|
|
28
|
+
// Get last sync (last document generation time)
|
|
29
|
+
const { data: lastDoc } = await supabase
|
|
30
|
+
.from("document_generations")
|
|
31
|
+
.select("created_at")
|
|
32
|
+
.eq("user_id", user.id)
|
|
33
|
+
.order("created_at", { ascending: false })
|
|
34
|
+
.limit(1)
|
|
35
|
+
.single();
|
|
36
|
+
|
|
37
|
+
res.json({
|
|
38
|
+
creditsAvailable: user.credits,
|
|
39
|
+
documentsGenerated: documentsGenerated ?? 0,
|
|
40
|
+
filesProcessed,
|
|
41
|
+
lastSync: lastDoc?.created_at ?? null,
|
|
42
|
+
});
|
|
43
|
+
} catch (error) {
|
|
44
|
+
console.error("Stats error:", error);
|
|
45
|
+
res.status(500).json({ error: "Failed to fetch stats" });
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// GET /dashboard/activity - Recent user activity
|
|
50
|
+
router.get("/activity", authMiddleware, async (req: AuthRequest, res) => {
|
|
51
|
+
try {
|
|
52
|
+
const user = req.user!;
|
|
53
|
+
const limit = Math.min(parseInt(String(req.query.limit ?? "10"), 10), 50);
|
|
54
|
+
|
|
55
|
+
// Get recent credit transactions
|
|
56
|
+
const { data: transactions } = await supabase
|
|
57
|
+
.from("credit_transactions")
|
|
58
|
+
.select("*")
|
|
59
|
+
.eq("user_id", user.id)
|
|
60
|
+
.order("created_at", { ascending: false })
|
|
61
|
+
.limit(limit);
|
|
62
|
+
|
|
63
|
+
const activities = (transactions ?? []).map((tx) => {
|
|
64
|
+
let activityType:
|
|
65
|
+
| "document_generated"
|
|
66
|
+
| "credits_purchased"
|
|
67
|
+
| "files_processed";
|
|
68
|
+
|
|
69
|
+
if (tx.type === "purchase") {
|
|
70
|
+
activityType = "credits_purchased";
|
|
71
|
+
} else if (tx.type === "usage") {
|
|
72
|
+
activityType = tx.description?.includes("file")
|
|
73
|
+
? "files_processed"
|
|
74
|
+
: "document_generated";
|
|
75
|
+
} else {
|
|
76
|
+
activityType = "document_generated";
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
id: tx.id,
|
|
81
|
+
type: activityType,
|
|
82
|
+
description: tx.description,
|
|
83
|
+
timestamp: tx.created_at,
|
|
84
|
+
creditsUsed: tx.type === "usage" ? Math.abs(tx.amount) : null,
|
|
85
|
+
};
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
res.json({ activities });
|
|
89
|
+
} catch (error) {
|
|
90
|
+
console.error("Activity error:", error);
|
|
91
|
+
res.status(500).json({ error: "Failed to fetch activity" });
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// GET /dashboard/usage - Credit usage over time
|
|
96
|
+
router.get("/usage", authMiddleware, async (req: AuthRequest, res) => {
|
|
97
|
+
try {
|
|
98
|
+
const user = req.user!;
|
|
99
|
+
const period = (req.query.period as string) ?? "30d";
|
|
100
|
+
|
|
101
|
+
// Calculate date range
|
|
102
|
+
const days = period === "7d" ? 7 : period === "90d" ? 90 : 30;
|
|
103
|
+
const startDate = new Date();
|
|
104
|
+
startDate.setDate(startDate.getDate() - days);
|
|
105
|
+
|
|
106
|
+
// Get usage transactions
|
|
107
|
+
const { data: transactions } = await supabase
|
|
108
|
+
.from("credit_transactions")
|
|
109
|
+
.select("amount, created_at")
|
|
110
|
+
.eq("user_id", user.id)
|
|
111
|
+
.eq("type", "usage")
|
|
112
|
+
.gte("created_at", startDate.toISOString())
|
|
113
|
+
.order("created_at", { ascending: true });
|
|
114
|
+
|
|
115
|
+
// Group by date
|
|
116
|
+
const usageByDate = new Map<string, number>();
|
|
117
|
+
(transactions ?? []).forEach((tx) => {
|
|
118
|
+
const date = tx.created_at.split("T")[0];
|
|
119
|
+
usageByDate.set(date, (usageByDate.get(date) ?? 0) + Math.abs(tx.amount));
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const data = Array.from(usageByDate.entries()).map(([date, credits]) => ({
|
|
123
|
+
date,
|
|
124
|
+
credits,
|
|
125
|
+
}));
|
|
126
|
+
|
|
127
|
+
const totalCreditsUsed = data.reduce((sum, d) => sum + d.credits, 0);
|
|
128
|
+
|
|
129
|
+
res.json({
|
|
130
|
+
period,
|
|
131
|
+
data,
|
|
132
|
+
totalCreditsUsed,
|
|
133
|
+
});
|
|
134
|
+
} catch (error) {
|
|
135
|
+
console.error("Usage error:", error);
|
|
136
|
+
res.status(500).json({ error: "Failed to fetch usage" });
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// GET /dashboard/documents - Document generation history
|
|
141
|
+
router.get("/documents", authMiddleware, async (req: AuthRequest, res) => {
|
|
142
|
+
try {
|
|
143
|
+
const user = req.user!;
|
|
144
|
+
const page = Math.max(parseInt(String(req.query.page ?? "1"), 10), 1);
|
|
145
|
+
const limit = Math.min(parseInt(String(req.query.limit ?? "20"), 10), 100);
|
|
146
|
+
const offset = (page - 1) * limit;
|
|
147
|
+
|
|
148
|
+
// Get total count
|
|
149
|
+
const { count: total } = await supabase
|
|
150
|
+
.from("document_generations")
|
|
151
|
+
.select("*", { count: "exact", head: true })
|
|
152
|
+
.eq("user_id", user.id);
|
|
153
|
+
|
|
154
|
+
// Get paginated documents
|
|
155
|
+
const { data: documents } = await supabase
|
|
156
|
+
.from("document_generations")
|
|
157
|
+
.select("*")
|
|
158
|
+
.eq("user_id", user.id)
|
|
159
|
+
.order("created_at", { ascending: false })
|
|
160
|
+
.range(offset, offset + limit - 1);
|
|
161
|
+
|
|
162
|
+
res.json({
|
|
163
|
+
documents: (documents ?? []).map((doc) => ({
|
|
164
|
+
id: doc.id,
|
|
165
|
+
project: doc.project_name,
|
|
166
|
+
files: doc.files_count,
|
|
167
|
+
credits: doc.credits_used,
|
|
168
|
+
createdAt: doc.created_at,
|
|
169
|
+
})),
|
|
170
|
+
pagination: {
|
|
171
|
+
page,
|
|
172
|
+
limit,
|
|
173
|
+
total: total ?? 0,
|
|
174
|
+
hasMore: offset + limit < (total ?? 0),
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
} catch (error) {
|
|
178
|
+
console.error("Documents error:", error);
|
|
179
|
+
res.status(500).json({ error: "Failed to fetch documents" });
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// GET /dashboard/billing - Current billing info
|
|
184
|
+
router.get("/billing", authMiddleware, async (req: AuthRequest, res) => {
|
|
185
|
+
try {
|
|
186
|
+
const user = req.user!;
|
|
187
|
+
|
|
188
|
+
// Get most recent successful payment
|
|
189
|
+
const { data: lastPayment } = await supabase
|
|
190
|
+
.from("payment_transactions")
|
|
191
|
+
.select("*")
|
|
192
|
+
.eq("user_id", user.id)
|
|
193
|
+
.eq("status", "successful")
|
|
194
|
+
.order("created_at", { ascending: false })
|
|
195
|
+
.limit(1)
|
|
196
|
+
.single();
|
|
197
|
+
|
|
198
|
+
let subscriptionStatus: "active" | "cancelled" | "past_due" | "none" =
|
|
199
|
+
"none";
|
|
200
|
+
let currentPlan: string | null = null;
|
|
201
|
+
let planName: string | null = null;
|
|
202
|
+
let planAmount: number | null = null;
|
|
203
|
+
let planCurrency: string | null = null;
|
|
204
|
+
let nextBillingDate: string | null = null;
|
|
205
|
+
let maxCredits = 10; // Default for free tier
|
|
206
|
+
|
|
207
|
+
if (lastPayment) {
|
|
208
|
+
const plan = getPlanById(lastPayment.plan_id);
|
|
209
|
+
if (plan) {
|
|
210
|
+
currentPlan = plan.id;
|
|
211
|
+
planName = plan.name;
|
|
212
|
+
planAmount = plan.amount;
|
|
213
|
+
planCurrency = plan.currency;
|
|
214
|
+
maxCredits = plan.credits;
|
|
215
|
+
|
|
216
|
+
// Calculate next billing date (30 days from last payment)
|
|
217
|
+
const lastPaymentDate = new Date(lastPayment.created_at);
|
|
218
|
+
const nextBilling = new Date(lastPaymentDate);
|
|
219
|
+
nextBilling.setDate(nextBilling.getDate() + 30);
|
|
220
|
+
nextBillingDate = nextBilling.toISOString();
|
|
221
|
+
|
|
222
|
+
// Check if subscription is still active (within 30 days)
|
|
223
|
+
subscriptionStatus = nextBilling > new Date() ? "active" : "past_due";
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
res.json({
|
|
228
|
+
currentPlan,
|
|
229
|
+
planName,
|
|
230
|
+
planAmount,
|
|
231
|
+
planCurrency,
|
|
232
|
+
nextBillingDate,
|
|
233
|
+
creditBalance: user.credits,
|
|
234
|
+
maxCredits,
|
|
235
|
+
subscriptionStatus,
|
|
236
|
+
});
|
|
237
|
+
} catch (error) {
|
|
238
|
+
console.error("Billing error:", error);
|
|
239
|
+
res.status(500).json({ error: "Failed to fetch billing info" });
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
export default router;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generate.d.ts","sourceRoot":"","sources":["generate.ts"],"names":[],"mappings":"AAKA,QAAA,MAAM,MAAM,4CAAW,CAAC;AA2DxB,eAAe,MAAM,CAAC"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Router } from "express";
|
|
2
|
+
import { supabase } from "../config/database.js";
|
|
3
|
+
import { authMiddleware } from "../middleware/auth.js";
|
|
4
|
+
import { generateDocumentation } from "../services/ai.js";
|
|
5
|
+
const router = Router();
|
|
6
|
+
// Generate documentation
|
|
7
|
+
router.post("/", authMiddleware, async (req, res) => {
|
|
8
|
+
try {
|
|
9
|
+
const user = req.user;
|
|
10
|
+
const { codeSkeleton, views, fileCount } = req.body;
|
|
11
|
+
if (!codeSkeleton) {
|
|
12
|
+
res.status(400).json({ error: "Code skeleton is required" });
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
// Calculate credits needed
|
|
16
|
+
const creditsPerFile = parseInt(process.env.CREDITS_PER_FILE ?? "1", 10);
|
|
17
|
+
const filesProcessed = fileCount ?? 1;
|
|
18
|
+
const creditsNeeded = filesProcessed * creditsPerFile;
|
|
19
|
+
// Check if user has enough credits
|
|
20
|
+
if (user.credits < creditsNeeded) {
|
|
21
|
+
res.status(402).json({
|
|
22
|
+
error: "Insufficient credits",
|
|
23
|
+
required: creditsNeeded,
|
|
24
|
+
available: user.credits,
|
|
25
|
+
});
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
// Generate documentation
|
|
29
|
+
const results = await generateDocumentation(codeSkeleton, views);
|
|
30
|
+
// Deduct credits
|
|
31
|
+
const newBalance = user.credits - creditsNeeded;
|
|
32
|
+
await supabase
|
|
33
|
+
.from("users")
|
|
34
|
+
.update({ credits: newBalance })
|
|
35
|
+
.eq("id", user.id);
|
|
36
|
+
// Log transaction
|
|
37
|
+
await supabase.from("credit_transactions").insert({
|
|
38
|
+
user_id: user.id,
|
|
39
|
+
amount: -creditsNeeded,
|
|
40
|
+
type: "usage",
|
|
41
|
+
description: `Generated documentation for ${filesProcessed} file(s)`,
|
|
42
|
+
});
|
|
43
|
+
res.json({
|
|
44
|
+
results,
|
|
45
|
+
creditsUsed: creditsNeeded,
|
|
46
|
+
creditsRemaining: newBalance,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
console.error("Generation error:", error);
|
|
51
|
+
res.status(500).json({ error: "Failed to generate documentation" });
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
export default router;
|
|
55
|
+
//# sourceMappingURL=generate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generate.js","sourceRoot":"","sources":["generate.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACjD,OAAO,EAAE,cAAc,EAAoB,MAAM,uBAAuB,CAAC;AACzE,OAAO,EAAE,qBAAqB,EAAwB,MAAM,mBAAmB,CAAC;AAEhF,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC;AAExB,yBAAyB;AACzB,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,cAAc,EAAE,KAAK,EAAE,GAAgB,EAAE,GAAG,EAAE,EAAE;IAC/D,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,GAAG,CAAC,IAAK,CAAC;QACvB,MAAM,EAAE,YAAY,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,GAAG,CAAC,IAE9C,CAAC;QAEF,IAAI,CAAC,YAAY,EAAE,CAAC;YAClB,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,2BAA2B,EAAE,CAAC,CAAC;YAC7D,OAAO;QACT,CAAC;QAED,2BAA2B;QAC3B,MAAM,cAAc,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,gBAAgB,IAAI,GAAG,EAAE,EAAE,CAAC,CAAC;QACzE,MAAM,cAAc,GAAG,SAAS,IAAI,CAAC,CAAC;QACtC,MAAM,aAAa,GAAG,cAAc,GAAG,cAAc,CAAC;QAEtD,mCAAmC;QACnC,IAAI,IAAI,CAAC,OAAO,GAAG,aAAa,EAAE,CAAC;YACjC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;gBACnB,KAAK,EAAE,sBAAsB;gBAC7B,QAAQ,EAAE,aAAa;gBACvB,SAAS,EAAE,IAAI,CAAC,OAAO;aACxB,CAAC,CAAC;YACH,OAAO;QACT,CAAC;QAED,yBAAyB;QACzB,MAAM,OAAO,GAAG,MAAM,qBAAqB,CAAC,YAAY,EAAE,KAAK,CAAC,CAAC;QAEjE,iBAAiB;QACjB,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,GAAG,aAAa,CAAC;QAChD,MAAM,QAAQ;aACX,IAAI,CAAC,OAAO,CAAC;aACb,MAAM,CAAC,EAAE,OAAO,EAAE,UAAU,EAAE,CAAC;aAC/B,EAAE,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,CAAC,CAAC;QAErB,kBAAkB;QAClB,MAAM,QAAQ,CAAC,IAAI,CAAC,qBAAqB,CAAC,CAAC,MAAM,CAAC;YAChD,OAAO,EAAE,IAAI,CAAC,EAAE;YAChB,MAAM,EAAE,CAAC,aAAa;YACtB,IAAI,EAAE,OAAO;YACb,WAAW,EAAE,+BAA+B,cAAc,UAAU;SACrE,CAAC,CAAC;QAEH,GAAG,CAAC,IAAI,CAAC;YACP,OAAO;YACP,WAAW,EAAE,aAAa;YAC1B,gBAAgB,EAAE,UAAU;SAC7B,CAAC,CAAC;IACL,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,KAAK,CAAC,mBAAmB,EAAE,KAAK,CAAC,CAAC;QAC1C,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,kCAAkC,EAAE,CAAC,CAAC;IACtE,CAAC;AACH,CAAC,CAAC,CAAC;AAEH,eAAe,MAAM,CAAC"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { Router } from "express";
|
|
2
|
+
import { supabase } from "../config/database.js";
|
|
3
|
+
import { authMiddleware, type AuthRequest } from "../middleware/auth.js";
|
|
4
|
+
import { generateDocumentation, type GenerateRequest } from "../services/ai.js";
|
|
5
|
+
|
|
6
|
+
const router = Router();
|
|
7
|
+
|
|
8
|
+
// Generate documentation
|
|
9
|
+
router.post("/", authMiddleware, async (req: AuthRequest, res) => {
|
|
10
|
+
try {
|
|
11
|
+
const user = req.user!;
|
|
12
|
+
const { codeSkeleton, views, fileCount, projectName } =
|
|
13
|
+
req.body as GenerateRequest & {
|
|
14
|
+
fileCount?: number;
|
|
15
|
+
projectName?: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
if (!codeSkeleton) {
|
|
19
|
+
res.status(400).json({ error: "Code skeleton is required" });
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Calculate credits needed
|
|
24
|
+
const creditsPerFile = parseInt(process.env.CREDITS_PER_FILE ?? "1", 10);
|
|
25
|
+
const filesProcessed = fileCount ?? 1;
|
|
26
|
+
const creditsNeeded = filesProcessed * creditsPerFile;
|
|
27
|
+
|
|
28
|
+
// Check if user has enough credits
|
|
29
|
+
if (user.credits < creditsNeeded) {
|
|
30
|
+
res.status(402).json({
|
|
31
|
+
error: "Insufficient credits",
|
|
32
|
+
required: creditsNeeded,
|
|
33
|
+
available: user.credits,
|
|
34
|
+
});
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Generate documentation
|
|
39
|
+
const results = await generateDocumentation(codeSkeleton, views);
|
|
40
|
+
|
|
41
|
+
// Deduct credits
|
|
42
|
+
const newBalance = user.credits - creditsNeeded;
|
|
43
|
+
await supabase
|
|
44
|
+
.from("users")
|
|
45
|
+
.update({ credits: newBalance })
|
|
46
|
+
.eq("id", user.id);
|
|
47
|
+
|
|
48
|
+
// Log credit transaction
|
|
49
|
+
await supabase.from("credit_transactions").insert({
|
|
50
|
+
user_id: user.id,
|
|
51
|
+
amount: -creditsNeeded,
|
|
52
|
+
type: "usage",
|
|
53
|
+
description: `Generated documentation for ${filesProcessed} file(s)`,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Log document generation
|
|
57
|
+
await supabase.from("document_generations").insert({
|
|
58
|
+
user_id: user.id,
|
|
59
|
+
project_name: projectName ?? "Untitled Project",
|
|
60
|
+
files_count: filesProcessed,
|
|
61
|
+
credits_used: creditsNeeded,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
res.json({
|
|
65
|
+
results,
|
|
66
|
+
creditsUsed: creditsNeeded,
|
|
67
|
+
creditsRemaining: newBalance,
|
|
68
|
+
});
|
|
69
|
+
} catch (error) {
|
|
70
|
+
console.error("Generation error:", error);
|
|
71
|
+
res.status(500).json({ error: "Failed to generate documentation" });
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
export default router;
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { Router } from "express";
|
|
2
|
+
import { supabase } from "../config/database.js";
|
|
3
|
+
import { authMiddleware, type AuthRequest } from "../middleware/auth.js";
|
|
4
|
+
import {
|
|
5
|
+
PLANS,
|
|
6
|
+
getPlanById,
|
|
7
|
+
initiatePayment,
|
|
8
|
+
verifyTransaction,
|
|
9
|
+
} from "../services/payment.js";
|
|
10
|
+
|
|
11
|
+
const router = Router();
|
|
12
|
+
|
|
13
|
+
// Get available plans
|
|
14
|
+
router.get("/plans", (_req, res) => {
|
|
15
|
+
res.json({
|
|
16
|
+
plans: PLANS.map((p) => ({
|
|
17
|
+
id: p.id,
|
|
18
|
+
name: p.name,
|
|
19
|
+
amount: p.amount,
|
|
20
|
+
currency: p.currency,
|
|
21
|
+
credits: p.credits,
|
|
22
|
+
description: p.description,
|
|
23
|
+
})),
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// Initiate payment
|
|
28
|
+
router.post("/initiate", authMiddleware, async (req: AuthRequest, res) => {
|
|
29
|
+
try {
|
|
30
|
+
const user = req.user!;
|
|
31
|
+
const { planId, redirectUrl } = req.body as {
|
|
32
|
+
planId?: string;
|
|
33
|
+
redirectUrl?: string;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
if (!planId) {
|
|
37
|
+
res.status(400).json({ error: "Plan ID is required" });
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const plan = getPlanById(planId);
|
|
42
|
+
if (!plan) {
|
|
43
|
+
res.status(400).json({ error: "Invalid plan ID" });
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const result = await initiatePayment({
|
|
48
|
+
email: user.email,
|
|
49
|
+
userId: user.id,
|
|
50
|
+
planId,
|
|
51
|
+
redirectUrl:
|
|
52
|
+
redirectUrl ??
|
|
53
|
+
`${process.env.FRONTEND_URL ?? "http://localhost:3000"}/payment/callback`,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Store pending transaction
|
|
57
|
+
await supabase.from("payment_transactions").insert({
|
|
58
|
+
user_id: user.id,
|
|
59
|
+
tx_ref: result.tx_ref,
|
|
60
|
+
plan_id: planId,
|
|
61
|
+
amount: plan.amount,
|
|
62
|
+
currency: plan.currency,
|
|
63
|
+
credits: plan.credits,
|
|
64
|
+
status: "pending",
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
res.json({
|
|
68
|
+
paymentLink: result.link,
|
|
69
|
+
txRef: result.tx_ref,
|
|
70
|
+
});
|
|
71
|
+
} catch (error) {
|
|
72
|
+
console.error("Payment initiation error:", error);
|
|
73
|
+
res.status(500).json({ error: "Failed to initiate payment" });
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Webhook for Flutterwave
|
|
78
|
+
router.post("/webhook", async (req, res) => {
|
|
79
|
+
try {
|
|
80
|
+
// Verify webhook signature
|
|
81
|
+
const secretHash = process.env.FLUTTERWAVE_SECRET_HASH;
|
|
82
|
+
const signature = req.headers["verif-hash"];
|
|
83
|
+
|
|
84
|
+
if (secretHash && signature !== secretHash) {
|
|
85
|
+
res.status(401).json({ error: "Invalid signature" });
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const { event, data } = req.body as {
|
|
90
|
+
event?: string;
|
|
91
|
+
data?: { id?: number; tx_ref?: string; status?: string };
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
if (event === "charge.completed" && data?.status === "successful") {
|
|
95
|
+
const transactionId = data.id;
|
|
96
|
+
if (!transactionId) {
|
|
97
|
+
res.status(400).json({ error: "Missing transaction ID" });
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Verify the transaction
|
|
102
|
+
const verification = await verifyTransaction(String(transactionId));
|
|
103
|
+
|
|
104
|
+
if (verification.status === "successful" && verification.userId) {
|
|
105
|
+
// Update transaction status
|
|
106
|
+
await supabase
|
|
107
|
+
.from("payment_transactions")
|
|
108
|
+
.update({ status: "successful", flutterwave_id: transactionId })
|
|
109
|
+
.eq("tx_ref", data.tx_ref);
|
|
110
|
+
|
|
111
|
+
// Add credits to user
|
|
112
|
+
const { data: user } = await supabase
|
|
113
|
+
.from("users")
|
|
114
|
+
.select("credits")
|
|
115
|
+
.eq("id", verification.userId)
|
|
116
|
+
.single();
|
|
117
|
+
|
|
118
|
+
if (user) {
|
|
119
|
+
const newCredits = (user.credits ?? 0) + (verification.credits ?? 0);
|
|
120
|
+
|
|
121
|
+
await supabase
|
|
122
|
+
.from("users")
|
|
123
|
+
.update({ credits: newCredits })
|
|
124
|
+
.eq("id", verification.userId);
|
|
125
|
+
|
|
126
|
+
// Log credit transaction
|
|
127
|
+
await supabase.from("credit_transactions").insert({
|
|
128
|
+
user_id: verification.userId,
|
|
129
|
+
amount: verification.credits,
|
|
130
|
+
type: "purchase",
|
|
131
|
+
description: `Subscription payment - ${verification.credits} credits`,
|
|
132
|
+
reference_id: data.tx_ref,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
res.json({ status: "ok" });
|
|
139
|
+
} catch (error) {
|
|
140
|
+
console.error("Webhook error:", error);
|
|
141
|
+
res.status(500).json({ error: "Webhook processing failed" });
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// Verify payment (for redirect callback)
|
|
146
|
+
router.get("/verify/:txRef", authMiddleware, async (req: AuthRequest, res) => {
|
|
147
|
+
try {
|
|
148
|
+
const { txRef } = req.params;
|
|
149
|
+
const user = req.user!;
|
|
150
|
+
|
|
151
|
+
// Get transaction from database
|
|
152
|
+
const { data: transaction } = await supabase
|
|
153
|
+
.from("payment_transactions")
|
|
154
|
+
.select("*")
|
|
155
|
+
.eq("tx_ref", txRef)
|
|
156
|
+
.eq("user_id", user.id)
|
|
157
|
+
.single();
|
|
158
|
+
|
|
159
|
+
if (!transaction) {
|
|
160
|
+
res.status(404).json({ error: "Transaction not found" });
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
res.json({
|
|
165
|
+
status: transaction.status,
|
|
166
|
+
credits: transaction.credits,
|
|
167
|
+
amount: transaction.amount,
|
|
168
|
+
currency: transaction.currency,
|
|
169
|
+
});
|
|
170
|
+
} catch (error) {
|
|
171
|
+
console.error("Verify error:", error);
|
|
172
|
+
res.status(500).json({ error: "Verification failed" });
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// Get payment history
|
|
177
|
+
router.get("/history", authMiddleware, async (req: AuthRequest, res) => {
|
|
178
|
+
const user = req.user!;
|
|
179
|
+
|
|
180
|
+
const { data: transactions } = await supabase
|
|
181
|
+
.from("payment_transactions")
|
|
182
|
+
.select("*")
|
|
183
|
+
.eq("user_id", user.id)
|
|
184
|
+
.order("created_at", { ascending: false })
|
|
185
|
+
.limit(20);
|
|
186
|
+
|
|
187
|
+
res.json({
|
|
188
|
+
transactions: transactions ?? [],
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
export default router;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface GenerateRequest {
|
|
2
|
+
codeSkeleton: string;
|
|
3
|
+
views?: string[];
|
|
4
|
+
}
|
|
5
|
+
export interface GenerateResult {
|
|
6
|
+
view: string;
|
|
7
|
+
content: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function generateDocumentation(codeSkeleton: string, views?: string[]): Promise<GenerateResult[]>;
|
|
10
|
+
//# sourceMappingURL=ai.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ai.d.ts","sourceRoot":"","sources":["ai.ts"],"names":[],"mappings":"AA2BA,MAAM,WAAW,eAAe;IAC9B,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAMD,wBAAsB,qBAAqB,CACzC,YAAY,EAAE,MAAM,EACpB,KAAK,GAAE,MAAM,EAA2C,GACvD,OAAO,CAAC,cAAc,EAAE,CAAC,CAsD3B"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { GoogleGenAI } from "@google/genai";
|
|
2
|
+
const STRATEGIC_VIEW_PROMPT = `You are a technical writer creating documentation for Project Managers.
|
|
3
|
+
Focus on business value and project status. NO code snippets. Use bullet points and bold text.
|
|
4
|
+
Include "What this feature enables" and a "Completeness Score" (0-100%).`;
|
|
5
|
+
const TECHNICAL_VIEW_PROMPT = `You are a technical writer creating documentation for Developers.
|
|
6
|
+
Focus on architecture and integration. Include function signatures, dependency maps, and code examples.
|
|
7
|
+
Use strict Markdown with syntax highlighting.`;
|
|
8
|
+
const PERSONAL_VIEW_PROMPT = `You are a technical writer creating documentation for Solo Developers.
|
|
9
|
+
Focus on productivity. Generate To-Do lists from TODO comments and Technical Debt summaries.
|
|
10
|
+
Use concise, checklist-style formatting.`;
|
|
11
|
+
function getPromptForView(view) {
|
|
12
|
+
switch (view) {
|
|
13
|
+
case "strategic":
|
|
14
|
+
return STRATEGIC_VIEW_PROMPT;
|
|
15
|
+
case "technical":
|
|
16
|
+
return TECHNICAL_VIEW_PROMPT;
|
|
17
|
+
case "personal":
|
|
18
|
+
return PERSONAL_VIEW_PROMPT;
|
|
19
|
+
default:
|
|
20
|
+
return TECHNICAL_VIEW_PROMPT;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
async function sleep(ms) {
|
|
24
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
25
|
+
}
|
|
26
|
+
export async function generateDocumentation(codeSkeleton, views = ["strategic", "technical", "personal"]) {
|
|
27
|
+
const apiKey = process.env.GEMINI_API_KEY;
|
|
28
|
+
if (!apiKey) {
|
|
29
|
+
throw new Error("Gemini API key not configured");
|
|
30
|
+
}
|
|
31
|
+
const ai = new GoogleGenAI({ apiKey });
|
|
32
|
+
const model = "gemini-2.0-flash";
|
|
33
|
+
const results = [];
|
|
34
|
+
for (const view of views) {
|
|
35
|
+
const systemPrompt = getPromptForView(view);
|
|
36
|
+
let lastError;
|
|
37
|
+
for (let attempt = 0; attempt < 5; attempt++) {
|
|
38
|
+
try {
|
|
39
|
+
const response = await ai.models.generateContent({
|
|
40
|
+
model,
|
|
41
|
+
contents: [
|
|
42
|
+
{
|
|
43
|
+
role: "user",
|
|
44
|
+
parts: [
|
|
45
|
+
{
|
|
46
|
+
text: `${systemPrompt}\n\n---\n\nAnalyze the following code skeleton and generate documentation:\n\n${codeSkeleton}`,
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
});
|
|
52
|
+
results.push({
|
|
53
|
+
view,
|
|
54
|
+
content: response.text ?? "",
|
|
55
|
+
});
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
lastError = error;
|
|
60
|
+
const status = error.status;
|
|
61
|
+
if (status === 503 || status === 429) {
|
|
62
|
+
const waitTime = Math.pow(2, attempt) * 1000;
|
|
63
|
+
await sleep(waitTime);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (results.length <= views.indexOf(view)) {
|
|
70
|
+
throw lastError ?? new Error("Failed to generate documentation");
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return results;
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=ai.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ai.js","sourceRoot":"","sources":["ai.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAE5C,MAAM,qBAAqB,GAAG;;yEAE2C,CAAC;AAE1E,MAAM,qBAAqB,GAAG;;8CAEgB,CAAC;AAE/C,MAAM,oBAAoB,GAAG;;yCAEY,CAAC;AAE1C,SAAS,gBAAgB,CAAC,IAAY;IACpC,QAAQ,IAAI,EAAE,CAAC;QACb,KAAK,WAAW;YACd,OAAO,qBAAqB,CAAC;QAC/B,KAAK,WAAW;YACd,OAAO,qBAAqB,CAAC;QAC/B,KAAK,UAAU;YACb,OAAO,oBAAoB,CAAC;QAC9B;YACE,OAAO,qBAAqB,CAAC;IACjC,CAAC;AACH,CAAC;AAYD,KAAK,UAAU,KAAK,CAAC,EAAU;IAC7B,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC;AAC3D,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,qBAAqB,CACzC,YAAoB,EACpB,QAAkB,CAAC,WAAW,EAAE,WAAW,EAAE,UAAU,CAAC;IAExD,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC;IAC1C,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,MAAM,IAAI,KAAK,CAAC,+BAA+B,CAAC,CAAC;IACnD,CAAC;IAED,MAAM,EAAE,GAAG,IAAI,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC;IACvC,MAAM,KAAK,GAAG,kBAAkB,CAAC;IACjC,MAAM,OAAO,GAAqB,EAAE,CAAC;IAErC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,YAAY,GAAG,gBAAgB,CAAC,IAAI,CAAC,CAAC;QAE5C,IAAI,SAA4B,CAAC;QACjC,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,GAAG,CAAC,EAAE,OAAO,EAAE,EAAE,CAAC;YAC7C,IAAI,CAAC;gBACH,MAAM,QAAQ,GAAG,MAAM,EAAE,CAAC,MAAM,CAAC,eAAe,CAAC;oBAC/C,KAAK;oBACL,QAAQ,EAAE;wBACR;4BACE,IAAI,EAAE,MAAM;4BACZ,KAAK,EAAE;gCACL;oCACE,IAAI,EAAE,GAAG,YAAY,iFAAiF,YAAY,EAAE;iCACrH;6BACF;yBACF;qBACF;iBACF,CAAC,CAAC;gBAEH,OAAO,CAAC,IAAI,CAAC;oBACX,IAAI;oBACJ,OAAO,EAAE,QAAQ,CAAC,IAAI,IAAI,EAAE;iBAC7B,CAAC,CAAC;gBACH,MAAM;YACR,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,SAAS,GAAG,KAAc,CAAC;gBAC3B,MAAM,MAAM,GAAI,KAA6B,CAAC,MAAM,CAAC;gBAErD,IAAI,MAAM,KAAK,GAAG,IAAI,MAAM,KAAK,GAAG,EAAE,CAAC;oBACrC,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;oBAC7C,MAAM,KAAK,CAAC,QAAQ,CAAC,CAAC;oBACtB,SAAS;gBACX,CAAC;gBACD,MAAM,KAAK,CAAC;YACd,CAAC;QACH,CAAC;QAED,IAAI,OAAO,CAAC,MAAM,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;YAC1C,MAAM,SAAS,IAAI,IAAI,KAAK,CAAC,kCAAkC,CAAC,CAAC;QACnE,CAAC;IACH,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC"}
|