spaps 0.5.0 ā 0.5.2
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/AI_TOOLS.json +114 -0
- package/README.md +204 -38
- package/bin/spaps.js +5 -312
- package/package.json +10 -6
- package/src/ai-helper.js +20 -20
- package/src/ai-tool-spec.js +298 -0
- package/src/cli-dispatcher.js +233 -0
- package/src/config.js +5 -0
- package/src/docs-html.js +3 -2
- package/src/docs-system.js +78 -129
- package/src/doctor.js +217 -0
- package/src/handlers.js +174 -0
- package/src/help-system.js +5 -3
- package/src/local-server.js +181 -16
package/src/handlers.js
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const { DEFAULT_PORT } = require('./config');
|
|
4
|
+
const { handleError } = require('./error-handler');
|
|
5
|
+
const { showInteractiveHelp, showQuickHelp } = require('./help-system');
|
|
6
|
+
const { showInteractiveDocs, showQuickReference, searchDocs } = require('./docs-system');
|
|
7
|
+
const { getQuickStartInstructions, getServerStatus, runQuickTest } = require('./ai-helper');
|
|
8
|
+
const { buildToolSpec } = require('./ai-tool-spec');
|
|
9
|
+
const { runDoctor } = require('./doctor');
|
|
10
|
+
|
|
11
|
+
function createHandlers(version, logo) {
|
|
12
|
+
return {
|
|
13
|
+
local: async ({ options }) => {
|
|
14
|
+
const isJson = options.json;
|
|
15
|
+
if (!isJson) console.log(logo);
|
|
16
|
+
try {
|
|
17
|
+
const LocalServer = require('./local-server.js');
|
|
18
|
+
const server = new LocalServer({ port: options.port, json: isJson, stripeMode: options.stripe, seedMode: options.seed });
|
|
19
|
+
if (isJson) {
|
|
20
|
+
await server.start();
|
|
21
|
+
console.log(JSON.stringify({
|
|
22
|
+
success: true,
|
|
23
|
+
command: 'local',
|
|
24
|
+
server: {
|
|
25
|
+
url: `http://localhost:${options.port}`,
|
|
26
|
+
docs: `http://localhost:${options.port}/docs`,
|
|
27
|
+
mode: 'local-development',
|
|
28
|
+
port: Number(options.port),
|
|
29
|
+
features: { autoAuth: true, corsEnabled: true, testUsers: ['user', 'admin', 'premium'], apiKeyRequired: false, stripeMode: options.stripe, seeded: options.seed }
|
|
30
|
+
}
|
|
31
|
+
}));
|
|
32
|
+
} else {
|
|
33
|
+
await server.start();
|
|
34
|
+
if (options.open) {
|
|
35
|
+
const { exec } = require('child_process');
|
|
36
|
+
const url = `http://localhost:${options.port}/docs`;
|
|
37
|
+
const start = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
38
|
+
exec(`${start} ${url}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
process.on('SIGINT', () => {
|
|
42
|
+
if (!isJson) console.log(chalk.yellow('\nš Shutting down SPAPS local server...'));
|
|
43
|
+
process.exit(0);
|
|
44
|
+
});
|
|
45
|
+
} catch (error) {
|
|
46
|
+
handleError(error, { port: options.port, command: 'local' }, { json: isJson });
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
quickstart: async ({ options }) => {
|
|
50
|
+
const instructions = getQuickStartInstructions(options.port);
|
|
51
|
+
if (options.json) {
|
|
52
|
+
console.log(JSON.stringify(instructions, null, 2));
|
|
53
|
+
} else {
|
|
54
|
+
console.log(chalk.yellow('\nš SPAPS Quick Start Instructions\n'));
|
|
55
|
+
console.log('1. Install SDK: npm install spaps-sdk');
|
|
56
|
+
console.log('2. Create test file with the code above');
|
|
57
|
+
console.log('3. Run: node test-spaps.js');
|
|
58
|
+
console.log('\nFor JSON output: npx spaps quickstart --json');
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
status: async ({ options }) => {
|
|
62
|
+
const status = await getServerStatus(options.port);
|
|
63
|
+
if (options.json) {
|
|
64
|
+
console.log(JSON.stringify(status));
|
|
65
|
+
} else {
|
|
66
|
+
if (!status.running) {
|
|
67
|
+
console.log(chalk.red('\nā SPAPS server is not running'));
|
|
68
|
+
console.log('Start it with:');
|
|
69
|
+
console.log(chalk.cyan(` npx spaps local --port ${options.port}`));
|
|
70
|
+
console.log();
|
|
71
|
+
} else {
|
|
72
|
+
console.log(chalk.green('\nā
SPAPS server is running!\n'));
|
|
73
|
+
console.log(' URL:', chalk.cyan(status.url));
|
|
74
|
+
console.log(' Docs:', chalk.cyan(status.docs));
|
|
75
|
+
console.log();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
test: async ({ options }) => {
|
|
80
|
+
const result = await runQuickTest(options.port);
|
|
81
|
+
console.log(JSON.stringify(result, null, 2));
|
|
82
|
+
},
|
|
83
|
+
init: async ({ options }) => {
|
|
84
|
+
const isJson = options.json;
|
|
85
|
+
const envContent = `# SPAPS Local Development\nSPAPS_API_URL=http://localhost:${DEFAULT_PORT}\n# SPAPS_API_KEY=your-api-key-here\n`;
|
|
86
|
+
const result = { success: true, command: 'init', files_created: [], files_skipped: [], next_steps: ['npx spaps local', 'npm install @spaps/sdk', 'Start coding!'] };
|
|
87
|
+
if (!fs.existsSync('.env.local')) {
|
|
88
|
+
fs.writeFileSync('.env.local', envContent);
|
|
89
|
+
result.files_created.push('.env.local');
|
|
90
|
+
if (!isJson) console.log(chalk.green('ā
Created .env.local'));
|
|
91
|
+
} else {
|
|
92
|
+
result.files_skipped.push('.env.local');
|
|
93
|
+
result.message = '.env.local already exists';
|
|
94
|
+
if (!isJson) console.log(chalk.yellow('ā ļø .env.local already exists'));
|
|
95
|
+
}
|
|
96
|
+
if (isJson) {
|
|
97
|
+
console.log(JSON.stringify(result));
|
|
98
|
+
} else {
|
|
99
|
+
console.log();
|
|
100
|
+
console.log(chalk.green('⨠SPAPS initialized!'));
|
|
101
|
+
console.log();
|
|
102
|
+
console.log('Next steps:');
|
|
103
|
+
console.log(chalk.cyan(' 1. Run: npx spaps local'));
|
|
104
|
+
console.log(chalk.cyan(' 2. Install SDK: npm install @spaps/sdk'));
|
|
105
|
+
console.log(chalk.cyan(' 3. Start coding!'));
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
create: () => {
|
|
109
|
+
console.log(chalk.yellow('š SPAPS'));
|
|
110
|
+
console.log(chalk.yellow(`š§ 'spaps create' coming in v0.3.0!`));
|
|
111
|
+
console.log();
|
|
112
|
+
console.log('For now, check out our examples:');
|
|
113
|
+
console.log(chalk.cyan(' https://github.com/yourusername/sweet-potato/tree/main/examples'));
|
|
114
|
+
},
|
|
115
|
+
types: () => {
|
|
116
|
+
console.log(chalk.yellow('š SPAPS'));
|
|
117
|
+
console.log(chalk.yellow(`š§ 'spaps types' coming in v0.4.0!`));
|
|
118
|
+
},
|
|
119
|
+
help: async ({ options }) => {
|
|
120
|
+
if (options.interactive) {
|
|
121
|
+
await showInteractiveHelp();
|
|
122
|
+
} else if (options.quick) {
|
|
123
|
+
showQuickHelp();
|
|
124
|
+
} else {
|
|
125
|
+
showQuickHelp();
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
docs: async ({ options }) => {
|
|
129
|
+
if (options.search) {
|
|
130
|
+
const results = searchDocs(options.search);
|
|
131
|
+
if (options.json) {
|
|
132
|
+
console.log(JSON.stringify({ results }, null, 2));
|
|
133
|
+
} else {
|
|
134
|
+
console.log(chalk.yellow(`\nš Search results for "${options.search}":\n`));
|
|
135
|
+
if (results.length === 0) {
|
|
136
|
+
console.log(chalk.gray(' No results found'));
|
|
137
|
+
} else {
|
|
138
|
+
results.forEach((result, i) => {
|
|
139
|
+
console.log(chalk.green(` ${i + 1}. ${result.title}`));
|
|
140
|
+
console.log(chalk.gray(` ${result.preview}`));
|
|
141
|
+
console.log();
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
console.log(chalk.blue(' Run: npx spaps docs --interactive'));
|
|
145
|
+
console.log(chalk.blue(' to browse full documentation\n'));
|
|
146
|
+
}
|
|
147
|
+
} else if (options.interactive) {
|
|
148
|
+
await showInteractiveDocs();
|
|
149
|
+
} else {
|
|
150
|
+
showQuickReference();
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
tools: async ({ options }) => {
|
|
154
|
+
const spec = buildToolSpec({ format: options.format || 'openai', port: options.port });
|
|
155
|
+
if (options.json) {
|
|
156
|
+
console.log(JSON.stringify(spec, null, 2));
|
|
157
|
+
} else {
|
|
158
|
+
console.log(chalk.yellow('\nš SPAPS AI Tool Spec (OpenAI-style)\n'));
|
|
159
|
+
console.log('Base URL:', spec.base_url);
|
|
160
|
+
console.log('Tools:');
|
|
161
|
+
spec.tools.forEach((t, i) => {
|
|
162
|
+
console.log(chalk.green(` ${i + 1}. ${t.name}`), '-', t.description);
|
|
163
|
+
console.log(chalk.gray(` ${t.method} ${t.path}`));
|
|
164
|
+
});
|
|
165
|
+
console.log('\nTip: npx spaps tools --json > spaps-tools.json');
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
doctor: async ({ options }) => {
|
|
169
|
+
await runDoctor({ port: options.port || DEFAULT_PORT, stripe: options.stripe || null, json: options.json });
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
module.exports = { createHandlers };
|
package/src/help-system.js
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
const chalk = require('chalk');
|
|
7
7
|
const prompts = require('prompts');
|
|
8
8
|
|
|
9
|
+
const { DEFAULT_PORT } = require('./config');
|
|
10
|
+
|
|
9
11
|
const HELP_TREE = {
|
|
10
12
|
root: {
|
|
11
13
|
question: 'What would you like to do?',
|
|
@@ -45,12 +47,12 @@ const HELP_TREE = {
|
|
|
45
47
|
title: 'Quick start (default settings)',
|
|
46
48
|
value: 'quick-start',
|
|
47
49
|
command: 'npx spaps local',
|
|
48
|
-
description:
|
|
50
|
+
description: `Start on port ${DEFAULT_PORT}`
|
|
49
51
|
},
|
|
50
52
|
{
|
|
51
53
|
title: 'Custom port',
|
|
52
54
|
value: 'custom-port',
|
|
53
|
-
command:
|
|
55
|
+
command: `npx spaps local --port ${DEFAULT_PORT + 1}`,
|
|
54
56
|
description: 'Choose your own port'
|
|
55
57
|
},
|
|
56
58
|
{
|
|
@@ -484,4 +486,4 @@ module.exports = {
|
|
|
484
486
|
showInteractiveHelp,
|
|
485
487
|
showQuickHelp,
|
|
486
488
|
HELP_TREE
|
|
487
|
-
};
|
|
489
|
+
};
|
package/src/local-server.js
CHANGED
|
@@ -8,19 +8,24 @@
|
|
|
8
8
|
const express = require('express');
|
|
9
9
|
const cors = require('cors');
|
|
10
10
|
const chalk = require('chalk');
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
11
13
|
const { generateDocsHTML } = require('./docs-html');
|
|
14
|
+
let swaggerUiDist = null;
|
|
15
|
+
try { swaggerUiDist = require('swagger-ui-dist'); } catch {}
|
|
12
16
|
const StripeLocalManager = require('./stripe-local');
|
|
13
17
|
const LocalAdminManager = require('./admin-local');
|
|
14
18
|
|
|
15
19
|
// Stripe configuration for test mode
|
|
16
20
|
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY || 'sk_test_51S1WOy2HT0E1dOewiHvzt7T96PDwjocSDDUuc2ur569AVA5fDj4UpNM66lujrda1tTYrgooG0Z1dNFZfwEZuZdcA00nuVLJW67');
|
|
17
21
|
const STRIPE_PUBLISHABLE_KEY = process.env.STRIPE_PUBLISHABLE_KEY || 'pk_test_51S1WOy2HT0E1dOewb2EkxZIaPkz7v3zMM9VxuBoxgNILYMmS85I4zrAWTkevyUQcaWlWUoC2NYnB8X5ZKd5e7Ifc005IzIW6H2';
|
|
18
|
-
const USE_REAL_STRIPE = process.env.USE_REAL_STRIPE !== 'false'; // Default to true
|
|
19
22
|
|
|
20
23
|
class LocalServer {
|
|
21
24
|
constructor(options = {}) {
|
|
22
25
|
this.port = options.port || process.env.PORT || 3456;
|
|
23
26
|
this.json = options.json || false;
|
|
27
|
+
this.stripeMode = options.stripeMode || (process.env.USE_REAL_STRIPE === 'false' ? 'mock' : 'real');
|
|
28
|
+
this.seedMode = options.seedMode || 'none';
|
|
24
29
|
this.app = express();
|
|
25
30
|
this.stripeManager = null;
|
|
26
31
|
this.adminManager = new LocalAdminManager();
|
|
@@ -29,6 +34,15 @@ class LocalServer {
|
|
|
29
34
|
this.setupStripeRoutes();
|
|
30
35
|
this.setupAdminRoutes();
|
|
31
36
|
this.setupCatchAll();
|
|
37
|
+
|
|
38
|
+
// Optional demo seeding (idempotent)
|
|
39
|
+
if (this.seedMode === 'demo') {
|
|
40
|
+
try {
|
|
41
|
+
this.seedDemoData();
|
|
42
|
+
} catch (e) {
|
|
43
|
+
if (!this.json) console.warn(chalk.yellow(`ā ļø Seed failed: ${e.message}`));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
32
46
|
}
|
|
33
47
|
|
|
34
48
|
setupMiddleware() {
|
|
@@ -44,6 +58,12 @@ class LocalServer {
|
|
|
44
58
|
this.app.use(express.json());
|
|
45
59
|
this.app.use(express.urlencoded({ extended: true }));
|
|
46
60
|
|
|
61
|
+
// Serve Swagger UI assets locally if available
|
|
62
|
+
if (swaggerUiDist && typeof swaggerUiDist.getAbsoluteFSPath === 'function') {
|
|
63
|
+
const uiPath = swaggerUiDist.getAbsoluteFSPath();
|
|
64
|
+
this.app.use('/swagger-ui', express.static(uiPath));
|
|
65
|
+
}
|
|
66
|
+
|
|
47
67
|
// Local mode indicator
|
|
48
68
|
this.app.use((req, res, next) => {
|
|
49
69
|
res.setHeader('X-SPAPS-Mode', 'local-development');
|
|
@@ -67,6 +87,16 @@ class LocalServer {
|
|
|
67
87
|
}
|
|
68
88
|
|
|
69
89
|
setupRoutes() {
|
|
90
|
+
// OpenAPI JSON - try to serve repo spec; fallback to manifest; else minimal stub
|
|
91
|
+
this.app.get('/openapi.json', async (_req, res) => {
|
|
92
|
+
try {
|
|
93
|
+
const spec = await this.loadOpenApiSpec();
|
|
94
|
+
return res.json(spec);
|
|
95
|
+
} catch (e) {
|
|
96
|
+
return res.status(500).json({ error: 'Failed to generate OpenAPI', message: e.message });
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
70
100
|
// Health check
|
|
71
101
|
this.app.get('/health', (req, res) => {
|
|
72
102
|
res.json({
|
|
@@ -167,7 +197,7 @@ class LocalServer {
|
|
|
167
197
|
// Stripe checkout sessions endpoint - REAL or MOCK based on config
|
|
168
198
|
this.app.post('/api/stripe/checkout-sessions', async (req, res) => {
|
|
169
199
|
try {
|
|
170
|
-
if (
|
|
200
|
+
if (this.stripeMode === 'real') {
|
|
171
201
|
// Real Stripe checkout session
|
|
172
202
|
const { product_name, amount, currency = 'usd', success_url, cancel_url, price_id } = req.body;
|
|
173
203
|
|
|
@@ -254,7 +284,7 @@ class LocalServer {
|
|
|
254
284
|
// Stripe products endpoint - REAL or MOCK based on config
|
|
255
285
|
this.app.get('/api/stripe/products', async (req, res) => {
|
|
256
286
|
try {
|
|
257
|
-
if (
|
|
287
|
+
if (this.stripeMode === 'real') {
|
|
258
288
|
// Fetch real Stripe products
|
|
259
289
|
const products = await stripe.products.list({
|
|
260
290
|
active: req.query.active !== undefined ? req.query.active === 'true' : undefined,
|
|
@@ -418,7 +448,7 @@ class LocalServer {
|
|
|
418
448
|
// Admin product sync endpoint - REAL or MOCK based on config
|
|
419
449
|
this.app.post('/api/v1/admin/products/sync', async (req, res) => {
|
|
420
450
|
try {
|
|
421
|
-
if (
|
|
451
|
+
if (this.stripeMode === 'real') {
|
|
422
452
|
// Get local products from admin manager
|
|
423
453
|
const localProducts = this.adminManager.listProducts();
|
|
424
454
|
const syncResults = [];
|
|
@@ -532,19 +562,148 @@ class LocalServer {
|
|
|
532
562
|
});
|
|
533
563
|
});
|
|
534
564
|
|
|
535
|
-
// Documentation endpoint
|
|
565
|
+
// Documentation endpoint: prefer Swagger UI bound to /openapi.json
|
|
536
566
|
this.app.get('/docs', (req, res) => {
|
|
537
|
-
|
|
567
|
+
if (swaggerUiDist && typeof swaggerUiDist.getAbsoluteFSPath === 'function') {
|
|
568
|
+
res.type('html').send(`
|
|
569
|
+
<!DOCTYPE html>
|
|
570
|
+
<html>
|
|
571
|
+
<head>
|
|
572
|
+
<meta charset="utf-8" />
|
|
573
|
+
<title>SPAPS API Docs</title>
|
|
574
|
+
<link rel="stylesheet" href="/swagger-ui/swagger-ui.css" />
|
|
575
|
+
<style>body { margin: 0; } #swagger-ui { max-width: 100%; }</style>
|
|
576
|
+
</head>
|
|
577
|
+
<body>
|
|
578
|
+
<div id="swagger-ui"></div>
|
|
579
|
+
<script src="/swagger-ui/swagger-ui-bundle.js"></script>
|
|
580
|
+
<script src="/swagger-ui/swagger-ui-standalone-preset.js"></script>
|
|
581
|
+
<script>
|
|
582
|
+
window.onload = () => {
|
|
583
|
+
window.ui = SwaggerUIBundle({
|
|
584
|
+
url: '/openapi.json',
|
|
585
|
+
dom_id: '#swagger-ui',
|
|
586
|
+
deepLinking: true,
|
|
587
|
+
presets: [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset],
|
|
588
|
+
layout: 'BaseLayout'
|
|
589
|
+
});
|
|
590
|
+
};
|
|
591
|
+
</script>
|
|
592
|
+
</body>
|
|
593
|
+
</html>`);
|
|
594
|
+
} else {
|
|
595
|
+
// Fallback to the existing docs page if Swagger UI is not available
|
|
596
|
+
const msg = 'Using fallback docs page. Install Swagger UI assets for the full API explorer: npm install swagger-ui-dist';
|
|
597
|
+
res.send(generateDocsHTML(this.port, msg));
|
|
598
|
+
}
|
|
538
599
|
});
|
|
539
600
|
}
|
|
540
601
|
|
|
602
|
+
async loadOpenApiSpec() {
|
|
603
|
+
// Try YAML OpenAPI
|
|
604
|
+
const yamlCandidates = [
|
|
605
|
+
path.resolve(process.cwd(), 'docs/api-reference.yaml'),
|
|
606
|
+
path.resolve(__dirname, '../../../docs/api-reference.yaml')
|
|
607
|
+
];
|
|
608
|
+
for (const p of yamlCandidates) {
|
|
609
|
+
try {
|
|
610
|
+
if (fs.existsSync(p)) {
|
|
611
|
+
let yaml;
|
|
612
|
+
try {
|
|
613
|
+
yaml = require('js-yaml');
|
|
614
|
+
} catch {}
|
|
615
|
+
if (yaml) {
|
|
616
|
+
const content = fs.readFileSync(p, 'utf8');
|
|
617
|
+
const parsed = yaml.load(content);
|
|
618
|
+
// ensure servers list points to local
|
|
619
|
+
parsed.servers = [{ url: `http://localhost:${this.port}` }];
|
|
620
|
+
return parsed;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
} catch {}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// Fallback: build from manifest
|
|
627
|
+
const manifestCandidates = [
|
|
628
|
+
path.resolve(process.cwd(), 'docs/manifest.json'),
|
|
629
|
+
path.resolve(__dirname, '../../../docs/manifest.json')
|
|
630
|
+
];
|
|
631
|
+
for (const p of manifestCandidates) {
|
|
632
|
+
try {
|
|
633
|
+
if (fs.existsSync(p)) {
|
|
634
|
+
const manifest = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
635
|
+
return this.buildOpenApiFromManifest(manifest);
|
|
636
|
+
}
|
|
637
|
+
} catch {}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// Last resort: minimal stub
|
|
641
|
+
return {
|
|
642
|
+
openapi: '3.0.0',
|
|
643
|
+
info: { title: 'SPAPS Local API', version: '0.0.0' },
|
|
644
|
+
servers: [{ url: `http://localhost:${this.port}` }],
|
|
645
|
+
paths: {
|
|
646
|
+
'/health': { get: { summary: 'Health', responses: { '200': { description: 'OK' } } } }
|
|
647
|
+
}
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
buildOpenApiFromManifest(manifest) {
|
|
652
|
+
const spec = {
|
|
653
|
+
openapi: '3.0.0',
|
|
654
|
+
info: { title: 'SPAPS API', version: String(manifest.version || '1.0.0') },
|
|
655
|
+
servers: [{ url: `http://localhost:${this.port}` }],
|
|
656
|
+
paths: {}
|
|
657
|
+
};
|
|
658
|
+
const toPath = (p) => p.replace(/:(\w+)/g, '{$1}');
|
|
659
|
+
for (const ep of manifest.endpoints || []) {
|
|
660
|
+
const pathKey = toPath(ep.path);
|
|
661
|
+
if (!spec.paths[pathKey]) spec.paths[pathKey] = {};
|
|
662
|
+
spec.paths[pathKey][String(ep.method || 'GET').toLowerCase()] = {
|
|
663
|
+
summary: ep.description || `${ep.method} ${ep.path}`,
|
|
664
|
+
tags: ep.tags || [],
|
|
665
|
+
responses: { '200': { description: 'OK' } }
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
return spec;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
seedDemoData() {
|
|
672
|
+
// Add a couple of customers and a completed + pending order if none exist
|
|
673
|
+
const existingCustomers = this.adminManager.listCustomers();
|
|
674
|
+
const products = this.adminManager.listProducts();
|
|
675
|
+
if (existingCustomers.length === 0 && products.length > 0) {
|
|
676
|
+
const alice = this.adminManager.createCustomer({ email: 'alice@example.com', name: 'Alice' });
|
|
677
|
+
const bob = this.adminManager.createCustomer({ email: 'bob@example.com', name: 'Bob' });
|
|
678
|
+
const p = products[0];
|
|
679
|
+
const order1 = this.adminManager.createOrder({
|
|
680
|
+
customer_id: alice.id,
|
|
681
|
+
customer_email: alice.email,
|
|
682
|
+
product_id: p.id,
|
|
683
|
+
price_id: p.price_id,
|
|
684
|
+
amount: p.price,
|
|
685
|
+
currency: p.currency
|
|
686
|
+
});
|
|
687
|
+
this.adminManager.updateOrderStatus(order1.id, 'completed');
|
|
688
|
+
this.adminManager.createOrder({
|
|
689
|
+
customer_id: bob.id,
|
|
690
|
+
customer_email: bob.email,
|
|
691
|
+
product_id: p.id,
|
|
692
|
+
price_id: p.price_id,
|
|
693
|
+
amount: p.price,
|
|
694
|
+
currency: p.currency
|
|
695
|
+
});
|
|
696
|
+
if (!this.json) console.log(chalk.gray('š± Seeded demo customers and orders'));
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
541
700
|
setupStripeRoutes() {
|
|
542
701
|
// Enhanced Stripe Product Management - Full CRUD
|
|
543
702
|
|
|
544
703
|
// GET /api/stripe/products - List all products with filtering
|
|
545
704
|
this.app.get('/api/stripe/products', async (req, res) => {
|
|
546
705
|
try {
|
|
547
|
-
if (
|
|
706
|
+
if (this.stripeMode === 'real') {
|
|
548
707
|
const { active, category, limit = '100' } = req.query;
|
|
549
708
|
|
|
550
709
|
const products = await stripe.products.list({
|
|
@@ -620,7 +779,7 @@ class LocalServer {
|
|
|
620
779
|
});
|
|
621
780
|
}
|
|
622
781
|
|
|
623
|
-
if (
|
|
782
|
+
if (this.stripeMode === 'real') {
|
|
624
783
|
// Create product in Stripe
|
|
625
784
|
const stripeProduct = await stripe.products.create({
|
|
626
785
|
name,
|
|
@@ -718,7 +877,7 @@ class LocalServer {
|
|
|
718
877
|
const { productId } = req.params;
|
|
719
878
|
const { name, description, images, metadata, active } = req.body;
|
|
720
879
|
|
|
721
|
-
if (
|
|
880
|
+
if (this.stripeMode === 'real') {
|
|
722
881
|
// Update in Stripe
|
|
723
882
|
const updateData = {};
|
|
724
883
|
if (name !== undefined) updateData.name = name;
|
|
@@ -780,7 +939,7 @@ class LocalServer {
|
|
|
780
939
|
});
|
|
781
940
|
}
|
|
782
941
|
|
|
783
|
-
if (
|
|
942
|
+
if (this.stripeMode === 'real') {
|
|
784
943
|
const priceData = {
|
|
785
944
|
product: product_id,
|
|
786
945
|
unit_amount: parseInt(unit_amount),
|
|
@@ -852,7 +1011,7 @@ class LocalServer {
|
|
|
852
1011
|
});
|
|
853
1012
|
}
|
|
854
1013
|
|
|
855
|
-
if (
|
|
1014
|
+
if (this.stripeMode === 'real') {
|
|
856
1015
|
const stripeProduct = await stripe.products.update(productId, {
|
|
857
1016
|
default_price: price_id
|
|
858
1017
|
});
|
|
@@ -895,7 +1054,7 @@ class LocalServer {
|
|
|
895
1054
|
try {
|
|
896
1055
|
const { productId } = req.params;
|
|
897
1056
|
|
|
898
|
-
if (
|
|
1057
|
+
if (this.stripeMode === 'real') {
|
|
899
1058
|
// Archive in Stripe (can't truly delete)
|
|
900
1059
|
const stripeProduct = await stripe.products.update(productId, {
|
|
901
1060
|
active: false
|
|
@@ -1058,7 +1217,7 @@ class LocalServer {
|
|
|
1058
1217
|
try {
|
|
1059
1218
|
let event;
|
|
1060
1219
|
|
|
1061
|
-
if (
|
|
1220
|
+
if (this.stripeMode === 'real') {
|
|
1062
1221
|
// Real Stripe webhook verification
|
|
1063
1222
|
const sig = req.headers['stripe-signature'];
|
|
1064
1223
|
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;
|
|
@@ -1076,7 +1235,13 @@ class LocalServer {
|
|
|
1076
1235
|
}
|
|
1077
1236
|
} else {
|
|
1078
1237
|
// Mock mode - accept all webhooks
|
|
1079
|
-
|
|
1238
|
+
if (Buffer.isBuffer(req.body)) {
|
|
1239
|
+
event = JSON.parse(req.body.toString());
|
|
1240
|
+
} else if (typeof req.body === 'string') {
|
|
1241
|
+
event = JSON.parse(req.body);
|
|
1242
|
+
} else {
|
|
1243
|
+
event = req.body;
|
|
1244
|
+
}
|
|
1080
1245
|
}
|
|
1081
1246
|
|
|
1082
1247
|
if (!this.json) {
|
|
@@ -1494,7 +1659,7 @@ class LocalServer {
|
|
|
1494
1659
|
console.log(chalk.yellow('š SPAPS Local Development Server'));
|
|
1495
1660
|
console.log(chalk.green(`⨠Running at: http://localhost:${this.port}`));
|
|
1496
1661
|
console.log(chalk.blue(`š Documentation: http://localhost:${this.port}/docs`));
|
|
1497
|
-
if (
|
|
1662
|
+
if (this.stripeMode === 'real') {
|
|
1498
1663
|
console.log(chalk.magenta('š³ Stripe: Real test mode (live API calls)'));
|
|
1499
1664
|
} else {
|
|
1500
1665
|
console.log(chalk.gray('š³ Stripe: Mock mode (simulated responses)'));
|
|
@@ -1535,4 +1700,4 @@ if (require.main === module) {
|
|
|
1535
1700
|
console.log(chalk.yellow('\nš Shutting down...'));
|
|
1536
1701
|
process.exit(0);
|
|
1537
1702
|
});
|
|
1538
|
-
}
|
|
1703
|
+
}
|